Upload 12 files
Browse files- .gitignore +82 -0
- README.md +69 -7
- agent.py +752 -0
- app.py +286 -0
- config.py +80 -0
- data_clean.csv +118 -0
- extension_tools.py +368 -0
- rag.py +518 -0
- rag_index/index.faiss +0 -0
- rag_index/index.pkl +3 -0
- requirements.txt +51 -0
- tools.py +945 -0
.gitignore
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GAIA Agent .gitignore
|
| 2 |
+
# HuggingFace Spaces 部署用
|
| 3 |
+
|
| 4 |
+
# ========================================
|
| 5 |
+
# 敏感文件 - 绝对不能上传
|
| 6 |
+
# ========================================
|
| 7 |
+
.env
|
| 8 |
+
.env.local
|
| 9 |
+
.env.*.local
|
| 10 |
+
|
| 11 |
+
# ========================================
|
| 12 |
+
# Python
|
| 13 |
+
# ========================================
|
| 14 |
+
__pycache__/
|
| 15 |
+
*.py[cod]
|
| 16 |
+
*$py.class
|
| 17 |
+
*.so
|
| 18 |
+
.Python
|
| 19 |
+
build/
|
| 20 |
+
develop-eggs/
|
| 21 |
+
dist/
|
| 22 |
+
downloads/
|
| 23 |
+
eggs/
|
| 24 |
+
.eggs/
|
| 25 |
+
lib/
|
| 26 |
+
lib64/
|
| 27 |
+
parts/
|
| 28 |
+
sdist/
|
| 29 |
+
var/
|
| 30 |
+
wheels/
|
| 31 |
+
*.egg-info/
|
| 32 |
+
.installed.cfg
|
| 33 |
+
*.egg
|
| 34 |
+
|
| 35 |
+
# ========================================
|
| 36 |
+
# 临时文件
|
| 37 |
+
# ========================================
|
| 38 |
+
temp/
|
| 39 |
+
*.tmp
|
| 40 |
+
*.temp
|
| 41 |
+
*.log
|
| 42 |
+
|
| 43 |
+
# ========================================
|
| 44 |
+
# IDE 和编辑器
|
| 45 |
+
# ========================================
|
| 46 |
+
.idea/
|
| 47 |
+
.vscode/
|
| 48 |
+
*.swp
|
| 49 |
+
*.swo
|
| 50 |
+
*~
|
| 51 |
+
.project
|
| 52 |
+
.pydevproject
|
| 53 |
+
.settings/
|
| 54 |
+
|
| 55 |
+
# ========================================
|
| 56 |
+
# Jupyter Notebook
|
| 57 |
+
# ========================================
|
| 58 |
+
.ipynb_checkpoints/
|
| 59 |
+
*.ipynb
|
| 60 |
+
|
| 61 |
+
# ========================================
|
| 62 |
+
# RAG 索引 - 保留预构建的索引以加快启动
|
| 63 |
+
# ========================================
|
| 64 |
+
# rag_index/ # 已注释,会上传索引
|
| 65 |
+
|
| 66 |
+
# ========================================
|
| 67 |
+
# Claude Code 配置
|
| 68 |
+
# ========================================
|
| 69 |
+
.claude/
|
| 70 |
+
|
| 71 |
+
# ========================================
|
| 72 |
+
# 测试文件
|
| 73 |
+
# ========================================
|
| 74 |
+
test_*.py
|
| 75 |
+
*_test.py
|
| 76 |
+
|
| 77 |
+
# ========================================
|
| 78 |
+
# 其他
|
| 79 |
+
# ========================================
|
| 80 |
+
*.parquet
|
| 81 |
+
sorted-*.md
|
| 82 |
+
convert_*.py
|
README.md
CHANGED
|
@@ -1,14 +1,76 @@
|
|
| 1 |
---
|
| 2 |
-
title: GAIA
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
-
short_description: A space for GAIA stage 1
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: GAIA Agent
|
| 3 |
+
emoji: "\U0001F916"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.0.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# GAIA Agent
|
| 14 |
+
|
| 15 |
+
基于 LangGraph 的 GAIA 基准测试 Agent,用于 HuggingFace Agents Course Unit 4 评测。
|
| 16 |
+
|
| 17 |
+
## 功能特点
|
| 18 |
+
|
| 19 |
+
- **ReAct 架构**: 基于 LangGraph StateGraph 实现的推理-行动循环
|
| 20 |
+
- **多种搜索工具**: DuckDuckGo、Wikipedia、arXiv、Tavily、YouTube、StackOverflow
|
| 21 |
+
- **文件处理**: 支持 PDF、Excel、图片 OCR、音频转写
|
| 22 |
+
- **代码执行**: 安全的 Python 沙箱环境
|
| 23 |
+
- **RAG 支持**: 可选的知识库检索增强
|
| 24 |
+
|
| 25 |
+
## 工具列表
|
| 26 |
+
|
| 27 |
+
| 工具 | 功能 |
|
| 28 |
+
|------|------|
|
| 29 |
+
| `web_search` | DuckDuckGo 网络搜索 |
|
| 30 |
+
| `wikipedia_search` | 维基百科搜索 |
|
| 31 |
+
| `arxiv_search` | arXiv 学术论文搜索 |
|
| 32 |
+
| `tavily_search` | Tavily 高质量搜索 |
|
| 33 |
+
| `fetch_task_files` | 下载任务附件 |
|
| 34 |
+
| `read_file` | 读取本地文件 |
|
| 35 |
+
| `parse_pdf` | 解析 PDF 文件 |
|
| 36 |
+
| `parse_excel` | 解析 Excel 文件 |
|
| 37 |
+
| `calc` | 安全数学计算 |
|
| 38 |
+
| `run_python` | Python 代码执行 |
|
| 39 |
+
|
| 40 |
+
## 环境变量
|
| 41 |
+
|
| 42 |
+
在 HuggingFace Spaces 的 Settings > Secrets 中配置:
|
| 43 |
+
|
| 44 |
+
| 变量 | 必需 | 说明 |
|
| 45 |
+
|------|------|------|
|
| 46 |
+
| `OPENAI_API_KEY` | 是 | OpenAI API Key |
|
| 47 |
+
| `OPENAI_BASE_URL` | 否 | 自定义 API 端点 |
|
| 48 |
+
| `MODEL` | 否 | 模型名称,默认 gpt-4o-mini |
|
| 49 |
+
| `TAVILY_API_KEY` | 否 | Tavily API Key(可选) |
|
| 50 |
+
|
| 51 |
+
## 本地运行
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
# 安装依赖
|
| 55 |
+
pip install -r requirements.txt
|
| 56 |
+
|
| 57 |
+
# 配置环境变量
|
| 58 |
+
cp .env.example .env
|
| 59 |
+
# 编辑 .env 填入 API Key
|
| 60 |
+
|
| 61 |
+
# 启动应用
|
| 62 |
+
python app.py
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
访问 http://localhost:7860
|
| 66 |
+
|
| 67 |
+
## 技术栈
|
| 68 |
+
|
| 69 |
+
- [LangGraph](https://github.com/langchain-ai/langgraph) - Agent 框架
|
| 70 |
+
- [LangChain](https://github.com/langchain-ai/langchain) - LLM 工具链
|
| 71 |
+
- [Gradio](https://gradio.app/) - Web UI 框架
|
| 72 |
+
- [FAISS](https://github.com/facebookresearch/faiss) - 向量检索
|
| 73 |
+
|
| 74 |
+
## 许可证
|
| 75 |
+
|
| 76 |
+
MIT License
|
agent.py
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent 核心模块 - GAIA LangGraph ReAct Agent
|
| 3 |
+
包含:AgentState, System Prompt, Graph 构建, 答案提取
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
from typing import Sequence, Literal, Annotated, Optional
|
| 8 |
+
|
| 9 |
+
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage
|
| 10 |
+
from langchain_openai import ChatOpenAI
|
| 11 |
+
from langgraph.graph import StateGraph, END
|
| 12 |
+
from langgraph.graph.message import add_messages
|
| 13 |
+
from langgraph.prebuilt import ToolNode
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
from typing import TypedDict
|
| 17 |
+
except ImportError:
|
| 18 |
+
from typing_extensions import TypedDict
|
| 19 |
+
|
| 20 |
+
from config import (
|
| 21 |
+
OPENAI_BASE_URL,
|
| 22 |
+
OPENAI_API_KEY,
|
| 23 |
+
MODEL,
|
| 24 |
+
TEMPERATURE,
|
| 25 |
+
MAX_ITERATIONS,
|
| 26 |
+
DEBUG,
|
| 27 |
+
LLM_TIMEOUT,
|
| 28 |
+
RATE_LIMIT_RETRY_MAX,
|
| 29 |
+
RATE_LIMIT_RETRY_BASE_DELAY,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# 导入工具
|
| 33 |
+
from tools import BASE_TOOLS
|
| 34 |
+
|
| 35 |
+
# 尝试导入扩展工具
|
| 36 |
+
try:
|
| 37 |
+
from extension_tools import EXTENSION_TOOLS
|
| 38 |
+
ALL_TOOLS = BASE_TOOLS + EXTENSION_TOOLS
|
| 39 |
+
except ImportError as e:
|
| 40 |
+
print(f"⚠️ 扩展工具加载失败: {e}")
|
| 41 |
+
print(" 提示: 请确保安装了 pandas 和 openpyxl (pip install pandas openpyxl)")
|
| 42 |
+
EXTENSION_TOOLS = []
|
| 43 |
+
ALL_TOOLS = BASE_TOOLS
|
| 44 |
+
|
| 45 |
+
# 尝试导入 RAG 工具
|
| 46 |
+
try:
|
| 47 |
+
from rag import RAG_TOOLS
|
| 48 |
+
ALL_TOOLS = ALL_TOOLS + RAG_TOOLS
|
| 49 |
+
except ImportError:
|
| 50 |
+
RAG_TOOLS = []
|
| 51 |
+
|
| 52 |
+
# RAG 短路辅助(可选导入,不影响工具加载)
|
| 53 |
+
try:
|
| 54 |
+
from rag import rag_lookup_answer
|
| 55 |
+
except ImportError:
|
| 56 |
+
rag_lookup_answer = None
|
| 57 |
+
|
| 58 |
+
# 打印已加载的工具列表(调试用)
|
| 59 |
+
_tool_names = [t.name for t in ALL_TOOLS]
|
| 60 |
+
if DEBUG:
|
| 61 |
+
print(f"✓ 已加载 {len(ALL_TOOLS)} 个工具: {_tool_names}")
|
| 62 |
+
if 'parse_excel' not in _tool_names:
|
| 63 |
+
print("⚠️ 警告: parse_excel 工具未加载,Excel 文件处理将不可用!")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ========================================
|
| 67 |
+
# System Prompt 设计
|
| 68 |
+
# ========================================
|
| 69 |
+
|
| 70 |
+
SYSTEM_PROMPT = """你是一个专业的问答助手,专门解答GAIA基准测试中的各类问题。你需要准确、简洁地回答问题。
|
| 71 |
+
|
| 72 |
+
## 你的能力
|
| 73 |
+
|
| 74 |
+
你可以使用以下工具来获取信息和处理任务:
|
| 75 |
+
|
| 76 |
+
### 知识库工具(RAG)
|
| 77 |
+
- `rag_query(question)`: 查询知识库中的相似问题,获取解题策略建议。返回推荐的工具和解题步骤。**遇到复杂问题时优先使用!**
|
| 78 |
+
- `rag_retrieve(question)`: 仅检索相似问题,不生成建议。返回原始的相似问题和解法。
|
| 79 |
+
- `rag_stats()`: 查看知识库状态(文档数量等)。
|
| 80 |
+
|
| 81 |
+
### 信息获取工具
|
| 82 |
+
- `web_search(query)`: 使用DuckDuckGo搜索网络信息。适用于查找人物、事件、地点、组织等外部知识。
|
| 83 |
+
- `wikipedia_search(query)`: 在维基百科中搜索,返回简短摘要(3句话)。适用于快速确认人物/事件的基本信息。
|
| 84 |
+
- `wikipedia_page(title, section)`: 获取维基百科页面的完整内容。**需要详细数据(如专辑列表、获奖记录、作品年表)时必须用此工具!**
|
| 85 |
+
- `tavily_search(query)`: 使用Tavily进行高质量网络搜索,返回最多3条结果。需要API Key。
|
| 86 |
+
- `arxiv_search(query)`: 在arXiv上搜索学术论文,返回最多3条结果。适用于查找科学研究和学术文献。
|
| 87 |
+
|
| 88 |
+
### 文件处理工具
|
| 89 |
+
- `fetch_task_files(task_id)`: 从评分服务器下载任务附件。当问题涉及附件时必须先调用此工具。
|
| 90 |
+
- `read_file(file_path)`: 读取本地文件内容,支持txt/csv/json/zip等格式。**注意:不支持Excel和PDF!**
|
| 91 |
+
- `parse_pdf(file_path)`: 解析PDF文件,提取文本内容。**PDF文件必须用此工具!**
|
| 92 |
+
- `parse_excel(file_path)`: 解析Excel文件(.xlsx/.xls),返回表格内容。**Excel文件必须用此工具!**
|
| 93 |
+
- `image_ocr(file_path)`: 对图片进行OCR文字识别。
|
| 94 |
+
- `transcribe_audio(file_path)`: 将音频文件转写为文字。
|
| 95 |
+
- `analyze_image(file_path, question)`: 使用AI分析图片内容。
|
| 96 |
+
|
| 97 |
+
### 计算和代码工具
|
| 98 |
+
- `calc(expression)`: 执行安全的数学计算,如 "2+3*4" 或 "sqrt(16)"。适用于简单算术。
|
| 99 |
+
- `run_python(code)`: 在沙箱中执行Python代码。支持 import math/re/json/datetime/collections/random/string/itertools/functools 模块。适用于复杂数据处理、排序、过滤、日期计算等操作。
|
| 100 |
+
|
| 101 |
+
## 工具使用策略
|
| 102 |
+
|
| 103 |
+
### 优先级顺序
|
| 104 |
+
0. **先查知识库**【最高优先级】:
|
| 105 |
+
- 首先调用 `rag_query(question)` 查询知识库
|
| 106 |
+
- 如果返回"知识库匹配成功",**直接使用该答案作为最终回答**,不需要再调用其他工具
|
| 107 |
+
- 如果返回"知识库参考",参考答案和步骤选择后续工具
|
| 108 |
+
- 如果无匹配,按后续优先级使用其他工具
|
| 109 |
+
1. **有附件的问题**【重要】:
|
| 110 |
+
- 第一步:用 `fetch_task_files(task_id)` 下载文件
|
| 111 |
+
- 第二步:根据文件扩展名选择正确的读取工具:
|
| 112 |
+
* `.xlsx` / `.xls` → 必须用 `parse_excel(file_path)`
|
| 113 |
+
* `.pdf` → 必须用 `parse_pdf(file_path)`
|
| 114 |
+
* `.txt` / `.csv` / `.json` / `.md` → 用 `read_file(file_path)`
|
| 115 |
+
* `.png` / `.jpg` / `.jpeg` → 用 `image_ocr(file_path)` 或 `analyze_image(file_path, question)`
|
| 116 |
+
* `.mp3` / `.wav` → 用 `transcribe_audio(file_path)`
|
| 117 |
+
- 第三步:分析文件内容,进行必要的计算或处理
|
| 118 |
+
- **禁止**:下载文件后不要用 web_search 搜索,文件内容已经本地可用!
|
| 119 |
+
2. **需要外部信息**:
|
| 120 |
+
- **百科知识查询流程**【重要】:
|
| 121 |
+
* 第一步:用 `wikipedia_search(query)` 确认页面标题
|
| 122 |
+
* 第二步:如果需要详细数据(专辑列表、作品年表、获奖记录等),必须用 `wikipedia_page(title, section)` 获取完整内容
|
| 123 |
+
* 示例:查 Mercedes Sosa 专辑数 → `wikipedia_search("Mercedes Sosa")` → `wikipedia_page("Mercedes Sosa", "Discography")`
|
| 124 |
+
- 通用搜索: 使用 `web_search` 搜索其他网络信息
|
| 125 |
+
- 学术论文: 使用 `arxiv_search` 查找研究文献
|
| 126 |
+
- 高质量结果: 使用 `tavily_search` (如果配置了API Key)
|
| 127 |
+
3. **需要计算**: 简单算术用 `calc`,复杂处理用 `run_python`
|
| 128 |
+
4. **数据处理**: 使用 `run_python` 进行排序、过滤、统计等操作
|
| 129 |
+
|
| 130 |
+
### 工具使用原则
|
| 131 |
+
- **只有问题明确提到"attached file"或"附件"时才调用 `fetch_task_files`**,否则不要调用
|
| 132 |
+
- 每次只调用一个必要的工具,分析结果后再决定下一步
|
| 133 |
+
- 如果工具返回错误,尝试调整参数或换用其他工具
|
| 134 |
+
- 搜索时使用精确的关键词,避免过于宽泛
|
| 135 |
+
- 读取大文件时注意内容可能被截断,关注关键信息
|
| 136 |
+
- **如果 `wikipedia_search` 返回的摘要不足以回答问题,立即使用 `wikipedia_page` 获取完整内容**
|
| 137 |
+
|
| 138 |
+
## 思考过程
|
| 139 |
+
|
| 140 |
+
在回答问题前,请按以下步骤思考:
|
| 141 |
+
1. **理解问题**: 问题在问什么?需要什么类型的信息?
|
| 142 |
+
2. **咨询知识库**: 如果问题复杂或不确定解法,用 `rag_query` 查看相似问题的解题策略
|
| 143 |
+
3. **判断工具**: 根据问题类型和 RAG 建议,选择合适的工具
|
| 144 |
+
4. **执行获取**: 调用工具获取信息
|
| 145 |
+
5. **分析整合**: 分析工具返回的信息,提取关键答案
|
| 146 |
+
6. **格式化输出**: 按要求格式输出最终答案
|
| 147 |
+
|
| 148 |
+
## 答案格式要求【非常重要】
|
| 149 |
+
|
| 150 |
+
最终答案必须遵循以下格式:
|
| 151 |
+
- **数字答案**: 直接输出数字,如 `42` 而不是 "答案是42"
|
| 152 |
+
- **人名/地名**: 直接输出名称,如 `Albert Einstein` 而不是 "答案是Albert Einstein"
|
| 153 |
+
- **日期答案**: 使用标准格式 `YYYY-MM-DD` 或按问题要求的格式
|
| 154 |
+
- **列表答案**: 用逗号分隔,如 `A, B, C`
|
| 155 |
+
- **是/否答案**: 输出 `Yes` 或 `No`
|
| 156 |
+
|
| 157 |
+
⚠️ 最终回答时,只输出答案本身,不要包含:
|
| 158 |
+
- 不要说"答案是..."、"The answer is..."
|
| 159 |
+
- 不要添加解释或推理过程
|
| 160 |
+
- 不要使用"最终答案:"等前缀
|
| 161 |
+
|
| 162 |
+
## 错误恢复
|
| 163 |
+
|
| 164 |
+
如果遇到问题:
|
| 165 |
+
- 工具调用失败: 检查参数,尝试简化或换用其他工具
|
| 166 |
+
- 搜索无结果: 尝试不同的关键词组合
|
| 167 |
+
- 文件读取失败: 确认文件路径正确,检查文件格式
|
| 168 |
+
- 计算错误: 检查表达式语法,考虑使用Python代码
|
| 169 |
+
|
| 170 |
+
## 示例
|
| 171 |
+
|
| 172 |
+
问题: "Who was the first person to walk on the moon?"
|
| 173 |
+
正确答案: Neil Armstrong
|
| 174 |
+
错误答案: The answer is Neil Armstrong.
|
| 175 |
+
|
| 176 |
+
问题: "What is 15% of 200?"
|
| 177 |
+
正确答案: 30
|
| 178 |
+
错误答案: 15% of 200 is 30.
|
| 179 |
+
|
| 180 |
+
### 文件处理示例【重要】
|
| 181 |
+
|
| 182 |
+
问题: "[Task ID: abc123] The attached Excel file contains sales data. What is the total revenue?"
|
| 183 |
+
|
| 184 |
+
✅ 正确流程:
|
| 185 |
+
1. fetch_task_files("abc123") → 下载文件到本地路径
|
| 186 |
+
2. parse_excel("/path/to/file.xlsx") → 读取Excel内容,得到表格数据
|
| 187 |
+
3. calc("100+200+300") 或 run_python("...") → 计算总收入
|
| 188 |
+
4. 输出最终答案
|
| 189 |
+
|
| 190 |
+
❌ 错误流程:
|
| 191 |
+
1. fetch_task_files("abc123") → 下载文件
|
| 192 |
+
2. web_search("sales data total revenue") → 错!文件内容在本地,不需要搜索网络!
|
| 193 |
+
|
| 194 |
+
### RAG 辅助示例
|
| 195 |
+
|
| 196 |
+
问题: "How many studio albums did Mercedes Sosa release between 2000 and 2009?"
|
| 197 |
+
|
| 198 |
+
✅ 推荐流程:
|
| 199 |
+
1. rag_query("How many studio albums did Mercedes Sosa release between 2000 and 2009?") → 获取建议:使用 wikipedia_page 查 Discography
|
| 200 |
+
2. wikipedia_search("Mercedes Sosa") → 确认页面存在
|
| 201 |
+
3. wikipedia_page("Mercedes Sosa", "Discography") → 获取完整专辑列表
|
| 202 |
+
4. run_python("...") → 筛选 2000-2009 年的专辑并计数
|
| 203 |
+
5. 输出最终答案
|
| 204 |
+
|
| 205 |
+
RAG 的价值:直接告诉你该用 wikipedia_page 而不是 web_search,节省试错时间。
|
| 206 |
+
|
| 207 |
+
现在请回答用户的问题。"""
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# ========================================
|
| 211 |
+
# Agent State 定义
|
| 212 |
+
# ========================================
|
| 213 |
+
|
| 214 |
+
class AgentState(TypedDict):
|
| 215 |
+
"""Agent 状态定义"""
|
| 216 |
+
# 核心字段
|
| 217 |
+
messages: Annotated[Sequence[BaseMessage], add_messages] # 消息历史
|
| 218 |
+
|
| 219 |
+
# 迭代控制
|
| 220 |
+
iteration_count: int # 当前迭代次数,防止无限循环
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# ========================================
|
| 224 |
+
# LLM 初始化
|
| 225 |
+
# ========================================
|
| 226 |
+
|
| 227 |
+
# 全局 LLM 实例(避免每次迭代重复创建)
|
| 228 |
+
_llm_instance = None
|
| 229 |
+
_llm_with_tools = None
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def get_llm():
|
| 233 |
+
"""获取 LLM 单例"""
|
| 234 |
+
global _llm_instance
|
| 235 |
+
if _llm_instance is None:
|
| 236 |
+
_llm_instance = ChatOpenAI(
|
| 237 |
+
model=MODEL,
|
| 238 |
+
temperature=TEMPERATURE,
|
| 239 |
+
base_url=OPENAI_BASE_URL,
|
| 240 |
+
api_key=OPENAI_API_KEY,
|
| 241 |
+
timeout=LLM_TIMEOUT,
|
| 242 |
+
max_retries=2,
|
| 243 |
+
)
|
| 244 |
+
return _llm_instance
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def get_llm_with_tools():
|
| 248 |
+
"""获取绑定工具的 LLM 单例"""
|
| 249 |
+
global _llm_with_tools
|
| 250 |
+
if _llm_with_tools is None:
|
| 251 |
+
_llm_with_tools = get_llm().bind_tools(ALL_TOOLS)
|
| 252 |
+
return _llm_with_tools
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def invoke_llm_with_retry(llm, messages, max_retries=None, base_delay=None):
|
| 256 |
+
"""
|
| 257 |
+
带重试逻辑的 LLM 调用(处理 429 速率限制错误)
|
| 258 |
+
|
| 259 |
+
Args:
|
| 260 |
+
llm: LLM 实例
|
| 261 |
+
messages: 消息列表
|
| 262 |
+
max_retries: 最大重试次数,默认使用配置值
|
| 263 |
+
base_delay: 基础延迟秒数,默认使用配置值
|
| 264 |
+
|
| 265 |
+
Returns:
|
| 266 |
+
LLM 响应
|
| 267 |
+
|
| 268 |
+
Raises:
|
| 269 |
+
原始异常(如果重试耗尽)
|
| 270 |
+
"""
|
| 271 |
+
import time
|
| 272 |
+
from openai import RateLimitError
|
| 273 |
+
|
| 274 |
+
if max_retries is None:
|
| 275 |
+
max_retries = RATE_LIMIT_RETRY_MAX
|
| 276 |
+
if base_delay is None:
|
| 277 |
+
base_delay = RATE_LIMIT_RETRY_BASE_DELAY
|
| 278 |
+
|
| 279 |
+
last_error = None
|
| 280 |
+
|
| 281 |
+
for attempt in range(max_retries + 1):
|
| 282 |
+
try:
|
| 283 |
+
return llm.invoke(messages)
|
| 284 |
+
except RateLimitError as e:
|
| 285 |
+
last_error = e
|
| 286 |
+
if attempt < max_retries:
|
| 287 |
+
# 指数退避:base_delay * 2^attempt
|
| 288 |
+
delay = base_delay * (2 ** attempt)
|
| 289 |
+
print(f"[Rate Limit] 429 错误,第 {attempt + 1}/{max_retries + 1} 次尝试,等待 {delay:.1f} 秒后重试...")
|
| 290 |
+
time.sleep(delay)
|
| 291 |
+
else:
|
| 292 |
+
print(f"[Rate Limit] 重试次数已耗尽 ({max_retries + 1} 次),抛出异常")
|
| 293 |
+
raise
|
| 294 |
+
except Exception as e:
|
| 295 |
+
# 其他错误直接抛出
|
| 296 |
+
raise
|
| 297 |
+
|
| 298 |
+
# 不应该到这里,但以防万一
|
| 299 |
+
if last_error:
|
| 300 |
+
raise last_error
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def create_llm():
|
| 304 |
+
"""创建 LLM 实例(保留兼容性)"""
|
| 305 |
+
return get_llm()
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
# ========================================
|
| 309 |
+
# Graph 节点定义
|
| 310 |
+
# ========================================
|
| 311 |
+
|
| 312 |
+
def assistant(state: AgentState) -> dict:
|
| 313 |
+
"""
|
| 314 |
+
LLM 推理节点
|
| 315 |
+
|
| 316 |
+
职责:
|
| 317 |
+
1. 接收当前状态
|
| 318 |
+
2. 构建完整消息(包含 System Prompt)
|
| 319 |
+
3. 调用 LLM 生成响应
|
| 320 |
+
4. 更新迭代计数
|
| 321 |
+
"""
|
| 322 |
+
messages = state["messages"]
|
| 323 |
+
iteration = state.get("iteration_count", 0) + 1
|
| 324 |
+
|
| 325 |
+
# 构建完整消息列表
|
| 326 |
+
full_messages = [SystemMessage(content=SYSTEM_PROMPT)] + list(messages)
|
| 327 |
+
|
| 328 |
+
# 接近迭代上限时添加强制结束警告
|
| 329 |
+
if iteration >= MAX_ITERATIONS - 1:
|
| 330 |
+
print(f"[Iteration {iteration}] FORCING FINAL ANSWER (no tools)")
|
| 331 |
+
warning = f"""
|
| 332 |
+
|
| 333 |
+
⚠️ 【最后机会】已进行 {iteration} 次迭代,达到上限 {MAX_ITERATIONS}。
|
| 334 |
+
你必须立即给出最终答案!不要再调用任何工具!
|
| 335 |
+
直接根据已有信息输出答案。如果信息不足,给出最佳估计。
|
| 336 |
+
"""
|
| 337 |
+
full_messages.append(SystemMessage(content=warning))
|
| 338 |
+
# 不绑定工具,强制 LLM 只输出文本
|
| 339 |
+
llm = get_llm()
|
| 340 |
+
try:
|
| 341 |
+
response = invoke_llm_with_retry(llm, full_messages)
|
| 342 |
+
except Exception as e:
|
| 343 |
+
print(f"[ERROR] LLM 调用失败: {type(e).__name__}: {str(e)}")
|
| 344 |
+
raise
|
| 345 |
+
elif iteration >= MAX_ITERATIONS - 2:
|
| 346 |
+
warning = f"\n\n⚠️ 警告:已进行 {iteration} 次迭代,接近上限 {MAX_ITERATIONS},请尽快给出最终答案,不要再搜索。"
|
| 347 |
+
full_messages.append(SystemMessage(content=warning))
|
| 348 |
+
# 使用单例 LLM(避免重复创建)
|
| 349 |
+
llm_with_tools = get_llm_with_tools()
|
| 350 |
+
try:
|
| 351 |
+
response = invoke_llm_with_retry(llm_with_tools, full_messages)
|
| 352 |
+
except Exception as e:
|
| 353 |
+
print(f"[ERROR] LLM 调用失败: {type(e).__name__}: {str(e)}")
|
| 354 |
+
raise
|
| 355 |
+
else:
|
| 356 |
+
# 使用单例 LLM(避免重复创建)
|
| 357 |
+
llm_with_tools = get_llm_with_tools()
|
| 358 |
+
try:
|
| 359 |
+
response = invoke_llm_with_retry(llm_with_tools, full_messages)
|
| 360 |
+
except Exception as e:
|
| 361 |
+
print(f"[ERROR] LLM 调用失败: {type(e).__name__}: {str(e)}")
|
| 362 |
+
raise
|
| 363 |
+
|
| 364 |
+
# 始终打印迭代信息(便于调试)
|
| 365 |
+
print(f"[Iteration {iteration}] LLM Response: {response.content[:200] if response.content else '(empty)'}...")
|
| 366 |
+
if hasattr(response, 'tool_calls') and response.tool_calls:
|
| 367 |
+
print(f"[Iteration {iteration}] Tool calls: {[tc['name'] for tc in response.tool_calls]}")
|
| 368 |
+
|
| 369 |
+
return {
|
| 370 |
+
"messages": [response],
|
| 371 |
+
"iteration_count": iteration
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def should_continue(state: AgentState) -> Literal["tools", "end"]:
|
| 376 |
+
"""
|
| 377 |
+
路由判断:决定继续使用工具还是结束
|
| 378 |
+
|
| 379 |
+
判断逻辑:
|
| 380 |
+
1. 达到迭代上限 → 强制结束
|
| 381 |
+
2. 有工具��用 → 继续执行工具
|
| 382 |
+
3. 无工具调用 → 返回答案,结束
|
| 383 |
+
"""
|
| 384 |
+
last_message = state["messages"][-1]
|
| 385 |
+
iteration = state.get("iteration_count", 0)
|
| 386 |
+
|
| 387 |
+
# 达到迭代上限,强制结束
|
| 388 |
+
if iteration >= MAX_ITERATIONS:
|
| 389 |
+
print(f"[Router] Reached max iterations ({MAX_ITERATIONS}), forcing end")
|
| 390 |
+
return "end"
|
| 391 |
+
|
| 392 |
+
# 检查是否有工具调用
|
| 393 |
+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
| 394 |
+
print(f"[Router] Has tool calls, continuing to tools")
|
| 395 |
+
return "tools"
|
| 396 |
+
|
| 397 |
+
# 无工具调用,返回答案
|
| 398 |
+
print(f"[Router] No tool calls, ending")
|
| 399 |
+
return "end"
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
# ========================================
|
| 403 |
+
# Graph 构建
|
| 404 |
+
# ========================================
|
| 405 |
+
|
| 406 |
+
def build_agent_graph():
|
| 407 |
+
"""
|
| 408 |
+
构建 Agent Graph
|
| 409 |
+
|
| 410 |
+
流程:
|
| 411 |
+
START → assistant → [should_continue] → tools → assistant → ... → END
|
| 412 |
+
"""
|
| 413 |
+
graph = StateGraph(AgentState)
|
| 414 |
+
|
| 415 |
+
# 添加节点
|
| 416 |
+
graph.add_node("assistant", assistant)
|
| 417 |
+
graph.add_node("tools", ToolNode(ALL_TOOLS))
|
| 418 |
+
|
| 419 |
+
# 设置入口点
|
| 420 |
+
graph.set_entry_point("assistant")
|
| 421 |
+
|
| 422 |
+
# 添加条件边
|
| 423 |
+
graph.add_conditional_edges(
|
| 424 |
+
"assistant",
|
| 425 |
+
should_continue,
|
| 426 |
+
{"tools": "tools", "end": END}
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
# 工具执行后返回 assistant
|
| 430 |
+
graph.add_edge("tools", "assistant")
|
| 431 |
+
|
| 432 |
+
return graph.compile()
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
# ========================================
|
| 436 |
+
# 答案提取
|
| 437 |
+
# ========================================
|
| 438 |
+
|
| 439 |
+
def extract_final_answer(result: dict) -> str:
|
| 440 |
+
"""
|
| 441 |
+
从 Agent 结果中提取最终答案
|
| 442 |
+
|
| 443 |
+
处理步骤:
|
| 444 |
+
1. 获取最后一条消息
|
| 445 |
+
2. 移除常见前缀
|
| 446 |
+
3. 移除尾部解释
|
| 447 |
+
4. 提取 JSON 格式答案
|
| 448 |
+
5. 清理格式
|
| 449 |
+
"""
|
| 450 |
+
messages = result.get("messages", [])
|
| 451 |
+
if not messages:
|
| 452 |
+
print("[extract_final_answer] No messages in result")
|
| 453 |
+
return "无法获取答案"
|
| 454 |
+
|
| 455 |
+
# 优先选择"无 tool_calls 的 AIMessage"
|
| 456 |
+
content = None
|
| 457 |
+
|
| 458 |
+
# 第一优先:无 tool_calls 的 AIMessage(真正的最终答案)
|
| 459 |
+
for msg in reversed(messages):
|
| 460 |
+
if isinstance(msg, AIMessage) and msg.content and str(msg.content).strip():
|
| 461 |
+
if not (hasattr(msg, "tool_calls") and msg.tool_calls):
|
| 462 |
+
content = msg.content
|
| 463 |
+
break
|
| 464 |
+
|
| 465 |
+
# 第二优先:有 tool_calls 的 AIMessage
|
| 466 |
+
if content is None:
|
| 467 |
+
for msg in reversed(messages):
|
| 468 |
+
if isinstance(msg, AIMessage) and msg.content and str(msg.content).strip():
|
| 469 |
+
content = msg.content
|
| 470 |
+
break
|
| 471 |
+
|
| 472 |
+
# 第三优先:任何有内容的消息(可能是 ToolMessage)
|
| 473 |
+
if content is None:
|
| 474 |
+
for msg in reversed(messages):
|
| 475 |
+
if hasattr(msg, "content") and msg.content and str(msg.content).strip():
|
| 476 |
+
content = msg.content
|
| 477 |
+
break
|
| 478 |
+
|
| 479 |
+
print(f"[extract_final_answer] Raw content: {content[:500] if content else '(empty)'}...")
|
| 480 |
+
|
| 481 |
+
if not content:
|
| 482 |
+
print("[extract_final_answer] Empty content in all messages")
|
| 483 |
+
return "无法获取答案"
|
| 484 |
+
|
| 485 |
+
answer = content.strip()
|
| 486 |
+
|
| 487 |
+
# Step 1: 移除常见前缀
|
| 488 |
+
prefix_patterns = [
|
| 489 |
+
# 英文前缀
|
| 490 |
+
r'^(?:the\s+)?(?:final\s+)?answer\s*(?:is|:)\s*',
|
| 491 |
+
r'^(?:the\s+)?result\s*(?:is|:)\s*',
|
| 492 |
+
r'^(?:therefore|thus|so|hence)[,:]?\s*',
|
| 493 |
+
r'^based\s+on\s+(?:the|my)\s+(?:analysis|research|calculations?)[,:]?\s*',
|
| 494 |
+
r'^after\s+(?:analyzing|reviewing|checking)[^,]*[,:]?\s*',
|
| 495 |
+
r'^according\s+to\s+[^,]*[,:]?\s*',
|
| 496 |
+
# 中文前缀
|
| 497 |
+
r'^(?:最终)?答案[是为::]\s*',
|
| 498 |
+
r'^(?:结果|结论)[是为::]\s*',
|
| 499 |
+
r'^(?:因此|所以|综上)[,,::]?\s*',
|
| 500 |
+
r'^根据(?:以上)?(?:分析|信息|计算)[,,::]?\s*',
|
| 501 |
+
r'^经过(?:分析|计算|查询)[,,::]?\s*',
|
| 502 |
+
]
|
| 503 |
+
|
| 504 |
+
for pattern in prefix_patterns:
|
| 505 |
+
answer = re.sub(pattern, '', answer, flags=re.IGNORECASE)
|
| 506 |
+
|
| 507 |
+
# Step 2: 移除尾部解释
|
| 508 |
+
suffix_patterns = [
|
| 509 |
+
r'\s*(?:This|That|The|It)\s+(?:is|was|represents|refers\s+to).*$',
|
| 510 |
+
r'\s*[(\(].*[)\)]$',
|
| 511 |
+
r'\s*[。\.]$',
|
| 512 |
+
r'\s*\n\n.*$', # 移除额外段落
|
| 513 |
+
]
|
| 514 |
+
|
| 515 |
+
for pattern in suffix_patterns:
|
| 516 |
+
answer = re.sub(pattern, '', answer, flags=re.IGNORECASE | re.DOTALL)
|
| 517 |
+
|
| 518 |
+
# Step 3: 提取 JSON 格式答案
|
| 519 |
+
json_patterns = [
|
| 520 |
+
r'\{["\']?(?:final_?)?answer["\']?\s*:\s*["\']?([^"\'}\n]+)["\']?\}',
|
| 521 |
+
r'"answer"\s*:\s*"([^"]+)"',
|
| 522 |
+
]
|
| 523 |
+
for pattern in json_patterns:
|
| 524 |
+
json_match = re.search(pattern, answer, re.IGNORECASE)
|
| 525 |
+
if json_match:
|
| 526 |
+
answer = json_match.group(1)
|
| 527 |
+
break
|
| 528 |
+
|
| 529 |
+
# Step 4: 清理
|
| 530 |
+
answer = answer.strip()
|
| 531 |
+
answer = re.sub(r'\s+', ' ', answer) # 合并空白
|
| 532 |
+
answer = answer.strip('"\'') # 移除引号
|
| 533 |
+
|
| 534 |
+
# Step 5: 数字格式处理
|
| 535 |
+
if re.match(r'^[\d,\.]+$', answer):
|
| 536 |
+
answer = answer.replace(',', '')
|
| 537 |
+
|
| 538 |
+
return answer
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def post_process_answer(answer: str, expected_type: str = None) -> str:
|
| 542 |
+
"""
|
| 543 |
+
根据预期类型后处理答案
|
| 544 |
+
|
| 545 |
+
Args:
|
| 546 |
+
answer: 原始答案
|
| 547 |
+
expected_type: 预期类型 (number, date, boolean, list)
|
| 548 |
+
|
| 549 |
+
Returns:
|
| 550 |
+
处理后的答案
|
| 551 |
+
"""
|
| 552 |
+
if expected_type == "number":
|
| 553 |
+
match = re.search(r'-?\d+\.?\d*', answer.replace(',', ''))
|
| 554 |
+
if match:
|
| 555 |
+
return match.group()
|
| 556 |
+
|
| 557 |
+
elif expected_type == "date":
|
| 558 |
+
# 尝试标准化日期格式
|
| 559 |
+
date_patterns = [
|
| 560 |
+
(r'(\d{4})-(\d{1,2})-(\d{1,2})', lambda m: f"{m.group(1)}-{int(m.group(2)):02d}-{int(m.group(3)):02d}"),
|
| 561 |
+
(r'(\d{1,2})/(\d{1,2})/(\d{4})', lambda m: f"{m.group(3)}-{int(m.group(1)):02d}-{int(m.group(2)):02d}"),
|
| 562 |
+
]
|
| 563 |
+
for pattern, formatter in date_patterns:
|
| 564 |
+
match = re.search(pattern, answer)
|
| 565 |
+
if match:
|
| 566 |
+
return formatter(match)
|
| 567 |
+
|
| 568 |
+
elif expected_type == "boolean":
|
| 569 |
+
lower = answer.lower().strip()
|
| 570 |
+
if lower in ['yes', 'true', '是', '对', 'correct']:
|
| 571 |
+
return "Yes"
|
| 572 |
+
elif lower in ['no', 'false', '否', '不', '错', 'incorrect']:
|
| 573 |
+
return "No"
|
| 574 |
+
|
| 575 |
+
elif expected_type == "list":
|
| 576 |
+
answer = re.sub(r'\s*[;;、]\s*', ', ', answer)
|
| 577 |
+
|
| 578 |
+
return answer
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
# ========================================
|
| 582 |
+
# GaiaAgent 入口类
|
| 583 |
+
# ========================================
|
| 584 |
+
|
| 585 |
+
class GaiaAgent:
|
| 586 |
+
"""
|
| 587 |
+
GAIA Agent 入口类
|
| 588 |
+
|
| 589 |
+
使用方法:
|
| 590 |
+
agent = GaiaAgent()
|
| 591 |
+
answer = agent("Who founded Microsoft?")
|
| 592 |
+
"""
|
| 593 |
+
|
| 594 |
+
def __init__(self):
|
| 595 |
+
"""初始化 Agent"""
|
| 596 |
+
self.graph = build_agent_graph()
|
| 597 |
+
|
| 598 |
+
def _needs_reformatting(self, answer: str) -> bool:
|
| 599 |
+
"""检查答案是否需要重新格式化"""
|
| 600 |
+
if not answer or answer == "无法获取答案":
|
| 601 |
+
return False
|
| 602 |
+
indicators = [
|
| 603 |
+
answer.startswith('http'),
|
| 604 |
+
'URL:' in answer,
|
| 605 |
+
len(answer) > 300,
|
| 606 |
+
answer.count('\n') > 3,
|
| 607 |
+
answer.startswith('1.') and '2.' in answer,
|
| 608 |
+
answer.startswith('- '),
|
| 609 |
+
'...' in answer and len(answer) > 100,
|
| 610 |
+
]
|
| 611 |
+
return any(indicators)
|
| 612 |
+
|
| 613 |
+
def _force_format_answer(self, result: dict) -> str:
|
| 614 |
+
"""强制格式化答案"""
|
| 615 |
+
messages = result.get("messages", [])
|
| 616 |
+
format_prompt = (
|
| 617 |
+
"根据上述对话收集的信息,输出最终答案。\n\n"
|
| 618 |
+
"【强制要求】只输出答案本身,不要解释、不要前缀。\n"
|
| 619 |
+
"- 数字:直接输出(如 42)\n"
|
| 620 |
+
"- 人名/地名:直接输出(如 Albert Einstein)\n"
|
| 621 |
+
"- 日期:YYYY-MM-DD\n"
|
| 622 |
+
"- 是/否:Yes 或 No\n\n"
|
| 623 |
+
"最终答案:"
|
| 624 |
+
)
|
| 625 |
+
full_messages = [SystemMessage(content=SYSTEM_PROMPT)] + list(messages)
|
| 626 |
+
full_messages.append(HumanMessage(content=format_prompt))
|
| 627 |
+
llm = get_llm()
|
| 628 |
+
try:
|
| 629 |
+
print("[Reformat] Forcing answer formatting...")
|
| 630 |
+
response = invoke_llm_with_retry(llm, full_messages)
|
| 631 |
+
formatted = extract_final_answer({"messages": [response]})
|
| 632 |
+
print(f"[Reformat] Result: {formatted[:100]}...")
|
| 633 |
+
return formatted
|
| 634 |
+
except Exception as e:
|
| 635 |
+
print(f"[Reformat] Error: {e}")
|
| 636 |
+
return "无法获取答案"
|
| 637 |
+
|
| 638 |
+
def __call__(self, question: str, task_id: str = None) -> str:
|
| 639 |
+
"""
|
| 640 |
+
执行问答
|
| 641 |
+
|
| 642 |
+
Args:
|
| 643 |
+
question: 用户问题
|
| 644 |
+
task_id: 任务 ID(可选,用于下载附件)
|
| 645 |
+
|
| 646 |
+
Returns:
|
| 647 |
+
最终答案
|
| 648 |
+
"""
|
| 649 |
+
# 如果有 task_id,注入到问题中
|
| 650 |
+
if task_id:
|
| 651 |
+
question_with_id = f"[Task ID: {task_id}]\n\n{question}"
|
| 652 |
+
else:
|
| 653 |
+
question_with_id = question
|
| 654 |
+
|
| 655 |
+
# ===== RAG 前置短路:高置信度匹配直接返回 =====
|
| 656 |
+
try:
|
| 657 |
+
if rag_lookup_answer is not None:
|
| 658 |
+
hit = rag_lookup_answer(question, min_similarity=0.85)
|
| 659 |
+
if hit and hit.get("answer"):
|
| 660 |
+
print(f"[GaiaAgent] RAG short-circuit hit: similarity={hit.get('similarity', 0):.2f}")
|
| 661 |
+
if DEBUG:
|
| 662 |
+
print(f"[Final Answer] {hit['answer']}")
|
| 663 |
+
return str(hit["answer"]).strip()
|
| 664 |
+
except Exception as e:
|
| 665 |
+
if DEBUG:
|
| 666 |
+
print(f"[GaiaAgent] RAG short-circuit failed: {type(e).__name__}: {e}")
|
| 667 |
+
# ===== RAG 短路检查结束 =====
|
| 668 |
+
|
| 669 |
+
# 初始状态
|
| 670 |
+
initial_state = {
|
| 671 |
+
"messages": [HumanMessage(content=question_with_id)],
|
| 672 |
+
"iteration_count": 0
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
try:
|
| 676 |
+
# 执行 Agent
|
| 677 |
+
result = self.graph.invoke(initial_state)
|
| 678 |
+
|
| 679 |
+
# 提取答案
|
| 680 |
+
answer = extract_final_answer(result)
|
| 681 |
+
|
| 682 |
+
# 检查答案是否需要格式化
|
| 683 |
+
if self._needs_reformatting(answer):
|
| 684 |
+
print(f"[GaiaAgent] Answer needs reformatting: {answer[:50]}...")
|
| 685 |
+
answer = self._force_format_answer(result)
|
| 686 |
+
|
| 687 |
+
if DEBUG:
|
| 688 |
+
print(f"[Final Answer] {answer}")
|
| 689 |
+
|
| 690 |
+
return answer if answer else "无法获取答案"
|
| 691 |
+
|
| 692 |
+
except Exception as e:
|
| 693 |
+
import traceback
|
| 694 |
+
error_msg = f"Agent 执行出错: {type(e).__name__}: {str(e)}"
|
| 695 |
+
print(f"[ERROR] {error_msg}")
|
| 696 |
+
print(traceback.format_exc())
|
| 697 |
+
return error_msg
|
| 698 |
+
|
| 699 |
+
def run_with_history(self, messages: list) -> dict:
|
| 700 |
+
"""
|
| 701 |
+
带历史消息执行
|
| 702 |
+
|
| 703 |
+
Args:
|
| 704 |
+
messages: 消息历史列表
|
| 705 |
+
|
| 706 |
+
Returns:
|
| 707 |
+
完整结果字典
|
| 708 |
+
"""
|
| 709 |
+
initial_state = {
|
| 710 |
+
"messages": messages,
|
| 711 |
+
"iteration_count": 0
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
return self.graph.invoke(initial_state)
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
# ========================================
|
| 718 |
+
# 便捷函数
|
| 719 |
+
# ========================================
|
| 720 |
+
|
| 721 |
+
def run_agent(question: str, task_id: str = None) -> str:
|
| 722 |
+
"""
|
| 723 |
+
运行 Agent 的便捷函数
|
| 724 |
+
|
| 725 |
+
Args:
|
| 726 |
+
question: 用户问题
|
| 727 |
+
task_id: 任务 ID(可选)
|
| 728 |
+
|
| 729 |
+
Returns:
|
| 730 |
+
最终答案
|
| 731 |
+
"""
|
| 732 |
+
agent = GaiaAgent()
|
| 733 |
+
return agent(question, task_id)
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
# ========================================
|
| 737 |
+
# 测试
|
| 738 |
+
# ========================================
|
| 739 |
+
|
| 740 |
+
if __name__ == "__main__":
|
| 741 |
+
# 简单测试
|
| 742 |
+
agent = GaiaAgent()
|
| 743 |
+
|
| 744 |
+
# 测试计算
|
| 745 |
+
print("Test 1: Calculation")
|
| 746 |
+
answer = agent("What is 15% of 200?")
|
| 747 |
+
print(f"Answer: {answer}\n")
|
| 748 |
+
|
| 749 |
+
# 测试搜索
|
| 750 |
+
print("Test 2: Search")
|
| 751 |
+
answer = agent("Who founded Microsoft?")
|
| 752 |
+
print(f"Answer: {answer}\n")
|
app.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GAIA Agent - HuggingFace Spaces Evaluation Runner
|
| 3 |
+
基于 LangGraph 的 GAIA benchmark 评估智能体
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import requests
|
| 10 |
+
import pandas as pd
|
| 11 |
+
|
| 12 |
+
from config import (
|
| 13 |
+
SCORING_API_URL,
|
| 14 |
+
DEBUG,
|
| 15 |
+
BATCH_QUESTION_DELAY,
|
| 16 |
+
)
|
| 17 |
+
from agent import GaiaAgent
|
| 18 |
+
|
| 19 |
+
# --- Constants ---
|
| 20 |
+
DEFAULT_API_URL = SCORING_API_URL
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# --- Agent Wrapper ---
|
| 24 |
+
class GAIAAgentWrapper:
|
| 25 |
+
"""
|
| 26 |
+
包装 GaiaAgent,适配 HuggingFace Spaces 评估接口
|
| 27 |
+
"""
|
| 28 |
+
def __init__(self):
|
| 29 |
+
print("Initializing GAIA Agent...")
|
| 30 |
+
self._agent = None
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def agent(self) -> GaiaAgent:
|
| 34 |
+
"""延迟初始化 Agent"""
|
| 35 |
+
if self._agent is None:
|
| 36 |
+
self._agent = GaiaAgent()
|
| 37 |
+
print("GAIA Agent initialized.")
|
| 38 |
+
return self._agent
|
| 39 |
+
|
| 40 |
+
def __call__(self, question: str, task_id: str = "") -> str:
|
| 41 |
+
"""
|
| 42 |
+
处理问题并返回答案
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
question: 问题文本
|
| 46 |
+
task_id: 任务 ID(用于下载附件)
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
答案字符串
|
| 50 |
+
"""
|
| 51 |
+
if DEBUG:
|
| 52 |
+
print(f"Agent received question (first 100 chars): {question[:100]}...")
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
if task_id:
|
| 56 |
+
answer = self.agent(question, task_id=task_id)
|
| 57 |
+
else:
|
| 58 |
+
answer = self.agent(question)
|
| 59 |
+
|
| 60 |
+
if DEBUG:
|
| 61 |
+
print(f"Agent returning answer: {answer[:100] if len(answer) > 100 else answer}")
|
| 62 |
+
|
| 63 |
+
return answer
|
| 64 |
+
except Exception as e:
|
| 65 |
+
error_msg = f"Agent error: {type(e).__name__}: {str(e)}"
|
| 66 |
+
print(error_msg)
|
| 67 |
+
return error_msg
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def run_and_submit_all(profile: gr.OAuthProfile | None):
|
| 71 |
+
"""
|
| 72 |
+
Fetches all questions, runs the GAIA Agent on them, submits all answers,
|
| 73 |
+
and displays the results.
|
| 74 |
+
"""
|
| 75 |
+
# --- Determine HF Space Runtime URL and Repo URL ---
|
| 76 |
+
space_id = os.getenv("SPACE_ID")
|
| 77 |
+
|
| 78 |
+
if profile:
|
| 79 |
+
username = f"{profile.username}"
|
| 80 |
+
print(f"User logged in: {username}")
|
| 81 |
+
else:
|
| 82 |
+
print("User not logged in.")
|
| 83 |
+
return "Please Login to Hugging Face with the button.", None
|
| 84 |
+
|
| 85 |
+
api_url = DEFAULT_API_URL
|
| 86 |
+
questions_url = f"{api_url}/questions"
|
| 87 |
+
submit_url = f"{api_url}/submit"
|
| 88 |
+
|
| 89 |
+
# 1. Instantiate Agent
|
| 90 |
+
try:
|
| 91 |
+
agent = GAIAAgentWrapper()
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"Error instantiating agent: {e}")
|
| 94 |
+
return f"Error initializing agent: {e}", None
|
| 95 |
+
|
| 96 |
+
# Agent code link for HuggingFace Spaces
|
| 97 |
+
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else "local"
|
| 98 |
+
print(f"Agent code: {agent_code}")
|
| 99 |
+
|
| 100 |
+
# 2. Fetch Questions
|
| 101 |
+
print(f"Fetching questions from: {questions_url}")
|
| 102 |
+
try:
|
| 103 |
+
response = requests.get(questions_url, timeout=30)
|
| 104 |
+
response.raise_for_status()
|
| 105 |
+
questions_data = response.json()
|
| 106 |
+
if not questions_data:
|
| 107 |
+
print("Fetched questions list is empty.")
|
| 108 |
+
return "Fetched questions list is empty or invalid format.", None
|
| 109 |
+
print(f"Fetched {len(questions_data)} questions.")
|
| 110 |
+
except requests.exceptions.RequestException as e:
|
| 111 |
+
print(f"Error fetching questions: {e}")
|
| 112 |
+
return f"Error fetching questions: {e}", None
|
| 113 |
+
except requests.exceptions.JSONDecodeError as e:
|
| 114 |
+
print(f"Error decoding JSON response from questions endpoint: {e}")
|
| 115 |
+
return f"Error decoding server response for questions: {e}", None
|
| 116 |
+
except Exception as e:
|
| 117 |
+
print(f"An unexpected error occurred fetching questions: {e}")
|
| 118 |
+
return f"An unexpected error occurred fetching questions: {e}", None
|
| 119 |
+
|
| 120 |
+
# 3. Run Agent on all questions
|
| 121 |
+
results_log = []
|
| 122 |
+
answers_payload = []
|
| 123 |
+
total_questions = len(questions_data)
|
| 124 |
+
print(f"Running agent on {total_questions} questions...")
|
| 125 |
+
|
| 126 |
+
for idx, item in enumerate(questions_data):
|
| 127 |
+
task_id = item.get("task_id")
|
| 128 |
+
question_text = item.get("question")
|
| 129 |
+
|
| 130 |
+
if not task_id or question_text is None:
|
| 131 |
+
print(f"Skipping item with missing task_id or question: {item}")
|
| 132 |
+
continue
|
| 133 |
+
|
| 134 |
+
# Rate limit delay (skip first question)
|
| 135 |
+
if idx > 0 and BATCH_QUESTION_DELAY > 0:
|
| 136 |
+
print(f"Waiting {BATCH_QUESTION_DELAY}s before next question (rate limit)...")
|
| 137 |
+
time.sleep(BATCH_QUESTION_DELAY)
|
| 138 |
+
|
| 139 |
+
print(f"\n[{idx + 1}/{total_questions}] Processing task: {task_id}")
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
submitted_answer = agent(question_text, task_id=task_id)
|
| 143 |
+
answers_payload.append({
|
| 144 |
+
"task_id": task_id,
|
| 145 |
+
"submitted_answer": submitted_answer
|
| 146 |
+
})
|
| 147 |
+
results_log.append({
|
| 148 |
+
"Task ID": task_id,
|
| 149 |
+
"Question": question_text[:100] + "..." if len(question_text) > 100 else question_text,
|
| 150 |
+
"Submitted Answer": submitted_answer
|
| 151 |
+
})
|
| 152 |
+
except Exception as e:
|
| 153 |
+
error_msg = f"AGENT ERROR: {type(e).__name__}: {e}"
|
| 154 |
+
print(f"Error running agent on task {task_id}: {e}")
|
| 155 |
+
results_log.append({
|
| 156 |
+
"Task ID": task_id,
|
| 157 |
+
"Question": question_text[:100] + "..." if len(question_text) > 100 else question_text,
|
| 158 |
+
"Submitted Answer": error_msg
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
if not answers_payload:
|
| 162 |
+
print("Agent did not produce any answers to submit.")
|
| 163 |
+
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
|
| 164 |
+
|
| 165 |
+
# 4. Prepare Submission
|
| 166 |
+
submission_data = {
|
| 167 |
+
"username": username.strip(),
|
| 168 |
+
"agent_code": agent_code,
|
| 169 |
+
"answers": answers_payload
|
| 170 |
+
}
|
| 171 |
+
status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
|
| 172 |
+
print(status_update)
|
| 173 |
+
|
| 174 |
+
# 5. Submit
|
| 175 |
+
print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
|
| 176 |
+
try:
|
| 177 |
+
response = requests.post(submit_url, json=submission_data, timeout=60)
|
| 178 |
+
response.raise_for_status()
|
| 179 |
+
result_data = response.json()
|
| 180 |
+
final_status = (
|
| 181 |
+
f"Submission Successful!\n"
|
| 182 |
+
f"User: {result_data.get('username')}\n"
|
| 183 |
+
f"Overall Score: {result_data.get('score', 'N/A')}% "
|
| 184 |
+
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
|
| 185 |
+
f"Message: {result_data.get('message', 'No message received.')}"
|
| 186 |
+
)
|
| 187 |
+
print("Submission successful.")
|
| 188 |
+
results_df = pd.DataFrame(results_log)
|
| 189 |
+
return final_status, results_df
|
| 190 |
+
except requests.exceptions.HTTPError as e:
|
| 191 |
+
error_detail = f"Server responded with status {e.response.status_code}."
|
| 192 |
+
try:
|
| 193 |
+
error_json = e.response.json()
|
| 194 |
+
error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
|
| 195 |
+
except requests.exceptions.JSONDecodeError:
|
| 196 |
+
error_detail += f" Response: {e.response.text[:500]}"
|
| 197 |
+
status_message = f"Submission Failed: {error_detail}"
|
| 198 |
+
print(status_message)
|
| 199 |
+
results_df = pd.DataFrame(results_log)
|
| 200 |
+
return status_message, results_df
|
| 201 |
+
except requests.exceptions.Timeout:
|
| 202 |
+
status_message = "Submission Failed: The request timed out."
|
| 203 |
+
print(status_message)
|
| 204 |
+
results_df = pd.DataFrame(results_log)
|
| 205 |
+
return status_message, results_df
|
| 206 |
+
except requests.exceptions.RequestException as e:
|
| 207 |
+
status_message = f"Submission Failed: Network error - {e}"
|
| 208 |
+
print(status_message)
|
| 209 |
+
results_df = pd.DataFrame(results_log)
|
| 210 |
+
return status_message, results_df
|
| 211 |
+
except Exception as e:
|
| 212 |
+
status_message = f"An unexpected error occurred during submission: {e}"
|
| 213 |
+
print(status_message)
|
| 214 |
+
results_df = pd.DataFrame(results_log)
|
| 215 |
+
return status_message, results_df
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
# --- Build Gradio Interface using Blocks ---
|
| 219 |
+
with gr.Blocks(title="GAIA Agent Evaluation") as demo:
|
| 220 |
+
gr.Markdown("# GAIA Agent Evaluation Runner")
|
| 221 |
+
gr.Markdown(
|
| 222 |
+
"""
|
| 223 |
+
**GAIA Agent** - 基于 LangGraph 的智能体,支持:
|
| 224 |
+
- RAG 知识库检索(高相似度直接返回答案)
|
| 225 |
+
- 网络搜索(DuckDuckGo)
|
| 226 |
+
- 文件处理(文本、ZIP、PDF、Excel)
|
| 227 |
+
- 代码执行(沙箱环境)
|
| 228 |
+
|
| 229 |
+
---
|
| 230 |
+
**Instructions:**
|
| 231 |
+
1. Log in to your Hugging Face account using the button below.
|
| 232 |
+
2. Click 'Run Evaluation & Submit All Answers' to start evaluation.
|
| 233 |
+
3. Wait for the agent to process all questions (this may take a while).
|
| 234 |
+
"""
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
gr.LoginButton()
|
| 238 |
+
|
| 239 |
+
run_button = gr.Button("Run Evaluation & Submit All Answers", variant="primary")
|
| 240 |
+
|
| 241 |
+
status_output = gr.Textbox(
|
| 242 |
+
label="Run Status / Submission Result",
|
| 243 |
+
lines=5,
|
| 244 |
+
interactive=False
|
| 245 |
+
)
|
| 246 |
+
results_table = gr.DataFrame(
|
| 247 |
+
label="Questions and Agent Answers",
|
| 248 |
+
wrap=True
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
run_button.click(
|
| 252 |
+
fn=run_and_submit_all,
|
| 253 |
+
outputs=[status_output, results_table]
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
if __name__ == "__main__":
|
| 258 |
+
print("\n" + "-" * 30 + " GAIA Agent Starting " + "-" * 30)
|
| 259 |
+
|
| 260 |
+
# Clear proxy settings for localhost
|
| 261 |
+
os.environ['NO_PROXY'] = 'localhost,127.0.0.1'
|
| 262 |
+
os.environ.pop('HTTP_PROXY', None)
|
| 263 |
+
os.environ.pop('HTTPS_PROXY', None)
|
| 264 |
+
os.environ.pop('http_proxy', None)
|
| 265 |
+
os.environ.pop('https_proxy', None)
|
| 266 |
+
|
| 267 |
+
# Check for SPACE_HOST and SPACE_ID at startup
|
| 268 |
+
space_host_startup = os.getenv("SPACE_HOST")
|
| 269 |
+
space_id_startup = os.getenv("SPACE_ID")
|
| 270 |
+
|
| 271 |
+
if space_host_startup:
|
| 272 |
+
print(f"SPACE_HOST found: {space_host_startup}")
|
| 273 |
+
print(f"Runtime URL: https://{space_host_startup}.hf.space")
|
| 274 |
+
else:
|
| 275 |
+
print("SPACE_HOST not found (running locally)")
|
| 276 |
+
|
| 277 |
+
if space_id_startup:
|
| 278 |
+
print(f"SPACE_ID found: {space_id_startup}")
|
| 279 |
+
print(f"Repo URL: https://huggingface.co/spaces/{space_id_startup}")
|
| 280 |
+
else:
|
| 281 |
+
print("SPACE_ID not found (running locally)")
|
| 282 |
+
|
| 283 |
+
print("-" * (60 + len(" GAIA Agent Starting ")) + "\n")
|
| 284 |
+
|
| 285 |
+
print("Launching GAIA Agent Evaluation Interface...")
|
| 286 |
+
demo.launch(debug=True, share=False)
|
config.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
配置管理模块 - GAIA Agent 配置
|
| 3 |
+
使用 .env 文件加载配置
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
# 加载 .env 文件(支持从父目录加载)
|
| 11 |
+
env_path = Path(__file__).parent / ".env"
|
| 12 |
+
if not env_path.exists():
|
| 13 |
+
env_path = Path(__file__).parent.parent / ".env"
|
| 14 |
+
load_dotenv(env_path)
|
| 15 |
+
|
| 16 |
+
# ========================================
|
| 17 |
+
# LLM 配置
|
| 18 |
+
# ========================================
|
| 19 |
+
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
| 20 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 21 |
+
MODEL = os.getenv("MODEL", "gpt-4o-mini")
|
| 22 |
+
TEMPERATURE = float(os.getenv("TEMPERATURE", "0"))
|
| 23 |
+
|
| 24 |
+
# ========================================
|
| 25 |
+
# API 配置
|
| 26 |
+
# ========================================
|
| 27 |
+
SCORING_API_URL = os.getenv("SCORING_API_URL", "https://agents-course-unit4-scoring.hf.space")
|
| 28 |
+
|
| 29 |
+
# ========================================
|
| 30 |
+
# Agent 配置
|
| 31 |
+
# ========================================
|
| 32 |
+
MAX_ITERATIONS = int(os.getenv("MAX_ITERATIONS", "10"))
|
| 33 |
+
|
| 34 |
+
# ========================================
|
| 35 |
+
# 超时配置(秒)
|
| 36 |
+
# ========================================
|
| 37 |
+
TOOL_TIMEOUT = int(os.getenv("TOOL_TIMEOUT", "30"))
|
| 38 |
+
TOTAL_TIMEOUT = int(os.getenv("TOTAL_TIMEOUT", "300"))
|
| 39 |
+
LLM_TIMEOUT = int(os.getenv("LLM_TIMEOUT", "120")) # LLM 调用超时
|
| 40 |
+
|
| 41 |
+
# ========================================
|
| 42 |
+
# 搜索配置
|
| 43 |
+
# ========================================
|
| 44 |
+
SEARCH_MAX_RESULTS = int(os.getenv("SEARCH_MAX_RESULTS", "5"))
|
| 45 |
+
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "")
|
| 46 |
+
WIKIPEDIA_MAX_RESULTS = int(os.getenv("WIKIPEDIA_MAX_RESULTS", "2"))
|
| 47 |
+
ARXIV_MAX_RESULTS = int(os.getenv("ARXIV_MAX_RESULTS", "3"))
|
| 48 |
+
TAVILY_MAX_RESULTS = int(os.getenv("TAVILY_MAX_RESULTS", "3"))
|
| 49 |
+
|
| 50 |
+
# ========================================
|
| 51 |
+
# 文件处理配置
|
| 52 |
+
# ========================================
|
| 53 |
+
MAX_FILE_SIZE = int(os.getenv("MAX_FILE_SIZE", "10000"))
|
| 54 |
+
|
| 55 |
+
# ========================================
|
| 56 |
+
# RAG 配置
|
| 57 |
+
# ========================================
|
| 58 |
+
RAG_PERSIST_DIR = os.getenv("RAG_PERSIST_DIR", "./rag_index")
|
| 59 |
+
RAG_CSV_PATH = os.getenv("RAG_CSV_PATH", "data_clean.csv")
|
| 60 |
+
RAG_EMBEDDING_MODEL = os.getenv("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
| 61 |
+
RAG_TOP_K = int(os.getenv("RAG_TOP_K", "3"))
|
| 62 |
+
|
| 63 |
+
# ========================================
|
| 64 |
+
# 速率限制配置
|
| 65 |
+
# ========================================
|
| 66 |
+
RATE_LIMIT_RETRY_MAX = int(os.getenv("RATE_LIMIT_RETRY_MAX", "5")) # 429错误最大重试次数
|
| 67 |
+
RATE_LIMIT_RETRY_BASE_DELAY = float(os.getenv("RATE_LIMIT_RETRY_BASE_DELAY", "10")) # 基础延迟秒数
|
| 68 |
+
BATCH_QUESTION_DELAY = float(os.getenv("BATCH_QUESTION_DELAY", "5")) # 批量测试问题间延迟秒数
|
| 69 |
+
|
| 70 |
+
# ========================================
|
| 71 |
+
# 调试配置
|
| 72 |
+
# ========================================
|
| 73 |
+
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
|
| 74 |
+
|
| 75 |
+
# ========================================
|
| 76 |
+
# 路径配置
|
| 77 |
+
# ========================================
|
| 78 |
+
BASE_DIR = Path(__file__).parent
|
| 79 |
+
TEMP_DIR = BASE_DIR / "temp"
|
| 80 |
+
TEMP_DIR.mkdir(exist_ok=True)
|
data_clean.csv
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
content,metadata
|
| 2 |
+
"If Eliud Kipchoge could maintain his record-making marathon pace indefinitely, how many thousand hours would it take him to run the distance between the Earth and the Moon its closest approach? Please use the minimum perigee value on the Wikipedia page for the Moon when carrying out your calculation. Round your result to the nearest 1000 hours and do not use any comma separators if necessary.","{""task_id"": ""e1fc63a2-da7a-432f-be78-7c4a95598703"", ""answer"": ""17"", ""steps"": ""1. Googled Eliud Kipchoge marathon pace to find 4min 37sec/mile\n2. Converted into fractions of hours.\n3. Found moon periapsis in miles (225,623 miles).\n4. Multiplied the two to find the number of hours and rounded to the nearest 100 hours."", ""tools"": ""1. A web browser.\n2. A search engine.\n3. A calculator."", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 3 |
+
How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia.,"{""task_id"": ""8e867cd7-cff9-4e6c-867a-ff5ddc2550be"", ""answer"": ""3"", ""steps"": ""1. I did a search for Mercedes Sosa\n2. I went to the Wikipedia page for her\n3. I scrolled down to \""Studio albums\""\n4. I counted the ones between 2000 and 2009"", ""tools"": ""1. web browser\n2. google search"", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 4 |
+
"Here's a fun riddle that I think you'll enjoy.
|
| 5 |
+
|
| 6 |
+
You have been selected to play the final round of the hit new game show ""Pick That Ping-Pong"". In this round, you will be competing for a large cash prize. Your job will be to pick one of several different numbered ping-pong balls, and then the game will commence. The host describes how the game works.
|
| 7 |
+
|
| 8 |
+
A device consisting of a winding clear ramp and a series of pistons controls the outcome of the game. The ramp feeds balls onto a platform. The platform has room for three ping-pong balls at a time. The three balls on the platform are each aligned with one of three pistons. At each stage of the game, one of the three pistons will randomly fire, ejecting the ball it strikes. If the piston ejects the ball in the first position on the platform the balls in the second and third position on the platform each advance one space, and the next ball on the ramp advances to the third position. If the piston ejects the ball in the second position, the ball in the first position is released and rolls away, the ball in the third position advances two spaces to occupy the first position, and the next two balls on the ramp advance to occupy the second and third positions on the platform. If the piston ejects the ball in the third position, the ball in the first position is released and rolls away, the ball in the second position advances one space to occupy the first position, and the next two balls on the ramp advance to occupy the second and third positions on the platform.
|
| 9 |
+
|
| 10 |
+
The ramp begins with 100 numbered ping-pong balls, arranged in ascending order from 1 to 100. The host activates the machine and the first three balls, numbered 1, 2, and 3, advance to the platform. Before the random firing of the pistons begins, you are asked which of the 100 balls you would like to pick. If your pick is ejected by one of the pistons, you win the grand prize, $10,000.
|
| 11 |
+
|
| 12 |
+
Which ball should you choose to maximize your odds of winning the big prize? Please provide your answer as the number of the ball selected.","{""task_id"": ""ec09fa32-d03f-4bf8-84b0-1f16922c3ae4"", ""answer"": ""3"", ""steps"": ""Step 1: Evaluate the problem statement provided in my user's prompt\nStep 2: Consider the probability of any ball on the platform earning the prize.\nStep 3: Evaluate the ball in position one. The probability of it earning the prize, P1, is 1/3\nStep 4: Using a calculator, evaluate the ball in position two. The probability of it earning the prize, P2, is the difference between 1 and the product of the complementary probabilities for each trial\nP2 = 1 - (2/3)(2/3)\nP2 = 5/9\nStep 5: Using a calculator, evaluate the ball in position three. The probability of it earning the prize, P3, is the difference between 1 and the product of the complementary probabilities for each trial\nP3 = 1 - (2/3)(2/3)(2/3)\nP3 = 19/27\nStep 6: Consider the possible outcomes of numbers higher than 3.\nStep 7: For each trial, either 1 or 2 balls from the ramp will advance to the platform. For any given selection, there is a 50% chance that the ball advances to position 2 or position 3.\nStep 8: As position three holds the highest chance of earning the prize, select the only ball known to occupy position three with certainty, ball 3.\nStep 9: Report the correct answer to my user, \""3\"""", ""tools"": ""None"", ""num_steps"": 9, ""has_file"": false, ""level"": ""1""}"
|
| 13 |
+
"What was the volume in m^3 of the fish bag that was calculated in the University of Leicester paper ""Can Hiccup Supply Enough Fish to Maintain a Dragon’s Diet?""","{""task_id"": ""5d0080cb-90d7-4712-bc33-848150e917d3"", ""answer"": ""0.1777"", ""steps"": ""1. Searched '\""Can Hiccup Supply Enough Fish to Maintain a Dragon’s Diet?\""' on Google.\n2. Opened \""Can Hiccup Supply Enough Fish to Maintain a Dragon’s Diet?\"" at https://journals.le.ac.uk/ojs1/index.php/jist/article/view/733.\n3. Clicked \""PDF\"".\n4. Found the calculations for the volume of the fish bag and noted them."", ""tools"": ""1. Web browser\n2. Search engine\n3. PDF access"", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 14 |
+
"In the video https://www.youtube.com/watch?v=L1vXCYZAYYM, what is the highest number of bird species to be on camera simultaneously?","{""task_id"": ""a1e91b78-d3d8-4675-bb8d-62741b4b68a6"", ""answer"": ""3"", ""steps"": ""1. Navigate to the YouTube link.\n2. Watch the video to see the highest number of bird species.\n3. Note the number."", ""tools"": ""1. Web browser\n2. Video parsing"", ""num_steps"": 3, ""has_file"": false, ""level"": ""1""}"
|
| 15 |
+
"Of the authors (First M. Last) that worked on the paper ""Pie Menus or Linear Menus, Which Is Better?"" in 2015, what was the title of the first paper authored by the one that had authored prior papers?","{""task_id"": ""46719c30-f4c3-4cad-be07-d5cb21eee6bb"", ""answer"": ""Mapping Human Oriented Information to Software Agents for Online Systems Usage"", ""steps"": ""1. Searched \""Pie Menus or Linear Menus, Which Is Better?\"" on Google.\n2. Opened \""Pie Menus or Linear Menus, Which Is Better?\"" on https://oda.oslomet.no/oda-xmlui/handle/10642/3162.\n3. Clicked each author's name.\n4. Noted the name that had no other papers listed.\n5. Searched \""Murano, Pietro\"" on Google.\n6. Opened http://www.pietromurano.org/.\n7. Clicked \""Publications\"".\n8. Found the earliest paper he contributed to."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 8, ""has_file"": false, ""level"": ""1""}"
|
| 16 |
+
"In Series 9, Episode 11 of Doctor Who, the Doctor is trapped inside an ever-shifting maze. What is this location called in the official script for the episode? Give the setting exactly as it appears in the first scene heading.","{""task_id"": ""4b6bb5f7-f634-410e-815d-e673ab7f8632"", ""answer"": ""THE CASTLE"", ""steps"": ""1. Search the web for “Doctor Who series 9 episode 11 official script”.\n2. Click result on the BBC website.\n3. Scroll through the PDF to read the script, noting that it takes place in a mechanical castle location.\n4. Scroll back to the first scene heading to note the answer, THE CASTLE"", ""tools"": ""1. Search engine\n2. Web browser\n3. PDF viewer"", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 17 |
+
"An office held a Secret Santa gift exchange where each of its twelve employees was assigned one other employee in the group to present with a gift. Each employee filled out a profile including three likes or hobbies. On the day of the gift exchange, only eleven gifts were given, each one specific to one of the recipient's interests. Based on the information in the document, who did not give a gift?","{""task_id"": ""cffe0e32-c9a6-4c52-9877-78ceb4aaa9fb"", ""answer"": ""Fred"", ""steps"": ""1. Open the document.\n2. Look at gifts and recipient interests.\n3. Match Galileo Galilei biography (could apply to astronomy or books -> Miguel or Micah)\n4. Match fishing reel (only applies to fishing -> Harry)\n5. Match Raku programming guide (Perl language, but could also apply to JavaScript enthusiast - > Fred or Jun)\n6. Match chisel set (could apply to camping or woodworking, but Harry is already fulfilled -> Jun, so Raku guide is for Fred)\n7. Match custom dice (could apply to board games or tabletop RPGs -> Lucy or Sara)\n8. Match “War and Peace” American film copy (could apply to old movies or Audrey Hepburn -> Perry or Alex)\n9. Match yarn (only applies to knitting -> Micah, so the Galileo biography is for Miguel)\n10. Match \""One Piece\"" graphic novel (could apply to books or manga, but Micah already has yarn -> Alex, so the \""War and Peace\"" film is for Perry)\n11. Match \""War and Peace\"" novel (could apply to books or historical fiction novels, but Micah has yarn -> Tyson)\n12. Match Starbucks gift card (only applies to coffee -> Lucy, so the dice are for Sara)\n13. Match foam exercise mat (only applies to yoga -> Georgette)\n14. Note which recipients have gifts (Miguel, Harry, Fred, Jun, Sara, Perry, Micah, Alex, Tyson, Lucy, Georgette) and which does not (Rebecca).\n15. Find who was supposed to give Rebecca a gift (Fred)."", ""tools"": ""1. Word document access"", ""num_steps"": 15, ""has_file"": true, ""level"": ""1""}"
|
| 18 |
+
".rewsna eht sa ""tfel"" drow eht fo etisoppo eht etirw ,ecnetnes siht dnatsrednu uoy fI","{""task_id"": ""2d83110e-a098-4ebb-9987-066c06fa42d0"", ""answer"": ""Right"", ""steps"": ""1. Read the instructions in reverse"", ""tools"": ""1. A word reversal tool / script"", ""num_steps"": 1, ""has_file"": false, ""level"": ""1""}"
|
| 19 |
+
"Each cell in the attached spreadsheet represents a plot of land. The color of the cell indicates who owns that plot. Green cells are plots owned by Earl Smith. Can Earl walk through every plot he owns (and no other plots) and return to his starting plot without backtracking? For this question, consider backtracking to be any instance where Earl would enter a plot of land he had already entered since leaving his starting plot.","{""task_id"": ""5cfb274c-0207-4aa7-9575-6ac0bd95d9b2"", ""answer"": ""No"", ""steps"": ""1. Open the spreadsheet\n2. Analyze the green cells.\n3. Note that the shape of Earl’s plots is not a loop. There are dead-ends that can’t be traversed without doubling back to a previously-traversed cell."", ""tools"": ""1. Excel\n2. Image recognition\n3. Color recognition"", ""num_steps"": 3, ""has_file"": true, ""level"": ""1""}"
|
| 20 |
+
"¬(A ∧ B) ↔ (¬A ∨ ¬B)
|
| 21 |
+
¬(A ∨ B) ↔ (¬A ∧ ¬B)
|
| 22 |
+
(A → B) ↔ (¬B → ¬A)
|
| 23 |
+
(A → B) ↔ (¬A ∨ B)
|
| 24 |
+
(¬A → B) ↔ (A ∨ ¬B)
|
| 25 |
+
¬(A → B) ↔ (A ∧ ¬B)
|
| 26 |
+
|
| 27 |
+
Which of the above is not logically equivalent to the rest? Provide the full statement that doesn't fit.","{""task_id"": ""27d5d136-8563-469e-92bf-fd103c28b57c"", ""answer"": ""(¬A → B) ↔ (A ∨ ¬B)"", ""steps"": ""1. Determine the truth values of the first statement: Recognize this is one of De Morgan's Laws showing how to distribute negation over the and conjunction - so it is a tautology.\n2. Determine the truth values of the second statement: Recognize this is one of De Morgan's Laws showing how to distribute negation over the or - so it is a tautology.\n3. Determine the truth values of the third statement: Recognize this is the definition of the contrapositive - so it is a tautology.\n4. Determine the truth values of the fourth statement: Recognize this as an alternative way of stating the conditional - so it is a tautology.\n5. Determine the truth values of the fifth statement: I don't recognize this, so check its truth values:\n6. A: True, B: True | (¬A → B) ↔ (A ∨ ¬B) = (¬T → T) ↔ (T ∨ ¬T) = (F → T) ↔ (T ∨ F) = T ↔ T = T\n7. A: True, B: False | (¬A → B) ↔ (A ∨ ¬B) = (¬T → F) ↔ (T ∨ ¬F) = (F → F) ↔ (T ∨ T) = T ↔ T = T\n8. A: False, B: True | (¬A → B) ↔ (A ∨ ¬B) = (¬F → T) ↔ (F ∨ ¬T) = (T → T) ↔ (F ∨ ¬T) = T ↔ (F ∨ F) = T ↔ F = F\n9. The fifth statement is not a tautology so is the statement that is not logically equivalent. We were asked for only one statement, so can stop here."", ""tools"": ""None"", ""num_steps"": 9, ""has_file"": false, ""level"": ""1""}"
|
| 28 |
+
"My family reunion is this week, and I was assigned the mashed potatoes to bring. The attendees include my married mother and father, my twin brother and his family, my aunt and her family, my grandma and her brother, her brother's daughter, and his daughter's family. All the adults but me have been married, and no one is divorced or remarried, but my grandpa and my grandma's sister-in-law passed away last year. All living spouses are attending. My brother has two children that are still kids, my aunt has one six-year-old, and my grandma's brother's daughter has three kids under 12. I figure each adult will eat about 1.5 potatoes of mashed potatoes and each kid will eat about 1/2 a potato of mashed potatoes, except my second cousins don't eat carbs. The average potato is about half a pound, and potatoes are sold in 5-pound bags. How many whole bags of potatoes do I need? Just give the number.","{""task_id"": ""dc28cf18-6431-458b-83ef-64b3ce566c10"", ""answer"": ""2"", ""steps"": ""1. Calculate the number of adults (mother, father, brother, brother's wife, aunt, aunt's husband, grandma, grandma's brother, grandma's brother's daughter, grandma's brother's daughter's husband, me = 11).\n2. Calculate the number of children (niece, nephew, cousin, grandma's brother's daughter's kids x3 = 6).\n3. Subtract the number of second cousins (grandma's brother's daughter's kids) (6 - 3 = 3).\n4. Calculate the adult potatoes (11 * 1.5 = 16.5).\n5. Calculate the child potatoes (3 * 0.5 = 1.5).\n6. Add to get the total potatoes (16.5 + 1.5 = 18).\n7. Multiply to get the pounds of potatoes (18 * 0.5 = 9 pounds).\n8. Calculate the number of 5-lb bags needed (9 / 5 = 1.8).\n9. Round up to get total bags (2)."", ""tools"": ""1. Calculator"", ""num_steps"": 9, ""has_file"": false, ""level"": ""1""}"
|
| 29 |
+
"In Emily Midkiff's June 2014 article in a journal named for the one of Hreidmar's sons that guarded his house, what word was quoted from two different authors in distaste for the nature of dragon depictions?","{""task_id"": ""b816bfce-3d80-4913-a07d-69b752ce6377"", ""answer"": ""fluffy"", ""steps"": ""1. Searched \""Hreidmar's sons\"" on Google.\n2. Opened https://en.wikipedia.org/wiki/Hrei%C3%B0marr.\n3. Noted Fafnir guarded his house.\n4. Searched \""Emily Midkiff June 2014 Fafnir\"" on Google.\n5. Opened \""Fafnir 2/2014 |\"" at http://journal.finfar.org/journal/archive/fafnir-22014/.\n6. Clicked the title '“Dragons are Tricksy”: The Uncanny Dragons of Children’s Literature'.\n7. Found the word in quotation marks from two different authors (Ruth Stein and Margaret Blount) in the text."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 7, ""has_file"": false, ""level"": ""1""}"
|
| 30 |
+
"Under DDC 633 on Bielefeld University Library's BASE, as of 2020, from what country was the unknown language article with a flag unique from the others?","{""task_id"": ""72e110e7-464c-453c-a309-90a95aed6538"", ""answer"": ""Guatemala"", ""steps"": ""1. Searched \""Bielefeld University Library's BASE\"" on Google.\n2. Opened https://www.base-search.net/.\n3. Clicked \""Browsing\"".\n4. Selected Clicked \""Dewey Decimal Classification (DDC) > 6 > 63 > 633.\n5. Refined to Unknown Language.\n6. Found the only article with a flag unique from the others in the search from pre-2020.\n7. Copied the country name from the institution."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 7, ""has_file"": false, ""level"": ""1""}"
|
| 31 |
+
"In the fictional language of Tizin, basic sentences are arranged with the Verb first, followed by the direct object, followed by the subject of the sentence. I want to express my love for apples to my Tizin friend.
|
| 32 |
+
|
| 33 |
+
The word that indicates oneself is ""Pa"" is the nominative form, ""Mato"" is the accusative form, and ""Sing"" is the genitive form.
|
| 34 |
+
|
| 35 |
+
The root verb that indicates an intense like for something is ""Maktay"". When it is used in the present, it is used in it's root form, when it is used in the preterit past, it is ""Tay"", and when it is used in the imperfect past, it is ""Aktay"". It is used differently than in English, and is better translated as ""is pleasing to"", meaning that the thing doing the liking is actually the object of the sentence rather than the subject.
|
| 36 |
+
|
| 37 |
+
The word for apples is borrowed from English in Tizin, and so it is ""Apple"" is the nominative form, ""Zapple"" is the accusative form, and ""Izapple"" is the genitive form.
|
| 38 |
+
|
| 39 |
+
Please translate ""I like apples"" to Tizin.","{""task_id"": ""42576abe-0deb-4869-8c63-225c2d75a95a"", ""answer"": ""Maktay mato apple"", ""steps"": ""1. Determine the order of words from the prompt (Verb - Object - Subject).\n2. Determine the present form of Like (\""Maktay\"")\n3. Determined that since the person doing the liking is the object of the sentence, the next word must be the one for oneself in object form.\n4. Determined the accusative form for onesself (\""mato\"").\n5. Determined the nominative form for apple. (\""apple\"").\n6. Put the words together in the correct order."", ""tools"": ""None"", ""num_steps"": 6, ""has_file"": false, ""level"": ""1""}"
|
| 40 |
+
"In Nature journal's Scientific Reports conference proceedings from 2012, in the article that did not mention plasmons or plasmonics, what nano-compound is studied? Don't use the prefix nano in your answer if there is one.","{""task_id"": ""b415aba4-4b68-4fc6-9b89-2c812e55a3e1"", ""answer"": ""diamond"", ""steps"": ""1. Searched \""nature scientific reports\"" on Google.\n2. Opened https://www.nature.com/srep/.\n3. Selected Explore Content > Research Articles.\n4. Filtered for Conference Proceedings from 2012.\n5. Opened each article link.\n6. Checked for \""plasmon\"" or \""plasmonic\"".\n7. Noted the nano-compound in the article that did not include either."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 7, ""has_file"": false, ""level"": ""1""}"
|
| 41 |
+
Review the chess position provided in the image. It is black's turn. Provide the correct next move for black which guarantees a win. Please provide your response in algebraic notation.,"{""task_id"": ""cca530fc-4052-43b2-b130-b30968d8aa44"", ""answer"": ""Rd5"", ""steps"": ""Step 1: Evaluate the position of the pieces in the chess position\nStep 2: Report the best move available for black: \""Rd5\"""", ""tools"": ""1. Image recognition tools"", ""num_steps"": 2, ""has_file"": true, ""level"": ""1""}"
|
| 42 |
+
"In the year 2022, and before December, what does ""R"" stand for in the three core policies of the type of content that was violated in the public logs on the Legume Wikipedia page?","{""task_id"": ""935e2cff-ae78-4218-b3f5-115589b19dae"", ""answer"": ""research"", ""steps"": ""1. Searched \""legume wikipedia\"" on Google.\n2. Opened \""Legume\"" on Wikipedia.\n3. Clicked \""View history\"".\n4. Clicked \""View logs for this page\"".\n5. Checked all types of logs.\n6. Set the date to November 2022.\n7. Followed the BLP link of the violation.\n8. Noted the meaning of \""R\""."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 8, ""has_file"": false, ""level"": ""1""}"
|
| 43 |
+
Who nominated the only Featured Article on English Wikipedia about a dinosaur that was promoted in November 2016?,"{""task_id"": ""4fc2f1ae-8625-45b5-ab34-ad4433bc21f8"", ""answer"": ""FunkMonk"", ""steps"": ""1. Search \""Wikipedia featured articles promoted in november 2016\""\n2. Click through to the appropriate page and find the person who nominated Giganotosaurus."", ""tools"": ""1. web browser\n2. search engine"", ""num_steps"": 2, ""has_file"": false, ""level"": ""1""}"
|
| 44 |
+
"What writer is quoted by Merriam-Webster for the Word of the Day from June 27, 2022?","{""task_id"": ""5188369a-3bbe-43d8-8b94-11558f909a08"", ""answer"": ""Annie Levin"", ""steps"": ""1. Search \""merriam-webster word of the day\"" on Google search.\n2. Opened the top \""Word of the Day\"" result from the Merriam-Webster dictionary online.\n3. Clicked \""SEE ALL WORDS OF THE DAY\"" at the bottom.\n4. Scrolled down to June 27, 2022.\n5. Opened the Word of the Day (\""jingoism\"").\n6. Scrolled down and identified context quote for \""jingoism\"".\n7. Noted the name attributed to the quote. "", ""tools"": ""1. Web browser\n2. Search engine\n3. Audio capability"", ""num_steps"": 7, ""has_file"": false, ""level"": ""1""}"
|
| 45 |
+
"Given this table defining * on the set S = {a, b, c, d, e}
|
| 46 |
+
|
| 47 |
+
|*|a|b|c|d|e|
|
| 48 |
+
|---|---|---|---|---|---|
|
| 49 |
+
|a|a|b|c|b|d|
|
| 50 |
+
|b|b|c|a|e|c|
|
| 51 |
+
|c|c|a|b|b|a|
|
| 52 |
+
|d|b|e|b|e|d|
|
| 53 |
+
|e|d|b|a|d|c|
|
| 54 |
+
|
| 55 |
+
provide the subset of S involved in any possible counter-examples that prove * is not commutative. Provide your answer as a comma separated list of the elements in the set in alphabetical order.","{""task_id"": ""6f37996b-2ac7-44b0-8e68-6d28256631b4"", ""answer"": ""b, e"", ""steps"": ""1. Compile the markdown.\n2. Look at the table across the diagonal to see if any portions are not symmetrical.\n3. See that b * e != e * b, but all others are symmetrical."", ""tools"": ""1. Markdown"", ""num_steps"": 3, ""has_file"": false, ""level"": ""1""}"
|
| 56 |
+
"As a comma separated list with no whitespace, using the provided image provide all the fractions that use / as the fraction line and the answers to the sample problems. Order the list by the order in which the fractions appear.","{""task_id"": ""9318445f-fe6a-4e1b-acbf-c68228c9906a"", ""answer"": ""3/4,1/4,3/4,3/4,2/4,1/2,5/35,7/21,30/5,30/5,3/4,1/15,1/3,4/9,1/8,32/23,103/170"", ""steps"": ""1. Find the fractions that use / as the fraction line before the sample problems start: 3/4,1/4,3/4,3/4,2/4,1/2,5/35,7/21,30/5,30/5\n2. Solve the sample problems:\n3. Problem 1: 3/4\n4. Problem 2: 1/15\n5. Problem 3: 1/3\n6. Problem 4: 4/9\n7. Problem 5: 1/8\n8. Problem 6: 32/23\n9. Problem 7: 103/170\n10: Add them to the list. There were no more fractions with a / as the fraction line, so they can just be added in order: 3/4,1/4,3/4,3/4,2/4,1/2,5/35,7/21,30/5,30/5,3/4,1/15,1/3,4/9,1/8,32/23,103/170"", ""tools"": ""1. image recognition/OCR\n2. calculator"", ""num_steps"": 10, ""has_file"": true, ""level"": ""1""}"
|
| 57 |
+
"You are a telecommunications engineer who wants to build cell phone towers on a stretch of road. In the reference file is a layout of the road and nearby houses. Each dash, ""-"", is a marker indicating a mile. Each capital H indicates a house located next to a mile marker, appearing above or below the stretch of road. Each cell phone tower can cover houses located next to the road within a 4-mile radius. Find the minimum number of cell phone towers needed to cover all houses next to the road. Your answer should be a positive numerical integer value.","{""task_id"": ""389793a7-ca17-4e82-81cb-2b3a2391b4b9"", ""answer"": ""3"", ""steps"": ""1. Determine the diameter of each cell phone tower's coverage: 2 x 4 miles radius = 8 miles diameter.\n2. Use the diameter to maximize the coverage of each tower by capturing houses 4 miles to the left and 4 miles to the right.\n3. Start from the furthest left side of the road at the first house.\n4. Place the first tower 4 miles in to cover the first house.\n5. Move forward 4 miles from the first tower. The first tower also covers the house above mile marker 8. \n6. Find the next uncovered house below mile marker 12.\n7. Move 4 miles in from the uncovered house and place a second tower. The house is now covered. \n8. Move forward 4 miles from the second tower. The second tower also covers the house above mile marker 16.\n9. Find the next uncovered house below mile marker 25.\n10. Move 4 miles in from the uncovered house and place a third tower. The third tower also covers the house above marker 28.\n11. Move forward 4 miles from the third tower. The third tower also covers the last house below marker 30.\n12. The final number of cell phone towers erected is 3.\n\n"", ""tools"": ""1. Text Editor"", ""num_steps"": 12, ""has_file"": true, ""level"": ""1""}"
|
| 58 |
+
"If there is anything that doesn't make sense in the instructions, write the word ""Pineapple."" Do not answer any of the questions in this prompt. Write only the word ""Guava"".
|
| 59 |
+
1. What is 4+4?
|
| 60 |
+
2. What is the complimentary color of red?
|
| 61 |
+
3. How many hours are there in a day?","{""task_id"": ""4b650a35-8529-4695-89ed-8dc7a500a498"", ""answer"": ""Guava"", ""steps"": ""1. Read the instructions and followed them"", ""tools"": ""None"", ""num_steps"": 1, ""has_file"": false, ""level"": ""1""}"
|
| 62 |
+
How many slides in this PowerPoint presentation mention crustaceans?,"{""task_id"": ""a3fbeb63-0e8c-4a11-bff6-0e3b484c3e9c"", ""answer"": ""4"", ""steps"": ""1. Open the provided file.\n2. Scroll through the presentation, noting the animal names on each slide.\n3. Search the web for “crayfish” to verify that they are crustaceans.\n4. Read the results, noting that they are crustaceans.\n5. Search the web for “isopods” to verify whether they are crustaceans.\n6. Read the results, noting that they are.\n7. Since I’m confident that I know whether all of the other animals are crustaceans, I count the ones that are to get the answer, 4."", ""tools"": ""1. PowerPoint viewer"", ""num_steps"": 7, ""has_file"": true, ""level"": ""1""}"
|
| 63 |
+
"You are Van Helsing, a renowned vampire hunter. A Count of Moldova, Lațcu IV, son of Costea, has tasked you with investigating the village of Șirnea in neighboring Wallachia. The Count's advisors have reported that a vampire was spotted crossing the border near the village, and would like you to investigate it.
|
| 64 |
+
|
| 65 |
+
You travel to the village of Șirnea, and you begin your investigation. One night, just before dawn, you catch a glimpse of a man in a long black cape with red lining leaping from roof-top to roof-top with superhuman agility. It's a vampire! You try to chase the creature back to its home, but the creature is too fast. However, because of the remoteness of the village, you know with absolute certainty that the vampire must be a resident of the village. You decide that your best course of action will be to visit all 100 residents of the town during the day. You know something about vampires and humans that will make your investigation possible; humans always tell the truth, but vampires always lie.
|
| 66 |
+
|
| 67 |
+
In the afternoon, you go from house to house, speaking with all 100 residents of Șirnea. You ask everyone the same question: ""How many vampires are living in Șirnea"". Everyone in the village gives the same response, ""At least one of us is a human.""
|
| 68 |
+
|
| 69 |
+
How many residents of Șirnea have been turned into vampires?","{""task_id"": ""c714ab3a-da30-4603-bacd-d008800188b9"", ""answer"": ""100"", ""steps"": ""Step 1: Evaluate the problem statement posed by my user.\nStep 2: Consider one known possible case: 1 Vampire, 99 humans\nStep 3: Step through the possible case with the answer provided by every resident \""At least one of us is a human.\""\nFor humans, who always tell the truth, the answer \""At least one of us is a human.\"" is true for the known possible case\nFor the vampire, who always lies, the answer \""At least one of us is a human.\"" is true, which violates the rule requiring the vampire to lie\nDiscount the case 1 Vampire, 99 Humans as possible\nStep 4: Consider the worst case: 100 Vampires, 0 Humans\nStep 5: Step through the worst case with the answer provided by every resident \""At least one of us is a human.\""\nFor humans, who always tell the truth, the answer \""At least one of us is a human.\"" is false, but 0 humans provide this response, making this statement irrelevant\nFor the vampire, who always lies, the answer \""At least one of us is a human.\"" is false, which respects the rule requiring vampires to lie\nConfirm the worst case as a provisional answer: 100 Vampires, 0 humans, answer: \""100\""\nStep 6: Consider a case with only one human: 99 Vampires, 1 Human\nStep 7: Step through the case with the answer provided by every resident \""At least one of us is a human.\""\nFor humans, who always tell the truth, the answer \""At least one of us is a human.\"" is true\nFor the vampire, who always lies, the answer \""At least one of us is a human.\"" is true, which violates the rule requiring vampires to lie\nDiscount the case of 99 Vampires, 1 Human as possible\nStep 8: Report the correct response to my user, \""100\"""", ""tools"": ""None"", ""num_steps"": 8, ""has_file"": false, ""level"": ""1""}"
|
| 70 |
+
"Examine the video at https://www.youtube.com/watch?v=1htKBjuUWec.
|
| 71 |
+
|
| 72 |
+
What does Teal'c say in response to the question ""Isn't that hot?""","{""task_id"": ""9d191bce-651d-4746-be2d-7ef8ecadb9c2"", ""answer"": ""Extremely"", ""steps"": ""1. Follow the link\n2. Watch the clip until the question \""Isn't that hot\"" is asked\n3. Take note of the reply."", ""tools"": ""1. Web browser\n2. Video processing software\n3. Audio processing software"", ""num_steps"": 3, ""has_file"": false, ""level"": ""1""}"
|
| 73 |
+
"You are given this Excel file as a map. You start on the START cell and move toward the END cell. You are allowed to move two cells per turn, and you may move up, down, left, or right. You may not move fewer than two cells, and you may not move backward. You must avoid moving onto any blue cells. On the eleventh turn, what is the 6-digit hex code (without prefix) of the color of the cell where you land after moving?","{""task_id"": ""65afbc8a-89ca-4ad5-8d62-355bb401f61d"", ""answer"": ""F478A7"", ""steps"": ""1. Opened Map.xlsx.\n2. Counted 11 turns of 2 spaces each (22 spaces) along the path of non-blue cells.\n3. Opened cell formatting for the cell.\n4. Clicked the \""Fill\"" tab.\n5. Clicked \""More Colors...\""\n6. Noted the hex code of the color."", ""tools"": ""1. Access to Excel files\n2. Color recognition\n3. Calculator (or ability to count)"", ""num_steps"": 6, ""has_file"": true, ""level"": ""1""}"
|
| 74 |
+
What is the surname of the equine veterinarian mentioned in 1.E Exercises from the chemistry materials licensed by Marisa Alviar-Agnew & Henry Agnew under the CK-12 license in LibreText's Introductory Chemistry materials as compiled 08/21/2023?,"{""task_id"": ""cabe07ed-9eca-40ea-8ead-410ef5e83f91"", ""answer"": ""Louvrier"", ""steps"": ""1. Search for \""1.E Exercises LibreText Introductory Chemistry\""\n2. Read to see the horse doctor mentioned."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 2, ""has_file"": false, ""level"": ""1""}"
|
| 75 |
+
"I'm making a grocery list for my mom, but she's a professor of botany and she's a real stickler when it comes to categorizing things. I need to add different foods to different categories on the grocery list, but if I make a mistake, she won't buy anything inserted in the wrong category. Here's the list I have so far:
|
| 76 |
+
|
| 77 |
+
milk, eggs, flour, whole bean coffee, Oreos, sweet potatoes, fresh basil, plums, green beans, rice, corn, bell pepper, whole allspice, acorns, broccoli, celery, zucchini, lettuce, peanuts
|
| 78 |
+
|
| 79 |
+
I need to make headings for the fruits and vegetables. Could you please create a list of just the vegetables from my list? If you could do that, then I can figure out how to categorize the rest of the list into the appropriate categories. But remember that my mom is a real stickler, so make sure that no botanical fruits end up on the vegetable list, or she won't get them when she's at the store. Please alphabetize the list of vegetables, and place each item in a comma separated list.","{""task_id"": ""3cef3a44-215e-4aed-8e3b-b1e3f08063b7"", ""answer"": ""broccoli, celery, fresh basil, lettuce, sweet potatoes"", ""steps"": ""Step 1: Evaluate the list provided by my user, eliminating objects which are neither fruits nor vegetables:\nsweet potatoes, fresh basil, plums, green beans, rice, corn, bell pepper, whole allspice, acorns, broccoli, celery, zucchini, lettuce, peanuts\nStep 2: Remove all items from the list which are botanical fruits, leaving a list of vegetables:\nsweet potatoes, fresh basil, broccoli, celery, lettuce\nStep 3: Alphabetize the remaining list as requested by my user:\nbroccoli, celery, fresh basil, lettuce, sweet potatoes\nStep 4: Provide the correct response in the requested format:\n\""broccoli\ncelery\nfresh basil\nlettuce\nsweet potatoes\"""", ""tools"": ""No tools required"", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 80 |
+
"Hi, I'm making a pie but I could use some help with my shopping list. I have everything I need for the crust, but I'm not sure about the filling. I got the recipe from my friend Aditi, but she left it as a voice memo and the speaker on my phone is buzzing so I can't quite make out what she's saying. Could you please listen to the recipe and list all of the ingredients that my friend described? I only want the ingredients for the filling, as I have everything I need to make my favorite pie crust. I've attached the recipe as Strawberry pie.mp3.
|
| 81 |
+
|
| 82 |
+
In your response, please only list the ingredients, not any measurements. So if the recipe calls for ""a pinch of salt"" or ""two cups of ripe strawberries"" the ingredients on the list would be ""salt"" and ""ripe strawberries"".
|
| 83 |
+
|
| 84 |
+
Please format your response as a comma separated list of ingredients. Also, please alphabetize the ingredients.","{""task_id"": ""99c9cc74-fdc8-46c6-8f8d-3ce2d3bfeea3"", ""answer"": ""cornstarch, freshly squeezed lemon juice, granulated sugar, pure vanilla extract, ripe strawberries"", ""steps"": ""Step 1: Load the file supplied to me by my user.\nStep 2: Using speech-to-text tools, convert the audio file to plain text and store it for the candidate word list:\n\n\""In a saucepan, combine ripe strawberries, granulated sugar, freshly squeezed lemon juice, and cornstarch. Cook the mixture over medium heat, stirring constantly, until it thickens to a smooth consistency. Remove from heat and stir in a dash of pure vanilla extract. Allow the strawberry pie filling to cool before using it as a delicious and fruity filling for your pie crust.\""\n\nStep 3: Evaluate the candidate word list and process it, stripping each ingredient encountered to a provisional response list:\n\nripe strawberries\ngranulated sugar\nfreshly squeezed lemon juice\ncornstarch\npure vanilla extract\n\nStep 4: Alphabetize the list of ingredients as requested by my user to create a finalized response:\n\ncornstarch\nfreshly squeezed lemon juice\ngranulated sugar\npure vanilla extract\nripe strawberries\n\nStep 5: Report the correct response to my user:\n\n\""cornstarch\nfreshly squeezed lemon juice\ngranulated sugar\npure vanilla extract\nripe strawberries\"""", ""tools"": ""1. A file interface\n2. A speech-to-text tool"", ""num_steps"": 5, ""has_file"": true, ""level"": ""1""}"
|
| 85 |
+
"In the Scikit-Learn July 2017 changelog, what other predictor base command received a bug fix? Just give the name, not a path.","{""task_id"": ""d0633230-7067-47a9-9dbf-ee11e0a2cdd6"", ""answer"": ""BaseLabelPropagation"", ""steps"": ""1. Searched \""Scikit-Learn July 2017 changelog\"" on Google.\n2. Opened \""Release History\"" from the Scikit-Learn website.\n3. Clicked \""Other versions\"" in the upper left.\n4. Opened the links, starting from the bottom, until one was found that included the \""July 2017\"" changelog under the News.\n5. Looked for the \""Bug fixes\"" section.\n6. Looked under \""Other predictors\"" in that section."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 6, ""has_file"": false, ""level"": ""1""}"
|
| 86 |
+
Who did the actor who played Ray in the Polish-language version of Everybody Loves Raymond play in Magda M.? Give only the first name.,"{""task_id"": ""305ac316-eef6-4446-960a-92d80d542f82"", ""answer"": ""Wojciech"", ""steps"": ""1. Search \""Polish-language version of Everybody Loves Raymond\"" and pull up the Wiki page for Wszyscy kochają Romana.\n2. See that Bartłomiej Kasprzykowski is marked as playing Ray and go to his Wiki page.\n3. See that he is stated to have played Wojciech Płaska in Magda M."", ""tools"": ""None"", ""num_steps"": 3, ""has_file"": false, ""level"": ""1""}"
|
| 87 |
+
"On the BBC Earth YouTube video of the Top 5 Silliest Animal Moments, what species of bird is featured?","{""task_id"": ""0383a3ee-47a7-41a4-b493-519bdefe0488"", ""answer"": ""Rockhopper penguin"", ""steps"": ""1. Search \""top 5 silliest animal moments bbc earth youtube\"" on Google search.\n2. Open the top link to \""Top 5 Silliest Animal Moments! | BBC Earth - YouTube\"".\n3. Listen to the video until the species is named."", ""tools"": ""1. Web browser\n2. Search engine\n3. Video recognition tools"", ""num_steps"": 3, ""has_file"": false, ""level"": ""1""}"
|
| 88 |
+
What is the final numeric output from the attached Python code?,"{""task_id"": ""f918266a-b3e0-4914-865d-4faa564f1aef"", ""answer"": ""0"", ""steps"": ""1. Run the attached Python code"", ""tools"": ""1. Python"", ""num_steps"": 1, ""has_file"": true, ""level"": ""1""}"
|
| 89 |
+
How many more blocks (also denoted as layers) in BERT base encoder than the encoder from the architecture proposed in Attention is All You Need?,"{""task_id"": ""11af4e1a-5f45-467d-9aeb-46f4bb0bf034"", ""answer"": ""6"", ""steps"": ""1. Search the internet for \""blocks in bert base\""\n2. Examine the search results page to locate the answer (12)\n3. Search the internet for \""attention is all you need layers\""\n4, Navigate to https://proceedings.neurips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf from the search results page\n5. Examine the architecture section of the PDF to locate the answer (12)\n6. Calculate the difference between the two numbers"", ""tools"": ""1. Web browser\n2. Search engine\n3. Calculator"", ""num_steps"": 6, ""has_file"": false, ""level"": ""1""}"
|
| 90 |
+
"Bob was invited to participate in a game show, and he advanced to the final round. The final round offered Bob the chance to win a large sum by playing a game against the host. The host has 30 shiny prop coins, each of which is worth $1,000 if Bob manages to win them by playing the game. The host hides the coins in three different prize boxes and then shuffles their order. The only rule restricting the host's coin placement is that one box must contain at least 2 coins, and one box must contain 6 more coins than another box. In order to play, Bob must submit three guesses, one guess for the number of coins in each box. The box is then opened and the number of coins is revealed. If Bob's guess is a number greater than the number of coins in the box, Bob earns no coins. If Bob guesses a number equal to or less than the number of coins in the box, Bob wins a number of coins equal to his guess.
|
| 91 |
+
|
| 92 |
+
If Bob plays uses the optimal strategy, what's the minimum amount of money he can win from the game?","{""task_id"": ""e142056d-56ab-4352-b091-b56054bd1359"", ""answer"": ""16000"", ""steps"": ""Step 1: Evaluate the problem statement provided by my user, storing the relevant information: \n30 coins with a value of $1,000 distributed between 3 boxes.\nEach box must contain at least 2 coins\nOne box must contain 6 more coins than another\n\nStep 2: Evaluate the base distribution: 2-8-20, noting that two boxes must contain at least 8 coins\n\nStep 3: Evaluate the most even allowable distribution: 8,8,14, noting that two boxes must contain at least 8 coins\n\nStep 4: Evaluate a case where Bob guesses 8 for each box in the outlier distributions.\nStep 5: For the worst case 2-8-20 distribution, Bob wins 0+8+8 = 16 coins\nStep 6: For the 8-8-14 distribution, Bob wins 8+8+8 = 24 coins\nStep 7: Convert the worst-case coin count to a prize value, 16*$1,000 = $16,000\nStep 8: Report the correct answer to my user: \""$16,000\"""", ""tools"": ""1. A calculator"", ""num_steps"": 8, ""has_file"": false, ""level"": ""1""}"
|
| 93 |
+
"Pull out the sentence in the following 5x7 block of text. Read from left to right and use all of the letters in order:
|
| 94 |
+
|
| 95 |
+
THESE
|
| 96 |
+
AGULL
|
| 97 |
+
GLIDE
|
| 98 |
+
DPEAC
|
| 99 |
+
EFULL
|
| 100 |
+
YTOMY
|
| 101 |
+
CHAIR","{""task_id"": ""50ad0280-0819-4bd9-b275-5de32d3b5bcb"", ""answer"": ""The seagull glided peacefully to my chair."", ""steps"": ""1. I start with the first line, \""T H E S E\"" and proceed to the next, \""A G U L L\"". At this point, I am able to discern that \""A G U L L\"" is probably meant to be \""A GULL\"". However, I continue to read through the rest of the lines to get a sense of any other words that might jump out that would substantiate \""A GULL\"" being accurate both semantically and syntactically. 2. So now I am on the last line and decide to work backwards. \""CHAIR\"" is on the last line all by itself and this does seem a plausible fit as a full word rather than a fragment of another word. When I look to the line directly above \""Y T O M Y\"", the word \""my\"" jumps out and this is a natural accompaniment to the noun often used to indicate possession. \n3. Eliminating the \""MY\"" at the end of \""Y T O MY\"" leaves \""Y T O\"" remaining in the line and I immediately recognize the preposition \""TO\"". It is a this point I am fairly confident that \""TO MY CHAIR\"" is most likely accurate. Given that there is only a \""Y\"" left, I discern it is more than likely the end of a word located in the row above.\n4. I am now on the fifth row down and am looking at the letters \""E F U L L\"" Attaching the \""Y\"" left over from the sixth row below I see \""E F U L L Y\"" I recognize the word \""FULLY\"" I know it can stand alone as an adverb or it can serve as a suffix to a larger adverb.\n5. Detaching the \""FULLY\"", leaves the \""E\"" alone on the line. Knowing it does not represent a word on its own in the English language, I look to attach it to the line above (row 4).\n6. The fourth row reads \""D P E A C\"". Adding the \""E\"" to the end, the first word I can separate out is \""ACE\"". However \""ACEFULLY\"" is not a word nor does \""ACE FULLY TO MY CHAIR\"" make sense. When working my way left through the line, continuing to attach each letter as I go, I land on the \""P\"" and am fairly confident that the word is \""PEACEFULLY\"".\n7. Eliminating the \""PEAC\"" from the row leaves me left with a \""D\"". Now I look at the row above, row 3 and see that the row comprises the word \""GLIDE\"" Adding the \""D\"" to the end of the word would not only be permissible in terms of a displaying appropriate tense but it also makes sense as I add it to the fragment I have so far. I now can read \""GLIDED PEACEFULLY TO MY CHAIR\"".\n8. Now, I am on the second line and if I were to read it from there on down it would read \""A GULL GLIDED PEACEFULLY TO MY CHAIR\"". While this reads well and makes sense semantically and syntactically on its own, it does not make sense when I add the first row. THESE A GULL GLIDED PEACEFULLY TO MY CHAIR. So now I am left with the conclusion that \""A GULL\"" is not correct. Either it is part of a larger word or the letters need to be broken down further. At a quick glace, I can see that they don't make sense being broken down further so I leave \""GULL\"" and add the \""A\"" to the string above. Immediately my eye sees that \""A can be added to \""SE\"" to make \""SEA\"" and that the remaining\nletters spell the word \""THE\"" I now know the sentence reads \""The seagull glided peacefully to my chair."", ""tools"": ""None"", ""num_steps"": 8, ""has_file"": false, ""level"": ""1""}"
|
| 102 |
+
"On Cornell Law School website's legal information institute, under the fifth section of federal rules alphabetically, what word was deleted in the last amendment to the first rule in the article that has ""witnesses"" in the most titles as of 2021?","{""task_id"": ""7673d772-ef80-4f0f-a602-1bf4485c9b43"", ""answer"": ""inference"", ""steps"": ""1. Searched \""Cornell Law School legal information institute\"" on Google.\n2. Opened https://www.law.cornell.edu/.\n3. Clicked Get The Law > Federal Rules > Federal Rules of Evidence (fourth section down).\n4. Found the article that has \""witnesses\"" in the most titles (VII).\n5. Opened the first rule (701).\n6. Scrolled to the last amendment as of 2021 (2011 amendment).\n7. Found the word that was deleted (inference)."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 7, ""has_file"": false, ""level"": ""1""}"
|
| 103 |
+
"Of the cities within the United States where U.S. presidents were born, which two are the farthest apart from the westernmost to the easternmost going east, giving the city names only? Give them to me in alphabetical order, in a comma-separated list","{""task_id"": ""c365c1c7-a3db-4d5e-a9a1-66f56eae7865"", ""answer"": ""Braintree, Honolulu"", ""steps"": ""1. Searched \""cities where us presidents are born\"" on Google.\n2. Opened \""List of presidents of the United States by home state\"" on Wikipedia.\n3. Searched the eastern cities to find the easternmost one (Braintree, MA).\n4. Checked the westernmost city (Honolulu, HI)."", ""tools"": ""1. Search engine\n2. Web browser"", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 104 |
+
"According to Girls Who Code, how long did it take in years for the percentage of computer scientists that were women to change by 13% from a starting point of 37%?","{""task_id"": ""7d4a7d1d-cac6-44a8-96e8-ea9584a70825"", ""answer"": ""22"", ""steps"": ""1. Searched \""Girls Who Code\"" on Google.\n2. Opened https://girlswhocode.com/.\n3. Clicked \""About Us\"".\n4. Noted that the chart started at 37% and declined to 24%.\n5. Subtracted the marked years to find the number of years (2017 - 1995 = 22)."", ""tools"": ""1. Web browser\n2. Search engine\n3. Calculator"", ""num_steps"": 5, ""has_file"": false, ""level"": ""1""}"
|
| 105 |
+
What was the complete title of the book in which two James Beard Award winners recommended the restaurant where Ali Khan enjoyed a New Mexican staple in his cost-conscious TV show that started in 2015? Write the numbers in plain text if there are some in the title.,"{""task_id"": ""dc22a632-937f-4e6a-b72f-ba0ff3f5ff97"", ""answer"": ""Five Hundred Things To Eat Before It's Too Late: and the Very Best Places to Eat Them"", ""steps"": ""1. Searched \""Ali Khan New Mexico staple TV show\"" on Google.\n2. Opened \""Albuquerque | Cheap Eats\"" at https://www.cookingchanneltv.com/shows/cheap-eats/episodes/albuquerque.\n3. Noted the New Mexico staple and the list of restaurants.\n4. Searched \""Albuquerque Cheap Eats carne avodava\"" on Google.\n5. Confirmed the restaurant name (Papa Felipe's) from the results.\n6. Searched \""James Beard Award winners Papa Felipe's\"" on Google.\n7. Opened \""Papa Felipe's Mexican Restaurant - Albuquerque, New ...\"" at https://www.nmgastronome.com/?p=4572.\n8. Clicked the link on the book title.\n9. Copied the full book title from Amazon."", ""tools"": ""1. Web browser\n2. Search engine"", ""num_steps"": 9, ""has_file"": false, ""level"": ""1""}"
|
| 106 |
+
How many at bats did the Yankee with the most walks in the 1977 regular season have that same season?,"{""task_id"": ""3f57289b-8c60-48be-bd80-01f8099ca449"", ""answer"": ""519"", ""steps"": ""1. Search \""yankee stats\"" to find their MLB stats page.\n2. Set the data to the 1977 regular season.\n3. Sort to find the most walks.\n4. See how many at bats the player had."", ""tools"": ""1. web browser\n2. search engine"", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 107 |
+
"In Audre Lorde’s poem “Father Son and Holy Ghost”, what is the number of the stanza in which some lines are indented?","{""task_id"": ""23dd907f-1261-4488-b21c-e9185af91d5e"", ""answer"": ""2"", ""steps"": ""1. Search the web for “Audre Lorde Father Son and Holy Ghost”.\n2. Click on Poetry Foundation result.\n3. Note the stanza that appears to have lines indented, the second one.\n4. Return to search results to confirm.\n5. Click on second result.\n6. Confirm that the indentation appears in the second stanza here as well."", ""tools"": ""1. Search engine\n2. Web browser"", ""num_steps"": 6, ""has_file"": false, ""level"": ""1""}"
|
| 108 |
+
"Hi, I was out sick from my classes on Friday, so I'm trying to figure out what I need to study for my Calculus mid-term next week. My friend from class sent me an audio recording of Professor Willowbrook giving out the recommended reading for the test, but my headphones are broken :(
|
| 109 |
+
|
| 110 |
+
Could you please listen to the recording for me and tell me the page numbers I'm supposed to go over? I've attached a file called Homework.mp3 that has the recording. Please provide just the page numbers as a comma-delimited list. And please provide the list in ascending order.","{""task_id"": ""1f975693-876d-457b-a649-393859e79bf3"", ""answer"": ""132, 133, 134, 197, 245"", ""steps"": ""Step 1: Load the file supplied by my user.\nStep 2: Using audio processing tools, convert the text of the audio file to speech:\n\n\""Before you all go, I want to remind you that the midterm is next week. Here's a little hint; you should be familiar with the differential equations on page 245, problems that are very similar to problems 32, 33, and 44 from that page might be on the test. And also some of you might want to brush up on the last page in the integration section, page 197. I know some of you struggled on last week's quiz. I foresee problem 22 from page 197 being on your midterm. Oh, and don't forget to brush up on the section on related rates, on pages 132, 133, and 134.\""\n\nStep 3: Evaluate the converted audio, recording each instance of page numbers: 245, 197, 197, 132, 133, 134\nStep 4: Sort the page numbers in ascending order, omitting duplicates, and store this list as the correct answer to my user's request: 132, 133, 134, 197, 245\nStep 5: Report the correct response to my user: \""132, 133, 134, 197, 245\"""", ""tools"": ""1. A file interface\n2. A speech-to-text audio processing tool"", ""num_steps"": 5, ""has_file"": true, ""level"": ""1""}"
|
| 111 |
+
"On June 6, 2023, an article by Carolyn Collins Petersen was published in Universe Today. This article mentions a team that produced a paper about their observations, linked at the bottom of the article. Find this paper. Under what NASA award number was the work performed by R. G. Arendt supported by?","{""task_id"": ""840bfca7-4f7b-481a-8794-c560c340185d"", ""answer"": ""80GSFC21M0002"", ""steps"": ""1. Google \""June 6, 2023 Carolyn Collins Petersen Universe Today\""\n2. Find the relevant link to the scientific paper and follow that link\n3. Open the PDF. \n4. Search for NASA award number"", ""tools"": ""1. Web browser\n2. Search engine\n3. Access to academic journal websites"", ""num_steps"": 4, ""has_file"": false, ""level"": ""1""}"
|
| 112 |
+
What was the actual enrollment count of the clinical trial on H. pylori in acne vulgaris patients from Jan-May 2018 as listed on the NIH website?,"{""task_id"": ""a0068077-79f4-461a-adfe-75c1a4148545"", ""answer"": ""90"", ""steps"": ""1. Searched \""nih\"" on Google search.\n2. Clicked the top link to nih.gov.\n3. Searched \""h pylori acne\"" in the search box.\n4. Clicked \""More\"" and selected \""Clinical Trials\"".\n5. Clicked the result about H. Pylori and acne.\n6. Checked the date to confirm it was January to May 2018.\n7. Opened \""Tabular View\"".\n8. Scrolled down to Actual Enrollment and recorded the number."", ""tools"": ""1. Search engine\n2. Web browser"", ""num_steps"": 8, ""has_file"": false, ""level"": ""1""}"
|
| 113 |
+
Where were the Vietnamese specimens described by Kuznetzov in Nedoshivina's 2010 paper eventually deposited? Just give me the city name without abbreviations.,"{""task_id"": ""bda648d7-d618-4883-88f4-3466eabd860e"", ""answer"": ""Saint Petersburg"", ""steps"": ""1. Search \""Kuznetzov Nedoshivina 2010\""\n2. Find the 2010 paper \""A catalogue of type specimens of the Tortricidae described by V. I. Kuznetzov from Vietnam and deposited in the Zoological Institute, St. Petersburg\"""", ""tools"": ""1. search engine"", ""num_steps"": 2, ""has_file"": false, ""level"": ""1""}"
|
| 114 |
+
"A standard Rubik’s cube has been broken into cubes making up its sides. The cubes are jumbled, and one is removed. There are 6 cubes with one colored face, 12 edge cubes with two colored faces, and 8 corner cubes with three colored faces. All blue cubes have been found. All cubes directly left, right, above, and below the orange center cube have been found, along with the center cube. The green corners have all been found, along with all green that borders yellow. For all orange cubes found, the opposite face’s cubes have been found. The removed cube has two colors on its faces. What are they? Answer using a comma separated list, with the colors ordered alphabetically.","{""task_id"": ""50ec8903-b81f-4257-9450-1085afd2c319"", ""answer"": ""green, white"", ""steps"": ""1. Set up a standard Rubik's cube (red opposite orange, white opposite yellow, green opposite blue).\n2. Eliminated blue cubes, along with adjacent colors.\n3. Eliminated orange cubes, along with adjacent colors.\n4. Eliminated green corners and the green/yellow edge.\n5. Eliminated red, opposite of orange, cubes and adjacent colors.\n6. Identified the last possible two-face cube."", ""tools"": ""1. Rubik's cube model"", ""num_steps"": 6, ""has_file"": false, ""level"": ""1""}"
|
| 115 |
+
"What country had the least number of athletes at the 1928 Summer Olympics? If there's a tie for a number of athletes, return the first in alphabetical order. Give the IOC country code as your answer.","{""task_id"": ""cf106601-ab4f-4af9-b045-5295fe67b37d"", ""answer"": ""CUB"", ""steps"": ""1. Look up the 1928 Summer Olympics on Wikipedia\n2. Look at a table of athletes from countries.\n3. See that two countries had 1 and 2 athletes, so disregard those and choose the Cuba as CUB."", ""tools"": ""None"", ""num_steps"": 3, ""has_file"": false, ""level"": ""1""}"
|
| 116 |
+
"Who are the pitchers with the number before and after Taishō Tamai's number as of July 2023? Give them to me in the form Pitcher Before, Pitcher After, use their last names only, in Roman characters.","{""task_id"": ""a0c07678-e491-4bbc-8f0b-07405144218f"", ""answer"": ""Yoshida, Uehara"", ""steps"": ""1. Look up Taishō Tamai on Wikipedia\n2. See the pitcher with the number 18 (before) is Kōsei Yoshida and number 20 (after) is Kenta Uehara"", ""tools"": ""1. Wikipedia"", ""num_steps"": 2, ""has_file"": false, ""level"": ""1""}"
|
| 117 |
+
The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places.,"{""task_id"": ""7bd855d8-463d-4ed5-93ca-5fe35145f733"", ""answer"": ""89706.00"", ""steps"": ""1. Open the attached file.\n2. Read the columns representing different menu items. Note that they all appear to be food except for the “soda” column.\n3. Write a function to sum the relevant columns.\n4. Ensure the answer follows the specified formatting."", ""tools"": ""1. Excel\n2. Calculator"", ""num_steps"": 4, ""has_file"": true, ""level"": ""1""}"
|
| 118 |
+
What is the first name of the only Malko Competition recipient from the 20th Century (after 1977) whose nationality on record is a country that no longer exists?,"{""task_id"": ""5a0c1adf-205e-4841-a666-7c3ef95def9d"", ""answer"": ""Claus"", ""steps"": ""1. Look at the Malko Competition page on Wikipedia\n2. Scan the winners to see that the 1983 winner, Claus Peter Flor is stated to be from East Germany."", ""tools"": ""None"", ""num_steps"": 2, ""has_file"": false, ""level"": ""1""}"
|
extension_tools.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
扩展工具模块 - GAIA Agent 扩展功能
|
| 3 |
+
包含:parse_pdf, parse_excel, image_ocr, transcribe_audio
|
| 4 |
+
|
| 5 |
+
注意:这些工具需要额外的依赖库,如果导入失败会优雅降级。
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from typing import Optional, List
|
| 10 |
+
from langchain_core.tools import tool
|
| 11 |
+
|
| 12 |
+
from config import MAX_FILE_SIZE, TOOL_TIMEOUT
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ========================================
|
| 16 |
+
# PDF 解析工具
|
| 17 |
+
# ========================================
|
| 18 |
+
|
| 19 |
+
@tool
|
| 20 |
+
def parse_pdf(file_path: str, page_numbers: str = "all") -> str:
|
| 21 |
+
"""
|
| 22 |
+
解析 PDF 文件,提取文本内容。
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
file_path: PDF 文件路径
|
| 26 |
+
page_numbers: 页码范围
|
| 27 |
+
- "all": 所有页面
|
| 28 |
+
- "1": 第 1 页
|
| 29 |
+
- "1-5": 第 1 到 5 页
|
| 30 |
+
- "1,3,5": 第 1、3、5 页
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
PDF 文本内容
|
| 34 |
+
|
| 35 |
+
限制:
|
| 36 |
+
- 扫描版 PDF 需配合 OCR
|
| 37 |
+
- 复杂排版可能顺序错乱
|
| 38 |
+
"""
|
| 39 |
+
try:
|
| 40 |
+
import pdfplumber
|
| 41 |
+
except ImportError:
|
| 42 |
+
return "PDF 解析不可用:请安装 pdfplumber 库 (pip install pdfplumber)"
|
| 43 |
+
|
| 44 |
+
if not os.path.exists(file_path):
|
| 45 |
+
return f"文件不存在: {file_path}"
|
| 46 |
+
|
| 47 |
+
if not file_path.lower().endswith('.pdf'):
|
| 48 |
+
return f"不是 PDF 文件: {file_path}"
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
with pdfplumber.open(file_path) as pdf:
|
| 52 |
+
total_pages = len(pdf.pages)
|
| 53 |
+
|
| 54 |
+
# 解析页码范围
|
| 55 |
+
if page_numbers == "all":
|
| 56 |
+
pages_to_read = range(total_pages)
|
| 57 |
+
elif "-" in page_numbers:
|
| 58 |
+
start, end = map(int, page_numbers.split("-"))
|
| 59 |
+
pages_to_read = range(start - 1, min(end, total_pages))
|
| 60 |
+
elif "," in page_numbers:
|
| 61 |
+
pages_to_read = [int(p) - 1 for p in page_numbers.split(",")]
|
| 62 |
+
pages_to_read = [p for p in pages_to_read if 0 <= p < total_pages]
|
| 63 |
+
else:
|
| 64 |
+
page_num = int(page_numbers) - 1
|
| 65 |
+
if 0 <= page_num < total_pages:
|
| 66 |
+
pages_to_read = [page_num]
|
| 67 |
+
else:
|
| 68 |
+
return f"页码超出范围,PDF 共有 {total_pages} 页"
|
| 69 |
+
|
| 70 |
+
# 提取文本
|
| 71 |
+
text_parts = []
|
| 72 |
+
for i in pages_to_read:
|
| 73 |
+
page = pdf.pages[i]
|
| 74 |
+
text = page.extract_text()
|
| 75 |
+
if text:
|
| 76 |
+
text_parts.append(f"--- 第 {i + 1} 页 ---\n{text}")
|
| 77 |
+
|
| 78 |
+
if not text_parts:
|
| 79 |
+
return "PDF 中没有提取到文本内容(可能是扫描版,请尝试使用 OCR)"
|
| 80 |
+
|
| 81 |
+
result = "\n\n".join(text_parts)
|
| 82 |
+
|
| 83 |
+
# 限制长度
|
| 84 |
+
if len(result) > MAX_FILE_SIZE:
|
| 85 |
+
return result[:MAX_FILE_SIZE] + f"\n\n... [内容已截断,共 {len(result)} 字符]"
|
| 86 |
+
|
| 87 |
+
return result
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
return f"PDF 解析出错: {type(e).__name__}: {str(e)}"
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ========================================
|
| 94 |
+
# Excel 解析工具
|
| 95 |
+
# ========================================
|
| 96 |
+
|
| 97 |
+
@tool
|
| 98 |
+
def parse_excel(file_path: str, sheet_name: str = None, max_rows: int = 100) -> str:
|
| 99 |
+
"""
|
| 100 |
+
解析 Excel 文件内容。
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
file_path: Excel 文件路径(.xlsx, .xls)
|
| 104 |
+
sheet_name: 工作表名称,默认第一个
|
| 105 |
+
max_rows: 最大读取行数,默认 100
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
表格内容(Markdown 格式)
|
| 109 |
+
"""
|
| 110 |
+
try:
|
| 111 |
+
import pandas as pd
|
| 112 |
+
except ImportError:
|
| 113 |
+
return "Excel 解析不可用:请安装 pandas 和 openpyxl 库"
|
| 114 |
+
|
| 115 |
+
if not os.path.exists(file_path):
|
| 116 |
+
return f"文件不存在: {file_path}"
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
# 读取 Excel
|
| 120 |
+
if sheet_name:
|
| 121 |
+
df = pd.read_excel(file_path, sheet_name=sheet_name, nrows=max_rows)
|
| 122 |
+
else:
|
| 123 |
+
df = pd.read_excel(file_path, nrows=max_rows)
|
| 124 |
+
|
| 125 |
+
# 获取工作表信息
|
| 126 |
+
excel_file = pd.ExcelFile(file_path)
|
| 127 |
+
sheet_names = excel_file.sheet_names
|
| 128 |
+
|
| 129 |
+
# 构建输出
|
| 130 |
+
output = []
|
| 131 |
+
output.append(f"工作表: {sheet_names}")
|
| 132 |
+
output.append(f"当前读取: {sheet_name or sheet_names[0]}")
|
| 133 |
+
output.append(f"数据形状: {df.shape[0]} 行 x {df.shape[1]} 列")
|
| 134 |
+
output.append("")
|
| 135 |
+
|
| 136 |
+
# 转换为 Markdown 表格
|
| 137 |
+
output.append(df.to_markdown(index=False))
|
| 138 |
+
|
| 139 |
+
result = "\n".join(output)
|
| 140 |
+
|
| 141 |
+
# 限制长度
|
| 142 |
+
if len(result) > MAX_FILE_SIZE:
|
| 143 |
+
return result[:MAX_FILE_SIZE] + f"\n\n... [内容已截断]"
|
| 144 |
+
|
| 145 |
+
return result
|
| 146 |
+
|
| 147 |
+
except Exception as e:
|
| 148 |
+
return f"Excel 解析出错: {type(e).__name__}: {str(e)}"
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# ========================================
|
| 152 |
+
# 图片 OCR 工具
|
| 153 |
+
# ========================================
|
| 154 |
+
|
| 155 |
+
@tool
|
| 156 |
+
def image_ocr(file_path: str, language: str = "eng") -> str:
|
| 157 |
+
"""
|
| 158 |
+
对图片进行 OCR 文字识别。
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
file_path: 图片路径(png/jpg/jpeg/bmp/gif/tiff)
|
| 162 |
+
language: 识别语言
|
| 163 |
+
- "eng": 英文
|
| 164 |
+
- "chi_sim": 简体中���
|
| 165 |
+
- "chi_tra": 繁体中文
|
| 166 |
+
- "eng+chi_sim": 多语言
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
识别出的文字
|
| 170 |
+
|
| 171 |
+
注意:
|
| 172 |
+
需要安装 Tesseract OCR 引擎
|
| 173 |
+
"""
|
| 174 |
+
try:
|
| 175 |
+
import pytesseract
|
| 176 |
+
from PIL import Image
|
| 177 |
+
except ImportError:
|
| 178 |
+
return "OCR 不可用:请安装 pytesseract 和 Pillow 库"
|
| 179 |
+
|
| 180 |
+
if not os.path.exists(file_path):
|
| 181 |
+
return f"文件不存在: {file_path}"
|
| 182 |
+
|
| 183 |
+
# 检查文件格式
|
| 184 |
+
valid_extensions = {'.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff', '.tif'}
|
| 185 |
+
ext = os.path.splitext(file_path)[1].lower()
|
| 186 |
+
if ext not in valid_extensions:
|
| 187 |
+
return f"不支持的图片格式: {ext},支持: {', '.join(valid_extensions)}"
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
# 打开图片
|
| 191 |
+
image = Image.open(file_path)
|
| 192 |
+
|
| 193 |
+
# 执行 OCR
|
| 194 |
+
text = pytesseract.image_to_string(image, lang=language)
|
| 195 |
+
|
| 196 |
+
if not text.strip():
|
| 197 |
+
return "图片中没有识别到文字内容"
|
| 198 |
+
|
| 199 |
+
# 清理文本
|
| 200 |
+
text = text.strip()
|
| 201 |
+
|
| 202 |
+
# 限制长度
|
| 203 |
+
if len(text) > MAX_FILE_SIZE:
|
| 204 |
+
return text[:MAX_FILE_SIZE] + f"\n\n... [内容已截断]"
|
| 205 |
+
|
| 206 |
+
return text
|
| 207 |
+
|
| 208 |
+
except pytesseract.TesseractNotFoundError:
|
| 209 |
+
return "OCR 引擎未安装:请安装 Tesseract OCR (https://github.com/tesseract-ocr/tesseract)"
|
| 210 |
+
except Exception as e:
|
| 211 |
+
return f"OCR 识别出错: {type(e).__name__}: {str(e)}"
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# ========================================
|
| 215 |
+
# 音频转写工具
|
| 216 |
+
# ========================================
|
| 217 |
+
|
| 218 |
+
@tool
|
| 219 |
+
def transcribe_audio(file_path: str, language: str = "auto") -> str:
|
| 220 |
+
"""
|
| 221 |
+
将音频文件转写为文字。
|
| 222 |
+
|
| 223 |
+
使用 OpenAI Whisper 模型进行转写。
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
file_path: 音频路径(mp3/wav/m4a/ogg/flac)
|
| 227 |
+
language: 语言代码
|
| 228 |
+
- "auto": 自动检测
|
| 229 |
+
- "en": 英文
|
| 230 |
+
- "zh": 中文
|
| 231 |
+
- "ja": 日文
|
| 232 |
+
等等
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
转写的文字内容
|
| 236 |
+
"""
|
| 237 |
+
try:
|
| 238 |
+
import whisper
|
| 239 |
+
except ImportError:
|
| 240 |
+
return "音频转写不可用:请安装 openai-whisper 库 (pip install openai-whisper)"
|
| 241 |
+
|
| 242 |
+
if not os.path.exists(file_path):
|
| 243 |
+
return f"文件不存在: {file_path}"
|
| 244 |
+
|
| 245 |
+
# 检查文件格式
|
| 246 |
+
valid_extensions = {'.mp3', '.wav', '.m4a', '.ogg', '.flac', '.wma', '.aac'}
|
| 247 |
+
ext = os.path.splitext(file_path)[1].lower()
|
| 248 |
+
if ext not in valid_extensions:
|
| 249 |
+
return f"不支持的音频格式: {ext},支持: {', '.join(valid_extensions)}"
|
| 250 |
+
|
| 251 |
+
try:
|
| 252 |
+
# 加载模型(使用 base 模型平衡速度和准确性)
|
| 253 |
+
model = whisper.load_model("base")
|
| 254 |
+
|
| 255 |
+
# 转写配置
|
| 256 |
+
options = {}
|
| 257 |
+
if language != "auto":
|
| 258 |
+
options["language"] = language
|
| 259 |
+
|
| 260 |
+
# 执行转写
|
| 261 |
+
result = model.transcribe(file_path, **options)
|
| 262 |
+
|
| 263 |
+
text = result.get("text", "").strip()
|
| 264 |
+
|
| 265 |
+
if not text:
|
| 266 |
+
return "音频中没有识别到语音内容"
|
| 267 |
+
|
| 268 |
+
# 添加语言检测信息
|
| 269 |
+
detected_lang = result.get("language", "unknown")
|
| 270 |
+
output = f"[检测到语言: {detected_lang}]\n\n{text}"
|
| 271 |
+
|
| 272 |
+
# 限制长度
|
| 273 |
+
if len(output) > MAX_FILE_SIZE:
|
| 274 |
+
return output[:MAX_FILE_SIZE] + f"\n\n... [内容已截断]"
|
| 275 |
+
|
| 276 |
+
return output
|
| 277 |
+
|
| 278 |
+
except Exception as e:
|
| 279 |
+
return f"音频转写出错: {type(e).__name__}: {str(e)}"
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
# ========================================
|
| 283 |
+
# 视觉分析工具(可选,基于多模态 LLM)
|
| 284 |
+
# ========================================
|
| 285 |
+
|
| 286 |
+
@tool
|
| 287 |
+
def analyze_image(file_path: str, question: str = "请描述这张图片的内容") -> str:
|
| 288 |
+
"""
|
| 289 |
+
使用多模态 LLM 分析图片内容。
|
| 290 |
+
|
| 291 |
+
适用于:
|
| 292 |
+
- 图片内容描述
|
| 293 |
+
- 图表数据提取
|
| 294 |
+
- 图片中的文字识别(比 OCR 更智能)
|
| 295 |
+
|
| 296 |
+
Args:
|
| 297 |
+
file_path: 图片路径
|
| 298 |
+
question: 关于图片的问题
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
LLM 对图片的分析结果
|
| 302 |
+
"""
|
| 303 |
+
try:
|
| 304 |
+
import base64
|
| 305 |
+
from langchain_openai import ChatOpenAI
|
| 306 |
+
from langchain_core.messages import HumanMessage
|
| 307 |
+
from config import OPENAI_BASE_URL, OPENAI_API_KEY, MODEL
|
| 308 |
+
except ImportError:
|
| 309 |
+
return "图片分析不可用:缺少必要的依赖"
|
| 310 |
+
|
| 311 |
+
if not os.path.exists(file_path):
|
| 312 |
+
return f"文件不存在: {file_path}"
|
| 313 |
+
|
| 314 |
+
try:
|
| 315 |
+
# 读取图片并编码
|
| 316 |
+
with open(file_path, "rb") as f:
|
| 317 |
+
image_data = base64.b64encode(f.read()).decode("utf-8")
|
| 318 |
+
|
| 319 |
+
# 检测图片格式
|
| 320 |
+
ext = os.path.splitext(file_path)[1].lower()
|
| 321 |
+
mime_types = {
|
| 322 |
+
'.png': 'image/png',
|
| 323 |
+
'.jpg': 'image/jpeg',
|
| 324 |
+
'.jpeg': 'image/jpeg',
|
| 325 |
+
'.gif': 'image/gif',
|
| 326 |
+
'.webp': 'image/webp',
|
| 327 |
+
}
|
| 328 |
+
mime_type = mime_types.get(ext, 'image/png')
|
| 329 |
+
|
| 330 |
+
# 构建多模态消息
|
| 331 |
+
message = HumanMessage(
|
| 332 |
+
content=[
|
| 333 |
+
{"type": "text", "text": question},
|
| 334 |
+
{
|
| 335 |
+
"type": "image_url",
|
| 336 |
+
"image_url": {
|
| 337 |
+
"url": f"data:{mime_type};base64,{image_data}"
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
]
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# 调用 LLM(添加超时保护)
|
| 344 |
+
llm = ChatOpenAI(
|
| 345 |
+
model=MODEL,
|
| 346 |
+
base_url=OPENAI_BASE_URL,
|
| 347 |
+
api_key=OPENAI_API_KEY,
|
| 348 |
+
timeout=60, # 60秒超时
|
| 349 |
+
max_retries=1,
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
response = llm.invoke([message])
|
| 353 |
+
return response.content
|
| 354 |
+
|
| 355 |
+
except Exception as e:
|
| 356 |
+
return f"图片分析出错: {type(e).__name__}: {str(e)}"
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
# ========================================
|
| 360 |
+
# 导出扩展工具列表
|
| 361 |
+
# ========================================
|
| 362 |
+
EXTENSION_TOOLS = [
|
| 363 |
+
parse_pdf,
|
| 364 |
+
parse_excel,
|
| 365 |
+
image_ocr,
|
| 366 |
+
transcribe_audio,
|
| 367 |
+
analyze_image,
|
| 368 |
+
]
|
rag.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RAG 模块 - GAIA 知识库检索增强生成
|
| 3 |
+
基于 GAIA metadata 构建预置知识库,提供问题解题参考
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
from typing import Optional, List
|
| 10 |
+
|
| 11 |
+
from langchain_core.documents import Document
|
| 12 |
+
from langchain_core.tools import tool
|
| 13 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 14 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 15 |
+
from langchain_openai import ChatOpenAI
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from langchain_community.vectorstores import FAISS
|
| 19 |
+
except ImportError:
|
| 20 |
+
from langchain.vectorstores import FAISS
|
| 21 |
+
|
| 22 |
+
from config import (
|
| 23 |
+
OPENAI_BASE_URL,
|
| 24 |
+
OPENAI_API_KEY,
|
| 25 |
+
MODEL,
|
| 26 |
+
TEMPERATURE,
|
| 27 |
+
RAG_PERSIST_DIR,
|
| 28 |
+
RAG_CSV_PATH,
|
| 29 |
+
RAG_EMBEDDING_MODEL,
|
| 30 |
+
RAG_TOP_K,
|
| 31 |
+
DEBUG,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# 使用本地 HuggingFace Embedding(免费,无需 API)
|
| 35 |
+
try:
|
| 36 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 37 |
+
USE_LOCAL_EMBEDDING = True
|
| 38 |
+
except ImportError:
|
| 39 |
+
try:
|
| 40 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 41 |
+
USE_LOCAL_EMBEDDING = True
|
| 42 |
+
except ImportError:
|
| 43 |
+
from langchain_openai import OpenAIEmbeddings
|
| 44 |
+
USE_LOCAL_EMBEDDING = False
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ========================================
|
| 48 |
+
# RAG Manager
|
| 49 |
+
# ========================================
|
| 50 |
+
|
| 51 |
+
class GAIARAGManager:
|
| 52 |
+
"""
|
| 53 |
+
GAIA RAG 管理器
|
| 54 |
+
|
| 55 |
+
功能:
|
| 56 |
+
- 从 GAIA metadata 构建知识库
|
| 57 |
+
- 检索相似问题,提供解题参考
|
| 58 |
+
- 不直接返回答案,只提供解题步骤和工具建议
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(self, persist_dir: str = RAG_PERSIST_DIR):
|
| 62 |
+
self.persist_dir = persist_dir
|
| 63 |
+
|
| 64 |
+
# 延迟初始化(首次使用时加载)
|
| 65 |
+
self._embeddings = None
|
| 66 |
+
self._llm = None
|
| 67 |
+
self._vectorstore = None
|
| 68 |
+
self._initialized = False
|
| 69 |
+
|
| 70 |
+
# 文本分割器(轻量级,可以立即初始化)
|
| 71 |
+
self.text_splitter = RecursiveCharacterTextSplitter(
|
| 72 |
+
chunk_size=1000,
|
| 73 |
+
chunk_overlap=200,
|
| 74 |
+
separators=["\n\n", "\n", "。", ".", " ", ""]
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# RAG Prompt(用于生成解题建议)
|
| 78 |
+
self.rag_prompt = ChatPromptTemplate.from_messages([
|
| 79 |
+
("system", """你是一个解题策略顾问。基于相似问题的解题经验,为新问题提供解题建议。
|
| 80 |
+
|
| 81 |
+
注意:
|
| 82 |
+
1. 只提供解题思路和工具建议,不要直接给出答案
|
| 83 |
+
2. 参考历史问题的解题步骤,但要根据新问题调整
|
| 84 |
+
3. 如果相似问题不太相关,明确说明
|
| 85 |
+
|
| 86 |
+
相似问题参考:
|
| 87 |
+
{context}"""),
|
| 88 |
+
("human", "新问题:{question}\n\n请给出解题建议:")
|
| 89 |
+
])
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def embeddings(self):
|
| 93 |
+
"""延迟加载嵌入模型"""
|
| 94 |
+
if self._embeddings is None:
|
| 95 |
+
if DEBUG:
|
| 96 |
+
print("[RAG] 正在加载嵌入模型...")
|
| 97 |
+
if USE_LOCAL_EMBEDDING:
|
| 98 |
+
self._embeddings = HuggingFaceEmbeddings(
|
| 99 |
+
model_name=RAG_EMBEDDING_MODEL,
|
| 100 |
+
model_kwargs={'device': 'cpu'},
|
| 101 |
+
encode_kwargs={'normalize_embeddings': True}
|
| 102 |
+
)
|
| 103 |
+
else:
|
| 104 |
+
self._embeddings = OpenAIEmbeddings(
|
| 105 |
+
base_url=OPENAI_BASE_URL,
|
| 106 |
+
api_key=OPENAI_API_KEY,
|
| 107 |
+
)
|
| 108 |
+
if DEBUG:
|
| 109 |
+
print("[RAG] 嵌入模型加载完成")
|
| 110 |
+
return self._embeddings
|
| 111 |
+
|
| 112 |
+
@property
|
| 113 |
+
def llm(self):
|
| 114 |
+
"""延迟加载 LLM"""
|
| 115 |
+
if self._llm is None:
|
| 116 |
+
self._llm = ChatOpenAI(
|
| 117 |
+
model=MODEL,
|
| 118 |
+
temperature=TEMPERATURE,
|
| 119 |
+
base_url=OPENAI_BASE_URL,
|
| 120 |
+
api_key=OPENAI_API_KEY,
|
| 121 |
+
)
|
| 122 |
+
return self._llm
|
| 123 |
+
|
| 124 |
+
@property
|
| 125 |
+
def vectorstore(self) -> Optional[FAISS]:
|
| 126 |
+
"""延迟加载向量存储"""
|
| 127 |
+
if not self._initialized:
|
| 128 |
+
self._load_index()
|
| 129 |
+
self._initialized = True
|
| 130 |
+
return self._vectorstore
|
| 131 |
+
|
| 132 |
+
@vectorstore.setter
|
| 133 |
+
def vectorstore(self, value):
|
| 134 |
+
self._vectorstore = value
|
| 135 |
+
|
| 136 |
+
def _load_index(self):
|
| 137 |
+
"""加载已有的向量索引"""
|
| 138 |
+
index_file = os.path.join(self.persist_dir, "index.faiss")
|
| 139 |
+
if os.path.exists(index_file):
|
| 140 |
+
try:
|
| 141 |
+
self.vectorstore = FAISS.load_local(
|
| 142 |
+
self.persist_dir,
|
| 143 |
+
self.embeddings,
|
| 144 |
+
allow_dangerous_deserialization=True
|
| 145 |
+
)
|
| 146 |
+
if DEBUG:
|
| 147 |
+
print(f"[RAG] 已加载索引: {self.persist_dir}")
|
| 148 |
+
except Exception as e:
|
| 149 |
+
if DEBUG:
|
| 150 |
+
print(f"[RAG] 加载索引失败: {e}")
|
| 151 |
+
self.vectorstore = None
|
| 152 |
+
else:
|
| 153 |
+
# 如果没有索引,尝试从默认 CSV 初始化
|
| 154 |
+
self._init_from_csv()
|
| 155 |
+
|
| 156 |
+
def _init_from_csv(self):
|
| 157 |
+
"""从默认 CSV 文件初始化向量库"""
|
| 158 |
+
# 检查多个可能的路径
|
| 159 |
+
possible_paths = [
|
| 160 |
+
RAG_CSV_PATH,
|
| 161 |
+
os.path.join(os.path.dirname(__file__), RAG_CSV_PATH),
|
| 162 |
+
os.path.join(os.path.dirname(__file__), "data_clean.csv"),
|
| 163 |
+
]
|
| 164 |
+
|
| 165 |
+
for csv_path in possible_paths:
|
| 166 |
+
if os.path.exists(csv_path):
|
| 167 |
+
if DEBUG:
|
| 168 |
+
print(f"[RAG] 从 CSV 初始化: {csv_path}")
|
| 169 |
+
self.load_csv(csv_path)
|
| 170 |
+
return
|
| 171 |
+
|
| 172 |
+
if DEBUG:
|
| 173 |
+
print("[RAG] 未找到 CSV 文件,知识库为空")
|
| 174 |
+
|
| 175 |
+
def load_csv(self, csv_path: str):
|
| 176 |
+
"""
|
| 177 |
+
从 CSV 文件加载文档
|
| 178 |
+
|
| 179 |
+
CSV 格式:
|
| 180 |
+
- content: 问题文本(用于 embedding)
|
| 181 |
+
- metadata: JSON 格式的元数据(answer, steps, tools, has_file)
|
| 182 |
+
"""
|
| 183 |
+
if not os.path.exists(csv_path):
|
| 184 |
+
raise FileNotFoundError(f"CSV 文件不存在: {csv_path}")
|
| 185 |
+
|
| 186 |
+
documents = []
|
| 187 |
+
with open(csv_path, newline="", encoding="utf-8") as f:
|
| 188 |
+
reader = csv.DictReader(f)
|
| 189 |
+
for row in reader:
|
| 190 |
+
content = row.get("content", "")
|
| 191 |
+
if not content:
|
| 192 |
+
continue
|
| 193 |
+
|
| 194 |
+
# 解析 metadata
|
| 195 |
+
try:
|
| 196 |
+
metadata = json.loads(row.get("metadata", "{}"))
|
| 197 |
+
except json.JSONDecodeError:
|
| 198 |
+
metadata = {}
|
| 199 |
+
|
| 200 |
+
metadata["csv_source"] = csv_path
|
| 201 |
+
documents.append(Document(page_content=content, metadata=metadata))
|
| 202 |
+
|
| 203 |
+
if not documents:
|
| 204 |
+
if DEBUG:
|
| 205 |
+
print("[RAG] CSV 中没有有效文档")
|
| 206 |
+
return
|
| 207 |
+
|
| 208 |
+
# 构建向量库
|
| 209 |
+
self.vectorstore = FAISS.from_documents(documents, self.embeddings)
|
| 210 |
+
|
| 211 |
+
# 持久化
|
| 212 |
+
os.makedirs(self.persist_dir, exist_ok=True)
|
| 213 |
+
self.vectorstore.save_local(self.persist_dir)
|
| 214 |
+
|
| 215 |
+
if DEBUG:
|
| 216 |
+
print(f"[RAG] 已加载 {len(documents)} 条文档")
|
| 217 |
+
|
| 218 |
+
def retrieve(self, query: str, k: int = RAG_TOP_K) -> List[Document]:
|
| 219 |
+
"""
|
| 220 |
+
检索相关文档
|
| 221 |
+
|
| 222 |
+
Args:
|
| 223 |
+
query: 查询文本
|
| 224 |
+
k: 返回文档数量
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
相关文档列表
|
| 228 |
+
"""
|
| 229 |
+
if self.vectorstore is None:
|
| 230 |
+
return []
|
| 231 |
+
|
| 232 |
+
return self.vectorstore.similarity_search(query, k=k)
|
| 233 |
+
|
| 234 |
+
def retrieve_with_scores(self, query: str, k: int = RAG_TOP_K) -> List[tuple]:
|
| 235 |
+
"""
|
| 236 |
+
检索相关文档(带相似度分数)
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
query: 查询文本
|
| 240 |
+
k: 返回文档数量
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
[(doc, score), ...] 列表
|
| 244 |
+
"""
|
| 245 |
+
if self.vectorstore is None:
|
| 246 |
+
return []
|
| 247 |
+
|
| 248 |
+
return self.vectorstore.similarity_search_with_score(query, k=k)
|
| 249 |
+
|
| 250 |
+
def get_solving_hints(self, question: str, k: int = RAG_TOP_K, score_threshold: float = 1.5) -> str:
|
| 251 |
+
"""
|
| 252 |
+
获取解题提示
|
| 253 |
+
|
| 254 |
+
根据相似问题,提取解题步骤和工具建议
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
question: 新问题
|
| 258 |
+
k: 检索数量
|
| 259 |
+
score_threshold: 相似度阈值(越小越相似,FAISS L2距离)
|
| 260 |
+
|
| 261 |
+
Returns:
|
| 262 |
+
解题提示文本
|
| 263 |
+
"""
|
| 264 |
+
docs_with_scores = self.retrieve_with_scores(question, k=k)
|
| 265 |
+
|
| 266 |
+
if not docs_with_scores:
|
| 267 |
+
return ""
|
| 268 |
+
|
| 269 |
+
# 过滤低相似度结果
|
| 270 |
+
relevant_docs = [(doc, score) for doc, score in docs_with_scores if score < score_threshold]
|
| 271 |
+
|
| 272 |
+
if not relevant_docs:
|
| 273 |
+
return ""
|
| 274 |
+
|
| 275 |
+
hints = []
|
| 276 |
+
for i, (doc, score) in enumerate(relevant_docs, 1):
|
| 277 |
+
meta = doc.metadata
|
| 278 |
+
steps = meta.get('steps', '')
|
| 279 |
+
tools = meta.get('tools', '')
|
| 280 |
+
has_file = meta.get('has_file', False)
|
| 281 |
+
|
| 282 |
+
hint_parts = [f"### 参考 {i} (相似度: {1/(1+score):.2f})"]
|
| 283 |
+
hint_parts.append(f"**相似问题**: {doc.page_content[:100]}...")
|
| 284 |
+
|
| 285 |
+
if steps:
|
| 286 |
+
hint_parts.append(f"**解题步骤**: {steps[:300]}...")
|
| 287 |
+
if tools:
|
| 288 |
+
hint_parts.append(f"**推荐工具**: {tools}")
|
| 289 |
+
if has_file:
|
| 290 |
+
hint_parts.append("**注意**: 该问题有附件文件")
|
| 291 |
+
|
| 292 |
+
hints.append("\n".join(hint_parts))
|
| 293 |
+
|
| 294 |
+
return "\n\n".join(hints)
|
| 295 |
+
|
| 296 |
+
def query(self, question: str, k: int = RAG_TOP_K) -> str:
|
| 297 |
+
"""
|
| 298 |
+
RAG 查询:检索 + 生成解题建议
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
question: 用户问题
|
| 302 |
+
k: 检索文档数量
|
| 303 |
+
|
| 304 |
+
Returns:
|
| 305 |
+
解题建议
|
| 306 |
+
"""
|
| 307 |
+
# 1. 检索相关文档
|
| 308 |
+
docs = self.retrieve(question, k=k)
|
| 309 |
+
|
| 310 |
+
if not docs:
|
| 311 |
+
return "知识库中没有找到相似问题。建议直接分析问题并使用适当的工具。"
|
| 312 |
+
|
| 313 |
+
# 2. 构建上下文
|
| 314 |
+
context_parts = []
|
| 315 |
+
for i, doc in enumerate(docs, 1):
|
| 316 |
+
meta = doc.metadata
|
| 317 |
+
context_parts.append(f"""
|
| 318 |
+
[相似问题 {i}]
|
| 319 |
+
问题: {doc.page_content}
|
| 320 |
+
解题步骤: {meta.get('steps', 'N/A')}
|
| 321 |
+
使用工具: {meta.get('tools', 'N/A')}
|
| 322 |
+
有附件: {'是' if meta.get('has_file') else '否'}
|
| 323 |
+
答案格式参考: {meta.get('answer', 'N/A')[:50]}...
|
| 324 |
+
""")
|
| 325 |
+
|
| 326 |
+
context = "\n".join(context_parts)
|
| 327 |
+
|
| 328 |
+
# 3. LLM 生成建议
|
| 329 |
+
chain = self.rag_prompt | self.llm
|
| 330 |
+
response = chain.invoke({
|
| 331 |
+
"context": context,
|
| 332 |
+
"question": question
|
| 333 |
+
})
|
| 334 |
+
|
| 335 |
+
return response.content
|
| 336 |
+
|
| 337 |
+
def get_stats(self) -> dict:
|
| 338 |
+
"""获取索引统计信息"""
|
| 339 |
+
if self.vectorstore is None:
|
| 340 |
+
return {"status": "empty", "doc_count": 0}
|
| 341 |
+
|
| 342 |
+
try:
|
| 343 |
+
doc_count = self.vectorstore.index.ntotal
|
| 344 |
+
except:
|
| 345 |
+
doc_count = "unknown"
|
| 346 |
+
|
| 347 |
+
return {
|
| 348 |
+
"status": "loaded",
|
| 349 |
+
"doc_count": doc_count,
|
| 350 |
+
"persist_dir": self.persist_dir
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
# ========================================
|
| 355 |
+
# 全局实例
|
| 356 |
+
# ========================================
|
| 357 |
+
|
| 358 |
+
_rag_manager: Optional[GAIARAGManager] = None
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def get_rag_manager() -> GAIARAGManager:
|
| 362 |
+
"""获取 RAG 管理器单例"""
|
| 363 |
+
global _rag_manager
|
| 364 |
+
if _rag_manager is None:
|
| 365 |
+
_rag_manager = GAIARAGManager()
|
| 366 |
+
return _rag_manager
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def _score_to_similarity(score) -> float:
|
| 370 |
+
"""FAISS L2 距离转 [0, 1] 相似度,处理异常值"""
|
| 371 |
+
try:
|
| 372 |
+
score_f = float(score)
|
| 373 |
+
except Exception:
|
| 374 |
+
return 0.0
|
| 375 |
+
if score_f != score_f: # NaN
|
| 376 |
+
return 0.0
|
| 377 |
+
if score_f < 0.0:
|
| 378 |
+
score_f = 0.0
|
| 379 |
+
return 1.0 / (1.0 + score_f)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def rag_lookup_answer(question: str, min_similarity: float = 0.85):
|
| 383 |
+
"""
|
| 384 |
+
RAG 短路查找:高置信度匹配时直接返回答案。
|
| 385 |
+
|
| 386 |
+
Returns:
|
| 387 |
+
命中: {"answer": str, "similarity": float, "score": float, "metadata": dict}
|
| 388 |
+
未命中/异常: None
|
| 389 |
+
"""
|
| 390 |
+
if not question or not str(question).strip():
|
| 391 |
+
return None
|
| 392 |
+
try:
|
| 393 |
+
manager = get_rag_manager()
|
| 394 |
+
results = manager.retrieve_with_scores(str(question).strip(), k=1)
|
| 395 |
+
if not results:
|
| 396 |
+
return None
|
| 397 |
+
best_doc, best_score = results[0]
|
| 398 |
+
similarity = _score_to_similarity(best_score)
|
| 399 |
+
answer = (best_doc.metadata.get("answer") or "").strip()
|
| 400 |
+
if not answer:
|
| 401 |
+
return None
|
| 402 |
+
if similarity > float(min_similarity):
|
| 403 |
+
return {
|
| 404 |
+
"answer": answer,
|
| 405 |
+
"similarity": float(similarity),
|
| 406 |
+
"score": float(best_score),
|
| 407 |
+
"metadata": dict(best_doc.metadata),
|
| 408 |
+
}
|
| 409 |
+
return None
|
| 410 |
+
except Exception as e:
|
| 411 |
+
if DEBUG:
|
| 412 |
+
print(f"[RAG] rag_lookup_answer failed: {type(e).__name__}: {e}")
|
| 413 |
+
return None
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
# ========================================
|
| 417 |
+
# Agent 工具
|
| 418 |
+
# ========================================
|
| 419 |
+
|
| 420 |
+
@tool
|
| 421 |
+
def rag_query(question: str) -> str:
|
| 422 |
+
"""
|
| 423 |
+
查询知识库。如果找到高度匹配的问题,直接返回答案;否则返回解题建议。
|
| 424 |
+
|
| 425 |
+
适用于:
|
| 426 |
+
- 快速查找已知问题的答案
|
| 427 |
+
- 获取相似问题的解题思路和推荐工具
|
| 428 |
+
|
| 429 |
+
Args:
|
| 430 |
+
question: 用户问题
|
| 431 |
+
|
| 432 |
+
Returns:
|
| 433 |
+
匹配答案或解题建议
|
| 434 |
+
"""
|
| 435 |
+
manager = get_rag_manager()
|
| 436 |
+
|
| 437 |
+
# 使用带分数的检索
|
| 438 |
+
results = manager.retrieve_with_scores(question, k=3)
|
| 439 |
+
if not results:
|
| 440 |
+
return "知识库中没有找到相似问题。建议使用 web_search 等工具获取信息。"
|
| 441 |
+
|
| 442 |
+
best_doc, best_score = results[0]
|
| 443 |
+
similarity = 1 / (1 + best_score)
|
| 444 |
+
|
| 445 |
+
# 高相似度 (>0.85):直接返回答案
|
| 446 |
+
if similarity > 0.85:
|
| 447 |
+
answer = best_doc.metadata.get('answer', '')
|
| 448 |
+
if answer:
|
| 449 |
+
return f"【知识库匹配成功】相似度: {similarity:.2f}\n直接答案: {answer}\n请直接使用此答案作为最终回答。"
|
| 450 |
+
|
| 451 |
+
# 中等相似度:返回答案 + 解题参考
|
| 452 |
+
if similarity > 0.6:
|
| 453 |
+
parts = []
|
| 454 |
+
for i, (doc, score) in enumerate(results[:2], 1):
|
| 455 |
+
sim = 1 / (1 + score)
|
| 456 |
+
meta = doc.metadata
|
| 457 |
+
parts.append(
|
| 458 |
+
f"[参考 {i}] 相似度: {sim:.2f}\n"
|
| 459 |
+
f"问题: {doc.page_content[:100]}...\n"
|
| 460 |
+
f"答案: {meta.get('answer', 'N/A')}\n"
|
| 461 |
+
f"步骤: {meta.get('steps', 'N/A')[:200]}\n"
|
| 462 |
+
f"工具: {meta.get('tools', 'N/A')}"
|
| 463 |
+
)
|
| 464 |
+
return "【知识库参考】\n" + "\n---\n".join(parts)
|
| 465 |
+
|
| 466 |
+
# 低相似度:仅返回工具建议
|
| 467 |
+
return manager.query(question)
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@tool
|
| 471 |
+
def rag_retrieve(query: str) -> str:
|
| 472 |
+
"""
|
| 473 |
+
仅检索知识库中的相关文档片段,不生成建议。
|
| 474 |
+
|
| 475 |
+
用于查看原始的相似问题和解题步骤。
|
| 476 |
+
|
| 477 |
+
Args:
|
| 478 |
+
query: 检索查询
|
| 479 |
+
|
| 480 |
+
Returns:
|
| 481 |
+
相关文档片段
|
| 482 |
+
"""
|
| 483 |
+
manager = get_rag_manager()
|
| 484 |
+
docs_with_scores = manager.retrieve_with_scores(query, k=3)
|
| 485 |
+
|
| 486 |
+
if not docs_with_scores:
|
| 487 |
+
return "知识库为空或未找到相关文档。"
|
| 488 |
+
|
| 489 |
+
results = []
|
| 490 |
+
for i, (doc, score) in enumerate(docs_with_scores, 1):
|
| 491 |
+
meta = doc.metadata
|
| 492 |
+
results.append(f"""[{i}] 相似度: {1/(1+score):.2f}
|
| 493 |
+
问题: {doc.page_content[:200]}...
|
| 494 |
+
解题步骤: {meta.get('steps', 'N/A')[:200]}...
|
| 495 |
+
工具: {meta.get('tools', 'N/A')}
|
| 496 |
+
""")
|
| 497 |
+
|
| 498 |
+
return "\n---\n".join(results)
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
@tool
|
| 502 |
+
def rag_stats() -> str:
|
| 503 |
+
"""
|
| 504 |
+
获取知识库统计信息。
|
| 505 |
+
|
| 506 |
+
Returns:
|
| 507 |
+
知识库状态和文档数量
|
| 508 |
+
"""
|
| 509 |
+
manager = get_rag_manager()
|
| 510 |
+
stats = manager.get_stats()
|
| 511 |
+
return f"知识库状态: {stats['status']}, 文档数量: {stats['doc_count']}"
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
# ========================================
|
| 515 |
+
# 导出 RAG 工具
|
| 516 |
+
# ========================================
|
| 517 |
+
|
| 518 |
+
RAG_TOOLS = [rag_query, rag_retrieve, rag_stats]
|
rag_index/index.faiss
ADDED
|
Binary file (81.5 kB). View file
|
|
|
rag_index/index.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ff44a2ea9960d8c4013b553cf3a8211f849e1e259f739e2aaf3e07f0fc0b34c8
|
| 3 |
+
size 58088
|
requirements.txt
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GAIA Agent 依赖清单
|
| 2 |
+
# 安装方式: pip install -r requirements.txt
|
| 3 |
+
|
| 4 |
+
# ========================================
|
| 5 |
+
# 核心依赖(必需)
|
| 6 |
+
# ========================================
|
| 7 |
+
langgraph>=0.2.0
|
| 8 |
+
langchain-core>=0.2.0
|
| 9 |
+
langchain-openai>=0.2.0
|
| 10 |
+
langchain-community>=0.2.0
|
| 11 |
+
requests>=2.31.0
|
| 12 |
+
python-dotenv>=1.0.0
|
| 13 |
+
gradio>=4.0.0
|
| 14 |
+
|
| 15 |
+
# ========================================
|
| 16 |
+
# 搜索工具依赖
|
| 17 |
+
# ========================================
|
| 18 |
+
ddgs>=6.0.0
|
| 19 |
+
wikipedia>=1.4.0
|
| 20 |
+
tavily-python>=0.3.0
|
| 21 |
+
arxiv>=2.0.0
|
| 22 |
+
|
| 23 |
+
# ========================================
|
| 24 |
+
# RAG 依赖
|
| 25 |
+
# ========================================
|
| 26 |
+
faiss-cpu>=1.8.0
|
| 27 |
+
langchain-huggingface>=0.1.0
|
| 28 |
+
sentence-transformers>=3.0.0
|
| 29 |
+
|
| 30 |
+
# ========================================
|
| 31 |
+
# 数据处理
|
| 32 |
+
# ========================================
|
| 33 |
+
pandas>=2.0.0
|
| 34 |
+
pyarrow>=14.0.0
|
| 35 |
+
|
| 36 |
+
# ========================================
|
| 37 |
+
# 扩展工具依赖(按需安装)
|
| 38 |
+
# ========================================
|
| 39 |
+
|
| 40 |
+
# PDF 解析
|
| 41 |
+
pdfplumber>=0.10.0
|
| 42 |
+
|
| 43 |
+
# Excel 解析
|
| 44 |
+
openpyxl>=3.1.0
|
| 45 |
+
|
| 46 |
+
# 图片 OCR(需要系统安装 Tesseract)
|
| 47 |
+
# pytesseract>=0.3.10
|
| 48 |
+
# Pillow>=10.0.0
|
| 49 |
+
|
| 50 |
+
# 音频转写(需要较大的模型文件)
|
| 51 |
+
# openai-whisper>=20231117
|
tools.py
ADDED
|
@@ -0,0 +1,945 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
核心工具模块 - GAIA Agent 基础工具
|
| 3 |
+
包含:web_search, fetch_task_files, read_file, calc, run_python
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import json
|
| 9 |
+
import tempfile
|
| 10 |
+
import zipfile
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
import requests
|
| 14 |
+
from langchain_core.tools import tool
|
| 15 |
+
|
| 16 |
+
from config import (
|
| 17 |
+
SCORING_API_URL,
|
| 18 |
+
SEARCH_MAX_RESULTS,
|
| 19 |
+
MAX_FILE_SIZE,
|
| 20 |
+
TOOL_TIMEOUT,
|
| 21 |
+
TEMP_DIR,
|
| 22 |
+
TAVILY_API_KEY,
|
| 23 |
+
WIKIPEDIA_MAX_RESULTS,
|
| 24 |
+
ARXIV_MAX_RESULTS,
|
| 25 |
+
TAVILY_MAX_RESULTS,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# DuckDuckGo 搜索 (新包名: ddgs)
|
| 29 |
+
try:
|
| 30 |
+
from ddgs import DDGS
|
| 31 |
+
except ImportError:
|
| 32 |
+
try:
|
| 33 |
+
from duckduckgo_search import DDGS
|
| 34 |
+
except ImportError:
|
| 35 |
+
DDGS = None
|
| 36 |
+
|
| 37 |
+
# Wikipedia 搜索
|
| 38 |
+
try:
|
| 39 |
+
import wikipedia
|
| 40 |
+
wikipedia.set_lang("en")
|
| 41 |
+
except ImportError:
|
| 42 |
+
wikipedia = None
|
| 43 |
+
|
| 44 |
+
# Tavily 搜索
|
| 45 |
+
try:
|
| 46 |
+
from tavily import TavilyClient
|
| 47 |
+
except ImportError:
|
| 48 |
+
TavilyClient = None
|
| 49 |
+
|
| 50 |
+
# arXiv 搜索
|
| 51 |
+
try:
|
| 52 |
+
import arxiv
|
| 53 |
+
except ImportError:
|
| 54 |
+
arxiv = None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ========================================
|
| 58 |
+
# 信息获取工具
|
| 59 |
+
# ========================================
|
| 60 |
+
|
| 61 |
+
@tool
|
| 62 |
+
def web_search(query: str, max_results: int = SEARCH_MAX_RESULTS) -> str:
|
| 63 |
+
"""
|
| 64 |
+
使用 DuckDuckGo 搜索网络信息。
|
| 65 |
+
|
| 66 |
+
适用场景:
|
| 67 |
+
- 查找人物信息(生卒年、职业、成就等)
|
| 68 |
+
- 查找事件详情(时间、地点、参与者等)
|
| 69 |
+
- 查找组织/公司信息
|
| 70 |
+
- 获取最新资讯
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
query: 搜索关键词,建议使用英文
|
| 74 |
+
max_results: 返回结果数量,默认5条
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
搜索结果摘要(标题+内容+URL)
|
| 78 |
+
"""
|
| 79 |
+
if DDGS is None:
|
| 80 |
+
return "搜索服务不可用:请安装 ddgs 库 (pip install ddgs)"
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
ddgs = DDGS()
|
| 84 |
+
results = list(ddgs.text(query, max_results=max_results))
|
| 85 |
+
|
| 86 |
+
if not results:
|
| 87 |
+
return f"没有找到与 '{query}' 相关的搜索结果。"
|
| 88 |
+
|
| 89 |
+
output = []
|
| 90 |
+
for i, r in enumerate(results, 1):
|
| 91 |
+
title = r.get('title', 'N/A')
|
| 92 |
+
body = r.get('body', 'N/A')
|
| 93 |
+
url = r.get('href', 'N/A')
|
| 94 |
+
output.append(f"{i}. {title}")
|
| 95 |
+
output.append(f" {body}")
|
| 96 |
+
output.append(f" URL: {url}")
|
| 97 |
+
output.append("")
|
| 98 |
+
|
| 99 |
+
return "\n".join(output)
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
return f"搜索出错: {type(e).__name__}: {str(e)}"
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@tool
|
| 106 |
+
def wikipedia_search(query: str, max_results: int = WIKIPEDIA_MAX_RESULTS) -> str:
|
| 107 |
+
"""
|
| 108 |
+
在维基百科中搜索信息。
|
| 109 |
+
|
| 110 |
+
适用场景:
|
| 111 |
+
- 查找人物传记、历史事件
|
| 112 |
+
- 获取概念定义和详细解释
|
| 113 |
+
- 查找地理、科学、文化等百科知识
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
query: 搜索关键词,建议使用英文
|
| 117 |
+
max_results: 返回结果数量,默认2条
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
维基百科文章摘要
|
| 121 |
+
"""
|
| 122 |
+
if wikipedia is None:
|
| 123 |
+
return "Wikipedia 搜索不可用:请安装 wikipedia 库 (pip install wikipedia)"
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
# 搜索相关页面
|
| 127 |
+
search_results = wikipedia.search(query, results=max_results)
|
| 128 |
+
|
| 129 |
+
if not search_results:
|
| 130 |
+
return f"没有找到与 '{query}' 相关的维基百科文章。"
|
| 131 |
+
|
| 132 |
+
output = []
|
| 133 |
+
for i, title in enumerate(search_results, 1):
|
| 134 |
+
try:
|
| 135 |
+
# 获取页面摘要
|
| 136 |
+
page = wikipedia.page(title, auto_suggest=False)
|
| 137 |
+
summary = wikipedia.summary(title, sentences=3, auto_suggest=False)
|
| 138 |
+
output.append(f"{i}. {page.title}")
|
| 139 |
+
output.append(f" {summary}")
|
| 140 |
+
output.append(f" URL: {page.url}")
|
| 141 |
+
output.append("")
|
| 142 |
+
except wikipedia.exceptions.DisambiguationError as e:
|
| 143 |
+
# 处理歧义页面,取第一个选项
|
| 144 |
+
if e.options:
|
| 145 |
+
try:
|
| 146 |
+
page = wikipedia.page(e.options[0], auto_suggest=False)
|
| 147 |
+
summary = wikipedia.summary(e.options[0], sentences=3, auto_suggest=False)
|
| 148 |
+
output.append(f"{i}. {page.title}")
|
| 149 |
+
output.append(f" {summary}")
|
| 150 |
+
output.append(f" URL: {page.url}")
|
| 151 |
+
output.append("")
|
| 152 |
+
except:
|
| 153 |
+
output.append(f"{i}. {title} (歧义页面,可选: {', '.join(e.options[:3])})")
|
| 154 |
+
output.append("")
|
| 155 |
+
except wikipedia.exceptions.PageError:
|
| 156 |
+
continue
|
| 157 |
+
|
| 158 |
+
return "\n".join(output) if output else f"没有找到与 '{query}' 相关的详细信息。"
|
| 159 |
+
|
| 160 |
+
except Exception as e:
|
| 161 |
+
return f"Wikipedia 搜索出错: {type(e).__name__}: {str(e)}"
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@tool
|
| 165 |
+
def wikipedia_page(title: str, section: str = None) -> str:
|
| 166 |
+
"""
|
| 167 |
+
获取维基百科页面的完整内容。
|
| 168 |
+
|
| 169 |
+
当 wikipedia_search 返回的摘要不够详细时使用此工具。
|
| 170 |
+
特别适用于需要获取列表、表格、详细数据的场景(如专辑列表、获奖记录等)。
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
title: 页面标题(从 wikipedia_search 结果中获取)
|
| 174 |
+
section: 可选,指定要获取的章节名(如 "Discography", "Awards")
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
页面完整内容或指定章节内容
|
| 178 |
+
"""
|
| 179 |
+
if wikipedia is None:
|
| 180 |
+
return "Wikipedia 不可用:请安装 wikipedia 库 (pip install wikipedia)"
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
page = wikipedia.page(title, auto_suggest=False)
|
| 184 |
+
content = page.content
|
| 185 |
+
|
| 186 |
+
# 如果指定了章节,尝试提取该章节
|
| 187 |
+
if section:
|
| 188 |
+
# 查找章节(支持 == Section == 和 === Subsection === 格式)
|
| 189 |
+
section_name = section.strip()
|
| 190 |
+
heading_re = re.compile(r'^(=+)\s*(.+?)\s*\1\s*$', re.MULTILINE)
|
| 191 |
+
headings = list(heading_re.finditer(content))
|
| 192 |
+
|
| 193 |
+
# 精确匹配
|
| 194 |
+
target_idx = None
|
| 195 |
+
for i, m in enumerate(headings):
|
| 196 |
+
if m.group(2).strip().lower() == section_name.lower():
|
| 197 |
+
target_idx = i
|
| 198 |
+
break
|
| 199 |
+
|
| 200 |
+
# 模糊匹配(包含)
|
| 201 |
+
matched_label = ""
|
| 202 |
+
if target_idx is None:
|
| 203 |
+
for i, m in enumerate(headings):
|
| 204 |
+
if section_name.lower() in m.group(2).strip().lower():
|
| 205 |
+
target_idx = i
|
| 206 |
+
matched_label = " (matched)"
|
| 207 |
+
break
|
| 208 |
+
|
| 209 |
+
if target_idx is not None:
|
| 210 |
+
level = len(headings[target_idx].group(1))
|
| 211 |
+
start = headings[target_idx].end()
|
| 212 |
+
end = len(content)
|
| 213 |
+
for m in headings[target_idx + 1:]:
|
| 214 |
+
if len(m.group(1)) <= level:
|
| 215 |
+
end = m.start()
|
| 216 |
+
break
|
| 217 |
+
section_text = content[start:end].strip()
|
| 218 |
+
content = f"{headings[target_idx].group(0)}{matched_label}\n{section_text}"
|
| 219 |
+
else:
|
| 220 |
+
available = [m.group(2).strip() for m in headings][:20]
|
| 221 |
+
content = (
|
| 222 |
+
f"未找到 '{section_name}' 章节。\n\n可用章节:\n"
|
| 223 |
+
+ "\n".join(available)
|
| 224 |
+
+ f"\n\n完整内容:\n{content[:3000]}"
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# 组装输出并限制长度(保留标题与 URL)
|
| 228 |
+
output = f"Wikipedia 页面: {page.title}\nURL: {page.url}\n\n{content}"
|
| 229 |
+
if len(output) > MAX_FILE_SIZE:
|
| 230 |
+
return output[:MAX_FILE_SIZE] + f"\n\n... [内容已截断,共 {len(output)} 字符]"
|
| 231 |
+
|
| 232 |
+
return output
|
| 233 |
+
|
| 234 |
+
except wikipedia.exceptions.DisambiguationError as e:
|
| 235 |
+
options = e.options[:10]
|
| 236 |
+
return f"'{title}' 是一个歧义页面,请指定更具体的标题:\n" + "\n".join(f" - {opt}" for opt in options)
|
| 237 |
+
except wikipedia.exceptions.PageError:
|
| 238 |
+
return f"找不到标题为 '{title}' 的维基百科页面。请检查标题拼写或使用 wikipedia_search 搜索。"
|
| 239 |
+
except Exception as e:
|
| 240 |
+
return f"Wikipedia 页面获取出错: {type(e).__name__}: {str(e)}"
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@tool
|
| 244 |
+
def tavily_search(query: str, max_results: int = TAVILY_MAX_RESULTS) -> str:
|
| 245 |
+
"""
|
| 246 |
+
使用 Tavily 进行高质量网络搜索(需要 API Key)。
|
| 247 |
+
|
| 248 |
+
适用场景:
|
| 249 |
+
- 需要高质量、准确的搜索结果
|
| 250 |
+
- 查找最新新闻和实时信息
|
| 251 |
+
- 需要更智能的搜索结果排序
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
query: 搜索关键词
|
| 255 |
+
max_results: 返回结果数量,默认3条
|
| 256 |
+
|
| 257 |
+
Returns:
|
| 258 |
+
搜索结果摘要
|
| 259 |
+
"""
|
| 260 |
+
if TavilyClient is None:
|
| 261 |
+
return "Tavily 搜索不可用:请安装 tavily-python 库 (pip install tavily-python)"
|
| 262 |
+
|
| 263 |
+
if not TAVILY_API_KEY:
|
| 264 |
+
return "Tavily 搜索不可用:请在 .env 文件中设置 TAVILY_API_KEY"
|
| 265 |
+
|
| 266 |
+
try:
|
| 267 |
+
client = TavilyClient(api_key=TAVILY_API_KEY)
|
| 268 |
+
response = client.search(query, max_results=max_results)
|
| 269 |
+
|
| 270 |
+
results = response.get('results', [])
|
| 271 |
+
if not results:
|
| 272 |
+
return f"没有找到与 '{query}' 相关的搜索结果。"
|
| 273 |
+
|
| 274 |
+
output = []
|
| 275 |
+
for i, r in enumerate(results, 1):
|
| 276 |
+
title = r.get('title', 'N/A')
|
| 277 |
+
content = r.get('content', 'N/A')
|
| 278 |
+
url = r.get('url', 'N/A')
|
| 279 |
+
output.append(f"{i}. {title}")
|
| 280 |
+
output.append(f" {content[:300]}..." if len(content) > 300 else f" {content}")
|
| 281 |
+
output.append(f" URL: {url}")
|
| 282 |
+
output.append("")
|
| 283 |
+
|
| 284 |
+
return "\n".join(output)
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
return f"Tavily 搜索出错: {type(e).__name__}: {str(e)}"
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
@tool
|
| 291 |
+
def arxiv_search(query: str, max_results: int = ARXIV_MAX_RESULTS) -> str:
|
| 292 |
+
"""
|
| 293 |
+
在 arXiv 上搜索学术论文。
|
| 294 |
+
|
| 295 |
+
适用场景:
|
| 296 |
+
- 查找最新学术研究论文
|
| 297 |
+
- 搜索特定领域的科学文献
|
| 298 |
+
- 获取论文摘要和作者信息
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
query: 搜索关键词(建议使用英文学术术语)
|
| 302 |
+
max_results: 返回结果数量,默认3条
|
| 303 |
+
|
| 304 |
+
Returns:
|
| 305 |
+
论文信息(标题、作��、摘要、链接)
|
| 306 |
+
"""
|
| 307 |
+
if arxiv is None:
|
| 308 |
+
return "arXiv 搜索不可用:请安装 arxiv 库 (pip install arxiv)"
|
| 309 |
+
|
| 310 |
+
try:
|
| 311 |
+
client = arxiv.Client()
|
| 312 |
+
search = arxiv.Search(
|
| 313 |
+
query=query,
|
| 314 |
+
max_results=max_results,
|
| 315 |
+
sort_by=arxiv.SortCriterion.Relevance
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
results = list(client.results(search))
|
| 319 |
+
|
| 320 |
+
if not results:
|
| 321 |
+
return f"没有找到与 '{query}' 相关的 arXiv 论文。"
|
| 322 |
+
|
| 323 |
+
output = []
|
| 324 |
+
for i, paper in enumerate(results, 1):
|
| 325 |
+
title = paper.title
|
| 326 |
+
authors = ", ".join([a.name for a in paper.authors[:3]])
|
| 327 |
+
if len(paper.authors) > 3:
|
| 328 |
+
authors += f" 等 ({len(paper.authors)} 位作者)"
|
| 329 |
+
summary = paper.summary[:400] + "..." if len(paper.summary) > 400 else paper.summary
|
| 330 |
+
published = paper.published.strftime("%Y-%m-%d")
|
| 331 |
+
url = paper.entry_id
|
| 332 |
+
|
| 333 |
+
output.append(f"{i}. {title}")
|
| 334 |
+
output.append(f" 作者: {authors}")
|
| 335 |
+
output.append(f" 发布日期: {published}")
|
| 336 |
+
output.append(f" 摘要: {summary}")
|
| 337 |
+
output.append(f" URL: {url}")
|
| 338 |
+
output.append("")
|
| 339 |
+
|
| 340 |
+
return "\n".join(output)
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
return f"arXiv 搜索出错: {type(e).__name__}: {str(e)}"
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
# ========================================
|
| 347 |
+
# YouTube 搜索工具
|
| 348 |
+
# ========================================
|
| 349 |
+
|
| 350 |
+
@tool
|
| 351 |
+
def youtube_search(query: str, max_results: int = 3) -> str:
|
| 352 |
+
"""
|
| 353 |
+
搜索 YouTube 视频信息。
|
| 354 |
+
|
| 355 |
+
适用场景:
|
| 356 |
+
- 查找教程视频
|
| 357 |
+
- 搜索特定主题的视频内容
|
| 358 |
+
- 获取视频标题、频道和描述
|
| 359 |
+
|
| 360 |
+
Args:
|
| 361 |
+
query: 搜索关键词
|
| 362 |
+
max_results: 返回结果数量,默认3条
|
| 363 |
+
|
| 364 |
+
Returns:
|
| 365 |
+
视频信息(标题、频道、链接)
|
| 366 |
+
"""
|
| 367 |
+
try:
|
| 368 |
+
from youtube_search import YoutubeSearch
|
| 369 |
+
except ImportError:
|
| 370 |
+
# 备选方案:使用 DuckDuckGo 搜索 YouTube
|
| 371 |
+
if DDGS is None:
|
| 372 |
+
return "YouTube 搜索不可用:请安装 youtube-search-python 库 (pip install youtube-search-python)"
|
| 373 |
+
|
| 374 |
+
try:
|
| 375 |
+
ddgs = DDGS()
|
| 376 |
+
results = list(ddgs.text(f"site:youtube.com {query}", max_results=max_results))
|
| 377 |
+
|
| 378 |
+
if not results:
|
| 379 |
+
return f"没有找到与 '{query}' 相关的 YouTube 视频。"
|
| 380 |
+
|
| 381 |
+
output = []
|
| 382 |
+
for i, r in enumerate(results, 1):
|
| 383 |
+
title = r.get('title', 'N/A')
|
| 384 |
+
url = r.get('href', 'N/A')
|
| 385 |
+
output.append(f"{i}. {title}")
|
| 386 |
+
output.append(f" URL: {url}")
|
| 387 |
+
output.append("")
|
| 388 |
+
|
| 389 |
+
return "\n".join(output)
|
| 390 |
+
except Exception as e:
|
| 391 |
+
return f"YouTube 搜索出错: {type(e).__name__}: {str(e)}"
|
| 392 |
+
|
| 393 |
+
try:
|
| 394 |
+
results = YoutubeSearch(query, max_results=max_results).to_dict()
|
| 395 |
+
|
| 396 |
+
if not results:
|
| 397 |
+
return f"没有找到与 '{query}' 相关的 YouTube 视频。"
|
| 398 |
+
|
| 399 |
+
output = []
|
| 400 |
+
for i, video in enumerate(results, 1):
|
| 401 |
+
title = video.get('title', 'N/A')
|
| 402 |
+
channel = video.get('channel', 'N/A')
|
| 403 |
+
duration = video.get('duration', 'N/A')
|
| 404 |
+
views = video.get('views', 'N/A')
|
| 405 |
+
url_suffix = video.get('url_suffix', '')
|
| 406 |
+
url = f"https://youtube.com{url_suffix}" if url_suffix else 'N/A'
|
| 407 |
+
|
| 408 |
+
output.append(f"{i}. {title}")
|
| 409 |
+
output.append(f" 频道: {channel}")
|
| 410 |
+
output.append(f" 时长: {duration} | 播放量: {views}")
|
| 411 |
+
output.append(f" URL: {url}")
|
| 412 |
+
output.append("")
|
| 413 |
+
|
| 414 |
+
return "\n".join(output)
|
| 415 |
+
|
| 416 |
+
except Exception as e:
|
| 417 |
+
return f"YouTube 搜索出错: {type(e).__name__}: {str(e)}"
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
# ========================================
|
| 421 |
+
# 新闻搜索工具
|
| 422 |
+
# ========================================
|
| 423 |
+
|
| 424 |
+
@tool
|
| 425 |
+
def news_search(query: str, max_results: int = 5) -> str:
|
| 426 |
+
"""
|
| 427 |
+
搜索最新新闻资讯。
|
| 428 |
+
|
| 429 |
+
适用场景:
|
| 430 |
+
- 查找最新新闻事件
|
| 431 |
+
- 获取时事热点信息
|
| 432 |
+
- 搜索特定主题的新闻报道
|
| 433 |
+
|
| 434 |
+
Args:
|
| 435 |
+
query: 搜索关键词
|
| 436 |
+
max_results: 返回结果数量,默认5条
|
| 437 |
+
|
| 438 |
+
Returns:
|
| 439 |
+
新闻标题、来源和摘要
|
| 440 |
+
"""
|
| 441 |
+
if DDGS is None:
|
| 442 |
+
return "新闻搜索不可用:请安装 ddgs 库 (pip install ddgs)"
|
| 443 |
+
|
| 444 |
+
try:
|
| 445 |
+
ddgs = DDGS()
|
| 446 |
+
results = list(ddgs.news(query, max_results=max_results))
|
| 447 |
+
|
| 448 |
+
if not results:
|
| 449 |
+
return f"没有找到与 '{query}' 相关的新闻。"
|
| 450 |
+
|
| 451 |
+
output = []
|
| 452 |
+
for i, r in enumerate(results, 1):
|
| 453 |
+
title = r.get('title', 'N/A')
|
| 454 |
+
body = r.get('body', 'N/A')
|
| 455 |
+
source = r.get('source', 'N/A')
|
| 456 |
+
date = r.get('date', 'N/A')
|
| 457 |
+
url = r.get('url', 'N/A')
|
| 458 |
+
|
| 459 |
+
output.append(f"{i}. {title}")
|
| 460 |
+
output.append(f" 来源: {source} | 日期: {date}")
|
| 461 |
+
output.append(f" {body[:200]}..." if len(body) > 200 else f" {body}")
|
| 462 |
+
output.append(f" URL: {url}")
|
| 463 |
+
output.append("")
|
| 464 |
+
|
| 465 |
+
return "\n".join(output)
|
| 466 |
+
|
| 467 |
+
except Exception as e:
|
| 468 |
+
return f"新闻搜索出错: {type(e).__name__}: {str(e)}"
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# ========================================
|
| 472 |
+
# StackOverflow 搜索工具
|
| 473 |
+
# ========================================
|
| 474 |
+
|
| 475 |
+
@tool
|
| 476 |
+
def stackoverflow_search(query: str, max_results: int = 3) -> str:
|
| 477 |
+
"""
|
| 478 |
+
在 StackOverflow 上搜索编程问题和解答。
|
| 479 |
+
|
| 480 |
+
适用场景:
|
| 481 |
+
- 查找编程问题的解决方案
|
| 482 |
+
- 搜索代码错误的修复方法
|
| 483 |
+
- 获取技术问题的讨论
|
| 484 |
+
|
| 485 |
+
Args:
|
| 486 |
+
query: 搜索关键词(建议包含编程语言或技术栈)
|
| 487 |
+
max_results: 返回结果数量,默认3条
|
| 488 |
+
|
| 489 |
+
Returns:
|
| 490 |
+
问题标题、回答数和链接
|
| 491 |
+
"""
|
| 492 |
+
try:
|
| 493 |
+
import requests
|
| 494 |
+
|
| 495 |
+
# 使用 StackExchange API
|
| 496 |
+
api_url = "https://api.stackexchange.com/2.3/search/advanced"
|
| 497 |
+
params = {
|
| 498 |
+
"order": "desc",
|
| 499 |
+
"sort": "relevance",
|
| 500 |
+
"q": query,
|
| 501 |
+
"site": "stackoverflow",
|
| 502 |
+
"pagesize": max_results,
|
| 503 |
+
"filter": "withbody"
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
response = requests.get(api_url, params=params, timeout=TOOL_TIMEOUT)
|
| 507 |
+
response.raise_for_status()
|
| 508 |
+
data = response.json()
|
| 509 |
+
|
| 510 |
+
items = data.get('items', [])
|
| 511 |
+
|
| 512 |
+
if not items:
|
| 513 |
+
return f"没有找到与 '{query}' 相关的 StackOverflow 问题。"
|
| 514 |
+
|
| 515 |
+
output = []
|
| 516 |
+
for i, item in enumerate(items, 1):
|
| 517 |
+
title = item.get('title', 'N/A')
|
| 518 |
+
score = item.get('score', 0)
|
| 519 |
+
answer_count = item.get('answer_count', 0)
|
| 520 |
+
is_answered = "✓ 已解答" if item.get('is_answered') else "○ 待解答"
|
| 521 |
+
tags = ", ".join(item.get('tags', [])[:5])
|
| 522 |
+
url = item.get('link', 'N/A')
|
| 523 |
+
|
| 524 |
+
output.append(f"{i}. {title}")
|
| 525 |
+
output.append(f" {is_answered} | 得分: {score} | 回答数: {answer_count}")
|
| 526 |
+
output.append(f" 标签: {tags}")
|
| 527 |
+
output.append(f" URL: {url}")
|
| 528 |
+
output.append("")
|
| 529 |
+
|
| 530 |
+
return "\n".join(output)
|
| 531 |
+
|
| 532 |
+
except Exception as e:
|
| 533 |
+
return f"StackOverflow 搜索出错: {type(e).__name__}: {str(e)}"
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
# ========================================
|
| 537 |
+
# Google 搜索工具
|
| 538 |
+
# ========================================
|
| 539 |
+
|
| 540 |
+
@tool
|
| 541 |
+
def google_search(query: str, max_results: int = 5) -> str:
|
| 542 |
+
"""
|
| 543 |
+
使用 Google 搜索网络信息(通过 DuckDuckGo 代理)。
|
| 544 |
+
|
| 545 |
+
适用场景:
|
| 546 |
+
- 综合网络搜索
|
| 547 |
+
- 查找官方网站和权威来源
|
| 548 |
+
- 获取多样化的搜索结果
|
| 549 |
+
|
| 550 |
+
Args:
|
| 551 |
+
query: 搜索关键词
|
| 552 |
+
max_results: 返回结果数量,默认5条
|
| 553 |
+
|
| 554 |
+
Returns:
|
| 555 |
+
搜索结果(标题+摘要+URL)
|
| 556 |
+
|
| 557 |
+
注意:
|
| 558 |
+
由于 Google API 限制,此工具通过 DuckDuckGo 实现类似功能
|
| 559 |
+
"""
|
| 560 |
+
# 复用 DuckDuckGo 搜索,但添加 Google 特定的搜索词
|
| 561 |
+
if DDGS is None:
|
| 562 |
+
return "Google 搜索不可用:请安装 ddgs 库 (pip install ddgs)"
|
| 563 |
+
|
| 564 |
+
try:
|
| 565 |
+
ddgs = DDGS()
|
| 566 |
+
results = list(ddgs.text(query, max_results=max_results))
|
| 567 |
+
|
| 568 |
+
if not results:
|
| 569 |
+
return f"没有找到与 '{query}' 相关的搜索结果。"
|
| 570 |
+
|
| 571 |
+
output = []
|
| 572 |
+
for i, r in enumerate(results, 1):
|
| 573 |
+
title = r.get('title', 'N/A')
|
| 574 |
+
body = r.get('body', 'N/A')
|
| 575 |
+
url = r.get('href', 'N/A')
|
| 576 |
+
output.append(f"{i}. {title}")
|
| 577 |
+
output.append(f" {body}")
|
| 578 |
+
output.append(f" URL: {url}")
|
| 579 |
+
output.append("")
|
| 580 |
+
|
| 581 |
+
return "\n".join(output)
|
| 582 |
+
|
| 583 |
+
except Exception as e:
|
| 584 |
+
return f"Google 搜索出错: {type(e).__name__}: {str(e)}"
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
# ========================================
|
| 588 |
+
# 文件处理工具
|
| 589 |
+
# ========================================
|
| 590 |
+
|
| 591 |
+
@tool
|
| 592 |
+
def fetch_task_files(task_id: str) -> str:
|
| 593 |
+
"""
|
| 594 |
+
从评分服务器下载任务相关的附件文件。
|
| 595 |
+
|
| 596 |
+
当问题涉及附件时必须先调用此工具下载文件,然后使用 read_file 或其他工具读取。
|
| 597 |
+
|
| 598 |
+
Args:
|
| 599 |
+
task_id: 任务 ID(从问题中获取)
|
| 600 |
+
|
| 601 |
+
Returns:
|
| 602 |
+
下载文件的本地路径,或错误信息
|
| 603 |
+
"""
|
| 604 |
+
try:
|
| 605 |
+
url = f"{SCORING_API_URL}/files/{task_id}"
|
| 606 |
+
response = requests.get(url, timeout=TOOL_TIMEOUT)
|
| 607 |
+
|
| 608 |
+
if response.status_code == 404:
|
| 609 |
+
return "该任务没有附件文件。"
|
| 610 |
+
|
| 611 |
+
response.raise_for_status()
|
| 612 |
+
|
| 613 |
+
# 从 Content-Disposition 获取文件名
|
| 614 |
+
content_disp = response.headers.get("Content-Disposition", "")
|
| 615 |
+
filename_match = re.search(r'filename="?([^";\n]+)"?', content_disp)
|
| 616 |
+
filename = filename_match.group(1) if filename_match else f"task_{task_id}_file"
|
| 617 |
+
|
| 618 |
+
# 保存到临时目录
|
| 619 |
+
file_path = TEMP_DIR / filename
|
| 620 |
+
|
| 621 |
+
with open(file_path, "wb") as f:
|
| 622 |
+
f.write(response.content)
|
| 623 |
+
|
| 624 |
+
# 返回文件信息和使用建议
|
| 625 |
+
file_size = len(response.content)
|
| 626 |
+
file_ext = os.path.splitext(filename)[1].lower()
|
| 627 |
+
|
| 628 |
+
# 根据文件类型给出下一步建议
|
| 629 |
+
next_step_hint = ""
|
| 630 |
+
if file_ext in ['.xlsx', '.xls']:
|
| 631 |
+
next_step_hint = "\n\n⚠️ 下一步:请立即使用 parse_excel(file_path) 工具读取此 Excel 文件内容,不要搜索网络。"
|
| 632 |
+
elif file_ext == '.pdf':
|
| 633 |
+
next_step_hint = "\n\n⚠️ 下一步:请立即使用 parse_pdf(file_path) 工具读取此 PDF 文件,不要搜索网络。"
|
| 634 |
+
elif file_ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
|
| 635 |
+
next_step_hint = "\n\n⚠️ 下一步:请使用 image_ocr(file_path) 或 analyze_image(file_path, question) 工具处理此图片。"
|
| 636 |
+
elif file_ext in ['.mp3', '.wav', '.m4a', '.ogg']:
|
| 637 |
+
next_step_hint = "\n\n⚠️ 下一步:请使用 transcribe_audio(file_path) 工具转写此音频文件。"
|
| 638 |
+
elif file_ext in ['.txt', '.csv', '.json', '.md', '.py', '.html', '.xml']:
|
| 639 |
+
next_step_hint = "\n\n⚠️ 下一步:请立即使用 read_file(file_path) 工具读取此文件内容。"
|
| 640 |
+
elif file_ext == '.zip':
|
| 641 |
+
next_step_hint = "\n\n⚠️ 下一步:请使用 read_file(file_path) 工具解压此 ZIP 文件。"
|
| 642 |
+
|
| 643 |
+
return f"文件已下载到: {file_path}\n文件大小: {file_size} 字节\n文件名: {filename}{next_step_hint}"
|
| 644 |
+
|
| 645 |
+
except requests.Timeout:
|
| 646 |
+
return f"下载超时({TOOL_TIMEOUT}秒),请稍后重试。"
|
| 647 |
+
except Exception as e:
|
| 648 |
+
return f"下载文件出错: {type(e).__name__}: {str(e)}"
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
@tool
|
| 652 |
+
def read_file(file_path: str, encoding: str = "utf-8") -> str:
|
| 653 |
+
"""
|
| 654 |
+
读取本地文件内容。
|
| 655 |
+
|
| 656 |
+
支持格式:txt, csv, json, py, html, xml, zip, md
|
| 657 |
+
|
| 658 |
+
Args:
|
| 659 |
+
file_path: 文件完整路径
|
| 660 |
+
encoding: 编码格式,默认 utf-8
|
| 661 |
+
|
| 662 |
+
Returns:
|
| 663 |
+
文件内容(超过指定字符数会截断)
|
| 664 |
+
|
| 665 |
+
注意:
|
| 666 |
+
- ZIP 文件会自动解压并列出内容
|
| 667 |
+
- JSON 文件会自动美化输出
|
| 668 |
+
- PDF/Excel 需使用专门的扩展工具
|
| 669 |
+
"""
|
| 670 |
+
try:
|
| 671 |
+
if not os.path.exists(file_path):
|
| 672 |
+
return f"文件不存在: {file_path}"
|
| 673 |
+
|
| 674 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 675 |
+
|
| 676 |
+
# 处理 ZIP 文件
|
| 677 |
+
if file_ext == '.zip':
|
| 678 |
+
extract_dir = file_path.replace('.zip', '_extracted')
|
| 679 |
+
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
| 680 |
+
zip_ref.extractall(extract_dir)
|
| 681 |
+
|
| 682 |
+
files = os.listdir(extract_dir)
|
| 683 |
+
file_list = "\n".join(f" - {f}" for f in files)
|
| 684 |
+
return f"ZIP 已解压到: {extract_dir}\n包含文件:\n{file_list}"
|
| 685 |
+
|
| 686 |
+
# 读取文本文件
|
| 687 |
+
with open(file_path, 'r', encoding=encoding, errors='ignore') as f:
|
| 688 |
+
content = f.read()
|
| 689 |
+
|
| 690 |
+
# JSON 美化
|
| 691 |
+
if file_ext == '.json':
|
| 692 |
+
try:
|
| 693 |
+
data = json.loads(content)
|
| 694 |
+
content = json.dumps(data, indent=2, ensure_ascii=False)
|
| 695 |
+
except json.JSONDecodeError:
|
| 696 |
+
pass # 保持原始内容
|
| 697 |
+
|
| 698 |
+
# 限制返回长度
|
| 699 |
+
if len(content) > MAX_FILE_SIZE:
|
| 700 |
+
return content[:MAX_FILE_SIZE] + f"\n\n... [内容已截断,共 {len(content)} 字符]"
|
| 701 |
+
|
| 702 |
+
return content
|
| 703 |
+
|
| 704 |
+
except Exception as e:
|
| 705 |
+
return f"读取文件出错: {type(e).__name__}: {str(e)}"
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
# ========================================
|
| 709 |
+
# 计算和代码工具
|
| 710 |
+
# ========================================
|
| 711 |
+
|
| 712 |
+
@tool
|
| 713 |
+
def calc(expression: str) -> str:
|
| 714 |
+
"""
|
| 715 |
+
执行安全的数学计算。
|
| 716 |
+
|
| 717 |
+
支持:
|
| 718 |
+
- 基础运算:+, -, *, /, **, %
|
| 719 |
+
- 数学函数:sqrt, sin, cos, tan, log, log10, exp, floor, ceil
|
| 720 |
+
- 常量:pi, e
|
| 721 |
+
|
| 722 |
+
Args:
|
| 723 |
+
expression: 数学表达式,如 "2+3*4" 或 "sqrt(16)"
|
| 724 |
+
|
| 725 |
+
Returns:
|
| 726 |
+
计算结果
|
| 727 |
+
"""
|
| 728 |
+
import math
|
| 729 |
+
|
| 730 |
+
# 允许的数学函数和常量
|
| 731 |
+
safe_dict = {
|
| 732 |
+
# 内置函数
|
| 733 |
+
'abs': abs, 'round': round, 'min': min, 'max': max,
|
| 734 |
+
'sum': sum, 'pow': pow, 'len': len,
|
| 735 |
+
# math 函数
|
| 736 |
+
'sqrt': math.sqrt, 'sin': math.sin, 'cos': math.cos,
|
| 737 |
+
'tan': math.tan, 'log': math.log, 'log10': math.log10,
|
| 738 |
+
'exp': math.exp, 'floor': math.floor, 'ceil': math.ceil,
|
| 739 |
+
'asin': math.asin, 'acos': math.acos, 'atan': math.atan,
|
| 740 |
+
'sinh': math.sinh, 'cosh': math.cosh, 'tanh': math.tanh,
|
| 741 |
+
'degrees': math.degrees, 'radians': math.radians,
|
| 742 |
+
'factorial': math.factorial, 'gcd': math.gcd,
|
| 743 |
+
# 常量
|
| 744 |
+
'pi': math.pi, 'e': math.e,
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
try:
|
| 748 |
+
# 清理表达式
|
| 749 |
+
expression = expression.strip()
|
| 750 |
+
|
| 751 |
+
# 安全执行
|
| 752 |
+
result = eval(expression, {"__builtins__": {}}, safe_dict)
|
| 753 |
+
|
| 754 |
+
# 格式化结果
|
| 755 |
+
if isinstance(result, float):
|
| 756 |
+
# 避免浮点数精度问题
|
| 757 |
+
if result.is_integer():
|
| 758 |
+
return str(int(result))
|
| 759 |
+
return str(round(result, 10))
|
| 760 |
+
|
| 761 |
+
return str(result)
|
| 762 |
+
|
| 763 |
+
except ZeroDivisionError:
|
| 764 |
+
return "计算出错: 除数不能为零"
|
| 765 |
+
except ValueError as e:
|
| 766 |
+
return f"计算出错: 无效的数学操作 - {str(e)}"
|
| 767 |
+
except Exception as e:
|
| 768 |
+
return f"计算出错: {type(e).__name__}: {str(e)}"
|
| 769 |
+
|
| 770 |
+
|
| 771 |
+
@tool
|
| 772 |
+
def run_python(code: str) -> str:
|
| 773 |
+
"""
|
| 774 |
+
在沙箱环境中执行 Python 代码。
|
| 775 |
+
|
| 776 |
+
支持 import 以下模块:
|
| 777 |
+
- math: 数学模块
|
| 778 |
+
- re: 正则表达式模块
|
| 779 |
+
- json: JSON 处理模块
|
| 780 |
+
- datetime: 日期时间模块
|
| 781 |
+
- collections: 集合工具模块
|
| 782 |
+
- random: 随机数模块
|
| 783 |
+
- string: 字符串常量模块
|
| 784 |
+
- itertools: 迭代器工具模块
|
| 785 |
+
- functools: 函数工具模块
|
| 786 |
+
|
| 787 |
+
可用内置函数:
|
| 788 |
+
- 类型: list, dict, set, tuple, str, int, float, bool, bytes
|
| 789 |
+
- 函数: print, len, range, enumerate, zip, map, filter, sorted, reversed
|
| 790 |
+
- 数值: sum, min, max, abs, round, pow, divmod, all, any
|
| 791 |
+
- 转换: ord, chr, hex, bin, oct, isinstance, type, format, repr
|
| 792 |
+
|
| 793 |
+
适用场景:
|
| 794 |
+
- 复杂数学计算
|
| 795 |
+
- 数据排序和过滤
|
| 796 |
+
- 字符串处理
|
| 797 |
+
- 日期计算
|
| 798 |
+
|
| 799 |
+
Args:
|
| 800 |
+
code: Python 代码,需使用 print() 输出结果
|
| 801 |
+
|
| 802 |
+
Returns:
|
| 803 |
+
代码的标准输出
|
| 804 |
+
|
| 805 |
+
示例:
|
| 806 |
+
from datetime import datetime, timedelta
|
| 807 |
+
today = datetime(2024, 1, 15)
|
| 808 |
+
print(today + timedelta(days=30))
|
| 809 |
+
"""
|
| 810 |
+
import io
|
| 811 |
+
import sys
|
| 812 |
+
import math
|
| 813 |
+
import re as re_module
|
| 814 |
+
import json as json_module
|
| 815 |
+
import datetime as datetime_module
|
| 816 |
+
import collections as collections_module
|
| 817 |
+
import random as random_module
|
| 818 |
+
import string as string_module
|
| 819 |
+
import itertools as itertools_module
|
| 820 |
+
import functools as functools_module
|
| 821 |
+
|
| 822 |
+
# 允许导入的模块白名单
|
| 823 |
+
ALLOWED_MODULES = {
|
| 824 |
+
'math': math,
|
| 825 |
+
're': re_module,
|
| 826 |
+
'json': json_module,
|
| 827 |
+
'datetime': datetime_module,
|
| 828 |
+
'collections': collections_module,
|
| 829 |
+
'random': random_module,
|
| 830 |
+
'string': string_module,
|
| 831 |
+
'itertools': itertools_module,
|
| 832 |
+
'functools': functools_module,
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
def restricted_import(name, globals=None, locals=None, fromlist=(), level=0):
|
| 836 |
+
"""受限的 import 函数,只允许导入白名单中的模块"""
|
| 837 |
+
if name not in ALLOWED_MODULES:
|
| 838 |
+
raise ImportError(f"不允许导入模块 '{name}',可用模块: {', '.join(ALLOWED_MODULES.keys())}")
|
| 839 |
+
return ALLOWED_MODULES[name]
|
| 840 |
+
|
| 841 |
+
# 受限的内置函数
|
| 842 |
+
safe_builtins = {
|
| 843 |
+
# 类型
|
| 844 |
+
'list': list, 'dict': dict, 'set': set, 'tuple': tuple,
|
| 845 |
+
'str': str, 'int': int, 'float': float, 'bool': bool,
|
| 846 |
+
'bytes': bytes, 'bytearray': bytearray,
|
| 847 |
+
# 函数
|
| 848 |
+
'print': print, 'len': len, 'range': range, 'enumerate': enumerate,
|
| 849 |
+
'zip': zip, 'map': map, 'filter': filter, 'sorted': sorted,
|
| 850 |
+
'reversed': reversed, 'iter': iter, 'next': next,
|
| 851 |
+
'sum': sum, 'min': min, 'max': max, 'abs': abs, 'round': round,
|
| 852 |
+
'pow': pow, 'divmod': divmod,
|
| 853 |
+
'all': all, 'any': any,
|
| 854 |
+
'isinstance': isinstance, 'type': type,
|
| 855 |
+
'ord': ord, 'chr': chr,
|
| 856 |
+
'hex': hex, 'bin': bin, 'oct': oct,
|
| 857 |
+
'format': format, 'repr': repr,
|
| 858 |
+
'hasattr': hasattr, 'getattr': getattr, 'setattr': setattr,
|
| 859 |
+
'slice': slice, 'object': object,
|
| 860 |
+
# 支持 import
|
| 861 |
+
'__import__': restricted_import,
|
| 862 |
+
# 常量
|
| 863 |
+
'True': True, 'False': False, 'None': None,
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
# 预注入的模块(可以直接使用,也可以 import)
|
| 867 |
+
preloaded = {
|
| 868 |
+
'math': math,
|
| 869 |
+
're': re_module,
|
| 870 |
+
'json': json_module,
|
| 871 |
+
'datetime': datetime_module.datetime,
|
| 872 |
+
'date': datetime_module.date,
|
| 873 |
+
'timedelta': datetime_module.timedelta,
|
| 874 |
+
'Counter': collections_module.Counter,
|
| 875 |
+
'defaultdict': collections_module.defaultdict,
|
| 876 |
+
'OrderedDict': collections_module.OrderedDict,
|
| 877 |
+
'random': random_module,
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
# 合并命名空间
|
| 881 |
+
namespace = {"__builtins__": safe_builtins}
|
| 882 |
+
namespace.update(preloaded)
|
| 883 |
+
|
| 884 |
+
# 捕获 stdout
|
| 885 |
+
old_stdout = sys.stdout
|
| 886 |
+
sys.stdout = io.StringIO()
|
| 887 |
+
|
| 888 |
+
try:
|
| 889 |
+
exec(code, namespace)
|
| 890 |
+
output = sys.stdout.getvalue()
|
| 891 |
+
|
| 892 |
+
if not output:
|
| 893 |
+
return "代码执行成功,无输出。请使用 print() 输出结果。"
|
| 894 |
+
|
| 895 |
+
# 限制输出长度
|
| 896 |
+
if len(output) > MAX_FILE_SIZE:
|
| 897 |
+
return output[:MAX_FILE_SIZE] + f"\n\n... [输出已截断,共 {len(output)} 字符]"
|
| 898 |
+
|
| 899 |
+
return output.strip()
|
| 900 |
+
|
| 901 |
+
except SyntaxError as e:
|
| 902 |
+
return f"语法错误: 第 {e.lineno} 行 - {e.msg}"
|
| 903 |
+
except NameError as e:
|
| 904 |
+
return f"名称错误: {str(e)}(该函数或变量在沙箱中不可用)"
|
| 905 |
+
except Exception as e:
|
| 906 |
+
return f"执行出错: {type(e).__name__}: {str(e)}"
|
| 907 |
+
finally:
|
| 908 |
+
sys.stdout = old_stdout
|
| 909 |
+
|
| 910 |
+
|
| 911 |
+
# ========================================
|
| 912 |
+
# 导出工具列表
|
| 913 |
+
# ========================================
|
| 914 |
+
BASE_TOOLS = [
|
| 915 |
+
# 搜索工具
|
| 916 |
+
web_search,
|
| 917 |
+
wikipedia_search,
|
| 918 |
+
wikipedia_page, # 获取完整 Wikipedia 页面内容
|
| 919 |
+
tavily_search,
|
| 920 |
+
arxiv_search,
|
| 921 |
+
youtube_search,
|
| 922 |
+
news_search,
|
| 923 |
+
stackoverflow_search,
|
| 924 |
+
google_search,
|
| 925 |
+
# 文件工具
|
| 926 |
+
fetch_task_files,
|
| 927 |
+
read_file,
|
| 928 |
+
# 计算工具
|
| 929 |
+
calc,
|
| 930 |
+
run_python,
|
| 931 |
+
]
|
| 932 |
+
|
| 933 |
+
# 尝试导入扩展工具
|
| 934 |
+
try:
|
| 935 |
+
from extension_tools import EXTENSION_TOOLS
|
| 936 |
+
ALL_TOOLS = BASE_TOOLS + EXTENSION_TOOLS
|
| 937 |
+
except ImportError:
|
| 938 |
+
ALL_TOOLS = BASE_TOOLS
|
| 939 |
+
|
| 940 |
+
# 尝试导入 RAG 工具
|
| 941 |
+
try:
|
| 942 |
+
from rag import RAG_TOOLS
|
| 943 |
+
ALL_TOOLS = ALL_TOOLS + RAG_TOOLS
|
| 944 |
+
except ImportError:
|
| 945 |
+
pass
|