Download agent.py from agents-course/Final_Assignment_Template: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/spaces/agents-course/Final_Assignment_Template/resolve/refs%2Fpr%2F541/agent.py
- Command line
-
hf download hf://spaces/agents-course/Final_Assignment_Template@refs/pr/541/agent.py
-
curl -L -o agent.py https://huggingface.co/spaces/agents-course/Final_Assignment_Template/resolve/refs%2Fpr%2F541/agent.py
18.3 kB
| """GAIA Level-1 agent for the HF Agents Course final assignment. | |
| Built on smolagents CodeAgent. Works with ANY of these free LLM backends — | |
| set whichever API key you can get and the agent auto-detects it: | |
| GROQ_API_KEY console.groq.com/keys (free, no card, fast) | |
| CEREBRAS_API_KEY cloud.cerebras.ai (free tier) | |
| OPENROUTER_API_KEY openrouter.ai/keys (has free models) | |
| MISTRAL_API_KEY console.mistral.ai (free tier) | |
| GOOGLE_API_KEY aistudio.google.com/apikey (free, best multimodal) | |
| HF_TOKEN huggingface.co/settings/tokens (needs credits) | |
| Generic escape hatch for any other OpenAI-compatible endpoint: | |
| OPENAI_API_KEY + OPENAI_BASE_URL + AGENT_MODEL | |
| Override the model with AGENT_MODEL if a default model id has been retired. | |
| """ | |
| import base64 | |
| import mimetypes | |
| import os | |
| import re | |
| import tempfile | |
| import time | |
| import requests | |
| from smolagents import ( | |
| CodeAgent, | |
| DuckDuckGoSearchTool, | |
| VisitWebpageTool, | |
| tool, | |
| ) | |
| DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space" | |
| WIKI_UA = "HF-Agents-Course-GAIA-Agent/1.0 (educational use)" | |
| # Provider registry: env var -> (base_url, default model, label) | |
| # Model ids are defaults only; override with AGENT_MODEL if one is retired. | |
| PROVIDERS = [ | |
| ( | |
| "GROQ_API_KEY", | |
| "https://api.groq.com/openai/v1", | |
| "llama-3.3-70b-versatile", | |
| "Groq", | |
| ), | |
| ( | |
| "CEREBRAS_API_KEY", | |
| "https://api.cerebras.ai/v1", | |
| "llama-3.3-70b", | |
| "Cerebras", | |
| ), | |
| ( | |
| "OPENROUTER_API_KEY", | |
| "https://openrouter.ai/api/v1", | |
| "meta-llama/llama-3.3-70b-instruct:free", | |
| "OpenRouter", | |
| ), | |
| ( | |
| "MISTRAL_API_KEY", | |
| "https://api.mistral.ai/v1", | |
| "mistral-large-latest", | |
| "Mistral", | |
| ), | |
| ( | |
| "GOOGLE_API_KEY", | |
| "https://generativelanguage.googleapis.com/v1beta/openai/", | |
| "gemini-2.5-flash", | |
| "Gemini", | |
| ), | |
| ( | |
| "OPENAI_API_KEY", | |
| os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), | |
| "gpt-4o-mini", | |
| "OpenAI-compatible", | |
| ), | |
| ] | |
| def _active_provider(): | |
| """Return (api_key, base_url, model_id, label) for the first key found.""" | |
| for env_var, base_url, default_model, label in PROVIDERS: | |
| key = os.getenv(env_var) | |
| if key: | |
| return key, base_url, os.getenv("AGENT_MODEL", default_model), label | |
| return None, None, None, None | |
| API_KEY, BASE_URL, MODEL_ID, PROVIDER = _active_provider() | |
| GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") | |
| # -------------------------------------------------------------------------- | |
| # Gemini-only helper: native audio / video understanding | |
| # -------------------------------------------------------------------------- | |
| def _gemini_generate(parts: list, retries: int = 3) -> str: | |
| model = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") | |
| url = ( | |
| "https://generativelanguage.googleapis.com/v1beta/models/" | |
| f"{model}:generateContent?key={GOOGLE_API_KEY}" | |
| ) | |
| for attempt in range(retries): | |
| resp = requests.post(url, json={"contents": [{"parts": parts}]}, timeout=180) | |
| if resp.status_code == 429 and attempt < retries - 1: | |
| time.sleep(20 * (attempt + 1)) | |
| continue | |
| resp.raise_for_status() | |
| return resp.json()["candidates"][0]["content"]["parts"][0]["text"] | |
| return "ERROR: Gemini rate limited." | |
| def _inline_part(file_path: str) -> dict: | |
| mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream" | |
| with open(file_path, "rb") as f: | |
| return { | |
| "inline_data": { | |
| "mime_type": mime, | |
| "data": base64.b64encode(f.read()).decode(), | |
| } | |
| } | |
| # -------------------------------------------------------------------------- | |
| # Tools | |
| # -------------------------------------------------------------------------- | |
| def wikipedia_page(title: str) -> str: | |
| """Fetch the full plain text of an English Wikipedia article. Use this | |
| instead of visit_webpage for Wikipedia — it never gets blocked. | |
| Args: | |
| title: Article title, e.g. "Mercedes Sosa" or "1928 Summer Olympics". | |
| """ | |
| try: | |
| resp = requests.get( | |
| "https://en.wikipedia.org/w/api.php", | |
| params={ | |
| "action": "query", | |
| "prop": "extracts", | |
| "explaintext": 1, | |
| "redirects": 1, | |
| "format": "json", | |
| "titles": title, | |
| }, | |
| headers={"User-Agent": WIKI_UA}, | |
| timeout=45, | |
| ) | |
| resp.raise_for_status() | |
| pages = resp.json()["query"]["pages"] | |
| page = list(pages.values())[0] | |
| if "extract" not in page: | |
| return f"No Wikipedia article found for '{title}'. Try wikipedia_search." | |
| return page["extract"][:60000] | |
| except Exception as e: | |
| return f"ERROR fetching Wikipedia page: {e}" | |
| def wikipedia_search(query: str) -> str: | |
| """Search English Wikipedia and return matching article titles with snippets. | |
| Use this to find the right title, then call wikipedia_page. | |
| Args: | |
| query: Search terms. | |
| """ | |
| try: | |
| resp = requests.get( | |
| "https://en.wikipedia.org/w/api.php", | |
| params={ | |
| "action": "query", | |
| "list": "search", | |
| "srsearch": query, | |
| "srlimit": 10, | |
| "format": "json", | |
| }, | |
| headers={"User-Agent": WIKI_UA}, | |
| timeout=45, | |
| ) | |
| resp.raise_for_status() | |
| hits = resp.json()["query"]["search"] | |
| return "\n".join( | |
| f"- {h['title']}: {re.sub('<[^<]+?>', '', h['snippet'])}" for h in hits | |
| ) or "No results." | |
| except Exception as e: | |
| return f"ERROR searching Wikipedia: {e}" | |
| def fetch_url(url: str) -> str: | |
| """Fetch a web page as text with a browser-like user agent. Use when | |
| visit_webpage fails with a 403 Forbidden error. | |
| Args: | |
| url: The full URL to fetch. | |
| """ | |
| try: | |
| resp = requests.get( | |
| url, | |
| headers={ | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " | |
| "(KHTML, like Gecko) Chrome/122.0 Safari/537.36" | |
| ) | |
| }, | |
| timeout=60, | |
| ) | |
| resp.raise_for_status() | |
| try: | |
| from markdownify import markdownify | |
| text = markdownify(resp.text) | |
| except Exception: | |
| text = resp.text | |
| text = re.sub(r"\n{3,}", "\n\n", text) | |
| return text[:50000] | |
| except Exception as e: | |
| return f"ERROR fetching url: {e}" | |
| def transcribe_audio(file_path: str) -> str: | |
| """Transcribe a local audio file (mp3/wav/m4a) to English text. | |
| Args: | |
| file_path: Absolute path to the local audio file to transcribe. | |
| """ | |
| # Gemini: native audio understanding | |
| if GOOGLE_API_KEY: | |
| try: | |
| return _gemini_generate( | |
| [{"text": "Transcribe this audio verbatim."}, _inline_part(file_path)] | |
| ) | |
| except Exception as e: | |
| return f"ERROR transcribing audio: {e}" | |
| # Groq hosts Whisper on an OpenAI-compatible endpoint | |
| if os.getenv("GROQ_API_KEY"): | |
| try: | |
| with open(file_path, "rb") as f: | |
| resp = requests.post( | |
| "https://api.groq.com/openai/v1/audio/transcriptions", | |
| headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"}, | |
| files={"file": (os.path.basename(file_path), f)}, | |
| data={"model": os.getenv("ASR_MODEL", "whisper-large-v3")}, | |
| timeout=180, | |
| ) | |
| resp.raise_for_status() | |
| return resp.json()["text"] | |
| except Exception as e: | |
| return f"ERROR transcribing audio: {e}" | |
| # HF Inference fallback | |
| try: | |
| from huggingface_hub import InferenceClient | |
| result = InferenceClient( | |
| token=os.getenv("HF_TOKEN") | |
| ).automatic_speech_recognition( | |
| file_path, model=os.getenv("ASR_MODEL", "openai/whisper-large-v3") | |
| ) | |
| return result.text if hasattr(result, "text") else str(result) | |
| except Exception as e: | |
| return f"ERROR transcribing audio (no ASR backend available): {e}" | |
| def analyze_image(file_path: str, question: str) -> str: | |
| """Answer a question about a local image file using a vision model. | |
| Args: | |
| file_path: Absolute path to the local image file (png/jpg). | |
| question: The question to answer about the image. Be specific; for | |
| chess positions, ask for a full square-by-square board reading | |
| AND the winning move, verified carefully. | |
| """ | |
| if GOOGLE_API_KEY: | |
| try: | |
| return _gemini_generate([{"text": question}, _inline_part(file_path)]) | |
| except Exception as e: | |
| return f"ERROR analyzing image: {e}" | |
| if not API_KEY: | |
| return "ERROR: no vision backend configured." | |
| try: | |
| mime = mimetypes.guess_type(file_path)[0] or "image/png" | |
| with open(file_path, "rb") as f: | |
| b64 = base64.b64encode(f.read()).decode() | |
| vision_model = os.getenv("VISION_MODEL", MODEL_ID) | |
| resp = requests.post( | |
| BASE_URL.rstrip("/") + "/chat/completions", | |
| headers={"Authorization": f"Bearer {API_KEY}"}, | |
| json={ | |
| "model": vision_model, | |
| "max_tokens": 1500, | |
| "messages": [ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "text", "text": question}, | |
| { | |
| "type": "image_url", | |
| "image_url": {"url": f"data:{mime};base64,{b64}"}, | |
| }, | |
| ], | |
| } | |
| ], | |
| }, | |
| timeout=180, | |
| ) | |
| resp.raise_for_status() | |
| return resp.json()["choices"][0]["message"]["content"] | |
| except Exception as e: | |
| return ( | |
| f"ERROR analyzing image: {e}. The model may not support images; " | |
| "set VISION_MODEL to a vision-capable model id." | |
| ) | |
| def analyze_youtube_video(video_url: str, question: str) -> str: | |
| """Watch a YouTube video and answer a question about its visual and audio | |
| content (counting things on screen, quotes, scenes). Requires a Gemini key. | |
| Args: | |
| video_url: Full YouTube URL, e.g. https://www.youtube.com/watch?v=XXXX | |
| question: The question to answer about the video. | |
| """ | |
| if not GOOGLE_API_KEY: | |
| return ( | |
| "ERROR: video analysis needs GOOGLE_API_KEY. Use " | |
| "get_youtube_transcript or web_search for descriptions instead." | |
| ) | |
| try: | |
| return _gemini_generate( | |
| [{"text": question}, {"file_data": {"file_uri": video_url}}] | |
| ) | |
| except Exception as e: | |
| return f"ERROR analyzing video: {e}" | |
| def get_youtube_transcript(video_url: str) -> str: | |
| """Fetch the transcript/captions of a YouTube video as plain text. | |
| Args: | |
| video_url: Full YouTube URL, e.g. https://www.youtube.com/watch?v=XXXX | |
| """ | |
| try: | |
| from youtube_transcript_api import YouTubeTranscriptApi | |
| m = re.search(r"(?:v=|youtu\.be/)([\w-]{11})", video_url) | |
| if not m: | |
| return "ERROR: could not parse video id from URL." | |
| vid = m.group(1) | |
| try: | |
| entries = [s.text for s in YouTubeTranscriptApi().fetch(vid)] | |
| except AttributeError: | |
| entries = [s["text"] for s in YouTubeTranscriptApi.get_transcript(vid)] | |
| return " ".join(entries)[:20000] | |
| except Exception as e: | |
| return f"ERROR fetching transcript: {e}. Try web_search instead." | |
| def read_file_as_text(file_path: str) -> str: | |
| """Read a local text-like file (py, txt, csv, json, md) and return its content. | |
| Args: | |
| file_path: Absolute path to the local file. | |
| """ | |
| try: | |
| with open(file_path, "r", encoding="utf-8", errors="replace") as f: | |
| return f.read()[:30000] | |
| except Exception as e: | |
| return f"ERROR reading file: {e}" | |
| # -------------------------------------------------------------------------- | |
| # Answer-format guidance (GAIA is scored by EXACT MATCH) | |
| # -------------------------------------------------------------------------- | |
| GAIA_INSTRUCTIONS = """You are a general AI assistant answering a benchmark | |
| question scored by EXACT string match. Work step by step with your tools, | |
| then call final_answer() with ONLY the answer itself. | |
| Formatting rules for the final answer (critical): | |
| - Do NOT write "FINAL ANSWER" or any prefix/suffix, explanation, or period | |
| at the end. Output the bare answer only. | |
| - Numbers: plain digits, no thousands separators, no units ($, %, kg) unless | |
| the question explicitly asks for them, no trailing ".0". | |
| - Strings: no articles ("the", "a"), no abbreviations unless asked. | |
| - Comma-separated lists: apply the rules above to each element, use ", " | |
| (comma + space) between elements, and respect any ordering the question | |
| asks for (e.g. alphabetical, ascending). | |
| - If asked for a first name / last name / city / country code only, return | |
| exactly that and nothing more. | |
| Tool strategy: | |
| - Wikipedia questions: use wikipedia_search then wikipedia_page. Do NOT use | |
| visit_webpage on wikipedia.org — it returns 403. The article text often | |
| contains a discography or results table; read it carefully and count. | |
| - If visit_webpage returns 403 Forbidden, retry that URL with fetch_url. | |
| - Attached files: a local path is given; use read_file_as_text, | |
| transcribe_audio, analyze_image, or pandas (pd.read_excel) for .xlsx. | |
| - For .xlsx, inspect the columns first, then compute. Format money like | |
| 89706.00 only when the question asks for two decimal places. | |
| - Python-code questions: read the code and reason through it carefully. | |
| - YouTube: try analyze_youtube_video, then get_youtube_transcript, then | |
| web_search for third-party descriptions of the video. | |
| - Some questions are pure reasoning (reversed text, a group-theory table). | |
| Solve those directly in python without searching. | |
| Reliability rules: | |
| - Never give up and guess a number you did not verify. If one source is | |
| blocked, try another tool or another source. | |
| - Re-read the question's exact wording before answering (e.g. "included", | |
| "as of July 2023", "IOC country code", "without abbreviations", | |
| "first name only"). | |
| """ | |
| class GAIAAgent: | |
| """Wraps a smolagents CodeAgent with GAIA-specific tooling and prompting.""" | |
| def __init__(self): | |
| if not API_KEY: | |
| raise RuntimeError( | |
| "No LLM API key found. Set one of: GROQ_API_KEY, " | |
| "CEREBRAS_API_KEY, OPENROUTER_API_KEY, MISTRAL_API_KEY, " | |
| "GOOGLE_API_KEY, or OPENAI_API_KEY (+OPENAI_BASE_URL)." | |
| ) | |
| from smolagents import OpenAIServerModel | |
| model = OpenAIServerModel( | |
| model_id=MODEL_ID, api_base=BASE_URL, api_key=API_KEY | |
| ) | |
| self.agent = CodeAgent( | |
| model=model, | |
| tools=[ | |
| DuckDuckGoSearchTool(), | |
| VisitWebpageTool(), | |
| fetch_url, | |
| wikipedia_search, | |
| wikipedia_page, | |
| transcribe_audio, | |
| analyze_image, | |
| analyze_youtube_video, | |
| get_youtube_transcript, | |
| read_file_as_text, | |
| ], | |
| additional_authorized_imports=[ | |
| "pandas", | |
| "numpy", | |
| "openpyxl", | |
| "json", | |
| "csv", | |
| "re", | |
| "math", | |
| "statistics", | |
| "itertools", | |
| "collections", | |
| "datetime", | |
| ], | |
| max_steps=15, | |
| ) | |
| print(f"GAIAAgent initialized (backend={PROVIDER}, model={MODEL_ID}).") | |
| def download_task_file(task_id: str, file_name: str) -> str | None: | |
| """Download the file attached to a task; returns a local path or None.""" | |
| if not file_name: | |
| return None | |
| url = f"{DEFAULT_API_URL}/files/{task_id}" | |
| for attempt in range(4): | |
| try: | |
| resp = requests.get(url, timeout=30) | |
| resp.raise_for_status() | |
| fd, path = tempfile.mkstemp(suffix=os.path.splitext(file_name)[1] or "") | |
| with os.fdopen(fd, "wb") as f: | |
| f.write(resp.content) | |
| return path | |
| except Exception as e: | |
| print(f"File download attempt {attempt + 1} failed for {task_id}: {e}") | |
| time.sleep(3 * (attempt + 1)) | |
| return None | |
| def _clean(answer: str) -> str: | |
| """Strip wrappers the model sometimes adds despite instructions.""" | |
| a = str(answer).strip() | |
| a = re.sub(r"^(final answer\s*:?\s*)", "", a, flags=re.IGNORECASE) | |
| a = a.strip().strip('"').strip("'").strip() | |
| if a.endswith("."): | |
| a = a[:-1] | |
| return a | |
| def __call__(self, question: str, task_id: str = "", file_name: str = "") -> str: | |
| prompt = GAIA_INSTRUCTIONS + "\n\nQuestion: " + question | |
| file_path = self.download_task_file(task_id, file_name) | |
| if file_path: | |
| prompt += ( | |
| f"\n\nAn attached file for this question was downloaded to the " | |
| f"local path: {file_path} (original name: {file_name})." | |
| ) | |
| elif file_name: | |
| prompt += ( | |
| f"\n\nNOTE: this question references an attached file " | |
| f"({file_name}) that could not be downloaded. Answer from " | |
| f"other sources if possible." | |
| ) | |
| try: | |
| return self._clean(self.agent.run(prompt)) | |
| except Exception as e: | |
| print(f"Agent error on task {task_id}: {e}") | |
| return f"AGENT ERROR: {e}" | |