feat: improve agent collaboration observability
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user