Enhance conversation history and runtime variable management

- Update ConversationRecorder to include source and nodeId metadata in transcripts for better context tracking.
- Introduce optional variable handling in DynamicVariableStore, allowing for unset variables to be rendered as empty without raising errors.
- Refactor WorkflowBrain to apply turn configurations and manage interaction policies dynamically, improving agent responsiveness.
- Implement tests to ensure proper handling of updated session variables and workflow metadata in various scenarios.
This commit is contained in:
Xin Wang
2026-07-14 11:08:11 +08:00
parent 665f727796
commit f74040adf3
18 changed files with 848 additions and 194 deletions

View File

@@ -9,6 +9,7 @@ from pipecat.frames.frames import (
LLMContextFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
LLMMessagesUpdateFrame,
LLMRunFrame,
LLMTextFrame,
OutputTransportMessageUrgentFrame,
@@ -391,6 +392,78 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
async def test_action_publishes_updated_session_variables(self):
tool = RuntimeTool(
id="lookup",
name="查询订单",
function_name="lookup_order",
type="http",
)
cfg = prepare_dynamic_config(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "lookup_action",
"type": "action",
"data": {
"toolId": "lookup",
"resultAssignments": {
"order_status": "order.status"
},
},
},
],
"edges": [],
},
dynamic_variable_definitions={
"order_status": {"type": "string", "default": "pending"}
},
tools=[tool],
),
{},
assistant_id="asst_workflow_action",
)
brain = WorkflowBrain(cfg)
queued = []
async def queue_frame(frame):
queued.append(frame)
async def execute(_tool, _arguments, *, result_assignments=None):
self.assertEqual(result_assignments, {"order_status": "order.status"})
brain._store.assign("order_status", "paid")
return {
"status": "ok",
"updated_variables": ["order_status"],
}
brain._runtime = BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
)
brain._tools.execute = execute
await brain._enter_action("lookup_action")
variable_events = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "workflow-variables"
]
self.assertEqual(variable_events[-1]["reason"], "action")
self.assertEqual(variable_events[-1]["changed"], ["order_status"])
self.assertEqual(variable_events[-1]["variables"], {"order_status": "paid"})
async def test_nodes_without_outgoing_edges_remain_active(self):
queued = []
@@ -492,6 +565,11 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
"defaultTtsResourceId": "tts_global",
"knowledgeBaseId": "kb_global",
"knowledgeMode": "automatic",
"enableInterrupt": False,
"turnConfig": {
"bargeIn": {"strategy": "transcription"},
"vad": {"confidence": 0.55},
},
},
"nodes": [
{
@@ -551,6 +629,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
queued = []
service_switches = []
knowledge_scopes = []
turn_configs = []
call_end = FakeCallEnd()
class FakeWorker:
@@ -585,6 +664,9 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
async def switch_services(llm_id, asr_id, tts_id):
service_switches.append((llm_id, asr_id, tts_id))
async def apply_turn_config(enable_interrupt, turn_config):
turn_configs.append((enable_interrupt, turn_config))
runtime = BrainRuntime(
context=context,
llm=llm,
@@ -596,15 +678,27 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
context_aggregator=pair,
switch_services=switch_services,
set_knowledge_scope=knowledge_scopes.append,
apply_turn_config=apply_turn_config,
)
await brain.setup(cfg, runtime)
await brain.on_connected()
self.assertEqual(brain._manager.current_node, "agent")
variable_events = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "workflow-variables"
]
self.assertEqual(variable_events[0]["reason"], "initialized")
self.assertEqual(variable_events[0]["variables"], {"user_name": "王先生"})
self.assertNotIn("system__conversation_id", variable_events[0]["variables"])
self.assertEqual(
service_switches,
[("llm_global", "asr_global", "tts_global")],
)
self.assertEqual(knowledge_scopes[-1]["knowledge_base_id"], "kb_global")
self.assertEqual(turn_configs[-1][0], False)
self.assertEqual(turn_configs[-1][1]["vad"]["confidence"], 0.55)
brain._engine.data("agent").update(
{
@@ -614,6 +708,11 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
"ttsResourceId": "tts_agent",
"knowledgeBaseId": "kb_agent",
"knowledgeMode": "on_demand",
"enableInterrupt": True,
"turnConfig": {
"bargeIn": {"strategy": "vad"},
"turnDetection": {"strategy": "smart_turn"},
},
}
)
await brain._apply_agent_stage("agent")
@@ -622,6 +721,11 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
("llm_agent", "asr_agent", "tts_agent"),
)
self.assertEqual(knowledge_scopes[-1]["knowledge_base_id"], "kb_agent")
self.assertEqual(turn_configs[-1][0], True)
self.assertEqual(
turn_configs[-1][1]["turnDetection"]["strategy"],
"smart_turn",
)
agent_config = brain._agent_config("agent")
self.assertIn("王先生", agent_config["role_message"])
self.assertIn("工作流路由已在用户一轮输入结束时完成", agent_config["role_message"])
@@ -654,11 +758,30 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
fixed_config["task_messages"],
[{"role": "assistant", "content": "您好,王先生"}],
)
self.assertEqual(fixed_config["pre_actions"][0]["node_id"], "agent")
worker.frames.clear()
queued.clear()
await brain._manager.set_node_from_config(fixed_config)
self.assertTrue(any(isinstance(frame, TTSSpeakFrame) for frame in queued))
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in worker.frames))
context_updates = [
frame
for frame in worker.frames
if isinstance(frame, LLMMessagesUpdateFrame)
]
self.assertEqual(
context_updates[-1].messages,
[{"role": "assistant", "content": "您好,王先生"}],
)
fixed_reply_events = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("source") == "workflow-fixed-reply"
]
self.assertEqual(fixed_reply_events[0]["content"], "您好,王先生")
self.assertEqual(fixed_reply_events[0]["nodeId"], "agent")
self.assertIn("您好,王先生", brain._store.values["system__conversation_history"])
self.assertFalse(
any(