| import base64 |
| import os |
| import re |
| import tempfile |
|
|
| import gradio as gr |
| import requests |
| import inspect |
| import pandas as pd |
| from smolagents import ( |
| CodeAgent, |
| DuckDuckGoSearchTool, |
| VisitWebpageTool, |
| OpenAIServerModel, |
| tool, |
| ) |
|
|
| |
| |
| DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space" |
|
|
|
|
| |
| |
| |
| GAIA_INSTRUCTIONS = """You are a general AI assistant answering a question from the GAIA benchmark. |
| Reason step by step and use your tools to find the answer: |
| - web_search / visit_webpage to research on the internet, |
| - get_task_file to download a file attached to this question (if any), |
| - Python code to parse files, do math, or process data. |
| |
| The task_id for this question is: {task_id} |
| Only call get_task_file("{task_id}") if the question text explicitly mentions |
| an attached file (e.g. "the audio file", "the spreadsheet", "the image", |
| "the Python code", "this file"). Most GAIA questions have no attachment — |
| for those, do NOT call get_task_file. If you do call it and get back |
| "NO_FILE_ATTACHED", proceed without an attachment. |
| |
| Available tools for multimodal / reference tasks: |
| - wikipedia_search(query) returns the article INTRO only. |
| - For tabular data (discography, filmography, awards, etc.) call |
| wikipedia_search(query, section="Discography") to get JUST that |
| section as Markdown with the table intact — much cleaner than |
| slicing yourself. |
| - As a last resort use wikipedia_search(query, full=True) for the |
| whole article. |
| - fetch_url(url) downloads a page with a real User-Agent and returns its |
| content as Markdown (tables preserved). Prefer this over visit_webpage |
| for Wikipedia and other sites that block default user agents. |
| - youtube_transcript(url) when the question references a YouTube video. |
| IMPORTANT: this fails on HF Spaces ~always (cloud IPs blocked). After |
| ONE failed try, switch to youtube_info(url) for title/description, |
| and use web_search to find an external summary of the video. Do not |
| keep retrying the transcript. |
| - youtube_info(url) scrapes the YouTube watch page (works from Spaces) |
| for title, description, channel, duration, and the auto-caption URL. |
| For "how long is the video" or "who is the speaker" this alone often |
| answers the question. Visual-counting questions usually still need |
| a web_search. |
| - transcribe_audio(path) for MP3 / WAV / M4A attachments. After |
| transcription, do NOT just split-and-sort every word — the audio |
| usually contains a spoken sentence with filler words ("I need you to |
| write down these ingredients..."). Re-read the transcript and extract |
| ONLY the entities that match the question's category (ingredients, |
| page numbers, names, etc.). Preserve multi-word items as one entity: |
| "granulated sugar" is one ingredient, not two; "vanilla extract" is |
| one, not two. |
| - describe_image(path, question) for PNG / JPG attachments, charts, |
| chess positions, etc. Ask a SPECIFIC question, e.g. |
| "What text appears at the bottom?" or "List every fruit in the image." |
| |
| GAIA is graded by EXACT MATCH. Use the OFFICIAL format spec verbatim: |
| |
| Your final answer should be a number OR as few words as possible OR a |
| comma separated list of numbers and/or strings. |
| - If you are asked for a number, don't use comma to write your number |
| neither use units such as $ or percent sign unless specified otherwise. |
| - If you are asked for a string, don't use articles, neither abbreviations |
| (e.g. for cities), and write the digits in plain text unless specified |
| otherwise. ("five" not "5" inside a string answer.) |
| - If you are asked for a comma separated list, apply the above rules |
| depending on whether the element is a number or a string. |
| |
| Pass JUST the answer value to the final_answer tool — no "FINAL ANSWER:" |
| prefix, no explanation, no surrounding quotes, no trailing period. |
| |
| Question: |
| {question} |
| """ |
|
|
|
|
| |
| @tool |
| def get_task_file(task_id: str) -> str: |
| """Downloads the file attached to a GAIA task and saves it locally. |
| |
| ONLY call this when the question text explicitly mentions an attached |
| file (spreadsheet, image, audio clip, Python file, etc.). If the |
| question is pure text (no file mentioned), do not call this — most |
| GAIA tasks have no attachment, and the server returns 404 for those. |
| |
| Returns the local path to the downloaded file, or one of: |
| - "NO_FILE_ATTACHED": the task has no attachment. Move on with |
| purely the question text; do not retry this tool. |
| - "ERROR: ...": transient/network error worth surfacing. |
| |
| Args: |
| task_id: The task_id of the current question. |
| """ |
| url = f"{DEFAULT_API_URL}/files/{task_id}" |
| try: |
| resp = requests.get(url, timeout=30) |
| if resp.status_code == 404: |
| return "NO_FILE_ATTACHED" |
| resp.raise_for_status() |
| except requests.exceptions.HTTPError as e: |
| if e.response is not None and e.response.status_code == 404: |
| return "NO_FILE_ATTACHED" |
| return f"ERROR: could not download file for task {task_id}: {e}" |
| except Exception as e: |
| return f"ERROR: could not download file for task {task_id}: {e}" |
|
|
| |
| filename = task_id |
| disposition = resp.headers.get("content-disposition", "") |
| match = re.search(r'filename="?([^"]+)"?', disposition) |
| if match: |
| filename = match.group(1) |
|
|
| path = os.path.join(tempfile.gettempdir(), filename) |
| with open(path, "wb") as f: |
| f.write(resp.content) |
| return path |
|
|
|
|
| def _html_to_markdown(html: str, *, is_wikipedia: bool) -> str: |
| """Convert HTML to clean Markdown, preserving tables and headings. |
| |
| For Wikipedia pages we scope to the article body and drop navboxes, |
| infoboxes, references, and edit links — those are pure noise for an |
| agent reasoning about article facts. |
| """ |
| try: |
| from bs4 import BeautifulSoup |
| from markdownify import markdownify as md |
| except ImportError: |
| return html |
|
|
| soup = BeautifulSoup(html, "html.parser") |
| root = soup.select_one("#mw-content-text") or soup.body or soup if is_wikipedia else soup |
| for selector in ( |
| "script", "style", "noscript", |
| |
| "table.navbox", "table.vertical-navbox", "table.infobox", |
| "table.sidebar", "div.reflist", "div.refbegin", "ol.references", |
| "sup.reference", "div.thumb", ".mw-editsection", |
| ".mw-jump-link", "#toc", ".hatnote", ".navigation-not-searchable", |
| ): |
| for el in root.select(selector): |
| el.decompose() |
| |
| |
| return md(str(root), heading_style="ATX", strip=["a", "img"]) |
|
|
|
|
| _WIKI_HEADERS = { |
| "User-Agent": "GaiaAgent/1.0 (https://huggingface.co/spaces/agents-course)", |
| "Accept": "application/json", |
| } |
|
|
|
|
| def _wiki_resolve_title(query: str) -> str | None: |
| """Resolve a free-text query to the canonical Wikipedia title, or None.""" |
| try: |
| r = requests.get( |
| "https://en.wikipedia.org/w/api.php", |
| params={ |
| "action": "query", "list": "search", "srsearch": query, |
| "srlimit": 1, "format": "json", |
| }, |
| headers=_WIKI_HEADERS, timeout=20, |
| ) |
| r.raise_for_status() |
| hits = r.json().get("query", {}).get("search", []) |
| return hits[0]["title"] if hits else None |
| except Exception: |
| return None |
|
|
|
|
| @tool |
| def wikipedia_search(query: str, full: bool = False, section: str = "", |
| max_chars: int = 40000) -> str: |
| """Look up a topic on Wikipedia. |
| |
| Prefer this over web_search for biographical, geographic, or other |
| encyclopedic facts — Wikipedia answers most GAIA Level 1 lookups directly. |
| |
| Three modes: |
| - default: returns the article's intro summary. |
| - full=True: returns the entire article body as Markdown (tables kept). |
| - section="Discography": returns ONLY that section (recommended when |
| you already know which section holds the answer — discography, |
| filmography, awards, etc.). Much smaller than full=True. |
| |
| Args: |
| query: The topic to search for (e.g. "Mercedes Sosa"). |
| full: If True, return the full article body as Markdown. |
| section: If set, return only the named section (case-sensitive H2/H3 |
| heading text, e.g. "Discography" or "Studio albums"). |
| max_chars: Truncate output to this length (default 40000). |
| """ |
| title = _wiki_resolve_title(query) |
| if not title: |
| return f"No Wikipedia page found for '{query}'." |
|
|
| safe_title = requests.utils.quote(title.replace(" ", "_"), safe="_()") |
| page_url = f"https://en.wikipedia.org/wiki/{safe_title}" |
| header = f"{title}\nURL: {page_url}\n\n" |
|
|
| if not full and not section: |
| |
| try: |
| r = requests.get( |
| f"https://en.wikipedia.org/api/rest_v1/page/summary/{safe_title}", |
| headers=_WIKI_HEADERS, timeout=20, |
| ) |
| r.raise_for_status() |
| return header + (r.json().get("extract") or "") |
| except Exception as e: |
| return f"ERROR: wikipedia summary failed: {e}" |
|
|
| |
| try: |
| r = requests.get( |
| f"https://en.wikipedia.org/api/rest_v1/page/html/{safe_title}", |
| headers={**_WIKI_HEADERS, "Accept": "text/html"}, timeout=30, |
| ) |
| r.raise_for_status() |
| html = r.text |
| except Exception as e: |
| return f"ERROR: wikipedia HTML fetch failed: {e}" |
|
|
| markdown = _html_to_markdown(html, is_wikipedia=True) |
|
|
| if section: |
| |
| pattern = re.compile( |
| rf"^(#{{2,4}})\s+{re.escape(section)}\s*$", |
| re.MULTILINE | re.IGNORECASE, |
| ) |
| match = pattern.search(markdown) |
| if not match: |
| headings = [ln for ln in markdown.split("\n") if ln.startswith("## ")] |
| return (f"Section '{section}' not found. Available H2 headings:\n" |
| + "\n".join(headings[:30])) |
| start = match.start() |
| level = len(match.group(1)) |
| |
| sibling = re.compile(rf"^#{{1,{level}}}(?!#)\s+", re.MULTILINE) |
| end_match = sibling.search(markdown, pos=match.end()) |
| end = end_match.start() if end_match else len(markdown) |
| return (header + markdown[start:end])[:max_chars] |
|
|
| return (header + markdown)[:max_chars] |
|
|
|
|
| @tool |
| def fetch_url(url: str, max_chars: int = 20000) -> str: |
| """Download a web page and return its content as Markdown. |
| |
| Use this when visit_webpage returns a 403 / 429 — many sites (Wikipedia |
| included) block requests that lack a real browser User-Agent. This tool |
| sends one, then converts the page to Markdown so that tables, headings, |
| and lists keep their structure. |
| |
| Args: |
| url: The URL to fetch. |
| max_chars: Truncate the result to this many characters (default 20000). |
| """ |
| try: |
| resp = requests.get( |
| url, |
| headers={ |
| "User-Agent": ( |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " |
| "AppleWebKit/537.36 (KHTML, like Gecko) " |
| "Chrome/124.0 Safari/537.36" |
| ), |
| "Accept": "text/html,application/xhtml+xml", |
| }, |
| timeout=30, |
| ) |
| resp.raise_for_status() |
| except Exception as e: |
| return f"ERROR: could not fetch {url}: {e}" |
|
|
| is_wiki = "wikipedia.org" in url |
| try: |
| text = _html_to_markdown(resp.text, is_wikipedia=is_wiki) |
| except Exception: |
| text = resp.text |
|
|
| return text[:max_chars] |
|
|
|
|
| def _extract_youtube_id(url: str) -> str | None: |
| m = re.search(r"(?:v=|youtu\.be/|/shorts/|/embed/)([0-9A-Za-z_-]{11})", url) |
| if m: |
| return m.group(1) |
| return url if re.fullmatch(r"[0-9A-Za-z_-]{11}", url) else None |
|
|
|
|
| @tool |
| def youtube_transcript(url: str) -> str: |
| """Fetch the transcript (subtitles) of a YouTube video. |
| |
| NOTE: On HuggingFace Spaces this often fails with an IP-block error |
| because YouTube blocks cloud-provider IPs. If you see that error, do |
| NOT keep retrying — call youtube_info(url) for title/description, and |
| use web_search() to find someone else's summary of the video content. |
| |
| Args: |
| url: A YouTube URL or bare 11-character video id. |
| """ |
| try: |
| from youtube_transcript_api import YouTubeTranscriptApi |
| except ImportError: |
| return ("ERROR: the 'youtube-transcript-api' package is not installed " |
| "in this Space.") |
|
|
| video_id = _extract_youtube_id(url) |
| if not video_id: |
| return "ERROR: could not extract YouTube video id from URL." |
| try: |
| |
| |
| if hasattr(YouTubeTranscriptApi, "get_transcript"): |
| chunks = YouTubeTranscriptApi.get_transcript(video_id) |
| return " ".join(c["text"] for c in chunks) |
| fetched = YouTubeTranscriptApi().fetch(video_id) |
| return " ".join(snippet.text for snippet in fetched) |
| except Exception as e: |
| msg = str(e) |
| if "IpBlocked" in msg or "RequestBlocked" in msg or "blocking requests" in msg: |
| return ("ERROR: YouTube IP-blocked this Space's transcript request. " |
| "DO NOT RETRY transcript. Instead call youtube_info(url) " |
| "for the title/description, then web_search for an external " |
| "summary or transcript.") |
| return f"ERROR: could not fetch transcript: {e}" |
|
|
|
|
| @tool |
| def youtube_info(url: str) -> str: |
| """Scrape a YouTube watch page for title, description, channel, and duration. |
| |
| Works from cloud IPs even when the transcript API is blocked — the watch |
| page itself is publicly fetchable. Use as a fallback when |
| youtube_transcript fails. The description sometimes contains the answer |
| outright; otherwise pair this with web_search. |
| |
| Args: |
| url: A YouTube URL or bare 11-character video id. |
| """ |
| video_id = _extract_youtube_id(url) |
| if not video_id: |
| return "ERROR: could not extract YouTube video id from URL." |
| page_url = f"https://www.youtube.com/watch?v={video_id}" |
| try: |
| resp = requests.get( |
| page_url, |
| headers={ |
| "User-Agent": ( |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " |
| "AppleWebKit/537.36 (KHTML, like Gecko) " |
| "Chrome/124.0 Safari/537.36" |
| ), |
| }, |
| timeout=20, |
| ) |
| resp.raise_for_status() |
| except Exception as e: |
| return f"ERROR: could not fetch YouTube page: {e}" |
|
|
| html = resp.text |
| |
| |
| def og(prop: str) -> str: |
| m = re.search( |
| rf'<meta\s+property="og:{prop}"\s+content="([^"]+)"', html |
| ) |
| return m.group(1) if m else "" |
|
|
| title = og("title") |
| description = og("description") |
| duration_match = re.search(r'"lengthSeconds":"(\d+)"', html) |
| duration_s = int(duration_match.group(1)) if duration_match else None |
| channel_match = re.search(r'"ownerChannelName":"([^"]+)"', html) |
| channel = channel_match.group(1) if channel_match else "" |
|
|
| |
| cap_match = re.search(r'"captionTracks":(\[[^\]]+\])', html) |
| captions_url = "" |
| if cap_match: |
| u = re.search(r'"baseUrl":"([^"]+)"', cap_match.group(1)) |
| if u: |
| captions_url = u.group(1).encode().decode("unicode_escape") |
|
|
| parts = [ |
| f"Title: {title}", |
| f"Channel: {channel}" if channel else "", |
| f"Duration: {duration_s}s" if duration_s else "", |
| f"Description: {description}", |
| ] |
| if captions_url: |
| parts.append( |
| f"Caption track URL (try fetch_url on this): {captions_url}" |
| ) |
| return "\n".join(p for p in parts if p) |
|
|
|
|
| def _hf_client(): |
| """Build a HuggingFace InferenceClient. Token is read from HF_TOKEN |
| (auto-injected in HF Spaces) or HUGGINGFACEHUB_API_TOKEN.""" |
| try: |
| from huggingface_hub import InferenceClient |
| except ImportError as e: |
| raise RuntimeError( |
| "huggingface_hub is not installed; cannot use audio/vision tools." |
| ) from e |
|
|
| token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN") |
| if not token: |
| raise RuntimeError( |
| "HF_TOKEN is not set — required for transcribe_audio / describe_image." |
| ) |
| return InferenceClient(token=token) |
|
|
|
|
| |
| |
| _ASR_PROVIDERS = ("auto", "fal-ai", "hf-inference") |
|
|
| |
| |
| _VLM_FALLBACKS = ( |
| "zai-org/GLM-4.5V", |
| "meta-llama/Llama-4-Scout-17B-16E-Instruct", |
| "Qwen/Qwen3.6-27B", |
| ) |
|
|
|
|
| @tool |
| def transcribe_audio(file_path: str) -> str: |
| """Transcribe an audio file (mp3, wav, m4a, flac) to text using Whisper. |
| |
| Use after get_task_file for any audio attachment. Returns the full text; |
| you can then parse it with Python (regex, splits) to extract the answer. |
| |
| Args: |
| file_path: Local path to the audio file. |
| """ |
| model = os.getenv("HF_ASR_MODEL", "openai/whisper-large-v3") |
| client = _hf_client() |
| errors = [] |
| for provider in _ASR_PROVIDERS: |
| try: |
| |
| |
| kwargs = {"model": model} |
| if provider != "auto": |
| kwargs["provider"] = provider |
| result = client.automatic_speech_recognition(file_path, **kwargs) |
| return getattr(result, "text", None) or str(result) |
| except TypeError: |
| |
| try: |
| result = client.automatic_speech_recognition(file_path, model=model) |
| return getattr(result, "text", None) or str(result) |
| except Exception as e: |
| errors.append(f"{provider}: {e}") |
| except Exception as e: |
| errors.append(f"{provider}: {e}") |
| return ("ERROR: transcription failed on all providers. " |
| "Tried: " + " | ".join(errors)) |
|
|
|
|
| @tool |
| def describe_image(file_path: str, question: str) -> str: |
| """Ask a vision-language model a question about an image. |
| |
| Use for any image attachment (photo, chart, chess board, diagram). Ask a |
| SPECIFIC question — vague prompts give vague answers. Examples: |
| describe_image(p, "List every item visible on the table, comma-separated.") |
| describe_image(p, "What move should White play? Use algebraic notation.") |
| |
| Args: |
| file_path: Local path to the image file. |
| question: A precise question about the image. |
| """ |
| |
| primary = os.getenv("HF_VLM_MODEL") |
| candidates = ([primary] if primary else []) + [ |
| m for m in _VLM_FALLBACKS if m != primary |
| ] |
|
|
| ext = os.path.splitext(file_path)[1].lower().lstrip(".") |
| mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", |
| "gif": "image/gif", "webp": "image/webp"}.get(ext, "image/jpeg") |
| try: |
| with open(file_path, "rb") as f: |
| b64 = base64.b64encode(f.read()).decode() |
| except Exception as e: |
| return f"ERROR: could not read image {file_path}: {e}" |
|
|
| messages = [{ |
| "role": "user", |
| "content": [ |
| {"type": "text", "text": question}, |
| {"type": "image_url", |
| "image_url": {"url": f"data:{mime};base64,{b64}"}}, |
| ], |
| }] |
|
|
| client = _hf_client() |
| errors = [] |
| for model in candidates: |
| try: |
| resp = client.chat_completion(model=model, messages=messages, max_tokens=512) |
| return resp.choices[0].message.content |
| except Exception as e: |
| errors.append(f"{model}: {e}") |
| return ("ERROR: vision call failed on all candidate models. " |
| "Tried: " + " | ".join(errors)) |
|
|
|
|
| |
| |
| class GaiaAgent: |
| """A tool-using agent (smolagents CodeAgent + DeepSeek V4 Pro) for GAIA. |
| |
| Required environment variables: |
| DEEPSEEK_API_KEY - your DeepSeek key (https://platform.deepseek.com) |
| HF_TOKEN - HuggingFace token (auto-set in HF Spaces), used for |
| audio transcription and image VLM calls |
| Optional: |
| DEEPSEEK_MODEL - default: "deepseek-v4-flash" (lighter/cheaper V4 |
| variant; thinking mode is on by default) |
| DEEPSEEK_API_BASE - default: "https://api.deepseek.com/v1" |
| HF_ASR_MODEL - override ASR model (default: openai/whisper-large-v3, |
| auto-routed via HF Inference Providers) |
| HF_VLM_MODEL - override VLM model. If unset, tries GLM-4.5V then |
| falls back to Llama-4-Scout-17B and Qwen3.6-27B. |
| """ |
|
|
| def __init__(self) -> None: |
| api_key = os.getenv("DEEPSEEK_API_KEY") |
| if not api_key: |
| raise RuntimeError( |
| "DEEPSEEK_API_KEY is not set. Add it as a Space secret " |
| "(Settings > Variables and secrets) or export it locally." |
| ) |
|
|
| model = OpenAIServerModel( |
| model_id=os.getenv("DEEPSEEK_MODEL", "deepseek-v4-flash"), |
| api_base=os.getenv("DEEPSEEK_API_BASE", "https://api.deepseek.com/v1"), |
| api_key=api_key, |
| temperature=0.0, |
| ) |
|
|
| self.agent = CodeAgent( |
| tools=[ |
| DuckDuckGoSearchTool(), |
| VisitWebpageTool(), |
| fetch_url, |
| get_task_file, |
| wikipedia_search, |
| youtube_transcript, |
| youtube_info, |
| transcribe_audio, |
| describe_image, |
| ], |
| model=model, |
| |
| additional_authorized_imports=[ |
| "pandas", |
| "numpy", |
| "openpyxl", |
| "csv", |
| "json", |
| "math", |
| "statistics", |
| "datetime", |
| "io", |
| "re", |
| "os", |
| "requests", |
| "bs4", |
| ], |
| max_steps=25, |
| verbosity_level=1, |
| ) |
| print("GaiaAgent initialized (smolagents CodeAgent + deepseek-v4-flash).") |
|
|
| def __call__(self, question: str, task_id: str | None = None) -> str: |
| print(f"Agent received question (first 80 chars): {question[:80]}...") |
| prompt = GAIA_INSTRUCTIONS.format(task_id=task_id or "", question=question) |
| result = self.agent.run(prompt) |
| answer = self._clean(result) |
| print(f"Agent returning answer: {answer}") |
| return answer |
|
|
| @staticmethod |
| def _clean(result) -> str: |
| """Normalize the agent output to the bare answer string. |
| |
| GAIA grades by exact string match, so we strip the noise the model |
| commonly leaves around the answer (label prefix, surrounding quotes, |
| trailing punctuation) without altering the answer itself. |
| """ |
| text = str(result).strip() |
| text = re.sub(r"(?i)^\s*final answer\s*:?\s*", "", text).strip() |
| |
| |
| if len(text) >= 2 and text[0] == text[-1] and text[0] in ("'", '"', "`"): |
| text = text[1:-1].strip() |
| |
| text = re.sub(r"[.,;]\s*$", "", text).strip() |
| return text |
|
|
| def run_and_submit_all( profile: gr.OAuthProfile | None): |
| """ |
| Fetches all questions, runs the BasicAgent on them, submits all answers, |
| and displays the results. |
| """ |
| |
| space_id = os.getenv("SPACE_ID") |
|
|
| if profile: |
| username= f"{profile.username}" |
| print(f"User logged in: {username}") |
| else: |
| print("User not logged in.") |
| return "Please Login to Hugging Face with the button.", None |
|
|
| api_url = DEFAULT_API_URL |
| questions_url = f"{api_url}/questions" |
| submit_url = f"{api_url}/submit" |
|
|
| |
| try: |
| agent = GaiaAgent() |
| except Exception as e: |
| print(f"Error instantiating agent: {e}") |
| return f"Error initializing agent: {e}", None |
| |
| agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" |
| print(agent_code) |
|
|
| |
| print(f"Fetching questions from: {questions_url}") |
| try: |
| response = requests.get(questions_url, timeout=15) |
| response.raise_for_status() |
| questions_data = response.json() |
| if not questions_data: |
| print("Fetched questions list is empty.") |
| return "Fetched questions list is empty or invalid format.", None |
| print(f"Fetched {len(questions_data)} questions.") |
| except requests.exceptions.RequestException as e: |
| print(f"Error fetching questions: {e}") |
| return f"Error fetching questions: {e}", None |
| except requests.exceptions.JSONDecodeError as e: |
| print(f"Error decoding JSON response from questions endpoint: {e}") |
| print(f"Response text: {response.text[:500]}") |
| return f"Error decoding server response for questions: {e}", None |
| except Exception as e: |
| print(f"An unexpected error occurred fetching questions: {e}") |
| return f"An unexpected error occurred fetching questions: {e}", None |
|
|
| |
| results_log = [] |
| answers_payload = [] |
| print(f"Running agent on {len(questions_data)} questions...") |
| for item in questions_data: |
| task_id = item.get("task_id") |
| question_text = item.get("question") |
| if not task_id or question_text is None: |
| print(f"Skipping item with missing task_id or question: {item}") |
| continue |
| try: |
| submitted_answer = agent(question_text, task_id) |
| answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer}) |
| results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer}) |
| except Exception as e: |
| print(f"Error running agent on task {task_id}: {e}") |
| results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"}) |
|
|
| if not answers_payload: |
| print("Agent did not produce any answers to submit.") |
| return "Agent did not produce any answers to submit.", pd.DataFrame(results_log) |
|
|
| |
| submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload} |
| status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..." |
| print(status_update) |
|
|
| |
| print(f"Submitting {len(answers_payload)} answers to: {submit_url}") |
| try: |
| response = requests.post(submit_url, json=submission_data, timeout=60) |
| response.raise_for_status() |
| result_data = response.json() |
| final_status = ( |
| f"Submission Successful!\n" |
| f"User: {result_data.get('username')}\n" |
| f"Overall Score: {result_data.get('score', 'N/A')}% " |
| f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n" |
| f"Message: {result_data.get('message', 'No message received.')}" |
| ) |
| print("Submission successful.") |
| results_df = pd.DataFrame(results_log) |
| return final_status, results_df |
| except requests.exceptions.HTTPError as e: |
| error_detail = f"Server responded with status {e.response.status_code}." |
| try: |
| error_json = e.response.json() |
| error_detail += f" Detail: {error_json.get('detail', e.response.text)}" |
| except requests.exceptions.JSONDecodeError: |
| error_detail += f" Response: {e.response.text[:500]}" |
| status_message = f"Submission Failed: {error_detail}" |
| print(status_message) |
| results_df = pd.DataFrame(results_log) |
| return status_message, results_df |
| except requests.exceptions.Timeout: |
| status_message = "Submission Failed: The request timed out." |
| print(status_message) |
| results_df = pd.DataFrame(results_log) |
| return status_message, results_df |
| except requests.exceptions.RequestException as e: |
| status_message = f"Submission Failed: Network error - {e}" |
| print(status_message) |
| results_df = pd.DataFrame(results_log) |
| return status_message, results_df |
| except Exception as e: |
| status_message = f"An unexpected error occurred during submission: {e}" |
| print(status_message) |
| results_df = pd.DataFrame(results_log) |
| return status_message, results_df |
|
|
|
|
| |
| with gr.Blocks() as demo: |
| gr.Markdown("# Basic Agent Evaluation Runner") |
| gr.Markdown( |
| """ |
| **Instructions:** |
| |
| 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ... |
| 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission. |
| 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score. |
| |
| --- |
| **Disclaimers:** |
| Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions). |
| This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async. |
| """ |
| ) |
|
|
| gr.LoginButton() |
|
|
| run_button = gr.Button("Run Evaluation & Submit All Answers") |
|
|
| status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False) |
| |
| results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True) |
|
|
| run_button.click( |
| fn=run_and_submit_all, |
| outputs=[status_output, results_table] |
| ) |
|
|
| if __name__ == "__main__": |
| print("\n" + "-"*30 + " App Starting " + "-"*30) |
| |
| space_host_startup = os.getenv("SPACE_HOST") |
| space_id_startup = os.getenv("SPACE_ID") |
|
|
| if space_host_startup: |
| print(f"✅ SPACE_HOST found: {space_host_startup}") |
| print(f" Runtime URL should be: https://{space_host_startup}.hf.space") |
| else: |
| print("ℹ️ SPACE_HOST environment variable not found (running locally?).") |
|
|
| if space_id_startup: |
| print(f"✅ SPACE_ID found: {space_id_startup}") |
| print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}") |
| print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main") |
| else: |
| print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.") |
|
|
| print("-"*(60 + len(" App Starting ")) + "\n") |
|
|
| print("Launching Gradio Interface for Basic Agent Evaluation...") |
| demo.launch(debug=True, share=False) |