631 lines
25 KiB
Python
631 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
|
|
from models import AssistantConfig, RuntimeModelResource
|
|
from fastapi import HTTPException
|
|
from routes.assistants import _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_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_mode_defaults_and_validation(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.assertEqual(agent["data"]["entrySpeech"], "")
|
|
self.assertTrue(agent["data"]["inheritGlobalConfig"])
|
|
self.assertEqual(agent["data"]["contextPolicy"], "fresh")
|
|
|
|
agent["data"]["entryMode"] = "fixed_speech"
|
|
self.assertTrue(
|
|
any("固定进入语不能为空" in error for error in validate_graph(normalized))
|
|
)
|
|
agent["data"]["entrySpeech"] = "您好,{{customer}}"
|
|
self.assertEqual(validate_graph(normalized), [])
|
|
|
|
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()
|
|
graph["settings"].update(
|
|
{
|
|
"defaultLlmResourceId": "llm_global",
|
|
"defaultAsrResourceId": "asr_global",
|
|
"defaultTtsResourceId": "tts_global",
|
|
"visionEnabled": True,
|
|
"visionModelResourceId": "vision_global",
|
|
"toolIds": ["tool_global"],
|
|
"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",))
|
|
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"],
|
|
"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",))
|
|
self.assertEqual(custom.knowledge_mode, "disabled")
|
|
self.assertTrue(custom.enable_interrupt)
|
|
self.assertEqual(
|
|
custom.turn_config["turnDetection"]["strategy"],
|
|
"smart_turn",
|
|
)
|
|
|
|
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()
|