Download app.py from elhassaneT/Final_Assignment_Template: direct link, hf CLI and curl.
- Browser
- Download file 20 kB
-
https://huggingface.co/spaces/elhassaneT/Final_Assignment_Template/resolve/main/app.py
- Command line
-
hf download hf://spaces/elhassaneT/Final_Assignment_Template/app.py
-
curl -L -o app.py https://huggingface.co/spaces/elhassaneT/Final_Assignment_Template/resolve/main/app.py
20 kB
| import gradio as gr | |
| import requests | |
| import os | |
| import io | |
| import re | |
| import base64 | |
| import time | |
| from typing import Optional | |
| try: | |
| import PyPDF2 | |
| HAS_PYPDF2 = True | |
| except ImportError: | |
| HAS_PYPDF2 = False | |
| # === CONFIGURATION === | |
| API_BASE = "https://agents-course-unit4-scoring.hf.space" | |
| HF_API_KEY = os.getenv("HF_TOKEN") | |
| HF_USERNAME = os.getenv("HF_USERNAME") | |
| GROQ_KEY = os.getenv("GROQ_API_KEY") | |
| TAVILY_KEY = os.getenv("TAVILY_API_KEY") | |
| # βββ GROQ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def call_groq(messages: list, max_tokens: int = 512, system: str = None, temp: float = 0.0) -> str: | |
| if not GROQ_KEY: | |
| return "" | |
| payload = [] | |
| if system: | |
| payload.append({"role": "system", "content": system}) | |
| payload.extend(messages) | |
| for model in ["llama-3.3-70b-versatile", "llama-3.1-70b-versatile"]: | |
| try: | |
| r = requests.post( | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| headers={"Authorization": f"Bearer {GROQ_KEY}", "Content-Type": "application/json"}, | |
| json={"model": model, "messages": payload, "temperature": temp, "max_tokens": max_tokens}, | |
| timeout=60, | |
| ) | |
| if r.status_code == 200: | |
| return r.json()["choices"][0]["message"]["content"].strip() | |
| elif r.status_code == 429: | |
| time.sleep(5) | |
| except Exception as e: | |
| print(f" Groq error ({model}): {e}") | |
| return "" | |
| # βββ SEARCH TOOLS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def tavily_search(query: str, max_results: int = 5) -> str: | |
| if not TAVILY_KEY: | |
| return "" | |
| try: | |
| r = requests.post( | |
| "https://api.tavily.com/search", | |
| json={"api_key": TAVILY_KEY, "query": query, "max_results": max_results, "search_depth": "advanced"}, | |
| timeout=30, | |
| ) | |
| if r.status_code == 200: | |
| items = r.json().get("results", []) | |
| parts = [f"[{x.get('title','')}]: {x.get('content','')[:600]}" for x in items] | |
| return "\n\n".join(parts) | |
| except Exception as e: | |
| print(f" Tavily error: {e}") | |
| return "" | |
| def ddg_search(query: str) -> str: | |
| try: | |
| r = requests.get( | |
| "https://api.duckduckgo.com/", | |
| params={"q": query, "format": "json", "no_html": 1, "skip_disambig": 1}, | |
| timeout=15, headers={"User-Agent": "Mozilla/5.0"}, | |
| ) | |
| data = r.json() | |
| parts = [] | |
| if data.get("AbstractText"): | |
| parts.append(data["AbstractText"]) | |
| for t in data.get("RelatedTopics", [])[:5]: | |
| if isinstance(t, dict) and t.get("Text"): | |
| parts.append(t["Text"]) | |
| return "\n\n".join(parts) | |
| except Exception as e: | |
| print(f" DDG error: {e}") | |
| return "" | |
| def wikipedia_search(query: str) -> str: | |
| """Search Wikipedia directly for factual questions.""" | |
| try: | |
| # First find the right page | |
| r = requests.get( | |
| "https://en.wikipedia.org/w/api.php", | |
| params={"action": "query", "list": "search", "srsearch": query, | |
| "format": "json", "srlimit": 3}, | |
| timeout=15, headers={"User-Agent": "GaiaAgent/1.0"}, | |
| ) | |
| results = r.json().get("query", {}).get("search", []) | |
| if not results: | |
| return "" | |
| # Get extract of top result | |
| title = results[0]["title"] | |
| r2 = requests.get( | |
| "https://en.wikipedia.org/w/api.php", | |
| params={"action": "query", "prop": "extracts", "exintro": True, | |
| "explaintext": True, "titles": title, "format": "json"}, | |
| timeout=15, headers={"User-Agent": "GaiaAgent/1.0"}, | |
| ) | |
| pages = r2.json().get("query", {}).get("pages", {}) | |
| for page in pages.values(): | |
| extract = page.get("extract", "") | |
| if extract: | |
| return f"[Wikipedia β {title}]: {extract[:1500]}" | |
| except Exception as e: | |
| print(f" Wikipedia error: {e}") | |
| return "" | |
| def smart_search(question: str, query: str) -> str: | |
| """Run multiple searches and combine results.""" | |
| results = [] | |
| # 1. Tavily (best) | |
| if TAVILY_KEY: | |
| t = tavily_search(query, max_results=4) | |
| if t: | |
| results.append(t) | |
| # 2. Wikipedia for factual/historical questions | |
| wiki_keywords = ["who", "what", "when", "where", "born", "died", "award", | |
| "won", "published", "film", "book", "song", "country", | |
| "capital", "president", "author", "founded", "invented"] | |
| if any(w in question.lower() for w in wiki_keywords): | |
| w = wikipedia_search(query) | |
| if w: | |
| results.append(w) | |
| # 3. DDG fallback | |
| if not results: | |
| d = ddg_search(query) | |
| if d: | |
| results.append(d) | |
| return "\n\n---\n\n".join(results)[:4000] | |
| # βββ FILE HANDLING ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def transcribe_audio(audio_bytes: bytes) -> str: | |
| for model in ["openai/whisper-large-v3", "openai/whisper-base"]: | |
| try: | |
| r = requests.post( | |
| f"https://api-inference.huggingface.co/models/{model}", | |
| headers={"Authorization": f"Bearer {HF_API_KEY}"}, | |
| data=audio_bytes, timeout=120, | |
| ) | |
| if r.status_code == 200: | |
| text = r.json().get("text", "") | |
| if text: | |
| return text | |
| except Exception as e: | |
| print(f" Whisper error: {e}") | |
| return "Transcription failed." | |
| def extract_pdf(pdf_bytes: bytes) -> str: | |
| if not HAS_PYPDF2: | |
| return "" | |
| try: | |
| reader = PyPDF2.PdfReader(io.BytesIO(pdf_bytes)) | |
| pages = [p.extract_text() for p in reader.pages if p.extract_text()] | |
| return "\n\n".join(pages)[:6000] | |
| except Exception as e: | |
| return f"PDF error: {e}" | |
| def describe_image_blip(image_bytes: bytes) -> str: | |
| try: | |
| r = requests.post( | |
| "https://api-inference.huggingface.co/models/Salesforce/blip-image-captioning-large", | |
| headers={"Authorization": f"Bearer {HF_API_KEY}"}, | |
| data=image_bytes, timeout=60, | |
| ) | |
| if r.status_code == 200: | |
| result = r.json() | |
| if isinstance(result, list) and result: | |
| return result[0].get("generated_text", "") | |
| except Exception as e: | |
| print(f" BLIP error: {e}") | |
| return "" | |
| def load_task_file(task_id: str, file_name: str, question: str = "") -> str: | |
| try: | |
| r = requests.get(f"{API_BASE}/files/{task_id}", timeout=45) | |
| if r.status_code != 200: | |
| return "" | |
| content = r.content | |
| ct = r.headers.get("content-type", "").lower() | |
| fn = file_name.lower() | |
| if "audio" in ct or fn.endswith((".mp3", ".wav", ".flac")): | |
| print(f" π΅ Transcribing audio...") | |
| return f"[AUDIO TRANSCRIPT]\n{transcribe_audio(content)}" | |
| elif "pdf" in ct or fn.endswith(".pdf"): | |
| print(f" π Extracting PDF...") | |
| return f"[PDF CONTENT]\n{extract_pdf(content)}" | |
| elif "image" in ct or fn.endswith((".jpg", ".jpeg", ".png", ".webp", ".gif")): | |
| print(f" πΌοΈ Describing image...") | |
| caption = describe_image_blip(content) | |
| # Also ask Groq to describe it via base64 if supported | |
| b64 = base64.b64encode(content).decode() | |
| groq_desc = call_groq([{ | |
| "role": "user", | |
| "content": f"This is a base64 image. The image captioning model says: '{caption}'. " | |
| f"Given the question '{question}', what specific details in the image are relevant? " | |
| f"List every visible element, number, text, and object." | |
| }], max_tokens=300) | |
| return f"[IMAGE]\nCaption: {caption}\nAnalysis: {groq_desc}" | |
| else: | |
| return f"[FILE CONTENT]\n{content.decode('utf-8', errors='replace')[:5000]}" | |
| except Exception as e: | |
| print(f" File error: {e}") | |
| return "" | |
| # βββ CORE SOLVER ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SYSTEM_PROMPT = """You are an expert research assistant solving GAIA benchmark questions. | |
| You have access to web search results and file contents. | |
| CRITICAL RULES: | |
| 1. Reply with ONLY the final answer β no explanation, no prefix, no "The answer is" | |
| 2. Use EXACTLY the format requested in the question | |
| 3. For comma-separated lists: word1, word2, word3 | |
| 4. For numbers: digits only (no units unless asked) | |
| 5. For yes/no: Yes or No | |
| 6. Base your answer STRICTLY on the provided context β do NOT invent facts | |
| 7. If the context clearly contains the answer, extract it precisely | |
| 8. Think carefully before answering""" | |
| def build_search_query(question: str) -> str: | |
| """Ask Groq to write the best search query.""" | |
| resp = call_groq([{ | |
| "role": "user", | |
| "content": f"Write a 5-7 word web search query to find the answer to:\n{question}\n\nReturn ONLY the search query:" | |
| }], max_tokens=40) | |
| resp = re.sub(r'["\'\n]', '', resp).strip() | |
| if not resp or len(resp) > 150: | |
| # Fallback: first 7 meaningful words | |
| words = re.sub(r'[^\w\s]', '', question).split()[:7] | |
| return " ".join(words) | |
| return resp | |
| def validate_and_clean(answer: str, question: str) -> str: | |
| """Ask Groq to validate the answer format matches what the question asks for.""" | |
| if not answer: | |
| return "UNKNOWN" | |
| # Strip prefixes | |
| prefixes = [ | |
| "FINAL ANSWER:", "Final Answer:", "final answer:", | |
| "Answer:", "answer:", "The answer is", "The answer is:", | |
| "Based on the context,", "Based on the search results,", | |
| "According to", "The result is", "It is", "That is", | |
| "Here is", "Here's", "I believe", "I think", | |
| ] | |
| for pfx in prefixes: | |
| if answer.lower().startswith(pfx.lower()): | |
| answer = answer[len(pfx):].strip(" :") | |
| break | |
| # Remove surrounding quotes | |
| if len(answer) > 2 and answer[0] == answer[-1] and answer[0] in ('"', "'"): | |
| answer = answer[1:-1] | |
| # Take only first meaningful line (avoid leaked reasoning) | |
| lines = [l.strip() for l in answer.strip().splitlines() if l.strip()] | |
| if len(lines) > 1: | |
| # Find the last line that doesn't look like reasoning | |
| reasoning_starters = ("step", "first", "then", "so ", "therefore", | |
| "thus", "note", "let", "we ", "i ", "the question") | |
| for line in reversed(lines): | |
| if not any(line.lower().startswith(w) for w in reasoning_starters): | |
| answer = line | |
| break | |
| # Clean trailing period | |
| if answer.endswith(".") and not re.search(r'\b[A-Z]{1,3}\.$', answer): | |
| answer = answer[:-1] | |
| # Normalize list format | |
| if "comma-separated" in question.lower() or "list" in question.lower(): | |
| answer = re.sub(r'\s+and\s+(?=[^,])', ', ', answer) | |
| answer = answer.strip(", ") | |
| return answer.strip() | |
| def solve_question(task_id: str, question: str, file_info: Optional[dict]) -> str: | |
| print(f"\n{'='*60}") | |
| print(f"Q: {question[:130]}") | |
| context_parts = [] | |
| # ββ Step 1: Load file βββββββββββββββββββββββββββββββββββββ | |
| if file_info and file_info.get("file_name"): | |
| file_ctx = load_task_file(task_id, file_info["file_name"], question) | |
| if file_ctx: | |
| context_parts.append(file_ctx) | |
| print(f" π File: {len(file_ctx)} chars") | |
| # ββ Step 2: Multi-pass web search βββββββββββββββββββββββββ | |
| query1 = build_search_query(question) | |
| print(f" π Search 1: '{query1}'") | |
| search1 = smart_search(question, query1) | |
| if search1: | |
| context_parts.append(f"[SEARCH: '{query1}']\n{search1}") | |
| # Second search with different angle if first is weak | |
| if not search1 or len(search1) < 200: | |
| # Try direct Wikipedia with key entities | |
| query2 = re.sub(r'\b(what|who|when|where|which|how|the|a|an|is|are|was|were)\b', '', | |
| question.lower(), flags=re.I).strip() | |
| query2 = " ".join(query2.split()[:6]) | |
| if query2 and query2 != query1: | |
| print(f" π Search 2 (fallback): '{query2}'") | |
| search2 = smart_search(question, query2) | |
| if search2: | |
| context_parts.append(f"[SEARCH 2: '{query2}']\n{search2}") | |
| # ββ Step 3: Build prompt ββββββββββββββββββββββββββββββββββ | |
| context = "\n\n" + "="*40 + "\n\n".join(context_parts) if context_parts else "No context available." | |
| # Detect expected format | |
| q_low = question.lower() | |
| fmt = "" | |
| if "comma-separated" in q_low: | |
| fmt = "\nβ οΈ FORMAT: comma-separated list only (e.g. apple, banana, cherry)" | |
| elif "how many" in q_low or "how much" in q_low or "what number" in q_low: | |
| fmt = "\nβ οΈ FORMAT: single number only" | |
| elif "what year" in q_low: | |
| fmt = "\nβ οΈ FORMAT: 4-digit year only" | |
| elif q_low.strip()[:3] in ("is ", "are", "was", "did", "doe"): | |
| fmt = "\nβ οΈ FORMAT: Yes or No only" | |
| elif "first name" in q_low: | |
| fmt = "\nβ οΈ FORMAT: first name only" | |
| elif "last name" in q_low or "surname" in q_low: | |
| fmt = "\nβ οΈ FORMAT: last name only" | |
| prompt = f"""QUESTION: {question} | |
| {fmt} | |
| CONTEXT: | |
| {context} | |
| Using only the context above, give the precise answer. Think step by step, then output ONLY the final answer:""" | |
| raw = call_groq([{"role": "user", "content": prompt}], max_tokens=400, system=SYSTEM_PROMPT) | |
| answer = validate_and_clean(raw, question) | |
| # ββ Step 4: If still empty, try once more with simpler prompt | |
| if not answer or answer == "UNKNOWN": | |
| simple = call_groq([{ | |
| "role": "user", | |
| "content": f"Based on this context, answer the question with ONLY the answer (no explanation):\n\nContext: {context[:2000]}\n\nQuestion: {question}\n\nAnswer:" | |
| }], max_tokens=100) | |
| answer = validate_and_clean(simple, question) | |
| print(f" β '{answer}'") | |
| return answer if answer else "UNKNOWN" | |
| # βββ GRADIO βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class GaiaAgent: | |
| def test_single(self) -> str: | |
| try: | |
| q = requests.get(f"{API_BASE}/random-question", timeout=15).json() | |
| has_file = bool(q.get("file_name")) | |
| answer = solve_question(q["task_id"], q["question"], q if has_file else None) | |
| return ( | |
| f"π TEST UNE QUESTION\n\n" | |
| f"Task ID : {q['task_id']}\n" | |
| f"Question: {q['question']}\n" | |
| f"Fichier : {q.get('file_name', 'Aucun')}\n\n" | |
| f"β RΓ©ponse : '{answer}'" | |
| ) | |
| except Exception as e: | |
| return f"β Erreur: {e}" | |
| def run_evaluation(self, progress=gr.Progress()) -> str: | |
| if not HF_API_KEY or not HF_USERNAME: | |
| return "β ERREUR: Ajoute HF_TOKEN et HF_USERNAME dans Secrets" | |
| if not GROQ_KEY: | |
| return "β ERREUR: Ajoute GROQ_API_KEY dans Secrets (gratuit sur console.groq.com)" | |
| progress(0.02, desc="π₯ Chargement des questions...") | |
| try: | |
| questions = requests.get(f"{API_BASE}/questions", timeout=30).json() | |
| except Exception as e: | |
| return f"β Impossible de charger les questions: {e}" | |
| total = len(questions) | |
| print(f"β {total} questions chargΓ©es") | |
| answers = [] | |
| log_lines = [f"β {total} questions chargΓ©es\n"] | |
| for idx, q in enumerate(questions): | |
| task_id = q["task_id"] | |
| has_file = bool(q.get("file_name")) | |
| progress((idx + 1) / total, desc=f"Q{idx+1}/{total}...") | |
| try: | |
| answer = solve_question(task_id, q["question"], q if has_file else None) | |
| answers.append({"task_id": task_id, "submitted_answer": answer}) | |
| log_lines.append(f"Q{idx+1}: '{answer[:70]}'") | |
| except Exception as e: | |
| print(f"Erreur Q{idx+1}: {e}") | |
| answers.append({"task_id": task_id, "submitted_answer": "UNKNOWN"}) | |
| log_lines.append(f"Q{idx+1}: β ERREUR β {str(e)[:60]}") | |
| time.sleep(1) | |
| progress(0.95, desc="π€ Soumission...") | |
| code_link = f"https://huggingface.co/spaces/{HF_USERNAME}/gaia-agent/tree/main" | |
| try: | |
| resp = requests.post( | |
| f"{API_BASE}/submit", | |
| json={"username": HF_USERNAME, "agent_code": code_link, "answers": answers}, | |
| timeout=60, | |
| ) | |
| if resp.status_code == 200: | |
| result = resp.json() | |
| score = result.get("score", 0) | |
| correct = result.get("correct_count", score) | |
| pct = (correct / total) * 100 if total else 0 | |
| passed = "π CERTIFICAT OBTENU!" if pct >= 30 else "β οΈ Besoin de 30%+ pour valider" | |
| return ( | |
| f"β Γvaluation terminΓ©e!\n\n" | |
| f"π Score: {correct}/{total} ({pct:.1f}%) β {passed}\n\n" | |
| f"π Code: {code_link}\n" | |
| f"π Leaderboard: https://huggingface.co/spaces/agents-course/Students_leaderboard\n\n" | |
| f"--- DΓ©tail ---\n" + "\n".join(log_lines) | |
| ) | |
| else: | |
| return f"β Erreur soumission ({resp.status_code}): {resp.text}\n\n" + "\n".join(log_lines) | |
| except Exception as e: | |
| return f"β Soumission Γ©chouΓ©e: {e}\n\n" + "\n".join(log_lines) | |
| def create_interface(): | |
| agent = GaiaAgent() | |
| status = [ | |
| f"{'β ' if GROQ_KEY else 'β'} GROQ_API_KEY β requis (gratuit sur console.groq.com)", | |
| f"{'β ' if TAVILY_KEY else 'β οΈ'} TAVILY_API_KEY β fortement recommandΓ© (gratuit sur app.tavily.com)", | |
| f"{'β ' if HF_API_KEY else 'β'} HF_TOKEN", | |
| f"{'β ' if HF_USERNAME else 'β'} HF_USERNAME", | |
| ] | |
| with gr.Blocks(title="GAIA Agent", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(""" | |
| # π€ GAIA Benchmark Agent | |
| | Secret | OΓΉ l'obtenir | Requis? | | |
| |--------|-------------|---------| | |
| | `HF_TOKEN` | hf.co/settings/tokens | β | | |
| | `HF_USERNAME` | Ton pseudo HF | β | | |
| | `GROQ_API_KEY` | [console.groq.com](https://console.groq.com) | β | | |
| | `TAVILY_API_KEY` | [app.tavily.com](https://app.tavily.com) | β RecommandΓ© | | |
| """) | |
| gr.Markdown("### Statut\n" + "\n".join(status)) | |
| with gr.Row(): | |
| run_btn = gr.Button("π Γvaluation complΓ¨te", variant="primary", scale=2) | |
| test_btn = gr.Button("π Tester une question", variant="secondary") | |
| output = gr.Textbox(label="RΓ©sultats", lines=25, show_copy_button=True) | |
| run_btn.click(fn=agent.run_evaluation, outputs=output, show_progress=True) | |
| test_btn.click(fn=agent.test_single, outputs=output) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = create_interface() | |
| demo.launch() |