tinyagent-py 0.0.13__tar.gz → 0.0.16__tar.gz
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.
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/PKG-INFO +25 -1
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/README.md +24 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/pyproject.toml +1 -1
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/helper.py +2 -2
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/modal_sandbox.py +1 -1
- tinyagent_py-0.0.16/tinyagent/code_agent/providers/__init__.py +17 -0
- tinyagent_py-0.0.16/tinyagent/code_agent/providers/base.py +381 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/providers/modal_provider.py +150 -27
- tinyagent_py-0.0.16/tinyagent/code_agent/providers/seatbelt_provider.py +1065 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/safety.py +6 -2
- tinyagent_py-0.0.16/tinyagent/code_agent/tiny_code_agent.py +1639 -0
- tinyagent_py-0.0.16/tinyagent/code_agent/utils.py +454 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/hooks/__init__.py +3 -1
- tinyagent_py-0.0.16/tinyagent/hooks/jupyter_notebook_callback.py +1464 -0
- tinyagent_py-0.0.16/tinyagent/hooks/token_tracker.py +564 -0
- tinyagent_py-0.0.16/tinyagent/prompts/summarize.yaml +96 -0
- tinyagent_py-0.0.16/tinyagent/prompts/truncation.yaml +13 -0
- tinyagent_py-0.0.16/tinyagent/tiny_agent.py +1763 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent_py.egg-info/PKG-INFO +25 -1
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent_py.egg-info/SOURCES.txt +5 -0
- tinyagent_py-0.0.13/tinyagent/code_agent/providers/__init__.py +0 -4
- tinyagent_py-0.0.13/tinyagent/code_agent/providers/base.py +0 -207
- tinyagent_py-0.0.13/tinyagent/code_agent/tiny_code_agent.py +0 -678
- tinyagent_py-0.0.13/tinyagent/code_agent/utils.py +0 -193
- tinyagent_py-0.0.13/tinyagent/tiny_agent.py +0 -1001
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/LICENSE +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/setup.cfg +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/__init__.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/__init__.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/example.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/tools/__init__.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/code_agent/tools/example_tools.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/hooks/gradio_callback.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/hooks/logging_manager.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/hooks/rich_code_ui_callback.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/hooks/rich_ui_callback.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/mcp_client.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/memory_manager.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/prompts/code_agent.yaml +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/storage/__init__.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/storage/base.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/storage/json_file_storage.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/storage/postgres_storage.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/storage/redis_storage.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent/storage/sqlite_storage.py +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent_py.egg-info/dependency_links.txt +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent_py.egg-info/requires.txt +0 -0
- {tinyagent_py-0.0.13 → tinyagent_py-0.0.16}/tinyagent_py.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: tinyagent-py
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.16
|
4
4
|
Summary: TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful
|
5
5
|
Author-email: Mahdi Golchin <golchin@askdev.ai>
|
6
6
|
Project-URL: Homepage, https://github.com/askbudi/tinyagent
|
@@ -274,6 +274,30 @@ agent = TinyCodeAgent(
|
|
274
274
|
)
|
275
275
|
```
|
276
276
|
|
277
|
+
### Automatic Git Checkpoints
|
278
|
+
|
279
|
+
TinyCodeAgent can automatically create Git checkpoints after each successful shell command execution. This helps track changes made by the agent and provides a safety net for reverting changes if needed.
|
280
|
+
|
281
|
+
```python
|
282
|
+
# Enable automatic Git checkpoints during initialization
|
283
|
+
agent = TinyCodeAgent(
|
284
|
+
model="gpt-4.1-mini",
|
285
|
+
auto_git_checkpoint=True # Enable automatic Git checkpoints
|
286
|
+
)
|
287
|
+
|
288
|
+
# Or enable/disable it later
|
289
|
+
agent.enable_auto_git_checkpoint(True) # Enable
|
290
|
+
agent.enable_auto_git_checkpoint(False) # Disable
|
291
|
+
|
292
|
+
# Check current status
|
293
|
+
is_enabled = agent.get_auto_git_checkpoint_status()
|
294
|
+
```
|
295
|
+
|
296
|
+
Each checkpoint includes:
|
297
|
+
- Descriptive commit message with the command description
|
298
|
+
- Timestamp of when the command was executed
|
299
|
+
- The actual command that was run
|
300
|
+
|
277
301
|
For detailed documentation, see the [TinyCodeAgent README](tinyagent/code_agent/README.md).
|
278
302
|
|
279
303
|
## How the TinyAgent Hook System Works
|
@@ -232,6 +232,30 @@ agent = TinyCodeAgent(
|
|
232
232
|
)
|
233
233
|
```
|
234
234
|
|
235
|
+
### Automatic Git Checkpoints
|
236
|
+
|
237
|
+
TinyCodeAgent can automatically create Git checkpoints after each successful shell command execution. This helps track changes made by the agent and provides a safety net for reverting changes if needed.
|
238
|
+
|
239
|
+
```python
|
240
|
+
# Enable automatic Git checkpoints during initialization
|
241
|
+
agent = TinyCodeAgent(
|
242
|
+
model="gpt-4.1-mini",
|
243
|
+
auto_git_checkpoint=True # Enable automatic Git checkpoints
|
244
|
+
)
|
245
|
+
|
246
|
+
# Or enable/disable it later
|
247
|
+
agent.enable_auto_git_checkpoint(True) # Enable
|
248
|
+
agent.enable_auto_git_checkpoint(False) # Disable
|
249
|
+
|
250
|
+
# Check current status
|
251
|
+
is_enabled = agent.get_auto_git_checkpoint_status()
|
252
|
+
```
|
253
|
+
|
254
|
+
Each checkpoint includes:
|
255
|
+
- Descriptive commit message with the command description
|
256
|
+
- Timestamp of when the command was executed
|
257
|
+
- The actual command that was run
|
258
|
+
|
235
259
|
For detailed documentation, see the [TinyCodeAgent README](tinyagent/code_agent/README.md).
|
236
260
|
|
237
261
|
## How the TinyAgent Hook System Works
|
@@ -12,7 +12,7 @@ tinyagent = ["prompts/*.yaml"]
|
|
12
12
|
|
13
13
|
[project]
|
14
14
|
name = "tinyagent-py"
|
15
|
-
version = "0.0.
|
15
|
+
version = "0.0.16"
|
16
16
|
description = "TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful"
|
17
17
|
readme = "README.md"
|
18
18
|
authors = [
|
@@ -47,13 +47,13 @@ You are an Agent, You need to solve the task, not suggesting user about how to s
|
|
47
47
|
|
48
48
|
""")
|
49
49
|
|
50
|
-
def load_template(path: str) -> str:
|
50
|
+
def load_template(path: str,key:str="system_prompt") -> str:
|
51
51
|
"""
|
52
52
|
Load the YAML file and extract its 'system_prompt' field.
|
53
53
|
"""
|
54
54
|
with open(path, "r") as f:
|
55
55
|
data = yaml.safe_load(f)
|
56
|
-
return data[
|
56
|
+
return data[key]
|
57
57
|
|
58
58
|
def render_system_prompt(template_str: str,
|
59
59
|
tools: dict,
|
@@ -78,7 +78,7 @@ def create_sandbox(
|
|
78
78
|
|
79
79
|
if apt_packages is None:
|
80
80
|
# Always install the basics required for most workflows
|
81
|
-
apt_packages = ("git", "curl", "nodejs", "npm")
|
81
|
+
apt_packages = ("git", "curl", "nodejs", "npm","ripgrep","tree")
|
82
82
|
|
83
83
|
if default_packages is None:
|
84
84
|
default_packages = (
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from .base import CodeExecutionProvider
|
2
|
+
from .modal_provider import ModalProvider
|
3
|
+
|
4
|
+
# Import SeatbeltProvider conditionally to avoid errors on non-macOS systems
|
5
|
+
import platform
|
6
|
+
if platform.system() == "Darwin":
|
7
|
+
try:
|
8
|
+
from .seatbelt_provider import SeatbeltProvider
|
9
|
+
except ImportError:
|
10
|
+
# If there's an issue importing, just don't make it available
|
11
|
+
pass
|
12
|
+
|
13
|
+
__all__ = ["CodeExecutionProvider", "ModalProvider"]
|
14
|
+
|
15
|
+
# Add SeatbeltProvider to __all__ if it was successfully imported
|
16
|
+
if platform.system() == "Darwin" and "SeatbeltProvider" in globals():
|
17
|
+
__all__.append("SeatbeltProvider")
|
@@ -0,0 +1,381 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Dict, List, Any, Optional, Set
|
3
|
+
from tinyagent.hooks.logging_manager import LoggingManager
|
4
|
+
import cloudpickle
|
5
|
+
|
6
|
+
|
7
|
+
class CodeExecutionProvider(ABC):
|
8
|
+
"""
|
9
|
+
Abstract base class for code execution providers.
|
10
|
+
|
11
|
+
This class defines the interface that all code execution providers must implement.
|
12
|
+
It allows for easy extension to support different execution environments
|
13
|
+
(Modal, Docker, local execution, cloud functions, etc.) with minimal code changes.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
log_manager: LoggingManager,
|
19
|
+
default_python_codes: Optional[List[str]] = None,
|
20
|
+
code_tools: List[Dict[str, Any]] = None,
|
21
|
+
pip_packages: List[str] = None,
|
22
|
+
secrets: Dict[str, Any] = None,
|
23
|
+
lazy_init: bool = True,
|
24
|
+
bypass_shell_safety: bool = False,
|
25
|
+
additional_safe_shell_commands: Optional[List[str]] = None,
|
26
|
+
additional_safe_control_operators: Optional[List[str]] = None,
|
27
|
+
**kwargs
|
28
|
+
):
|
29
|
+
self.log_manager = log_manager
|
30
|
+
self.default_python_codes = default_python_codes or []
|
31
|
+
self.code_tools = code_tools or []
|
32
|
+
self.pip_packages = pip_packages or []
|
33
|
+
self.secrets = secrets or {}
|
34
|
+
self.lazy_init = lazy_init
|
35
|
+
self.kwargs = kwargs
|
36
|
+
self.executed_default_codes = False
|
37
|
+
self._globals_dict = kwargs.get("globals_dict", {})
|
38
|
+
self._locals_dict = kwargs.get("locals_dict", {})
|
39
|
+
self._user_variables = {}
|
40
|
+
self.code_tools_definitions = []
|
41
|
+
|
42
|
+
# Shell safety configuration
|
43
|
+
self.bypass_shell_safety = bypass_shell_safety
|
44
|
+
|
45
|
+
# Safe shell commands that don't modify the system or access sensitive data
|
46
|
+
self.safe_shell_commands: Set[str] = {
|
47
|
+
"ls", "cat", "grep", "find", "echo", "pwd", "whoami", "date",
|
48
|
+
"head", "tail", "wc", "sort", "uniq", "tr", "cut", "sed", "awk",
|
49
|
+
"ps", "df", "du", "uname", "which", "type", "file", "stat", "rg", "if",
|
50
|
+
"tree"
|
51
|
+
}
|
52
|
+
|
53
|
+
# Add additional safe shell commands if provided
|
54
|
+
if additional_safe_shell_commands:
|
55
|
+
if "*" in additional_safe_shell_commands:
|
56
|
+
# If wildcard is provided, allow all commands (effectively bypassing the check)
|
57
|
+
self.bypass_shell_safety = True
|
58
|
+
else:
|
59
|
+
self.safe_shell_commands.update(additional_safe_shell_commands)
|
60
|
+
|
61
|
+
# Safe control operators for shell commands
|
62
|
+
self.safe_control_operators: Set[str] = {"&&", "||", ";", "|"}
|
63
|
+
|
64
|
+
# Add additional safe control operators if provided
|
65
|
+
if additional_safe_control_operators:
|
66
|
+
if "*" in additional_safe_control_operators:
|
67
|
+
# If wildcard is provided, allow all operators
|
68
|
+
self.safe_control_operators = set("*")
|
69
|
+
else:
|
70
|
+
self.safe_control_operators.update(additional_safe_control_operators)
|
71
|
+
|
72
|
+
@abstractmethod
|
73
|
+
async def execute_python(
|
74
|
+
self,
|
75
|
+
code_lines: List[str],
|
76
|
+
timeout: int = 120
|
77
|
+
) -> Dict[str, Any]:
|
78
|
+
"""
|
79
|
+
Execute Python code and return the result.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
code_lines: List of Python code lines to execute
|
83
|
+
timeout: Maximum execution time in seconds
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
Dictionary containing execution results with keys:
|
87
|
+
- printed_output: stdout from the execution
|
88
|
+
- return_value: the return value if any
|
89
|
+
- stderr: stderr from the execution
|
90
|
+
- error_traceback: exception traceback if any error occurred
|
91
|
+
"""
|
92
|
+
pass
|
93
|
+
|
94
|
+
@abstractmethod
|
95
|
+
async def execute_shell(
|
96
|
+
self,
|
97
|
+
command: List[str],
|
98
|
+
timeout: int = 10,
|
99
|
+
workdir: Optional[str] = None
|
100
|
+
) -> Dict[str, Any]:
|
101
|
+
"""
|
102
|
+
Execute a shell command securely and return the result.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
command: List of command parts to execute
|
106
|
+
timeout: Maximum execution time in seconds
|
107
|
+
workdir: Working directory for command execution
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Dictionary containing execution results with keys:
|
111
|
+
- stdout: stdout from the execution
|
112
|
+
- stderr: stderr from the execution
|
113
|
+
- exit_code: exit code from the command
|
114
|
+
"""
|
115
|
+
pass
|
116
|
+
|
117
|
+
def is_safe_command(self, command: List[str]) -> Dict[str, Any]:
|
118
|
+
"""
|
119
|
+
Check if a shell command is safe to execute.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
command: List of command parts to check
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
Dictionary with:
|
126
|
+
- safe: Boolean indicating if command is safe
|
127
|
+
- reason: Reason why command is not safe (if applicable)
|
128
|
+
"""
|
129
|
+
# If shell safety checks are bypassed, consider all commands safe
|
130
|
+
if self.bypass_shell_safety:
|
131
|
+
return {"safe": True}
|
132
|
+
|
133
|
+
if type(command) == str:
|
134
|
+
command = command.split(" ")
|
135
|
+
if not command or not isinstance(command, list) or len(command) == 0:
|
136
|
+
return {"safe": False, "reason": "Empty or invalid command"}
|
137
|
+
|
138
|
+
# Special handling for bash -c or bash -lc commands
|
139
|
+
if len(command) >= 3 and command[0] == "bash" and command[1] in ["-c", "-lc"]:
|
140
|
+
# For bash -c or bash -lc, we need to parse the command string that follows
|
141
|
+
# We'll extract commands from the bash command string and check them
|
142
|
+
bash_cmd_str = command[2]
|
143
|
+
|
144
|
+
# Simple parsing of the bash command to extract command names
|
145
|
+
# This is a basic implementation and might not cover all edge cases
|
146
|
+
import shlex
|
147
|
+
import re
|
148
|
+
|
149
|
+
try:
|
150
|
+
# Shell script keywords that should be allowed
|
151
|
+
shell_keywords = {
|
152
|
+
"if", "then", "else", "elif", "fi", "for", "do", "done",
|
153
|
+
"while", "until", "case", "esac", "in", "function", "select",
|
154
|
+
"time", "coproc", "true", "false"
|
155
|
+
}
|
156
|
+
|
157
|
+
# Split the command by common shell operators
|
158
|
+
cmd_parts = re.split(r'(\||;|&&|\|\||>|>>|<|<<)', bash_cmd_str)
|
159
|
+
commands_to_check = []
|
160
|
+
|
161
|
+
for part in cmd_parts:
|
162
|
+
part = part.strip()
|
163
|
+
if part and part not in ['|', ';', '&&', '||', '>', '>>', '<', '<<']:
|
164
|
+
# Get the first word which is typically the command
|
165
|
+
try:
|
166
|
+
words = shlex.split(part)
|
167
|
+
if words:
|
168
|
+
cmd_name = words[0].split('/')[-1] # Extract binary name
|
169
|
+
|
170
|
+
# Skip shell keywords
|
171
|
+
if cmd_name in shell_keywords:
|
172
|
+
continue
|
173
|
+
|
174
|
+
# Skip variable assignments (e.g., VAR=value)
|
175
|
+
if re.match(r'^[A-Za-z_][A-Za-z0-9_]*=', cmd_name):
|
176
|
+
continue
|
177
|
+
|
178
|
+
if cmd_name not in self.safe_shell_commands and '*' not in cmd_name and '?' not in cmd_name:
|
179
|
+
return {"safe": False, "reason": f"Unsafe command in bash script: {cmd_name}"}
|
180
|
+
except Exception:
|
181
|
+
# If parsing fails, be cautious and reject
|
182
|
+
return {"safe": False, "reason": "Could not parse bash command safely"}
|
183
|
+
|
184
|
+
# All commands in the bash script are safe
|
185
|
+
return {"safe": True}
|
186
|
+
except Exception as e:
|
187
|
+
return {"safe": False, "reason": f"Error parsing bash command: {str(e)}"}
|
188
|
+
|
189
|
+
# Normal command processing for non-bash -c commands
|
190
|
+
# Shell operators that might be passed as separate arguments
|
191
|
+
shell_operators = ['|', '>', '<', '>>', '<<', '&&', '||', ';']
|
192
|
+
|
193
|
+
# Extract actual commands from the command list, ignoring shell operators
|
194
|
+
commands_to_check = []
|
195
|
+
i = 0
|
196
|
+
while i < len(command):
|
197
|
+
if command[i] in shell_operators:
|
198
|
+
i += 1
|
199
|
+
continue
|
200
|
+
|
201
|
+
# Extract the binary name
|
202
|
+
bin_name = command[i].split("/")[-1]
|
203
|
+
commands_to_check.append(bin_name)
|
204
|
+
|
205
|
+
# Skip to next command after an operator
|
206
|
+
i += 1
|
207
|
+
while i < len(command) and command[i] not in shell_operators:
|
208
|
+
i += 1
|
209
|
+
|
210
|
+
# Check if all commands are in the safe list
|
211
|
+
for cmd in commands_to_check:
|
212
|
+
# Handle wildcards in command names (e.g., *.py)
|
213
|
+
if '*' in cmd or '?' in cmd:
|
214
|
+
continue
|
215
|
+
|
216
|
+
if cmd not in self.safe_shell_commands:
|
217
|
+
return {"safe": False, "reason": f"Unsafe command: {cmd}"}
|
218
|
+
|
219
|
+
return {"safe": True}
|
220
|
+
|
221
|
+
@abstractmethod
|
222
|
+
async def cleanup(self):
|
223
|
+
"""Clean up any resources used by the provider."""
|
224
|
+
pass
|
225
|
+
|
226
|
+
def add_tools(self, tools: List[Any]) -> None:
|
227
|
+
"""
|
228
|
+
Add tools to the execution environment.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
tools: List of tool objects to add
|
232
|
+
"""
|
233
|
+
tools_str_list = ["import cloudpickle"]
|
234
|
+
tools_str_list.append("###########<tools>###########\n")
|
235
|
+
for tool in tools:
|
236
|
+
tools_str_list.append(
|
237
|
+
f"globals()['{tool._tool_metadata['name']}'] = cloudpickle.loads({cloudpickle.dumps(tool)})"
|
238
|
+
)
|
239
|
+
tools_str_list.append("\n\n")
|
240
|
+
tools_str_list.append("###########</tools>###########\n")
|
241
|
+
tools_str_list.append("\n\n")
|
242
|
+
self.code_tools_definitions.extend(tools_str_list)
|
243
|
+
|
244
|
+
def set_code_tools(self, tools: List[Any]) -> None:
|
245
|
+
"""
|
246
|
+
Set the code tools available in the execution environment.
|
247
|
+
Replaces any existing tools with the new list.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
tools: List of tool objects to set
|
251
|
+
"""
|
252
|
+
# Clear existing tools
|
253
|
+
self.code_tools = tools.copy()
|
254
|
+
self.code_tools_definitions = []
|
255
|
+
|
256
|
+
# Add the new tools
|
257
|
+
if tools:
|
258
|
+
self.add_tools(tools)
|
259
|
+
|
260
|
+
def set_user_variables(self, variables: Dict[str, Any]) -> None:
|
261
|
+
"""
|
262
|
+
Set user variables that will be available in the Python environment.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
variables: Dictionary of variable name -> value pairs
|
266
|
+
"""
|
267
|
+
self._user_variables = variables.copy()
|
268
|
+
|
269
|
+
# Add variables to the execution environment by serializing them
|
270
|
+
# This ensures they are available when code is executed
|
271
|
+
variables_str_list = ["import cloudpickle"]
|
272
|
+
variables_str_list.append("###########<user_variables>###########\n")
|
273
|
+
|
274
|
+
for var_name, var_value in variables.items():
|
275
|
+
# Serialize the variable and add it to globals
|
276
|
+
serialized_var = cloudpickle.dumps(var_value)
|
277
|
+
variables_str_list.append(
|
278
|
+
f"globals()['{var_name}'] = cloudpickle.loads({serialized_var})"
|
279
|
+
)
|
280
|
+
|
281
|
+
variables_str_list.append("\n###########</user_variables>###########\n")
|
282
|
+
variables_str_list.append("\n")
|
283
|
+
|
284
|
+
# Remove any existing user variables from default codes
|
285
|
+
self._remove_existing_user_variables()
|
286
|
+
|
287
|
+
# Add new variables to default codes at the beginning (after tools if any)
|
288
|
+
# This ensures variables are available from the start
|
289
|
+
if variables_str_list:
|
290
|
+
# Find where to insert (after tools section if it exists)
|
291
|
+
insert_index = 0
|
292
|
+
for i, code in enumerate(self.code_tools_definitions):
|
293
|
+
if "###########</tools>###########" in code:
|
294
|
+
insert_index = i + 1
|
295
|
+
break
|
296
|
+
|
297
|
+
# Insert the variables code
|
298
|
+
for j, var_code in enumerate(variables_str_list):
|
299
|
+
self.code_tools_definitions.insert(insert_index + j, var_code)
|
300
|
+
|
301
|
+
def _remove_existing_user_variables(self) -> None:
|
302
|
+
"""Remove existing user variables from default python codes."""
|
303
|
+
# Find and remove the user variables section
|
304
|
+
start_index = None
|
305
|
+
end_index = None
|
306
|
+
|
307
|
+
for i, code in enumerate(self.code_tools_definitions):
|
308
|
+
if "###########<user_variables>###########" in code:
|
309
|
+
start_index = i - 1 if i > 0 and "import cloudpickle" in self.code_tools_definitions[i-1] else i
|
310
|
+
elif "###########</user_variables>###########" in code:
|
311
|
+
end_index = i + 2 # Include the newline after
|
312
|
+
break
|
313
|
+
|
314
|
+
if start_index is not None and end_index is not None:
|
315
|
+
# Remove the old variables section
|
316
|
+
del self.code_tools_definitions[start_index:end_index]
|
317
|
+
|
318
|
+
def get_user_variables(self) -> Dict[str, Any]:
|
319
|
+
"""
|
320
|
+
Get a copy of current user variables.
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
Dictionary of current user variables
|
324
|
+
"""
|
325
|
+
return self._user_variables.copy()
|
326
|
+
|
327
|
+
def update_user_variables_from_globals(self, globals_dict: Dict[str, Any]) -> None:
|
328
|
+
"""
|
329
|
+
Extract and update user variables from the globals dictionary after code execution.
|
330
|
+
This ensures that any modifications to user variables during code execution are preserved.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
globals_dict: The globals dictionary after code execution
|
334
|
+
"""
|
335
|
+
if not globals_dict or not self._user_variables:
|
336
|
+
return
|
337
|
+
|
338
|
+
# Update user variables with values from globals
|
339
|
+
for var_name in list(self._user_variables.keys()):
|
340
|
+
if var_name in globals_dict:
|
341
|
+
try:
|
342
|
+
# Try to serialize the value to ensure it's valid
|
343
|
+
cloudpickle.dumps(globals_dict[var_name])
|
344
|
+
# Update the user variable with the new value
|
345
|
+
self._user_variables[var_name] = globals_dict[var_name]
|
346
|
+
except Exception:
|
347
|
+
# If serialization fails, keep the old value
|
348
|
+
pass
|
349
|
+
|
350
|
+
# Check for new variables that might have been created
|
351
|
+
# This handles cases where LLM creates new variables that should be preserved
|
352
|
+
for var_name, var_value in globals_dict.items():
|
353
|
+
# Skip special variables, modules, and functions
|
354
|
+
if (var_name.startswith('__') or
|
355
|
+
var_name in ['builtins', 'cloudpickle'] or
|
356
|
+
callable(var_value) or
|
357
|
+
var_name in self._user_variables):
|
358
|
+
continue
|
359
|
+
|
360
|
+
try:
|
361
|
+
# Try to serialize the value to ensure it's valid
|
362
|
+
cloudpickle.dumps(var_value)
|
363
|
+
# Add the new variable to user variables
|
364
|
+
self._user_variables[var_name] = var_value
|
365
|
+
except Exception:
|
366
|
+
# If serialization fails, skip this variable
|
367
|
+
pass
|
368
|
+
|
369
|
+
def shell_response_to_llm_understandable(self, response: Dict[str, Any]) -> str:
|
370
|
+
"""
|
371
|
+
Convert a shell command response to a format that is understandable by the LLM.
|
372
|
+
"""
|
373
|
+
if response.get('stderr',None) not in [None,""]:
|
374
|
+
error_message = "Bash Error: " + response['stderr']
|
375
|
+
if "No such file or directory" in response['stderr']:
|
376
|
+
error_message.replace("No such file or directory", "No such file or directory, Have you provided the correct absolute path? If you are unsure use ls first to make sure the path exists")
|
377
|
+
if "Command timed out after" in response['stderr']:
|
378
|
+
error_message += ", Make sure your command is specific enough. And only if it is the most specific and optimized command then try to increase the timeout parameter if you need to more time for this command."
|
379
|
+
return error_message
|
380
|
+
else:
|
381
|
+
return response['stdout']
|