deepthought commited on
Commit
88c0a98
Β·
1 Parent(s): f4dee4e

2025-05-13

Browse files
Files changed (6) hide show
  1. README.md +9 -8
  2. agent.py +528 -0
  3. agent_gemini.py +700 -0
  4. app.py +227 -0
  5. requirements.txt +18 -0
  6. system_prompt.txt +5 -0
README.md CHANGED
@@ -1,14 +1,15 @@
1
  ---
2
- title: Final Assignment
3
- emoji: 😻
4
- colorFrom: blue
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 5.29.0
8
  app_file: app.py
9
  pinned: false
10
- license: mit
11
- short_description: HuggigFace Agentic AI course
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Template Final Assignment
3
+ emoji: πŸ•΅πŸ»β€β™‚οΈ
4
+ colorFrom: indigo
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.25.2
8
  app_file: app.py
9
  pinned: false
10
+ hf_oauth: true
11
+ # optional, default duration is 8 hours/480 minutes. Max duration is 30 days/43200 minutes.
12
+ hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
agent.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from smolagents import (
2
+ CodeAgent,
3
+ DuckDuckGoSearchTool,
4
+ HfApiModel,
5
+ LiteLLMModel,
6
+ OpenAIServerModel,
7
+ PythonInterpreterTool,
8
+ tool,
9
+ InferenceClientModel,
10
+ )
11
+ from typing import List, Dict, Any, Optional
12
+ import os
13
+ import tempfile
14
+ import re
15
+ import json
16
+ import requests
17
+ from urllib.parse import urlparse
18
+
19
+
20
+ @tool
21
+ def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
22
+ """
23
+ Save content to a temporary file and return the path.
24
+ Useful for processing files from the GAIA API.
25
+
26
+ Args:
27
+ content: The content to save to the file
28
+ filename: Optional filename, will generate a random name if not provided
29
+
30
+ Returns:
31
+ Path to the saved file
32
+ """
33
+ temp_dir = tempfile.gettempdir()
34
+ if filename is None:
35
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
36
+ filepath = temp_file.name
37
+ else:
38
+ filepath = os.path.join(temp_dir, filename)
39
+
40
+ # Write content to the file
41
+ with open(filepath, "w") as f:
42
+ f.write(content)
43
+
44
+ return f"File saved to {filepath}. You can read this file to process its contents."
45
+
46
+
47
+ @tool
48
+ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
49
+ """
50
+ Download a file from a URL and save it to a temporary location.
51
+
52
+ Args:
53
+ url: The URL to download from
54
+ filename: Optional filename, will generate one based on URL if not provided
55
+
56
+ Returns:
57
+ Path to the downloaded file
58
+ """
59
+ try:
60
+ # Parse URL to get filename if not provided
61
+ if not filename:
62
+ path = urlparse(url).path
63
+ filename = os.path.basename(path)
64
+ if not filename:
65
+ # Generate a random name if we couldn't extract one
66
+ import uuid
67
+
68
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
69
+
70
+ # Create temporary file
71
+ temp_dir = tempfile.gettempdir()
72
+ filepath = os.path.join(temp_dir, filename)
73
+
74
+ # Download the file
75
+ response = requests.get(url, stream=True)
76
+ response.raise_for_status()
77
+
78
+ # Save the file
79
+ with open(filepath, "wb") as f:
80
+ for chunk in response.iter_content(chunk_size=8192):
81
+ f.write(chunk)
82
+
83
+ return f"File downloaded to {filepath}. You can now process this file."
84
+ except Exception as e:
85
+ return f"Error downloading file: {str(e)}"
86
+
87
+
88
+ @tool
89
+ def extract_text_from_image(image_path: str) -> str:
90
+ """
91
+ Extract text from an image using pytesseract (if available).
92
+
93
+ Args:
94
+ image_path: Path to the image file
95
+
96
+ Returns:
97
+ Extracted text or error message
98
+ """
99
+ try:
100
+ # Try to import pytesseract
101
+ import pytesseract
102
+ from PIL import Image
103
+
104
+ # Open the image
105
+ image = Image.open(image_path)
106
+
107
+ # Extract text
108
+ text = pytesseract.image_to_string(image)
109
+
110
+ return f"Extracted text from image:\n\n{text}"
111
+ except ImportError:
112
+ return "Error: pytesseract is not installed. Please install it with 'pip install pytesseract' and ensure Tesseract OCR is installed on your system."
113
+ except Exception as e:
114
+ return f"Error extracting text from image: {str(e)}"
115
+
116
+
117
+ @tool
118
+ def analyze_csv_file(file_path: str, query: str) -> str:
119
+ """
120
+ Analyze a CSV file using pandas and answer a question about it.
121
+
122
+ Args:
123
+ file_path: Path to the CSV file
124
+ query: Question about the data
125
+
126
+ Returns:
127
+ Analysis result or error message
128
+ """
129
+ try:
130
+ import pandas as pd
131
+
132
+ # Read the CSV file
133
+ df = pd.read_csv(file_path)
134
+
135
+ # Run various analyses based on the query
136
+ result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
137
+ result += f"Columns: {', '.join(df.columns)}\n\n"
138
+
139
+ # Add summary statistics
140
+ result += "Summary statistics:\n"
141
+ result += str(df.describe())
142
+
143
+ return result
144
+ except ImportError:
145
+ return "Error: pandas is not installed. Please install it with 'pip install pandas'."
146
+ except Exception as e:
147
+ return f"Error analyzing CSV file: {str(e)}"
148
+
149
+
150
+ @tool
151
+ def analyze_excel_file(file_path: str, query: str) -> str:
152
+ """
153
+ Analyze an Excel file using pandas and answer a question about it.
154
+
155
+ Args:
156
+ file_path: Path to the Excel file
157
+ query: Question about the data
158
+
159
+ Returns:
160
+ Analysis result or error message
161
+ """
162
+ try:
163
+ import pandas as pd
164
+
165
+ # Read the Excel file
166
+ df = pd.read_excel(file_path)
167
+
168
+ # Run various analyses based on the query
169
+ result = (
170
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
171
+ )
172
+ result += f"Columns: {', '.join(df.columns)}\n\n"
173
+
174
+ # Add summary statistics
175
+ result += "Summary statistics:\n"
176
+ result += str(df.describe())
177
+
178
+ return result
179
+ except ImportError:
180
+ return "Error: pandas and openpyxl are not installed. Please install them with 'pip install pandas openpyxl'."
181
+ except Exception as e:
182
+ return f"Error analyzing Excel file: {str(e)}"
183
+
184
+
185
+ class GAIAAgent:
186
+ def __init__(
187
+ self,
188
+ model_type: str = "HfApiModel",
189
+ model_id: Optional[str] = None,
190
+ api_key: Optional[str] = None,
191
+ api_base: Optional[str] = None,
192
+ temperature: float = 0.2,
193
+ executor_type: str = "local", # Changed from use_e2b to executor_type
194
+ additional_imports: List[str] = None,
195
+ additional_tools: List[Any] = None,
196
+ system_prompt: Optional[
197
+ str
198
+ ] = None, # We'll still accept this parameter but not use it directly
199
+ verbose: bool = False,
200
+ provider: Optional[str] = None, # Add provider for InferenceClientModel
201
+ timeout: Optional[int] = None, # Add timeout for InferenceClientModel
202
+ ):
203
+ """
204
+ Initialize a GAIAAgent with specified configuration
205
+
206
+ Args:
207
+ model_type: Type of model to use (HfApiModel, LiteLLMModel, OpenAIServerModel, InferenceClientModel)
208
+ model_id: ID of the model to use
209
+ api_key: API key for the model provider
210
+ api_base: Base URL for API calls
211
+ temperature: Temperature for text generation
212
+ executor_type: Type of executor for code execution ('local' or 'e2b')
213
+ additional_imports: Additional Python modules to allow importing
214
+ additional_tools: Additional tools to provide to the agent
215
+ system_prompt: Custom system prompt to use (not directly used, kept for backward compatibility)
216
+ verbose: Enable verbose logging
217
+ provider: Provider for InferenceClientModel (e.g., "hf-inference")
218
+ timeout: Timeout in seconds for API calls
219
+ """
220
+ # Set verbosity
221
+ self.verbose = verbose
222
+ self.system_prompt = system_prompt # Store for potential future use
223
+
224
+ # Initialize model based on configuration
225
+ if model_type == "HfApiModel":
226
+ if api_key is None:
227
+ api_key = os.getenv("HUGGINGFACEHUB_API_TOKEN")
228
+ if not api_key:
229
+ raise ValueError(
230
+ "No Hugging Face token provided. Please set HUGGINGFACEHUB_API_TOKEN environment variable or pass api_key parameter."
231
+ )
232
+
233
+ if self.verbose:
234
+ print(f"Using Hugging Face token: {api_key[:5]}...")
235
+
236
+ self.model = HfApiModel(
237
+ model_id=model_id or "meta-llama/Llama-3-70B-Instruct",
238
+ token=api_key,
239
+ temperature=temperature,
240
+ )
241
+ elif model_type == "InferenceClientModel":
242
+ if api_key is None:
243
+ api_key = os.getenv("HUGGINGFACEHUB_API_TOKEN")
244
+ if not api_key:
245
+ raise ValueError(
246
+ "No Hugging Face token provided. Please set HUGGINGFACEHUB_API_TOKEN environment variable or pass api_key parameter."
247
+ )
248
+
249
+ if self.verbose:
250
+ print(f"Using Hugging Face token: {api_key[:5]}...")
251
+
252
+ self.model = InferenceClientModel(
253
+ model_id=model_id or "meta-llama/Llama-3-70B-Instruct",
254
+ provider=provider or "hf-inference",
255
+ token=api_key,
256
+ timeout=timeout or 120,
257
+ temperature=temperature,
258
+ )
259
+ elif model_type == "LiteLLMModel":
260
+ from smolagents import LiteLLMModel
261
+
262
+ self.model = LiteLLMModel(
263
+ model_id=model_id or "gpt-4o",
264
+ api_key=api_key or os.getenv("OPENAI_API_KEY"),
265
+ temperature=temperature,
266
+ )
267
+ elif model_type == "OpenAIServerModel":
268
+ # Check for xAI API key and base URL first
269
+ xai_api_key = os.getenv("XAI_API_KEY")
270
+ xai_api_base = os.getenv("XAI_API_BASE")
271
+
272
+ # If xAI credentials are available, use them
273
+ if xai_api_key and api_key is None:
274
+ api_key = xai_api_key
275
+ if self.verbose:
276
+ print(f"Using xAI API key: {api_key[:5]}...")
277
+
278
+ # If no API key specified, fall back to OPENAI_API_KEY
279
+ if api_key is None:
280
+ api_key = os.getenv("OPENAI_API_KEY")
281
+ if not api_key:
282
+ raise ValueError(
283
+ "No OpenAI API key provided. Please set OPENAI_API_KEY or XAI_API_KEY environment variable or pass api_key parameter."
284
+ )
285
+
286
+ # If xAI API base is available and no api_base is provided, use it
287
+ if xai_api_base and api_base is None:
288
+ api_base = xai_api_base
289
+ if self.verbose:
290
+ print(f"Using xAI API base URL: {api_base}")
291
+
292
+ # If no API base specified but environment variable available, use it
293
+ if api_base is None:
294
+ api_base = os.getenv("AGENT_API_BASE")
295
+ if api_base and self.verbose:
296
+ print(f"Using API base from AGENT_API_BASE: {api_base}")
297
+
298
+ self.model = OpenAIServerModel(
299
+ model_id=model_id or "gpt-4o",
300
+ api_key=api_key,
301
+ api_base=api_base,
302
+ temperature=temperature,
303
+ )
304
+ else:
305
+ raise ValueError(f"Unknown model type: {model_type}")
306
+
307
+ if self.verbose:
308
+ print(f"Initialized model: {model_type} - {model_id}")
309
+
310
+ # Initialize default tools
311
+ self.tools = [
312
+ DuckDuckGoSearchTool(),
313
+ PythonInterpreterTool(),
314
+ save_and_read_file,
315
+ download_file_from_url,
316
+ analyze_csv_file,
317
+ analyze_excel_file,
318
+ ]
319
+
320
+ # Add extract_text_from_image if PIL and pytesseract are available
321
+ try:
322
+ import pytesseract
323
+ from PIL import Image
324
+
325
+ self.tools.append(extract_text_from_image)
326
+ if self.verbose:
327
+ print("Added image processing tool")
328
+ except ImportError:
329
+ if self.verbose:
330
+ print("Image processing libraries not available")
331
+
332
+ # Add any additional tools
333
+ if additional_tools:
334
+ self.tools.extend(additional_tools)
335
+
336
+ if self.verbose:
337
+ print(f"Initialized with {len(self.tools)} tools")
338
+
339
+ # Setup imports allowed
340
+ self.imports = [
341
+ "pandas",
342
+ "numpy",
343
+ "datetime",
344
+ "json",
345
+ "re",
346
+ "math",
347
+ "os",
348
+ "requests",
349
+ "csv",
350
+ "urllib",
351
+ ]
352
+ if additional_imports:
353
+ self.imports.extend(additional_imports)
354
+
355
+ # Initialize the CodeAgent
356
+ executor_kwargs = {}
357
+ if executor_type == "e2b":
358
+ try:
359
+ # Try to import e2b dependencies to check if they're available
360
+ from e2b_code_interpreter import Sandbox
361
+
362
+ if self.verbose:
363
+ print("Using e2b executor")
364
+ except ImportError:
365
+ if self.verbose:
366
+ print("e2b dependencies not found, falling back to local executor")
367
+ executor_type = "local" # Fallback to local if e2b is not available
368
+
369
+ self.agent = CodeAgent(
370
+ tools=self.tools,
371
+ model=self.model,
372
+ additional_authorized_imports=self.imports,
373
+ executor_type=executor_type,
374
+ executor_kwargs=executor_kwargs,
375
+ verbosity_level=2 if self.verbose else 0,
376
+ )
377
+
378
+ if self.verbose:
379
+ print("Agent initialized and ready")
380
+
381
+ def answer_question(
382
+ self, question: str, task_file_path: Optional[str] = None
383
+ ) -> str:
384
+ """
385
+ Process a GAIA benchmark question and return the answer
386
+
387
+ Args:
388
+ question: The question to answer
389
+ task_file_path: Optional path to a file associated with the question
390
+
391
+ Returns:
392
+ The answer to the question
393
+ """
394
+ try:
395
+ if self.verbose:
396
+ print(f"Processing question: {question}")
397
+ if task_file_path:
398
+ print(f"With associated file: {task_file_path}")
399
+
400
+ # Create a context with file information if available
401
+ context = question
402
+ file_content = None
403
+
404
+ # If there's a file, read it and include its content in the context
405
+ if task_file_path:
406
+ try:
407
+ with open(task_file_path, "r") as f:
408
+ file_content = f.read()
409
+
410
+ # Determine file type from extension
411
+ import os
412
+
413
+ file_ext = os.path.splitext(task_file_path)[1].lower()
414
+
415
+ context = f"""
416
+ Question: {question}
417
+
418
+ This question has an associated file. Here is the file content:
419
+
420
+ ```{file_ext}
421
+ {file_content}
422
+ ```
423
+
424
+ Analyze the file content above to answer the question.
425
+ """
426
+ except Exception as file_e:
427
+ context = f"""
428
+ Question: {question}
429
+
430
+ This question has an associated file at path: {task_file_path}
431
+ However, there was an error reading the file: {file_e}
432
+ You can still try to answer the question based on the information provided.
433
+ """
434
+
435
+ # Check for special cases that need specific formatting
436
+ # Reversed text questions
437
+ if question.startswith(".") or ".rewsna eht sa" in question:
438
+ context = f"""
439
+ This question appears to be in reversed text. Here's the reversed version:
440
+ {question[::-1]}
441
+
442
+ Now answer the question above. Remember to format your answer exactly as requested.
443
+ """
444
+
445
+ # Add a prompt to ensure precise answers
446
+ full_prompt = f"""{context}
447
+
448
+ When answering, provide ONLY the precise answer requested.
449
+ Do not include explanations, steps, reasoning, or additional text.
450
+ Be direct and specific. GAIA benchmark requires exact matching answers.
451
+ For example, if asked "What is the capital of France?", respond simply with "Paris".
452
+ """
453
+
454
+ # Run the agent with the question
455
+ answer = self.agent.run(full_prompt)
456
+
457
+ # Clean up the answer to ensure it's in the expected format
458
+ # Remove common prefixes that models often add
459
+ answer = self._clean_answer(answer)
460
+
461
+ if self.verbose:
462
+ print(f"Generated answer: {answer}")
463
+
464
+ return answer
465
+ except Exception as e:
466
+ error_msg = f"Error answering question: {e}"
467
+ if self.verbose:
468
+ print(error_msg)
469
+ return error_msg
470
+
471
+ def _clean_answer(self, answer: any) -> str:
472
+ """
473
+ Clean up the answer to remove common prefixes and formatting
474
+ that models often add but that can cause exact match failures.
475
+
476
+ Args:
477
+ answer: The raw answer from the model
478
+
479
+ Returns:
480
+ The cleaned answer as a string
481
+ """
482
+ # Convert non-string types to strings
483
+ if not isinstance(answer, str):
484
+ # Handle numeric types (float, int)
485
+ if isinstance(answer, float):
486
+ # Format floating point numbers properly
487
+ # Check if it's an integer value in float form (e.g., 12.0)
488
+ if answer.is_integer():
489
+ formatted_answer = str(int(answer))
490
+ else:
491
+ # For currency values that might need formatting
492
+ if abs(answer) >= 1000:
493
+ formatted_answer = f"${answer:,.2f}"
494
+ else:
495
+ formatted_answer = str(answer)
496
+ return formatted_answer
497
+ elif isinstance(answer, int):
498
+ return str(answer)
499
+ else:
500
+ # For any other type
501
+ return str(answer)
502
+
503
+ # Now we know answer is a string, so we can safely use string methods
504
+ # Normalize whitespace
505
+ answer = answer.strip()
506
+
507
+ # Remove common prefixes and formatting that models add
508
+ prefixes_to_remove = [
509
+ "The answer is ",
510
+ "Answer: ",
511
+ "Final answer: ",
512
+ "The result is ",
513
+ "To answer this question: ",
514
+ "Based on the information provided, ",
515
+ "According to the information: ",
516
+ ]
517
+
518
+ for prefix in prefixes_to_remove:
519
+ if answer.startswith(prefix):
520
+ answer = answer[len(prefix) :].strip()
521
+
522
+ # Remove quotes if they wrap the entire answer
523
+ if (answer.startswith('"') and answer.endswith('"')) or (
524
+ answer.startswith("'") and answer.endswith("'")
525
+ ):
526
+ answer = answer[1:-1].strip()
527
+
528
+ return answer
agent_gemini.py ADDED
@@ -0,0 +1,700 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import time
4
+ import re
5
+ import json
6
+ from typing import List, Optional, Dict, Any
7
+ from urllib.parse import urlparse
8
+ import requests
9
+ import yt_dlp
10
+ from bs4 import BeautifulSoup
11
+ from difflib import SequenceMatcher
12
+
13
+ from langchain_core.messages import HumanMessage, SystemMessage
14
+ from langchain_google_genai import ChatGoogleGenerativeAI
15
+ from langchain_community.utilities import (
16
+ DuckDuckGoSearchAPIWrapper,
17
+ WikipediaAPIWrapper,
18
+ )
19
+ from langchain.agents import (
20
+ Tool,
21
+ AgentExecutor,
22
+ ConversationalAgent,
23
+ initialize_agent,
24
+ AgentType,
25
+ )
26
+ from langchain.memory import ConversationBufferMemory
27
+ from langchain.prompts import MessagesPlaceholder
28
+ from langchain.tools import BaseTool, Tool, tool
29
+ from google.generativeai.types import HarmCategory, HarmBlockThreshold
30
+ from PIL import Image
31
+ import google.generativeai as genai
32
+ from pydantic import Field
33
+
34
+ from smolagents import WikipediaSearchTool
35
+
36
+
37
+ class SmolagentToolWrapper(BaseTool):
38
+ """Wrapper for smolagents tools to make them compatible with LangChain."""
39
+
40
+ wrapped_tool: object = Field(description="The wrapped smolagents tool")
41
+
42
+ def __init__(self, tool):
43
+ """Initialize the wrapper with a smolagents tool."""
44
+ super().__init__(
45
+ name=tool.name,
46
+ description=tool.description,
47
+ return_direct=False,
48
+ wrapped_tool=tool,
49
+ )
50
+
51
+ def _run(self, query: str) -> str:
52
+ """Use the wrapped tool to execute the query."""
53
+ try:
54
+ # For WikipediaSearchTool
55
+ if hasattr(self.wrapped_tool, "search"):
56
+ return self.wrapped_tool.search(query)
57
+ # For DuckDuckGoSearchTool and others
58
+ return self.wrapped_tool(query)
59
+ except Exception as e:
60
+ return f"Error using tool: {str(e)}"
61
+
62
+ def _arun(self, query: str) -> str:
63
+ """Async version - just calls sync version since smolagents tools don't support async."""
64
+ return self._run(query)
65
+
66
+
67
+ class WebSearchTool:
68
+ def __init__(self):
69
+ self.last_request_time = 0
70
+ self.min_request_interval = 2.0 # Minimum time between requests in seconds
71
+ self.max_retries = 10
72
+
73
+ def search(self, query: str, domain: Optional[str] = None) -> str:
74
+ """Perform web search with rate limiting and retries."""
75
+ for attempt in range(self.max_retries):
76
+ # Implement rate limiting
77
+ current_time = time.time()
78
+ time_since_last = current_time - self.last_request_time
79
+ if time_since_last < self.min_request_interval:
80
+ time.sleep(self.min_request_interval - time_since_last)
81
+
82
+ try:
83
+ # Make the search request
84
+ results = self._do_search(query, domain)
85
+ self.last_request_time = time.time()
86
+ return results
87
+ except Exception as e:
88
+ if "202 Ratelimit" in str(e):
89
+ if attempt < self.max_retries - 1:
90
+ # Exponential backoff
91
+ wait_time = (2**attempt) * self.min_request_interval
92
+ time.sleep(wait_time)
93
+ continue
94
+ return f"Search failed after {self.max_retries} attempts: {str(e)}"
95
+
96
+ return "Search failed due to rate limiting"
97
+
98
+ def _do_search(self, query: str, domain: Optional[str] = None) -> str:
99
+ """Perform the actual search request."""
100
+ try:
101
+ # Construct search URL
102
+ base_url = "https://html.duckduckgo.com/html"
103
+ params = {"q": query}
104
+ if domain:
105
+ params["q"] += f" site:{domain}"
106
+
107
+ # Make request with increased timeout
108
+ response = requests.get(base_url, params=params, timeout=10)
109
+ response.raise_for_status()
110
+
111
+ if response.status_code == 202:
112
+ raise Exception("202 Ratelimit")
113
+
114
+ # Extract search results
115
+ results = []
116
+ soup = BeautifulSoup(response.text, "html.parser")
117
+ for result in soup.find_all("div", {"class": "result"}):
118
+ title = result.find("a", {"class": "result__a"})
119
+ snippet = result.find("a", {"class": "result__snippet"})
120
+ if title and snippet:
121
+ results.append(
122
+ {
123
+ "title": title.get_text(),
124
+ "snippet": snippet.get_text(),
125
+ "url": title.get("href"),
126
+ }
127
+ )
128
+
129
+ # Format results
130
+ formatted_results = []
131
+ for r in results[:10]: # Limit to top 5 results
132
+ formatted_results.append(
133
+ f"[{r['title']}]({r['url']})\n{r['snippet']}\n"
134
+ )
135
+
136
+ return "## Search Results\n\n" + "\n".join(formatted_results)
137
+
138
+ except requests.RequestException as e:
139
+ raise Exception(f"Search request failed: {str(e)}")
140
+
141
+
142
+ def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
143
+ """
144
+ Save content to a temporary file and return the path.
145
+ Useful for processing files from the GAIA API.
146
+
147
+ Args:
148
+ content: The content to save to the file
149
+ filename: Optional filename, will generate a random name if not provided
150
+
151
+ Returns:
152
+ Path to the saved file
153
+ """
154
+ temp_dir = tempfile.gettempdir()
155
+ if filename is None:
156
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
157
+ filepath = temp_file.name
158
+ else:
159
+ filepath = os.path.join(temp_dir, filename)
160
+
161
+ # Write content to the file
162
+ with open(filepath, "w") as f:
163
+ f.write(content)
164
+
165
+ return f"File saved to {filepath}. You can read this file to process its contents."
166
+
167
+
168
+ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
169
+ """
170
+ Download a file from a URL and save it to a temporary location.
171
+
172
+ Args:
173
+ url: The URL to download from
174
+ filename: Optional filename, will generate one based on URL if not provided
175
+
176
+ Returns:
177
+ Path to the downloaded file
178
+ """
179
+ try:
180
+ # Parse URL to get filename if not provided
181
+ if not filename:
182
+ path = urlparse(url).path
183
+ filename = os.path.basename(path)
184
+ if not filename:
185
+ # Generate a random name if we couldn't extract one
186
+ import uuid
187
+
188
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
189
+
190
+ # Create temporary file
191
+ temp_dir = tempfile.gettempdir()
192
+ filepath = os.path.join(temp_dir, filename)
193
+
194
+ # Download the file
195
+ response = requests.get(url, stream=True)
196
+ response.raise_for_status()
197
+
198
+ # Save the file
199
+ with open(filepath, "wb") as f:
200
+ for chunk in response.iter_content(chunk_size=8192):
201
+ f.write(chunk)
202
+
203
+ return f"File downloaded to {filepath}. You can now process this file."
204
+ except Exception as e:
205
+ return f"Error downloading file: {str(e)}"
206
+
207
+
208
+ def extract_text_from_image(image_path: str) -> str:
209
+ """
210
+ Extract text from an image using pytesseract (if available).
211
+
212
+ Args:
213
+ image_path: Path to the image file
214
+
215
+ Returns:
216
+ Extracted text or error message
217
+ """
218
+ try:
219
+ # Try to import pytesseract
220
+ import pytesseract
221
+ from PIL import Image
222
+
223
+ # Open the image
224
+ image = Image.open(image_path)
225
+
226
+ # Extract text
227
+ text = pytesseract.image_to_string(image)
228
+
229
+ return f"Extracted text from image:\n\n{text}"
230
+ except ImportError:
231
+ return "Error: pytesseract is not installed. Please install it with 'pip install pytesseract' and ensure Tesseract OCR is installed on your system."
232
+ except Exception as e:
233
+ return f"Error extracting text from image: {str(e)}"
234
+
235
+
236
+ def analyze_csv_file(file_path: str, query: str) -> str:
237
+ """
238
+ Analyze a CSV file using pandas and answer a question about it.
239
+
240
+ Args:
241
+ file_path: Path to the CSV file
242
+ query: Question about the data
243
+
244
+ Returns:
245
+ Analysis result or error message
246
+ """
247
+ try:
248
+ import pandas as pd
249
+
250
+ # Read the CSV file
251
+ df = pd.read_csv(file_path)
252
+
253
+ # Run various analyses based on the query
254
+ result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
255
+ result += f"Columns: {', '.join(df.columns)}\n\n"
256
+
257
+ # Add summary statistics
258
+ result += "Summary statistics:\n"
259
+ result += str(df.describe())
260
+
261
+ return result
262
+ except ImportError:
263
+ return "Error: pandas is not installed. Please install it with 'pip install pandas'."
264
+ except Exception as e:
265
+ return f"Error analyzing CSV file: {str(e)}"
266
+
267
+
268
+ @tool
269
+ def analyze_excel_file(file_path: str, query: str) -> str:
270
+ """
271
+ Analyze an Excel file using pandas and answer a question about it.
272
+
273
+ Args:
274
+ file_path: Path to the Excel file
275
+ query: Question about the data
276
+
277
+ Returns:
278
+ Analysis result or error message
279
+ """
280
+ try:
281
+ import pandas as pd
282
+
283
+ # Read the Excel file
284
+ df = pd.read_excel(file_path)
285
+
286
+ # Run various analyses based on the query
287
+ result = (
288
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
289
+ )
290
+ result += f"Columns: {', '.join(df.columns)}\n\n"
291
+
292
+ # Add summary statistics
293
+ result += "Summary statistics:\n"
294
+ result += str(df.describe())
295
+
296
+ return result
297
+ except ImportError:
298
+ return "Error: pandas and openpyxl are not installed. Please install them with 'pip install pandas openpyxl'."
299
+ except Exception as e:
300
+ return f"Error analyzing Excel file: {str(e)}"
301
+
302
+
303
+ class GeminiAgent:
304
+ def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash"):
305
+ # Suppress warnings
306
+ import warnings
307
+
308
+ warnings.filterwarnings("ignore", category=UserWarning)
309
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
310
+ warnings.filterwarnings("ignore", message=".*will be deprecated.*")
311
+ warnings.filterwarnings("ignore", "LangChain.*")
312
+
313
+ self.api_key = api_key
314
+ self.model_name = model_name
315
+
316
+ # Configure Gemini
317
+ genai.configure(api_key=api_key)
318
+
319
+ # Initialize the LLM
320
+ self.llm = self._setup_llm()
321
+
322
+ # Setup tools
323
+ self.tools = [
324
+ SmolagentToolWrapper(WikipediaSearchTool()),
325
+ Tool(
326
+ name="analyze_video",
327
+ func=self._analyze_video,
328
+ description="Analyze YouTube video content directly",
329
+ ),
330
+ Tool(
331
+ name="analyze_image",
332
+ func=self._analyze_image,
333
+ description="Analyze image content",
334
+ ),
335
+ Tool(
336
+ name="analyze_table",
337
+ func=self._analyze_table,
338
+ description="Analyze table or matrix data",
339
+ ),
340
+ Tool(
341
+ name="analyze_list",
342
+ func=self._analyze_list,
343
+ description="Analyze and categorize list items",
344
+ ),
345
+ Tool(
346
+ name="web_search",
347
+ func=self._web_search,
348
+ description="Search the web for information",
349
+ ),
350
+ ]
351
+
352
+ # Setup memory
353
+ self.memory = ConversationBufferMemory(
354
+ memory_key="chat_history", return_messages=True
355
+ )
356
+
357
+ # Initialize agent
358
+ self.agent = self._setup_agent()
359
+
360
+ def run(self, query: str) -> str:
361
+ """Run the agent on a query with incremental retries."""
362
+ max_retries = 3
363
+ base_sleep = 1 # Start with 1 second sleep
364
+
365
+ for attempt in range(max_retries):
366
+ try:
367
+ # If no match found in answer bank, use the agent
368
+ response = self.agent.run(query)
369
+ return response
370
+
371
+ except Exception as e:
372
+ sleep_time = base_sleep * (attempt + 1) # Incremental sleep: 1s, 2s, 3s
373
+ if attempt < max_retries - 1:
374
+ print(
375
+ f"Attempt {attempt + 1} failed. Retrying in {sleep_time} seconds..."
376
+ )
377
+ time.sleep(sleep_time)
378
+ continue
379
+ return f"Error processing query after {max_retries} attempts: {str(e)}"
380
+
381
+ print("Agent processed all queries!")
382
+
383
+ def _clean_response(self, response: str) -> str:
384
+ """Clean up the response from the agent."""
385
+ # Remove any tool invocation artifacts
386
+ cleaned = re.sub(
387
+ r"> Entering new AgentExecutor chain...|> Finished chain.", "", response
388
+ )
389
+ cleaned = re.sub(
390
+ r"Thought:.*?Action:.*?Action Input:.*?Observation:.*?\n",
391
+ "",
392
+ cleaned,
393
+ flags=re.DOTALL,
394
+ )
395
+ return cleaned.strip()
396
+
397
+ def run_interactive(self):
398
+ print("AI Assistant Ready! (Type 'exit' to quit)")
399
+
400
+ while True:
401
+ query = input("You: ").strip()
402
+ if query.lower() == "exit":
403
+ print("Goodbye!")
404
+ break
405
+
406
+ print("Assistant:", self.run(query))
407
+
408
+ def _web_search(self, query: str, domain: Optional[str] = None) -> str:
409
+ """Perform web search with rate limiting and retries."""
410
+ try:
411
+ # Use DuckDuckGo API wrapper for more reliable results
412
+ search = DuckDuckGoSearchAPIWrapper(max_results=5)
413
+ results = search.run(f"{query} {f'site:{domain}' if domain else ''}")
414
+
415
+ if not results or results.strip() == "":
416
+ return "No search results found."
417
+
418
+ return results
419
+
420
+ except Exception as e:
421
+ return f"Search error: {str(e)}"
422
+
423
+ def _analyze_video(self, url: str) -> str:
424
+ """Analyze video content using Gemini's video understanding capabilities."""
425
+ try:
426
+ # Validate URL
427
+ parsed_url = urlparse(url)
428
+ if not all([parsed_url.scheme, parsed_url.netloc]):
429
+ return (
430
+ "Please provide a valid video URL with http:// or https:// prefix."
431
+ )
432
+
433
+ # Check if it's a YouTube URL
434
+ if "youtube.com" not in url and "youtu.be" not in url:
435
+ return "Only YouTube videos are supported at this time."
436
+
437
+ try:
438
+ # Configure yt-dlp with minimal extraction
439
+ ydl_opts = {
440
+ "quiet": True,
441
+ "no_warnings": True,
442
+ "extract_flat": True,
443
+ "no_playlist": True,
444
+ "youtube_include_dash_manifest": False,
445
+ }
446
+
447
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
448
+ try:
449
+ # Try basic info extraction
450
+ info = ydl.extract_info(url, download=False, process=False)
451
+ if not info:
452
+ return "Could not extract video information."
453
+
454
+ title = info.get("title", "Unknown")
455
+ description = info.get("description", "")
456
+
457
+ # Create a detailed prompt with available metadata
458
+ prompt = f"""Please analyze this YouTube video:
459
+ Title: {title}
460
+ URL: {url}
461
+ Description: {description}
462
+
463
+ Please provide a detailed analysis focusing on:
464
+ 1. Main topic and key points from the title and description
465
+ 2. Expected visual elements and scenes
466
+ 3. Overall message or purpose
467
+ 4. Target audience"""
468
+
469
+ # Use the LLM with proper message format
470
+ messages = [HumanMessage(content=prompt)]
471
+ response = self.llm.invoke(messages)
472
+ return (
473
+ response.content
474
+ if hasattr(response, "content")
475
+ else str(response)
476
+ )
477
+
478
+ except Exception as e:
479
+ if "Sign in to confirm" in str(e):
480
+ return "This video requires age verification or sign-in. Please provide a different video URL."
481
+ return f"Error accessing video: {str(e)}"
482
+
483
+ except Exception as e:
484
+ return f"Error extracting video info: {str(e)}"
485
+
486
+ except Exception as e:
487
+ return f"Error analyzing video: {str(e)}"
488
+
489
+ def _analyze_table(self, table_data: str) -> str:
490
+ """Analyze table or matrix data."""
491
+ try:
492
+ if not table_data or not isinstance(table_data, str):
493
+ return "Please provide valid table data for analysis."
494
+
495
+ prompt = f"""Please analyze this table:
496
+
497
+ {table_data}
498
+
499
+ Provide a detailed analysis including:
500
+ 1. Structure and format
501
+ 2. Key patterns or relationships
502
+ 3. Notable findings
503
+ 4. Any mathematical properties (if applicable)"""
504
+
505
+ messages = [HumanMessage(content=prompt)]
506
+ response = self.llm.invoke(messages)
507
+ return response.content if hasattr(response, "content") else str(response)
508
+
509
+ except Exception as e:
510
+ return f"Error analyzing table: {str(e)}"
511
+
512
+ def _analyze_image(self, image_data: str) -> str:
513
+ """Analyze image content."""
514
+ try:
515
+ if not image_data or not isinstance(image_data, str):
516
+ return "Please provide a valid image for analysis."
517
+
518
+ prompt = f"""Please analyze this image:
519
+
520
+ {image_data}
521
+
522
+ Focus on:
523
+ 1. Visual elements and objects
524
+ 2. Colors and composition
525
+ 3. Text or numbers (if present)
526
+ 4. Overall context and meaning"""
527
+
528
+ messages = [HumanMessage(content=prompt)]
529
+ response = self.llm.invoke(messages)
530
+ return response.content if hasattr(response, "content") else str(response)
531
+
532
+ except Exception as e:
533
+ return f"Error analyzing image: {str(e)}"
534
+
535
+ def _analyze_list(self, list_data: str) -> str:
536
+ """Analyze and categorize list items."""
537
+ if not list_data:
538
+ return "No list data provided."
539
+ try:
540
+ items = [x.strip() for x in list_data.split(",")]
541
+ if not items:
542
+ return "Please provide a comma-separated list of items."
543
+ # Add list analysis logic here
544
+ return "Please provide the list items for analysis."
545
+ except Exception as e:
546
+ return f"Error analyzing list: {str(e)}"
547
+
548
+ def _setup_llm(self):
549
+ """Set up the language model."""
550
+ # Set up model with video capabilities
551
+ generation_config = {
552
+ "temperature": 0.0,
553
+ "max_output_tokens": 2000,
554
+ "candidate_count": 1,
555
+ }
556
+
557
+ safety_settings = {
558
+ HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
559
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
560
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
561
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
562
+ }
563
+
564
+ return ChatGoogleGenerativeAI(
565
+ model="gemini-2.0-flash",
566
+ google_api_key=self.api_key,
567
+ temperature=0,
568
+ max_output_tokens=2000,
569
+ generation_config=generation_config,
570
+ safety_settings=safety_settings,
571
+ system_message=SystemMessage(
572
+ content=(
573
+ "You are a precise AI assistant that helps users find information and analyze content. "
574
+ "You can directly understand and analyze YouTube videos, images, and other content. "
575
+ "When analyzing videos, focus on relevant details like dialogue, text, and key visual elements. "
576
+ "For lists, tables, and structured data, ensure proper formatting and organization. "
577
+ "If you need additional context, clearly explain what is needed."
578
+ )
579
+ ),
580
+ )
581
+
582
+ def _setup_agent(self) -> AgentExecutor:
583
+ """Set up the agent with tools and system message."""
584
+
585
+ # Define the system message template
586
+ PREFIX = """You are a helpful AI assistant that can use various tools to answer questions and analyze content. You have access to tools for web search, Wikipedia lookup, and multimedia analysis.
587
+
588
+ TOOLS:
589
+ ------
590
+ You have access to the following tools:"""
591
+
592
+ FORMAT_INSTRUCTIONS = """To use a tool, use the following format:
593
+
594
+ Thought: Do I need to use a tool? Yes
595
+ Action: the action to take, should be one of [{tool_names}]
596
+ Action Input: the input to the action
597
+ Observation: the result of the action
598
+
599
+ When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:
600
+
601
+ Thought: Do I need to use a tool? No
602
+ Final Answer: [your response here]
603
+
604
+ Begin! Remember to ALWAYS include 'Thought:', 'Action:', 'Action Input:', and 'Final Answer:' in your responses."""
605
+
606
+ SUFFIX = """Previous conversation history:
607
+ {chat_history}
608
+
609
+ New question: {input}
610
+ {agent_scratchpad}"""
611
+
612
+ # Create the base agent
613
+ agent = ConversationalAgent.from_llm_and_tools(
614
+ llm=self.llm,
615
+ tools=self.tools,
616
+ prefix=PREFIX,
617
+ format_instructions=FORMAT_INSTRUCTIONS,
618
+ suffix=SUFFIX,
619
+ input_variables=["input", "chat_history", "agent_scratchpad", "tool_names"],
620
+ handle_parsing_errors=True,
621
+ )
622
+
623
+ # Initialize agent executor with custom output handling
624
+ return AgentExecutor.from_agent_and_tools(
625
+ agent=agent,
626
+ tools=self.tools,
627
+ memory=self.memory,
628
+ max_iterations=5,
629
+ verbose=True,
630
+ handle_parsing_errors=True,
631
+ return_only_outputs=True, # This ensures we only get the final output
632
+ )
633
+
634
+
635
+ @tool
636
+ def analyze_csv_file(file_path: str, query: str) -> str:
637
+ """
638
+ Analyze a CSV file using pandas and answer a question about it.
639
+
640
+ Args:
641
+ file_path: Path to the CSV file
642
+ query: Question about the data
643
+
644
+ Returns:
645
+ Analysis result or error message
646
+ """
647
+ try:
648
+ import pandas as pd
649
+
650
+ # Read the CSV file
651
+ df = pd.read_csv(file_path)
652
+
653
+ # Run various analyses based on the query
654
+ result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
655
+ result += f"Columns: {', '.join(df.columns)}\n\n"
656
+
657
+ # Add summary statistics
658
+ result += "Summary statistics:\n"
659
+ result += str(df.describe())
660
+
661
+ return result
662
+ except ImportError:
663
+ return "Error: pandas is not installed. Please install it with 'pip install pandas'."
664
+ except Exception as e:
665
+ return f"Error analyzing CSV file: {str(e)}"
666
+
667
+
668
+ @tool
669
+ def analyze_excel_file(file_path: str, query: str) -> str:
670
+ """
671
+ Analyze an Excel file using pandas and answer a question about it.
672
+
673
+ Args:
674
+ file_path: Path to the Excel file
675
+ query: Question about the data
676
+
677
+ Returns:
678
+ Analysis result or error message
679
+ """
680
+ try:
681
+ import pandas as pd
682
+
683
+ # Read the Excel file
684
+ df = pd.read_excel(file_path)
685
+
686
+ # Run various analyses based on the query
687
+ result = (
688
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
689
+ )
690
+ result += f"Columns: {', '.join(df.columns)}\n\n"
691
+
692
+ # Add summary statistics
693
+ result += "Summary statistics:\n"
694
+ result += str(df.describe())
695
+
696
+ return result
697
+ except ImportError:
698
+ return "Error: pandas and openpyxl are not installed. Please install them with 'pip install pandas openpyxl'."
699
+ except Exception as e:
700
+ return f"Error analyzing Excel file: {str(e)}"
app.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import requests
4
+ import pandas as pd
5
+ from dotenv import load_dotenv
6
+ from agent_gemini import GeminiAgent
7
+
8
+ # Constants
9
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
10
+
11
+
12
+ class BasicAgent:
13
+ def __init__(self):
14
+ print("Initializing the BasicAgent")
15
+
16
+ # Get Gemini API key
17
+ api_key = os.getenv("GOOGLE_API_KEY")
18
+ if not api_key:
19
+ raise ValueError("GOOGLE_API_KEY environment variable not set.")
20
+
21
+ # Initialize GeminiAgent
22
+ self.agent = GeminiAgent(api_key=api_key)
23
+ print("GeminiAgent initialized successfully")
24
+
25
+ def __call__(self, question: str) -> str:
26
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
27
+ final_answer = self.agent.run(question)
28
+ print(f"Agent returning fixed answer: {final_answer}")
29
+ return final_answer
30
+
31
+
32
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
33
+ """
34
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
35
+ and displays the results.
36
+ """
37
+ # --- Determine HF Space Runtime URL and Repo URL ---
38
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
39
+
40
+ if profile:
41
+ username = f"{profile.username}"
42
+ print(f"User logged in: {username}")
43
+ else:
44
+ print("User not logged in.")
45
+ return "Please Login to Hugging Face with the button.", None
46
+
47
+ api_url = DEFAULT_API_URL
48
+ questions_url = f"{api_url}/questions"
49
+ submit_url = f"{api_url}/submit"
50
+
51
+ # 1. Instantiate Agent ( modify this part to create your agent)
52
+ try:
53
+ agent = BasicAgent()
54
+ except Exception as e:
55
+ print(f"Error instantiating agent: {e}")
56
+ return f"Error initializing agent: {e}", None
57
+ # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
58
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
59
+ print(agent_code)
60
+
61
+ # 2. Fetch Questions
62
+ print(f"Fetching questions from: {questions_url}")
63
+ try:
64
+ response = requests.get(questions_url, timeout=15)
65
+ response.raise_for_status()
66
+ questions_data = response.json()
67
+ if not questions_data:
68
+ print("Fetched questions list is empty.")
69
+ return "Fetched questions list is empty or invalid format.", None
70
+ print(f"Fetched {len(questions_data)} questions.")
71
+ except requests.exceptions.RequestException as e:
72
+ print(f"Error fetching questions: {e}")
73
+ return f"Error fetching questions: {e}", None
74
+ except requests.exceptions.JSONDecodeError as e:
75
+ print(f"Error decoding JSON response from questions endpoint: {e}")
76
+ print(f"Response text: {response.text[:500]}")
77
+ return f"Error decoding server response for questions: {e}", None
78
+ except Exception as e:
79
+ print(f"An unexpected error occurred fetching questions: {e}")
80
+ return f"An unexpected error occurred fetching questions: {e}", None
81
+
82
+ # 3. Run your Agent
83
+ results_log = []
84
+ answers_payload = []
85
+ print(f"Running agent on {len(questions_data)} questions...")
86
+ for item in questions_data:
87
+ task_id = item.get("task_id")
88
+ question_text = item.get("question")
89
+ if not task_id or question_text is None:
90
+ print(f"Skipping item with missing task_id or question: {item}")
91
+ continue
92
+ try:
93
+ submitted_answer = agent(question_text)
94
+ answers_payload.append(
95
+ {"task_id": task_id, "submitted_answer": submitted_answer}
96
+ )
97
+ results_log.append(
98
+ {
99
+ "Task ID": task_id,
100
+ "Question": question_text,
101
+ "Submitted Answer": submitted_answer,
102
+ }
103
+ )
104
+ except Exception as e:
105
+ print(f"Error running agent on task {task_id}: {e}")
106
+ results_log.append(
107
+ {
108
+ "Task ID": task_id,
109
+ "Question": question_text,
110
+ "Submitted Answer": f"AGENT ERROR: {e}",
111
+ }
112
+ )
113
+
114
+ if not answers_payload:
115
+ print("Agent did not produce any answers to submit.")
116
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
117
+
118
+ # 4. Prepare Submission
119
+ submission_data = {
120
+ "username": username.strip(),
121
+ "agent_code": agent_code,
122
+ "answers": answers_payload,
123
+ }
124
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
125
+ print(status_update)
126
+
127
+ # 5. Submit
128
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
129
+ try:
130
+ response = requests.post(submit_url, json=submission_data, timeout=60)
131
+ response.raise_for_status()
132
+ result_data = response.json()
133
+ final_status = (
134
+ f"Submission Successful!\n"
135
+ f"User: {result_data.get('username')}\n"
136
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
137
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
138
+ f"Message: {result_data.get('message', 'No message received.')}"
139
+ )
140
+ print("Submission successful.")
141
+ results_df = pd.DataFrame(results_log)
142
+ return final_status, results_df
143
+ except requests.exceptions.HTTPError as e:
144
+ error_detail = f"Server responded with status {e.response.status_code}."
145
+ try:
146
+ error_json = e.response.json()
147
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
148
+ except requests.exceptions.JSONDecodeError:
149
+ error_detail += f" Response: {e.response.text[:500]}"
150
+ status_message = f"Submission Failed: {error_detail}"
151
+ print(status_message)
152
+ results_df = pd.DataFrame(results_log)
153
+ return status_message, results_df
154
+ except requests.exceptions.Timeout:
155
+ status_message = "Submission Failed: The request timed out."
156
+ print(status_message)
157
+ results_df = pd.DataFrame(results_log)
158
+ return status_message, results_df
159
+ except requests.exceptions.RequestException as e:
160
+ status_message = f"Submission Failed: Network error - {e}"
161
+ print(status_message)
162
+ results_df = pd.DataFrame(results_log)
163
+ return status_message, results_df
164
+ except Exception as e:
165
+ status_message = f"An unexpected error occurred during submission: {e}"
166
+ print(status_message)
167
+ results_df = pd.DataFrame(results_log)
168
+ return status_message, results_df
169
+
170
+
171
+ # --- Build Gradio Interface using Blocks ---
172
+ with gr.Blocks() as demo:
173
+ gr.Markdown("# Basic Agent Evaluation Runner")
174
+ gr.Markdown(
175
+ """
176
+ **Instructions:**
177
+
178
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
179
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
180
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
181
+
182
+ ---
183
+ **Disclaimers:**
184
+ 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).
185
+ 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.
186
+ """
187
+ )
188
+
189
+ gr.LoginButton()
190
+
191
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
192
+
193
+ status_output = gr.Textbox(
194
+ label="Run Status / Submission Result", lines=5, interactive=False
195
+ )
196
+ # Removed max_rows=10 from DataFrame constructor
197
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
198
+
199
+ run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
200
+
201
+ if __name__ == "__main__":
202
+ print("\n" + "-" * 30 + " App Starting " + "-" * 30)
203
+ # Check for SPACE_HOST and SPACE_ID at startup for information
204
+ space_host_startup = os.getenv("SPACE_HOST")
205
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
206
+
207
+ if space_host_startup:
208
+ print(f"βœ… SPACE_HOST found: {space_host_startup}")
209
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
210
+ else:
211
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
212
+
213
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
214
+ print(f"βœ… SPACE_ID found: {space_id_startup}")
215
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
216
+ print(
217
+ f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main"
218
+ )
219
+ else:
220
+ print(
221
+ "ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined."
222
+ )
223
+
224
+ print("-" * (60 + len(" App Starting ")) + "\n")
225
+
226
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
227
+ demo.launch(debug=True, share=False)
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ langchain>=0.1.0
3
+ langchain-core>=0.1.0
4
+ langchain-community>=0.0.10
5
+ langchain-google-genai>=0.0.6
6
+ google-generativeai>=0.3.0
7
+ python-dotenv>=1.0.0
8
+ google-api-python-client>=2.108.0
9
+ duckduckgo-search>=4.4
10
+ tiktoken>=0.5.2
11
+ google-cloud-speech>=2.24.0
12
+ requests>=2.31.0
13
+ pydub>=0.25.1
14
+ yt-dlp>=2023.12.30
15
+ smolagents>=0.1.3
16
+ wikipedia>=1.4.0
17
+ Pillow>=10.2.0
18
+ wikipedia-api>=0.6.0
system_prompt.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ You are a helpful assistant tasked with answering questions using a set of tools.
2
+ Now, I will ask you a question. Report your thoughts, and finish your answer with the following template:
3
+ FINAL ANSWER: [YOUR FINAL ANSWER].
4
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
5
+ Your answer should only start with "FINAL ANSWER: ", then follows with the answer.