Files
ai-video-fullstack/backend/services/workflow/agent.py

138 lines
4.9 KiB
Python

"""Resolve and apply one Workflow Agent's complete stage configuration."""
from __future__ import annotations
from models import AssistantConfig
from pipecat.flows import ContextStrategy, ContextStrategyConfig, NodeConfig
from pipecat.frames.frames import LLMUpdateSettingsFrame
from pipecat.services.settings import LLMSettings
from services.brains.base import BrainRuntime
from services.runtime_variables import DynamicVariableStore
from services.vision import VISION_SYSTEM_HINT
from services.workflow_engine import WorkflowEngine
from services.workflow_router import WorkflowLLMRouter
AGENT_STAGE_INSTRUCTION = (
"工作流路由已在用户一轮输入结束时完成。只执行当前阶段任务,"
"不要自行解释、模拟或宣布节点切换。"
)
class WorkflowAgentStage:
"""Translate graph-level Agent data into Pipecat Flows configuration."""
def __init__(
self,
*,
cfg: AssistantConfig,
engine: WorkflowEngine,
store: DynamicVariableStore,
runtime: BrainRuntime,
) -> None:
self._cfg = cfg
self._engine = engine
self._store = store
self._runtime = runtime
def role_message(self, node_id: str) -> str:
stage_prompt = self._engine.prompt_for(node_id, self._store)
stage = self._engine.agent_stage_config(node_id)
if stage.vision_enabled:
stage_prompt = f"{stage_prompt}\n\n[视觉能力]\n{VISION_SYSTEM_HINT}"
return (
f"{stage_prompt}\n\n[工作流执行规则]\n"
f"{AGENT_STAGE_INSTRUCTION}"
)
async def refresh_prompt(self, node_id: str) -> None:
await self._runtime.queue_frame(
LLMUpdateSettingsFrame(
delta=LLMSettings(
system_instruction=self.role_message(node_id)
)
)
)
def router_for_node(
self,
node_id: str,
default_router: WorkflowLLMRouter,
) -> WorkflowLLMRouter:
resource_id = self._engine.agent_stage_config(node_id).llm_resource_id
resource = self._cfg.workflow_model_resources.get(resource_id)
if not resource:
return default_router
from services.pipecat.service_factory import config_with_resource
return WorkflowLLMRouter(config_with_resource(self._cfg, resource))
async def apply(self, node_id: str) -> None:
stage = self._engine.agent_stage_config(node_id)
if self._runtime.set_input_enabled:
self._runtime.set_input_enabled(True)
if self._runtime.apply_turn_config:
await self._runtime.apply_turn_config(
stage.enable_interrupt,
stage.turn_config,
)
if self._runtime.switch_services:
await self._runtime.switch_services(
stage.llm_resource_id or None,
stage.asr_resource_id or None,
stage.tts_resource_id or None,
)
if self._runtime.set_knowledge_scope:
self._runtime.set_knowledge_scope(
{
"knowledge_base_id": stage.knowledge_base_id,
"mode": stage.knowledge_mode,
"top_n": stage.knowledge_top_n,
"score_threshold": stage.knowledge_score_threshold,
}
)
if self._runtime.set_vision_scope:
self._runtime.set_vision_scope(
{
"enabled": stage.vision_enabled,
"vision_model_resource_id": stage.vision_model_resource_id,
"llm_resource_id": stage.llm_resource_id,
}
)
def node_config(
self,
node_id: str,
*,
functions: list,
leading_messages: list[dict[str, str]] | None = None,
) -> NodeConfig:
data = self._engine.data(node_id)
entry_mode = str(data.get("entryMode") or "wait_user")
entry_speech = self._store.render(str(data.get("entrySpeech") or ""))
strategy = (
ContextStrategy.RESET
if data.get("contextPolicy") == "fresh"
else ContextStrategy.APPEND
)
fixed_reply_messages = (
[{"role": "assistant", "content": entry_speech}]
if entry_mode == "fixed_speech" and entry_speech
else []
)
return {
"name": node_id,
"role_message": self.role_message(node_id),
"task_messages": [
*(leading_messages or []),
*fixed_reply_messages,
],
"functions": functions,
"context_strategy": ContextStrategyConfig(strategy=strategy),
# Direct node activations let WorkflowRuntime decide whether to run
# the LLM. A Pipecat function transition may explicitly override
# this because FlowManager must coordinate the tool result first.
"respond_immediately": False,
}