fix(workflow): prevent fixed speech continuation
This commit is contained in:
@@ -42,6 +42,7 @@ from services.action_runtime import (
|
|||||||
ActionStatus,
|
ActionStatus,
|
||||||
)
|
)
|
||||||
from services.action_stage import ActionStageRunner, ActionStageSpec, StageAction
|
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.knowledge import search as search_knowledge
|
||||||
from services.message_policy import (
|
from services.message_policy import (
|
||||||
MESSAGE_COMPLETION_POLICIES,
|
MESSAGE_COMPLETION_POLICIES,
|
||||||
@@ -794,9 +795,9 @@ class WorkflowBrain(BaseBrain):
|
|||||||
source="workflow-edge-transition",
|
source="workflow-edge-transition",
|
||||||
node_id=str(edge.get("target") or "") or None,
|
node_id=str(edge.get("target") or "") or None,
|
||||||
)
|
)
|
||||||
context_messages.append(
|
context_message = fixed_speech_context_message(content)
|
||||||
{"role": "assistant", "content": content}
|
if context_message is not None:
|
||||||
)
|
context_messages.append(context_message)
|
||||||
return await self._resolve_path(
|
return await self._resolve_path(
|
||||||
str(edge.get("target") or ""),
|
str(edge.get("target") or ""),
|
||||||
leading_messages=context_messages,
|
leading_messages=context_messages,
|
||||||
@@ -828,10 +829,11 @@ class WorkflowBrain(BaseBrain):
|
|||||||
if triggering_user_message
|
if triggering_user_message
|
||||||
else {"role": "user", "content": triggering_user_text}
|
else {"role": "user", "content": triggering_user_text}
|
||||||
)
|
)
|
||||||
agent_messages = [
|
# Fixed speech is represented by system facts. Put those
|
||||||
current_user_message,
|
# facts before the triggering user turn so RESET contexts
|
||||||
*context_messages,
|
# 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)
|
return self._agent_config(node_id, agent_messages)
|
||||||
if node_type == "end":
|
if node_type == "end":
|
||||||
await self._enter_end(node_id)
|
await self._enter_end(node_id)
|
||||||
@@ -871,9 +873,9 @@ class WorkflowBrain(BaseBrain):
|
|||||||
source="workflow-edge-transition",
|
source="workflow-edge-transition",
|
||||||
node_id=target_id or None,
|
node_id=target_id or None,
|
||||||
)
|
)
|
||||||
context_messages.append(
|
context_message = fixed_speech_context_message(content)
|
||||||
{"role": "assistant", "content": content}
|
if context_message is not None:
|
||||||
)
|
context_messages.append(context_message)
|
||||||
node_id = str(edge.get("target") or "")
|
node_id = str(edge.get("target") or "")
|
||||||
raise RuntimeError("工作流连续自动跳转超过安全上限")
|
raise RuntimeError("工作流连续自动跳转超过安全上限")
|
||||||
|
|
||||||
@@ -974,9 +976,9 @@ class WorkflowBrain(BaseBrain):
|
|||||||
dict(message) for message in continuation.context_messages
|
dict(message) for message in continuation.context_messages
|
||||||
]
|
]
|
||||||
if result.speech:
|
if result.speech:
|
||||||
context_messages.append(
|
context_message = fixed_speech_context_message(result.speech)
|
||||||
{"role": "assistant", "content": result.speech}
|
if context_message is not None:
|
||||||
)
|
context_messages.append(context_message)
|
||||||
|
|
||||||
if not self._engine.has_outgoing(continuation.node_id):
|
if not self._engine.has_outgoing(continuation.node_id):
|
||||||
self._state.enter(
|
self._state.enter(
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ from services.brains.base import BrainRuntime
|
|||||||
from services.runtime_variables import DynamicVariableStore
|
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:
|
class FixedSpeechOutput:
|
||||||
"""Display and synthesize fixed speech without waiting for playback."""
|
"""Display and synthesize fixed speech without waiting for playback."""
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from services.brains.dify_llm import (
|
|||||||
normalize_api_base,
|
normalize_api_base,
|
||||||
)
|
)
|
||||||
from services.brains.workflow_brain import ConfiguredFlowManager, WorkflowBrain
|
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.runtime_variables import prepare_dynamic_config
|
||||||
from services.action_runtime import ActionOutcome, ActionStatus
|
from services.action_runtime import ActionOutcome, ActionStatus
|
||||||
from services.workflow.models import (
|
from services.workflow.models import (
|
||||||
@@ -1723,7 +1724,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
{
|
{
|
||||||
"id": "agent1",
|
"id": "agent1",
|
||||||
"type": "agent",
|
"type": "agent",
|
||||||
"data": {"prompt": "收集基本信息"},
|
"data": {
|
||||||
|
"prompt": "收集基本信息",
|
||||||
|
"entryMode": "generate",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "middle",
|
"id": "middle",
|
||||||
@@ -1846,6 +1850,21 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
if manager.current_node == "agent1":
|
if manager.current_node == "agent1":
|
||||||
break
|
break
|
||||||
self.assertEqual(manager.current_node, "agent1")
|
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
|
# The user-turn processor must return while the second Message is
|
||||||
# still waiting for its transport playback boundary.
|
# still waiting for its transport playback boundary.
|
||||||
@@ -1868,8 +1887,13 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
manager.configs[-1]["task_messages"],
|
manager.configs[-1]["task_messages"],
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
f"{FIXED_SPEECH_CONTEXT_MARKER}\n现在进入信息确认。"
|
||||||
|
),
|
||||||
|
},
|
||||||
{"role": "user", "content": "基本信息已经收集完成"},
|
{"role": "user", "content": "基本信息已经收集完成"},
|
||||||
{"role": "assistant", "content": "现在进入信息确认。"},
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2550,7 +2574,14 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
for frame in worker.frames
|
for frame in worker.frames
|
||||||
if isinstance(frame, LLMMessagesAppendFrame)
|
if isinstance(frame, LLMMessagesAppendFrame)
|
||||||
and frame.messages
|
and frame.messages
|
||||||
== [{"role": "assistant", "content": "正在为你结束流程"}]
|
== [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
f"{FIXED_SPEECH_CONTEXT_MARKER}\n正在为你结束流程"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
self.assertTrue(transition_context_frames)
|
self.assertTrue(transition_context_frames)
|
||||||
transition_events = [
|
transition_events = [
|
||||||
|
|||||||
Reference in New Issue
Block a user