letta-nightly 0.5.5.dev20241122170833__py3-none-any.whl → 0.6.0.dev20241204051808__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (70) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent.py +155 -166
  3. letta/agent_store/chroma.py +2 -0
  4. letta/agent_store/db.py +1 -1
  5. letta/cli/cli.py +12 -8
  6. letta/cli/cli_config.py +1 -1
  7. letta/client/client.py +765 -137
  8. letta/config.py +2 -2
  9. letta/constants.py +10 -14
  10. letta/errors.py +12 -0
  11. letta/functions/function_sets/base.py +38 -1
  12. letta/functions/functions.py +40 -57
  13. letta/functions/helpers.py +0 -4
  14. letta/functions/schema_generator.py +279 -18
  15. letta/helpers/tool_rule_solver.py +6 -5
  16. letta/llm_api/helpers.py +99 -5
  17. letta/llm_api/openai.py +8 -2
  18. letta/local_llm/utils.py +13 -6
  19. letta/log.py +7 -9
  20. letta/main.py +1 -1
  21. letta/metadata.py +53 -38
  22. letta/o1_agent.py +1 -4
  23. letta/orm/__init__.py +2 -0
  24. letta/orm/block.py +7 -3
  25. letta/orm/blocks_agents.py +32 -0
  26. letta/orm/errors.py +8 -0
  27. letta/orm/mixins.py +8 -0
  28. letta/orm/organization.py +8 -1
  29. letta/orm/sandbox_config.py +56 -0
  30. letta/orm/sqlalchemy_base.py +68 -10
  31. letta/persistence_manager.py +1 -0
  32. letta/schemas/agent.py +57 -52
  33. letta/schemas/block.py +85 -26
  34. letta/schemas/blocks_agents.py +32 -0
  35. letta/schemas/enums.py +14 -0
  36. letta/schemas/letta_base.py +10 -1
  37. letta/schemas/letta_request.py +11 -23
  38. letta/schemas/letta_response.py +1 -2
  39. letta/schemas/memory.py +41 -76
  40. letta/schemas/message.py +3 -3
  41. letta/schemas/sandbox_config.py +114 -0
  42. letta/schemas/tool.py +37 -1
  43. letta/schemas/tool_rule.py +13 -5
  44. letta/server/rest_api/app.py +5 -4
  45. letta/server/rest_api/interface.py +12 -19
  46. letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
  47. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
  48. letta/server/rest_api/routers/v1/__init__.py +4 -9
  49. letta/server/rest_api/routers/v1/agents.py +145 -61
  50. letta/server/rest_api/routers/v1/blocks.py +50 -5
  51. letta/server/rest_api/routers/v1/sandbox_configs.py +127 -0
  52. letta/server/rest_api/routers/v1/sources.py +8 -1
  53. letta/server/rest_api/routers/v1/tools.py +139 -13
  54. letta/server/rest_api/utils.py +6 -0
  55. letta/server/server.py +397 -340
  56. letta/server/static_files/assets/index-9fa459a2.js +1 -1
  57. letta/services/block_manager.py +23 -2
  58. letta/services/blocks_agents_manager.py +106 -0
  59. letta/services/per_agent_lock_manager.py +18 -0
  60. letta/services/sandbox_config_manager.py +256 -0
  61. letta/services/tool_execution_sandbox.py +352 -0
  62. letta/services/tool_manager.py +16 -22
  63. letta/services/tool_sandbox_env/.gitkeep +0 -0
  64. letta/settings.py +4 -0
  65. letta/utils.py +0 -7
  66. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/METADATA +8 -6
  67. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/RECORD +70 -60
  68. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/LICENSE +0 -0
  69. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/WHEEL +0 -0
  70. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,256 @@
1
+ from pathlib import Path
2
+ from typing import Dict, List, Optional
3
+
4
+ from letta.log import get_logger
5
+ from letta.orm.errors import NoResultFound
6
+ from letta.orm.sandbox_config import SandboxConfig as SandboxConfigModel
7
+ from letta.orm.sandbox_config import SandboxEnvironmentVariable as SandboxEnvVarModel
8
+ from letta.schemas.sandbox_config import E2BSandboxConfig, LocalSandboxConfig
9
+ from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
10
+ from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate
11
+ from letta.schemas.sandbox_config import SandboxEnvironmentVariable as PydanticEnvVar
12
+ from letta.schemas.sandbox_config import (
13
+ SandboxEnvironmentVariableCreate,
14
+ SandboxEnvironmentVariableUpdate,
15
+ SandboxType,
16
+ )
17
+ from letta.schemas.user import User as PydanticUser
18
+ from letta.utils import enforce_types, printd
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class SandboxConfigManager:
24
+ """Manager class to handle business logic related to SandboxConfig and SandboxEnvironmentVariable."""
25
+
26
+ def __init__(self, settings):
27
+ from letta.server.server import db_context
28
+
29
+ self.session_maker = db_context
30
+ self.e2b_template_id = settings.e2b_sandbox_template_id
31
+
32
+ @enforce_types
33
+ def get_or_create_default_sandbox_config(self, sandbox_type: SandboxType, actor: PydanticUser) -> PydanticSandboxConfig:
34
+ sandbox_config = self.get_sandbox_config_by_type(sandbox_type, actor=actor)
35
+ if not sandbox_config:
36
+ logger.info(f"Creating new sandbox config of type {sandbox_type}, none found for organization {actor.organization_id}.")
37
+
38
+ # TODO: Add more sandbox types later
39
+ if sandbox_type == SandboxType.E2B:
40
+ default_config = E2BSandboxConfig(template=self.e2b_template_id).model_dump(exclude_none=True)
41
+ else:
42
+ default_local_sandbox_path = str(Path(__file__).parent / "tool_sandbox_env")
43
+ default_config = LocalSandboxConfig(sandbox_dir=default_local_sandbox_path).model_dump(exclude_none=True)
44
+
45
+ sandbox_config = self.create_or_update_sandbox_config(SandboxConfigCreate(config=default_config), actor=actor)
46
+ return sandbox_config
47
+
48
+ @enforce_types
49
+ def create_or_update_sandbox_config(self, sandbox_config_create: SandboxConfigCreate, actor: PydanticUser) -> PydanticSandboxConfig:
50
+ """Create or update a sandbox configuration based on the PydanticSandboxConfig schema."""
51
+ config = sandbox_config_create.config
52
+ sandbox_type = config.type
53
+ sandbox_config = PydanticSandboxConfig(
54
+ type=sandbox_type, config=config.model_dump(exclude_none=True), organization_id=actor.organization_id
55
+ )
56
+
57
+ # Attempt to retrieve the existing sandbox configuration by type within the organization
58
+ db_sandbox = self.get_sandbox_config_by_type(sandbox_config.type, actor=actor)
59
+ if db_sandbox:
60
+ # Prepare the update data, excluding fields that should not be reset
61
+ update_data = sandbox_config.model_dump(exclude_unset=True, exclude_none=True)
62
+ update_data = {key: value for key, value in update_data.items() if getattr(db_sandbox, key) != value}
63
+
64
+ # If there are changes, update the sandbox configuration
65
+ if update_data:
66
+ db_sandbox = self.update_sandbox_config(db_sandbox.id, SandboxConfigUpdate(**update_data), actor)
67
+ else:
68
+ printd(
69
+ f"`create_or_update_sandbox_config` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
70
+ f"type={sandbox_config.type}, but found existing configuration with nothing to update."
71
+ )
72
+
73
+ return db_sandbox
74
+ else:
75
+ # If the sandbox configuration doesn't exist, create a new one
76
+ with self.session_maker() as session:
77
+ db_sandbox = SandboxConfigModel(**sandbox_config.model_dump(exclude_none=True))
78
+ db_sandbox.create(session, actor=actor)
79
+ return db_sandbox.to_pydantic()
80
+
81
+ @enforce_types
82
+ def update_sandbox_config(
83
+ self, sandbox_config_id: str, sandbox_update: SandboxConfigUpdate, actor: PydanticUser
84
+ ) -> PydanticSandboxConfig:
85
+ """Update an existing sandbox configuration."""
86
+ with self.session_maker() as session:
87
+ sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
88
+ # We need to check that the sandbox_update provided is the same type as the original sandbox
89
+ if sandbox.type != sandbox_update.config.type:
90
+ raise ValueError(
91
+ f"Mismatched type for sandbox config update: tried to update sandbox_config of type {sandbox.type} with config of type {sandbox_update.config.type}"
92
+ )
93
+
94
+ update_data = sandbox_update.model_dump(exclude_unset=True, exclude_none=True)
95
+ update_data = {key: value for key, value in update_data.items() if getattr(sandbox, key) != value}
96
+
97
+ if update_data:
98
+ for key, value in update_data.items():
99
+ setattr(sandbox, key, value)
100
+ sandbox.update(db_session=session, actor=actor)
101
+ else:
102
+ printd(
103
+ f"`update_sandbox_config` called with user_id={actor.id}, organization_id={actor.organization_id}, "
104
+ f"name={sandbox.type}, but nothing to update."
105
+ )
106
+ return sandbox.to_pydantic()
107
+
108
+ @enforce_types
109
+ def delete_sandbox_config(self, sandbox_config_id: str, actor: PydanticUser) -> PydanticSandboxConfig:
110
+ """Delete a sandbox configuration by its ID."""
111
+ with self.session_maker() as session:
112
+ sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
113
+ sandbox.hard_delete(db_session=session, actor=actor)
114
+ return sandbox.to_pydantic()
115
+
116
+ @enforce_types
117
+ def list_sandbox_configs(
118
+ self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
119
+ ) -> List[PydanticSandboxConfig]:
120
+ """List all sandbox configurations with optional pagination."""
121
+ with self.session_maker() as session:
122
+ sandboxes = SandboxConfigModel.list(
123
+ db_session=session,
124
+ cursor=cursor,
125
+ limit=limit,
126
+ organization_id=actor.organization_id,
127
+ )
128
+ return [sandbox.to_pydantic() for sandbox in sandboxes]
129
+
130
+ @enforce_types
131
+ def get_sandbox_config_by_id(self, sandbox_config_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
132
+ """Retrieve a sandbox configuration by its ID."""
133
+ with self.session_maker() as session:
134
+ try:
135
+ sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
136
+ return sandbox.to_pydantic()
137
+ except NoResultFound:
138
+ return None
139
+
140
+ @enforce_types
141
+ def get_sandbox_config_by_type(self, type: SandboxType, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
142
+ """Retrieve a sandbox config by its type."""
143
+ with self.session_maker() as session:
144
+ try:
145
+ sandboxes = SandboxConfigModel.list(
146
+ db_session=session,
147
+ type=type,
148
+ organization_id=actor.organization_id,
149
+ limit=1,
150
+ )
151
+ if sandboxes:
152
+ return sandboxes[0].to_pydantic()
153
+ return None
154
+ except NoResultFound:
155
+ return None
156
+
157
+ @enforce_types
158
+ def create_sandbox_env_var(
159
+ self, env_var_create: SandboxEnvironmentVariableCreate, sandbox_config_id: str, actor: PydanticUser
160
+ ) -> PydanticEnvVar:
161
+ """Create a new sandbox environment variable."""
162
+ env_var = PydanticEnvVar(**env_var_create.model_dump(), sandbox_config_id=sandbox_config_id, organization_id=actor.organization_id)
163
+
164
+ db_env_var = self.get_sandbox_env_var_by_key_and_sandbox_config_id(env_var.key, env_var.sandbox_config_id, actor=actor)
165
+ if db_env_var:
166
+ update_data = env_var.model_dump(exclude_unset=True, exclude_none=True)
167
+ update_data = {key: value for key, value in update_data.items() if getattr(db_env_var, key) != value}
168
+ # If there are changes, update the environment variable
169
+ if update_data:
170
+ db_env_var = self.update_sandbox_env_var(db_env_var.id, SandboxEnvironmentVariableUpdate(**update_data), actor)
171
+ else:
172
+ printd(
173
+ f"`create_or_update_sandbox_env_var` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
174
+ f"key={env_var.key}, but found existing variable with nothing to update."
175
+ )
176
+
177
+ return db_env_var
178
+ else:
179
+ with self.session_maker() as session:
180
+ env_var = SandboxEnvVarModel(**env_var.model_dump(exclude_none=True))
181
+ env_var.create(session, actor=actor)
182
+ return env_var.to_pydantic()
183
+
184
+ @enforce_types
185
+ def update_sandbox_env_var(
186
+ self, env_var_id: str, env_var_update: SandboxEnvironmentVariableUpdate, actor: PydanticUser
187
+ ) -> PydanticEnvVar:
188
+ """Update an existing sandbox environment variable."""
189
+ with self.session_maker() as session:
190
+ env_var = SandboxEnvVarModel.read(db_session=session, identifier=env_var_id, actor=actor)
191
+ update_data = env_var_update.model_dump(exclude_unset=True, exclude_none=True)
192
+ update_data = {key: value for key, value in update_data.items() if getattr(env_var, key) != value}
193
+
194
+ if update_data:
195
+ for key, value in update_data.items():
196
+ setattr(env_var, key, value)
197
+ env_var.update(db_session=session, actor=actor)
198
+ else:
199
+ printd(
200
+ f"`update_sandbox_env_var` called with user_id={actor.id}, organization_id={actor.organization_id}, "
201
+ f"key={env_var.key}, but nothing to update."
202
+ )
203
+ return env_var.to_pydantic()
204
+
205
+ @enforce_types
206
+ def delete_sandbox_env_var(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
207
+ """Delete a sandbox environment variable by its ID."""
208
+ with self.session_maker() as session:
209
+ env_var = SandboxEnvVarModel.read(db_session=session, identifier=env_var_id, actor=actor)
210
+ env_var.hard_delete(db_session=session, actor=actor)
211
+ return env_var.to_pydantic()
212
+
213
+ @enforce_types
214
+ def list_sandbox_env_vars(
215
+ self, sandbox_config_id: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
216
+ ) -> List[PydanticEnvVar]:
217
+ """List all sandbox environment variables with optional pagination."""
218
+ with self.session_maker() as session:
219
+ env_vars = SandboxEnvVarModel.list(
220
+ db_session=session,
221
+ cursor=cursor,
222
+ limit=limit,
223
+ organization_id=actor.organization_id,
224
+ sandbox_config_id=sandbox_config_id,
225
+ )
226
+ return [env_var.to_pydantic() for env_var in env_vars]
227
+
228
+ @enforce_types
229
+ def get_sandbox_env_vars_as_dict(
230
+ self, sandbox_config_id: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
231
+ ) -> Dict[str, str]:
232
+ env_vars = self.list_sandbox_env_vars(sandbox_config_id, actor, cursor, limit)
233
+ result = {}
234
+ for env_var in env_vars:
235
+ result[env_var.key] = env_var.value
236
+ return result
237
+
238
+ @enforce_types
239
+ def get_sandbox_env_var_by_key_and_sandbox_config_id(
240
+ self, key: str, sandbox_config_id: str, actor: Optional[PydanticUser] = None
241
+ ) -> Optional[PydanticEnvVar]:
242
+ """Retrieve a sandbox environment variable by its key and sandbox_config_id."""
243
+ with self.session_maker() as session:
244
+ try:
245
+ env_var = SandboxEnvVarModel.list(
246
+ db_session=session,
247
+ key=key,
248
+ sandbox_config_id=sandbox_config_id,
249
+ organization_id=actor.organization_id,
250
+ limit=1,
251
+ )
252
+ if env_var:
253
+ return env_var[0].to_pydantic()
254
+ return None
255
+ except NoResultFound:
256
+ return None
@@ -0,0 +1,352 @@
1
+ import ast
2
+ import base64
3
+ import io
4
+ import os
5
+ import pickle
6
+ import runpy
7
+ import sys
8
+ import tempfile
9
+ from typing import Any, Optional
10
+
11
+ from letta.log import get_logger
12
+ from letta.schemas.agent import AgentState
13
+ from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult, SandboxType
14
+ from letta.schemas.tool import Tool
15
+ from letta.services.sandbox_config_manager import SandboxConfigManager
16
+ from letta.services.tool_manager import ToolManager
17
+ from letta.services.user_manager import UserManager
18
+ from letta.settings import tool_settings
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class ToolExecutionSandbox:
24
+ METADATA_CONFIG_STATE_KEY = "config_state"
25
+ REQUIREMENT_TXT_NAME = "requirements.txt"
26
+
27
+ # This is the variable name in the auto-generated code that contains the function results
28
+ # We make this a long random string to avoid collisions with any variables in the user's code
29
+ LOCAL_SANDBOX_RESULT_VAR_NAME = "result_ZQqiequkcFwRwwGQMqkt"
30
+
31
+ def __init__(self, tool_name: str, args: dict, user_id: str, force_recreate=False, tool_object: Optional[Tool] = None):
32
+ self.tool_name = tool_name
33
+ self.args = args
34
+
35
+ # Get the user
36
+ # This user corresponds to the agent_state's user_id field
37
+ # agent_state is the state of the agent that invoked this run
38
+ self.user = UserManager().get_user_by_id(user_id=user_id)
39
+
40
+ # If a tool object is provided, we use it directly, otherwise pull via name
41
+ if tool_object is not None:
42
+ self.tool = tool_object
43
+ else:
44
+ # Get the tool via name
45
+ # TODO: So in theory, it's possible this retrieves a tool not provisioned to the agent
46
+ # TODO: That would probably imply that agent_state is incorrectly configured
47
+ self.tool = ToolManager().get_tool_by_name(tool_name=tool_name, actor=self.user)
48
+ if not self.tool:
49
+ raise ValueError(
50
+ f"Agent attempted to invoke tool {self.tool_name} that does not exist for organization {self.user.organization_id}"
51
+ )
52
+
53
+ self.sandbox_config_manager = SandboxConfigManager(tool_settings)
54
+ self.force_recreate = force_recreate
55
+
56
+ def run(self, agent_state: Optional[AgentState] = None) -> Optional[SandboxRunResult]:
57
+ """
58
+ Run the tool in a sandbox environment.
59
+
60
+ Args:
61
+ agent_state (Optional[AgentState]): The state of the agent invoking the tool
62
+
63
+ Returns:
64
+ Tuple[Any, Optional[AgentState]]: Tuple containing (tool_result, agent_state)
65
+ """
66
+ if tool_settings.e2b_api_key:
67
+ logger.debug(f"Using e2b sandbox to execute {self.tool_name}")
68
+ code = self.generate_execution_script(agent_state=agent_state)
69
+ result = self.run_e2b_sandbox(code=code)
70
+ else:
71
+ logger.debug(f"Using local sandbox to execute {self.tool_name}")
72
+ code = self.generate_execution_script(agent_state=agent_state)
73
+ result = self.run_local_dir_sandbox(code=code)
74
+
75
+ # Log out any stdout from the tool run
76
+ logger.debug(f"Executed tool '{self.tool_name}', logging stdout from tool run: \n")
77
+ for log_line in result.stdout:
78
+ logger.debug(f"{log_line}")
79
+ logger.debug(f"Ending stdout log from tool run.")
80
+
81
+ # Return result
82
+ return result
83
+
84
+ # local sandbox specific functions
85
+ from contextlib import contextmanager
86
+
87
+ @contextmanager
88
+ def temporary_env_vars(self, env_vars: dict):
89
+ original_env = os.environ.copy() # Backup original environment variables
90
+ os.environ.update(env_vars) # Update with the new variables
91
+ try:
92
+ yield
93
+ finally:
94
+ os.environ.clear()
95
+ os.environ.update(original_env) # Restore original environment variables
96
+
97
+ def run_local_dir_sandbox(self, code: str) -> Optional[SandboxRunResult]:
98
+ sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.user)
99
+ local_configs = sbx_config.get_local_config()
100
+
101
+ # Get environment variables for the sandbox
102
+ env_vars = self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100)
103
+
104
+ # Safety checks
105
+ if not os.path.isdir(local_configs.sandbox_dir):
106
+ raise FileNotFoundError(f"Sandbox directory does not exist: {local_configs.sandbox_dir}")
107
+
108
+ # Write the code to a temp file in the sandbox_dir
109
+ with tempfile.NamedTemporaryFile(mode="w", dir=local_configs.sandbox_dir, suffix=".py", delete=False) as temp_file:
110
+ temp_file.write(code)
111
+ temp_file.flush()
112
+ temp_file_path = temp_file.name
113
+
114
+ # Save the old stdout
115
+ old_stdout = sys.stdout
116
+ try:
117
+ # Redirect stdout to capture script output
118
+ captured_stdout = io.StringIO()
119
+ sys.stdout = captured_stdout
120
+
121
+ # Execute the temp file
122
+ with self.temporary_env_vars(env_vars):
123
+ result = runpy.run_path(temp_file_path, init_globals=env_vars)
124
+
125
+ # Fetch the result
126
+ func_result = result.get(self.LOCAL_SANDBOX_RESULT_VAR_NAME)
127
+ func_return, agent_state = self.parse_best_effort(func_result)
128
+
129
+ # Restore stdout and collect captured output
130
+ sys.stdout = old_stdout
131
+ stdout_output = captured_stdout.getvalue()
132
+
133
+ return SandboxRunResult(
134
+ func_return=func_return,
135
+ agent_state=agent_state,
136
+ stdout=[stdout_output],
137
+ sandbox_config_fingerprint=sbx_config.fingerprint(),
138
+ )
139
+ except Exception as e:
140
+ logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}")
141
+ raise e
142
+ finally:
143
+ # Clean up the temp file and restore stdout
144
+ sys.stdout = old_stdout
145
+ os.remove(temp_file_path)
146
+
147
+ # e2b sandbox specific functions
148
+
149
+ def run_e2b_sandbox(self, code: str) -> Optional[SandboxRunResult]:
150
+ sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=self.user)
151
+ sbx = self.get_running_e2b_sandbox_with_same_state(sbx_config)
152
+ if not sbx or self.force_recreate:
153
+ sbx = self.create_e2b_sandbox_with_metadata_hash(sandbox_config=sbx_config)
154
+
155
+ # Since this sandbox was used, we extend its lifecycle by the timeout
156
+ sbx.set_timeout(sbx_config.get_e2b_config().timeout)
157
+
158
+ # Get environment variables for the sandbox
159
+ # TODO: We set limit to 100 here, but maybe we want it uncapped? Realistically this should be fine.
160
+ env_vars = self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100)
161
+ execution = sbx.run_code(code, envs=env_vars)
162
+ if execution.error is not None:
163
+ logger.error(f"Executing tool {self.tool_name} failed with {execution.error}")
164
+ # Raise a concise exception as this gets returned to the LLM
165
+ raise self.parse_exception_from_e2b_execution(execution)
166
+ elif len(execution.results) == 0:
167
+ return None
168
+ else:
169
+ func_return, agent_state = self.parse_best_effort(execution.results[0].text)
170
+ return SandboxRunResult(
171
+ func_return=func_return,
172
+ agent_state=agent_state,
173
+ stdout=execution.logs.stdout + execution.logs.stderr,
174
+ sandbox_config_fingerprint=sbx_config.fingerprint(),
175
+ )
176
+
177
+ def parse_exception_from_e2b_execution(self, e2b_execution: "Execution") -> Exception:
178
+ builtins_dict = __builtins__ if isinstance(__builtins__, dict) else vars(__builtins__)
179
+ # Dynamically fetch the exception class from builtins, defaulting to Exception if not found
180
+ exception_class = builtins_dict.get(e2b_execution.error.name, Exception)
181
+ return exception_class(e2b_execution.error.value)
182
+
183
+ def get_running_e2b_sandbox_with_same_state(self, sandbox_config: SandboxConfig) -> Optional["Sandbox"]:
184
+ from e2b_code_interpreter import Sandbox
185
+
186
+ # List running sandboxes and access metadata.
187
+ running_sandboxes = self.list_running_e2b_sandboxes()
188
+
189
+ # Hash the config to check the state
190
+ state_hash = sandbox_config.fingerprint()
191
+ for sandbox in running_sandboxes:
192
+ if self.METADATA_CONFIG_STATE_KEY in sandbox.metadata and sandbox.metadata[self.METADATA_CONFIG_STATE_KEY] == state_hash:
193
+ return Sandbox.connect(sandbox.sandbox_id)
194
+
195
+ return None
196
+
197
+ def create_e2b_sandbox_with_metadata_hash(self, sandbox_config: SandboxConfig) -> "Sandbox":
198
+ from e2b_code_interpreter import Sandbox
199
+
200
+ state_hash = sandbox_config.fingerprint()
201
+ e2b_config = sandbox_config.get_e2b_config()
202
+ if e2b_config.template:
203
+ sbx = Sandbox(sandbox_config.get_e2b_config().template, metadata={self.METADATA_CONFIG_STATE_KEY: state_hash})
204
+ else:
205
+ # no template
206
+ sbx = Sandbox(metadata={self.METADATA_CONFIG_STATE_KEY: state_hash}, **e2b_config.model_dump(exclude={"pip_requirements"}))
207
+
208
+ # install pip requirements
209
+ if e2b_config.pip_requirements:
210
+ for package in e2b_config.pip_requirements:
211
+ sbx.commands.run(f"pip install {package}")
212
+ return sbx
213
+
214
+ def list_running_e2b_sandboxes(self):
215
+ from e2b_code_interpreter import Sandbox
216
+
217
+ # List running sandboxes and access metadata.
218
+ return Sandbox.list()
219
+
220
+ # general utility functions
221
+
222
+ def parse_best_effort(self, text: str) -> Any:
223
+ result = pickle.loads(base64.b64decode(text))
224
+ agent_state = None
225
+ if not result["agent_state"] is None:
226
+ agent_state = result["agent_state"]
227
+ return result["results"], agent_state
228
+
229
+ def parse_function_arguments(self, source_code: str, tool_name: str):
230
+ """Get arguments of a function from its source code"""
231
+ tree = ast.parse(source_code)
232
+ args = []
233
+ for node in ast.walk(tree):
234
+ if isinstance(node, ast.FunctionDef) and node.name == tool_name:
235
+ for arg in node.args.args:
236
+ args.append(arg.arg)
237
+ return args
238
+
239
+ def generate_execution_script(self, agent_state: AgentState) -> str:
240
+ """
241
+ Generate code to run inside of execution sandbox.
242
+ Passes into a serialized agent state into the code, to be accessed by the tool.
243
+
244
+ Args:
245
+ agent_state (AgentState): The agent state
246
+
247
+ Returns:
248
+ code (str): The generated code strong
249
+ """
250
+ # dump JSON representation of agent state to re-load
251
+ code = "from typing import *\n"
252
+ code += "import pickle\n"
253
+ code += "import sys\n"
254
+ code += "import base64\n"
255
+
256
+ # Load the agent state data into the program
257
+ if agent_state:
258
+ code += "import letta\n"
259
+ code += "from letta import * \n"
260
+ import pickle
261
+
262
+ agent_state_pickle = pickle.dumps(agent_state)
263
+ code += f"agent_state = pickle.loads({agent_state_pickle})\n"
264
+ else:
265
+ # agent state is None
266
+ code += "agent_state = None\n"
267
+
268
+ for param in self.args:
269
+ code += self.initialize_param(param, self.args[param])
270
+
271
+ if "agent_state" in self.parse_function_arguments(self.tool.source_code, self.tool.name):
272
+ inject_agent_state = True
273
+ else:
274
+ inject_agent_state = False
275
+
276
+ code += "\n" + self.tool.source_code + "\n"
277
+
278
+ # TODO: handle wrapped print
279
+
280
+ code += (
281
+ self.LOCAL_SANDBOX_RESULT_VAR_NAME
282
+ + ' = {"results": '
283
+ + self.invoke_function_call(inject_agent_state=inject_agent_state)
284
+ + ', "agent_state": agent_state}\n'
285
+ )
286
+ code += (
287
+ f"{self.LOCAL_SANDBOX_RESULT_VAR_NAME} = base64.b64encode(pickle.dumps({self.LOCAL_SANDBOX_RESULT_VAR_NAME})).decode('utf-8')\n"
288
+ )
289
+ code += f"{self.LOCAL_SANDBOX_RESULT_VAR_NAME}\n"
290
+ return code
291
+
292
+ def _convert_param_to_value(self, param_type: str, raw_value: str) -> str:
293
+
294
+ if param_type == "string":
295
+ value = "pickle.loads(" + str(pickle.dumps(raw_value)) + ")"
296
+
297
+ elif param_type == "integer" or param_type == "boolean" or param_type == "number":
298
+ value = raw_value
299
+
300
+ elif param_type == "array":
301
+ value = raw_value
302
+
303
+ elif param_type == "object":
304
+ value = raw_value
305
+
306
+ else:
307
+ raise TypeError(f"Unsupported type: {param_type}, raw_value={raw_value}")
308
+ return str(value)
309
+
310
+ def initialize_param(self, name: str, raw_value: str) -> str:
311
+ params = self.tool.json_schema["parameters"]["properties"]
312
+ spec = params.get(name)
313
+ if spec is None:
314
+ # ignore extra params (like 'self') for now
315
+ return ""
316
+
317
+ param_type = spec.get("type")
318
+ if param_type is None and spec.get("parameters"):
319
+ param_type = spec["parameters"].get("type")
320
+
321
+ value = self._convert_param_to_value(param_type, raw_value)
322
+
323
+ return name + " = " + value + "\n"
324
+
325
+ def invoke_function_call(self, inject_agent_state: bool) -> str:
326
+ """
327
+ Generate the code string to call the function.
328
+
329
+ Args:
330
+ inject_agent_state (bool): Whether to inject the agent's state as an input into the tool
331
+
332
+ Returns:
333
+ str: Generated code string for calling the tool
334
+ """
335
+ kwargs = []
336
+ for name in self.args:
337
+ if name in self.tool.json_schema["parameters"]["properties"]:
338
+ kwargs.append(name)
339
+
340
+ param_list = [f"{arg}={arg}" for arg in kwargs]
341
+ if inject_agent_state:
342
+ param_list.append("agent_state=agent_state")
343
+ params = ", ".join(param_list)
344
+ # if "agent_state" in kwargs:
345
+ # params += ", agent_state=agent_state"
346
+ # TODO: fix to figure out when to insert agent state or not
347
+ # params += "agent_state=agent_state"
348
+
349
+ func_call_str = self.tool.name + "(" + params + ")"
350
+ return func_call_str
351
+
352
+ #