diff --git a/backend/services/brains/workflow_brain.py b/backend/services/brains/workflow_brain.py index fb02ae2..634c54e 100644 --- a/backend/services/brains/workflow_brain.py +++ b/backend/services/brains/workflow_brain.py @@ -42,6 +42,7 @@ from services.action_runtime import ( ActionStatus, ) from services.action_stage import ActionStageRunner, ActionStageSpec, StageAction +from services.fixed_speech import fixed_speech_context_message from services.knowledge import search as search_knowledge from services.message_policy import ( MESSAGE_COMPLETION_POLICIES, @@ -794,9 +795,9 @@ class WorkflowBrain(BaseBrain): source="workflow-edge-transition", node_id=str(edge.get("target") or "") or None, ) - context_messages.append( - {"role": "assistant", "content": content} - ) + context_message = fixed_speech_context_message(content) + if context_message is not None: + context_messages.append(context_message) return await self._resolve_path( str(edge.get("target") or ""), leading_messages=context_messages, @@ -828,10 +829,11 @@ class WorkflowBrain(BaseBrain): if triggering_user_message else {"role": "user", "content": triggering_user_text} ) - agent_messages = [ - current_user_message, - *context_messages, - ] + # Fixed speech is represented by system facts. Put those + # facts before the triggering user turn so RESET contexts + # still end with the real user input rather than a control + # message. + agent_messages = [*context_messages, current_user_message] return self._agent_config(node_id, agent_messages) if node_type == "end": await self._enter_end(node_id) @@ -871,9 +873,9 @@ class WorkflowBrain(BaseBrain): source="workflow-edge-transition", node_id=target_id or None, ) - context_messages.append( - {"role": "assistant", "content": content} - ) + context_message = fixed_speech_context_message(content) + if context_message is not None: + context_messages.append(context_message) node_id = str(edge.get("target") or "") raise RuntimeError("工作流连续自动跳转超过安全上限") @@ -974,9 +976,9 @@ class WorkflowBrain(BaseBrain): dict(message) for message in continuation.context_messages ] if result.speech: - context_messages.append( - {"role": "assistant", "content": result.speech} - ) + context_message = fixed_speech_context_message(result.speech) + if context_message is not None: + context_messages.append(context_message) if not self._engine.has_outgoing(continuation.node_id): self._state.enter( diff --git a/backend/services/fixed_speech.py b/backend/services/fixed_speech.py index da84f0f..daae6cc 100644 --- a/backend/services/fixed_speech.py +++ b/backend/services/fixed_speech.py @@ -12,6 +12,20 @@ from services.brains.base import BrainRuntime from services.runtime_variables import DynamicVariableStore +FIXED_SPEECH_CONTEXT_MARKER = "[会话事实:以下固定消息已向用户播报]" + + +def fixed_speech_context_message(content: str) -> dict[str, str] | None: + """Keep deterministic speech in context without inviting assistant continuation.""" + text = content.strip() + if not text: + return None + return { + "role": "system", + "content": f"{FIXED_SPEECH_CONTEXT_MARKER}\n{text}", + } + + class FixedSpeechOutput: """Display and synthesize fixed speech without waiting for playback.""" diff --git a/backend/tests/test_brains.py b/backend/tests/test_brains.py index 115e323..90a1e32 100644 --- a/backend/tests/test_brains.py +++ b/backend/tests/test_brains.py @@ -30,6 +30,7 @@ from services.brains.dify_llm import ( normalize_api_base, ) from services.brains.workflow_brain import ConfiguredFlowManager, WorkflowBrain +from services.fixed_speech import FIXED_SPEECH_CONTEXT_MARKER from services.runtime_variables import prepare_dynamic_config from services.action_runtime import ActionOutcome, ActionStatus from services.workflow.models import ( @@ -1723,7 +1724,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): { "id": "agent1", "type": "agent", - "data": {"prompt": "收集基本信息"}, + "data": { + "prompt": "收集基本信息", + "entryMode": "generate", + }, }, { "id": "middle", @@ -1846,6 +1850,21 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): if manager.current_node == "agent1": break self.assertEqual(manager.current_node, "agent1") + self.assertEqual( + manager.configs[-1]["task_messages"], + [ + { + "role": "system", + "content": f"{FIXED_SPEECH_CONTEXT_MARKER}\n欢迎使用。", + } + ], + ) + self.assertFalse( + any( + message["role"] == "assistant" + for message in manager.configs[-1]["task_messages"] + ) + ) # The user-turn processor must return while the second Message is # still waiting for its transport playback boundary. @@ -1868,8 +1887,13 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): self.assertEqual( manager.configs[-1]["task_messages"], [ + { + "role": "system", + "content": ( + f"{FIXED_SPEECH_CONTEXT_MARKER}\n现在进入信息确认。" + ), + }, {"role": "user", "content": "基本信息已经收集完成"}, - {"role": "assistant", "content": "现在进入信息确认。"}, ], ) @@ -2550,7 +2574,14 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): for frame in worker.frames if isinstance(frame, LLMMessagesAppendFrame) and frame.messages - == [{"role": "assistant", "content": "正在为你结束流程"}] + == [ + { + "role": "system", + "content": ( + f"{FIXED_SPEECH_CONTEXT_MARKER}\n正在为你结束流程" + ), + } + ] ] self.assertTrue(transition_context_frames) transition_events = [