feat: organize tools and enable qwen realtime calls
This commit is contained in:
@@ -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"})
|
||||
|
||||
Reference in New Issue
Block a user