emanuelediluzio commited on
Commit
67bb288
·
verified ·
1 Parent(s): f26b53a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +431 -141
app.py CHANGED
@@ -1,5 +1,8 @@
1
  import os
2
  import re
 
 
 
3
  import gradio as gr
4
  import requests
5
  import pandas as pd
@@ -9,9 +12,15 @@ from smolagents import CodeAgent, DuckDuckGoSearchTool, InferenceClientModel, to
9
  # --- Constants ---
10
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
11
 
12
- # Modello gratuito che funziona senza token Pro
13
- MAIN_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct"
14
- FALLBACK_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct"
 
 
 
 
 
 
15
 
16
  # ==========================================
17
  # 🔧 TOOL 1: LETTURA WEBPAGE
@@ -24,15 +33,20 @@ def visit_webpage(url: str) -> str:
24
  url: The full URL of the webpage to visit.
25
  """
26
  try:
27
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
28
- response = requests.get(url, headers=headers, timeout=15)
 
 
29
  response.raise_for_status()
30
- soup = BeautifulSoup(response.text, 'html.parser')
31
- for el in soup(["script", "style", "nav", "footer", "header", "aside"]):
32
  el.extract()
33
- return soup.get_text(separator='\n', strip=True)[:15000]
 
 
 
34
  except Exception as e:
35
- return f"Error: {str(e)}"
36
 
37
 
38
  # ==========================================
@@ -43,56 +57,135 @@ def get_youtube_transcript(video_url: str) -> str:
43
  """Fetches the transcript/captions of a YouTube video.
44
  Use this whenever the question refers to a YouTube video URL.
45
  Args:
46
- video_url: The full YouTube video URL.
47
  """
48
  try:
49
  from youtube_transcript_api import YouTubeTranscriptApi
50
- match = re.search(r'(?:v=|youtu\.be/)([^&\n?#]+)', video_url)
51
- if not match:
52
- return "Could not extract video ID from URL."
53
- video_id = match.group(1)
54
- entries = YouTubeTranscriptApi.get_transcript(video_id, languages=['en', 'it', 'auto'])
55
- return ' '.join([e['text'] for e in entries])[:10000]
 
 
 
 
 
 
 
 
 
 
56
  except Exception as e:
57
- return f"Transcript not available: {str(e)}"
58
 
59
 
60
  # ==========================================
61
- # 📂 TOOL 3: DOWNLOAD FILE DA GAIA
62
  # ==========================================
63
  @tool
64
  def download_task_file(task_id: str) -> str:
65
- """Downloads and reads the file attached to a GAIA task (if any).
 
66
  Always try this if the question might reference an attached document, table, or file.
67
  Args:
68
  task_id: The task_id string from the GAIA question.
69
  """
70
  try:
71
  file_url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
72
- response = requests.get(file_url, timeout=15)
73
  if response.status_code == 404:
74
  return "No file attached to this task."
75
  response.raise_for_status()
76
- content_type = response.headers.get('Content-Type', '')
77
-
78
- if any(t in content_type for t in ['text', 'json', 'csv']):
79
- return response.text[:10000]
80
-
81
- if 'pdf' in content_type:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  try:
83
- import io, PyPDF2
84
  reader = PyPDF2.PdfReader(io.BytesIO(response.content))
85
- return ''.join([p.extract_text() or '' for p in reader.pages])[:10000]
86
- except Exception:
87
- return f"PDF attached but could not extract text."
88
-
89
- if 'image' in content_type:
90
- return f"Image file attached ({content_type})."
91
-
 
 
 
 
 
 
 
 
 
 
92
  try:
93
- return response.content.decode('utf-8')[:10000]
 
94
  except Exception:
95
- return f"Binary file attached ({content_type})."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  except Exception as e:
97
  return f"Error: {str(e)}"
98
 
@@ -101,34 +194,41 @@ def download_task_file(task_id: str) -> str:
101
  # 🔍 PRE-PROCESSING
102
  # ==========================================
103
  def preprocess_question(question: str) -> str:
 
104
  stripped = question.strip()
105
- reversed_q = stripped[::-1].strip()
106
- keywords = ['answer', 'write', 'what', 'who', 'how', 'find', 'list', 'if you', 'understand']
107
- if len(reversed_q) > 15:
108
- has_keywords_reversed = any(w in reversed_q.lower() for w in keywords)
109
- has_keywords_original = any(w in stripped.lower() for w in keywords)
110
- if has_keywords_reversed and not has_keywords_original:
111
- print(f"[PRE-PROCESS] Testo invertito {reversed_q[:80]}")
112
- return reversed_q
 
 
113
  return question
114
 
115
 
116
  # ==========================================
117
- # 🤖 FALLBACK DIRETTO VIA HF API
118
  # ==========================================
119
- def call_hf_direct(question: str) -> str:
120
- """
121
- Chiama l'API HF Serverless Inference direttamente via requests.
122
- Non dipende da smolagents da huggingface_hub funziona sempre.
123
- """
124
- prompt = f"""Answer the following question with ONLY the final answer.
125
- No explanation. No preamble. Just the bare answer.
126
- - Numbers only for numeric answers.
127
- - Single word/name for name answers.
128
- - If the text seems reversed/backwards, reverse it first then answer.
129
- - No punctuation at the end.
 
 
 
130
 
131
  Question: {question}
 
132
  Answer:"""
133
 
134
  hf_token = os.getenv("HF_TOKEN", "")
@@ -136,132 +236,302 @@ Answer:"""
136
  if hf_token:
137
  headers["Authorization"] = f"Bearer {hf_token}"
138
 
139
- # Prova il modello principale
140
- for model in [FALLBACK_MODEL, "mistralai/Mixtral-8x7B-Instruct-v0.1"]:
141
  try:
142
  api_url = f"https://api-inference.huggingface.co/models/{model}"
143
  payload = {
144
  "inputs": prompt,
145
  "parameters": {
146
- "max_new_tokens": 100,
147
  "temperature": 0.1,
148
  "return_full_text": False,
149
- }
150
  }
151
- resp = requests.post(api_url, headers=headers, json=payload, timeout=30)
152
-
153
  if resp.status_code == 200:
154
  data = resp.json()
155
  if isinstance(data, list) and len(data) > 0:
156
  raw = data[0].get("generated_text", "").strip()
157
  if raw:
158
- print(f"[🔄 FALLBACK HF OK via {model}]: {raw[:80]}")
159
- return clean_answer(raw)
 
 
 
 
160
  else:
161
- print(f"[FALLBACK {model}] Status {resp.status_code}: {resp.text[:200]}")
 
162
  except Exception as e:
163
- print(f"[FALLBACK {model} ERROR]: {e}")
164
  continue
165
 
166
  return "I don't know"
167
 
168
 
 
 
 
169
  def clean_answer(raw: str) -> str:
 
170
  answer = str(raw).strip()
171
- # Prendi solo la prima riga se è multilinea
172
- first_line = answer.split('\n')[0].strip()
173
- if first_line:
174
- answer = first_line
175
 
 
 
 
 
 
 
176
  prefixes = [
177
- "the answer is", "final answer:", "answer:", "final answer is",
178
- "the result is", "the word is", "the name is", "the number is",
179
- "the correct answer is", "based on", "according to",
 
 
 
 
 
180
  ]
181
  lower = answer.lower()
182
  for prefix in prefixes:
183
  if lower.startswith(prefix):
184
  answer = answer[len(prefix):].strip()
185
  lower = answer.lower()
 
 
 
186
  break
187
 
188
- if answer.endswith('.') and not re.search(r'\d\.$', answer):
 
189
  answer = answer[:-1].strip()
190
 
191
- answer = answer.replace("**", "").strip('"').strip("'").strip()
 
 
 
 
 
 
192
  return answer
193
 
194
 
195
  # ==========================================
196
- # 🧠 IL SUPER AGENTE
197
  # ==========================================
198
  class SuperAgent:
199
  def __init__(self):
200
- print("Inizializzazione agente...")
201
-
202
- self.model = InferenceClientModel(model_id=MAIN_MODEL)
203
-
204
- self.tools = [
205
- DuckDuckGoSearchTool(),
206
- visit_webpage,
207
- get_youtube_transcript,
208
- download_task_file,
209
- ]
210
-
211
- self.agent = CodeAgent(
212
- tools=self.tools,
213
- model=self.model,
214
- max_steps=8,
215
- additional_authorized_imports=[
216
- "requests", "bs4", "json", "time", "math", "datetime",
217
- "pandas", "numpy", "re", "csv", "urllib", "collections",
218
- "itertools", "string", "unicodedata"
219
- ]
220
- )
221
-
222
- self.agent_prompt = """You are an expert AI solving the GAIA benchmark. Find the EXACT correct answer.
223
 
224
- STRATEGY (follow in order):
225
- 1. If question has a YouTube URL → call get_youtube_transcript(url) immediately.
226
- 2. If question has a website URL → call visit_webpage(url).
227
- 3. If question might have an attached file → call download_task_file(task_id).
228
- 4. For factual questions → DuckDuckGoSearchTool, then visit_webpage to confirm.
229
- 5. For math/text/date → write Python to compute directly.
230
- 6. If text looks scrambled/reversed → use Python: text[::-1]
231
-
232
- OUTPUT (CRITICAL — follow exactly):
233
- - Output ONLY the bare answer. Nothing else.
234
- - Number answers: just the digit(s). Example: 3
235
- - Name/word answers: just the word. Example: Einstein
236
- - List answers: comma-separated. Example: cat, dog, bird
237
- - NEVER output: "The answer is", "FINAL ANSWER:", "Based on", or any explanation.
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  Question: {question}"""
240
 
241
  def __call__(self, question: str, task_id: str = "") -> str:
242
- print(f"\n[Q]: {question[:100]}...")
 
 
243
 
 
244
  processed = preprocess_question(question)
245
 
246
- context = ""
 
247
  if task_id:
248
- context = f"\n(task_id='{task_id}' — use download_task_file('{task_id}') if a file is referenced)\n"
249
-
250
- full_q = processed + context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
- # Tentativo 1: agente con tools
253
- try:
254
- raw = self.agent.run(self.agent_prompt.format(question=full_q))
255
- answer = clean_answer(raw)
256
- if answer and answer.lower() not in ["error", "none", "n/a", "", "i don't know", "unknown"]:
257
- print(f"[✅ AGENT]: {answer}")
258
- return answer
259
- print(f"[⚠️ AGENT risposta vuota/invalida: '{answer}'] → fallback")
260
- except Exception as e:
261
- print(f"[⚠️ AGENT CRASH: {e}] → fallback")
262
 
263
- # Tentativo 2: chiamata diretta HF API
264
- return call_hf_direct(processed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
 
267
  # ==========================================
@@ -274,11 +544,14 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
274
  return "Per favore, fai il Login con Hugging Face.", None
275
 
276
  username = profile.username
277
- print(f"Utente: {username}")
 
 
278
 
279
  try:
280
  agent = SuperAgent()
281
  except Exception as e:
 
282
  return f"Errore inizializzazione agente: {e}", None
283
 
284
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
@@ -289,63 +562,80 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
289
  questions_data = resp.json()
290
  if not questions_data:
291
  return "Lista domande vuota.", None
 
292
  except Exception as e:
293
  return f"Errore download domande: {e}", None
294
 
295
  results_log = []
296
  answers_payload = []
297
 
298
- print(f"Elaboro {len(questions_data)} domande...")
299
- for item in questions_data:
300
  task_id = item.get("task_id", "")
301
  question_text = item.get("question")
302
  if not task_id or question_text is None:
303
  continue
 
 
304
  try:
305
  answer = agent(question_text, task_id=task_id)
306
  except Exception as e:
307
  answer = "I don't know"
308
- print(f"[LOOP ERROR {task_id}]: {e}")
 
309
 
310
  answers_payload.append({"task_id": task_id, "submitted_answer": answer})
311
  results_log.append({
312
  "Task ID": task_id,
313
  "Question": question_text[:120],
314
- "Submitted Answer": answer
315
  })
316
 
317
  if not answers_payload:
318
  return "Nessuna risposta prodotta.", pd.DataFrame(results_log)
319
 
 
 
 
320
  try:
321
  resp = requests.post(
322
  f"{DEFAULT_API_URL}/submit",
323
- json={"username": username, "agent_code": agent_code, "answers": answers_payload},
324
- timeout=60
 
 
 
 
325
  )
326
  resp.raise_for_status()
327
  r = resp.json()
328
  status = (
329
  f"✅ Invio Completato!\n"
330
  f"👤 {r.get('username')}\n"
331
- f"🏆 {r.get('score', 'N/A')}% ({r.get('correct_count', '?')}/{r.get('total_attempted', '?')} corrette)\n"
 
332
  f"📝 {r.get('message', '')}"
333
  )
 
334
  return status, pd.DataFrame(results_log)
335
  except Exception as e:
336
  return f"❌ Invio fallito: {e}", pd.DataFrame(results_log)
337
 
338
 
339
  # ==========================================
340
- # 🖥️ INTERFACCIA
341
  # ==========================================
342
  with gr.Blocks() as demo:
343
  gr.Markdown("# 🚀 Super Agente - Final Assignment Runner")
 
 
 
 
344
  gr.LoginButton()
345
- run_button = gr.Button("Avvia Valutazione & Invia Risposte", variant="primary")
346
- status_output = gr.Textbox(label="Stato / Risultato", lines=5, interactive=False)
347
  results_table = gr.DataFrame(label="Domande e Risposte", wrap=True)
348
  run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
349
 
350
  if __name__ == "__main__":
 
351
  demo.launch(debug=True, share=False)
 
1
  import os
2
  import re
3
+ import io
4
+ import json
5
+ import traceback
6
  import gradio as gr
7
  import requests
8
  import pandas as pd
 
12
  # --- Constants ---
13
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
14
 
15
+ # Modelli in ordine di preferenza (tutti gratuiti su HF Inference API)
16
+ MODEL_CANDIDATES = [
17
+ "Qwen/Qwen2.5-Coder-32B-Instruct",
18
+ "Qwen/Qwen2.5-72B-Instruct",
19
+ "meta-llama/Meta-Llama-3.1-8B-Instruct",
20
+ "mistralai/Mixtral-8x7B-Instruct-v0.1",
21
+ "HuggingFaceH4/zephyr-7b-beta",
22
+ ]
23
+
24
 
25
  # ==========================================
26
  # 🔧 TOOL 1: LETTURA WEBPAGE
 
33
  url: The full URL of the webpage to visit.
34
  """
35
  try:
36
+ headers = {
37
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
38
+ }
39
+ response = requests.get(url, headers=headers, timeout=20)
40
  response.raise_for_status()
41
+ soup = BeautifulSoup(response.text, "html.parser")
42
+ for el in soup(["script", "style", "nav", "footer", "header", "aside", "noscript"]):
43
  el.extract()
44
+ text = soup.get_text(separator="\n", strip=True)
45
+ # Pulizia extra
46
+ lines = [l.strip() for l in text.splitlines() if l.strip()]
47
+ return "\n".join(lines)[:15000]
48
  except Exception as e:
49
+ return f"Error fetching {url}: {str(e)}"
50
 
51
 
52
  # ==========================================
 
57
  """Fetches the transcript/captions of a YouTube video.
58
  Use this whenever the question refers to a YouTube video URL.
59
  Args:
60
+ video_url: The full YouTube video URL (or just the video ID).
61
  """
62
  try:
63
  from youtube_transcript_api import YouTubeTranscriptApi
64
+
65
+ match = re.search(r"(?:v=|youtu\.be/|embed/)([^&\n?#]+)", video_url)
66
+ video_id = match.group(1) if match else video_url.strip()
67
+
68
+ try:
69
+ entries = YouTubeTranscriptApi.get_transcript(video_id, languages=["en"])
70
+ except Exception:
71
+ try:
72
+ entries = YouTubeTranscriptApi.get_transcript(video_id)
73
+ except Exception:
74
+ transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
75
+ transcript = transcript_list.find_generated_transcript(["en", "it", "fr", "de", "es"])
76
+ entries = transcript.fetch()
77
+
78
+ full = " ".join([e["text"] for e in entries])
79
+ return full[:12000]
80
  except Exception as e:
81
+ return f"Transcript error: {str(e)}"
82
 
83
 
84
  # ==========================================
85
+ # 📂 TOOL 3: DOWNLOAD + PARSE FILE DA GAIA
86
  # ==========================================
87
  @tool
88
  def download_task_file(task_id: str) -> str:
89
+ """Downloads and reads the file attached to a GAIA task.
90
+ Handles text, CSV, JSON, PDF, Excel (.xlsx/.xls), Python, and audio files.
91
  Always try this if the question might reference an attached document, table, or file.
92
  Args:
93
  task_id: The task_id string from the GAIA question.
94
  """
95
  try:
96
  file_url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
97
+ response = requests.get(file_url, timeout=30)
98
  if response.status_code == 404:
99
  return "No file attached to this task."
100
  response.raise_for_status()
101
+ ct = response.headers.get("Content-Type", "")
102
+ cd = response.headers.get("Content-Disposition", "")
103
+
104
+ # Detect filename from Content-Disposition
105
+ filename = ""
106
+ if "filename=" in cd:
107
+ filename = cd.split("filename=")[-1].strip('" ')
108
+ ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
109
+
110
+ print(f" [FILE] type={ct}, name={filename}, ext={ext}, size={len(response.content)}")
111
+
112
+ # --- TEXT / CSV / JSON ---
113
+ if any(t in ct for t in ["text", "json", "csv"]) or ext in ["txt", "csv", "json", "py", "md"]:
114
+ text = response.text
115
+ if ext == "csv" or "csv" in ct:
116
+ try:
117
+ df = pd.read_csv(io.StringIO(text))
118
+ return f"CSV file with {len(df)} rows, columns: {list(df.columns)}\n\n{df.to_string()}"[:12000]
119
+ except Exception:
120
+ pass
121
+ return text[:12000]
122
+
123
+ # --- EXCEL ---
124
+ if "spreadsheet" in ct or "excel" in ct or ext in ["xlsx", "xls"]:
125
+ try:
126
+ df = pd.read_excel(io.BytesIO(response.content), engine="openpyxl")
127
+ summary = f"Excel file with {len(df)} rows, columns: {list(df.columns)}\n"
128
+ summary += f"Data types: {dict(df.dtypes)}\n\n"
129
+ summary += df.to_string()
130
+ return summary[:12000]
131
+ except Exception as e:
132
+ return f"Excel file but read error: {e}"
133
+
134
+ # --- PDF ---
135
+ if "pdf" in ct or ext == "pdf":
136
  try:
137
+ import PyPDF2
138
  reader = PyPDF2.PdfReader(io.BytesIO(response.content))
139
+ pages_text = []
140
+ for i, page in enumerate(reader.pages):
141
+ t = page.extract_text() or ""
142
+ pages_text.append(f"[Page {i+1}]\n{t}")
143
+ return "\n".join(pages_text)[:12000]
144
+ except Exception as e:
145
+ return f"PDF attached but read error: {e}"
146
+
147
+ # --- AUDIO (mp3, wav) ---
148
+ if "audio" in ct or ext in ["mp3", "wav", "m4a", "ogg"]:
149
+ return f"Audio file attached ({ct}, {len(response.content)} bytes). Cannot transcribe directly."
150
+
151
+ # --- IMAGE ---
152
+ if "image" in ct or ext in ["png", "jpg", "jpeg", "gif", "webp"]:
153
+ return f"Image file attached ({ct}, {len(response.content)} bytes)."
154
+
155
+ # --- Fallback: try decode as text ---
156
  try:
157
+ decoded = response.content.decode("utf-8")
158
+ return decoded[:12000]
159
  except Exception:
160
+ return f"Binary file ({ct}, {len(response.content)} bytes). Cannot parse."
161
+
162
+ except Exception as e:
163
+ return f"File download error: {str(e)}"
164
+
165
+
166
+ # ==========================================
167
+ # 🧮 TOOL 4: PYTHON EVAL SICURO
168
+ # ==========================================
169
+ @tool
170
+ def python_compute(code: str) -> str:
171
+ """Executes a Python expression or short script and returns the result.
172
+ Use for math calculations, string manipulation, date computations, etc.
173
+ Args:
174
+ code: A Python expression or short script. Use print() for output.
175
+ """
176
+ try:
177
+ # Prova prima come espressione
178
+ result = eval(code)
179
+ return str(result)
180
+ except SyntaxError:
181
+ # Se è uno statement, eseguilo e cattura stdout
182
+ import contextlib
183
+ import sys
184
+ f = io.StringIO()
185
+ with contextlib.redirect_stdout(f):
186
+ exec(code)
187
+ output = f.getvalue().strip()
188
+ return output if output else "Code executed (no output)"
189
  except Exception as e:
190
  return f"Error: {str(e)}"
191
 
 
194
  # 🔍 PRE-PROCESSING
195
  # ==========================================
196
  def preprocess_question(question: str) -> str:
197
+ """Detect reversed text and fix it."""
198
  stripped = question.strip()
199
+ reversed_q = stripped[::-1]
200
+
201
+ keywords_en = ["answer", "what", "who", "how", "find", "list", "which", "where", "when", "the"]
202
+ keywords_present_original = sum(1 for w in keywords_en if w in stripped.lower())
203
+ keywords_present_reversed = sum(1 for w in keywords_en if w in reversed_q.lower())
204
+
205
+ if keywords_present_reversed > keywords_present_original and len(stripped) > 20:
206
+ print(f" [PRE-PROCESS] Reversed text detected! Using reversed version.")
207
+ return reversed_q
208
+
209
  return question
210
 
211
 
212
  # ==========================================
213
+ # 🔄 CHIAMATA DIRETTA HF INFERENCE API
214
  # ==========================================
215
+ def call_hf_direct(question: str, task_context: str = "") -> str:
216
+ """Fallback: chiama HF Inference API direttamente senza smolagents."""
217
+
218
+ prompt = f"""You are answering a question from the GAIA benchmark.
219
+ Give ONLY the final answer — no explanation, no preamble, no "The answer is".
220
+
221
+ Rules:
222
+ - For numbers: just digits (e.g., 42)
223
+ - For names: just the name (e.g., Einstein)
224
+ - For lists: comma-separated (e.g., apple, banana, cherry)
225
+ - No period at the end unless part of the answer
226
+ - If text seems reversed, reverse it first
227
+
228
+ {task_context}
229
 
230
  Question: {question}
231
+
232
  Answer:"""
233
 
234
  hf_token = os.getenv("HF_TOKEN", "")
 
236
  if hf_token:
237
  headers["Authorization"] = f"Bearer {hf_token}"
238
 
239
+ for model in MODEL_CANDIDATES:
 
240
  try:
241
  api_url = f"https://api-inference.huggingface.co/models/{model}"
242
  payload = {
243
  "inputs": prompt,
244
  "parameters": {
245
+ "max_new_tokens": 150,
246
  "temperature": 0.1,
247
  "return_full_text": False,
248
+ },
249
  }
250
+ resp = requests.post(api_url, headers=headers, json=payload, timeout=45)
251
+
252
  if resp.status_code == 200:
253
  data = resp.json()
254
  if isinstance(data, list) and len(data) > 0:
255
  raw = data[0].get("generated_text", "").strip()
256
  if raw:
257
+ answer = clean_answer(raw)
258
+ if answer and answer.lower() not in [
259
+ "i don't know", "unknown", "n/a", "none", "error", "",
260
+ ]:
261
+ print(f" [FALLBACK OK via {model}]: {answer[:100]}")
262
+ return answer
263
  else:
264
+ print(f" [FALLBACK {model}] HTTP {resp.status_code}")
265
+
266
  except Exception as e:
267
+ print(f" [FALLBACK {model} ERROR]: {e}")
268
  continue
269
 
270
  return "I don't know"
271
 
272
 
273
+ # ==========================================
274
+ # 🧹 PULIZIA RISPOSTA
275
+ # ==========================================
276
  def clean_answer(raw: str) -> str:
277
+ """Pulisci la risposta grezza dall'agente."""
278
  answer = str(raw).strip()
 
 
 
 
279
 
280
+ # Se multilinea, prendi la prima riga non vuota significativa
281
+ lines = [l.strip() for l in answer.split("\n") if l.strip()]
282
+ if lines:
283
+ answer = lines[0]
284
+
285
+ # Rimuovi prefissi comuni
286
  prefixes = [
287
+ "the answer is:", "the answer is", "final answer:", "final answer is:",
288
+ "final answer is", "answer:", "answer is:", "answer is",
289
+ "the result is:", "the result is", "result:",
290
+ "the correct answer is:", "the correct answer is",
291
+ "the word is", "the name is", "the number is",
292
+ "based on my research,", "based on the information,",
293
+ "based on the search results,", "according to",
294
+ "here is the answer:", "sure,", "sure!",
295
  ]
296
  lower = answer.lower()
297
  for prefix in prefixes:
298
  if lower.startswith(prefix):
299
  answer = answer[len(prefix):].strip()
300
  lower = answer.lower()
301
+ # Rimuovi anche eventuali virgolette dopo il prefisso
302
+ if answer.startswith('"') or answer.startswith("'"):
303
+ answer = answer[1:]
304
  break
305
 
306
+ # Rimuovi punto finale (ma non se è un decimale tipo "3.14")
307
+ if answer.endswith(".") and not re.search(r"\d\.$", answer):
308
  answer = answer[:-1].strip()
309
 
310
+ # Rimuovi markdown bold, virgolette
311
+ answer = answer.replace("**", "").strip('"').strip("'").strip("`").strip()
312
+
313
+ # Se la risposta inizia con "is " (residuo), rimuovilo
314
+ if answer.lower().startswith("is "):
315
+ answer = answer[3:].strip()
316
+
317
  return answer
318
 
319
 
320
  # ==========================================
321
+ # 🧠 AGENTE PRINCIPALE
322
  # ==========================================
323
  class SuperAgent:
324
  def __init__(self):
325
+ print("=" * 60)
326
+ print("🚀 Inizializzazione SuperAgent...")
327
+ print("=" * 60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
+ hf_token = os.getenv("HF_TOKEN", "")
330
+ print(f" HF_TOKEN presente: {bool(hf_token)}")
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ # Prova a inizializzare il modello per smolagents
333
+ self.agent = None
334
+ for model_id in MODEL_CANDIDATES[:3]: # Prova i primi 3
335
+ try:
336
+ print(f" Trying model: {model_id}")
337
+ model = InferenceClientModel(
338
+ model_id=model_id,
339
+ token=hf_token if hf_token else None,
340
+ )
341
+
342
+ self.agent = CodeAgent(
343
+ tools=[
344
+ DuckDuckGoSearchTool(),
345
+ visit_webpage,
346
+ get_youtube_transcript,
347
+ download_task_file,
348
+ python_compute,
349
+ ],
350
+ model=model,
351
+ max_steps=6,
352
+ additional_authorized_imports=[
353
+ "requests", "bs4", "json", "time", "math", "datetime",
354
+ "pandas", "numpy", "re", "csv", "urllib", "collections",
355
+ "itertools", "string", "unicodedata", "statistics",
356
+ ],
357
+ )
358
+ print(f" ✅ Agent inizializzato con {model_id}")
359
+ break
360
+ except Exception as e:
361
+ print(f" ❌ {model_id} fallito: {e}")
362
+ continue
363
+
364
+ if self.agent is None:
365
+ print(" ⚠️ Nessun modello disponibile per l'agente — solo fallback diretto.")
366
+
367
+ def _build_prompt(self, question: str, task_id: str, file_context: str = "") -> str:
368
+ """Costruisci il prompt per l'agente."""
369
+ file_hint = ""
370
+ if task_id:
371
+ file_hint = f'\nThis question has task_id="{task_id}". Call download_task_file("{task_id}") to check for attached files.'
372
+
373
+ extra_context = ""
374
+ if file_context:
375
+ extra_context = f"\n\nFILE CONTENT:\n{file_context}\n"
376
+
377
+ return f"""You are an expert AI assistant solving GAIA benchmark questions.
378
+ Your goal: find the EXACT correct answer.
379
+
380
+ STRATEGY (follow in this order):
381
+ 1. If the question has a YouTube URL → call get_youtube_transcript(url)
382
+ 2. If the question has any URL → call visit_webpage(url)
383
+ 3. If there might be an attached file → call download_task_file(task_id)
384
+ 4. For factual questions → use DuckDuckGoSearchTool, then visit_webpage to verify
385
+ 5. For calculations → use python_compute() or write Python directly
386
+ 6. If text looks reversed/scrambled → reverse it with Python: text[::-1]
387
+
388
+ ANSWER FORMAT (CRITICAL):
389
+ - Output ONLY the final answer. No explanation. No prefix.
390
+ - Numbers: just digits (e.g., 3)
391
+ - Names: just the name (e.g., Einstein)
392
+ - Lists: comma-separated (e.g., cat, dog, bird)
393
+ - NEVER say "The answer is..." or "FINAL ANSWER:" or any preamble
394
+ {file_hint}{extra_context}
395
  Question: {question}"""
396
 
397
  def __call__(self, question: str, task_id: str = "") -> str:
398
+ print(f"\n{'─'*60}")
399
+ print(f"[Q]: {question[:150]}...")
400
+ print(f"[TASK]: {task_id}")
401
 
402
+ # 1. Pre-process (reversed text detection)
403
  processed = preprocess_question(question)
404
 
405
+ # 2. Se c'è un task_id, prova a scaricare il file subito per avere contesto
406
+ file_context = ""
407
  if task_id:
408
+ try:
409
+ fc = download_task_file.__wrapped__(task_id) if hasattr(download_task_file, '__wrapped__') else ""
410
+ if fc and "No file" not in fc and "error" not in fc.lower():
411
+ file_context = fc
412
+ print(f" [FILE PRE-FETCH]: {len(file_context)} chars")
413
+ except Exception:
414
+ # Smolagents tool wrapper, proviamo direttamente
415
+ try:
416
+ file_url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
417
+ resp = requests.get(file_url, timeout=15)
418
+ if resp.status_code == 200:
419
+ ct = resp.headers.get("Content-Type", "")
420
+ cd = resp.headers.get("Content-Disposition", "")
421
+ filename = ""
422
+ if "filename=" in cd:
423
+ filename = cd.split("filename=")[-1].strip('" ')
424
+ ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
425
+
426
+ if any(t in ct for t in ["text", "json", "csv"]) or ext in ["txt", "csv", "json", "py"]:
427
+ file_context = resp.text[:8000]
428
+ elif "spreadsheet" in ct or "excel" in ct or ext in ["xlsx", "xls"]:
429
+ try:
430
+ df = pd.read_excel(io.BytesIO(resp.content), engine="openpyxl")
431
+ file_context = f"Excel: {len(df)} rows, cols={list(df.columns)}\n{df.to_string()}"[:8000]
432
+ except Exception:
433
+ pass
434
+ elif "pdf" in ct or ext == "pdf":
435
+ try:
436
+ import PyPDF2
437
+ reader = PyPDF2.PdfReader(io.BytesIO(resp.content))
438
+ file_context = "\n".join(
439
+ [p.extract_text() or "" for p in reader.pages]
440
+ )[:8000]
441
+ except Exception:
442
+ pass
443
+ print(f" [FILE PRE-FETCH direct]: {len(file_context)} chars")
444
+ except Exception as e:
445
+ print(f" [FILE PRE-FETCH failed]: {e}")
446
+
447
+ # 3. Detect special question types and handle directly
448
+ answer = self._handle_special_cases(processed, task_id, file_context)
449
+ if answer:
450
+ print(f" [SPECIAL CASE]: {answer}")
451
+ return answer
452
+
453
+ # 4. Tentativo con agente smolagents
454
+ if self.agent:
455
+ try:
456
+ prompt = self._build_prompt(processed, task_id, file_context)
457
+ raw = self.agent.run(prompt)
458
+ answer = clean_answer(str(raw))
459
+ if self._is_valid_answer(answer):
460
+ print(f" [✅ AGENT]: {answer}")
461
+ return answer
462
+ print(f" [⚠️ AGENT invalid: '{answer}']")
463
+ except Exception as e:
464
+ print(f" [⚠️ AGENT ERROR]: {e}")
465
+ traceback.print_exc()
466
+
467
+ # 5. Fallback: HF API diretta
468
+ print(" [→ FALLBACK HF DIRECT]")
469
+ context_for_fallback = ""
470
+ if file_context:
471
+ context_for_fallback = f"\nAttached file content:\n{file_context[:3000]}\n"
472
+
473
+ answer = call_hf_direct(processed, context_for_fallback)
474
+ print(f" [FINAL]: {answer}")
475
+ return answer
476
+
477
+ def _is_valid_answer(self, answer: str) -> bool:
478
+ """Controlla se una risposta è valida (non vuota e non un errore generico)."""
479
+ if not answer:
480
+ return False
481
+ invalid = [
482
+ "i don't know", "unknown", "n/a", "none", "error",
483
+ "i cannot", "i can't", "not available", "no answer",
484
+ "could not", "unable to", "i'm not sure",
485
+ ]
486
+ return answer.lower().strip() not in invalid
487
 
488
+ def _handle_special_cases(self, question: str, task_id: str, file_context: str) -> str:
489
+ """Gestisci direttamente casi speciali che non richiedono l'agente."""
490
+ q_lower = question.lower()
 
 
 
 
 
 
 
491
 
492
+ # --- EXCEL con domanda su totali/somme ---
493
+ if file_context and ("total" in q_lower or "sum" in q_lower or "sales" in q_lower):
494
+ try:
495
+ # Prova a parsare il contesto come DataFrame
496
+ if file_context.startswith("Excel:") or file_context.startswith("CSV"):
497
+ # Ri-scarica il file e calcola
498
+ file_url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
499
+ resp = requests.get(file_url, timeout=15)
500
+ ct = resp.headers.get("Content-Type", "")
501
+ cd = resp.headers.get("Content-Disposition", "")
502
+ filename = ""
503
+ if "filename=" in cd:
504
+ filename = cd.split("filename=")[-1].strip('" ')
505
+ ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
506
+
507
+ if "spreadsheet" in ct or "excel" in ct or ext in ["xlsx", "xls"]:
508
+ df = pd.read_excel(io.BytesIO(resp.content), engine="openpyxl")
509
+ elif ext == "csv" or "csv" in ct:
510
+ df = pd.read_csv(io.BytesIO(resp.content))
511
+ else:
512
+ return ""
513
+
514
+ # Trova colonne numeriche e calcola totali
515
+ numeric_cols = df.select_dtypes(include=["number"]).columns.tolist()
516
+ if numeric_cols:
517
+ totals = {col: df[col].sum() for col in numeric_cols}
518
+ # Se chiede "total sales", cerca colonna "sales"
519
+ for col in numeric_cols:
520
+ if "sale" in col.lower() or "total" in col.lower() or "amount" in col.lower():
521
+ val = df[col].sum()
522
+ # Formatta come numero intero se è un intero
523
+ if val == int(val):
524
+ return str(int(val))
525
+ return f"${val:,.2f}" if val > 100 else str(val)
526
+ # Altrimenti somma la prima colonna numerica
527
+ val = list(totals.values())[0]
528
+ if val == int(val):
529
+ return str(int(val))
530
+ return str(val)
531
+ except Exception as e:
532
+ print(f" [SPECIAL CASE Excel error]: {e}")
533
+
534
+ return ""
535
 
536
 
537
  # ==========================================
 
544
  return "Per favore, fai il Login con Hugging Face.", None
545
 
546
  username = profile.username
547
+ print(f"\n{'='*60}")
548
+ print(f"👤 Utente: {username}")
549
+ print(f"{'='*60}")
550
 
551
  try:
552
  agent = SuperAgent()
553
  except Exception as e:
554
+ traceback.print_exc()
555
  return f"Errore inizializzazione agente: {e}", None
556
 
557
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
 
562
  questions_data = resp.json()
563
  if not questions_data:
564
  return "Lista domande vuota.", None
565
+ print(f"\n📋 {len(questions_data)} domande da elaborare")
566
  except Exception as e:
567
  return f"Errore download domande: {e}", None
568
 
569
  results_log = []
570
  answers_payload = []
571
 
572
+ for i, item in enumerate(questions_data):
 
573
  task_id = item.get("task_id", "")
574
  question_text = item.get("question")
575
  if not task_id or question_text is None:
576
  continue
577
+
578
+ print(f"\n[{i+1}/{len(questions_data)}] ────────────────────────")
579
  try:
580
  answer = agent(question_text, task_id=task_id)
581
  except Exception as e:
582
  answer = "I don't know"
583
+ print(f" [LOOP ERROR]: {e}")
584
+ traceback.print_exc()
585
 
586
  answers_payload.append({"task_id": task_id, "submitted_answer": answer})
587
  results_log.append({
588
  "Task ID": task_id,
589
  "Question": question_text[:120],
590
+ "Submitted Answer": answer,
591
  })
592
 
593
  if not answers_payload:
594
  return "Nessuna risposta prodotta.", pd.DataFrame(results_log)
595
 
596
+ print(f"\n{'='*60}")
597
+ print(f"📤 Invio {len(answers_payload)} risposte...")
598
+
599
  try:
600
  resp = requests.post(
601
  f"{DEFAULT_API_URL}/submit",
602
+ json={
603
+ "username": username,
604
+ "agent_code": agent_code,
605
+ "answers": answers_payload,
606
+ },
607
+ timeout=120,
608
  )
609
  resp.raise_for_status()
610
  r = resp.json()
611
  status = (
612
  f"✅ Invio Completato!\n"
613
  f"👤 {r.get('username')}\n"
614
+ f"🏆 {r.get('score', 'N/A')}% "
615
+ f"({r.get('correct_count', '?')}/{r.get('total_attempted', '?')} corrette)\n"
616
  f"📝 {r.get('message', '')}"
617
  )
618
+ print(f"\n{status}")
619
  return status, pd.DataFrame(results_log)
620
  except Exception as e:
621
  return f"❌ Invio fallito: {e}", pd.DataFrame(results_log)
622
 
623
 
624
  # ==========================================
625
+ # 🖥️ INTERFACCIA GRADIO
626
  # ==========================================
627
  with gr.Blocks() as demo:
628
  gr.Markdown("# 🚀 Super Agente - Final Assignment Runner")
629
+ gr.Markdown(
630
+ "Login con HF, poi clicca il bottone. "
631
+ "L'agente proverà più modelli e strategie per rispondere al GAIA benchmark."
632
+ )
633
  gr.LoginButton()
634
+ run_button = gr.Button("🔥 Avvia Valutazione & Invia Risposte", variant="primary")
635
+ status_output = gr.Textbox(label="Stato / Risultato", lines=6, interactive=False)
636
  results_table = gr.DataFrame(label="Domande e Risposte", wrap=True)
637
  run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
638
 
639
  if __name__ == "__main__":
640
+ print("🚀 Avvio app...")
641
  demo.launch(debug=True, share=False)