"""Pure Workflow v3 graph queries and deterministic edge evaluation.""" from __future__ import annotations import re from typing import Any from services.node_specs import normalize_graph from services.runtime_variables import DynamicVariableStore 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 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 prompt_for(self, node_id: str, store: DynamicVariableStore) -> str: prompt = store.render(str(self.data(node_id).get("prompt") or "").strip()) sections = [f"[当前阶段:{self.name(node_id)}]"] if 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"} ]