feat: organize tools and enable qwen realtime calls

This commit is contained in:
Xin Wang
2026-08-14 00:15:00 +08:00
parent eb111a3890
commit 5be21f5ccc
71 changed files with 5771 additions and 2026 deletions
+152
View File
@@ -10,6 +10,7 @@ from pipecat.frames.frames import OutputTransportMessageUrgentFrame, TTSAudioRaw
from services import model_resource_tester
from services.interface_catalog import INTERFACE_DEFINITIONS
from services.pipecat.qwen_audio_realtime import QwenAudioRealtimeService
from services.pipecat.realtime_tools import RealtimeTool, RealtimeToolResult
from services.pipecat.service_factory import (
create_realtime_service,
realtime_audio_sample_rates,
@@ -63,9 +64,160 @@ class QwenAudioRealtimeServiceTest(unittest.IsolatedAsyncioTestCase):
"silence_duration_ms": 800,
},
"max_history_turns": 20,
"tools": [],
"tool_choice": "auto",
},
)
async def test_session_update_uses_qwen_nested_function_schema(self):
service = _service()
websocket = _OpenWebSocket()
service._websocket = websocket
service._session_ready.set()
tool = RealtimeTool(
name="lookup_order",
description="查询订单",
properties={"order_id": {"type": "string"}},
required=("order_id",),
)
await service.update_session("Use tools.", [tool])
self.assertEqual(
websocket.messages[-1]["session"]["tools"],
[
{
"type": "function",
"function": {
"name": "lookup_order",
"description": "查询订单",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
},
},
}
],
)
async def test_multiple_function_calls_are_flushed_once_in_output_order(self):
service = _service()
websocket = _OpenWebSocket()
service._websocket = websocket
service._session_ready.set()
service.push_frame = AsyncMock()
calls: list[tuple[str, dict]] = []
async def dispatch(name, arguments, _call_id):
calls.append((name, arguments))
return RealtimeToolResult({"status": "ok", "name": name})
service.set_tool_dispatcher(dispatch)
await service._handle_server_event(
{"type": "response.created", "response": {"id": "resp_1"}}
)
await service._handle_server_event(
{
"type": "response.function_call_arguments.done",
"response_id": "resp_1",
"output_index": 1,
"call_id": "call_2",
"name": "second",
"arguments": '{"position":2}',
}
)
await service._handle_server_event(
{
"type": "response.function_call_arguments.done",
"response_id": "resp_1",
"output_index": 0,
"call_id": "call_1",
"name": "first",
"arguments": '{"position":1}',
}
)
# Qwen can emit output_item.done for the same call as arguments.done.
await service._handle_server_event(
{
"type": "response.output_item.done",
"response_id": "resp_1",
"output_index": 0,
"item": {
"type": "function_call",
"call_id": "call_1",
"name": "first",
"arguments": '{"position":1}',
},
}
)
await service._handle_server_event(
{
"type": "response.done",
"response": {"id": "resp_1", "status": "completed"},
}
)
await service._tool_session.wait_pending()
self.assertEqual(
calls,
[("first", {"position": 1}), ("second", {"position": 2})],
)
outputs = [
item
for item in websocket.messages
if item["type"] == "conversation.item.create"
and item["item"]["type"] == "function_call_output"
]
self.assertEqual(
[item["item"]["call_id"] for item in outputs],
["call_1", "call_2"],
)
self.assertEqual(
sum(item["type"] == "response.create" for item in websocket.messages),
1,
)
async def test_malformed_arguments_do_not_execute_side_effect(self):
service = _service()
websocket = _OpenWebSocket()
service._websocket = websocket
service._session_ready.set()
service.push_frame = AsyncMock()
dispatcher = AsyncMock()
service.set_tool_dispatcher(dispatcher)
await service._handle_server_event(
{"type": "response.created", "response": {"id": "resp_bad"}}
)
await service._handle_server_event(
{
"type": "response.function_call_arguments.done",
"response_id": "resp_bad",
"call_id": "call_bad",
"name": "end_conversation",
"arguments": "{bad json",
}
)
await service._handle_server_event(
{
"type": "response.done",
"response": {"id": "resp_bad", "status": "completed"},
}
)
await service._tool_session.wait_pending()
dispatcher.assert_not_awaited()
output = next(
item["item"]["output"]
for item in websocket.messages
if item["type"] == "conversation.item.create"
and item["item"]["type"] == "function_call_output"
)
self.assertIn("JSON", json.loads(output)["message"])
def test_smart_turn_and_value_ranges_are_explicit(self):
service = _service(turn_detection_mode="smart_turn")
self.assertEqual(service._turn_detection_config(), {"type": "smart_turn"})