feat(workflow): add edge-tool routing and realtime runtime
This commit is contained in:
128
backend/services/pipecat/realtime_tools.py
Normal file
128
backend/services/pipecat/realtime_tools.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Provider-neutral function calling for speech-to-speech sessions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RealtimeTool:
|
||||
"""Small JSON-schema tool definition understood by both providers."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
properties: dict[str, Any] = field(default_factory=dict)
|
||||
required: tuple[str, ...] = ()
|
||||
|
||||
def provider_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": self.properties,
|
||||
"required": list(self.required),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RealtimeToolResult:
|
||||
"""Tool output plus whether the model should continue immediately."""
|
||||
|
||||
output: dict[str, Any]
|
||||
continue_response: bool = True
|
||||
after_output: Callable[[], Awaitable[None]] | None = None
|
||||
|
||||
|
||||
RealtimeToolDispatcher = Callable[
|
||||
[str, dict[str, Any], str], Awaitable[RealtimeToolResult]
|
||||
]
|
||||
SendProviderEvent = Callable[[dict[str, Any]], Awaitable[None]]
|
||||
|
||||
|
||||
class RealtimeToolSession:
|
||||
"""Serialize provider calls and answer every call id at most once."""
|
||||
|
||||
def __init__(self, send_event: SendProviderEvent) -> None:
|
||||
self._send_event = send_event
|
||||
self._dispatcher: RealtimeToolDispatcher | None = None
|
||||
self._handled_call_ids: set[str] = set()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def set_dispatcher(self, dispatcher: RealtimeToolDispatcher | None) -> None:
|
||||
self._dispatcher = dispatcher
|
||||
|
||||
async def handle_call(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
call_id: str,
|
||||
arguments: str | dict[str, Any] | None,
|
||||
) -> None:
|
||||
if not call_id:
|
||||
logger.warning("Realtime function call 缺少 call_id,已忽略")
|
||||
return
|
||||
async with self._lock:
|
||||
if call_id in self._handled_call_ids:
|
||||
return
|
||||
self._handled_call_ids.add(call_id)
|
||||
parsed = self._parse_arguments(arguments)
|
||||
try:
|
||||
if self._dispatcher is None:
|
||||
result = RealtimeToolResult(
|
||||
{"status": "error", "message": "当前会话未注册工具处理器"}
|
||||
)
|
||||
else:
|
||||
result = await self._dispatcher(name, parsed, call_id)
|
||||
except Exception as exc: # noqa: BLE001 - return tool errors to provider
|
||||
logger.exception(f"Realtime 工具 {name} 执行失败:{exc}")
|
||||
result = RealtimeToolResult(
|
||||
{
|
||||
"status": "error",
|
||||
"message": f"工具执行失败:{type(exc).__name__}",
|
||||
}
|
||||
)
|
||||
|
||||
await self._send_event(
|
||||
{
|
||||
"type": "conversation.item.create",
|
||||
"item": {
|
||||
"type": "function_call_output",
|
||||
"call_id": call_id,
|
||||
"output": json.dumps(
|
||||
result.output,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
if result.after_output is not None:
|
||||
await result.after_output()
|
||||
if result.continue_response:
|
||||
await self._send_event({"type": "response.create"})
|
||||
|
||||
def clear(self) -> None:
|
||||
self._handled_call_ids.clear()
|
||||
|
||||
@staticmethod
|
||||
def _parse_arguments(
|
||||
arguments: str | dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
if isinstance(arguments, dict):
|
||||
return dict(arguments)
|
||||
if not arguments:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(arguments)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return {}
|
||||
return dict(parsed) if isinstance(parsed, dict) else {}
|
||||
Reference in New Issue
Block a user