agent功能开发增加MCP后端
This commit is contained in:
@@ -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()
|
||||
|
||||
72
src/components/ChatBot/EChartsRenderer.js
Normal file
72
src/components/ChatBot/EChartsRenderer.js
Normal file
@@ -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 (
|
||||
<Box
|
||||
ref={chartRef}
|
||||
width="100%"
|
||||
height={`${height}px`}
|
||||
bg={bgColor}
|
||||
borderRadius="md"
|
||||
boxShadow="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
166
src/components/ChatBot/MarkdownWithCharts.js
Normal file
166
src/components/ChatBot/MarkdownWithCharts.js
Normal file
@@ -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 (
|
||||
<VStack align="stretch" spacing={4}>
|
||||
{parts.map((part, index) => {
|
||||
if (part.type === 'text') {
|
||||
// 渲染普通 Markdown
|
||||
return (
|
||||
<Box key={index}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
// 自定义渲染样式
|
||||
p: ({ children }) => (
|
||||
<Text mb={2} fontSize="sm">
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
h1: ({ children }) => (
|
||||
<Text fontSize="xl" fontWeight="bold" mb={3}>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<Text fontSize="lg" fontWeight="bold" mb={2}>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<Text fontSize="md" fontWeight="bold" mb={2}>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<Box as="ul" pl={4} mb={2}>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<Box as="ol" pl={4} mb={2}>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<Box as="li" fontSize="sm" mb={1}>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
code: ({ inline, children }) =>
|
||||
inline ? (
|
||||
<Code fontSize="sm" px={1}>
|
||||
{children}
|
||||
</Code>
|
||||
) : (
|
||||
<Code display="block" p={3} borderRadius="md" fontSize="sm" whiteSpace="pre-wrap">
|
||||
{children}
|
||||
</Code>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<Box
|
||||
borderLeftWidth="4px"
|
||||
borderLeftColor="blue.500"
|
||||
pl={4}
|
||||
py={2}
|
||||
fontStyle="italic"
|
||||
color="gray.600"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{part.content}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
);
|
||||
} else if (part.type === 'chart') {
|
||||
// 渲染 ECharts 图表
|
||||
try {
|
||||
const chartOption = JSON.parse(part.content);
|
||||
return (
|
||||
<Box key={index}>
|
||||
<EChartsRenderer option={chartOption} height={350} />
|
||||
</Box>
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('解析 ECharts 配置失败', error, part.content);
|
||||
return (
|
||||
<Alert status="warning" key={index} borderRadius="md">
|
||||
<AlertIcon />
|
||||
<VStack align="flex-start" spacing={1}>
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
图表配置解析失败
|
||||
</Text>
|
||||
<Code fontSize="xs" maxW="100%" overflow="auto">
|
||||
{part.content.substring(0, 200)}
|
||||
{part.content.length > 200 ? '...' : ''}
|
||||
</Code>
|
||||
</VStack>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
@@ -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 }) => {
|
||||
<HStack align="flex-start" maxW="85%">
|
||||
<Avatar size="sm" bg="green.500" icon={<FiCpu fontSize="1rem" />} />
|
||||
<VStack align="stretch" flex="1" spacing={3}>
|
||||
{/* 最终总结 */}
|
||||
{/* 最终总结(支持 Markdown + ECharts) */}
|
||||
<Box
|
||||
bg={agentBubbleBg}
|
||||
px={4}
|
||||
@@ -811,9 +802,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
||||
borderColor={borderColor}
|
||||
boxShadow="md"
|
||||
>
|
||||
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||
{message.content}
|
||||
</Text>
|
||||
<MarkdownWithCharts content={message.content} />
|
||||
|
||||
{/* 元数据 */}
|
||||
{message.metadata && (
|
||||
|
||||
Reference in New Issue
Block a user