"""
agent.py — LangChain ReAct agent for the GAIA benchmark.
Tools included:
• DuckDuckGo web search (free, no API key)
• Wikipedia lookup
• Python REPL (math, data manipulation)
• File download + reading (text, CSV, PDF via pdfminer)
• Image understanding via HuggingFace Inference API (free tier)
• ArXiv search
"""
import os
import io
import re
import tempfile
import traceback
from typing import Optional
import requests
# LangChain core
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import Tool, tool
from langchain_core.prompts import PromptTemplate
from langchain_huggingface import HuggingFaceEndpoint
# Community tools
from langchain_community.tools import DuckDuckGoSearchRun, WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
from langchain_experimental.tools.python.tool import PythonREPLTool
# ---------------------------------------------------------------------------
# 1. LLM — free HuggingFace Inference Endpoint
# ---------------------------------------------------------------------------
def get_llm():
"""
Use a capable open model via HF Inference API (free tier).
Qwen2.5-72B-Instruct is a strong publicly available model.
You can swap for meta-llama/Meta-Llama-3-70B-Instruct, etc.
Requires HF_TOKEN env var (free account works).
"""
hf_token = os.getenv("HF_TOKEN")
if not hf_token:
raise EnvironmentError(
"HF_TOKEN environment variable not set. "
"Add it in your HuggingFace Space secrets."
)
llm = HuggingFaceEndpoint(
repo_id="Qwen/Qwen2.5-72B-Instruct",
huggingfacehub_api_token=hf_token,
task="text-generation",
max_new_tokens=1024,
temperature=0.1,
do_sample=False,
repetition_penalty=1.1,
)
return llm
# ---------------------------------------------------------------------------
# 2. Tool definitions
# ---------------------------------------------------------------------------
# -- Web search --
def make_search_tool():
search = DuckDuckGoSearchRun()
return Tool(
name="web_search",
func=search.run,
description=(
"Search the web for current information using DuckDuckGo. "
"Use this for facts, recent events, people, places, or anything "
"that requires up-to-date knowledge. Input: a search query string."
),
)
# -- Wikipedia --
def make_wikipedia_tool():
wiki = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(top_k_results=3, doc_content_chars_max=3000))
return Tool(
name="wikipedia",
func=wiki.run,
description=(
"Look up encyclopedic information on Wikipedia. "
"Best for well-known topics, historical facts, science, biographies. "
"Input: a topic or question string."
),
)
# -- Python REPL --
def make_python_tool():
repl = PythonREPLTool()
return Tool(
name="python_repl",
func=repl.run,
description=(
"Execute Python code for calculations, data processing, string manipulation, "
"logic, and analysis. pandas, math, re, json, csv, datetime are available. "
"Input: valid Python code as a string. Always print() the result you need."
),
)
# -- File reader --
@tool
def read_file_from_url(url: str) -> str:
"""
Download a file from a URL and return its text content.
Supports: plain text (.txt, .py, .json, .csv, .md), PDF, and basic image description.
Input: a URL string pointing to the file.
"""
try:
resp = requests.get(url, timeout=30)
resp.raise_for_status()
content_type = resp.headers.get("content-type", "")
# PDF
if "pdf" in content_type or url.lower().endswith(".pdf"):
try:
from pdfminer.high_level import extract_text
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
f.write(resp.content)
tmp_path = f.name
text = extract_text(tmp_path)
os.unlink(tmp_path)
return text[:5000] if text else "Could not extract PDF text."
except ImportError:
return "pdfminer not available. Install pdfminer.six to read PDFs."
# CSV
if "csv" in content_type or url.lower().endswith(".csv"):
import csv
decoded = resp.content.decode("utf-8", errors="replace")
lines = decoded.splitlines()
return "\n".join(lines[:50]) # first 50 rows
# Excel
if url.lower().endswith((".xlsx", ".xls")):
try:
import pandas as pd
from io import BytesIO
df = pd.read_excel(BytesIO(resp.content))
return df.to_string(max_rows=50)
except Exception as e:
return f"Could not read Excel file: {e}"
# Image — describe via HF
if "image" in content_type or url.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".gif")):
return describe_image_bytes(resp.content)
# Default: text
return resp.content.decode("utf-8", errors="replace")[:5000]
except Exception as e:
return f"Error reading file from {url}: {e}"
# -- Image understanding via HF Inference API --
def describe_image_bytes(image_bytes: bytes) -> str:
"""Use HF Inference API to caption an image."""
hf_token = os.getenv("HF_TOKEN", "")
headers = {}
if hf_token:
headers["Authorization"] = f"Bearer {hf_token}"
# Use Salesforce BLIP image captioning (free on HF Inference API)
api_url = "https://api-inference.huggingface.co/models/Salesforce/blip-image-captioning-large"
try:
response = requests.post(api_url, headers=headers, data=image_bytes, timeout=30)
result = response.json()
if isinstance(result, list) and result:
return result[0].get("generated_text", "No caption generated.")
return str(result)
except Exception as e:
return f"Image description failed: {e}"
@tool
def describe_image_from_url(url: str) -> str:
"""
Download an image from a URL and return a text description of its contents.
Use this when a question refers to an image file.
Input: a direct URL to an image (jpg, png, webp, etc.).
"""
try:
resp = requests.get(url, timeout=30)
resp.raise_for_status()
return describe_image_bytes(resp.content)
except Exception as e:
return f"Could not describe image: {e}"
# -- ArXiv search --
@tool
def arxiv_search(query: str) -> str:
"""
Search ArXiv for scientific papers. Use for questions about research, ML papers,
physics, mathematics, computer science publications.
Input: a search query string.
Returns: titles, authors, and abstracts of top results.
"""
try:
import urllib.parse
encoded = urllib.parse.quote(query)
url = f"https://export.arxiv.org/api/query?search_query=all:{encoded}&start=0&max_results=3"
resp = requests.get(url, timeout=15)
resp.raise_for_status()
# Parse simple XML
text = resp.text
entries = re.findall(r"