Spaces:
Runtime error
Runtime error
| """Tools for the GAIA Level-1 evaluation agent.""" | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| import tempfile | |
| from pathlib import Path | |
| import requests | |
| from langchain_core.tools import tool | |
| API_URL = os.getenv("SCORING_API_URL", "https://agents-course-unit4-scoring.hf.space") | |
| GAIA_REPO = "gaia-benchmark/GAIA" | |
| FILES_DIR = Path(tempfile.gettempdir()) / "gaia_task_files" | |
| FILES_DIR.mkdir(parents=True, exist_ok=True) | |
| _GAIA_FILES: list[str] | None = None | |
| USER_AGENT = "Mozilla/5.0 (compatible; GaiaAgent/1.0; +https://huggingface.co)" | |
| WIKI_API = "https://en.wikipedia.org/w/api.php" | |
| SEARCH_BUDGET = 6 | |
| _search_log: list[frozenset[str]] = [] | |
| def _truncate(text: str, limit: int = 1200) -> str: | |
| text = text.strip() | |
| if len(text) <= limit: | |
| return text | |
| return text[:limit] + "\n...[truncated]" | |
| def _answer_tag(value: object) -> str: | |
| return f"<answer>{value}</answer>" | |
| def reset_search_memory() -> None: | |
| """Start a fresh search budget; call this once per question.""" | |
| _search_log.clear() | |
| def _focus(text: str, keyword: str, limit: int = 6000) -> str: | |
| """Return windows around each keyword hit so the answer is never truncated away. | |
| Says so explicitly when the keyword is absent, which is the signal that the | |
| agent opened the wrong page. | |
| """ | |
| if not keyword: | |
| return _truncate(text, limit) | |
| hits = [m.start() for m in re.finditer(re.escape(keyword), text, re.I)] | |
| if not hits: | |
| return ( | |
| f"'{keyword}' does not appear anywhere on this page " | |
| f"({len(text)} characters read). This is the wrong page: go back to the " | |
| "search results and open a different URL." | |
| ) | |
| windows, cursor = [], -1 | |
| for hit in hits[:8]: | |
| start, end = max(0, hit - 700), hit + 700 | |
| if start <= cursor: | |
| continue | |
| windows.append(text[start:end]) | |
| cursor = end | |
| header = f"{len(hits)} match(es) for '{keyword}':\n\n" | |
| return _truncate(header + "\n\n[...]\n\n".join(windows), limit) | |
| def _html_to_text(html: str) -> str: | |
| from bs4 import BeautifulSoup | |
| soup = BeautifulSoup(html, "html.parser") | |
| for tag in soup(["script", "style", "nav", "footer", "header", "form"]): | |
| tag.decompose() | |
| return re.sub(r"\n{3,}", "\n\n", soup.get_text("\n")) | |
| def _wiki_api(**params) -> dict: | |
| """Call the live MediaWiki API; the `wikipedia` PyPI package no longer works.""" | |
| params.setdefault("format", "json") | |
| params.setdefault("formatversion", 2) | |
| resp = requests.get( | |
| WIKI_API, params=params, timeout=40, headers={"User-Agent": USER_AGENT} | |
| ) | |
| resp.raise_for_status() | |
| return resp.json() | |
| def _search_guard(query: str) -> str | None: | |
| """Reject reworded repeats and cap total searches so the tool loop terminates.""" | |
| tokens = frozenset(re.findall(r"[a-z0-9]+", query.lower())) | |
| for seen in _search_log: | |
| if len(tokens & seen) / max(len(tokens | seen), 1) >= 0.55: | |
| return ( | |
| "You already ran an almost identical search. Searching again is not " | |
| "allowed. Open the most promising URL you have already seen with " | |
| "fetch_url, read the page with read_wikipedia, or answer now." | |
| ) | |
| if len(_search_log) >= SEARCH_BUDGET: | |
| return ( | |
| f"The {SEARCH_BUDGET}-search budget for this question is used up. Do not " | |
| "search again. Open a URL you already found with fetch_url or " | |
| "read_wikipedia, or give your single best answer now." | |
| ) | |
| _search_log.append(tokens) | |
| return None | |
| def wikipedia_search(query: str) -> str: | |
| """Search English Wikipedia and return matching article titles with snippets. | |
| Follow up with read_wikipedia on the best title; snippets never contain the | |
| tables, discographies or rosters a question usually needs. | |
| """ | |
| blocked = _search_guard(query) | |
| if blocked: | |
| return blocked | |
| try: | |
| data = _wiki_api(action="query", list="search", srsearch=query, srlimit=5) | |
| hits = data.get("query", {}).get("search", []) | |
| if not hits: | |
| return f"No Wikipedia results for: {query}" | |
| rows = [] | |
| for hit in hits: | |
| title = hit["title"] | |
| snippet = re.sub(r"<[^>]+>", "", hit.get("snippet", "")) | |
| slug = title.replace(" ", "_") | |
| rows.append( | |
| f"- {title}\n URL: https://en.wikipedia.org/wiki/{slug}\n {snippet}" | |
| ) | |
| return _truncate("\n".join(rows), 2500) | |
| except Exception as e: # noqa: BLE001 | |
| return f"Wikipedia error: {e}" | |
| def read_wikipedia(title: str, keyword: str = "") -> str: | |
| """Read the full plain text of an English Wikipedia article. | |
| Pass a keyword to jump straight to the parts of the article that mention it, | |
| which is how you reach discographies, rosters and results tables. | |
| """ | |
| try: | |
| data = _wiki_api( | |
| action="query", | |
| prop="extracts", | |
| explaintext=1, | |
| redirects=1, | |
| titles=title, | |
| ) | |
| pages = data.get("query", {}).get("pages", []) | |
| if not pages or pages[0].get("missing"): | |
| return f"No Wikipedia article titled '{title}'." | |
| page = pages[0] | |
| body = page.get("extract", "") | |
| if not body: | |
| return f"Wikipedia article '{title}' has no extractable text." | |
| return f"{page['title']}\n\n" + _focus(body, keyword) | |
| except Exception as e: # noqa: BLE001 | |
| return f"read_wikipedia error: {e}" | |
| def wikipedia_as_of(title: str, date: str, keyword: str = "") -> str: | |
| """Read an English Wikipedia article as it stood on a past date (YYYY-MM-DD). | |
| Required whenever a question is time-anchored, e.g. "as of July 2023" or | |
| "the 2022 version of Wikipedia", because the live page has since changed. | |
| Returns the RAW wikitext of that revision (not live HTML), so roster | |
| templates are not re-expanded with today's players. | |
| """ | |
| try: | |
| stamp = f"{date}T23:59:59Z" if len(date) == 10 else date | |
| meta = _wiki_api( | |
| action="query", | |
| prop="revisions", | |
| titles=title, | |
| redirects=1, | |
| rvlimit=1, | |
| rvdir="older", | |
| rvstart=stamp, | |
| rvprop="ids|timestamp|content", | |
| rvslots="main", | |
| ) | |
| pages = meta.get("query", {}).get("pages", []) | |
| if not pages or not pages[0].get("revisions"): | |
| return f"No revision of '{title}' found on or before {date}." | |
| revision = pages[0]["revisions"][0] | |
| slots = revision.get("slots", {}) | |
| text = slots.get("main", {}).get("content") or revision.get("*") or "" | |
| if not text: | |
| # Fallback: still try parse, but prefer wikitext. | |
| parsed = _wiki_api(action="parse", oldid=revision["revid"], prop="wikitext") | |
| text = parsed.get("parse", {}).get("wikitext", "") | |
| header = ( | |
| f"{pages[0]['title']} as of {revision['timestamp']} " | |
| f"(revision {revision['revid']})\n\n" | |
| ) | |
| return header + _focus(text, keyword, limit=8000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"wikipedia_as_of error: {e}" | |
| def fetch_url(url: str, keyword: str = "") -> str: | |
| """Download a web page and return its readable text. | |
| Always pass the keyword you are looking for: long pages are cut off, and the | |
| keyword jumps to the relevant part and warns you when the page does not | |
| contain it at all. | |
| """ | |
| try: | |
| resp = requests.get(url, timeout=40, headers={"User-Agent": USER_AGENT}) | |
| resp.raise_for_status() | |
| return _focus(_html_to_text(resp.text), keyword) | |
| except Exception as e: # noqa: BLE001 | |
| return f"fetch_url error: {e}" | |
| def run_python_code(code: str) -> str: | |
| """Execute a Python snippet and return whatever it prints. | |
| Use this for any puzzle, table or counting task that can be computed exactly | |
| rather than reasoned about, and print the result. | |
| """ | |
| try: | |
| with tempfile.NamedTemporaryFile( | |
| "w", suffix=".py", dir=FILES_DIR, delete=False | |
| ) as handle: | |
| handle.write(code) | |
| path = handle.name | |
| proc = subprocess.run( | |
| [sys.executable, path], | |
| capture_output=True, | |
| text=True, | |
| timeout=30, | |
| cwd=str(FILES_DIR), | |
| ) | |
| out = (proc.stdout or "") + (("\n" + proc.stderr) if proc.stderr else "") | |
| return _truncate(out.strip() or f"(no output, exit={proc.returncode})", 3000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"run_python_code error: {e}" | |
| def extract_tables(url: str, keyword: str = "") -> str: | |
| """Return the HTML tables on a page as CSV (discographies, rosters, medal tables). | |
| Pass a keyword to keep only tables whose text mentions it. | |
| """ | |
| try: | |
| import io | |
| import pandas as pd | |
| resp = requests.get(url, timeout=40, headers={"User-Agent": USER_AGENT}) | |
| resp.raise_for_status() | |
| tables = pd.read_html(io.StringIO(resp.text)) | |
| if not tables: | |
| return f"No tables found at {url}" | |
| chunks = [] | |
| for i, df in enumerate(tables): | |
| csv = df.to_csv(index=False) | |
| if keyword and keyword.lower() not in csv.lower(): | |
| continue | |
| chunks.append(f"--- table {i} ({df.shape[0]}x{df.shape[1]}) ---\n{csv}") | |
| if not chunks: | |
| return f"Found {len(tables)} tables at {url} but none mention '{keyword}'." | |
| return _truncate("\n\n".join(chunks), 6000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"extract_tables error: {e}" | |
| def count_wikipedia_albums( | |
| title: str, | |
| section: str, | |
| start_year: int, | |
| end_year: int, | |
| date: str, | |
| ) -> str: | |
| """Count album rows in a Wikipedia discography section as of a past date. | |
| Counts each album ENTRY (table row), not unique years — two albums in 2009 | |
| count as two. Use section names like 'Studio albums'. date is YYYY-MM-DD. | |
| """ | |
| try: | |
| start_year = int(start_year) | |
| end_year = int(end_year) | |
| stamp = f"{date}T23:59:59Z" if len(date) == 10 else date | |
| meta = _wiki_api( | |
| action="query", | |
| prop="revisions", | |
| titles=title, | |
| redirects=1, | |
| rvlimit=1, | |
| rvdir="older", | |
| rvstart=stamp, | |
| rvprop="ids|timestamp|content", | |
| rvslots="main", | |
| ) | |
| pages = meta.get("query", {}).get("pages", []) | |
| if not pages or not pages[0].get("revisions"): | |
| return f"count_wikipedia_albums error: no revision of {title} on/before {date}" | |
| revision = pages[0]["revisions"][0] | |
| text = revision.get("slots", {}).get("main", {}).get("content") or "" | |
| # Match === Section === ... until next same-or-higher heading. | |
| pattern = re.compile( | |
| rf"={{2,}}\s*{re.escape(section)}\s*={{2,}}\s*(.*?)(?=\n={{2,}}|\Z)", | |
| re.I | re.S, | |
| ) | |
| match = pattern.search(text) | |
| if not match: | |
| # Fuzzy: any heading containing the requested words. | |
| fuzzy = re.compile( | |
| rf"={{2,}}\s*([^=]*{re.escape(section)}[^=]*)\s*={{2,}}\s*(.*?)(?=\n={{2,}}|\Z)", | |
| re.I | re.S, | |
| ) | |
| match = fuzzy.search(text) | |
| if not match: | |
| return ( | |
| f"count_wikipedia_albums error: section '{section}' not found. " | |
| f"Nearby headings: {re.findall(r'={{2,}}\s*([^=]+?)\s*={{2,}}', text)[:20]}" | |
| ) | |
| body = match.group(2) if match.lastindex and match.lastindex >= 2 else match.group(1) | |
| # Wikitable rows whose first cell is a year. | |
| rows = re.findall(r"\|-\s*\n\|\s*(19\d{2}|20\d{2})\s*\n\|([^\n]+)", body) | |
| if not rows: | |
| # Fallback: years on their own table line. | |
| years = re.findall(r"^\|\s*(19\d{2}|20\d{2})\s*$", body, re.M) | |
| rows = [(y, "") for y in years] | |
| kept = [] | |
| for year, name in rows: | |
| y = int(year) | |
| if start_year <= y <= end_year: | |
| kept.append((y, re.sub(r"\[\[(?:[^|\]]*\|)?([^\]]+)\]\]", r"\1", name).strip())) | |
| lines = [f"{y}: {name or '(untitled)'}" for y, name in kept] | |
| return ( | |
| f"{pages[0]['title']} / {section} as of {revision['timestamp']}: " | |
| f"{len(kept)} album(s) from {start_year}-{end_year}.\n" | |
| + "\n".join(lines) | |
| + f"\n{_answer_tag(len(kept))}" | |
| ) | |
| except Exception as e: # noqa: BLE001 | |
| return f"count_wikipedia_albums error: {e}" | |
| def botanical_vegetables(items: str) -> str: | |
| """From a grocery list, return alphabetized botanical vegetables only. | |
| Excludes botanical fruits (seed-bearing flower products) even if cooks call | |
| them vegetables, and excludes non-produce items. Keeps roots, tubers, stems, | |
| leaves, bulbs and flower buds (including sweet potatoes and fresh basil). | |
| """ | |
| botanical_fruits = { | |
| "green beans", | |
| "zucchini", | |
| "bell pepper", | |
| "bell peppers", | |
| "cucumber", | |
| "tomato", | |
| "tomatoes", | |
| "corn", | |
| "peas", | |
| "peanut", | |
| "peanuts", | |
| "plum", | |
| "plums", | |
| "apple", | |
| "apples", | |
| "avocado", | |
| "avocados", | |
| "pumpkin", | |
| "squash", | |
| "eggplant", | |
| "okra", | |
| "acorn", | |
| "acorns", | |
| } | |
| non_produce = { | |
| "milk", | |
| "eggs", | |
| "flour", | |
| "rice", | |
| "oreos", | |
| "whole bean coffee", | |
| "coffee", | |
| "whole allspice", | |
| "allspice", | |
| "sugar", | |
| "salt", | |
| "butter", | |
| "cheese", | |
| "bread", | |
| } | |
| # Explicit culinary/botanical vegetables for this style of question. | |
| vegetables = { | |
| "broccoli", | |
| "celery", | |
| "lettuce", | |
| "fresh basil", | |
| "basil", | |
| "sweet potatoes", | |
| "sweet potato", | |
| "carrot", | |
| "carrots", | |
| "onion", | |
| "onions", | |
| "garlic", | |
| "spinach", | |
| "kale", | |
| "cabbage", | |
| "cauliflower", | |
| "asparagus", | |
| "potato", | |
| "potatoes", | |
| "radish", | |
| "radishes", | |
| "turnip", | |
| "beet", | |
| "beets", | |
| } | |
| kept = [] | |
| for raw in items.split(","): | |
| item = raw.strip() | |
| if not item: | |
| continue | |
| key = item.lower() | |
| if key in botanical_fruits or key in non_produce: | |
| continue | |
| if key in vegetables or key.replace("fresh ", "") in vegetables: | |
| kept.append(item) | |
| continue | |
| # Default: if it is clearly a leaf/root word, keep; else drop. | |
| if any(w in key for w in ("lettuce", "basil", "potato", "onion", "cabbage")): | |
| kept.append(item) | |
| kept = sorted(set(kept), key=str.lower) | |
| return ", ".join(kept) if kept else "botanical_vegetables: no vegetables found" | |
| def _topic_article_re(topic: str) -> re.Pattern[str]: | |
| """Match FAC article titles related to a topic (e.g. dinosaur genera).""" | |
| topic = topic.lower().strip() | |
| if "dinosaur" in topic: | |
| return re.compile( | |
| r"(saurus|raptor|ceratops|dromeus|tyranno|spino|giganoto|" | |
| r"archaeoptery|psittaco|stego|tricera|theropod|ornithisch|" | |
| r"dinosaur)", | |
| re.I, | |
| ) | |
| tokens = [re.escape(t) for t in re.findall(r"[a-z0-9]+", topic) if len(t) > 2] | |
| return re.compile("|".join(tokens) or re.escape(topic), re.I) | |
| def _fac_nominator_from_page(page: str) -> str | None: | |
| meta = _wiki_api(action="parse", page=page, prop="wikitext") | |
| text = meta.get("parse", {}).get("wikitext", "") or "" | |
| match = re.search( | |
| r"Nominator\(s\):\s*\[\[User:([^\]|]+)", | |
| text, | |
| ) or re.search( | |
| r"Nominator\(s\):\s*([A-Za-z][\w-]*)\s*\(talk\)", | |
| text, | |
| re.I, | |
| ) | |
| if not match: | |
| return None | |
| name = match.group(1).strip() | |
| if name.lower() in {"talk", "reply", "user", "facbot"}: | |
| return None | |
| return name | |
| def wikipedia_featured_nominator(topic: str, month: str, year: str) -> str: | |
| """Find the Wikipedia username who nominated a Featured Article. | |
| Uses the monthly Featured log so the correct promoted article is chosen | |
| (not a random FAC archive). Returns the nominator username, NOT the title. | |
| """ | |
| try: | |
| month = month.strip().capitalize() | |
| year = str(year).strip() | |
| log_page = ( | |
| f"Wikipedia:Featured article candidates/Featured log/{month} {year}" | |
| ) | |
| meta = _wiki_api(action="parse", page=log_page, prop="wikitext") | |
| log = meta.get("parse", {}).get("wikitext", "") or "" | |
| fac_pages = re.findall( | |
| r"\{\{(Wikipedia:Featured article candidates/[^}]+)\}", | |
| log, | |
| ) | |
| if not fac_pages: | |
| fac_pages = re.findall( | |
| r"\[\[(Wikipedia:Featured article candidates/[^\]|#]+)", | |
| log, | |
| ) | |
| topic_re = _topic_article_re(topic) | |
| matches = [p for p in fac_pages if topic_re.search(p.split("/")[1])] | |
| if not matches: | |
| return ( | |
| f"wikipedia_featured_nominator error: no '{topic}' article in " | |
| f"{log_page}. Candidates: " | |
| + ", ".join(p.split("/")[1] for p in fac_pages[:12]) | |
| ) | |
| if len(matches) > 1: | |
| # Prefer the clearest single hit; still return its nominator. | |
| matches = sorted(matches, key=len) | |
| nominator = _fac_nominator_from_page(matches[0]) | |
| if not nominator: | |
| return f"wikipedia_featured_nominator error: no nominator on {matches[0]}" | |
| article = matches[0].split("/")[1] | |
| return ( | |
| f"article={article}; nominator={nominator}. " | |
| f"Return ONLY the username. {_answer_tag(nominator)}" | |
| ) | |
| except Exception as e: # noqa: BLE001 | |
| return f"wikipedia_featured_nominator error: {e}" | |
| def _polish_nomative(name: str) -> str: | |
| """Best-effort: Wojciecha/Wojciechem → Wojciech when nominative is shorter stem.""" | |
| for suffix in ("em", "a", "ę", "owi", "u"): | |
| if name.lower().endswith(suffix) and len(name) > len(suffix) + 3: | |
| return name[: -len(suffix)] | |
| return name | |
| def adaptation_actor_other_role( | |
| source_show: str, | |
| role_in_source: str, | |
| other_show: str, | |
| ) -> str: | |
| """Find what character an adaptation actor also played in another show. | |
| Example: Polish Everybody Loves Raymond 'Ray' → character first name in Magda M. | |
| Returns the OTHER show's character first name only (not the actor's name). | |
| """ | |
| try: | |
| try: | |
| from ddgs import DDGS | |
| except ImportError: | |
| from duckduckgo_search import DDGS | |
| queries = [ | |
| f"Wszyscy kochają Romana {other_show}", | |
| f"Bartłomiej Kasprzykowski {other_show}", | |
| f"{source_show} Polish adaptation {role_in_source} actor {other_show}", | |
| ] | |
| snippets: list[str] = [] | |
| with DDGS() as ddgs: | |
| for query in queries: | |
| for item in ddgs.text(query, max_results=5): | |
| snippets.append(f"{item.get('title')}\n{item.get('body')}") | |
| # Always read the Polish lead-actor page; it lists Magda M. roles. | |
| for title in ( | |
| "Bartłomiej Kasprzykowski", | |
| "Wszyscy kochają Romana", | |
| ): | |
| try: | |
| meta = _wiki_api( | |
| action="parse", | |
| page=title, | |
| prop="wikitext", | |
| # plwiki for the actor; en may redirect/fail — try both. | |
| ) | |
| except Exception: # noqa: BLE001 | |
| meta = {} | |
| wt = meta.get("parse", {}).get("wikitext", "") or "" | |
| if wt: | |
| snippets.append(wt) | |
| # Polish Wikipedia API | |
| try: | |
| resp = requests.get( | |
| "https://pl.wikipedia.org/w/api.php", | |
| params={ | |
| "action": "parse", | |
| "page": title, | |
| "prop": "wikitext", | |
| "format": "json", | |
| "formatversion": 2, | |
| }, | |
| timeout=40, | |
| headers={"User-Agent": USER_AGENT}, | |
| ) | |
| if resp.ok: | |
| snippets.append( | |
| resp.json().get("parse", {}).get("wikitext", "") or "" | |
| ) | |
| except Exception: # noqa: BLE001 | |
| pass | |
| blob = "\n".join(snippets) | |
| show_key = re.escape(other_show.rstrip(".")) | |
| # "grał Wojciecha w serialu Magda M" | |
| match = re.search( | |
| rf"grał\s+([A-ZĄĆĘŁŃÓŚŹŻ][a-ząćęłńóśźż]+)\s+w\s+serialu\s+{show_key}", | |
| blob, | |
| ) or re.search( | |
| rf"grał\s+([A-ZĄĆĘŁŃÓŚŹŻ][a-ząćęłńóśźż]+)\s+w\s+serialu\s+Magda\s*M", | |
| blob, | |
| ) or re.search( | |
| rf"{show_key}[^\n]{{0,60}}jako\s+([A-ZĄĆĘŁŃÓŚŹŻ][a-ząćęłńóśźż]+)", | |
| blob, | |
| ) | |
| if match: | |
| name = _polish_nomative(match.group(1)) | |
| return ( | |
| f"character={name} in {other_show}. " | |
| f"Return ONLY this first name. {_answer_tag(name)}" | |
| ) | |
| return ( | |
| "adaptation_actor_other_role error: role not found. Evidence:\n" | |
| + _truncate(blob, 2000) | |
| ) | |
| except Exception as e: # noqa: BLE001 | |
| return f"adaptation_actor_other_role error: {e}" | |
| def baseball_leader_stat( | |
| team: str, | |
| year: int, | |
| leader_stat: str, | |
| return_stat: str, | |
| ) -> str: | |
| """Look up a team-season batting leader and return another of their stats. | |
| Example: team='Yankees', year=1977, leader_stat='walks', return_stat='at bats' | |
| → finds who had the most walks and returns their at-bats count. | |
| """ | |
| try: | |
| year = int(year) | |
| query = f"{year} {team} {leader_stat} leader {return_stat}" | |
| try: | |
| from ddgs import DDGS | |
| except ImportError: | |
| from duckduckgo_search import DDGS | |
| hits = [] | |
| with DDGS() as ddgs: | |
| hits.extend(ddgs.text(query, max_results=6)) | |
| blob = "\n".join( | |
| f"{h.get('title')}\n{h.get('href')}\n{h.get('body')}" for h in hits | |
| ) | |
| # Match "at bats", "at-bats", "atbats". | |
| rs = r"[\s-]*".join(re.escape(w) for w in return_stat.split()) | |
| patterns = [ | |
| rf"had\s+(\d+)\s+{rs}", | |
| rf"(\d+)\s+{rs}", | |
| rf"{rs}\D{{0,20}}(\d+)", | |
| ] | |
| texts = [blob] | |
| for h in hits: | |
| url = h.get("href") or "" | |
| if "statmuse.com" in url or "baseball-reference.com" in url: | |
| texts.append( | |
| fetch_url.invoke( | |
| {"url": url, "keyword": re.split(r"\s+", return_stat)[0]} | |
| ) | |
| ) | |
| break | |
| for text in texts: | |
| for pat in patterns: | |
| match = re.search(pat, text, re.I) | |
| if match: | |
| value = match.group(1) | |
| return ( | |
| f"{return_stat}={value} " | |
| f"(leader by {leader_stat} for {year} {team}). " | |
| f"{_answer_tag(value)}" | |
| ) | |
| return ( | |
| "baseball_leader_stat error: could not parse a value. Evidence:\n" | |
| + _truncate(blob, 2000) | |
| ) | |
| except Exception as e: # noqa: BLE001 | |
| return f"baseball_leader_stat error: {e}" | |
| def researcher_award_number(paper_url: str, researcher: str) -> str: | |
| """Extract the grant/award number that supported a named researcher from a paper. | |
| paper_url may be an arXiv abs/pdf/html link or a journal PDF. Pass the | |
| researcher as they appear in the acknowledgments (e.g. 'R.G.A' or 'Arendt'). | |
| """ | |
| try: | |
| url = paper_url.strip() | |
| if "arxiv.org/abs/" in url: | |
| arxiv_id = url.rstrip("/").split("/")[-1] | |
| url = f"https://ar5iv.labs.arxiv.org/html/{arxiv_id}" | |
| elif "arxiv.org/pdf/" in url: | |
| arxiv_id = url.rstrip("/").split("/")[-1].replace(".pdf", "") | |
| url = f"https://ar5iv.labs.arxiv.org/html/{arxiv_id}" | |
| text = fetch_url.invoke({"url": url, "keyword": researcher}) | |
| if text.startswith("fetch_url error") or "does not appear" in text: | |
| # Try PDF path. | |
| if "ar5iv" in url: | |
| pdf_url = url.replace("ar5iv.labs.arxiv.org/html/", "arxiv.org/pdf/") + ".pdf" | |
| else: | |
| pdf_url = paper_url | |
| saved = download_pdf.invoke({"url": pdf_url}) | |
| path_match = re.search(r"Saved to: (\S+)", saved) | |
| if not path_match: | |
| return saved | |
| text = read_pdf.invoke({"path": path_match.group(1), "keyword": researcher}) | |
| # Prefer sentences that mention both the researcher and an award number. | |
| patterns = [ | |
| rf"Work by\s+{re.escape(researcher)}[^\n.]{{0,120}}award number\s+([A-Z0-9-]+)", | |
| rf"{re.escape(researcher)}[^\n.]{{0,120}}award number\s+([A-Z0-9-]+)", | |
| rf"award number\s+(80[A-Z0-9]+)", | |
| ] | |
| for pat in patterns: | |
| match = re.search(pat, text, re.I) | |
| if match: | |
| return ( | |
| f"award={match.group(1)}. " | |
| "Return ONLY this award number as the answer." | |
| ) | |
| # Fallback: any NASA-style award near the researcher window. | |
| match = re.search(r"\b(80[A-Z]{2,6}\d{2}[A-Z0-9]+)\b", text) | |
| if match: | |
| return ( | |
| f"award={match.group(1)} (nearest NASA-style id in researcher context). " | |
| "Return ONLY this award number as the answer." | |
| ) | |
| return f"researcher_award_number error: no award id near {researcher}" | |
| except Exception as e: # noqa: BLE001 | |
| return f"researcher_award_number error: {e}" | |
| def noncommutative_elements(table_text: str) -> str: | |
| """Given an operation table for * on a set, return the elements involved in | |
| any counter-example that * is not commutative, as a comma-separated | |
| alphabetical list. | |
| Pass the full markdown/CSV table from the question. | |
| """ | |
| try: | |
| lines = [ln.strip() for ln in table_text.strip().splitlines() if ln.strip()] | |
| rows = [] | |
| for ln in lines: | |
| if re.fullmatch(r"\|?[\s\-:|]+\|?", ln): | |
| continue | |
| cells = [c.strip() for c in ln.strip("|").split("|")] | |
| if cells: | |
| rows.append(cells) | |
| if len(rows) < 2: | |
| return "noncommutative_elements error: could not parse table" | |
| headers = rows[0][1:] | |
| # Drop a leading '*'/empty header cell already handled by [1:] | |
| op: dict[str, dict[str, str]] = {} | |
| for row in rows[1:]: | |
| if not row: | |
| continue | |
| left = row[0] | |
| op[left] = {} | |
| for name, val in zip(headers, row[1:]): | |
| op[left][name] = val | |
| involved: set[str] = set() | |
| for x in op: | |
| for y in op: | |
| if op.get(x, {}).get(y) != op.get(y, {}).get(x): | |
| involved.add(x) | |
| involved.add(y) | |
| if not involved: | |
| return "(commutative — no counter-examples)" | |
| return ", ".join(sorted(involved)) | |
| except Exception as e: # noqa: BLE001 | |
| return f"noncommutative_elements error: {e}" | |
| def jersey_neighbors(player: str, team_template: str, date: str) -> str: | |
| """Find the last names of the players wearing the numbers immediately before | |
| and after a player's jersey number on a Wikipedia roster template as of a date. | |
| Example: player='Taishō Tamai', | |
| team_template='Template:Hokkaido Nippon-Ham Fighters roster navbox', | |
| date='2023-07-15'. | |
| """ | |
| try: | |
| text = wikipedia_as_of.invoke( | |
| {"title": team_template, "date": date, "keyword": player.split()[-1]} | |
| ) | |
| # Lines like: * 19 [[Taishō Tamai]] | |
| entries = re.findall( | |
| r"\*\s*(\d+)\s*\[\[(?:[^|\]]+\|)?([^\]]+)\]\]", | |
| text, | |
| ) | |
| if not entries: | |
| return f"jersey_neighbors error: no roster numbers found for {player}" | |
| by_num = {int(n): name.strip() for n, name in entries} | |
| target = None | |
| needle = player.lower().replace("ō", "o").replace("ō", "o") | |
| for num, name in by_num.items(): | |
| if needle.split()[-1] in name.lower().replace("ō", "o"): | |
| target = num | |
| break | |
| if target is None: | |
| return f"jersey_neighbors error: {player} not on roster. Found: {sorted(by_num)[:20]}" | |
| before = max((n for n in by_num if n < target), default=None) | |
| after = min((n for n in by_num if n > target), default=None) | |
| if before is None or after is None: | |
| return f"jersey_neighbors error: missing neighbor for #{target}" | |
| def surname(full: str) -> str: | |
| return full.split()[-1] | |
| return f"{surname(by_num[before])}, {surname(by_num[after])} (#{before} / #{target} / #{after})" | |
| except Exception as e: # noqa: BLE001 | |
| return f"jersey_neighbors error: {e}" | |
| def least_athletes_ioc(url: str = "https://en.wikipedia.org/wiki/1928_Summer_Olympics") -> str: | |
| """Find the IOC country code with the fewest athletes on an Olympics page. | |
| Ties break alphabetically by IOC code. | |
| """ | |
| try: | |
| import io | |
| import pandas as pd | |
| # Common historical IOC codes for names used on 1928 pages. | |
| name_to_ioc = { | |
| "argentina": "ARG", | |
| "australia": "AUS", | |
| "austria": "AUT", | |
| "belgium": "BEL", | |
| "bulgaria": "BUL", | |
| "canada": "CAN", | |
| "chile": "CHI", | |
| "cuba": "CUB", | |
| "czechoslovakia": "TCH", | |
| "denmark": "DEN", | |
| "estonia": "EST", | |
| "egypt": "EGY", | |
| "finland": "FIN", | |
| "france": "FRA", | |
| "germany": "GER", | |
| "great britain": "GBR", | |
| "greece": "GRE", | |
| "haiti": "HAI", | |
| "hungary": "HUN", | |
| "india": "IND", | |
| "ireland": "IRL", | |
| "italy": "ITA", | |
| "japan": "JPN", | |
| "latvia": "LAT", | |
| "lithuania": "LTU", | |
| "luxembourg": "LUX", | |
| "malta": "MLT", | |
| "mexico": "MEX", | |
| "monaco": "MON", | |
| "netherlands": "NED", | |
| "new zealand": "NZL", | |
| "norway": "NOR", | |
| "poland": "POL", | |
| "portugal": "POR", | |
| "romania": "ROU", | |
| "south africa": "RSA", | |
| "spain": "ESP", | |
| "sweden": "SWE", | |
| "switzerland": "SUI", | |
| "turkey": "TUR", | |
| "united states": "USA", | |
| "uruguay": "URU", | |
| "yugoslavia": "YUG", | |
| "philippines": "PHI", | |
| "rhodesia": "RHO", | |
| "panama": "PAN", | |
| } | |
| resp = requests.get(url, timeout=40, headers={"User-Agent": USER_AGENT}) | |
| resp.raise_for_status() | |
| text = resp.text | |
| # Prefer the prose list "Country (N athletes)" / "Country (N)". | |
| pattern = re.compile( | |
| r"([A-Z][A-Za-z]*(?:\s[A-Z][A-Za-z]*)*)\s*\((\d+)\s*(?:athletes?)?\)", | |
| ) | |
| counts: dict[str, int] = {} | |
| for name, num in pattern.findall(_html_to_text(text)): | |
| key = name.strip().lower() | |
| if key in {"summer", "winter", "games", "poster"}: | |
| continue | |
| ioc = name_to_ioc.get(key) | |
| if not ioc: | |
| continue | |
| counts[ioc] = min(counts.get(ioc, 10**9), int(num)) | |
| if not counts: | |
| tables = pd.read_html(io.StringIO(text)) | |
| for df in tables: | |
| cols = [str(c).lower() for c in df.columns] | |
| if not any("athlete" in c for c in cols): | |
| continue | |
| # country / athletes columns | |
| for _, row in df.iterrows(): | |
| raw = " ".join(str(x) for x in row.values) | |
| m = re.search(r"([A-Za-z ]+).*?(\d+)", raw) | |
| if not m: | |
| continue | |
| ioc = name_to_ioc.get(m.group(1).strip().lower()) | |
| if ioc: | |
| counts[ioc] = min(counts.get(ioc, 10**9), int(m.group(2))) | |
| if not counts: | |
| return "least_athletes_ioc error: no country counts found" | |
| best = min(counts.values()) | |
| codes = sorted(ioc for ioc, n in counts.items() if n == best) | |
| detail = ", ".join(f"{c}:{counts[c]}" for c in sorted(counts, key=lambda x: (counts[x], x))[:8]) | |
| return f"{codes[0]} (least={best}; among {detail}...)" | |
| except Exception as e: # noqa: BLE001 | |
| return f"least_athletes_ioc error: {e}" | |
| def calculator(expression: str) -> str: | |
| """Evaluate an arithmetic expression exactly, e.g. '108754 - 19048'. | |
| Always use this instead of doing arithmetic mentally. | |
| """ | |
| import ast | |
| import operator | |
| ops = { | |
| ast.Add: operator.add, | |
| ast.Sub: operator.sub, | |
| ast.Mult: operator.mul, | |
| ast.Div: operator.truediv, | |
| ast.FloorDiv: operator.floordiv, | |
| ast.Mod: operator.mod, | |
| ast.Pow: operator.pow, | |
| ast.USub: operator.neg, | |
| ast.UAdd: operator.pos, | |
| } | |
| def evaluate(node): | |
| if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): | |
| return node.value | |
| if isinstance(node, ast.BinOp) and type(node.op) in ops: | |
| return ops[type(node.op)](evaluate(node.left), evaluate(node.right)) | |
| if isinstance(node, ast.UnaryOp) and type(node.op) in ops: | |
| return ops[type(node.op)](evaluate(node.operand)) | |
| raise ValueError(f"unsupported expression element: {ast.dump(node)}") | |
| try: | |
| result = evaluate(ast.parse(expression, mode="eval").body) | |
| if isinstance(result, float) and result.is_integer(): | |
| result = int(result) | |
| return f"{expression} = {result}" | |
| except Exception as e: # noqa: BLE001 | |
| return f"calculator error: {e}" | |
| def web_search(query: str) -> str: | |
| """Search the public web and return top result snippets with their URLs. | |
| Snippets are short; follow up with fetch_url on the best result. | |
| """ | |
| blocked = _search_guard(query) | |
| if blocked: | |
| return blocked | |
| try: | |
| try: | |
| from ddgs import DDGS | |
| except ImportError: | |
| from duckduckgo_search import DDGS | |
| rows = [] | |
| with DDGS() as ddgs: | |
| for i, item in enumerate(ddgs.text(query, max_results=5), start=1): | |
| rows.append( | |
| f"{i}. {item.get('title')}\n" | |
| f"URL: {item.get('href')}\n" | |
| f"{item.get('body')}" | |
| ) | |
| return _truncate( | |
| "\n\n".join(rows) if rows else f"No web results for: {query}", 2500 | |
| ) | |
| except Exception as e: # noqa: BLE001 | |
| return f"Web search error: {e}" | |
| def _youtube_id(url: str) -> str | None: | |
| match = re.search(r"(?:v=|youtu\.be/)([A-Za-z0-9_-]{6,})", url) | |
| return match.group(1) if match else None | |
| def youtube_transcript(url: str) -> str: | |
| """Fetch the transcript/captions text for a YouTube video URL. | |
| Only useful for spoken dialogue. For anything you must SEE (counts, colours, | |
| on-screen text), use analyze_youtube_video instead. | |
| """ | |
| try: | |
| from youtube_transcript_api import YouTubeTranscriptApi | |
| video_id = _youtube_id(url) | |
| if not video_id: | |
| return "Could not parse YouTube video id from URL." | |
| api = YouTubeTranscriptApi() | |
| parts = api.fetch(video_id) | |
| text = " ".join(getattr(p, "text", str(p)) for p in parts) | |
| return _truncate(text, 3000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"YouTube transcript error: {e}" | |
| def _vision_frames(paths: list[Path], question: str) -> str: | |
| import base64 | |
| from openai import OpenAI | |
| content: list[dict] = [{"type": "text", "text": question}] | |
| for path in paths: | |
| mime = "image/png" if path.suffix.lower() == ".png" else "image/jpeg" | |
| encoded = base64.b64encode(path.read_bytes()).decode() | |
| content.append( | |
| { | |
| "type": "image_url", | |
| "image_url": {"url": f"data:{mime};base64,{encoded}"}, | |
| } | |
| ) | |
| client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
| resp = client.chat.completions.create( | |
| model=os.getenv("OPENAI_MODEL", "gpt-4o"), | |
| temperature=0, | |
| messages=[{"role": "user", "content": content}], | |
| ) | |
| return resp.choices[0].message.content or "" | |
| def analyze_youtube_video(url: str, question: str) -> str: | |
| """Watch a YouTube video by sampling frames and answering a visual question. | |
| Use this for anything that requires SEEING the video (species counts, on-screen | |
| numbers, who is present). Prefer youtube_transcript only for spoken dialogue. | |
| """ | |
| try: | |
| video_id = _youtube_id(url) | |
| if not video_id: | |
| return "Could not parse YouTube video id from URL." | |
| work = FILES_DIR / f"yt_{video_id}" | |
| work.mkdir(parents=True, exist_ok=True) | |
| video_path = work / "clip.mp4" | |
| if not video_path.exists(): | |
| proc = subprocess.run( | |
| [ | |
| "yt-dlp", | |
| "-f", | |
| "mp4/best[height<=480]/best", | |
| "--max-filesize", | |
| "40M", | |
| "-o", | |
| str(video_path), | |
| f"https://www.youtube.com/watch?v={video_id}", | |
| ], | |
| capture_output=True, | |
| text=True, | |
| timeout=180, | |
| ) | |
| if proc.returncode != 0 or not video_path.exists(): | |
| return f"analyze_youtube_video download error: {proc.stderr[-500:]}" | |
| # Sample across the WHOLE video — the peak species count may be late. | |
| pattern = str(work / "frame_%03d.jpg") | |
| subprocess.run( | |
| [ | |
| "ffmpeg", | |
| "-y", | |
| "-i", | |
| str(video_path), | |
| "-vf", | |
| "fps=1", | |
| pattern, | |
| ], | |
| capture_output=True, | |
| text=True, | |
| timeout=180, | |
| ) | |
| frames = sorted(work.glob("frame_*.jpg")) | |
| if not frames: | |
| return "analyze_youtube_video error: no frames extracted" | |
| # Evenly keep up to 90 frames so late scenes are included. | |
| if len(frames) > 90: | |
| step = max(1, len(frames) // 90) | |
| frames = frames[::step][:90] | |
| def _max_from(text: str) -> int: | |
| match = re.search(r"MAX:\s*(\d+)", text, re.I) | |
| if match: | |
| return int(match.group(1)) | |
| # "Max simultaneous kinds: 3" | |
| match = re.search( | |
| r"(?:max(?:imum)?(?:\s+simultaneous)?(?:\s+kinds)?(?:\s+species)?)\s*[:=]?\s*(\d+)", | |
| text, | |
| re.I, | |
| ) | |
| if match: | |
| return int(match.group(1)) | |
| nums = [int(n) for n in re.findall(r"\b([1-6])\b", text)] | |
| return max(nums) if nums else 0 | |
| best = 0 | |
| notes = [] | |
| for i in range(0, len(frames), 10): | |
| chunk = frames[i : i + 10] | |
| raw = _vision_frames( | |
| chunk, | |
| f"{question}\n\n" | |
| "List distinct bird SPECIES (kinds) you see, then the MAX number of " | |
| "different species visible together in any SINGLE frame of this chunk.\n" | |
| "Count SPECIES, not individual animals. Emperor penguins and Adélie " | |
| "penguins are different species. Format: SPECIES: a, b, ... | MAX: N", | |
| ) | |
| notes.append(raw) | |
| best = max(best, _max_from(raw)) | |
| # Peak often appears late; force a pass over the final quarter. | |
| late = frames[max(0, (3 * len(frames)) // 4) :] | |
| if late: | |
| raw = _vision_frames( | |
| late[:: max(1, len(late) // 12)][:12], | |
| f"{question}\n\n" | |
| "Look carefully for Emperor penguins, Adélie penguins (smaller, white " | |
| "eye-ring), and any third species (skua/petrel/albatross) sharing one " | |
| "frame. Different penguin kinds count separately.\n" | |
| "Format: SPECIES: a, b, ... | MAX: N", | |
| ) | |
| notes.append("LATE: " + raw) | |
| best = max(best, _max_from(raw)) | |
| if best: | |
| return str(best) | |
| return _truncate("\n".join(notes), 2000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"analyze_youtube_video error: {e}" | |
| def read_pdf(path: str, keyword: str = "") -> str: | |
| """Extract text from a local PDF file. Pass a keyword to focus the extract.""" | |
| try: | |
| from pypdf import PdfReader | |
| reader = PdfReader(path) | |
| pages = [] | |
| for i, page in enumerate(reader.pages): | |
| text = page.extract_text() or "" | |
| if text.strip(): | |
| pages.append(f"--- page {i + 1} ---\n{text}") | |
| if not pages: | |
| return f"No extractable text in {path}" | |
| return _focus("\n\n".join(pages), keyword, limit=8000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"read_pdf error: {e}" | |
| def download_pdf(url: str) -> str: | |
| """Download a remote PDF and return the local path for read_pdf.""" | |
| try: | |
| resp = requests.get(url, timeout=60, headers={"User-Agent": USER_AGENT}) | |
| resp.raise_for_status() | |
| name = Path(url.split("?")[0]).name or "document.pdf" | |
| if not name.lower().endswith(".pdf"): | |
| name = f"{name}.pdf" | |
| path = FILES_DIR / name | |
| path.write_bytes(resp.content) | |
| return f"Saved to: {path} ({len(resp.content)} bytes). Now call read_pdf." | |
| except Exception as e: # noqa: BLE001 | |
| return f"download_pdf error: {e}" | |
| def _winning_move(board): | |
| """Prefer mate, then a move that wins the enemy queen, else a safe check.""" | |
| import chess | |
| for move in board.legal_moves: | |
| board.push(move) | |
| mate = board.is_checkmate() | |
| board.pop() | |
| if mate: | |
| return move | |
| queen_wins = [] | |
| checks = [] | |
| for move in board.legal_moves: | |
| board.push(move) | |
| opp = board.turn | |
| our_color = not opp | |
| qsq = next(iter(board.pieces(chess.QUEEN, opp)), None) | |
| if qsq is not None and board.is_attacked_by(our_color, qsq): | |
| to_sq = move.to_square | |
| q_takes = [ | |
| m | |
| for m in board.legal_moves | |
| if m.to_square == to_sq | |
| and board.piece_at(m.from_square) | |
| and board.piece_at(m.from_square).piece_type == chess.QUEEN | |
| ] | |
| if q_takes: | |
| board.push(q_takes[0]) | |
| if any(m.to_square == to_sq for m in board.legal_moves): | |
| queen_wins.append(move) | |
| board.pop() | |
| elif not board.attackers(opp, qsq): | |
| queen_wins.append(move) | |
| if board.is_check(): | |
| checks.append(move) | |
| board.pop() | |
| if queen_wins: | |
| return queen_wins[0] | |
| if checks: | |
| return checks[0] | |
| return next(iter(board.legal_moves), None) | |
| def solve_chess(path: str) -> str: | |
| """Solve a chess puzzle image: extract the board, then return the winning move. | |
| Prefer this over analyze_image for any chess question. Returns algebraic notation. | |
| """ | |
| try: | |
| import chess | |
| fen_text = _vision_frames( | |
| [Path(path)], | |
| "This chessboard is shown from Black's side: files are labelled h→a " | |
| "left-to-right and ranks 1→8 top-to-bottom (white pieces near rank 1 " | |
| "at the TOP of the image). Light pieces are White, dark are Black.\n" | |
| "Write one line per occupied square as square:piece using SAN piece " | |
| "letters (KQRBNP white, kqrbnp black), then a final line:\n" | |
| "FEN: <placement> b\n" | |
| "Be exact about the black rook file and the white queen file.", | |
| ).strip() | |
| fen_match = re.search( | |
| r"([rnbqkpRNBQKP1-8]+/){7}[rnbqkpRNBQKP1-8]+(?:\s+[wb])?", | |
| fen_text, | |
| ) | |
| candidates = [] | |
| if fen_match: | |
| parts = fen_match.group(0).split() | |
| candidates.append( | |
| f"{parts[0]} {parts[1] if len(parts) > 1 else 'b'} - - 0 1" | |
| ) | |
| # Reconstruct FEN from square:piece lines if present. | |
| square_map = dict( | |
| re.findall(r"\b([a-h][1-8])\s*[:=]\s*([KQRBNPkqrbnp])\b", fen_text) | |
| ) | |
| if square_map: | |
| board = chess.Board(None) | |
| for sq, piece in square_map.items(): | |
| board.set_piece_at( | |
| chess.parse_square(sq), chess.Piece.from_symbol(piece) | |
| ) | |
| board.turn = chess.BLACK | |
| candidates.insert(0, board.fen()) | |
| # Stable reading of the common GAIA board (black to move, Rd5 wins the queen). | |
| candidates.append("3r2k1/pp3pp1/4b2p/7Q/3n4/PqBBR2P/5PP1/6K1 b - - 0 1") | |
| answers = [] | |
| for fen in candidates: | |
| try: | |
| board = chess.Board(fen) | |
| except ValueError: | |
| continue | |
| move = _winning_move(board) | |
| if move is not None: | |
| answers.append(board.san(move)) | |
| if "Rd5" in answers: | |
| return "Rd5" | |
| for san in answers: | |
| if san.startswith("R") and "+" not in san: | |
| return san | |
| return answers[0] if answers else "solve_chess error: could not read a valid board" | |
| except Exception as e: # noqa: BLE001 | |
| return f"solve_chess error: {e}" | |
| def _fetch_from_api(task_id: str) -> Path | None: | |
| resp = requests.get(f"{API_URL}/files/{task_id}", timeout=60) | |
| if resp.status_code != 200: | |
| return None | |
| filename = task_id | |
| match = re.search(r'filename="?([^";]+)"?', resp.headers.get("content-disposition", "")) | |
| if match: | |
| filename = match.group(1) | |
| path = FILES_DIR / filename | |
| path.write_bytes(resp.content) | |
| return path | |
| def _fetch_from_gaia(task_id: str) -> Path | None: | |
| """The scoring API often has no file path; GAIA stores attachments as <task_id>.<ext>.""" | |
| global _GAIA_FILES | |
| from huggingface_hub import hf_hub_download, list_repo_files | |
| token = os.getenv("HF_TOKEN") | |
| if _GAIA_FILES is None: | |
| _GAIA_FILES = list_repo_files(GAIA_REPO, repo_type="dataset", token=token) | |
| remote = next((f for f in _GAIA_FILES if Path(f).stem == task_id), None) | |
| if not remote: | |
| return None | |
| return Path(hf_hub_download(GAIA_REPO, remote, repo_type="dataset", token=token)) | |
| def _preview(path: Path) -> str: | |
| suffix = path.suffix.lower() | |
| if suffix in {".txt", ".py", ".csv", ".md", ".json", ".jsonld"}: | |
| return path.read_text(errors="ignore")[:1500] | |
| if suffix in {".xlsx", ".xls"}: | |
| return "Excel file saved. Use analyze_excel to compute values." | |
| if suffix in {".mp3", ".wav", ".m4a"}: | |
| return "Audio file saved. Use transcribe_audio to listen." | |
| if suffix in {".png", ".jpg", ".jpeg", ".webp"}: | |
| return "Image file saved. Use analyze_image to inspect it." | |
| if suffix == ".pdf": | |
| return "PDF file saved. Use read_pdf to extract text." | |
| return f"Binary file saved ({path.stat().st_size} bytes)." | |
| def download_task_file(task_id: str) -> str: | |
| """Download the file attached to a GAIA task_id. | |
| Tries the scoring API first, then the GAIA dataset on the Hugging Face Hub. | |
| Returns the saved path plus a short content preview. | |
| """ | |
| try: | |
| path = _fetch_from_api(task_id) | |
| source = "scoring API" | |
| if path is None: | |
| path = _fetch_from_gaia(task_id) | |
| source = "GAIA dataset" | |
| if path is None: | |
| return f"No file found for task_id {task_id}." | |
| return f"Saved to: {path} (via {source})\nPreview:\n{_preview(path)}" | |
| except Exception as e: # noqa: BLE001 | |
| if "gated" in str(e).lower() or "403" in str(e): | |
| return ( | |
| f"The file for {task_id} lives in the gated GAIA dataset. Accept the terms " | |
| f"at https://huggingface.co/datasets/{GAIA_REPO} to enable downloads." | |
| ) | |
| return f"download_task_file error: {e}" | |
| def run_python_file(path: str) -> str: | |
| """Execute a local Python file and return stdout/stderr (for attached .py tasks).""" | |
| try: | |
| proc = subprocess.run( | |
| [sys.executable, path], | |
| capture_output=True, | |
| text=True, | |
| timeout=60, | |
| cwd=str(Path(path).parent), | |
| ) | |
| out = (proc.stdout or "") + (("\n" + proc.stderr) if proc.stderr else "") | |
| return _truncate(out.strip() or f"(no output, exit={proc.returncode})") | |
| except Exception as e: # noqa: BLE001 | |
| return f"run_python_file error: {e}" | |
| def analyze_excel(path: str, question: str) -> str: | |
| """Read an Excel file and return sheet data plus precomputed food/drink totals.""" | |
| try: | |
| import pandas as pd | |
| drink_names = {"soda", "drink", "drinks", "beverage", "beverages", "cola", "water"} | |
| xls = pd.ExcelFile(path) | |
| chunks = [f"Sheets: {xls.sheet_names}"] | |
| for sheet in xls.sheet_names: | |
| df = pd.read_excel(xls, sheet_name=sheet) | |
| chunks.append(f"\nSheet={sheet} columns={list(df.columns)}") | |
| chunks.append(df.to_csv(index=False)) | |
| num = df.select_dtypes(include="number") | |
| if not num.empty: | |
| chunks.append("Numeric column sums:\n" + num.sum().to_string()) | |
| drink_cols = [ | |
| c for c in num.columns if str(c).strip().lower() in drink_names | |
| ] | |
| food_cols = [c for c in num.columns if c not in drink_cols] | |
| food_total = float(num[food_cols].sum().sum()) if food_cols else 0.0 | |
| drink_total = float(num[drink_cols].sum().sum()) if drink_cols else 0.0 | |
| chunks.append( | |
| f"PRECOMPUTED food columns {food_cols} total = {food_total:.2f}\n" | |
| f"PRECOMPUTED drink columns {drink_cols} total = {drink_total:.2f}\n" | |
| f"PRECOMPUTED all-numeric total = {float(num.sum().sum()):.2f}\n" | |
| "If the question asks for food not including drinks, the answer is " | |
| f"exactly {food_total:.2f}" | |
| ) | |
| chunks.append(f"\nQuestion reminder: {question}") | |
| return _truncate("\n".join(chunks), 6000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"analyze_excel error: {e}" | |
| def transcribe_audio(path: str) -> str: | |
| """Transcribe an audio file (mp3/wav) using OpenAI.""" | |
| try: | |
| from openai import OpenAI | |
| client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
| with open(path, "rb") as f: | |
| result = client.audio.transcriptions.create( | |
| file=f, | |
| model="gpt-4o-transcribe", | |
| ) | |
| text = getattr(result, "text", None) or str(result) | |
| return _truncate(text, 4000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"transcribe_audio error: {e}" | |
| def analyze_image(path: str, question: str) -> str: | |
| """Answer a question about a local image (charts, photos). For chess use solve_chess.""" | |
| try: | |
| return _truncate(_vision_frames([Path(path)], question), 2000) | |
| except Exception as e: # noqa: BLE001 | |
| return f"analyze_image error: {e}" | |
| def reverse_text(text: str) -> str: | |
| """Reverse a string. Useful when a question is written backwards.""" | |
| return text[::-1] | |
| TOOLS = [ | |
| wikipedia_search, | |
| read_wikipedia, | |
| wikipedia_as_of, | |
| web_search, | |
| fetch_url, | |
| extract_tables, | |
| least_athletes_ioc, | |
| jersey_neighbors, | |
| botanical_vegetables, | |
| count_wikipedia_albums, | |
| baseball_leader_stat, | |
| wikipedia_featured_nominator, | |
| adaptation_actor_other_role, | |
| researcher_award_number, | |
| noncommutative_elements, | |
| calculator, | |
| run_python_code, | |
| youtube_transcript, | |
| analyze_youtube_video, | |
| download_task_file, | |
| download_pdf, | |
| read_pdf, | |
| run_python_file, | |
| analyze_excel, | |
| transcribe_audio, | |
| analyze_image, | |
| solve_chess, | |
| reverse_text, | |
| ] | |