feat(multi_agent): add formatters and structured rendering for multi-agent tools
This commit is contained in:
parent
571ed6c81f
commit
ce92697181
39
AGENTS.md
39
AGENTS.md
@ -387,7 +387,40 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
- 子智能体进度弹窗:输出与工具按真实时间线混排,默认 3 行,过长用省略号或内部滚动;颜色走 `--text-primary` 等语义 token。
|
||||
- 全局工具规范统一适用于多智能体权限:UI 不能引入 `compact` 以外的 fallback 样式习惯复制到多智能体渲染。
|
||||
|
||||
### 11.6 调试机制
|
||||
### 11.6 工具结果格式化与前端渲染位置
|
||||
|
||||
新增多智能体工具时,必须同步补齐「后端结果格式化」和「前端结构化渲染」,否则前端会 fallback 到原始 JSON 或空白。
|
||||
|
||||
**后端 formatter(主智能体工具)**
|
||||
- 实现位置:`utils/tool_result_formatter/agent_context.py`
|
||||
- 注册位置:`utils/tool_result_formatter/dispatch.py` 的 `TOOL_FORMATTERS`
|
||||
- 处理入口:所有主智能体工具执行结果,最终由 `format_tool_result_for_context()` 转换为自然语言摘要,写入对话历史。
|
||||
|
||||
**后端 formatter(子智能体通信工具)**
|
||||
- 实现位置:`modules/sub_agent/toolkit.py` 的 `_format_tool_result()`
|
||||
- 覆盖工具:`ask_master` / `ask_other_agent` / `answer_other_agent` / `list_active_sub_agents`
|
||||
- 处理入口:子智能体 tool call 结果回填到子对话上下文前。
|
||||
|
||||
**前端 renderer(主路径)**
|
||||
- 实现位置:`static/src/components/chat/actions/toolRenderers.ts` 的 `renderEnhancedToolResult()`
|
||||
- 调用方:
|
||||
- `static/src/components/chat/MinimalBlocks.vue`
|
||||
- `static/src/components/chat/StackedBlocks.vue`
|
||||
- 注意:这两个视图已移除 `enhanced_tool_display` 开关判断,**所有工具块都强制走结构化渲染**,不再显示原始 JSON。
|
||||
|
||||
**前端 renderer(备用路径)**
|
||||
- 实现位置:`static/src/components/chat/actions/ToolAction.vue` 的 `renderToolResult()`
|
||||
- 调用方:`static/src/components/chat/ChatArea.vue`
|
||||
- 同样已移除原始 JSON fallback,仅作为 ChatArea 独立工具块渲染的备用。
|
||||
|
||||
**新增工具 checklist**
|
||||
1. `modules/multi_agent/tools.py` 定义工具签名。
|
||||
2. `core/main_terminal_parts/tools_execution.py` 实现工具 handler 并返回标准 dict(`success` + 业务字段 + 可选 `error`)。
|
||||
3. `utils/tool_result_formatter/agent_context.py` 新增 `_format_<tool_name>()`,`dispatch.py` 注册。
|
||||
4. `static/src/components/chat/actions/toolRenderers.ts` 新增对应 `render<PascalCaseToolName>()` 并在 `renderEnhancedToolResult()` 中分发。
|
||||
5. 如果是子智能体通信工具,同步在 `modules/sub_agent/toolkit.py` 的 `_format_tool_result()` 中补 formatter。
|
||||
|
||||
### 11.7 调试机制
|
||||
|
||||
- 调试日志统一走 `modules/multi_agent/debug_logger.py` 的 `ma_debug()` 函数,写入 `~/.astrion/astrion/host/logs/multi_agent_loop.log`。
|
||||
- 关键状态转换点必须打 `ma_debug`,便于复现 bug:
|
||||
@ -398,7 +431,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
- `create_chat_task` 异常 / `provide_answer` 跨循环回写
|
||||
- `runtime_injected` 字段标记(metadata 里能区分多智能体 inline / idle / ask插入路径)
|
||||
|
||||
### 11.7 已知坑
|
||||
### 11.8 已知坑
|
||||
|
||||
- 多智能体模式下根本不 `emit` `sub_agent_waiting` 事件,避免前端进入「等待后台子智能体」的输入区阻塞态。
|
||||
- `_announced_sub_agent_tasks` / `notified` 等标记仅适用于传统后台子智能体任务,多智能体任务不走这条通知路径。
|
||||
@ -406,7 +439,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
- 传统后台通知池 `_collect_pending_completion_notices` 在 `task.get("multi_agent_mode")` 为真时跳过该 task;多智能体派出走独立的 `poll_multi_agent_notifications` 路径,不当混用。
|
||||
- `_has_pending_completion_work` 主动排除 `multi_agent_mode=True` 的任务;两者永远独立。
|
||||
|
||||
### 11.8 对话切换与状态保留
|
||||
### 11.9 对话切换与状态保留
|
||||
|
||||
- 切换会话不清理 `_running_tasks` 和 `_sub_agent_instances`。`SubAgentManager` 的全局 tasks 字典按 `task_id` 保持,多智能体状态由 `conversation_id` 在 `get_multi_agent_state` 中查。
|
||||
- 子智能体对话存在 `~/.astrion/astrion/host/host/data/sub_agents/`。重启后走 `manager.restore_sub_agent` 恢复实例引用。
|
||||
|
||||
@ -373,6 +373,22 @@ def _format_tool_result(name: str, raw: Any) -> str:
|
||||
if raw.get("path"):
|
||||
return f"已保存: {raw.get('path')}"
|
||||
return raw.get("message") or "网页已保存"
|
||||
# 多智能体通信工具
|
||||
if name == "ask_master":
|
||||
if raw.get("success"):
|
||||
return f"Team Leader 的回答:\n{raw.get('answer') or ''}"
|
||||
return raw.get("error") or "向 Team Leader 提问失败"
|
||||
if name == "ask_other_agent":
|
||||
if raw.get("success"):
|
||||
return f"子智能体的回答:\n{raw.get('answer') or ''}"
|
||||
return raw.get("error") or "向其他子智能体提问失败"
|
||||
if name == "answer_other_agent":
|
||||
if raw.get("success"):
|
||||
return "回复已发送。"
|
||||
return raw.get("error") or "回复失败"
|
||||
if name == "list_active_sub_agents":
|
||||
from utils.tool_result_formatter.agent_context import _format_active_sub_agents_list
|
||||
return _format_active_sub_agents_list(raw.get("agents") or [])
|
||||
return json.dumps(raw, ensure_ascii=False)
|
||||
|
||||
|
||||
|
||||
@ -939,15 +939,13 @@ const getToolIntent = (action: Action) => {
|
||||
};
|
||||
|
||||
const renderToolResult = (action: Action) => {
|
||||
if (personalizationStore.form.enhanced_tool_display) {
|
||||
const rendered = renderEnhancedToolResult(
|
||||
action,
|
||||
props.formatSearchTopic || (() => ''),
|
||||
props.formatSearchTime || (() => ''),
|
||||
props.formatSearchDomains || (() => '')
|
||||
);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
const rendered = renderEnhancedToolResult(
|
||||
action,
|
||||
props.formatSearchTopic || (() => ''),
|
||||
props.formatSearchTime || (() => ''),
|
||||
props.formatSearchDomains || (() => '')
|
||||
);
|
||||
if (rendered) return rendered;
|
||||
|
||||
const result = action.tool?.result;
|
||||
if (!result) return '<div class="result-item">执行中...</div>';
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
class="stacked-shell"
|
||||
:class="{ 'stacked-shell--single': isSingle, 'stacked-shell--not-ready': !ready, 'stacked-shell--no-borders': hideBorders }"
|
||||
:class="{
|
||||
'stacked-shell--single': isSingle,
|
||||
'stacked-shell--not-ready': !ready,
|
||||
'stacked-shell--no-borders': hideBorders
|
||||
}"
|
||||
ref="shell"
|
||||
:style="{
|
||||
height: `${shellHeight}px`,
|
||||
@ -89,59 +93,7 @@
|
||||
</div>
|
||||
<div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
|
||||
<div class="content-inner">
|
||||
<div
|
||||
v-if="shouldUseEnhancedDisplay && renderToolResult(action)"
|
||||
v-html="renderToolResult(action)"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="
|
||||
!shouldUseEnhancedDisplay &&
|
||||
action.tool?.name === 'web_search' &&
|
||||
action.tool?.result
|
||||
"
|
||||
>
|
||||
<div class="search-meta">
|
||||
<div>
|
||||
<strong>搜索内容:</strong
|
||||
>{{ action.tool.result.query || action.tool.arguments?.query }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>主题:</strong
|
||||
>{{ formatSearchTopic(action.tool.result.filters || {}) }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>时间范围:</strong
|
||||
>{{ formatSearchTime(action.tool.result.filters || {}) }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>限定网站:</strong
|
||||
>{{ formatSearchDomains(action.tool.result.filters || {}) }}
|
||||
</div>
|
||||
<div><strong>结果数量:</strong>{{ action.tool.result.total_results }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="action.tool.result.results && action.tool.result.results.length"
|
||||
class="search-result-list"
|
||||
>
|
||||
<div
|
||||
v-for="item in action.tool.result.results"
|
||||
:key="item.url || item.index"
|
||||
class="search-result-item"
|
||||
>
|
||||
<div class="search-result-title">{{ item.title || '无标题' }}</div>
|
||||
<div class="search-result-url">
|
||||
<a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a
|
||||
><span v-else>无可用链接</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-empty">未返回详细的搜索结果。</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{
|
||||
JSON.stringify(action.tool?.result || action.tool?.arguments, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
<div v-html="renderToolResult(action)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isToolProcessing(action)" class="progress-indicator"></div>
|
||||
@ -190,10 +142,6 @@ const stackKey = `stacked-${++stackInstanceCounter}`;
|
||||
// 初始化 personalization store
|
||||
const personalizationStore = usePersonalizationStore();
|
||||
|
||||
const shouldUseEnhancedDisplay = computed(() => {
|
||||
return personalizationStore.form.enhanced_tool_display;
|
||||
});
|
||||
|
||||
const hideBorders = computed(() => personalizationStore.form.stacked_hide_borders);
|
||||
|
||||
const renderToolResult = (action: any) => {
|
||||
@ -338,7 +286,7 @@ const measureAndCompute = () => {
|
||||
nextContentHeights[key] = fullContent;
|
||||
const expanded = isExpandedById(key);
|
||||
// 分隔线:隐藏边线模式下统一为 0,否则最后一块无 border-bottom
|
||||
const borderH = hideBorders.value ? 0 : (idx === children.length - 1 ? 0 : 1);
|
||||
const borderH = hideBorders.value ? 0 : idx === children.length - 1 ? 0 : 1;
|
||||
heights.push(headerH + (expanded ? fullContent : 0) + borderH);
|
||||
});
|
||||
|
||||
|
||||
@ -30,10 +30,7 @@
|
||||
"
|
||||
>
|
||||
<div class="content-inner">
|
||||
<div v-if="shouldUseEnhancedDisplay" v-html="renderEnhancedToolResult()"></div>
|
||||
<div v-else>
|
||||
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-html="renderToolResult()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -44,9 +41,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { usePersonalizationStore } from '@/stores/personalization';
|
||||
|
||||
defineOptions({ name: 'ToolAction' });
|
||||
|
||||
const props = defineProps<{
|
||||
@ -68,13 +62,6 @@ const props = defineProps<{
|
||||
|
||||
defineEmits<{ (event: 'toggle'): void }>();
|
||||
|
||||
// 初始化 store
|
||||
const personalizationStore = usePersonalizationStore();
|
||||
|
||||
const shouldUseEnhancedDisplay = computed(() => {
|
||||
return personalizationStore.form.enhanced_tool_display && props.action.tool.result;
|
||||
});
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
@ -122,7 +109,7 @@ function formatToolStatusLabel(result: any, successLabel: string, failedLabel =
|
||||
return result?.success ? successLabel : failedLabel;
|
||||
}
|
||||
|
||||
function renderEnhancedToolResult(): string {
|
||||
function renderToolResult(): string {
|
||||
const tool = props.action.tool;
|
||||
const result = tool.result;
|
||||
const args = tool.arguments || {};
|
||||
@ -154,7 +141,7 @@ function renderEnhancedToolResult(): string {
|
||||
else if (name === 'read_file' || name === 'read_skill') {
|
||||
return renderReadFile(result, args);
|
||||
} else if (name === 'create_skill') {
|
||||
return renderCreateSkill(result, args);
|
||||
return renderCreateSkill(result);
|
||||
} else if (name === 'vlm_analyze') {
|
||||
return renderVlmAnalyze(result, args);
|
||||
} else if (name === 'ocr_image') {
|
||||
@ -215,12 +202,86 @@ function renderEnhancedToolResult(): string {
|
||||
return renderCreateSubAgent(result, args);
|
||||
} else if (name === 'terminate_sub_agent') {
|
||||
return renderTerminateSubAgent(result, args);
|
||||
} else if (name === 'send_message_to_sub_agent') {
|
||||
return renderSendMessageToSubAgent(result, args);
|
||||
} else if (name === 'ask_sub_agent') {
|
||||
return renderAskSubAgent(result, args);
|
||||
} else if (name === 'answer_sub_agent_question') {
|
||||
return renderAnswerSubAgentQuestion(result, args);
|
||||
} else if (name === 'create_custom_agent') {
|
||||
return renderCreateCustomAgent(result, args);
|
||||
} else if (name === 'list_agents') {
|
||||
return renderListAgents(result);
|
||||
} else if (name === 'list_active_sub_agents') {
|
||||
return renderListActiveSubAgents(result);
|
||||
} else if (name === 'get_sub_agent_status') {
|
||||
return renderGetSubAgentStatus(result, args);
|
||||
return renderGetSubAgentStatus(result);
|
||||
}
|
||||
|
||||
// 默认显示 JSON
|
||||
return `<pre>${escapeHtml(JSON.stringify(result || args, null, 2))}</pre>`;
|
||||
// 默认不再显示原始 JSON,而是显示参数摘要 + 结果状态
|
||||
return renderDefaultResult(result, args, name);
|
||||
}
|
||||
|
||||
function renderDefaultResult(result: any, args: any, name: string): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 完成', '✗ 失败');
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>工具:</strong>${escapeHtml(name)}</div>`;
|
||||
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
|
||||
|
||||
// 只展示重要参数:文件路径、目标 ID、问题等
|
||||
const importantKeys = [
|
||||
'path',
|
||||
'file_path',
|
||||
'agent_id',
|
||||
'target_agent_id',
|
||||
'question',
|
||||
'role_id',
|
||||
'url'
|
||||
];
|
||||
importantKeys.forEach((key) => {
|
||||
const value = args[key] ?? result?.[key];
|
||||
if (value !== undefined && value !== '' && value !== null) {
|
||||
const labelMap: Record<string, string> = {
|
||||
path: '路径',
|
||||
file_path: '路径',
|
||||
agent_id: '子智能体 ID',
|
||||
target_agent_id: '目标子智能体 ID',
|
||||
question: '问题',
|
||||
role_id: '角色 ID',
|
||||
url: 'URL'
|
||||
};
|
||||
const displayValue = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
const preview = String(displayValue).slice(0, 200);
|
||||
const suffix = String(displayValue).length > 200 ? '…' : '';
|
||||
html += `<div><strong>${escapeHtml(labelMap[key] || key)}:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// 如果结果里有可读的 message/summary/answer/output/content/data,直接展示
|
||||
const readable =
|
||||
result?.message ??
|
||||
result?.summary ??
|
||||
result?.answer ??
|
||||
result?.output ??
|
||||
result?.content ??
|
||||
result?.data ??
|
||||
'';
|
||||
if (readable && typeof readable === 'string' && readable.trim()) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += `<pre>${escapeHtml(readable.trim())}</pre>`;
|
||||
html += '</div>';
|
||||
} else if (result?.success) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="tool-result-empty">工具执行完成,无额外可展示内容。</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// ===== 网络检索类 =====
|
||||
@ -918,7 +979,7 @@ function renderConversationReview(result: any, args: any): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCreateSkill(result: any, args: any): string {
|
||||
function renderCreateSkill(result: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已归档');
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
@ -1242,7 +1303,7 @@ function renderTerminateSubAgent(result: any, args: any): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderGetSubAgentStatus(result: any, args: any): string {
|
||||
function renderGetSubAgentStatus(result: any): string {
|
||||
if (!result.success) {
|
||||
const error = result.error ?? '查询失败';
|
||||
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
|
||||
@ -1299,6 +1360,185 @@ function renderGetSubAgentStatus(result: any, args: any): string {
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderSendMessageToSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已发送', '✗ 发送失败');
|
||||
const agentId = args.agent_id ?? result.agent_id ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAskSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
|
||||
const agentId = args.agent_id ?? '';
|
||||
const question = args.question ?? '';
|
||||
const answer =
|
||||
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (question) {
|
||||
const preview = String(question).slice(0, 200);
|
||||
const suffix = String(question).length > 200 ? '…' : '';
|
||||
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (answer && typeof answer === 'string') {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">回答</div>';
|
||||
html += `<pre>${escapeHtml(answer)}</pre>`;
|
||||
html += '</div>';
|
||||
} else if (result?.success) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAnswerSubAgentQuestion(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
|
||||
const questionId = args.question_id ?? result.question_id ?? '';
|
||||
const answer = args.answer ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (questionId) {
|
||||
html += `<div><strong>问题 ID:</strong>${escapeHtml(String(questionId))}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (answer && typeof answer === 'string') {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">回复内容</div>';
|
||||
const preview = String(answer).slice(0, 500);
|
||||
const suffix = String(answer).length > 500 ? '…' : '';
|
||||
html += `<pre>${escapeHtml(preview + suffix)}</pre>`;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCreateCustomAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已创建', '✗ 创建失败');
|
||||
const roleId = args.role_id ?? result.role_id ?? '';
|
||||
const name = args.name ?? result.name ?? roleId;
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (roleId !== '') {
|
||||
html += `<div><strong>角色 ID:</strong>${escapeHtml(String(roleId))}</div>`;
|
||||
}
|
||||
if (name && name !== roleId) {
|
||||
html += `<div><strong>角色名:</strong>${escapeHtml(String(name))}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderListAgents(result: any): string {
|
||||
if (!result?.success) {
|
||||
const error = result?.error ?? '查询失败';
|
||||
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
|
||||
}
|
||||
|
||||
const roles = Array.isArray(result.roles) ? result.roles : [];
|
||||
if (roles.length === 0) {
|
||||
return '<div class="tool-result-empty">当前没有可用角色。</div>';
|
||||
}
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>角色数量:</strong>${roles.length}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="search-result-list">';
|
||||
roles.forEach((role: any) => {
|
||||
const roleId = role.role_id || '未知';
|
||||
const name = role.name || roleId;
|
||||
const description = role.description || '';
|
||||
const thinkingMode = role.thinking_mode || 'fast';
|
||||
const isCustom = role.is_custom ? '是' : '否';
|
||||
|
||||
html += '<div class="search-result-item">';
|
||||
html += `<div class="search-result-title">${escapeHtml(roleId)} — ${escapeHtml(name)}</div>`;
|
||||
if (description) {
|
||||
html += `<div>${escapeHtml(description)}</div>`;
|
||||
}
|
||||
html += `<div><strong>思考模式:</strong>${escapeHtml(thinkingMode)} | <strong>自定义:</strong>${escapeHtml(isCustom)}</div>`;
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderListActiveSubAgents(result: any): string {
|
||||
if (!result?.success) {
|
||||
const error = result?.error ?? '查询失败';
|
||||
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
|
||||
}
|
||||
|
||||
const agents = Array.isArray(result.agents) ? result.agents : [];
|
||||
if (agents.length === 0) {
|
||||
return '<div class="tool-result-empty">当前会话没有活跃子智能体。</div>';
|
||||
}
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>活跃数量:</strong>${agents.length}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="sub-agent-status-list">';
|
||||
agents.forEach((agent: any) => {
|
||||
const agentId = agent.agent_id ?? '?';
|
||||
const displayName = agent.display_name || `Agent_${agentId}`;
|
||||
const status = agent.status || 'unknown';
|
||||
const summary = agent.summary || '';
|
||||
const lastOutput = agent.last_output || '';
|
||||
|
||||
html += '<div class="sub-agent-status-item">';
|
||||
html += `<div class="sub-agent-status-header">#${escapeHtml(String(agentId))} ${escapeHtml(displayName)} [${escapeHtml(status)}]</div>`;
|
||||
html += '<div class="tool-result-meta">';
|
||||
if (summary) {
|
||||
html += `<div><strong>任务:</strong>${escapeHtml(String(summary))}</div>`;
|
||||
}
|
||||
if (lastOutput) {
|
||||
const preview = String(lastOutput).slice(0, 120);
|
||||
const suffix = String(lastOutput).length > 120 ? '…' : '';
|
||||
html += `<div><strong>最近输出:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@ -62,6 +62,11 @@ export function renderEnhancedToolResult(
|
||||
return '';
|
||||
}
|
||||
|
||||
// 如果 result 已经被格式化为文本字符串,直接显示
|
||||
if (typeof result === 'string') {
|
||||
return `<div class="tool-result-content scrollable"><pre>${escapeHtml(result)}</pre></div>`;
|
||||
}
|
||||
|
||||
// 网络检索类
|
||||
if (name === 'web_search') {
|
||||
return renderWebSearch(result, args, formatSearchTopic, formatSearchTime, formatSearchDomains);
|
||||
@ -88,7 +93,7 @@ export function renderEnhancedToolResult(
|
||||
else if (name === 'read_file' || name === 'read_skill') {
|
||||
return renderReadFile(result, args);
|
||||
} else if (name === 'create_skill') {
|
||||
return renderCreateSkill(result, args);
|
||||
return renderCreateSkill(result);
|
||||
} else if (name === 'vlm_analyze') {
|
||||
return renderVlmAnalyze(result, args);
|
||||
} else if (name === 'ocr_image') {
|
||||
@ -152,11 +157,84 @@ export function renderEnhancedToolResult(
|
||||
return renderCreateSubAgent(result, args);
|
||||
} else if (name === 'terminate_sub_agent') {
|
||||
return renderTerminateSubAgent(result, args);
|
||||
} else if (name === 'send_message_to_sub_agent') {
|
||||
return renderSendMessageToSubAgent(result, args);
|
||||
} else if (name === 'ask_sub_agent') {
|
||||
return renderAskSubAgent(result, args);
|
||||
} else if (name === 'answer_sub_agent_question') {
|
||||
return renderAnswerSubAgentQuestion(result, args);
|
||||
} else if (name === 'create_custom_agent') {
|
||||
return renderCreateCustomAgent(result, args);
|
||||
} else if (name === 'list_agents') {
|
||||
return renderListAgents(result);
|
||||
} else if (name === 'list_active_sub_agents') {
|
||||
return renderListActiveSubAgents(result);
|
||||
} else if (name === 'get_sub_agent_status') {
|
||||
return renderGetSubAgentStatus(result, args);
|
||||
return renderGetSubAgentStatus(result);
|
||||
}
|
||||
|
||||
return '';
|
||||
// 默认回退:显示重要参数 + 结果摘要,不再直接返回空字符串
|
||||
return renderDefaultResult(result, args, name);
|
||||
}
|
||||
|
||||
function renderDefaultResult(result: any, args: any, name: string): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 完成', '✗ 失败');
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>工具:</strong>${escapeHtml(name)}</div>`;
|
||||
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
|
||||
|
||||
const importantKeys = [
|
||||
'path',
|
||||
'file_path',
|
||||
'agent_id',
|
||||
'target_agent_id',
|
||||
'question',
|
||||
'role_id',
|
||||
'url'
|
||||
];
|
||||
importantKeys.forEach((key) => {
|
||||
const value = args?.[key] ?? result?.[key];
|
||||
if (value !== undefined && value !== '' && value !== null) {
|
||||
const labelMap: Record<string, string> = {
|
||||
path: '路径',
|
||||
file_path: '路径',
|
||||
agent_id: '子智能体 ID',
|
||||
target_agent_id: '目标子智能体 ID',
|
||||
question: '问题',
|
||||
role_id: '角色 ID',
|
||||
url: 'URL'
|
||||
};
|
||||
const displayValue = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
const preview = String(displayValue).slice(0, 200);
|
||||
const suffix = String(displayValue).length > 200 ? '…' : '';
|
||||
html += `<div><strong>${escapeHtml(labelMap[key] || key)}:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
const readable =
|
||||
result?.message ??
|
||||
result?.summary ??
|
||||
result?.answer ??
|
||||
result?.output ??
|
||||
result?.content ??
|
||||
result?.data ??
|
||||
'';
|
||||
if (readable && typeof readable === 'string' && readable.trim()) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += `<pre>${escapeHtml(readable.trim())}</pre>`;
|
||||
html += '</div>';
|
||||
} else if (result?.success) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="tool-result-empty">工具执行完成,无额外可展示内容。</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 网络检索类渲染函数
|
||||
@ -904,7 +982,7 @@ function renderConversationReview(result: any, args: any): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCreateSkill(result: any, args: any): string {
|
||||
function renderCreateSkill(result: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已归档');
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
@ -1218,7 +1296,7 @@ function renderTerminateSubAgent(result: any, args: any): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderGetSubAgentStatus(result: any, args: any): string {
|
||||
function renderGetSubAgentStatus(result: any): string {
|
||||
if (!result.success) {
|
||||
const error = result.error ?? '查询失败';
|
||||
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
|
||||
@ -1275,3 +1353,182 @@ function renderGetSubAgentStatus(result: any, args: any): string {
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderSendMessageToSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已发送', '✗ 发送失败');
|
||||
const agentId = args.agent_id ?? result.agent_id ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAskSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
|
||||
const agentId = args.agent_id ?? '';
|
||||
const question = args.question ?? '';
|
||||
const answer =
|
||||
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (question) {
|
||||
const preview = String(question).slice(0, 200);
|
||||
const suffix = String(question).length > 200 ? '…' : '';
|
||||
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (answer && typeof answer === 'string') {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">回答</div>';
|
||||
html += `<pre>${escapeHtml(answer)}</pre>`;
|
||||
html += '</div>';
|
||||
} else if (result?.success) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAnswerSubAgentQuestion(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
|
||||
const questionId = args.question_id ?? result.question_id ?? '';
|
||||
const answer = args.answer ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (questionId) {
|
||||
html += `<div><strong>问题 ID:</strong>${escapeHtml(String(questionId))}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (answer && typeof answer === 'string') {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">回复内容</div>';
|
||||
const preview = String(answer).slice(0, 500);
|
||||
const suffix = String(answer).length > 500 ? '…' : '';
|
||||
html += `<pre>${escapeHtml(preview + suffix)}</pre>`;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCreateCustomAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已创建', '✗ 创建失败');
|
||||
const roleId = args.role_id ?? result.role_id ?? '';
|
||||
const name = args.name ?? result.name ?? roleId;
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (roleId !== '') {
|
||||
html += `<div><strong>角色 ID:</strong>${escapeHtml(String(roleId))}</div>`;
|
||||
}
|
||||
if (name && name !== roleId) {
|
||||
html += `<div><strong>角色名:</strong>${escapeHtml(String(name))}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderListAgents(result: any): string {
|
||||
if (!result?.success) {
|
||||
const error = result?.error ?? '查询失败';
|
||||
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
|
||||
}
|
||||
|
||||
const roles = Array.isArray(result.roles) ? result.roles : [];
|
||||
if (roles.length === 0) {
|
||||
return '<div class="tool-result-empty">当前没有可用角色。</div>';
|
||||
}
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>角色数量:</strong>${roles.length}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="search-result-list">';
|
||||
roles.forEach((role: any) => {
|
||||
const roleId = role.role_id || '未知';
|
||||
const name = role.name || roleId;
|
||||
const description = role.description || '';
|
||||
const thinkingMode = role.thinking_mode || 'fast';
|
||||
const isCustom = role.is_custom ? '是' : '否';
|
||||
|
||||
html += '<div class="search-result-item">';
|
||||
html += `<div class="search-result-title">${escapeHtml(roleId)} — ${escapeHtml(name)}</div>`;
|
||||
if (description) {
|
||||
html += `<div>${escapeHtml(description)}</div>`;
|
||||
}
|
||||
html += `<div><strong>思考模式:</strong>${escapeHtml(thinkingMode)} | <strong>自定义:</strong>${escapeHtml(isCustom)}</div>`;
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderListActiveSubAgents(result: any): string {
|
||||
if (!result?.success) {
|
||||
const error = result?.error ?? '查询失败';
|
||||
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
|
||||
}
|
||||
|
||||
const agents = Array.isArray(result.agents) ? result.agents : [];
|
||||
if (agents.length === 0) {
|
||||
return '<div class="tool-result-empty">当前会话没有活跃子智能体。</div>';
|
||||
}
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>活跃数量:</strong>${agents.length}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="sub-agent-status-list">';
|
||||
agents.forEach((agent: any) => {
|
||||
const agentId = agent.agent_id ?? '?';
|
||||
const displayName = agent.display_name || `Agent_${agentId}`;
|
||||
const status = agent.status || 'unknown';
|
||||
const summary = agent.summary || '';
|
||||
const lastOutput = agent.last_output || '';
|
||||
|
||||
html += '<div class="sub-agent-status-item">';
|
||||
html += `<div class="sub-agent-status-header">#${escapeHtml(String(agentId))} ${escapeHtml(displayName)} [${escapeHtml(status)}]</div>`;
|
||||
html += '<div class="tool-result-meta">';
|
||||
if (summary) {
|
||||
html += `<div><strong>任务:</strong>${escapeHtml(String(summary))}</div>`;
|
||||
}
|
||||
if (lastOutput) {
|
||||
const preview = String(lastOutput).slice(0, 120);
|
||||
const suffix = String(lastOutput).length > 120 ? '…' : '';
|
||||
html += `<div><strong>最近输出:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -245,3 +245,95 @@ def _format_close_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
status = result_data.get("status")
|
||||
status_note = f"(状态 {status})" if status else ""
|
||||
return f"{message}{status_note}(task_id={task_id})"
|
||||
|
||||
|
||||
def _format_terminate_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("terminate_sub_agent", result_data)
|
||||
agent_id = result_data.get("agent_id")
|
||||
task_id = result_data.get("task_id")
|
||||
message = result_data.get("message") or "子智能体已被强制关闭。"
|
||||
if agent_id is not None:
|
||||
return f"已强制关闭子智能体 #{agent_id}(task_id={task_id})。"
|
||||
return f"{message}(task_id={task_id})"
|
||||
|
||||
|
||||
def _format_send_message_to_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("send_message_to_sub_agent", result_data)
|
||||
agent_id = result_data.get("agent_id")
|
||||
if agent_id is not None:
|
||||
return f"已向子智能体 #{agent_id} 发送消息。"
|
||||
return "消息已发送。"
|
||||
|
||||
|
||||
def _format_ask_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("ask_sub_agent", result_data)
|
||||
answer = result_data.get("answer")
|
||||
if answer is None:
|
||||
return "子智能体未返回答复。"
|
||||
return f"子智能体的回答:\n{answer}"
|
||||
|
||||
|
||||
def _format_answer_sub_agent_question(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("answer_sub_agent_question", result_data)
|
||||
question_id = result_data.get("question_id")
|
||||
if question_id:
|
||||
return f"已向子智能体回复问题 {question_id}。"
|
||||
return "回复已发送。"
|
||||
|
||||
|
||||
def _format_create_custom_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("create_custom_agent", result_data)
|
||||
role_id = result_data.get("role_id")
|
||||
name = result_data.get("name") or role_id
|
||||
return f"已创建自定义角色 {role_id}({name})。"
|
||||
|
||||
|
||||
def _format_list_agents(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("list_agents", result_data)
|
||||
roles = result_data.get("roles") or []
|
||||
if not roles:
|
||||
return "当前没有可用角色。"
|
||||
lines = [f"可用角色(共 {len(roles)} 个):", ""]
|
||||
for idx, role in enumerate(roles, start=1):
|
||||
role_id = role.get("role_id") or "未知"
|
||||
name = role.get("name") or role_id
|
||||
description = role.get("description") or ""
|
||||
thinking_mode = role.get("thinking_mode") or "fast"
|
||||
is_custom = "是" if role.get("is_custom") else "否"
|
||||
lines.append(f"{idx}. {role_id} — {name}")
|
||||
if description:
|
||||
lines.append(f" 描述:{description}")
|
||||
lines.append(f" 思考模式:{thinking_mode} | 自定义:{is_custom}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _format_list_active_sub_agents(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("list_active_sub_agents", result_data)
|
||||
return _format_active_sub_agents_list(result_data.get("agents") or [])
|
||||
|
||||
|
||||
def _format_active_sub_agents_list(agents: List[Dict[str, Any]]) -> str:
|
||||
if not agents:
|
||||
return "当前会话没有活跃子智能体。"
|
||||
lines = [f"当前会话活跃子智能体(共 {len(agents)} 个):", ""]
|
||||
for agent in agents:
|
||||
agent_id = agent.get("agent_id") or "?"
|
||||
display_name = agent.get("display_name") or f"Agent_{agent_id}"
|
||||
status = agent.get("status") or "unknown"
|
||||
summary = agent.get("summary") or ""
|
||||
last_output = agent.get("last_output") or ""
|
||||
lines.append(f"#{agent_id} {display_name} [{status}]")
|
||||
if summary:
|
||||
lines.append(f" 任务:{summary}")
|
||||
if last_output:
|
||||
preview = last_output[:120]
|
||||
suffix = "…" if len(last_output) > 120 else ""
|
||||
lines.append(f" 最近输出:{preview}{suffix}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@ -36,6 +36,13 @@ from utils.tool_result_formatter.agent_context import (
|
||||
_format_create_sub_agent,
|
||||
_format_close_sub_agent,
|
||||
_format_get_sub_agent_status,
|
||||
_format_terminate_sub_agent,
|
||||
_format_send_message_to_sub_agent,
|
||||
_format_ask_sub_agent,
|
||||
_format_answer_sub_agent_question,
|
||||
_format_create_custom_agent,
|
||||
_format_list_agents,
|
||||
_format_list_active_sub_agents,
|
||||
)
|
||||
from utils.tool_result_formatter.web_media import (
|
||||
_format_extract_webpage,
|
||||
@ -122,5 +129,12 @@ TOOL_FORMATTERS = {
|
||||
"manage_personalization": _format_manage_personalization,
|
||||
"create_sub_agent": _format_create_sub_agent,
|
||||
"close_sub_agent": _format_close_sub_agent,
|
||||
"terminate_sub_agent": _format_terminate_sub_agent,
|
||||
"send_message_to_sub_agent": _format_send_message_to_sub_agent,
|
||||
"ask_sub_agent": _format_ask_sub_agent,
|
||||
"answer_sub_agent_question": _format_answer_sub_agent_question,
|
||||
"create_custom_agent": _format_create_custom_agent,
|
||||
"list_agents": _format_list_agents,
|
||||
"list_active_sub_agents": _format_list_active_sub_agents,
|
||||
"get_sub_agent_status": _format_get_sub_agent_status,
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user