elhassaneT's picture
Update app.py
ccf468e verified
Raw History Blame Contribute Delete
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()