- Introduce new fields `dify_api_url` and `dify_api_key` in `AssistantConfig` for Dify API integration. - Update `requirements.txt` to include `dify-client-python` for Dify SDK support. - Modify `config_resolver` to handle Dify connection information. - Add a new `globalNode` type in workflow specifications to provide unified settings across workflows. - Enhance node specifications with additional constraints and default values for better configuration management. - Update frontend components to support the new `globalNode` type and its properties, improving workflow editor functionality.
189 lines
6.7 KiB
Python
189 lines
6.7 KiB
Python
"""Local graph-driven workflow assistant and its per-call state."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from loguru import logger
|
|
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 services.brains.base import BaseBrain, BrainRuntime, BrainSpec
|
|
from services.workflow_engine import WorkflowEngine
|
|
|
|
|
|
@dataclass
|
|
class WorkflowState:
|
|
current: str
|
|
ended: bool = False
|
|
turns_in_node: int = 0
|
|
end_turn_id: str | None = None
|
|
|
|
|
|
class WorkflowBrain(BaseBrain):
|
|
spec = BrainSpec(
|
|
type="workflow",
|
|
supported_runtime_modes=frozenset({"pipeline"}),
|
|
owns_context=True,
|
|
)
|
|
_FALLBACK_AFTER_TURNS = 2
|
|
|
|
def __init__(self, graph: dict[str, Any]):
|
|
self._engine = WorkflowEngine(graph or {})
|
|
if not self._engine.has_graph() or not self._engine.start_id:
|
|
raise ValueError("WorkflowBrain 缺少有效的 startCall 节点")
|
|
self._state = WorkflowState(current=self._engine.start_id)
|
|
self._history: list[dict[str, str]] = []
|
|
self._cfg: AssistantConfig | None = None
|
|
self._runtime: BrainRuntime | None = None
|
|
|
|
async def greeting(self, cfg: AssistantConfig) -> str:
|
|
return self._engine.greeting() or cfg.greeting
|
|
|
|
def system_prompt(self, cfg: AssistantConfig) -> str:
|
|
return self._engine.system_prompt_for(self._state.current)
|
|
|
|
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._cfg = cfg
|
|
self._runtime = runtime
|
|
for edge in self._engine.edges:
|
|
if edge.get("target"):
|
|
runtime.llm.register_function(
|
|
self._engine.edge_fn_name(edge),
|
|
self._make_transition_handler(edge),
|
|
)
|
|
self._apply_node(self._state.current)
|
|
logger.info(
|
|
f"工作流模式启用: 起始节点={self._engine.name(self._state.current)}"
|
|
)
|
|
|
|
async def on_connected(self) -> None:
|
|
await self._emit_node_active(self._state.current)
|
|
|
|
def record_user_message(self, content: str) -> None:
|
|
if content:
|
|
self._history.append({"role": "user", "content": content})
|
|
|
|
async def on_assistant_text_start(self, turn_id: str) -> None:
|
|
if self._state.ended and self._state.end_turn_id is None:
|
|
self._state.end_turn_id = turn_id
|
|
|
|
async def on_assistant_text_end(
|
|
self,
|
|
turn_id: str,
|
|
content: str,
|
|
interrupted: bool,
|
|
) -> None:
|
|
if not content or interrupted:
|
|
return
|
|
self._history.append({"role": "assistant", "content": content})
|
|
if turn_id == self._state.end_turn_id:
|
|
runtime = self._require_runtime()
|
|
runtime.call_end.begin("completed")
|
|
runtime.call_end.arm_after_speech()
|
|
elif not self._state.ended:
|
|
self._state.turns_in_node += 1
|
|
await self._fallback_route()
|
|
|
|
def _apply_node(self, node_id: str) -> None:
|
|
runtime = self._require_runtime()
|
|
runtime.set_system_prompt(self._engine.system_prompt_for(node_id))
|
|
if self._engine.is_end(node_id):
|
|
runtime.set_tools([])
|
|
return
|
|
runtime.set_tools(
|
|
[
|
|
FunctionSchema(
|
|
name=self._engine.edge_fn_name(edge),
|
|
description=self._engine.edge_description(edge),
|
|
properties={},
|
|
required=[],
|
|
)
|
|
for edge in self._engine.outgoing(node_id)
|
|
]
|
|
)
|
|
|
|
async def _go_to_node(self, target: str) -> None:
|
|
self._state.current = target
|
|
self._state.turns_in_node = 0
|
|
if self._engine.is_end(target):
|
|
self._state.ended = True
|
|
await self._emit_node_active(target)
|
|
self._apply_node(target)
|
|
|
|
async def _emit_node_active(self, node_id: str | None) -> None:
|
|
if node_id:
|
|
await self._require_runtime().queue_frame(
|
|
OutputTransportMessageUrgentFrame(
|
|
message={"type": "node-active", "nodeId": node_id}
|
|
)
|
|
)
|
|
|
|
async def _speak_transition(self, edge: dict | None) -> None:
|
|
speech = self._engine.edge_transition_speech(edge)
|
|
if speech:
|
|
await self._require_runtime().queue_frame(
|
|
TTSSpeakFrame(speech, append_to_context=False)
|
|
)
|
|
|
|
def _make_transition_handler(self, edge: dict):
|
|
target = str(edge.get("target"))
|
|
|
|
async def handler(params) -> None:
|
|
logger.info(f"LLM 触发转移 → {self._engine.name(target)}")
|
|
if not self._engine.is_end(target):
|
|
await self._speak_transition(edge)
|
|
await self._go_to_node(target)
|
|
await params.result_callback({"status": "ok"})
|
|
|
|
return handler
|
|
|
|
async def _fallback_route(self) -> None:
|
|
if self._state.ended:
|
|
return
|
|
if self._state.turns_in_node < self._FALLBACK_AFTER_TURNS:
|
|
return
|
|
if not self._engine.outgoing(self._state.current):
|
|
return
|
|
|
|
cfg = self._require_config()
|
|
target = await self._engine.route(
|
|
self._state.current,
|
|
self._history,
|
|
api_key=self._require(cfg.llm_api_key, "LLM apiKey"),
|
|
base_url=self._require(cfg.llm_base_url, "LLM apiUrl"),
|
|
model=self._require(cfg.model, "LLM modelId"),
|
|
)
|
|
if target and target != self._state.current:
|
|
logger.info(f"文本兜底触发转移 → {self._engine.name(target)}")
|
|
if not self._engine.is_end(target):
|
|
await self._speak_transition(
|
|
self._engine.find_edge(self._state.current, target)
|
|
)
|
|
await self._go_to_node(target)
|
|
|
|
def _require_runtime(self) -> BrainRuntime:
|
|
if self._runtime is None:
|
|
raise RuntimeError("WorkflowBrain 尚未绑定 pipeline runtime")
|
|
return self._runtime
|
|
|
|
def _require_config(self) -> AssistantConfig:
|
|
if self._cfg is None:
|
|
raise RuntimeError("WorkflowBrain 尚未初始化配置")
|
|
return self._cfg
|
|
|
|
@staticmethod
|
|
def _require(value: str, label: str) -> str:
|
|
if value:
|
|
return value
|
|
raise ValueError(f"缺少模型资源配置: {label}")
|