diff --git a/mcp_server.py b/mcp_server.py index 371e2825..971a94e5 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -1298,7 +1298,50 @@ class MCPAgentIntegrated: messages = [ { "role": "system", - "content": "你是专业的金融研究助手。根据执行结果,生成简洁清晰的报告。" + "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]}] +} +``` + +### 示例 +如果有股价数据,可以这样呈现: + +**股价走势分析** + +近30日股价呈现上涨趋势,最高达到1850元。 + +```echarts +{ + "title": {"text": "近30日股价走势", "left": "center"}, + "tooltip": {"trigger": "axis"}, + "xAxis": {"type": "category", "data": ["2024-01-01", "2024-01-02", "2024-01-03"]}, + "yAxis": {"type": "value", "name": "股价(元)"}, + "series": [{"name": "收盘价", "type": "line", "data": [1800, 1820, 1850], "smooth": true}] +} +``` + +**重要提示**: +- ECharts 配置必须是合法的 JSON 格式 +- 只在有明确数值数据时才生成图表 +- 不要凭空捏造数据""" }, { "role": "user", @@ -1309,7 +1352,7 @@ class MCPAgentIntegrated: 执行结果: {results_text} -请生成专业的分析报告(300字以内)。""" +请生成专业的分析报告(500字以内)。如果结果中包含数值型数据,请使用 ECharts 图表进行可视化展示。""" }, ] @@ -1318,7 +1361,7 @@ class MCPAgentIntegrated: model="kimi-k2-turbo-preview", # 使用非思考模型,更快 messages=messages, temperature=0.7, - max_tokens=1000, + max_tokens=2000, # 增加 token 限制以支持图表配置 ) summary = response.choices[0].message.content @@ -1632,7 +1675,7 @@ async def agent_chat(request: AgentChatRequest): try: # 将执行步骤转换为JSON字符串 steps_json = json.dumps( - [{"tool": step.tool, "result": step.result} for step in response.steps], + [{"tool": step.tool, "status": step.status, "result": step.result} for step in response.step_results], ensure_ascii=False ) @@ -1642,12 +1685,12 @@ async def agent_chat(request: AgentChatRequest): user_nickname=request.user_nickname or "匿名用户", user_avatar=request.user_avatar or "", message_type="assistant", - message=response.final_answer, - plan=response.plan, + message=response.final_summary, # 使用 final_summary 而不是 final_answer + plan=response.plan.dict() if response.plan else None, # 转换为字典 steps=steps_json, ) except Exception as e: - logger.error(f"保存 Agent 回复失败: {e}") + logger.error(f"保存 Agent 回复失败: {e}", exc_info=True) # 在响应中返回 session_id response_dict = response.dict() diff --git a/src/components/ChatBot/EChartsRenderer.js b/src/components/ChatBot/EChartsRenderer.js new file mode 100644 index 00000000..396b4ef5 --- /dev/null +++ b/src/components/ChatBot/EChartsRenderer.js @@ -0,0 +1,72 @@ +// src/components/ChatBot/EChartsRenderer.js +// ECharts 图表渲染组件 + +import React, { useEffect, useRef } from 'react'; +import { Box, useColorModeValue } from '@chakra-ui/react'; +import * as echarts from 'echarts'; + +/** + * ECharts 图表渲染组件 + * @param {Object} option - ECharts 配置对象 + * @param {number} height - 图表高度(默认 400px) + */ +export const EChartsRenderer = ({ option, height = 400 }) => { + const chartRef = useRef(null); + const chartInstance = useRef(null); + const bgColor = useColorModeValue('white', 'gray.800'); + + useEffect(() => { + if (!chartRef.current || !option) return; + + // 初始化图表 + if (!chartInstance.current) { + chartInstance.current = echarts.init(chartRef.current); + } + + // 设置默认主题配置 + const defaultOption = { + backgroundColor: 'transparent', + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + ...option, + }; + + // 设置图表配置 + chartInstance.current.setOption(defaultOption, true); + + // 响应式调整大小 + const handleResize = () => { + chartInstance.current?.resize(); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + // chartInstance.current?.dispose(); // 不要销毁,避免重新渲染时闪烁 + }; + }, [option]); + + // 组件卸载时销毁图表 + useEffect(() => { + return () => { + chartInstance.current?.dispose(); + chartInstance.current = null; + }; + }, []); + + return ( + + ); +}; diff --git a/src/components/ChatBot/MarkdownWithCharts.js b/src/components/ChatBot/MarkdownWithCharts.js new file mode 100644 index 00000000..6f140d14 --- /dev/null +++ b/src/components/ChatBot/MarkdownWithCharts.js @@ -0,0 +1,166 @@ +// src/components/ChatBot/MarkdownWithCharts.js +// 支持 ECharts 图表的 Markdown 渲染组件 + +import React from 'react'; +import { Box, Alert, AlertIcon, Text, VStack, Code } from '@chakra-ui/react'; +import ReactMarkdown from 'react-markdown'; +import { EChartsRenderer } from './EChartsRenderer'; +import { logger } from '@utils/logger'; + +/** + * 解析 Markdown 内容,提取 ECharts 代码块 + * @param {string} markdown - Markdown 文本 + * @returns {Array} - 包含文本和图表的数组 + */ +const parseMarkdownWithCharts = (markdown) => { + if (!markdown) return []; + + const parts = []; + const echartsRegex = /```echarts\s*\n([\s\S]*?)```/g; + let lastIndex = 0; + let match; + + while ((match = echartsRegex.exec(markdown)) !== null) { + // 添加代码块前的文本 + if (match.index > lastIndex) { + const textBefore = markdown.substring(lastIndex, match.index).trim(); + if (textBefore) { + parts.push({ type: 'text', content: textBefore }); + } + } + + // 添加 ECharts 配置 + const chartConfig = match[1].trim(); + parts.push({ type: 'chart', content: chartConfig }); + + lastIndex = match.index + match[0].length; + } + + // 添加剩余文本 + if (lastIndex < markdown.length) { + const textAfter = markdown.substring(lastIndex).trim(); + if (textAfter) { + parts.push({ type: 'text', content: textAfter }); + } + } + + // 如果没有找到图表,返回整个 markdown 作为文本 + if (parts.length === 0) { + parts.push({ type: 'text', content: markdown }); + } + + return parts; +}; + +/** + * 支持 ECharts 图表的 Markdown 渲染组件 + * @param {string} content - Markdown 文本 + */ +export const MarkdownWithCharts = ({ content }) => { + const parts = parseMarkdownWithCharts(content); + + return ( + + {parts.map((part, index) => { + if (part.type === 'text') { + // 渲染普通 Markdown + return ( + + ( + + {children} + + ), + h1: ({ children }) => ( + + {children} + + ), + h2: ({ children }) => ( + + {children} + + ), + h3: ({ children }) => ( + + {children} + + ), + ul: ({ children }) => ( + + {children} + + ), + ol: ({ children }) => ( + + {children} + + ), + li: ({ children }) => ( + + {children} + + ), + code: ({ inline, children }) => + inline ? ( + + {children} + + ) : ( + + {children} + + ), + blockquote: ({ children }) => ( + + {children} + + ), + }} + > + {part.content} + + + ); + } else if (part.type === 'chart') { + // 渲染 ECharts 图表 + try { + const chartOption = JSON.parse(part.content); + return ( + + + + ); + } catch (error) { + logger.error('解析 ECharts 配置失败', error, part.content); + return ( + + + + + 图表配置解析失败 + + + {part.content.substring(0, 200)} + {part.content.length > 200 ? '...' : ''} + + + + ); + } + } + return null; + })} + + ); +}; diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js index 56cd16d1..738e255a 100644 --- a/src/views/AgentChat/index.js +++ b/src/views/AgentChat/index.js @@ -53,6 +53,7 @@ import { import { useAuth } from '@contexts/AuthContext'; import { PlanCard } from '@components/ChatBot/PlanCard'; import { StepResultCard } from '@components/ChatBot/StepResultCard'; +import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts'; import { logger } from '@utils/logger'; import axios from 'axios'; @@ -312,25 +313,15 @@ const AgentChatV3 = () => { } // 显示执行步骤 - if (data.steps && data.steps.length > 0) { - stepResults = data.steps; - addMessage({ - type: MessageTypes.AGENT_EXECUTING, - content: '正在执行步骤...', - plan: currentPlan, - stepResults: stepResults, - timestamp: new Date().toISOString(), - }); + if (data.step_results && data.step_results.length > 0) { + stepResults = data.step_results; setCurrentProgress(70); } - // 移除执行中消息 - setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); - - // 显示最终结果 + // 显示最终结果(包含执行步骤) addMessage({ type: MessageTypes.AGENT_RESPONSE, - content: data.final_answer || data.message || '处理完成', + content: data.final_summary || data.message || '处理完成', plan: currentPlan, stepResults: stepResults, metadata: data.metadata, @@ -801,7 +792,7 @@ const MessageRenderer = ({ message, userAvatar }) => { } /> - {/* 最终总结 */} + {/* 最终总结(支持 Markdown + ECharts) */} { borderColor={borderColor} boxShadow="md" > - - {message.content} - + {/* 元数据 */} {message.metadata && (