Add workflow support and enhance runtime configuration in models and services

- 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.
This commit is contained in:
Xin Wang
2026-07-13 16:13:27 +08:00
parent 6108b00007
commit 32aef14ddb
27 changed files with 2563 additions and 910 deletions

View File

@@ -111,6 +111,19 @@ class BrainRegistryTests(unittest.TestCase):
)
self.assertIn("user_name", assistant.dynamic_variable_definitions)
def test_workflow_keeps_dynamic_variables_and_tool_bindings(self):
assistant = AssistantUpsert(
name="workflow",
type="workflow",
toolIds=["tool_a"],
dynamicVariableDefinitions={
"customer": {"type": "string", "required": False, "default": "王先生"}
},
graph={},
)
self.assertEqual(assistant.tool_ids, ["tool_a"])
self.assertIn("customer", assistant.dynamic_variable_definitions)
class DifyHelpersTests(unittest.TestCase):
def test_normalize_api_base(self):
@@ -363,7 +376,7 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
params = FakeFunctionParams(
{"order_id": "A/1", "Authorization": "attacker-value"}
)
with patch("services.brains.prompt_brain.httpx.AsyncClient", FakeClient):
with patch("services.tool_executor.httpx.AsyncClient", FakeClient):
await llm.functions["lookup_order"](params)
self.assertEqual(requests[0][1], "https://example.test/orders/A%2F1")
@@ -377,35 +390,91 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
async def test_transition_and_end_are_owned_by_workflow_brain(self):
graph = {
"specVersion": 3,
"settings": {"globalPrompt": "全局规则"},
"nodes": [
{
"id": "start",
"type": "startCall",
"data": {"name": "开始", "prompt": "收集需求"},
"type": "start",
"data": {"name": "Start"},
},
{
"id": "agent",
"type": "agent",
"data": {
"name": "收集需求",
"prompt": "服务 {{user_name}}",
"contextPolicy": "fresh",
},
},
{
"id": "end",
"type": "endCall",
"data": {"name": "结束", "prompt": "礼貌结束"},
"type": "end",
"data": {"name": "End", "message": "感谢来电", "scope": "session"},
},
],
"edges": [
{
"id": "finish",
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
},
{
"id": "finish",
"source": "agent",
"target": "end",
"data": {"condition": "需求已收集"},
"data": {
"mode": "llm",
"priority": 10,
"condition": "需求已收集",
},
}
],
}
brain = WorkflowBrain(graph)
cfg = prepare_dynamic_config(
AssistantConfig(
type="workflow",
graph=graph,
dynamic_variable_definitions={
"user_name": {"type": "string", "required": True}
},
),
{"user_name": "王先生"},
assistant_id="asst_workflow",
)
brain = WorkflowBrain(cfg)
llm = FakeLLM()
context = LLMContext(messages=[])
queued = []
prompts = []
visible_tools = []
call_end = FakeCallEnd()
class FakeWorker:
def __init__(self):
self.frames = []
self.handlers = {}
def set_reached_downstream_filter(self, *_args):
pass
def event_handler(self, name):
def decorator(fn):
self.handlers[name] = fn
return fn
return decorator
async def queue_frame(self, frame):
self.frames.append(frame)
async def queue_frames(self, frames):
self.frames.extend(frames)
worker = FakeWorker()
pair = SimpleNamespace(
user=lambda: SimpleNamespace(_context=context),
assistant=lambda: SimpleNamespace(has_function_calls_in_progress=False),
)
async def queue_frame(frame):
queued.append(frame)
@@ -413,30 +482,34 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
context=context,
llm=llm,
queue_frame=queue_frame,
set_system_prompt=prompts.append,
set_tools=lambda tools: visible_tools.append(tools or []),
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=call_end,
worker=worker,
context_aggregator=pair,
)
await brain.setup(AssistantConfig(type="workflow", graph=graph), runtime)
self.assertIn("goto_finish", llm.functions)
self.assertIn("收集需求", prompts[-1])
self.assertEqual(visible_tools[-1][0].name, "goto_finish")
params = FakeFunctionParams()
await llm.functions["goto_finish"](params)
self.assertEqual(params.result, {"status": "ok"})
self.assertIn("礼貌结束", prompts[-1])
self.assertEqual(visible_tools[-1], [])
await brain.on_assistant_text_start("closing-turn")
await brain.on_assistant_text_end(
"closing-turn",
"感谢来电,再见。",
False,
await brain.setup(cfg, runtime)
await brain.on_connected()
self.assertEqual(brain._manager.current_node, "agent")
agent_config = brain._agent_config("agent")
self.assertIn("王先生", agent_config["role_message"])
self.assertIn("完成当前阶段任务", agent_config["role_message"])
self.assertEqual(agent_config["task_messages"], [])
self.assertEqual(
agent_config["context_strategy"].strategy.value,
"reset",
)
edge_function = next(
function
for function in brain._agent_config("agent")["functions"]
if function.name == "goto_finish"
)
_, terminal = await edge_function.handler({}, brain._manager)
self.assertEqual(terminal["name"], "end")
self.assertTrue(call_end.ending)
self.assertTrue(call_end.armed)
self.assertTrue(any(getattr(frame, "text", "") == "感谢来电" for frame in queued))
if __name__ == "__main__":