DukeDDrake1999 commited on
Commit
11ffa11
·
verified ·
1 Parent(s): 635387e

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +535 -535
agent.py CHANGED
@@ -1,535 +1,535 @@
1
- import ast
2
- import io
3
- import mimetypes
4
- import os
5
- import re
6
- import tempfile
7
- import time
8
- from pathlib import Path
9
-
10
- import pandas as pd
11
- import requests
12
- from dotenv import load_dotenv
13
- from google import genai
14
- from google.genai import types as genai_types
15
- from smolagents import CodeAgent, DuckDuckGoSearchTool, OpenAIModel, Tool
16
-
17
-
18
- load_dotenv()
19
-
20
- DEFAULT_SCORING_API_URL = "https://agents-course-unit4-scoring.hf.space"
21
- DEFAULT_GEMINI_MODEL_ID = os.getenv("GEMINI_MODEL_ID", "gemini/gemini-2.0-flash")
22
- GEMINI_OPENAI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
23
- GAIA_OUTPUT_PROMPT = """
24
- You are an evaluation-grade GAIA benchmark AI assistant. Your execution must be perfectly precise and highly deterministic. Your sole purpose is to isolate and output the exact, minimal final answer.
25
-
26
- ### CORE DIRECTIVE
27
- You must NEVER output explanations, intermediate reasoning, conversational filler, or comments.
28
- Your entire output must consist ONLY of the final answer, strictly enclosed within square brackets.
29
- Format: [FINAL_ANSWER]
30
-
31
- ### DATA FORMATTING RULES
32
- 1. NUMERICAL DATA:
33
- - Output digits only (e.g., [4], not [four]).
34
- - Exclude commas, currency symbols, percentage signs, or physical units unless the prompt explicitly dictates their inclusion.
35
- - Strip all approximation qualifiers (e.g., "around", "roughly", "~").
36
-
37
- 2. STRING DATA:
38
- - Omit definite and indefinite articles ("a", "an", "the").
39
- - Use full, unabbreviated words unless the prompt specifically requests an abbreviation.
40
-
41
- 3. LISTS AND SETS:
42
- - Output as a single comma-separated string with exactly one space after each comma (e.g., [a, b, c]).
43
- - Exclude conjunctions (e.g., do not use "and" or "or").
44
- - Exclude internal list wrappers (do not include {} or () inside the main answer brackets).
45
- - Sort elements alphabetically or numerically in ascending order unless the prompt specifies an alternative sorting logic.
46
-
47
- ### EXECUTION PROTOCOL
48
- 1. SOURCE EXTRACTION: When processing data from web searches, files (via `run_query_with_file`), or video tools, extract only the atomic fact that satisfies the query. Do not summarize or quote surrounding context.
49
- 2. LITERALISM: Default to the narrowest, most literal interpretation of the prompt. Do not synthesize assumptions.
50
- 3. FALLBACK: If the requisite data to answer is demonstrably absent after exhaustive search, output exactly: [unknown]
51
-
52
- ### EXAMPLES
53
- Q: What is 2 + 2?
54
- A: [4]
55
-
56
- Q: How many studio albums were published by Mercedes Sosa between 2000 and 2009 (inclusive)? Use 2022 English Wikipedia.
57
- A: [3]
58
-
59
- Q: Given the following group table on set S = {a, b, c, d, e}, identify any subset involved in counterexamples to commutativity.
60
- A: [b, e]
61
-
62
- Q: How many at bats did the Yankee with the most walks in the 1977 regular season have that same season?
63
- A: [519]
64
- """.strip()
65
-
66
-
67
- def _get_gemini_api_key() -> str:
68
- api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
69
- if not api_key:
70
- raise RuntimeError(
71
- "Missing Gemini API key. Set GEMINI_API_KEY in your Hugging Face Space secrets."
72
- )
73
- return api_key
74
-
75
-
76
- def _normalize_model_id(model_id: str | None = None) -> str:
77
- selected = model_id or DEFAULT_GEMINI_MODEL_ID
78
- if selected.startswith("gemini/"):
79
- return selected.split("/", 1)[1]
80
- return selected
81
-
82
-
83
- def _genai_client() -> genai.Client:
84
- return genai.Client(api_key=_get_gemini_api_key())
85
-
86
-
87
- def _extract_text(response) -> str:
88
- text = getattr(response, "text", None)
89
- if text:
90
- return text.strip()
91
-
92
- parts: list[str] = []
93
- for candidate in getattr(response, "candidates", []) or []:
94
- content = getattr(candidate, "content", None)
95
- for part in getattr(content, "parts", []) or []:
96
- piece = getattr(part, "text", None)
97
- if piece:
98
- parts.append(piece)
99
- return "\n".join(parts).strip()
100
-
101
-
102
- def _call_gemini_text(prompt: str, system_instruction: str | None = None) -> str:
103
- config = genai_types.GenerateContentConfig(temperature=0)
104
- if system_instruction:
105
- config.system_instruction = system_instruction
106
-
107
- response = _genai_client().models.generate_content(
108
- model=_normalize_model_id(),
109
- contents=prompt,
110
- config=config,
111
- )
112
- return _extract_text(response)
113
-
114
-
115
- def _decode_text_bytes(payload: bytes) -> str:
116
- for encoding in ("utf-8", "utf-8-sig", "cp1252", "latin-1"):
117
- try:
118
- return payload.decode(encoding)
119
- except UnicodeDecodeError:
120
- continue
121
- return payload.decode("utf-8", errors="replace")
122
-
123
-
124
- def _download_task_file(task_id: str) -> tuple[bytes, str]:
125
- response = requests.get(f"{DEFAULT_SCORING_API_URL}/files/{task_id}", timeout=60)
126
- response.raise_for_status()
127
- content_type = response.headers.get("content-type", "application/octet-stream")
128
- return response.content, content_type
129
-
130
-
131
- def _mime_from_name(file_name: str | None, fallback: str) -> str:
132
- guessed, _ = mimetypes.guess_type(file_name or "")
133
- return guessed or fallback or "application/octet-stream"
134
-
135
-
136
- def _wait_until_active(client: genai.Client, uploaded_file) -> None:
137
- state = getattr(uploaded_file, "state", None)
138
- state_name = getattr(state, "name", None)
139
- while state_name and state_name != "ACTIVE":
140
- if state_name == "FAILED":
141
- raise RuntimeError("Gemini file processing failed.")
142
- time.sleep(3)
143
- uploaded_file = client.files.get(name=uploaded_file.name)
144
- state = getattr(uploaded_file, "state", None)
145
- state_name = getattr(state, "name", None)
146
-
147
-
148
- def _query_uploaded_file(file_bytes: bytes, file_name: str, mime_type: str, user_query: str) -> str:
149
- client = _genai_client()
150
- suffix = Path(file_name or "attachment").suffix
151
- temp_path = None
152
- uploaded_file = None
153
-
154
- try:
155
- with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
156
- temp_file.write(file_bytes)
157
- temp_path = temp_file.name
158
-
159
- uploaded_file = client.files.upload(
160
- file=temp_path,
161
- config={"mime_type": mime_type},
162
- )
163
- _wait_until_active(client, uploaded_file)
164
-
165
- response = client.models.generate_content(
166
- model=_normalize_model_id(),
167
- contents=[uploaded_file, user_query],
168
- config=genai_types.GenerateContentConfig(temperature=0),
169
- )
170
- return _extract_text(response)
171
- finally:
172
- if temp_path and os.path.exists(temp_path):
173
- os.remove(temp_path)
174
- if uploaded_file and getattr(uploaded_file, "name", None):
175
- try:
176
- client.files.delete(name=uploaded_file.name)
177
- except Exception:
178
- pass
179
-
180
-
181
- def _excel_to_text(file_bytes: bytes) -> str:
182
- workbook = pd.read_excel(io.BytesIO(file_bytes), sheet_name=None)
183
- sections: list[str] = []
184
- for sheet_name, frame in workbook.items():
185
- csv_text = frame.fillna("").to_csv(index=False)
186
- sections.append(f"Sheet: {sheet_name}\n{csv_text}")
187
- return "\n\n".join(sections)
188
-
189
-
190
- def _textual_file_answer(file_text: str, user_query: str) -> str:
191
- prompt = (
192
- "You are analyzing an attached file for a GAIA benchmark question.\n"
193
- "Answer the question using only the file contents below.\n"
194
- "Return only the direct final answer, with no explanation.\n\n"
195
- f"Question:\n{user_query}\n\n"
196
- f"File contents:\n{file_text[:50000]}"
197
- )
198
- return _call_gemini_text(prompt)
199
-
200
-
201
- def _normalize_riddle_prompt(prompt: str) -> str:
202
- stripped = prompt.strip()
203
- if stripped and stripped.count(" ") > 3:
204
- weird_ratio = sum(char in ".,!?;:'\"()-" for char in stripped) / max(len(stripped), 1)
205
- if weird_ratio < 0.2:
206
- reversed_candidate = stripped[::-1]
207
- if re.search(r"\b(the|and|you|write|understand|sentence)\b", reversed_candidate.lower()):
208
- return reversed_candidate
209
- return stripped
210
-
211
-
212
- def _normalize_final_answer(raw_answer: str) -> str:
213
- cleaned = raw_answer.strip()
214
- if cleaned.startswith("[") and cleaned.endswith("]") and len(cleaned) >= 2:
215
- inner = cleaned[1:-1].strip()
216
- if inner:
217
- return inner
218
- return cleaned
219
-
220
-
221
- class MathSolver(Tool):
222
- name = "math_solver"
223
- description = "Evaluate arithmetic expressions with operators like +, -, *, /, //, %, and **."
224
- inputs = {
225
- "expression": {
226
- "type": "string",
227
- "description": "The arithmetic expression to evaluate.",
228
- }
229
- }
230
- output_type = "string"
231
-
232
- def forward(self, expression: str) -> str:
233
- def eval_node(node: ast.AST) -> int | float:
234
- if isinstance(node, ast.Expression):
235
- return eval_node(node.body)
236
- if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
237
- return node.value
238
- if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
239
- operand = eval_node(node.operand)
240
- return operand if isinstance(node.op, ast.UAdd) else -operand
241
- if isinstance(node, ast.BinOp) and isinstance(
242
- node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow)
243
- ):
244
- left = eval_node(node.left)
245
- right = eval_node(node.right)
246
- if isinstance(node.op, ast.Add):
247
- return left + right
248
- if isinstance(node.op, ast.Sub):
249
- return left - right
250
- if isinstance(node.op, ast.Mult):
251
- return left * right
252
- if isinstance(node.op, ast.Div):
253
- return left / right
254
- if isinstance(node.op, ast.FloorDiv):
255
- return left // right
256
- if isinstance(node.op, ast.Mod):
257
- return left % right
258
- return left**right
259
- raise ValueError("Unsupported expression.")
260
-
261
- try:
262
- parsed = ast.parse(expression, mode="eval")
263
- result = eval_node(parsed)
264
- except Exception as exc:
265
- return f"Math error: {exc}"
266
-
267
- if isinstance(result, float) and result.is_integer():
268
- return str(int(result))
269
- return str(result)
270
-
271
-
272
- class RiddleSolver(Tool):
273
- name = "riddle_solver"
274
- description = "Solve riddles, wordplay, or short trick questions when the wording matters."
275
- inputs = {
276
- "prompt": {
277
- "type": "string",
278
- "description": "The riddle or wordplay prompt to solve.",
279
- }
280
- }
281
- output_type = "string"
282
-
283
- def forward(self, prompt: str) -> str:
284
- normalized_prompt = _normalize_riddle_prompt(prompt)
285
- return _call_gemini_text(
286
- normalized_prompt,
287
- system_instruction=(
288
- "Solve the user's riddle or wordplay. Return only the direct answer with no explanation."
289
- ),
290
- )
291
-
292
-
293
- class TextTransformer(Tool):
294
- name = "text_transformer"
295
- description = "Apply deterministic text transforms like reverse, upper, lower, title, strip, or swapcase."
296
- inputs = {
297
- "text": {
298
- "type": "string",
299
- "description": "The source text.",
300
- },
301
- "operation": {
302
- "type": "string",
303
- "description": "One of: reverse, upper, lower, title, strip, swapcase.",
304
- },
305
- }
306
- output_type = "string"
307
-
308
- def forward(self, text: str, operation: str) -> str:
309
- normalized_operation = operation.strip().lower()
310
- if normalized_operation == "reverse":
311
- return text[::-1]
312
- if normalized_operation == "upper":
313
- return text.upper()
314
- if normalized_operation == "lower":
315
- return text.lower()
316
- if normalized_operation == "title":
317
- return text.title()
318
- if normalized_operation == "strip":
319
- return text.strip()
320
- if normalized_operation == "swapcase":
321
- return text.swapcase()
322
- return "Unsupported operation."
323
-
324
-
325
- class GeminiVideoQA(Tool):
326
- name = "gemini_video_qa"
327
- description = "Answer questions about a public video URL, including YouTube links, using Gemini multimodal analysis."
328
- inputs = {
329
- "video_url": {
330
- "type": "string",
331
- "description": "The public video URL to inspect.",
332
- },
333
- "user_query": {
334
- "type": "string",
335
- "description": "The exact question to answer about the video.",
336
- },
337
- }
338
- output_type = "string"
339
-
340
- def forward(self, video_url: str, user_query: str) -> str:
341
- response = _genai_client().models.generate_content(
342
- model=_normalize_model_id(),
343
- contents=genai_types.Content(
344
- role="user",
345
- parts=[
346
- genai_types.Part(
347
- file_data=genai_types.FileData(file_uri=video_url)
348
- ),
349
- genai_types.Part(text=user_query),
350
- ],
351
- ),
352
- config=genai_types.GenerateContentConfig(temperature=0),
353
- )
354
- return _extract_text(response)
355
-
356
-
357
- class WikiTitleFinder(Tool):
358
- name = "wiki_title_finder"
359
- description = "Find likely English Wikipedia page titles for a topic."
360
- inputs = {
361
- "query": {
362
- "type": "string",
363
- "description": "The topic or search phrase to find on English Wikipedia.",
364
- }
365
- }
366
- output_type = "string"
367
-
368
- def forward(self, query: str) -> str:
369
- response = requests.get(
370
- "https://en.wikipedia.org/w/api.php",
371
- params={
372
- "action": "query",
373
- "list": "search",
374
- "srsearch": query,
375
- "srlimit": 5,
376
- "format": "json",
377
- },
378
- timeout=20,
379
- )
380
- response.raise_for_status()
381
- results = response.json().get("query", {}).get("search", [])
382
- if not results:
383
- return "No matching Wikipedia titles found."
384
- return ", ".join(item["title"] for item in results)
385
-
386
-
387
- class WikiContentFetcher(Tool):
388
- name = "wiki_content_fetcher"
389
- description = "Fetch plain-text content from an English Wikipedia page."
390
- inputs = {
391
- "page_title": {
392
- "type": "string",
393
- "description": "The exact English Wikipedia page title to fetch.",
394
- }
395
- }
396
- output_type = "string"
397
-
398
- def forward(self, page_title: str) -> str:
399
- response = requests.get(
400
- "https://en.wikipedia.org/w/api.php",
401
- params={
402
- "action": "query",
403
- "prop": "extracts",
404
- "explaintext": 1,
405
- "redirects": 1,
406
- "titles": page_title,
407
- "format": "json",
408
- },
409
- timeout=20,
410
- )
411
- response.raise_for_status()
412
- pages = response.json().get("query", {}).get("pages", {})
413
- for page in pages.values():
414
- extract = (page or {}).get("extract")
415
- if extract:
416
- return extract[:12000]
417
- return "Wikipedia page not found."
418
-
419
-
420
- class GoogleSearchTool(Tool):
421
- name = "google_search"
422
- description = "Search the live web using Gemini grounding with Google Search and return a concise result."
423
- inputs = {
424
- "query": {
425
- "type": "string",
426
- "description": "The web query to search for.",
427
- }
428
- }
429
- output_type = "string"
430
-
431
- def forward(self, query: str) -> str:
432
- grounding_tool = genai_types.Tool(google_search=genai_types.GoogleSearch())
433
- response = _genai_client().models.generate_content(
434
- model=_normalize_model_id(),
435
- contents=query,
436
- config=genai_types.GenerateContentConfig(
437
- temperature=0,
438
- tools=[grounding_tool],
439
- ),
440
- )
441
- return _extract_text(response)
442
-
443
-
444
- class FileAttachmentQueryTool(Tool):
445
- name = "run_query_with_file"
446
- description = (
447
- "Download an attached GAIA benchmark file by task_id, inspect it, and answer a question about it."
448
- )
449
- inputs = {
450
- "task_id": {
451
- "type": "string",
452
- "description": "The GAIA task identifier used to download the attachment.",
453
- },
454
- "file_name": {
455
- "type": "string",
456
- "description": "The attachment file name, including the extension.",
457
- },
458
- "user_query": {
459
- "type": "string",
460
- "description": "The question to answer about the attached file.",
461
- },
462
- }
463
- output_type = "string"
464
-
465
- def forward(self, task_id: str, file_name: str, user_query: str) -> str:
466
- file_bytes, content_type = _download_task_file(task_id)
467
- suffix = Path(file_name or "").suffix.lower()
468
- mime_type = _mime_from_name(file_name, content_type)
469
-
470
- if suffix in {".txt", ".md", ".json", ".csv", ".py", ".html", ".xml", ".yaml", ".yml", ".log"}:
471
- file_text = _decode_text_bytes(file_bytes)
472
- return _textual_file_answer(file_text, user_query)
473
-
474
- if suffix in {".xlsx", ".xls"}:
475
- file_text = _excel_to_text(file_bytes)
476
- return _textual_file_answer(file_text, user_query)
477
-
478
- return _query_uploaded_file(file_bytes, file_name, mime_type, user_query)
479
-
480
-
481
- class BasicAgent:
482
- def __init__(self):
483
- _get_gemini_api_key()
484
- self.agent = CodeAgent(
485
- model=OpenAIModel(
486
- model_id=_normalize_model_id(),
487
- api_base=GEMINI_OPENAI_BASE_URL,
488
- api_key=_get_gemini_api_key(),
489
- temperature=0,
490
- ),
491
- tools=[
492
- MathSolver(),
493
- RiddleSolver(),
494
- TextTransformer(),
495
- GeminiVideoQA(),
496
- WikiTitleFinder(),
497
- WikiContentFetcher(),
498
- GoogleSearchTool(),
499
- DuckDuckGoSearchTool(),
500
- FileAttachmentQueryTool(),
501
- ],
502
- add_base_tools=False,
503
- max_steps=8,
504
- )
505
- self.agent.prompt_templates["system_prompt"] += (
506
- "\n\n"
507
- f"{GAIA_OUTPUT_PROMPT}\n\n"
508
- "Additional tool routing rules:\n"
509
- "- If attachment metadata is present, use run_query_with_file.\n"
510
- "- If a public video URL is present, use gemini_video_qa.\n"
511
- "- Use google_search or web_search for live web facts.\n"
512
- "- Use wiki_title_finder and wiki_content_fetcher when the prompt explicitly asks for Wikipedia.\n"
513
- "- Use text_transformer for reversal or casing tasks and math_solver for arithmetic."
514
- )
515
-
516
- def __call__(self, question: str, task_id: str | None = None, file_name: str | None = None) -> str:
517
- prompt_parts = [question.strip()]
518
- if task_id and file_name:
519
- prompt_parts.append(
520
- "\nAttachment metadata:\n"
521
- f"- task_id: {task_id}\n"
522
- f"- file_name: {file_name}\n"
523
- "Use run_query_with_file if the question requires the attachment."
524
- )
525
-
526
- result = self.agent.run("\n".join(prompt_parts).strip())
527
- return _normalize_final_answer(str(result))
528
-
529
-
530
- if __name__ == "__main__":
531
- sample_question = (
532
- "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? "
533
- "You can use the latest 2022 version of english wikipedia."
534
- )
535
- print(BasicAgent()(sample_question))
 
1
+ import ast
2
+ import io
3
+ import mimetypes
4
+ import os
5
+ import re
6
+ import tempfile
7
+ import time
8
+ from pathlib import Path
9
+
10
+ import pandas as pd
11
+ import requests
12
+ from dotenv import load_dotenv
13
+ from google import genai
14
+ from google.genai import types as genai_types
15
+ from smolagents import CodeAgent, DuckDuckGoSearchTool, OpenAIModel, Tool
16
+
17
+
18
+ load_dotenv()
19
+
20
+ DEFAULT_SCORING_API_URL = "https://agents-course-unit4-scoring.hf.space"
21
+ DEFAULT_GEMINI_MODEL_ID = os.getenv("GEMINI_MODEL_ID", "gemini/gemini-2.5-flash")
22
+ GEMINI_OPENAI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
23
+ GAIA_OUTPUT_PROMPT = """
24
+ You are an evaluation-grade GAIA benchmark AI assistant. Your execution must be perfectly precise and highly deterministic. Your sole purpose is to isolate and output the exact, minimal final answer.
25
+
26
+ ### CORE DIRECTIVE
27
+ You must NEVER output explanations, intermediate reasoning, conversational filler, or comments.
28
+ Your entire output must consist ONLY of the final answer, strictly enclosed within square brackets.
29
+ Format: [FINAL_ANSWER]
30
+
31
+ ### DATA FORMATTING RULES
32
+ 1. NUMERICAL DATA:
33
+ - Output digits only (e.g., [4], not [four]).
34
+ - Exclude commas, currency symbols, percentage signs, or physical units unless the prompt explicitly dictates their inclusion.
35
+ - Strip all approximation qualifiers (e.g., "around", "roughly", "~").
36
+
37
+ 2. STRING DATA:
38
+ - Omit definite and indefinite articles ("a", "an", "the").
39
+ - Use full, unabbreviated words unless the prompt specifically requests an abbreviation.
40
+
41
+ 3. LISTS AND SETS:
42
+ - Output as a single comma-separated string with exactly one space after each comma (e.g., [a, b, c]).
43
+ - Exclude conjunctions (e.g., do not use "and" or "or").
44
+ - Exclude internal list wrappers (do not include {} or () inside the main answer brackets).
45
+ - Sort elements alphabetically or numerically in ascending order unless the prompt specifies an alternative sorting logic.
46
+
47
+ ### EXECUTION PROTOCOL
48
+ 1. SOURCE EXTRACTION: When processing data from web searches, files (via `run_query_with_file`), or video tools, extract only the atomic fact that satisfies the query. Do not summarize or quote surrounding context.
49
+ 2. LITERALISM: Default to the narrowest, most literal interpretation of the prompt. Do not synthesize assumptions.
50
+ 3. FALLBACK: If the requisite data to answer is demonstrably absent after exhaustive search, output exactly: [unknown]
51
+
52
+ ### EXAMPLES
53
+ Q: What is 2 + 2?
54
+ A: [4]
55
+
56
+ Q: How many studio albums were published by Mercedes Sosa between 2000 and 2009 (inclusive)? Use 2022 English Wikipedia.
57
+ A: [3]
58
+
59
+ Q: Given the following group table on set S = {a, b, c, d, e}, identify any subset involved in counterexamples to commutativity.
60
+ A: [b, e]
61
+
62
+ Q: How many at bats did the Yankee with the most walks in the 1977 regular season have that same season?
63
+ A: [519]
64
+ """.strip()
65
+
66
+
67
+ def _get_gemini_api_key() -> str:
68
+ api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
69
+ if not api_key:
70
+ raise RuntimeError(
71
+ "Missing Gemini API key. Set GEMINI_API_KEY in your Hugging Face Space secrets."
72
+ )
73
+ return api_key
74
+
75
+
76
+ def _normalize_model_id(model_id: str | None = None) -> str:
77
+ selected = model_id or DEFAULT_GEMINI_MODEL_ID
78
+ if selected.startswith("gemini/"):
79
+ return selected.split("/", 1)[1]
80
+ return selected
81
+
82
+
83
+ def _genai_client() -> genai.Client:
84
+ return genai.Client(api_key=_get_gemini_api_key())
85
+
86
+
87
+ def _extract_text(response) -> str:
88
+ text = getattr(response, "text", None)
89
+ if text:
90
+ return text.strip()
91
+
92
+ parts: list[str] = []
93
+ for candidate in getattr(response, "candidates", []) or []:
94
+ content = getattr(candidate, "content", None)
95
+ for part in getattr(content, "parts", []) or []:
96
+ piece = getattr(part, "text", None)
97
+ if piece:
98
+ parts.append(piece)
99
+ return "\n".join(parts).strip()
100
+
101
+
102
+ def _call_gemini_text(prompt: str, system_instruction: str | None = None) -> str:
103
+ config = genai_types.GenerateContentConfig(temperature=0)
104
+ if system_instruction:
105
+ config.system_instruction = system_instruction
106
+
107
+ response = _genai_client().models.generate_content(
108
+ model=_normalize_model_id(),
109
+ contents=prompt,
110
+ config=config,
111
+ )
112
+ return _extract_text(response)
113
+
114
+
115
+ def _decode_text_bytes(payload: bytes) -> str:
116
+ for encoding in ("utf-8", "utf-8-sig", "cp1252", "latin-1"):
117
+ try:
118
+ return payload.decode(encoding)
119
+ except UnicodeDecodeError:
120
+ continue
121
+ return payload.decode("utf-8", errors="replace")
122
+
123
+
124
+ def _download_task_file(task_id: str) -> tuple[bytes, str]:
125
+ response = requests.get(f"{DEFAULT_SCORING_API_URL}/files/{task_id}", timeout=60)
126
+ response.raise_for_status()
127
+ content_type = response.headers.get("content-type", "application/octet-stream")
128
+ return response.content, content_type
129
+
130
+
131
+ def _mime_from_name(file_name: str | None, fallback: str) -> str:
132
+ guessed, _ = mimetypes.guess_type(file_name or "")
133
+ return guessed or fallback or "application/octet-stream"
134
+
135
+
136
+ def _wait_until_active(client: genai.Client, uploaded_file) -> None:
137
+ state = getattr(uploaded_file, "state", None)
138
+ state_name = getattr(state, "name", None)
139
+ while state_name and state_name != "ACTIVE":
140
+ if state_name == "FAILED":
141
+ raise RuntimeError("Gemini file processing failed.")
142
+ time.sleep(3)
143
+ uploaded_file = client.files.get(name=uploaded_file.name)
144
+ state = getattr(uploaded_file, "state", None)
145
+ state_name = getattr(state, "name", None)
146
+
147
+
148
+ def _query_uploaded_file(file_bytes: bytes, file_name: str, mime_type: str, user_query: str) -> str:
149
+ client = _genai_client()
150
+ suffix = Path(file_name or "attachment").suffix
151
+ temp_path = None
152
+ uploaded_file = None
153
+
154
+ try:
155
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
156
+ temp_file.write(file_bytes)
157
+ temp_path = temp_file.name
158
+
159
+ uploaded_file = client.files.upload(
160
+ file=temp_path,
161
+ config={"mime_type": mime_type},
162
+ )
163
+ _wait_until_active(client, uploaded_file)
164
+
165
+ response = client.models.generate_content(
166
+ model=_normalize_model_id(),
167
+ contents=[uploaded_file, user_query],
168
+ config=genai_types.GenerateContentConfig(temperature=0),
169
+ )
170
+ return _extract_text(response)
171
+ finally:
172
+ if temp_path and os.path.exists(temp_path):
173
+ os.remove(temp_path)
174
+ if uploaded_file and getattr(uploaded_file, "name", None):
175
+ try:
176
+ client.files.delete(name=uploaded_file.name)
177
+ except Exception:
178
+ pass
179
+
180
+
181
+ def _excel_to_text(file_bytes: bytes) -> str:
182
+ workbook = pd.read_excel(io.BytesIO(file_bytes), sheet_name=None)
183
+ sections: list[str] = []
184
+ for sheet_name, frame in workbook.items():
185
+ csv_text = frame.fillna("").to_csv(index=False)
186
+ sections.append(f"Sheet: {sheet_name}\n{csv_text}")
187
+ return "\n\n".join(sections)
188
+
189
+
190
+ def _textual_file_answer(file_text: str, user_query: str) -> str:
191
+ prompt = (
192
+ "You are analyzing an attached file for a GAIA benchmark question.\n"
193
+ "Answer the question using only the file contents below.\n"
194
+ "Return only the direct final answer, with no explanation.\n\n"
195
+ f"Question:\n{user_query}\n\n"
196
+ f"File contents:\n{file_text[:50000]}"
197
+ )
198
+ return _call_gemini_text(prompt)
199
+
200
+
201
+ def _normalize_riddle_prompt(prompt: str) -> str:
202
+ stripped = prompt.strip()
203
+ if stripped and stripped.count(" ") > 3:
204
+ weird_ratio = sum(char in ".,!?;:'\"()-" for char in stripped) / max(len(stripped), 1)
205
+ if weird_ratio < 0.2:
206
+ reversed_candidate = stripped[::-1]
207
+ if re.search(r"\b(the|and|you|write|understand|sentence)\b", reversed_candidate.lower()):
208
+ return reversed_candidate
209
+ return stripped
210
+
211
+
212
+ def _normalize_final_answer(raw_answer: str) -> str:
213
+ cleaned = raw_answer.strip()
214
+ if cleaned.startswith("[") and cleaned.endswith("]") and len(cleaned) >= 2:
215
+ inner = cleaned[1:-1].strip()
216
+ if inner:
217
+ return inner
218
+ return cleaned
219
+
220
+
221
+ class MathSolver(Tool):
222
+ name = "math_solver"
223
+ description = "Evaluate arithmetic expressions with operators like +, -, *, /, //, %, and **."
224
+ inputs = {
225
+ "expression": {
226
+ "type": "string",
227
+ "description": "The arithmetic expression to evaluate.",
228
+ }
229
+ }
230
+ output_type = "string"
231
+
232
+ def forward(self, expression: str) -> str:
233
+ def eval_node(node: ast.AST) -> int | float:
234
+ if isinstance(node, ast.Expression):
235
+ return eval_node(node.body)
236
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
237
+ return node.value
238
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
239
+ operand = eval_node(node.operand)
240
+ return operand if isinstance(node.op, ast.UAdd) else -operand
241
+ if isinstance(node, ast.BinOp) and isinstance(
242
+ node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow)
243
+ ):
244
+ left = eval_node(node.left)
245
+ right = eval_node(node.right)
246
+ if isinstance(node.op, ast.Add):
247
+ return left + right
248
+ if isinstance(node.op, ast.Sub):
249
+ return left - right
250
+ if isinstance(node.op, ast.Mult):
251
+ return left * right
252
+ if isinstance(node.op, ast.Div):
253
+ return left / right
254
+ if isinstance(node.op, ast.FloorDiv):
255
+ return left // right
256
+ if isinstance(node.op, ast.Mod):
257
+ return left % right
258
+ return left**right
259
+ raise ValueError("Unsupported expression.")
260
+
261
+ try:
262
+ parsed = ast.parse(expression, mode="eval")
263
+ result = eval_node(parsed)
264
+ except Exception as exc:
265
+ return f"Math error: {exc}"
266
+
267
+ if isinstance(result, float) and result.is_integer():
268
+ return str(int(result))
269
+ return str(result)
270
+
271
+
272
+ class RiddleSolver(Tool):
273
+ name = "riddle_solver"
274
+ description = "Solve riddles, wordplay, or short trick questions when the wording matters."
275
+ inputs = {
276
+ "prompt": {
277
+ "type": "string",
278
+ "description": "The riddle or wordplay prompt to solve.",
279
+ }
280
+ }
281
+ output_type = "string"
282
+
283
+ def forward(self, prompt: str) -> str:
284
+ normalized_prompt = _normalize_riddle_prompt(prompt)
285
+ return _call_gemini_text(
286
+ normalized_prompt,
287
+ system_instruction=(
288
+ "Solve the user's riddle or wordplay. Return only the direct answer with no explanation."
289
+ ),
290
+ )
291
+
292
+
293
+ class TextTransformer(Tool):
294
+ name = "text_transformer"
295
+ description = "Apply deterministic text transforms like reverse, upper, lower, title, strip, or swapcase."
296
+ inputs = {
297
+ "text": {
298
+ "type": "string",
299
+ "description": "The source text.",
300
+ },
301
+ "operation": {
302
+ "type": "string",
303
+ "description": "One of: reverse, upper, lower, title, strip, swapcase.",
304
+ },
305
+ }
306
+ output_type = "string"
307
+
308
+ def forward(self, text: str, operation: str) -> str:
309
+ normalized_operation = operation.strip().lower()
310
+ if normalized_operation == "reverse":
311
+ return text[::-1]
312
+ if normalized_operation == "upper":
313
+ return text.upper()
314
+ if normalized_operation == "lower":
315
+ return text.lower()
316
+ if normalized_operation == "title":
317
+ return text.title()
318
+ if normalized_operation == "strip":
319
+ return text.strip()
320
+ if normalized_operation == "swapcase":
321
+ return text.swapcase()
322
+ return "Unsupported operation."
323
+
324
+
325
+ class GeminiVideoQA(Tool):
326
+ name = "gemini_video_qa"
327
+ description = "Answer questions about a public video URL, including YouTube links, using Gemini multimodal analysis."
328
+ inputs = {
329
+ "video_url": {
330
+ "type": "string",
331
+ "description": "The public video URL to inspect.",
332
+ },
333
+ "user_query": {
334
+ "type": "string",
335
+ "description": "The exact question to answer about the video.",
336
+ },
337
+ }
338
+ output_type = "string"
339
+
340
+ def forward(self, video_url: str, user_query: str) -> str:
341
+ response = _genai_client().models.generate_content(
342
+ model=_normalize_model_id(),
343
+ contents=genai_types.Content(
344
+ role="user",
345
+ parts=[
346
+ genai_types.Part(
347
+ file_data=genai_types.FileData(file_uri=video_url)
348
+ ),
349
+ genai_types.Part(text=user_query),
350
+ ],
351
+ ),
352
+ config=genai_types.GenerateContentConfig(temperature=0),
353
+ )
354
+ return _extract_text(response)
355
+
356
+
357
+ class WikiTitleFinder(Tool):
358
+ name = "wiki_title_finder"
359
+ description = "Find likely English Wikipedia page titles for a topic."
360
+ inputs = {
361
+ "query": {
362
+ "type": "string",
363
+ "description": "The topic or search phrase to find on English Wikipedia.",
364
+ }
365
+ }
366
+ output_type = "string"
367
+
368
+ def forward(self, query: str) -> str:
369
+ response = requests.get(
370
+ "https://en.wikipedia.org/w/api.php",
371
+ params={
372
+ "action": "query",
373
+ "list": "search",
374
+ "srsearch": query,
375
+ "srlimit": 5,
376
+ "format": "json",
377
+ },
378
+ timeout=20,
379
+ )
380
+ response.raise_for_status()
381
+ results = response.json().get("query", {}).get("search", [])
382
+ if not results:
383
+ return "No matching Wikipedia titles found."
384
+ return ", ".join(item["title"] for item in results)
385
+
386
+
387
+ class WikiContentFetcher(Tool):
388
+ name = "wiki_content_fetcher"
389
+ description = "Fetch plain-text content from an English Wikipedia page."
390
+ inputs = {
391
+ "page_title": {
392
+ "type": "string",
393
+ "description": "The exact English Wikipedia page title to fetch.",
394
+ }
395
+ }
396
+ output_type = "string"
397
+
398
+ def forward(self, page_title: str) -> str:
399
+ response = requests.get(
400
+ "https://en.wikipedia.org/w/api.php",
401
+ params={
402
+ "action": "query",
403
+ "prop": "extracts",
404
+ "explaintext": 1,
405
+ "redirects": 1,
406
+ "titles": page_title,
407
+ "format": "json",
408
+ },
409
+ timeout=20,
410
+ )
411
+ response.raise_for_status()
412
+ pages = response.json().get("query", {}).get("pages", {})
413
+ for page in pages.values():
414
+ extract = (page or {}).get("extract")
415
+ if extract:
416
+ return extract[:12000]
417
+ return "Wikipedia page not found."
418
+
419
+
420
+ class GoogleSearchTool(Tool):
421
+ name = "google_search"
422
+ description = "Search the live web using Gemini grounding with Google Search and return a concise result."
423
+ inputs = {
424
+ "query": {
425
+ "type": "string",
426
+ "description": "The web query to search for.",
427
+ }
428
+ }
429
+ output_type = "string"
430
+
431
+ def forward(self, query: str) -> str:
432
+ grounding_tool = genai_types.Tool(google_search=genai_types.GoogleSearch())
433
+ response = _genai_client().models.generate_content(
434
+ model=_normalize_model_id(),
435
+ contents=query,
436
+ config=genai_types.GenerateContentConfig(
437
+ temperature=0,
438
+ tools=[grounding_tool],
439
+ ),
440
+ )
441
+ return _extract_text(response)
442
+
443
+
444
+ class FileAttachmentQueryTool(Tool):
445
+ name = "run_query_with_file"
446
+ description = (
447
+ "Download an attached GAIA benchmark file by task_id, inspect it, and answer a question about it."
448
+ )
449
+ inputs = {
450
+ "task_id": {
451
+ "type": "string",
452
+ "description": "The GAIA task identifier used to download the attachment.",
453
+ },
454
+ "file_name": {
455
+ "type": "string",
456
+ "description": "The attachment file name, including the extension.",
457
+ },
458
+ "user_query": {
459
+ "type": "string",
460
+ "description": "The question to answer about the attached file.",
461
+ },
462
+ }
463
+ output_type = "string"
464
+
465
+ def forward(self, task_id: str, file_name: str, user_query: str) -> str:
466
+ file_bytes, content_type = _download_task_file(task_id)
467
+ suffix = Path(file_name or "").suffix.lower()
468
+ mime_type = _mime_from_name(file_name, content_type)
469
+
470
+ if suffix in {".txt", ".md", ".json", ".csv", ".py", ".html", ".xml", ".yaml", ".yml", ".log"}:
471
+ file_text = _decode_text_bytes(file_bytes)
472
+ return _textual_file_answer(file_text, user_query)
473
+
474
+ if suffix in {".xlsx", ".xls"}:
475
+ file_text = _excel_to_text(file_bytes)
476
+ return _textual_file_answer(file_text, user_query)
477
+
478
+ return _query_uploaded_file(file_bytes, file_name, mime_type, user_query)
479
+
480
+
481
+ class BasicAgent:
482
+ def __init__(self):
483
+ _get_gemini_api_key()
484
+ self.agent = CodeAgent(
485
+ model=OpenAIModel(
486
+ model_id=_normalize_model_id(),
487
+ api_base=GEMINI_OPENAI_BASE_URL,
488
+ api_key=_get_gemini_api_key(),
489
+ temperature=0,
490
+ ),
491
+ tools=[
492
+ MathSolver(),
493
+ RiddleSolver(),
494
+ TextTransformer(),
495
+ GeminiVideoQA(),
496
+ WikiTitleFinder(),
497
+ WikiContentFetcher(),
498
+ GoogleSearchTool(),
499
+ DuckDuckGoSearchTool(),
500
+ FileAttachmentQueryTool(),
501
+ ],
502
+ add_base_tools=False,
503
+ max_steps=8,
504
+ )
505
+ self.agent.prompt_templates["system_prompt"] += (
506
+ "\n\n"
507
+ f"{GAIA_OUTPUT_PROMPT}\n\n"
508
+ "Additional tool routing rules:\n"
509
+ "- If attachment metadata is present, use run_query_with_file.\n"
510
+ "- If a public video URL is present, use gemini_video_qa.\n"
511
+ "- Use google_search or web_search for live web facts.\n"
512
+ "- Use wiki_title_finder and wiki_content_fetcher when the prompt explicitly asks for Wikipedia.\n"
513
+ "- Use text_transformer for reversal or casing tasks and math_solver for arithmetic."
514
+ )
515
+
516
+ def __call__(self, question: str, task_id: str | None = None, file_name: str | None = None) -> str:
517
+ prompt_parts = [question.strip()]
518
+ if task_id and file_name:
519
+ prompt_parts.append(
520
+ "\nAttachment metadata:\n"
521
+ f"- task_id: {task_id}\n"
522
+ f"- file_name: {file_name}\n"
523
+ "Use run_query_with_file if the question requires the attachment."
524
+ )
525
+
526
+ result = self.agent.run("\n".join(prompt_parts).strip())
527
+ return _normalize_final_answer(str(result))
528
+
529
+
530
+ if __name__ == "__main__":
531
+ sample_question = (
532
+ "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? "
533
+ "You can use the latest 2022 version of english wikipedia."
534
+ )
535
+ print(BasicAgent()(sample_question))