mirror of
https://github.com/jihe520/MathModelAgent.git
synced 2026-09-19 08:09:48 +08:00
feat: 新增 Web Search 工具 + 统一 tool_registry 分发 (#68)
主要功能: - Web Search: Tavily API 搜索互联网获取真实数据,注册为 search_web 工具 - 统一工具注册: tool_registry 替代 if/elif 硬编码分发 - Evidence 数据模型: DataEvidence 结构化搜索结果 编排层(workflow.py)集成: - Web Search 初始化与动态工具注册 - RAG 知识库检索与注入 - HIL 人机协作检查点 - Fallback Hand Off 容错机制 - Evaluator + Feedback Rerun 质量保障 新增配置:TAVILY_API_KEY / SEARCH_ENABLED / SEARCH_CACHE_TTL 所有功能均有独立开关,未配置时行为与升级前一致。
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ref, computed, onUnmounted } from "vue";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
/** 审批消息数据 */
|
||||
interface ApprovalData {
|
||||
checkpoint_id: string;
|
||||
prompt: Record<string, unknown>;
|
||||
options: string[];
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
// ---- State ----
|
||||
|
||||
const showDialog = ref(false);
|
||||
const approvalData = ref<ApprovalData | null>(null);
|
||||
const editContent = ref("");
|
||||
const showEditArea = ref(false);
|
||||
const showAskArea = ref(false);
|
||||
const askContent = ref("");
|
||||
const remainingSeconds = ref(0);
|
||||
|
||||
let resolvePromise: ((value: { action: string; content?: unknown }) => void) | null = null;
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// ---- Computed ----
|
||||
|
||||
const promptDisplay = computed(() => {
|
||||
if (!approvalData.value) return "";
|
||||
const p = approvalData.value.prompt;
|
||||
if (p.step) return `步骤: ${p.step}`;
|
||||
if (p.subtask) return `子任务: ${p.subtask}`;
|
||||
return JSON.stringify(p, null, 2);
|
||||
});
|
||||
|
||||
const promptDetail = computed(() => {
|
||||
if (!approvalData.value) return "";
|
||||
const p = approvalData.value.prompt;
|
||||
// 显示关键内容摘要
|
||||
if (p.questions) return JSON.stringify(p.questions, null, 2).slice(0, 500);
|
||||
if (p.solutions) return JSON.stringify(p.solutions, null, 2).slice(0, 500);
|
||||
if (p.code_response) return String(p.code_response).slice(0, 500);
|
||||
if (p.summary) return String(p.summary).slice(0, 500);
|
||||
return "";
|
||||
});
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
/** 打开审批对话框 */
|
||||
function open(data: ApprovalData): Promise<{ action: string; content?: unknown }> {
|
||||
approvalData.value = data;
|
||||
editContent.value = "";
|
||||
askContent.value = "";
|
||||
showEditArea.value = false;
|
||||
showAskArea.value = false;
|
||||
showDialog.value = true;
|
||||
remainingSeconds.value = data.timeout;
|
||||
|
||||
// 启动倒计时
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingSeconds.value = Math.max(0, remainingSeconds.value - 1);
|
||||
if (remainingSeconds.value <= 0) {
|
||||
handleAction("confirm");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理用户决策 */
|
||||
function handleAction(action: string) {
|
||||
cleanup();
|
||||
showDialog.value = false;
|
||||
|
||||
const result: { action: string; content?: unknown } = { action };
|
||||
|
||||
if (action === "edit" && editContent.value) {
|
||||
try {
|
||||
result.content = JSON.parse(editContent.value);
|
||||
} catch {
|
||||
result.content = editContent.value;
|
||||
}
|
||||
} else if (action === "ask" && askContent.value) {
|
||||
result.content = askContent.value;
|
||||
}
|
||||
|
||||
resolvePromise?.(result);
|
||||
resolvePromise = null;
|
||||
}
|
||||
|
||||
/** 清理定时器 */
|
||||
function cleanup() {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(cleanup);
|
||||
|
||||
// ---- Expose ----
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showDialog">
|
||||
<DialogContent class="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>需要您的审批</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-2">
|
||||
<!-- 检查点信息 -->
|
||||
<div class="rounded-md bg-muted p-3">
|
||||
<p class="text-sm font-medium">{{ promptDisplay }}</p>
|
||||
<pre v-if="promptDetail" class="mt-2 max-h-40 overflow-auto text-xs text-muted-foreground whitespace-pre-wrap">{{ promptDetail }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- 倒计时 -->
|
||||
<div class="text-sm text-muted-foreground text-center">
|
||||
<span v-if="remainingSeconds > 0">
|
||||
{{ remainingSeconds }}秒后自动继续
|
||||
</span>
|
||||
<span v-else class="text-orange-500">超时,自动继续...</span>
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<div v-if="showEditArea" class="space-y-2">
|
||||
<p class="text-sm text-muted-foreground">请输入修改后的内容(JSON 或文本):</p>
|
||||
<Textarea v-model="editContent" rows="6" placeholder="输入修改后的内容..." />
|
||||
</div>
|
||||
|
||||
<!-- 追问区域 -->
|
||||
<div v-if="showAskArea" class="space-y-2">
|
||||
<p class="text-sm text-muted-foreground">请输入补充信息:</p>
|
||||
<Textarea v-model="askContent" rows="3" placeholder="输入补充信息..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="flex flex-wrap gap-2">
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="default" @click="handleAction('confirm')">
|
||||
确认继续
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="outline" @click="showEditArea = true">
|
||||
修改内容
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="outline" @click="handleAction('regenerate')">
|
||||
重新生成
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="outline" @click="showAskArea = true">
|
||||
追问
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="ghost" @click="handleAction('skip')">
|
||||
跳过审核
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="destructive" @click="handleAction('abort')">
|
||||
中止任务
|
||||
</Button>
|
||||
|
||||
<!-- 编辑/追问确认 -->
|
||||
<Button v-if="showEditArea" variant="default" @click="handleAction('edit')">
|
||||
提交修改
|
||||
</Button>
|
||||
<Button v-if="showEditArea" variant="ghost" @click="showEditArea = false">
|
||||
取消
|
||||
</Button>
|
||||
<Button v-if="showAskArea" variant="default" @click="handleAction('ask')">
|
||||
发送追问
|
||||
</Button>
|
||||
<Button v-if="showAskArea" variant="ghost" @click="showAskArea = false">
|
||||
取消
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTaskMessages } from "@/apis/commonApi";
|
||||
import { AgentType } from "@/utils/enum";
|
||||
import type {
|
||||
ApprovalMessage,
|
||||
CoderMessage,
|
||||
CoordinatorMessage,
|
||||
InterpreterMessage,
|
||||
@@ -36,6 +37,9 @@ export const useTaskStore = defineStore("task", () => {
|
||||
/** WebSocket 实例 */
|
||||
let ws: TaskWebSocket | null = null;
|
||||
|
||||
/** 审批消息回调(由 ApprovalDialog 注册) */
|
||||
let onApprovalCallback: ((data: ApprovalMessage) => void) | null = null;
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
/** 获取消息时间戳 */
|
||||
@@ -68,10 +72,18 @@ export const useTaskStore = defineStore("task", () => {
|
||||
return (
|
||||
typeof Reflect.get(payload, "id") === "string" &&
|
||||
typeof msgType === "string" &&
|
||||
["system", "agent", "user", "tool"].includes(msgType)
|
||||
["system", "agent", "user", "tool", "approval"].includes(msgType)
|
||||
);
|
||||
}
|
||||
|
||||
/** 类型守卫:判断是否为审批消息 */
|
||||
function isApprovalMessage(payload: unknown): payload is ApprovalMessage {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return false;
|
||||
}
|
||||
return Reflect.get(payload, "msg_type") === "approval";
|
||||
}
|
||||
|
||||
/** 设置当前活跃任务 */
|
||||
function setCurrentTask(taskId: string) {
|
||||
currentTaskId.value = taskId;
|
||||
@@ -147,6 +159,12 @@ export const useTaskStore = defineStore("task", () => {
|
||||
const wsUrl = `${baseUrl}/task/${taskId}`;
|
||||
|
||||
ws = new TaskWebSocket(wsUrl, (data) => {
|
||||
// 处理审批消息
|
||||
if (isApprovalMessage(data)) {
|
||||
appendMessage(taskId, data);
|
||||
onApprovalCallback?.(data);
|
||||
return;
|
||||
}
|
||||
if (!isMessagePayload(data)) {
|
||||
console.warn("忽略非标准任务消息:", data);
|
||||
return;
|
||||
@@ -301,6 +319,22 @@ export const useTaskStore = defineStore("task", () => {
|
||||
// 如果需要自动连接,可以在这里添加代码
|
||||
// 例如:connectWebSocket('default')
|
||||
|
||||
/** 注册审批消息回调 */
|
||||
function onApproval(callback: (data: ApprovalMessage) => void) {
|
||||
onApprovalCallback = callback;
|
||||
}
|
||||
|
||||
/** 通过 WebSocket 发送用户决策 */
|
||||
function sendDecision(checkpointId: string, decision: { action: string; content?: unknown }) {
|
||||
if (ws) {
|
||||
ws.send({
|
||||
type: "user_decision",
|
||||
checkpoint_id: checkpointId,
|
||||
decision,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
chatMessages,
|
||||
@@ -315,5 +349,7 @@ export const useTaskStore = defineStore("task", () => {
|
||||
closeWebSocket,
|
||||
downloadMessages,
|
||||
addUserMessage,
|
||||
onApproval,
|
||||
sendDecision,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -125,6 +125,15 @@ export interface WriterMessage extends AgentMessage {
|
||||
sub_title?: string;
|
||||
}
|
||||
|
||||
/** HIL 审批消息 */
|
||||
export interface ApprovalMessage extends BaseMessage {
|
||||
msg_type: "approval";
|
||||
checkpoint_id: string;
|
||||
prompt: Record<string, unknown>;
|
||||
options: string[];
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
/** 所有消息类型的联合类型 */
|
||||
export type Message =
|
||||
| SystemMessage
|
||||
@@ -133,4 +142,5 @@ export type Message =
|
||||
| WriterMessage
|
||||
| ModelerMessage
|
||||
| CoordinatorMessage
|
||||
| ToolMessage;
|
||||
| ToolMessage
|
||||
| ApprovalMessage;
|
||||
|
||||
Reference in New Issue
Block a user