feat: add configurable client tools and photo input

This commit is contained in:
Xin Wang
2026-07-30 19:06:03 +08:00
parent 510a277b5a
commit 913435785e
24 changed files with 1802 additions and 139 deletions

View File

@@ -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(