Files
ai-video-fullstack/backend/services/brains/prompt_brain.py
Xin Wang 32aef14ddb Add workflow support and enhance runtime configuration in models and services
- Introduce RuntimeModelResource and RuntimeKnowledgeBase classes to manage workflow resources.
- Update AssistantConfig to include workflow_model_resources and workflow_knowledge_bases for better integration.
- Refactor validation and processing logic in routes and services to accommodate workflow types.
- Implement dynamic variable support for workflow assistants and enhance graph normalization.
- Add ToolExecutor for reusable tool execution across different assistant types.
- Update various services to ensure compatibility with new workflow features and improve error handling.
2026-07-13 16:13:27 +08:00

171 lines
6.2 KiB
Python

"""Local prompt assistant, including prompt-only reusable tools."""
from __future__ import annotations
from uuid import uuid4
from models import AssistantConfig
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.frames.frames import OutputTransportMessageUrgentFrame, TTSSpeakFrame
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.frame_processor import FrameProcessor
from pipecat.services.llm_service import (
FunctionCallParams,
FunctionCallResultProperties,
)
from pipecat.utils.time import time_now_iso8601
from services.brains.base import BaseBrain, BrainRuntime, BrainSpec
from services.runtime_variables import (
DynamicVariableStore,
)
from services.tool_executor import ToolExecutionError, ToolExecutor
class PromptBrain(BaseBrain):
spec = BrainSpec(
type="prompt",
supported_runtime_modes=frozenset({"pipeline", "realtime"}),
owns_context=True,
)
def __init__(self, cfg: AssistantConfig):
self._cfg = cfg
self._dynamic_enabled = True
self._store = DynamicVariableStore.from_config(cfg)
self._tools = ToolExecutor(self._store)
self._runtime: BrainRuntime | None = None
async def greeting(self, cfg: AssistantConfig) -> str:
return self._store.render(cfg.greeting) if self._dynamic_enabled else cfg.greeting
def system_prompt(self, cfg: AssistantConfig) -> str:
return self._store.render(cfg.prompt) if self._dynamic_enabled else cfg.prompt
def build_llm(self, cfg: AssistantConfig, context: LLMContext) -> FrameProcessor:
from services.pipecat.service_factory import create_llm
return create_llm(cfg)
async def setup(self, cfg: AssistantConfig, runtime: BrainRuntime) -> None:
self._runtime = runtime
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 == "http":
schema, handler = self._make_http_tool(tool, runtime)
else:
continue
schemas.append(schema)
runtime.llm.register_function(tool.function_name, handler)
runtime.set_tools(schemas)
def record_user_message(self, content: str) -> None:
if not self._dynamic_enabled:
return
self._store.record("user", content)
self._refresh_prompt()
async def on_assistant_text_end(
self,
_turn_id: str,
content: str,
interrupted: bool,
) -> None:
if content and not interrupted:
self._store.record("agent", content, completed_agent_turn=True)
self._refresh_prompt()
def _refresh_prompt(self) -> None:
if self._dynamic_enabled and self._runtime is not None:
self._runtime.set_system_prompt(self._store.render(self._cfg.prompt))
def _make_http_tool(self, tool, runtime: BrainRuntime):
properties, required = self._tools.schema_parts(tool)
self._tools.register_secrets(tool)
async def call_http(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)
except (ToolExecutionError, ValueError) as exc:
await params.result_callback(
{"status": "error", "message": f"HTTP 工具调用失败: {exc}"}
)
schema = FunctionSchema(
name=tool.function_name,
description=tool.description or f"调用 {tool.name}",
properties=properties,
required=required,
)
return schema, call_http
@staticmethod
def _make_end_call_tool(tool, runtime: BrainRuntime):
config = (tool.definition or {}).get("config") or {}
message_type = str(config.get("message_type") or "none")
custom_message = str(config.get("custom_message") or "").strip()
capture_reason = bool(config.get("capture_reason", True))
async def end_call(params: FunctionCallParams) -> None:
reason = str(params.arguments.get("reason") or "end_call_tool").strip()
runtime.call_end.begin(reason)
await params.result_callback(
{"status": "success", "action": "ending_call"},
properties=FunctionCallResultProperties(run_llm=False),
)
if message_type != "custom" or not custom_message:
await runtime.call_end.finish()
return
turn_id = uuid4().hex
timestamp = time_now_iso8601()
for message in (
{
"type": "assistant-text-start",
"turn_id": turn_id,
"timestamp": timestamp,
},
{
"type": "assistant-text-delta",
"turn_id": turn_id,
"delta": custom_message,
},
{
"type": "assistant-text-end",
"turn_id": turn_id,
"content": custom_message,
"interrupted": False,
},
):
await runtime.queue_frame(
OutputTransportMessageUrgentFrame(message=message)
)
runtime.call_end.arm_after_speech()
await runtime.queue_frame(
TTSSpeakFrame(custom_message, append_to_context=False)
)
properties = (
{
"reason": {
"type": "string",
"description": "结束本次通话的简短原因。",
}
}
if capture_reason
else {}
)
schema = FunctionSchema(
name=tool.function_name,
description=tool.description or "结束当前通话。",
properties=properties,
required=["reason"] if capture_reason else [],
)
return schema, end_call