agent功能开发增加MCP后端
This commit is contained in:
@@ -1298,7 +1298,50 @@ class MCPAgentIntegrated:
|
|||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"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",
|
"role": "user",
|
||||||
@@ -1309,7 +1352,7 @@ class MCPAgentIntegrated:
|
|||||||
执行结果:
|
执行结果:
|
||||||
{results_text}
|
{results_text}
|
||||||
|
|
||||||
请生成专业的分析报告(300字以内)。"""
|
请生成专业的分析报告(500字以内)。如果结果中包含数值型数据,请使用 ECharts 图表进行可视化展示。"""
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1318,7 +1361,7 @@ class MCPAgentIntegrated:
|
|||||||
model="kimi-k2-turbo-preview", # 使用非思考模型,更快
|
model="kimi-k2-turbo-preview", # 使用非思考模型,更快
|
||||||
messages=messages,
|
messages=messages,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
max_tokens=1000,
|
max_tokens=2000, # 增加 token 限制以支持图表配置
|
||||||
)
|
)
|
||||||
|
|
||||||
summary = response.choices[0].message.content
|
summary = response.choices[0].message.content
|
||||||
@@ -1632,7 +1675,7 @@ async def agent_chat(request: AgentChatRequest):
|
|||||||
try:
|
try:
|
||||||
# 将执行步骤转换为JSON字符串
|
# 将执行步骤转换为JSON字符串
|
||||||
steps_json = json.dumps(
|
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
|
ensure_ascii=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1642,12 +1685,12 @@ async def agent_chat(request: AgentChatRequest):
|
|||||||
user_nickname=request.user_nickname or "匿名用户",
|
user_nickname=request.user_nickname or "匿名用户",
|
||||||
user_avatar=request.user_avatar or "",
|
user_avatar=request.user_avatar or "",
|
||||||
message_type="assistant",
|
message_type="assistant",
|
||||||
message=response.final_answer,
|
message=response.final_summary, # 使用 final_summary 而不是 final_answer
|
||||||
plan=response.plan,
|
plan=response.plan.dict() if response.plan else None, # 转换为字典
|
||||||
steps=steps_json,
|
steps=steps_json,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存 Agent 回复失败: {e}")
|
logger.error(f"保存 Agent 回复失败: {e}", exc_info=True)
|
||||||
|
|
||||||
# 在响应中返回 session_id
|
# 在响应中返回 session_id
|
||||||
response_dict = response.dict()
|
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 { useAuth } from '@contexts/AuthContext';
|
||||||
import { PlanCard } from '@components/ChatBot/PlanCard';
|
import { PlanCard } from '@components/ChatBot/PlanCard';
|
||||||
import { StepResultCard } from '@components/ChatBot/StepResultCard';
|
import { StepResultCard } from '@components/ChatBot/StepResultCard';
|
||||||
|
import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -312,25 +313,15 @@ const AgentChatV3 = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 显示执行步骤
|
// 显示执行步骤
|
||||||
if (data.steps && data.steps.length > 0) {
|
if (data.step_results && data.step_results.length > 0) {
|
||||||
stepResults = data.steps;
|
stepResults = data.step_results;
|
||||||
addMessage({
|
|
||||||
type: MessageTypes.AGENT_EXECUTING,
|
|
||||||
content: '正在执行步骤...',
|
|
||||||
plan: currentPlan,
|
|
||||||
stepResults: stepResults,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
setCurrentProgress(70);
|
setCurrentProgress(70);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除执行中消息
|
// 显示最终结果(包含执行步骤)
|
||||||
setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING));
|
|
||||||
|
|
||||||
// 显示最终结果
|
|
||||||
addMessage({
|
addMessage({
|
||||||
type: MessageTypes.AGENT_RESPONSE,
|
type: MessageTypes.AGENT_RESPONSE,
|
||||||
content: data.final_answer || data.message || '处理完成',
|
content: data.final_summary || data.message || '处理完成',
|
||||||
plan: currentPlan,
|
plan: currentPlan,
|
||||||
stepResults: stepResults,
|
stepResults: stepResults,
|
||||||
metadata: data.metadata,
|
metadata: data.metadata,
|
||||||
@@ -801,7 +792,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
<HStack align="flex-start" maxW="85%">
|
<HStack align="flex-start" maxW="85%">
|
||||||
<Avatar size="sm" bg="green.500" icon={<FiCpu fontSize="1rem" />} />
|
<Avatar size="sm" bg="green.500" icon={<FiCpu fontSize="1rem" />} />
|
||||||
<VStack align="stretch" flex="1" spacing={3}>
|
<VStack align="stretch" flex="1" spacing={3}>
|
||||||
{/* 最终总结 */}
|
{/* 最终总结(支持 Markdown + ECharts) */}
|
||||||
<Box
|
<Box
|
||||||
bg={agentBubbleBg}
|
bg={agentBubbleBg}
|
||||||
px={4}
|
px={4}
|
||||||
@@ -811,9 +802,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
boxShadow="md"
|
boxShadow="md"
|
||||||
>
|
>
|
||||||
<Text fontSize="sm" whiteSpace="pre-wrap">
|
<MarkdownWithCharts content={message.content} />
|
||||||
{message.content}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* 元数据 */}
|
{/* 元数据 */}
|
||||||
{message.metadata && (
|
{message.metadata && (
|
||||||
|
|||||||
Reference in New Issue
Block a user