feat: improve agent collaboration observability

This commit is contained in:
2026-06-15 11:33:08 +08:00
parent 7b7c82309c
commit d5f1a4510f
13 changed files with 593 additions and 36 deletions
@@ -50,10 +50,48 @@ def _node_label_from_map(node_map: Dict[str, Any], node_id: str) -> str:
return data.get('label') or node.get('label') or node_id
def _safe_preview(value: Any, max_length: int = 220) -> str:
if value is None or value == '':
return ''
if isinstance(value, str):
text = value
else:
import json
try:
text = json.dumps(value, ensure_ascii=False)
except TypeError:
text = str(value)
text = text.strip()
return text if len(text) <= max_length else f'{text[:max_length]}...'
def _node_collaboration_mode(node_type: str, data: Dict[str, Any]) -> str:
if data.get('agent_code'):
return 'agent'
if node_type == 'parallel':
return 'parallel'
if node_type == 'subflow':
return 'subflow'
if node_type == 'intent':
return 'router'
if node_type in ('confirm', 'question', 'choice'):
return 'human_input'
if node_type == 'llm':
return 'llm'
return 'workflow'
def _node_collaboration_metadata(node_map: Dict[str, Any], node_id: str, node_type: str) -> dict:
node = node_map.get(node_id) or {}
data = node.get('data') or {}
metadata: Dict[str, Any] = {}
metadata: Dict[str, Any] = {
'node_id': node_id,
'node_type': node_type,
'node_label': _node_label_from_map(node_map, node_id),
'collaboration_role': data.get('label') or node_id,
'collaboration_mode': _node_collaboration_mode(node_type, data),
}
agent_code = (data.get('agent_code') or '').strip()
if agent_code:
@@ -83,15 +121,96 @@ def _node_collaboration_metadata(node_map: Dict[str, Any], node_id: str, node_ty
if branch.get('id')
]
if node_type in ('template', 'llm') and agent_code:
metadata['collaboration_role'] = data.get('label') or node_id
if node_type == 'subflow':
metadata['collaboration_role'] = data.get('label') or node_id
return metadata
def _build_communication_metadata(
node_map: Dict[str, Any],
node_id: str,
node_type: str,
*,
event_type: str,
status: Optional[str] = None,
branch_id: Optional[str] = None,
branch_label: Optional[str] = None,
target_node_id: Optional[str] = None,
output: Any = None,
error: Any = None,
waiting_config: Optional[Dict[str, Any]] = None,
) -> dict:
actor = _node_collaboration_metadata(node_map, node_id, node_type)
message = _safe_preview(error or output)
if waiting_config:
message = (
waiting_config.get('title')
or waiting_config.get('question')
or waiting_config.get('content')
or message
)
communication: Dict[str, Any] = {
'event': event_type,
'status': status or '',
'actor': {
key: actor.get(key)
for key in (
'node_id',
'node_type',
'node_label',
'agent_code',
'agent_name',
'collaboration_role',
'collaboration_mode',
)
if actor.get(key)
},
'message': _safe_preview(message),
}
if branch_id:
communication['channel'] = 'parallel_branch'
communication['branch_id'] = branch_id
communication['branch_label'] = branch_label or branch_id
elif node_type == 'subflow':
communication['channel'] = 'subflow'
elif node_type in ('confirm', 'question', 'choice') or event_type == 'waiting_input':
communication['channel'] = 'human_input'
else:
communication['channel'] = 'workflow_edge'
if target_node_id:
target_type = (node_map.get(target_node_id) or {}).get('type', '')
target = _node_collaboration_metadata(node_map, target_node_id, target_type)
communication['target'] = {
key: target.get(key)
for key in (
'node_id',
'node_type',
'node_label',
'agent_code',
'agent_name',
'collaboration_role',
'collaboration_mode',
)
if target.get(key)
}
return communication
def _resolve_handoff_target(
current_node_id: str,
result: Optional[NodeResult],
edge_map: Dict[str, List[str]],
parallel_edge_map: Dict[str, Dict[str, str]],
node_map: Dict[str, Any],
) -> Optional[str]:
if result and result.next_node_id:
if result.next_node_id in parallel_edge_map.get(current_node_id, {}):
return parallel_edge_map[current_node_id][result.next_node_id]
if result.next_node_id in node_map:
return result.next_node_id
next_nodes = edge_map.get(current_node_id, [])
return next_nodes[0] if next_nodes else None
def _make_execution_log_entry(
node_map: Dict[str, Any],
node_id: str,
@@ -100,8 +219,25 @@ def _make_execution_log_entry(
) -> dict:
metadata = dict(extra.pop('metadata', {}) or {})
collaboration = _node_collaboration_metadata(node_map, node_id, node_type)
branch_id = extra.get('branch') or extra.get('branch_id')
branch_label = extra.get('branch_label')
if branch_id:
collaboration['branch_id'] = branch_id
collaboration['branch_label'] = branch_label or branch_id
if collaboration:
metadata['collaboration'] = collaboration
metadata['communication'] = _build_communication_metadata(
node_map,
node_id,
node_type,
event_type='node_complete',
status=extra.get('status'),
branch_id=branch_id,
branch_label=branch_label,
target_node_id=extra.get('target_node_id'),
output=extra.get('output'),
error=extra.get('error'),
)
return {
'node_id': node_id,
'node_type': node_type,
@@ -133,6 +269,10 @@ def _event_collaboration_metadata(
'model_id',
'output_variable',
'subflow_name',
'prompt_tokens',
'completion_tokens',
'confidence',
'matched_branch',
):
value = result_metadata.get(key)
if value is not None and value != '':
@@ -159,8 +299,27 @@ def _merge_event_collaboration(
result: Optional[NodeResult] = None,
) -> Dict[str, Any]:
collaboration = _event_collaboration_metadata(node_map, node_id, node_type, result)
if event.get('branch_id'):
collaboration['branch_id'] = event.get('branch_id')
collaboration['branch_label'] = event.get('branch_label') or event.get('branch_id')
if collaboration:
event['collaboration'] = collaboration
event_error = None
if event.get('type') == 'error':
event_error = event.get('error_message') or event.get('message') or event.get('content')
event['communication'] = _build_communication_metadata(
node_map,
node_id,
node_type,
event_type=event.get('type') or '',
status=event.get('status'),
branch_id=event.get('branch_id'),
branch_label=event.get('branch_label'),
target_node_id=event.get('target_node_id'),
output=(event.get('outputs') or {}).get('output') if isinstance(event.get('outputs'), dict) else event.get('outputs'),
error=event_error,
waiting_config=event.get('waiting_config') or event.get('config'),
)
return event
@@ -653,6 +812,13 @@ class AIWorkflowService:
start_time = time.time()
result = await node_instance.execute_async(context)
elapsed = int((time.time() - start_time) * 1000)
target_node_id = _resolve_handoff_target(
current_node_id,
result,
edge_map,
parallel_edge_map,
node_map,
)
# 记录日志
logs.append(_make_execution_log_entry(
@@ -666,6 +832,7 @@ class AIWorkflowService:
tokens_used=result.tokens_used,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id,
))
total_tokens += result.tokens_used
@@ -819,6 +986,13 @@ class AIWorkflowService:
start_time = time.time()
result = await node_instance.execute_async(branch_context)
elapsed = int((time.time() - start_time) * 1000)
target_node_id = _resolve_handoff_target(
current_id,
result,
edge_map,
parallel_edge_map,
node_map,
)
branch_logs.append(_make_execution_log_entry(
node_map,
@@ -832,6 +1006,7 @@ class AIWorkflowService:
metadata=_node_result_metadata(result),
branch=branch_id,
branch_label=branch_labels.get(branch_id, branch_id),
target_node_id=target_node_id,
))
branch_tokens += result.tokens_used
@@ -1072,6 +1247,7 @@ class AIWorkflowService:
'output': result.output,
'output_variables': result.metadata.get('frontend_output_variables', result.output_variables),
},
'target_node_id': target_node_id,
}, result), node_map, current_id, node_type, result)
if not result.success:
@@ -1782,6 +1958,13 @@ class AIWorkflowService:
result = await node_instance.execute_async(context)
elapsed = int((time.time() - start_time) * 1000)
target_node_id = _resolve_handoff_target(
current_node_id,
result,
edge_map,
parallel_edge_map,
node_map,
)
# 处理节点事件(如消息节点发送消息)
if result.events:
@@ -1815,7 +1998,9 @@ class AIWorkflowService:
status='waiting',
output=result.output,
elapsed_time=elapsed,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id,
)
logs.append(log_entry)
@@ -1838,6 +2023,7 @@ class AIWorkflowService:
'node_id': current_node_id,
'node_type': node_type,
'config': result.waiting_config,
'waiting_config': result.waiting_config,
}, node_map, current_node_id, node_type, result)
# 暂停工作流执行,等待用户输入后续流
@@ -1855,6 +2041,7 @@ class AIWorkflowService:
tokens_used=result.tokens_used,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id,
)
logs.append(log_entry)
@@ -1882,6 +2069,7 @@ class AIWorkflowService:
'output': result.output,
'output_variables': result.metadata.get('frontend_output_variables', result.output_variables),
},
'target_node_id': target_node_id,
}, result), node_map, current_node_id, node_type, result)
if not result.success:
@@ -2335,6 +2523,7 @@ class AIWorkflowService:
output=result.output,
elapsed_time=elapsed,
metadata=_node_result_metadata(result),
target_node_id=target_node_id,
)
logs.append(log_entry)
@@ -2353,6 +2542,7 @@ class AIWorkflowService:
'node_id': current_node_id,
'node_type': node_type,
'config': result.waiting_config,
'waiting_config': result.waiting_config,
}, node_map, current_node_id, node_type, result)
# 如果在循环中,添加迭代信息
if loop_state:
@@ -2371,6 +2561,7 @@ class AIWorkflowService:
elapsed_time=elapsed,
tokens_used=result.tokens_used,
metadata=_node_result_metadata(result),
target_node_id=target_node_id,
)
logs.append(log_entry)
@@ -2401,6 +2592,7 @@ class AIWorkflowService:
'output': result.output,
'output_variables': result.metadata.get('frontend_output_variables', result.output_variables),
},
'target_node_id': target_node_id,
}, result), node_map, current_node_id, node_type, result)
# 如果在循环中,添加迭代信息
if loop_state: