From fff13edcdfcee8b52bf8b62343f99edf761457c0 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Wed, 17 Dec 2025 13:53:16 +0800 Subject: [PATCH] update pay ui --- __pycache__/mcp_server.cpython-310.pyc | Bin 89375 -> 90316 bytes mcp_server.py | 579 ++++++++++++------------- 2 files changed, 277 insertions(+), 302 deletions(-) diff --git a/__pycache__/mcp_server.cpython-310.pyc b/__pycache__/mcp_server.cpython-310.pyc index cddcd1c76962fb9966219d7576eca8e0b1f4f3e6..dd509476075b608ec7c294f04937b5012959aca8 100644 GIT binary patch delta 27663 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 F{|6^Rv)%vz delta 26039 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 diff --git a/mcp_server.py b/mcp_server.py index 15d5d219..a4b40a12 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -2060,6 +2060,11 @@ class MCPAgentIntegrated: tool_handlers: Dict[str, Any], ) -> Dict[str, Any]: """执行单个工具""" + # 详细日志:打印工具名和参数 + logger.info(f"[Tool Call] ========== 工具调用开始 ==========") + logger.info(f"[Tool Call] 工具名: {tool_name}") + logger.info(f"[Tool Call] 参数类型: {type(arguments)}") + logger.info(f"[Tool Call] 参数内容: {json.dumps(arguments, ensure_ascii=False, indent=2)}") # 特殊工具:summarize_news(使用 DeepMoney) if tool_name == "summarize_news": @@ -2071,9 +2076,13 @@ class MCPAgentIntegrated: # 调用 MCP 工具 handler = tool_handlers.get(tool_name) if not handler: + logger.error(f"[Tool Call] 工具 '{tool_name}' 未找到!可用工具: {list(tool_handlers.keys())}") raise ValueError(f"Tool '{tool_name}' not found") + logger.info(f"[Tool Call] 调用 handler: {handler.__name__}") result = await handler(arguments) + logger.info(f"[Tool Call] 返回结果类型: {type(result)}") + logger.info(f"[Tool Call] ========== 工具调用结束 ==========") return result async def summarize_news_with_deepmoney(self, data: str, focus: str) -> str: @@ -2412,7 +2421,11 @@ class MCPAgentIntegrated: chat_history: List[dict] = None, # 新增:历史对话记录 is_new_session: bool = False, # 新增:是否是新会话(用于生成标题) ) -> AsyncGenerator[str, None]: - """主流程(流式输出)- 逐步返回执行结果""" + """ + 主流程(流式输出)- 使用原生 OpenAI Tool Calling API + + 支持 vLLM 的 --enable-auto-tool-choice --tool-call-parser qwen3_coder + """ logger.info(f"[Agent Stream] 处理查询: {user_query}") if chat_history: logger.info(f"[Agent Stream] 带有 {len(chat_history)} 条历史消息") @@ -2424,36 +2437,79 @@ class MCPAgentIntegrated: # 如果传入了自定义模型配置,使用自定义配置,否则使用默认 LLM if model_config: - planning_client = OpenAI( + llm_client = OpenAI( api_key=model_config["api_key"], base_url=model_config["base_url"], ) - planning_model = model_config["model"] - logger.info(f"[Agent Stream] 使用自定义模型: {planning_model}") + llm_model = model_config["model"] + llm_max_tokens = model_config.get("max_tokens", 8192) + logger.info(f"[Agent Stream] 使用自定义模型: {llm_model}") else: - planning_client = self.llm_client - planning_model = self.llm_model - logger.info(f"[Agent Stream] 使用默认模型: {planning_model}") + llm_client = self.llm_client + llm_model = self.llm_model + llm_max_tokens = self.llm_max_tokens + logger.info(f"[Agent Stream] 使用默认模型: {llm_model}") + + # 将工具列表转换为 OpenAI tools 格式 + openai_tools = [] + for tool in tools: + openai_tools.append({ + "type": "function", + "function": { + "name": tool["name"], + "description": tool["description"], + "parameters": tool["parameters"] + } + }) + logger.info(f"[Agent Stream] 已加载 {len(openai_tools)} 个工具") + + # 获取当前时间信息 + now = datetime.now() + current_time_info = f"""当前时间: {now.strftime('%Y年%m月%d日 %H:%M:%S')} {['周一', '周二', '周三', '周四', '周五', '周六', '周日'][now.weekday()]} +A股交易时间: 上午 9:30-11:30,下午 13:00-15:00 +时间语义: "今天"={now.strftime('%Y-%m-%d')}, "最近"=最近5-10个交易日""" + + # 构建系统提示词(适用于原生 tool calling) + system_prompt = f"""你是"价小前",北京价值前沿科技公司的AI投研聊天助手。 + +## 你的能力 +- 专业领域: 股票投资研究、市场分析、新闻解读、财务分析 +- 你可以调用各种工具来查询股票数据、新闻、概念板块等信息 +- 根据用户问题,智能选择并调用合适的工具 + +{current_time_info} + +## 重要知识 +- 贵州茅台: 600519 +- 涨停: 涨幅约10% +- 概念板块: 相同题材股票分类 + +## 工具使用原则 +1. 根据用户问题,选择最合适的工具 +2. 可以多次调用工具来完成复杂任务 +3. 获取数据后,给出专业的分析和总结 +4. 如果需要总结新闻类数据,使用 summarize_news 工具 + +## 输出要求 +- 使用 Markdown 格式,结构清晰 +- 重要数据用 **加粗** 标注 +- 如有数值数据,可使用 ECharts 图表展示(使用 ```echarts 代码块)""" try: # 发送开始事件 yield self._format_sse("status", {"stage": "start", "message": "开始处理查询"}) - # 阶段1: 使用选中的模型制定计划(流式,带 DeepMoney 备选) - yield self._format_sse("status", {"stage": "planning", "message": "正在制定执行计划..."}) - - # 构建消息列表(包含历史对话上下文) + # 构建消息列表 messages = [ - {"role": "system", "content": self.get_planning_prompt(tools)}, + {"role": "system", "content": system_prompt}, ] - # 添加历史对话(最近 10 轮,避免上下文过长) + # 添加历史对话(最近 10 轮) if chat_history: - recent_history = chat_history[-10:] # 最近 10 条消息 + recent_history = chat_history[-10:] for msg in recent_history: role = "user" if msg.get("message_type") == "user" else "assistant" content = msg.get("message", "") - # 截断过长的历史消息 if len(content) > 500: content = content[:500] + "..." messages.append({"role": role, "content": content}) @@ -2462,183 +2518,183 @@ class MCPAgentIntegrated: # 添加当前用户查询 messages.append({"role": "user", "content": user_query}) - reasoning_content = "" - plan_content = "" - use_fallback = False - - try: - # 尝试使用选中的模型流式 API - # 从模型配置获取 max_tokens,默认 8192 - model_max_tokens = model_config.get("max_tokens", 8192) if model_config else 8192 - stream = planning_client.chat.completions.create( - model=planning_model, - messages=messages, - temperature=1.0, - max_tokens=model_max_tokens, - stream=True, # 启用流式输出 - ) - - # 逐块接收 LLM 的响应 - for chunk in stream: - if chunk.choices[0].delta.content: - content_chunk = chunk.choices[0].delta.content - plan_content += content_chunk - - # 发送思考过程片段 - yield self._format_sse("thinking", { - "content": content_chunk, - "stage": "planning" - }) - - # 提取 reasoning_content(如果有) - if hasattr(chunk.choices[0], 'delta') and hasattr(chunk.choices[0].delta, 'reasoning_content'): - reasoning_chunk = chunk.choices[0].delta.reasoning_content - if reasoning_chunk: - reasoning_content += reasoning_chunk - # 发送推理过程片段 - yield self._format_sse("reasoning", { - "content": reasoning_chunk - }) - - except Exception as llm_error: - # 检查是否是内容风控错误(400) - error_str = str(llm_error) - if "400" in error_str and ("content_filter" in error_str or "high risk" in error_str): - logger.warning(f"[Planning] LLM 内容风控拒绝,切换到 DeepMoney: {error_str}") - use_fallback = True - - yield self._format_sse("status", { - "stage": "planning", - "message": "切换到备用模型制定计划..." - }) - - try: - # 使用 DeepMoney 备选方案(非流式) - fallback_response = self.deepmoney_client.chat.completions.create( - model=self.deepmoney_model, - messages=messages, - temperature=0.7, - max_tokens=DEEPMONEY_CONFIG.get("max_tokens", 8192), - ) - - plan_content = fallback_response.choices[0].message.content - - # 发送完整的计划内容(一次性) - yield self._format_sse("thinking", { - "content": plan_content, - "stage": "planning" - }) - - logger.info(f"[Planning] DeepMoney 备选方案成功") - - except Exception as fallback_error: - logger.error(f"[Planning] DeepMoney 备选方案也失败: {fallback_error}") - raise Exception(f"LLM 和 DeepMoney 都无法生成计划: {llm_error}, {fallback_error}") - else: - # 不是内容风控错误,直接抛出 - logger.error(f"[Planning] LLM 调用失败(非风控原因): {llm_error}") - raise - - # 解析完整的计划 - plan_json = plan_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)} 步") - - # 发送完整计划 - yield self._format_sse("plan", { - "goal": plan.goal, - "reasoning": plan.reasoning, - "steps": [ - {"tool": step.tool, "arguments": step.arguments, "reason": step.reason} - for step in plan.steps - ], - }) - - # 阶段2: 执行工具(逐步返回) - yield self._format_sse("status", {"stage": "executing", "message": f"开始执行 {len(plan.steps)} 个步骤"}) - + # 用于收集执行结果 step_results = [] collected_data = {} + plan_steps = [] # 记录执行的步骤,用于前端显示 + step_index = 0 + max_tool_calls = 10 # 最大工具调用次数,防止无限循环 - for i, step in enumerate(plan.steps): - # 发送步骤开始事件 - yield self._format_sse("step_start", { - "step_index": i, - "tool": step.tool, - "arguments": step.arguments, - "reason": step.reason, - }) + yield self._format_sse("status", {"stage": "thinking", "message": "正在分析问题..."}) - start_time = datetime.now() + # 循环处理,直到模型不再调用工具 + while step_index < max_tool_calls: + logger.info(f"[Agent Stream] 第 {step_index + 1} 轮 LLM 调用") + # 使用原生 tool calling(非流式,因为需要获取 tool_calls) 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, + response = llm_client.chat.completions.create( + model=llm_model, + messages=messages, + tools=openai_tools, + tool_choice="auto", + temperature=0.7, + max_tokens=llm_max_tokens, + stream=False, # 工具调用需要非流式 ) - step_results.append(step_result) - collected_data[f"step_{i+1}_{step.tool}"] = result - - # 发送步骤完成事件(包含结果) - yield self._format_sse("step_complete", { - "step_index": i, - "tool": step.tool, - "status": "success", - "result": result, - "execution_time": execution_time, - }) - except Exception as e: - execution_time = (datetime.now() - start_time).total_seconds() + logger.error(f"[Agent Stream] LLM 调用失败: {e}") + raise - step_result = StepResult( - step_index=i, - tool=step.tool, - arguments=step.arguments, - status="failed", - error=str(e), - execution_time=execution_time, - ) - step_results.append(step_result) + assistant_message = response.choices[0].message + logger.info(f"[Agent Stream] LLM 响应: finish_reason={response.choices[0].finish_reason}") - # 发送步骤失败事件 - yield self._format_sse("step_complete", { - "step_index": i, - "tool": step.tool, - "status": "failed", - "error": str(e), - "execution_time": execution_time, - }) + # 检查是否有工具调用 + if assistant_message.tool_calls: + logger.info(f"[Agent Stream] 检测到 {len(assistant_message.tool_calls)} 个工具调用") - # 阶段3: LLM 生成总结(流式) + # 将 assistant 消息添加到历史(包含 tool_calls) + messages.append(assistant_message) + + # 如果是第一次工具调用,发送计划事件 + if step_index == 0: + # 构建计划数据 + plan_data = { + "goal": f"分析用户问题:{user_query[:50]}...", + "reasoning": "使用工具获取相关数据进行分析", + "steps": [] + } + for tc in assistant_message.tool_calls: + try: + args = json.loads(tc.function.arguments) if tc.function.arguments else {} + except: + args = {} + plan_data["steps"].append({ + "tool": tc.function.name, + "arguments": args, + "reason": f"调用 {tc.function.name}" + }) + + yield self._format_sse("plan", plan_data) + yield self._format_sse("status", {"stage": "executing", "message": f"开始执行 {len(assistant_message.tool_calls)} 个工具调用"}) + + # 执行每个工具调用 + for tool_call in assistant_message.tool_calls: + tool_name = tool_call.function.name + tool_call_id = tool_call.id + + try: + arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {} + except json.JSONDecodeError: + arguments = {} + logger.warning(f"[Agent Stream] 工具参数解析失败: {tool_call.function.arguments}") + + logger.info(f"[Tool Call] ========== 工具调用开始 ==========") + logger.info(f"[Tool Call] 工具名: {tool_name}") + logger.info(f"[Tool Call] tool_call_id: {tool_call_id}") + logger.info(f"[Tool Call] 参数内容: {json.dumps(arguments, ensure_ascii=False)}") + + # 发送步骤开始事件 + yield self._format_sse("step_start", { + "step_index": step_index, + "tool": tool_name, + "arguments": arguments, + "reason": f"调用 {tool_name}", + }) + + start_time = datetime.now() + + try: + # 特殊处理 summarize_news + if tool_name == "summarize_news": + data_arg = arguments.get("data", "") + if data_arg in ["前面的新闻数据", "前面收集的所有数据", ""]: + arguments["data"] = json.dumps(collected_data, ensure_ascii=False, indent=2) + + # 执行工具 + result = await self.execute_tool(tool_name, arguments, tool_handlers) + execution_time = (datetime.now() - start_time).total_seconds() + + # 记录结果 + step_result = StepResult( + step_index=step_index, + tool=tool_name, + arguments=arguments, + status="success", + result=result, + execution_time=execution_time, + ) + step_results.append(step_result) + collected_data[f"step_{step_index+1}_{tool_name}"] = result + plan_steps.append({"tool": tool_name, "arguments": arguments, "reason": f"调用 {tool_name}"}) + + # 发送步骤完成事件 + yield self._format_sse("step_complete", { + "step_index": step_index, + "tool": tool_name, + "status": "success", + "result": result, + "execution_time": execution_time, + }) + + # 将工具结果添加到消息历史 + result_str = json.dumps(result, ensure_ascii=False) if isinstance(result, (dict, list)) else str(result) + messages.append({ + "role": "tool", + "tool_call_id": tool_call_id, + "content": result_str[:5000] # 截断过长的结果 + }) + + logger.info(f"[Tool Call] 执行成功,耗时 {execution_time:.2f}s") + + except Exception as e: + execution_time = (datetime.now() - start_time).total_seconds() + error_msg = str(e) + + step_result = StepResult( + step_index=step_index, + tool=tool_name, + arguments=arguments, + status="failed", + error=error_msg, + execution_time=execution_time, + ) + step_results.append(step_result) + + # 发送步骤失败事件 + yield self._format_sse("step_complete", { + "step_index": step_index, + "tool": tool_name, + "status": "failed", + "error": error_msg, + "execution_time": execution_time, + }) + + # 将错误信息添加到消息历史 + messages.append({ + "role": "tool", + "tool_call_id": tool_call_id, + "content": f"工具执行失败: {error_msg}" + }) + + logger.error(f"[Tool Call] 执行失败: {error_msg}") + + logger.info(f"[Tool Call] ========== 工具调用结束 ==========") + step_index += 1 + + else: + # 没有工具调用,模型生成了最终回复 + logger.info(f"[Agent Stream] 模型生成最终回复") + break + + # 构建 plan 对象(用于保存到 ES) + plan = ExecutionPlan( + goal=f"分析用户问题:{user_query[:50]}...", + reasoning="使用工具获取相关数据进行分析", + steps=[ToolCall(tool=s["tool"], arguments=s["arguments"], reason=s["reason"]) for s in plan_steps], + ) + + # 阶段3: 生成最终总结 yield self._format_sse("status", {"stage": "summarizing", "message": "正在生成最终总结..."}) # 收集成功的结果 @@ -2647,84 +2703,34 @@ class MCPAgentIntegrated: # 初始化 final_summary final_summary = "" - if not successful_results: + if not successful_results and step_index == 0: + # 如果没有执行任何工具(模型直接回复),使用模型的回复 + if assistant_message and assistant_message.content: + final_summary = assistant_message.content + # 流式发送(虽然已经是完整的,但保持前端兼容) + yield self._format_sse("summary_chunk", {"content": final_summary}) + else: + final_summary = "抱歉,我无法处理您的请求。" + yield self._format_sse("summary_chunk", {"content": final_summary}) + elif not successful_results: + # 所有步骤都失败 final_summary = "很抱歉,所有步骤都执行失败,无法生成分析报告。" - yield self._format_sse("summary", { - "content": final_summary, - "metadata": { - "total_steps": len(plan.steps), - "successful_steps": 0, - "failed_steps": len(step_results), - "total_execution_time": sum(r.execution_time for r in step_results), - }, - }) + yield self._format_sse("summary_chunk", {"content": final_summary}) else: - # 构建结果文本(精简版) - 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": """你是专业的金融研究助手。根据执行结果,生成简洁清晰的报告。 - -## 数据可视化能力 -如果执行结果中包含数值型数据(如财务指标、交易数据、时间序列等),你可以使用 ECharts 生成图表来增强报告的可读性。 - -支持的图表类型: -- 折线图(line):适合时间序列数据(如股价走势、财务指标趋势) -- 柱状图(bar):适合对比数据(如不同年份的收入、利润对比) -- 饼图(pie):适合占比数据(如业务结构、资产分布) - -### 图表格式(使用 Markdown 代码块) -在报告中插入图表时,使用以下格式: - -```echarts -{ - "title": {"text": "图表标题"}, - "tooltip": {}, - "xAxis": {"type": "category", "data": ["类别1", "类别2"]}, - "yAxis": {"type": "value"}, - "series": [{"name": "数据系列", "type": "line", "data": [100, 200]}] -} -``` - -**重要提示**: -- ECharts 配置必须是合法的 JSON 格式 -- 只在有明确数值数据时才生成图表 -- 不要凭空捏造数据""" - }, - { - "role": "user", - "content": f"""用户问题:{user_query} - -执行计划:{plan.goal} - -执行结果: -{results_text} - -请生成专业的分析报告(500字以内)。如果结果中包含数值型数据,请使用 ECharts 图表进行可视化展示。""" - }, - ] - - # 使用流式 API 生成总结(带 DeepMoney 备选) - final_summary = "" - + # 有成功的工具调用,使用流式 API 生成最终回复 try: - summary_stream = self.llm_client.chat.completions.create( - model=self.llm_model, - messages=messages, + # 使用流式 API 生成最终回复(不再传入 tools,让模型生成文本回复) + summary_stream = llm_client.chat.completions.create( + model=llm_model, + messages=messages, # messages 已包含所有工具调用历史 temperature=0.7, - max_tokens=self.llm_max_tokens, + max_tokens=llm_max_tokens, stream=True, # 启用流式输出 ) # 逐块发送总结内容 for chunk in summary_stream: - if chunk.choices[0].delta.content: + if chunk.choices and chunk.choices[0].delta.content: content_chunk = chunk.choices[0].delta.content final_summary += content_chunk @@ -2736,57 +2742,26 @@ class MCPAgentIntegrated: logger.info("[Summary] 流式总结完成") except Exception as llm_error: - # 检查是否是内容风控错误(400) - error_str = str(llm_error) - if "400" in error_str and ("content_filter" in error_str or "high risk" in error_str): - logger.warning(f"[Summary] LLM 内容风控拒绝,切换到 DeepMoney: {error_str}") + logger.error(f"[Summary] 流式总结失败: {llm_error}") + # 降级:使用工具调用结果的简单拼接 + results_text = "\n\n".join([ + f"**{r.tool}**: {str(r.result)[:500]}..." + for r in successful_results[:5] + ]) + final_summary = f"根据查询结果:\n\n{results_text}" + yield self._format_sse("summary_chunk", {"content": final_summary}) + logger.warning("[Summary] 使用降级方案") - yield self._format_sse("status", { - "stage": "summarizing", - "message": "切换到备用模型生成总结..." - }) - - try: - # 使用 DeepMoney 备选方案(非流式) - fallback_response = self.deepmoney_client.chat.completions.create( - model=self.deepmoney_model, - messages=messages, - temperature=0.7, - max_tokens=DEEPMONEY_CONFIG.get("max_tokens", 8192), - ) - - final_summary = fallback_response.choices[0].message.content - - # 发送完整的总结内容(一次性) - yield self._format_sse("summary_chunk", { - "content": final_summary - }) - - logger.info(f"[Summary] DeepMoney 备选方案成功") - - except Exception as fallback_error: - logger.error(f"[Summary] DeepMoney 备选方案也失败: {fallback_error}") - # 使用降级方案:简单拼接执行结果 - final_summary = f"执行了 {len(plan.steps)} 个步骤,其中 {len(successful_results)} 个成功。\n\n执行结果:\n{results_text[:500]}..." - yield self._format_sse("summary_chunk", { - "content": final_summary - }) - logger.warning("[Summary] 使用降级方案(简单拼接)") - else: - # 不是内容风控错误,直接抛出 - logger.error(f"[Summary] LLM 调用失败(非风控原因): {llm_error}") - raise - - # 发送完整的总结和元数据 - yield self._format_sse("summary", { - "content": final_summary, - "metadata": { - "total_steps": len(plan.steps), - "successful_steps": len(successful_results), - "failed_steps": len([r for r in step_results if r.status == "failed"]), - "total_execution_time": sum(r.execution_time for r in step_results), - }, - }) + # 发送完整的总结和元数据 + yield self._format_sse("summary", { + "content": final_summary, + "metadata": { + "total_steps": len(plan.steps) if plan_steps else 0, + "successful_steps": len(successful_results), + "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) if step_results else 0, + }, + }) # 保存 Agent 回复到 ES(如果提供了 session_id) if session_id and user_id: @@ -2834,8 +2809,8 @@ class MCPAgentIntegrated: except Exception as e: logger.error(f"[ES] 保存 Agent 回复失败: {e}", exc_info=True) - # 发送完成事件 - yield self._format_sse("done", {"message": "处理完成"}) + # 发送完成事件(包含 session_id) + yield self._format_sse("done", {"message": "处理完成", "session_id": session_id}) except Exception as e: logger.error(f"[Agent Stream] 错误: {str(e)}", exc_info=True)