Download agent.py from AndreasG10/Final_Assignment_Template: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/spaces/AndreasG10/Final_Assignment_Template/resolve/main/agent.py
- Command line
-
hf download hf://spaces/AndreasG10/Final_Assignment_Template/agent.py
-
curl -L -o agent.py https://huggingface.co/spaces/AndreasG10/Final_Assignment_Template/resolve/main/agent.py
12.9 kB
| """ | |
| GAIA agent for the Hugging Face Agents Course (Unit 4 final assignment). | |
| Default: local Ollama (same as your original setup). Configure with env vars: | |
| LLM_BACKEND "ollama" (default) | "hf" (HF Inference, needs HF_TOKEN) | "litellm" | |
| LLM_MODEL_ID default for ollama: ollama_chat/qwen2.5:7b | |
| LLM_API_BASE default for ollama: http://localhost:11434 | |
| LLM_API_KEY only for "litellm" | |
| NUM_CTX Ollama context window (default 8192) | |
| MAX_PAGE_CHARS max characters a web tool returns (default 5000, keeps the 8k context safe) | |
| """ | |
| import os | |
| import re | |
| import tempfile | |
| from pathlib import Path | |
| from urllib.parse import urlparse, parse_qs | |
| import pandas as pd | |
| import requests | |
| from bs4 import BeautifulSoup | |
| from smolagents import ( | |
| CodeAgent, | |
| DuckDuckGoSearchTool, | |
| InferenceClientModel, | |
| LiteLLMModel, | |
| tool, | |
| ) | |
| MAX_PAGE_CHARS = int(os.getenv("MAX_PAGE_CHARS", "5000")) | |
| HEADERS = {"User-Agent": "Mozilla/5.0 (GAIA course agent; educational use)"} | |
| # --------------------------------------------------------------------------- # | |
| # Model | |
| # --------------------------------------------------------------------------- # | |
| def build_model(): | |
| backend = os.getenv("LLM_BACKEND", "ollama").lower() | |
| if backend == "ollama": | |
| return LiteLLMModel( | |
| model_id=os.getenv("LLM_MODEL_ID", "ollama_chat/qwen2.5:7b"), | |
| api_base=os.getenv("LLM_API_BASE", "http://localhost:11434"), | |
| api_key="ollama", | |
| num_ctx=int(os.getenv("NUM_CTX", "8192")), | |
| ) | |
| if backend == "litellm": | |
| kwargs = {"model_id": os.environ["LLM_MODEL_ID"]} | |
| if os.getenv("LLM_API_KEY"): | |
| kwargs["api_key"] = os.getenv("LLM_API_KEY") | |
| if os.getenv("LLM_API_BASE"): | |
| kwargs["api_base"] = os.getenv("LLM_API_BASE") | |
| return LiteLLMModel(**kwargs) | |
| token = os.getenv("HF_TOKEN") | |
| if not token: | |
| raise RuntimeError("HF_TOKEN is not set (needed for LLM_BACKEND=hf).") | |
| return InferenceClientModel( | |
| model_id=os.getenv("LLM_MODEL_ID", "Qwen/Qwen2.5-72B-Instruct"), token=token | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # Helpers | |
| # --------------------------------------------------------------------------- # | |
| def _page_text(html: str) -> str: | |
| soup = BeautifulSoup(html, "html.parser") | |
| for tag in soup(["script", "style", "nav", "footer", "header", "aside"]): | |
| tag.decompose() | |
| return soup.get_text(separator="\n", strip=True) | |
| def download_task_file(api_url: str, task_id: str, file_name: str | None = None) -> str | None: | |
| """Download the file attached to a task (if any). Returns a local path or None.""" | |
| try: | |
| r = requests.get(f"{api_url}/files/{task_id}", timeout=30) | |
| if r.status_code == 404: | |
| return None | |
| r.raise_for_status() | |
| except requests.RequestException as e: | |
| print(f"Could not download file for task {task_id}: {e}") | |
| return None | |
| if not file_name: | |
| cd = r.headers.get("content-disposition", "") | |
| m = re.search(r'filename="?([^";]+)"?', cd) | |
| file_name = m.group(1) if m else task_id | |
| out_dir = Path(tempfile.gettempdir()) / "gaia_files" / task_id | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| path = out_dir / Path(file_name).name | |
| path.write_bytes(r.content) | |
| return str(path) | |
| def clean_answer(text: str) -> str: | |
| """The scorer is EXACT MATCH, so strip anything that is not the answer.""" | |
| t = str(text).strip() | |
| # Never submit the literal "FINAL ANSWER" marker (course requirement). | |
| t = re.sub(r"(?is)^.*?final answer\s*[:\-]\s*", "", t).strip() | |
| # Remove trailing sentence periods, then wrapping quotes. | |
| t = t.rstrip(".").strip() | |
| if len(t) >= 2 and t[0] == t[-1] and t[0] in "\"'": | |
| t = t[1:-1].strip() | |
| return t | |
| # --------------------------------------------------------------------------- # | |
| # Tools | |
| # --------------------------------------------------------------------------- # | |
| def visit_webpage(url: str) -> str: | |
| """ | |
| Read the textual content of a public webpage. | |
| Args: | |
| url: The URL of the webpage to retrieve. | |
| """ | |
| try: | |
| r = requests.get(url, timeout=20, headers=HEADERS) | |
| r.raise_for_status() | |
| except requests.RequestException as e: | |
| return f"Error fetching {url}: {e}" | |
| return _page_text(r.text)[:MAX_PAGE_CHARS] | |
| def extract_webpage_section(url: str, keyword: str) -> str: | |
| """ | |
| Retrieve a webpage and return the text around a keyword (useful for long pages). | |
| Args: | |
| url: URL of the webpage. | |
| keyword: Section title or phrase to search for. | |
| """ | |
| try: | |
| r = requests.get(url, timeout=20, headers=HEADERS) | |
| r.raise_for_status() | |
| except requests.RequestException as e: | |
| return f"Error fetching {url}: {e}" | |
| text = _page_text(r.text) | |
| pos = text.lower().find(keyword.lower()) | |
| if pos == -1: | |
| return "Keyword not found." | |
| start = max(0, pos - 300) | |
| return text[start : start + MAX_PAGE_CHARS] | |
| def wikipedia_search(query: str) -> str: | |
| """ | |
| Search English Wikipedia and return the titles and snippets of the best matches. | |
| Args: | |
| query: What to search for. | |
| """ | |
| try: | |
| r = requests.get( | |
| "https://en.wikipedia.org/w/api.php", | |
| params={ | |
| "action": "query", | |
| "list": "search", | |
| "srsearch": query, | |
| "srlimit": 5, | |
| "format": "json", | |
| }, | |
| headers=HEADERS, | |
| timeout=20, | |
| ) | |
| r.raise_for_status() | |
| hits = r.json()["query"]["search"] | |
| except Exception as e: | |
| return f"Wikipedia search failed: {e}" | |
| if not hits: | |
| return "No results." | |
| return "\n".join( | |
| f"- {h['title']}: {BeautifulSoup(h['snippet'], 'html.parser').get_text()}" | |
| for h in hits | |
| ) | |
| def wikipedia_page(title: str, section_keyword: str = "") -> str: | |
| """ | |
| Get the plain text of an English Wikipedia article (tables included as text). | |
| Args: | |
| title: Exact article title, e.g. "Mercedes Sosa". | |
| section_keyword: Optional word/phrase (e.g. "Discography") - if given, return the text around it. | |
| """ | |
| try: | |
| r = requests.get( | |
| f"https://en.wikipedia.org/wiki/{title.replace(' ', '_')}", | |
| headers=HEADERS, | |
| timeout=20, | |
| ) | |
| r.raise_for_status() | |
| except requests.RequestException as e: | |
| return f"Error fetching article '{title}': {e}" | |
| soup = BeautifulSoup(r.text, "html.parser") | |
| content = soup.find(id="mw-content-text") or soup | |
| for tag in content(["script", "style", "sup"]) + content.select(".navbox, .reflist"): | |
| tag.decompose() | |
| text = content.get_text(separator="\n", strip=True) | |
| if section_keyword: | |
| pos = text.lower().find(section_keyword.lower()) | |
| if pos != -1: | |
| return text[max(0, pos - 200) : pos + MAX_PAGE_CHARS] | |
| return text[:MAX_PAGE_CHARS] | |
| def read_file(path: str) -> str: | |
| """ | |
| Read a file attached to the task. Supports text/code/json/csv/xlsx files. | |
| For audio use transcribe_audio, for images use describe_image. | |
| Args: | |
| path: Local path of the file. | |
| """ | |
| p = Path(path) | |
| if not p.exists(): | |
| return f"File not found: {path}" | |
| ext = p.suffix.lower() | |
| try: | |
| if ext in (".xlsx", ".xls"): | |
| sheets = pd.read_excel(p, sheet_name=None) | |
| return "\n\n".join( | |
| f"## Sheet: {name}\n{df.to_string()}" for name, df in sheets.items() | |
| )[:20000] | |
| if ext == ".csv": | |
| return pd.read_csv(p).to_string()[:20000] | |
| return p.read_text(errors="replace")[:20000] | |
| except Exception as e: | |
| return f"Could not read {path}: {e}" | |
| def transcribe_audio(path: str) -> str: | |
| """ | |
| Transcribe a local audio file (mp3, wav, ...) to text. | |
| Args: | |
| path: Local path of the audio file. | |
| """ | |
| try: | |
| from huggingface_hub import InferenceClient | |
| client = InferenceClient(token=os.getenv("HF_TOKEN")) | |
| out = client.automatic_speech_recognition( | |
| path, model=os.getenv("ASR_MODEL_ID", "openai/whisper-large-v3") | |
| ) | |
| return out.text | |
| except Exception as e: | |
| return f"Transcription failed: {e}" | |
| def describe_image(path: str, question: str) -> str: | |
| """ | |
| Ask a vision model a question about a local image file. | |
| Args: | |
| path: Local path of the image. | |
| question: What you want to know about the image. | |
| """ | |
| try: | |
| import base64 | |
| import mimetypes | |
| from huggingface_hub import InferenceClient | |
| mime = mimetypes.guess_type(path)[0] or "image/png" | |
| b64 = base64.b64encode(Path(path).read_bytes()).decode() | |
| client = InferenceClient(token=os.getenv("HF_TOKEN")) | |
| resp = client.chat_completion( | |
| model=os.getenv("VISION_MODEL_ID", "Qwen/Qwen2.5-VL-72B-Instruct"), | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}, | |
| {"type": "text", "text": question}, | |
| ], | |
| } | |
| ], | |
| max_tokens=500, | |
| ) | |
| return resp.choices[0].message.content | |
| except Exception as e: | |
| return f"Image analysis failed: {e}" | |
| def youtube_transcript(url: str) -> str: | |
| """ | |
| Get the transcript (captions) of a YouTube video. | |
| Args: | |
| url: The YouTube video URL. | |
| """ | |
| try: | |
| from youtube_transcript_api import YouTubeTranscriptApi | |
| parsed = urlparse(url) | |
| video_id = parse_qs(parsed.query).get("v", [None])[0] or parsed.path.strip("/").split("/")[-1] | |
| segments = YouTubeTranscriptApi().fetch(video_id) | |
| return " ".join(s.text for s in segments)[:MAX_PAGE_CHARS * 2] | |
| except Exception as e: | |
| return f"Could not get transcript: {e}" | |
| # --------------------------------------------------------------------------- # | |
| # Agent | |
| # --------------------------------------------------------------------------- # | |
| INSTRUCTIONS = """ | |
| You are solving questions from the GAIA benchmark. Answers are graded by EXACT MATCH. | |
| Method: | |
| - Work out exactly what is asked and which source/date/category it specifies; use that source. | |
| - Use the tools: web search, Wikipedia tools, visit_webpage, and for attached files | |
| read_file / transcribe_audio / describe_image. Use python for any counting or arithmetic | |
| instead of doing it in your head. | |
| - For counting or list questions, enumerate every candidate first, check each against ALL | |
| constraints (e.g. studio albums only, date ranges inclusive/exclusive), then count. | |
| - Never invent facts. If one source fails, try another. | |
| Final answer format (very important): | |
| - Call final_answer(...) with ONLY the answer - no explanation, no sentence, no "FINAL ANSWER". | |
| - Numbers: plain digits, no thousands separators, no units or % unless the question asks. | |
| - Strings: no articles (a/an/the) and no abbreviations unless asked; digits written as digits. | |
| - Lists: comma-separated, in the order requested, following the same rules per element. | |
| """.strip() | |
| class BasicAgent: | |
| def __init__(self): | |
| self.model = build_model() | |
| self.agent = CodeAgent( | |
| tools=[ | |
| DuckDuckGoSearchTool(max_results=5), | |
| wikipedia_search, | |
| wikipedia_page, | |
| visit_webpage, | |
| extract_webpage_section, | |
| read_file, | |
| transcribe_audio, | |
| describe_image, | |
| youtube_transcript, | |
| ], | |
| model=self.model, | |
| instructions=INSTRUCTIONS, | |
| additional_authorized_imports=[ | |
| "re", "math", "statistics", "itertools", "collections", | |
| "datetime", "json", "csv", "pandas", "numpy", "unicodedata", | |
| ], | |
| max_steps=10, | |
| verbosity_level=1, | |
| ) | |
| def __call__(self, question: str, file_path: str | None = None) -> str: | |
| task = question | |
| if file_path: | |
| task += ( | |
| f"\n\nAn attached file for this question is available locally at: {file_path}" | |
| "\nOpen it with the appropriate tool (read_file, transcribe_audio, describe_image)." | |
| ) | |
| last_error = None | |
| for _ in range(2): # one retry for transient API / search failures | |
| try: | |
| return clean_answer(self.agent.run(task)) | |
| except Exception as e: # noqa: BLE001 | |
| last_error = e | |
| print(f"Agent attempt failed: {e}") | |
| raise last_error | |