From 322b1dd845c7d55a87870c4102c0661fe87b999c Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 7 Nov 2025 20:23:54 +0800 Subject: [PATCH] =?UTF-8?q?agent=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0MCP=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/mcp_server.cpython-310.pyc | Bin 33180 -> 36620 bytes mcp_server.py | 177 +++++++++++++++++- src/components/ChatBot/ChatInterfaceV2.js | 213 ++++++++++++++++------ 3 files changed, 332 insertions(+), 58 deletions(-) diff --git a/__pycache__/mcp_server.cpython-310.pyc b/__pycache__/mcp_server.cpython-310.pyc index 0b667eb0441c9f64c3d41019872be0231120d681..ef7143306d19fc895d8714cc5ff4459c9d76a9d2 100644 GIT binary patch delta 13523 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 delta 10460 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 Pk1IyFkjwhgeB%EAY*?}l diff --git a/mcp_server.py b/mcp_server.py index 534816b5..61ea6a53 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -6,9 +6,9 @@ MCP Server for Financial Data Search from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel, Field -from typing import List, Dict, Any, Optional, Literal +from typing import List, Dict, Any, Optional, Literal, AsyncGenerator from datetime import datetime, date import logging import httpx @@ -16,6 +16,7 @@ from enum import Enum import mcp_database as db from openai import OpenAI import json +import asyncio # 配置日志 logging.basicConfig(level=logging.INFO) @@ -1327,7 +1328,7 @@ class MCPAgentIntegrated: tools: List[dict], tool_handlers: Dict[str, Any], ) -> AgentResponse: - """主流程""" + """主流程(非流式)""" logger.info(f"[Agent] 处理查询: {user_query}") try: @@ -1368,6 +1369,131 @@ class MCPAgentIntegrated: message=f"处理失败: {str(e)}", ) + async def process_query_stream( + self, + user_query: str, + tools: List[dict], + tool_handlers: Dict[str, Any], + ) -> AsyncGenerator[str, None]: + """主流程(流式输出)- 逐步返回执行结果""" + logger.info(f"[Agent Stream] 处理查询: {user_query}") + + try: + # 发送开始事件 + yield self._format_sse("status", {"stage": "start", "message": "开始处理查询"}) + + # 阶段1: Kimi 制定计划 + yield self._format_sse("status", {"stage": "planning", "message": "正在制定执行计划..."}) + + plan = await self.create_plan(user_query, tools) + + # 发送计划 + yield self._format_sse("plan", { + "goal": plan.goal, + "reasoning": plan.reasoning, + "steps": [ + {"tool": step.tool, "arguments": step.arguments, "reason": step.reason} + for step in plan.steps + ], + }) + + # 阶段2: 执行工具(逐步返回) + yield self._format_sse("status", {"stage": "executing", "message": f"开始执行 {len(plan.steps)} 个步骤"}) + + step_results = [] + collected_data = {} + + for i, step in enumerate(plan.steps): + # 发送步骤开始事件 + yield self._format_sse("step_start", { + "step_index": i, + "tool": step.tool, + "arguments": step.arguments, + "reason": step.reason, + }) + + start_time = datetime.now() + + try: + # 替换占位符 + arguments = step.arguments.copy() + if step.tool == "summarize_news": + if arguments.get("data") in ["前面的新闻数据", "前面收集的所有数据"]: + arguments["data"] = json.dumps(collected_data, ensure_ascii=False, indent=2) + + # 执行工具 + result = await self.execute_tool(step.tool, arguments, tool_handlers) + execution_time = (datetime.now() - start_time).total_seconds() + + step_result = StepResult( + step_index=i, + tool=step.tool, + arguments=arguments, + status="success", + result=result, + execution_time=execution_time, + ) + step_results.append(step_result) + collected_data[f"step_{i+1}_{step.tool}"] = result + + # 发送步骤完成事件(包含结果) + yield self._format_sse("step_complete", { + "step_index": i, + "tool": step.tool, + "status": "success", + "result": result, + "execution_time": execution_time, + }) + + except Exception as e: + execution_time = (datetime.now() - start_time).total_seconds() + + step_result = StepResult( + step_index=i, + tool=step.tool, + arguments=step.arguments, + status="failed", + error=str(e), + execution_time=execution_time, + ) + step_results.append(step_result) + + # 发送步骤失败事件 + yield self._format_sse("step_complete", { + "step_index": i, + "tool": step.tool, + "status": "failed", + "error": str(e), + "execution_time": execution_time, + }) + + # 阶段3: Kimi 生成总结 + yield self._format_sse("status", {"stage": "summarizing", "message": "正在生成最终总结..."}) + + final_summary = await self.generate_final_summary(user_query, plan, step_results) + + # 发送最终总结 + yield self._format_sse("summary", { + "content": final_summary, + "metadata": { + "total_steps": len(plan.steps), + "successful_steps": len([r for r in step_results if r.status == "success"]), + "failed_steps": len([r for r in step_results if r.status == "failed"]), + "total_execution_time": sum(r.execution_time for r in step_results), + }, + }) + + # 发送完成事件 + yield self._format_sse("done", {"message": "处理完成"}) + + except Exception as e: + logger.error(f"[Agent Stream] 错误: {str(e)}", exc_info=True) + yield self._format_sse("error", {"message": f"处理失败: {str(e)}"}) + + def _format_sse(self, event: str, data: dict) -> str: + """格式化 SSE 消息""" + return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" + # 创建 Agent 实例(全局) agent = MCPAgentIntegrated() @@ -1406,7 +1532,7 @@ async def chat(request: ChatRequest): @app.post("/agent/chat", response_model=AgentResponse) async def agent_chat(request: AgentChatRequest): - """智能代理对话端点""" + """智能代理对话端点(非流式)""" logger.info(f"Agent chat: {request.message}") # 获取工具列表 @@ -1441,6 +1567,49 @@ async def agent_chat(request: AgentChatRequest): return response +@app.post("/agent/chat/stream") +async def agent_chat_stream(request: AgentChatRequest): + """智能代理对话端点(流式 SSE)""" + logger.info(f"Agent chat stream: {request.message}") + + # 获取工具列表 + tools = [tool.dict() for tool in TOOLS] + + # 添加特殊工具:summarize_news + tools.append({ + "name": "summarize_news", + "description": "使用 DeepMoney 模型总结新闻数据,提取关键信息", + "parameters": { + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "要总结的新闻数据(JSON格式)" + }, + "focus": { + "type": "string", + "description": "关注点,例如:'市场影响'、'投资机会'等" + } + }, + "required": ["data"] + } + }) + + # 返回流式响应 + return StreamingResponse( + agent.process_query_stream( + user_query=request.message, + tools=tools, + tool_handlers=TOOL_HANDLERS, + ), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", # 禁用 Nginx 缓冲 + }, + ) + # ==================== 健康检查 ==================== @app.get("/health") diff --git a/src/components/ChatBot/ChatInterfaceV2.js b/src/components/ChatBot/ChatInterfaceV2.js index d617d729..b9785e34 100644 --- a/src/components/ChatBot/ChatInterfaceV2.js +++ b/src/components/ChatBot/ChatInterfaceV2.js @@ -95,7 +95,7 @@ export const ChatInterfaceV2 = () => { }); }; - // 发送消息(Agent模式) + // 发送消息(Agent模式 - 流式) const handleSendMessage = async () => { if (!inputValue.trim() || isProcessing) return; @@ -106,10 +106,16 @@ export const ChatInterfaceV2 = () => { }; addMessage(userMessage); + const userInput = inputValue; // 保存输入值 setInputValue(''); setIsProcessing(true); setCurrentProgress(0); + // 用于存储步骤结果 + let currentPlan = null; + let stepResults = []; + let executingMessageId = null; + try { // 1. 显示思考状态 addMessage({ @@ -120,18 +126,40 @@ export const ChatInterfaceV2 = () => { setCurrentProgress(10); - // 调用 Agent API - const response = await fetch(`${mcpService.baseURL.replace('/mcp', '')}/mcp/agent/chat`, { + // 使用 EventSource 接收流式数据 + const eventSource = new EventSource( + `${mcpService.baseURL.replace('/mcp', '')}/mcp/agent/chat/stream`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: userInput, + conversation_history: messages + .filter(m => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) + .map(m => ({ + isUser: m.type === MessageTypes.USER, + content: m.content, + })), + }), + } + ); + + // 由于 EventSource 不支持 POST,我们使用 fetch + ReadableStream + const response = await fetch(`${mcpService.baseURL.replace('/mcp', '')}/mcp/agent/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - message: inputValue, - conversation_history: messages.filter(m => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE).map(m => ({ - isUser: m.type === MessageTypes.USER, - content: m.content, - })), + message: userInput, + conversation_history: messages + .filter(m => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) + .map(m => ({ + isUser: m.type === MessageTypes.USER, + content: m.content, + })), }), }); @@ -139,62 +167,139 @@ export const ChatInterfaceV2 = () => { throw new Error('Agent请求失败'); } - const agentResponse = await response.json(); - logger.info('Agent response', agentResponse); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; - // 移除思考消息 - setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_THINKING)); + // 读取流式数据 + while (true) { + const { done, value } = await reader.read(); + if (done) break; - if (!agentResponse.success) { - throw new Error(agentResponse.message || '处理失败'); - } + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n\n'); + buffer = lines.pop(); // 保留不完整的行 - setCurrentProgress(30); + for (const line of lines) { + if (!line.trim()) continue; - // 2. 显示执行计划 - if (agentResponse.plan) { - addMessage({ - type: MessageTypes.AGENT_PLAN, - content: '已制定执行计划', - plan: agentResponse.plan, - timestamp: new Date().toISOString(), - }); - } + // 解析 SSE 消息 + const eventMatch = line.match(/^event: (.+)$/m); + const dataMatch = line.match(/^data: (.+)$/m); - setCurrentProgress(40); + if (!eventMatch || !dataMatch) continue; - // 3. 显示执行过程 - if (agentResponse.step_results && agentResponse.step_results.length > 0) { - addMessage({ - type: MessageTypes.AGENT_EXECUTING, - content: '正在执行步骤...', - plan: agentResponse.plan, - stepResults: agentResponse.step_results, - timestamp: new Date().toISOString(), - }); + const event = eventMatch[1]; + const data = JSON.parse(dataMatch[1]); - // 模拟进度更新 - for (let i = 0; i < agentResponse.step_results.length; i++) { - setCurrentProgress(40 + (i + 1) / agentResponse.step_results.length * 50); - await new Promise(resolve => setTimeout(resolve, 100)); + logger.info(`SSE Event: ${event}`, data); + + // 处理不同类型的事件 + switch (event) { + case 'status': + if (data.stage === 'planning') { + // 移除思考消息,显示规划中 + setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_THINKING)); + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: data.message, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(20); + } else if (data.stage === 'executing') { + setCurrentProgress(30); + } else if (data.stage === 'summarizing') { + setCurrentProgress(90); + } + break; + + case 'plan': + // 移除思考消息 + setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_THINKING)); + + // 显示执行计划 + currentPlan = data; + addMessage({ + type: MessageTypes.AGENT_PLAN, + content: '已制定执行计划', + plan: data, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(30); + break; + + case 'step_start': + // 如果还没有执行中消息,创建一个 + if (!executingMessageId) { + const executingMsg = { + type: MessageTypes.AGENT_EXECUTING, + content: '正在执行步骤...', + plan: currentPlan, + stepResults: [], + timestamp: new Date().toISOString(), + }; + addMessage(executingMsg); + executingMessageId = Date.now(); + } + break; + + case 'step_complete': + // 添加步骤结果 + stepResults.push({ + step_index: data.step_index, + tool: data.tool, + status: data.status, + result: data.result, + error: data.error, + execution_time: data.execution_time, + arguments: data.arguments, + }); + + // 更新执行中消息 + setMessages(prev => + prev.map(msg => + msg.type === MessageTypes.AGENT_EXECUTING + ? { ...msg, stepResults: [...stepResults] } + : msg + ) + ); + + // 更新进度 + if (currentPlan) { + const progress = 30 + ((data.step_index + 1) / currentPlan.steps.length) * 60; + setCurrentProgress(progress); + } + break; + + case 'summary': + // 移除执行中消息 + setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_EXECUTING)); + + // 显示最终结果 + addMessage({ + type: MessageTypes.AGENT_RESPONSE, + content: data.content, + plan: currentPlan, + stepResults: stepResults, + metadata: data.metadata, + timestamp: new Date().toISOString(), + }); + setCurrentProgress(100); + break; + + case 'error': + throw new Error(data.message); + + case 'done': + logger.info('Stream完成'); + break; + + default: + logger.warn('未知事件类型:', event); + } } } - setCurrentProgress(100); - - // 移除执行中消息 - setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_EXECUTING)); - - // 4. 显示最终结果 - addMessage({ - type: MessageTypes.AGENT_RESPONSE, - content: agentResponse.message || agentResponse.final_summary, - plan: agentResponse.plan, - stepResults: agentResponse.step_results, - metadata: agentResponse.metadata, - timestamp: new Date().toISOString(), - }); - } catch (error) { logger.error('Agent chat error', error);