Skip to content

Commit 6ca33c0

Browse files
Enhance TinyCodeAgent and CodeExecutionProvider to synchronize user variables with execution context. This update ensures user variables are preserved and updated after code execution, even in the event of errors. Additionally, improved error handling for syntax errors during code parsing and enhanced global/local variable management in ModalProvider to maintain state consistency.
1 parent 03184a9 commit 6ca33c0

4 files changed

Lines changed: 122 additions & 18 deletions

File tree

tinyagent/code_agent/providers/base.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from abc import ABC, abstractmethod
22
from typing import Dict, List, Any, Optional
33
from tinyagent.hooks.logging_manager import LoggingManager
4+
import cloudpickle
45

56

67
class CodeExecutionProvider(ABC):
@@ -69,8 +70,6 @@ def add_tools(self, tools: List[Any]) -> None:
6970
Args:
7071
tools: List of tool objects to add
7172
"""
72-
import cloudpickle
73-
7473
tools_str_list = ["import cloudpickle"]
7574
tools_str_list.append("###########<tools>###########\n")
7675
for tool in tools:
@@ -82,15 +81,29 @@ def add_tools(self, tools: List[Any]) -> None:
8281
tools_str_list.append("\n\n")
8382
self.code_tools_definitions.extend(tools_str_list)
8483

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+
85100
def set_user_variables(self, variables: Dict[str, Any]) -> None:
86101
"""
87102
Set user variables that will be available in the Python environment.
88103
89104
Args:
90105
variables: Dictionary of variable name -> value pairs
91106
"""
92-
import cloudpickle
93-
94107
self._user_variables = variables.copy()
95108

96109
# Add variables to the execution environment by serializing them
@@ -149,4 +162,46 @@ def get_user_variables(self) -> Dict[str, Any]:
149162
Returns:
150163
Dictionary of current user variables
151164
"""
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

tinyagent/code_agent/providers/modal_provider.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,17 +143,28 @@ async def execute_python(self, code_lines: List[str], timeout: int = 120) -> Dic
143143
print(full_code)
144144
print("#" * 100)
145145

146-
147146

148147
# Use Modal's native execution methods
149148
response = self._python_executor(full_code, self._globals_dict, self._locals_dict)
150149

151150
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!<response>!!!!!!!!!!!!!!!!!!!!!!!!!")
152151

153-
# Update the instance globals and locals with the execution results
154-
self._globals_dict = cloudpickle.loads(make_session_blob(response["updated_globals"]))
155-
self._locals_dict = cloudpickle.loads(make_session_blob(response["updated_locals"]))
156-
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)}")
157168

158169
self._log_response(response)
159170

tinyagent/code_agent/tiny_code_agent.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,16 @@ def _setup_code_execution_tool(self):
261261
async def run_python(code_lines: List[str], timeout: int = 120) -> str:
262262
"""Execute Python code using the configured provider."""
263263
try:
264+
# Before execution, ensure provider has the latest user variables
265+
if self.user_variables:
266+
self.code_provider.set_user_variables(self.user_variables)
267+
264268
result = await self.code_provider.execute_python(code_lines, timeout)
269+
270+
# After execution, update TinyCodeAgent's user_variables from the provider
271+
# This ensures they stay in sync
272+
self.user_variables = self.code_provider.get_user_variables()
273+
265274
return str(result)
266275
except Exception as e:
267276
print("!"*100)
@@ -272,6 +281,11 @@ async def run_python(code_lines: List[str], timeout: int = 120) -> str:
272281
print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
273282
print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
274283
print("!"*100)
284+
285+
# Even after an exception, update user_variables from the provider
286+
# This ensures any variables that were successfully created/modified are preserved
287+
self.user_variables = self.code_provider.get_user_variables()
288+
275289
return f"Error executing code: {str(e)}"
276290

277291
self.agent.add_tool(run_python)

tinyagent/code_agent/utils.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,34 @@ def custom_print(*args, **kwargs):
116116
#updated_globals['print'] = custom_print
117117

118118
# Parse the code
119-
tree = ast.parse(code, mode="exec")
120-
compiled = compile(tree, filename="<ast>", mode="exec")
119+
try:
120+
tree = ast.parse(code, mode="exec")
121+
compiled = compile(tree, filename="<ast>", mode="exec")
122+
except SyntaxError as e:
123+
# Return syntax error without executing
124+
return {
125+
"printed_output": "",
126+
"return_value": None,
127+
"stderr": "",
128+
"error_traceback": f"Syntax error: {str(e)}",
129+
"updated_globals": updated_globals,
130+
"updated_locals": updated_locals
131+
}
132+
121133
stdout_buf = io.StringIO()
122134
stderr_buf = io.StringIO()
123135
# Execute with exception handling
124136
error_traceback = None
125137
output = None
126138

139+
# Merge all variables into globals to avoid scoping issues with generator expressions
140+
# When exec() is called with both globals and locals, generator expressions can't
141+
# access local variables. By using only globals, everything runs in global scope.
142+
merged_globals = updated_globals.copy()
143+
merged_globals.update(updated_locals)
144+
127145
with contextlib.redirect_stdout(stdout_buf), contextlib.redirect_stderr(stderr_buf):
128146
try:
129-
# Merge all variables into globals to avoid scoping issues with generator expressions
130-
# When exec() is called with both globals and locals, generator expressions can't
131-
# access local variables. By using only globals, everything runs in global scope.
132-
merged_globals = updated_globals.copy()
133-
merged_globals.update(updated_locals)
134-
135147
# Add 'exec' to authorized_functions for internal use
136148
internal_authorized_functions = ['exec','eval']
137149
if authorized_functions is not None and not isinstance(authorized_functions, bool):
@@ -152,6 +164,18 @@ def custom_print(*args, **kwargs):
152164
except Exception:
153165
# Capture the full traceback as a string
154166
error_traceback = traceback.format_exc()
167+
168+
# CRITICAL FIX: Even when an exception occurs, we need to update the globals and locals
169+
# with any variables that were successfully created/modified before the exception
170+
for key, value in merged_globals.items():
171+
# Skip special variables and modules
172+
if key.startswith('__') or key in ['builtins', 'traceback', 'contextlib', 'io', 'ast', 'sys']:
173+
continue
174+
175+
# Update both dictionaries with the current state
176+
if key in updated_locals or key not in updated_globals:
177+
updated_locals[key] = value
178+
updated_globals[key] = value
155179

156180
# Join all captured output
157181
#printed_output = ''.join(output_buffer)

0 commit comments

Comments
 (0)