Mouhamedamar commited on
Commit
01f3f18
Β·
verified Β·
1 Parent(s): 2c4de7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +461 -426
app.py CHANGED
@@ -1,520 +1,555 @@
1
  import os
2
  import re
3
- import base64
4
- import gradio as gr
5
  import requests
 
 
6
  import pandas as pd
7
- from concurrent.futures import ThreadPoolExecutor, as_completed
8
 
9
- # --- Constants ---
 
 
10
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- # ─────────────────────────────────────────────────────────────
13
- # LLM DIRECT β€” appel HTTP simple, pas de smolagents
14
- # ─────────────────────────────────────────────────────────────
15
-
16
- def call_llm(messages: list, max_tokens: int = 512) -> str:
17
- """Appel direct Γ  Qwen2.5-72B via HF Inference API."""
18
- hf_token = os.getenv("HF_TOKEN", "")
19
- models = [
20
- "Qwen/Qwen2.5-72B-Instruct",
21
- "Qwen/Qwen2.5-32B-Instruct",
22
- "meta-llama/Llama-3.3-70B-Instruct",
23
- "mistralai/Mistral-7B-Instruct-v0.3",
24
- ]
25
- for model_id in models:
 
 
26
  try:
27
- resp = requests.post(
28
- f"https://api-inference.huggingface.co/models/{model_id}/v1/chat/completions",
29
- headers={"Authorization": f"Bearer {hf_token}", "Content-Type": "application/json"},
30
- json={"model": model_id, "messages": messages, "max_tokens": max_tokens, "temperature": 0.1},
31
- timeout=60,
32
- )
33
- resp.raise_for_status()
34
- content = resp.json()["choices"][0]["message"]["content"].strip()
35
- print(f" LLM ({model_id[:30]}): {content[:80]}")
36
- return content
37
  except Exception as e:
38
- print(f" LLM {model_id[:30]} failed: {e}")
39
- continue
40
- return "ERROR: all models failed"
41
-
42
-
43
- def call_vision(image_bytes: bytes, content_type: str, question: str) -> str:
44
- """Appel Qwen2.5-VL-7B pour analyser une image."""
45
- hf_token = os.getenv("HF_TOKEN", "")
46
- ext = content_type.split("/")[-1].split(";")[0].strip()
47
- if ext == "jpg":
48
- ext = "jpeg"
49
- media_type = f"image/{ext}"
50
- b64 = base64.b64encode(image_bytes).decode("utf-8")
51
- vision_models = ["Qwen/Qwen2.5-VL-7B-Instruct", "Qwen/Qwen2-VL-7B-Instruct"]
52
- for model_id in vision_models:
 
 
 
 
 
 
 
53
  try:
54
- resp = requests.post(
55
- f"https://api-inference.huggingface.co/models/{model_id}/v1/chat/completions",
56
- headers={"Authorization": f"Bearer {hf_token}"},
57
- json={
58
- "model": model_id,
59
- "messages": [{
60
- "role": "user",
61
- "content": [
62
- {"type": "image_url", "image_url": {"url": f"data:{media_type};base64,{b64}"}},
63
- {"type": "text", "text": f"{question}\nGive ONLY the exact answer, nothing else."}
64
- ]
65
- }],
66
- "max_tokens": 200,
67
- "temperature": 0.1,
68
- },
69
- timeout=60,
70
- )
71
- resp.raise_for_status()
72
- answer = resp.json()["choices"][0]["message"]["content"].strip()
73
- print(f" Vision ({model_id[:30]}): {answer[:80]}")
74
- return answer
75
  except Exception as e:
76
- print(f" Vision {model_id[:30]} failed: {e}")
77
- continue
78
- return "Unable to analyze image"
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- # ─────────────────────────────────────────────────────────────
82
- # TOOLS β€” fonctions Python pures
83
- # ─────────────────────────────────────────────────────────────
84
 
85
- def tool_search(query: str) -> str:
86
- """Recherche DuckDuckGo."""
 
 
 
 
87
  try:
88
- headers = {"User-Agent": "Mozilla/5.0"}
89
- resp = requests.get(
90
- "https://api.duckduckgo.com/",
91
- params={"q": query, "format": "json", "no_html": 1, "skip_disambig": 1},
92
- headers=headers, timeout=10,
93
- )
94
- data = resp.json()
95
  results = []
96
  if data.get("AbstractText"):
97
  results.append(data["AbstractText"])
98
- for t in data.get("RelatedTopics", [])[:5]:
99
- if isinstance(t, dict) and t.get("Text"):
100
- results.append(t["Text"])
101
- if results:
102
- return "\n".join(results[:5])
103
- # Fallback lite
104
- resp2 = requests.get(
105
- "https://lite.duckduckgo.com/lite/",
106
- params={"q": query}, headers=headers, timeout=10,
107
- )
108
- clean = re.sub(r"<[^>]+>", " ", resp2.text)
109
- clean = re.sub(r"\s+", " ", clean).strip()
110
- return clean[:2000]
111
  except Exception as e:
112
  return f"Search error: {e}"
113
 
114
 
115
- def tool_download_file(task_id: str) -> tuple:
116
- """TΓ©lΓ©charge le fichier associΓ© Γ  un task_id. Retourne (bytes, content_type)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  try:
118
- url = f"{DEFAULT_API_URL}/files/{task_id}"
119
- resp = requests.get(url, timeout=30)
120
- if resp.status_code != 200:
121
- return None, f"HTTP {resp.status_code}"
122
- return resp.content, resp.headers.get("Content-Type", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  except Exception as e:
124
- return None, str(e)
 
125
 
 
 
 
126
 
127
- def tool_parse_file(content: bytes, content_type: str) -> str:
128
- """Parse le contenu d'un fichier en texte."""
129
  try:
130
- if any(t in content_type for t in ["text", "json", "csv"]):
131
- return content.decode("utf-8", errors="replace")[:6000]
132
- if "excel" in content_type or "spreadsheet" in content_type or "openxmlformats" in content_type:
133
- import io, openpyxl
134
- wb = openpyxl.load_workbook(io.BytesIO(content))
135
- out = []
136
- for sname in wb.sheetnames:
137
- ws = wb[sname]
138
- out.append(f"=== Sheet: {sname} ===")
139
- for row in ws.iter_rows(values_only=True):
140
- out.append("\t".join("" if c is None else str(c) for c in row))
141
- return "\n".join(out)[:8000]
142
- if "pdf" in content_type:
143
- import io, pypdf
144
- reader = pypdf.PdfReader(io.BytesIO(content))
145
- return "\n".join(p.extract_text() or "" for p in reader.pages[:15])[:6000]
146
- return content.decode("utf-8", errors="replace")[:4000]
147
  except Exception as e:
148
- return f"Parse error: {e}"
 
149
 
150
 
151
- def tool_python(code: str) -> str:
152
- """ExΓ©cute du code Python."""
153
- import io, sys, traceback
154
- buf = io.StringIO()
155
- old = sys.stdout; sys.stdout = buf
156
  try:
157
- exec(code, {"__builtins__": __builtins__, "pd": pd, "re": re})
158
- return buf.getvalue() or "(no output)"
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  except Exception:
160
- return traceback.format_exc()
161
- finally:
162
- sys.stdout = old
 
 
 
 
163
 
 
 
 
164
 
165
- # ─────────────────────────────────────────────────────────────
166
- # AGENT β€” boucle ReAct simple et robuste
167
- # ─────────────────────────────────────────────────────────────
168
 
169
- SYSTEM_PROMPT = """You are a precise AI assistant solving GAIA benchmark questions.
 
 
170
 
171
- You can use tools by writing exactly:
172
- SEARCH: <your search query>
173
- CODE: <python code using print() to show results>
174
- FILE: already provided in context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  RULES:
177
  1. Think step by step before answering.
178
- 2. Use SEARCH for Wikipedia facts, historical data, people, places.
179
- 3. Use CODE for calculations, data analysis, string manipulation.
180
- 4. Final answer must be on its own line starting with: ANSWER: <value>
181
- 5. Answer must be SHORT and EXACT: a number, name, short phrase, or comma-separated list.
182
- 6. NO explanation after ANSWER:
183
- 7. For reversed text like ".rewsna" β†’ decode it first then answer.
184
- 8. For botanical vegetables β†’ exclude bell pepper, corn, zucchini, green beans (they are botanical fruits).
185
- 9. Never write "I don't know" β€” always give best answer.
186
-
187
- Example:
188
- Question: What is the capital of France?
189
- ANSWER: Paris
190
-
191
- Question: How many albums did X release between 2000-2009?
192
- SEARCH: X discography Wikipedia
193
- [results show 3 albums]
194
- ANSWER: 3
195
  """
196
 
197
 
198
- def run_react_agent(question: str, task_id: str = None, max_steps: int = 5) -> str:
199
- """Boucle ReAct : pense β†’ agit β†’ observe β†’ rΓ©pond."""
200
- print(f"\n{'='*50}\nQ: {question[:80]}")
201
-
202
- # Contexte initial
203
- file_text = ""
204
- file_bytes = None
205
- file_ctype = ""
206
- is_image = False
207
-
208
- # TΓ©lΓ©charger le fichier si task_id disponible
209
- if task_id:
210
- file_bytes, file_ctype = tool_download_file(task_id)
211
- if file_bytes:
212
- if any(t in file_ctype for t in ["image", "png", "jpg", "jpeg", "gif", "webp"]):
213
- is_image = True
214
- print(f" β†’ Image file detected")
215
- else:
216
- file_text = tool_parse_file(file_bytes, file_ctype)
217
- print(f" β†’ File parsed ({len(file_text)} chars)")
218
-
219
- # Route directe pour les images
220
- if is_image and file_bytes:
221
- answer = call_vision(file_bytes, file_ctype, question)
222
- return clean_answer(answer)
223
-
224
- # Construction du contexte
225
- context = f"Question: {question}\n"
226
- if file_text:
227
- context += f"\n[ATTACHED FILE CONTENT]:\n{file_text[:4000]}\n"
228
-
229
- messages = [
230
- {"role": "system", "content": SYSTEM_PROMPT},
231
- {"role": "user", "content": context + "\nThink step by step, use tools if needed, then give ANSWER:"},
232
- ]
233
-
234
- for step in range(max_steps):
235
- response = call_llm(messages, max_tokens=600)
236
-
237
- # Chercher ANSWER: dans la rΓ©ponse
238
- answer_match = re.search(r"ANSWER:\s*(.+?)(?:\n|$)", response, re.IGNORECASE)
239
-
240
- # Chercher des tool calls
241
- search_match = re.search(r"SEARCH:\s*(.+?)(?:\n|$)", response, re.IGNORECASE)
242
- code_match = re.search(r"CODE:\s*```?python?\n?(.*?)```?(?:\n|$)", response, re.IGNORECASE | re.DOTALL)
243
- if not code_match:
244
- code_match = re.search(r"CODE:\s*(.+?)(?=SEARCH:|ANSWER:|$)", response, re.IGNORECASE | re.DOTALL)
245
-
246
- # Si on a une rΓ©ponse finale ET pas de tool calls β†’ on s'arrΓͺte
247
- if answer_match and not search_match and not code_match:
248
- return clean_answer(answer_match.group(1).strip())
249
-
250
- # ExΓ©cuter les tools
251
- tool_results = []
252
-
253
- if search_match:
254
- query = search_match.group(1).strip()
255
- print(f" SEARCH: {query[:60]}")
256
- result = tool_search(query)
257
- tool_results.append(f"[Search '{query}']:\n{result[:1500]}")
258
-
259
- if code_match:
260
- code = code_match.group(1).strip()
261
- print(f" CODE: {code[:60]}")
262
- result = tool_python(code)
263
- tool_results.append(f"[Code output]:\n{result[:1000]}")
264
-
265
- if tool_results:
266
- # Ajouter les rΓ©sultats et relancer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  messages.append({"role": "assistant", "content": response})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  messages.append({
269
  "role": "user",
270
- "content": "\n\n".join(tool_results) + "\n\nBased on these results, give your final ANSWER: (short, exact, nothing else after it)"
271
  })
272
- elif answer_match:
273
- # On a une rΓ©ponse mΓͺme avec des tool calls β†’ l'utiliser
274
- return clean_answer(answer_match.group(1).strip())
275
- else:
276
- # Pas de tool calls, pas de rΓ©ponse β†’ forcer
277
- messages.append({"role": "assistant", "content": response})
278
- messages.append({"role": "user", "content": "Give your final ANSWER: now. Just the answer, nothing else."})
279
-
280
- # Dernier essai
281
- final = call_llm(messages, max_tokens=100)
282
- m = re.search(r"ANSWER:\s*(.+?)(?:\n|$)", final, re.IGNORECASE)
283
- if m:
284
- return clean_answer(m.group(1).strip())
285
-
286
- # RΓ©ponse directe sans structure
287
- direct = call_llm([
288
- {"role": "system", "content": "Answer with ONLY the exact answer. No explanation. No preamble."},
289
- {"role": "user", "content": f"Question: {question}\nAnswer:"}
290
- ], max_tokens=150)
291
- return clean_answer(direct)
292
-
293
-
294
- def clean_answer(answer: str) -> str:
295
- """Nettoie les prΓ©fixes verbeux."""
296
- answer = answer.strip()
297
- prefixes = [
298
- "final answer:", "answer:", "the answer is", "result:",
299
- "the final answer is", "based on", "according to",
300
- "therefore,", "thus,", "so the answer is",
301
- ]
302
- low = answer.lower()
303
- for p in prefixes:
304
- if low.startswith(p):
305
- answer = answer[len(p):].strip()
306
- low = answer.lower()
307
- # Si trop long, prendre dernière ligne courte
308
- if len(answer) > 250:
309
- lines = [l.strip() for l in answer.splitlines() if l.strip()]
310
- for line in reversed(lines):
311
- if 0 < len(line) < 200:
312
- return line
313
- return answer
314
-
315
-
316
- # ─────────────────────────────────────────────────────────────
317
- # CLASSE AGENT (interface compatible avec le template)
318
- # ─────────────────────────────────────────────────────────────
319
 
320
- class GAIAAgent:
321
- def __init__(self):
322
- hf_token = os.getenv("HF_TOKEN", "")
323
- if not hf_token:
324
- raise ValueError("HF_TOKEN not set! Add it in Space Settings β†’ Secrets.")
325
- print("GAIAAgent initialized (direct HTTP ReAct loop, no smolagents).")
326
 
327
- def __call__(self, question: str) -> str:
328
- # Extraire task_id
329
- m = re.match(r"\[task_id=([^\]]+)\]\s*", question)
330
- task_id = m.group(1) if m else None
331
- clean_q = re.sub(r"^\[task_id=[^\]]+\]\s*", "", question).strip()
332
- return run_react_agent(clean_q, task_id=task_id, max_steps=5)
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
- # ─────────────────────────────────────────────────────────────
336
- # run_and_submit_all
337
- # ─────────────────────────────────────────────────────────────
 
338
 
339
  def run_and_submit_all(profile: gr.OAuthProfile | None):
340
- """
341
- Fetches all questions, runs the GAIAAgent on them, submits all answers,
342
- and displays the results.
343
- """
344
  space_id = os.getenv("SPACE_ID")
345
 
346
  if profile:
347
- username = f"{profile.username}"
348
  print(f"User logged in: {username}")
349
  else:
350
- print("User not logged in.")
351
- return "Please Login to Hugging Face with the button.", None
352
 
353
  api_url = DEFAULT_API_URL
354
  questions_url = f"{api_url}/questions"
355
- submit_url = f"{api_url}/submit"
356
 
357
- # 1. Instantiate Agent
358
  try:
359
- agent = GAIAAgent()
360
  except Exception as e:
361
- print(f"Error instantiating agent: {e}")
362
  return f"Error initializing agent: {e}", None
363
 
364
- agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
365
- print(agent_code)
366
 
367
- # 2. Fetch Questions
368
- print(f"Fetching questions from: {questions_url}")
369
  try:
370
- response = requests.get(questions_url, timeout=15)
371
- response.raise_for_status()
372
- questions_data = response.json()
373
  if not questions_data:
374
- print("Fetched questions list is empty.")
375
- return "Fetched questions list is empty or invalid format.", None
376
  print(f"Fetched {len(questions_data)} questions.")
377
- except requests.exceptions.RequestException as e:
378
- print(f"Error fetching questions: {e}")
379
- return f"Error fetching questions: {e}", None
380
- except requests.exceptions.JSONDecodeError as e:
381
- print(f"Error decoding JSON response from questions endpoint: {e}")
382
- print(f"Response text: {response.text[:500]}")
383
- return f"Error decoding server response for questions: {e}", None
384
  except Exception as e:
385
- print(f"An unexpected error occurred fetching questions: {e}")
386
- return f"An unexpected error occurred fetching questions: {e}", None
387
 
388
- # 3. Run Agent β€” PARALLÈLE avec timeout par question
389
- results_log = []
390
  answers_payload = []
391
- print(f"Running agent on {len(questions_data)} questions (4 threads)...")
392
 
393
- def process_one(item):
394
- task_id = item.get("task_id")
395
  question_text = item.get("question")
396
  if not task_id or question_text is None:
397
- return None
398
  try:
399
- question_with_id = f"[task_id={task_id}] {question_text}"
400
- submitted_answer = agent(question_with_id)
401
- print(f"βœ… [{task_id[:8]}] {submitted_answer[:60]}")
402
- return {"task_id": task_id, "question_text": question_text, "answer": submitted_answer}
403
  except Exception as e:
404
- print(f"❌ [{task_id[:8]}] {e}")
405
- return {"task_id": task_id, "question_text": question_text, "answer": "I don't know"}
406
-
407
- with ThreadPoolExecutor(max_workers=4) as executor:
408
- futures = {executor.submit(process_one, item): item for item in questions_data}
409
- for future in as_completed(futures):
410
- try:
411
- result = future.result(timeout=120)
412
- except Exception as e:
413
- print(f"Future error: {e}")
414
- continue
415
- if result is None:
416
- continue
417
- answers_payload.append({"task_id": result["task_id"], "submitted_answer": result["answer"]})
418
- results_log.append({"Task ID": result["task_id"], "Question": result["question_text"], "Submitted Answer": result["answer"]})
419
 
420
  if not answers_payload:
421
- print("Agent did not produce any answers to submit.")
422
- return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
423
-
424
- # 4. Prepare Submission
425
- submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
426
- status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
427
- print(status_update)
428
-
429
- # 5. Submit
430
- print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
431
  try:
432
- response = requests.post(submit_url, json=submission_data, timeout=60)
433
- response.raise_for_status()
434
- result_data = response.json()
435
- final_status = (
436
  f"Submission Successful!\n"
437
- f"User: {result_data.get('username')}\n"
438
- f"Overall Score: {result_data.get('score', 'N/A')}% "
439
- f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
440
- f"Message: {result_data.get('message', 'No message received.')}"
441
  )
442
- print("Submission successful.")
443
- results_df = pd.DataFrame(results_log)
444
- return final_status, results_df
445
- except requests.exceptions.HTTPError as e:
446
- error_detail = f"Server responded with status {e.response.status_code}."
447
- try:
448
- error_json = e.response.json()
449
- error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
450
- except requests.exceptions.JSONDecodeError:
451
- error_detail += f" Response: {e.response.text[:500]}"
452
- status_message = f"Submission Failed: {error_detail}"
453
- print(status_message)
454
- results_df = pd.DataFrame(results_log)
455
- return status_message, results_df
456
- except requests.exceptions.Timeout:
457
- status_message = "Submission Failed: The request timed out."
458
- print(status_message)
459
- results_df = pd.DataFrame(results_log)
460
- return status_message, results_df
461
- except requests.exceptions.RequestException as e:
462
- status_message = f"Submission Failed: Network error - {e}"
463
- print(status_message)
464
- results_df = pd.DataFrame(results_log)
465
- return status_message, results_df
466
  except Exception as e:
467
- status_message = f"An unexpected error occurred during submission: {e}"
468
- print(status_message)
469
- results_df = pd.DataFrame(results_log)
470
- return status_message, results_df
471
 
 
 
 
 
 
 
472
 
473
- # --- Build Gradio Interface using Blocks ---
474
  with gr.Blocks() as demo:
475
- gr.Markdown("# Basic Agent Evaluation Runner")
476
- gr.Markdown(
477
- """
478
- **Instructions:**
479
-
480
- 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
481
- 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
482
- 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
483
-
484
- ---
485
- **Disclaimers:**
486
- 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).
487
- 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.
488
- """
489
- )
490
 
491
  gr.LoginButton()
492
 
493
- run_button = gr.Button("Run Evaluation & Submit All Answers")
 
 
494
 
495
- status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
496
- results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
497
 
498
- run_button.click(
499
- fn=run_and_submit_all,
500
- outputs=[status_output, results_table]
501
- )
502
 
503
  if __name__ == "__main__":
504
- print("\n" + "-"*30 + " App Starting " + "-"*30)
505
- space_host_startup = os.getenv("SPACE_HOST")
506
- space_id_startup = os.getenv("SPACE_ID")
507
- if space_host_startup:
508
- print(f"βœ… SPACE_HOST found: {space_host_startup}")
509
- print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
510
- else:
511
- print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
512
- if space_id_startup:
513
- print(f"βœ… SPACE_ID found: {space_id_startup}")
514
- print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
515
- print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
516
- else:
517
- print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
518
- print("-"*(60 + len(" App Starting ")) + "\n")
519
- print("Launching Gradio Interface for Basic Agent Evaluation...")
520
  demo.launch(debug=True, share=False)
 
1
  import os
2
  import re
3
+ import json
4
+ import time
5
  import requests
6
+ import tempfile
7
+ import gradio as gr
8
  import pandas as pd
9
+ from pathlib import Path
10
 
11
+ # ─────────────────────────────────────────────
12
+ # Constants
13
+ # ─────────────────────────────────────────────
14
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
15
+ HF_API_URL = "https://api-inference.huggingface.co/models"
16
+
17
+ # ─────────────────────────────────────────────
18
+ # HF Inference helpers
19
+ # ─────────────────────────────────────────────
20
+
21
+ def hf_token() -> str:
22
+ tok = os.getenv("HF_TOKEN", "")
23
+ if not tok:
24
+ raise EnvironmentError("HF_TOKEN environment variable is not set.")
25
+ return tok
26
+
27
+ def hf_headers() -> dict:
28
+ return {"Authorization": f"Bearer {hf_token()}"}
29
 
30
+
31
+ def chat_llm(messages: list[dict], model: str = "meta-llama/Llama-3.3-70B-Instruct",
32
+ max_tokens: int = 1024, temperature: float = 0.1) -> str:
33
+ """
34
+ Chat completion via HF Inference API (serverless).
35
+ messages = [{"role": "user"|"assistant"|"system", "content": "..."}]
36
+ """
37
+ url = f"https://api-inference.huggingface.co/models/{model}/v1/chat/completions"
38
+ payload = {
39
+ "model": model,
40
+ "messages": messages,
41
+ "max_tokens": max_tokens,
42
+ "temperature": temperature,
43
+ "stream": False,
44
+ }
45
+ for attempt in range(3):
46
  try:
47
+ r = requests.post(url, headers=hf_headers(), json=payload, timeout=120)
48
+ if r.status_code == 503: # model loading
49
+ wait = int(r.headers.get("X-Wait-Time", 20))
50
+ print(f"Model loading, waiting {wait}s …")
51
+ time.sleep(wait)
52
+ continue
53
+ r.raise_for_status()
54
+ data = r.json()
55
+ return data["choices"][0]["message"]["content"].strip()
 
56
  except Exception as e:
57
+ print(f"chat_llm attempt {attempt+1} error: {e}")
58
+ time.sleep(5)
59
+ return ""
60
+
61
+
62
+ def vision_llm(image_bytes: bytes, prompt: str,
63
+ model: str = "meta-llama/Llama-3.2-11B-Vision-Instruct") -> str:
64
+ """
65
+ Vision (image β†’ text) via HF Inference API.
66
+ """
67
+ import base64
68
+ b64 = base64.b64encode(image_bytes).decode()
69
+ url = f"https://api-inference.huggingface.co/models/{model}/v1/chat/completions"
70
+ messages = [{
71
+ "role": "user",
72
+ "content": [
73
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
74
+ {"type": "text", "text": prompt},
75
+ ]
76
+ }]
77
+ payload = {"model": model, "messages": messages, "max_tokens": 512}
78
+ for attempt in range(3):
79
  try:
80
+ r = requests.post(url, headers=hf_headers(), json=payload, timeout=120)
81
+ if r.status_code == 503:
82
+ time.sleep(int(r.headers.get("X-Wait-Time", 20)))
83
+ continue
84
+ r.raise_for_status()
85
+ return r.json()["choices"][0]["message"]["content"].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  except Exception as e:
87
+ print(f"vision_llm attempt {attempt+1}: {e}")
88
+ time.sleep(5)
89
+ return ""
90
+
91
 
92
+ def transcribe_audio(audio_bytes: bytes,
93
+ model: str = "openai/whisper-large-v3") -> str:
94
+ """Transcribe audio bytes via HF Inference API."""
95
+ url = f"{HF_API_URL}/{model}"
96
+ for attempt in range(3):
97
+ try:
98
+ r = requests.post(url, headers=hf_headers(), data=audio_bytes, timeout=120)
99
+ if r.status_code == 503:
100
+ time.sleep(int(r.headers.get("X-Wait-Time", 20)))
101
+ continue
102
+ r.raise_for_status()
103
+ return r.json().get("text", "")
104
+ except Exception as e:
105
+ print(f"transcribe attempt {attempt+1}: {e}")
106
+ time.sleep(5)
107
+ return ""
108
 
 
 
 
109
 
110
+ # ─────────────────────────────────────────────
111
+ # Tool: Web search (DuckDuckGo – no key needed)
112
+ # ─────────────────────────────────────────────
113
+
114
+ def web_search(query: str, max_results: int = 5) -> str:
115
+ """Search the web via DuckDuckGo Instant Answer API."""
116
  try:
117
+ url = "https://api.duckduckgo.com/"
118
+ params = {"q": query, "format": "json", "no_html": 1, "skip_disambig": 1}
119
+ r = requests.get(url, params=params, timeout=15)
120
+ r.raise_for_status()
121
+ data = r.json()
 
 
122
  results = []
123
  if data.get("AbstractText"):
124
  results.append(data["AbstractText"])
125
+ for topic in data.get("RelatedTopics", [])[:max_results]:
126
+ if isinstance(topic, dict) and topic.get("Text"):
127
+ results.append(topic["Text"])
128
+ return "\n".join(results) if results else "No results found."
 
 
 
 
 
 
 
 
 
129
  except Exception as e:
130
  return f"Search error: {e}"
131
 
132
 
133
+ def web_fetch(url: str, max_chars: int = 4000) -> str:
134
+ """Fetch a webpage and return its text content."""
135
+ try:
136
+ headers = {"User-Agent": "Mozilla/5.0 (compatible; GAIAAgent/1.0)"}
137
+ r = requests.get(url, headers=headers, timeout=20)
138
+ r.raise_for_status()
139
+ # Simple HTML β†’ text stripping
140
+ text = re.sub(r"<[^>]+>", " ", r.text)
141
+ text = re.sub(r"\s+", " ", text).strip()
142
+ return text[:max_chars]
143
+ except Exception as e:
144
+ return f"Fetch error: {e}"
145
+
146
+
147
+ def wikipedia_search(query: str) -> str:
148
+ """Search Wikipedia for a query."""
149
  try:
150
+ url = "https://en.wikipedia.org/w/api.php"
151
+ params = {
152
+ "action": "query", "list": "search",
153
+ "srsearch": query, "format": "json", "srlimit": 3,
154
+ }
155
+ r = requests.get(url, params=params, timeout=15)
156
+ data = r.json()
157
+ snippets = []
158
+ for item in data["query"]["search"]:
159
+ snippets.append(f"**{item['title']}**: {re.sub('<[^>]+>','',item['snippet'])}")
160
+ if not snippets:
161
+ return "No Wikipedia results."
162
+ # Fetch first article intro
163
+ title = data["query"]["search"][0]["title"]
164
+ ex_url = "https://en.wikipedia.org/w/api.php"
165
+ ex_params = {
166
+ "action": "query", "prop": "extracts",
167
+ "exintro": True, "explaintext": True,
168
+ "titles": title, "format": "json",
169
+ }
170
+ er = requests.get(ex_url, params=ex_params, timeout=15)
171
+ pages = er.json()["query"]["pages"]
172
+ extract = next(iter(pages.values())).get("extract", "")[:3000]
173
+ return f"Search hits:\n" + "\n".join(snippets) + f"\n\nTop article ({title}):\n{extract}"
174
  except Exception as e:
175
+ return f"Wikipedia error: {e}"
176
+
177
 
178
+ # ─────────────────────────────────────────────
179
+ # Tool: File download from GAIA API
180
+ # ─────────────────────────────────────────────
181
 
182
+ def download_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> bytes | None:
183
+ """Download a file associated with a task_id from the GAIA API."""
184
  try:
185
+ url = f"{api_url}/files/{task_id}"
186
+ r = requests.get(url, timeout=30)
187
+ if r.status_code == 200:
188
+ return r.content
189
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
190
  except Exception as e:
191
+ print(f"File download error: {e}")
192
+ return None
193
 
194
 
195
+ def read_pdf_bytes(pdf_bytes: bytes) -> str:
196
+ """Extract text from PDF bytes using pypdf."""
 
 
 
197
  try:
198
+ import io
199
+ from pypdf import PdfReader
200
+ reader = PdfReader(io.BytesIO(pdf_bytes))
201
+ text = "\n".join(page.extract_text() or "" for page in reader.pages)
202
+ return text[:6000]
203
+ except Exception as e:
204
+ return f"PDF read error: {e}"
205
+
206
+
207
+ def read_excel_bytes(excel_bytes: bytes) -> str:
208
+ """Read Excel/CSV bytes and return a string summary."""
209
+ try:
210
+ import io
211
+ df = pd.read_excel(io.BytesIO(excel_bytes))
212
+ return df.to_string(index=False)[:4000]
213
  except Exception:
214
+ try:
215
+ import io
216
+ df = pd.read_csv(io.BytesIO(excel_bytes))
217
+ return df.to_string(index=False)[:4000]
218
+ except Exception as e:
219
+ return f"Excel read error: {e}"
220
+
221
 
222
+ def read_python_bytes(code_bytes: bytes) -> str:
223
+ """Return Python code as string."""
224
+ return code_bytes.decode("utf-8", errors="replace")[:4000]
225
 
 
 
 
226
 
227
+ # ─────────────────────────────────────────────
228
+ # Tool: Python code execution (safe sandbox)
229
+ # ─────────────────────────────────────────────
230
 
231
+ def run_python(code: str) -> str:
232
+ """Execute Python code and return stdout/result."""
233
+ import subprocess, sys
234
+ try:
235
+ result = subprocess.run(
236
+ [sys.executable, "-c", code],
237
+ capture_output=True, text=True, timeout=30,
238
+ )
239
+ out = result.stdout.strip()
240
+ err = result.stderr.strip()
241
+ if out:
242
+ return out
243
+ if err:
244
+ return f"STDERR: {err}"
245
+ return "(no output)"
246
+ except subprocess.TimeoutExpired:
247
+ return "Code execution timed out."
248
+ except Exception as e:
249
+ return f"Execution error: {e}"
250
+
251
+
252
+ # ─────────────────────────────────────────────
253
+ # Core Agent
254
+ # ─────────────────────────────────────────────
255
+
256
+ SYSTEM_PROMPT = """You are an expert research agent solving questions from the GAIA benchmark.
257
+ You reason carefully and use tools when needed.
258
+
259
+ TOOLS available (call them by writing a JSON block):
260
+ - web_search(query) – search the web
261
+ - wikipedia_search(query) – search Wikipedia
262
+ - web_fetch(url) – fetch a webpage
263
+ - run_python(code) – execute Python code and get result
264
+ - read_file_summary – (provided automatically when a file is attached)
265
 
266
  RULES:
267
  1. Think step by step before answering.
268
+ 2. For factual questions, search Wikipedia or the web.
269
+ 3. For math/logic, write and run Python code.
270
+ 4. Your FINAL answer must be on the LAST line starting with ANSWER:
271
+ followed by ONLY the answer (no extra explanation).
272
+ 5. Keep answers as short as possible – exact match is required.
273
+ 6. Never include "FINAL ANSWER" text in your response.
274
+
275
+ TOOL CALL FORMAT (use when needed):
276
+ {"tool": "web_search", "args": {"query": "..."}}
277
+ {"tool": "wikipedia_search", "args": {"query": "..."}}
278
+ {"tool": "web_fetch", "args": {"url": "..."}}
279
+ {"tool": "run_python", "args": {"code": "..."}}
 
 
 
 
 
280
  """
281
 
282
 
283
+ def call_tool(tool_name: str, args: dict) -> str:
284
+ if tool_name == "web_search":
285
+ return web_search(args.get("query", ""))
286
+ elif tool_name == "wikipedia_search":
287
+ return wikipedia_search(args.get("query", ""))
288
+ elif tool_name == "web_fetch":
289
+ return web_fetch(args.get("url", ""))
290
+ elif tool_name == "run_python":
291
+ return run_python(args.get("code", ""))
292
+ return f"Unknown tool: {tool_name}"
293
+
294
+
295
+ def extract_tool_call(text: str) -> tuple[str, dict] | None:
296
+ """Extract a tool call JSON from the model output."""
297
+ pattern = r'\{[^{}]*"tool"\s*:\s*"([^"]+)"[^{}]*"args"\s*:\s*(\{[^{}]*\})[^{}]*\}'
298
+ match = re.search(pattern, text, re.DOTALL)
299
+ if match:
300
+ tool_name = match.group(1)
301
+ try:
302
+ args = json.loads(match.group(2))
303
+ return tool_name, args
304
+ except json.JSONDecodeError:
305
+ pass
306
+ return None
307
+
308
+
309
+ def extract_answer(text: str) -> str:
310
+ """Extract the final answer from the model's response."""
311
+ # Look for ANSWER: on a line
312
+ for line in reversed(text.split("\n")):
313
+ line = line.strip()
314
+ if line.upper().startswith("ANSWER:"):
315
+ answer = line[7:].strip()
316
+ return answer
317
+ # Fallback: last non-empty line
318
+ lines = [l.strip() for l in text.split("\n") if l.strip()]
319
+ return lines[-1] if lines else text.strip()
320
+
321
+
322
+ class GAIAAgent:
323
+ def __init__(self, api_url: str = DEFAULT_API_URL, max_turns: int = 8):
324
+ self.api_url = api_url
325
+ self.max_turns = max_turns
326
+ print("GAIAAgent initialized.")
327
+
328
+ def __call__(self, question: str, task_id: str = "") -> str:
329
+ print(f"\n{'='*60}")
330
+ print(f"Task: {task_id}")
331
+ print(f"Question: {question[:120]}...")
332
+
333
+ # Check for attached file
334
+ file_context = ""
335
+ if task_id:
336
+ file_bytes = download_task_file(task_id, self.api_url)
337
+ if file_bytes:
338
+ # Detect file type by magic bytes or try different parsers
339
+ if file_bytes[:4] == b"%PDF":
340
+ content = read_pdf_bytes(file_bytes)
341
+ file_context = f"\n[ATTACHED FILE - PDF]\n{content}\n[END FILE]\n"
342
+ elif file_bytes[:2] in (b"PK", b"\xd0\xcf"): # ZIP/XLSX or OLE/XLS
343
+ content = read_excel_bytes(file_bytes)
344
+ file_context = f"\n[ATTACHED FILE - SPREADSHEET]\n{content}\n[END FILE]\n"
345
+ elif file_bytes[:3] in (b"ID3", b"\xff\xfb", b"\xff\xf3"): # MP3
346
+ content = transcribe_audio(file_bytes)
347
+ file_context = f"\n[ATTACHED FILE - AUDIO TRANSCRIPTION]\n{content}\n[END FILE]\n"
348
+ else:
349
+ # Try as text/code
350
+ try:
351
+ content = file_bytes.decode("utf-8", errors="replace")[:4000]
352
+ file_context = f"\n[ATTACHED FILE - TEXT/CODE]\n{content}\n[END FILE]\n"
353
+ except Exception:
354
+ file_context = "\n[ATTACHED FILE - binary, unreadable]\n"
355
+ print(f"File loaded: {len(file_bytes)} bytes")
356
+
357
+ # Detect YouTube URL in question
358
+ yt_match = re.search(r'https?://(?:www\.)?youtube\.com/watch\?v=([A-Za-z0-9_-]+)|https?://youtu\.be/([A-Za-z0-9_-]+)', question)
359
+ yt_context = ""
360
+ if yt_match:
361
+ vid_id = yt_match.group(1) or yt_match.group(2)
362
+ # Fetch YouTube transcript via a public endpoint
363
+ yt_context = self._get_youtube_transcript(vid_id)
364
+
365
+ user_content = question
366
+ if file_context:
367
+ user_content += "\n" + file_context
368
+ if yt_context:
369
+ user_content += "\n" + yt_context
370
+
371
+ messages = [
372
+ {"role": "system", "content": SYSTEM_PROMPT},
373
+ {"role": "user", "content": user_content},
374
+ ]
375
+
376
+ for turn in range(self.max_turns):
377
+ response = chat_llm(messages)
378
+ print(f"\n[Turn {turn+1}] Model response:\n{response[:400]}...")
379
+
380
+ if not response:
381
+ break
382
+
383
  messages.append({"role": "assistant", "content": response})
384
+
385
+ # Check for tool call
386
+ tool_call = extract_tool_call(response)
387
+ if tool_call:
388
+ tool_name, args = tool_call
389
+ print(f" β†’ Calling tool: {tool_name}({args})")
390
+ tool_result = call_tool(tool_name, args)
391
+ print(f" β†’ Tool result: {tool_result[:200]}...")
392
+ messages.append({
393
+ "role": "user",
394
+ "content": f"Tool result for {tool_name}:\n{tool_result}\n\nContinue reasoning and provide your final answer with ANSWER: on the last line."
395
+ })
396
+ continue
397
+
398
+ # Check if we have a final answer
399
+ if "ANSWER:" in response.upper():
400
+ answer = extract_answer(response)
401
+ print(f" β†’ Final answer: {answer}")
402
+ return answer
403
+
404
+ # If no tool call and no answer, prompt for conclusion
405
  messages.append({
406
  "role": "user",
407
+ "content": "Please provide your final answer now. Write ANSWER: followed by just the answer on the last line."
408
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
+ # Last attempt to extract answer
411
+ if messages and messages[-1]["role"] == "assistant":
412
+ return extract_answer(messages[-1]["content"])
 
 
 
413
 
414
+ return "Unable to determine answer."
 
 
 
 
 
415
 
416
+ def _get_youtube_transcript(self, video_id: str) -> str:
417
+ """Try to get YouTube transcript/captions."""
418
+ try:
419
+ # Try youtube-transcript-api style endpoint
420
+ url = f"https://www.youtube.com/watch?v={video_id}"
421
+ page = web_fetch(url, max_chars=8000)
422
+ # Look for caption data in the page
423
+ if "captionTracks" in page:
424
+ cap_match = re.search(r'"captionTracks":\[.*?"baseUrl":"([^"]+)"', page)
425
+ if cap_match:
426
+ cap_url = cap_match.group(1).replace("\\u0026", "&")
427
+ cap_text = web_fetch(cap_url, max_chars=5000)
428
+ # Strip XML tags
429
+ cap_text = re.sub(r"<[^>]+>", " ", cap_text)
430
+ cap_text = re.sub(r"\s+", " ", cap_text).strip()
431
+ return f"\n[YOUTUBE TRANSCRIPT]\n{cap_text}\n[END TRANSCRIPT]\n"
432
+ # Fallback: return page description
433
+ desc_match = re.search(r'"description":.*?"simpleText":"([^"]{20,})"', page)
434
+ if desc_match:
435
+ return f"\n[YOUTUBE VIDEO DESCRIPTION]\n{desc_match.group(1)[:1000]}\n"
436
+ return f"\n[YOUTUBE PAGE (partial)]\n{page[:2000]}\n"
437
+ except Exception as e:
438
+ return f"\n[Could not fetch YouTube content: {e}]\n"
439
 
440
+
441
+ # ─────────────────────────────────────────────
442
+ # Gradio UI
443
+ # ─────────────────────────────────────────────
444
 
445
  def run_and_submit_all(profile: gr.OAuthProfile | None):
 
 
 
 
446
  space_id = os.getenv("SPACE_ID")
447
 
448
  if profile:
449
+ username = profile.username
450
  print(f"User logged in: {username}")
451
  else:
452
+ return "Please login to Hugging Face first.", None
 
453
 
454
  api_url = DEFAULT_API_URL
455
  questions_url = f"{api_url}/questions"
456
+ submit_url = f"{api_url}/submit"
457
 
458
+ # Instantiate agent
459
  try:
460
+ agent = GAIAAgent(api_url=api_url)
461
  except Exception as e:
 
462
  return f"Error initializing agent: {e}", None
463
 
464
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else "local"
 
465
 
466
+ # Fetch questions
 
467
  try:
468
+ r = requests.get(questions_url, timeout=15)
469
+ r.raise_for_status()
470
+ questions_data = r.json()
471
  if not questions_data:
472
+ return "No questions returned.", None
 
473
  print(f"Fetched {len(questions_data)} questions.")
 
 
 
 
 
 
 
474
  except Exception as e:
475
+ return f"Error fetching questions: {e}", None
 
476
 
477
+ # Run agent
478
+ results_log = []
479
  answers_payload = []
 
480
 
481
+ for item in questions_data:
482
+ task_id = item.get("task_id")
483
  question_text = item.get("question")
484
  if not task_id or question_text is None:
485
+ continue
486
  try:
487
+ answer = agent(question_text, task_id=task_id)
 
 
 
488
  except Exception as e:
489
+ print(f"Agent error on {task_id}: {e}")
490
+ answer = f"AGENT ERROR: {e}"
491
+
492
+ answers_payload.append({"task_id": task_id, "submitted_answer": answer})
493
+ results_log.append({
494
+ "Task ID": task_id,
495
+ "Question": question_text[:80],
496
+ "Submitted Answer": answer,
497
+ })
498
+ # Be polite to the API
499
+ time.sleep(1)
 
 
 
 
500
 
501
  if not answers_payload:
502
+ return "No answers produced.", pd.DataFrame(results_log)
503
+
504
+ # Submit
505
+ submission_data = {
506
+ "username": username.strip(),
507
+ "agent_code": agent_code,
508
+ "answers": answers_payload,
509
+ }
 
 
510
  try:
511
+ r = requests.post(submit_url, json=submission_data, timeout=120)
512
+ r.raise_for_status()
513
+ result = r.json()
514
+ status = (
515
  f"Submission Successful!\n"
516
+ f"User: {result.get('username')}\n"
517
+ f"Score: {result.get('score', 'N/A')}% "
518
+ f"({result.get('correct_count', '?')}/{result.get('total_attempted', '?')} correct)\n"
519
+ f"Message: {result.get('message', '')}"
520
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  except Exception as e:
522
+ status = f"Submission error: {e}"
 
 
 
523
 
524
+ return status, pd.DataFrame(results_log)
525
+
526
+
527
+ # ─────────────────────────────────────────────
528
+ # Build Gradio app
529
+ # ─────────────────────────────────────────────
530
 
 
531
  with gr.Blocks() as demo:
532
+ gr.Markdown("# πŸ€– GAIA Agent – HF Inference Powered")
533
+ gr.Markdown("""
534
+ **How it works:**
535
+ - Uses `meta-llama/Llama-3.3-70B-Instruct` for reasoning
536
+ - Uses `meta-llama/Llama-3.2-11B-Vision-Instruct` for images
537
+ - Uses `openai/whisper-large-v3` for audio transcription
538
+ - Has web search, Wikipedia search, webpage fetching, and Python execution tools
539
+ - Automatically downloads and reads attached task files (PDF, Excel, audio, code)
540
+
541
+ **Setup:** Set the `HF_TOKEN` environment variable in your Space secrets.
542
+ """)
 
 
 
 
543
 
544
  gr.LoginButton()
545
 
546
+ run_btn = gr.Button("πŸš€ Run Evaluation & Submit All Answers", variant="primary")
547
+ status_out = gr.Textbox(label="Status / Score", lines=6, interactive=False)
548
+ results_tbl = gr.DataFrame(label="Questions & Answers", wrap=True)
549
 
550
+ run_btn.click(fn=run_and_submit_all, outputs=[status_out, results_tbl])
 
551
 
 
 
 
 
552
 
553
  if __name__ == "__main__":
554
+ print("Starting GAIA Agent …")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  demo.launch(debug=True, share=False)