rnrahate007 commited on
Commit
81c8e68
Β·
verified Β·
1 Parent(s): 27dd1e5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -106
app.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  import re
 
3
  import traceback
4
  import requests
5
  import gradio as gr
@@ -9,16 +10,14 @@ from google.genai import types
9
 
10
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
11
 
 
12
 
13
- # ── Answer cleaner ────────────────────────────────────────────────────
14
  def clean_answer(text: str) -> str:
15
  if not text:
16
  return "N/A"
17
  text = text.strip()
18
- # Strip code fences
19
  text = re.sub(r"^```[a-zA-Z]*\s*", "", text)
20
  text = re.sub(r"\s*```$", "", text)
21
- # Strip label prefixes
22
  for prefix in [
23
  "final answer:", "answer:", "the answer is:",
24
  "the final answer is:", "result:",
@@ -26,114 +25,197 @@ def clean_answer(text: str) -> str:
26
  if text.lower().startswith(prefix):
27
  text = text[len(prefix):].strip()
28
  break
29
- return " ".join(text.split())[:200]
30
 
31
 
32
  def extract_text(response) -> str:
33
  """
34
- Pull the final answer text out of a GenerateContentResponse.
35
- When google_search is used, Gemini emits:
36
- [search_tool_use part] β†’ [search_tool_result part] β†’ [text part]
37
- We want the LAST text part.
38
  """
39
- text_parts = []
40
  try:
41
  for candidate in response.candidates:
42
  for part in candidate.content.parts:
 
43
  if hasattr(part, "text") and part.text and part.text.strip():
44
- text_parts.append(part.text.strip())
 
 
 
 
 
45
  except Exception as e:
46
  print(f" [extract_text error] {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- if text_parts:
49
- return text_parts[-1] # last text part = final answer after search
50
 
51
- # Last resort: .text property
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  try:
53
- return response.text.strip()
 
 
 
 
 
 
54
  except Exception:
55
- return ""
 
 
 
 
56
 
57
 
58
  # ── Agent ─────────────────────────────────────────────────────────────
 
59
  class BasicAgent:
60
 
61
  SYSTEM = """You are an expert assistant solving GAIA benchmark questions.
62
 
63
- STRICT OUTPUT FORMAT:
64
- - Return ONLY the final answer. No explanation. No preamble. No punctuation tail.
65
- - Numbers: digits only, no commas (e.g. 1234567 not 1,234,567). Omit trailing .0.
66
- - Lists: comma-separated, in the order the question requests (default: alphabetical).
67
- - Yes/no questions: exactly "yes" or "no" (lowercase).
68
- - Dates: use the format the question implies (e.g. "January 5, 1990" or "1990-01-05").
69
- - Names: full name unless the question asks for first/last only.
70
 
71
- Use Google Search whenever you need facts, dates, counts, or external data.
72
- Think carefully step by step, then output ONLY the answer."""
 
 
 
 
 
73
 
74
  def __init__(self):
75
  api_key = os.getenv("GEMINI_API_KEY")
76
  if not api_key:
77
  raise EnvironmentError("GEMINI_API_KEY not set.")
78
-
79
  self.client = genai.Client(api_key=api_key)
80
- self.model_id = "gemini-2.0-flash" # stable + has search grounding
81
 
82
- # Search-enabled config (separate from code-exec to avoid conflicts)
83
  self.search_config = types.GenerateContentConfig(
84
  system_instruction=self.SYSTEM,
85
  tools=[types.Tool(google_search=types.GoogleSearch())],
86
- temperature=0, # deterministic
87
  )
88
-
89
- # Code-execution config (for maths / data questions)
90
  self.code_config = types.GenerateContentConfig(
91
  system_instruction=self.SYSTEM,
92
  tools=[types.Tool(code_execution=types.ToolCodeExecution())],
93
  temperature=0,
94
  )
95
-
96
- # Plain config for the verify pass
97
  self.plain_config = types.GenerateContentConfig(
98
- system_instruction=(
99
- "You are a strict answer formatter. "
100
- "Return ONLY the final answer, no explanation."
101
- ),
102
  temperature=0,
103
  )
 
104
 
105
- print(f"βœ… BasicAgent ready (model={self.model_id})")
106
-
107
- # ------------------------------------------------------------------
108
- def __call__(self, question: str) -> str:
109
  try:
110
- return self._run(question)
111
  except Exception as exc:
112
  print(f" [FATAL] {exc}\n{traceback.format_exc()}")
113
  return "N/A"
114
 
115
- def _run(self, question: str) -> str:
116
- q_lower = question.lower()
 
 
 
117
 
118
- # Route to code-exec for pure maths / counting questions
119
- needs_code = any(kw in q_lower for kw in [
120
- "calculate", "compute", "how many", "sum of", "average",
121
- "percentage", "multiply", "divide", "convert",
 
 
 
 
122
  ])
123
- config = self.code_config if needs_code else self.search_config
124
 
125
- resp = self.client.models.generate_content(
126
- model=self.model_id, contents=question, config=config,
 
 
 
 
 
 
 
 
127
  )
128
- raw = extract_text(resp)
129
- ans = clean_answer(raw)
130
- print(f" [raw] {raw[:120]!r}")
131
  print(f" [ans] {ans!r}")
132
 
 
133
  if not ans or ans == "N/A":
134
- # Fallback: try plain (no tools)
135
  resp2 = self.client.models.generate_content(
136
- model=self.model_id, contents=question, config=self.plain_config,
137
  )
138
  ans = clean_answer(extract_text(resp2))
139
  print(f" [fallback] {ans!r}")
@@ -141,69 +223,87 @@ Think carefully step by step, then output ONLY the answer."""
141
  return ans or "N/A"
142
 
143
 
144
- # ── Debug helper – fetch & show first N answers without submitting ────
145
- def debug_first_n(n: int = 3):
146
- """Call this from __main__ to see raw vs cleaned answers."""
147
- agent = BasicAgent()
148
- resp = requests.get(f"{DEFAULT_API_URL}/questions", timeout=15)
149
- questions = resp.json()[:n]
150
- for item in questions:
151
- print("\n" + "="*60)
152
- print(f"Q: {item['question']}")
153
- ans = agent(item["question"])
154
- print(f"FINAL SUBMITTED: {ans!r}")
155
-
156
-
157
- # ── Gradio runner ─────────────────────────────────────────────────────
158
- def run_and_submit_all(profile: gr.OAuthProfile | None):
159
- if not profile:
160
- return "Please log in to Hugging Face first.", None
161
-
162
- username = profile.username
163
- space_id = os.getenv("SPACE_ID", "")
164
- agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
165
 
166
- try:
167
- agent = BasicAgent()
168
- except Exception as e:
169
- return f"Error initialising agent: {e}", None
170
-
171
- # Fetch questions
172
  try:
173
  r = requests.get(f"{DEFAULT_API_URL}/questions", timeout=15)
174
  r.raise_for_status()
175
- questions_data = r.json()
176
- print(f"Fetched {len(questions_data)} questions.")
 
177
  except Exception as e:
178
- return f"Error fetching questions: {e}", None
179
 
180
- results_log, answers_payload = [], []
181
 
 
 
182
  for item in questions_data:
183
- task_id = item.get("task_id")
184
- q_text = item.get("question")
185
  if not task_id or q_text is None:
186
  continue
187
-
188
  print(f"\n── {task_id} ──")
189
  print(f"Q: {q_text[:150]}")
190
  try:
191
- answer = agent(q_text)
192
  except Exception as e:
193
  answer = "N/A"
194
  print(f" ERROR: {e}")
195
-
196
  answers_payload.append({"task_id": task_id, "submitted_answer": answer})
197
  results_log.append({
198
- "Task ID": task_id,
199
- "Question": q_text[:120],
200
  "Submitted Answer": answer,
 
201
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  if not answers_payload:
204
  return "No answers produced.", pd.DataFrame(results_log)
205
 
206
- # Submit
207
  try:
208
  r = requests.post(
209
  f"{DEFAULT_API_URL}/submit",
@@ -229,26 +329,29 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
229
 
230
 
231
  # ── UI ────────────────────────────────────────────────────────────────
 
232
  with gr.Blocks() as demo:
233
- gr.Markdown("# GAIA Agent β€” Gemini 2.0 Flash + Search")
234
  gr.Markdown(
235
- "1. Set `GEMINI_API_KEY` in Space secrets.\n"
236
- "2. Log in below.\n"
237
- "3. Click **Run Evaluation**."
238
  )
239
  gr.LoginButton()
240
- btn = gr.Button("β–Ά Run Evaluation & Submit All Answers", variant="primary")
241
- status = gr.Textbox(label="Status", lines=6, interactive=False)
242
- table = gr.DataFrame(label="Answers", wrap=True)
243
- btn.click(fn=run_and_submit_all, outputs=[status, table])
 
 
 
 
 
 
244
 
245
 
246
  if __name__ == "__main__":
247
  print("\n── Env check ──")
248
  for v in ("SPACE_HOST", "SPACE_ID", "GEMINI_API_KEY"):
249
  print(f" {v}: {'SET' if os.getenv(v) else 'MISSING'}")
250
-
251
- # Uncomment to preview first 3 answers before submitting:
252
- # debug_first_n(3)
253
-
254
  demo.launch(debug=True, share=False)
 
1
  import os
2
  import re
3
+ import base64
4
  import traceback
5
  import requests
6
  import gradio as gr
 
10
 
11
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
12
 
13
+ # ── Helpers ───────────────────────────────────────────────────────────
14
 
 
15
  def clean_answer(text: str) -> str:
16
  if not text:
17
  return "N/A"
18
  text = text.strip()
 
19
  text = re.sub(r"^```[a-zA-Z]*\s*", "", text)
20
  text = re.sub(r"\s*```$", "", text)
 
21
  for prefix in [
22
  "final answer:", "answer:", "the answer is:",
23
  "the final answer is:", "result:",
 
25
  if text.lower().startswith(prefix):
26
  text = text[len(prefix):].strip()
27
  break
28
+ return " ".join(text.split())[:300]
29
 
30
 
31
  def extract_text(response) -> str:
32
  """
33
+ Collect ALL text and code-execution output parts, return the last one.
34
+ Handles: plain text, google_search grounding, code_execution output.
 
 
35
  """
36
+ parts_text = []
37
  try:
38
  for candidate in response.candidates:
39
  for part in candidate.content.parts:
40
+ # Plain text part
41
  if hasattr(part, "text") and part.text and part.text.strip():
42
+ parts_text.append(part.text.strip())
43
+ # Code execution result
44
+ if hasattr(part, "code_execution_result") and part.code_execution_result:
45
+ out = getattr(part.code_execution_result, "output", "")
46
+ if out and str(out).strip():
47
+ parts_text.append(str(out).strip())
48
  except Exception as e:
49
  print(f" [extract_text error] {e}")
50
+ if parts_text:
51
+ return parts_text[-1]
52
+ try:
53
+ t = response.text
54
+ return t.strip() if t else ""
55
+ except Exception:
56
+ return ""
57
+
58
+
59
+ def fetch_file(task_id: str) -> tuple[bytes | None, str]:
60
+ """
61
+ Download the file attached to a GAIA task.
62
+ Returns (file_bytes, filename) or (None, "").
63
+ """
64
+ url = f"{DEFAULT_API_URL}/files/{task_id}"
65
+ try:
66
+ r = requests.get(url, timeout=20)
67
+ if r.status_code == 200:
68
+ # Try to get filename from Content-Disposition header
69
+ cd = r.headers.get("Content-Disposition", "")
70
+ fname = ""
71
+ if "filename=" in cd:
72
+ fname = cd.split("filename=")[-1].strip().strip('"')
73
+ if not fname:
74
+ fname = f"attachment_{task_id}"
75
+ print(f" [file] Downloaded {len(r.content)} bytes ({fname})")
76
+ return r.content, fname
77
+ except Exception as e:
78
+ print(f" [file] Download failed: {e}")
79
+ return None, ""
80
 
 
 
81
 
82
+ def build_contents(question: str, file_bytes: bytes | None, fname: str) -> list:
83
+ """
84
+ Build the `contents` list for generate_content(), optionally including a file.
85
+ Supports: images, PDFs, plain-text, CSV, audio.
86
+ """
87
+ if file_bytes is None:
88
+ return [question]
89
+
90
+ ext = fname.rsplit(".", 1)[-1].lower() if "." in fname else ""
91
+
92
+ # --- Image ---
93
+ if ext in ("png", "jpg", "jpeg", "gif", "webp", "bmp"):
94
+ mime = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
95
+ "gif": "image/gif", "webp": "image/webp", "bmp": "image/bmp"}.get(ext, "image/png")
96
+ return [
97
+ types.Part.from_bytes(data=file_bytes, mime_type=mime),
98
+ question,
99
+ ]
100
+
101
+ # --- PDF ---
102
+ if ext == "pdf":
103
+ return [
104
+ types.Part.from_bytes(data=file_bytes, mime_type="application/pdf"),
105
+ question,
106
+ ]
107
+
108
+ # --- Audio ---
109
+ if ext in ("mp3", "wav", "ogg", "flac", "m4a"):
110
+ mime = {"mp3": "audio/mpeg", "wav": "audio/wav", "ogg": "audio/ogg",
111
+ "flac": "audio/flac", "m4a": "audio/mp4"}.get(ext, "audio/mpeg")
112
+ return [
113
+ types.Part.from_bytes(data=file_bytes, mime_type=mime),
114
+ question,
115
+ ]
116
+
117
+ # --- Plain text / CSV / code β€” embed as text context ---
118
  try:
119
+ text_content = file_bytes.decode("utf-8", errors="replace")
120
+ augmented = (
121
+ f"The following file ({fname}) is attached:\n\n"
122
+ f"```\n{text_content[:8000]}\n```\n\n"
123
+ f"{question}"
124
+ )
125
+ return [augmented]
126
  except Exception:
127
+ pass
128
+
129
+ # Last resort: base64-encode unknown binary
130
+ b64 = base64.b64encode(file_bytes).decode()
131
+ return [f"[File {fname} base64]: {b64[:500]}…\n\n{question}"]
132
 
133
 
134
  # ── Agent ─────────────────────────────────────────────────────────────
135
+
136
  class BasicAgent:
137
 
138
  SYSTEM = """You are an expert assistant solving GAIA benchmark questions.
139
 
140
+ RULES:
141
+ - Use Google Search for any factual, date, count, or real-world question.
142
+ - If a file is attached, read it carefully before answering.
143
+ - Think step-by-step internally, but output ONLY the final answer.
 
 
 
144
 
145
+ STRICT OUTPUT FORMAT:
146
+ - Return ONLY the final answer. No explanation, no preamble, no trailing punctuation.
147
+ - Numbers: digits only, no commas (1234567 not 1,234,567). Drop trailing .0 from whole numbers.
148
+ - Lists: comma-separated, alphabetical order unless stated otherwise.
149
+ - Yes/no: exactly "yes" or "no" (lowercase).
150
+ - Names: full name unless the question asks for first or last name only.
151
+ - Dates: match the format implied by the question."""
152
 
153
  def __init__(self):
154
  api_key = os.getenv("GEMINI_API_KEY")
155
  if not api_key:
156
  raise EnvironmentError("GEMINI_API_KEY not set.")
 
157
  self.client = genai.Client(api_key=api_key)
158
+ self.model_id = "gemini-2.0-flash"
159
 
 
160
  self.search_config = types.GenerateContentConfig(
161
  system_instruction=self.SYSTEM,
162
  tools=[types.Tool(google_search=types.GoogleSearch())],
163
+ temperature=0,
164
  )
 
 
165
  self.code_config = types.GenerateContentConfig(
166
  system_instruction=self.SYSTEM,
167
  tools=[types.Tool(code_execution=types.ToolCodeExecution())],
168
  temperature=0,
169
  )
 
 
170
  self.plain_config = types.GenerateContentConfig(
171
+ system_instruction=self.SYSTEM,
 
 
 
172
  temperature=0,
173
  )
174
+ print(f"βœ… BasicAgent ready ({self.model_id})")
175
 
176
+ def __call__(self, question: str, task_id: str = "") -> str:
 
 
 
177
  try:
178
+ return self._run(question, task_id)
179
  except Exception as exc:
180
  print(f" [FATAL] {exc}\n{traceback.format_exc()}")
181
  return "N/A"
182
 
183
+ def _run(self, question: str, task_id: str) -> str:
184
+ # Try to download attached file
185
+ file_bytes, fname = (None, "")
186
+ if task_id:
187
+ file_bytes, fname = fetch_file(task_id)
188
 
189
+ contents = build_contents(question, file_bytes, fname)
190
+ q_lower = question.lower()
191
+
192
+ # Choose config
193
+ has_file = file_bytes is not None
194
+ needs_code = any(kw in q_lower for kw in [
195
+ "calculate", "compute", "sum of", "average", "percentage",
196
+ "multiply", "divide", "convert", "how many",
197
  ])
 
198
 
199
+ if has_file:
200
+ # Files: use plain config (search can't see the file)
201
+ config = self.plain_config
202
+ elif needs_code:
203
+ config = self.code_config
204
+ else:
205
+ config = self.search_config
206
+
207
+ resp = self.client.models.generate_content(
208
+ model=self.model_id, contents=contents, config=config,
209
  )
210
+ raw = extract_text(resp)
211
+ ans = clean_answer(raw)
212
+ print(f" [raw] {raw[:150]!r}")
213
  print(f" [ans] {ans!r}")
214
 
215
+ # Fallback: plain call if empty
216
  if not ans or ans == "N/A":
 
217
  resp2 = self.client.models.generate_content(
218
+ model=self.model_id, contents=contents, config=self.plain_config,
219
  )
220
  ans = clean_answer(extract_text(resp2))
221
  print(f" [fallback] {ans!r}")
 
223
  return ans or "N/A"
224
 
225
 
226
+ # ── Shared run logic ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ def _fetch_questions() -> list | str:
 
 
 
 
 
229
  try:
230
  r = requests.get(f"{DEFAULT_API_URL}/questions", timeout=15)
231
  r.raise_for_status()
232
+ data = r.json()
233
+ print(f"Fetched {len(data)} questions.")
234
+ return data
235
  except Exception as e:
236
+ return f"Error fetching questions: {e}"
237
 
 
238
 
239
+ def _run_agent_on_questions(agent, questions_data: list) -> tuple[list, list]:
240
+ results_log, answers_payload = [], []
241
  for item in questions_data:
242
+ task_id = item.get("task_id", "")
243
+ q_text = item.get("question", "")
244
  if not task_id or q_text is None:
245
  continue
 
246
  print(f"\n── {task_id} ──")
247
  print(f"Q: {q_text[:150]}")
248
  try:
249
+ answer = agent(q_text, task_id)
250
  except Exception as e:
251
  answer = "N/A"
252
  print(f" ERROR: {e}")
253
+ print(f"β†’ {answer!r}")
254
  answers_payload.append({"task_id": task_id, "submitted_answer": answer})
255
  results_log.append({
256
+ "Task ID": task_id,
257
+ "Question": q_text[:120],
258
  "Submitted Answer": answer,
259
+ "Has File": "yes" if item.get("file_name") else "no",
260
  })
261
+ return results_log, answers_payload
262
+
263
+
264
+ # ── Dry Run (no submit) ───────────────────────────────────────────────
265
+
266
+ def dry_run(profile: gr.OAuthProfile | None):
267
+ if not profile:
268
+ return "Please log in first.", None
269
+ try:
270
+ agent = BasicAgent()
271
+ except Exception as e:
272
+ return f"Agent init error: {e}", None
273
+
274
+ data = _fetch_questions()
275
+ if isinstance(data, str):
276
+ return data, None
277
+
278
+ sample = data[:3] # first 3 questions only
279
+ logs, _ = _run_agent_on_questions(agent, sample)
280
+ return f"Dry run complete β€” showing {len(logs)} answers (not submitted).", pd.DataFrame(logs)
281
+
282
+
283
+ # ── Full Run + Submit ─────────────────────────────────────────────────
284
+
285
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
286
+ if not profile:
287
+ return "Please log in to Hugging Face first.", None
288
+
289
+ username = profile.username
290
+ space_id = os.getenv("SPACE_ID", "")
291
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
292
+
293
+ try:
294
+ agent = BasicAgent()
295
+ except Exception as e:
296
+ return f"Error initialising agent: {e}", None
297
+
298
+ data = _fetch_questions()
299
+ if isinstance(data, str):
300
+ return data, None
301
+
302
+ results_log, answers_payload = _run_agent_on_questions(agent, data)
303
 
304
  if not answers_payload:
305
  return "No answers produced.", pd.DataFrame(results_log)
306
 
 
307
  try:
308
  r = requests.post(
309
  f"{DEFAULT_API_URL}/submit",
 
329
 
330
 
331
  # ── UI ────────────────────────────────────────────────────────────────
332
+
333
  with gr.Blocks() as demo:
334
+ gr.Markdown("# GAIA Agent β€” Gemini 2.0 Flash + Search + File Support")
335
  gr.Markdown(
336
+ "**Setup:** Add `GEMINI_API_KEY` to Space secrets, then log in.\n\n"
337
+ "- **Dry Run** β€” answers the first 3 questions and shows results *without* submitting.\n"
338
+ "- **Full Run** β€” answers all questions and submits to the leaderboard."
339
  )
340
  gr.LoginButton()
341
+
342
+ with gr.Row():
343
+ dry_btn = gr.Button("πŸ” Dry Run (first 3, no submit)", variant="secondary")
344
+ full_btn = gr.Button("β–Ά Full Run & Submit All", variant="primary")
345
+
346
+ status = gr.Textbox(label="Status", lines=6, interactive=False)
347
+ table = gr.DataFrame(label="Answers", wrap=True)
348
+
349
+ dry_btn.click( fn=dry_run, outputs=[status, table])
350
+ full_btn.click(fn=run_and_submit_all, outputs=[status, table])
351
 
352
 
353
  if __name__ == "__main__":
354
  print("\n── Env check ──")
355
  for v in ("SPACE_HOST", "SPACE_ID", "GEMINI_API_KEY"):
356
  print(f" {v}: {'SET' if os.getenv(v) else 'MISSING'}")
 
 
 
 
357
  demo.launch(debug=True, share=False)