feat(multi_agent): add formatters and structured rendering for multi-agent tools

This commit is contained in:
JOJO 2026-07-14 22:11:48 +08:00
parent 571ed6c81f
commit ce92697181
8 changed files with 695 additions and 97 deletions

View File

@ -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` 恢复实例引用。

View File

@ -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)

View File

@ -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>';

View File

@ -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);
});

View File

@ -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>

View File

@ -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;
}

View File

@ -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)

View File

@ -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,
}