tinyagent-py 0.0.11__py3-none-any.whl → 0.0.13__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.
- tinyagent/code_agent/modal_sandbox.py +3 -1
- tinyagent/code_agent/providers/base.py +60 -5
- tinyagent/code_agent/providers/modal_provider.py +61 -18
- tinyagent/code_agent/safety.py +546 -0
- tinyagent/code_agent/tiny_code_agent.py +105 -0
- tinyagent/code_agent/utils.py +90 -17
- tinyagent/hooks/gradio_callback.py +100 -35
- tinyagent/tiny_agent.py +4 -7
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/METADATA +11 -1
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/RECORD +13 -12
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/top_level.txt +0 -0
@@ -63,6 +63,7 @@ def create_sandbox(
|
|
63
63
|
pip_install: Sequence[str] | None = None,
|
64
64
|
image_name: str = "tinyagent-sandbox-image",
|
65
65
|
app_name: str = "persistent-code-session",
|
66
|
+
force_build: bool = False,
|
66
67
|
**sandbox_kwargs,
|
67
68
|
) -> Tuple[modal.Sandbox, modal.App]:
|
68
69
|
"""Create (or lookup) a `modal.Sandbox` pre-configured for code execution.
|
@@ -99,7 +100,7 @@ def create_sandbox(
|
|
99
100
|
|
100
101
|
# Build image -----------------------------------------------------------
|
101
102
|
agent_image = (
|
102
|
-
modal.Image.debian_slim(python_version=python_version)
|
103
|
+
modal.Image.debian_slim(python_version=python_version,force_build=force_build)
|
103
104
|
.apt_install(*apt_packages)
|
104
105
|
.pip_install(*full_pip_list)
|
105
106
|
)
|
@@ -196,6 +197,7 @@ class SandboxSession:
|
|
196
197
|
modal_secrets: modal.Secret,
|
197
198
|
*,
|
198
199
|
timeout: int = 5 * 60,
|
200
|
+
|
199
201
|
**create_kwargs,
|
200
202
|
) -> None:
|
201
203
|
self.modal_secrets = modal_secrets
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
2
|
from typing import Dict, List, Any, Optional
|
3
3
|
from tinyagent.hooks.logging_manager import LoggingManager
|
4
|
+
import cloudpickle
|
4
5
|
|
5
6
|
|
6
7
|
class CodeExecutionProvider(ABC):
|
@@ -69,8 +70,6 @@ class CodeExecutionProvider(ABC):
|
|
69
70
|
Args:
|
70
71
|
tools: List of tool objects to add
|
71
72
|
"""
|
72
|
-
import cloudpickle
|
73
|
-
|
74
73
|
tools_str_list = ["import cloudpickle"]
|
75
74
|
tools_str_list.append("###########<tools>###########\n")
|
76
75
|
for tool in tools:
|
@@ -82,6 +81,22 @@ class CodeExecutionProvider(ABC):
|
|
82
81
|
tools_str_list.append("\n\n")
|
83
82
|
self.code_tools_definitions.extend(tools_str_list)
|
84
83
|
|
84
|
+
def set_code_tools(self, tools: List[Any]) -> None:
|
85
|
+
"""
|
86
|
+
Set the code tools available in the execution environment.
|
87
|
+
Replaces any existing tools with the new list.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
tools: List of tool objects to set
|
91
|
+
"""
|
92
|
+
# Clear existing tools
|
93
|
+
self.code_tools = tools.copy()
|
94
|
+
self.code_tools_definitions = []
|
95
|
+
|
96
|
+
# Add the new tools
|
97
|
+
if tools:
|
98
|
+
self.add_tools(tools)
|
99
|
+
|
85
100
|
def set_user_variables(self, variables: Dict[str, Any]) -> None:
|
86
101
|
"""
|
87
102
|
Set user variables that will be available in the Python environment.
|
@@ -89,8 +104,6 @@ class CodeExecutionProvider(ABC):
|
|
89
104
|
Args:
|
90
105
|
variables: Dictionary of variable name -> value pairs
|
91
106
|
"""
|
92
|
-
import cloudpickle
|
93
|
-
|
94
107
|
self._user_variables = variables.copy()
|
95
108
|
|
96
109
|
# Add variables to the execution environment by serializing them
|
@@ -149,4 +162,46 @@ class CodeExecutionProvider(ABC):
|
|
149
162
|
Returns:
|
150
163
|
Dictionary of current user variables
|
151
164
|
"""
|
152
|
-
return self._user_variables.copy()
|
165
|
+
return self._user_variables.copy()
|
166
|
+
|
167
|
+
def update_user_variables_from_globals(self, globals_dict: Dict[str, Any]) -> None:
|
168
|
+
"""
|
169
|
+
Extract and update user variables from the globals dictionary after code execution.
|
170
|
+
This ensures that any modifications to user variables during code execution are preserved.
|
171
|
+
|
172
|
+
Args:
|
173
|
+
globals_dict: The globals dictionary after code execution
|
174
|
+
"""
|
175
|
+
if not globals_dict or not self._user_variables:
|
176
|
+
return
|
177
|
+
|
178
|
+
# Update user variables with values from globals
|
179
|
+
for var_name in list(self._user_variables.keys()):
|
180
|
+
if var_name in globals_dict:
|
181
|
+
try:
|
182
|
+
# Try to serialize the value to ensure it's valid
|
183
|
+
cloudpickle.dumps(globals_dict[var_name])
|
184
|
+
# Update the user variable with the new value
|
185
|
+
self._user_variables[var_name] = globals_dict[var_name]
|
186
|
+
except Exception:
|
187
|
+
# If serialization fails, keep the old value
|
188
|
+
pass
|
189
|
+
|
190
|
+
# Check for new variables that might have been created
|
191
|
+
# This handles cases where LLM creates new variables that should be preserved
|
192
|
+
for var_name, var_value in globals_dict.items():
|
193
|
+
# Skip special variables, modules, and functions
|
194
|
+
if (var_name.startswith('__') or
|
195
|
+
var_name in ['builtins', 'cloudpickle'] or
|
196
|
+
callable(var_value) or
|
197
|
+
var_name in self._user_variables):
|
198
|
+
continue
|
199
|
+
|
200
|
+
try:
|
201
|
+
# Try to serialize the value to ensure it's valid
|
202
|
+
cloudpickle.dumps(var_value)
|
203
|
+
# Add the new variable to user variables
|
204
|
+
self._user_variables[var_name] = var_value
|
205
|
+
except Exception:
|
206
|
+
# If serialization fails, skip this variable
|
207
|
+
pass
|
@@ -26,6 +26,7 @@ class ModalProvider(CodeExecutionProvider):
|
|
26
26
|
default_packages: Optional[List[str]] = None,
|
27
27
|
apt_packages: Optional[List[str]] = None,
|
28
28
|
python_version: Optional[str] = None,
|
29
|
+
authorized_imports: list[str] | None = None,
|
29
30
|
modal_secrets: Dict[str, Union[str, None]] | None = None,
|
30
31
|
lazy_init: bool = True,
|
31
32
|
sandbox_name: str = "tinycodeagent-sandbox",
|
@@ -48,6 +49,7 @@ class ModalProvider(CodeExecutionProvider):
|
|
48
49
|
(git, curl, …) so you only need to specify the extras.
|
49
50
|
python_version: Python version used for the sandbox image. If
|
50
51
|
``None`` the current interpreter version is used.
|
52
|
+
authorized_imports: Optional allow-list of modules the user code is permitted to import. Supports wildcard patterns (e.g. "pandas.*"). If ``None`` the safety layer blocks only the predefined dangerous modules.
|
51
53
|
"""
|
52
54
|
|
53
55
|
# Resolve default values ------------------------------------------------
|
@@ -70,6 +72,7 @@ class ModalProvider(CodeExecutionProvider):
|
|
70
72
|
self.default_packages: List[str] = default_packages
|
71
73
|
self.apt_packages: List[str] = apt_packages
|
72
74
|
self.python_version: str = python_version
|
75
|
+
self.authorized_imports = authorized_imports
|
73
76
|
|
74
77
|
# ----------------------------------------------------------------------
|
75
78
|
final_packages = list(set(self.default_packages + (pip_packages or [])))
|
@@ -89,6 +92,7 @@ class ModalProvider(CodeExecutionProvider):
|
|
89
92
|
self.modal_secrets = modal.Secret.from_dict(self.secrets)
|
90
93
|
self.app = None
|
91
94
|
self._app_run_python = None
|
95
|
+
self.is_trusted_code = kwargs.get("trust_code", False)
|
92
96
|
|
93
97
|
self._setup_modal_app()
|
94
98
|
|
@@ -139,17 +143,28 @@ class ModalProvider(CodeExecutionProvider):
|
|
139
143
|
print(full_code)
|
140
144
|
print("#" * 100)
|
141
145
|
|
142
|
-
|
143
146
|
|
144
147
|
# Use Modal's native execution methods
|
145
148
|
response = self._python_executor(full_code, self._globals_dict, self._locals_dict)
|
146
149
|
|
147
150
|
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!<response>!!!!!!!!!!!!!!!!!!!!!!!!!")
|
148
151
|
|
149
|
-
#
|
150
|
-
|
151
|
-
|
152
|
-
|
152
|
+
# Always update globals and locals dictionaries, regardless of whether there was an error
|
153
|
+
# This ensures variables are preserved even when code execution fails
|
154
|
+
try:
|
155
|
+
# Update globals and locals from the response
|
156
|
+
if "updated_globals" in response:
|
157
|
+
self._globals_dict = cloudpickle.loads(make_session_blob(response["updated_globals"]))
|
158
|
+
|
159
|
+
if "updated_locals" in response:
|
160
|
+
self._locals_dict = cloudpickle.loads(make_session_blob(response["updated_locals"]))
|
161
|
+
|
162
|
+
# Update user variables from the updated globals and locals
|
163
|
+
# This preserves any changes made to variables by the LLM
|
164
|
+
self.update_user_variables_from_globals(self._globals_dict)
|
165
|
+
self.update_user_variables_from_globals(self._locals_dict)
|
166
|
+
except Exception as e:
|
167
|
+
print(f"Warning: Failed to update globals/locals after execution: {str(e)}")
|
153
168
|
|
154
169
|
self._log_response(response)
|
155
170
|
|
@@ -164,18 +179,30 @@ class ModalProvider(CodeExecutionProvider):
|
|
164
179
|
if self.executed_default_codes:
|
165
180
|
print("✔️ default codes already executed")
|
166
181
|
full_code = "\n".join(self.code_tools_definitions) +"\n\n"+code
|
182
|
+
# Code tools and default code are trusted, user code is not
|
167
183
|
else:
|
168
184
|
full_code = "\n".join(self.code_tools_definitions) +"\n\n"+ "\n".join(self.default_python_codes) + "\n\n" + code
|
169
185
|
self.executed_default_codes = True
|
186
|
+
# First execution includes framework code which is trusted
|
170
187
|
|
171
188
|
# Use Modal's native execution methods
|
172
189
|
if self.local_execution:
|
173
|
-
|
174
|
-
|
190
|
+
return self._app_run_python.local(
|
191
|
+
full_code,
|
192
|
+
globals_dict or {},
|
193
|
+
locals_dict or {},
|
194
|
+
self.authorized_imports,
|
195
|
+
self.is_trusted_code,
|
196
|
+
)
|
175
197
|
else:
|
176
|
-
# Use Modal's .remote() method for remote execution
|
177
198
|
with self.app.run():
|
178
|
-
return self._app_run_python.remote(
|
199
|
+
return self._app_run_python.remote(
|
200
|
+
full_code,
|
201
|
+
globals_dict or {},
|
202
|
+
locals_dict or {},
|
203
|
+
self.authorized_imports,
|
204
|
+
self.is_trusted_code,
|
205
|
+
)
|
179
206
|
|
180
207
|
def _log_response(self, response: Dict[str, Any]):
|
181
208
|
"""Log the response from code execution."""
|
@@ -184,15 +211,31 @@ class ModalProvider(CodeExecutionProvider):
|
|
184
211
|
print("#########################<printed_output>#########################")
|
185
212
|
print(response["printed_output"])
|
186
213
|
print("#########################</printed_output>#########################")
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
214
|
+
if response.get("return_value",None) not in [None,""]:
|
215
|
+
print("#########################<return_value>#########################")
|
216
|
+
print(response["return_value"])
|
217
|
+
print("#########################</return_value>#########################")
|
218
|
+
if response.get("stderr",None) not in [None,""]:
|
219
|
+
print("#########################<stderr>#########################")
|
220
|
+
print(response["stderr"])
|
221
|
+
print("#########################</stderr>#########################")
|
222
|
+
if response.get("error_traceback",None) not in [None,""]:
|
223
|
+
print("#########################<traceback>#########################")
|
224
|
+
# Check if this is a security exception and highlight it in red if so
|
225
|
+
error_text = response["error_traceback"]
|
226
|
+
if "SECURITY" in error_text:
|
227
|
+
try:
|
228
|
+
from ..modal_sandbox import COLOR
|
229
|
+
except ImportError:
|
230
|
+
# Fallback colors if modal_sandbox is not available
|
231
|
+
COLOR = {
|
232
|
+
"RED": "\033[91m",
|
233
|
+
"ENDC": "\033[0m",
|
234
|
+
}
|
235
|
+
print(f"{COLOR['RED']}{error_text}{COLOR['ENDC']}")
|
236
|
+
else:
|
237
|
+
print(error_text)
|
238
|
+
print("#########################</traceback>#########################")
|
196
239
|
|
197
240
|
async def cleanup(self):
|
198
241
|
"""Clean up Modal resources."""
|