feat(workflow): enhance message stages and image routing

This commit is contained in:
Xin Wang
2026-08-03 12:38:02 +08:00
parent b0991f239e
commit 4c43e167db
13 changed files with 593 additions and 157 deletions

View File

@@ -43,6 +43,12 @@ from services.action_runtime import (
)
from services.action_stage import ActionStageRunner, ActionStageSpec, StageAction
from services.knowledge import search as search_knowledge
from services.message_policy import (
MESSAGE_COMPLETION_POLICIES,
MESSAGE_CONFIRMATION,
MESSAGE_INTERRUPTIBLE,
MESSAGE_PLAYBACK,
)
from services.message_stage import (
MessageDisplaySpec,
MessageStageResult,
@@ -332,13 +338,33 @@ class WorkflowBrain(BaseBrain):
user_message: dict[str, Any] | None = None,
) -> bool:
"""Serialized implementation so one user turn cannot transition twice."""
self.record_user_message(content)
self._state.begin_user_turn(content)
manager = self._require_manager()
current = self._state.current_node_id
if not current:
return True
continuation = self._pending_message
if (
self._engine.node_type(current) == "message"
and continuation is not None
and continuation.node_id == current
):
if self._message_completion_policy(current) != MESSAGE_INTERRUPTIBLE:
# Protected Message stages keep their playback/confirmation gate.
# Normal transports reject this input before it reaches the brain;
# this guard also covers programmatic context injections.
return True
self.record_user_message(content)
self._state.begin_user_turn(content)
return await self._interrupt_message_continuation(
continuation,
content=content,
user_message=user_message,
)
self.record_user_message(content)
self._state.begin_user_turn(content)
self._state.status = WorkflowStatus.ROUTING
decision = await self._edge_evaluator.evaluate(
current,
@@ -366,6 +392,79 @@ class WorkflowBrain(BaseBrain):
return await self._continue_current_node_after_no_transition(current)
async def _interrupt_message_continuation(
self,
continuation: _MessageContinuation,
*,
content: str,
user_message: dict[str, Any] | None,
) -> bool:
"""Finish an interruptible Message once and forward the user turn."""
if self._pending_message is not continuation:
return True
self._pending_message = None
task = continuation.task
if task is not None and task is not asyncio.current_task():
task.cancel()
runtime = self._require_runtime()
if runtime.set_input_enabled is not None:
runtime.set_input_enabled(True)
await self._emit_trace(
"message_interrupted",
nodeId=continuation.node_id,
reason="user_input",
)
context_messages = [
dict(message) for message in continuation.context_messages
]
if not self._engine.has_outgoing(continuation.node_id):
self._state.enter(
continuation.node_id,
WorkflowStatus.WAITING_USER,
)
return True
self._state.status = WorkflowStatus.ROUTING
decision = await self._edge_evaluator.evaluate(
continuation.node_id,
current_user_message=user_message,
)
if decision.status == RouteStatus.ERROR:
await self._require_output().emit_error(
decision.error or "工作流路由失败",
node_id=continuation.node_id,
code="workflow_routing_error",
)
self._state.enter(
continuation.node_id,
WorkflowStatus.WAITING_USER,
)
return True
manager = self._require_manager()
if not decision.edge or manager.current_node != continuation.node_id:
self._state.enter(
continuation.node_id,
WorkflowStatus.WAITING_USER,
)
return True
next_config = await self._follow_edge(
decision.edge,
leading_messages=context_messages,
triggering_user_text=content,
triggering_user_message=user_message,
)
await self._activate_node_config(
next_config,
triggering_user_text=content,
)
return True
async def _continue_current_node_after_no_transition(
self,
node_id: str,
@@ -811,7 +910,9 @@ class WorkflowBrain(BaseBrain):
await self._emit_node_active(node_id)
runtime = self._require_runtime()
if runtime.set_input_enabled is not None:
runtime.set_input_enabled(False)
runtime.set_input_enabled(
self._message_completion_policy(node_id) == MESSAGE_INTERRUPTIBLE
)
continuation.task = asyncio.create_task(
self._complete_message_continuation(continuation),
name=f"workflow-message-{node_id}-{continuation.token}",
@@ -967,8 +1068,8 @@ class WorkflowBrain(BaseBrain):
data = self._engine.data(node_id)
runtime = self._require_runtime()
speech = self._store.render(str(data.get("speech") or "")).strip()
show_message = bool(data.get("showMessage", False))
require_confirmation = bool(data.get("requireConfirmation", False))
completion_policy = self._message_completion_policy(node_id)
require_confirmation = completion_policy == MESSAGE_CONFIRMATION
display = (
MessageDisplaySpec(
title=self._store.render(
@@ -981,14 +1082,14 @@ class WorkflowBrain(BaseBrain):
str(data.get("confirmLabel") or "确认")
).strip(),
)
if show_message
if require_confirmation
else None
)
result = await self._message_stages.run(
MessageStageSpec(
speech=speech,
display=display,
require_confirmation=require_confirmation,
completion_policy=completion_policy,
),
speak=lambda content: self._queue_visible_speech(
content,
@@ -1001,7 +1102,8 @@ class WorkflowBrain(BaseBrain):
"message_started",
nodeId=node_id,
hasSpeech=bool(speech),
showsMessage=show_message,
showsMessage=require_confirmation,
completionPolicy=completion_policy,
requiresConfirmation=require_confirmation,
),
)
@@ -1026,6 +1128,13 @@ class WorkflowBrain(BaseBrain):
)
return result
def _message_completion_policy(self, node_id: str) -> str:
value = str(
self._engine.data(node_id).get("completionPolicy")
or MESSAGE_PLAYBACK
)
return value if value in MESSAGE_COMPLETION_POLICIES else MESSAGE_PLAYBACK
def _set_last_action(self, outcome: ActionOutcome) -> None:
legacy_status = {
ActionStatus.SUCCESS: "ok",