feat: add prompt startup actions and shared vision config

This commit is contained in:
Xin Wang
2026-08-01 23:31:14 +08:00
parent b747144ff1
commit 0331f8cd07
22 changed files with 1238 additions and 344 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from models import AssistantConfig, RuntimeTool
from pipecat.frames.frames import (
@@ -29,8 +29,8 @@ from services.brains.dify_llm import (
)
from services.brains.workflow_brain import WorkflowBrain
from services.runtime_variables import prepare_dynamic_config
from services.action_runtime import ActionError, ActionOutcome, ActionStatus
from services.workflow.models import (
ActionStatus,
LLMRouteResult,
RouteStatus,
WorkflowStatus,
@@ -246,6 +246,206 @@ class DifyLLMServiceTests(unittest.IsolatedAsyncioTestCase):
class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
async def test_preflight_runs_multiple_server_tools_in_order(self):
tools = [
RuntimeTool(
id=f"preflight_{index}",
name=f"预检动作 {index}",
function_name=f"preflight_{index}",
type="http",
)
for index in (1, 2)
]
cfg = AssistantConfig(
type="prompt",
tools=tools,
startup={
"actions": [
{
"id": tool.id,
"phase": "preflight",
"tool_id": tool.id,
"required": True,
}
for tool in tools
]
},
)
brain = build_brain(cfg)
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=noop_queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
),
)
brain._actions.execute = AsyncMock(
side_effect=[
ActionOutcome(
invocation_id=f"act_{index}",
status=ActionStatus.SUCCESS,
duration_ms=index,
)
for index in (1, 2)
]
)
await brain.run_preflight()
self.assertEqual(
[call.args[0].id for call in brain._actions.execute.await_args_list],
["preflight_1", "preflight_2"],
)
async def test_opening_actions_wait_for_confirmation_and_greeting(self):
tools = [
RuntimeTool(
id=f"opening_{index}",
name=f"开场动作 {index}",
function_name="show_message" if index == 1 else "load_opening_data",
type="client" if index == 1 else "http",
)
for index in (1, 2)
]
cfg = AssistantConfig(
type="prompt",
tools=tools,
startup={
"execution_mode": "sequential",
"actions": [
{
"id": f"opening_{index}",
"phase": "opening",
"tool_id": f"opening_{index}",
"arguments": {},
"required": True,
}
for index in (1, 2)
],
},
)
brain = build_brain(cfg)
input_states = []
queued = []
async def queue_frame(frame):
queued.append(frame)
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
set_input_enabled=input_states.append,
),
)
brain._actions.execute = AsyncMock(
side_effect=[
ActionOutcome(
invocation_id=f"act_{index}",
status=ActionStatus.SUCCESS,
duration_ms=index,
)
for index in (1, 2)
]
)
await brain.on_connected(greeting_pending=True)
await brain.on_client_ready()
self.assertEqual(input_states, [False])
called_tool_ids = [
call.args[0].id for call in brain._actions.execute.await_args_list
]
self.assertEqual(called_tool_ids, ["opening_1", "opening_2"])
self.assertEqual(
len(
[
frame
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "startup-action-result"
]
),
2,
)
await brain.on_greeting_finished()
self.assertEqual(input_states, [False, True])
# Replayed client-ready must not execute startup actions twice.
await brain.on_client_ready()
self.assertEqual(brain._actions.execute.await_count, 2)
async def test_required_opening_failure_keeps_input_blocked_and_ends_call(self):
tool = RuntimeTool(
id="opening_message",
name="开场确认",
function_name="show_message",
type="client",
)
cfg = AssistantConfig(
type="prompt",
tools=[tool],
startup={
"actions": [
{
"id": "opening_message",
"phase": "opening",
"tool_id": tool.id,
"arguments": {},
"required": True,
}
]
},
)
brain = build_brain(cfg)
call_end = FakeCallEnd()
input_states = []
async def queue_frame(_frame):
pass
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=call_end,
set_input_enabled=input_states.append,
),
)
brain._actions.execute = AsyncMock(
return_value=ActionOutcome(
invocation_id="act_failed",
status=ActionStatus.FAILURE,
duration_ms=1,
error=ActionError(
code="tool_error",
message="用户未确认",
),
)
)
await brain.on_connected(greeting_pending=False)
await brain.on_client_ready()
self.assertEqual(input_states, [False])
self.assertTrue(call_end.ending)
self.assertTrue(call_end.finished)
self.assertEqual(call_end.reason, "startup_action_failed")
async def test_realtime_prompt_brain_renders_dynamic_variables(self):
cfg = prepare_dynamic_config(
AssistantConfig(