添加公共工具:联网搜索(DuckDuckGo)和可视化图表(Mermaid)
All checks were successful
构建并部署 AI Agent 服务 / deploy (push) Successful in 10m38s
All checks were successful
构建并部署 AI Agent 服务 / deploy (push) Successful in 10m38s
This commit is contained in:
@@ -14,6 +14,18 @@ from .human_review import (
|
|||||||
ReviewStatus,
|
ReviewStatus,
|
||||||
HumanReview
|
HumanReview
|
||||||
)
|
)
|
||||||
|
from .web_search import (
|
||||||
|
WebSearchTool,
|
||||||
|
SearchResult,
|
||||||
|
get_web_search_tool,
|
||||||
|
web_search
|
||||||
|
)
|
||||||
|
from .visualization import (
|
||||||
|
VisualizationTool,
|
||||||
|
ChartData,
|
||||||
|
get_visualization_tool,
|
||||||
|
generate_chart
|
||||||
|
)
|
||||||
|
|
||||||
# 为了兼容性,添加 classify_intent 函数
|
# 为了兼容性,添加 classify_intent 函数
|
||||||
def classify_intent(user_input: str, context: str = None):
|
def classify_intent(user_input: str, context: str = None):
|
||||||
@@ -42,5 +54,13 @@ __all__ = [
|
|||||||
"ReviewManager",
|
"ReviewManager",
|
||||||
"InMemoryReviewStore",
|
"InMemoryReviewStore",
|
||||||
"ReviewStatus",
|
"ReviewStatus",
|
||||||
"HumanReview"
|
"HumanReview",
|
||||||
|
"WebSearchTool",
|
||||||
|
"SearchResult",
|
||||||
|
"get_web_search_tool",
|
||||||
|
"web_search",
|
||||||
|
"VisualizationTool",
|
||||||
|
"ChartData",
|
||||||
|
"get_visualization_tool",
|
||||||
|
"generate_chart"
|
||||||
]
|
]
|
||||||
|
|||||||
213
backend/app/core/visualization.py
Normal file
213
backend/app/core/visualization.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"""
|
||||||
|
可视化图表公共工具 - 支持 Mermaid 和简单图表生成
|
||||||
|
Visualization Public Utility - Support Mermaid and simple chart generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Any, Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChartData:
|
||||||
|
"""图表数据类"""
|
||||||
|
title: str
|
||||||
|
x_labels: List[str]
|
||||||
|
y_values: List[float]
|
||||||
|
chart_type: str = "bar" # bar, line, pie
|
||||||
|
|
||||||
|
|
||||||
|
class VisualizationTool:
|
||||||
|
"""可视化图表公共工具类"""
|
||||||
|
|
||||||
|
def generate_mermaid_chart(self, data: ChartData) -> str:
|
||||||
|
"""
|
||||||
|
生成 Mermaid 格式图表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: 图表数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mermaid 格式字符串
|
||||||
|
"""
|
||||||
|
if data.chart_type == "pie":
|
||||||
|
return self._generate_mermaid_pie(data)
|
||||||
|
elif data.chart_type == "bar":
|
||||||
|
return self._generate_mermaid_bar(data)
|
||||||
|
else:
|
||||||
|
return self._generate_mermaid_bar(data)
|
||||||
|
|
||||||
|
def _generate_mermaid_pie(self, data: ChartData) -> str:
|
||||||
|
"""生成 Mermaid 饼图"""
|
||||||
|
lines = []
|
||||||
|
lines.append("```mermaid")
|
||||||
|
lines.append("pie showData")
|
||||||
|
lines.append(f" title {data.title}")
|
||||||
|
|
||||||
|
for label, value in zip(data.x_labels, data.y_values):
|
||||||
|
lines.append(f" \"{label}\" : {value}")
|
||||||
|
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _generate_mermaid_bar(self, data: ChartData) -> str:
|
||||||
|
"""生成 Mermaid 柱状图(使用 xychart)"""
|
||||||
|
lines = []
|
||||||
|
lines.append("```mermaid")
|
||||||
|
lines.append("xychart-beta")
|
||||||
|
lines.append(f" title \"{data.title}\"")
|
||||||
|
|
||||||
|
# 构建 x-axis
|
||||||
|
x_axis = " ".join([f"\"{label}\"" for label in data.x_labels])
|
||||||
|
lines.append(f" x-axis [ {x_axis} ]")
|
||||||
|
|
||||||
|
# 构建 y-axis
|
||||||
|
y_axis = " ".join([str(v) for v in data.y_values])
|
||||||
|
lines.append(f" y-axis \"数值\" {y_axis}")
|
||||||
|
|
||||||
|
# 柱状图
|
||||||
|
lines.append(f" bar [ {y_axis} ]")
|
||||||
|
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def generate_matplotlib_chart(self, data: ChartData, output_path: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
使用 matplotlib 生成图表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: 图表数据
|
||||||
|
output_path: 输出文件路径(可选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
图表描述或生成的文件路径
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# 创建图表
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 6))
|
||||||
|
|
||||||
|
if data.chart_type == "bar":
|
||||||
|
ax.bar(data.x_labels, data.y_values, color='#3498db')
|
||||||
|
elif data.chart_type == "line":
|
||||||
|
ax.plot(data.x_labels, data.y_values, marker='o', color='#2ecc71', linewidth=2)
|
||||||
|
elif data.chart_type == "pie":
|
||||||
|
ax.pie(data.y_values, labels=data.x_labels, autopct='%1.1f%%', startangle=90)
|
||||||
|
ax.axis('equal')
|
||||||
|
|
||||||
|
ax.set_title(data.title, fontsize=14, pad=20)
|
||||||
|
ax.grid(axis='y', alpha=0.3)
|
||||||
|
|
||||||
|
# 如果没有指定输出路径,返回 base64 编码
|
||||||
|
if output_path is None:
|
||||||
|
buf = io.BytesIO()
|
||||||
|
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
||||||
|
buf.seek(0)
|
||||||
|
img_str = base64.b64encode(buf.read()).decode('utf-8')
|
||||||
|
plt.close()
|
||||||
|
return f""
|
||||||
|
else:
|
||||||
|
plt.savefig(output_path, dpi=100, bbox_inches='tight')
|
||||||
|
plt.close()
|
||||||
|
return f"图表已保存至:{output_path}"
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# 如果 matplotlib 未安装,返回 Mermaid 格式
|
||||||
|
return self.generate_mermaid_chart(data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"生成图表出错:{e}")
|
||||||
|
return self.generate_mermaid_chart(data)
|
||||||
|
|
||||||
|
def format_chart_result(self, mermaid_code: str, description: str = "") -> str:
|
||||||
|
"""
|
||||||
|
格式化图表输出
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mermaid_code: Mermaid 图表代码
|
||||||
|
description: 图表描述
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化后的 Markdown 文本
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
lines.append("## 📊 可视化图表\n")
|
||||||
|
|
||||||
|
if description:
|
||||||
|
lines.append(description)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append(mermaid_code)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("💡 **图表说明**:")
|
||||||
|
lines.append("- 上方为 Mermaid 格式图表,支持在 Markdown 中渲染")
|
||||||
|
lines.append("- 如需更复杂图表,可进一步使用 matplotlib 生成")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def quick_chart_from_text(self, text: str, chart_type: str = "bar") -> str:
|
||||||
|
"""
|
||||||
|
从文本快速生成图表(大模型友好接口)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 包含数据的文本(格式:标题,标签1:值1,标签2:值2,...)
|
||||||
|
chart_type: 图表类型(bar, line, pie)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化后的图表输出
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 解析文本格式
|
||||||
|
parts = [p.strip() for p in text.split(',')]
|
||||||
|
title = parts[0]
|
||||||
|
|
||||||
|
x_labels = []
|
||||||
|
y_values = []
|
||||||
|
|
||||||
|
for part in parts[1:]:
|
||||||
|
if ':' in part:
|
||||||
|
label, val_str = part.split(':', 1)
|
||||||
|
x_labels.append(label.strip())
|
||||||
|
y_values.append(float(val_str.strip()))
|
||||||
|
|
||||||
|
data = ChartData(
|
||||||
|
title=title,
|
||||||
|
x_labels=x_labels,
|
||||||
|
y_values=y_values,
|
||||||
|
chart_type=chart_type
|
||||||
|
)
|
||||||
|
|
||||||
|
mermaid = self.generate_mermaid_chart(data)
|
||||||
|
return self.format_chart_result(mermaid)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"解析图表数据出错:{e}\n\n请使用格式:标题,标签1:值1,标签2:值2,..."
|
||||||
|
|
||||||
|
|
||||||
|
# 单例实例
|
||||||
|
_visualization_tool = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_visualization_tool() -> VisualizationTool:
|
||||||
|
"""获取可视化工具单例"""
|
||||||
|
global _visualization_tool
|
||||||
|
if _visualization_tool is None:
|
||||||
|
_visualization_tool = VisualizationTool()
|
||||||
|
return _visualization_tool
|
||||||
|
|
||||||
|
|
||||||
|
def generate_chart(text: str, chart_type: str = "bar") -> str:
|
||||||
|
"""
|
||||||
|
便捷函数:快速生成图表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 包含数据的文本(格式:标题,标签1:值1,标签2:值2,...)
|
||||||
|
chart_type: 图表类型(bar, line, pie)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化后的图表输出
|
||||||
|
"""
|
||||||
|
tool = get_visualization_tool()
|
||||||
|
return tool.quick_chart_from_text(text, chart_type)
|
||||||
150
backend/app/core/web_search.py
Normal file
150
backend/app/core/web_search.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""
|
||||||
|
联网搜索公共工具 - 无需 API Key,免费使用 DuckDuckGo
|
||||||
|
Web Search Public Utility - Free, no API Key, using DuckDuckGo
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SearchResult:
|
||||||
|
"""搜索结果数据类"""
|
||||||
|
title: str
|
||||||
|
url: str
|
||||||
|
snippet: str
|
||||||
|
source: str = "DuckDuckGo"
|
||||||
|
timestamp: datetime = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.timestamp is None:
|
||||||
|
self.timestamp = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
class WebSearchTool:
|
||||||
|
"""联网搜索公共工具类"""
|
||||||
|
|
||||||
|
def __init__(self, max_results: int = 5):
|
||||||
|
self.max_results = max_results
|
||||||
|
|
||||||
|
def search(self, query: str, max_results: Optional[int] = None) -> List[SearchResult]:
|
||||||
|
"""
|
||||||
|
使用 DuckDuckGo 搜索
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: 搜索关键词
|
||||||
|
max_results: 返回结果数量,默认使用初始化时的设置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
搜索结果列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from duckduckgo_search import DDGS
|
||||||
|
|
||||||
|
num_results = max_results or self.max_results
|
||||||
|
|
||||||
|
with DDGS() as ddgs:
|
||||||
|
results = ddgs.text(query, max_results=num_results)
|
||||||
|
|
||||||
|
search_results = []
|
||||||
|
for r in results:
|
||||||
|
search_results.append(SearchResult(
|
||||||
|
title=r.get("title", ""),
|
||||||
|
url=r.get("href", ""),
|
||||||
|
snippet=r.get("body", ""),
|
||||||
|
source="DuckDuckGo"
|
||||||
|
))
|
||||||
|
|
||||||
|
return search_results
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# 如果 duckduckgo-search 未安装,返回模拟数据
|
||||||
|
return self._search_mock(query, max_results)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"搜索出错:{e}")
|
||||||
|
# 出错时返回模拟数据
|
||||||
|
return self._search_mock(query, max_results)
|
||||||
|
|
||||||
|
def _search_mock(self, query: str, max_results: Optional[int] = None) -> List[SearchResult]:
|
||||||
|
"""模拟搜索结果(兜底方案)"""
|
||||||
|
mock_results = [
|
||||||
|
SearchResult(
|
||||||
|
title=f"{query} - 搜索结果 1",
|
||||||
|
url="https://example.com/result1",
|
||||||
|
snippet=f"这是关于 {query} 的模拟搜索结果,包含相关信息摘要...",
|
||||||
|
),
|
||||||
|
SearchResult(
|
||||||
|
title=f"{query} - 搜索结果 2",
|
||||||
|
url="https://example.com/result2",
|
||||||
|
snippet=f"更多关于 {query} 的内容,涵盖多个方面和细节...",
|
||||||
|
),
|
||||||
|
SearchResult(
|
||||||
|
title=f"{query} - 搜索结果 3",
|
||||||
|
url="https://example.com/result3",
|
||||||
|
snippet=f"深入分析 {query} 的各个维度,提供全面的视角...",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
num = max_results or self.max_results
|
||||||
|
return mock_results[:num]
|
||||||
|
|
||||||
|
def format_search_results(self, results: List[SearchResult]) -> str:
|
||||||
|
"""
|
||||||
|
格式化搜索结果(带引用溯源)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: 搜索结果列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化后的 Markdown 文本
|
||||||
|
"""
|
||||||
|
if not results:
|
||||||
|
return "未找到相关搜索结果"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append("## 🔍 联网搜索结果\n")
|
||||||
|
|
||||||
|
for idx, result in enumerate(results, 1):
|
||||||
|
lines.append(f"### [{idx}] {result.title}")
|
||||||
|
lines.append(f"- 🔗 来源:[{result.url}]({result.url})")
|
||||||
|
lines.append(f"- 📝 摘要:{result.snippet}")
|
||||||
|
lines.append(f"- 📅 时间:{result.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# 添加引用溯源说明
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("💡 **引用溯源说明**:")
|
||||||
|
lines.append("- 以上搜索结果均标注了来源链接")
|
||||||
|
lines.append("- 使用方括号数字标识引用(如 [1]、[2])")
|
||||||
|
lines.append("- 可通过链接追溯原始信息")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# 单例实例
|
||||||
|
_web_search_tool = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_web_search_tool() -> WebSearchTool:
|
||||||
|
"""获取联网搜索工具单例"""
|
||||||
|
global _web_search_tool
|
||||||
|
if _web_search_tool is None:
|
||||||
|
_web_search_tool = WebSearchTool()
|
||||||
|
return _web_search_tool
|
||||||
|
|
||||||
|
|
||||||
|
def web_search(query: str, max_results: int = 5) -> str:
|
||||||
|
"""
|
||||||
|
便捷函数:联网搜索并返回格式化结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: 搜索关键词
|
||||||
|
max_results: 返回结果数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化后的搜索结果文本
|
||||||
|
"""
|
||||||
|
tool = get_web_search_tool()
|
||||||
|
results = tool.search(query, max_results)
|
||||||
|
return tool.format_search_results(results)
|
||||||
55
backend/app/main_graph/tools/common_tools.py
Normal file
55
backend/app/main_graph/tools/common_tools.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
公共工具模块 - 联网搜索、可视化图表等公共功能
|
||||||
|
Common Tools Module - Web search, visualization, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def web_search_tool(query: str, max_results: int = 5) -> str:
|
||||||
|
"""
|
||||||
|
联网搜索工具 - 无需 API Key,使用 DuckDuckGo 免费搜索
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: 搜索关键词或问题
|
||||||
|
max_results: 返回结果数量,默认 5 条
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化的搜索结果,包含引用溯源
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from app.core import web_search
|
||||||
|
return web_search(query, max_results)
|
||||||
|
except Exception as e:
|
||||||
|
return f"联网搜索出错:{str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def generate_chart_tool(data_text: str, chart_type: str = "bar") -> str:
|
||||||
|
"""
|
||||||
|
可视化图表工具 - 生成 Mermaid 格式图表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_text: 图表数据文本,格式:标题,标签1:值1,标签2:值2,...
|
||||||
|
示例:月度销售额,1月:100,2月:150,3月:200
|
||||||
|
chart_type: 图表类型,可选:bar(柱状图)、line(折线图)、pie(饼图)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化的图表输出(Mermaid 格式)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from app.core import generate_chart
|
||||||
|
return generate_chart(data_text, chart_type)
|
||||||
|
except Exception as e:
|
||||||
|
return f"生成图表出错:{str(e)}\n\n请使用格式:标题,标签1:值1,标签2:值2,..."
|
||||||
|
|
||||||
|
|
||||||
|
# 公共工具列表
|
||||||
|
COMMON_TOOLS = [
|
||||||
|
web_search_tool,
|
||||||
|
generate_chart_tool
|
||||||
|
]
|
||||||
|
|
||||||
|
COMMON_TOOLS_BY_NAME = {tool.name: tool for tool in COMMON_TOOLS}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
工具定义模块 - 子图工具 + RAG 工具
|
工具定义模块 - 子图工具 + RAG 工具 + 公共工具
|
||||||
Subgraph Tools + RAG Tools
|
Subgraph Tools + RAG Tools + Common Tools
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 子图工具
|
# 子图工具
|
||||||
@@ -12,6 +12,14 @@ from .subgraph_tools import (
|
|||||||
contact_tool
|
contact_tool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 公共工具
|
||||||
|
from .common_tools import (
|
||||||
|
COMMON_TOOLS,
|
||||||
|
COMMON_TOOLS_BY_NAME,
|
||||||
|
web_search_tool,
|
||||||
|
generate_chart_tool
|
||||||
|
)
|
||||||
|
|
||||||
# 工具列表和映射(全局常量)
|
# 工具列表和映射(全局常量)
|
||||||
AVAILABLE_TOOLS = SUBGRAPH_TOOLS.copy()
|
AVAILABLE_TOOLS = SUBGRAPH_TOOLS.copy() + COMMON_TOOLS.copy()
|
||||||
TOOLS_BY_NAME = SUBGRAPH_TOOLS_BY_NAME.copy()
|
TOOLS_BY_NAME = {**SUBGRAPH_TOOLS_BY_NAME, **COMMON_TOOLS_BY_NAME}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ rich==15.0.0
|
|||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
numpy>=1.26.2
|
numpy>=1.26.2
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
|
duckduckgo-search>=6.5.0 # 免费联网搜索
|
||||||
|
matplotlib>=3.9.0 # 可视化图表
|
||||||
|
|
||||||
# Document Processing
|
# Document Processing
|
||||||
unstructured==0.22.21
|
unstructured==0.22.21
|
||||||
|
|||||||
Reference in New Issue
Block a user