Make end call tool can be cancelled

This commit is contained in:
Xin Wang
2026-08-12 09:18:40 +08:00
parent fc026e765e
commit d0bb5b3868
15 changed files with 350 additions and 48 deletions
+58 -4
View File
@@ -60,10 +60,24 @@ class FakeCallEnd:
self.response_started = False
self.waited_for_text: bool | None = None
self.tracked_speeches = 0
self.interruptible = False
def begin(self, reason: str) -> None:
@property
def accepts_input(self) -> bool:
return not self.ending or self.interruptible
def begin(self, reason: str, *, interruptible: bool = False) -> None:
self.ending = True
self.reason = reason
self.interruptible = interruptible
def cancel_pending_end(self) -> bool:
if not self.ending or not self.interruptible:
return False
self.ending = False
self.interruptible = False
self.armed = False
return True
def begin_response(self) -> None:
self.response_started = True
@@ -105,14 +119,14 @@ async def noop_queue_frame(_frame):
return None
def system_runtime_tool(kind: str) -> RuntimeTool:
def system_runtime_tool(kind: str, **config) -> RuntimeTool:
return RuntimeTool(
id=f"tool_{kind}",
name=kind,
function_name=kind,
type="system",
description=f"{kind} system tool",
definition={"type": "system", "config": {"kind": kind}},
definition={"type": "system", "config": {"kind": kind, **config}},
)
@@ -175,6 +189,12 @@ class BrainRegistryTests(unittest.TestCase):
):
self.assertEqual(SystemToolConfig(kind=kind).kind, kind)
def test_end_interruption_is_opt_in(self):
self.assertFalse(SystemToolConfig().allow_end_interruption)
self.assertTrue(
SystemToolConfig(allowEndInterruption=True).allow_end_interruption
)
def test_system_tool_config_rejects_unknown_kind(self):
with self.assertRaises(ValueError):
SystemToolConfig(kind="magic")
@@ -1018,6 +1038,36 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
self.assertFalse(call_end.finished)
self.assertTrue(call_end.armed)
async def test_end_conversation_passes_interruptible_tool_setting(self):
cfg = AssistantConfig(
type="prompt",
tools=[
system_runtime_tool(
"end_conversation",
allow_end_interruption=True,
)
],
)
brain = build_brain(cfg)
llm = FakeLLM()
call_end = FakeCallEnd()
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=llm,
queue_frame=lambda _frame: None,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=call_end,
),
)
await llm.functions["end_conversation"](FakeFunctionParams({}))
self.assertTrue(call_end.ending)
self.assertTrue(call_end.interruptible)
async def test_end_conversation_finishes_when_no_speech(self):
cfg = AssistantConfig(
type="prompt",
@@ -1481,7 +1531,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
call_end=call_end,
)
tool = brain._workflow_end_conversation_tool(
system_runtime_tool("end_conversation"),
system_runtime_tool(
"end_conversation",
allow_end_interruption=True,
),
"agent",
)
@@ -1489,6 +1542,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
result = await tool.handler({"reason": "用户告别"}, None)
self.assertEqual(result["action"], "ending_call")
self.assertTrue(call_end.ending)
self.assertTrue(call_end.interruptible)
self.assertEqual(call_end.reason, "用户告别")
self.assertTrue(getattr(tool.handler, "_suppress_followup_llm"))