rnrahate007 commited on
Commit
173009f
Β·
verified Β·
1 Parent(s): c6b2eae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +358 -51
app.py CHANGED
@@ -1,84 +1,391 @@
1
  import os
2
  import re
 
 
 
 
 
3
  from google import genai
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  class BasicAgent:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  def __init__(self):
8
  api_key = os.getenv("GEMINI_API_KEY")
9
  if not api_key:
10
- raise ValueError("Missing GEMINI_API_KEY")
 
 
 
11
 
12
- genai.configure(api_key=api_key)
 
 
 
 
13
 
14
- self.model = genai.GenerativeModel("gemini-1.5-flash")
 
 
15
 
16
- print("βœ… PASS Agent Ready")
17
 
18
- def clean(self, text: str) -> str:
19
- if not text:
20
- return "N/A"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- text = text.strip()
 
 
23
 
24
- # remove junk
25
- text = text.replace("Final Answer:", "")
26
- text = text.replace("Answer:", "")
27
- text = text.strip()
 
 
28
 
29
- # keep only first line
30
- text = text.split("\n")[0]
 
 
 
 
31
 
32
- # remove sentences
33
- text = text.split(".")[0]
 
34
 
35
- return text.strip()[:100]
36
 
37
- def __call__(self, question: str, task_id: str = "") -> str:
 
38
  try:
39
- # πŸ”₯ STEP 1 β€” Solve
40
- prompt1 = f"""
41
- Solve the question.
 
 
 
 
 
 
42
 
43
- IMPORTANT:
44
- - Think step by step internally
45
- - Return ONLY final answer
46
- - No explanation
47
 
48
- Question:
49
- {question}
50
- """
51
- r1 = self.model.generate_content(prompt1)
52
- ans1 = r1.text if hasattr(r1, "text") else ""
53
 
54
- ans1 = self.clean(ans1)
 
 
 
 
 
55
 
56
- # πŸ”₯ STEP 2 β€” VERIFY (this is the magic)
57
- prompt2 = f"""
58
- Question: {question}
 
 
 
59
 
60
- Proposed Answer: {ans1}
 
 
61
 
62
- Check if this is correct.
63
- If wrong, fix it.
 
 
 
 
64
 
65
- Return ONLY final answer.
66
- No explanation.
67
- """
68
- r2 = self.model.generate_content(prompt2)
69
- final = r2.text if hasattr(r2, "text") else ""
70
 
71
- final = self.clean(final)
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- # πŸ”₯ fallback if broken
74
- if not final or final == "N/A":
75
- final = ans1
 
76
 
77
- print("Q:", question[:80])
78
- print("A:", final)
 
 
 
 
79
 
80
- return final
 
81
 
 
 
82
  except Exception as e:
83
- print("ERROR:", e)
84
- return "N/A"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import re
3
+ import base64
4
+ import traceback
5
+ import requests
6
+ import gradio as gr
7
+ import pandas as pd
8
  from google import genai
9
+ from google.genai import types
10
+
11
+ # --- Constants ---
12
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
13
+
14
+
15
+ # ── Utilities ─────────────────────────────────────────────────────────
16
+
17
+ def clean_answer(text: str) -> str:
18
+ """Normalize model output for exact-match scoring."""
19
+ if not text:
20
+ return ""
21
+ text = text.strip()
22
+ # Remove markdown fences
23
+ text = re.sub(r"^```[a-zA-Z]*\s*", "", text)
24
+ text = re.sub(r"\s*```$", "", text)
25
+ # Remove common label prefixes
26
+ for prefix in [
27
+ "final answer:", "answer:", "the answer is:",
28
+ "the final answer is:", "result:", "output:",
29
+ ]:
30
+ if text.lower().startswith(prefix):
31
+ text = text[len(prefix):].strip()
32
+ break
33
+ # Collapse whitespace, cap length
34
+ return " ".join(text.split())[:300]
35
+
36
+
37
+ def extract_text(response) -> str:
38
+ """
39
+ Extract the final answer text from a GenerateContentResponse.
40
+ Works for plain text, google_search grounding, and code_execution results.
41
+ Always returns the LAST meaningful text part (= answer after tool use).
42
+ """
43
+ parts_text = []
44
+ try:
45
+ for candidate in response.candidates:
46
+ for part in candidate.content.parts:
47
+ if hasattr(part, "text") and part.text and part.text.strip():
48
+ parts_text.append(part.text.strip())
49
+ if hasattr(part, "code_execution_result") and part.code_execution_result:
50
+ out = getattr(part.code_execution_result, "output", "")
51
+ if out and str(out).strip():
52
+ parts_text.append(str(out).strip())
53
+ except Exception:
54
+ pass
55
+ if parts_text:
56
+ return parts_text[-1]
57
+ try:
58
+ return (response.text or "").strip()
59
+ except Exception:
60
+ return ""
61
+
62
+
63
+ def fetch_task_file(task_id: str) -> tuple:
64
+ """Download file attached to a GAIA task. Returns (bytes, filename)."""
65
+ try:
66
+ r = requests.get(f"{DEFAULT_API_URL}/files/{task_id}", timeout=20)
67
+ if r.status_code == 200 and r.content:
68
+ cd = r.headers.get("Content-Disposition", "")
69
+ name = ""
70
+ if "filename=" in cd:
71
+ name = cd.split("filename=")[-1].strip().strip('"')
72
+ name = name or f"file_{task_id}"
73
+ print(f" [file] {name} ({len(r.content)} bytes)")
74
+ return r.content, name
75
+ except Exception as e:
76
+ print(f" [file] fetch error: {e}")
77
+ return None, ""
78
+
79
+
80
+ def build_contents(question: str, file_bytes, fname: str) -> list:
81
+ """Package question + optional file into Gemini contents list."""
82
+ if file_bytes is None:
83
+ return [question]
84
+ ext = fname.rsplit(".", 1)[-1].lower() if "." in fname else ""
85
+
86
+ IMAGE_MIME = {"png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg",
87
+ "gif":"image/gif","webp":"image/webp","bmp":"image/bmp"}
88
+ AUDIO_MIME = {"mp3":"audio/mpeg","wav":"audio/wav","ogg":"audio/ogg",
89
+ "flac":"audio/flac","m4a":"audio/mp4"}
90
+
91
+ if ext in IMAGE_MIME:
92
+ return [types.Part.from_bytes(data=file_bytes, mime_type=IMAGE_MIME[ext]), question]
93
+ if ext == "pdf":
94
+ return [types.Part.from_bytes(data=file_bytes, mime_type="application/pdf"), question]
95
+ if ext in AUDIO_MIME:
96
+ return [types.Part.from_bytes(data=file_bytes, mime_type=AUDIO_MIME[ext]), question]
97
+
98
+ # Text-based files: embed as context
99
+ try:
100
+ txt = file_bytes.decode("utf-8", errors="replace")
101
+ return [f"Attached file ({fname}):\n```\n{txt[:12000]}\n```\n\n{question}"]
102
+ except Exception:
103
+ b64 = base64.b64encode(file_bytes).decode()
104
+ return [f"Attached file ({fname}) base64:\n{b64[:2000]}\n\n{question}"]
105
+
106
+
107
+ # ── Agent ─────────────────────────────────────────────────────────────
108
 
109
  class BasicAgent:
110
+ """
111
+ Gemini 2.0 Flash agent with Google Search grounding, code execution,
112
+ and file-attachment support for GAIA benchmark questions.
113
+ """
114
+
115
+ SYSTEM = """You are a precise expert assistant solving GAIA benchmark evaluation questions.
116
+
117
+ CRITICAL OUTPUT RULE:
118
+ Output ONLY the final answer β€” nothing else.
119
+ No explanation, no reasoning, no preamble, no trailing sentence.
120
+
121
+ FORMAT RULES:
122
+ - Numbers : digits only, no thousand-separators. Drop .0 from whole numbers.
123
+ - Lists : comma-separated values, alphabetical order unless otherwise specified.
124
+ - Yes/No : exactly "yes" or "no" (lowercase).
125
+ - Names : full name unless only first or last is requested.
126
+ - Dates : match the format implied by the question.
127
+ - Units : include units only if the question asks for them.
128
+
129
+ STRATEGY:
130
+ 1. Read the question (and any attached file) carefully.
131
+ 2. Use Google Search for any fact, date, count, name, or external data you need.
132
+ 3. Use code execution for arithmetic, unit conversion, or data analysis.
133
+ 4. Think step-by-step internally.
134
+ 5. Output ONLY the single final answer."""
135
+
136
+ CODE_KEYWORDS = {
137
+ "calculate","compute","sum","total","average","mean","median",
138
+ "percentage","multiply","divide","convert","how many","count",
139
+ "square root","power","factorial","modulo","remainder",
140
+ }
141
 
142
  def __init__(self):
143
  api_key = os.getenv("GEMINI_API_KEY")
144
  if not api_key:
145
+ raise EnvironmentError("GEMINI_API_KEY environment variable is not set.")
146
+
147
+ self.client = genai.Client(api_key=api_key)
148
+ self.model = "gemini-2.0-flash"
149
 
150
+ cfg = lambda tools: types.GenerateContentConfig(
151
+ system_instruction=self.SYSTEM,
152
+ tools=tools,
153
+ temperature=0,
154
+ )
155
 
156
+ self.search_cfg = cfg([types.Tool(google_search=types.GoogleSearch())])
157
+ self.code_cfg = cfg([types.Tool(code_execution=types.ToolCodeExecution())])
158
+ self.plain_cfg = cfg([]) # for file questions (model reads the file itself)
159
 
160
+ print(f"BasicAgent initialized (model={self.model})")
161
 
162
+ def __call__(self, question: str) -> str:
163
+ # task_id is embedded in the question only when called from run_and_submit_all;
164
+ # we extract it via a side-channel attribute set before each call.
165
+ task_id = getattr(self, "_current_task_id", "")
166
+ try:
167
+ return self._run(question, task_id)
168
+ except Exception:
169
+ print(traceback.format_exc())
170
+ # Last-resort plain call β€” never return empty
171
+ try:
172
+ r = self.client.models.generate_content(
173
+ model=self.model, contents=question, config=self.plain_cfg)
174
+ ans = clean_answer(extract_text(r))
175
+ return ans if ans else self._forced_answer(question)
176
+ except Exception:
177
+ return self._forced_answer(question)
178
+
179
+ def _run(self, question: str, task_id: str) -> str:
180
+ file_bytes, fname = fetch_task_file(task_id) if task_id else (None, "")
181
+ contents = build_contents(question, file_bytes, fname)
182
+ q_lower = question.lower()
183
+
184
+ has_file = file_bytes is not None
185
+ needs_code = not has_file and any(kw in q_lower for kw in self.CODE_KEYWORDS)
186
 
187
+ config = self.plain_cfg if has_file else (
188
+ self.code_cfg if needs_code else
189
+ self.search_cfg)
190
 
191
+ resp = self.client.models.generate_content(
192
+ model=self.model, contents=contents, config=config)
193
+ raw = extract_text(resp)
194
+ ans = clean_answer(raw)
195
+ print(f" raw : {raw[:120]!r}")
196
+ print(f" ans : {ans!r}")
197
 
198
+ # If empty, retry with search
199
+ if not ans:
200
+ resp2 = self.client.models.generate_content(
201
+ model=self.model, contents=contents, config=self.search_cfg)
202
+ ans = clean_answer(extract_text(resp2))
203
+ print(f" retry: {ans!r}")
204
 
205
+ # Still empty β†’ force a plain answer (never return blank)
206
+ if not ans:
207
+ ans = self._forced_answer(question)
208
 
209
+ return ans
210
 
211
+ def _forced_answer(self, question: str) -> str:
212
+ """Absolute last resort β€” plain call with no tools, no format rules."""
213
  try:
214
+ r = self.client.models.generate_content(
215
+ model=self.model,
216
+ contents=f"Answer in one word or number only:\n{question}",
217
+ config=types.GenerateContentConfig(temperature=0),
218
+ )
219
+ ans = clean_answer(extract_text(r))
220
+ return ans if ans else "unknown"
221
+ except Exception:
222
+ return "unknown"
223
 
 
 
 
 
224
 
225
+ # ── Gradio runner (original HF template structure preserved) ──────────
 
 
 
 
226
 
227
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
228
+ """
229
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
230
+ and displays the results.
231
+ """
232
+ space_id = os.getenv("SPACE_ID")
233
 
234
+ if profile:
235
+ username = f"{profile.username}"
236
+ print(f"User logged in: {username}")
237
+ else:
238
+ print("User not logged in.")
239
+ return "Please Login to Hugging Face with the button.", None
240
 
241
+ api_url = DEFAULT_API_URL
242
+ questions_url = f"{api_url}/questions"
243
+ submit_url = f"{api_url}/submit"
244
 
245
+ # 1. Instantiate Agent
246
+ try:
247
+ agent = BasicAgent()
248
+ except Exception as e:
249
+ print(f"Error instantiating agent: {e}")
250
+ return f"Error initializing agent: {e}", None
251
 
252
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
253
+ print(agent_code)
 
 
 
254
 
255
+ # 2. Fetch Questions
256
+ print(f"Fetching questions from: {questions_url}")
257
+ try:
258
+ response = requests.get(questions_url, timeout=15)
259
+ response.raise_for_status()
260
+ questions_data = response.json()
261
+ if not questions_data:
262
+ return "Fetched questions list is empty or invalid format.", None
263
+ print(f"Fetched {len(questions_data)} questions.")
264
+ except requests.exceptions.RequestException as e:
265
+ return f"Error fetching questions: {e}", None
266
+ except Exception as e:
267
+ return f"An unexpected error occurred fetching questions: {e}", None
268
 
269
+ # 3. Run Agent
270
+ results_log = []
271
+ answers_payload = []
272
+ print(f"Running agent on {len(questions_data)} questions...")
273
 
274
+ for item in questions_data:
275
+ task_id = item.get("task_id")
276
+ question_text = item.get("question")
277
+ if not task_id or question_text is None:
278
+ print(f"Skipping item with missing task_id or question: {item}")
279
+ continue
280
 
281
+ print(f"\n[{task_id}] {question_text[:120]}")
282
+ agent._current_task_id = task_id # pass task_id for file download
283
 
284
+ try:
285
+ submitted_answer = agent(question_text)
286
  except Exception as e:
287
+ submitted_answer = "unknown"
288
+ print(f"Error running agent on task {task_id}: {e}")
289
+
290
+ print(f" β†’ submitted: {submitted_answer!r}")
291
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
292
+ results_log.append({
293
+ "Task ID": task_id,
294
+ "Question": question_text,
295
+ "Submitted Answer": submitted_answer,
296
+ })
297
+
298
+ if not answers_payload:
299
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
300
+
301
+ # 4. Prepare Submission
302
+ submission_data = {
303
+ "username": username.strip(),
304
+ "agent_code": agent_code,
305
+ "answers": answers_payload,
306
+ }
307
+ print(f"Submitting {len(answers_payload)} answers for user '{username}'...")
308
+
309
+ # 5. Submit
310
+ try:
311
+ response = requests.post(submit_url, json=submission_data, timeout=60)
312
+ response.raise_for_status()
313
+ result_data = response.json()
314
+ final_status = (
315
+ f"Submission Successful!\n"
316
+ f"User: {result_data.get('username')}\n"
317
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
318
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
319
+ f"Message: {result_data.get('message', 'No message received.')}"
320
+ )
321
+ print("Submission successful.")
322
+ return final_status, pd.DataFrame(results_log)
323
+ except requests.exceptions.HTTPError as e:
324
+ error_detail = f"Server responded with status {e.response.status_code}."
325
+ try:
326
+ error_json = e.response.json()
327
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
328
+ except requests.exceptions.JSONDecodeError:
329
+ error_detail += f" Response: {e.response.text[:500]}"
330
+ status_message = f"Submission Failed: {error_detail}"
331
+ print(status_message)
332
+ return status_message, pd.DataFrame(results_log)
333
+ except requests.exceptions.Timeout:
334
+ return "Submission Failed: The request timed out.", pd.DataFrame(results_log)
335
+ except requests.exceptions.RequestException as e:
336
+ return f"Submission Failed: Network error - {e}", pd.DataFrame(results_log)
337
+ except Exception as e:
338
+ return f"An unexpected error occurred during submission: {e}", pd.DataFrame(results_log)
339
+
340
+
341
+ # --- Build Gradio Interface using Blocks ---
342
+ with gr.Blocks() as demo:
343
+ gr.Markdown("# Basic Agent Evaluation Runner")
344
+ gr.Markdown(
345
+ """
346
+ **Instructions:**
347
+
348
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
349
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
350
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
351
+
352
+ ---
353
+ **Disclaimers:**
354
+ 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).
355
+ 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.
356
+ """
357
+ )
358
+
359
+ gr.LoginButton()
360
+
361
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
362
+
363
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
364
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
365
+
366
+ run_button.click(
367
+ fn=run_and_submit_all,
368
+ outputs=[status_output, results_table]
369
+ )
370
+
371
+ if __name__ == "__main__":
372
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
373
+ space_host_startup = os.getenv("SPACE_HOST")
374
+ space_id_startup = os.getenv("SPACE_ID")
375
+
376
+ if space_host_startup:
377
+ print(f"βœ… SPACE_HOST found: {space_host_startup}")
378
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
379
+ else:
380
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
381
+
382
+ if space_id_startup:
383
+ print(f"βœ… SPACE_ID found: {space_id_startup}")
384
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
385
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
386
+ else:
387
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
388
+
389
+ print("-"*(60 + len(" App Starting ")) + "\n")
390
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
391
+ demo.launch(debug=True, share=False)