| | """ |
| | 核心工具模块 - GAIA Agent 基础工具 |
| | 包含:web_search, fetch_task_files, read_file, calc, run_python |
| | """ |
| |
|
| | import os |
| | import re |
| | import json |
| | import tempfile |
| | import zipfile |
| | from typing import Optional |
| |
|
| | import requests |
| | from langchain_core.tools import tool |
| |
|
| | from config import ( |
| | SCORING_API_URL, |
| | SEARCH_MAX_RESULTS, |
| | MAX_FILE_SIZE, |
| | TOOL_TIMEOUT, |
| | TEMP_DIR, |
| | TAVILY_API_KEY, |
| | WIKIPEDIA_MAX_RESULTS, |
| | ARXIV_MAX_RESULTS, |
| | TAVILY_MAX_RESULTS, |
| | ) |
| |
|
| | |
| | try: |
| | from ddgs import DDGS |
| | except ImportError: |
| | try: |
| | from duckduckgo_search import DDGS |
| | except ImportError: |
| | DDGS = None |
| |
|
| | |
| | try: |
| | import wikipedia |
| | wikipedia.set_lang("en") |
| | except ImportError: |
| | wikipedia = None |
| |
|
| | |
| | try: |
| | from tavily import TavilyClient |
| | except ImportError: |
| | TavilyClient = None |
| |
|
| | |
| | try: |
| | import arxiv |
| | except ImportError: |
| | arxiv = None |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @tool |
| | def web_search(query: str, max_results: int = SEARCH_MAX_RESULTS) -> str: |
| | """ |
| | 使用 DuckDuckGo 搜索网络信息。 |
| | |
| | 适用场景: |
| | - 查找人物信息(生卒年、职业、成就等) |
| | - 查找事件详情(时间、地点、参与者等) |
| | - 查找组织/公司信息 |
| | - 获取最新资讯 |
| | |
| | Args: |
| | query: 搜索关键词,建议使用英文 |
| | max_results: 返回结果数量,默认5条 |
| | |
| | Returns: |
| | 搜索结果摘要(标题+内容+URL) |
| | """ |
| | if DDGS is None: |
| | return "搜索服务不可用:请安装 ddgs 库 (pip install ddgs)" |
| |
|
| | try: |
| | ddgs = DDGS() |
| | results = list(ddgs.text(query, max_results=max_results)) |
| |
|
| | if not results: |
| | return f"没有找到与 '{query}' 相关的搜索结果。" |
| |
|
| | output = [] |
| | for i, r in enumerate(results, 1): |
| | title = r.get('title', 'N/A') |
| | body = r.get('body', 'N/A') |
| | url = r.get('href', 'N/A') |
| | output.append(f"{i}. {title}") |
| | output.append(f" {body}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| |
|
| | return "\n".join(output) |
| |
|
| | except Exception as e: |
| | return f"搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | @tool |
| | def wikipedia_search(query: str, max_results: int = WIKIPEDIA_MAX_RESULTS) -> str: |
| | """ |
| | 在维基百科中搜索信息。 |
| | |
| | 适用场景: |
| | - 查找人物传记、历史事件 |
| | - 获取概念定义和详细解释 |
| | - 查找地理、科学、文化等百科知识 |
| | |
| | Args: |
| | query: 搜索关键词,建议使用英文 |
| | max_results: 返回结果数量,默认2条 |
| | |
| | Returns: |
| | 维基百科文章摘要 |
| | """ |
| | if wikipedia is None: |
| | return "Wikipedia 搜索不可用:请安装 wikipedia 库 (pip install wikipedia)" |
| |
|
| | try: |
| | |
| | search_results = wikipedia.search(query, results=max_results) |
| |
|
| | if not search_results: |
| | return f"没有找到与 '{query}' 相关的维基百科文章。" |
| |
|
| | output = [] |
| | for i, title in enumerate(search_results, 1): |
| | try: |
| | |
| | page = wikipedia.page(title, auto_suggest=False) |
| | summary = wikipedia.summary(title, sentences=3, auto_suggest=False) |
| | output.append(f"{i}. {page.title}") |
| | output.append(f" {summary}") |
| | output.append(f" URL: {page.url}") |
| | output.append("") |
| | except wikipedia.exceptions.DisambiguationError as e: |
| | |
| | if e.options: |
| | try: |
| | page = wikipedia.page(e.options[0], auto_suggest=False) |
| | summary = wikipedia.summary(e.options[0], sentences=3, auto_suggest=False) |
| | output.append(f"{i}. {page.title}") |
| | output.append(f" {summary}") |
| | output.append(f" URL: {page.url}") |
| | output.append("") |
| | except: |
| | output.append(f"{i}. {title} (歧义页面,可选: {', '.join(e.options[:3])})") |
| | output.append("") |
| | except wikipedia.exceptions.PageError: |
| | continue |
| |
|
| | return "\n".join(output) if output else f"没有找到与 '{query}' 相关的详细信息。" |
| |
|
| | except Exception as e: |
| | return f"Wikipedia 搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | @tool |
| | def wikipedia_page(title: str, section: str = None) -> str: |
| | """ |
| | 获取维基百科页面的完整内容。 |
| | |
| | 当 wikipedia_search 返回的摘要不够详细时使用此工具。 |
| | 特别适用于需要获取列表、表格、详细数据的场景(如专辑列表、获奖记录等)。 |
| | |
| | Args: |
| | title: 页面标题(从 wikipedia_search 结果中获取) |
| | section: 可选,指定要获取的章节名(如 "Discography", "Awards") |
| | |
| | Returns: |
| | 页面完整内容或指定章节内容 |
| | """ |
| | if wikipedia is None: |
| | return "Wikipedia 不可用:请安装 wikipedia 库 (pip install wikipedia)" |
| |
|
| | try: |
| | page = wikipedia.page(title, auto_suggest=False) |
| | content = page.content |
| |
|
| | |
| | if section: |
| | |
| | section_name = section.strip() |
| | heading_re = re.compile(r'^(=+)\s*(.+?)\s*\1\s*$', re.MULTILINE) |
| | headings = list(heading_re.finditer(content)) |
| |
|
| | |
| | target_idx = None |
| | for i, m in enumerate(headings): |
| | if m.group(2).strip().lower() == section_name.lower(): |
| | target_idx = i |
| | break |
| |
|
| | |
| | matched_label = "" |
| | if target_idx is None: |
| | for i, m in enumerate(headings): |
| | if section_name.lower() in m.group(2).strip().lower(): |
| | target_idx = i |
| | matched_label = " (matched)" |
| | break |
| |
|
| | if target_idx is not None: |
| | level = len(headings[target_idx].group(1)) |
| | start = headings[target_idx].end() |
| | end = len(content) |
| | for m in headings[target_idx + 1:]: |
| | if len(m.group(1)) <= level: |
| | end = m.start() |
| | break |
| | section_text = content[start:end].strip() |
| | content = f"{headings[target_idx].group(0)}{matched_label}\n{section_text}" |
| | else: |
| | available = [m.group(2).strip() for m in headings][:20] |
| | content = ( |
| | f"未找到 '{section_name}' 章节。\n\n可用章节:\n" |
| | + "\n".join(available) |
| | + f"\n\n完整内容:\n{content[:3000]}" |
| | ) |
| |
|
| | |
| | output = f"Wikipedia 页面: {page.title}\nURL: {page.url}\n\n{content}" |
| | if len(output) > MAX_FILE_SIZE: |
| | return output[:MAX_FILE_SIZE] + f"\n\n... [内容已截断,共 {len(output)} 字符]" |
| |
|
| | return output |
| |
|
| | except wikipedia.exceptions.DisambiguationError as e: |
| | options = e.options[:10] |
| | return f"'{title}' 是一个歧义页面,请指定更具体的标题:\n" + "\n".join(f" - {opt}" for opt in options) |
| | except wikipedia.exceptions.PageError: |
| | return f"找不到标题为 '{title}' 的维基百科页面。请检查标题拼写或使用 wikipedia_search 搜索。" |
| | except Exception as e: |
| | return f"Wikipedia 页面获取出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | @tool |
| | def tavily_search(query: str, max_results: int = TAVILY_MAX_RESULTS) -> str: |
| | """ |
| | 使用 Tavily 进行高质量网络搜索(需要 API Key)。 |
| | |
| | 适用场景: |
| | - 需要高质量、准确的搜索结果 |
| | - 查找最新新闻和实时信息 |
| | - 需要更智能的搜索结果排序 |
| | |
| | Args: |
| | query: 搜索关键词 |
| | max_results: 返回结果数量,默认3条 |
| | |
| | Returns: |
| | 搜索结果摘要 |
| | """ |
| | if TavilyClient is None: |
| | return "Tavily 搜索不可用:请安装 tavily-python 库 (pip install tavily-python)" |
| |
|
| | if not TAVILY_API_KEY: |
| | return "Tavily 搜索不可用:请在 .env 文件中设置 TAVILY_API_KEY" |
| |
|
| | try: |
| | client = TavilyClient(api_key=TAVILY_API_KEY) |
| | response = client.search(query, max_results=max_results) |
| |
|
| | results = response.get('results', []) |
| | if not results: |
| | return f"没有找到与 '{query}' 相关的搜索结果。" |
| |
|
| | output = [] |
| | for i, r in enumerate(results, 1): |
| | title = r.get('title', 'N/A') |
| | content = r.get('content', 'N/A') |
| | url = r.get('url', 'N/A') |
| | output.append(f"{i}. {title}") |
| | output.append(f" {content[:300]}..." if len(content) > 300 else f" {content}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| |
|
| | return "\n".join(output) |
| |
|
| | except Exception as e: |
| | return f"Tavily 搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | @tool |
| | def arxiv_search(query: str, max_results: int = ARXIV_MAX_RESULTS) -> str: |
| | """ |
| | 在 arXiv 上搜索学术论文。 |
| | |
| | 适用场景: |
| | - 查找最新学术研究论文 |
| | - 搜索特定领域的科学文献 |
| | - 获取论文摘要和作者信息 |
| | |
| | Args: |
| | query: 搜索关键词(建议使用英文学术术语) |
| | max_results: 返回结果数量,默认3条 |
| | |
| | Returns: |
| | 论文信息(标题、作者、摘要、链接) |
| | """ |
| | if arxiv is None: |
| | return "arXiv 搜索不可用:请安装 arxiv 库 (pip install arxiv)" |
| |
|
| | try: |
| | client = arxiv.Client() |
| | search = arxiv.Search( |
| | query=query, |
| | max_results=max_results, |
| | sort_by=arxiv.SortCriterion.Relevance |
| | ) |
| |
|
| | results = list(client.results(search)) |
| |
|
| | if not results: |
| | return f"没有找到与 '{query}' 相关的 arXiv 论文。" |
| |
|
| | output = [] |
| | for i, paper in enumerate(results, 1): |
| | title = paper.title |
| | authors = ", ".join([a.name for a in paper.authors[:3]]) |
| | if len(paper.authors) > 3: |
| | authors += f" 等 ({len(paper.authors)} 位作者)" |
| | summary = paper.summary[:400] + "..." if len(paper.summary) > 400 else paper.summary |
| | published = paper.published.strftime("%Y-%m-%d") |
| | url = paper.entry_id |
| |
|
| | output.append(f"{i}. {title}") |
| | output.append(f" 作者: {authors}") |
| | output.append(f" 发布日期: {published}") |
| | output.append(f" 摘要: {summary}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| |
|
| | return "\n".join(output) |
| |
|
| | except Exception as e: |
| | return f"arXiv 搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @tool |
| | def youtube_search(query: str, max_results: int = 3) -> str: |
| | """ |
| | 搜索 YouTube 视频信息。 |
| | |
| | 适用场景: |
| | - 查找教程视频 |
| | - 搜索特定主题的视频内容 |
| | - 获取视频标题、频道和描述 |
| | |
| | Args: |
| | query: 搜索关键词 |
| | max_results: 返回结果数量,默认3条 |
| | |
| | Returns: |
| | 视频信息(标题、频道、链接) |
| | """ |
| | try: |
| | from youtube_search import YoutubeSearch |
| | except ImportError: |
| | |
| | if DDGS is None: |
| | return "YouTube 搜索不可用:请安装 youtube-search-python 库 (pip install youtube-search-python)" |
| | |
| | try: |
| | ddgs = DDGS() |
| | results = list(ddgs.text(f"site:youtube.com {query}", max_results=max_results)) |
| | |
| | if not results: |
| | return f"没有找到与 '{query}' 相关的 YouTube 视频。" |
| | |
| | output = [] |
| | for i, r in enumerate(results, 1): |
| | title = r.get('title', 'N/A') |
| | url = r.get('href', 'N/A') |
| | output.append(f"{i}. {title}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| | |
| | return "\n".join(output) |
| | except Exception as e: |
| | return f"YouTube 搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| | try: |
| | results = YoutubeSearch(query, max_results=max_results).to_dict() |
| | |
| | if not results: |
| | return f"没有找到与 '{query}' 相关的 YouTube 视频。" |
| | |
| | output = [] |
| | for i, video in enumerate(results, 1): |
| | title = video.get('title', 'N/A') |
| | channel = video.get('channel', 'N/A') |
| | duration = video.get('duration', 'N/A') |
| | views = video.get('views', 'N/A') |
| | url_suffix = video.get('url_suffix', '') |
| | url = f"https://youtube.com{url_suffix}" if url_suffix else 'N/A' |
| | |
| | output.append(f"{i}. {title}") |
| | output.append(f" 频道: {channel}") |
| | output.append(f" 时长: {duration} | 播放量: {views}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| | |
| | return "\n".join(output) |
| | |
| | except Exception as e: |
| | return f"YouTube 搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @tool |
| | def news_search(query: str, max_results: int = 5) -> str: |
| | """ |
| | 搜索最新新闻资讯。 |
| | |
| | 适用场景: |
| | - 查找最新新闻事件 |
| | - 获取时事热点信息 |
| | - 搜索特定主题的新闻报道 |
| | |
| | Args: |
| | query: 搜索关键词 |
| | max_results: 返回结果数量,默认5条 |
| | |
| | Returns: |
| | 新闻标题、来源和摘要 |
| | """ |
| | if DDGS is None: |
| | return "新闻搜索不可用:请安装 ddgs 库 (pip install ddgs)" |
| |
|
| | try: |
| | ddgs = DDGS() |
| | results = list(ddgs.news(query, max_results=max_results)) |
| | |
| | if not results: |
| | return f"没有找到与 '{query}' 相关的新闻。" |
| | |
| | output = [] |
| | for i, r in enumerate(results, 1): |
| | title = r.get('title', 'N/A') |
| | body = r.get('body', 'N/A') |
| | source = r.get('source', 'N/A') |
| | date = r.get('date', 'N/A') |
| | url = r.get('url', 'N/A') |
| | |
| | output.append(f"{i}. {title}") |
| | output.append(f" 来源: {source} | 日期: {date}") |
| | output.append(f" {body[:200]}..." if len(body) > 200 else f" {body}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| | |
| | return "\n".join(output) |
| | |
| | except Exception as e: |
| | return f"新闻搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @tool |
| | def stackoverflow_search(query: str, max_results: int = 3) -> str: |
| | """ |
| | 在 StackOverflow 上搜索编程问题和解答。 |
| | |
| | 适用场景: |
| | - 查找编程问题的解决方案 |
| | - 搜索代码错误的修复方法 |
| | - 获取技术问题的讨论 |
| | |
| | Args: |
| | query: 搜索关键词(建议包含编程语言或技术栈) |
| | max_results: 返回结果数量,默认3条 |
| | |
| | Returns: |
| | 问题标题、回答数和链接 |
| | """ |
| | try: |
| | import requests |
| | |
| | |
| | api_url = "https://api.stackexchange.com/2.3/search/advanced" |
| | params = { |
| | "order": "desc", |
| | "sort": "relevance", |
| | "q": query, |
| | "site": "stackoverflow", |
| | "pagesize": max_results, |
| | "filter": "withbody" |
| | } |
| | |
| | response = requests.get(api_url, params=params, timeout=TOOL_TIMEOUT) |
| | response.raise_for_status() |
| | data = response.json() |
| | |
| | items = data.get('items', []) |
| | |
| | if not items: |
| | return f"没有找到与 '{query}' 相关的 StackOverflow 问题。" |
| | |
| | output = [] |
| | for i, item in enumerate(items, 1): |
| | title = item.get('title', 'N/A') |
| | score = item.get('score', 0) |
| | answer_count = item.get('answer_count', 0) |
| | is_answered = "✓ 已解答" if item.get('is_answered') else "○ 待解答" |
| | tags = ", ".join(item.get('tags', [])[:5]) |
| | url = item.get('link', 'N/A') |
| | |
| | output.append(f"{i}. {title}") |
| | output.append(f" {is_answered} | 得分: {score} | 回答数: {answer_count}") |
| | output.append(f" 标签: {tags}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| | |
| | return "\n".join(output) |
| | |
| | except Exception as e: |
| | return f"StackOverflow 搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @tool |
| | def google_search(query: str, max_results: int = 5) -> str: |
| | """ |
| | 使用 Google 搜索网络信息(通过 DuckDuckGo 代理)。 |
| | |
| | 适用场景: |
| | - 综合网络搜索 |
| | - 查找官方网站和权威来源 |
| | - 获取多样化的搜索结果 |
| | |
| | Args: |
| | query: 搜索关键词 |
| | max_results: 返回结果数量,默认5条 |
| | |
| | Returns: |
| | 搜索结果(标题+摘要+URL) |
| | |
| | 注意: |
| | 由于 Google API 限制,此工具通过 DuckDuckGo 实现类似功能 |
| | """ |
| | |
| | if DDGS is None: |
| | return "Google 搜索不可用:请安装 ddgs 库 (pip install ddgs)" |
| |
|
| | try: |
| | ddgs = DDGS() |
| | results = list(ddgs.text(query, max_results=max_results)) |
| | |
| | if not results: |
| | return f"没有找到与 '{query}' 相关的搜索结果。" |
| | |
| | output = [] |
| | for i, r in enumerate(results, 1): |
| | title = r.get('title', 'N/A') |
| | body = r.get('body', 'N/A') |
| | url = r.get('href', 'N/A') |
| | output.append(f"{i}. {title}") |
| | output.append(f" {body}") |
| | output.append(f" URL: {url}") |
| | output.append("") |
| | |
| | return "\n".join(output) |
| | |
| | except Exception as e: |
| | return f"Google 搜索出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @tool |
| | def fetch_task_files(task_id: str) -> str: |
| | """ |
| | 从评分服务器下载任务相关的附件文件。 |
| | |
| | 当问题涉及附件时必须先调用此工具下载文件,然后使用 read_file 或其他工具读取。 |
| | |
| | Args: |
| | task_id: 任务 ID(从问题中获取) |
| | |
| | Returns: |
| | 下载文件的本地路径,或错误信息 |
| | """ |
| | try: |
| | url = f"{SCORING_API_URL}/files/{task_id}" |
| | response = requests.get(url, timeout=TOOL_TIMEOUT) |
| |
|
| | if response.status_code == 404: |
| | return "该任务没有附件文件。" |
| |
|
| | response.raise_for_status() |
| |
|
| | |
| | content_disp = response.headers.get("Content-Disposition", "") |
| | filename_match = re.search(r'filename="?([^";\n]+)"?', content_disp) |
| | filename = filename_match.group(1) if filename_match else f"task_{task_id}_file" |
| |
|
| | |
| | file_path = TEMP_DIR / filename |
| |
|
| | with open(file_path, "wb") as f: |
| | f.write(response.content) |
| |
|
| | |
| | file_size = len(response.content) |
| | file_ext = os.path.splitext(filename)[1].lower() |
| |
|
| | |
| | next_step_hint = "" |
| | if file_ext in ['.xlsx', '.xls']: |
| | next_step_hint = "\n\n⚠️ 下一步:请立即使用 parse_excel(file_path) 工具读取此 Excel 文件内容,不要搜索网络。" |
| | elif file_ext == '.pdf': |
| | next_step_hint = "\n\n⚠️ 下一步:请立即使用 parse_pdf(file_path) 工具读取此 PDF 文件,不要搜索网络。" |
| | elif file_ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']: |
| | next_step_hint = "\n\n⚠️ 下一步:请使用 image_ocr(file_path) 或 analyze_image(file_path, question) 工具处理此图片。" |
| | elif file_ext in ['.mp3', '.wav', '.m4a', '.ogg']: |
| | next_step_hint = "\n\n⚠️ 下一步:请使用 transcribe_audio(file_path) 工具转写此音频文件。" |
| | elif file_ext in ['.txt', '.csv', '.json', '.md', '.py', '.html', '.xml']: |
| | next_step_hint = "\n\n⚠️ 下一步:请立即使用 read_file(file_path) 工具读取此文件内容。" |
| | elif file_ext == '.zip': |
| | next_step_hint = "\n\n⚠️ 下一步:请使用 read_file(file_path) 工具解压此 ZIP 文件。" |
| |
|
| | return f"文件已下载到: {file_path}\n文件大小: {file_size} 字节\n文件名: {filename}{next_step_hint}" |
| |
|
| | except requests.Timeout: |
| | return f"下载超时({TOOL_TIMEOUT}秒),请稍后重试。" |
| | except Exception as e: |
| | return f"下载文件出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | @tool |
| | def read_file(file_path: str, encoding: str = "utf-8") -> str: |
| | """ |
| | 读取本地文件内容。 |
| | |
| | 支持格式:txt, csv, json, py, html, xml, zip, md |
| | |
| | Args: |
| | file_path: 文件完整路径 |
| | encoding: 编码格式,默认 utf-8 |
| | |
| | Returns: |
| | 文件内容(超过指定字符数会截断) |
| | |
| | 注意: |
| | - ZIP 文件会自动解压并列出内容 |
| | - JSON 文件会自动美化输出 |
| | - PDF/Excel 需使用专门的扩展工具 |
| | """ |
| | try: |
| | if not os.path.exists(file_path): |
| | return f"文件不存在: {file_path}" |
| |
|
| | file_ext = os.path.splitext(file_path)[1].lower() |
| |
|
| | |
| | if file_ext == '.zip': |
| | extract_dir = file_path.replace('.zip', '_extracted') |
| | with zipfile.ZipFile(file_path, 'r') as zip_ref: |
| | zip_ref.extractall(extract_dir) |
| |
|
| | files = os.listdir(extract_dir) |
| | file_list = "\n".join(f" - {f}" for f in files) |
| | return f"ZIP 已解压到: {extract_dir}\n包含文件:\n{file_list}" |
| |
|
| | |
| | with open(file_path, 'r', encoding=encoding, errors='ignore') as f: |
| | content = f.read() |
| |
|
| | |
| | if file_ext == '.json': |
| | try: |
| | data = json.loads(content) |
| | content = json.dumps(data, indent=2, ensure_ascii=False) |
| | except json.JSONDecodeError: |
| | pass |
| |
|
| | |
| | if len(content) > MAX_FILE_SIZE: |
| | return content[:MAX_FILE_SIZE] + f"\n\n... [内容已截断,共 {len(content)} 字符]" |
| |
|
| | return content |
| |
|
| | except Exception as e: |
| | return f"读取文件出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @tool |
| | def calc(expression: str) -> str: |
| | """ |
| | 执行安全的数学计算。 |
| | |
| | 支持: |
| | - 基础运算:+, -, *, /, **, % |
| | - 数学函数:sqrt, sin, cos, tan, log, log10, exp, floor, ceil |
| | - 常量:pi, e |
| | |
| | Args: |
| | expression: 数学表达式,如 "2+3*4" 或 "sqrt(16)" |
| | |
| | Returns: |
| | 计算结果 |
| | """ |
| | import math |
| |
|
| | |
| | safe_dict = { |
| | |
| | 'abs': abs, 'round': round, 'min': min, 'max': max, |
| | 'sum': sum, 'pow': pow, 'len': len, |
| | |
| | 'sqrt': math.sqrt, 'sin': math.sin, 'cos': math.cos, |
| | 'tan': math.tan, 'log': math.log, 'log10': math.log10, |
| | 'exp': math.exp, 'floor': math.floor, 'ceil': math.ceil, |
| | 'asin': math.asin, 'acos': math.acos, 'atan': math.atan, |
| | 'sinh': math.sinh, 'cosh': math.cosh, 'tanh': math.tanh, |
| | 'degrees': math.degrees, 'radians': math.radians, |
| | 'factorial': math.factorial, 'gcd': math.gcd, |
| | |
| | 'pi': math.pi, 'e': math.e, |
| | } |
| |
|
| | try: |
| | |
| | expression = expression.strip() |
| |
|
| | |
| | result = eval(expression, {"__builtins__": {}}, safe_dict) |
| |
|
| | |
| | if isinstance(result, float): |
| | |
| | if result.is_integer(): |
| | return str(int(result)) |
| | return str(round(result, 10)) |
| |
|
| | return str(result) |
| |
|
| | except ZeroDivisionError: |
| | return "计算出错: 除数不能为零" |
| | except ValueError as e: |
| | return f"计算出错: 无效的数学操作 - {str(e)}" |
| | except Exception as e: |
| | return f"计算出错: {type(e).__name__}: {str(e)}" |
| |
|
| |
|
| | @tool |
| | def run_python(code: str) -> str: |
| | """ |
| | 在沙箱环境中执行 Python 代码。 |
| | |
| | 支持 import 以下模块: |
| | - math: 数学模块 |
| | - re: 正则表达式模块 |
| | - json: JSON 处理模块 |
| | - datetime: 日期时间模块 |
| | - collections: 集合工具模块 |
| | - random: 随机数模块 |
| | - string: 字符串常量模块 |
| | - itertools: 迭代器工具模块 |
| | - functools: 函数工具模块 |
| | |
| | 可用内置函数: |
| | - 类型: list, dict, set, tuple, str, int, float, bool, bytes |
| | - 函数: print, len, range, enumerate, zip, map, filter, sorted, reversed |
| | - 数值: sum, min, max, abs, round, pow, divmod, all, any |
| | - 转换: ord, chr, hex, bin, oct, isinstance, type, format, repr |
| | |
| | 适用场景: |
| | - 复杂数学计算 |
| | - 数据排序和过滤 |
| | - 字符串处理 |
| | - 日期计算 |
| | |
| | Args: |
| | code: Python 代码,需使用 print() 输出结果 |
| | |
| | Returns: |
| | 代码的标准输出 |
| | |
| | 示例: |
| | from datetime import datetime, timedelta |
| | today = datetime(2024, 1, 15) |
| | print(today + timedelta(days=30)) |
| | """ |
| | import io |
| | import sys |
| | import math |
| | import re as re_module |
| | import json as json_module |
| | import datetime as datetime_module |
| | import collections as collections_module |
| | import random as random_module |
| | import string as string_module |
| | import itertools as itertools_module |
| | import functools as functools_module |
| |
|
| | |
| | ALLOWED_MODULES = { |
| | 'math': math, |
| | 're': re_module, |
| | 'json': json_module, |
| | 'datetime': datetime_module, |
| | 'collections': collections_module, |
| | 'random': random_module, |
| | 'string': string_module, |
| | 'itertools': itertools_module, |
| | 'functools': functools_module, |
| | } |
| |
|
| | def restricted_import(name, globals=None, locals=None, fromlist=(), level=0): |
| | """受限的 import 函数,只允许导入白名单中的模块""" |
| | if name not in ALLOWED_MODULES: |
| | raise ImportError(f"不允许导入模块 '{name}',可用模块: {', '.join(ALLOWED_MODULES.keys())}") |
| | return ALLOWED_MODULES[name] |
| |
|
| | |
| | safe_builtins = { |
| | |
| | 'list': list, 'dict': dict, 'set': set, 'tuple': tuple, |
| | 'str': str, 'int': int, 'float': float, 'bool': bool, |
| | 'bytes': bytes, 'bytearray': bytearray, |
| | |
| | 'print': print, 'len': len, 'range': range, 'enumerate': enumerate, |
| | 'zip': zip, 'map': map, 'filter': filter, 'sorted': sorted, |
| | 'reversed': reversed, 'iter': iter, 'next': next, |
| | 'sum': sum, 'min': min, 'max': max, 'abs': abs, 'round': round, |
| | 'pow': pow, 'divmod': divmod, |
| | 'all': all, 'any': any, |
| | 'isinstance': isinstance, 'type': type, |
| | 'ord': ord, 'chr': chr, |
| | 'hex': hex, 'bin': bin, 'oct': oct, |
| | 'format': format, 'repr': repr, |
| | 'hasattr': hasattr, 'getattr': getattr, 'setattr': setattr, |
| | 'slice': slice, 'object': object, |
| | |
| | '__import__': restricted_import, |
| | |
| | 'True': True, 'False': False, 'None': None, |
| | } |
| |
|
| | |
| | preloaded = { |
| | 'math': math, |
| | 're': re_module, |
| | 'json': json_module, |
| | 'datetime': datetime_module.datetime, |
| | 'date': datetime_module.date, |
| | 'timedelta': datetime_module.timedelta, |
| | 'Counter': collections_module.Counter, |
| | 'defaultdict': collections_module.defaultdict, |
| | 'OrderedDict': collections_module.OrderedDict, |
| | 'random': random_module, |
| | } |
| |
|
| | |
| | namespace = {"__builtins__": safe_builtins} |
| | namespace.update(preloaded) |
| |
|
| | |
| | old_stdout = sys.stdout |
| | sys.stdout = io.StringIO() |
| |
|
| | try: |
| | exec(code, namespace) |
| | output = sys.stdout.getvalue() |
| |
|
| | if not output: |
| | return "代码执行成功,无输出。请使用 print() 输出结果。" |
| |
|
| | |
| | if len(output) > MAX_FILE_SIZE: |
| | return output[:MAX_FILE_SIZE] + f"\n\n... [输出已截断,共 {len(output)} 字符]" |
| |
|
| | return output.strip() |
| |
|
| | except SyntaxError as e: |
| | return f"语法错误: 第 {e.lineno} 行 - {e.msg}" |
| | except NameError as e: |
| | return f"名称错误: {str(e)}(该函数或变量在沙箱中不可用)" |
| | except Exception as e: |
| | return f"执行出错: {type(e).__name__}: {str(e)}" |
| | finally: |
| | sys.stdout = old_stdout |
| |
|
| |
|
| | |
| | |
| | |
| | BASE_TOOLS = [ |
| | |
| | web_search, |
| | wikipedia_search, |
| | wikipedia_page, |
| | tavily_search, |
| | arxiv_search, |
| | youtube_search, |
| | news_search, |
| | stackoverflow_search, |
| | google_search, |
| | |
| | fetch_task_files, |
| | read_file, |
| | |
| | calc, |
| | run_python, |
| | ] |
| |
|
| | |
| | try: |
| | from extension_tools import EXTENSION_TOOLS |
| | ALL_TOOLS = BASE_TOOLS + EXTENSION_TOOLS |
| | except ImportError: |
| | ALL_TOOLS = BASE_TOOLS |
| |
|
| | |
| | try: |
| | from rag import RAG_TOOLS |
| | ALL_TOOLS = ALL_TOOLS + RAG_TOOLS |
| | except ImportError: |
| | pass |
| |
|