fix: record message confirmations in context
This commit is contained in:
@@ -43,6 +43,7 @@ from services.message_stage import (
|
||||
MessageDisplaySpec,
|
||||
MessageStageRunner,
|
||||
MessageStageSpec,
|
||||
confirmation_context_message,
|
||||
)
|
||||
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
||||
from services.system_tools import state_update_properties, system_tool_kind
|
||||
@@ -226,8 +227,12 @@ class PromptBrain(BaseBrain):
|
||||
self.prepare_greeting_context(speech, runtime.context)
|
||||
try:
|
||||
if opening_message is not None:
|
||||
message_spec = self._opening_message_stage_spec(
|
||||
speech,
|
||||
opening_message,
|
||||
)
|
||||
message_result = await self._message_stages.run(
|
||||
self._opening_message_stage_spec(speech, opening_message),
|
||||
message_spec,
|
||||
speak=self._speak_opening,
|
||||
set_input_enabled=runtime.set_input_enabled,
|
||||
input_already_blocked=self._opening_input_blocked,
|
||||
@@ -241,6 +246,12 @@ class PromptBrain(BaseBrain):
|
||||
message_result.error or "开场消息显示失败"
|
||||
)
|
||||
return
|
||||
context_message = confirmation_context_message(
|
||||
message_result,
|
||||
source="prompt-opening",
|
||||
)
|
||||
if context_message is not None:
|
||||
runtime.context.add_message(context_message)
|
||||
|
||||
if opening_actions:
|
||||
result = await self._action_stages.run(
|
||||
|
||||
@@ -58,6 +58,7 @@ from services.message_stage import (
|
||||
MessageStageResult,
|
||||
MessageStageRunner,
|
||||
MessageStageSpec,
|
||||
confirmation_context_message,
|
||||
)
|
||||
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
||||
from services.system_tools import state_update_properties, system_tool_kind
|
||||
@@ -1421,6 +1422,12 @@ class WorkflowBrain(BaseBrain):
|
||||
context_message = fixed_speech_context_message(result.speech)
|
||||
if context_message is not None:
|
||||
context_messages.append(context_message)
|
||||
confirmation_message = confirmation_context_message(
|
||||
result,
|
||||
source=f"workflow-message:{continuation.node_id}",
|
||||
)
|
||||
if confirmation_message is not None:
|
||||
context_messages.append(confirmation_message)
|
||||
|
||||
if not self._engine.has_outgoing(continuation.node_id):
|
||||
self._state.enter(
|
||||
|
||||
@@ -17,6 +17,7 @@ from services.message_policy import (
|
||||
|
||||
|
||||
BUILTIN_SHOW_MESSAGE = "show_message"
|
||||
MESSAGE_CONFIRMATION_CONTEXT_MARKER = "[客户端交互事件,不是语音转写]"
|
||||
SpeechCompletion = Awaitable[None] | None
|
||||
Speak = Callable[[str], Awaitable[SpeechCompletion]]
|
||||
StartedHook = Callable[[], Awaitable[None]]
|
||||
@@ -40,15 +41,54 @@ class MessageStageSpec:
|
||||
completion_policy: MessageCompletionPolicy = MESSAGE_PLAYBACK
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MessageConfirmation:
|
||||
"""The exact dialog and user action that completed a confirmation stage."""
|
||||
|
||||
title: str
|
||||
message: str
|
||||
confirm_label: str
|
||||
action: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MessageStageResult:
|
||||
"""Result used by Workflow routing and Prompt opening failure handling."""
|
||||
|
||||
succeeded: bool
|
||||
speech: str = ""
|
||||
action: str | None = None
|
||||
confirmation: MessageConfirmation | None = None
|
||||
error: str | None = None
|
||||
|
||||
@property
|
||||
def action(self) -> str | None:
|
||||
"""Keep existing trace and routing callers independent of result shape."""
|
||||
return self.confirmation.action if self.confirmation is not None else None
|
||||
|
||||
|
||||
def confirmation_context_message(
|
||||
result: MessageStageResult,
|
||||
*,
|
||||
source: str,
|
||||
) -> dict[str, str] | None:
|
||||
"""Represent a confirmed client dialog as one provider-neutral user event."""
|
||||
confirmation = result.confirmation
|
||||
if not result.succeeded or confirmation is None:
|
||||
return None
|
||||
return {
|
||||
"role": "user",
|
||||
"content": "\n".join(
|
||||
[
|
||||
MESSAGE_CONFIRMATION_CONTEXT_MARKER,
|
||||
f"用户已阅读消息弹窗并点击“{confirmation.confirm_label}”。",
|
||||
f"弹窗标题:{confirmation.title}",
|
||||
f"弹窗内容:{confirmation.message}",
|
||||
f"操作结果:{confirmation.action}",
|
||||
f"事件来源:{source}",
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class MessageStageRunner:
|
||||
"""Run one atomic user-visible message stage.
|
||||
@@ -110,12 +150,12 @@ class MessageStageRunner:
|
||||
if speech and speak is not None:
|
||||
playback_completion = await speak(speech)
|
||||
|
||||
action: str | None = None
|
||||
confirmation: MessageConfirmation | None = None
|
||||
if spec.display is not None:
|
||||
result = await self._show_message(spec, speech=speech)
|
||||
if not result.succeeded:
|
||||
return result
|
||||
action = result.action
|
||||
confirmation = result.confirmation
|
||||
|
||||
# Confirmation is the gate. It deliberately does not wait for the
|
||||
# audio completion future, so the user can continue immediately.
|
||||
@@ -128,7 +168,7 @@ class MessageStageRunner:
|
||||
result = MessageStageResult(
|
||||
succeeded=True,
|
||||
speech=speech,
|
||||
action=action,
|
||||
confirmation=confirmation,
|
||||
)
|
||||
return result
|
||||
except asyncio.CancelledError:
|
||||
@@ -207,8 +247,23 @@ class MessageStageRunner:
|
||||
if isinstance(data, dict)
|
||||
else None
|
||||
)
|
||||
if require_confirmation and action is None:
|
||||
return MessageStageResult(
|
||||
succeeded=False,
|
||||
speech=speech,
|
||||
error="客户端未返回确认操作",
|
||||
)
|
||||
return MessageStageResult(
|
||||
succeeded=True,
|
||||
speech=speech,
|
||||
action=action,
|
||||
confirmation=(
|
||||
MessageConfirmation(
|
||||
title=display.title,
|
||||
message=display.message,
|
||||
confirm_label=display.confirm_label,
|
||||
action=action,
|
||||
)
|
||||
if require_confirmation and action is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ from services.message_stage import (
|
||||
MessageDisplaySpec,
|
||||
MessageStageRunner,
|
||||
MessageStageSpec,
|
||||
confirmation_context_message,
|
||||
)
|
||||
from services.pipecat.call_lifecycle import playback_marker_for
|
||||
from services.pipecat.realtime_tools import RealtimeTool, RealtimeToolResult
|
||||
@@ -893,7 +894,17 @@ class WorkflowRealtimeController:
|
||||
node_id=node_id,
|
||||
code="workflow_message_error",
|
||||
)
|
||||
return result.succeeded
|
||||
return False
|
||||
context_message = confirmation_context_message(
|
||||
result,
|
||||
source=f"workflow-message:{node_id}",
|
||||
)
|
||||
if context_message is not None:
|
||||
await self._runtime.realtime.send_text(
|
||||
context_message["content"],
|
||||
run_immediately=False,
|
||||
)
|
||||
return True
|
||||
|
||||
async def _enter_action(self, node_id: str) -> bool:
|
||||
self._state.enter(node_id, WorkflowStatus.RUNNING_ACTION)
|
||||
|
||||
Reference in New Issue
Block a user