"""Pure Workflow v3 graph queries and deterministic edge evaluation.""" from __future__ import annotations import re from dataclasses import dataclass from typing import Any from services.node_specs import normalize_graph from services.runtime_variables import DynamicVariableStore @dataclass(frozen=True) class AgentStageConfig: """The complete assistant configuration active inside one Agent node.""" inherits_global: bool llm_resource_id: str asr_resource_id: str tts_resource_id: str tool_ids: tuple[str, ...] knowledge_base_id: str | None knowledge_mode: str knowledge_top_n: int knowledge_score_threshold: float class WorkflowEngine: def __init__(self, graph: dict[str, Any]): self.graph = normalize_graph(graph) self.settings = self.graph.get("settings") or {} self.nodes: dict[str, dict] = { str(node["id"]): node for node in self.graph.get("nodes") or [] if node.get("id") } self.edges: list[dict] = list(self.graph.get("edges") or []) self.start_id = next( ( node_id for node_id, node in self.nodes.items() if node.get("type") == "start" ), None, ) def has_graph(self) -> bool: return bool(self.start_id) def node_type(self, node_id: str | None) -> str | None: return self.nodes.get(node_id or "", {}).get("type") def data(self, node_id: str | None) -> dict: return self.nodes.get(node_id or "", {}).get("data") or {} def name(self, node_id: str | None) -> str: return str(self.data(node_id).get("name") or self.node_type(node_id) or "") def outgoing(self, node_id: str | None) -> list[dict]: result = [edge for edge in self.edges if edge.get("source") == node_id] return sorted( result, key=lambda edge: int((edge.get("data") or {}).get("priority", 10)), ) def has_outgoing(self, node_id: str | None) -> bool: return any(edge.get("source") == node_id for edge in self.edges) def edge_mode(self, edge: dict) -> str: return str((edge.get("data") or {}).get("mode") or "always") def edge_fn_name(self, edge: dict) -> str: raw = edge.get("id") or f"{edge.get('source')}_{edge.get('target')}" slug = re.sub(r"[^a-z0-9]+", "_", str(raw).lower()).strip("_") return f"goto_{slug or 'next'}" def edge_description(self, edge: dict) -> str: data = edge.get("data") or {} target = self.name(str(edge.get("target") or "")) condition = str(data.get("condition") or "").strip() if condition: return f"当满足以下条件时转到「{target}」:{condition}" return f"当当前阶段任务完成时转到「{target}」。" def edge_transition_speech(self, edge: dict | None) -> str: if not edge: return "" data = edge.get("data") or {} return str( data.get("transitionSpeech") or data.get("transition_speech") or "" ) def global_prompt(self) -> str: return str(self.settings.get("globalPrompt") or "").strip() def inherits_global_config(self, node_id: str) -> bool: """Return the Agent's explicit configuration scope, defaulting to global.""" return bool(self.data(node_id).get("inheritGlobalConfig", True)) def agent_stage_config(self, node_id: str) -> AgentStageConfig: """Resolve either Workflow defaults or one Agent's complete override.""" data = self.data(node_id) inherits_global = self.inherits_global_config(node_id) source = self.settings if inherits_global else data llm_key = "defaultLlmResourceId" if inherits_global else "llmResourceId" asr_key = "defaultAsrResourceId" if inherits_global else "asrResourceId" tts_key = "defaultTtsResourceId" if inherits_global else "ttsResourceId" knowledge_base_id = str(source.get("knowledgeBaseId") or "") return AgentStageConfig( inherits_global=inherits_global, llm_resource_id=str(source.get(llm_key) or ""), asr_resource_id=str(source.get(asr_key) or ""), tts_resource_id=str(source.get(tts_key) or ""), tool_ids=tuple(str(tool_id) for tool_id in source.get("toolIds") or []), knowledge_base_id=knowledge_base_id or None, knowledge_mode=( str(source.get("knowledgeMode") or "automatic") if knowledge_base_id else "disabled" ), knowledge_top_n=int(source.get("knowledgeTopN") or 5), knowledge_score_threshold=float( source.get("knowledgeScoreThreshold") or 0.0 ), ) def prompt_for(self, node_id: str, store: DynamicVariableStore) -> str: """Build the Agent system prompt according to its inheritance setting.""" prompt = store.render(str(self.data(node_id).get("prompt") or "").strip()) sections = [f"[当前阶段:{self.name(node_id)}]"] if self.inherits_global_config(node_id) and self.global_prompt(): sections.append(f"[全局规则]\n{store.render(self.global_prompt())}") if prompt: sections.append(f"[当前阶段任务]\n{prompt}") return "\n\n".join(sections) def greeting(self, store: DynamicVariableStore) -> str: return store.render(str(self.data(self.start_id).get("greeting") or "")) def expression_matches(self, expression: dict, values: dict[str, Any]) -> bool: results = [] for rule in expression.get("rules") or []: name = str(rule.get("variable") or "") operator = str(rule.get("operator") or "") expected = rule.get("value") exists = name in values actual = values.get(name) try: if operator == "exists": matched = exists if expected is not False else not exists elif operator == "eq": matched = actual == expected elif operator == "neq": matched = actual != expected elif operator == "gt": matched = actual > expected elif operator == "gte": matched = actual >= expected elif operator == "lt": matched = actual < expected elif operator == "lte": matched = actual <= expected elif operator == "contains": matched = expected in actual elif operator == "in": matched = actual in expected else: matched = False except (TypeError, ValueError): matched = False results.append(matched) if not results: return False return ( all(results) if expression.get("combinator", "and") == "and" else any(results) ) def deterministic_edge( self, node_id: str, store: DynamicVariableStore, *, include_default: bool, ) -> dict | None: default = None for edge in self.outgoing(node_id): data = edge.get("data") or {} mode = data.get("mode") if mode == "expression" and self.expression_matches( data.get("expression") or {}, store.values ): return edge if mode == "always": default = edge return default if include_default else None def llm_edges(self, node_id: str) -> list[dict]: return [ edge for edge in self.outgoing(node_id) if self.edge_mode(edge) in {"llm", "always"} ]