feat: add configurable client tools and photo input
This commit is contained in:
@@ -17,6 +17,7 @@ from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||
from pipecat.frames.frames import Frame
|
||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||
from pipecat.processors.frame_processor import FrameProcessor
|
||||
from services.client_tools import ClientToolPort
|
||||
|
||||
|
||||
GREETING_CONTEXT_MARKER = "[会话事实:助手开场白已播放]"
|
||||
@@ -74,6 +75,7 @@ class BrainRuntime:
|
||||
set_system_prompt: Callable[[str], None]
|
||||
set_tools: Callable[[list[FunctionSchema] | None], None]
|
||||
call_end: CallEndPort
|
||||
client_tools: ClientToolPort | None = None
|
||||
worker: Any = None
|
||||
context_aggregator: Any = None
|
||||
transport: Any = None
|
||||
|
||||
@@ -20,6 +20,7 @@ from services.runtime_variables import (
|
||||
DynamicVariableStore,
|
||||
)
|
||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||
from services.tool_policy import policy_for_tool
|
||||
|
||||
|
||||
class PromptBrain(BaseBrain):
|
||||
@@ -50,17 +51,23 @@ class PromptBrain(BaseBrain):
|
||||
|
||||
async def setup(self, cfg: AssistantConfig, runtime: BrainRuntime) -> None:
|
||||
self._runtime = runtime
|
||||
self._tools.set_client_tools(runtime.client_tools)
|
||||
self._waiting_for_generated_end_speech = False
|
||||
schemas: list[FunctionSchema] = []
|
||||
for tool in cfg.tools:
|
||||
if tool.type == "end_call":
|
||||
schema, handler = self._make_end_call_tool(tool, runtime)
|
||||
elif tool.type in {"http", "mcp"}:
|
||||
elif tool.type in {"http", "mcp", "client"}:
|
||||
schema, handler = self._make_remote_tool(tool, runtime)
|
||||
else:
|
||||
continue
|
||||
schemas.append(schema)
|
||||
runtime.llm.register_function(tool.function_name, handler)
|
||||
policy = policy_for_tool(tool)
|
||||
runtime.llm.register_function(
|
||||
tool.function_name,
|
||||
handler,
|
||||
cancel_on_interruption=policy.cancel_on_interruption,
|
||||
)
|
||||
runtime.set_tools(schemas)
|
||||
|
||||
def record_user_message(self, content: str) -> None:
|
||||
@@ -99,16 +106,27 @@ class PromptBrain(BaseBrain):
|
||||
def _make_remote_tool(self, tool, runtime: BrainRuntime):
|
||||
properties, required = self._tools.schema_parts(tool)
|
||||
self._tools.register_secrets(tool)
|
||||
policy = policy_for_tool(tool)
|
||||
|
||||
async def call_http(params: FunctionCallParams) -> None:
|
||||
async def return_result(params: FunctionCallParams, result: dict) -> None:
|
||||
if not policy.runs_llm_after_result:
|
||||
await params.result_callback(
|
||||
result,
|
||||
properties=FunctionCallResultProperties(run_llm=False),
|
||||
)
|
||||
else:
|
||||
await params.result_callback(result)
|
||||
|
||||
async def call_tool(params: FunctionCallParams) -> None:
|
||||
try:
|
||||
result = await self._tools.execute(tool, dict(params.arguments or {}))
|
||||
if result["updated_variables"]:
|
||||
self._refresh_prompt()
|
||||
await params.result_callback(result)
|
||||
await return_result(params, result)
|
||||
except (ToolExecutionError, ValueError) as exc:
|
||||
await params.result_callback(
|
||||
{"status": "error", "message": f"工具调用失败: {exc}"}
|
||||
await return_result(
|
||||
params,
|
||||
{"status": "error", "message": f"工具调用失败: {exc}"},
|
||||
)
|
||||
|
||||
schema = FunctionSchema(
|
||||
@@ -117,7 +135,7 @@ class PromptBrain(BaseBrain):
|
||||
properties=properties,
|
||||
required=required,
|
||||
)
|
||||
return schema, call_http
|
||||
return schema, call_tool
|
||||
|
||||
def _make_end_call_tool(self, tool, runtime: BrainRuntime):
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
from dataclasses import replace
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
@@ -23,10 +24,15 @@ from pipecat.frames.frames import (
|
||||
)
|
||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||
from pipecat.processors.frame_processor import FrameProcessor
|
||||
from pipecat.services.llm_service import (
|
||||
FunctionCallParams,
|
||||
FunctionCallResultProperties,
|
||||
)
|
||||
from services.brains.base import BaseBrain, BrainRuntime, BrainSpec
|
||||
from services.knowledge import search as search_knowledge
|
||||
from services.runtime_variables import DynamicVariableStore
|
||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||
from services.tool_policy import policy_for_tool
|
||||
from services.workflow.agent import WorkflowAgentStage
|
||||
from services.workflow.models import (
|
||||
RouteStatus,
|
||||
@@ -42,6 +48,38 @@ from services.workflow_router import WorkflowLLMRouter
|
||||
MAX_AUTOMATIC_HOPS = 50
|
||||
|
||||
|
||||
class ConfiguredFlowManager(FlowManager):
|
||||
"""Preserve Flow transitions while suppressing late async-tool replies."""
|
||||
|
||||
async def _create_transition_func(self, name, handler):
|
||||
transition = await super()._create_transition_func(name, handler)
|
||||
if not getattr(handler, "_suppress_followup_llm", False):
|
||||
return transition
|
||||
|
||||
async def configured_transition(params: FunctionCallParams) -> None:
|
||||
original_callback = params.result_callback
|
||||
|
||||
async def result_callback(result, *, properties=None):
|
||||
if properties and properties.on_context_updated:
|
||||
# Deterministic Workflow transitions already use run_llm=False
|
||||
# and must retain their context-updated callback.
|
||||
configured_properties = properties
|
||||
elif properties:
|
||||
configured_properties = replace(properties, run_llm=False)
|
||||
else:
|
||||
configured_properties = FunctionCallResultProperties(
|
||||
run_llm=False
|
||||
)
|
||||
await original_callback(
|
||||
result,
|
||||
properties=configured_properties,
|
||||
)
|
||||
|
||||
await transition(replace(params, result_callback=result_callback))
|
||||
|
||||
return configured_transition
|
||||
|
||||
|
||||
class WorkflowBrain(BaseBrain):
|
||||
spec = BrainSpec(
|
||||
type="workflow",
|
||||
@@ -100,7 +138,7 @@ class WorkflowBrain(BaseBrain):
|
||||
self._cfg = cfg
|
||||
self._runtime = runtime
|
||||
self._store = DynamicVariableStore.from_config(cfg)
|
||||
self._tools = ToolExecutor(self._store)
|
||||
self._tools = ToolExecutor(self._store, client_tools=runtime.client_tools)
|
||||
self._tool_by_id = {tool.id: tool for tool in cfg.tools}
|
||||
self._router = WorkflowLLMRouter(cfg)
|
||||
self._edge_evaluator = WorkflowEdgeEvaluator(
|
||||
@@ -120,7 +158,7 @@ class WorkflowBrain(BaseBrain):
|
||||
self._ended = False
|
||||
self._greeting_context_message = None
|
||||
self._startup_waiting_for_greeting = False
|
||||
self._manager = FlowManager(
|
||||
self._manager = ConfiguredFlowManager(
|
||||
worker=runtime.worker,
|
||||
llm=runtime.llm,
|
||||
context_aggregator=runtime.context_aggregator,
|
||||
@@ -328,7 +366,7 @@ class WorkflowBrain(BaseBrain):
|
||||
functions: list[FlowsFunctionSchema] = []
|
||||
for tool_id in stage.tool_ids:
|
||||
tool = self._tool_by_id.get(str(tool_id))
|
||||
if tool and tool.type in {"http", "mcp"}:
|
||||
if tool and tool.type in {"http", "mcp", "client"}:
|
||||
functions.append(self._flow_tool(tool, node_id))
|
||||
knowledge_function = self._knowledge_function(node_id)
|
||||
if knowledge_function:
|
||||
@@ -409,6 +447,7 @@ class WorkflowBrain(BaseBrain):
|
||||
def _flow_tool(self, tool: RuntimeTool, node_id: str) -> FlowsFunctionSchema:
|
||||
properties, required = self._tools.schema_parts(tool)
|
||||
self._tools.register_secrets(tool)
|
||||
policy = policy_for_tool(tool)
|
||||
|
||||
async def handler(args, _flow_manager):
|
||||
transition_id = self._state.transition_id
|
||||
@@ -456,13 +495,21 @@ class WorkflowBrain(BaseBrain):
|
||||
)
|
||||
return result
|
||||
|
||||
if not policy.runs_llm_after_result:
|
||||
setattr(handler, "_suppress_followup_llm", True)
|
||||
|
||||
return FlowsFunctionSchema(
|
||||
name=tool.function_name,
|
||||
description=tool.description or f"调用 {tool.name}",
|
||||
properties=properties,
|
||||
required=required,
|
||||
handler=handler,
|
||||
cancel_on_interruption=True,
|
||||
cancel_on_interruption=policy.cancel_on_interruption,
|
||||
timeout_secs=(
|
||||
float(((tool.definition or {}).get("config") or {}).get("timeout_seconds") or 3)
|
||||
if tool.type == "client"
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
def _flow_managed_transition_config(
|
||||
|
||||
Reference in New Issue
Block a user