Files
ai-video-fullstack/backend/tests/test_client_tools.py

158 lines
5.0 KiB
Python

import asyncio
import unittest
from models import RuntimeTool
from pipecat.frames.frames import (
InputTransportMessageFrame,
OutputTransportMessageUrgentFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from services.client_tools import ClientToolBroker, ClientToolError
from services.runtime_variables import DynamicVariableStore
from services.tool_executor import ToolExecutor
from schemas import ClientToolConfig
def client_tool() -> RuntimeTool:
return RuntimeTool(
id="tool_photo_button",
name="显示拍照按钮",
function_name="set_photo_button_visible",
type="client",
definition={
"schema_version": 1,
"type": "client",
"config": {
"parameters": [
{
"name": "visible",
"type": "boolean",
"required": True,
}
],
"timeout_seconds": 3,
"dynamic_variable_assignments": {
"photo_button_visible": "visible"
},
},
},
)
class FakeClientTools:
def __init__(self, result):
self.result = result
self.calls = []
async def call(
self,
function_name,
arguments,
*,
timeout_seconds,
wait_for_response=True,
):
self.calls.append(
(function_name, arguments, timeout_seconds, wait_for_response)
)
return self.result
class ClientToolExecutorTests(unittest.IsolatedAsyncioTestCase):
def test_fire_and_forget_rejects_result_assignments(self):
with self.assertRaisesRegex(ValueError, "不能配置结果变量赋值"):
ClientToolConfig(
wait_for_response=False,
dynamic_variable_assignments={"visible": "visible"},
)
async def test_success_updates_configured_variable(self):
store = DynamicVariableStore(
{"photo_button_visible": False},
variable_types={"photo_button_visible": "boolean"},
)
port = FakeClientTools({"status": "ok", "data": {"visible": True}})
executor = ToolExecutor(store, client_tools=port)
result = await executor.execute(client_tool(), {"visible": True})
self.assertEqual(result["updated_variables"], ["photo_button_visible"])
self.assertIs(store.values["photo_button_visible"], True)
self.assertEqual(
port.calls,
[("set_photo_button_visible", {"visible": True}, 3.0, True)],
)
async def test_failure_does_not_update_variable(self):
store = DynamicVariableStore({"photo_button_visible": False})
executor = ToolExecutor(
store,
client_tools=FakeClientTools(
{"status": "error", "message": "unsupported"}
),
)
result = await executor.execute(client_tool(), {"visible": True})
self.assertEqual(result["updated_variables"], [])
self.assertIs(store.values["photo_button_visible"], False)
class ClientToolBrokerTests(unittest.IsolatedAsyncioTestCase):
async def test_correlates_result_and_times_out(self):
broker = ClientToolBroker()
outbound = []
async def push_frame(frame, direction=FrameDirection.DOWNSTREAM):
outbound.append((frame, direction))
broker.push_frame = push_frame
call = asyncio.create_task(
broker.call("set_photo_button_visible", {"visible": True}, timeout_seconds=1)
)
await asyncio.sleep(0)
message = outbound[0][0].message
self.assertIsInstance(outbound[0][0], OutputTransportMessageUrgentFrame)
await broker.process_frame(
InputTransportMessageFrame(
message={
"type": "client-tool-result",
"tool_call_id": message["tool_call_id"],
"status": "ok",
"data": {"visible": True},
}
),
FrameDirection.DOWNSTREAM,
)
self.assertEqual(
await call,
{"status": "ok", "data": {"visible": True}},
)
with self.assertRaisesRegex(ClientToolError, "超时"):
await broker.call("never_returns", {}, timeout_seconds=0.001)
async def test_fire_and_forget_does_not_create_pending_call(self):
broker = ClientToolBroker()
outbound = []
async def push_frame(frame, direction=FrameDirection.DOWNSTREAM):
outbound.append((frame, direction))
broker.push_frame = push_frame
result = await broker.call(
"open_panel",
{"visible": True},
timeout_seconds=1,
wait_for_response=False,
)
self.assertEqual(result, {"status": "ok", "data": {"dispatched": True}})
self.assertEqual(broker._pending, {})
self.assertFalse(outbound[0][0].message["wait_for_response"])
if __name__ == "__main__":
unittest.main()