Spaces:
Sleeping
Sleeping
Upload 29 files
Browse files- README.md +133 -131
- app.py +3 -5
- config.yaml +2 -2
- gaia/__init__.py +0 -0
- gaia/__pycache__/__init__.cpython-313.pyc +0 -0
- gaia/__pycache__/agent.cpython-313.pyc +0 -0
- gaia/__pycache__/states.cpython-313.pyc +0 -0
- gaia/__pycache__/utils.cpython-313.pyc +0 -0
- gaia/agent.py +360 -0
- gaia/prompts/prompt.yaml +50 -0
- gaia/prompts/vlm_prompt.yaml +15 -0
- gaia/states.py +27 -0
- gaia/tools/__init__.py +40 -0
- gaia/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- gaia/tools/__pycache__/basic.cpython-313.pyc +0 -0
- gaia/tools/__pycache__/dispatcher.cpython-313.pyc +0 -0
- gaia/tools/__pycache__/files.cpython-313.pyc +0 -0
- gaia/tools/__pycache__/media.cpython-313.pyc +0 -0
- gaia/tools/__pycache__/web.cpython-313.pyc +0 -0
- gaia/tools/basic.py +60 -0
- gaia/tools/dispatcher.py +55 -0
- gaia/tools/files.py +296 -0
- gaia/tools/media.py +96 -0
- gaia/tools/web.py +209 -0
- gaia/utils.py +168 -0
- notebooks/create_vector_database.ipynb +111 -0
- notebooks/test.ipynb +362 -0
- requirements.txt +1 -0
- scripts/create_vector_database.py +52 -0
README.md
CHANGED
|
@@ -1,131 +1,133 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
```
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
```
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
| 82 |
-
|
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
#
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🌍 GAIA Benchmark Agent
|
| 2 |
+
|
| 3 |
+
**An autonomous, multi-modal agent that tackles the GAIA reasoning benchmark.**
|
| 4 |
+
|
| 5 |
+

|
| 6 |
+

|
| 7 |
+

|
| 8 |
+

|
| 9 |
+
|
| 10 |
+
> [!NOTE]
|
| 11 |
+
> Developed as part of the **Hugging Face Agents Course (Unit 4: GAIA)**.
|
| 12 |
+
|
| 13 |
+
## 📖 Overview
|
| 14 |
+
|
| 15 |
+
GAIA tasks require multi-step reasoning, tool use, and the ability to process diverse file types (documents, spreadsheets, audio, images, code). This agent uses a **LangGraph** state machine to plan, retrieve few-shot context, call tools, and produce a strictly-formatted answer.
|
| 16 |
+
|
| 17 |
+
The solver model is **`Qwen/Qwen3-32B`** via Hugging Face Inference Providers; the formatter (separate node) reuses the same model bound to a single `emit_final_answer` tool to enforce the strict GAIA output contract. A Supabase + BM25 hybrid retriever surfaces similar past tasks as few-shot exemplars.
|
| 18 |
+
|
| 19 |
+
## 🚀 Key Features
|
| 20 |
+
|
| 21 |
+
- **🧠 Plan-Execute-Observe-Refine loop** driven by LangGraph.
|
| 22 |
+
- **🪜 Two-stage output**: a solver node reasons freely, then a dedicated formatter node returns the GAIA-compliant answer via a Pydantic-shaped tool call — eliminating regex parsing of the LLM's free text.
|
| 23 |
+
- **📂 Multi-modal file processing**:
|
| 24 |
+
- **Documents**: PDF, Word (`.docx`), PowerPoint (`.pptx`), Text
|
| 25 |
+
- **Data**: Excel (`.xlsx`, multi-sheet), CSV, JSON-LD, PDB (Protein Data Bank)
|
| 26 |
+
- **Media**: Audio transcription (`openai/whisper-large-v3` via HF), image analysis (`Qwen/Qwen3-VL-32B-Instruct`)
|
| 27 |
+
- **Code**: Python source files (read & execute)
|
| 28 |
+
- **Archives**: ZIP extraction + inspection
|
| 29 |
+
- **🔍 Hybrid RAG**: Vector search (Supabase RPC over `Alibaba-NLP/gte-modernbert-base`) **+** BM25 over a local 165-question corpus, fused with Reciprocal Rank Fusion, then reranked with a ModernBERT cross-encoder.
|
| 30 |
+
- **🌐 Web tooling**: DuckDuckGo, Tavily, Wikipedia, ArXiv, full-page extraction (Trafilatura), YouTube transcripts.
|
| 31 |
+
- **🛠️ Modular tool layout**: tools are organised by domain in `gaia/tools/` (basic, web, files, media, dispatcher) so adding a new capability is a single-file change.
|
| 32 |
+
|
| 33 |
+
## 🏗️ Architecture
|
| 34 |
+
|
| 35 |
+
```mermaid
|
| 36 |
+
graph TD
|
| 37 |
+
START --> FileDL["File Downloader<br/>(fetches /files/{task_id})"]
|
| 38 |
+
FileDL --> Retriever["Retriever<br/>(Vector + BM25 + RRF)"]
|
| 39 |
+
Retriever --> Reranker["Reranker<br/>(ModernBERT cross-encoder)"]
|
| 40 |
+
Reranker --> Processor["Solver<br/>(Qwen3-32B + tools)"]
|
| 41 |
+
Processor -->|tool call| Tools["Tool Node"]
|
| 42 |
+
Tools --> Processor
|
| 43 |
+
Processor -->|done| Formatter["Formatter<br/>(emit_final_answer tool)"]
|
| 44 |
+
Formatter --> END
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
| Node | Role |
|
| 48 |
+
| :--- | :--- |
|
| 49 |
+
| **`file_downloader_node`** | If the question has an associated file, download from `{api.base_url}/files/{task_id}` and cache on disk under `data/task_files/{task_id}/`. |
|
| 50 |
+
| **`retriever_node`** | Hybrid search: Supabase vector RPC + local BM25 over `data/metadata.jsonl`, fused with RRF. Returns up to 20 candidate task IDs. |
|
| 51 |
+
| **`reranker_node`** | `Alibaba-NLP/gte-reranker-modernbert-base` re-scores candidates and injects the top-K as few-shot examples (Question + Final Answer + Solution Steps). |
|
| 52 |
+
| **`processor_node`** | Qwen3-32B with all tools bound. Reasons, calls tools, loops until satisfied. |
|
| 53 |
+
| **`tools`** | LangGraph `ToolNode` executing the chosen tool, then returning control to the processor. |
|
| 54 |
+
| **`formatter_node`** | A second Qwen3-32B call bound to a single `emit_final_answer(answer: str)` tool — produces the strictly-formatted value the GAIA scorer compares against. |
|
| 55 |
+
|
| 56 |
+
## 🛠️ Stack
|
| 57 |
+
|
| 58 |
+
| Category | Libraries | Purpose |
|
| 59 |
+
| :--- | :--- | :--- |
|
| 60 |
+
| **Orchestration** | `langgraph`, `langchain`, `langchain-huggingface` | State graph, tool binding, structured output. |
|
| 61 |
+
| **LLM / VLM / ASR** | `huggingface_hub` Inference API | `Qwen/Qwen3-32B`, `Qwen/Qwen3-VL-32B-Instruct`, `openai/whisper-large-v3`. |
|
| 62 |
+
| **Embeddings & Vector Store** | `sentence-transformers`, `supabase` | Semantic search via `Alibaba-NLP/gte-modernbert-base`. |
|
| 63 |
+
| **Keyword search** | `bm25s` | Local BM25 index over the 165-question GAIA corpus. |
|
| 64 |
+
| **Documents** | `pypdf`, `python-docx`, `python-pptx`, `openpyxl` | Office formats. |
|
| 65 |
+
| **Data** | `polars`, `biopython` | Tabular and PDB structural analysis. |
|
| 66 |
+
| **Media** | `pillow`, `librosa`, `soundfile` | Image + audio I/O. |
|
| 67 |
+
| **Web** | `ddgs`, `tavily-python`, `wikipedia`, `arxiv`, `trafilatura`, `youtube-transcript-api` | Search, page extraction, captions. |
|
| 68 |
+
| **UI** | `gradio` | Evaluation runner (HF Space entry point). |
|
| 69 |
+
|
| 70 |
+
## 💻 Installation & Setup
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
git clone <repo_url>
|
| 74 |
+
cd gaia
|
| 75 |
+
uv sync # or: pip install -r requirements.txt
|
| 76 |
+
cp .env.example .env # populate the keys below
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Required environment variables (place in `.env` or set as HF Space Secrets):
|
| 80 |
+
|
| 81 |
+
| Variable | Required? | Purpose |
|
| 82 |
+
| :--- | :--- | :--- |
|
| 83 |
+
| `HF_INFERENCE_KEY` | yes | Hugging Face token — must have **"Make calls to Inference Providers"** permission. |
|
| 84 |
+
| `SUPABASE_URL` | yes (if `retrievers.enable_vector_search: true`) | Supabase project URL. |
|
| 85 |
+
| `SUPABASE_SERVICE_KEY` | yes (same) | Supabase `service_role` key. |
|
| 86 |
+
| `TAVILY_API_KEY` | optional | Only needed when the agent picks `tavily_web_search`. |
|
| 87 |
+
|
| 88 |
+
## 🎮 Usage
|
| 89 |
+
|
| 90 |
+
Start the Gradio interface:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
python app.py
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
The UI requires a Hugging Face login. Click **"Run Evaluation & Submit All Answers"** to fetch the GAIA question set, run the agent on each, and submit to the scoring API.
|
| 97 |
+
|
| 98 |
+
To (re)populate the Supabase vector store from the local corpus:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python scripts/create_vector_database.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## 📂 Project Structure
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
.
|
| 108 |
+
├── app.py # HF Space entry point (Gradio)
|
| 109 |
+
├── config.yaml # All tunable parameters
|
| 110 |
+
├── pyproject.toml / requirements.txt # Dependencies (uv + pip parity)
|
| 111 |
+
├── gaia/ # Application package
|
| 112 |
+
│ ├── agent.py # LangGraph nodes, graph, formatter
|
| 113 |
+
│ ├── states.py # AgentState TypedDict
|
| 114 |
+
│ ├── utils.py # config / prompt loaders, BM25, RRF, answer + youtube helpers
|
| 115 |
+
│ ├── prompts/
|
| 116 |
+
│ │ ├── prompt.yaml # Solver system prompt
|
| 117 |
+
│ │ └── vlm_prompt.yaml # analyze_image system prompt
|
| 118 |
+
│ └── tools/
|
| 119 |
+
│ ├── __init__.py # Aggregates tools_list
|
| 120 |
+
│ ├── basic.py # calculator, python_eval
|
| 121 |
+
│ ├── web.py # ddg / tavily / wiki / arxiv / fetch_webpage / youtube_transcript
|
| 122 |
+
│ ├── files.py # PDF, DOCX, PPTX, TXT, CSV, XLSX, JSON-LD, PDB, Python, ZIP
|
| 123 |
+
│ ├── media.py # analyze_image (VLM), transcribe_audio (ASR), shared HF client
|
| 124 |
+
│ └── dispatcher.py # read_file extension router
|
| 125 |
+
├── scripts/
|
| 126 |
+
│ └── create_vector_database.py # One-shot embedder for Supabase
|
| 127 |
+
├── notebooks/ # Exploratory work
|
| 128 |
+
├── data/
|
| 129 |
+
│ └── metadata.jsonl # Local GAIA corpus (165 examples)
|
| 130 |
+
└── models/ # HF model cache (gitignored)
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
All tunable knobs — model IDs, retrieval depth, thinking mode, recursion limit — live in `config.yaml`; no code change required to swap models or tweak retrieval.
|
app.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
import os
|
| 2 |
-
import re
|
| 3 |
import gradio as gr
|
| 4 |
import requests
|
| 5 |
import inspect
|
| 6 |
import pandas as pd
|
| 7 |
from langchain_core.messages import HumanMessage
|
| 8 |
-
from agent import agent_graph
|
|
|
|
| 9 |
|
| 10 |
# (Keep Constants as is)
|
| 11 |
# --- Constants ---
|
|
@@ -26,9 +26,7 @@ class BasicAgent:
|
|
| 26 |
})
|
| 27 |
fixed_answer = (response.get("final_answer") or "").strip()
|
| 28 |
if not fixed_answer:
|
| 29 |
-
|
| 30 |
-
match = re.search(r'FINAL ANSWER:\s*(.*)', content, re.DOTALL | re.IGNORECASE)
|
| 31 |
-
fixed_answer = match.group(1).strip() if match else content.strip()
|
| 32 |
print(f"Agent returning fixed answer: {fixed_answer}")
|
| 33 |
return fixed_answer
|
| 34 |
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
import requests
|
| 4 |
import inspect
|
| 5 |
import pandas as pd
|
| 6 |
from langchain_core.messages import HumanMessage
|
| 7 |
+
from gaia.agent import agent_graph
|
| 8 |
+
from gaia.utils import extract_final_answer
|
| 9 |
|
| 10 |
# (Keep Constants as is)
|
| 11 |
# --- Constants ---
|
|
|
|
| 26 |
})
|
| 27 |
fixed_answer = (response.get("final_answer") or "").strip()
|
| 28 |
if not fixed_answer:
|
| 29 |
+
fixed_answer = extract_final_answer(response['messages'][-1].content)
|
|
|
|
|
|
|
| 30 |
print(f"Agent returning fixed answer: {fixed_answer}")
|
| 31 |
return fixed_answer
|
| 32 |
|
config.yaml
CHANGED
|
@@ -28,7 +28,7 @@ models:
|
|
| 28 |
vlm:
|
| 29 |
model_name: "Qwen/Qwen3-VL-32B-Instruct" # Hugging Face model ID
|
| 30 |
asr:
|
| 31 |
-
model_name: "
|
| 32 |
#device: "cuda" # cpu, cuda, or mps (for Mac)
|
| 33 |
#parameters:
|
| 34 |
# temperature: 0.7
|
|
@@ -36,7 +36,7 @@ models:
|
|
| 36 |
# repetition_penalty: 1.1
|
| 37 |
|
| 38 |
graph:
|
| 39 |
-
recursion_limit:
|
| 40 |
|
| 41 |
api:
|
| 42 |
base_url: "https://agents-course-unit4-scoring.hf.space"
|
|
|
|
| 28 |
vlm:
|
| 29 |
model_name: "Qwen/Qwen3-VL-32B-Instruct" # Hugging Face model ID
|
| 30 |
asr:
|
| 31 |
+
model_name: "openai/whisper-large-v3" # Hugging Face model ID — must have a provider on HF Inference Providers
|
| 32 |
#device: "cuda" # cpu, cuda, or mps (for Mac)
|
| 33 |
#parameters:
|
| 34 |
# temperature: 0.7
|
|
|
|
| 36 |
# repetition_penalty: 1.1
|
| 37 |
|
| 38 |
graph:
|
| 39 |
+
recursion_limit: 40 # Max graph-node visits before bailing. ~5 + 2*(tool calls), so 40 ≈ 17 tool calls.
|
| 40 |
|
| 41 |
api:
|
| 42 |
base_url: "https://agents-course-unit4-scoring.hf.space"
|
gaia/__init__.py
ADDED
|
File without changes
|
gaia/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (142 Bytes). View file
|
|
|
gaia/__pycache__/agent.cpython-313.pyc
ADDED
|
Binary file (15.3 kB). View file
|
|
|
gaia/__pycache__/states.cpython-313.pyc
ADDED
|
Binary file (1.42 kB). View file
|
|
|
gaia/__pycache__/utils.cpython-313.pyc
ADDED
|
Binary file (9.12 kB). View file
|
|
|
gaia/agent.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GAIA Agent with Multi-Modal File Processing and Hybrid Retrieval.
|
| 3 |
+
|
| 4 |
+
This module defines a LangGraph agent that can:
|
| 5 |
+
1. Retrieve similar questions using Hybrid Search (Vector + BM25) and Reranking
|
| 6 |
+
2. Process files using tools (PDF, XLSX, MP3, etc.)
|
| 7 |
+
3. Answer questions using web search, calculator, and other tools
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import bm25s
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
|
| 15 |
+
from langgraph.graph import START, END, StateGraph
|
| 16 |
+
from langgraph.prebuilt import tools_condition, ToolNode
|
| 17 |
+
|
| 18 |
+
from sentence_transformers import SentenceTransformer, CrossEncoder
|
| 19 |
+
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 20 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
| 21 |
+
from langchain_core.tools import tool
|
| 22 |
+
from supabase.client import Client, create_client
|
| 23 |
+
|
| 24 |
+
from gaia.utils import (
|
| 25 |
+
load_config, load_prompt, init_bm25_index, reciprocal_rank_fusion,
|
| 26 |
+
extract_final_answer, download_task_file,
|
| 27 |
+
)
|
| 28 |
+
from gaia.tools import tools_list
|
| 29 |
+
from gaia.states import AgentState
|
| 30 |
+
|
| 31 |
+
load_dotenv()
|
| 32 |
+
config = load_config()
|
| 33 |
+
|
| 34 |
+
# Environment details and others
|
| 35 |
+
hf_key = os.getenv("HF_INFERENCE_KEY")
|
| 36 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 37 |
+
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 38 |
+
|
| 39 |
+
# ============================================
|
| 40 |
+
# Model & Embeddings Setup
|
| 41 |
+
# ============================================
|
| 42 |
+
|
| 43 |
+
enable_keyword_search = config["retrievers"]["enable_keyword_search"]
|
| 44 |
+
enable_vector_search = config["retrievers"]["enable_vector_search"]
|
| 45 |
+
|
| 46 |
+
# BM25 Retriever
|
| 47 |
+
bm25_retriever, bm25_corpus, bm25_ids = None, None, None
|
| 48 |
+
if enable_keyword_search:
|
| 49 |
+
bm25_retriever, bm25_corpus, bm25_ids = init_bm25_index(corpus_file=config["data"])
|
| 50 |
+
|
| 51 |
+
bm25_id_to_text = {}
|
| 52 |
+
if bm25_corpus and bm25_ids:
|
| 53 |
+
bm25_id_to_text = dict(zip(bm25_ids, bm25_corpus))
|
| 54 |
+
|
| 55 |
+
embeddings, supabase = None, None
|
| 56 |
+
if enable_vector_search:
|
| 57 |
+
# Embeddings for Vector Search
|
| 58 |
+
embeddings = SentenceTransformer(model_name_or_path=config["models"]["embeddings"]["model_name"], cache_folder=config["models"]["cache_folder"])
|
| 59 |
+
|
| 60 |
+
# Supabase Vector Store
|
| 61 |
+
supabase: Client = create_client(supabase_url, supabase_key)
|
| 62 |
+
|
| 63 |
+
# Reranker Model (ModernBERT Cross-Encoder)
|
| 64 |
+
reranker = CrossEncoder(config["models"]["reranker"]["model_name"], cache_folder=config["models"]["cache_folder"])
|
| 65 |
+
|
| 66 |
+
# LLM for Agent
|
| 67 |
+
llm = HuggingFaceEndpoint(
|
| 68 |
+
repo_id=config["models"]["llm"]["model_name"],
|
| 69 |
+
temperature=config["models"]["llm"]["parameters"]["temperature"],
|
| 70 |
+
repetition_penalty=config["models"]["llm"]["parameters"]["repetition_penalty"],
|
| 71 |
+
provider=config["models"]["llm"]["parameters"]["provider"],
|
| 72 |
+
huggingfacehub_api_token=hf_key
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
agent_llm = ChatHuggingFace(llm=llm)
|
| 76 |
+
agent_with_tools = agent_llm.bind_tools(tools_list)
|
| 77 |
+
|
| 78 |
+
_PROMPTS_DIR = Path(__file__).parent / "prompts"
|
| 79 |
+
_system_prompt = load_prompt(str(_PROMPTS_DIR / "prompt.yaml"))
|
| 80 |
+
_thinking_enabled = config["models"]["llm"]["parameters"].get("thinking_enabled", True)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@tool
|
| 84 |
+
def emit_final_answer(answer: str) -> str:
|
| 85 |
+
"""Emit the final answer to the GAIA question in the strict scoring format.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
answer: The raw answer value only.
|
| 89 |
+
Numbers: plain digits, no commas, no units, no symbols (write '1000000', not '1,000,000' or '$50').
|
| 90 |
+
Strings: no articles ('a', 'an', 'the'), no markdown, no surrounding quotes, no trailing punctuation.
|
| 91 |
+
Lists: comma-separated with no extra spaces, in the order requested by the question.
|
| 92 |
+
"""
|
| 93 |
+
return answer
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
formatter_llm = agent_llm.bind_tools([emit_final_answer], tool_choice="emit_final_answer")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ============================================
|
| 100 |
+
# Graph Nodes
|
| 101 |
+
# ============================================
|
| 102 |
+
|
| 103 |
+
def file_downloader_node(state: AgentState) -> AgentState:
|
| 104 |
+
"""
|
| 105 |
+
Download the task file from the scoring API if one is associated with the question.
|
| 106 |
+
Saves to a local directory and stores the path in state. Detailed failure
|
| 107 |
+
diagnostics are logged so HF Space logs reveal the actual cause (network, HTTP, filesystem).
|
| 108 |
+
"""
|
| 109 |
+
print("--- FILE DOWNLOADER NODE ---")
|
| 110 |
+
file_name = state.get("file_name", "")
|
| 111 |
+
task_id = state.get("task_id", "")
|
| 112 |
+
|
| 113 |
+
if not file_name or not task_id:
|
| 114 |
+
return {"file_path": ""}
|
| 115 |
+
|
| 116 |
+
local_path, err = download_task_file(
|
| 117 |
+
task_id=task_id,
|
| 118 |
+
file_name=file_name,
|
| 119 |
+
base_url=config["api"]["base_url"],
|
| 120 |
+
files_dir=config["api"]["files_dir"],
|
| 121 |
+
)
|
| 122 |
+
if local_path:
|
| 123 |
+
print(f"Downloaded: {file_name} → {local_path}")
|
| 124 |
+
return {"file_path": local_path}
|
| 125 |
+
print(f"File download failed for task {task_id} ({file_name}): {err}")
|
| 126 |
+
return {"file_path": ""}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def retriever_node(state: AgentState) -> AgentState:
|
| 130 |
+
"""
|
| 131 |
+
Hybrid Search Node: Retrieve docs via Vector Search + BM25, combine with RRF.
|
| 132 |
+
"""
|
| 133 |
+
print("--- RETRIEVER NODE ---")
|
| 134 |
+
messages = state.get("messages", [])
|
| 135 |
+
if not messages:
|
| 136 |
+
return {"retrieved_docs": []}
|
| 137 |
+
|
| 138 |
+
question_content = messages[0].content
|
| 139 |
+
|
| 140 |
+
if not enable_vector_search and not enable_keyword_search:
|
| 141 |
+
print("No retrieval method enabled.")
|
| 142 |
+
return {"retrieved_docs": []}
|
| 143 |
+
|
| 144 |
+
# 1. Vector Search
|
| 145 |
+
vector_docs = []
|
| 146 |
+
if supabase and embeddings:
|
| 147 |
+
try:
|
| 148 |
+
response = supabase.rpc(
|
| 149 |
+
config["retrievers"]["vector_store"]["query"],
|
| 150 |
+
{"query_embedding": embeddings.encode(question_content).tolist(),
|
| 151 |
+
"match_count": config["retrievers"]["vector_store"]["k"],
|
| 152 |
+
"match_threshold": config["retrievers"]["vector_store"]["threshold"]
|
| 153 |
+
}
|
| 154 |
+
).execute()
|
| 155 |
+
|
| 156 |
+
vector_docs = response.data
|
| 157 |
+
|
| 158 |
+
except Exception as e:
|
| 159 |
+
print(f"Vector search error: {e}")
|
| 160 |
+
|
| 161 |
+
# 2. BM25 Search
|
| 162 |
+
bm25_docs = []
|
| 163 |
+
if bm25_retriever and bm25_corpus and bm25_ids:
|
| 164 |
+
try:
|
| 165 |
+
query_tokens = bm25s.tokenize([question_content], stopwords="en")
|
| 166 |
+
results, scores = bm25_retriever.retrieve(query_tokens, k=config["retrievers"]["bm25"]["k"])
|
| 167 |
+
indices = results[0]
|
| 168 |
+
|
| 169 |
+
for i, idx in enumerate(indices):
|
| 170 |
+
content = bm25_corpus[idx]
|
| 171 |
+
task_id = bm25_ids[idx]
|
| 172 |
+
score = scores[0][i]
|
| 173 |
+
bm25_dict = {"content":content, "metadata": {"source": "bm25_search", "task_id": task_id, "score": score}}
|
| 174 |
+
bm25_docs.append(bm25_dict)
|
| 175 |
+
except Exception as e:
|
| 176 |
+
print(f"BM25 search error: {e}")
|
| 177 |
+
|
| 178 |
+
# 3. RRF Fusion
|
| 179 |
+
final_candidates = []
|
| 180 |
+
if vector_docs and bm25_docs:
|
| 181 |
+
fused = reciprocal_rank_fusion([vector_docs, bm25_docs])
|
| 182 |
+
final_candidates = [id for id, doc, score in fused]
|
| 183 |
+
else:
|
| 184 |
+
final_candidates = vector_docs + bm25_docs
|
| 185 |
+
final_candidates = [doc["metadata"]["task_id"] for doc in final_candidates]
|
| 186 |
+
|
| 187 |
+
top_candidates = final_candidates[:20]
|
| 188 |
+
|
| 189 |
+
return {"retrieved_docs": top_candidates}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def reranker_node(state: AgentState) -> AgentState:
|
| 193 |
+
"""
|
| 194 |
+
Reranker Node: Re-order candidates using Cross-Encoder and return top 3.
|
| 195 |
+
"""
|
| 196 |
+
print("--- RERANKER NODE ---")
|
| 197 |
+
candidates = state.get("retrieved_docs", [])
|
| 198 |
+
messages = state.get("messages", [])
|
| 199 |
+
|
| 200 |
+
if not candidates or not messages:
|
| 201 |
+
return {"messages": []}
|
| 202 |
+
|
| 203 |
+
question = messages[0].content
|
| 204 |
+
|
| 205 |
+
# Deduplicate candidates — candidates are task_id strings; resolve to text via corpus lookup
|
| 206 |
+
unique_candidates = []
|
| 207 |
+
seen_content = set()
|
| 208 |
+
for task_id in candidates:
|
| 209 |
+
text = bm25_id_to_text.get(task_id)
|
| 210 |
+
if text and text not in seen_content:
|
| 211 |
+
unique_candidates.append(text)
|
| 212 |
+
seen_content.add(text)
|
| 213 |
+
|
| 214 |
+
if not unique_candidates:
|
| 215 |
+
return {"messages": []}
|
| 216 |
+
|
| 217 |
+
pairs = [[question, doc_text] for doc_text in unique_candidates]
|
| 218 |
+
|
| 219 |
+
try:
|
| 220 |
+
scores = reranker.predict(pairs)
|
| 221 |
+
|
| 222 |
+
scored_docs = sorted(
|
| 223 |
+
zip(unique_candidates, scores),
|
| 224 |
+
key=lambda x: x[1],
|
| 225 |
+
reverse=True
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
top_k = config["retrievers"]["final_rrf_k"]
|
| 229 |
+
top_results = scored_docs[:top_k]
|
| 230 |
+
|
| 231 |
+
context_str = "Here are similar questions and answers for reference:\n\n"
|
| 232 |
+
for i, (doc_text, score) in enumerate(top_results):
|
| 233 |
+
context_str += f"--- Example {i+1} (Score: {score:.2f}) ---\n{doc_text}\n\n"
|
| 234 |
+
|
| 235 |
+
context_message = HumanMessage(content=context_str)
|
| 236 |
+
|
| 237 |
+
return {"messages": [context_message]}
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
print(f"Reranker error: {e}")
|
| 241 |
+
if unique_candidates:
|
| 242 |
+
fallback_msg = HumanMessage(content=f"Reference (Fallback):\n{unique_candidates[0]}")
|
| 243 |
+
return {"messages": [fallback_msg]}
|
| 244 |
+
|
| 245 |
+
return {"messages": []}
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def processor_node(state: AgentState) -> AgentState:
|
| 249 |
+
"""
|
| 250 |
+
Processor Node: Main LLM agent that answers the question.
|
| 251 |
+
"""
|
| 252 |
+
prompt_content = _system_prompt.content + ("" if _thinking_enabled else "\n/no_think")
|
| 253 |
+
system_prompt = SystemMessage(content=prompt_content)
|
| 254 |
+
messages = state.get("messages", [])
|
| 255 |
+
task_id = state.get("task_id", "")
|
| 256 |
+
file_name = state.get("file_name", "")
|
| 257 |
+
file_path = state.get("file_path", "")
|
| 258 |
+
|
| 259 |
+
full_messages = [system_prompt]
|
| 260 |
+
|
| 261 |
+
if file_name:
|
| 262 |
+
if file_path:
|
| 263 |
+
file_msg = HumanMessage(
|
| 264 |
+
content=f"Note: A file named '{file_name}' is associated with this question. It is available at path: {file_path}"
|
| 265 |
+
)
|
| 266 |
+
else:
|
| 267 |
+
file_msg = HumanMessage(
|
| 268 |
+
content=(
|
| 269 |
+
f"Note: A file named '{file_name}' is associated with this question "
|
| 270 |
+
f"(task_id={task_id}), but the automatic download failed. "
|
| 271 |
+
f"Call `retry_file_download(task_id='{task_id}', file_name='{file_name}')` "
|
| 272 |
+
f"once to attempt it again. On success, you'll get back the local path you "
|
| 273 |
+
f"can pass to the appropriate read tool. If retry also fails, state clearly "
|
| 274 |
+
f"that the file is unavailable rather than fabricating data."
|
| 275 |
+
)
|
| 276 |
+
)
|
| 277 |
+
full_messages.append(file_msg)
|
| 278 |
+
|
| 279 |
+
full_messages.extend(messages)
|
| 280 |
+
|
| 281 |
+
response = agent_with_tools.invoke(full_messages)
|
| 282 |
+
|
| 283 |
+
return {"messages": [response]}
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def formatter_node(state: AgentState) -> AgentState:
|
| 287 |
+
"""Extract and reformat the solver's answer into a strict GAIA-compliant value."""
|
| 288 |
+
print("--- FORMATTER NODE ---")
|
| 289 |
+
messages = state.get("messages", [])
|
| 290 |
+
if not messages:
|
| 291 |
+
return {"final_answer": ""}
|
| 292 |
+
|
| 293 |
+
question = ""
|
| 294 |
+
for m in messages:
|
| 295 |
+
if isinstance(m, HumanMessage):
|
| 296 |
+
question = m.content
|
| 297 |
+
break
|
| 298 |
+
|
| 299 |
+
solver_output = messages[-1].content or ""
|
| 300 |
+
|
| 301 |
+
prompt = [
|
| 302 |
+
SystemMessage(content=(
|
| 303 |
+
"You extract the final answer from an agent's reasoning and apply the GAIA "
|
| 304 |
+
"formatting rules exactly. You MUST call the `emit_final_answer` tool with "
|
| 305 |
+
"the extracted value. If the agent never produced an answer, call it with an "
|
| 306 |
+
"empty string."
|
| 307 |
+
)),
|
| 308 |
+
HumanMessage(content=(
|
| 309 |
+
f"Question:\n{question}\n\n"
|
| 310 |
+
f"Agent reasoning and conclusion:\n{solver_output}\n\n"
|
| 311 |
+
"Extract the final answer and call emit_final_answer."
|
| 312 |
+
)),
|
| 313 |
+
]
|
| 314 |
+
|
| 315 |
+
try:
|
| 316 |
+
result = formatter_llm.invoke(prompt)
|
| 317 |
+
for tc in getattr(result, "tool_calls", None) or []:
|
| 318 |
+
if tc.get("name") == "emit_final_answer":
|
| 319 |
+
return {"final_answer": str(tc.get("args", {}).get("answer", "")).strip()}
|
| 320 |
+
# Model returned text instead of calling the tool — fall back to regex.
|
| 321 |
+
return {"final_answer": extract_final_answer(result.content)}
|
| 322 |
+
except Exception as e:
|
| 323 |
+
print(f"Formatter error: {e}")
|
| 324 |
+
return {"final_answer": extract_final_answer(solver_output)}
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
# ============================================
|
| 328 |
+
# Graph Construction
|
| 329 |
+
# ============================================
|
| 330 |
+
|
| 331 |
+
def agent_graph():
|
| 332 |
+
"""
|
| 333 |
+
Build and compile the agent graph.
|
| 334 |
+
"""
|
| 335 |
+
workflow = StateGraph(AgentState)
|
| 336 |
+
|
| 337 |
+
# Add nodes
|
| 338 |
+
workflow.add_node("file_downloader_node", file_downloader_node)
|
| 339 |
+
workflow.add_node("retriever_node", retriever_node)
|
| 340 |
+
workflow.add_node("reranker_node", reranker_node)
|
| 341 |
+
workflow.add_node("processor_node", processor_node)
|
| 342 |
+
workflow.add_node("tools", ToolNode(tools_list))
|
| 343 |
+
workflow.add_node("formatter_node", formatter_node)
|
| 344 |
+
|
| 345 |
+
# Add edges
|
| 346 |
+
workflow.add_edge(START, "file_downloader_node")
|
| 347 |
+
workflow.add_edge("file_downloader_node", "retriever_node")
|
| 348 |
+
workflow.add_edge("retriever_node", "reranker_node")
|
| 349 |
+
workflow.add_edge("reranker_node", "processor_node")
|
| 350 |
+
workflow.add_edge("tools", "processor_node")
|
| 351 |
+
workflow.add_conditional_edges(
|
| 352 |
+
"processor_node",
|
| 353 |
+
tools_condition,
|
| 354 |
+
{"tools": "tools", END: "formatter_node"},
|
| 355 |
+
)
|
| 356 |
+
workflow.add_edge("formatter_node", END)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
compiled = workflow.compile()
|
| 360 |
+
return compiled.with_config({"recursion_limit": config["graph"]["recursion_limit"]})
|
gaia/prompts/prompt.yaml
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: "GAIA Agent"
|
| 2 |
+
prompt: |
|
| 3 |
+
# CONTEXT & PERSONA
|
| 4 |
+
You are an elite autonomous agent designed to solve the GAIA benchmark. Your goal is to solve complex, multi-step problems that require reasoning, file processing, and internet research. You have access to a suite of powerful tools to interact with the world and process data.
|
| 5 |
+
|
| 6 |
+
# TOOL USAGE STRATEGY
|
| 7 |
+
1. **File First**: If the user mentions a file (e.g., "attached", "spreadsheet", "image"), YOU MUST PROCESS IT IMMEDIATELY.
|
| 8 |
+
- Look for file path information provided in the context.
|
| 9 |
+
- Select the specific tool for the file type (e.g., `read_excel` for .xlsx, `read_pdf` for .pdf).
|
| 10 |
+
- If the specific tool fails, fallback to the generic `read_file` tool.
|
| 11 |
+
- For images, use `analyze_image` with a specific question about what you need to extract from the image.
|
| 12 |
+
2. **Search Smart**: Pick the right tool for the source.
|
| 13 |
+
- **Wikipedia questions** (named "Wikipedia", or asking about Featured Articles, Olympics, historical events, biographies, etc.):
|
| 14 |
+
- If you can guess the exact page title from the question, call `wikipedia_page_fetch(title)` directly — this is the fastest path and returns the full page.
|
| 15 |
+
- Otherwise, call `wiki_search` once to find candidate titles, then `wikipedia_page_fetch` on the best one.
|
| 16 |
+
- Namespaced pages work too, e.g. `wikipedia_page_fetch("Wikipedia:Featured_article_candidates/Featured_log/November_2016")`.
|
| 17 |
+
- **General web research**: prefer `tavily_web_search` (cleaner, LLM-optimised snippets). Use `duck_web_search` only if Tavily fails.
|
| 18 |
+
- **When any search result gives you a specific URL**, call `fetch_webpage` to read the full page — do not loop on snippets.
|
| 19 |
+
- **Do not repeat queries** with trivial rewording. If a search did not help, switch tools or pivot (try a different angle, fetch a referenced page, or go Wikipedia-direct).
|
| 20 |
+
- **For any YouTube URL**, call `youtube_transcript(url)` to read the spoken content. Only transcript text is returned; if the question requires visual analysis of frames, state that the transcript does not contain the answer rather than guessing.
|
| 21 |
+
3. **Calculate Precisely**: Use the `calculator` tool for math. Note that `read_excel` and `read_csv` already include column statistics (sum, min, max, mean) — check those first before reaching for `calculator`.
|
| 22 |
+
4. **Run Code Directly**: If a question asks what a Python script outputs, use `python_eval` to execute it. Do not try to trace execution mentally.
|
| 23 |
+
5. **Don't Loop**: Each tool call has a cost. If you have already searched a topic twice without progress, escalate (fetch a specific page, switch to Wikipedia-direct, or commit to your best-supported answer).
|
| 24 |
+
|
| 25 |
+
# REASONING FRAMEWORK
|
| 26 |
+
Follow this Chain-of-Thought (CoT) process for every step:
|
| 27 |
+
1. **Analyze**: What is the core question? What data do I have? What is missing?
|
| 28 |
+
2. **Plan**: What is the next best tool to use? Why?
|
| 29 |
+
3. **Execute**: Call the tool.
|
| 30 |
+
4. **Observe**: Analyze the tool output. Does it answer the question?
|
| 31 |
+
5. **Refine**: If the output is insufficient, adjust the plan and try a different angle.
|
| 32 |
+
|
| 33 |
+
# FINAL ANSWER
|
| 34 |
+
When you have reached the answer, end your message with a single concluding line of the form:
|
| 35 |
+
|
| 36 |
+
FINAL ANSWER: <value>
|
| 37 |
+
|
| 38 |
+
A downstream formatter will normalise the value into the strict GAIA scoring format (no commas in numbers, no articles in strings, comma-separated lists, etc.) — so prioritise correctness of the value itself over format pedantry. Do not wrap the line in markdown, do not add commentary after it, and do not restate it.
|
| 39 |
+
|
| 40 |
+
# EXAMPLE SCENARIOS
|
| 41 |
+
- Question: "What is the sum of the 'Total' column in the attached file.xlsx?"
|
| 42 |
+
Thought: I need to read the excel file first using `read_excel`, then check the column stats it returns.
|
| 43 |
+
Action: `read_excel(file_path="...")`
|
| 44 |
+
Observation: (Dataframe + describe() stats…)
|
| 45 |
+
FINAL ANSWER: 700
|
| 46 |
+
|
| 47 |
+
- Question: "Which city is the capital of France?"
|
| 48 |
+
FINAL ANSWER: Paris
|
| 49 |
+
|
| 50 |
+
Take a deep breath and think step by step.
|
gaia/prompts/vlm_prompt.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: "GAIA VLM Assistant"
|
| 2 |
+
prompt: |
|
| 3 |
+
# ROLE
|
| 4 |
+
You are an expert visual assistant AI capable of analyzing images with high precision. Your goal is to extract visual information to answer a SPECIFIC question provided by the user.
|
| 5 |
+
|
| 6 |
+
# INSTRUCTION
|
| 7 |
+
- Analyze the provided image thoroughly.
|
| 8 |
+
- Focus specifically on the information needed to answer the user's question.
|
| 9 |
+
- Provide a clear, detailed description of the relevant visual elements.
|
| 10 |
+
- If the image contains text (documents, charts, screenshots), transcribe or summarize it accurately.
|
| 11 |
+
- Do NOT generate unrelated descriptions. Stick to the context of the question.
|
| 12 |
+
|
| 13 |
+
# FORMAT
|
| 14 |
+
- Start with a direct answer or observation related to the question.
|
| 15 |
+
- Follow with supporting details from the image.
|
gaia/states.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated, TypedDict, List
|
| 2 |
+
from langgraph.graph.message import add_messages
|
| 3 |
+
from langchain_core.messages import BaseMessage
|
| 4 |
+
from langchain_core.documents import Document
|
| 5 |
+
|
| 6 |
+
# ============================================
|
| 7 |
+
# State Definition
|
| 8 |
+
# ============================================
|
| 9 |
+
|
| 10 |
+
class AgentState(TypedDict):
|
| 11 |
+
"""
|
| 12 |
+
State schema for the GAIA agent graph.
|
| 13 |
+
|
| 14 |
+
Attributes:
|
| 15 |
+
messages: List of conversation messages (auto-accumulated via add_messages)
|
| 16 |
+
task_id: The GAIA task identifier for the current question
|
| 17 |
+
file_name: Name of the attached file (empty string if no file)
|
| 18 |
+
file_path: Local filesystem path to the downloaded file (empty if no file or download failed)
|
| 19 |
+
retrieved_docs: List of candidate documents from the retriever node
|
| 20 |
+
final_answer: GAIA-formatted answer produced by the formatter node
|
| 21 |
+
"""
|
| 22 |
+
messages: Annotated[list[BaseMessage], add_messages]
|
| 23 |
+
task_id: str
|
| 24 |
+
file_name: str
|
| 25 |
+
file_path: str
|
| 26 |
+
retrieved_docs: List[Document]
|
| 27 |
+
final_answer: str
|
gaia/tools/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GAIA agent tool roster. Importing this package exposes every tool and the aggregated `tools_list`."""
|
| 2 |
+
from gaia.tools.basic import calculator, python_eval
|
| 3 |
+
from gaia.tools.web import (
|
| 4 |
+
duck_web_search, tavily_web_search, wiki_search, wikipedia_page_fetch,
|
| 5 |
+
arxiv_search, fetch_webpage, youtube_transcript, retry_file_download,
|
| 6 |
+
)
|
| 7 |
+
from gaia.tools.files import (
|
| 8 |
+
read_pdf, read_docx, read_pptx, read_text_file,
|
| 9 |
+
read_csv, read_excel, read_jsonld, read_pdb,
|
| 10 |
+
read_python_file, extract_zip,
|
| 11 |
+
)
|
| 12 |
+
from gaia.tools.media import analyze_image, transcribe_audio
|
| 13 |
+
from gaia.tools.dispatcher import read_file
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
tools_list = [
|
| 17 |
+
calculator,
|
| 18 |
+
duck_web_search,
|
| 19 |
+
wiki_search,
|
| 20 |
+
wikipedia_page_fetch,
|
| 21 |
+
arxiv_search,
|
| 22 |
+
tavily_web_search,
|
| 23 |
+
fetch_webpage,
|
| 24 |
+
youtube_transcript,
|
| 25 |
+
retry_file_download,
|
| 26 |
+
python_eval,
|
| 27 |
+
read_pdf,
|
| 28 |
+
read_docx,
|
| 29 |
+
read_pptx,
|
| 30 |
+
read_text_file,
|
| 31 |
+
read_csv,
|
| 32 |
+
read_excel,
|
| 33 |
+
read_jsonld,
|
| 34 |
+
read_pdb,
|
| 35 |
+
transcribe_audio,
|
| 36 |
+
read_python_file,
|
| 37 |
+
extract_zip,
|
| 38 |
+
analyze_image,
|
| 39 |
+
read_file,
|
| 40 |
+
]
|
gaia/tools/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (1.13 kB). View file
|
|
|
gaia/tools/__pycache__/basic.cpython-313.pyc
ADDED
|
Binary file (2.87 kB). View file
|
|
|
gaia/tools/__pycache__/dispatcher.cpython-313.pyc
ADDED
|
Binary file (4.5 kB). View file
|
|
|
gaia/tools/__pycache__/files.cpython-313.pyc
ADDED
|
Binary file (13.8 kB). View file
|
|
|
gaia/tools/__pycache__/media.cpython-313.pyc
ADDED
|
Binary file (4.69 kB). View file
|
|
|
gaia/tools/__pycache__/web.cpython-313.pyc
ADDED
|
Binary file (10 kB). View file
|
|
|
gaia/tools/basic.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Basic computation tools: calculator and Python code execution."""
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
import tempfile
|
| 5 |
+
|
| 6 |
+
from langchain_core.tools import tool
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@tool
|
| 10 |
+
def calculator(a: float, b: float, type: str) -> float:
|
| 11 |
+
"""Performs mathematical calculations, addition, subtraction, multiplication, division, modulus.
|
| 12 |
+
Args:
|
| 13 |
+
a (float): first float number
|
| 14 |
+
b (float): second float number
|
| 15 |
+
type (str): the type of calculation to perform, can be addition, subtraction, multiplication, division, modulus
|
| 16 |
+
"""
|
| 17 |
+
if type == "addition":
|
| 18 |
+
return a + b
|
| 19 |
+
elif type == "subtraction":
|
| 20 |
+
return a - b
|
| 21 |
+
elif type == "multiplication":
|
| 22 |
+
return a * b
|
| 23 |
+
elif type == "division":
|
| 24 |
+
if b == 0:
|
| 25 |
+
raise ValueError("Cannot divide by zero.")
|
| 26 |
+
return a / b
|
| 27 |
+
elif type == "modulus":
|
| 28 |
+
return a % b
|
| 29 |
+
else:
|
| 30 |
+
raise TypeError(f"{type} is not an option for type, choose one of addition, subtraction, multiplication, division, modulus")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@tool
|
| 34 |
+
def python_eval(code: str) -> str:
|
| 35 |
+
"""
|
| 36 |
+
Execute a Python code snippet and return its stdout output.
|
| 37 |
+
Use this when a question asks what a script outputs, or when computation requires running code.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
code: Python source code to execute.
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
The stdout output of the code, or an error/timeout message.
|
| 44 |
+
"""
|
| 45 |
+
try:
|
| 46 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
| 47 |
+
f.write(code)
|
| 48 |
+
tmp_path = f.name
|
| 49 |
+
result = subprocess.run(
|
| 50 |
+
['python3', tmp_path],
|
| 51 |
+
capture_output=True, text=True, timeout=30
|
| 52 |
+
)
|
| 53 |
+
os.unlink(tmp_path)
|
| 54 |
+
if result.returncode == 0:
|
| 55 |
+
return f"Output:\n{result.stdout}"
|
| 56 |
+
return f"[python_eval] exit {result.returncode}:\n{result.stderr}"
|
| 57 |
+
except subprocess.TimeoutExpired:
|
| 58 |
+
return "[python_eval] execution timed out (30s limit)"
|
| 59 |
+
except Exception as e:
|
| 60 |
+
return f"[python_eval] failed: {e}"
|
gaia/tools/dispatcher.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generic file-reading dispatcher: routes a path to the correct extension-specific tool."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from langchain_core.tools import tool
|
| 5 |
+
|
| 6 |
+
from gaia.tools.files import (
|
| 7 |
+
read_pdf, read_docx, read_pptx, read_text_file,
|
| 8 |
+
read_csv, read_excel, read_jsonld, read_pdb,
|
| 9 |
+
read_python_file, extract_zip,
|
| 10 |
+
)
|
| 11 |
+
from gaia.tools.media import analyze_image, transcribe_audio
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@tool
|
| 15 |
+
def read_file(file_path: str) -> str:
|
| 16 |
+
"""
|
| 17 |
+
Automatically read a file based on its extension.
|
| 18 |
+
|
| 19 |
+
Supported formats: PDF, DOCX, PPTX, TXT, CSV, XLSX, JSON-LD, PDB, Python, ZIP, JPG, JPEG, PNG, MP3, WAV, FLAC, OGG, M4A
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
file_path: Path to the file to read.
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
The processed content of the file.
|
| 26 |
+
"""
|
| 27 |
+
ext = Path(file_path).suffix.lower()
|
| 28 |
+
|
| 29 |
+
processors = {
|
| 30 |
+
'.pdf': lambda p: read_pdf.invoke(p),
|
| 31 |
+
'.docx': lambda p: read_docx.invoke(p),
|
| 32 |
+
'.pptx': lambda p: read_pptx.invoke(p),
|
| 33 |
+
'.txt': lambda p: read_text_file.invoke(p),
|
| 34 |
+
'.csv': lambda p: read_csv.invoke(p),
|
| 35 |
+
'.xlsx': lambda p: read_excel.invoke(p),
|
| 36 |
+
'.jsonld': lambda p: read_jsonld.invoke(p),
|
| 37 |
+
'.pdb': lambda p: read_pdb.invoke(p),
|
| 38 |
+
'.py': lambda p: read_python_file.invoke(p),
|
| 39 |
+
'.mp3': lambda p: transcribe_audio.invoke(p),
|
| 40 |
+
'.wav': lambda p: transcribe_audio.invoke(p),
|
| 41 |
+
'.flac': lambda p: transcribe_audio.invoke(p),
|
| 42 |
+
'.ogg': lambda p: transcribe_audio.invoke(p),
|
| 43 |
+
'.m4a': lambda p: transcribe_audio.invoke(p),
|
| 44 |
+
'.zip': lambda p: extract_zip.invoke(p),
|
| 45 |
+
'.jpg': lambda p: analyze_image.invoke({"image_path": p, "question": "Describe this image in detail."}),
|
| 46 |
+
'.jpeg': lambda p: analyze_image.invoke({"image_path": p, "question": "Describe this image in detail."}),
|
| 47 |
+
'.png': lambda p: analyze_image.invoke({"image_path": p, "question": "Describe this image in detail."}),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
processor = processors.get(ext)
|
| 51 |
+
|
| 52 |
+
if processor:
|
| 53 |
+
return processor(file_path)
|
| 54 |
+
|
| 55 |
+
return f"[Unsupported file type: {ext}]"
|
gaia/tools/files.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""File / document / archive reading tools."""
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import zipfile
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from langchain_core.tools import tool
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@tool
|
| 11 |
+
def read_pdf(file_path: str) -> str:
|
| 12 |
+
"""
|
| 13 |
+
Extract text content from a PDF file.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
file_path: Path to the PDF file to read.
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
The text content of the PDF, with page separators.
|
| 20 |
+
"""
|
| 21 |
+
from pypdf import PdfReader
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
reader = PdfReader(file_path)
|
| 25 |
+
text = []
|
| 26 |
+
for i, page in enumerate(reader.pages):
|
| 27 |
+
page_text = page.extract_text()
|
| 28 |
+
if page_text:
|
| 29 |
+
text.append(f"--- Page {i+1} ---\n{page_text}")
|
| 30 |
+
|
| 31 |
+
return "\n\n".join(text) if text else "[Empty PDF]"
|
| 32 |
+
except Exception as e:
|
| 33 |
+
return f"[read_pdf] failed to read PDF: {e}"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@tool
|
| 37 |
+
def read_docx(file_path: str) -> str:
|
| 38 |
+
"""
|
| 39 |
+
Extract text content from a Word document (.docx).
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
file_path: Path to the Word document to read.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
The text content of the document.
|
| 46 |
+
"""
|
| 47 |
+
from docx import Document
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
doc = Document(file_path)
|
| 51 |
+
text_parts = []
|
| 52 |
+
|
| 53 |
+
paragraphs = [para.text for para in doc.paragraphs if para.text.strip()]
|
| 54 |
+
if paragraphs:
|
| 55 |
+
text_parts.append("\n".join(paragraphs))
|
| 56 |
+
|
| 57 |
+
for i, table in enumerate(doc.tables):
|
| 58 |
+
rows = [" | ".join(cell.text.strip() for cell in row.cells) for row in table.rows]
|
| 59 |
+
rows = [r for r in rows if r.strip()]
|
| 60 |
+
if rows:
|
| 61 |
+
text_parts.append(f"--- Table {i+1} ---\n" + "\n".join(rows))
|
| 62 |
+
|
| 63 |
+
return "\n\n".join(text_parts) if text_parts else "[Empty document]"
|
| 64 |
+
except Exception as e:
|
| 65 |
+
return f"[read_docx] failed to read DOCX: {e}"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@tool
|
| 69 |
+
def read_pptx(file_path: str) -> str:
|
| 70 |
+
"""
|
| 71 |
+
Extract text content from a PowerPoint presentation (.pptx).
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
file_path: Path to the PowerPoint file to read.
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
The text content from all slides.
|
| 78 |
+
"""
|
| 79 |
+
from pptx import Presentation
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
prs = Presentation(file_path)
|
| 83 |
+
text = []
|
| 84 |
+
|
| 85 |
+
for slide_num, slide in enumerate(prs.slides, 1):
|
| 86 |
+
slide_text = [f"--- Slide {slide_num} ---"]
|
| 87 |
+
for shape in slide.shapes:
|
| 88 |
+
if hasattr(shape, "text") and shape.text.strip():
|
| 89 |
+
slide_text.append(shape.text)
|
| 90 |
+
if len(slide_text) > 1:
|
| 91 |
+
text.append("\n".join(slide_text))
|
| 92 |
+
|
| 93 |
+
return "\n\n".join(text) if text else "[Empty presentation]"
|
| 94 |
+
except Exception as e:
|
| 95 |
+
return f"[read_pptx] failed to read PPTX: {e}"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@tool
|
| 99 |
+
def read_text_file(file_path: str) -> str:
|
| 100 |
+
"""
|
| 101 |
+
Read content from a plain text file (.txt).
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
file_path: Path to the text file to read.
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
The content of the text file.
|
| 108 |
+
"""
|
| 109 |
+
try:
|
| 110 |
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
| 111 |
+
return f.read()
|
| 112 |
+
except Exception as e:
|
| 113 |
+
return f"[read_text_file] failed: {e}"
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
@tool
|
| 117 |
+
def read_csv(file_path: str) -> str:
|
| 118 |
+
"""
|
| 119 |
+
Read and analyze a CSV file using polars.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
file_path: Path to the CSV file to read.
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
Summary of the CSV including schema, row count, and data preview.
|
| 126 |
+
"""
|
| 127 |
+
import polars as pl
|
| 128 |
+
|
| 129 |
+
try:
|
| 130 |
+
df = pl.read_csv(file_path)
|
| 131 |
+
|
| 132 |
+
output = f"CSV File — {len(df)} rows, {len(df.columns)} columns\n"
|
| 133 |
+
output += f"Columns: {df.columns}\n\n"
|
| 134 |
+
output += f"Column Statistics:\n{df.describe()}\n\n"
|
| 135 |
+
output += f"Data (first 20 rows):\n{df.head(20)}"
|
| 136 |
+
if len(df) <= 50:
|
| 137 |
+
output += f"\n\nComplete data:\n{df}"
|
| 138 |
+
return output
|
| 139 |
+
except Exception as e:
|
| 140 |
+
return f"[read_csv] failed to read CSV: {e}"
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@tool
|
| 144 |
+
def read_excel(file_path: str, sheet_id: int = 0) -> str:
|
| 145 |
+
"""
|
| 146 |
+
Read and analyze an Excel file (.xlsx) using polars.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
file_path: Path to the Excel file to read.
|
| 150 |
+
sheet_id: The sheet index to read (0-based). Default is 0 (first sheet).
|
| 151 |
+
|
| 152 |
+
Returns:
|
| 153 |
+
Summary of the Excel sheet including schema, row count, and data preview.
|
| 154 |
+
"""
|
| 155 |
+
import polars as pl
|
| 156 |
+
import openpyxl
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
wb = openpyxl.load_workbook(file_path, read_only=True)
|
| 160 |
+
sheet_names = wb.sheetnames
|
| 161 |
+
wb.close()
|
| 162 |
+
except Exception:
|
| 163 |
+
sheet_names = []
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
df = pl.read_excel(file_path, sheet_id=sheet_id)
|
| 167 |
+
sheet_label = sheet_names[sheet_id] if sheet_id < len(sheet_names) else str(sheet_id)
|
| 168 |
+
|
| 169 |
+
output = f"Excel File — Available sheets: {sheet_names}\n\n"
|
| 170 |
+
output += f"Sheet {sheet_id} ('{sheet_label}') — {len(df)} rows, {len(df.columns)} columns\n"
|
| 171 |
+
output += f"Columns: {df.columns}\n\n"
|
| 172 |
+
output += f"Column Statistics:\n{df.describe()}\n\n"
|
| 173 |
+
output += f"Data (first 20 rows):\n{df.head(20)}"
|
| 174 |
+
if len(df) <= 50:
|
| 175 |
+
output += f"\n\nComplete data:\n{df}"
|
| 176 |
+
return output
|
| 177 |
+
except Exception as e:
|
| 178 |
+
return f"[read_excel] failed to read Excel: {e}"
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@tool
|
| 182 |
+
def read_jsonld(file_path: str) -> str:
|
| 183 |
+
"""
|
| 184 |
+
Read and parse a JSON-LD file.
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
file_path: Path to the JSON-LD file to read.
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
The formatted JSON content.
|
| 191 |
+
"""
|
| 192 |
+
try:
|
| 193 |
+
with open(file_path, 'r') as f:
|
| 194 |
+
data = json.load(f)
|
| 195 |
+
return f"JSON-LD Content:\n{json.dumps(data, indent=2)}"
|
| 196 |
+
except Exception as e:
|
| 197 |
+
return f"[read_jsonld] failed to read JSON-LD: {e}"
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@tool
|
| 201 |
+
def read_pdb(file_path: str) -> str:
|
| 202 |
+
"""
|
| 203 |
+
Read and analyze a PDB (Protein Data Bank) file for protein structure analysis.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
file_path: Path to the PDB file to read.
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
Analysis of the protein structure including atoms, chains, and coordinates.
|
| 210 |
+
"""
|
| 211 |
+
from Bio.PDB import PDBParser
|
| 212 |
+
import numpy as np
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
parser = PDBParser(QUIET=True)
|
| 216 |
+
structure = parser.get_structure("protein", file_path)
|
| 217 |
+
|
| 218 |
+
info = ["=== PDB Structure Analysis ==="]
|
| 219 |
+
|
| 220 |
+
atoms = list(structure.get_atoms())
|
| 221 |
+
info.append(f"Total atoms: {len(atoms)}")
|
| 222 |
+
|
| 223 |
+
for model in structure:
|
| 224 |
+
info.append(f"\nModel {model.id}:")
|
| 225 |
+
for chain in model:
|
| 226 |
+
residues = list(chain.get_residues())
|
| 227 |
+
info.append(f" Chain {chain.id}: {len(residues)} residues")
|
| 228 |
+
|
| 229 |
+
if len(atoms) >= 2:
|
| 230 |
+
info.append("\nFirst atoms (for distance calculations):")
|
| 231 |
+
for i, atom in enumerate(atoms[:5]):
|
| 232 |
+
coord = atom.get_coord()
|
| 233 |
+
info.append(
|
| 234 |
+
f" Atom {i+1}: {atom.get_name()} at "
|
| 235 |
+
f"[{coord[0]:.4f}, {coord[1]:.4f}, {coord[2]:.4f}]"
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
dist = np.linalg.norm(atoms[0].get_coord() - atoms[1].get_coord())
|
| 239 |
+
info.append(f"\nDistance between first two atoms: {dist:.4f} Angstroms")
|
| 240 |
+
|
| 241 |
+
return "\n".join(info)
|
| 242 |
+
except Exception as e:
|
| 243 |
+
return f"[read_pdb] failed to read PDB: {e}"
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
@tool
|
| 247 |
+
def read_python_file(file_path: str) -> str:
|
| 248 |
+
"""
|
| 249 |
+
Read a Python source code file.
|
| 250 |
+
|
| 251 |
+
Args:
|
| 252 |
+
file_path: Path to the Python file to read.
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
The Python code content.
|
| 256 |
+
"""
|
| 257 |
+
try:
|
| 258 |
+
with open(file_path, 'r') as f:
|
| 259 |
+
code = f.read()
|
| 260 |
+
return f"Python Code:\n```python\n{code}\n```"
|
| 261 |
+
except Exception as e:
|
| 262 |
+
return f"[read_python_file] failed: {e}"
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
@tool
|
| 266 |
+
def extract_zip(file_path: str) -> str:
|
| 267 |
+
"""
|
| 268 |
+
Extract a ZIP archive and list its contents.
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
file_path: Path to the ZIP file to extract.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
List of files extracted from the archive with their paths.
|
| 275 |
+
"""
|
| 276 |
+
try:
|
| 277 |
+
extract_dir = Path(file_path).parent / Path(file_path).stem
|
| 278 |
+
extract_dir.mkdir(exist_ok=True)
|
| 279 |
+
|
| 280 |
+
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
| 281 |
+
zip_ref.extractall(extract_dir)
|
| 282 |
+
|
| 283 |
+
results = [f"ZIP Archive extracted to: {extract_dir}\n\nContents:"]
|
| 284 |
+
|
| 285 |
+
for root, dirs, files in os.walk(extract_dir):
|
| 286 |
+
for file in files:
|
| 287 |
+
full_path = os.path.join(root, file)
|
| 288 |
+
rel_path = os.path.relpath(full_path, extract_dir)
|
| 289 |
+
file_size = os.path.getsize(full_path)
|
| 290 |
+
results.append(f" - {rel_path} ({file_size} bytes)")
|
| 291 |
+
|
| 292 |
+
results.append(f"\nUse the appropriate read tool on the extracted files at: {extract_dir}/")
|
| 293 |
+
|
| 294 |
+
return "\n".join(results)
|
| 295 |
+
except Exception as e:
|
| 296 |
+
return f"[extract_zip] failed: {e}"
|
gaia/tools/media.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multi-modal tools: image analysis (VLM) and audio transcription (ASR)."""
|
| 2 |
+
import base64
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
from huggingface_hub import InferenceClient
|
| 11 |
+
from langchain_core.tools import tool
|
| 12 |
+
|
| 13 |
+
from gaia.utils import load_config, load_prompt
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
_PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"
|
| 17 |
+
|
| 18 |
+
_config = load_config()
|
| 19 |
+
_vlm_model_name = _config["models"]["vlm"]["model_name"]
|
| 20 |
+
_vlm_system_prompt = load_prompt(str(_PROMPTS_DIR / "vlm_prompt.yaml")).content
|
| 21 |
+
_asr_model_name = _config["models"]["asr"]["model_name"]
|
| 22 |
+
_hf_client = InferenceClient(token=os.getenv("HF_INFERENCE_KEY"))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@tool
|
| 26 |
+
def analyze_image(image_path: str, question: str) -> str:
|
| 27 |
+
"""
|
| 28 |
+
Analyze an image using a Vision Language Model (VLM) to answer a specific question.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
image_path: Path to the image file (JPG, PNG).
|
| 32 |
+
question: The specific question to answer about the image.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
A detailed description or answer based on the visual content.
|
| 36 |
+
"""
|
| 37 |
+
try:
|
| 38 |
+
if not os.path.exists(image_path):
|
| 39 |
+
return f"[analyze_image] image file not found at {image_path}"
|
| 40 |
+
|
| 41 |
+
with open(image_path, "rb") as img_file:
|
| 42 |
+
image_data = base64.b64encode(img_file.read()).decode("utf-8")
|
| 43 |
+
ext = Path(image_path).suffix.lower().lstrip(".")
|
| 44 |
+
mime_type = "image/jpeg" if ext in ("jpg", "jpeg") else f"image/{ext}"
|
| 45 |
+
image_url = f"data:{mime_type};base64,{image_data}"
|
| 46 |
+
|
| 47 |
+
messages = [
|
| 48 |
+
{
|
| 49 |
+
"role": "user",
|
| 50 |
+
"content": [
|
| 51 |
+
{"type": "image_url", "image_url": {"url": image_url}},
|
| 52 |
+
{"type": "text", "text": f"{_vlm_system_prompt}\n\nQuestion: {question}"}
|
| 53 |
+
]
|
| 54 |
+
}
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
output = _hf_client.chat_completion(
|
| 58 |
+
messages=messages,
|
| 59 |
+
model=_vlm_model_name,
|
| 60 |
+
max_tokens=1000
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
return output.choices[0].message.content
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
return f"[analyze_image] VLM call failed: {e}"
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@tool
|
| 70 |
+
def transcribe_audio(file_path: str) -> str:
|
| 71 |
+
"""
|
| 72 |
+
Transcribe an audio file (MP3, WAV, etc.) to text using Whisper.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
file_path: Path to the audio file to transcribe.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
The transcribed text from the audio, or a detailed `[transcribe_audio] ...`
|
| 79 |
+
error string identifying file path, size, model, and exception class+message.
|
| 80 |
+
"""
|
| 81 |
+
if not os.path.exists(file_path):
|
| 82 |
+
return f"[transcribe_audio] file not found at {file_path}"
|
| 83 |
+
file_size = os.path.getsize(file_path)
|
| 84 |
+
if file_size == 0:
|
| 85 |
+
return f"[transcribe_audio] file is empty at {file_path}"
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
with open(file_path, "rb") as f:
|
| 89 |
+
audio_bytes = f.read()
|
| 90 |
+
result = _hf_client.automatic_speech_recognition(audio=audio_bytes, model=_asr_model_name)
|
| 91 |
+
return f"Audio Transcription:\n{result.text}"
|
| 92 |
+
except Exception as e:
|
| 93 |
+
return (
|
| 94 |
+
f"[transcribe_audio] ASR call failed for {file_path} ({file_size} bytes) "
|
| 95 |
+
f"with model '{_asr_model_name}': {type(e).__name__}: {e}"
|
| 96 |
+
)
|
gaia/tools/web.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Web search and fetching tools: DuckDuckGo, Tavily, Wikipedia, Arxiv, webpage fetch, YouTube transcripts."""
|
| 2 |
+
from langchain_community.tools import DuckDuckGoSearchRun
|
| 3 |
+
from langchain_community.tools.tavily_search import TavilySearchResults
|
| 4 |
+
from langchain_community.document_loaders import WikipediaLoader, ArxivLoader
|
| 5 |
+
from langchain_core.tools import tool
|
| 6 |
+
|
| 7 |
+
from gaia.utils import extract_youtube_id, load_config, download_task_file
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
_ddg_search = None
|
| 11 |
+
_tavily_search = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _get_ddg():
|
| 15 |
+
global _ddg_search
|
| 16 |
+
if _ddg_search is None:
|
| 17 |
+
_ddg_search = DuckDuckGoSearchRun()
|
| 18 |
+
return _ddg_search
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _get_tavily():
|
| 22 |
+
global _tavily_search
|
| 23 |
+
if _tavily_search is None:
|
| 24 |
+
_tavily_search = TavilySearchResults(max_results=3)
|
| 25 |
+
return _tavily_search
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@tool
|
| 29 |
+
def duck_web_search(query: str) -> str:
|
| 30 |
+
"""Use DuckDuckGo to search the web.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
query: The search query.
|
| 34 |
+
"""
|
| 35 |
+
search = _get_ddg().invoke(input=query)
|
| 36 |
+
return {"duckduckgo_web_search": search}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@tool
|
| 40 |
+
def wiki_search(query: str) -> str:
|
| 41 |
+
"""Search Wikipedia for a query and return maximum 3 results.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
query: The search query."""
|
| 45 |
+
documents = WikipediaLoader(query=query, load_max_docs=3, doc_content_chars_max=20000).load()
|
| 46 |
+
processed_documents = "\n\n---\n\n".join(
|
| 47 |
+
[
|
| 48 |
+
f'Document title: {document.metadata.get("title", "")}. Summary: {document.metadata.get("summary", "")}. Documents details: {document.page_content}'
|
| 49 |
+
for document in documents
|
| 50 |
+
])
|
| 51 |
+
return {"wiki_results": processed_documents}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@tool
|
| 55 |
+
def wikipedia_page_fetch(title: str) -> str:
|
| 56 |
+
"""Fetch the full text of a Wikipedia page by its exact title.
|
| 57 |
+
|
| 58 |
+
Use this when you can guess the page title from the question (e.g., "1928 Summer
|
| 59 |
+
Olympics", "List of Featured Articles"). Faster than search + fetch_webpage when
|
| 60 |
+
the title is obvious. Returns full page content, not a search summary.
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
title: The exact Wikipedia page title (including namespace prefix if applicable,
|
| 64 |
+
e.g., "Wikipedia:Featured_article_candidates/Featured_log/November_2016").
|
| 65 |
+
|
| 66 |
+
Returns:
|
| 67 |
+
The page content prefixed with title and URL, or a `[wikipedia_page_fetch] ...`
|
| 68 |
+
error string.
|
| 69 |
+
"""
|
| 70 |
+
import wikipedia
|
| 71 |
+
try:
|
| 72 |
+
page = wikipedia.page(title, auto_suggest=False)
|
| 73 |
+
return f"Wikipedia: {page.title}\nURL: {page.url}\n\n{page.content}"
|
| 74 |
+
except wikipedia.exceptions.DisambiguationError as e:
|
| 75 |
+
return f"[wikipedia_page_fetch] '{title}' is a disambiguation page. Options: {e.options[:10]}"
|
| 76 |
+
except wikipedia.exceptions.PageError:
|
| 77 |
+
return f"[wikipedia_page_fetch] page not found: '{title}'. Try wiki_search to find the correct title."
|
| 78 |
+
except Exception as e:
|
| 79 |
+
return f"[wikipedia_page_fetch] failed: {e}"
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@tool
|
| 83 |
+
def arxiv_search(query: str) -> str:
|
| 84 |
+
"""Search Arxiv for a query and return maximum 3 result.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
query: The search query."""
|
| 88 |
+
documents = ArxivLoader(query=query, load_max_docs=3).load()
|
| 89 |
+
processed_documents = "\n\n---\n\n".join(
|
| 90 |
+
[
|
| 91 |
+
f'Document title: {document.metadata.get("title", "")}. Summary: {document.metadata.get("summary", "")}. Documents details: {document.page_content}'
|
| 92 |
+
for document in documents
|
| 93 |
+
])
|
| 94 |
+
return {"arxiv_results": processed_documents}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@tool
|
| 98 |
+
def tavily_web_search(query: str) -> str:
|
| 99 |
+
"""Search the web using Tavily for a query and return maximum 3 results.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
query: The search query."""
|
| 103 |
+
search_documents = _get_tavily().invoke(input=query)
|
| 104 |
+
web_results = "\n\n---\n\n".join(
|
| 105 |
+
[
|
| 106 |
+
f'Document title: {document["title"]}. Contents: {document["content"]}. Relevance Score: {document["score"]}'
|
| 107 |
+
for document in search_documents
|
| 108 |
+
])
|
| 109 |
+
return {"web_results": web_results}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@tool
|
| 113 |
+
def fetch_webpage(url: str) -> str:
|
| 114 |
+
"""
|
| 115 |
+
Fetch and extract the main text content from a webpage.
|
| 116 |
+
Use this when a search result points to a specific URL you need to read in full.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
url: The full URL of the page to fetch.
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
The extracted text content of the page.
|
| 123 |
+
"""
|
| 124 |
+
import trafilatura
|
| 125 |
+
try:
|
| 126 |
+
downloaded = trafilatura.fetch_url(url)
|
| 127 |
+
if downloaded is None:
|
| 128 |
+
return f"[fetch_webpage] could not fetch {url}"
|
| 129 |
+
text = trafilatura.extract(downloaded, include_tables=True, include_links=False)
|
| 130 |
+
if text is None:
|
| 131 |
+
return f"[fetch_webpage] could not extract content from {url}"
|
| 132 |
+
return f"Page content from {url}:\n\n{text}"
|
| 133 |
+
except Exception as e:
|
| 134 |
+
return f"[fetch_webpage] failed: {e}"
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@tool
|
| 138 |
+
def retry_file_download(task_id: str, file_name: str) -> str:
|
| 139 |
+
"""Retry downloading the task file from the GAIA scoring API.
|
| 140 |
+
|
| 141 |
+
Use this when the initial automatic download failed (you will see a message like
|
| 142 |
+
"the automatic download failed" in the question context). Returns the local path
|
| 143 |
+
on success, or an error string starting with `[retry_file_download]`.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
task_id: The task ID for the current question.
|
| 147 |
+
file_name: The original file name from the question metadata.
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
Local filesystem path to the downloaded file, or an error description.
|
| 151 |
+
"""
|
| 152 |
+
cfg = load_config()
|
| 153 |
+
local_path, err = download_task_file(
|
| 154 |
+
task_id=task_id,
|
| 155 |
+
file_name=file_name,
|
| 156 |
+
base_url=cfg["api"]["base_url"],
|
| 157 |
+
files_dir=cfg["api"]["files_dir"],
|
| 158 |
+
)
|
| 159 |
+
if local_path:
|
| 160 |
+
return local_path
|
| 161 |
+
return f"[retry_file_download] {err}"
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@tool
|
| 165 |
+
def youtube_transcript(url: str) -> str:
|
| 166 |
+
"""Fetch the transcript (captions) of a YouTube video as plain text.
|
| 167 |
+
|
| 168 |
+
Use this whenever a question references a YouTube URL — the spoken content of
|
| 169 |
+
the video is available via captions. Note: this returns text only; questions
|
| 170 |
+
that require visual analysis of the frames cannot be answered from the
|
| 171 |
+
transcript alone.
|
| 172 |
+
|
| 173 |
+
Prefers manually-written English captions; falls back to auto-generated English,
|
| 174 |
+
and finally to any available language.
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
url: The full YouTube URL (watch, youtu.be, embed, shorts) or a bare 11-char video ID.
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
The concatenated transcript text, or an error string starting with `[youtube_transcript]`.
|
| 181 |
+
"""
|
| 182 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
| 183 |
+
from youtube_transcript_api._errors import (
|
| 184 |
+
TranscriptsDisabled, NoTranscriptFound, VideoUnavailable,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
video_id = extract_youtube_id(url)
|
| 188 |
+
if not video_id:
|
| 189 |
+
return f"[youtube_transcript] could not parse video ID from: {url}"
|
| 190 |
+
|
| 191 |
+
try:
|
| 192 |
+
ytt_api = YouTubeTranscriptApi()
|
| 193 |
+
try:
|
| 194 |
+
fetched = ytt_api.fetch(video_id, languages=['en'])
|
| 195 |
+
except NoTranscriptFound:
|
| 196 |
+
transcript_list = ytt_api.list(video_id)
|
| 197 |
+
transcript = next(iter(transcript_list))
|
| 198 |
+
fetched = transcript.fetch()
|
| 199 |
+
|
| 200 |
+
text = " ".join(snippet.text for snippet in fetched)
|
| 201 |
+
return f"YouTube transcript for {url}:\n\n{text}"
|
| 202 |
+
except TranscriptsDisabled:
|
| 203 |
+
return f"[youtube_transcript] transcripts are disabled for {url}"
|
| 204 |
+
except VideoUnavailable:
|
| 205 |
+
return f"[youtube_transcript] video unavailable: {url}"
|
| 206 |
+
except NoTranscriptFound:
|
| 207 |
+
return f"[youtube_transcript] no transcript found for {url}"
|
| 208 |
+
except Exception as e:
|
| 209 |
+
return f"[youtube_transcript] failed: {e}"
|
gaia/utils.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
import bm25s
|
| 6 |
+
import requests
|
| 7 |
+
import yaml
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from langchain_core.messages import SystemMessage
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
_YOUTUBE_ID_RE = re.compile(
|
| 13 |
+
r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/|youtube\.com/v/|youtube\.com/shorts/)([\w-]{11})'
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def extract_youtube_id(url: str) -> str | None:
|
| 18 |
+
"""Pull the 11-char video ID from any common YouTube URL form, or accept a bare ID."""
|
| 19 |
+
m = _YOUTUBE_ID_RE.search(url)
|
| 20 |
+
if m:
|
| 21 |
+
return m.group(1)
|
| 22 |
+
if re.fullmatch(r'[\w-]{11}', url.strip()):
|
| 23 |
+
return url.strip()
|
| 24 |
+
return None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
_FINAL_ANSWER_RE = re.compile(r'FINAL ANSWER:\s*(.*)', re.DOTALL | re.IGNORECASE)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def extract_final_answer(content: str) -> str:
|
| 31 |
+
"""Pull the value after 'FINAL ANSWER:' (case-insensitive), or return the content stripped."""
|
| 32 |
+
content = content or ""
|
| 33 |
+
m = _FINAL_ANSWER_RE.search(content)
|
| 34 |
+
return (m.group(1) if m else content).strip()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load_config(path="config.yaml"):
|
| 38 |
+
with open(path, "r") as f:
|
| 39 |
+
return yaml.safe_load(f)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def download_task_file(
|
| 43 |
+
task_id: str,
|
| 44 |
+
file_name: str,
|
| 45 |
+
base_url: str,
|
| 46 |
+
files_dir: str,
|
| 47 |
+
max_retries: int = 3,
|
| 48 |
+
timeout: int = 30,
|
| 49 |
+
) -> tuple[str | None, str]:
|
| 50 |
+
"""Download a task file from the GAIA scoring API.
|
| 51 |
+
|
| 52 |
+
Returns (local_path, error_message). On success the error string is empty;
|
| 53 |
+
on failure local_path is None and the error string says what went wrong.
|
| 54 |
+
Retries on 5xx and network errors with exponential backoff (2s, 4s); 4xx
|
| 55 |
+
is treated as a definitive "no file" answer and not retried.
|
| 56 |
+
"""
|
| 57 |
+
if not task_id or not file_name:
|
| 58 |
+
return None, "missing task_id or file_name"
|
| 59 |
+
|
| 60 |
+
safe_name = Path(file_name).name
|
| 61 |
+
if not safe_name:
|
| 62 |
+
return None, f"invalid file_name '{file_name}'"
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
save_dir = Path(files_dir) / task_id
|
| 66 |
+
save_dir.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return None, f"could not create cache dir: {e}"
|
| 69 |
+
|
| 70 |
+
local_path = save_dir / safe_name
|
| 71 |
+
if local_path.exists() and local_path.stat().st_size > 0:
|
| 72 |
+
return str(local_path), ""
|
| 73 |
+
|
| 74 |
+
url = f"{base_url}/files/{task_id}"
|
| 75 |
+
last_err = ""
|
| 76 |
+
for attempt in range(1, max_retries + 1):
|
| 77 |
+
try:
|
| 78 |
+
response = requests.get(url, timeout=timeout)
|
| 79 |
+
status = response.status_code
|
| 80 |
+
if status >= 500:
|
| 81 |
+
last_err = f"HTTP {status} (attempt {attempt}/{max_retries})"
|
| 82 |
+
if attempt < max_retries:
|
| 83 |
+
time.sleep(2 * attempt)
|
| 84 |
+
continue
|
| 85 |
+
if status >= 400:
|
| 86 |
+
return None, f"HTTP {status} from {url}"
|
| 87 |
+
if not response.content:
|
| 88 |
+
last_err = f"empty body (attempt {attempt}/{max_retries})"
|
| 89 |
+
if attempt < max_retries:
|
| 90 |
+
time.sleep(2 * attempt)
|
| 91 |
+
continue
|
| 92 |
+
local_path.write_bytes(response.content)
|
| 93 |
+
return str(local_path), ""
|
| 94 |
+
except requests.RequestException as e:
|
| 95 |
+
last_err = f"{type(e).__name__}: {e} (attempt {attempt}/{max_retries})"
|
| 96 |
+
if attempt < max_retries:
|
| 97 |
+
time.sleep(2 * attempt)
|
| 98 |
+
except Exception as e:
|
| 99 |
+
return None, f"unexpected error: {type(e).__name__}: {e}"
|
| 100 |
+
|
| 101 |
+
return None, f"all {max_retries} attempts failed; last: {last_err}"
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def load_prompt(prompt_location: str) -> SystemMessage:
|
| 105 |
+
"""Load system prompt from YAML file."""
|
| 106 |
+
with open(prompt_location) as f:
|
| 107 |
+
try:
|
| 108 |
+
prompt = yaml.safe_load(f)["prompt"]
|
| 109 |
+
return SystemMessage(content=prompt)
|
| 110 |
+
except yaml.YAMLError as exc:
|
| 111 |
+
print(exc)
|
| 112 |
+
return SystemMessage(content="You are a helpful assistant.")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def init_bm25_index(corpus_file = "data/metadata.jsonl"):
|
| 117 |
+
"""BM25 Index Initialization (Local Corpus)"""
|
| 118 |
+
try:
|
| 119 |
+
if not os.path.exists(corpus_file):
|
| 120 |
+
print(f"Warning: {corpus_file} not found. BM25 will use empty index.")
|
| 121 |
+
return None, [], []
|
| 122 |
+
|
| 123 |
+
search_texts = [] # question-only — used for BM25 indexing
|
| 124 |
+
corpus_texts = [] # Q+A+Steps — returned for context injection
|
| 125 |
+
corpus_ids = []
|
| 126 |
+
with open(corpus_file, "r") as f:
|
| 127 |
+
for line in f:
|
| 128 |
+
item = json.loads(line)
|
| 129 |
+
question = item.get('Question', '')
|
| 130 |
+
answer = item.get('Final answer', '')
|
| 131 |
+
steps = item.get('Annotator Metadata', {}).get('Steps', '')
|
| 132 |
+
search_texts.append(question)
|
| 133 |
+
parts = [f"Question: {question}"]
|
| 134 |
+
if answer:
|
| 135 |
+
parts.append(f"Final Answer: {answer}")
|
| 136 |
+
if steps:
|
| 137 |
+
parts.append(f"Solution Steps: {steps}")
|
| 138 |
+
corpus_texts.append("\n".join(parts))
|
| 139 |
+
corpus_ids.append(item.get('task_id', ''))
|
| 140 |
+
|
| 141 |
+
corpus_tokens = bm25s.tokenize(search_texts, stopwords="en", stemmer=None)
|
| 142 |
+
|
| 143 |
+
retriever_bm25 = bm25s.BM25()
|
| 144 |
+
retriever_bm25.index(corpus_tokens)
|
| 145 |
+
|
| 146 |
+
print(f"BM25 Index initialized with {len(corpus_texts)} documents.")
|
| 147 |
+
return retriever_bm25, corpus_texts, corpus_ids
|
| 148 |
+
except Exception as e:
|
| 149 |
+
print(f"Error initializing BM25: {e}")
|
| 150 |
+
return None, [], []
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def reciprocal_rank_fusion(results: list[list[dict]], k=60) -> list[tuple[dict, float]]:
|
| 154 |
+
"""
|
| 155 |
+
Fuse multiple ranked lists using Reciprocal Rank Fusion (RRF).
|
| 156 |
+
"""
|
| 157 |
+
fused_scores = {}
|
| 158 |
+
|
| 159 |
+
for rank_list in results:
|
| 160 |
+
for rank, doc in enumerate(rank_list):
|
| 161 |
+
doc_id = doc["metadata"]["task_id"]
|
| 162 |
+
doc_content = doc["content"]
|
| 163 |
+
if doc_id not in fused_scores:
|
| 164 |
+
fused_scores[doc_id] = {"id": doc_id, "content": doc_content, "score": 0.0}
|
| 165 |
+
fused_scores[doc_id]["score"] += 1.0 / (k + rank + 1)
|
| 166 |
+
|
| 167 |
+
sorted_results = sorted(fused_scores.values(), key=lambda x: x["score"], reverse=True)
|
| 168 |
+
return [(item["id"], item["content"], item["score"]) for item in sorted_results]
|
notebooks/create_vector_database.ipynb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "a9f7a25f",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [],
|
| 9 |
+
"source": [
|
| 10 |
+
"# Loading environment variables and initializing Supabase client and SentenceTransformer model\n",
|
| 11 |
+
"import os\n",
|
| 12 |
+
"import json\n",
|
| 13 |
+
"from dotenv import load_dotenv\n",
|
| 14 |
+
"from supabase.client import Client, create_client\n",
|
| 15 |
+
"from sentence_transformers import SentenceTransformer\n",
|
| 16 |
+
"from utils import load_config\n",
|
| 17 |
+
"\n",
|
| 18 |
+
"load_dotenv()\n",
|
| 19 |
+
"\n",
|
| 20 |
+
"config = load_config()\n",
|
| 21 |
+
"data = config[\"data\"]\n",
|
| 22 |
+
"\n",
|
| 23 |
+
"supabase_url = os.getenv(\"SUPABASE_URL\")\n",
|
| 24 |
+
"supabase_key = os.getenv(\"SUPABASE_SERVICE_KEY\")\n",
|
| 25 |
+
"\n",
|
| 26 |
+
"supabase: Client = create_client(supabase_url, supabase_key)\n",
|
| 27 |
+
"embeddings = SentenceTransformer(model_name_or_path=config[\"vector_store\"][\"embedding_model_name\"], cache_folder=config[\"models\"][\"cache_folder\"])"
|
| 28 |
+
]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"cell_type": "code",
|
| 32 |
+
"execution_count": 2,
|
| 33 |
+
"id": "f2c5492b",
|
| 34 |
+
"metadata": {},
|
| 35 |
+
"outputs": [
|
| 36 |
+
{
|
| 37 |
+
"name": "stderr",
|
| 38 |
+
"output_type": "stream",
|
| 39 |
+
"text": [
|
| 40 |
+
"/home/kpatelis/projects/gaia/.venv/lib/python3.13/site-packages/torch/_dynamo/guards.py:1114: RuntimeWarning: Guards may run slower on Python 3.13.0. Consider upgrading to Python 3.13.1+.\n",
|
| 41 |
+
" warnings.warn(\n",
|
| 42 |
+
"/home/kpatelis/projects/gaia/.venv/lib/python3.13/site-packages/torch/_dynamo/guards.py:1114: RuntimeWarning: Guards may run slower on Python 3.13.0. Consider upgrading to Python 3.13.1+.\n",
|
| 43 |
+
" warnings.warn(\n"
|
| 44 |
+
]
|
| 45 |
+
}
|
| 46 |
+
],
|
| 47 |
+
"source": [
|
| 48 |
+
"# Reading JSONL file and creating documents with embeddings\n",
|
| 49 |
+
"with open(data, 'r') as jsonl_file:\n",
|
| 50 |
+
" json_list = list(jsonl_file)\n",
|
| 51 |
+
"\n",
|
| 52 |
+
"documents = []\n",
|
| 53 |
+
"for json_str in json_list:\n",
|
| 54 |
+
" json_data = json.loads(json_str)\n",
|
| 55 |
+
" content = f\"{json_data['Question']}\"\n",
|
| 56 |
+
" embedding = embeddings.encode(content, normalize_embeddings=True).tolist()\n",
|
| 57 |
+
" document = {\n",
|
| 58 |
+
" \"content\": content,\n",
|
| 59 |
+
" \"metadata\": {\n",
|
| 60 |
+
" \"source\": \"vector_search\",\n",
|
| 61 |
+
" \"task_id\": json_data['task_id']\n",
|
| 62 |
+
" },\n",
|
| 63 |
+
" \"embedding\": embedding,\n",
|
| 64 |
+
" }\n",
|
| 65 |
+
" documents.append(document)"
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"cell_type": "code",
|
| 70 |
+
"execution_count": 3,
|
| 71 |
+
"id": "26ddbafd",
|
| 72 |
+
"metadata": {},
|
| 73 |
+
"outputs": [],
|
| 74 |
+
"source": [
|
| 75 |
+
"# Inserting documents into Supabase\n",
|
| 76 |
+
"\n",
|
| 77 |
+
"# Note1: pgvector needs to be enabled, to turn to vector database\n",
|
| 78 |
+
"# Note2: Table needs to be created beforehand in Supabase, with column types\n",
|
| 79 |
+
"try:\n",
|
| 80 |
+
" response = (\n",
|
| 81 |
+
" supabase.table(\"gaia_documents\")\n",
|
| 82 |
+
" .insert(documents)\n",
|
| 83 |
+
" .execute()\n",
|
| 84 |
+
" )\n",
|
| 85 |
+
"except Exception as exception:\n",
|
| 86 |
+
" print(\"Error inserting data into Supabase:\", exception)"
|
| 87 |
+
]
|
| 88 |
+
}
|
| 89 |
+
],
|
| 90 |
+
"metadata": {
|
| 91 |
+
"kernelspec": {
|
| 92 |
+
"display_name": "gaia",
|
| 93 |
+
"language": "python",
|
| 94 |
+
"name": "python3"
|
| 95 |
+
},
|
| 96 |
+
"language_info": {
|
| 97 |
+
"codemirror_mode": {
|
| 98 |
+
"name": "ipython",
|
| 99 |
+
"version": 3
|
| 100 |
+
},
|
| 101 |
+
"file_extension": ".py",
|
| 102 |
+
"mimetype": "text/x-python",
|
| 103 |
+
"name": "python",
|
| 104 |
+
"nbconvert_exporter": "python",
|
| 105 |
+
"pygments_lexer": "ipython3",
|
| 106 |
+
"version": "3.13.0"
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"nbformat": 4,
|
| 110 |
+
"nbformat_minor": 5
|
| 111 |
+
}
|
notebooks/test.ipynb
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "5b9902eb",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [
|
| 9 |
+
{
|
| 10 |
+
"data": {
|
| 11 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 12 |
+
"model_id": "903fa846aba0401ba19cada8e6c8822c",
|
| 13 |
+
"version_major": 2,
|
| 14 |
+
"version_minor": 0
|
| 15 |
+
},
|
| 16 |
+
"text/plain": [
|
| 17 |
+
"Split strings: 0%| | 0/165 [00:00<?, ?it/s]"
|
| 18 |
+
]
|
| 19 |
+
},
|
| 20 |
+
"metadata": {},
|
| 21 |
+
"output_type": "display_data"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"data": {
|
| 25 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 26 |
+
"model_id": "dcac27eb462440d4af110bad3875e30e",
|
| 27 |
+
"version_major": 2,
|
| 28 |
+
"version_minor": 0
|
| 29 |
+
},
|
| 30 |
+
"text/plain": [
|
| 31 |
+
"BM25S Count Tokens: 0%| | 0/165 [00:00<?, ?it/s]"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"metadata": {},
|
| 35 |
+
"output_type": "display_data"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"data": {
|
| 39 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 40 |
+
"model_id": "d8478a4875064b8b9dce4c307cafc4de",
|
| 41 |
+
"version_major": 2,
|
| 42 |
+
"version_minor": 0
|
| 43 |
+
},
|
| 44 |
+
"text/plain": [
|
| 45 |
+
"BM25S Compute Scores: 0%| | 0/165 [00:00<?, ?it/s]"
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
"metadata": {},
|
| 49 |
+
"output_type": "display_data"
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"name": "stdout",
|
| 53 |
+
"output_type": "stream",
|
| 54 |
+
"text": [
|
| 55 |
+
"BM25 Index initialized with 165 documents.\n"
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"data": {
|
| 60 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 61 |
+
"model_id": "bd92d31479044b398df857e3bcd002d4",
|
| 62 |
+
"version_major": 2,
|
| 63 |
+
"version_minor": 0
|
| 64 |
+
},
|
| 65 |
+
"text/plain": [
|
| 66 |
+
"modules.json: 0%| | 0.00/229 [00:00<?, ?B/s]"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
"metadata": {},
|
| 70 |
+
"output_type": "display_data"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"data": {
|
| 74 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 75 |
+
"model_id": "12e26abd3b3c4cfd8729ddaa7c22989e",
|
| 76 |
+
"version_major": 2,
|
| 77 |
+
"version_minor": 0
|
| 78 |
+
},
|
| 79 |
+
"text/plain": [
|
| 80 |
+
"config_sentence_transformers.json: 0%| | 0.00/205 [00:00<?, ?B/s]"
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
"metadata": {},
|
| 84 |
+
"output_type": "display_data"
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"data": {
|
| 88 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 89 |
+
"model_id": "b6f025204e5b4e89b3f09675cd9c9c7d",
|
| 90 |
+
"version_major": 2,
|
| 91 |
+
"version_minor": 0
|
| 92 |
+
},
|
| 93 |
+
"text/plain": [
|
| 94 |
+
"README.md: 0.00B [00:00, ?B/s]"
|
| 95 |
+
]
|
| 96 |
+
},
|
| 97 |
+
"metadata": {},
|
| 98 |
+
"output_type": "display_data"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"data": {
|
| 102 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 103 |
+
"model_id": "3ef0c9c4684a462ca31eae8fea18a6a6",
|
| 104 |
+
"version_major": 2,
|
| 105 |
+
"version_minor": 0
|
| 106 |
+
},
|
| 107 |
+
"text/plain": [
|
| 108 |
+
"config.json: 0.00B [00:00, ?B/s]"
|
| 109 |
+
]
|
| 110 |
+
},
|
| 111 |
+
"metadata": {},
|
| 112 |
+
"output_type": "display_data"
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"data": {
|
| 116 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 117 |
+
"model_id": "c200e6620591481c90bcd5f408adfe58",
|
| 118 |
+
"version_major": 2,
|
| 119 |
+
"version_minor": 0
|
| 120 |
+
},
|
| 121 |
+
"text/plain": [
|
| 122 |
+
"model.safetensors: 0%| | 0.00/298M [00:00<?, ?B/s]"
|
| 123 |
+
]
|
| 124 |
+
},
|
| 125 |
+
"metadata": {},
|
| 126 |
+
"output_type": "display_data"
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"data": {
|
| 130 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 131 |
+
"model_id": "8aed0e33a1d841ecbf5c452b4f100bec",
|
| 132 |
+
"version_major": 2,
|
| 133 |
+
"version_minor": 0
|
| 134 |
+
},
|
| 135 |
+
"text/plain": [
|
| 136 |
+
"tokenizer_config.json: 0.00B [00:00, ?B/s]"
|
| 137 |
+
]
|
| 138 |
+
},
|
| 139 |
+
"metadata": {},
|
| 140 |
+
"output_type": "display_data"
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"data": {
|
| 144 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 145 |
+
"model_id": "25480a7027e6469e8ea2d80b27131192",
|
| 146 |
+
"version_major": 2,
|
| 147 |
+
"version_minor": 0
|
| 148 |
+
},
|
| 149 |
+
"text/plain": [
|
| 150 |
+
"tokenizer.json: 0.00B [00:00, ?B/s]"
|
| 151 |
+
]
|
| 152 |
+
},
|
| 153 |
+
"metadata": {},
|
| 154 |
+
"output_type": "display_data"
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"data": {
|
| 158 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 159 |
+
"model_id": "01dfe70506ba4d78bd270bdf96501d40",
|
| 160 |
+
"version_major": 2,
|
| 161 |
+
"version_minor": 0
|
| 162 |
+
},
|
| 163 |
+
"text/plain": [
|
| 164 |
+
"special_tokens_map.json: 0%| | 0.00/694 [00:00<?, ?B/s]"
|
| 165 |
+
]
|
| 166 |
+
},
|
| 167 |
+
"metadata": {},
|
| 168 |
+
"output_type": "display_data"
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"data": {
|
| 172 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 173 |
+
"model_id": "299bfe9f54bf4d7e9dbac5bac484ffa6",
|
| 174 |
+
"version_major": 2,
|
| 175 |
+
"version_minor": 0
|
| 176 |
+
},
|
| 177 |
+
"text/plain": [
|
| 178 |
+
"config.json: 0%| | 0.00/297 [00:00<?, ?B/s]"
|
| 179 |
+
]
|
| 180 |
+
},
|
| 181 |
+
"metadata": {},
|
| 182 |
+
"output_type": "display_data"
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"ename": "SupabaseException",
|
| 186 |
+
"evalue": "supabase_url is required",
|
| 187 |
+
"output_type": "error",
|
| 188 |
+
"traceback": [
|
| 189 |
+
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
| 190 |
+
"\u001b[31mSupabaseException\u001b[39m Traceback (most recent call last)",
|
| 191 |
+
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 56\u001b[39m\n\u001b[32m 53\u001b[39m embeddings = SentenceTransformer(model_name_or_path=config[\u001b[33m\"\u001b[39m\u001b[33mmodels\u001b[39m\u001b[33m\"\u001b[39m][\u001b[33m\"\u001b[39m\u001b[33membeddings\u001b[39m\u001b[33m\"\u001b[39m][\u001b[33m\"\u001b[39m\u001b[33mmodel_name\u001b[39m\u001b[33m\"\u001b[39m], cache_folder=config[\u001b[33m\"\u001b[39m\u001b[33mmodels\u001b[39m\u001b[33m\"\u001b[39m][\u001b[33m\"\u001b[39m\u001b[33mcache_folder\u001b[39m\u001b[33m\"\u001b[39m])\n\u001b[32m 55\u001b[39m \u001b[38;5;66;03m# Supabase Vector Store\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m56\u001b[39m supabase: Client = \u001b[43mcreate_client\u001b[49m\u001b[43m(\u001b[49m\u001b[43msupabase_url\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msupabase_key\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 58\u001b[39m \u001b[38;5;66;03m# Reranker Model (ModernBERT Cross-Encoder)\u001b[39;00m\n\u001b[32m 59\u001b[39m reranker = CrossEncoder(config[\u001b[33m\"\u001b[39m\u001b[33mmodels\u001b[39m\u001b[33m\"\u001b[39m][\u001b[33m\"\u001b[39m\u001b[33mreranker\u001b[39m\u001b[33m\"\u001b[39m][\u001b[33m\"\u001b[39m\u001b[33mmodel_name\u001b[39m\u001b[33m\"\u001b[39m], cache_folder=config[\u001b[33m\"\u001b[39m\u001b[33mmodels\u001b[39m\u001b[33m\"\u001b[39m][\u001b[33m\"\u001b[39m\u001b[33mcache_folder\u001b[39m\u001b[33m\"\u001b[39m])\n",
|
| 192 |
+
"\u001b[36mFile \u001b[39m\u001b[32m~/projects/gaia/.venv/lib/python3.13/site-packages/supabase/_sync/client.py:371\u001b[39m, in \u001b[36mcreate_client\u001b[39m\u001b[34m(supabase_url, supabase_key, options)\u001b[39m\n\u001b[32m 340\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcreate_client\u001b[39m(\n\u001b[32m 341\u001b[39m supabase_url: \u001b[38;5;28mstr\u001b[39m,\n\u001b[32m 342\u001b[39m supabase_key: \u001b[38;5;28mstr\u001b[39m,\n\u001b[32m 343\u001b[39m options: Optional[ClientOptions] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 344\u001b[39m ) -> Client:\n\u001b[32m 345\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Create client function to instantiate supabase client like JS runtime.\u001b[39;00m\n\u001b[32m 346\u001b[39m \n\u001b[32m 347\u001b[39m \u001b[33;03m Parameters\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 369\u001b[39m \u001b[33;03m Client\u001b[39;00m\n\u001b[32m 370\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m371\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mClient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 372\u001b[39m \u001b[43m \u001b[49m\u001b[43msupabase_url\u001b[49m\u001b[43m=\u001b[49m\u001b[43msupabase_url\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msupabase_key\u001b[49m\u001b[43m=\u001b[49m\u001b[43msupabase_key\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m=\u001b[49m\u001b[43moptions\u001b[49m\n\u001b[32m 373\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n",
|
| 193 |
+
"\u001b[36mFile \u001b[39m\u001b[32m~/projects/gaia/.venv/lib/python3.13/site-packages/supabase/_sync/client.py:104\u001b[39m, in \u001b[36mClient.create\u001b[39m\u001b[34m(cls, supabase_url, supabase_key, options)\u001b[39m\n\u001b[32m 96\u001b[39m \u001b[38;5;129m@classmethod\u001b[39m\n\u001b[32m 97\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcreate\u001b[39m(\n\u001b[32m 98\u001b[39m \u001b[38;5;28mcls\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 101\u001b[39m options: Optional[ClientOptions] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 102\u001b[39m ) -> \u001b[33m\"\u001b[39m\u001b[33mClient\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m 103\u001b[39m auth_header = options.headers.get(\u001b[33m\"\u001b[39m\u001b[33mAuthorization\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mif\u001b[39;00m options \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m104\u001b[39m client = \u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43msupabase_url\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msupabase_key\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 106\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m auth_header \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 107\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n",
|
| 194 |
+
"\u001b[36mFile \u001b[39m\u001b[32m~/projects/gaia/.venv/lib/python3.13/site-packages/supabase/_sync/client.py:56\u001b[39m, in \u001b[36mClient.__init__\u001b[39m\u001b[34m(self, supabase_url, supabase_key, options)\u001b[39m\n\u001b[32m 42\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Instantiate the client.\u001b[39;00m\n\u001b[32m 43\u001b[39m \n\u001b[32m 44\u001b[39m \u001b[33;03mParameters\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 52\u001b[39m \u001b[33;03m `DEFAULT_OPTIONS` dict.\u001b[39;00m\n\u001b[32m 53\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 55\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m supabase_url:\n\u001b[32m---> \u001b[39m\u001b[32m56\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m SupabaseException(\u001b[33m\"\u001b[39m\u001b[33msupabase_url is required\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 57\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m supabase_key:\n\u001b[32m 58\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m SupabaseException(\u001b[33m\"\u001b[39m\u001b[33msupabase_key is required\u001b[39m\u001b[33m\"\u001b[39m)\n",
|
| 195 |
+
"\u001b[31mSupabaseException\u001b[39m: supabase_url is required"
|
| 196 |
+
]
|
| 197 |
+
}
|
| 198 |
+
],
|
| 199 |
+
"source": [
|
| 200 |
+
"\"\"\"\n",
|
| 201 |
+
"GAIA Agent with Multi-Modal File Processing and Hybrid Retrieval.\n",
|
| 202 |
+
"\n",
|
| 203 |
+
"This module defines a LangGraph agent that can:\n",
|
| 204 |
+
"1. Retrieve similar questions using Hybrid Search (Vector + BM25) and Reranking\n",
|
| 205 |
+
"2. Process files using tools (PDF, XLSX, MP3, etc.)\n",
|
| 206 |
+
"3. Answer questions using web search, calculator, and other tools\n",
|
| 207 |
+
"\"\"\"\n",
|
| 208 |
+
"\n",
|
| 209 |
+
"import os\n",
|
| 210 |
+
"import bm25s\n",
|
| 211 |
+
"from dotenv import load_dotenv\n",
|
| 212 |
+
"\n",
|
| 213 |
+
"from langgraph.graph import START, END, StateGraph\n",
|
| 214 |
+
"from langgraph.prebuilt import tools_condition, ToolNode\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"from sentence_transformers import SentenceTransformer, CrossEncoder\n",
|
| 217 |
+
"from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint\n",
|
| 218 |
+
"from langchain_core.messages import HumanMessage\n",
|
| 219 |
+
"from supabase.client import Client, create_client\n",
|
| 220 |
+
"\n",
|
| 221 |
+
"from utils import load_config, load_prompt, init_bm25_index, reciprocal_rank_fusion\n",
|
| 222 |
+
"from tools import tools_list\n",
|
| 223 |
+
"from states import AgentState\n",
|
| 224 |
+
"\n",
|
| 225 |
+
"load_dotenv()\n",
|
| 226 |
+
"config = load_config()\n",
|
| 227 |
+
"\n",
|
| 228 |
+
"# Environment details and others\n",
|
| 229 |
+
"hf_key = os.getenv(\"HF_INFERENCE_KEY\")\n",
|
| 230 |
+
"supabase_url = os.getenv(\"SUPABASE_URL\")\n",
|
| 231 |
+
"supabase_key = os.getenv(\"SUPABASE_SERVICE_KEY\")\n",
|
| 232 |
+
"\n",
|
| 233 |
+
"# ============================================\n",
|
| 234 |
+
"# Model & Embeddings Setup\n",
|
| 235 |
+
"# ============================================\n",
|
| 236 |
+
"\n",
|
| 237 |
+
"# Model List\n",
|
| 238 |
+
"\n",
|
| 239 |
+
"llm_model_name = \"Qwen/Qwen3-32B-Instruct\"\n",
|
| 240 |
+
"\n",
|
| 241 |
+
"enable_keyword_search = config[\"retrievers\"][\"enable_keyword_search\"]\n",
|
| 242 |
+
"enable_vector_search = config[\"retrievers\"][\"enable_vector_search\"]\n",
|
| 243 |
+
"\n",
|
| 244 |
+
"# BM25 Retriever\n",
|
| 245 |
+
"bm25_retriever, bm25_corpus, bm25_ids = None, None, None\n",
|
| 246 |
+
"if enable_keyword_search:\n",
|
| 247 |
+
" bm25_retriever, bm25_corpus, bm25_ids = init_bm25_index(corpus_file=config[\"data\"])\n",
|
| 248 |
+
"\n",
|
| 249 |
+
"embeddings, supabase = None, None\n",
|
| 250 |
+
"if enable_vector_search:\n",
|
| 251 |
+
" # Embeddings for Vector Search\n",
|
| 252 |
+
" embeddings = SentenceTransformer(model_name_or_path=config[\"models\"][\"embeddings\"][\"model_name\"], cache_folder=config[\"models\"][\"cache_folder\"])\n",
|
| 253 |
+
"\n",
|
| 254 |
+
" # Supabase Vector Store\n",
|
| 255 |
+
" supabase: Client = create_client(supabase_url, supabase_key)\n",
|
| 256 |
+
"\n",
|
| 257 |
+
"# Reranker Model (ModernBERT Cross-Encoder)\n",
|
| 258 |
+
"reranker = CrossEncoder(config[\"models\"][\"reranker\"][\"model_name\"], cache_folder=config[\"models\"][\"cache_folder\"])\n",
|
| 259 |
+
"\n",
|
| 260 |
+
"# LLM for Agent\n",
|
| 261 |
+
"llm = HuggingFaceEndpoint(\n",
|
| 262 |
+
" repo_id=config[\"models\"][\"llm\"][\"model_name\"],\n",
|
| 263 |
+
" temperature=config[\"models\"][\"llm\"][\"parameters\"][\"temperature\"],\n",
|
| 264 |
+
" repetition_penalty=config[\"models\"][\"llm\"][\"parameters\"][\"repetition_penalty\"],\n",
|
| 265 |
+
" provider=config[\"models\"][\"llm\"][\"parameters\"][\"provider\"],\n",
|
| 266 |
+
" huggingfacehub_api_token=hf_key\n",
|
| 267 |
+
")\n",
|
| 268 |
+
"\n",
|
| 269 |
+
"agent_llm = ChatHuggingFace(llm=llm)\n",
|
| 270 |
+
"agent_with_tools = agent_llm.bind_tools(tools_list)\n",
|
| 271 |
+
"\n",
|
| 272 |
+
"\n",
|
| 273 |
+
"# ============================================\n",
|
| 274 |
+
"# Graph Nodes\n",
|
| 275 |
+
"# ============================================\n",
|
| 276 |
+
"\n",
|
| 277 |
+
"def retriever_node(state: AgentState) -> AgentState:\n",
|
| 278 |
+
" \"\"\"\n",
|
| 279 |
+
" Hybrid Search Node: Retrieve docs via Vector Search + BM25, combine with RRF.\n",
|
| 280 |
+
" \"\"\"\n",
|
| 281 |
+
" print(\"--- RETRIEVER NODE ---\")\n",
|
| 282 |
+
" messages = state.get(\"messages\", [])\n",
|
| 283 |
+
" if not messages:\n",
|
| 284 |
+
" return {\"retrieved_docs\": []}\n",
|
| 285 |
+
" \n",
|
| 286 |
+
" question_content = messages[0].content\n",
|
| 287 |
+
" \n",
|
| 288 |
+
" if not enable_vector_search and not enable_keyword_search:\n",
|
| 289 |
+
" print(\"No retrieval method enabled.\")\n",
|
| 290 |
+
" return {\"retrieved_docs\": []}\n",
|
| 291 |
+
" \n",
|
| 292 |
+
" # 1. Vector Search\n",
|
| 293 |
+
" vector_docs = []\n",
|
| 294 |
+
" if supabase and embeddings:\n",
|
| 295 |
+
" try:\n",
|
| 296 |
+
" response = supabase.rpc(\n",
|
| 297 |
+
" config[\"retrievers\"][\"vector_store\"][\"query\"],\n",
|
| 298 |
+
" {\"query_embedding\": embeddings.encode(question_content).tolist(), \n",
|
| 299 |
+
" \"match_count\": config[\"retrievers\"][\"vector_store\"][\"k\"], \n",
|
| 300 |
+
" \"match_threshold\": config[\"retrievers\"][\"vector_store\"][\"threshold\"]\n",
|
| 301 |
+
" } \n",
|
| 302 |
+
" ).execute()\n",
|
| 303 |
+
"\n",
|
| 304 |
+
" vector_docs = response.data\n",
|
| 305 |
+
"\n",
|
| 306 |
+
" except Exception as e:\n",
|
| 307 |
+
" print(f\"Vector search error: {e}\")\n",
|
| 308 |
+
" \n",
|
| 309 |
+
" # 2. BM25 Search\n",
|
| 310 |
+
" bm25_docs = []\n",
|
| 311 |
+
" if bm25_retriever and bm25_corpus and bm25_ids:\n",
|
| 312 |
+
" try:\n",
|
| 313 |
+
" query_tokens = bm25s.tokenize([question_content], stopwords=\"en\")\n",
|
| 314 |
+
" results, scores = bm25_retriever.retrieve(query_tokens, k=config[\"retrievers\"][\"bm25\"][\"k\"])\n",
|
| 315 |
+
" indices = results[0]\n",
|
| 316 |
+
" \n",
|
| 317 |
+
" for i, idx in enumerate(indices):\n",
|
| 318 |
+
" content = bm25_corpus[idx]\n",
|
| 319 |
+
" task_id = bm25_ids[idx]\n",
|
| 320 |
+
" score = scores[0][i]\n",
|
| 321 |
+
" bm25_dict = {\"content\":content, \"metadata\": {\"source\": \"bm25_search\", \"task_id\": task_id, \"score\": score}}\n",
|
| 322 |
+
" bm25_docs.append(bm25_dict)\n",
|
| 323 |
+
" except Exception as e:\n",
|
| 324 |
+
" print(f\"BM25 search error: {e}\")\n",
|
| 325 |
+
"\n",
|
| 326 |
+
" # 3. RRF Fusion\n",
|
| 327 |
+
" final_candidates = []\n",
|
| 328 |
+
" if vector_docs and bm25_docs:\n",
|
| 329 |
+
" fused = reciprocal_rank_fusion([vector_docs, bm25_docs])\n",
|
| 330 |
+
" final_candidates = [id for id, doc, score in fused]\n",
|
| 331 |
+
" else:\n",
|
| 332 |
+
" final_candidates = vector_docs + bm25_docs\n",
|
| 333 |
+
" final_candidates = [doc[\"metadata\"][\"task_id\"] for doc in final_candidates]\n",
|
| 334 |
+
" \n",
|
| 335 |
+
" top_candidates = final_candidates[:20]\n",
|
| 336 |
+
" \n",
|
| 337 |
+
" return {\"retrieved_docs\": top_candidates}"
|
| 338 |
+
]
|
| 339 |
+
}
|
| 340 |
+
],
|
| 341 |
+
"metadata": {
|
| 342 |
+
"kernelspec": {
|
| 343 |
+
"display_name": "gaia",
|
| 344 |
+
"language": "python",
|
| 345 |
+
"name": "python3"
|
| 346 |
+
},
|
| 347 |
+
"language_info": {
|
| 348 |
+
"codemirror_mode": {
|
| 349 |
+
"name": "ipython",
|
| 350 |
+
"version": 3
|
| 351 |
+
},
|
| 352 |
+
"file_extension": ".py",
|
| 353 |
+
"mimetype": "text/x-python",
|
| 354 |
+
"name": "python",
|
| 355 |
+
"nbconvert_exporter": "python",
|
| 356 |
+
"pygments_lexer": "ipython3",
|
| 357 |
+
"version": "3.13.1"
|
| 358 |
+
}
|
| 359 |
+
},
|
| 360 |
+
"nbformat": 4,
|
| 361 |
+
"nbformat_minor": 5
|
| 362 |
+
}
|
requirements.txt
CHANGED
|
@@ -44,6 +44,7 @@ tavily-python>=0.5.0
|
|
| 44 |
wikipedia>=1.4.0
|
| 45 |
arxiv>=2.1.0
|
| 46 |
trafilatura>=1.6.0
|
|
|
|
| 47 |
|
| 48 |
# Excel
|
| 49 |
openpyxl>=3.1.0
|
|
|
|
| 44 |
wikipedia>=1.4.0
|
| 45 |
arxiv>=2.1.0
|
| 46 |
trafilatura>=1.6.0
|
| 47 |
+
youtube-transcript-api>=1.0.0
|
| 48 |
|
| 49 |
# Excel
|
| 50 |
openpyxl>=3.1.0
|
scripts/create_vector_database.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Populate the Supabase vector store with GAIA benchmark embeddings.
|
| 3 |
+
|
| 4 |
+
Prerequisites:
|
| 5 |
+
- SUPABASE_URL and SUPABASE_SERVICE_KEY in .env
|
| 6 |
+
- pgvector extension enabled in Supabase
|
| 7 |
+
- gaia_documents table created with columns: content (text), metadata (jsonb), embedding (vector)
|
| 8 |
+
"""
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from supabase.client import Client, create_client
|
| 13 |
+
from sentence_transformers import SentenceTransformer
|
| 14 |
+
from gaia.utils import load_config
|
| 15 |
+
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
config = load_config()
|
| 19 |
+
data_path = config["data"]
|
| 20 |
+
|
| 21 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 22 |
+
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 23 |
+
|
| 24 |
+
supabase: Client = create_client(supabase_url, supabase_key)
|
| 25 |
+
embeddings = SentenceTransformer(
|
| 26 |
+
model_name_or_path=config["vector_store"]["embedding_model_name"],
|
| 27 |
+
cache_folder=config["models"]["cache_folder"],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
with open(data_path, "r") as jsonl_file:
|
| 31 |
+
json_list = list(jsonl_file)
|
| 32 |
+
|
| 33 |
+
documents = []
|
| 34 |
+
for json_str in json_list:
|
| 35 |
+
json_data = json.loads(json_str)
|
| 36 |
+
content = json_data["Question"]
|
| 37 |
+
embedding = embeddings.encode(content, normalize_embeddings=True).tolist()
|
| 38 |
+
documents.append({
|
| 39 |
+
"content": content,
|
| 40 |
+
"metadata": {
|
| 41 |
+
"source": "vector_search",
|
| 42 |
+
"task_id": json_data["task_id"],
|
| 43 |
+
},
|
| 44 |
+
"embedding": embedding,
|
| 45 |
+
})
|
| 46 |
+
|
| 47 |
+
print(f"Inserting {len(documents)} documents into Supabase...")
|
| 48 |
+
try:
|
| 49 |
+
response = supabase.table("gaia_documents").insert(documents).execute()
|
| 50 |
+
print("Done.")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print("Error inserting data into Supabase:", e)
|