diff --git a/CLAUDE.md b/CLAUDE.md
index b1460323..5bda7fa7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2956,8 +2956,59 @@ refactor(components): 将 EventCard 拆分为原子组件
> **📝 页面级变更历史**: 特定页面的详细变更历史和技术文档已迁移到各自的文档中:
> - **Community 页面**: [docs/Community.md](./docs/Community.md) - 页面架构、组件结构、数据流、变更历史
+> - **Agent 系统**: [AGENT_INTEGRATION_COMPLETE.md](./AGENT_INTEGRATION_COMPLETE.md) - Agent 系统集成完整说明
> - **其他页面**: 根据需要创建独立的页面文档
+### 2025-11-07: Agent 系统集成到 mcp_server.py
+
+**影响范围**: 后端 MCP 服务器 + 前端 Agent 聊天功能
+
+**集成成果**:
+- 将独立的 Agent 系统完全集成到 `mcp_server.py` 中
+- 使用 **Kimi (kimi-k2-thinking)** 进行计划制定和推理
+- 使用 **DeepMoney (本地部署)** 进行新闻总结
+- 实现三阶段智能分析流程(计划→执行→总结)
+- 前端使用 ChatInterfaceV2 + 可折叠卡片展示执行过程
+- **无需运行多个脚本**,所有功能集成在单一服务中
+
+**技术要点**:
+- 新增 `MCPAgentIntegrated` 类(991-1367行)
+- 新增 `/agent/chat` API 端点
+- 新增特殊工具 `summarize_news`(使用 DeepMoney)
+- Kimi 使用 `reasoning_content` 字段记录思考过程
+- 自动替换占位符("前面的新闻数据" → 实际数据)
+
+**前端组件**:
+- `ChatInterfaceV2.js` - 新版聊天界面
+- `PlanCard.js` - 执行计划展示
+- `StepResultCard.js` - 步骤结果卡片(可折叠)
+- 路由:`/agent-chat`
+
+**详细文档**: 参见 [AGENT_INTEGRATION_COMPLETE.md](./AGENT_INTEGRATION_COMPLETE.md)
+
+### 2025-10-30: EventList.js 组件化重构
+
+**影响范围**: Community 页面核心组件
+
+**重构成果**:
+- 将 1095 行的 `EventList.js` 拆分为 497 行主组件 + 10 个子组件
+- 代码行数减少 **54.6%** (598 行)
+- 创建了 7 个原子组件 (Atoms) 和 2 个组合组件 (Molecules)
+
+**新增组件**:
+- `EventCard/` - 统一入口,智能路由紧凑/详细模式
+ - `CompactEventCard.js` - 紧凑模式事件卡片
+ - `DetailedEventCard.js` - 详细模式事件卡片
+ - 7 个原子组件: EventTimeline, EventImportanceBadge, EventStats, EventFollowButton, EventPriceDisplay, EventDescription, EventHeader
+
+**新增工具函数**:
+- `src/utils/priceFormatters.js` - 价格格式化工具 (getPriceChangeColor, formatPriceChange, PriceArrow)
+- `src/constants/animations.js` - 动画常量 (pulseAnimation, fadeIn, slideInUp)
+
+**优势**: 提高了代码可维护性、可复用性、可测试性和性能
+
+**详细文档**: 参见 [docs/Community.md](./docs/Community.md)
+
---
## 更新本文档
diff --git a/__pycache__/mcp_server.cpython-310.pyc b/__pycache__/mcp_server.cpython-310.pyc
new file mode 100644
index 00000000..0b667eb0
Binary files /dev/null and b/__pycache__/mcp_server.cpython-310.pyc differ
diff --git a/docs/AGENT_DEPLOYMENT.md b/docs/AGENT_DEPLOYMENT.md
new file mode 100644
index 00000000..c16ac10a
--- /dev/null
+++ b/docs/AGENT_DEPLOYMENT.md
@@ -0,0 +1,381 @@
+# AI Agent 系统部署指南
+
+## 🎯 系统架构
+
+### 三阶段流程
+
+```
+用户输入
+ ↓
+[阶段1: 计划制定 Planning]
+ - LLM 分析用户需求
+ - 确定需要哪些工具
+ - 制定执行计划(steps)
+ ↓
+[阶段2: 工具执行 Execution]
+ - 按计划顺序调用 MCP 工具
+ - 收集数据
+ - 异常处理和重试
+ ↓
+[阶段3: 结果总结 Summarization]
+ - LLM 综合分析所有数据
+ - 生成自然语言报告
+ ↓
+输出给用户
+```
+
+## 📦 文件清单
+
+### 后端文件
+
+```
+mcp_server.py # MCP 工具服务器(已有)
+mcp_agent_system.py # Agent 系统核心逻辑(新增)
+mcp_config.py # 配置文件(已有)
+mcp_database.py # 数据库操作(已有)
+```
+
+### 前端文件
+
+```
+src/components/ChatBot/
+├── ChatInterfaceV2.js # 新版聊天界面(漂亮)
+├── PlanCard.js # 执行计划卡片
+├── StepResultCard.js # 步骤结果卡片(可折叠)
+├── ChatInterface.js # 旧版聊天界面(保留)
+├── MessageBubble.js # 消息气泡组件(保留)
+└── index.js # 统一导出
+
+src/views/AgentChat/
+└── index.js # Agent 聊天页面
+```
+
+## 🚀 部署步骤
+
+### 1. 安装依赖
+
+```bash
+# 进入项目目录
+cd /home/ubuntu/vf_react
+
+# 安装 OpenAI SDK(支持多个LLM提供商)
+pip install openai
+```
+
+### 2. 获取 LLM API Key
+
+**推荐:通义千问(便宜且中文能力强)**
+
+1. 访问 https://dashscope.console.aliyun.com/
+2. 注册/登录阿里云账号
+3. 开通 DashScope 服务
+4. 创建 API Key
+5. 复制 API Key(格式:`sk-xxx...`)
+
+**其他选择**:
+- DeepSeek: https://platform.deepseek.com/ (最便宜)
+- OpenAI: https://platform.openai.com/ (需要翻墙)
+
+### 3. 配置环境变量
+
+```bash
+# 编辑环境变量
+sudo nano /etc/environment
+
+# 添加以下内容(选择一个)
+# 方式1: 通义千问(推荐)
+DASHSCOPE_API_KEY="sk-your-key-here"
+
+# 方式2: DeepSeek(更便宜)
+DEEPSEEK_API_KEY="sk-your-key-here"
+
+# 方式3: OpenAI
+OPENAI_API_KEY="sk-your-key-here"
+
+# 保存并退出,然后重新加载
+source /etc/environment
+
+# 验证环境变量
+echo $DASHSCOPE_API_KEY
+```
+
+### 4. 修改 mcp_server.py
+
+在文件末尾(`if __name__ == "__main__":` 之前)添加:
+
+```python
+# ==================== Agent 端点 ====================
+
+from mcp_agent_system import MCPAgent, ChatRequest, AgentResponse
+
+# 创建 Agent 实例
+agent = MCPAgent(provider="qwen") # 或 "deepseek", "openai"
+
+@app.post("/agent/chat", response_model=AgentResponse)
+async def agent_chat(request: ChatRequest):
+ """智能代理对话端点"""
+ logger.info(f"Agent chat: {request.message}")
+
+ # 获取工具列表和处理器
+ tools = [tool.dict() for tool in TOOLS]
+
+ # 处理查询
+ response = await agent.process_query(
+ user_query=request.message,
+ tools=tools,
+ tool_handlers=TOOL_HANDLERS,
+ )
+
+ return response
+```
+
+### 5. 重启 MCP 服务
+
+```bash
+# 如果使用 systemd
+sudo systemctl restart mcp-server
+
+# 或者手动重启
+pkill -f mcp_server
+nohup uvicorn mcp_server:app --host 0.0.0.0 --port 8900 > mcp_server.log 2>&1 &
+
+# 查看日志
+tail -f mcp_server.log
+```
+
+### 6. 测试 Agent API
+
+```bash
+# 测试 Agent 端点
+curl -X POST http://localhost:8900/agent/chat \
+ -H "Content-Type: application/json" \
+ -d '{
+ "message": "全面分析贵州茅台这只股票",
+ "conversation_history": []
+ }'
+
+# 应该返回类似这样的JSON:
+# {
+# "success": true,
+# "message": "根据分析,贵州茅台...",
+# "plan": {
+# "goal": "全面分析贵州茅台",
+# "steps": [...]
+# },
+# "step_results": [...],
+# "metadata": {...}
+# }
+```
+
+### 7. 部署前端
+
+```bash
+# 在本地构建
+npm run build
+
+# 上传到服务器
+scp -r build/* ubuntu@your-server:/var/www/valuefrontier.cn/
+
+# 或者在服务器上构建
+cd /home/ubuntu/vf_react
+npm run build
+sudo cp -r build/* /var/www/valuefrontier.cn/
+```
+
+### 8. 重启 Nginx
+
+```bash
+sudo systemctl reload nginx
+```
+
+## ✅ 验证部署
+
+### 1. 测试后端 API
+
+```bash
+# 测试工具列表
+curl https://valuefrontier.cn/mcp/tools
+
+# 测试 Agent
+curl -X POST https://valuefrontier.cn/mcp/agent/chat \
+ -H "Content-Type: application/json" \
+ -d '{
+ "message": "今日涨停股票有哪些",
+ "conversation_history": []
+ }'
+```
+
+### 2. 测试前端
+
+1. 访问 https://valuefrontier.cn/agent-chat
+2. 输入问题:"全面分析贵州茅台这只股票"
+3. 观察:
+ - ✓ 是否显示执行计划卡片
+ - ✓ 是否显示步骤执行过程
+ - ✓ 是否显示最终总结
+ - ✓ 步骤结果卡片是否可折叠
+
+### 3. 测试用例
+
+```
+测试1: 简单查询
+输入:查询贵州茅台的股票信息
+预期:调用 get_stock_basic_info,返回基本信息
+
+测试2: 深度分析(推荐)
+输入:全面分析贵州茅台这只股票
+预期:
+ - 步骤1: get_stock_basic_info
+ - 步骤2: get_stock_financial_index
+ - 步骤3: get_stock_trade_data
+ - 步骤4: search_china_news
+ - 步骤5: summarize_with_llm
+
+测试3: 市场热点
+输入:今日涨停股票有哪些亮点
+预期:
+ - 步骤1: search_limit_up_stocks
+ - 步骤2: get_concept_statistics
+ - 步骤3: summarize_with_llm
+
+测试4: 概念分析
+输入:新能源概念板块的投资机会
+预期:
+ - 步骤1: search_concepts(新能源)
+ - 步骤2: search_china_news(新能源)
+ - 步骤3: summarize_with_llm
+```
+
+## 🐛 故障排查
+
+### 问题1: Agent 返回 "Provider not configured"
+
+**原因**: 环境变量未设置
+
+**解决**:
+```bash
+# 检查环境变量
+echo $DASHSCOPE_API_KEY
+
+# 如果为空,重新设置
+export DASHSCOPE_API_KEY="sk-xxx..."
+
+# 重启服务
+sudo systemctl restart mcp-server
+```
+
+### 问题2: Agent 返回 JSON 解析错误
+
+**原因**: LLM 没有返回正确的 JSON 格式
+
+**解决**: 在 `mcp_agent_system.py` 中已经处理了代码块标记清理,如果还有问题:
+1. 检查 LLM 的 temperature 参数(建议 0.3)
+2. 检查 prompt 是否清晰
+3. 尝试不同的 LLM 提供商
+
+### 问题3: 前端显示 "查询失败"
+
+**原因**: 后端 API 未正确配置或 Nginx 代理问题
+
+**解决**:
+```bash
+# 1. 检查 MCP 服务是否运行
+ps aux | grep mcp_server
+
+# 2. 检查 Nginx 配置
+sudo nginx -t
+
+# 3. 查看错误日志
+sudo tail -f /var/log/nginx/error.log
+tail -f /home/ubuntu/vf_react/mcp_server.log
+```
+
+### 问题4: 执行步骤失败
+
+**原因**: 某个 MCP 工具调用失败
+
+**解决**: 查看步骤结果卡片中的错误信息,通常是:
+- API 超时:增加 timeout
+- 参数错误:检查工具定义
+- 数据库连接失败:检查数据库连接
+
+## 💰 成本估算
+
+### 使用通义千问(qwen-plus)
+
+**价格**: ¥0.004/1000 tokens
+
+**典型对话消耗**:
+- 简单查询(1步): ~500 tokens = ¥0.002
+- 深度分析(5步): ~3000 tokens = ¥0.012
+- 平均每次对话: ¥0.005
+
+**月度成本**(1000次深度分析):
+- 1000次 × ¥0.012 = ¥12
+
+**结论**: 非常便宜!1000次深度分析只需要12元。
+
+### 使用 DeepSeek(更便宜)
+
+**价格**: ¥0.001/1000 tokens(比通义千问便宜4倍)
+
+**月度成本**(1000次深度分析):
+- 1000次 × ¥0.003 = ¥3
+
+## 📊 监控和优化
+
+### 1. 添加日志监控
+
+```bash
+# 实时查看 Agent 日志
+tail -f mcp_server.log | grep -E "\[Agent\]|\[Planning\]|\[Execution\]|\[Summary\]"
+```
+
+### 2. 性能优化建议
+
+1. **缓存计划**: 相似的问题可以复用执行计划
+2. **并行执行**: 独立的工具调用可以并行执行
+3. **流式输出**: 使用 Server-Sent Events 实时返回进度
+4. **结果缓存**: 相同的工具调用结果可以缓存
+
+### 3. 添加统计分析
+
+在 `mcp_server.py` 中添加:
+
+```python
+from datetime import datetime
+import json
+
+# 记录每次 Agent 调用
+@app.post("/agent/chat")
+async def agent_chat(request: ChatRequest):
+ start_time = datetime.now()
+
+ response = await agent.process_query(...)
+
+ duration = (datetime.now() - start_time).total_seconds()
+
+ # 记录到日志
+ logger.info(f"Agent query completed in {duration:.2f}s", extra={
+ "query": request.message,
+ "steps": len(response.plan.steps) if response.plan else 0,
+ "success": response.success,
+ "duration": duration,
+ })
+
+ return response
+```
+
+## 🎉 完成!
+
+现在你的 AI Agent 系统已经部署完成!
+
+访问 https://valuefrontier.cn/agent-chat 开始使用。
+
+**特点**:
+- ✅ 三阶段智能分析(计划-执行-总结)
+- ✅ 漂亮的UI界面(卡片式展示)
+- ✅ 步骤结果可折叠查看
+- ✅ 实时进度反馈
+- ✅ 异常处理和重试
+- ✅ 成本低廉(¥3-12/月)
diff --git a/docs/MCP_ARCHITECTURE.md b/docs/MCP_ARCHITECTURE.md
new file mode 100644
index 00000000..23ea3b6b
--- /dev/null
+++ b/docs/MCP_ARCHITECTURE.md
@@ -0,0 +1,309 @@
+# MCP 架构说明
+
+## 🎯 MCP 是什么?
+
+**MCP (Model Context Protocol)** 是一个**工具调用协议**,它的核心职责是:
+
+1. ✅ **定义工具接口**:告诉 LLM 有哪些工具可用,每个工具需要什么参数
+2. ✅ **执行工具调用**:根据请求调用对应的后端 API
+3. ✅ **返回结构化数据**:将 API 结果返回给调用方
+
+**MCP 不负责**:
+- ❌ 自然语言理解(NLU)
+- ❌ 意图识别
+- ❌ 结果总结
+- ❌ 对话管理
+
+## 📊 当前架构
+
+### 方案 1:简单关键词匹配(已实现)
+
+```
+用户输入:"查询贵州茅台的股票信息"
+ ↓
+前端 ChatInterface (关键词匹配)
+ ↓
+MCP 工具层 (search_china_news)
+ ↓
+返回 JSON 数据
+ ↓
+前端显示原始数据
+```
+
+**问题**:
+- ✗ 只能识别简单关键词
+- ✗ 无法理解复杂意图
+- ✗ 返回的是原始 JSON,用户体验差
+
+### 方案 2:集成 LLM(推荐)
+
+```
+用户输入:"查询贵州茅台的股票信息"
+ ↓
+LLM (Claude/GPT-4/通义千问)
+ ↓ 理解意图:需要查询股票代码 600519 的基本信息
+ ↓ 选择工具:get_stock_basic_info
+ ↓ 提取参数:{"seccode": "600519"}
+MCP 工具层
+ ↓ 调用 API,获取数据
+返回结构化数据
+ ↓
+LLM 总结结果
+ ↓ "贵州茅台(600519)是中国知名的白酒生产企业,
+ 当前股价 1650.00 元,市值 2.07 万亿..."
+前端显示自然语言回复
+```
+
+**优势**:
+- ✓ 理解复杂意图
+- ✓ 自动选择合适的工具
+- ✓ 自然语言总结,用户体验好
+- ✓ 支持多轮对话
+
+## 🔧 实现方案
+
+### 选项 A:前端集成 LLM(快速实现)
+
+**适用场景**:快速原型、小规模应用
+
+**优点**:
+- 实现简单
+- 无需修改后端
+
+**缺点**:
+- API Key 暴露在前端(安全风险)
+- 每个用户都消耗 API 额度
+- 无法统一管理和监控
+
+**实现步骤**:
+
+1. 修改 `src/components/ChatBot/ChatInterface.js`:
+
+```javascript
+import { llmService } from '../../services/llmService';
+
+const handleSendMessage = async () => {
+ // ...
+
+ // 使用 LLM 服务替代简单的 mcpService.chat
+ const response = await llmService.chat(inputValue, messages);
+
+ // ...
+};
+```
+
+2. 配置 API Key(在 `.env.local`):
+
+```bash
+REACT_APP_OPENAI_API_KEY=sk-xxx...
+# 或者使用通义千问(更便宜)
+REACT_APP_DASHSCOPE_API_KEY=sk-xxx...
+```
+
+### 选项 B:后端集成 LLM(生产推荐)⭐
+
+**适用场景**:生产环境、需要安全和性能
+
+**优点**:
+- ✓ API Key 安全(不暴露给前端)
+- ✓ 统一管理和监控
+- ✓ 可以做缓存优化
+- ✓ 可以做速率限制
+
+**缺点**:
+- 需要修改后端
+- 增加服务器成本
+
+**实现步骤**:
+
+#### 1. 安装依赖
+
+```bash
+pip install openai
+```
+
+#### 2. 修改 `mcp_server.py`,添加聊天端点
+
+在文件末尾添加:
+
+```python
+from mcp_chat_endpoint import MCPChatAssistant, ChatRequest, ChatResponse
+
+# 创建聊天助手实例
+chat_assistant = MCPChatAssistant(provider="qwen") # 推荐使用通义千问
+
+@app.post("/chat", response_model=ChatResponse)
+async def chat_endpoint(request: ChatRequest):
+ """智能对话端点 - 使用LLM理解意图并调用工具"""
+ logger.info(f"Chat request: {request.message}")
+
+ # 获取可用工具列表
+ tools = [tool.dict() for tool in TOOLS]
+
+ # 调用聊天助手
+ response = await chat_assistant.chat(
+ user_message=request.message,
+ conversation_history=request.conversation_history,
+ tools=tools,
+ )
+
+ return response
+```
+
+#### 3. 配置环境变量
+
+在服务器上设置:
+
+```bash
+# 方式1:使用通义千问(推荐,价格便宜)
+export DASHSCOPE_API_KEY="sk-xxx..."
+
+# 方式2:使用 OpenAI
+export OPENAI_API_KEY="sk-xxx..."
+
+# 方式3:使用 DeepSeek(最便宜)
+export DEEPSEEK_API_KEY="sk-xxx..."
+```
+
+#### 4. 修改前端 `mcpService.js`
+
+```javascript
+/**
+ * 智能对话 - 使用后端LLM处理
+ */
+async chat(userMessage, conversationHistory = []) {
+ try {
+ const response = await this.client.post('/chat', {
+ message: userMessage,
+ conversation_history: conversationHistory,
+ });
+
+ return {
+ success: true,
+ data: response,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message || '对话处理失败',
+ };
+ }
+}
+```
+
+#### 5. 修改前端 `ChatInterface.js`
+
+```javascript
+const handleSendMessage = async () => {
+ // ...
+
+ try {
+ // 调用后端聊天API
+ const response = await mcpService.chat(inputValue, messages);
+
+ if (response.success) {
+ const botMessage = {
+ id: Date.now() + 1,
+ content: response.data.message, // LLM总结的自然语言
+ isUser: false,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ toolUsed: response.data.tool_used, // 可选:显示使用了哪个工具
+ rawData: response.data.raw_data, // 可选:原始数据(折叠显示)
+ };
+ setMessages((prev) => [...prev, botMessage]);
+ }
+ } catch (error) {
+ // ...
+ }
+};
+```
+
+## 💰 LLM 选择和成本
+
+### 推荐:通义千问(阿里云)
+
+**优点**:
+- 价格便宜(1000次对话约 ¥1-2)
+- 中文理解能力强
+- 国内访问稳定
+
+**价格**:
+- qwen-plus: ¥0.004/1000 tokens(约 ¥0.001/次对话)
+- qwen-turbo: ¥0.002/1000 tokens(更便宜)
+
+**获取 API Key**:
+1. 访问 https://dashscope.console.aliyun.com/
+2. 创建 API Key
+3. 设置环境变量 `DASHSCOPE_API_KEY`
+
+### 其他选择
+
+| 提供商 | 模型 | 价格 | 优点 | 缺点 |
+|--------|------|------|------|------|
+| **通义千问** | qwen-plus | ¥0.001/次 | 便宜、中文好 | - |
+| **DeepSeek** | deepseek-chat | ¥0.0005/次 | 最便宜 | 新公司 |
+| **OpenAI** | gpt-4o-mini | $0.15/1M tokens | 能力强 | 贵、需翻墙 |
+| **Claude** | claude-3-haiku | $0.25/1M tokens | 理解力强 | 贵、需翻墙 |
+
+## 🚀 部署步骤
+
+### 1. 后端部署
+
+```bash
+# 安装依赖
+pip install openai
+
+# 设置 API Key
+export DASHSCOPE_API_KEY="sk-xxx..."
+
+# 重启服务
+sudo systemctl restart mcp-server
+
+# 测试聊天端点
+curl -X POST https://valuefrontier.cn/mcp/chat \
+ -H "Content-Type: application/json" \
+ -d '{"message": "查询贵州茅台的股票信息"}'
+```
+
+### 2. 前端部署
+
+```bash
+# 构建
+npm run build
+
+# 部署
+scp -r build/* user@server:/var/www/valuefrontier.cn/
+```
+
+### 3. 验证
+
+访问 https://valuefrontier.cn/agent-chat,测试对话:
+
+**测试用例**:
+1. "查询贵州茅台的股票信息" → 应返回自然语言总结
+2. "今日涨停的股票有哪些" → 应返回涨停股票列表并总结
+3. "新能源概念板块表现如何" → 应搜索概念并分析
+
+## 📊 对比总结
+
+| 特性 | 简单匹配 | 前端LLM | 后端LLM ⭐ |
+|------|---------|---------|-----------|
+| 实现难度 | 简单 | 中等 | 中等 |
+| 用户体验 | 差 | 好 | 好 |
+| 安全性 | 高 | 低 | 高 |
+| 成本 | 无 | 用户承担 | 服务器承担 |
+| 可维护性 | 差 | 中 | 好 |
+| **推荐指数** | ⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
+
+## 🎯 最终推荐
+
+**生产环境:后端集成 LLM (方案 B)**
+- 使用通义千问(qwen-plus)
+- 成本低(约 ¥50/月,10000次对话)
+- 安全可靠
+
+**快速原型:前端集成 LLM (方案 A)**
+- 适合演示
+- 快速验证可行性
+- 后续再迁移到后端
diff --git a/kimi_integration.py b/kimi_integration.py
new file mode 100644
index 00000000..80969666
--- /dev/null
+++ b/kimi_integration.py
@@ -0,0 +1,361 @@
+"""
+Kimi API 集成示例
+演示如何将MCP工具与Kimi大模型结合使用
+"""
+
+from openai import OpenAI
+import json
+from typing import List, Dict, Any
+from mcp_client_example import MCPClient
+
+# Kimi API配置
+KIMI_API_KEY = "sk-TzB4VYJfCoXGcGrGMiewukVRzjuDsbVCkaZXi2LvkS8s60E5"
+KIMI_BASE_URL = "https://api.moonshot.cn/v1"
+KIMI_MODEL = "kimi-k2-turbpreview"
+
+# 初始化Kimi客户端
+kimi_client = OpenAI(
+ api_key=KIMI_API_KEY,
+ base_url=KIMI_BASE_URL,
+)
+
+# 初始化MCP客户端
+mcp_client = MCPClient()
+
+
+def convert_mcp_tools_to_kimi_format() -> tuple[List[Dict], Dict]:
+ """
+ 将MCP工具转换为Kimi API的tools格式
+
+ Returns:
+ tools: Kimi格式的工具列表
+ tool_map: 工具名称到执行函数的映射
+ """
+ # 获取所有MCP工具
+ mcp_tools_response = mcp_client.list_tools()
+ mcp_tools = mcp_tools_response["tools"]
+
+ # 转换为Kimi格式
+ kimi_tools = []
+ tool_map = {}
+
+ for tool in mcp_tools:
+ # Kimi工具格式
+ kimi_tool = {
+ "type": "function",
+ "function": {
+ "name": tool["name"],
+ "description": tool["description"],
+ "parameters": tool["parameters"]
+ }
+ }
+ kimi_tools.append(kimi_tool)
+
+ # 创建工具执行函数
+ tool_name = tool["name"]
+ tool_map[tool_name] = lambda args, name=tool_name: execute_mcp_tool(name, args)
+
+ return kimi_tools, tool_map
+
+
+def execute_mcp_tool(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ 执行MCP工具
+
+ Args:
+ tool_name: 工具名称
+ arguments: 工具参数
+
+ Returns:
+ 工具执行结果
+ """
+ print(f"[工具调用] {tool_name}")
+ print(f"[参数] {json.dumps(arguments, ensure_ascii=False, indent=2)}")
+
+ result = mcp_client.call_tool(tool_name, arguments)
+
+ print(f"[结果] 成功: {result.get('success', False)}")
+
+ return result
+
+
+def chat_with_kimi(user_message: str, verbose: bool = True) -> str:
+ """
+ 与Kimi进行对话,支持工具调用
+
+ Args:
+ user_message: 用户消息
+ verbose: 是否打印详细信息
+
+ Returns:
+ Kimi的回复
+ """
+ # 获取Kimi格式的工具
+ tools, tool_map = convert_mcp_tools_to_kimi_format()
+
+ if verbose:
+ print(f"\n{'='*60}")
+ print(f"加载了 {len(tools)} 个工具")
+ print(f"{'='*60}\n")
+
+ # 初始化对话
+ messages = [
+ {
+ "role": "system",
+ "content": """你是一个专业的金融数据分析助手,由 Moonshot AI 提供支持。
+你可以使用各种工具来帮助用户查询和分析金融数据,包括:
+- 新闻搜索(全球新闻、中国新闻、医疗新闻)
+- 公司研究(路演信息、研究报告)
+- 概念板块分析
+- 股票分析(涨停分析、财务数据、交易数据)
+- 财务报表(资产负债表、现金流量表)
+
+请根据用户的问题,选择合适的工具来获取信息,并提供专业的分析和建议。"""
+ },
+ {
+ "role": "user",
+ "content": user_message
+ }
+ ]
+
+ if verbose:
+ print(f"[用户]: {user_message}\n")
+
+ # 对话循环,处理工具调用
+ finish_reason = None
+ iteration = 0
+ max_iterations = 10 # 防止无限循环
+
+ while finish_reason is None or finish_reason == "tool_calls":
+ iteration += 1
+ if iteration > max_iterations:
+ print("[警告] 达到最大迭代次数")
+ break
+
+ if verbose and iteration > 1:
+ print(f"\n[轮次 {iteration}]")
+
+ # 调用Kimi API
+ completion = kimi_client.chat.completions.create(
+ model=KIMI_MODEL,
+ messages=messages,
+ temperature=0.6, # Kimi推荐的temperature值
+ tools=tools,
+ )
+
+ choice = completion.choices[0]
+ finish_reason = choice.finish_reason
+
+ if verbose:
+ print(f"[Kimi] finish_reason: {finish_reason}")
+
+ # 处理工具调用
+ if finish_reason == "tool_calls":
+ # 将Kimi的消息添加到上下文
+ messages.append(choice.message)
+
+ # 执行每个工具调用
+ for tool_call in choice.message.tool_calls:
+ tool_name = tool_call.function.name
+ tool_arguments = json.loads(tool_call.function.arguments)
+
+ # 执行工具
+ tool_result = tool_map[tool_name](tool_arguments)
+
+ # 将工具结果添加到消息中
+ messages.append({
+ "role": "tool",
+ "tool_call_id": tool_call.id,
+ "name": tool_name,
+ "content": json.dumps(tool_result, ensure_ascii=False),
+ })
+
+ if verbose:
+ print() # 空行分隔
+
+ # 返回最终回复
+ final_response = choice.message.content
+
+ if verbose:
+ print(f"\n[Kimi]: {final_response}\n")
+ print(f"{'='*60}")
+
+ return final_response
+
+
+def demo_simple_query():
+ """演示1: 简单查询"""
+ print("\n" + "="*60)
+ print("演示1: 简单新闻查询")
+ print("="*60)
+
+ response = chat_with_kimi("帮我查找关于人工智能的最新新闻")
+ return response
+
+
+def demo_stock_analysis():
+ """演示2: 股票分析"""
+ print("\n" + "="*60)
+ print("演示2: 股票财务分析")
+ print("="*60)
+
+ response = chat_with_kimi("帮我分析贵州茅台(600519)的财务状况")
+ return response
+
+
+def demo_concept_research():
+ """演示3: 概念研究"""
+ print("\n" + "="*60)
+ print("演示3: 概念板块研究")
+ print("="*60)
+
+ response = chat_with_kimi("查找新能源汽车相关的概念板块,并告诉我涨幅最高的是哪些")
+ return response
+
+
+def demo_industry_comparison():
+ """演示4: 行业对比"""
+ print("\n" + "="*60)
+ print("演示4: 行业内股票对比")
+ print("="*60)
+
+ response = chat_with_kimi("帮我找出半导体行业的龙头股票,并对比它们的财务指标")
+ return response
+
+
+def demo_comprehensive_analysis():
+ """演示5: 综合分析"""
+ print("\n" + "="*60)
+ print("演示5: 综合分析")
+ print("="*60)
+
+ response = chat_with_kimi("""
+ 我想投资白酒行业,请帮我:
+ 1. 搜索白酒行业的主要上市公司
+ 2. 对比贵州茅台和五粮液的财务数据
+ 3. 查看最近的行业新闻
+ 4. 给出投资建议
+ """)
+ return response
+
+
+def interactive_chat():
+ """交互式对话"""
+ print("\n" + "="*60)
+ print("Kimi 金融助手 - 交互模式")
+ print("="*60)
+ print("提示:输入 'quit' 或 'exit' 退出")
+ print("="*60 + "\n")
+
+ while True:
+ try:
+ user_input = input("你: ").strip()
+
+ if not user_input:
+ continue
+
+ if user_input.lower() in ['quit', 'exit', '退出']:
+ print("\n再见!")
+ break
+
+ response = chat_with_kimi(user_input)
+
+ except KeyboardInterrupt:
+ print("\n\n再见!")
+ break
+ except Exception as e:
+ print(f"\n[错误] {str(e)}\n")
+
+
+def test_kimi_connection():
+ """测试Kimi API连接"""
+ print("\n" + "="*60)
+ print("测试 Kimi API 连接")
+ print("="*60 + "\n")
+
+ try:
+ # 简单的测试请求
+ response = kimi_client.chat.completions.create(
+ model=KIMI_MODEL,
+ messages=[
+ {"role": "user", "content": "你好,请介绍一下你自己"}
+ ],
+ temperature=0.6
+ )
+
+ print("[✓] 连接成功!")
+ print(f"[✓] 模型: {KIMI_MODEL}")
+ print(f"[✓] 回复: {response.choices[0].message.content}\n")
+
+ return True
+ except Exception as e:
+ print(f"[✗] 连接失败: {str(e)}\n")
+ return False
+
+
+def show_available_tools():
+ """显示所有可用工具"""
+ print("\n" + "="*60)
+ print("可用工具列表")
+ print("="*60 + "\n")
+
+ tools, _ = convert_mcp_tools_to_kimi_format()
+
+ for i, tool in enumerate(tools, 1):
+ func = tool["function"]
+ print(f"{i}. {func['name']}")
+ print(f" 描述: {func['description'][:80]}...")
+ print()
+
+ print(f"总计: {len(tools)} 个工具\n")
+
+
+if __name__ == "__main__":
+ import sys
+
+ # 首先测试连接
+ if not test_kimi_connection():
+ print("请检查API Key和网络连接")
+ sys.exit(1)
+
+ # 显示可用工具
+ show_available_tools()
+
+ # 运行演示
+ print("\n选择运行模式:")
+ print("1. 简单查询演示")
+ print("2. 股票分析演示")
+ print("3. 概念研究演示")
+ print("4. 行业对比演示")
+ print("5. 综合分析演示")
+ print("6. 交互式对话")
+ print("7. 运行所有演示")
+
+ try:
+ choice = input("\n请选择 (1-7): ").strip()
+
+ if choice == "1":
+ demo_simple_query()
+ elif choice == "2":
+ demo_stock_analysis()
+ elif choice == "3":
+ demo_concept_research()
+ elif choice == "4":
+ demo_industry_comparison()
+ elif choice == "5":
+ demo_comprehensive_analysis()
+ elif choice == "6":
+ interactive_chat()
+ elif choice == "7":
+ demo_simple_query()
+ demo_stock_analysis()
+ demo_concept_research()
+ demo_industry_comparison()
+ demo_comprehensive_analysis()
+ else:
+ print("无效选择")
+
+ except KeyboardInterrupt:
+ print("\n\n程序已退出")
+ finally:
+ mcp_client.close()
diff --git a/mcp_agent_system.py b/mcp_agent_system.py
new file mode 100644
index 00000000..2dfa836d
--- /dev/null
+++ b/mcp_agent_system.py
@@ -0,0 +1,470 @@
+"""
+MCP Agent System - 基于 DeepResearch 逻辑的智能代理系统
+三阶段流程:计划制定 → 工具执行 → 结果总结
+"""
+
+from pydantic import BaseModel
+from typing import List, Dict, Any, Optional, Literal
+from datetime import datetime
+import json
+import logging
+from openai import OpenAI
+import asyncio
+import os
+
+logger = logging.getLogger(__name__)
+
+# ==================== 数据模型 ====================
+
+class ToolCall(BaseModel):
+ """工具调用"""
+ tool: str
+ arguments: Dict[str, Any]
+ reason: str # 为什么要调用这个工具
+
+class ExecutionPlan(BaseModel):
+ """执行计划"""
+ goal: str # 用户的目标
+ steps: List[ToolCall] # 执行步骤
+ reasoning: str # 规划reasoning
+
+class StepResult(BaseModel):
+ """单步执行结果"""
+ step_index: int
+ tool: str
+ arguments: Dict[str, Any]
+ status: Literal["success", "failed", "skipped"]
+ result: Optional[Any] = None
+ error: Optional[str] = None
+ execution_time: float = 0
+
+class AgentResponse(BaseModel):
+ """Agent响应"""
+ success: bool
+ message: str # 自然语言总结
+ plan: Optional[ExecutionPlan] = None # 执行计划
+ step_results: List[StepResult] = [] # 每步的结果
+ final_summary: Optional[str] = None # 最终总结
+ metadata: Optional[Dict[str, Any]] = None
+
+class ChatRequest(BaseModel):
+ """聊天请求"""
+ message: str
+ conversation_history: List[Dict[str, str]] = []
+ stream: bool = False # 是否流式输出
+
+# ==================== Agent 系统 ====================
+
+class MCPAgent:
+ """MCP 智能代理 - 三阶段执行"""
+
+ def __init__(self, provider: str = "qwen"):
+ self.provider = provider
+
+ # LLM 配置
+ config = {
+ "qwen": {
+ "api_key": os.getenv("DASHSCOPE_API_KEY", ""),
+ "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ "model": "qwen-plus",
+ },
+ "deepseek": {
+ "api_key": os.getenv("DEEPSEEK_API_KEY", ""),
+ "base_url": "https://api.deepseek.com/v1",
+ "model": "deepseek-chat",
+ },
+ "openai": {
+ "api_key": os.getenv("OPENAI_API_KEY", ""),
+ "base_url": "https://api.openai.com/v1",
+ "model": "gpt-4o-mini",
+ },
+ }.get(provider)
+
+ if not config or not config["api_key"]:
+ raise ValueError(f"Provider '{provider}' not configured. Please set API key.")
+
+ self.client = OpenAI(
+ api_key=config["api_key"],
+ base_url=config["base_url"],
+ )
+ self.model = config["model"]
+
+ # ==================== 阶段 1: 计划制定 ====================
+
+ def get_planning_prompt(self, tools: List[dict]) -> str:
+ """获取计划制定的系统提示词"""
+ tools_desc = "\n\n".join([
+ f"**{tool['name']}**\n"
+ f"描述:{tool['description']}\n"
+ f"参数:{json.dumps(tool['parameters'], ensure_ascii=False, indent=2)}"
+ for tool in tools
+ ])
+
+ return f"""你是一个专业的金融研究助手。你需要根据用户的问题,制定一个详细的执行计划。
+
+## 可用工具
+
+{tools_desc}
+
+## 重要知识
+- 贵州茅台股票代码: 600519
+- 涨停: 股价单日涨幅约10%
+- 概念板块: 相同题材的股票分类
+
+## 特殊工具说明
+- **summarize_with_llm**: 这是一个特殊工具,用于让你总结和分析收集到的数据
+ - 当需要对多个数据源进行综合分析时使用
+ - 当需要生成研究报告时使用
+ - 参数: {{"data": "要分析的数据", "task": "分析任务描述"}}
+
+## 任务
+分析用户问题,制定执行计划。返回 JSON 格式:
+
+```json
+{{
+ "goal": "用户的目标(一句话概括)",
+ "reasoning": "你的分析思路(为什么这样规划)",
+ "steps": [
+ {{
+ "tool": "工具名称",
+ "arguments": {{"参数名": "参数值"}},
+ "reason": "为什么要执行这一步"
+ }}
+ ]
+}}
+```
+
+## 规划原则
+1. **从简到繁**: 先获取基础信息,再深入分析
+2. **数据先行**: 先收集数据,再总结分析
+3. **合理组合**: 可以调用多个工具,但不要超过5个
+4. **包含总结**: 最后一步通常是 summarize_with_llm
+
+## 示例
+
+用户:"帮我全面分析一下贵州茅台这只股票"
+
+你的计划:
+```json
+{{
+ "goal": "全面分析贵州茅台股票",
+ "reasoning": "需要获取基本信息、财务指标、交易数据,然后综合分析",
+ "steps": [
+ {{
+ "tool": "get_stock_basic_info",
+ "arguments": {{"seccode": "600519"}},
+ "reason": "获取股票基本信息(公司名称、行业、市值等)"
+ }},
+ {{
+ "tool": "get_stock_financial_index",
+ "arguments": {{"seccode": "600519", "limit": 5}},
+ "reason": "获取最近5期财务指标(营收、利润、ROE等)"
+ }},
+ {{
+ "tool": "get_stock_trade_data",
+ "arguments": {{"seccode": "600519", "limit": 30}},
+ "reason": "获取最近30天交易数据(价格走势、成交量)"
+ }},
+ {{
+ "tool": "search_china_news",
+ "arguments": {{"query": "贵州茅台", "top_k": 5}},
+ "reason": "获取最新新闻,了解市场动态"
+ }},
+ {{
+ "tool": "summarize_with_llm",
+ "arguments": {{
+ "data": "前面收集的所有数据",
+ "task": "综合分析贵州茅台的投资价值,包括基本面、财务状况、股价走势、市场情绪"
+ }},
+ "reason": "综合所有数据,生成投资分析报告"
+ }}
+ ]
+}}
+```
+
+只返回JSON,不要额外解释。"""
+
+ async def create_plan(self, user_query: str, tools: List[dict]) -> ExecutionPlan:
+ """阶段1: 创建执行计划"""
+ logger.info(f"[Planning] Creating plan for: {user_query}")
+
+ messages = [
+ {"role": "system", "content": self.get_planning_prompt(tools)},
+ {"role": "user", "content": user_query},
+ ]
+
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=0.3,
+ max_tokens=1500,
+ )
+
+ plan_json = response.choices[0].message.content.strip()
+ logger.info(f"[Planning] Raw response: {plan_json}")
+
+ # 清理可能的代码块标记
+ if "```json" in plan_json:
+ plan_json = plan_json.split("```json")[1].split("```")[0].strip()
+ elif "```" in plan_json:
+ plan_json = plan_json.split("```")[1].split("```")[0].strip()
+
+ plan_data = json.loads(plan_json)
+
+ plan = ExecutionPlan(
+ goal=plan_data["goal"],
+ reasoning=plan_data.get("reasoning", ""),
+ steps=[
+ ToolCall(**step) for step in plan_data["steps"]
+ ],
+ )
+
+ logger.info(f"[Planning] Plan created: {len(plan.steps)} steps")
+ return plan
+
+ # ==================== 阶段 2: 工具执行 ====================
+
+ async def execute_tool(
+ self,
+ tool_name: str,
+ arguments: Dict[str, Any],
+ tool_handlers: Dict[str, Any],
+ ) -> Dict[str, Any]:
+ """执行单个工具"""
+
+ # 特殊处理:summarize_with_llm
+ if tool_name == "summarize_with_llm":
+ return await self.summarize_with_llm(
+ data=arguments.get("data", ""),
+ task=arguments.get("task", "总结数据"),
+ )
+
+ # 调用 MCP 工具
+ handler = tool_handlers.get(tool_name)
+ if not handler:
+ raise ValueError(f"Tool '{tool_name}' not found")
+
+ result = await handler(arguments)
+ return result
+
+ async def execute_plan(
+ self,
+ plan: ExecutionPlan,
+ tool_handlers: Dict[str, Any],
+ ) -> List[StepResult]:
+ """阶段2: 执行计划中的所有步骤"""
+ logger.info(f"[Execution] Starting execution: {len(plan.steps)} steps")
+
+ results = []
+ collected_data = {} # 收集的数据,供后续步骤使用
+
+ for i, step in enumerate(plan.steps):
+ logger.info(f"[Execution] Step {i+1}/{len(plan.steps)}: {step.tool}")
+
+ start_time = datetime.now()
+
+ try:
+ # 替换 arguments 中的占位符
+ arguments = step.arguments.copy()
+ if step.tool == "summarize_with_llm" and arguments.get("data") == "前面收集的所有数据":
+ # 将收集的数据传递给总结工具
+ 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,
+ )
+ results.append(step_result)
+
+ # 收集数据
+ collected_data[f"step_{i+1}_{step.tool}"] = result
+
+ logger.info(f"[Execution] Step {i+1} completed in {execution_time:.2f}s")
+
+ except Exception as e:
+ logger.error(f"[Execution] Step {i+1} failed: {str(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,
+ )
+ results.append(step_result)
+
+ # 根据错误类型决定是否继续
+ if "not found" in str(e).lower():
+ logger.warning(f"[Execution] Stopping due to critical error")
+ break
+ else:
+ logger.warning(f"[Execution] Continuing despite error")
+ continue
+
+ logger.info(f"[Execution] Execution completed: {len(results)} steps")
+ return results
+
+ async def summarize_with_llm(self, data: str, task: str) -> str:
+ """特殊工具:使用 LLM 总结数据"""
+ logger.info(f"[LLM Summary] Task: {task}")
+
+ messages = [
+ {
+ "role": "system",
+ "content": "你是一个专业的金融分析师。根据提供的数据,完成指定的分析任务。"
+ },
+ {
+ "role": "user",
+ "content": f"## 任务\n{task}\n\n## 数据\n{data}\n\n请根据数据完成分析任务,用专业且易懂的语言呈现。"
+ },
+ ]
+
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=0.7,
+ max_tokens=2000,
+ )
+
+ summary = response.choices[0].message.content
+ return summary
+
+ # ==================== 阶段 3: 结果总结 ====================
+
+ async def generate_final_summary(
+ self,
+ user_query: str,
+ plan: ExecutionPlan,
+ step_results: List[StepResult],
+ ) -> str:
+ """阶段3: 生成最终总结"""
+ logger.info("[Summary] Generating final summary")
+
+ # 收集所有成功的结果
+ successful_results = [r for r in step_results if r.status == "success"]
+
+ if not successful_results:
+ return "很抱歉,所有步骤都执行失败,无法生成分析报告。"
+
+ # 构建总结提示
+ results_text = "\n\n".join([
+ f"**步骤 {r.step_index + 1}: {r.tool}**\n"
+ f"结果: {json.dumps(r.result, ensure_ascii=False, indent=2)[:1000]}..."
+ for r in successful_results
+ ])
+
+ messages = [
+ {
+ "role": "system",
+ "content": "你是一个专业的金融研究助手。根据执行结果,生成一份简洁清晰的报告。"
+ },
+ {
+ "role": "user",
+ "content": f"""
+用户问题:{user_query}
+
+执行计划:{plan.goal}
+
+执行结果:
+{results_text}
+
+请根据以上信息,生成一份专业的分析报告(300字以内)。
+"""
+ },
+ ]
+
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=0.7,
+ max_tokens=1000,
+ )
+
+ summary = response.choices[0].message.content
+ logger.info("[Summary] Final summary generated")
+ return summary
+
+ # ==================== 主流程 ====================
+
+ async def process_query(
+ self,
+ user_query: str,
+ tools: List[dict],
+ tool_handlers: Dict[str, Any],
+ ) -> AgentResponse:
+ """主流程:处理用户查询"""
+ logger.info(f"[Agent] Processing query: {user_query}")
+
+ try:
+ # 阶段 1: 创建计划
+ plan = await self.create_plan(user_query, tools)
+
+ # 阶段 2: 执行计划
+ step_results = await self.execute_plan(plan, tool_handlers)
+
+ # 阶段 3: 生成总结
+ final_summary = await self.generate_final_summary(
+ user_query, plan, step_results
+ )
+
+ return AgentResponse(
+ success=True,
+ message=final_summary,
+ plan=plan,
+ step_results=step_results,
+ final_summary=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),
+ },
+ )
+
+ except Exception as e:
+ logger.error(f"[Agent] Error: {str(e)}", exc_info=True)
+ return AgentResponse(
+ success=False,
+ message=f"处理失败: {str(e)}",
+ )
+
+# ==================== FastAPI 端点 ====================
+
+"""
+在 mcp_server.py 中添加:
+
+from mcp_agent_system import MCPAgent, ChatRequest, AgentResponse
+
+# 创建 Agent 实例
+agent = MCPAgent(provider="qwen")
+
+@app.post("/agent/chat", response_model=AgentResponse)
+async def agent_chat(request: ChatRequest):
+ \"\"\"智能代理对话端点\"\"\"
+ logger.info(f"Agent chat: {request.message}")
+
+ # 获取工具列表和处理器
+ tools = [tool.dict() for tool in TOOLS]
+
+ # 处理查询
+ response = await agent.process_query(
+ user_query=request.message,
+ tools=tools,
+ tool_handlers=TOOL_HANDLERS,
+ )
+
+ return response
+"""
diff --git a/mcp_chat_endpoint.py b/mcp_chat_endpoint.py
new file mode 100644
index 00000000..11da0ad8
--- /dev/null
+++ b/mcp_chat_endpoint.py
@@ -0,0 +1,295 @@
+"""
+MCP Chat Endpoint - 添加到 mcp_server.py
+集成LLM实现智能对话,自动调用MCP工具并总结结果
+"""
+
+from pydantic import BaseModel
+from typing import List, Dict, Any, Optional
+import os
+import json
+from openai import OpenAI
+import logging
+
+logger = logging.getLogger(__name__)
+
+# ==================== LLM配置 ====================
+
+# 支持多种LLM提供商
+LLM_PROVIDERS = {
+ "openai": {
+ "api_key": os.getenv("OPENAI_API_KEY", ""),
+ "base_url": "https://api.openai.com/v1",
+ "model": "gpt-4o-mini", # 便宜且快速
+ },
+ "qwen": {
+ "api_key": os.getenv("DASHSCOPE_API_KEY", ""),
+ "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ "model": "qwen-plus",
+ },
+ "deepseek": {
+ "api_key": os.getenv("DEEPSEEK_API_KEY", ""),
+ "base_url": "https://api.deepseek.com/v1",
+ "model": "deepseek-chat",
+ },
+}
+
+# 默认使用的LLM提供商
+DEFAULT_PROVIDER = "qwen" # 推荐使用通义千问,价格便宜
+
+# ==================== 数据模型 ====================
+
+class Message(BaseModel):
+ """消息"""
+ role: str # system, user, assistant
+ content: str
+
+class ChatRequest(BaseModel):
+ """聊天请求"""
+ message: str
+ conversation_history: List[Dict[str, str]] = []
+ provider: Optional[str] = DEFAULT_PROVIDER
+
+class ChatResponse(BaseModel):
+ """聊天响应"""
+ success: bool
+ message: str
+ tool_used: Optional[str] = None
+ raw_data: Optional[Any] = None
+ error: Optional[str] = None
+
+# ==================== LLM助手类 ====================
+
+class MCPChatAssistant:
+ """MCP聊天助手 - 集成LLM和工具调用"""
+
+ def __init__(self, provider: str = DEFAULT_PROVIDER):
+ self.provider = provider
+ config = LLM_PROVIDERS.get(provider)
+
+ if not config or not config["api_key"]:
+ logger.warning(f"LLM provider '{provider}' not configured, using fallback mode")
+ self.client = None
+ else:
+ self.client = OpenAI(
+ api_key=config["api_key"],
+ base_url=config["base_url"],
+ )
+ self.model = config["model"]
+
+ def get_system_prompt(self, tools: List[dict]) -> str:
+ """构建系统提示词"""
+ tools_desc = "\n\n".join([
+ f"**{tool['name']}**\n描述:{tool['description']}\n参数:{json.dumps(tool['parameters'], ensure_ascii=False, indent=2)}"
+ for tool in tools
+ ])
+
+ return f"""你是一个专业的金融投资助手。你可以使用以下工具来帮助用户查询信息:
+
+{tools_desc}
+
+## 工作流程
+1. **理解用户意图**:分析用户问题,确定需要什么信息
+2. **选择工具**:从上面的工具中选择最合适的一个或多个
+3. **提取参数**:从用户输入中提取工具需要的参数
+4. **返回工具调用指令**(JSON格式):
+ {{"tool": "工具名", "arguments": {{...}}}}
+
+## 重要规则
+- 贵州茅台的股票代码是 **600519**
+- 如果用户提到股票名称,尝试推断股票代码
+- 如果不确定需要什么信息,使用 search_china_news 搜索相关新闻
+- 涨停是指股票当日涨幅达到10%左右
+- 只返回工具调用指令,不要额外解释
+
+## 示例
+用户:"查询贵州茅台的股票信息"
+你:{{"tool": "get_stock_basic_info", "arguments": {{"seccode": "600519"}}}}
+
+用户:"今日涨停的股票有哪些"
+你:{{"tool": "search_limit_up_stocks", "arguments": {{"query": "", "mode": "hybrid", "page_size": 10}}}}
+
+用户:"新能源概念板块表现如何"
+你:{{"tool": "search_concepts", "arguments": {{"query": "新能源", "size": 10, "sort_by": "change_pct"}}}}
+"""
+
+ async def chat(self, user_message: str, conversation_history: List[Dict[str, str]], tools: List[dict]) -> ChatResponse:
+ """智能对话"""
+ try:
+ if not self.client:
+ # 降级到简单匹配
+ return await self.fallback_chat(user_message)
+
+ # 1. 构建消息历史
+ messages = [
+ {"role": "system", "content": self.get_system_prompt(tools)},
+ ]
+
+ # 添加历史对话(最多保留最近10轮)
+ for msg in conversation_history[-20:]:
+ messages.append({
+ "role": "user" if msg.get("isUser") else "assistant",
+ "content": msg.get("content", ""),
+ })
+
+ messages.append({"role": "user", "content": user_message})
+
+ # 2. 调用LLM获取工具调用指令
+ logger.info(f"Calling LLM with {len(messages)} messages")
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=0.3, # 低温度,更确定性
+ max_tokens=500,
+ )
+
+ tool_call_instruction = response.choices[0].message.content.strip()
+ logger.info(f"LLM response: {tool_call_instruction}")
+
+ # 3. 解析工具调用指令
+ try:
+ tool_call = json.loads(tool_call_instruction)
+ tool_name = tool_call.get("tool")
+ tool_args = tool_call.get("arguments", {})
+
+ if not tool_name:
+ raise ValueError("No tool specified")
+
+ # 4. 调用工具(这里需要导入 mcp_server 的工具处理器)
+ from mcp_server import TOOL_HANDLERS
+
+ handler = TOOL_HANDLERS.get(tool_name)
+ if not handler:
+ raise ValueError(f"Tool '{tool_name}' not found")
+
+ tool_result = await handler(tool_args)
+
+ # 5. 让LLM总结结果
+ summary_messages = messages + [
+ {"role": "assistant", "content": tool_call_instruction},
+ {"role": "system", "content": f"工具 {tool_name} 返回的数据:\n{json.dumps(tool_result, ensure_ascii=False, indent=2)}\n\n请用自然语言总结这些数据,给用户一个简洁清晰的回复(不超过200字)。"}
+ ]
+
+ summary_response = self.client.chat.completions.create(
+ model=self.model,
+ messages=summary_messages,
+ temperature=0.7,
+ max_tokens=300,
+ )
+
+ summary = summary_response.choices[0].message.content
+
+ return ChatResponse(
+ success=True,
+ message=summary,
+ tool_used=tool_name,
+ raw_data=tool_result,
+ )
+
+ except json.JSONDecodeError:
+ # LLM没有返回JSON格式,直接返回其回复
+ return ChatResponse(
+ success=True,
+ message=tool_call_instruction,
+ )
+ except Exception as tool_error:
+ logger.error(f"Tool execution error: {str(tool_error)}")
+ return ChatResponse(
+ success=False,
+ message="工具调用失败",
+ error=str(tool_error),
+ )
+
+ except Exception as e:
+ logger.error(f"Chat error: {str(e)}", exc_info=True)
+ return ChatResponse(
+ success=False,
+ message="对话处理失败",
+ error=str(e),
+ )
+
+ async def fallback_chat(self, user_message: str) -> ChatResponse:
+ """降级方案:简单关键词匹配"""
+ from mcp_server import TOOL_HANDLERS
+
+ try:
+ # 茅台特殊处理
+ if "茅台" in user_message or "贵州茅台" in user_message:
+ handler = TOOL_HANDLERS.get("get_stock_basic_info")
+ result = await handler({"seccode": "600519"})
+ return ChatResponse(
+ success=True,
+ message="已为您查询贵州茅台(600519)的股票信息:",
+ tool_used="get_stock_basic_info",
+ raw_data=result,
+ )
+
+ # 涨停分析
+ elif "涨停" in user_message:
+ handler = TOOL_HANDLERS.get("search_limit_up_stocks")
+ query = user_message.replace("涨停", "").strip()
+ result = await handler({"query": query, "mode": "hybrid", "page_size": 10})
+ return ChatResponse(
+ success=True,
+ message="已为您查询涨停股票信息:",
+ tool_used="search_limit_up_stocks",
+ raw_data=result,
+ )
+
+ # 概念板块
+ elif "概念" in user_message or "板块" in user_message:
+ handler = TOOL_HANDLERS.get("search_concepts")
+ query = user_message.replace("概念", "").replace("板块", "").strip()
+ result = await handler({"query": query, "size": 10, "sort_by": "change_pct"})
+ return ChatResponse(
+ success=True,
+ message=f"已为您查询'{query}'相关概念板块:",
+ tool_used="search_concepts",
+ raw_data=result,
+ )
+
+ # 默认:搜索新闻
+ else:
+ handler = TOOL_HANDLERS.get("search_china_news")
+ result = await handler({"query": user_message, "top_k": 5})
+ return ChatResponse(
+ success=True,
+ message="已为您搜索相关新闻:",
+ tool_used="search_china_news",
+ raw_data=result,
+ )
+
+ except Exception as e:
+ logger.error(f"Fallback chat error: {str(e)}")
+ return ChatResponse(
+ success=False,
+ message="查询失败",
+ error=str(e),
+ )
+
+# ==================== FastAPI端点 ====================
+
+# 在 mcp_server.py 中添加以下代码:
+
+"""
+from mcp_chat_endpoint import MCPChatAssistant, ChatRequest, ChatResponse
+
+# 创建聊天助手实例
+chat_assistant = MCPChatAssistant(provider="qwen") # 或 "openai", "deepseek"
+
+@app.post("/chat", response_model=ChatResponse)
+async def chat_endpoint(request: ChatRequest):
+ \"\"\"智能对话端点 - 使用LLM理解意图并调用工具\"\"\"
+ logger.info(f"Chat request: {request.message}")
+
+ # 获取可用工具列表
+ tools = [tool.dict() for tool in TOOLS]
+
+ # 调用聊天助手
+ response = await chat_assistant.chat(
+ user_message=request.message,
+ conversation_history=request.conversation_history,
+ tools=tools,
+ )
+
+ return response
+"""
diff --git a/mcp_client_example.py b/mcp_client_example.py
new file mode 100644
index 00000000..19727435
--- /dev/null
+++ b/mcp_client_example.py
@@ -0,0 +1,248 @@
+"""
+MCP客户端使用示例
+演示如何调用MCP服务器的各种工具
+"""
+
+import httpx
+import json
+from typing import Dict, Any
+
+
+class MCPClient:
+ """MCP客户端"""
+
+ def __init__(self, base_url: str = "http://localhost:8900"):
+ self.base_url = base_url
+ self.client = httpx.Client(timeout=60.0)
+
+ def list_tools(self):
+ """列出所有可用工具"""
+ response = self.client.get(f"{self.base_url}/tools")
+ response.raise_for_status()
+ return response.json()
+
+ def get_tool(self, tool_name: str):
+ """获取特定工具的定义"""
+ response = self.client.get(f"{self.base_url}/tools/{tool_name}")
+ response.raise_for_status()
+ return response.json()
+
+ def call_tool(self, tool_name: str, arguments: Dict[str, Any]):
+ """调用工具"""
+ payload = {
+ "tool": tool_name,
+ "arguments": arguments
+ }
+ response = self.client.post(f"{self.base_url}/tools/call", json=payload)
+ response.raise_for_status()
+ return response.json()
+
+ def close(self):
+ """关闭客户端"""
+ self.client.close()
+
+
+def print_result(title: str, result: Dict[str, Any]):
+ """打印结果"""
+ print(f"\n{'=' * 60}")
+ print(f"{title}")
+ print(f"{'=' * 60}")
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+
+
+def main():
+ """主函数 - 演示各种工具的使用"""
+
+ client = MCPClient()
+
+ try:
+ # 1. 列出所有工具
+ print("\n示例1: 列出所有可用工具")
+ tools = client.list_tools()
+ print(f"可用工具数量: {len(tools['tools'])}")
+ for tool in tools['tools']:
+ print(f" - {tool['name']}: {tool['description'][:50]}...")
+
+ # 2. 搜索中国新闻
+ print("\n示例2: 搜索中国新闻(关键词:人工智能)")
+ result = client.call_tool(
+ "search_china_news",
+ {
+ "query": "人工智能",
+ "top_k": 5
+ }
+ )
+ if result['success']:
+ print_result("中国新闻搜索结果", result['data'])
+
+ # 3. 搜索概念板块(按涨跌幅排序)
+ print("\n示例3: 搜索概念板块(关键词:新能源,按涨跌幅排序)")
+ result = client.call_tool(
+ "search_concepts",
+ {
+ "query": "新能源",
+ "size": 5,
+ "sort_by": "change_pct"
+ }
+ )
+ if result['success']:
+ print_result("概念搜索结果", result['data'])
+
+ # 4. 获取股票的相关概念
+ print("\n示例4: 获取股票相关概念(股票代码:600519)")
+ result = client.call_tool(
+ "get_stock_concepts",
+ {
+ "stock_code": "600519",
+ "size": 10
+ }
+ )
+ if result['success']:
+ print_result("股票概念结果", result['data'])
+
+ # 5. 搜索涨停股票
+ print("\n示例5: 搜索涨停股票(关键词:锂电池)")
+ result = client.call_tool(
+ "search_limit_up_stocks",
+ {
+ "query": "锂电池",
+ "mode": "hybrid",
+ "page_size": 5
+ }
+ )
+ if result['success']:
+ print_result("涨停股票搜索结果", result['data'])
+
+ # 6. 搜索研究报告
+ print("\n示例6: 搜索研究报告(关键词:投资策略)")
+ result = client.call_tool(
+ "search_research_reports",
+ {
+ "query": "投资策略",
+ "mode": "hybrid",
+ "size": 3
+ }
+ )
+ if result['success']:
+ print_result("研究报告搜索结果", result['data'])
+
+ # 7. 获取概念统计数据
+ print("\n示例7: 获取概念统计(最近7天)")
+ result = client.call_tool(
+ "get_concept_statistics",
+ {
+ "days": 7,
+ "min_stock_count": 3
+ }
+ )
+ if result['success']:
+ print_result("概念统计结果", result['data'])
+
+ # 8. 搜索路演信息
+ print("\n示例8: 搜索路演信息(关键词:业绩)")
+ result = client.call_tool(
+ "search_roadshows",
+ {
+ "query": "业绩",
+ "size": 3
+ }
+ )
+ if result['success']:
+ print_result("路演搜索结果", result['data'])
+
+ # 9. 获取股票基本信息
+ print("\n示例9: 获取股票基本信息(股票:600519)")
+ result = client.call_tool(
+ "get_stock_basic_info",
+ {
+ "seccode": "600519"
+ }
+ )
+ if result['success']:
+ print_result("股票基本信息", result['data'])
+
+ # 10. 获取股票财务指标
+ print("\n示例10: 获取股票财务指标(股票:600519,最近5期)")
+ result = client.call_tool(
+ "get_stock_financial_index",
+ {
+ "seccode": "600519",
+ "limit": 5
+ }
+ )
+ if result['success']:
+ print_result("财务指标", result['data'])
+
+ # 11. 获取股票交易数据
+ print("\n示例11: 获取股票交易数据(股票:600519,最近10天)")
+ result = client.call_tool(
+ "get_stock_trade_data",
+ {
+ "seccode": "600519",
+ "limit": 10
+ }
+ )
+ if result['success']:
+ print_result("交易数据", result['data'])
+
+ # 12. 按行业搜索股票
+ print("\n示例12: 按行业搜索股票(行业:半导体)")
+ result = client.call_tool(
+ "search_stocks_by_criteria",
+ {
+ "industry": "半导体",
+ "limit": 10
+ }
+ )
+ if result['success']:
+ print_result("行业股票", result['data'])
+
+ # 13. 股票对比分析
+ print("\n示例13: 股票对比分析(600519 vs 000858)")
+ result = client.call_tool(
+ "get_stock_comparison",
+ {
+ "seccodes": ["600519", "000858"],
+ "metric": "financial"
+ }
+ )
+ if result['success']:
+ print_result("股票对比", result['data'])
+
+ except Exception as e:
+ print(f"\n错误: {str(e)}")
+
+ finally:
+ client.close()
+
+
+def test_single_tool():
+ """测试单个工具(用于快速测试)"""
+ client = MCPClient()
+
+ try:
+ # 修改这里来测试不同的工具
+ result = client.call_tool(
+ "search_china_news",
+ {
+ "query": "芯片",
+ "exact_match": True,
+ "top_k": 3
+ }
+ )
+
+ print_result("测试结果", result)
+
+ except Exception as e:
+ print(f"错误: {str(e)}")
+
+ finally:
+ client.close()
+
+
+if __name__ == "__main__":
+ # 运行完整示例
+ main()
+
+ # 或者测试单个工具
+ # test_single_tool()
diff --git a/mcp_config.py b/mcp_config.py
new file mode 100644
index 00000000..fd82ea95
--- /dev/null
+++ b/mcp_config.py
@@ -0,0 +1,108 @@
+"""
+MCP服务器配置文件
+集中管理所有配置项
+"""
+
+from typing import Dict
+from pydantic import BaseSettings
+
+class Settings(BaseSettings):
+ """应用配置"""
+
+ # 服务器配置
+ SERVER_HOST: str = "0.0.0.0"
+ SERVER_PORT: int = 8900
+ DEBUG: bool = True
+
+ # 后端API服务端点
+ NEWS_API_URL: str = "http://222.128.1.157:21891"
+ ROADSHOW_API_URL: str = "http://222.128.1.157:19800"
+ CONCEPT_API_URL: str = "http://222.128.1.157:16801"
+ STOCK_ANALYSIS_API_URL: str = "http://222.128.1.157:8811"
+
+ # HTTP客户端配置
+ HTTP_TIMEOUT: float = 60.0
+ HTTP_MAX_RETRIES: int = 3
+
+ # 日志配置
+ LOG_LEVEL: str = "INFO"
+ LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+
+ # CORS配置
+ CORS_ORIGINS: list = ["*"]
+ CORS_CREDENTIALS: bool = True
+ CORS_METHODS: list = ["*"]
+ CORS_HEADERS: list = ["*"]
+
+ # LLM配置(如果需要集成)
+ LLM_PROVIDER: str = "openai" # openai, anthropic, etc.
+ LLM_API_KEY: str = ""
+ LLM_MODEL: str = "gpt-4"
+ LLM_BASE_URL: str = ""
+
+ # 速率限制
+ RATE_LIMIT_ENABLED: bool = False
+ RATE_LIMIT_PER_MINUTE: int = 60
+
+ # 缓存配置
+ CACHE_ENABLED: bool = True
+ CACHE_TTL: int = 300 # 秒
+
+ class Config:
+ env_file = ".env"
+ case_sensitive = True
+
+
+# 全局设置实例
+settings = Settings()
+
+
+# 工具类别映射(用于组织和展示)
+TOOL_CATEGORIES: Dict[str, list] = {
+ "新闻搜索": [
+ "search_news",
+ "search_china_news",
+ "search_medical_news"
+ ],
+ "公司研究": [
+ "search_roadshows",
+ "search_research_reports"
+ ],
+ "概念板块": [
+ "search_concepts",
+ "get_concept_details",
+ "get_stock_concepts",
+ "get_concept_statistics"
+ ],
+ "股票分析": [
+ "search_limit_up_stocks",
+ "get_daily_stock_analysis"
+ ]
+}
+
+
+# 工具优先级(用于LLM选择工具时的提示)
+TOOL_PRIORITIES: Dict[str, int] = {
+ "search_china_news": 10, # 最常用
+ "search_concepts": 9,
+ "search_limit_up_stocks": 8,
+ "search_research_reports": 8,
+ "get_stock_concepts": 7,
+ "search_news": 6,
+ "get_daily_stock_analysis": 5,
+ "get_concept_statistics": 5,
+ "search_medical_news": 4,
+ "search_roadshows": 4,
+ "get_concept_details": 3,
+}
+
+
+# 默认参数配置
+DEFAULT_PARAMS = {
+ "top_k": 20,
+ "page_size": 20,
+ "size": 10,
+ "sort_by": "change_pct",
+ "mode": "hybrid",
+ "exact_match": False,
+}
diff --git a/mcp_database.py b/mcp_database.py
new file mode 100644
index 00000000..ec2df704
--- /dev/null
+++ b/mcp_database.py
@@ -0,0 +1,546 @@
+"""
+MySQL数据库查询模块
+提供股票财务数据查询功能
+"""
+
+import aiomysql
+import logging
+from typing import Dict, List, Any, Optional
+from datetime import datetime, date
+from decimal import Decimal
+import json
+
+logger = logging.getLogger(__name__)
+
+# MySQL连接配置
+MYSQL_CONFIG = {
+ 'host': '222.128.1.157',
+ 'port': 33060,
+ 'user': 'root',
+ 'password': 'Zzl5588161!',
+ 'db': 'stock',
+ 'charset': 'utf8mb4',
+ 'autocommit': True
+}
+
+# 全局连接池
+_pool = None
+
+
+class DateTimeEncoder(json.JSONEncoder):
+ """JSON编码器,处理datetime和Decimal类型"""
+ def default(self, obj):
+ if isinstance(obj, (datetime, date)):
+ return obj.isoformat()
+ if isinstance(obj, Decimal):
+ return float(obj)
+ return super().default(obj)
+
+
+async def get_pool():
+ """获取MySQL连接池"""
+ global _pool
+ if _pool is None:
+ _pool = await aiomysql.create_pool(
+ host=MYSQL_CONFIG['host'],
+ port=MYSQL_CONFIG['port'],
+ user=MYSQL_CONFIG['user'],
+ password=MYSQL_CONFIG['password'],
+ db=MYSQL_CONFIG['db'],
+ charset=MYSQL_CONFIG['charset'],
+ autocommit=MYSQL_CONFIG['autocommit'],
+ minsize=1,
+ maxsize=10
+ )
+ logger.info("MySQL connection pool created")
+ return _pool
+
+
+async def close_pool():
+ """关闭MySQL连接池"""
+ global _pool
+ if _pool:
+ _pool.close()
+ await _pool.wait_closed()
+ _pool = None
+ logger.info("MySQL connection pool closed")
+
+
+def convert_row(row: Dict) -> Dict:
+ """转换数据库行,处理特殊类型"""
+ if not row:
+ return {}
+
+ result = {}
+ for key, value in row.items():
+ if isinstance(value, Decimal):
+ result[key] = float(value)
+ elif isinstance(value, (datetime, date)):
+ result[key] = value.isoformat()
+ else:
+ result[key] = value
+ return result
+
+
+async def get_stock_basic_info(seccode: str) -> Optional[Dict[str, Any]]:
+ """
+ 获取股票基本信息
+
+ Args:
+ seccode: 股票代码
+
+ Returns:
+ 股票基本信息字典
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ SELECT
+ SECCODE, SECNAME, ORGNAME,
+ F001V as english_name,
+ F003V as legal_representative,
+ F004V as registered_address,
+ F005V as office_address,
+ F010D as establishment_date,
+ F011V as website,
+ F012V as email,
+ F013V as phone,
+ F015V as main_business,
+ F016V as business_scope,
+ F017V as company_profile,
+ F030V as industry_level1,
+ F032V as industry_level2,
+ F034V as sw_industry_level1,
+ F036V as sw_industry_level2,
+ F026V as province,
+ F028V as city,
+ F041V as chairman,
+ F042V as general_manager,
+ UPDATE_DATE as update_date
+ FROM ea_baseinfo
+ WHERE SECCODE = %s
+ LIMIT 1
+ """
+
+ await cursor.execute(query, (seccode,))
+ result = await cursor.fetchone()
+
+ if result:
+ return convert_row(result)
+ return None
+
+
+async def get_stock_financial_index(
+ seccode: str,
+ start_date: Optional[str] = None,
+ end_date: Optional[str] = None,
+ limit: int = 10
+) -> List[Dict[str, Any]]:
+ """
+ 获取股票财务指标
+
+ Args:
+ seccode: 股票代码
+ start_date: 开始日期 YYYY-MM-DD
+ end_date: 结束日期 YYYY-MM-DD
+ limit: 返回条数
+
+ Returns:
+ 财务指标列表
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ # 构建查询
+ query = """
+ SELECT
+ SECCODE, SECNAME, ENDDATE, STARTDATE,
+ F069D as report_year,
+ F003N as eps, -- 每股收益
+ F004N as basic_eps,
+ F008N as bps, -- 每股净资产
+ F014N as roe, -- 净资产收益率
+ F016N as roa, -- 总资产报酬率
+ F017N as net_profit_margin, -- 净利润率
+ F022N as receivable_turnover, -- 应收账款周转率
+ F023N as inventory_turnover, -- 存货周转率
+ F025N as total_asset_turnover, -- 总资产周转率
+ F041N as debt_ratio, -- 资产负债率
+ F042N as current_ratio, -- 流动比率
+ F043N as quick_ratio, -- 速动比率
+ F052N as revenue_growth, -- 营业收入增长率
+ F053N as profit_growth, -- 净利润增长率
+ F089N as revenue, -- 营业收入
+ F090N as operating_cost, -- 营业成本
+ F101N as net_profit, -- 净利润
+ F102N as net_profit_parent -- 归母净利润
+ FROM ea_financialindex
+ WHERE SECCODE = %s
+ """
+
+ params = [seccode]
+
+ if start_date:
+ query += " AND ENDDATE >= %s"
+ params.append(start_date)
+
+ if end_date:
+ query += " AND ENDDATE <= %s"
+ params.append(end_date)
+
+ query += " ORDER BY ENDDATE DESC LIMIT %s"
+ params.append(limit)
+
+ await cursor.execute(query, params)
+ results = await cursor.fetchall()
+
+ return [convert_row(row) for row in results]
+
+
+async def get_stock_trade_data(
+ seccode: str,
+ start_date: Optional[str] = None,
+ end_date: Optional[str] = None,
+ limit: int = 30
+) -> List[Dict[str, Any]]:
+ """
+ 获取股票交易数据
+
+ Args:
+ seccode: 股票代码
+ start_date: 开始日期 YYYY-MM-DD
+ end_date: 结束日期 YYYY-MM-DD
+ limit: 返回条数
+
+ Returns:
+ 交易数据列表
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ SELECT
+ SECCODE, SECNAME, TRADEDATE,
+ F002N as prev_close, -- 昨日收盘价
+ F003N as open_price, -- 开盘价
+ F005N as high_price, -- 最高价
+ F006N as low_price, -- 最低价
+ F007N as close_price, -- 收盘价
+ F004N as volume, -- 成交量
+ F011N as turnover, -- 成交金额
+ F009N as change_amount, -- 涨跌额
+ F010N as change_pct, -- 涨跌幅
+ F012N as turnover_rate, -- 换手率
+ F013N as amplitude, -- 振幅
+ F026N as pe_ratio, -- 市盈率
+ F020N as total_shares, -- 总股本
+ F021N as circulating_shares -- 流通股本
+ FROM ea_trade
+ WHERE SECCODE = %s
+ """
+
+ params = [seccode]
+
+ if start_date:
+ query += " AND TRADEDATE >= %s"
+ params.append(start_date)
+
+ if end_date:
+ query += " AND TRADEDATE <= %s"
+ params.append(end_date)
+
+ query += " ORDER BY TRADEDATE DESC LIMIT %s"
+ params.append(limit)
+
+ await cursor.execute(query, params)
+ results = await cursor.fetchall()
+
+ return [convert_row(row) for row in results]
+
+
+async def get_stock_balance_sheet(
+ seccode: str,
+ start_date: Optional[str] = None,
+ end_date: Optional[str] = None,
+ limit: int = 8
+) -> List[Dict[str, Any]]:
+ """
+ 获取资产负债表数据
+
+ Args:
+ seccode: 股票代码
+ start_date: 开始日期
+ end_date: 结束日期
+ limit: 返回条数
+
+ Returns:
+ 资产负债表数据列表
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ SELECT
+ SECCODE, SECNAME, ENDDATE,
+ F001D as report_year,
+ F006N as cash, -- 货币资金
+ F009N as receivables, -- 应收账款
+ F015N as inventory, -- 存货
+ F019N as current_assets, -- 流动资产合计
+ F023N as long_term_investment, -- 长期股权投资
+ F025N as fixed_assets, -- 固定资产
+ F037N as noncurrent_assets, -- 非流动资产合计
+ F038N as total_assets, -- 资产总计
+ F039N as short_term_loan, -- 短期借款
+ F042N as payables, -- 应付账款
+ F052N as current_liabilities, -- 流动负债合计
+ F053N as long_term_loan, -- 长期借款
+ F060N as noncurrent_liabilities, -- 非流动负债合计
+ F061N as total_liabilities, -- 负债合计
+ F062N as share_capital, -- 股本
+ F063N as capital_reserve, -- 资本公积
+ F065N as retained_earnings, -- 未分配利润
+ F070N as total_equity -- 所有者权益合计
+ FROM ea_asset
+ WHERE SECCODE = %s
+ """
+
+ params = [seccode]
+
+ if start_date:
+ query += " AND ENDDATE >= %s"
+ params.append(start_date)
+
+ if end_date:
+ query += " AND ENDDATE <= %s"
+ params.append(end_date)
+
+ query += " ORDER BY ENDDATE DESC LIMIT %s"
+ params.append(limit)
+
+ await cursor.execute(query, params)
+ results = await cursor.fetchall()
+
+ return [convert_row(row) for row in results]
+
+
+async def get_stock_cashflow(
+ seccode: str,
+ start_date: Optional[str] = None,
+ end_date: Optional[str] = None,
+ limit: int = 8
+) -> List[Dict[str, Any]]:
+ """
+ 获取现金流量表数据
+
+ Args:
+ seccode: 股票代码
+ start_date: 开始日期
+ end_date: 结束日期
+ limit: 返回条数
+
+ Returns:
+ 现金流量表数据列表
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ SELECT
+ SECCODE, SECNAME, ENDDATE, STARTDATE,
+ F001D as report_year,
+ F009N as operating_cash_inflow, -- 经营活动现金流入
+ F014N as operating_cash_outflow, -- 经营活动现金流出
+ F015N as net_operating_cashflow, -- 经营活动现金流量净额
+ F021N as investing_cash_inflow, -- 投资活动现金流入
+ F026N as investing_cash_outflow, -- 投资活动现金流出
+ F027N as net_investing_cashflow, -- 投资活动现金流量净额
+ F031N as financing_cash_inflow, -- 筹资活动现金流入
+ F035N as financing_cash_outflow, -- 筹资活动现金流出
+ F036N as net_financing_cashflow, -- 筹资活动现金流量净额
+ F039N as net_cash_increase, -- 现金及现金等价物净增加额
+ F044N as net_profit, -- 净利润
+ F046N as depreciation, -- 固定资产折旧
+ F060N as net_operating_cashflow_adjusted -- 经营活动现金流量净额(补充)
+ FROM ea_cashflow
+ WHERE SECCODE = %s
+ """
+
+ params = [seccode]
+
+ if start_date:
+ query += " AND ENDDATE >= %s"
+ params.append(start_date)
+
+ if end_date:
+ query += " AND ENDDATE <= %s"
+ params.append(end_date)
+
+ query += " ORDER BY ENDDATE DESC LIMIT %s"
+ params.append(limit)
+
+ await cursor.execute(query, params)
+ results = await cursor.fetchall()
+
+ return [convert_row(row) for row in results]
+
+
+async def search_stocks_by_criteria(
+ industry: Optional[str] = None,
+ province: Optional[str] = None,
+ min_market_cap: Optional[float] = None,
+ max_market_cap: Optional[float] = None,
+ limit: int = 50
+) -> List[Dict[str, Any]]:
+ """
+ 按条件搜索股票
+
+ Args:
+ industry: 行业名称
+ province: 省份
+ min_market_cap: 最小市值(亿元)
+ max_market_cap: 最大市值(亿元)
+ limit: 返回条数
+
+ Returns:
+ 股票列表
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ SELECT DISTINCT
+ b.SECCODE,
+ b.SECNAME,
+ b.F030V as industry_level1,
+ b.F032V as industry_level2,
+ b.F034V as sw_industry_level1,
+ b.F026V as province,
+ b.F028V as city,
+ b.F015V as main_business,
+ t.F007N as latest_price,
+ t.F010N as change_pct,
+ t.F026N as pe_ratio,
+ t.TRADEDATE as latest_trade_date
+ FROM ea_baseinfo b
+ LEFT JOIN (
+ SELECT SECCODE, MAX(TRADEDATE) as max_date
+ FROM ea_trade
+ GROUP BY SECCODE
+ ) latest ON b.SECCODE = latest.SECCODE
+ LEFT JOIN ea_trade t ON b.SECCODE = t.SECCODE
+ AND t.TRADEDATE = latest.max_date
+ WHERE 1=1
+ """
+
+ params = []
+
+ if industry:
+ query += " AND (b.F030V LIKE %s OR b.F032V LIKE %s OR b.F034V LIKE %s)"
+ pattern = f"%{industry}%"
+ params.extend([pattern, pattern, pattern])
+
+ if province:
+ query += " AND b.F026V = %s"
+ params.append(province)
+
+ if min_market_cap or max_market_cap:
+ # 市值 = 最新价 * 总股本 / 100000000(转换为亿元)
+ if min_market_cap:
+ query += " AND (t.F007N * t.F020N / 100000000) >= %s"
+ params.append(min_market_cap)
+
+ if max_market_cap:
+ query += " AND (t.F007N * t.F020N / 100000000) <= %s"
+ params.append(max_market_cap)
+
+ query += " ORDER BY t.TRADEDATE DESC LIMIT %s"
+ params.append(limit)
+
+ await cursor.execute(query, params)
+ results = await cursor.fetchall()
+
+ return [convert_row(row) for row in results]
+
+
+async def get_stock_comparison(
+ seccodes: List[str],
+ metric: str = "financial"
+) -> Dict[str, Any]:
+ """
+ 股票对比分析
+
+ Args:
+ seccodes: 股票代码列表
+ metric: 对比指标类型 (financial/trade)
+
+ Returns:
+ 对比数据
+ """
+ pool = await get_pool()
+
+ if not seccodes or len(seccodes) < 2:
+ return {"error": "至少需要2个股票代码进行对比"}
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ placeholders = ','.join(['%s'] * len(seccodes))
+
+ if metric == "financial":
+ # 对比最新财务指标
+ query = f"""
+ SELECT
+ f.SECCODE, f.SECNAME, f.ENDDATE,
+ f.F003N as eps,
+ f.F008N as bps,
+ f.F014N as roe,
+ f.F017N as net_profit_margin,
+ f.F041N as debt_ratio,
+ f.F052N as revenue_growth,
+ f.F053N as profit_growth,
+ f.F089N as revenue,
+ f.F101N as net_profit
+ FROM ea_financialindex f
+ INNER JOIN (
+ SELECT SECCODE, MAX(ENDDATE) as max_date
+ FROM ea_financialindex
+ WHERE SECCODE IN ({placeholders})
+ GROUP BY SECCODE
+ ) latest ON f.SECCODE = latest.SECCODE
+ AND f.ENDDATE = latest.max_date
+ """
+ else: # trade
+ # 对比最新交易数据
+ query = f"""
+ SELECT
+ t.SECCODE, t.SECNAME, t.TRADEDATE,
+ t.F007N as close_price,
+ t.F010N as change_pct,
+ t.F012N as turnover_rate,
+ t.F026N as pe_ratio,
+ t.F020N as total_shares,
+ t.F021N as circulating_shares
+ FROM ea_trade t
+ INNER JOIN (
+ SELECT SECCODE, MAX(TRADEDATE) as max_date
+ FROM ea_trade
+ WHERE SECCODE IN ({placeholders})
+ GROUP BY SECCODE
+ ) latest ON t.SECCODE = latest.SECCODE
+ AND t.TRADEDATE = latest.max_date
+ """
+
+ await cursor.execute(query, seccodes)
+ results = await cursor.fetchall()
+
+ return {
+ "comparison_type": metric,
+ "stocks": [convert_row(row) for row in results]
+ }
diff --git a/mcp_server.py b/mcp_server.py
new file mode 100644
index 00000000..534816b5
--- /dev/null
+++ b/mcp_server.py
@@ -0,0 +1,1541 @@
+"""
+MCP Server for Financial Data Search
+基于FastAPI的MCP服务端,整合多个金融数据搜索API
+支持LLM调用和Web聊天功能
+"""
+
+from fastapi import FastAPI, HTTPException, Request
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel, Field
+from typing import List, Dict, Any, Optional, Literal
+from datetime import datetime, date
+import logging
+import httpx
+from enum import Enum
+import mcp_database as db
+from openai import OpenAI
+import json
+
+# 配置日志
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# 创建FastAPI应用
+app = FastAPI(
+ title="Financial Data MCP Server",
+ description="Model Context Protocol server for financial data search and analysis",
+ version="1.0.0"
+)
+
+# 添加CORS中间件
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# ==================== 配置 ====================
+
+class ServiceEndpoints:
+ """API服务端点配置"""
+ NEWS_API = "http://222.128.1.157:21891" # 新闻API
+ ROADSHOW_API = "http://222.128.1.157:19800" # 路演API
+ CONCEPT_API = "http://localhost:6801" # 概念API(本地)
+ STOCK_ANALYSIS_API = "http://222.128.1.157:8811" # 涨停分析+研报API
+
+# HTTP客户端配置
+HTTP_CLIENT = httpx.AsyncClient(timeout=60.0)
+
+# ==================== Agent系统配置 ====================
+
+# Kimi 配置 - 用于计划制定和深度推理
+KIMI_CONFIG = {
+ "api_key": "sk-TzB4VYJfCoXGcGrGMiewukVRzjuDsbVCkaZXi2LvkS8s60E5",
+ "base_url": "https://api.moonshot.cn/v1",
+ "model": "kimi-k2-thinking", # 思考模型
+}
+
+# DeepMoney 配置 - 用于新闻总结
+DEEPMONEY_CONFIG = {
+ "api_key": "", # 空值
+ "base_url": "http://111.62.35.50:8000/v1",
+ "model": "deepmoney",
+}
+
+# ==================== MCP协议数据模型 ====================
+
+class ToolParameter(BaseModel):
+ """工具参数定义"""
+ type: str
+ description: str
+ enum: Optional[List[str]] = None
+ default: Optional[Any] = None
+
+class ToolDefinition(BaseModel):
+ """工具定义"""
+ name: str
+ description: str
+ parameters: Dict[str, Any] # 支持完整的 JSON Schema 格式
+
+class ToolCallRequest(BaseModel):
+ """工具调用请求"""
+ tool: str
+ arguments: Dict[str, Any] = {}
+
+class ToolCallResponse(BaseModel):
+ """工具调用响应"""
+ success: bool
+ data: Optional[Any] = None
+ error: Optional[str] = None
+ metadata: Optional[Dict[str, Any]] = None
+
+# ==================== Agent系统数据模型 ====================
+
+class ToolCall(BaseModel):
+ """工具调用"""
+ tool: str
+ arguments: Dict[str, Any]
+ reason: str
+
+class ExecutionPlan(BaseModel):
+ """执行计划"""
+ goal: str
+ steps: List[ToolCall]
+ reasoning: str
+
+class StepResult(BaseModel):
+ """单步执行结果"""
+ step_index: int
+ tool: str
+ arguments: Dict[str, Any]
+ status: Literal["success", "failed", "skipped"]
+ result: Optional[Any] = None
+ error: Optional[str] = None
+ execution_time: float = 0
+
+class AgentResponse(BaseModel):
+ """Agent响应"""
+ success: bool
+ message: str
+ plan: Optional[ExecutionPlan] = None
+ step_results: List[StepResult] = []
+ final_summary: Optional[str] = None
+ metadata: Optional[Dict[str, Any]] = None
+
+class ConversationMessage(BaseModel):
+ """对话历史消息"""
+ isUser: bool
+ content: str
+
+class AgentChatRequest(BaseModel):
+ """聊天请求"""
+ message: str
+ conversation_history: List[ConversationMessage] = []
+
+# ==================== MCP工具定义 ====================
+
+TOOLS: List[ToolDefinition] = [
+ ToolDefinition(
+ name="search_news",
+ description="搜索全球新闻,支持关键词搜索和日期过滤。适用于查找国际新闻、行业动态等。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "搜索关键词,例如:'人工智能'、'新能源汽车'"
+ },
+ "source": {
+ "type": "string",
+ "description": "新闻来源筛选,可选"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "top_k": {
+ "type": "integer",
+ "description": "返回结果数量,默认20",
+ "default": 20
+ }
+ },
+ "required": ["query"]
+ }
+ ),
+ ToolDefinition(
+ name="search_china_news",
+ description="搜索中国新闻,使用KNN语义搜索。支持精确匹配模式,适合查找股票、公司相关新闻。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "搜索关键词"
+ },
+ "exact_match": {
+ "type": "boolean",
+ "description": "是否精确匹配(用于股票代码、公司名称等),默认false",
+ "default": False
+ },
+ "source": {
+ "type": "string",
+ "description": "新闻来源筛选"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "top_k": {
+ "type": "integer",
+ "description": "返回结果数量,默认20",
+ "default": 20
+ }
+ },
+ "required": ["query"]
+ }
+ ),
+ ToolDefinition(
+ name="search_medical_news",
+ description="搜索医疗健康类新闻,包括医药、医疗设备、生物技术等领域。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "搜索关键词"
+ },
+ "source": {
+ "type": "string",
+ "description": "新闻来源"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "top_k": {
+ "type": "integer",
+ "description": "返回结果数量",
+ "default": 10
+ }
+ },
+ "required": ["query"]
+ }
+ ),
+ ToolDefinition(
+ name="search_roadshows",
+ description="搜索上市公司路演、投资者交流活动记录。可按公司代码、日期范围搜索。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "搜索关键词,可以是公司名称、主题等"
+ },
+ "company_code": {
+ "type": "string",
+ "description": "公司股票代码,例如:'600519.SH'"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS"
+ },
+ "size": {
+ "type": "integer",
+ "description": "返回结果数量",
+ "default": 10
+ }
+ },
+ "required": ["query"]
+ }
+ ),
+ ToolDefinition(
+ name="search_concepts",
+ description="搜索股票概念板块,支持按涨跌幅、股票数量排序。返回概念详情及相关股票列表。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "搜索关键词,例如:'新能源'、'人工智能'"
+ },
+ "size": {
+ "type": "integer",
+ "description": "每页结果数量",
+ "default": 10
+ },
+ "page": {
+ "type": "integer",
+ "description": "页码",
+ "default": 1
+ },
+ "sort_by": {
+ "type": "string",
+ "description": "排序方式:change_pct(涨跌幅), _score(相关度), stock_count(股票数), concept_name(名称)",
+ "enum": ["change_pct", "_score", "stock_count", "concept_name"],
+ "default": "change_pct"
+ },
+ "trade_date": {
+ "type": "string",
+ "description": "交易日期,格式:YYYY-MM-DD,默认最新"
+ }
+ },
+ "required": ["query"]
+ }
+ ),
+ ToolDefinition(
+ name="get_concept_details",
+ description="根据概念ID获取详细信息,包括描述、相关股票、涨跌幅数据等。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "concept_id": {
+ "type": "string",
+ "description": "概念ID"
+ },
+ "trade_date": {
+ "type": "string",
+ "description": "交易日期,格式:YYYY-MM-DD"
+ }
+ },
+ "required": ["concept_id"]
+ }
+ ),
+ ToolDefinition(
+ name="get_stock_concepts",
+ description="查询指定股票的所有相关概念板块,包括涨跌幅信息。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "stock_code": {
+ "type": "string",
+ "description": "股票代码或名称"
+ },
+ "size": {
+ "type": "integer",
+ "description": "返回概念数量",
+ "default": 50
+ },
+ "sort_by": {
+ "type": "string",
+ "description": "排序方式",
+ "enum": ["stock_count", "concept_name", "recent"],
+ "default": "stock_count"
+ },
+ "trade_date": {
+ "type": "string",
+ "description": "交易日期,格式:YYYY-MM-DD"
+ }
+ },
+ "required": ["stock_code"]
+ }
+ ),
+ ToolDefinition(
+ name="get_concept_statistics",
+ description="获取概念板块统计数据,包括涨幅榜、跌幅榜、活跃榜、波动榜、连涨榜。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "days": {
+ "type": "integer",
+ "description": "统计天数(与start_date/end_date互斥)"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "min_stock_count": {
+ "type": "integer",
+ "description": "最少股票数量过滤",
+ "default": 3
+ }
+ },
+ "required": []
+ }
+ ),
+ ToolDefinition(
+ name="search_limit_up_stocks",
+ description="搜索涨停股票,支持按日期、关键词、板块等条件搜索。包括混合语义搜索。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "搜索关键词(涨停原因、公司名称等)"
+ },
+ "date": {
+ "type": "string",
+ "description": "日期,格式:YYYYMMDD"
+ },
+ "mode": {
+ "type": "string",
+ "description": "搜索模式",
+ "enum": ["hybrid", "text", "vector"],
+ "default": "hybrid"
+ },
+ "sectors": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "板块筛选"
+ },
+ "page_size": {
+ "type": "integer",
+ "description": "每页结果数",
+ "default": 20
+ }
+ },
+ "required": ["query"]
+ }
+ ),
+ ToolDefinition(
+ name="get_daily_stock_analysis",
+ description="获取指定日期的涨停股票分析,包括板块分析、词云、趋势图表等。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "date": {
+ "type": "string",
+ "description": "日期,格式:YYYYMMDD"
+ }
+ },
+ "required": ["date"]
+ }
+ ),
+ ToolDefinition(
+ name="search_research_reports",
+ description="搜索研究报告,支持文本和语义混合搜索。可按作者、证券、日期等筛选。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "搜索关键词"
+ },
+ "mode": {
+ "type": "string",
+ "description": "搜索模式",
+ "enum": ["hybrid", "text", "vector"],
+ "default": "hybrid"
+ },
+ "exact_match": {
+ "type": "string",
+ "description": "是否精确匹配:0=模糊,1=精确",
+ "enum": ["0", "1"],
+ "default": "0"
+ },
+ "security_code": {
+ "type": "string",
+ "description": "证券代码筛选"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "size": {
+ "type": "integer",
+ "description": "返回结果数量",
+ "default": 10
+ }
+ },
+ "required": ["query"]
+ }
+ ),
+ ToolDefinition(
+ name="get_stock_basic_info",
+ description="获取股票基本信息,包括公司名称、行业、地址、主营业务、高管等基础数据。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "seccode": {
+ "type": "string",
+ "description": "股票代码,例如:600519"
+ }
+ },
+ "required": ["seccode"]
+ }
+ ),
+ ToolDefinition(
+ name="get_stock_financial_index",
+ description="获取股票财务指标,包括每股收益、净资产收益率、营收增长率等关键财务数据。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "seccode": {
+ "type": "string",
+ "description": "股票代码"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "limit": {
+ "type": "integer",
+ "description": "返回条数,默认10",
+ "default": 10
+ }
+ },
+ "required": ["seccode"]
+ }
+ ),
+ ToolDefinition(
+ name="get_stock_trade_data",
+ description="获取股票交易数据,包括价格、成交量、涨跌幅、换手率等日线行情数据。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "seccode": {
+ "type": "string",
+ "description": "股票代码"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "limit": {
+ "type": "integer",
+ "description": "返回条数,默认30",
+ "default": 30
+ }
+ },
+ "required": ["seccode"]
+ }
+ ),
+ ToolDefinition(
+ name="get_stock_balance_sheet",
+ description="获取股票资产负债表,包括资产、负债、所有者权益等财务状况数据。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "seccode": {
+ "type": "string",
+ "description": "股票代码"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "limit": {
+ "type": "integer",
+ "description": "返回条数,默认8",
+ "default": 8
+ }
+ },
+ "required": ["seccode"]
+ }
+ ),
+ ToolDefinition(
+ name="get_stock_cashflow",
+ description="获取股票现金流量表,包括经营、投资、筹资活动现金流数据。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "seccode": {
+ "type": "string",
+ "description": "股票代码"
+ },
+ "start_date": {
+ "type": "string",
+ "description": "开始日期,格式:YYYY-MM-DD"
+ },
+ "end_date": {
+ "type": "string",
+ "description": "结束日期,格式:YYYY-MM-DD"
+ },
+ "limit": {
+ "type": "integer",
+ "description": "返回条数,默认8",
+ "default": 8
+ }
+ },
+ "required": ["seccode"]
+ }
+ ),
+ ToolDefinition(
+ name="search_stocks_by_criteria",
+ description="按条件搜索股票,支持按行业、地区、市值等条件筛选股票列表。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "industry": {
+ "type": "string",
+ "description": "行业名称,支持模糊匹配"
+ },
+ "province": {
+ "type": "string",
+ "description": "省份名称"
+ },
+ "min_market_cap": {
+ "type": "number",
+ "description": "最小市值(亿元)"
+ },
+ "max_market_cap": {
+ "type": "number",
+ "description": "最大市值(亿元)"
+ },
+ "limit": {
+ "type": "integer",
+ "description": "返回条数,默认50",
+ "default": 50
+ }
+ },
+ "required": []
+ }
+ ),
+ ToolDefinition(
+ name="get_stock_comparison",
+ description="股票对比分析,支持多只股票的财务指标或交易数据对比。",
+ parameters={
+ "type": "object",
+ "properties": {
+ "seccodes": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "股票代码列表,至少2个"
+ },
+ "metric": {
+ "type": "string",
+ "description": "对比指标类型",
+ "enum": ["financial", "trade"],
+ "default": "financial"
+ }
+ },
+ "required": ["seccodes"]
+ }
+ ),
+]
+
+# ==================== MCP协议端点 ====================
+
+@app.get("/")
+async def root():
+ """服务根端点"""
+ return {
+ "name": "Financial Data MCP Server",
+ "version": "1.0.0",
+ "protocol": "MCP",
+ "description": "Model Context Protocol server for financial data search and analysis"
+ }
+
+@app.get("/tools")
+async def list_tools():
+ """列出所有可用工具"""
+ return {
+ "tools": [tool.dict() for tool in TOOLS]
+ }
+
+@app.get("/tools/{tool_name}")
+async def get_tool(tool_name: str):
+ """获取特定工具的定义"""
+ tool = next((t for t in TOOLS if t.name == tool_name), None)
+ if not tool:
+ raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")
+ return tool.dict()
+
+@app.post("/tools/call")
+async def call_tool(request: ToolCallRequest):
+ """调用工具"""
+ logger.info(f"Tool call: {request.tool} with args: {request.arguments}")
+
+ try:
+ # 路由到对应的工具处理函数
+ handler = TOOL_HANDLERS.get(request.tool)
+ if not handler:
+ raise HTTPException(status_code=404, detail=f"Tool '{request.tool}' not found")
+
+ result = await handler(request.arguments)
+
+ return ToolCallResponse(
+ success=True,
+ data=result,
+ metadata={
+ "tool": request.tool,
+ "timestamp": datetime.now().isoformat()
+ }
+ )
+
+ except Exception as e:
+ logger.error(f"Tool call error: {str(e)}", exc_info=True)
+ return ToolCallResponse(
+ success=False,
+ error=str(e),
+ metadata={
+ "tool": request.tool,
+ "timestamp": datetime.now().isoformat()
+ }
+ )
+
+# ==================== 工具处理函数 ====================
+
+async def handle_search_news(args: Dict[str, Any]) -> Any:
+ """处理新闻搜索"""
+ params = {
+ "query": args.get("query"),
+ "source": args.get("source"),
+ "start_date": args.get("start_date"),
+ "end_date": args.get("end_date"),
+ "top_k": args.get("top_k", 20)
+ }
+ # 移除None值
+ params = {k: v for k, v in params.items() if v is not None}
+
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_news", params=params)
+ response.raise_for_status()
+ return response.json()
+
+async def handle_search_china_news(args: Dict[str, Any]) -> Any:
+ """处理中国新闻搜索"""
+ params = {
+ "query": args.get("query"),
+ "exact_match": args.get("exact_match", False),
+ "source": args.get("source"),
+ "start_date": args.get("start_date"),
+ "end_date": args.get("end_date"),
+ "top_k": args.get("top_k", 20)
+ }
+ params = {k: v for k, v in params.items() if v is not None}
+
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_china_news", params=params)
+ response.raise_for_status()
+ return response.json()
+
+async def handle_search_medical_news(args: Dict[str, Any]) -> Any:
+ """处理医疗新闻搜索"""
+ params = {
+ "query": args["query"],
+ "source": args.get("source"),
+ "start_date": args.get("start_date"),
+ "end_date": args.get("end_date"),
+ "top_k": args.get("top_k", 10)
+ }
+ params = {k: v for k, v in params.items() if v is not None}
+
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_medical_news", params=params)
+ response.raise_for_status()
+ return response.json()
+
+async def handle_search_roadshows(args: Dict[str, Any]) -> Any:
+ """处理路演搜索"""
+ params = {
+ "query": args["query"],
+ "company_code": args.get("company_code"),
+ "start_date": args.get("start_date"),
+ "end_date": args.get("end_date"),
+ "size": args.get("size", 10)
+ }
+ params = {k: v for k, v in params.items() if v is not None}
+
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.ROADSHOW_API}/search", params=params)
+ response.raise_for_status()
+ return response.json()
+
+async def handle_search_concepts(args: Dict[str, Any]) -> Any:
+ """处理概念搜索"""
+ payload = {
+ "query": args["query"],
+ "size": args.get("size", 10),
+ "page": args.get("page", 1),
+ "search_size": 100,
+ "sort_by": args.get("sort_by", "change_pct"),
+ "use_knn": True
+ }
+ if args.get("trade_date"):
+ payload["trade_date"] = args["trade_date"]
+
+ response = await HTTP_CLIENT.post(f"{ServiceEndpoints.CONCEPT_API}/search", json=payload)
+ response.raise_for_status()
+ return response.json()
+
+async def handle_get_concept_details(args: Dict[str, Any]) -> Any:
+ """处理概念详情获取"""
+ concept_id = args["concept_id"]
+ params = {}
+ if args.get("trade_date"):
+ params["trade_date"] = args["trade_date"]
+
+ response = await HTTP_CLIENT.get(
+ f"{ServiceEndpoints.CONCEPT_API}/concept/{concept_id}",
+ params=params
+ )
+ response.raise_for_status()
+ return response.json()
+
+async def handle_get_stock_concepts(args: Dict[str, Any]) -> Any:
+ """处理股票概念获取"""
+ stock_code = args["stock_code"]
+ params = {
+ "size": args.get("size", 50),
+ "sort_by": args.get("sort_by", "stock_count"),
+ "include_description": True
+ }
+ if args.get("trade_date"):
+ params["trade_date"] = args["trade_date"]
+
+ response = await HTTP_CLIENT.get(
+ f"{ServiceEndpoints.CONCEPT_API}/stock/{stock_code}/concepts",
+ params=params
+ )
+ response.raise_for_status()
+ return response.json()
+
+async def handle_get_concept_statistics(args: Dict[str, Any]) -> Any:
+ """处理概念统计获取"""
+ params = {}
+ if args.get("days"):
+ params["days"] = args["days"]
+ if args.get("start_date"):
+ params["start_date"] = args["start_date"]
+ if args.get("end_date"):
+ params["end_date"] = args["end_date"]
+ if args.get("min_stock_count"):
+ params["min_stock_count"] = args["min_stock_count"]
+
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.CONCEPT_API}/statistics", params=params)
+ response.raise_for_status()
+ return response.json()
+
+async def handle_search_limit_up_stocks(args: Dict[str, Any]) -> Any:
+ """处理涨停股票搜索"""
+ payload = {
+ "query": args["query"],
+ "mode": args.get("mode", "hybrid"),
+ "page_size": args.get("page_size", 20)
+ }
+ if args.get("date"):
+ payload["date"] = args["date"]
+ if args.get("sectors"):
+ payload["sectors"] = args["sectors"]
+
+ response = await HTTP_CLIENT.post(
+ f"{ServiceEndpoints.STOCK_ANALYSIS_API}/api/v1/stocks/search/hybrid",
+ json=payload
+ )
+ response.raise_for_status()
+ return response.json()
+
+async def handle_get_daily_stock_analysis(args: Dict[str, Any]) -> Any:
+ """处理每日股票分析获取"""
+ date = args["date"]
+ response = await HTTP_CLIENT.get(
+ f"{ServiceEndpoints.STOCK_ANALYSIS_API}/api/v1/analysis/daily/{date}"
+ )
+ response.raise_for_status()
+ return response.json()
+
+async def handle_search_research_reports(args: Dict[str, Any]) -> Any:
+ """处理研报搜索"""
+ params = {
+ "query": args["query"],
+ "mode": args.get("mode", "hybrid"),
+ "exact_match": args.get("exact_match", "0"),
+ "size": args.get("size", 10)
+ }
+ if args.get("security_code"):
+ params["security_code"] = args["security_code"]
+ if args.get("start_date"):
+ params["start_date"] = args["start_date"]
+ if args.get("end_date"):
+ params["end_date"] = args["end_date"]
+
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.STOCK_ANALYSIS_API}/search", params=params)
+ response.raise_for_status()
+ return response.json()
+
+async def handle_get_stock_basic_info(args: Dict[str, Any]) -> Any:
+ """处理股票基本信息查询"""
+ seccode = args["seccode"]
+ result = await db.get_stock_basic_info(seccode)
+ if result:
+ return {"success": True, "data": result}
+ else:
+ return {"success": False, "error": f"未找到股票代码 {seccode} 的信息"}
+
+async def handle_get_stock_financial_index(args: Dict[str, Any]) -> Any:
+ """处理股票财务指标查询"""
+ seccode = args["seccode"]
+ start_date = args.get("start_date")
+ end_date = args.get("end_date")
+ limit = args.get("limit", 10)
+
+ result = await db.get_stock_financial_index(seccode, start_date, end_date, limit)
+ return {
+ "success": True,
+ "data": result,
+ "count": len(result)
+ }
+
+async def handle_get_stock_trade_data(args: Dict[str, Any]) -> Any:
+ """处理股票交易数据查询"""
+ seccode = args["seccode"]
+ start_date = args.get("start_date")
+ end_date = args.get("end_date")
+ limit = args.get("limit", 30)
+
+ result = await db.get_stock_trade_data(seccode, start_date, end_date, limit)
+ return {
+ "success": True,
+ "data": result,
+ "count": len(result)
+ }
+
+async def handle_get_stock_balance_sheet(args: Dict[str, Any]) -> Any:
+ """处理资产负债表查询"""
+ seccode = args["seccode"]
+ start_date = args.get("start_date")
+ end_date = args.get("end_date")
+ limit = args.get("limit", 8)
+
+ result = await db.get_stock_balance_sheet(seccode, start_date, end_date, limit)
+ return {
+ "success": True,
+ "data": result,
+ "count": len(result)
+ }
+
+async def handle_get_stock_cashflow(args: Dict[str, Any]) -> Any:
+ """处理现金流量表查询"""
+ seccode = args["seccode"]
+ start_date = args.get("start_date")
+ end_date = args.get("end_date")
+ limit = args.get("limit", 8)
+
+ result = await db.get_stock_cashflow(seccode, start_date, end_date, limit)
+ return {
+ "success": True,
+ "data": result,
+ "count": len(result)
+ }
+
+async def handle_search_stocks_by_criteria(args: Dict[str, Any]) -> Any:
+ """处理按条件搜索股票"""
+ industry = args.get("industry")
+ province = args.get("province")
+ min_market_cap = args.get("min_market_cap")
+ max_market_cap = args.get("max_market_cap")
+ limit = args.get("limit", 50)
+
+ result = await db.search_stocks_by_criteria(
+ industry, province, min_market_cap, max_market_cap, limit
+ )
+ return {
+ "success": True,
+ "data": result,
+ "count": len(result)
+ }
+
+async def handle_get_stock_comparison(args: Dict[str, Any]) -> Any:
+ """处理股票对比分析"""
+ seccodes = args["seccodes"]
+ metric = args.get("metric", "financial")
+
+ result = await db.get_stock_comparison(seccodes, metric)
+ return {
+ "success": True,
+ "data": result
+ }
+
+# 工具处理函数映射
+TOOL_HANDLERS = {
+ "search_news": handle_search_news,
+ "search_china_news": handle_search_china_news,
+ "search_medical_news": handle_search_medical_news,
+ "search_roadshows": handle_search_roadshows,
+ "search_concepts": handle_search_concepts,
+ "get_concept_details": handle_get_concept_details,
+ "get_stock_concepts": handle_get_stock_concepts,
+ "get_concept_statistics": handle_get_concept_statistics,
+ "search_limit_up_stocks": handle_search_limit_up_stocks,
+ "get_daily_stock_analysis": handle_get_daily_stock_analysis,
+ "search_research_reports": handle_search_research_reports,
+ "get_stock_basic_info": handle_get_stock_basic_info,
+ "get_stock_financial_index": handle_get_stock_financial_index,
+ "get_stock_trade_data": handle_get_stock_trade_data,
+ "get_stock_balance_sheet": handle_get_stock_balance_sheet,
+ "get_stock_cashflow": handle_get_stock_cashflow,
+ "search_stocks_by_criteria": handle_search_stocks_by_criteria,
+ "get_stock_comparison": handle_get_stock_comparison,
+}
+
+# ==================== Agent系统实现 ====================
+
+class MCPAgentIntegrated:
+ """集成版 MCP Agent - 使用 Kimi 和 DeepMoney"""
+
+ def __init__(self):
+ # 初始化 Kimi 客户端(计划制定)
+ self.kimi_client = OpenAI(
+ api_key=KIMI_CONFIG["api_key"],
+ base_url=KIMI_CONFIG["base_url"],
+ )
+ self.kimi_model = KIMI_CONFIG["model"]
+
+ # 初始化 DeepMoney 客户端(新闻总结)
+ self.deepmoney_client = OpenAI(
+ api_key=DEEPMONEY_CONFIG["api_key"],
+ base_url=DEEPMONEY_CONFIG["base_url"],
+ )
+ self.deepmoney_model = DEEPMONEY_CONFIG["model"]
+
+ def get_planning_prompt(self, tools: List[dict]) -> str:
+ """获取计划制定的系统提示词"""
+ tools_desc = "\n\n".join([
+ f"**{tool['name']}**\n"
+ f"描述:{tool['description']}\n"
+ f"参数:{json.dumps(tool['parameters'], ensure_ascii=False, indent=2)}"
+ for tool in tools
+ ])
+
+ return f"""你是一个专业的金融研究助手。根据用户问题,制定详细的执行计划。
+
+## 可用工具
+
+{tools_desc}
+
+## 特殊工具
+- **summarize_news**: 使用 DeepMoney 模型总结新闻数据
+ - 参数: {{"data": "新闻列表JSON", "focus": "关注点"}}
+ - 适用场景: 当需要总结新闻、研报等文本数据时
+
+## 重要知识
+- 贵州茅台: 600519
+- 涨停: 涨幅约10%
+- 概念板块: 相同题材股票分类
+
+## 任务
+分析用户问题,制定执行计划。返回 JSON:
+
+```json
+{{
+ "goal": "用户目标",
+ "reasoning": "分析思路",
+ "steps": [
+ {{
+ "tool": "工具名",
+ "arguments": {{"参数": "值"}},
+ "reason": "原因"
+ }}
+ ]
+}}
+```
+
+## 规划原则
+1. 先收集数据,再分析总结
+2. 使用 summarize_news 总结新闻类数据
+3. 不超过5个步骤
+4. 最后一步通常是总结
+
+## 示例
+
+用户:"贵州茅台最近有什么新闻"
+
+计划:
+```json
+{{
+ "goal": "查询并总结贵州茅台最新新闻",
+ "reasoning": "先搜索新闻,再用 DeepMoney 总结",
+ "steps": [
+ {{
+ "tool": "search_china_news",
+ "arguments": {{"query": "贵州茅台", "top_k": 10}},
+ "reason": "搜索贵州茅台相关新闻"
+ }},
+ {{
+ "tool": "summarize_news",
+ "arguments": {{
+ "data": "前面的新闻数据",
+ "focus": "贵州茅台的重要动态和市场影响"
+ }},
+ "reason": "使用DeepMoney总结新闻要点"
+ }}
+ ]
+}}
+```
+
+只返回JSON,不要其他内容。"""
+
+ async def create_plan(self, user_query: str, tools: List[dict]) -> ExecutionPlan:
+ """阶段1: 使用 Kimi 创建执行计划(带思考过程)"""
+ logger.info(f"[Planning] Kimi开始制定计划: {user_query}")
+
+ messages = [
+ {"role": "system", "content": self.get_planning_prompt(tools)},
+ {"role": "user", "content": user_query},
+ ]
+
+ # 使用 Kimi 思考模型
+ response = self.kimi_client.chat.completions.create(
+ model=self.kimi_model,
+ messages=messages,
+ temperature=1.0, # Kimi 推荐
+ max_tokens=16000, # 足够容纳 reasoning_content
+ )
+
+ choice = response.choices[0]
+ message = choice.message
+
+ # 提取思考过程
+ reasoning_content = ""
+ if hasattr(message, "reasoning_content"):
+ reasoning_content = getattr(message, "reasoning_content")
+ logger.info(f"[Planning] Kimi思考过程: {reasoning_content[:200]}...")
+
+ # 提取计划内容
+ plan_json = message.content.strip()
+
+ # 清理可能的代码块标记
+ if "```json" in plan_json:
+ plan_json = plan_json.split("```json")[1].split("```")[0].strip()
+ elif "```" in plan_json:
+ plan_json = plan_json.split("```")[1].split("```")[0].strip()
+
+ plan_data = json.loads(plan_json)
+
+ plan = ExecutionPlan(
+ goal=plan_data["goal"],
+ reasoning=plan_data.get("reasoning", "") + "\n\n" + (reasoning_content[:500] if reasoning_content else ""),
+ steps=[ToolCall(**step) for step in plan_data["steps"]],
+ )
+
+ logger.info(f"[Planning] 计划制定完成: {len(plan.steps)} 步")
+ return plan
+
+ async def execute_tool(
+ self,
+ tool_name: str,
+ arguments: Dict[str, Any],
+ tool_handlers: Dict[str, Any],
+ ) -> Dict[str, Any]:
+ """执行单个工具"""
+
+ # 特殊工具:summarize_news(使用 DeepMoney)
+ if tool_name == "summarize_news":
+ return await self.summarize_news_with_deepmoney(
+ data=arguments.get("data", ""),
+ focus=arguments.get("focus", "关键信息"),
+ )
+
+ # 调用 MCP 工具
+ handler = tool_handlers.get(tool_name)
+ if not handler:
+ raise ValueError(f"Tool '{tool_name}' not found")
+
+ result = await handler(arguments)
+ return result
+
+ async def summarize_news_with_deepmoney(self, data: str, focus: str) -> str:
+ """使用 DeepMoney 模型总结新闻"""
+ logger.info(f"[DeepMoney] 总结新闻,关注点: {focus}")
+
+ messages = [
+ {
+ "role": "system",
+ "content": "你是一个专业的金融新闻分析师,擅长提取关键信息并进行总结。"
+ },
+ {
+ "role": "user",
+ "content": f"请总结以下新闻数据,关注点:{focus}\n\n数据:\n{data[:3000]}"
+ },
+ ]
+
+ try:
+ response = self.deepmoney_client.chat.completions.create(
+ model=self.deepmoney_model,
+ messages=messages,
+ temperature=0.7,
+ max_tokens=1000,
+ )
+
+ summary = response.choices[0].message.content
+ logger.info(f"[DeepMoney] 总结完成")
+ return summary
+
+ except Exception as e:
+ logger.error(f"[DeepMoney] 总结失败: {str(e)}")
+ # 降级:返回简化摘要
+ return f"新闻总结失败,原始数据:{data[:500]}..."
+
+ async def execute_plan(
+ self,
+ plan: ExecutionPlan,
+ tool_handlers: Dict[str, Any],
+ ) -> List[StepResult]:
+ """阶段2: 执行计划"""
+ logger.info(f"[Execution] 开始执行: {len(plan.steps)} 步")
+
+ results = []
+ collected_data = {}
+
+ for i, step in enumerate(plan.steps):
+ logger.info(f"[Execution] 步骤 {i+1}/{len(plan.steps)}: {step.tool}")
+
+ 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,
+ )
+ results.append(step_result)
+
+ # 收集数据
+ collected_data[f"step_{i+1}_{step.tool}"] = result
+
+ logger.info(f"[Execution] 步骤 {i+1} 完成: {execution_time:.2f}s")
+
+ except Exception as e:
+ logger.error(f"[Execution] 步骤 {i+1} 失败: {str(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,
+ )
+ results.append(step_result)
+
+ # 继续执行其他步骤
+ continue
+
+ logger.info(f"[Execution] 执行完成")
+ return results
+
+ async def generate_final_summary(
+ self,
+ user_query: str,
+ plan: ExecutionPlan,
+ step_results: List[StepResult],
+ ) -> str:
+ """阶段3: 使用 Kimi 生成最终总结"""
+ logger.info("[Summary] Kimi生成最终总结")
+
+ # 收集成功的结果
+ successful_results = [r for r in step_results if r.status == "success"]
+
+ if not successful_results:
+ return "很抱歉,所有步骤都执行失败,无法生成分析报告。"
+
+ # 构建结果文本(精简版)
+ results_text = "\n\n".join([
+ f"**步骤 {r.step_index + 1}: {r.tool}**\n"
+ f"结果: {str(r.result)[:800]}..."
+ for r in successful_results[:3] # 只取前3个,避免超长
+ ])
+
+ messages = [
+ {
+ "role": "system",
+ "content": "你是专业的金融研究助手。根据执行结果,生成简洁清晰的报告。"
+ },
+ {
+ "role": "user",
+ "content": f"""用户问题:{user_query}
+
+执行计划:{plan.goal}
+
+执行结果:
+{results_text}
+
+请生成专业的分析报告(300字以内)。"""
+ },
+ ]
+
+ try:
+ response = self.kimi_client.chat.completions.create(
+ model="kimi-k2-turbo-preview", # 使用非思考模型,更快
+ messages=messages,
+ temperature=0.7,
+ max_tokens=1000,
+ )
+
+ summary = response.choices[0].message.content
+ logger.info("[Summary] 总结完成")
+ return summary
+
+ except Exception as e:
+ logger.error(f"[Summary] 总结失败: {str(e)}")
+ # 降级:返回最后一步的结果
+ if successful_results:
+ last_result = successful_results[-1]
+ if isinstance(last_result.result, str):
+ return last_result.result
+ else:
+ return json.dumps(last_result.result, ensure_ascii=False, indent=2)
+ return "总结生成失败"
+
+ async def process_query(
+ self,
+ user_query: str,
+ tools: List[dict],
+ tool_handlers: Dict[str, Any],
+ ) -> AgentResponse:
+ """主流程"""
+ logger.info(f"[Agent] 处理查询: {user_query}")
+
+ try:
+ # 阶段1: Kimi 制定计划
+ plan = await self.create_plan(user_query, tools)
+
+ # 阶段2: 执行工具
+ step_results = await self.execute_plan(plan, tool_handlers)
+
+ # 阶段3: Kimi 生成总结
+ final_summary = await self.generate_final_summary(
+ user_query, plan, step_results
+ )
+
+ return AgentResponse(
+ success=True,
+ message=final_summary,
+ plan=plan,
+ step_results=step_results,
+ final_summary=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),
+ "model_used": {
+ "planning": self.kimi_model,
+ "summarization": "kimi-k2-turbo-preview",
+ "news_summary": self.deepmoney_model,
+ },
+ },
+ )
+
+ except Exception as e:
+ logger.error(f"[Agent] 错误: {str(e)}", exc_info=True)
+ return AgentResponse(
+ success=False,
+ message=f"处理失败: {str(e)}",
+ )
+
+# 创建 Agent 实例(全局)
+agent = MCPAgentIntegrated()
+
+# ==================== Web聊天接口 ====================
+
+class ChatMessage(BaseModel):
+ """聊天消息"""
+ role: Literal["user", "assistant", "system"]
+ content: str
+
+class ChatRequest(BaseModel):
+ """聊天请求"""
+ messages: List[ChatMessage]
+ stream: bool = False
+
+@app.post("/chat")
+async def chat(request: ChatRequest):
+ """
+ Web聊天接口
+
+ 这是一个简化的接口,实际应该集成LLM API(如OpenAI、Claude等)
+ 这里只是演示如何使用工具
+ """
+ # TODO: 集成实际的LLM API
+ # 1. 将消息发送给LLM
+ # 2. LLM返回需要调用的工具
+ # 3. 调用工具并获取结果
+ # 4. 将工具结果返回给LLM
+ # 5. LLM生成最终回复
+
+ return {
+ "message": "Chat endpoint placeholder - integrate with your LLM provider",
+ "available_tools": len(TOOLS),
+ "hint": "Use POST /tools/call to invoke tools"
+ }
+
+@app.post("/agent/chat", response_model=AgentResponse)
+async def agent_chat(request: AgentChatRequest):
+ """智能代理对话端点"""
+ logger.info(f"Agent chat: {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"]
+ }
+ })
+
+ # 处理查询
+ response = await agent.process_query(
+ user_query=request.message,
+ tools=tools,
+ tool_handlers=TOOL_HANDLERS,
+ )
+
+ return response
+
+# ==================== 健康检查 ====================
+
+@app.get("/health")
+async def health_check():
+ """健康检查"""
+ # 检查各个后端服务的健康状态
+ services_status = {}
+
+ try:
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_news?query=test&top_k=1", timeout=5.0)
+ services_status["news_api"] = "healthy" if response.status_code == 200 else "unhealthy"
+ except:
+ services_status["news_api"] = "unhealthy"
+
+ try:
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.CONCEPT_API}/", timeout=5.0)
+ services_status["concept_api"] = "healthy" if response.status_code == 200 else "unhealthy"
+ except:
+ services_status["concept_api"] = "unhealthy"
+
+ try:
+ response = await HTTP_CLIENT.get(f"{ServiceEndpoints.STOCK_ANALYSIS_API}/api/v1/health", timeout=5.0)
+ services_status["stock_analysis_api"] = "healthy" if response.status_code == 200 else "unhealthy"
+ except:
+ services_status["stock_analysis_api"] = "unhealthy"
+
+ return {
+ "status": "healthy",
+ "timestamp": datetime.now().isoformat(),
+ "services": services_status
+ }
+
+# ==================== 错误处理 ====================
+
+@app.exception_handler(HTTPException)
+async def http_exception_handler(request: Request, exc: HTTPException):
+ """HTTP异常处理"""
+ return JSONResponse(
+ status_code=exc.status_code,
+ content={
+ "success": False,
+ "error": exc.detail,
+ "timestamp": datetime.now().isoformat()
+ }
+ )
+
+@app.exception_handler(Exception)
+async def general_exception_handler(request: Request, exc: Exception):
+ """通用异常处理"""
+ logger.error(f"Unexpected error: {str(exc)}", exc_info=True)
+ return JSONResponse(
+ status_code=500,
+ content={
+ "success": False,
+ "error": "Internal server error",
+ "detail": str(exc),
+ "timestamp": datetime.now().isoformat()
+ }
+ )
+
+# ==================== 应用启动/关闭 ====================
+
+@app.on_event("startup")
+async def startup_event():
+ """应用启动"""
+ logger.info("MCP Server starting up...")
+ logger.info(f"Registered {len(TOOLS)} tools")
+ # 初始化数据库连接池
+ try:
+ await db.get_pool()
+ logger.info("MySQL connection pool initialized")
+ except Exception as e:
+ logger.error(f"Failed to initialize MySQL pool: {e}")
+
+@app.on_event("shutdown")
+async def shutdown_event():
+ """应用关闭"""
+ logger.info("MCP Server shutting down...")
+ await HTTP_CLIENT.aclose()
+ # 关闭数据库连接池
+ try:
+ await db.close_pool()
+ logger.info("MySQL connection pool closed")
+ except Exception as e:
+ logger.error(f"Failed to close MySQL pool: {e}")
+
+# ==================== 主程序 ====================
+
+if __name__ == "__main__":
+ import uvicorn
+
+ uvicorn.run(
+ "mcp_server:app",
+ host="0.0.0.0",
+ port=8900,
+ reload=True,
+ log_level="info"
+ )
diff --git a/mcp_server_agent_integration.py b/mcp_server_agent_integration.py
new file mode 100644
index 00000000..fbb47f88
--- /dev/null
+++ b/mcp_server_agent_integration.py
@@ -0,0 +1,492 @@
+"""
+集成到 mcp_server.py 的 Agent 系统
+使用 Kimi (kimi-k2-thinking) 和 DeepMoney 两个模型
+"""
+
+from openai import OpenAI
+from pydantic import BaseModel
+from typing import List, Dict, Any, Optional, Literal
+from datetime import datetime
+import json
+import logging
+
+logger = logging.getLogger(__name__)
+
+# ==================== 模型配置 ====================
+
+# Kimi 配置 - 用于计划制定和深度推理
+KIMI_CONFIG = {
+ "api_key": "sk-TzB4VYJfCoXGcGrGMiewukVRzjuDsbVCkaZXi2LvkS8s60E5",
+ "base_url": "https://api.moonshot.cn/v1",
+ "model": "kimi-k2-thinking", # 思考模型
+}
+
+# DeepMoney 配置 - 用于新闻总结
+DEEPMONEY_CONFIG = {
+ "api_key": "", # 空值
+ "base_url": "http://111.62.35.50:8000/v1",
+ "model": "deepmoney",
+}
+
+# ==================== 数据模型 ====================
+
+class ToolCall(BaseModel):
+ """工具调用"""
+ tool: str
+ arguments: Dict[str, Any]
+ reason: str
+
+class ExecutionPlan(BaseModel):
+ """执行计划"""
+ goal: str
+ steps: List[ToolCall]
+ reasoning: str
+
+class StepResult(BaseModel):
+ """单步执行结果"""
+ step_index: int
+ tool: str
+ arguments: Dict[str, Any]
+ status: Literal["success", "failed", "skipped"]
+ result: Optional[Any] = None
+ error: Optional[str] = None
+ execution_time: float = 0
+
+class AgentResponse(BaseModel):
+ """Agent响应"""
+ success: bool
+ message: str
+ plan: Optional[ExecutionPlan] = None
+ step_results: List[StepResult] = []
+ final_summary: Optional[str] = None
+ metadata: Optional[Dict[str, Any]] = None
+
+class ChatRequest(BaseModel):
+ """聊天请求"""
+ message: str
+ conversation_history: List[Dict[str, str]] = []
+
+# ==================== Agent 系统 ====================
+
+class MCPAgentIntegrated:
+ """集成版 MCP Agent - 使用 Kimi 和 DeepMoney"""
+
+ def __init__(self):
+ # 初始化 Kimi 客户端(计划制定)
+ self.kimi_client = OpenAI(
+ api_key=KIMI_CONFIG["api_key"],
+ base_url=KIMI_CONFIG["base_url"],
+ )
+ self.kimi_model = KIMI_CONFIG["model"]
+
+ # 初始化 DeepMoney 客户端(新闻总结)
+ self.deepmoney_client = OpenAI(
+ api_key=DEEPMONEY_CONFIG["api_key"],
+ base_url=DEEPMONEY_CONFIG["base_url"],
+ )
+ self.deepmoney_model = DEEPMONEY_CONFIG["model"]
+
+ def get_planning_prompt(self, tools: List[dict]) -> str:
+ """获取计划制定的系统提示词"""
+ tools_desc = "\n\n".join([
+ f"**{tool['name']}**\n"
+ f"描述:{tool['description']}\n"
+ f"参数:{json.dumps(tool['parameters'], ensure_ascii=False, indent=2)}"
+ for tool in tools
+ ])
+
+ return f"""你是一个专业的金融研究助手。根据用户问题,制定详细的执行计划。
+
+## 可用工具
+
+{tools_desc}
+
+## 特殊工具
+- **summarize_news**: 使用 DeepMoney 模型总结新闻数据
+ - 参数: {{"data": "新闻列表JSON", "focus": "关注点"}}
+ - 适用场景: 当需要总结新闻、研报等文本数据时
+
+## 重要知识
+- 贵州茅台: 600519
+- 涨停: 涨幅约10%
+- 概念板块: 相同题材股票分类
+
+## 任务
+分析用户问题,制定执行计划。返回 JSON:
+
+```json
+{{
+ "goal": "用户目标",
+ "reasoning": "分析思路",
+ "steps": [
+ {{
+ "tool": "工具名",
+ "arguments": {{"参数": "值"}},
+ "reason": "原因"
+ }}
+ ]
+}}
+```
+
+## 规划原则
+1. 先收集数据,再分析总结
+2. 使用 summarize_news 总结新闻类数据
+3. 不超过5个步骤
+4. 最后一步通常是总结
+
+## 示例
+
+用户:"贵州茅台最近有什么新闻"
+
+计划:
+```json
+{{
+ "goal": "查询并总结贵州茅台最新新闻",
+ "reasoning": "先搜索新闻,再用 DeepMoney 总结",
+ "steps": [
+ {{
+ "tool": "search_china_news",
+ "arguments": {{"query": "贵州茅台", "top_k": 10}},
+ "reason": "搜索贵州茅台相关新闻"
+ }},
+ {{
+ "tool": "summarize_news",
+ "arguments": {{
+ "data": "前面的新闻数据",
+ "focus": "贵州茅台的重要动态和市场影响"
+ }},
+ "reason": "使用DeepMoney总结新闻要点"
+ }}
+ ]
+}}
+```
+
+只返回JSON,不要其他内容。"""
+
+ async def create_plan(self, user_query: str, tools: List[dict]) -> ExecutionPlan:
+ """阶段1: 使用 Kimi 创建执行计划(带思考过程)"""
+ logger.info(f"[Planning] Kimi开始制定计划: {user_query}")
+
+ messages = [
+ {"role": "system", "content": self.get_planning_prompt(tools)},
+ {"role": "user", "content": user_query},
+ ]
+
+ # 使用 Kimi 思考模型
+ response = self.kimi_client.chat.completions.create(
+ model=self.kimi_model,
+ messages=messages,
+ temperature=1.0, # Kimi 推荐
+ max_tokens=16000, # 足够容纳 reasoning_content
+ )
+
+ choice = response.choices[0]
+ message = choice.message
+
+ # 提取思考过程
+ reasoning_content = ""
+ if hasattr(message, "reasoning_content"):
+ reasoning_content = getattr(message, "reasoning_content")
+ logger.info(f"[Planning] Kimi思考过程: {reasoning_content[:200]}...")
+
+ # 提取计划内容
+ plan_json = message.content.strip()
+
+ # 清理可能的代码块标记
+ if "```json" in plan_json:
+ plan_json = plan_json.split("```json")[1].split("```")[0].strip()
+ elif "```" in plan_json:
+ plan_json = plan_json.split("```")[1].split("```")[0].strip()
+
+ plan_data = json.loads(plan_json)
+
+ plan = ExecutionPlan(
+ goal=plan_data["goal"],
+ reasoning=plan_data.get("reasoning", "") + "\n\n" + (reasoning_content[:500] if reasoning_content else ""),
+ steps=[ToolCall(**step) for step in plan_data["steps"]],
+ )
+
+ logger.info(f"[Planning] 计划制定完成: {len(plan.steps)} 步")
+ return plan
+
+ async def execute_tool(
+ self,
+ tool_name: str,
+ arguments: Dict[str, Any],
+ tool_handlers: Dict[str, Any],
+ ) -> Dict[str, Any]:
+ """执行单个工具"""
+
+ # 特殊工具:summarize_news(使用 DeepMoney)
+ if tool_name == "summarize_news":
+ return await self.summarize_news_with_deepmoney(
+ data=arguments.get("data", ""),
+ focus=arguments.get("focus", "关键信息"),
+ )
+
+ # 调用 MCP 工具
+ handler = tool_handlers.get(tool_name)
+ if not handler:
+ raise ValueError(f"Tool '{tool_name}' not found")
+
+ result = await handler(arguments)
+ return result
+
+ async def summarize_news_with_deepmoney(self, data: str, focus: str) -> str:
+ """使用 DeepMoney 模型总结新闻"""
+ logger.info(f"[DeepMoney] 总结新闻,关注点: {focus}")
+
+ messages = [
+ {
+ "role": "system",
+ "content": "你是一个专业的金融新闻分析师,擅长提取关键信息并进行总结。"
+ },
+ {
+ "role": "user",
+ "content": f"请总结以下新闻数据,关注点:{focus}\n\n数据:\n{data[:3000]}"
+ },
+ ]
+
+ try:
+ response = self.deepmoney_client.chat.completions.create(
+ model=self.deepmoney_model,
+ messages=messages,
+ temperature=0.7,
+ max_tokens=1000,
+ )
+
+ summary = response.choices[0].message.content
+ logger.info(f"[DeepMoney] 总结完成")
+ return summary
+
+ except Exception as e:
+ logger.error(f"[DeepMoney] 总结失败: {str(e)}")
+ # 降级:返回简化摘要
+ return f"新闻总结失败,原始数据:{data[:500]}..."
+
+ async def execute_plan(
+ self,
+ plan: ExecutionPlan,
+ tool_handlers: Dict[str, Any],
+ ) -> List[StepResult]:
+ """阶段2: 执行计划"""
+ logger.info(f"[Execution] 开始执行: {len(plan.steps)} 步")
+
+ results = []
+ collected_data = {}
+
+ for i, step in enumerate(plan.steps):
+ logger.info(f"[Execution] 步骤 {i+1}/{len(plan.steps)}: {step.tool}")
+
+ 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,
+ )
+ results.append(step_result)
+
+ # 收集数据
+ collected_data[f"step_{i+1}_{step.tool}"] = result
+
+ logger.info(f"[Execution] 步骤 {i+1} 完成: {execution_time:.2f}s")
+
+ except Exception as e:
+ logger.error(f"[Execution] 步骤 {i+1} 失败: {str(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,
+ )
+ results.append(step_result)
+
+ # 继续执行其他步骤
+ continue
+
+ logger.info(f"[Execution] 执行完成")
+ return results
+
+ async def generate_final_summary(
+ self,
+ user_query: str,
+ plan: ExecutionPlan,
+ step_results: List[StepResult],
+ ) -> str:
+ """阶段3: 使用 Kimi 生成最终总结"""
+ logger.info("[Summary] Kimi生成最终总结")
+
+ # 收集成功的结果
+ successful_results = [r for r in step_results if r.status == "success"]
+
+ if not successful_results:
+ return "很抱歉,所有步骤都执行失败,无法生成分析报告。"
+
+ # 构建结果文本(精简版)
+ results_text = "\n\n".join([
+ f"**步骤 {r.step_index + 1}: {r.tool}**\n"
+ f"结果: {str(r.result)[:800]}..."
+ for r in successful_results[:3] # 只取前3个,避免超长
+ ])
+
+ messages = [
+ {
+ "role": "system",
+ "content": "你是专业的金融研究助手。根据执行结果,生成简洁清晰的报告。"
+ },
+ {
+ "role": "user",
+ "content": f"""用户问题:{user_query}
+
+执行计划:{plan.goal}
+
+执行结果:
+{results_text}
+
+请生成专业的分析报告(300字以内)。"""
+ },
+ ]
+
+ try:
+ response = self.kimi_client.chat.completions.create(
+ model="kimi-k2-turbpreview", # 使用非思考模型,更快
+ messages=messages,
+ temperature=0.7,
+ max_tokens=1000,
+ )
+
+ summary = response.choices[0].message.content
+ logger.info("[Summary] 总结完成")
+ return summary
+
+ except Exception as e:
+ logger.error(f"[Summary] 总结失败: {str(e)}")
+ # 降级:返回最后一步的结果
+ if successful_results:
+ last_result = successful_results[-1]
+ if isinstance(last_result.result, str):
+ return last_result.result
+ else:
+ return json.dumps(last_result.result, ensure_ascii=False, indent=2)
+ return "总结生成失败"
+
+ async def process_query(
+ self,
+ user_query: str,
+ tools: List[dict],
+ tool_handlers: Dict[str, Any],
+ ) -> AgentResponse:
+ """主流程"""
+ logger.info(f"[Agent] 处理查询: {user_query}")
+
+ try:
+ # 阶段1: Kimi 制定计划
+ plan = await self.create_plan(user_query, tools)
+
+ # 阶段2: 执行工具
+ step_results = await self.execute_plan(plan, tool_handlers)
+
+ # 阶段3: Kimi 生成总结
+ final_summary = await self.generate_final_summary(
+ user_query, plan, step_results
+ )
+
+ return AgentResponse(
+ success=True,
+ message=final_summary,
+ plan=plan,
+ step_results=step_results,
+ final_summary=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),
+ "model_used": {
+ "planning": self.kimi_model,
+ "summarization": "kimi-k2-turbpreview",
+ "news_summary": self.deepmoney_model,
+ },
+ },
+ )
+
+ except Exception as e:
+ logger.error(f"[Agent] 错误: {str(e)}", exc_info=True)
+ return AgentResponse(
+ success=False,
+ message=f"处理失败: {str(e)}",
+ )
+
+# ==================== 添加到 mcp_server.py ====================
+
+"""
+在 mcp_server.py 中添加以下代码:
+
+# 导入 Agent 系统
+from mcp_server_agent_integration import MCPAgentIntegrated, ChatRequest, AgentResponse
+
+# 创建 Agent 实例(全局)
+agent = MCPAgentIntegrated()
+
+# 添加端点
+@app.post("/agent/chat", response_model=AgentResponse)
+async def agent_chat(request: ChatRequest):
+ \"\"\"智能代理对话端点\"\"\"
+ logger.info(f"Agent chat: {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"]
+ }
+ })
+
+ # 处理查询
+ response = await agent.process_query(
+ user_query=request.message,
+ tools=tools,
+ tool_handlers=TOOL_HANDLERS,
+ )
+
+ return response
+"""
diff --git a/src/components/ChatBot/ChatInterface.js b/src/components/ChatBot/ChatInterface.js
new file mode 100644
index 00000000..35984432
--- /dev/null
+++ b/src/components/ChatBot/ChatInterface.js
@@ -0,0 +1,376 @@
+// src/components/ChatBot/ChatInterface.js
+// 聊天界面主组件
+
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ Box,
+ Flex,
+ Input,
+ IconButton,
+ VStack,
+ HStack,
+ Text,
+ Spinner,
+ useColorModeValue,
+ useToast,
+ Divider,
+ Badge,
+ Menu,
+ MenuButton,
+ MenuList,
+ MenuItem,
+ Button,
+} from '@chakra-ui/react';
+import { FiSend, FiRefreshCw, FiSettings, FiDownload } from 'react-icons/fi';
+import { ChevronDownIcon } from '@chakra-ui/icons';
+import MessageBubble from './MessageBubble';
+import { mcpService } from '../../services/mcpService';
+import { logger } from '../../utils/logger';
+
+/**
+ * 聊天界面组件
+ */
+export const ChatInterface = () => {
+ const [messages, setMessages] = useState([
+ {
+ id: 1,
+ content: '你好!我是AI投资助手,我可以帮你查询股票信息、新闻资讯、概念板块、涨停分析等。请问有什么可以帮到你的?',
+ isUser: false,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ const [inputValue, setInputValue] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [availableTools, setAvailableTools] = useState([]);
+
+ const messagesEndRef = useRef(null);
+ const inputRef = useRef(null);
+ const toast = useToast();
+
+ // 颜色主题
+ const bgColor = useColorModeValue('white', 'gray.800');
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
+ const inputBg = useColorModeValue('gray.50', 'gray.700');
+
+ // 加载可用工具列表
+ useEffect(() => {
+ const loadTools = async () => {
+ const result = await mcpService.listTools();
+ if (result.success) {
+ setAvailableTools(result.data);
+ logger.info('ChatInterface', '已加载MCP工具', { count: result.data.length });
+ }
+ };
+ loadTools();
+ }, []);
+
+ // 自动滚动到底部
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+ // 发送消息
+ const handleSendMessage = async () => {
+ if (!inputValue.trim() || isLoading) return;
+
+ const userMessage = {
+ id: Date.now(),
+ content: inputValue,
+ isUser: true,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ };
+
+ setMessages((prev) => [...prev, userMessage]);
+ setInputValue('');
+ setIsLoading(true);
+
+ try {
+ // 调用MCP服务
+ const response = await mcpService.chat(inputValue, messages);
+
+ let botMessage;
+ if (response.success) {
+ // 根据返回的数据类型构造消息
+ const data = response.data;
+
+ if (typeof data === 'string') {
+ botMessage = {
+ id: Date.now() + 1,
+ content: data,
+ isUser: false,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ };
+ } else if (Array.isArray(data)) {
+ // 数据列表
+ botMessage = {
+ id: Date.now() + 1,
+ content: `找到 ${data.length} 条结果:`,
+ isUser: false,
+ type: 'data',
+ data: data,
+ timestamp: new Date().toISOString(),
+ };
+ } else if (typeof data === 'object') {
+ // 对象数据
+ botMessage = {
+ id: Date.now() + 1,
+ content: JSON.stringify(data, null, 2),
+ isUser: false,
+ type: 'markdown',
+ timestamp: new Date().toISOString(),
+ };
+ } else {
+ botMessage = {
+ id: Date.now() + 1,
+ content: '抱歉,我无法理解这个查询结果。',
+ isUser: false,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ };
+ }
+ } else {
+ botMessage = {
+ id: Date.now() + 1,
+ content: `抱歉,查询失败:${response.error}`,
+ isUser: false,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ };
+ }
+
+ setMessages((prev) => [...prev, botMessage]);
+ } catch (error) {
+ logger.error('ChatInterface', 'handleSendMessage', error);
+ const errorMessage = {
+ id: Date.now() + 1,
+ content: `抱歉,发生了错误:${error.message}`,
+ isUser: false,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ };
+ setMessages((prev) => [...prev, errorMessage]);
+ } finally {
+ setIsLoading(false);
+ inputRef.current?.focus();
+ }
+ };
+
+ // 处理键盘事件
+ const handleKeyPress = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSendMessage();
+ }
+ };
+
+ // 清空对话
+ const handleClearChat = () => {
+ setMessages([
+ {
+ id: 1,
+ content: '对话已清空。有什么可以帮到你的?',
+ isUser: false,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ };
+
+ // 复制消息
+ const handleCopyMessage = () => {
+ toast({
+ title: '已复制',
+ status: 'success',
+ duration: 2000,
+ isClosable: true,
+ });
+ };
+
+ // 反馈
+ const handleFeedback = (type) => {
+ logger.info('ChatInterface', 'Feedback', { type });
+ toast({
+ title: type === 'positive' ? '感谢反馈!' : '我们会改进',
+ status: 'info',
+ duration: 2000,
+ isClosable: true,
+ });
+ };
+
+ // 快捷问题
+ const quickQuestions = [
+ '查询贵州茅台的股票信息',
+ '搜索人工智能相关新闻',
+ '今日涨停股票有哪些',
+ '新能源概念板块分析',
+ ];
+
+ const handleQuickQuestion = (question) => {
+ setInputValue(question);
+ inputRef.current?.focus();
+ };
+
+ // 导出对话
+ const handleExportChat = () => {
+ const chatText = messages
+ .map((msg) => `[${msg.isUser ? '用户' : 'AI'}] ${msg.content}`)
+ .join('\n\n');
+
+ const blob = new Blob([chatText], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `chat_${new Date().toISOString().slice(0, 10)}.txt`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+ {/* 头部工具栏 */}
+
+
+ AI投资助手
+ 在线
+ {availableTools.length > 0 && (
+ {availableTools.length} 个工具
+ )}
+
+
+ }
+ size="sm"
+ variant="ghost"
+ aria-label="清空对话"
+ onClick={handleClearChat}
+ />
+ }
+ size="sm"
+ variant="ghost"
+ aria-label="导出对话"
+ onClick={handleExportChat}
+ />
+
+
+
+
+ {/* 消息列表 */}
+
+
+ {messages.map((message) => (
+
+ ))}
+ {isLoading && (
+
+
+
+ AI正在思考...
+
+
+ )}
+
+
+
+
+ {/* 快捷问题(仅在消息较少时显示) */}
+ {messages.length <= 2 && (
+
+ 快捷问题:
+
+ {quickQuestions.map((question, idx) => (
+
+ ))}
+
+
+ )}
+
+
+
+ {/* 输入框 */}
+
+
+ setInputValue(e.target.value)}
+ onKeyPress={handleKeyPress}
+ placeholder="输入消息... (Shift+Enter换行,Enter发送)"
+ bg={inputBg}
+ border="none"
+ _focus={{ boxShadow: 'none' }}
+ mr={2}
+ disabled={isLoading}
+ />
+ }
+ colorScheme="blue"
+ aria-label="发送"
+ onClick={handleSendMessage}
+ isLoading={isLoading}
+ disabled={!inputValue.trim()}
+ />
+
+
+
+ );
+};
+
+export default ChatInterface;
diff --git a/src/components/ChatBot/ChatInterfaceV2.js b/src/components/ChatBot/ChatInterfaceV2.js
new file mode 100644
index 00000000..d617d729
--- /dev/null
+++ b/src/components/ChatBot/ChatInterfaceV2.js
@@ -0,0 +1,576 @@
+// src/components/ChatBot/ChatInterfaceV2.js
+// 重新设计的聊天界面 - 更漂亮、支持Agent模式
+
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ Box,
+ Flex,
+ Input,
+ IconButton,
+ VStack,
+ HStack,
+ Text,
+ Spinner,
+ useColorModeValue,
+ useToast,
+ Divider,
+ Badge,
+ Button,
+ Avatar,
+ Heading,
+ Progress,
+ Fade,
+} from '@chakra-ui/react';
+import { FiSend, FiRefreshCw, FiDownload, FiCpu, FiUser, FiZap } from 'react-icons/fi';
+import { PlanCard } from './PlanCard';
+import { StepResultCard } from './StepResultCard';
+import { mcpService } from '../../services/mcpService';
+import { logger } from '../../utils/logger';
+
+/**
+ * Agent消息类型
+ */
+const MessageTypes = {
+ USER: 'user',
+ AGENT_THINKING: 'agent_thinking',
+ AGENT_PLAN: 'agent_plan',
+ AGENT_EXECUTING: 'agent_executing',
+ AGENT_RESPONSE: 'agent_response',
+ ERROR: 'error',
+};
+
+/**
+ * 聊天界面V2组件 - Agent模式
+ */
+export const ChatInterfaceV2 = () => {
+ const [messages, setMessages] = useState([
+ {
+ id: 1,
+ type: MessageTypes.AGENT_RESPONSE,
+ content: '你好!我是AI投资研究助手。我会通过多步骤分析来帮助你深入了解金融市场。\n\n你可以问我:\n• 全面分析某只股票\n• 某个行业的投资机会\n• 今日市场热点\n• 某个概念板块的表现',
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ const [inputValue, setInputValue] = useState('');
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [currentProgress, setCurrentProgress] = useState(0);
+
+ const messagesEndRef = useRef(null);
+ const inputRef = useRef(null);
+ const toast = useToast();
+
+ // 颜色主题
+ const bgColor = useColorModeValue('gray.50', 'gray.900');
+ const chatBg = useColorModeValue('white', 'gray.800');
+ const inputBg = useColorModeValue('white', 'gray.700');
+ const userBubbleBg = useColorModeValue('blue.500', 'blue.600');
+ const agentBubbleBg = useColorModeValue('white', 'gray.700');
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
+
+ // 自动滚动到底部
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+ // 添加消息
+ const addMessage = (message) => {
+ setMessages((prev) => [...prev, { ...message, id: Date.now() }]);
+ };
+
+ // 更新最后一条消息
+ const updateLastMessage = (updates) => {
+ setMessages((prev) => {
+ const newMessages = [...prev];
+ if (newMessages.length > 0) {
+ newMessages[newMessages.length - 1] = {
+ ...newMessages[newMessages.length - 1],
+ ...updates,
+ };
+ }
+ return newMessages;
+ });
+ };
+
+ // 发送消息(Agent模式)
+ const handleSendMessage = async () => {
+ if (!inputValue.trim() || isProcessing) return;
+
+ const userMessage = {
+ type: MessageTypes.USER,
+ content: inputValue,
+ timestamp: new Date().toISOString(),
+ };
+
+ addMessage(userMessage);
+ setInputValue('');
+ setIsProcessing(true);
+ setCurrentProgress(0);
+
+ try {
+ // 1. 显示思考状态
+ addMessage({
+ type: MessageTypes.AGENT_THINKING,
+ content: '正在分析你的问题...',
+ timestamp: new Date().toISOString(),
+ });
+
+ setCurrentProgress(10);
+
+ // 调用 Agent API
+ const response = await fetch(`${mcpService.baseURL.replace('/mcp', '')}/mcp/agent/chat`, {
+ 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,
+ })),
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Agent请求失败');
+ }
+
+ const agentResponse = await response.json();
+ logger.info('Agent response', agentResponse);
+
+ // 移除思考消息
+ setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_THINKING));
+
+ if (!agentResponse.success) {
+ throw new Error(agentResponse.message || '处理失败');
+ }
+
+ setCurrentProgress(30);
+
+ // 2. 显示执行计划
+ if (agentResponse.plan) {
+ addMessage({
+ type: MessageTypes.AGENT_PLAN,
+ content: '已制定执行计划',
+ plan: agentResponse.plan,
+ timestamp: new Date().toISOString(),
+ });
+ }
+
+ setCurrentProgress(40);
+
+ // 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(),
+ });
+
+ // 模拟进度更新
+ 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));
+ }
+ }
+
+ 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);
+
+ // 移除思考/执行中消息
+ setMessages(prev => prev.filter(
+ m => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING
+ ));
+
+ addMessage({
+ type: MessageTypes.ERROR,
+ content: `处理失败:${error.message}`,
+ timestamp: new Date().toISOString(),
+ });
+
+ toast({
+ title: '处理失败',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ } finally {
+ setIsProcessing(false);
+ setCurrentProgress(0);
+ inputRef.current?.focus();
+ }
+ };
+
+ // 处理键盘事件
+ const handleKeyPress = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSendMessage();
+ }
+ };
+
+ // 清空对话
+ const handleClearChat = () => {
+ setMessages([
+ {
+ id: 1,
+ type: MessageTypes.AGENT_RESPONSE,
+ content: '对话已清空。有什么可以帮到你的?',
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ };
+
+ // 导出对话
+ const handleExportChat = () => {
+ const chatText = messages
+ .filter(m => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE)
+ .map((msg) => `[${msg.type === MessageTypes.USER ? '用户' : 'AI助手'}] ${msg.content}`)
+ .join('\n\n');
+
+ const blob = new Blob([chatText], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `chat_${new Date().toISOString().slice(0, 10)}.txt`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ // 快捷问题
+ const quickQuestions = [
+ '全面分析贵州茅台这只股票',
+ '今日涨停股票有哪些亮点',
+ '新能源概念板块的投资机会',
+ '半导体行业最新动态',
+ ];
+
+ return (
+
+ {/* 头部 */}
+
+
+
+ }
+ />
+
+ AI投资研究助手
+
+
+
+
+ 智能分析
+
+
+
+ 多步骤深度研究
+
+
+
+
+
+
+ }
+ size="sm"
+ variant="ghost"
+ aria-label="清空对话"
+ onClick={handleClearChat}
+ />
+ }
+ size="sm"
+ variant="ghost"
+ aria-label="导出对话"
+ onClick={handleExportChat}
+ />
+
+
+
+ {/* 进度条 */}
+ {isProcessing && (
+
+ )}
+
+
+ {/* 消息列表 */}
+
+
+ {messages.map((message) => (
+
+
+
+ ))}
+
+
+
+
+ {/* 快捷问题 */}
+ {messages.length <= 2 && !isProcessing && (
+
+ 💡 试试这些问题:
+
+ {quickQuestions.map((question, idx) => (
+
+ ))}
+
+
+ )}
+
+ {/* 输入框 */}
+
+
+ setInputValue(e.target.value)}
+ onKeyPress={handleKeyPress}
+ placeholder="输入你的问题,我会进行深度分析..."
+ bg={inputBg}
+ border="1px"
+ borderColor={borderColor}
+ _focus={{ borderColor: 'blue.500', boxShadow: '0 0 0 1px #3182CE' }}
+ mr={2}
+ disabled={isProcessing}
+ size="lg"
+ />
+ : }
+ colorScheme="blue"
+ aria-label="发送"
+ onClick={handleSendMessage}
+ isLoading={isProcessing}
+ disabled={!inputValue.trim() || isProcessing}
+ size="lg"
+ />
+
+
+
+ );
+};
+
+/**
+ * 消息渲染器
+ */
+const MessageRenderer = ({ message }) => {
+ const userBubbleBg = useColorModeValue('blue.500', 'blue.600');
+ const agentBubbleBg = useColorModeValue('white', 'gray.700');
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
+
+ switch (message.type) {
+ case MessageTypes.USER:
+ return (
+
+
+
+
+ {message.content}
+
+
+ } />
+
+
+ );
+
+ case MessageTypes.AGENT_THINKING:
+ return (
+
+
+ } />
+
+
+
+
+ {message.content}
+
+
+
+
+
+ );
+
+ case MessageTypes.AGENT_PLAN:
+ return (
+
+
+ } />
+
+
+
+
+
+ );
+
+ case MessageTypes.AGENT_EXECUTING:
+ return (
+
+
+ } />
+
+
+ {message.stepResults?.map((result, idx) => (
+
+ ))}
+
+
+
+ );
+
+ case MessageTypes.AGENT_RESPONSE:
+ return (
+
+
+ } />
+
+ {/* 最终总结 */}
+
+
+ {message.content}
+
+
+ {/* 元数据 */}
+ {message.metadata && (
+
+ 总步骤: {message.metadata.total_steps}
+ ✓ {message.metadata.successful_steps}
+ {message.metadata.failed_steps > 0 && (
+ ✗ {message.metadata.failed_steps}
+ )}
+ 耗时: {message.metadata.total_execution_time?.toFixed(1)}s
+
+ )}
+
+
+ {/* 执行详情(可选) */}
+ {message.plan && message.stepResults && message.stepResults.length > 0 && (
+
+
+
+ 📊 执行详情(点击展开查看)
+
+ {message.stepResults.map((result, idx) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+
+ case MessageTypes.ERROR:
+ return (
+
+
+ } />
+
+ {message.content}
+
+
+
+ );
+
+ default:
+ return null;
+ }
+};
+
+export default ChatInterfaceV2;
diff --git a/src/components/ChatBot/MessageBubble.js b/src/components/ChatBot/MessageBubble.js
new file mode 100644
index 00000000..ce491702
--- /dev/null
+++ b/src/components/ChatBot/MessageBubble.js
@@ -0,0 +1,149 @@
+// src/components/ChatBot/MessageBubble.js
+// 聊天消息气泡组件
+
+import React from 'react';
+import {
+ Box,
+ Flex,
+ Text,
+ Avatar,
+ useColorModeValue,
+ IconButton,
+ HStack,
+ Code,
+ Badge,
+ VStack,
+} from '@chakra-ui/react';
+import { FiCopy, FiThumbsUp, FiThumbsDown } from 'react-icons/fi';
+import ReactMarkdown from 'react-markdown';
+
+/**
+ * 消息气泡组件
+ * @param {Object} props
+ * @param {Object} props.message - 消息对象
+ * @param {boolean} props.isUser - 是否是用户消息
+ * @param {Function} props.onCopy - 复制消息回调
+ * @param {Function} props.onFeedback - 反馈回调
+ */
+export const MessageBubble = ({ message, isUser, onCopy, onFeedback }) => {
+ const userBg = useColorModeValue('blue.500', 'blue.600');
+ const botBg = useColorModeValue('gray.100', 'gray.700');
+ const userColor = 'white';
+ const botColor = useColorModeValue('gray.800', 'white');
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(message.content);
+ onCopy?.();
+ };
+
+ return (
+
+
+ {/* 头像 */}
+
+
+ {/* 消息内容 */}
+
+
+ {message.type === 'text' ? (
+
+ {message.content}
+
+ ) : message.type === 'markdown' ? (
+
+ {message.content}
+
+ ) : message.type === 'data' ? (
+
+ {message.data && Array.isArray(message.data) && message.data.slice(0, 5).map((item, idx) => (
+
+ {Object.entries(item).map(([key, value]) => (
+
+ {key}:
+ {String(value)}
+
+ ))}
+
+ ))}
+ {message.data && message.data.length > 5 && (
+
+ +{message.data.length - 5} 更多结果
+
+ )}
+
+ ) : null}
+
+
+ {/* 消息操作按钮(仅AI消息) */}
+ {!isUser && (
+
+ }
+ size="xs"
+ variant="ghost"
+ aria-label="复制"
+ onClick={handleCopy}
+ />
+ }
+ size="xs"
+ variant="ghost"
+ aria-label="赞"
+ onClick={() => onFeedback?.('positive')}
+ />
+ }
+ size="xs"
+ variant="ghost"
+ aria-label="踩"
+ onClick={() => onFeedback?.('negative')}
+ />
+
+ )}
+
+ {/* 时间戳 */}
+
+ {message.timestamp ? new Date(message.timestamp).toLocaleTimeString('zh-CN', {
+ hour: '2-digit',
+ minute: '2-digit',
+ }) : ''}
+
+
+
+
+ );
+};
+
+export default MessageBubble;
diff --git a/src/components/ChatBot/PlanCard.js b/src/components/ChatBot/PlanCard.js
new file mode 100644
index 00000000..53714598
--- /dev/null
+++ b/src/components/ChatBot/PlanCard.js
@@ -0,0 +1,145 @@
+// src/components/ChatBot/PlanCard.js
+// 执行计划展示卡片
+
+import React from 'react';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Badge,
+ Accordion,
+ AccordionItem,
+ AccordionButton,
+ AccordionPanel,
+ AccordionIcon,
+ Icon,
+ useColorModeValue,
+ Divider,
+} from '@chakra-ui/react';
+import { FiTarget, FiCheckCircle, FiXCircle, FiClock, FiTool } from 'react-icons/fi';
+
+/**
+ * 执行计划卡片组件
+ */
+export const PlanCard = ({ plan, stepResults }) => {
+ const cardBg = useColorModeValue('blue.50', 'blue.900');
+ const borderColor = useColorModeValue('blue.200', 'blue.700');
+ const successColor = useColorModeValue('green.500', 'green.300');
+ const errorColor = useColorModeValue('red.500', 'red.300');
+ const pendingColor = useColorModeValue('gray.400', 'gray.500');
+
+ const getStepStatus = (stepIndex) => {
+ if (!stepResults || stepResults.length === 0) return 'pending';
+ const result = stepResults.find(r => r.step_index === stepIndex);
+ return result ? result.status : 'pending';
+ };
+
+ const getStepIcon = (status) => {
+ switch (status) {
+ case 'success':
+ return FiCheckCircle;
+ case 'failed':
+ return FiXCircle;
+ default:
+ return FiClock;
+ }
+ };
+
+ const getStepColor = (status) => {
+ switch (status) {
+ case 'success':
+ return successColor;
+ case 'failed':
+ return errorColor;
+ default:
+ return pendingColor;
+ }
+ };
+
+ return (
+
+
+ {/* 目标 */}
+
+
+ 执行目标
+
+
+ {plan.goal}
+
+
+
+
+ {/* 规划思路 */}
+ {plan.reasoning && (
+ <>
+ 规划思路:
+
+ {plan.reasoning}
+
+
+ >
+ )}
+
+ {/* 执行步骤 */}
+
+ 执行步骤
+ {plan.steps.length} 步
+
+
+
+ {plan.steps.map((step, index) => {
+ const status = getStepStatus(index);
+ const StepIcon = getStepIcon(status);
+ const stepColor = getStepColor(status);
+
+ return (
+
+
+
+
+
+ 步骤 {index + 1}: {step.tool}
+
+
+ {status === 'success' ? '✓ 完成' :
+ status === 'failed' ? '✗ 失败' : '⏳ 等待'}
+
+
+
+ {step.reason}
+
+
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default PlanCard;
diff --git a/src/components/ChatBot/StepResultCard.js b/src/components/ChatBot/StepResultCard.js
new file mode 100644
index 00000000..282acaf9
--- /dev/null
+++ b/src/components/ChatBot/StepResultCard.js
@@ -0,0 +1,186 @@
+// src/components/ChatBot/StepResultCard.js
+// 步骤结果展示卡片(可折叠)
+
+import React, { useState } from 'react';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Badge,
+ Collapse,
+ Icon,
+ IconButton,
+ Code,
+ useColorModeValue,
+ Divider,
+} from '@chakra-ui/react';
+import { FiChevronDown, FiChevronUp, FiCheckCircle, FiXCircle, FiClock, FiDatabase } from 'react-icons/fi';
+
+/**
+ * 步骤结果卡片组件
+ */
+export const StepResultCard = ({ stepResult }) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ const cardBg = useColorModeValue('white', 'gray.700');
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
+ const successColor = useColorModeValue('green.500', 'green.300');
+ const errorColor = useColorModeValue('red.500', 'red.300');
+
+ const getStatusIcon = () => {
+ switch (stepResult.status) {
+ case 'success':
+ return FiCheckCircle;
+ case 'failed':
+ return FiXCircle;
+ default:
+ return FiClock;
+ }
+ };
+
+ const getStatusColor = () => {
+ switch (stepResult.status) {
+ case 'success':
+ return 'green';
+ case 'failed':
+ return 'red';
+ default:
+ return 'gray';
+ }
+ };
+
+ const StatusIcon = getStatusIcon();
+ const statusColorScheme = getStatusColor();
+
+ // 格式化数据以便展示
+ const formatResult = (data) => {
+ if (typeof data === 'string') return data;
+ if (Array.isArray(data)) {
+ return `找到 ${data.length} 条记录`;
+ }
+ if (typeof data === 'object') {
+ return JSON.stringify(data, null, 2);
+ }
+ return String(data);
+ };
+
+ return (
+
+ {/* 头部 - 始终可见 */}
+ setIsExpanded(!isExpanded)}
+ _hover={{ bg: useColorModeValue('gray.50', 'gray.600') }}
+ >
+
+
+
+
+
+ 步骤 {stepResult.step_index + 1}: {stepResult.tool}
+
+
+ {stepResult.status === 'success' ? '成功' :
+ stepResult.status === 'failed' ? '失败' : '执行中'}
+
+
+
+ 耗时: {stepResult.execution_time?.toFixed(2)}s
+
+
+
+
+ }
+ size="sm"
+ variant="ghost"
+ aria-label={isExpanded ? "收起" : "展开"}
+ />
+
+
+ {/* 内容 - 可折叠 */}
+
+
+
+
+ {/* 参数 */}
+ {stepResult.arguments && Object.keys(stepResult.arguments).length > 0 && (
+
+
+
+ 请求参数:
+
+
+ {JSON.stringify(stepResult.arguments, null, 2)}
+
+
+ )}
+
+ {/* 结果或错误 */}
+ {stepResult.status === 'success' && stepResult.result && (
+
+ 执行结果:
+
+ {typeof stepResult.result === 'string' ? (
+ {stepResult.result}
+ ) : Array.isArray(stepResult.result) ? (
+
+ 找到 {stepResult.result.length} 条记录:
+ {stepResult.result.slice(0, 3).map((item, idx) => (
+
+ {JSON.stringify(item, null, 2)}
+
+ ))}
+ {stepResult.result.length > 3 && (
+
+ ...还有 {stepResult.result.length - 3} 条记录
+
+ )}
+
+ ) : (
+
+ {JSON.stringify(stepResult.result, null, 2)}
+
+ )}
+
+
+ )}
+
+ {stepResult.status === 'failed' && stepResult.error && (
+
+ 错误信息:
+
+ {stepResult.error}
+
+
+ )}
+
+
+
+ );
+};
+
+export default StepResultCard;
diff --git a/src/components/ChatBot/index.js b/src/components/ChatBot/index.js
new file mode 100644
index 00000000..4b1f4dee
--- /dev/null
+++ b/src/components/ChatBot/index.js
@@ -0,0 +1,11 @@
+// src/components/ChatBot/index.js
+// 聊天机器人组件统一导出
+
+export { ChatInterface } from './ChatInterface';
+export { ChatInterfaceV2 } from './ChatInterfaceV2';
+export { MessageBubble } from './MessageBubble';
+export { PlanCard } from './PlanCard';
+export { StepResultCard } from './StepResultCard';
+
+// 默认导出新版本
+export { ChatInterfaceV2 as default } from './ChatInterfaceV2';
diff --git a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js
index 1649deff..89d7d7ad 100644
--- a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js
+++ b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js
@@ -243,6 +243,26 @@ const MobileDrawer = memo(({
AGENT社群
+ handleNavigate('/agent-chat')}
+ py={1}
+ px={3}
+ borderRadius="md"
+ _hover={{ bg: 'gray.100' }}
+ cursor="pointer"
+ bg={location.pathname.includes('/agent-chat') ? 'blue.50' : 'transparent'}
+ borderLeft={location.pathname.includes('/agent-chat') ? '3px solid' : 'none'}
+ borderColor="blue.600"
+ fontWeight={location.pathname.includes('/agent-chat') ? 'bold' : 'normal'}
+ >
+
+ AI聊天助手
+
+ AI
+ NEW
+
+
+
{
as={Button}
variant="ghost"
rightIcon={}
+ bg={isActive(['/agent-chat']) ? 'blue.50' : 'transparent'}
+ color={isActive(['/agent-chat']) ? 'blue.600' : 'inherit'}
+ fontWeight={isActive(['/agent-chat']) ? 'bold' : 'normal'}
+ borderBottom={isActive(['/agent-chat']) ? '2px solid' : 'none'}
+ borderColor="blue.600"
+ _hover={{ bg: isActive(['/agent-chat']) ? 'blue.100' : 'gray.50' }}
onMouseEnter={agentCommunityMenu.handleMouseEnter}
onMouseLeave={agentCommunityMenu.handleMouseLeave}
onClick={agentCommunityMenu.handleClick}
@@ -207,10 +213,31 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
+