Files
ai-video-fullstack/backend/tests/test_workflow_v3.py
2026-08-04 17:05:26 +08:00

908 lines
34 KiB
Python

from __future__ import annotations
import asyncio
import unittest
from copy import deepcopy
from types import SimpleNamespace
from unittest.mock import AsyncMock
from models import AssistantConfig, RuntimeModelResource
from fastapi import HTTPException
from routes.assistants import (
_validate_system_tool_selection,
_validate_workflow,
_validate_workflow_references,
)
from schemas import AssistantUpsert
from services.pipecat.service_factory import config_with_resource
from services.node_specs import graph_references, normalize_graph, validate_graph
from services.runtime_variables import DynamicVariableStore, prepare_dynamic_config
from services.vision import config_with_vision_resource
from services.workflow_engine import WorkflowEngine
def valid_graph():
return {
"specVersion": 3,
"settings": {"globalPrompt": "服务 {{customer}}"},
"nodes": [
{"id": "start", "type": "start", "data": {"name": "Start"}},
{
"id": "agent",
"type": "agent",
"data": {"name": "Agent", "prompt": "处理订单", "contextPolicy": "fresh"},
},
{"id": "end", "type": "end", "data": {"name": "End", "scope": "flow"}},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
},
{
"id": "paid",
"source": "agent",
"target": "end",
"data": {
"mode": "expression",
"priority": 10,
"expression": {
"combinator": "and",
"rules": [
{"variable": "order_status", "operator": "eq", "value": "paid"}
],
},
},
},
],
}
class WorkflowGraphTests(unittest.TestCase):
def test_workflow_removes_assistant_and_start_greetings(self):
body = AssistantUpsert(
name="无开场白工作流",
type="workflow",
greeting="旧助手级开场白",
graph=valid_graph(),
)
body.graph["nodes"][0]["data"]["greeting"] = "旧 Start 开场白"
normalized = normalize_graph(body.graph)
self.assertEqual(body.greeting, "")
self.assertNotIn("greeting", normalized["nodes"][0]["data"])
def test_revision_pins_the_normalized_workflow_snapshot(self):
first = WorkflowEngine(valid_graph())
same_graph = deepcopy(valid_graph())
same_graph["settings"] = dict(reversed(list(same_graph["settings"].items())))
second = WorkflowEngine(same_graph)
self.assertEqual(first.revision, second.revision)
self.assertTrue(first.revision.startswith("sha256:"))
metadata = first.session_metadata()
first.graph["settings"]["globalPrompt"] = "运行时外部修改"
self.assertEqual(
metadata["workflow"]["snapshot"]["settings"]["globalPrompt"],
"服务 {{customer}}",
)
changed = valid_graph()
changed["settings"]["globalPrompt"] = "另一份配置"
self.assertNotEqual(first.revision, WorkflowEngine(changed).revision)
def test_workflow_graph_owns_flat_vision_session_flag(self):
graph = valid_graph()
graph["settings"].update(
{
"defaultLlmResourceId": "llm_global",
"visionEnabled": True,
"visionModelResourceId": "vision_global",
}
)
body = AssistantUpsert(
name="视觉工作流",
type="workflow",
graph=graph,
visionEnabled=False,
visionModelResourceId="legacy_vision",
)
_validate_workflow(body)
self.assertTrue(body.vision_enabled)
self.assertIsNone(body.vision_model_resource_id)
def test_agent_entry_modes_only_control_conversation_start(self):
graph = valid_graph()
normalized = normalize_graph(graph)
agent = next(node for node in normalized["nodes"] if node["type"] == "agent")
self.assertEqual(agent["data"]["entryMode"], "wait_user")
self.assertNotIn("entrySpeech", agent["data"])
self.assertTrue(agent["data"]["inheritGlobalConfig"])
self.assertEqual(agent["data"]["contextPolicy"], "fresh")
agent["data"]["entryMode"] = "generate"
self.assertEqual(validate_graph(normalized), [])
agent["data"]["entryMode"] = "fixed_speech"
agent["data"]["entrySpeech"] = "您好,{{customer}}"
cleaned = normalize_graph(normalized)
cleaned_agent = next(
node for node in cleaned["nodes"] if node["type"] == "agent"
)
self.assertEqual(cleaned_agent["data"]["entryMode"], "wait_user")
self.assertNotIn("entrySpeech", cleaned_agent["data"])
def test_edge_speech_fields_are_removed(self):
graph = valid_graph()
graph["edges"][0]["data"]["transitionSpeech"] = "不再播放"
graph["edges"][1]["data"]["transition_speech"] = "旧字段也不再播放"
normalized = normalize_graph(graph)
for edge in normalized["edges"]:
self.assertNotIn("transitionSpeech", edge["data"])
self.assertNotIn("transition_speech", edge["data"])
def test_action_defaults_preserve_legacy_result_assignment_behavior(self):
graph = valid_graph()
graph["nodes"].extend(
[
{
"id": "legacy_override",
"type": "action",
"data": {"resultAssignments": {"status": "order.status"}},
},
{"id": "legacy_none", "type": "action", "data": {}},
{
"id": "explicit_inherit",
"type": "action",
"data": {"resultAssignmentMode": "inherit"},
},
]
)
normalized = normalize_graph(graph)
actions = {
node["id"]: node["data"]
for node in normalized["nodes"]
if node["type"] == "action"
}
self.assertEqual(
actions["legacy_override"]["resultAssignmentMode"], "override"
)
self.assertEqual(actions["legacy_none"]["resultAssignmentMode"], "none")
self.assertEqual(
actions["explicit_inherit"]["resultAssignmentMode"], "inherit"
)
self.assertEqual(actions["legacy_none"]["resultAssignments"], {})
self.assertEqual(actions["legacy_none"]["userInputPolicy"], "queue")
self.assertNotIn("speech", actions["legacy_none"])
def test_action_advanced_settings_are_validated(self):
graph = valid_graph()
graph["nodes"].append(
{
"id": "action",
"type": "action",
"data": {
"resultAssignmentMode": "unexpected",
"resultAssignments": [],
"userInputPolicy": "cancel",
},
}
)
errors = validate_graph(graph)
self.assertTrue(any("结果变量模式无效" in error for error in errors))
self.assertTrue(any("结果变量映射必须是对象" in error for error in errors))
self.assertTrue(any("用户输入策略无效" in error for error in errors))
def test_message_defaults_and_validation(self):
graph = valid_graph()
graph["nodes"].append(
{
"id": "message",
"type": "message",
"data": {"speech": "请确认重要信息"},
}
)
normalized = normalize_graph(graph)
message = next(
node for node in normalized["nodes"] if node["type"] == "message"
)
self.assertEqual(message["data"]["completionPolicy"], "playback")
self.assertNotIn("requireConfirmation", message["data"])
self.assertNotIn("showMessage", message["data"])
self.assertEqual(message["data"]["confirmLabel"], "确认")
message["data"].update(
{
"speech": "",
"title": "重要提示",
"message": "",
"confirmLabel": "确认",
"completionPolicy": "confirmation",
}
)
errors = validate_graph(normalized)
self.assertTrue(any("弹窗消息必须为" in error for error in errors))
self.assertTrue(any("必须配置播报内容" in error for error in errors))
message["data"].update(
{"speech": "", "completionPolicy": "playback"}
)
errors = validate_graph(normalized)
self.assertTrue(any("必须配置播报内容" in error for error in errors))
message["data"]["completionPolicy"] = "unknown"
errors = validate_graph(normalized)
self.assertTrue(any("完成策略无效" in error for error in errors))
legacy = valid_graph()
legacy["nodes"].append(
{
"id": "legacy-message",
"type": "message",
"data": {
"speech": "请确认",
"showMessage": True,
"title": "提示",
"message": "请确认",
"confirmLabel": "确认",
"requireConfirmation": True,
},
}
)
legacy_message = normalize_graph(legacy)["nodes"][-1]["data"]
self.assertEqual(legacy_message["completionPolicy"], "confirmation")
self.assertNotIn("requireConfirmation", legacy_message)
self.assertNotIn("showMessage", legacy_message)
def test_voice_resource_creates_isolated_runtime_config(self):
base = AssistantConfig(type="workflow", asr="default", voice="default")
asr = RuntimeModelResource(
id="asr_1",
capability="ASR",
interface_type="openai-asr",
values={"modelId": "sensevoice", "language": "zh", "apiUrl": "https://asr.test"},
secrets={"apiKey": "secret"},
)
resolved = config_with_resource(base, asr)
self.assertEqual(resolved.asr, "sensevoice")
self.assertEqual(resolved.stt_api_key, "secret")
self.assertEqual(base.asr, "default")
llm = RuntimeModelResource(
id="llm_1",
capability="LLM",
interface_type="openai-llm",
values={"modelId": "deepseek-chat", "apiUrl": "https://llm.test/v1"},
secrets={"apiKey": "llm-secret"},
)
llm_resolved = config_with_resource(base, llm)
self.assertEqual(llm_resolved.model, "deepseek-chat")
self.assertEqual(llm_resolved.llm_api_key, "llm-secret")
def test_global_and_custom_agent_references_are_preserved(self):
graph = valid_graph()
graph["settings"].update(
{
"defaultLlmResourceId": "llm_global",
"defaultAsrResourceId": "asr_global",
"defaultTtsResourceId": "tts_global",
"visionEnabled": True,
"visionModelResourceId": "vision_global",
"toolIds": ["tool_global"],
"knowledgeBaseId": "kb_global",
}
)
agent = next(node for node in graph["nodes"] if node["type"] == "agent")
agent["data"].update(
{
"inheritGlobalConfig": False,
"llmResourceId": "llm_agent",
"asrResourceId": "asr_agent",
"ttsResourceId": "tts_agent",
"visionEnabled": True,
"visionModelResourceId": "vision_agent",
"toolIds": ["tool_agent"],
"knowledgeBaseId": "kb_agent",
}
)
refs = graph_references(graph)
self.assertEqual(
refs["model_resources"],
{
"llm_global",
"asr_global",
"tts_global",
"vision_global",
"llm_agent",
"asr_agent",
"tts_agent",
"vision_agent",
},
)
self.assertEqual(refs["tools"], {"tool_global", "tool_agent"})
self.assertEqual(refs["knowledge_bases"], {"kb_global", "kb_agent"})
def test_existing_agent_override_disables_implicit_inheritance(self):
graph = valid_graph()
agent = next(node for node in graph["nodes"] if node["type"] == "agent")
agent["data"]["toolIds"] = ["legacy_tool"]
normalized = normalize_graph(graph)
normalized_agent = next(
node for node in normalized["nodes"] if node["type"] == "agent"
)
self.assertFalse(normalized_agent["data"]["inheritGlobalConfig"])
def test_inherited_agent_ignores_stale_custom_references(self):
graph = valid_graph()
agent = next(node for node in graph["nodes"] if node["type"] == "agent")
agent["data"].update(
{
"inheritGlobalConfig": True,
"llmResourceId": "stale_llm",
"asrResourceId": "stale_asr",
"ttsResourceId": "stale_tts",
"toolIds": ["stale_tool"],
"knowledgeBaseId": "stale_kb",
}
)
refs = graph_references(graph)
self.assertNotIn("stale_llm", refs["model_resources"])
self.assertNotIn("stale_tool", refs["tools"])
self.assertNotIn("stale_kb", refs["knowledge_bases"])
def test_agent_effective_config_inherits_then_switches_to_override(self):
graph = valid_graph()
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
agent["data"].update(
{
"stateVariableNames": ["customer", "order_status"],
}
)
graph["settings"].update(
{
"defaultLlmResourceId": "llm_global",
"defaultAsrResourceId": "asr_global",
"defaultTtsResourceId": "tts_global",
"visionEnabled": True,
"visionModelResourceId": "vision_global",
"toolIds": ["tool_global", "update_state", "skip_turn"],
"knowledgeBaseId": "kb_global",
"knowledgeMode": "on_demand",
"knowledgeTopN": 8,
"knowledgeScoreThreshold": 0.4,
"enableInterrupt": False,
"turnConfig": {
"bargeIn": {"strategy": "transcription"},
"turnDetection": {"strategy": "silence", "silenceTimeoutSecs": 1.2},
},
}
)
engine = WorkflowEngine(graph)
inherited = engine.agent_stage_config("agent")
self.assertEqual(inherited.llm_resource_id, "llm_global")
self.assertTrue(inherited.vision_enabled)
self.assertEqual(
inherited.vision_model_resource_id,
"vision_global",
)
self.assertTrue(engine.uses_vision())
self.assertEqual(
inherited.tool_ids,
("tool_global", "update_state", "skip_turn"),
)
self.assertEqual(
inherited.state_variable_names,
("customer", "order_status"),
)
self.assertEqual(inherited.knowledge_mode, "on_demand")
self.assertFalse(inherited.enable_interrupt)
self.assertEqual(
inherited.turn_config["bargeIn"]["strategy"],
"transcription",
)
engine.data("agent").update(
{
"inheritGlobalConfig": False,
"llmResourceId": "llm_agent",
"toolIds": ["tool_agent", "update_state", "skip_turn"],
"knowledgeBaseId": "",
"visionEnabled": False,
"visionModelResourceId": "",
"enableInterrupt": True,
"turnConfig": {
"bargeIn": {"strategy": "vad"},
"turnDetection": {"strategy": "smart_turn"},
},
}
)
custom = engine.agent_stage_config("agent")
self.assertEqual(custom.llm_resource_id, "llm_agent")
self.assertFalse(custom.vision_enabled)
self.assertIsNone(custom.vision_model_resource_id)
self.assertFalse(engine.uses_vision())
self.assertEqual(
custom.tool_ids,
("tool_agent", "update_state", "skip_turn"),
)
self.assertEqual(custom.knowledge_mode, "disabled")
self.assertTrue(custom.enable_interrupt)
self.assertEqual(
custom.turn_config["turnDetection"]["strategy"],
"smart_turn",
)
def test_update_state_node_and_agent_state_scope_are_validated(self):
graph = valid_graph()
graph["nodes"].insert(
1,
{
"id": "set_state",
"type": "update_state",
"data": {
"name": "记录状态",
"assignments": {"confirmed": True},
},
},
)
graph["edges"][0]["target"] = "set_state"
graph["edges"].insert(
1,
{
"id": "after_state",
"source": "set_state",
"target": "agent",
"data": {"mode": "always", "priority": 0},
},
)
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
agent["data"].update(
{
"stateVariableNames": ["customer"],
}
)
body = AssistantUpsert(
name="状态工作流",
type="workflow",
dynamicVariableDefinitions={
"confirmed": {
"type": "boolean",
"required": False,
"default": False,
},
"customer": {
"type": "string",
"required": False,
"default": None,
},
},
graph=graph,
)
_validate_workflow(body)
normalized_state = next(
node for node in body.graph["nodes"] if node["type"] == "update_state"
)
self.assertEqual(normalized_state["data"]["assignments"], {"confirmed": True})
normalized_state["data"]["assignments"] = {"missing": "x"}
with self.assertRaisesRegex(HTTPException, "未声明变量:missing"):
_validate_workflow(body)
def test_agent_update_state_tool_requires_an_authorized_variable(self):
graph = valid_graph()
graph["settings"]["toolIds"] = ["tool_update_state"]
agent = next(node for node in graph["nodes"] if node["id"] == "agent")
body = AssistantUpsert(
name="缺少授权",
type="workflow",
dynamicVariableDefinitions={
"customer": {
"type": "string",
"required": False,
"default": None,
}
},
graph=graph,
)
_validate_workflow(body)
system_tool = SimpleNamespace(
id="tool_update_state",
status="active",
type="system",
definition={"config": {"kind": "update_state"}},
)
result = SimpleNamespace(
scalars=lambda: SimpleNamespace(all=lambda: [system_tool])
)
session = SimpleNamespace(execute=AsyncMock(return_value=result))
with self.assertRaisesRegex(HTTPException, "必须授权至少一个变量"):
asyncio.run(_validate_system_tool_selection(session, body))
def test_vision_resource_creates_isolated_runtime_config(self):
base = AssistantConfig(type="workflow", model="text-only")
resource = RuntimeModelResource(
id="vision_1",
capability="LLM",
interface_type="openai-llm",
values={
"modelId": "vision-model",
"apiUrl": "https://vision.test/v1",
},
secrets={"apiKey": "vision-secret"},
support_image_input=True,
)
resolved = config_with_vision_resource(base, resource)
self.assertEqual(resolved.vision_model, "vision-model")
self.assertEqual(resolved.vision_llm_api_key, "vision-secret")
self.assertEqual(resolved.vision_llm_base_url, "https://vision.test/v1")
self.assertEqual(base.model, "text-only")
def test_start_agent_action_and_handoff_may_have_no_outgoing_edge(self):
terminal_graphs = [
{
"specVersion": 3,
"settings": {},
"nodes": [{"id": "start", "type": "start", "data": {}}],
"edges": [],
},
{
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "agent",
"type": "agent",
"data": {"prompt": "持续处理用户问题"},
},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
}
],
},
{
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "handoff", "type": "handoff", "data": {}},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "handoff",
"data": {"mode": "always", "priority": 0},
}
],
},
]
for graph in terminal_graphs:
with self.subTest(node=graph["nodes"][-1]["type"]):
self.assertEqual(validate_graph(graph), [])
action_without_exit = {
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "action", "type": "action", "data": {}},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "action",
"data": {"mode": "always", "priority": 0},
}
],
}
self.assertEqual(validate_graph(action_without_exit), [])
def test_agent_cannot_have_only_one_default_path(self):
graph = {
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "agent", "type": "agent", "data": {}},
{"id": "end", "type": "end", "data": {}},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
},
{
"id": "leave",
"source": "agent",
"target": "end",
"data": {"mode": "always", "priority": 0},
},
],
}
self.assertTrue(
any(
"Agent 节点 agent 不能只有默认路径" in error
for error in validate_graph(graph)
)
)
graph["edges"][1]["data"] = {
"mode": "llm",
"priority": 10,
"condition": "当前阶段已经完成",
}
self.assertEqual(validate_graph(graph), [])
def test_all_source_nodes_allow_multiple_conditional_paths(self):
expression = {
"combinator": "and",
"rules": [{"variable": "route", "operator": "eq", "value": "yes"}],
}
cases = {
"start": {
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "agent", "type": "agent", "data": {}},
{"id": "action", "type": "action", "data": {}},
{"id": "end", "type": "end", "data": {}},
],
"edges": [
{"id": "default", "source": "start", "target": "agent", "data": {"mode": "always", "priority": 10}},
{"id": "llm", "source": "start", "target": "end", "data": {"mode": "llm", "priority": 20, "condition": "应当结束"}},
{"id": "expr", "source": "start", "target": "action", "data": {"mode": "expression", "priority": 30, "expression": expression}},
{"id": "action-end", "source": "action", "target": "end", "data": {"mode": "always", "priority": 10}},
],
},
"agent": {
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "agent", "type": "agent", "data": {}},
{"id": "action", "type": "action", "data": {}},
{"id": "handoff", "type": "handoff", "data": {}},
{"id": "end", "type": "end", "data": {}},
],
"edges": [
{"id": "begin", "source": "start", "target": "agent", "data": {"mode": "always", "priority": 10}},
{"id": "default", "source": "agent", "target": "end", "data": {"mode": "always", "priority": 10}},
{"id": "llm", "source": "agent", "target": "action", "data": {"mode": "llm", "priority": 20, "condition": "执行操作"}},
{"id": "expr", "source": "agent", "target": "handoff", "data": {"mode": "expression", "priority": 30, "expression": expression}},
{"id": "action-end", "source": "action", "target": "end", "data": {"mode": "always", "priority": 10}},
],
},
"action": {
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "action", "type": "action", "data": {}},
{"id": "agent", "type": "agent", "data": {}},
{"id": "handoff", "type": "handoff", "data": {}},
{"id": "end", "type": "end", "data": {}},
],
"edges": [
{"id": "begin", "source": "start", "target": "action", "data": {"mode": "always", "priority": 10}},
{"id": "default", "source": "action", "target": "agent", "data": {"mode": "always", "priority": 10}},
{"id": "llm", "source": "action", "target": "end", "data": {"mode": "llm", "priority": 20, "condition": "执行失败"}},
{"id": "expr", "source": "action", "target": "handoff", "data": {"mode": "expression", "priority": 30, "expression": expression}},
],
},
"handoff": {
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "handoff", "type": "handoff", "data": {}},
{"id": "agent", "type": "agent", "data": {}},
{"id": "action", "type": "action", "data": {}},
{"id": "end", "type": "end", "data": {}},
],
"edges": [
{"id": "begin", "source": "start", "target": "handoff", "data": {"mode": "always", "priority": 10}},
{"id": "default", "source": "handoff", "target": "agent", "data": {"mode": "always", "priority": 10}},
{"id": "llm", "source": "handoff", "target": "end", "data": {"mode": "llm", "priority": 20, "condition": "转接完成"}},
{"id": "expr", "source": "handoff", "target": "action", "data": {"mode": "expression", "priority": 30, "expression": expression}},
{"id": "action-end", "source": "action", "target": "end", "data": {"mode": "always", "priority": 10}},
],
},
}
for source_type, parts in cases.items():
graph = {"specVersion": 3, "settings": {}, **parts}
with self.subTest(source_type=source_type):
self.assertEqual(validate_graph(graph), [])
graph["edges"].append(
{
"id": "duplicate-default",
"source": source_type,
"target": "end",
"data": {"mode": "always", "priority": 40},
}
)
self.assertTrue(
any("最多只能有一条默认路径" in error for error in validate_graph(graph))
)
def test_v2_condition_on_non_agent_becomes_llm_path(self):
graph = normalize_graph(
{
"nodes": [
{"id": "start", "type": "start", "data": {}},
{"id": "handoff", "type": "handoff", "data": {}},
],
"edges": [
{
"id": "route",
"source": "start",
"target": "handoff",
"data": {"condition": "用户需要人工服务"},
}
],
}
)
self.assertEqual(graph["edges"][0]["data"]["mode"], "llm")
def test_v2_start_prompt_is_preserved_in_synthetic_agent(self):
graph = normalize_graph(
{
"nodes": [
{"id": "s", "type": "startCall", "data": {"prompt": "询问需求"}},
{"id": "e", "type": "endCall", "data": {"prompt": "再见"}},
],
"edges": [
{"id": "done", "source": "s", "target": "e", "data": {"condition": "完成"}}
],
}
)
migrated = next(node for node in graph["nodes"] if node["type"] == "agent")
self.assertEqual(migrated["data"]["prompt"], "询问需求")
self.assertEqual(validate_graph(graph), [])
def test_rejects_automatic_cycle_and_duplicate_priorities(self):
graph = valid_graph()
graph["nodes"].insert(1, {"id": "action", "type": "action", "data": {}})
graph["edges"] = [
{"id": "a", "source": "start", "target": "action", "data": {"mode": "always", "priority": 0}},
{"id": "b", "source": "action", "target": "start", "data": {"mode": "always", "priority": 0}},
{"id": "c", "source": "agent", "target": "end", "data": {"mode": "always", "priority": 10}},
]
errors = validate_graph(graph)
self.assertTrue(any("无等待循环" in error for error in errors))
def test_expression_routes_using_session_variables(self):
cfg = prepare_dynamic_config(
AssistantConfig(
type="workflow",
graph=valid_graph(),
dynamic_variable_definitions={
"customer": {"type": "string", "default": "王先生"},
"order_status": {"type": "string", "default": "pending"},
},
),
{},
assistant_id="asst_test",
)
store = DynamicVariableStore.from_config(cfg)
engine = WorkflowEngine(cfg.graph)
self.assertIsNone(engine.deterministic_edge("agent", store, include_default=False))
store.assign("order_status", "paid")
self.assertEqual(
engine.deterministic_edge("agent", store, include_default=False)["target"],
"end",
)
self.assertIn("王先生", engine.prompt_for("agent", store))
inherited_prompt = engine.prompt_for("agent", store)
self.assertIn("服务 王先生", inherited_prompt)
self.assertIn("处理订单", inherited_prompt)
engine.data("agent")["inheritGlobalConfig"] = False
custom_prompt = engine.prompt_for("agent", store)
self.assertNotIn("服务 王先生", custom_prompt)
self.assertIn("处理订单", custom_prompt)
def test_missing_variable_only_matches_explicit_exists_rule(self):
engine = WorkflowEngine(valid_graph())
values = {}
self.assertFalse(
engine.expression_matches(
{
"combinator": "and",
"rules": [
{
"variable": "order_status",
"operator": "neq",
"value": "paid",
}
],
},
values,
)
)
self.assertTrue(
engine.expression_matches(
{
"combinator": "and",
"rules": [
{
"variable": "order_status",
"operator": "exists",
"value": False,
}
],
},
values,
)
)
class WorkflowVisionReferenceTests(unittest.IsolatedAsyncioTestCase):
async def test_visual_model_must_support_image_input(self):
graph = valid_graph()
graph["settings"].update(
{
"defaultLlmResourceId": "llm_global",
"visionEnabled": True,
"visionModelResourceId": "vision_global",
}
)
body = AssistantUpsert(
name="视觉工作流",
type="workflow",
graph=graph,
)
_validate_workflow(body)
resources = {
"llm_global": SimpleNamespace(
enabled=True,
capability="LLM",
support_image_input=False,
),
"vision_global": SimpleNamespace(
enabled=True,
capability="LLM",
support_image_input=False,
),
}
class FakeSession:
async def get(self, _model, resource_id):
return resources.get(resource_id)
with self.assertRaisesRegex(HTTPException, "必须支持图片输入"):
await _validate_workflow_references(FakeSession(), body)
resources["vision_global"].support_image_input = True
await _validate_workflow_references(FakeSession(), body)
if __name__ == "__main__":
unittest.main()