- Introduce RuntimeModelResource and RuntimeKnowledgeBase classes to manage workflow resources. - Update AssistantConfig to include workflow_model_resources and workflow_knowledge_bases for better integration. - Refactor validation and processing logic in routes and services to accommodate workflow types. - Implement dynamic variable support for workflow assistants and enhance graph normalization. - Add ToolExecutor for reusable tool execution across different assistant types. - Update various services to ensure compatibility with new workflow features and improve error handling.
141 lines
5.4 KiB
Python
141 lines
5.4 KiB
Python
"""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"}
|
|
]
|