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
@@ -116,17 +116,12 @@ class IntentNode(BaseNode):
error='请至少配置一个意图', error='请至少配置一个意图',
) )
if not model_id: llm_service = LLMService(context.db_session)
return NodeResult( model_id = await llm_service.resolve_chat_model_id(model_id)
success=False,
error='请选择用于意图识别的模型',
)
# 检查模型是否支持 Function Calling # 检查模型是否支持 Function Calling
supports_fc = self._check_model_supports_function_call(model_id) supports_fc = self._check_model_supports_function_call(model_id)
llm_service = LLMService(context.db_session)
if supports_fc: if supports_fc:
# 使用 Function Calling 方式(更准确) # 使用 Function Calling 方式(更准确)
intent_name, confidence, total_tokens = await self._execute_with_function_calling_async( intent_name, confidence, total_tokens = await self._execute_with_function_calling_async(
@@ -169,6 +164,7 @@ class IntentNode(BaseNode):
'intent': intent_name, 'intent': intent_name,
'confidence': confidence, 'confidence': confidence,
'matched_branch': next_node_id, 'matched_branch': next_node_id,
'model_id': model_id,
}, },
) )
@@ -163,6 +163,7 @@ class LLMNode(BaseNode):
# 调用 LLM(异步) # 调用 LLM(异步)
llm_service = LLMService(context.db_session) llm_service = LLMService(context.db_session)
model_id = await llm_service.resolve_chat_model_id(model_id)
output_var = self.config.get('output_variable', 'llm_response') output_var = self.config.get('output_variable', 'llm_response')
# 根据输出模式选择执行方式 # 根据输出模式选择执行方式
@@ -172,6 +173,11 @@ class LLMNode(BaseNode):
llm_service, model_id, messages, temperature, max_tokens, llm_service, model_id, messages, temperature, max_tokens,
output_schema, output_var, start_time output_schema, output_var, start_time
) )
result.metadata = {
**(result.metadata or {}),
'model_id': str(model_id),
'output_variable': output_var,
}
return result return result
else: else:
# 普通文本输出模式(异步) # 普通文本输出模式(异步)
@@ -194,9 +200,11 @@ class LLMNode(BaseNode):
tokens_used=response.total_tokens, tokens_used=response.total_tokens,
elapsed_time=elapsed_time, elapsed_time=elapsed_time,
metadata={ metadata={
'model_id': str(model_id),
'model': response.model, 'model': response.model,
'prompt_tokens': response.prompt_tokens, 'prompt_tokens': response.prompt_tokens,
'completion_tokens': response.completion_tokens, 'completion_tokens': response.completion_tokens,
'output_variable': output_var,
}, },
) )
@@ -314,6 +322,11 @@ class LLMNode(BaseNode):
llm_service, model_id, messages, temperature, max_tokens, llm_service, model_id, messages, temperature, max_tokens,
output_schema, output_var, start_time output_schema, output_var, start_time
) )
result.metadata = {
**(result.metadata or {}),
'model_id': str(model_id),
'output_variable': output_var,
}
return result return result
else: else:
# 普通文本输出模式 # 普通文本输出模式
@@ -350,6 +363,10 @@ class LLMNode(BaseNode):
}, },
tokens_used=total_tokens, tokens_used=total_tokens,
elapsed_time=elapsed_time, elapsed_time=elapsed_time,
metadata={
'model_id': str(model_id),
'output_variable': output_var,
},
) )
except Exception as e: except Exception as e:
@@ -251,7 +251,7 @@ class AgentMessageResponse(BaseModel):
content: str = "" content: str = ""
attachments: List[AttachmentResponse] = Field(default_factory=list) attachments: List[AttachmentResponse] = Field(default_factory=list)
status: str = "completed" status: str = "completed"
reasoning_steps: List[ReasoningStep] = Field(default_factory=list) reasoning_steps: List[Dict[str, Any]] = Field(default_factory=list)
tool_calls: List[ToolCallRecord] = Field(default_factory=list) tool_calls: List[ToolCallRecord] = Field(default_factory=list)
prompt_tokens: int = 0 prompt_tokens: int = 0
completion_tokens: int = 0 completion_tokens: int = 0
@@ -3,6 +3,7 @@
""" """
import logging import logging
import time import time
from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sqlalchemy import select from sqlalchemy import select
@@ -45,8 +46,94 @@ class AgentService:
for key, value in self._agent_collaboration_metadata(agent).items(): for key, value in self._agent_collaboration_metadata(agent).items():
collaboration.setdefault(key, value) collaboration.setdefault(key, value)
enriched["collaboration"] = collaboration enriched["collaboration"] = collaboration
communication = dict(enriched.get("communication") or {})
actor = dict(communication.get("actor") or {})
for key in (
"agent_id",
"agent_code",
"agent_name",
"collaboration_role",
"collaboration_mode",
"model_id",
):
if collaboration.get(key):
actor.setdefault(key, collaboration[key])
communication.setdefault("event", enriched.get("type") or "")
communication.setdefault("status", "failed" if enriched.get("type") == "error" else "completed")
communication.setdefault("channel", "agent_chat")
communication.setdefault("actor", actor)
communication.setdefault(
"message",
enriched.get("content")
or enriched.get("message")
or enriched.get("accumulated_content")
or "",
)
enriched["communication"] = communication
return enriched return enriched
@staticmethod
def _event_to_reasoning_step(event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
event_type = event.get('type')
if event_type not in {
'action',
'annotation_reply',
'error',
'knowledge_retrieval',
'loop_complete',
'loop_iteration_complete',
'loop_iteration_error',
'loop_iteration_start',
'node_complete',
'node_start',
'observation',
'parallel_complete',
'parallel_start',
'thought',
'tool_call',
'tool_result',
'waiting_input',
}:
return None
collaboration = event.get('collaboration') or {}
communication = event.get('communication') or {}
config = event.get('waiting_config') or event.get('config') or {}
content = (
event.get('node_label')
or event.get('content')
or communication.get('message')
or config.get('title')
or config.get('question')
or event.get('node_type')
or event_type
)
step = {
'type': event_type,
'content': content,
'tool': event.get('tool') or '',
'params': event.get('params') or config,
'node_id': event.get('node_id') or '',
'node_type': event.get('node_type') or '',
'branch_id': event.get('branch_id') or collaboration.get('branch_id') or '',
'branch_label': event.get('branch_label') or collaboration.get('branch_label') or '',
'agent_code': collaboration.get('agent_code') or event.get('agent_code') or '',
'agent_name': collaboration.get('agent_name') or event.get('agent_name') or '',
'model': collaboration.get('model') or event.get('model') or '',
'model_id': collaboration.get('model_id') or event.get('model_id') or '',
'subflow_name': collaboration.get('subflow_name') or event.get('subflow_name') or '',
'from_subflow': bool(collaboration.get('from_subflow') or event.get('from_subflow')),
'collaboration_role': collaboration.get('collaboration_role') or '',
'collaboration_mode': collaboration.get('collaboration_mode') or event.get('collaboration_mode') or '',
'communication': communication,
'output': event.get('outputs') or event.get('output'),
'status': 'running' if event_type in {'node_start', 'parallel_start', 'waiting_input', 'loop_iteration_start'} else 'completed',
'timestamp': datetime.now().isoformat(),
}
return {key: value for key, value in step.items() if value not in (None, '', {}, [])}
async def chat( async def chat(
self, self,
agent: Agent, agent: Agent,
@@ -209,16 +296,31 @@ class AgentService:
) )
self._db.add(assistant_msg) self._db.add(assistant_msg)
await self._db.flush() await self._db.flush()
reasoning_steps: List[Dict[str, Any]] = []
yield { yield self._attach_agent_collaboration({
'type': 'start', 'type': 'start',
'conversation_id': str(conversation.id), 'conversation_id': str(conversation.id),
'message_id': str(assistant_msg.id), 'message_id': str(assistant_msg.id),
} }, agent)
try: try:
# 构建系统提示词 # 构建系统提示词
system_prompt = self._build_system_prompt_simple(agent) system_prompt = self._build_system_prompt_simple(agent)
model_id = await self._resolve_agent_model_id(agent)
if not model_id:
raise ValueError('未找到可用的 chat 模型,请先在模型配置中启用一个模型')
event = self._attach_agent_collaboration({
'type': 'thought',
'content': '智能体开始分析用户需求并调用默认 chat 模型',
'model_id': model_id,
}, agent)
step = self._event_to_reasoning_step(event)
if step:
reasoning_steps.append(step)
assistant_msg.reasoning_steps = reasoning_steps.copy()
yield event
# 标注直接回复检查(高相似度时跳过 LLM,直接返回标注答案) # 标注直接回复检查(高相似度时跳过 LLM,直接返回标注答案)
annotation_reply = await self._check_annotation_direct_reply(agent, user_message) annotation_reply = await self._check_annotation_direct_reply(agent, user_message)
@@ -257,11 +359,16 @@ class AgentService:
knowledge_context = await self._retrieve_knowledge_context(agent, user_message) knowledge_context = await self._retrieve_knowledge_context(agent, user_message)
if knowledge_context: if knowledge_context:
system_prompt = self._inject_knowledge_context(system_prompt, knowledge_context) system_prompt = self._inject_knowledge_context(system_prompt, knowledge_context)
yield { event = {
'type': 'knowledge_retrieval', 'type': 'knowledge_retrieval',
'content': knowledge_context['summary'], 'content': knowledge_context['summary'],
'result_count': knowledge_context['result_count'], 'result_count': knowledge_context['result_count'],
} }
step = self._event_to_reasoning_step(event)
if step:
reasoning_steps.append(step)
assistant_msg.reasoning_steps = reasoning_steps.copy()
yield event
# 获取对话历史 # 获取对话历史
history = await self._get_conversation_history(conversation, limit=10) history = await self._get_conversation_history(conversation, limit=10)
@@ -297,6 +404,7 @@ class AgentService:
'type': 'llm_chunk', 'type': 'llm_chunk',
'content': chunk.content, 'content': chunk.content,
'accumulated_content': accumulated_content, 'accumulated_content': accumulated_content,
'model_id': model_id,
} }
if chunk.is_finished: if chunk.is_finished:
@@ -317,6 +425,7 @@ class AgentService:
yield { yield {
'type': 'answer', 'type': 'answer',
'content': final_answer, 'content': final_answer,
'model_id': model_id,
} }
# 更新助手消息 # 更新助手消息
@@ -325,6 +434,7 @@ class AgentService:
assistant_msg.status = 'completed' assistant_msg.status = 'completed'
assistant_msg.total_tokens = total_tokens assistant_msg.total_tokens = total_tokens
assistant_msg.elapsed_time = elapsed_time assistant_msg.elapsed_time = elapsed_time
assistant_msg.reasoning_steps = reasoning_steps.copy()
# 更新对话统计 # 更新对话统计
conversation.message_count = (conversation.message_count or 0) + 2 conversation.message_count = (conversation.message_count or 0) + 2
@@ -352,6 +462,10 @@ class AgentService:
logger.exception(f'Agent chat error: {e}') logger.exception(f'Agent chat error: {e}')
assistant_msg.status = 'failed' assistant_msg.status = 'failed'
assistant_msg.error_message = str(e) assistant_msg.error_message = str(e)
error_step = self._event_to_reasoning_step({'type': 'error', 'content': str(e)})
if error_step:
reasoning_steps.append(error_step)
assistant_msg.reasoning_steps = reasoning_steps.copy()
await self._db.commit() await self._db.commit()
yield { yield {
'type': 'error', 'type': 'error',
@@ -438,6 +552,7 @@ class AgentService:
) )
self._db.add(assistant_msg) self._db.add(assistant_msg)
await self._db.flush() await self._db.flush()
reasoning_steps: List[Dict[str, Any]] = []
yield { yield {
'type': 'start', 'type': 'start',
@@ -508,6 +623,25 @@ class AgentService:
if output: if output:
final_content = output final_content = output
step = self._event_to_reasoning_step(event)
if step:
existing_index = next(
(
index
for index, item in enumerate(reasoning_steps)
if item.get('node_id')
and item.get('node_id') == step.get('node_id')
and item.get('type') == 'node_start'
and step.get('type') == 'node_complete'
),
-1,
)
if existing_index >= 0:
reasoning_steps[existing_index] = step
else:
reasoning_steps.append(step)
assistant_msg.reasoning_steps = reasoning_steps.copy()
yield event yield event
else: else:
# 新对话,启动工作流 - 使用异步方法 # 新对话,启动工作流 - 使用异步方法
@@ -574,6 +708,25 @@ class AgentService:
if output: if output:
final_content = output final_content = output
step = self._event_to_reasoning_step(event)
if step:
existing_index = next(
(
index
for index, item in enumerate(reasoning_steps)
if item.get('node_id')
and item.get('node_id') == step.get('node_id')
and item.get('type') == 'node_start'
and step.get('type') == 'node_complete'
),
-1,
)
if existing_index >= 0:
reasoning_steps[existing_index] = step
else:
reasoning_steps.append(step)
assistant_msg.reasoning_steps = reasoning_steps.copy()
yield event yield event
# 更新助手消息 # 更新助手消息
@@ -585,6 +738,7 @@ class AgentService:
assistant_msg.status = 'completed' assistant_msg.status = 'completed'
assistant_msg.elapsed_time = elapsed_time assistant_msg.elapsed_time = elapsed_time
assistant_msg.total_tokens = total_tokens assistant_msg.total_tokens = total_tokens
assistant_msg.reasoning_steps = reasoning_steps.copy()
# 更新对话统计 # 更新对话统计
conversation.message_count = (conversation.message_count or 0) + 2 conversation.message_count = (conversation.message_count or 0) + 2
@@ -614,6 +768,10 @@ class AgentService:
logger.exception(f'Dialog flow error: {e}') logger.exception(f'Dialog flow error: {e}')
assistant_msg.status = 'failed' assistant_msg.status = 'failed'
assistant_msg.error_message = str(e) assistant_msg.error_message = str(e)
error_step = self._event_to_reasoning_step({'type': 'error', 'content': str(e)})
if error_step:
reasoning_steps.append(error_step)
assistant_msg.reasoning_steps = reasoning_steps.copy()
await self._db.commit() await self._db.commit()
yield { yield {
'type': 'error', 'type': 'error',
@@ -30,6 +30,38 @@ class LLMService:
self._provider_cache: Dict[str, BaseLLMProvider] = {} self._provider_cache: Dict[str, BaseLLMProvider] = {}
self._db = db self._db = db
@staticmethod
def _is_missing_model_id(model_id: Optional[str]) -> bool:
if model_id is None:
return True
return str(model_id).strip().lower() in {"", "none", "null", "undefined"}
async def resolve_chat_model_id(self, model_id: Optional[str]) -> str:
"""解析 chat 模型。未指定时使用当前启用的默认 chat 模型。"""
if not self._is_missing_model_id(model_id):
return str(model_id)
if not self._db:
raise ValueError("未找到可用的 chat 模型,请先在模型配置中启用一个模型")
from ai_platform.models import LLMModel
result = await self._db.execute(
select(LLMModel)
.where(
LLMModel.is_deleted == False,
LLMModel.is_active == True,
LLMModel.model_type == "chat",
)
.order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc())
)
model = result.scalars().first()
if not model:
raise ValueError("未找到可用的 chat 模型,请先在模型配置中启用一个模型")
logger.info("No model_id supplied, fallback to default chat model %s", model.id)
return str(model.id)
async def _get_provider_async(self, model_id: str) -> tuple: async def _get_provider_async(self, model_id: str) -> tuple:
""" """
异步获取模型对应的提供商实例 异步获取模型对应的提供商实例
@@ -45,6 +77,8 @@ class LLMService:
if not self._db: if not self._db:
raise ValueError("数据库会话未初始化") raise ValueError("数据库会话未初始化")
model_id = await self.resolve_chat_model_id(model_id)
# 查询模型 # 查询模型
result = await self._db.execute( result = await self._db.execute(
select(LLMModel).where( select(LLMModel).where(
@@ -319,14 +353,18 @@ class LLMService:
async def get_provider(): async def get_provider():
return await self._get_provider_async(model_id) return await self._get_provider_async(model_id)
loop = asyncio.get_event_loop() try:
if loop.is_running(): loop = asyncio.get_event_loop()
# 如果已有事件循环在运行,使用线程池 except RuntimeError:
with concurrent.futures.ThreadPoolExecutor() as executor: provider, model_name = asyncio.run(get_provider())
future = executor.submit(asyncio.run, get_provider())
provider, model_name = future.result()
else: else:
provider, model_name = loop.run_until_complete(get_provider()) if loop.is_running():
# 如果已有事件循环在运行,使用线程池
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, get_provider())
provider, model_name = future.result()
else:
provider, model_name = loop.run_until_complete(get_provider())
# 使用获取到的 provider 进行流式调用 # 使用获取到的 provider 进行流式调用
yield from self.chat_stream_sync_with_provider( yield from self.chat_stream_sync_with_provider(
@@ -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 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: def _node_collaboration_metadata(node_map: Dict[str, Any], node_id: str, node_type: str) -> dict:
node = node_map.get(node_id) or {} node = node_map.get(node_id) or {}
data = node.get('data') 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() agent_code = (data.get('agent_code') or '').strip()
if agent_code: 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 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 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( def _make_execution_log_entry(
node_map: Dict[str, Any], node_map: Dict[str, Any],
node_id: str, node_id: str,
@@ -100,8 +219,25 @@ def _make_execution_log_entry(
) -> dict: ) -> dict:
metadata = dict(extra.pop('metadata', {}) or {}) metadata = dict(extra.pop('metadata', {}) or {})
collaboration = _node_collaboration_metadata(node_map, node_id, node_type) 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: if collaboration:
metadata['collaboration'] = 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 { return {
'node_id': node_id, 'node_id': node_id,
'node_type': node_type, 'node_type': node_type,
@@ -133,6 +269,10 @@ def _event_collaboration_metadata(
'model_id', 'model_id',
'output_variable', 'output_variable',
'subflow_name', 'subflow_name',
'prompt_tokens',
'completion_tokens',
'confidence',
'matched_branch',
): ):
value = result_metadata.get(key) value = result_metadata.get(key)
if value is not None and value != '': if value is not None and value != '':
@@ -159,8 +299,27 @@ def _merge_event_collaboration(
result: Optional[NodeResult] = None, result: Optional[NodeResult] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
collaboration = _event_collaboration_metadata(node_map, node_id, node_type, result) 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: if collaboration:
event['collaboration'] = 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 return event
@@ -653,6 +812,13 @@ class AIWorkflowService:
start_time = time.time() start_time = time.time()
result = await node_instance.execute_async(context) result = await node_instance.execute_async(context)
elapsed = int((time.time() - start_time) * 1000) 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( logs.append(_make_execution_log_entry(
@@ -666,6 +832,7 @@ class AIWorkflowService:
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id,
)) ))
total_tokens += result.tokens_used total_tokens += result.tokens_used
@@ -819,6 +986,13 @@ class AIWorkflowService:
start_time = time.time() start_time = time.time()
result = await node_instance.execute_async(branch_context) result = await node_instance.execute_async(branch_context)
elapsed = int((time.time() - start_time) * 1000) 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( branch_logs.append(_make_execution_log_entry(
node_map, node_map,
@@ -832,6 +1006,7 @@ class AIWorkflowService:
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
branch=branch_id, branch=branch_id,
branch_label=branch_labels.get(branch_id, branch_id), branch_label=branch_labels.get(branch_id, branch_id),
target_node_id=target_node_id,
)) ))
branch_tokens += result.tokens_used branch_tokens += result.tokens_used
@@ -1072,6 +1247,7 @@ class AIWorkflowService:
'output': result.output, 'output': result.output,
'output_variables': result.metadata.get('frontend_output_variables', result.output_variables), '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) }, result), node_map, current_id, node_type, result)
if not result.success: if not result.success:
@@ -1782,6 +1958,13 @@ class AIWorkflowService:
result = await node_instance.execute_async(context) result = await node_instance.execute_async(context)
elapsed = int((time.time() - start_time) * 1000) 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: if result.events:
@@ -1815,7 +1998,9 @@ class AIWorkflowService:
status='waiting', status='waiting',
output=result.output, output=result.output,
elapsed_time=elapsed, elapsed_time=elapsed,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id,
) )
logs.append(log_entry) logs.append(log_entry)
@@ -1838,6 +2023,7 @@ class AIWorkflowService:
'node_id': current_node_id, 'node_id': current_node_id,
'node_type': node_type, 'node_type': node_type,
'config': result.waiting_config, 'config': result.waiting_config,
'waiting_config': result.waiting_config,
}, node_map, current_node_id, node_type, result) }, node_map, current_node_id, node_type, result)
# 暂停工作流执行,等待用户输入后续流 # 暂停工作流执行,等待用户输入后续流
@@ -1855,6 +2041,7 @@ class AIWorkflowService:
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id,
) )
logs.append(log_entry) logs.append(log_entry)
@@ -1882,6 +2069,7 @@ class AIWorkflowService:
'output': result.output, 'output': result.output,
'output_variables': result.metadata.get('frontend_output_variables', result.output_variables), '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) }, result), node_map, current_node_id, node_type, result)
if not result.success: if not result.success:
@@ -2335,6 +2523,7 @@ class AIWorkflowService:
output=result.output, output=result.output,
elapsed_time=elapsed, elapsed_time=elapsed,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
target_node_id=target_node_id,
) )
logs.append(log_entry) logs.append(log_entry)
@@ -2353,6 +2542,7 @@ class AIWorkflowService:
'node_id': current_node_id, 'node_id': current_node_id,
'node_type': node_type, 'node_type': node_type,
'config': result.waiting_config, 'config': result.waiting_config,
'waiting_config': result.waiting_config,
}, node_map, current_node_id, node_type, result) }, node_map, current_node_id, node_type, result)
# 如果在循环中,添加迭代信息 # 如果在循环中,添加迭代信息
if loop_state: if loop_state:
@@ -2371,6 +2561,7 @@ class AIWorkflowService:
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
target_node_id=target_node_id,
) )
logs.append(log_entry) logs.append(log_entry)
@@ -2401,6 +2592,7 @@ class AIWorkflowService:
'output': result.output, 'output': result.output,
'output_variables': result.metadata.get('frontend_output_variables', result.output_variables), '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) }, result), node_map, current_node_id, node_type, result)
# 如果在循环中,添加迭代信息 # 如果在循环中,添加迭代信息
if loop_state: if loop_state:
@@ -281,6 +281,9 @@ export interface ExecutionLogEntry {
error?: string; error?: string;
elapsed_time?: number; elapsed_time?: number;
tokens_used?: number; tokens_used?: number;
branch?: string;
branch_label?: string;
metadata?: Record<string, any>;
} }
export interface WorkflowRunListItem { export interface WorkflowRunListItem {
@@ -194,6 +194,21 @@ const getStepColor = (step: ReasoningStep) => {
const getStepMetaItems = (step: ReasoningStep) => { const getStepMetaItems = (step: ReasoningStep) => {
const items: string[] = []; const items: string[] = [];
const communication = step.communication;
const actor = communication?.actor || {};
const target = communication?.target || {};
const actorName =
actor.agent_name ||
actor.agent_code ||
actor.node_label ||
step.agent_name ||
step.agent_code;
const targetName = target.agent_name || target.agent_code || target.node_label;
if (actorName && targetName) {
items.push(`交接: ${actorName} -> ${targetName}`);
} else if (communication?.channel) {
items.push(`通道: ${communication.channel}`);
}
if (step.agent_name || step.agent_code) { if (step.agent_name || step.agent_code) {
items.push(`智能体: ${step.agent_name || step.agent_code}`); items.push(`智能体: ${step.agent_name || step.agent_code}`);
} }
@@ -206,6 +221,9 @@ const getStepMetaItems = (step: ReasoningStep) => {
if (step.model || step.model_id) { if (step.model || step.model_id) {
items.push(`模型: ${step.model || step.model_id}`); items.push(`模型: ${step.model || step.model_id}`);
} }
if (communication?.message && communication.message !== step.content) {
items.push(`消息: ${communication.message}`);
}
return items; return items;
}; };
@@ -72,6 +72,16 @@ export interface ReasoningStep {
from_subflow?: boolean; from_subflow?: boolean;
collaboration_role?: string; collaboration_role?: string;
collaboration_mode?: string; collaboration_mode?: string;
communication?: {
actor?: Record<string, any>;
branch_id?: string;
branch_label?: string;
channel?: string;
event?: string;
message?: string;
status?: string;
target?: Record<string, any>;
};
output?: any; output?: any;
/** 步骤状态:running 执行中,completed 已完成 */ /** 步骤状态:running 执行中,completed 已完成 */
status?: 'completed' | 'running'; status?: 'completed' | 'running';
@@ -750,6 +750,7 @@ onUnmounted(() => {
:enable-image="enableImage" :enable-image="enableImage"
:enable-voice="enableVoice" :enable-voice="enableVoice"
:enable-feedback="enableFeedback" :enable-feedback="enableFeedback"
:show-reasoning-steps="true"
:show-clear-button="showClearButton" :show-clear-button="showClearButton"
:empty-text="emptyText" :empty-text="emptyText"
:welcome-config="welcomeConfig" :welcome-config="welcomeConfig"
@@ -231,6 +231,17 @@ export function useChatApi(props: AiChatPanelProps): {
params: step.params, params: step.params,
node_id: step.node_id, node_id: step.node_id,
node_type: step.node_type, node_type: step.node_type,
branch_id: step.branch_id,
branch_label: step.branch_label,
agent_code: step.agent_code,
agent_name: step.agent_name,
model: step.model,
model_id: step.model_id,
subflow_name: step.subflow_name,
from_subflow: step.from_subflow,
collaboration_role: step.collaboration_role,
collaboration_mode: step.collaboration_mode,
communication: step.communication,
output: step.output, output: step.output,
status: step.status, status: step.status,
timestamp: step.timestamp, timestamp: step.timestamp,
@@ -172,6 +172,7 @@ export function useEventHandler(config: EventHandlerConfig) {
const buildReasoningMeta = (event: StreamEvent): Partial<ReasoningStep> => { const buildReasoningMeta = (event: StreamEvent): Partial<ReasoningStep> => {
const collaboration = event.collaboration || {}; const collaboration = event.collaboration || {};
const communication = event.communication || {};
return { return {
branch_id: event.branch_id, branch_id: event.branch_id,
branch_label: event.branch_label, branch_label: event.branch_label,
@@ -187,6 +188,7 @@ export function useEventHandler(config: EventHandlerConfig) {
collaboration_role: collaboration_role:
collaboration.collaboration_role || collaboration.role || event.collaboration_role, collaboration.collaboration_role || collaboration.role || event.collaboration_role,
collaboration_mode: event.collaboration_mode || collaboration.collaboration_mode, collaboration_mode: event.collaboration_mode || collaboration.collaboration_mode,
communication,
}; };
}; };
@@ -230,6 +232,35 @@ export function useEventHandler(config: EventHandlerConfig) {
break; break;
} }
case 'annotation_reply':
case 'knowledge_retrieval':
case 'observation':
case 'thought':
case 'tool_call':
case 'tool_result': {
currentSteps.value.push({
type: event.type as ReasoningStep['type'],
content:
event.content ||
event.message ||
event.tool ||
event.type,
tool: event.tool,
params: event.params,
result: event.result,
node_id: event.node_id,
node_type: event.node_type,
...buildReasoningMeta(event),
status: 'completed',
timestamp: new Date().toISOString(),
});
updateAssistantMessage(msgId, {
reasoning_steps: [...currentSteps.value],
...buildMessageMeta(event),
});
break;
}
case 'answer': { case 'answer': {
const initialMsg = messages.value.find((m) => m.id === msgId); const initialMsg = messages.value.find((m) => m.id === msgId);
if (initialMsg && !initialMsg.content?.trim()) { if (initialMsg && !initialMsg.content?.trim()) {
@@ -118,11 +118,25 @@ const collaborationTimeline = computed(() => {
.map((log, index) => { .map((log, index) => {
const meta = getLogMeta(log); const meta = getLogMeta(log);
const collaboration = meta.collaboration || {}; const collaboration = meta.collaboration || {};
const agentCode = meta.agent_code || collaboration.agent_code; const communication = meta.communication || {};
const agentName = meta.agent_name || collaboration.agent_name || agentCode; const actor = communication.actor || {};
const branch = log.branch_label || log.branch || collaboration.branch_label; const target = communication.target || {};
const agentCode = actor.agent_code || meta.agent_code || collaboration.agent_code;
const agentName =
actor.agent_name ||
actor.agent_code ||
meta.agent_name ||
collaboration.agent_name ||
agentCode;
const branch =
communication.branch_label ||
log.branch_label ||
log.branch ||
collaboration.branch_label;
const subflowName = collaboration.subflow_name || meta.subflow_name; const subflowName = collaboration.subflow_name || meta.subflow_name;
const model = meta.model || collaboration.model || meta.model_id || collaboration.model_id; const model = meta.model || collaboration.model || meta.model_id || collaboration.model_id;
const targetName =
target.agent_name || target.agent_code || target.node_label || '';
const title = const title =
agentName || agentName ||
subflowName || subflowName ||
@@ -131,8 +145,10 @@ const collaborationTimeline = computed(() => {
log.node_id || log.node_id ||
`#${index + 1}`; `#${index + 1}`;
const summaryParts = [ const summaryParts = [
targetName ? `交接给:${targetName}` : '',
branch ? `分支:${branch}` : '', branch ? `分支:${branch}` : '',
subflowName ? `子流程:${subflowName}` : '', subflowName ? `子流程:${subflowName}` : '',
communication.channel ? `通道:${communication.channel}` : '',
model ? `模型:${model}` : '', model ? `模型:${model}` : '',
].filter(Boolean); ].filter(Boolean);
@@ -143,9 +159,10 @@ const collaborationTimeline = computed(() => {
subtitle: log.node_label || log.node_id, subtitle: log.node_label || log.node_id,
status: log.status, status: log.status,
summary: summaryParts.join(' · '), summary: summaryParts.join(' · '),
message: communication.message || previewValue(log.error || log.output),
elapsed: log.elapsed_time, elapsed: log.elapsed_time,
tokens: log.tokens_used || 0, tokens: log.tokens_used || 0,
hasSignal: Boolean(agentName || branch || subflowName || model), hasSignal: Boolean(agentName || branch || subflowName || model || communication.channel),
}; };
}) })
.filter((item) => item.hasSignal); .filter((item) => item.hasSignal);
@@ -248,6 +265,35 @@ function getLogMeta(log: any) {
return log?.metadata || {}; return log?.metadata || {};
} }
function getCommunication(log: any) {
return getLogMeta(log).communication || {};
}
function getCollaboration(log: any) {
return getLogMeta(log).collaboration || {};
}
function getActorName(log: any) {
const communication = getCommunication(log);
const actor = communication.actor || {};
const collaboration = getCollaboration(log);
return (
actor.agent_name ||
actor.agent_code ||
collaboration.agent_name ||
collaboration.agent_code ||
actor.node_label ||
log.node_label ||
log.node_id ||
'-'
);
}
function getTargetName(log: any) {
const target = getCommunication(log).target || {};
return target.agent_name || target.agent_code || target.node_label || '';
}
function selectLog(nodeId?: string) { function selectLog(nodeId?: string) {
if (!nodeId) return; if (!nodeId) return;
activeNodeId.value = nodeId; activeNodeId.value = nodeId;
@@ -468,29 +514,56 @@ defineExpose({ open });
{{ log.error }} {{ log.error }}
</div> </div>
<div <div
v-if="getLogMeta(log).agent_code" v-if="getCollaboration(log).agent_code || getCommunication(log).channel"
class="border-border bg-muted/40 grid grid-cols-2 gap-2 rounded-md border p-2" class="border-border bg-muted/40 grid grid-cols-2 gap-2 rounded-md border p-2"
> >
<div> <div>
<div class="text-muted-foreground"> <div class="text-muted-foreground">
{{ $t('ai-platform.workflowRuns.detail.agent') }} 执行者
</div> </div>
<div class="font-medium"> <div class="font-medium">
{{ getLogMeta(log).agent_name || getLogMeta(log).agent_code }} {{ getActorName(log) }}
</div> </div>
</div> </div>
<div> <div>
<div class="text-muted-foreground"> <div class="text-muted-foreground">
{{ $t('ai-platform.workflowRuns.detail.agentCode') }} 交接目标
</div>
<div class="font-medium">
{{ getTargetName(log) || '-' }}
</div>
</div>
<div>
<div class="text-muted-foreground">
协作通道
</div>
<div class="break-words">
{{ getCommunication(log).channel || getCollaboration(log).collaboration_mode || '-' }}
</div>
</div>
<div>
<div class="text-muted-foreground">分支/子流程</div>
<div class="break-words">
{{
getCommunication(log).branch_label ||
getCollaboration(log).branch_label ||
getCollaboration(log).subflow_name ||
'-'
}}
</div> </div>
<div class="font-mono">{{ getLogMeta(log).agent_code }}</div>
</div> </div>
<div> <div>
<div class="text-muted-foreground"> <div class="text-muted-foreground">
{{ $t('ai-platform.workflowRuns.detail.model') }} {{ $t('ai-platform.workflowRuns.detail.model') }}
</div> </div>
<div class="break-words"> <div class="break-words">
{{ getLogMeta(log).model || getLogMeta(log).model_id || '-' }} {{
getLogMeta(log).model ||
getCollaboration(log).model ||
getLogMeta(log).model_id ||
getCollaboration(log).model_id ||
'-'
}}
</div> </div>
</div> </div>
<div> <div>
@@ -502,6 +575,15 @@ defineExpose({ open });
{{ getLogMeta(log).completion_tokens || 0 }} {{ getLogMeta(log).completion_tokens || 0 }}
</div> </div>
</div> </div>
<div
v-if="getCommunication(log).message"
class="col-span-2"
>
<div class="text-muted-foreground">通信摘要</div>
<div class="break-words">
{{ getCommunication(log).message }}
</div>
</div>
</div> </div>
<div> <div>
<div class="text-muted-foreground mb-1 font-medium"> <div class="text-muted-foreground mb-1 font-medium">