feat: add workflow action outcomes and tracing

This commit is contained in:
Xin Wang
2026-08-01 11:21:31 +08:00
parent ad5ff061bb
commit b747144ff1
13 changed files with 558 additions and 32 deletions

View File

@@ -29,7 +29,12 @@ from services.brains.dify_llm import (
)
from services.brains.workflow_brain import WorkflowBrain
from services.runtime_variables import prepare_dynamic_config
from services.workflow.models import LLMRouteResult, RouteStatus, WorkflowStatus
from services.workflow.models import (
ActionStatus,
LLMRouteResult,
RouteStatus,
WorkflowStatus,
)
class FakeLLM:
@@ -831,11 +836,18 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
session_id="conv_action",
)
brain._tools.execute = execute
await brain._enter_action("lookup_action")
outcome = await brain._enter_action("lookup_action")
self.assertEqual(outcome.status, ActionStatus.SUCCESS)
self.assertEqual(outcome.updated_variables, ("order_status",))
self.assertEqual(
brain._store.values["system__last_action_invocation_id"],
outcome.invocation_id,
)
variable_events = [
frame.message
for frame in queued
@@ -845,6 +857,17 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(variable_events[-1]["reason"], "action")
self.assertEqual(variable_events[-1]["changed"], ["order_status"])
self.assertEqual(variable_events[-1]["variables"], {"order_status": "paid"})
completed_event = next(
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("event") == "action_completed"
)
self.assertEqual(completed_event["outcome"]["status"], "success")
self.assertEqual(completed_event["outcome"]["updatedVariables"], ["order_status"])
self.assertEqual(completed_event["sessionId"], "conv_action")
self.assertEqual(completed_event["workflowRevision"], brain._engine.revision)
self.assertNotIn("result", completed_event["outcome"])
async def test_action_result_assignment_modes_reach_tool_executor(self):
tool = RuntimeTool(
@@ -958,14 +981,87 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
)
brain._tools.execute = execute
await brain._enter_action("action")
outcome = await brain._enter_action("action")
self.assertEqual(outcome.status, ActionStatus.FAILURE)
self.assertEqual(outcome.error.code, "tool_error")
self.assertEqual(brain._store.values["system__last_action_status"], "error")
self.assertEqual(
brain._store.values["system__last_action_error"],
"用户关闭了确认弹窗",
)
async def test_cancelled_action_does_not_follow_failure_or_default_edge(self):
tool = RuntimeTool(
id="client_action",
name="客户端操作",
function_name="show_message",
type="client",
)
brain = WorkflowBrain(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "action",
"type": "action",
"data": {"toolId": "client_action"},
},
{"id": "end", "type": "end", "data": {}},
],
"edges": [
{
"id": "after_action",
"source": "action",
"target": "end",
"data": {"mode": "always"},
}
],
},
tools=[tool],
)
)
queued = []
async def queue_frame(frame):
queued.append(frame)
async def execute(_tool, _arguments, *, result_assignments=None):
return {
"status": "error",
"message": "会话已结束",
"updated_variables": [],
}
brain._runtime = BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
)
brain._tools.execute = execute
config = await brain._resolve_path("action")
self.assertEqual(config["name"], "action")
self.assertEqual(
brain._store.values["system__last_action_status"],
"cancelled",
)
self.assertFalse(
any(
isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("event") == "edge_selected"
for frame in queued
)
)
async def test_action_block_policy_only_suppresses_input_while_running(self):
tool = RuntimeTool(
id="client_action",