agno 1.7.5__py3-none-any.whl → 1.7.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. agno/agent/agent.py +5 -24
  2. agno/app/agui/async_router.py +5 -5
  3. agno/app/agui/sync_router.py +5 -5
  4. agno/app/agui/utils.py +84 -14
  5. agno/app/playground/app.py +3 -2
  6. agno/document/chunking/row.py +39 -0
  7. agno/document/reader/base.py +0 -7
  8. agno/embedder/jina.py +73 -0
  9. agno/embedder/openai.py +5 -1
  10. agno/memory/agent.py +2 -2
  11. agno/memory/team.py +2 -2
  12. agno/models/anthropic/claude.py +9 -1
  13. agno/models/aws/bedrock.py +311 -15
  14. agno/models/google/gemini.py +26 -6
  15. agno/models/litellm/chat.py +38 -7
  16. agno/models/message.py +1 -0
  17. agno/models/openai/chat.py +1 -22
  18. agno/models/openai/responses.py +5 -5
  19. agno/models/portkey/__init__.py +3 -0
  20. agno/models/portkey/portkey.py +88 -0
  21. agno/models/xai/xai.py +54 -0
  22. agno/run/v2/workflow.py +4 -0
  23. agno/storage/mysql.py +2 -0
  24. agno/storage/postgres.py +5 -3
  25. agno/storage/session/v2/workflow.py +29 -5
  26. agno/storage/singlestore.py +4 -1
  27. agno/storage/sqlite.py +0 -1
  28. agno/team/team.py +38 -36
  29. agno/tools/bitbucket.py +292 -0
  30. agno/tools/daytona.py +411 -63
  31. agno/tools/evm.py +123 -0
  32. agno/tools/jina.py +13 -6
  33. agno/tools/linkup.py +54 -0
  34. agno/tools/mcp.py +170 -26
  35. agno/tools/mem0.py +15 -2
  36. agno/tools/models/morph.py +186 -0
  37. agno/tools/postgres.py +186 -168
  38. agno/tools/zep.py +21 -32
  39. agno/utils/log.py +16 -0
  40. agno/utils/models/claude.py +1 -0
  41. agno/utils/string.py +14 -0
  42. agno/vectordb/pgvector/pgvector.py +4 -5
  43. agno/workflow/v2/workflow.py +152 -25
  44. agno/workflow/workflow.py +90 -63
  45. {agno-1.7.5.dist-info → agno-1.7.7.dist-info}/METADATA +20 -3
  46. {agno-1.7.5.dist-info → agno-1.7.7.dist-info}/RECORD +50 -42
  47. {agno-1.7.5.dist-info → agno-1.7.7.dist-info}/WHEEL +0 -0
  48. {agno-1.7.5.dist-info → agno-1.7.7.dist-info}/entry_points.txt +0 -0
  49. {agno-1.7.5.dist-info → agno-1.7.7.dist-info}/licenses/LICENSE +0 -0
  50. {agno-1.7.5.dist-info → agno-1.7.7.dist-info}/top_level.txt +0 -0
agno/tools/daytona.py CHANGED
@@ -1,23 +1,56 @@
1
1
  import json
2
2
  from os import getenv
3
- from typing import Any, Dict, List, Optional
3
+ from pathlib import Path
4
+ from textwrap import dedent
5
+ from typing import Dict, Optional, Union
4
6
 
7
+ from agno.agent import Agent
8
+ from agno.team import Team
5
9
  from agno.tools import Toolkit
6
10
  from agno.utils.code_execution import prepare_python_code
7
- from agno.utils.log import logger
11
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
8
12
 
9
13
  try:
10
- from daytona_sdk import (
14
+ from daytona import (
11
15
  CodeLanguage,
12
- CreateSandboxParams,
16
+ CreateSandboxFromSnapshotParams,
13
17
  Daytona,
14
18
  DaytonaConfig,
15
19
  Sandbox,
16
- SandboxTargetRegion,
17
20
  )
18
- from daytona_sdk.common.process import ExecuteResponse
19
21
  except ImportError:
20
- raise ImportError("`daytona_sdk` not installed. Please install using `pip install daytona_sdk`")
22
+ raise ImportError("`daytona` not installed. Please install using `pip install daytona`")
23
+
24
+ DEFAULT_INSTRUCTIONS = dedent(
25
+ """\
26
+ You have access to a persistent Daytona sandbox for code execution. The sandbox maintains state across interactions.
27
+
28
+ Available tools:
29
+ - `run_code`: Execute code in the sandbox
30
+ - `run_shell_command`: Execute shell commands (bash)
31
+ - `create_file`: Create or update files
32
+ - `read_file`: Read file contents
33
+ - `list_files`: List directory contents
34
+ - `delete_file`: Delete files or directories
35
+ - `change_directory`: Change the working directory
36
+
37
+ MANDATORY: When users ask for code (Python, JavaScript, TypeScript, etc.), you MUST:
38
+ 1. Write the code
39
+ 2. Execute it using run_code tool
40
+ 3. Show the actual output/results
41
+ 4. Never just provide code without executing it
42
+
43
+ CRITICAL WORKFLOW:
44
+ 1. Before running Python scripts, check if required packages are installed
45
+ 2. Install missing packages with: run_shell_command("pip install package1 package2")
46
+ 3. When running scripts, capture both output AND errors
47
+ 4. If a script produces no output, check for errors or add print statements
48
+
49
+ IMPORTANT: Always use single quotes for the content parameter when creating files
50
+
51
+ Remember: Your job is to provide working, executed code examples, not just code snippets!
52
+ """
53
+ )
21
54
 
22
55
 
23
56
  class DaytonaTools(Toolkit):
@@ -25,106 +58,421 @@ class DaytonaTools(Toolkit):
25
58
  self,
26
59
  api_key: Optional[str] = None,
27
60
  api_url: Optional[str] = None,
61
+ sandbox_id: Optional[str] = None,
28
62
  sandbox_language: Optional[CodeLanguage] = None,
29
- sandbox_target_region: Optional[SandboxTargetRegion] = None,
63
+ sandbox_target: Optional[str] = None,
30
64
  sandbox_os: Optional[str] = None,
65
+ auto_stop_interval: Optional[int] = 60, # Stop after 1 hour
31
66
  sandbox_os_user: Optional[str] = None,
32
67
  sandbox_env_vars: Optional[Dict[str, str]] = None,
33
68
  sandbox_labels: Optional[Dict[str, str]] = None,
34
69
  sandbox_public: Optional[bool] = None,
35
- sandbox_auto_stop_interval: Optional[int] = None,
36
70
  organization_id: Optional[str] = None,
37
- timeout: int = 300, # 5 minutes default timeout
71
+ timeout: int = 300,
72
+ auto_create_sandbox: bool = True,
73
+ verify_ssl: Optional[bool] = False,
74
+ persistent: bool = True,
75
+ instructions: Optional[str] = None,
76
+ add_instructions: bool = False,
38
77
  **kwargs,
39
78
  ):
40
- """Daytona Toolkit for remote code execution.
41
-
42
- Args:
43
- api_key: Daytona API key (defaults to DAYTONA_API_KEY environment variable)
44
- api_url: Daytona API URL (defaults to DAYTONA_API_URL environment variable)
45
- sandbox_language: The programming language to run on the sandbox (default: python)
46
- sandbox_target_region: The region where the sandbox will be created
47
- sandbox_os: The operating system to run on the sandbox (default: ubuntu)
48
- sandbox_os_user: The user to run the sandbox as (default: root)
49
- sandbox_env_vars: The environment variables to set in the sandbox
50
- sandbox_labels: The labels to set in the sandbox
51
- sandbox_public: Whether the sandbox should be public
52
- sandbox_auto_stop_interval: The interval in minutes after which the sandbox will be stopped if no activity occurs
53
- organization_id: The contextual Daytona organization ID for the sandbox
54
- timeout: Timeout in seconds for communication with the sandbox (default: 5 minutes)
55
- """
56
-
57
79
  self.api_key = api_key or getenv("DAYTONA_API_KEY")
58
80
  if not self.api_key:
59
81
  raise ValueError("DAYTONA_API_KEY not set. Please set the DAYTONA_API_KEY environment variable.")
60
82
 
61
83
  self.api_url = api_url or getenv("DAYTONA_API_URL")
62
- self.sandbox_target_region = sandbox_target_region
84
+ self.sandbox_id = sandbox_id
85
+ self.sandbox_target = sandbox_target
63
86
  self.organization_id = organization_id
64
- self.sandbox_language = sandbox_language
87
+ self.sandbox_language = sandbox_language or CodeLanguage.PYTHON
65
88
  self.sandbox_os = sandbox_os
89
+ self.auto_stop_interval = auto_stop_interval
66
90
  self.sandbox_os_user = sandbox_os_user
67
91
  self.sandbox_env_vars = sandbox_env_vars
68
- self.sandbox_labels = sandbox_labels
92
+ self.sandbox_labels = sandbox_labels or {}
69
93
  self.sandbox_public = sandbox_public
70
- self.sandbox_auto_stop_interval = sandbox_auto_stop_interval
71
94
  self.timeout = timeout
95
+ self.auto_create_sandbox = auto_create_sandbox
96
+ self.persistent = persistent
97
+ self.verify_ssl = verify_ssl
98
+
99
+ # Set instructions - use default if none provided
100
+ self.instructions = instructions or DEFAULT_INSTRUCTIONS
101
+
102
+ if not self.verify_ssl:
103
+ self._disable_ssl_verification()
72
104
 
73
105
  self.config = DaytonaConfig(
74
106
  api_key=self.api_key,
75
107
  api_url=self.api_url,
76
- target=self.sandbox_target_region,
108
+ target=self.sandbox_target,
77
109
  organization_id=self.organization_id,
78
- ) # type: ignore
110
+ )
111
+
112
+ self.daytona = Daytona(self.config)
113
+
114
+ super().__init__(
115
+ name="daytona_tools",
116
+ tools=[
117
+ self.run_code,
118
+ self.run_shell_command,
119
+ self.create_file,
120
+ self.read_file,
121
+ self.list_files,
122
+ self.delete_file,
123
+ self.change_directory,
124
+ ],
125
+ instructions=self.instructions,
126
+ add_instructions=add_instructions,
127
+ **kwargs,
128
+ )
129
+
130
+ def _disable_ssl_verification(self) -> None:
131
+ try:
132
+ from daytona_api_client import Configuration
133
+
134
+ original_init = Configuration.__init__
135
+
136
+ # Create a wrapper that sets verify_ssl = False
137
+ def patched_init(self, *args, **kwargs):
138
+ original_init(self, *args, **kwargs)
139
+ self.verify_ssl = False
140
+
141
+ setattr(Configuration, "__init__", patched_init)
142
+ import urllib3
79
143
 
144
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
145
+ log_debug(
146
+ "SSL certificate verification is disabled",
147
+ )
148
+ except ImportError:
149
+ log_warning("Could not import daytona_api_client.Configuration for SSL patching")
150
+
151
+ def _get_working_directory(self, agent: Union[Agent, Team]) -> str:
152
+ """Get the current working directory from agent session state."""
153
+ if agent and hasattr(agent, "session_state"):
154
+ if agent.session_state is None:
155
+ agent.session_state = {}
156
+ return agent.session_state.get("working_directory", "/home/daytona")
157
+ return "/home/daytona"
158
+
159
+ def _set_working_directory(self, agent: Union[Agent, Team], directory: str) -> None:
160
+ """Set the working directory in agent session state."""
161
+ if agent and hasattr(agent, "session_state"):
162
+ if agent.session_state is None:
163
+ agent.session_state = {}
164
+ agent.session_state["working_directory"] = directory
165
+ log_info(f"Updated working directory to: {directory}")
166
+
167
+ def _get_or_create_sandbox(self, agent: Union[Agent, Team]) -> Sandbox:
168
+ """Get existing sandbox or create new one"""
169
+ try:
170
+ sandbox = None
171
+
172
+ # Use explicit sandbox
173
+ if self.sandbox_id:
174
+ try:
175
+ sandbox = self.daytona.get(self.sandbox_id)
176
+ log_debug(f"Using explicit sandbox: {self.sandbox_id}")
177
+ except Exception as e:
178
+ log_debug(f"Failed to get sandbox {self.sandbox_id}: {e}")
179
+ sandbox = None
180
+
181
+ # Use persistent sandbox
182
+ elif self.persistent and hasattr(agent, "session_state"):
183
+ if agent.session_state is None:
184
+ agent.session_state = {}
185
+
186
+ sandbox_id = agent.session_state.get("sandbox_id")
187
+ if sandbox_id:
188
+ try:
189
+ sandbox = self.daytona.get(sandbox_id)
190
+ log_debug(f"Using persistent sandbox: {sandbox_id}")
191
+ except Exception as e:
192
+ log_debug(f"Failed to get sandbox {sandbox_id}: {e}")
193
+ sandbox = None
194
+
195
+ # Create new sandbox if none found
196
+ if sandbox is None:
197
+ sandbox = self._create_new_sandbox(agent)
198
+ # Store sandbox ID for persistent sandboxes
199
+ if self.persistent and hasattr(agent, "session_state"):
200
+ if agent.session_state is None:
201
+ agent.session_state = {}
202
+ agent.session_state["sandbox_id"] = sandbox.id
203
+
204
+ # Ensure sandbox is started
205
+ if sandbox.state != "started":
206
+ log_info(f"Starting sandbox {sandbox.id}")
207
+ self.daytona.start(sandbox, timeout=self.timeout)
208
+
209
+ return sandbox
210
+ except Exception as e:
211
+ if self.auto_create_sandbox:
212
+ log_warning(f"Error in sandbox management: {e}. Creating new sandbox.")
213
+ return self._create_new_sandbox(agent)
214
+ else:
215
+ raise e
216
+
217
+ def _create_new_sandbox(self, agent: Optional[Union[Agent, Team]] = None) -> Sandbox:
218
+ """Create a new sandbox with the configured parameters."""
80
219
  try:
81
- params = CreateSandboxParams(
220
+ labels = self.sandbox_labels.copy()
221
+ labels.setdefault("created_by", "agno_daytona_toolkit")
222
+ labels.setdefault("language", str(self.sandbox_language))
223
+
224
+ if self.persistent:
225
+ labels.setdefault("persistent", "true")
226
+
227
+ params = CreateSandboxFromSnapshotParams(
82
228
  language=self.sandbox_language,
83
229
  os_user=self.sandbox_os_user,
84
230
  env_vars=self.sandbox_env_vars,
85
- labels=self.sandbox_labels,
231
+ auto_stop_interval=self.auto_stop_interval,
232
+ labels=labels,
86
233
  public=self.sandbox_public,
87
- auto_stop_interval=self.sandbox_auto_stop_interval,
88
- timeout=self.timeout,
89
234
  )
90
- daytona = Daytona(self.config)
91
- self.sandbox: Sandbox = daytona.create(params)
92
- except Exception as e:
93
- logger.error(f"Error creating Daytona sandbox: {e}")
94
- raise e
235
+ sandbox = self.daytona.create(params, timeout=self.timeout)
95
236
 
96
- # Last execution result for reference
97
- self.last_execution: Optional[ExecuteResponse] = None
237
+ # Add the sandbox_id to the Agent state
238
+ if self.persistent and agent and hasattr(agent, "session_state"):
239
+ if agent.session_state is None:
240
+ agent.session_state = {}
241
+ agent.session_state["sandbox_id"] = sandbox.id
98
242
 
99
- tools: List[Any] = []
243
+ log_info(f"Created new Daytona sandbox: {sandbox.id}")
244
+ return sandbox
245
+ except Exception as e:
246
+ log_error(f"Error creating Daytona sandbox: {e}")
247
+ raise e
100
248
 
101
- if self.sandbox_language == CodeLanguage.PYTHON:
102
- tools.append(self.run_python_code)
103
- else:
104
- tools.append(self.run_code)
249
+ # Tools
250
+ def run_code(self, agent: Union[Agent, Team], code: str) -> str:
251
+ """Execute Python code in the Daytona sandbox.
105
252
 
106
- super().__init__(name="daytona_tools", tools=tools, **kwargs)
253
+ Args:
254
+ code: Code to execute
107
255
 
108
- def run_python_code(self, code: str) -> str:
109
- """Prepare and run Python code in the contextual Daytona sandbox."""
256
+ Returns:
257
+ Execution output as a string
258
+ """
110
259
  try:
111
- executable_code = prepare_python_code(code)
260
+ current_sandbox = self._get_or_create_sandbox(agent)
112
261
 
113
- execution = self.sandbox.process.code_run(executable_code)
262
+ if self.sandbox_language == CodeLanguage.PYTHON:
263
+ code = prepare_python_code(code)
114
264
 
115
- self.last_execution = execution
116
- self.result = execution.result
265
+ response = current_sandbox.process.code_run(code)
266
+
267
+ self.result = response.result
117
268
  return self.result
118
269
  except Exception as e:
119
270
  return json.dumps({"status": "error", "message": f"Error executing code: {str(e)}"})
120
271
 
121
- def run_code(self, code: str) -> str:
122
- """General function to run non-Python code in the contextual Daytona sandbox."""
272
+ def run_shell_command(self, agent: Union[Agent, Team], command: str) -> str:
273
+ """Execute a shell command in the sandbox.
274
+
275
+ Args:
276
+ command: Shell command to execute
277
+
278
+ Returns:
279
+ Command output as a string
280
+ """
123
281
  try:
124
- response = self.sandbox.process.code_run(code)
282
+ current_sandbox = self._get_or_create_sandbox(agent)
125
283
 
126
- self.last_execution = response
127
- self.result = response.result
128
- return self.result
284
+ # Use persistent working directory if not specified
285
+ cwd = self._get_working_directory(agent)
286
+
287
+ # Handle cd commands specially to update working directory
288
+ if command.strip().startswith("cd "):
289
+ new_dir = command.strip()[3:].strip()
290
+ # Convert to Path
291
+ new_path = Path(new_dir)
292
+
293
+ # Resolve relative paths
294
+ if not new_path.is_absolute():
295
+ # Get current absolute path first
296
+ result = current_sandbox.process.exec(f"cd {cwd} && pwd", cwd="/")
297
+ current_abs_path = Path(result.result.strip())
298
+ new_path = current_abs_path / new_path
299
+
300
+ # Normalize the path
301
+ new_path_str = str(new_path.resolve())
302
+
303
+ # Test if directory exists
304
+ test_result = current_sandbox.process.exec(
305
+ f"test -d {new_path_str} && echo 'exists' || echo 'not found'", cwd="/"
306
+ )
307
+ if "exists" in test_result.result:
308
+ self._set_working_directory(agent, new_path_str)
309
+ return f"Changed directory to: {new_path_str}"
310
+ else:
311
+ return f"Error: Directory {new_path_str} not found"
312
+
313
+ # Execute the command
314
+ response = current_sandbox.process.exec(command, cwd=cwd)
315
+ return response.result
129
316
  except Exception as e:
130
- return json.dumps({"status": "error", "message": f"Error executing code: {str(e)}"})
317
+ return json.dumps({"status": "error", "message": f"Error executing command: {str(e)}"})
318
+
319
+ def create_file(self, agent: Union[Agent, Team], file_path: str, content: str) -> str:
320
+ """Create or update a file in the sandbox.
321
+
322
+ Args:
323
+ file_path: Path to the file (relative to current directory or absolute)
324
+ content: Content to write to the file
325
+
326
+ Returns:
327
+ Success message or error
328
+ """
329
+ try:
330
+ current_sandbox = self._get_or_create_sandbox(agent)
331
+
332
+ # Convert to Path object
333
+ path = Path(file_path)
334
+
335
+ # Handle relative paths
336
+ if not path.is_absolute():
337
+ path = Path(self._get_working_directory(agent)) / path
338
+
339
+ # Ensure the path is normalized
340
+ path_str = str(path)
341
+
342
+ # Create directory if needed
343
+ parent_dir = str(path.parent)
344
+ if parent_dir and parent_dir != "/":
345
+ result = current_sandbox.process.exec(f"mkdir -p {parent_dir}")
346
+ if result.exit_code != 0:
347
+ return json.dumps({"status": "error", "message": f"Failed to create directory: {result.result}"})
348
+
349
+ # Write the file using shell command
350
+ # Use cat with heredoc for better handling of special characters
351
+ escaped_content = content.replace("'", "'\"'\"'")
352
+ command = f"cat > '{path_str}' << 'EOF'\n{escaped_content}\nEOF"
353
+ result = current_sandbox.process.exec(command)
354
+
355
+ if result.exit_code != 0:
356
+ return json.dumps({"status": "error", "message": f"Failed to create file: {result.result}"})
357
+
358
+ return f"File created/updated: {path_str}"
359
+ except Exception as e:
360
+ return json.dumps({"status": "error", "message": f"Error creating file: {str(e)}"})
361
+
362
+ def read_file(self, agent: Union[Agent, Team], file_path: str) -> str:
363
+ """Read a file from the sandbox.
364
+
365
+ Args:
366
+ file_path: Path to the file (relative to current directory or absolute)
367
+
368
+ Returns:
369
+ File content or error message
370
+ """
371
+ try:
372
+ current_sandbox = self._get_or_create_sandbox(agent)
373
+
374
+ # Convert to Path object
375
+ path = Path(file_path)
376
+
377
+ # Handle relative paths
378
+ if not path.is_absolute():
379
+ path = Path(self._get_working_directory(agent)) / path
380
+
381
+ path_str = str(path)
382
+
383
+ # Read file using cat
384
+ result = current_sandbox.process.exec(f"cat '{path_str}'")
385
+
386
+ if result.exit_code != 0:
387
+ return json.dumps({"status": "error", "message": f"Error reading file: {result.result}"})
388
+
389
+ return result.result
390
+ except Exception as e:
391
+ return json.dumps({"status": "error", "message": f"Error reading file: {str(e)}"})
392
+
393
+ def list_files(self, agent: Union[Agent, Team], directory: Optional[str] = None) -> str:
394
+ """List files in a directory.
395
+
396
+ Args:
397
+ directory: Directory to list (defaults to current working directory)
398
+
399
+ Returns:
400
+ List of files and directories as formatted string
401
+ """
402
+ try:
403
+ current_sandbox = self._get_or_create_sandbox(agent)
404
+
405
+ # Use current directory if not specified
406
+ if directory is None:
407
+ dir_path = Path(self._get_working_directory(agent))
408
+ else:
409
+ dir_path = Path(directory)
410
+ # Handle relative paths
411
+ if not dir_path.is_absolute():
412
+ dir_path = Path(self._get_working_directory(agent)) / dir_path
413
+
414
+ path_str = str(dir_path)
415
+
416
+ # List files using ls -la for detailed info
417
+ result = current_sandbox.process.exec(f"ls -la '{path_str}'")
418
+
419
+ if result.exit_code != 0:
420
+ return json.dumps({"status": "error", "message": f"Error listing directory: {result.result}"})
421
+
422
+ return f"Contents of {path_str}:\n{result.result}"
423
+ except Exception as e:
424
+ return json.dumps({"status": "error", "message": f"Error listing files: {str(e)}"})
425
+
426
+ def delete_file(self, agent: Union[Agent, Team], file_path: str) -> str:
427
+ """Delete a file or directory from the sandbox.
428
+
429
+ Args:
430
+ file_path: Path to the file or directory (relative to current directory or absolute)
431
+
432
+ Returns:
433
+ Success message or error
434
+ """
435
+ try:
436
+ current_sandbox = self._get_or_create_sandbox(agent)
437
+
438
+ # Convert to Path object
439
+ path = Path(file_path)
440
+
441
+ # Handle relative paths
442
+ if not path.is_absolute():
443
+ path = Path(self._get_working_directory(agent)) / path
444
+
445
+ path_str = str(path)
446
+
447
+ # Check if it's a directory or file
448
+ check_result = current_sandbox.process.exec(f"test -d '{path_str}' && echo 'directory' || echo 'file'")
449
+
450
+ if "directory" in check_result.result:
451
+ # Remove directory recursively
452
+ result = current_sandbox.process.exec(f"rm -rf '{path_str}'")
453
+ else:
454
+ # Remove file
455
+ result = current_sandbox.process.exec(f"rm -f '{path_str}'")
456
+
457
+ if result.exit_code != 0:
458
+ return json.dumps({"status": "error", "message": f"Failed to delete: {result.result}"})
459
+
460
+ return f"Deleted: {path_str}"
461
+ except Exception as e:
462
+ return json.dumps({"status": "error", "message": f"Error deleting file: {str(e)}"})
463
+
464
+ def change_directory(self, agent: Union[Agent, Team], directory: str) -> str:
465
+ """Change the current working directory.
466
+
467
+ Args:
468
+ directory: Directory to change to (relative to current directory or absolute)
469
+
470
+ Returns:
471
+ Success message or error
472
+ """
473
+ try:
474
+ result = self.run_shell_command(agent, f"cd {directory}")
475
+ self._set_working_directory(agent, directory)
476
+ return result
477
+ except Exception as e:
478
+ return json.dumps({"status": "error", "message": f"Error changing directory: {str(e)}"})
agno/tools/evm.py ADDED
@@ -0,0 +1,123 @@
1
+ from os import getenv
2
+ from typing import Optional
3
+
4
+ from agno.tools import Toolkit
5
+ from agno.utils.log import log_debug, log_error
6
+
7
+ try:
8
+ from eth_account.account import LocalAccount
9
+ from eth_account.datastructures import SignedTransaction
10
+ from hexbytes import HexBytes
11
+ from web3 import Web3
12
+ from web3.main import Web3 as Web3Type
13
+ from web3.providers.rpc import HTTPProvider
14
+ from web3.types import TxParams, TxReceipt
15
+ except ImportError:
16
+ raise ImportError("`web3` not installed. Please install using `pip install web3`")
17
+
18
+
19
+ class EvmTools(Toolkit):
20
+ def __init__(
21
+ self,
22
+ private_key: Optional[str] = None,
23
+ rpc_url: Optional[str] = None,
24
+ **kwargs,
25
+ ):
26
+ """Initialize EVM tools for blockchain interactions.
27
+
28
+ Args:
29
+ private_key: Private key for signing transactions (defaults to EVM_PRIVATE_KEY env var)
30
+ rpc_url: RPC URL for blockchain connection (defaults to EVM_RPC_URL env var)
31
+ **kwargs: Additional arguments passed to parent Toolkit class
32
+ """
33
+
34
+ self.private_key = private_key or getenv("EVM_PRIVATE_KEY")
35
+ self.rpc_url = rpc_url or getenv("EVM_RPC_URL")
36
+
37
+ if not self.private_key:
38
+ log_error("Private Key is required")
39
+ raise ValueError("Private Key is required")
40
+ if not self.rpc_url:
41
+ log_error("RPC Url is needed to interact with EVM blockchain")
42
+ raise ValueError("RPC Url is needed to interact with EVM blockchain")
43
+
44
+ # Ensure private key has 0x prefix
45
+ if not self.private_key.startswith("0x"):
46
+ self.private_key = f"0x{self.private_key}"
47
+
48
+ # Initialize Web3 client and account
49
+ self.web3_client: "Web3Type" = Web3(HTTPProvider(self.rpc_url))
50
+ self.account: "LocalAccount" = self.web3_client.eth.account.from_key(self.private_key)
51
+ log_debug(f"Your wallet address is: {self.account.address}")
52
+
53
+ super().__init__(name="evm_tools", tools=[self.send_transaction], **kwargs)
54
+
55
+ def get_max_priority_fee_per_gas(self) -> int:
56
+ """Get the max priority fee per gas for the transaction.
57
+
58
+ Returns:
59
+ int: The max priority fee per gas for the transaction (1 gwei)
60
+ """
61
+ max_priority_fee_per_gas = self.web3_client.to_wei(1, "gwei")
62
+ return max_priority_fee_per_gas
63
+
64
+ def get_max_fee_per_gas(self, max_priority_fee_per_gas: int) -> int:
65
+ """Get the max fee per gas for the transaction.
66
+
67
+ Args:
68
+ max_priority_fee_per_gas: The max priority fee per gas
69
+
70
+ Returns:
71
+ int: The max fee per gas for the transaction
72
+ """
73
+ latest_block = self.web3_client.eth.get_block("latest")
74
+ base_fee_per_gas = latest_block.get("baseFeePerGas")
75
+ if base_fee_per_gas is None:
76
+ log_error("Base fee per gas not found in the latest block.")
77
+ raise ValueError("Base fee per gas not found in the latest block.")
78
+ max_fee_per_gas = (2 * base_fee_per_gas) + max_priority_fee_per_gas
79
+ return max_fee_per_gas
80
+
81
+ def send_transaction(self, to_address: str, amount_in_wei: int) -> str:
82
+ """Sends a transaction to the address provided.
83
+
84
+ Args:
85
+ to_address: The address to which you want to send ETH
86
+ amount_in_wei: The amount of ETH to send in wei
87
+
88
+ Returns:
89
+ str: The transaction hash of the transaction or error message
90
+ """
91
+ try:
92
+ max_priority_fee_per_gas = self.get_max_priority_fee_per_gas()
93
+ max_fee_per_gas = self.get_max_fee_per_gas(max_priority_fee_per_gas)
94
+
95
+ transaction_params: "TxParams" = {
96
+ "from": self.account.address,
97
+ "to": to_address,
98
+ "value": amount_in_wei, # type: ignore[typeddict-item]
99
+ "nonce": self.web3_client.eth.get_transaction_count(self.account.address),
100
+ "gas": 21000,
101
+ "maxFeePerGas": max_fee_per_gas, # type: ignore[typeddict-item]
102
+ "maxPriorityFeePerGas": max_priority_fee_per_gas, # type: ignore[typeddict-item]
103
+ "chainId": self.web3_client.eth.chain_id,
104
+ "type": 2, # EIP-1559 dynamic fee transaction
105
+ }
106
+
107
+ signed_transaction: "SignedTransaction" = self.web3_client.eth.account.sign_transaction(
108
+ transaction_params, self.private_key
109
+ )
110
+ transaction_hash: "HexBytes" = self.web3_client.eth.send_raw_transaction(signed_transaction.raw_transaction)
111
+ log_debug(f"Ongoing Transaction hash: 0x{transaction_hash.hex()}")
112
+
113
+ transaction_receipt: "TxReceipt" = self.web3_client.eth.wait_for_transaction_receipt(transaction_hash)
114
+ if transaction_receipt.get("status") == 1:
115
+ log_debug(f"Transaction successful! Transaction hash: 0x{transaction_hash.hex()}")
116
+ return f"0x{transaction_hash.hex()}"
117
+ else:
118
+ log_error("Transaction failed!")
119
+ raise Exception("Transaction failed!")
120
+
121
+ except Exception as e:
122
+ log_error(f"Error sending transaction: {e}")
123
+ return f"error: {e}"