feat: separate prompt opening and entry behavior

This commit is contained in:
Xin Wang
2026-08-04 12:21:07 +08:00
parent 1af1cd7fed
commit 78bba090a2
8 changed files with 264 additions and 24 deletions

View File

@@ -366,6 +366,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
tools=[tool],
startup={
"execution_mode": "sequential",
"entry_mode": "generate",
"opening_message": {
"title": "重要提示",
"message": "请确认已阅读。",
@@ -434,6 +435,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(called_tool_ids, [])
self.assertEqual(client_calls[0][0], "show_message")
self.assertFalse(client_calls[0][1]["dismissible"])
self.assertTrue(client_calls[0][2]["interrupt_on_result"])
self.assertTrue(
any(
isinstance(frame, TTSSpeakFrame)
@@ -478,6 +480,157 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
1,
)
async def test_opening_confirmation_can_wait_for_user_afterwards(self):
cfg = AssistantConfig(
type="prompt",
greeting="请先确认",
startup={
"entry_mode": "wait_user",
"opening_message": {
"title": "重要提示",
"message": "请确认已阅读。",
},
},
)
brain = build_brain(cfg)
queued = []
input_states = []
async def queue_frame(frame):
queued.append(frame)
class FakeClientTools:
async def call(self, *_args, **_kwargs):
return {"status": "ok", "data": {"action": "confirmed"}}
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(),
client_tools=FakeClientTools(),
set_input_enabled=input_states.append,
),
)
await brain.on_connected(greeting_pending=False)
await brain.on_client_ready()
self.assertEqual(input_states, [False, True])
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in queued))
async def test_prompt_generate_entry_waits_for_shared_greeting_playback(self):
cfg = AssistantConfig(
type="prompt",
greeting="固定开场白",
startup={"entry_mode": "generate"},
)
brain = build_brain(cfg)
queued = []
input_states = []
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,
),
)
await brain.on_connected(greeting_pending=True)
await brain.on_client_ready()
self.assertEqual(input_states, [False])
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in queued))
await brain.on_greeting_finished()
self.assertEqual(input_states, [False, True])
self.assertEqual(
sum(isinstance(frame, LLMRunFrame) for frame in queued),
1,
)
async def test_opening_confirmation_can_keep_speech_playing_before_entry(self):
cfg = AssistantConfig(
type="prompt",
greeting="必须完整播放的开场白",
startup={
"entry_mode": "generate",
"opening_message": {
"title": "重要提示",
"message": "请确认已阅读。",
"skip_speech_on_confirm": False,
},
},
)
brain = build_brain(cfg)
queued = []
input_states = []
message_displayed = asyncio.Event()
class TrackedCallEnd(FakeCallEnd):
def __init__(self):
super().__init__()
self.playback_completion = None
def track_speech(self):
self.tracked_speeches += 1
self.playback_completion = asyncio.get_running_loop().create_future()
return self.playback_completion
class FakeClientTools:
async def call(self, *_args, **options):
self.options = options
message_displayed.set()
return {"status": "ok", "data": {"action": "confirmed"}}
async def queue_frame(frame):
queued.append(frame)
call_end = TrackedCallEnd()
client_tools = FakeClientTools()
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,
client_tools=client_tools,
set_input_enabled=input_states.append,
),
)
await brain.on_connected(greeting_pending=False)
opening_task = asyncio.create_task(brain.on_client_ready())
await message_displayed.wait()
await asyncio.sleep(0)
self.assertFalse(client_tools.options["interrupt_on_result"])
self.assertFalse(opening_task.done())
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in queued))
call_end.playback_completion.set_result(None)
await opening_task
self.assertEqual(input_states, [False, True])
self.assertEqual(
sum(isinstance(frame, LLMRunFrame) for frame in queued),
1,
)
async def test_required_opening_failure_keeps_input_blocked_and_ends_call(self):
cfg = AssistantConfig(
type="prompt",

View File

@@ -109,6 +109,8 @@ class StartupActionValidationTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(body.startup.actions, [])
self.assertEqual(body.startup.opening_message.confirm_label, "确认")
self.assertTrue(body.startup.opening_message.skip_speech_on_confirm)
self.assertEqual(body.startup.entry_mode, "generate")
async def test_startup_tool_does_not_need_conversation_binding(self):
tool = SimpleNamespace(