Enhance DuplexPipeline to support follow-up context for manual opener tool calls

- Introduced logic to trigger a follow-up turn when the manual opener greeting is empty.
- Updated `_execute_manual_opener_tool_calls` to return structured tool call and result data.
- Added `_build_manual_opener_follow_up_context` method to construct context for follow-up turns.
- Modified `_handle_turn` to accept system context for improved conversation management.
- Enhanced tests to validate the new follow-up behavior and ensure proper context handling.
This commit is contained in:
Xin Wang
2026-03-02 14:27:44 +08:00
parent fb017f9952
commit 3aa9e0f432
2 changed files with 115 additions and 4 deletions

View File

@@ -350,6 +350,76 @@ async def test_manual_opener_legacy_voice_message_prompt_is_normalized(monkeypat
assert tool_events[0].get("arguments") == {"msg": "您好"}
@pytest.mark.asyncio
async def test_manual_opener_empty_greeting_triggers_follow_up_turn(monkeypatch):
pipeline, _events = _build_pipeline(monkeypatch, [[LLMStreamEvent(type="done")]])
pipeline.apply_runtime_overrides(
{
"generatedOpenerEnabled": False,
"greeting": "",
"output": {"mode": "text"},
"tools": [
{
"type": "function",
"executor": "client",
"waitForResponse": True,
"function": {
"name": "text_choice_prompt",
"description": "Prompt choice",
"parameters": {"type": "object", "properties": {"question": {"type": "string"}}},
},
}
],
"manualOpenerToolCalls": [
{
"toolName": "text_choice_prompt",
"arguments": {
"question": "请选择业务类型",
"options": ["账单", "报修"],
},
}
],
}
)
async def _fake_manual_opener_calls() -> Dict[str, List[Dict[str, Any]]]:
return {
"toolCalls": [
{
"tool_call_id": "call_opener_1",
"tool_name": "text_choice_prompt",
"arguments": {"question": "请选择业务类型", "options": ["账单", "报修"]},
}
],
"toolResults": [
{
"tool_call_id": "call_opener_1",
"name": "text_choice_prompt",
"output": {"selected": "报修"},
"status": {"code": 200, "message": "ok"},
}
],
}
called: Dict[str, Any] = {}
waiter = asyncio.Event()
async def _fake_handle_turn(user_text: str, system_context: str | None = None) -> None:
called["user_text"] = user_text
called["system_context"] = system_context or ""
waiter.set()
monkeypatch.setattr(pipeline, "_execute_manual_opener_tool_calls", _fake_manual_opener_calls)
monkeypatch.setattr(pipeline, "_handle_turn", _fake_handle_turn)
await pipeline.emit_initial_greeting()
await asyncio.wait_for(waiter.wait(), timeout=1.0)
assert called.get("user_text") == ""
assert "opener_tool_results" in called.get("system_context", "")
assert "报修" in called.get("system_context", "")
@pytest.mark.asyncio
async def test_ws_message_parses_tool_call_results():
msg = parse_client_message(