Gralon commited on
Commit
dab1aa6
·
verified ·
1 Parent(s): bff8ed8

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +76 -5
  2. app.py +393 -0
  3. requirements.txt +11 -0
  4. tools.py +376 -0
README.md CHANGED
@@ -1,12 +1,83 @@
1
  ---
2
- title: MyGAIASimpleAgent Clean
3
- emoji: 👀
4
- colorFrom: red
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.26.0
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: GAIA Agent - Hugging Face Agents Course
3
+ emoji: 🧠
4
+ colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 5.25.2
8
  app_file: app.py
9
  pinned: false
10
+ hf_oauth: true
11
+ hf_oauth_expiration_minutes: 480
12
  ---
13
 
14
+ # GAIA Agent - Hugging Face Agents Course
15
+
16
+ This project implements an intelligent agent for the final assessment of the Hugging Face Agents course. The agent is designed to achieve a score of 30% or higher on the GAIA benchmark.
17
+
18
+ ## Features
19
+
20
+ - **Efficient Implementation**: Minimal yet powerful solution using smolagents
21
+ - **OpenAI Integration**: Option to use gpt-4o-mini (cost efficient) or gpt-4o (higher accuracy)
22
+ - **Web Search Capabilities**: Leverages DuckDuckGo search with rate limiting protections
23
+ - **File Processing**: Handles various file types like CSV, Excel, and images
24
+ - **Reverse Text Detection**: Automatically detects and handles reversed text questions
25
+ - **Cost Controls**: Sample size slider and model selection options to manage API costs
26
+
27
+ ## Usage
28
+
29
+ 1. Clone this repository
30
+ 2. Install the required dependencies:
31
+ ```bash
32
+ pip install -r requirements.txt
33
+ ```
34
+ 3. Create a `.env` file with your OpenAI API key:
35
+ ```
36
+ OPENAI_API_KEY=your_key_here
37
+ OPENAI_MODEL_ID=gpt-4o-mini # or gpt-4o for higher accuracy
38
+ ```
39
+ 4. Run the application:
40
+ ```bash
41
+ python app.py
42
+ ```
43
+
44
+ ## How it Works
45
+
46
+ The agent uses a CodeAgent from smolagents with enhanced prompting and multiple tools to solve the GAIA questions. It employs a straightforward approach that:
47
+
48
+ 1. Receives questions from the GAIA API
49
+ 2. Processes questions with specialized handling for reversed text
50
+ 3. Uses appropriate tools based on the question type
51
+ 4. Returns precise answers in the expected format
52
+
53
+ The agent is specifically designed to follow the GAIA benchmark format requirements, ensuring all answers are provided in the exact format expected by the evaluation system.
54
+
55
+ ## Tools
56
+
57
+ - Web search (DuckDuckGo with rate limiting protection)
58
+ - Reverse text analysis
59
+ - File processing tools for CSV and Excel files
60
+ - Image OCR capabilities
61
+ - Date and time utilities
62
+ - File download handling
63
+
64
+ ## Deployment
65
+
66
+ To deploy on Hugging Face Spaces:
67
+
68
+ 1. Create a new Space on Hugging Face
69
+ 2. Upload all files from this repository (EXCLUDING the .env file)
70
+ 3. Add the following secrets in your Space settings:
71
+ - OPENAI_API_KEY: Your OpenAI API key
72
+ - OPENAI_MODEL_ID: The model to use (gpt-4o-mini or gpt-4o)
73
+ 4. Set HF_OAUTH to true in your Space settings to enable login/authentication
74
+
75
+ ## Testing
76
+
77
+ You can use the test_single.py script to test the agent with individual questions locally:
78
+
79
+ ```bash
80
+ python test_single.py
81
+ ```
82
+
83
+ This helps verify functionality without incurring high API costs during development.
app.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import requests
4
+ import inspect
5
+ import pandas as pd
6
+ from dotenv import load_dotenv
7
+ from smolagents import CodeAgent, DuckDuckGoSearchTool, OpenAIServerModel
8
+ from tools import (
9
+ ReverseTextTool,
10
+ ExtractTextFromImageTool,
11
+ AnalyzeCSVTool,
12
+ AnalyzeExcelTool,
13
+ DateCalculatorTool,
14
+ DownloadFileTool
15
+ )
16
+
17
+ # Try to load environment variables
18
+ try:
19
+ load_dotenv()
20
+ print("Loaded environment variables from .env file")
21
+ except Exception as e:
22
+ print(f"Note: Could not load .env file - {e}")
23
+
24
+ # --- Constants ---
25
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
26
+
27
+ # --- GAIA Agent Definition ---
28
+ class GAIAAgent:
29
+ def __init__(self, verbose=False):
30
+ self.verbose = verbose
31
+ print("Initializing GAIA Agent...")
32
+
33
+ # Get API key
34
+ api_key = os.environ.get("OPENAI_API_KEY")
35
+ if not api_key:
36
+ raise ValueError("OpenAI API key not found. Please set the OPENAI_API_KEY environment variable.")
37
+
38
+ # Initialize model with gpt-4o-mini for cost efficiency
39
+ model_id = os.environ.get("OPENAI_MODEL_ID", "gpt-4o-mini") # Use environment variable or default to mini
40
+ print(f"Using OpenAI model: {model_id}")
41
+
42
+ model = OpenAIServerModel(
43
+ model_id=model_id,
44
+ api_key=api_key,
45
+ temperature=0.1
46
+ )
47
+
48
+ # Initialize tools with rate limiting for web search
49
+ # Note: Use a more compatible approach to rate limiting
50
+ # Instead of wait_time parameter, we'll handle delays explicitly in the agent prompts
51
+ duck_search_tool = DuckDuckGoSearchTool()
52
+
53
+ self.tools = [
54
+ duck_search_tool, # Web search
55
+ ReverseTextTool(), # Handling reversed text
56
+ ExtractTextFromImageTool(), # OCR for images
57
+ AnalyzeCSVTool(), # CSV analysis
58
+ AnalyzeExcelTool(), # Excel analysis
59
+ DateCalculatorTool(), # Date calculations
60
+ DownloadFileTool() # File downloads
61
+ ]
62
+
63
+ # Add more authorized imports to prevent common errors
64
+ additional_imports = [
65
+ "PyPDF2", "pdf2image", "pillow", "nltk", "sklearn",
66
+ "networkx", "matplotlib", "seaborn", "scipy", "time"
67
+ ]
68
+
69
+ # Initialize CodeAgent with planning, base tools and additional imports
70
+ self.agent = CodeAgent(
71
+ tools=self.tools,
72
+ model=model,
73
+ add_base_tools=True, # Add memory and other base tools
74
+ planning_interval=3, # Refresh planning every 3 steps
75
+ verbosity_level=2 if self.verbose else 0,
76
+ additional_authorized_imports=additional_imports
77
+ )
78
+
79
+ print("GAIA Agent initialized and ready")
80
+
81
+ def _is_reversed_text(self, text):
82
+ """Check if the text appears to be reversed"""
83
+ # Common patterns in reversed text
84
+ return (
85
+ text.startswith(".") or
86
+ ".rewsna eht sa" in text or
87
+ "esrever" in text or
88
+ "sdrawkcab" in text
89
+ )
90
+
91
+ def __call__(self, question: str) -> str:
92
+ """Process a question and return the answer"""
93
+ if self.verbose:
94
+ print(f"Processing question: {question[:100]}..." if len(question) > 100 else f"Processing question: {question}")
95
+
96
+ # Check if the question contains reversed text
97
+ if self._is_reversed_text(question):
98
+ if self.verbose:
99
+ print("Detected reversed text, will handle accordingly")
100
+
101
+ # Create a prompt that explicitly mentions the reversed text with GAIA guidelines
102
+ # Add guidance to limit tool usage and prevent infinite loops
103
+ prompt = f"""
104
+ You are a general AI assistant. I will ask you a question.
105
+
106
+ This question appears to be in reversed text. Here is the reversed version for clarity:
107
+ {question[::-1]}
108
+
109
+ Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].
110
+
111
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
112
+ - 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.
113
+ - 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.
114
+ - 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.
115
+
116
+ IMPORTANT NOTES TO LIMIT COSTS AND PREVENT ERRORS:
117
+ - Use web search sparingly and only when absolutely necessary.
118
+ - Limit to 1-2 web searches per question.
119
+ - If a search fails due to rate limiting, add a 3-5 second delay using time.sleep() before retrying with a different search term.
120
+ - Do not import libraries that aren't available - stick to basic Python and the tools provided.
121
+ - Focus on answering directly with what you already know when possible.
122
+ - If you've made more than 3 attempts to solve a problem, prioritize providing your best guess.
123
+ - Always add a delay of 2-3 seconds between web searches using time.sleep() to avoid rate limiting.
124
+
125
+ Remember to structure your response in Python code format using the final_answer() function.
126
+ """
127
+ else:
128
+ # For normal questions, create a standard prompt following GAIA guidelines
129
+ # Add guidance to limit tool usage and prevent infinite loops
130
+ prompt = f"""
131
+ You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].
132
+
133
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
134
+ - 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.
135
+ - 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.
136
+ - 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.
137
+
138
+ Question: {question}
139
+
140
+ IMPORTANT NOTES TO LIMIT COSTS AND PREVENT ERRORS:
141
+ - Use web search sparingly and only when absolutely necessary.
142
+ - Limit to 1-2 web searches per question.
143
+ - If a search fails due to rate limiting, add a 3-5 second delay using time.sleep() before retrying with a different search term.
144
+ - Do not import libraries that aren't available - stick to basic Python and the tools provided.
145
+ - Focus on answering directly with what you already know when possible.
146
+ - If you've made more than 3 attempts to solve a problem, prioritize providing your best guess.
147
+ - Always add a delay of 2-3 seconds between web searches using time.sleep() to avoid rate limiting.
148
+
149
+ Remember to structure your response in Python code format using the final_answer() function.
150
+ """
151
+
152
+ # Run the agent
153
+ try:
154
+ answer = self.agent.run(prompt)
155
+
156
+ if self.verbose:
157
+ print(f"Generated answer: {answer}")
158
+
159
+ return answer
160
+ except Exception as e:
161
+ error_msg = f"Error processing question: {e}"
162
+ if self.verbose:
163
+ print(error_msg)
164
+ return error_msg
165
+
166
+ def run_and_submit_all(profile: gr.OAuthProfile | None, sample_size: int = 0):
167
+ """
168
+ Fetches all questions, runs the agent on them, submits all answers,
169
+ and displays the results.
170
+
171
+ Args:
172
+ profile: User profile for authentication
173
+ sample_size: Number of questions to process (0 for all questions)
174
+ """
175
+ # --- Determine HF Space Runtime URL and Repo URL ---
176
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
177
+
178
+ if profile:
179
+ username = f"{profile.username}"
180
+ print(f"User logged in: {username}")
181
+ else:
182
+ print("User not logged in.")
183
+ return "Please Login to Hugging Face with the button.", None
184
+
185
+ api_url = DEFAULT_API_URL
186
+ questions_url = f"{api_url}/questions"
187
+ submit_url = f"{api_url}/submit"
188
+
189
+ # 1. Instantiate Agent
190
+ try:
191
+ agent = GAIAAgent(verbose=True)
192
+ except Exception as e:
193
+ print(f"Error instantiating agent: {e}")
194
+ return f"Error initializing agent: {e}", None
195
+
196
+ # Get the code URL for submission
197
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
198
+ print(f"Agent code URL: {agent_code}")
199
+
200
+ # 2. Fetch Questions
201
+ print(f"Fetching questions from: {questions_url}")
202
+ try:
203
+ response = requests.get(questions_url, timeout=15)
204
+ response.raise_for_status()
205
+ questions_data = response.json()
206
+ if not questions_data:
207
+ print("Fetched questions list is empty.")
208
+ return "Fetched questions list is empty or invalid format.", None
209
+ print(f"Fetched {len(questions_data)} questions.")
210
+ except requests.exceptions.RequestException as e:
211
+ print(f"Error fetching questions: {e}")
212
+ return f"Error fetching questions: {e}", None
213
+ except requests.exceptions.JSONDecodeError as e:
214
+ print(f"Error decoding JSON response from questions endpoint: {e}")
215
+ print(f"Response text: {response.text[:500]}")
216
+ return f"Error decoding server response for questions: {e}", None
217
+ except Exception as e:
218
+ print(f"An unexpected error occurred fetching questions: {e}")
219
+ return f"An unexpected error occurred fetching questions: {e}", None
220
+
221
+ # 3. Run Agent on Questions
222
+ results_log = []
223
+ answers_payload = []
224
+
225
+ # Limit number of questions if sample_size is specified
226
+ if sample_size > 0 and sample_size < len(questions_data):
227
+ import random
228
+ print(f"Using a sample of {sample_size} questions from {len(questions_data)} total questions")
229
+ questions_data = random.sample(questions_data, sample_size)
230
+
231
+ print(f"Running agent on {len(questions_data)} questions...")
232
+ for i, item in enumerate(questions_data):
233
+ task_id = item.get("task_id")
234
+ question_text = item.get("question")
235
+ if not task_id or question_text is None:
236
+ print(f"Skipping item with missing task_id or question: {item}")
237
+ continue
238
+ try:
239
+ print(f"Processing question {i+1}/{len(questions_data)}: Task ID {task_id}")
240
+ submitted_answer = agent(question_text)
241
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
242
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
243
+ print(f"Successfully processed question {i+1}")
244
+
245
+ # Add a delay between questions to avoid rate limiting
246
+ if i < len(questions_data) - 1:
247
+ import time
248
+ print("Waiting 2 seconds before next question...")
249
+ time.sleep(2)
250
+
251
+ except Exception as e:
252
+ print(f"Error running agent on task {task_id}: {e}")
253
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
254
+
255
+ if not answers_payload:
256
+ print("Agent did not produce any answers to submit.")
257
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
258
+
259
+ # 4. Prepare Submission
260
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
261
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
262
+ print(status_update)
263
+
264
+ # 5. Submit
265
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
266
+ try:
267
+ response = requests.post(submit_url, json=submission_data, timeout=60)
268
+ response.raise_for_status()
269
+ result_data = response.json()
270
+ final_status = (
271
+ f"Submission Successful!\n"
272
+ f"User: {result_data.get('username')}\n"
273
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
274
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
275
+ f"Message: {result_data.get('message', 'No message received.')}"
276
+ )
277
+ print("Submission successful.")
278
+ results_df = pd.DataFrame(results_log)
279
+ return final_status, results_df
280
+ except requests.exceptions.HTTPError as e:
281
+ error_detail = f"Server responded with status {e.response.status_code}."
282
+ try:
283
+ error_json = e.response.json()
284
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
285
+ except requests.exceptions.JSONDecodeError:
286
+ error_detail += f" Response: {e.response.text[:500]}"
287
+ status_message = f"Submission Failed: {error_detail}"
288
+ print(status_message)
289
+ results_df = pd.DataFrame(results_log)
290
+ return status_message, results_df
291
+ except requests.exceptions.Timeout:
292
+ status_message = "Submission Failed: The request timed out."
293
+ print(status_message)
294
+ results_df = pd.DataFrame(results_log)
295
+ return status_message, results_df
296
+ except requests.exceptions.RequestException as e:
297
+ status_message = f"Submission Failed: Network error - {e}"
298
+ print(status_message)
299
+ results_df = pd.DataFrame(results_log)
300
+ return status_message, results_df
301
+ except Exception as e:
302
+ status_message = f"An unexpected error occurred during submission: {e}"
303
+ print(status_message)
304
+ results_df = pd.DataFrame(results_log)
305
+ return status_message, results_df
306
+
307
+ def test_single_question(question: str) -> str:
308
+ """Test the agent on a single question"""
309
+ try:
310
+ agent = GAIAAgent(verbose=True)
311
+ answer = agent(question)
312
+ return answer
313
+ except Exception as e:
314
+ return f"Error: {e}"
315
+
316
+ # --- Build Gradio Interface using Blocks ---
317
+ with gr.Blocks() as demo:
318
+ gr.Markdown("# GAIA Agent Evaluation Runner")
319
+ gr.Markdown(
320
+ """
321
+ ## Instructions:
322
+
323
+ 1. Log in to your Hugging Face account using the button below
324
+ 2. Test your agent on individual questions in the Testing tab
325
+ 3. Run the full evaluation on the GAIA benchmark in the Evaluation tab
326
+
327
+ This agent is designed to achieve a score of 30% or higher on the GAIA benchmark.
328
+ """
329
+ )
330
+
331
+ gr.LoginButton()
332
+
333
+ with gr.Tab("Test Single Question"):
334
+ test_input = gr.Textbox(label="Enter a question to test", lines=3)
335
+ test_output = gr.Textbox(label="Answer", lines=3)
336
+ test_button = gr.Button("Test Question")
337
+
338
+ test_button.click(
339
+ fn=test_single_question,
340
+ inputs=test_input,
341
+ outputs=test_output
342
+ )
343
+
344
+ with gr.Tab("Full Evaluation"):
345
+ with gr.Row():
346
+ sample_size = gr.Slider(
347
+ minimum=0,
348
+ maximum=20,
349
+ value=0,
350
+ step=1,
351
+ label="Sample Size (0 for all questions)",
352
+ info="Set a number to limit how many questions to process (reduces costs)"
353
+ )
354
+
355
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
356
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
357
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
358
+
359
+ run_button.click(
360
+ fn=run_and_submit_all,
361
+ inputs=[gr.LoginButton(), sample_size],
362
+ outputs=[status_output, results_table]
363
+ )
364
+
365
+ if __name__ == "__main__":
366
+ print("\n" + "-"*30 + " GAIA Agent Starting " + "-"*30)
367
+
368
+ # Check for API key
369
+ api_key = os.environ.get("OPENAI_API_KEY")
370
+ if not api_key:
371
+ print("WARNING: OpenAI API key not found. Please set OPENAI_API_KEY environment variable.")
372
+ else:
373
+ print("OpenAI API key found.")
374
+
375
+ # Check environment variables
376
+ space_host = os.getenv("SPACE_HOST")
377
+ space_id = os.getenv("SPACE_ID")
378
+
379
+ if space_host:
380
+ print(f"✅ Running in Hugging Face Space: {space_host}")
381
+ print(f" Runtime URL: https://{space_host}.hf.space")
382
+ else:
383
+ print("ℹ️ Running locally")
384
+
385
+ if space_id:
386
+ print(f"✅ Space ID: {space_id}")
387
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id}")
388
+ print(f" Code URL: https://huggingface.co/spaces/{space_id}/tree/main")
389
+
390
+ print("-"*78 + "\n")
391
+
392
+ print("Launching Gradio Interface...")
393
+ demo.launch(debug=True)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ gradio[oauth]
3
+ itsdangerous
4
+ requests
5
+ pandas
6
+ numpy
7
+ smolagents
8
+ smolagents[openai]
9
+ python-dotenv
10
+ openai>=1.0.0
11
+ litellm
tools.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from smolagents import Tool
2
+ import pandas as pd
3
+ import os
4
+ import tempfile
5
+ import requests
6
+ from urllib.parse import urlparse
7
+ import json
8
+ import re
9
+ from datetime import datetime, timedelta
10
+
11
+ class ReverseTextTool(Tool):
12
+ name = "reverse_text"
13
+ description = "Reverses the text in a string."
14
+ inputs = {
15
+ "text": {
16
+ "type": "string",
17
+ "description": "The text to reverse."
18
+ }
19
+ }
20
+ output_type = "string"
21
+
22
+ def forward(self, text: str) -> str:
23
+ return text[::-1]
24
+
25
+ class ExtractTextFromImageTool(Tool):
26
+ name = "extract_text_from_image"
27
+ description = "Extracts text from an image file using OCR."
28
+ inputs = {
29
+ "image_path": {
30
+ "type": "string",
31
+ "description": "Path to the image file."
32
+ }
33
+ }
34
+ output_type = "string"
35
+
36
+ def forward(self, image_path: str) -> str:
37
+ try:
38
+ # Try to import pytesseract
39
+ import pytesseract
40
+ from PIL import Image
41
+
42
+ # Open the image
43
+ image = Image.open(image_path)
44
+
45
+ # Try different configurations for better results
46
+ configs = [
47
+ '--psm 6', # Assume a single uniform block of text
48
+ '--psm 3', # Automatic page segmentation, but no OSD
49
+ '--psm 1', # Automatic page segmentation with OSD
50
+ ]
51
+
52
+ results = []
53
+ for config in configs:
54
+ try:
55
+ text = pytesseract.image_to_string(image, config=config)
56
+ if text.strip():
57
+ results.append(text)
58
+ except Exception:
59
+ continue
60
+
61
+ if results:
62
+ # Return the longest result, which is likely the most complete
63
+ return f"Extracted text from image:\n\n{max(results, key=len)}"
64
+ else:
65
+ return "No text could be extracted from the image."
66
+ except ImportError:
67
+ return "Error: pytesseract is not installed. Please install it with 'pip install pytesseract' and ensure Tesseract OCR is installed on your system."
68
+ except Exception as e:
69
+ return f"Error extracting text from image: {str(e)}"
70
+
71
+ class AnalyzeCSVTool(Tool):
72
+ name = "analyze_csv_file"
73
+ description = "Analyzes a CSV file and provides information about its contents."
74
+ inputs = {
75
+ "file_path": {
76
+ "type": "string",
77
+ "description": "Path to the CSV file."
78
+ },
79
+ "query": {
80
+ "type": "string",
81
+ "description": "Optional query about the data.",
82
+ "default": "",
83
+ "nullable": True
84
+ }
85
+ }
86
+ output_type = "string"
87
+
88
+ def forward(self, file_path: str, query: str = "") -> str:
89
+ try:
90
+ # Read CSV file with different encodings if needed
91
+ for encoding in ['utf-8', 'latin1', 'iso-8859-1', 'cp1252']:
92
+ try:
93
+ df = pd.read_csv(file_path, encoding=encoding)
94
+ break
95
+ except UnicodeDecodeError:
96
+ continue
97
+ else:
98
+ return "Error: Could not read the CSV file with any of the attempted encodings."
99
+
100
+ # Basic information
101
+ result = f"CSV file has {len(df)} rows and {len(df.columns)} columns.\n"
102
+ result += f"Columns: {', '.join(df.columns)}\n\n"
103
+
104
+ # If there's a specific query
105
+ if query:
106
+ if "count" in query.lower():
107
+ result += f"Row count: {len(df)}\n"
108
+
109
+ # Look for column-specific queries
110
+ for col in df.columns:
111
+ if col.lower() in query.lower():
112
+ result += f"\nColumn '{col}' information:\n"
113
+ if pd.api.types.is_numeric_dtype(df[col]):
114
+ result += f"Min: {df[col].min()}\n"
115
+ result += f"Max: {df[col].max()}\n"
116
+ result += f"Mean: {df[col].mean()}\n"
117
+ result += f"Median: {df[col].median()}\n"
118
+ else:
119
+ # For categorical data
120
+ value_counts = df[col].value_counts().head(10)
121
+ result += f"Unique values: {df[col].nunique()}\n"
122
+ result += f"Top values:\n{value_counts.to_string()}\n"
123
+
124
+ # General statistics for all columns
125
+ else:
126
+ # For numeric columns
127
+ numeric_cols = df.select_dtypes(include=['number']).columns
128
+ if len(numeric_cols) > 0:
129
+ result += "Numeric columns statistics:\n"
130
+ result += df[numeric_cols].describe().to_string()
131
+ result += "\n\n"
132
+
133
+ # For categorical columns, show counts of unique values
134
+ cat_cols = df.select_dtypes(exclude=['number']).columns
135
+ if len(cat_cols) > 0:
136
+ result += "Categorical columns:\n"
137
+ for col in cat_cols[:5]: # Limit to first 5 columns
138
+ result += f"- {col}: {df[col].nunique()} unique values\n"
139
+
140
+ return result
141
+ except Exception as e:
142
+ return f"Error analyzing CSV file: {str(e)}"
143
+
144
+ class AnalyzeExcelTool(Tool):
145
+ name = "analyze_excel_file"
146
+ description = "Analyzes an Excel file and provides information about its contents."
147
+ inputs = {
148
+ "file_path": {
149
+ "type": "string",
150
+ "description": "Path to the Excel file."
151
+ },
152
+ "query": {
153
+ "type": "string",
154
+ "description": "Optional query about the data.",
155
+ "default": "",
156
+ "nullable": True
157
+ },
158
+ "sheet_name": {
159
+ "type": "string",
160
+ "description": "Name of the sheet to analyze (defaults to first sheet).",
161
+ "default": None,
162
+ "nullable": True
163
+ }
164
+ }
165
+ output_type = "string"
166
+
167
+ def forward(self, file_path: str, query: str = "", sheet_name: str = None) -> str:
168
+ try:
169
+ # Read sheet names first
170
+ excel_file = pd.ExcelFile(file_path)
171
+ sheet_names = excel_file.sheet_names
172
+
173
+ # Info about all sheets
174
+ result = f"Excel file contains {len(sheet_names)} sheets: {', '.join(sheet_names)}\n\n"
175
+
176
+ # If sheet name is specified, use it; otherwise use first sheet
177
+ if sheet_name is None:
178
+ sheet_name = sheet_names[0]
179
+ elif sheet_name not in sheet_names:
180
+ return f"Error: Sheet '{sheet_name}' not found. Available sheets: {', '.join(sheet_names)}"
181
+
182
+ # Read the specified sheet
183
+ df = pd.read_excel(file_path, sheet_name=sheet_name)
184
+
185
+ # Basic information
186
+ result += f"Sheet '{sheet_name}' has {len(df)} rows and {len(df.columns)} columns.\n"
187
+ result += f"Columns: {', '.join(df.columns)}\n\n"
188
+
189
+ # Handle query similar to CSV tool
190
+ if query:
191
+ if "count" in query.lower():
192
+ result += f"Row count: {len(df)}\n"
193
+
194
+ # Look for column-specific queries
195
+ for col in df.columns:
196
+ if col.lower() in query.lower():
197
+ result += f"\nColumn '{col}' information:\n"
198
+ if pd.api.types.is_numeric_dtype(df[col]):
199
+ result += f"Min: {df[col].min()}\n"
200
+ result += f"Max: {df[col].max()}\n"
201
+ result += f"Mean: {df[col].mean()}\n"
202
+ result += f"Median: {df[col].median()}\n"
203
+ else:
204
+ # For categorical data
205
+ value_counts = df[col].value_counts().head(10)
206
+ result += f"Unique values: {df[col].nunique()}\n"
207
+ result += f"Top values:\n{value_counts.to_string()}\n"
208
+ else:
209
+ # For numeric columns
210
+ numeric_cols = df.select_dtypes(include=['number']).columns
211
+ if len(numeric_cols) > 0:
212
+ result += "Numeric columns statistics:\n"
213
+ result += df[numeric_cols].describe().to_string()
214
+ result += "\n\n"
215
+
216
+ # For categorical columns, show counts of unique values
217
+ cat_cols = df.select_dtypes(exclude=['number']).columns
218
+ if len(cat_cols) > 0:
219
+ result += "Categorical columns:\n"
220
+ for col in cat_cols[:5]: # Limit to first 5 columns
221
+ result += f"- {col}: {df[col].nunique()} unique values\n"
222
+
223
+ return result
224
+ except Exception as e:
225
+ return f"Error analyzing Excel file: {str(e)}"
226
+
227
+ class DateCalculatorTool(Tool):
228
+ name = "date_calculator"
229
+ description = "Performs date calculations like adding days, formatting dates, etc."
230
+ inputs = {
231
+ "query": {
232
+ "type": "string",
233
+ "description": "The date calculation to perform (e.g., 'What day is 10 days from today?', 'Format 2023-05-15 as MM/DD/YYYY')"
234
+ }
235
+ }
236
+ output_type = "string"
237
+
238
+ def forward(self, query: str) -> str:
239
+ try:
240
+ # Get current date/time
241
+ if re.search(r'(today|now|current date|current time)', query, re.IGNORECASE):
242
+ now = datetime.now()
243
+
244
+ if 'time' in query.lower():
245
+ return f"Current date and time: {now.strftime('%Y-%m-%d %H:%M:%S')}"
246
+ else:
247
+ return f"Today's date: {now.strftime('%Y-%m-%d')}"
248
+
249
+ # Add days to a date
250
+ add_match = re.search(r'(what|when).+?(\d+)\s+(day|days|week|weeks|month|months|year|years)\s+(from|after)\s+(.+)', query, re.IGNORECASE)
251
+ if add_match:
252
+ amount = int(add_match.group(2))
253
+ unit = add_match.group(3).lower()
254
+ date_text = add_match.group(5).strip()
255
+
256
+ # Parse the date
257
+ if date_text.lower() in ['today', 'now']:
258
+ base_date = datetime.now()
259
+ else:
260
+ try:
261
+ # Try various date formats
262
+ for fmt in ['%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%B %d, %Y']:
263
+ try:
264
+ base_date = datetime.strptime(date_text, fmt)
265
+ break
266
+ except ValueError:
267
+ continue
268
+ else:
269
+ return f"Could not parse date: {date_text}"
270
+ except Exception as e:
271
+ return f"Error parsing date: {e}"
272
+
273
+ # Calculate new date
274
+ if 'day' in unit:
275
+ new_date = base_date + timedelta(days=amount)
276
+ elif 'week' in unit:
277
+ new_date = base_date + timedelta(weeks=amount)
278
+ elif 'month' in unit:
279
+ # Simplified month calculation
280
+ new_month = base_date.month + amount
281
+ new_year = base_date.year + (new_month - 1) // 12
282
+ new_month = ((new_month - 1) % 12) + 1
283
+ new_date = base_date.replace(year=new_year, month=new_month)
284
+ elif 'year' in unit:
285
+ new_date = base_date.replace(year=base_date.year + amount)
286
+
287
+ return f"Date {amount} {unit} from {base_date.strftime('%Y-%m-%d')} is {new_date.strftime('%Y-%m-%d')}"
288
+
289
+ # Format a date
290
+ format_match = re.search(r'format\s+(.+?)\s+as\s+(.+)', query, re.IGNORECASE)
291
+ if format_match:
292
+ date_text = format_match.group(1).strip()
293
+ format_spec = format_match.group(2).strip()
294
+
295
+ # Parse the date
296
+ if date_text.lower() in ['today', 'now']:
297
+ date_obj = datetime.now()
298
+ else:
299
+ try:
300
+ # Try various date formats
301
+ for fmt in ['%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%B %d, %Y']:
302
+ try:
303
+ date_obj = datetime.strptime(date_text, fmt)
304
+ break
305
+ except ValueError:
306
+ continue
307
+ else:
308
+ return f"Could not parse date: {date_text}"
309
+ except Exception as e:
310
+ return f"Error parsing date: {e}"
311
+
312
+ # Convert format specification to strftime format
313
+ format_mapping = {
314
+ 'YYYY': '%Y',
315
+ 'YY': '%y',
316
+ 'MM': '%m',
317
+ 'DD': '%d',
318
+ 'HH': '%H',
319
+ 'mm': '%M',
320
+ 'ss': '%S'
321
+ }
322
+
323
+ strftime_format = format_spec
324
+ for key, value in format_mapping.items():
325
+ strftime_format = strftime_format.replace(key, value)
326
+
327
+ return f"Formatted date: {date_obj.strftime(strftime_format)}"
328
+
329
+ return "I couldn't understand the date calculation query."
330
+ except Exception as e:
331
+ return f"Error performing date calculation: {str(e)}"
332
+
333
+ class DownloadFileTool(Tool):
334
+ name = "download_file"
335
+ description = "Downloads a file from a URL and saves it locally."
336
+ inputs = {
337
+ "url": {
338
+ "type": "string",
339
+ "description": "The URL to download from."
340
+ },
341
+ "filename": {
342
+ "type": "string",
343
+ "description": "Optional filename to save as (default: derived from URL).",
344
+ "default": None,
345
+ "nullable": True
346
+ }
347
+ }
348
+ output_type = "string"
349
+
350
+ def forward(self, url: str, filename: str = None) -> str:
351
+ try:
352
+ # Parse URL to get filename if not provided
353
+ if not filename:
354
+ path = urlparse(url).path
355
+ filename = os.path.basename(path)
356
+ if not filename:
357
+ # Generate a random name if we couldn't extract one
358
+ import uuid
359
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
360
+
361
+ # Create temporary file
362
+ temp_dir = tempfile.gettempdir()
363
+ filepath = os.path.join(temp_dir, filename)
364
+
365
+ # Download the file
366
+ response = requests.get(url, stream=True)
367
+ response.raise_for_status()
368
+
369
+ # Save the file
370
+ with open(filepath, 'wb') as f:
371
+ for chunk in response.iter_content(chunk_size=8192):
372
+ f.write(chunk)
373
+
374
+ return f"File downloaded to {filepath}. You can now analyze this file."
375
+ except Exception as e:
376
+ return f"Error downloading file: {str(e)}"