Spaces:
Build error
Build error
| from __future__ import annotations | |
| import ast | |
| import os | |
| import re | |
| from collections.abc import Iterable | |
| from dataclasses import dataclass, field | |
| from difflib import SequenceMatcher | |
| from functools import lru_cache | |
| from io import BytesIO | |
| from typing import Any | |
| from urllib.parse import parse_qsl, unquote, urlencode, urljoin, urlparse, urlunparse | |
| from uuid import uuid4 | |
| import requests | |
| import trafilatura | |
| import wikipedia | |
| import wikipedia.wikipedia as wiki_module | |
| from bs4 import BeautifulSoup | |
| from bs4.element import Tag | |
| from langchain.agents import create_agent | |
| from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse | |
| from langchain.tools import tool | |
| from langchain_chroma import Chroma | |
| from langchain_community.tools import DuckDuckGoSearchResults, WikipediaQueryRun | |
| from langchain_community.utilities import WikipediaAPIWrapper | |
| from langchain_core.messages import SystemMessage, ToolMessage | |
| from langchain_openai import ChatOpenAI, OpenAIEmbeddings | |
| from pydantic import BaseModel, Field | |
| from pypdf import PdfReader | |
| # wiki patches | |
| wikipedia.set_lang("en") | |
| wiki_module.API_URL = "https://en.wikipedia.org/w/api.php" | |
| wikipedia.set_user_agent("GAIA-LangChain-Agent/1.0 (contact: placeholder-email@example.com)") | |
| URL_PATTERN = re.compile(r"https?://[^\s<>\"']+", re.IGNORECASE) | |
| MAX_TOOL_CALLS = 5 | |
| MAX_SEARCH_CALLS = 2 | |
| MAX_WEBPAGE_READS = 2 | |
| MAX_AGENT_ITERATIONS = 6 | |
| MAX_TOOL_OUTPUT_CHARS = 10_000 | |
| _DROP_QUERY_KEYS = { | |
| "download", | |
| "display", | |
| "print", | |
| "output", | |
| "view", | |
| "format", | |
| "share", | |
| "source", | |
| "ref", | |
| "referrer", | |
| "fbclid", | |
| "gclid", | |
| "mc_cid", | |
| "mc_eid", | |
| } | |
| def canonicalize_url(url: str) -> str: | |
| """Return a stable source identity, collapsing common wrappers and variants.""" | |
| value = unquote(url.strip()) | |
| for _ in range(3): | |
| parsed = urlparse(value) | |
| host = (parsed.hostname or "").lower() | |
| if host in {"r.jina.ai", "r.jina.ai/http", "r.jina.ai/https"}: | |
| wrapped = parsed.path.lstrip("/") | |
| if wrapped.startswith(("http://", "https://")): | |
| value = wrapped | |
| continue | |
| query = dict(parse_qsl(parsed.query, keep_blank_values=True)) | |
| if host in {"www.google.com", "google.com", "webcache.googleusercontent.com"}: | |
| wrapped = query.get("url") or query.get("q") | |
| if wrapped and wrapped.startswith(("http://", "https://")): | |
| value = wrapped | |
| continue | |
| break | |
| parsed = urlparse(value) | |
| host = (parsed.hostname or "").lower() | |
| port = parsed.port | |
| netloc = host if port in {None, 80, 443} else f"{host}:{port}" | |
| path = re.sub(r"/(?:print|download|amp)/?$", "", parsed.path, flags=re.I) or "/" | |
| kept = [] | |
| for key, val in parse_qsl(parsed.query, keep_blank_values=True): | |
| lowered = key.casefold() | |
| if lowered.startswith("utm_") or lowered in _DROP_QUERY_KEYS: | |
| continue | |
| kept.append((key, val)) | |
| return urlunparse( | |
| ( | |
| parsed.scheme.lower() or "https", | |
| netloc, | |
| path.rstrip("/") or "/", | |
| "", | |
| urlencode(sorted(kept)), | |
| "", | |
| ) | |
| ) | |
| def _normalized_query(query: str) -> str: | |
| words = re.findall(r"[a-z0-9]+", query.casefold()) | |
| stop = {"a", "an", "and", "for", "in", "of", "on", "the", "to", "what", "who", "is"} | |
| return " ".join(sorted(word for word in words if word not in stop)) | |
| def _queries_similar(left: str, right: str) -> bool: | |
| a, b = _normalized_query(left), _normalized_query(right) | |
| if not a or not b: | |
| return a == b | |
| aset, bset = set(a.split()), set(b.split()) | |
| jaccard = len(aset & bset) / len(aset | bset) | |
| return jaccard >= 0.8 or SequenceMatcher(None, a, b).ratio() >= 0.88 | |
| class ResearchBudget: | |
| """Per-agent-run deterministic retrieval accounting.""" | |
| total_calls: int = 0 | |
| search_calls: int = 0 | |
| webpage_reads: int = 0 | |
| model_iterations: int = 0 | |
| output_chars: int = 0 | |
| queries: list[str] = field(default_factory=list) | |
| urls: set[str] = field(default_factory=set) | |
| focused_failures: set[str] = field(default_factory=set) | |
| def reject_tool_call(self, name: str, args: dict[str, Any]) -> str | None: | |
| if self.total_calls >= MAX_TOOL_CALLS: | |
| return "LIMIT_REACHED: maximum total tool calls reached; answer from existing evidence." | |
| self.total_calls += 1 | |
| if name in {"duckduckgo_search", "wikipedia_search"}: | |
| query = str(args.get("query", "")) | |
| if self.search_calls >= MAX_SEARCH_CALLS: | |
| return "LIMIT_REACHED: maximum search calls reached; answer from existing evidence." | |
| if any(_queries_similar(query, previous) for previous in self.queries): | |
| return "DUPLICATE: equivalent search already attempted; use existing evidence." | |
| self.search_calls += 1 | |
| self.queries.append(query) | |
| elif name == "read_webpage": | |
| url = canonicalize_url(str(args.get("url", ""))) | |
| focus = str(args.get("focus", "")).strip() | |
| if self.webpage_reads >= MAX_WEBPAGE_READS: | |
| return ( | |
| "LIMIT_REACHED: maximum webpage reads reached; answer from existing evidence." | |
| ) | |
| if url in self.urls: | |
| if not focus and url in self.focused_failures: | |
| return "DUPLICATE: empty-focus retry forbidden after focused failure." | |
| return "DUPLICATE: canonical URL already attempted; use existing evidence." | |
| self.webpage_reads += 1 | |
| self.urls.add(url) | |
| return None | |
| def compact_output(self, name: str, args: dict[str, Any], content: str) -> str: | |
| status = content.split(":", 1)[0] | |
| if name == "read_webpage" and status in {"NOT_FOUND", "LOW_QUALITY", "ERROR"}: | |
| canonical = canonicalize_url(str(args.get("url", ""))) | |
| if str(args.get("focus", "")).strip(): | |
| self.focused_failures.add(canonical) | |
| label = (urlparse(canonical).hostname or "Webpage").removeprefix("www.") | |
| content = f"{label} read failed: {content[:180]}; canonical URL already attempted." | |
| remaining = max(0, MAX_TOOL_OUTPUT_CHARS - self.output_chars) | |
| content = _truncate_text(content, remaining) | |
| self.output_chars += len(content) | |
| return ( | |
| content | |
| or "LIMIT_REACHED: cumulative tool output exhausted; answer from existing evidence." | |
| ) | |
| class FinalAnswer(BaseModel): | |
| """Exact answer returned to the GAIA evaluator.""" | |
| answer: str = Field(description="Only the exact final answer, with no explanation or label.") | |
| def _extract_urls(text: str) -> list[str]: | |
| return [match.rstrip(".,;:!?)]") for match in URL_PATTERN.findall(text)] | |
| def _is_youtube_url(url: str) -> bool: | |
| host = (urlparse(url).hostname or "").lower() | |
| return host in {"youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be"} | |
| def _normalize_answer(value: Any) -> str: | |
| text = str(value or "").strip() | |
| text = re.sub(r"^```(?:text)?\s*", "", text, flags=re.IGNORECASE) | |
| text = re.sub(r"\s*```$", "", text) | |
| text = re.sub( | |
| r"^(?:final\s+answer|answer|response)\s*:\s*", | |
| "", | |
| text, | |
| flags=re.IGNORECASE, | |
| ) | |
| return text.strip() | |
| _DDG = DuckDuckGoSearchResults(output_format="list", num_results=8) | |
| _WIKIPEDIA = WikipediaQueryRun( | |
| api_wrapper=WikipediaAPIWrapper( # type: ignore[call-arg] | |
| top_k_results=1, | |
| doc_content_chars_max=10_000, | |
| ) | |
| ) | |
| def duckduckgo_search(query: str) -> str: | |
| """ | |
| Search the public web and return relevant titles, snippets, and URLs. | |
| Args: | |
| query: A concise, single-purpose search query using distinctive names, | |
| titles, dates, identifiers, or exact quoted phrases. Operators such | |
| as `site:` and `filetype:` may be used when they materially narrow | |
| the results. Avoid full questions, filler words, unrelated topics, | |
| and near-duplicate searches. | |
| Returns: | |
| Relevant DuckDuckGo search results or a readable error message. | |
| """ | |
| try: | |
| raw = _DDG.invoke(query) | |
| return _format_search_results(raw, query) | |
| except Exception as exc: | |
| return f"DuckDuckGo search failed for {query!r}: {type(exc).__name__}: {exc}" | |
| def wikipedia_search(query: str) -> str: | |
| """ | |
| Search English Wikipedia for one article or topic. | |
| Args: | |
| query: A concise article title, entity name, or topic, such as | |
| "Mercedes Sosa", "Giganotosaurus", or | |
| "History of artificial intelligence". | |
| Do not include URLs, domain names, full questions, or search | |
| operators such as `site:` and `inurl:`. | |
| Returns: | |
| Content from the most relevant English Wikipedia article, or a readable | |
| error message if retrieval fails. | |
| """ | |
| try: | |
| return str(_WIKIPEDIA.invoke(query)) | |
| except Exception as exc: | |
| return f"Wikipedia search failed for {query!r}: {type(exc).__name__}: {exc}" | |
| def _format_search_results(raw: Any, query: str) -> str: | |
| """Filter and bound public-search output to three useful results.""" | |
| if isinstance(raw, str): | |
| try: | |
| raw = ast.literal_eval(raw) | |
| except (SyntaxError, ValueError): | |
| raw = [] | |
| if not isinstance(raw, list): | |
| return "ERROR: search returned no usable results." | |
| query_terms = set(re.findall(r"[a-z0-9]+", query.casefold())) | |
| blocked_hosts = { | |
| "facebook.com", | |
| "instagram.com", | |
| "linkedin.com", | |
| "pinterest.com", | |
| "tiktok.com", | |
| "x.com", | |
| } | |
| blocked_terms = {"buy", "cart", "coupon", "job", "jobs", "salary", "shop", "shopping"} | |
| usable: list[str] = [] | |
| seen: set[str] = set() | |
| for item in raw: | |
| if not isinstance(item, dict): | |
| continue | |
| title = _clean_text(str(item.get("title") or "")) | |
| snippet = _clean_text(str(item.get("snippet") or item.get("body") or "")) | |
| url = str(item.get("link") or item.get("href") or item.get("url") or "").strip() | |
| parsed = urlparse(url) | |
| host = (parsed.hostname or "").removeprefix("www.").lower() | |
| searchable = f"{title} {snippet} {url}".casefold() | |
| canonical = canonicalize_url(url) if url else "" | |
| reflected = parsed.path in {"", "/", "/search"} and bool(parse_qsl(parsed.query)) | |
| irrelevant = blocked_terms & set(re.findall(r"[a-z0-9]+", searchable)) | |
| overlap = query_terms & set(re.findall(r"[a-z0-9]+", searchable)) | |
| if ( | |
| not title | |
| or not snippet | |
| or not url | |
| or parsed.scheme not in {"http", "https"} | |
| or any(host == domain or host.endswith(f".{domain}") for domain in blocked_hosts) | |
| or reflected | |
| or irrelevant | |
| or not overlap | |
| or canonical in seen | |
| ): | |
| continue | |
| seen.add(canonical) | |
| usable.append(f"{title}\n{snippet}\n{url}") | |
| if len(usable) == 3: | |
| break | |
| if not usable: | |
| return "ERROR: search returned no usable results." | |
| return _truncate_text("\n\n".join(usable), 2_000) | |
| def _clean_text(text: str) -> str: | |
| """Normalize whitespace while preserving useful line boundaries.""" | |
| lines: list[str] = [] | |
| for line in text.splitlines(): | |
| cleaned = " ".join(line.split()) | |
| if cleaned: | |
| lines.append(cleaned) | |
| return "\n".join(lines) | |
| def _normalize_match_text(text: str) -> str: | |
| """Normalize heading and focus text for conservative matching.""" | |
| text = re.sub(r"\[\s*edit\s*\]", " ", text, flags=re.IGNORECASE) | |
| text = re.sub(r"\[[0-9]+\]", " ", text) | |
| return " ".join(re.findall(r"[\w]+", text.casefold(), flags=re.UNICODE)) | |
| def _heading_match_score(heading: str, focus: str) -> float: | |
| """Score exact and strong partial heading matches; return zero for weak ones.""" | |
| normalized_heading = _normalize_match_text(heading) | |
| normalized_focus = _normalize_match_text(focus) | |
| if not normalized_heading or not normalized_focus: | |
| return 0.0 | |
| if normalized_heading == normalized_focus: | |
| return 100.0 | |
| heading_tokens = set(normalized_heading.split()) | |
| focus_tokens = set(normalized_focus.split()) | |
| overlap = heading_tokens & focus_tokens | |
| if not overlap: | |
| return 0.0 | |
| shorter, longer = sorted((normalized_heading, normalized_focus), key=len) | |
| if shorter in longer: | |
| # A distinctive one-word focus (for example "November") can identify a | |
| # longer heading, but short generic fragments should not. | |
| if len(shorter) >= 5: | |
| return 80.0 + 15.0 * (len(shorter) / len(longer)) | |
| return 0.0 | |
| focus_coverage = len(overlap) / len(focus_tokens) | |
| heading_coverage = len(overlap) / len(heading_tokens) | |
| if len(overlap) >= 2 and focus_coverage >= 0.7 and heading_coverage >= 0.5: | |
| return 60.0 + 20.0 * focus_coverage + 10.0 * heading_coverage | |
| return 0.0 | |
| def _heading_level(tag: Tag) -> int | None: | |
| """Return the level of a heading tag or a modern Wikipedia wrapper.""" | |
| if re.fullmatch(r"h[1-6]", tag.name or ""): | |
| return int(tag.name[1]) | |
| classes = set(tag.get("class") or []) | |
| if tag.name == "div" and "mw-heading" in classes: | |
| heading = tag.find(re.compile(r"^h[1-6]$")) | |
| if isinstance(heading, Tag): | |
| return int(heading.name[1]) | |
| return None | |
| def _truncate_text(text: str, max_chars: int) -> str: | |
| """Truncate cleaned text at a line boundary when practical.""" | |
| text = text.strip() | |
| if len(text) <= max_chars: | |
| return text | |
| excerpt = text[:max_chars] | |
| line_end = excerpt.rfind("\n") | |
| if line_end >= max_chars // 2: | |
| excerpt = excerpt[:line_end] | |
| return excerpt.rstrip() | |
| def _element_text(node: Tag) -> str: | |
| """Render tables row-by-row and other elements with meaningful line breaks.""" | |
| if node.name == "table": | |
| rows: list[str] = [] | |
| for row in node.find_all("tr"): | |
| cells = [ | |
| _clean_text(cell.get_text(" ", strip=True)) | |
| for cell in row.find_all(["th", "td"], recursive=False) | |
| ] | |
| if cells: | |
| rows.append(" | ".join(cells)) | |
| return "\n".join(rows) | |
| return node.get_text("\n", strip=True) | |
| def _focused_excerpt( | |
| text: str, | |
| focus: str, | |
| *, | |
| max_chars: int = 4_000, | |
| ) -> str: | |
| """Return a bounded portion of text surrounding the requested topic.""" | |
| text = text.strip() | |
| if len(text) <= max_chars: | |
| return text | |
| if not focus.strip(): | |
| return text[:max_chars] | |
| normalized_focus = " ".join(focus.casefold().split()) | |
| lowered_text = text.casefold() | |
| # Prefer an exact phrase match. | |
| position = lowered_text.find(normalized_focus) | |
| # Fall back to the line containing the most focus terms. | |
| if position == -1: | |
| terms = {term for term in re.findall(r"[a-z0-9]+", normalized_focus) if len(term) >= 3} | |
| best_score = 0 | |
| best_position = -1 | |
| current_position = 0 | |
| for line in text.splitlines(): | |
| lowered_line = line.lower() | |
| score = sum(term in lowered_line for term in terms) | |
| if score > best_score: | |
| best_score = score | |
| best_position = current_position | |
| current_position += len(line) + 1 | |
| position = best_position | |
| # No meaningful focus match: return a bounded beginning of the page. | |
| if position == -1: | |
| return text[:max_chars] | |
| # Keep limited context before the match and more context after it. | |
| before_chars = max_chars // 5 | |
| start = max(0, position - before_chars) | |
| end = min(len(text), start + max_chars) | |
| # Shift the window backwards near the end of the document. | |
| if end == len(text): | |
| start = max(0, end - max_chars) | |
| excerpt = text[start:end] | |
| # Avoid beginning or ending in the middle of a line where possible. | |
| if start > 0 and "\n" in excerpt: | |
| excerpt = excerpt.split("\n", 1)[1] | |
| if end < len(text) and "\n" in excerpt: | |
| excerpt = excerpt.rsplit("\n", 1)[0] | |
| return _truncate_text(excerpt, max_chars) | |
| def _strong_phrase_present(text: str, focus: str) -> bool: | |
| normalized_text = _normalize_match_text(text) | |
| normalized_focus = _normalize_match_text(focus) | |
| if not normalized_focus: | |
| return True | |
| if normalized_focus in normalized_text: | |
| return True | |
| focus_terms = set(normalized_focus.split()) | |
| if len(focus_terms) < 2: | |
| return False | |
| for line in text.splitlines(): | |
| line_terms = set(_normalize_match_text(line).split()) | |
| if len(focus_terms & line_terms) / len(focus_terms) >= 0.8: | |
| return True | |
| return False | |
| def _is_low_quality_extraction(text: str) -> bool: | |
| """Detect interface/generated markup dominating useful prose.""" | |
| lowered = text.casefold() | |
| signals = ( | |
| "property get", | |
| "generated metadata", | |
| "page actions", | |
| "navigation menu", | |
| "toggle navigation", | |
| "download page", | |
| "print page", | |
| "toolbar", | |
| "contents hide", | |
| "back to top", | |
| "skip to main content", | |
| ) | |
| signal_hits = sum(lowered.count(signal) for signal in signals) | |
| lines = [line.strip() for line in text.splitlines() if line.strip()] | |
| nav_labels = { | |
| "home", | |
| "contents", | |
| "search", | |
| "tools", | |
| "edit", | |
| "share", | |
| "download", | |
| "print", | |
| "next", | |
| "previous", | |
| } | |
| nav_count = sum(line.casefold().strip(":") in nav_labels for line in lines) | |
| words = re.findall(r"[a-z0-9]+", lowered) | |
| unique_density = len(set(words)) / max(1, len(words)) | |
| prose_lines = sum(len(line.split()) >= 8 for line in lines) | |
| return ( | |
| signal_hits >= 3 | |
| or (nav_count >= 5 and nav_count >= len(lines) / 3) | |
| or (len(words) >= 40 and unique_density < 0.12) | |
| or (len(lines) >= 12 and prose_lines / len(lines) < 0.08 and signal_hits >= 1) | |
| ) | |
| def _trafilatura_excerpt(html: str, url: str, focus: str) -> str | None: | |
| """Return a validated generic-main-content fallback from existing HTML.""" | |
| extracted = trafilatura.extract( | |
| html, | |
| url=url, | |
| output_format="markdown", | |
| include_tables=True, | |
| include_links=True, | |
| include_comments=False, | |
| favor_recall=True, | |
| ) | |
| if not extracted: | |
| return None | |
| cleaned = _clean_text( | |
| "\n".join( | |
| line | |
| for line in extracted.splitlines() | |
| if not re.search(r"\\(?:re)?newcommand|\\definecolor", line) | |
| ) | |
| ) | |
| if _is_low_quality_extraction(cleaned): | |
| return None | |
| if focus.strip() and not _strong_phrase_present(cleaned, focus): | |
| return _semantic_excerpt(cleaned, focus) | |
| return _focused_excerpt(cleaned, focus, max_chars=4_000) | |
| def _semantic_excerpt(text: str, focus: str) -> str | None: | |
| """Use an ephemeral small-embedding index when lexical focus matching fails.""" | |
| chunks: list[str] = [] | |
| start = 0 | |
| bounded = text[:50_000] | |
| while start < len(bounded): | |
| end = min(len(bounded), start + 1_200) | |
| if end < len(bounded): | |
| boundary = bounded.rfind("\n", start + 600, end) | |
| if boundary > start: | |
| end = boundary | |
| chunk = bounded[start:end].strip() | |
| if chunk: | |
| chunks.append(chunk) | |
| if end == len(bounded): | |
| break | |
| start = max(start + 1, end - 200) | |
| if not chunks: | |
| return None | |
| store = Chroma.from_texts( | |
| chunks, | |
| embedding=OpenAIEmbeddings(model="text-embedding-3-small"), | |
| collection_name=f"web-{uuid4().hex}", | |
| collection_metadata={"hnsw:space": "cosine"}, | |
| ) | |
| try: | |
| matches = store.similarity_search_with_relevance_scores(focus, k=min(2, len(chunks))) | |
| finally: | |
| store.delete_collection() | |
| selected = [document.page_content for document, score in matches if score >= 0.25] | |
| if not selected: | |
| return None | |
| excerpt = _truncate_text("\n\n".join(selected), 4_000) | |
| return None if _is_low_quality_extraction(excerpt) else excerpt | |
| def _structural_section( | |
| content_root: Tag, | |
| focus: str, | |
| *, | |
| max_chars: int = 5_000, | |
| ) -> tuple[str, list[Tag]] | None: | |
| """Extract the best matching heading and its structurally bounded section.""" | |
| if not focus.strip(): | |
| return None | |
| best: tuple[float, Tag] | None = None | |
| for heading in content_root.find_all(re.compile(r"^h[1-6]$")): | |
| if not isinstance(heading, Tag): | |
| continue | |
| score = _heading_match_score(heading.get_text(" ", strip=True), focus) | |
| # A partial page-title match usually identifies the page rather than the | |
| # requested section. Exact h1 matches remain valid for overview requests. | |
| if heading.name == "h1" and score < 100: | |
| score = 0 | |
| if score > 0 and (best is None or score > best[0]): | |
| best = (score, heading) | |
| if best is None: | |
| return None | |
| heading = best[1] | |
| level = int(heading.name[1]) | |
| wrapper = heading.parent | |
| if not ( | |
| isinstance(wrapper, Tag) | |
| and wrapper.name == "div" | |
| and "mw-heading" in set(wrapper.get("class") or []) | |
| ): | |
| wrapper = heading | |
| selected: list[Tag] = [wrapper] | |
| for sibling in wrapper.next_siblings: | |
| if not isinstance(sibling, Tag): | |
| continue | |
| sibling_level = _heading_level(sibling) | |
| if sibling_level is not None and sibling_level <= level: | |
| break | |
| selected.append(sibling) | |
| section_text = _clean_text("\n".join(_element_text(node) for node in selected)) | |
| return _truncate_text(section_text, max_chars), selected | |
| def _relevant_links( | |
| roots: Tag | BeautifulSoup | Iterable[Tag], | |
| *, | |
| base_url: str, | |
| focus: str, | |
| limit: int = 3, | |
| context_relevant: bool = False, | |
| ) -> list[str]: | |
| """Return a small set of links related to the requested focus.""" | |
| focus_terms = {term for term in re.findall(r"[a-z0-9]+", focus.lower()) if len(term) >= 3} | |
| if isinstance(roots, (Tag, BeautifulSoup)): | |
| root_items: Iterable[Tag | BeautifulSoup] = [roots] | |
| else: | |
| root_items = roots | |
| candidates: list[tuple[int, int, str]] = [] | |
| seen: set[str] = set() | |
| order = 0 | |
| for root in root_items: | |
| anchors = list(root.find_all("a", href=True)) | |
| if root.name == "a" and root.get("href"): | |
| anchors = [root, *anchors] | |
| for anchor in anchors: | |
| label = " ".join(anchor.get_text(" ", strip=True).split()) | |
| href = str(anchor.get("href") or "").strip() | |
| if ( | |
| not label | |
| or not href | |
| or label.casefold() in {"edit", "reply", "add links", "add languages"} | |
| or href.startswith(("#", "javascript:", "mailto:")) | |
| or "action=edit" in href | |
| ): | |
| continue | |
| absolute_url = urljoin(base_url, href) | |
| if absolute_url in seen: | |
| continue | |
| parent_text = "" | |
| if anchor.parent is not None: | |
| parent_text = " ".join(anchor.parent.get_text(" ", strip=True).split())[:500] | |
| searchable = f"{label} {parent_text}".casefold() | |
| label_text = label.casefold() | |
| score = sum(2 for term in focus_terms if term in label_text) | |
| score += sum(1 for term in focus_terms if term in searchable) | |
| if context_relevant: | |
| score += 1 | |
| if not focus_terms or score == 0: | |
| continue | |
| seen.add(absolute_url) | |
| candidates.append((score, order, f"{label}: {absolute_url}")) | |
| order += 1 | |
| candidates.sort(key=lambda item: (-item[0], item[1])) | |
| return [value for _, _, value in candidates[:limit]] | |
| def read_webpage(url: str, focus: str) -> str: | |
| """ | |
| Open a webpage and extract evidence for a specific fact, section, or linked source. | |
| Args: | |
| url: Exact webpage URL. | |
| focus: A concise phrasing likely to occur on the web page to locate using headings, entities, fields, | |
| relationships, citations, link labels, titles, or facts. | |
| Returns: | |
| A compact relevant excerpt and up to three supporting links. | |
| Status values: | |
| INVALID_FOCUS | |
| NOT_FOUND | |
| LOW_QUALITY | |
| ERROR | |
| """ | |
| try: | |
| if not focus.strip(): | |
| return "INVALID_FOCUS: provide a specific heading, phrase, field, or fact." | |
| response = requests.get( | |
| url, | |
| timeout=25, | |
| headers={"User-Agent": "Mozilla/5.0 GAIA-LangChain-Agent/1.0"}, | |
| ) | |
| response.raise_for_status() | |
| content_type = response.headers.get("Content-Type", "").lower() | |
| is_pdf = "application/pdf" in content_type or urlparse(response.url).path.lower().endswith( | |
| ".pdf" | |
| ) | |
| if is_pdf: | |
| reader = PdfReader(BytesIO(response.content)) | |
| raw_text = "\n".join(page.extract_text() or "" for page in reader.pages) | |
| pdf_text = _clean_text(raw_text) | |
| if _is_low_quality_extraction(pdf_text): | |
| return ( | |
| "LOW_QUALITY: extracted PDF content is dominated by interface " | |
| "or generated text." | |
| ) | |
| if focus.strip() and not _strong_phrase_present(pdf_text, focus): | |
| return "NOT_FOUND: requested focus is absent from the PDF." | |
| excerpt = _focused_excerpt(pdf_text, focus, max_chars=4_000) | |
| return f"OK: FOCUS: {focus or 'document beginning'}\n\nPDF TEXT:\n{excerpt}" | |
| soup = BeautifulSoup(response.text, "html.parser") | |
| for node in soup( | |
| [ | |
| "script", | |
| "style", | |
| "nav", | |
| "footer", | |
| "aside", | |
| "noscript", | |
| "form", | |
| "svg", | |
| ] | |
| ): | |
| node.decompose() | |
| content_root = ( | |
| soup.find("main") | |
| or soup.find(id="mw-content-text") | |
| or soup.find("article") | |
| or soup.body | |
| or soup | |
| ) | |
| structural = _structural_section(content_root, focus, max_chars=5_000) | |
| if structural is not None: | |
| excerpt, link_roots = structural | |
| links = _relevant_links( | |
| link_roots, | |
| base_url=response.url, | |
| focus=focus, | |
| limit=3, | |
| context_relevant=True, | |
| ) | |
| else: | |
| page_text = _clean_text(content_root.get_text("\n", strip=True)) | |
| if focus.strip() and not _strong_phrase_present(page_text, focus): | |
| fallback = _trafilatura_excerpt(response.text, response.url, focus) | |
| if fallback is None: | |
| return "NOT_FOUND: requested focus is absent." | |
| excerpt, links = fallback, [] | |
| elif _is_low_quality_extraction(page_text): | |
| fallback = _trafilatura_excerpt(response.text, response.url, focus) | |
| if fallback is not None: | |
| excerpt, links = fallback, [] | |
| else: | |
| return ( | |
| "LOW_QUALITY: extracted content is dominated by navigation, " | |
| "metadata, or interface text." | |
| ) | |
| else: | |
| excerpt = _focused_excerpt(page_text, focus, max_chars=4_000) | |
| links = _relevant_links( | |
| content_root, | |
| base_url=response.url, | |
| focus=focus, | |
| limit=3, | |
| ) | |
| sparse_interface = len(excerpt.split()) < 100 and bool( | |
| re.search(r"\b(?:last updated|save as pdf|download page)\b", excerpt, re.I) | |
| ) | |
| if _is_low_quality_extraction(excerpt) or sparse_interface: | |
| fallback = _trafilatura_excerpt(response.text, response.url, focus) | |
| if fallback is None: | |
| return ( | |
| "LOW_QUALITY: focused extraction is dominated by navigation, " | |
| "metadata, or interface text." | |
| ) | |
| excerpt, links = fallback, [] | |
| excerpt = _truncate_text(excerpt, 4_000) | |
| rendered = f"OK: FOCUS: {focus or 'page beginning'}\n\nPAGE TEXT:\n{excerpt}" | |
| if links: | |
| rendered += "\n\nRELEVANT LINKS:\n" + "\n".join(links) | |
| return rendered | |
| except requests.HTTPError as exc: | |
| return _truncate_text(f"ERROR: HTTP request failed: {type(exc).__name__}: {exc}", 280) | |
| except requests.RequestException as exc: | |
| return _truncate_text(f"ERROR: request failed: {type(exc).__name__}: {exc}", 280) | |
| except Exception as exc: | |
| return _truncate_text(f"ERROR: processing failed: {type(exc).__name__}: {exc}", 280) | |
| SYSTEM_PROMPT = """ | |
| You solve exact-answer questions and return only the required final answer. | |
| ## Decide whether tools are needed | |
| Answer directly when the question can be solved from supplied text, tables, lists, arithmetic, logic, classification, or transformation. | |
| Use tools only when external information is required. | |
| ## Tool use | |
| * Do not emit planning prose. | |
| * Use concise, high-signal queries containing only relevant names, titles, dates, identifiers, roles, and exact phrases. | |
| * Do not repeat equivalent searches or retry the same failed source. | |
| * Open an existing useful URL instead of searching for it again. | |
| * For historical questions, verify that the source reflects the requested date rather than the current state. | |
| * When reading a page, use a short focus: an exact heading, field, table title, entity, identifier, or distinctive phrase. | |
| * Do not use full instructions or multi-step sentences as `focus`. | |
| * Use the narrowest useful focus. | |
| ## Multi-step questions | |
| 1. Identify the intermediate entity or source. | |
| 2. Retrieve the requested final field or value. | |
| 3. Stop when the answer is supported. | |
| Do not add a separate verification call unless the evidence is ambiguous, incomplete, inconsistent, or unreliable. | |
| ## After each tool result | |
| Check whether one reliable source provides: | |
| * the correct subject; | |
| * the requested relationship or field; | |
| * the corresponding value; | |
| * the required date, category, or source scope. | |
| If all are present, answer immediately. | |
| If the result is insufficient, change strategy meaningfully. Do not repeat the same query, URL, focus, source variant, or failed approach. | |
| ## Verification | |
| Check the requested entity, date range, role, category, table column, spelling, ordering, units, and precision. | |
| For counts, identify the qualifying items before counting. | |
| For lists, include only qualifying items and preserve the requested order and format. | |
| ## Evidence rule | |
| Do not invent factual answers. | |
| The final answer must either: | |
| * appear in retrieved evidence; or | |
| * be directly derived from supplied or retrieved evidence. | |
| If no supported answer can be established, return `Unknown`. | |
| ## Output | |
| Return only the requested answer. | |
| Do not include explanations, labels, citations, Markdown, or introductory text. | |
| Preserve required spelling, capitalization, punctuation, ordering, delimiters, units, currency symbols, decimal precision, and notation. | |
| """.strip() | |
| class DeterministicGuardrailMiddleware(AgentMiddleware): | |
| """Enforce retrieval and iteration limits independently of model compliance.""" | |
| def wrap_model_call(self, request: ModelRequest, handler) -> ModelResponse: | |
| budget: ResearchBudget = request.runtime.context | |
| budget.model_iterations += 1 | |
| tools = list(request.tools) | |
| force_final = ( | |
| budget.model_iterations >= MAX_AGENT_ITERATIONS or budget.total_calls >= MAX_TOOL_CALLS | |
| ) | |
| if force_final: | |
| tools = [] | |
| else: | |
| if budget.search_calls >= MAX_SEARCH_CALLS: | |
| tools = [ | |
| tool | |
| for tool in tools | |
| if getattr(tool, "name", "") not in {"duckduckgo_search", "wikipedia_search"} | |
| ] | |
| if budget.webpage_reads >= MAX_WEBPAGE_READS: | |
| tools = [tool for tool in tools if getattr(tool, "name", "") != "read_webpage"] | |
| if len(tools) != len(request.tools): | |
| existing = request.system_message.text if request.system_message else "" | |
| suffix = "\nHard limit reached. Call no unavailable tools; answer now from existing evidence." | |
| request = request.override( | |
| tools=tools, | |
| tool_choice=None, | |
| system_message=SystemMessage(content=existing + suffix), | |
| ) | |
| return handler(request) | |
| def wrap_tool_call(self, request, handler): | |
| budget: ResearchBudget = request.runtime.context | |
| name = request.tool_call["name"] | |
| args = request.tool_call.get("args") or {} | |
| rejection = budget.reject_tool_call(name, args) | |
| if rejection: | |
| return ToolMessage( | |
| content=rejection, tool_call_id=request.tool_call["id"], status="error" | |
| ) | |
| result = handler(request) | |
| if isinstance(result, ToolMessage): | |
| compacted = budget.compact_output(name, args, str(result.content)) | |
| return result.model_copy(update={"content": compacted}) | |
| return result | |
| def _model() -> ChatOpenAI: | |
| return ChatOpenAI( | |
| model=os.getenv("OPENAI_MODEL", "gpt-5"), | |
| temperature=0, | |
| max_retries=2, | |
| ) | |
| def _agent(): | |
| return create_agent( | |
| model=_model(), | |
| tools=[duckduckgo_search, wikipedia_search, read_webpage], | |
| system_prompt=SYSTEM_PROMPT, | |
| middleware=[DeterministicGuardrailMiddleware()], | |
| context_schema=ResearchBudget, | |
| response_format=FinalAnswer, | |
| ) | |
| def _direct_openai(question: str) -> str: | |
| response = _model().invoke( | |
| [ | |
| ("system", SYSTEM_PROMPT + "\nDo not call tools. Return only the exact answer."), | |
| ("user", question), | |
| ] | |
| ) | |
| return _normalize_answer(response.content) | |
| def _answer_youtube(question: str, video_url: str) -> str: | |
| from google import genai | |
| from google.genai import types | |
| api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") | |
| client = genai.Client(api_key=api_key) if api_key else genai.Client() | |
| response = client.models.generate_content( | |
| model=os.getenv("GEMINI_VIDEO_MODEL", "gemini-3.5-flash"), | |
| contents=types.Content( | |
| parts=[ | |
| types.Part(file_data=types.FileData(file_uri=video_url)), | |
| types.Part( | |
| text=( | |
| question + "\nReturn only the exact requested answer. Do not include an " | |
| "explanation, label, citation, or Markdown." | |
| ) | |
| ), | |
| ] | |
| ), | |
| ) | |
| return _normalize_answer(response.text or "") | |
| def answer_question(question: str, file_name: str | None = None) -> str | None: | |
| """Answer one GAIA question, or return None for a named file attachment task.""" | |
| if file_name and file_name.strip(): | |
| return None | |
| youtube_urls = [url for url in _extract_urls(question) if _is_youtube_url(url)] | |
| if youtube_urls: | |
| try: | |
| return _answer_youtube(question, youtube_urls[0]) | |
| except Exception: | |
| return _direct_openai(question) | |
| try: | |
| result = _agent().invoke( | |
| {"messages": [{"role": "user", "content": question}]}, | |
| context=ResearchBudget(), | |
| ) | |
| structured = result.get("structured_response") | |
| if isinstance(structured, FinalAnswer): | |
| return _normalize_answer(structured.answer) | |
| if isinstance(structured, dict) and "answer" in structured: | |
| return _normalize_answer(structured["answer"]) | |
| messages = result.get("messages") or [] | |
| if messages: | |
| return _normalize_answer(messages[-1].content) | |
| raise RuntimeError("LangChain agent returned no final answer") | |
| except Exception: | |
| return _direct_openai(question) | |