emanuelediluzio commited on
Commit
5014c70
·
verified ·
1 Parent(s): b7d39e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +260 -72
app.py CHANGED
@@ -9,104 +9,280 @@ from smolagents import CodeAgent, DuckDuckGoSearchTool, InferenceClientModel, to
9
  # --- Constants ---
10
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
11
 
 
12
  # ==========================================
13
- # 🚀 TOOL 1: RICERCA E LETTURA WEB AVANZATA
14
  # ==========================================
15
  @tool
16
  def visit_webpage(url: str) -> str:
17
- """Visits a webpage and extracts its main clean text. Use this to read Wikipedia, news, or articles.
 
18
  Args:
19
- url: The URL of the webpage to visit.
20
  """
21
  try:
22
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
 
 
 
 
 
 
23
  response = requests.get(url, headers=headers, timeout=15)
24
  response.raise_for_status()
25
-
26
  soup = BeautifulSoup(response.text, 'html.parser')
27
- # Rimuove tutto ciò che non è testo utile
28
  for element in soup(["script", "style", "nav", "footer", "header", "aside"]):
29
  element.extract()
30
-
31
  text = soup.get_text(separator='\n', strip=True)
32
- # Prende i primi 15000 caratteri di puro testo informativo
33
- return text[:15000]
34
  except Exception as e:
35
  return f"Error reading the webpage: {str(e)}"
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  # ==========================================
38
  # 🧠 IL SUPER AGENTE
39
  # ==========================================
40
  class SuperAgent:
41
  def __init__(self):
42
  print("Inizializzazione del SUPER Agente AI in corso...")
43
-
44
- # 1. Modello
45
- self.model = InferenceClientModel(model_id="Qwen/Qwen2.5-Coder-32B-Instruct")
46
-
47
- # 2. Tools
48
- self.tools = [DuckDuckGoSearchTool(), visit_webpage]
49
-
50
- # 3. Agente (Potenza massima, importazioni analitiche sbloccate)
 
 
 
 
 
 
 
 
 
 
 
 
51
  self.agent = CodeAgent(
52
  tools=self.tools,
53
  model=self.model,
54
- max_steps=20, # Aumentati i tentativi a 20!
55
  additional_authorized_imports=[
56
- "requests", "bs4", "json", "time", "math", "datetime",
57
- "pandas", "numpy", "re", "csv", "urllib"
 
58
  ]
59
  )
60
-
61
- # 4. Prompt Estremo per GAIA
62
- self.prompt_template = """
63
- You are an elite AI data analyst solving the GAIA benchmark.
64
- You are provided with a question. You MUST use your tools to find the answer.
65
-
66
- CRITICAL RULES FOR YOUR FINAL OUTPUT:
67
- 1. EXACT MATCH ONLY: Output ONLY the final answer. Nothing else.
68
- 2. If the answer is a number, return JUST the number (e.g., '14' or '1998').
69
- 3. If the answer is a name/word, return JUST the word.
70
- 4. NEVER use phrases like "The answer is", "Based on my search", or "FINAL ANSWER:".
71
- 5. If the question requires math, date calculation, or text processing, write the Python code to solve it internally.
72
-
73
- Question: {question}
74
- """
75
-
76
- def __call__(self, question: str) -> str:
77
- print(f"\n[DOMANDA RICEVUTA]: {question[:80]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  try:
79
- formatted_prompt = self.prompt_template.format(question=question)
80
  raw_answer = self.agent.run(formatted_prompt)
81
- final_answer = str(raw_answer).strip()
82
-
83
- # --- FILTRO ANTI-BLABLA ---
84
- # Se l'LLM si ostina a inserire testo, lo forziamo a tacere tagliando le frasi comuni.
85
- prefixes_to_cut = ["The answer is", "FINAL ANSWER:", "Answer:", "final answer is", "The requested word is", "The highest number is"]
86
- for prefix in prefixes_to_cut:
87
- if prefix.lower() in final_answer.lower():
88
- idx = final_answer.lower().rfind(prefix.lower()) + len(prefix)
89
- final_answer = final_answer[idx:].strip()
90
-
91
- # Toglie il punto finale se l'ha messo per sbaglio (es. "1994." -> "1994")
92
- if final_answer.endswith('.'):
93
- final_answer = final_answer[:-1]
94
-
95
- # Toglie virgolette extra o asterischi di formattazione Markdown
96
- final_answer = final_answer.replace("**", "").replace('"', "").replace("'", "")
97
-
98
- print(f"[RISPOSTA PULITA TROVATA]: {final_answer}")
99
- return final_answer
100
  except Exception as e:
101
- print(f"Errore durante l'elaborazione: {e}")
102
- return "Error"
 
 
 
 
103
 
104
 
105
  # ==========================================
106
  # ⚙️ INTERFACCIA E RUNNER
107
  # ==========================================
108
  def run_and_submit_all(profile: gr.OAuthProfile | None):
109
- space_id = os.getenv("SPACE_ID")
110
 
111
  if profile:
112
  username = f"{profile.username}"
@@ -122,7 +298,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
122
  agent = SuperAgent()
123
  except Exception as e:
124
  return f"Errore nell'inizializzazione dell'agente: {e}", None
125
-
126
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
127
 
128
  try:
@@ -130,31 +306,44 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
130
  response.raise_for_status()
131
  questions_data = response.json()
132
  if not questions_data:
133
- return "La lista delle domande scaricata è vuota.", None
134
  except Exception as e:
135
  return f"Errore nel download delle domande: {e}", None
136
 
137
  results_log = []
138
  answers_payload = []
139
-
140
  print(f"Avvio elaborazione su {len(questions_data)} domande...")
141
  for item in questions_data:
142
- task_id = item.get("task_id")
143
  question_text = item.get("question")
144
  if not task_id or question_text is None:
145
  continue
146
  try:
147
- submitted_answer = agent(question_text)
148
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
149
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
 
 
 
 
150
  except Exception as e:
151
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
 
 
 
 
 
152
 
153
  if not answers_payload:
154
  return "L'agente non ha prodotto risposte da inviare.", pd.DataFrame(results_log)
155
 
156
- submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
157
-
 
 
 
 
158
  try:
159
  response = requests.post(submit_url, json=submission_data, timeout=60)
160
  response.raise_for_status()
@@ -171,6 +360,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
171
  status_message = f"❌ Invio Fallito: {e}"
172
  return status_message, pd.DataFrame(results_log)
173
 
 
174
  # --- Build Gradio Interface ---
175
  with gr.Blocks() as demo:
176
  gr.Markdown("# 🚀 Super Agente - Final Assignment Runner")
@@ -185,6 +375,4 @@ with gr.Blocks() as demo:
185
  )
186
 
187
  if __name__ == "__main__":
188
- space_host_startup = os.getenv("SPACE_HOST")
189
- space_id_startup = os.getenv("SPACE_ID")
190
  demo.launch(debug=True, share=False)
 
9
  # --- Constants ---
10
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
11
 
12
+
13
  # ==========================================
14
+ # 🔧 TOOL 1: LETTURA WEBPAGE
15
  # ==========================================
16
  @tool
17
  def visit_webpage(url: str) -> str:
18
+ """Visits a webpage and extracts its main clean text content.
19
+ Use this to read Wikipedia pages, news articles, or any online resource.
20
  Args:
21
+ url: The full URL of the webpage to visit (e.g. 'https://en.wikipedia.org/wiki/...')
22
  """
23
  try:
24
+ headers = {
25
+ 'User-Agent': (
26
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
27
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
28
+ 'Chrome/91.0.4472.124 Safari/537.36'
29
+ )
30
+ }
31
  response = requests.get(url, headers=headers, timeout=15)
32
  response.raise_for_status()
 
33
  soup = BeautifulSoup(response.text, 'html.parser')
 
34
  for element in soup(["script", "style", "nav", "footer", "header", "aside"]):
35
  element.extract()
 
36
  text = soup.get_text(separator='\n', strip=True)
37
+ return text[:15000]
 
38
  except Exception as e:
39
  return f"Error reading the webpage: {str(e)}"
40
 
41
+
42
+ # ==========================================
43
+ # 🎬 TOOL 2: TRASCRIZIONE YOUTUBE
44
+ # ==========================================
45
+ @tool
46
+ def get_youtube_transcript(video_url: str) -> str:
47
+ """Fetches the transcript/captions of a YouTube video.
48
+ Use this whenever the question refers to a YouTube video URL.
49
+ Args:
50
+ video_url: The full YouTube video URL (e.g. 'https://www.youtube.com/watch?v=...')
51
+ """
52
+ try:
53
+ from youtube_transcript_api import YouTubeTranscriptApi
54
+ match = re.search(r'(?:v=|youtu\.be/)([^&\n?#]+)', video_url)
55
+ if not match:
56
+ return "Could not extract video ID from URL."
57
+ video_id = match.group(1)
58
+ transcript_list = YouTubeTranscriptApi.get_transcript(video_id, languages=['en', 'it', 'auto'])
59
+ full_text = ' '.join([entry['text'] for entry in transcript_list])
60
+ return full_text[:10000]
61
+ except Exception as e:
62
+ return f"Transcript not available: {str(e)}"
63
+
64
+
65
+ # ==========================================
66
+ # 📂 TOOL 3: DOWNLOAD FILE DA GAIA
67
+ # ==========================================
68
+ @tool
69
+ def download_task_file(task_id: str) -> str:
70
+ """Downloads and reads the file attached to a GAIA task (if any).
71
+ Returns the text content of the file or a description of it.
72
+ Args:
73
+ task_id: The task_id string from the GAIA question.
74
+ """
75
+ try:
76
+ file_url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
77
+ response = requests.get(file_url, timeout=15)
78
+ if response.status_code == 404:
79
+ return "No file attached to this task."
80
+ response.raise_for_status()
81
+ content_type = response.headers.get('Content-Type', '')
82
+
83
+ if 'text' in content_type or 'json' in content_type or 'csv' in content_type:
84
+ return response.text[:10000]
85
+
86
+ if 'pdf' in content_type:
87
+ try:
88
+ import io
89
+ import PyPDF2
90
+ pdf_reader = PyPDF2.PdfReader(io.BytesIO(response.content))
91
+ text = ''
92
+ for page in pdf_reader.pages:
93
+ text += page.extract_text() or ''
94
+ return text[:10000]
95
+ except Exception:
96
+ return f"PDF downloaded ({len(response.content)} bytes) but could not extract text."
97
+
98
+ if 'image' in content_type:
99
+ return f"Image file attached (content-type: {content_type}). Size: {len(response.content)} bytes. Cannot parse directly."
100
+
101
+ # fallback: try as plain text
102
+ try:
103
+ return response.content.decode('utf-8')[:10000]
104
+ except Exception:
105
+ return f"Binary file attached (content-type: {content_type}, size: {len(response.content)} bytes)."
106
+ except Exception as e:
107
+ return f"Error downloading task file: {str(e)}"
108
+
109
+
110
+ # ==========================================
111
+ # 🔍 PRE-PROCESSING DOMANDA
112
+ # ==========================================
113
+ def preprocess_question(question: str) -> str:
114
+ """Handles special question formats before sending to the agent."""
115
+
116
+ # 1. Testo scritto al contrario (reversed text)
117
+ stripped = question.strip()
118
+ reversed_q = stripped[::-1].strip()
119
+ if any(word in reversed_q.lower() for word in ['answer', 'write', 'what', 'who', 'how', 'find', 'list', 'if you']):
120
+ if len(reversed_q) > 10:
121
+ print(f"[PRE-PROCESS] Testo invertito rilevato. Versione corretta: {reversed_q[:80]}")
122
+ return reversed_q
123
+
124
+ return question
125
+
126
+
127
  # ==========================================
128
  # 🧠 IL SUPER AGENTE
129
  # ==========================================
130
  class SuperAgent:
131
  def __init__(self):
132
  print("Inizializzazione del SUPER Agente AI in corso...")
133
+
134
+ # Modello principale — Llama 3.3 70B per ragionamento general-purpose
135
+ self.model = InferenceClientModel(
136
+ model_id="meta-llama/Llama-3.3-70B-Instruct"
137
+ )
138
+
139
+ # Modello di fallback leggero per risposte dirette senza tools
140
+ self.fallback_model = InferenceClientModel(
141
+ model_id="Qwen/Qwen2.5-72B-Instruct"
142
+ )
143
+
144
+ # Tools disponibili
145
+ self.tools = [
146
+ DuckDuckGoSearchTool(),
147
+ visit_webpage,
148
+ get_youtube_transcript,
149
+ download_task_file,
150
+ ]
151
+
152
+ # Agente principale
153
  self.agent = CodeAgent(
154
  tools=self.tools,
155
  model=self.model,
156
+ max_steps=10,
157
  additional_authorized_imports=[
158
+ "requests", "bs4", "json", "time", "math", "datetime",
159
+ "pandas", "numpy", "re", "csv", "urllib", "collections",
160
+ "itertools", "string", "unicodedata"
161
  ]
162
  )
163
+
164
+ # Prompt ottimizzato per GAIA
165
+ self.prompt_template = """You are an expert AI assistant solving the GAIA benchmark evaluation.
166
+ Your goal is to find the EXACT correct answer to the question below.
167
+
168
+ STRATEGY:
169
+ - If the question references a YouTube video URL → use get_youtube_transcript tool first.
170
+ - If the question references a website or Wikipedia use visit_webpage tool.
171
+ - If the question seems to have an attached file use download_task_file with the task_id.
172
+ - For factual questions use DuckDuckGoSearchTool to search, then visit_webpage to confirm.
173
+ - For math, date arithmetic, text manipulation write Python code to compute the answer directly.
174
+ - If the text looks reversed or scrambled reverse it first with Python.
175
+
176
+ OUTPUT RULES (CRITICAL):
177
+ 1. Return ONLY the final answer. No explanation, no preamble.
178
+ 2. Numbers: return just the number (e.g. '3' or '1998').
179
+ 3. Names/words: return just the word or name.
180
+ 4. Lists: return comma-separated values.
181
+ 5. NEVER say "The answer is", "FINAL ANSWER:", "Based on", etc.
182
+
183
+ Question: {question}
184
+ """
185
+
186
+ # Prompt diretto per fallback (senza tools)
187
+ self.direct_prompt = """You are an expert assistant. Answer the following question with ONLY the final answer.
188
+ No explanation. No preamble. Just the answer itself.
189
+
190
+ If the text is reversed, reverse it and answer accordingly.
191
+ If it is a math question, compute and give only the result.
192
+ If it is a factual question, give only the fact.
193
+
194
+ Question: {question}
195
+ Answer:"""
196
+
197
+ def _clean_answer(self, raw: str) -> str:
198
+ """Rimuove prefissi verbosi e formattazione indesiderata dalla risposta."""
199
+ answer = str(raw).strip()
200
+
201
+ # Rimuovi prefissi verbosi comuni
202
+ prefixes = [
203
+ "the answer is", "final answer:", "answer:", "final answer is",
204
+ "the requested word is", "the highest number is", "the result is",
205
+ "based on", "according to", "the word is", "the name is",
206
+ "the number is", "the correct answer is", "the response is",
207
+ ]
208
+ lower = answer.lower()
209
+ for prefix in prefixes:
210
+ if lower.startswith(prefix):
211
+ answer = answer[len(prefix):].strip()
212
+ lower = answer.lower()
213
+ break
214
+ # Cerca anche nel mezzo della stringa come ultima occorrenza
215
+ idx = lower.rfind(prefix)
216
+ if idx != -1:
217
+ candidate = answer[idx + len(prefix):].strip()
218
+ # Solo se il candidate è breve (vera risposta finale)
219
+ if len(candidate) < 200:
220
+ answer = candidate
221
+ lower = answer.lower()
222
+ break
223
+
224
+ # Toglie punto finale
225
+ if answer.endswith('.') and not re.search(r'\d\.$', answer):
226
+ answer = answer[:-1].strip()
227
+
228
+ # Toglie markdown e virgolette extra
229
+ answer = answer.replace("**", "").strip('"').strip("'").strip()
230
+
231
+ return answer
232
+
233
+ def _direct_answer(self, question: str) -> str:
234
+ """Chiede direttamente al modello senza usare l'agente con tools."""
235
+ try:
236
+ prompt = self.direct_prompt.format(question=question)
237
+ response = self.fallback_model([{"role": "user", "content": prompt}])
238
+ # InferenceClientModel restituisce un ChatMessage
239
+ if hasattr(response, 'content'):
240
+ raw = response.content
241
+ else:
242
+ raw = str(response)
243
+ return self._clean_answer(raw)
244
+ except Exception as e:
245
+ print(f"[FALLBACK MODEL ERROR]: {e}")
246
+ return "I don't know"
247
+
248
+ def __call__(self, question: str, task_id: str = "") -> str:
249
+ print(f"\n[DOMANDA]: {question[:100]}...")
250
+
251
+ # Pre-processing
252
+ processed_question = preprocess_question(question)
253
+
254
+ # Se task_id disponibile, aggiungilo al contesto del prompt
255
+ context = ""
256
+ if task_id:
257
+ context = f"\nNote: This question has task_id '{task_id}'. Use download_task_file('{task_id}') if a file might be attached.\n"
258
+
259
+ full_question = processed_question + context
260
+
261
+ # --- TENTATIVO 1: Agente completo con tools ---
262
  try:
263
+ formatted_prompt = self.prompt_template.format(question=full_question)
264
  raw_answer = self.agent.run(formatted_prompt)
265
+ final_answer = self._clean_answer(raw_answer)
266
+
267
+ if final_answer and final_answer.lower() not in ["error", "none", "n/a", ""]:
268
+ print(f"[✅ RISPOSTA AGENTE]: {final_answer}")
269
+ return final_answer
270
+ else:
271
+ print("[⚠️ Agente ha restituito risposta vuota/nulla, provo fallback...]")
 
 
 
 
 
 
 
 
 
 
 
 
272
  except Exception as e:
273
+ print(f"[⚠️ AGENT ERROR]: {e} — provo fallback diretto...")
274
+
275
+ # --- TENTATIVO 2: Modello diretto senza tools ---
276
+ fallback_answer = self._direct_answer(processed_question)
277
+ print(f"[🔄 FALLBACK RISPOSTA]: {fallback_answer}")
278
+ return fallback_answer
279
 
280
 
281
  # ==========================================
282
  # ⚙️ INTERFACCIA E RUNNER
283
  # ==========================================
284
  def run_and_submit_all(profile: gr.OAuthProfile | None):
285
+ space_id = os.getenv("SPACE_ID")
286
 
287
  if profile:
288
  username = f"{profile.username}"
 
298
  agent = SuperAgent()
299
  except Exception as e:
300
  return f"Errore nell'inizializzazione dell'agente: {e}", None
301
+
302
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
303
 
304
  try:
 
306
  response.raise_for_status()
307
  questions_data = response.json()
308
  if not questions_data:
309
+ return "La lista delle domande scaricata è vuota.", None
310
  except Exception as e:
311
  return f"Errore nel download delle domande: {e}", None
312
 
313
  results_log = []
314
  answers_payload = []
315
+
316
  print(f"Avvio elaborazione su {len(questions_data)} domande...")
317
  for item in questions_data:
318
+ task_id = item.get("task_id", "")
319
  question_text = item.get("question")
320
  if not task_id or question_text is None:
321
  continue
322
  try:
323
+ submitted_answer = agent(question_text, task_id=task_id)
324
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
325
+ results_log.append({
326
+ "Task ID": task_id,
327
+ "Question": question_text[:120],
328
+ "Submitted Answer": submitted_answer
329
+ })
330
  except Exception as e:
331
+ answers_payload.append({"task_id": task_id, "submitted_answer": "I don't know"})
332
+ results_log.append({
333
+ "Task ID": task_id,
334
+ "Question": question_text[:120],
335
+ "Submitted Answer": f"AGENT ERROR: {e}"
336
+ })
337
 
338
  if not answers_payload:
339
  return "L'agente non ha prodotto risposte da inviare.", pd.DataFrame(results_log)
340
 
341
+ submission_data = {
342
+ "username": username.strip(),
343
+ "agent_code": agent_code,
344
+ "answers": answers_payload
345
+ }
346
+
347
  try:
348
  response = requests.post(submit_url, json=submission_data, timeout=60)
349
  response.raise_for_status()
 
360
  status_message = f"❌ Invio Fallito: {e}"
361
  return status_message, pd.DataFrame(results_log)
362
 
363
+
364
  # --- Build Gradio Interface ---
365
  with gr.Blocks() as demo:
366
  gr.Markdown("# 🚀 Super Agente - Final Assignment Runner")
 
375
  )
376
 
377
  if __name__ == "__main__":
 
 
378
  demo.launch(debug=True, share=False)