From 4fe6b68819d3b382f8df6898c7e9d3d764a0cc2f Mon Sep 17 00:00:00 2001 From: root <953994191@qq.com> Date: Wed, 29 Apr 2026 23:10:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=AC=E5=85=B1=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=EF=BC=9A=E8=81=94=E7=BD=91=E6=90=9C=E7=B4=A2=EF=BC=88?= =?UTF-8?q?DuckDuckGo=EF=BC=89=E5=92=8C=E5=8F=AF=E8=A7=86=E5=8C=96?= =?UTF-8?q?=E5=9B=BE=E8=A1=A8=EF=BC=88Mermaid=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/core/__init__.py | 22 +- backend/app/core/visualization.py | 213 +++++++++++++++++++ backend/app/core/web_search.py | 150 +++++++++++++ backend/app/main_graph/tools/common_tools.py | 55 +++++ backend/app/main_graph/tools/graph_tools.py | 16 +- backend/requirements.txt | 2 + 6 files changed, 453 insertions(+), 5 deletions(-) create mode 100644 backend/app/core/visualization.py create mode 100644 backend/app/core/web_search.py create mode 100644 backend/app/main_graph/tools/common_tools.py diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py index 7c53d48..aa03c1b 100644 --- a/backend/app/core/__init__.py +++ b/backend/app/core/__init__.py @@ -14,6 +14,18 @@ from .human_review import ( ReviewStatus, 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 函数 def classify_intent(user_input: str, context: str = None): @@ -42,5 +54,13 @@ __all__ = [ "ReviewManager", "InMemoryReviewStore", "ReviewStatus", - "HumanReview" + "HumanReview", + "WebSearchTool", + "SearchResult", + "get_web_search_tool", + "web_search", + "VisualizationTool", + "ChartData", + "get_visualization_tool", + "generate_chart" ] diff --git a/backend/app/core/visualization.py b/backend/app/core/visualization.py new file mode 100644 index 0000000..13a86e1 --- /dev/null +++ b/backend/app/core/visualization.py @@ -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"![{data.title}](data:image/png;base64,{img_str})" + 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) diff --git a/backend/app/core/web_search.py b/backend/app/core/web_search.py new file mode 100644 index 0000000..55f0237 --- /dev/null +++ b/backend/app/core/web_search.py @@ -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) diff --git a/backend/app/main_graph/tools/common_tools.py b/backend/app/main_graph/tools/common_tools.py new file mode 100644 index 0000000..a52c908 --- /dev/null +++ b/backend/app/main_graph/tools/common_tools.py @@ -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} diff --git a/backend/app/main_graph/tools/graph_tools.py b/backend/app/main_graph/tools/graph_tools.py index e87352a..ca8cc0d 100644 --- a/backend/app/main_graph/tools/graph_tools.py +++ b/backend/app/main_graph/tools/graph_tools.py @@ -1,6 +1,6 @@ """ -工具定义模块 - 子图工具 + RAG 工具 -Subgraph Tools + RAG Tools +工具定义模块 - 子图工具 + RAG 工具 + 公共工具 +Subgraph Tools + RAG Tools + Common Tools """ # 子图工具 @@ -12,6 +12,14 @@ from .subgraph_tools import ( contact_tool ) +# 公共工具 +from .common_tools import ( + COMMON_TOOLS, + COMMON_TOOLS_BY_NAME, + web_search_tool, + generate_chart_tool +) + # 工具列表和映射(全局常量) -AVAILABLE_TOOLS = SUBGRAPH_TOOLS.copy() -TOOLS_BY_NAME = SUBGRAPH_TOOLS_BY_NAME.copy() +AVAILABLE_TOOLS = SUBGRAPH_TOOLS.copy() + COMMON_TOOLS.copy() +TOOLS_BY_NAME = {**SUBGRAPH_TOOLS_BY_NAME, **COMMON_TOOLS_BY_NAME} diff --git a/backend/requirements.txt b/backend/requirements.txt index e26cef0..1465c93 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -40,6 +40,8 @@ rich==15.0.0 PyYAML==6.0.3 numpy>=1.26.2 pyjwt==2.8.0 +duckduckgo-search>=6.5.0 # 免费联网搜索 +matplotlib>=3.9.0 # 可视化图表 # Document Processing unstructured==0.22.21