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

147 lines
5.2 KiB
Python

"""Resolve and apply one Workflow Agent's complete stage configuration."""
from __future__ import annotations
from copy import deepcopy
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,
greeting_context_message: dict[str, str] | None,
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
)
greeting_messages = (
[deepcopy(greeting_context_message)]
if strategy == ContextStrategy.RESET and greeting_context_message
else []
)
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": [
*greeting_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,
}