letta-nightly 0.5.4.dev20241122104229__py3-none-any.whl → 0.5.4.dev20241124104049__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 (36) hide show
  1. letta/agent.py +23 -3
  2. letta/agent_store/db.py +1 -1
  3. letta/client/client.py +290 -0
  4. letta/constants.py +5 -14
  5. letta/functions/helpers.py +0 -4
  6. letta/functions/schema_generator.py +24 -4
  7. letta/local_llm/utils.py +6 -3
  8. letta/log.py +7 -9
  9. letta/orm/__init__.py +2 -0
  10. letta/orm/block.py +5 -2
  11. letta/orm/blocks_agents.py +29 -0
  12. letta/orm/mixins.py +8 -0
  13. letta/orm/organization.py +8 -1
  14. letta/orm/sandbox_config.py +56 -0
  15. letta/orm/sqlalchemy_base.py +9 -3
  16. letta/schemas/blocks_agents.py +32 -0
  17. letta/schemas/letta_base.py +9 -0
  18. letta/schemas/memory.py +9 -8
  19. letta/schemas/sandbox_config.py +114 -0
  20. letta/server/rest_api/routers/v1/__init__.py +4 -9
  21. letta/server/rest_api/routers/v1/sandbox_configs.py +108 -0
  22. letta/server/rest_api/routers/v1/tools.py +3 -5
  23. letta/server/rest_api/utils.py +6 -0
  24. letta/server/server.py +10 -5
  25. letta/services/block_manager.py +4 -2
  26. letta/services/blocks_agents_manager.py +84 -0
  27. letta/services/sandbox_config_manager.py +256 -0
  28. letta/services/tool_execution_sandbox.py +326 -0
  29. letta/services/tool_manager.py +10 -10
  30. letta/services/tool_sandbox_env/.gitkeep +0 -0
  31. letta/settings.py +4 -0
  32. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/METADATA +3 -1
  33. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/RECORD +36 -27
  34. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/LICENSE +0 -0
  35. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/WHEEL +0 -0
  36. {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241124104049.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,84 @@
1
+ import warnings
2
+ from typing import List
3
+
4
+ from letta.orm.blocks_agents import BlocksAgents as BlocksAgentsModel
5
+ from letta.orm.errors import NoResultFound
6
+ from letta.schemas.blocks_agents import BlocksAgents as PydanticBlocksAgents
7
+ from letta.utils import enforce_types
8
+
9
+
10
+ # TODO: DELETE THIS ASAP
11
+ # TODO: So we have a patch where we manually specify CRUD operations
12
+ # TODO: This is because Agent is NOT migrated to the ORM yet
13
+ # TODO: Once we migrate Agent to the ORM, we should deprecate any agents relationship table managers
14
+ class BlocksAgentsManager:
15
+ """Manager class to handle business logic related to Blocks and Agents."""
16
+
17
+ def __init__(self):
18
+ from letta.server.server import db_context
19
+
20
+ self.session_maker = db_context
21
+
22
+ @enforce_types
23
+ def add_block_to_agent(self, agent_id: str, block_id: str, block_label: str) -> PydanticBlocksAgents:
24
+ """Add a block to an agent. If the label already exists on that agent, this will error."""
25
+ with self.session_maker() as session:
26
+ try:
27
+ # Check if the block-label combination already exists for this agent
28
+ blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
29
+ warnings.warn(f"Block label '{block_label}' already exists for agent '{agent_id}'.")
30
+ except NoResultFound:
31
+ blocks_agents_record = PydanticBlocksAgents(agent_id=agent_id, block_id=block_id, block_label=block_label)
32
+ blocks_agents_record = BlocksAgentsModel(**blocks_agents_record.model_dump(exclude_none=True))
33
+ blocks_agents_record.create(session)
34
+
35
+ return blocks_agents_record.to_pydantic()
36
+
37
+ @enforce_types
38
+ def remove_block_with_label_from_agent(self, agent_id: str, block_label: str) -> PydanticBlocksAgents:
39
+ """Remove a block with a label from an agent."""
40
+ with self.session_maker() as session:
41
+ try:
42
+ # Find and delete the block-label association for the agent
43
+ blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
44
+ blocks_agents_record.hard_delete(session)
45
+ return blocks_agents_record.to_pydantic()
46
+ except NoResultFound:
47
+ raise ValueError(f"Block label '{block_label}' not found for agent '{agent_id}'.")
48
+
49
+ @enforce_types
50
+ def remove_block_with_id_from_agent(self, agent_id: str, block_id: str) -> PydanticBlocksAgents:
51
+ """Remove a block with a label from an agent."""
52
+ with self.session_maker() as session:
53
+ try:
54
+ # Find and delete the block-label association for the agent
55
+ blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_id=block_id)
56
+ blocks_agents_record.hard_delete(session)
57
+ return blocks_agents_record.to_pydantic()
58
+ except NoResultFound:
59
+ raise ValueError(f"Block id '{block_id}' not found for agent '{agent_id}'.")
60
+
61
+ @enforce_types
62
+ def update_block_id_for_agent(self, agent_id: str, block_label: str, new_block_id: str) -> PydanticBlocksAgents:
63
+ """Update the block ID for a specific block label for an agent."""
64
+ with self.session_maker() as session:
65
+ try:
66
+ blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
67
+ blocks_agents_record.block_id = new_block_id
68
+ return blocks_agents_record.to_pydantic()
69
+ except NoResultFound:
70
+ raise ValueError(f"Block label '{block_label}' not found for agent '{agent_id}'.")
71
+
72
+ @enforce_types
73
+ def list_block_ids_for_agent(self, agent_id: str) -> List[str]:
74
+ """List all blocks associated with a specific agent."""
75
+ with self.session_maker() as session:
76
+ blocks_agents_record = BlocksAgentsModel.list(db_session=session, agent_id=agent_id)
77
+ return [record.block_id for record in blocks_agents_record]
78
+
79
+ @enforce_types
80
+ def list_agent_ids_with_block(self, block_id: str) -> List[str]:
81
+ """List all agents associated with a specific block."""
82
+ with self.session_maker() as session:
83
+ blocks_agents_record = BlocksAgentsModel.list(db_session=session, block_id=block_id)
84
+ return [record.agent_id for record in blocks_agents_record]
@@ -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,326 @@
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.services.sandbox_config_manager import SandboxConfigManager
15
+ from letta.services.tool_manager import ToolManager
16
+ from letta.services.user_manager import UserManager
17
+ from letta.settings import tool_settings
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class ToolExecutionSandbox:
23
+ METADATA_CONFIG_STATE_KEY = "config_state"
24
+ REQUIREMENT_TXT_NAME = "requirements.txt"
25
+
26
+ # This is the variable name in the auto-generated code that contains the function results
27
+ # We make this a long random string to avoid collisions with any variables in the user's code
28
+ LOCAL_SANDBOX_RESULT_VAR_NAME = "result_ZQqiequkcFwRwwGQMqkt"
29
+
30
+ def __init__(self, tool_name: str, args: dict, user_id: str, force_recreate=False):
31
+ self.tool_name = tool_name
32
+ self.args = args
33
+
34
+ # Get the user
35
+ # This user corresponds to the agent_state's user_id field
36
+ # agent_state is the state of the agent that invoked this run
37
+ self.user = UserManager().get_user_by_id(user_id=user_id)
38
+
39
+ # Get the tool
40
+ # TODO: So in theory, it's possible this retrieves a tool not provisioned to the agent
41
+ # TODO: That would probably imply that agent_state is incorrectly configured
42
+ self.tool = ToolManager().get_tool_by_name(tool_name=tool_name, actor=self.user)
43
+ if not self.tool:
44
+ raise ValueError(
45
+ f"Agent attempted to invoke tool {self.tool_name} that does not exist for organization {self.user.organization_id}"
46
+ )
47
+
48
+ self.sandbox_config_manager = SandboxConfigManager(tool_settings)
49
+ self.force_recreate = force_recreate
50
+
51
+ def run(self, agent_state: Optional[AgentState] = None) -> Optional[SandboxRunResult]:
52
+ """
53
+ Run the tool in a sandbox environment.
54
+
55
+ Args:
56
+ agent_state (Optional[AgentState]): The state of the agent invoking the tool
57
+
58
+ Returns:
59
+ Tuple[Any, Optional[AgentState]]: Tuple containing (tool_result, agent_state)
60
+ """
61
+ if tool_settings.e2b_api_key:
62
+ logger.info(f"Using e2b sandbox to execute {self.tool_name}")
63
+ code = self.generate_execution_script(agent_state=agent_state)
64
+ result = self.run_e2b_sandbox(code=code)
65
+ else:
66
+ logger.info(f"Using local sandbox to execute {self.tool_name}")
67
+ code = self.generate_execution_script(agent_state=agent_state)
68
+ result = self.run_local_dir_sandbox(code=code)
69
+
70
+ # Log out any stdout from the tool run
71
+ logger.info(f"Executed tool '{self.tool_name}', logging stdout from tool run: \n")
72
+ for log_line in result.stdout:
73
+ logger.info(f"{log_line}")
74
+ logger.info(f"Ending stdout log from tool run.")
75
+
76
+ # Return result
77
+ return result
78
+
79
+ # local sandbox specific functions
80
+ from contextlib import contextmanager
81
+
82
+ @contextmanager
83
+ def temporary_env_vars(self, env_vars: dict):
84
+ original_env = os.environ.copy() # Backup original environment variables
85
+ os.environ.update(env_vars) # Update with the new variables
86
+ try:
87
+ yield
88
+ finally:
89
+ os.environ.clear()
90
+ os.environ.update(original_env) # Restore original environment variables
91
+
92
+ def run_local_dir_sandbox(self, code: str) -> Optional[SandboxRunResult]:
93
+ sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.user)
94
+ local_configs = sbx_config.get_local_config()
95
+
96
+ # Get environment variables for the sandbox
97
+ env_vars = self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100)
98
+
99
+ # Safety checks
100
+ if not os.path.isdir(local_configs.sandbox_dir):
101
+ raise FileNotFoundError(f"Sandbox directory does not exist: {local_configs.sandbox_dir}")
102
+
103
+ # Write the code to a temp file in the sandbox_dir
104
+ with tempfile.NamedTemporaryFile(mode="w", dir=local_configs.sandbox_dir, suffix=".py", delete=False) as temp_file:
105
+ temp_file.write(code)
106
+ temp_file.flush()
107
+ temp_file_path = temp_file.name
108
+
109
+ # Save the old stdout
110
+ old_stdout = sys.stdout
111
+ try:
112
+ # Redirect stdout to capture script output
113
+ captured_stdout = io.StringIO()
114
+ sys.stdout = captured_stdout
115
+
116
+ # Execute the temp file
117
+ with self.temporary_env_vars(env_vars):
118
+ result = runpy.run_path(temp_file_path, init_globals=env_vars)
119
+
120
+ # Fetch the result
121
+ func_result = result.get(self.LOCAL_SANDBOX_RESULT_VAR_NAME)
122
+ func_return, agent_state = self.parse_best_effort(func_result)
123
+
124
+ # Restore stdout and collect captured output
125
+ sys.stdout = old_stdout
126
+ stdout_output = captured_stdout.getvalue()
127
+
128
+ return SandboxRunResult(
129
+ func_return=func_return,
130
+ agent_state=agent_state,
131
+ stdout=[stdout_output],
132
+ sandbox_config_fingerprint=sbx_config.fingerprint(),
133
+ )
134
+ except Exception as e:
135
+ raise RuntimeError(f"Executing tool {self.tool_name} has an unexpected error: {e}")
136
+ finally:
137
+ # Clean up the temp file and restore stdout
138
+ sys.stdout = old_stdout
139
+ os.remove(temp_file_path)
140
+
141
+ # e2b sandbox specific functions
142
+
143
+ def run_e2b_sandbox(self, code: str) -> Optional[SandboxRunResult]:
144
+ sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=self.user)
145
+ sbx = self.get_running_e2b_sandbox_with_same_state(sbx_config)
146
+ if not sbx or self.force_recreate:
147
+ sbx = self.create_e2b_sandbox_with_metadata_hash(sandbox_config=sbx_config)
148
+
149
+ # Since this sandbox was used, we extend its lifecycle by the timeout
150
+ sbx.set_timeout(sbx_config.get_e2b_config().timeout)
151
+
152
+ # Get environment variables for the sandbox
153
+ # TODO: We set limit to 100 here, but maybe we want it uncapped? Realistically this should be fine.
154
+ env_vars = self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100)
155
+ execution = sbx.run_code(code, envs=env_vars)
156
+ if execution.error is not None:
157
+ raise Exception(f"Executing tool {self.tool_name} failed with {execution.error}. Generated code: \n\n{code}")
158
+ elif len(execution.results) == 0:
159
+ return None
160
+ else:
161
+ func_return, agent_state = self.parse_best_effort(execution.results[0].text)
162
+ return SandboxRunResult(
163
+ func_return=func_return,
164
+ agent_state=agent_state,
165
+ stdout=execution.logs.stdout + execution.logs.stderr,
166
+ sandbox_config_fingerprint=sbx_config.fingerprint(),
167
+ )
168
+
169
+ def get_running_e2b_sandbox_with_same_state(self, sandbox_config: SandboxConfig) -> Optional["Sandbox"]:
170
+ from e2b_code_interpreter import Sandbox
171
+
172
+ # List running sandboxes and access metadata.
173
+ running_sandboxes = self.list_running_e2b_sandboxes()
174
+
175
+ # Hash the config to check the state
176
+ state_hash = sandbox_config.fingerprint()
177
+ for sandbox in running_sandboxes:
178
+ if self.METADATA_CONFIG_STATE_KEY in sandbox.metadata and sandbox.metadata[self.METADATA_CONFIG_STATE_KEY] == state_hash:
179
+ return Sandbox.connect(sandbox.sandbox_id)
180
+
181
+ return None
182
+
183
+ def create_e2b_sandbox_with_metadata_hash(self, sandbox_config: SandboxConfig) -> "Sandbox":
184
+ from e2b_code_interpreter import Sandbox
185
+
186
+ state_hash = sandbox_config.fingerprint()
187
+ e2b_config = sandbox_config.get_e2b_config()
188
+ if e2b_config.template:
189
+ sbx = Sandbox(sandbox_config.get_e2b_config().template, metadata={self.METADATA_CONFIG_STATE_KEY: state_hash})
190
+ else:
191
+ # no template
192
+ sbx = Sandbox(metadata={self.METADATA_CONFIG_STATE_KEY: state_hash}, **e2b_config.model_dump(exclude={"pip_requirements"}))
193
+
194
+ # install pip requirements
195
+ if e2b_config.pip_requirements:
196
+ for package in e2b_config.pip_requirements:
197
+ sbx.commands.run(f"pip install {package}")
198
+ return sbx
199
+
200
+ def list_running_e2b_sandboxes(self):
201
+ from e2b_code_interpreter import Sandbox
202
+
203
+ # List running sandboxes and access metadata.
204
+ return Sandbox.list()
205
+
206
+ # general utility functions
207
+
208
+ def parse_best_effort(self, text: str) -> Any:
209
+ result = pickle.loads(base64.b64decode(text))
210
+ agent_state = None
211
+ if not result["agent_state"] is None:
212
+ agent_state = result["agent_state"]
213
+ return result["results"], agent_state
214
+
215
+ def parse_function_arguments(self, source_code: str, tool_name: str):
216
+ """Get arguments of a function from its source code"""
217
+ tree = ast.parse(source_code)
218
+ args = []
219
+ for node in ast.walk(tree):
220
+ if isinstance(node, ast.FunctionDef) and node.name == tool_name:
221
+ for arg in node.args.args:
222
+ args.append(arg.arg)
223
+ return args
224
+
225
+ def generate_execution_script(self, agent_state: AgentState) -> str:
226
+ """
227
+ Generate code to run inside of execution sandbox.
228
+ Passes into a serialized agent state into the code, to be accessed by the tool.
229
+
230
+ Args:
231
+ agent_state (AgentState): The agent state
232
+
233
+ Returns:
234
+ code (str): The generated code strong
235
+ """
236
+ # dump JSON representation of agent state to re-load
237
+ code = "from typing import *\n"
238
+ code += "import pickle\n"
239
+ code += "import sys\n"
240
+ code += "import base64\n"
241
+
242
+ # Load the agent state data into the program
243
+ if agent_state:
244
+ code += "import letta\n"
245
+ code += "from letta import * \n"
246
+ import pickle
247
+
248
+ agent_state_pickle = pickle.dumps(agent_state)
249
+ code += f"agent_state = pickle.loads({agent_state_pickle})\n"
250
+ else:
251
+ # agent state is None
252
+ code += "agent_state = None\n"
253
+
254
+ for param in self.args:
255
+ code += self.initialize_param(param, self.args[param])
256
+
257
+ if "agent_state" in self.parse_function_arguments(self.tool.source_code, self.tool.name):
258
+ inject_agent_state = True
259
+ else:
260
+ inject_agent_state = False
261
+
262
+ code += "\n" + self.tool.source_code + "\n"
263
+
264
+ # TODO: handle wrapped print
265
+
266
+ code += (
267
+ self.LOCAL_SANDBOX_RESULT_VAR_NAME
268
+ + ' = {"results": '
269
+ + self.invoke_function_call(inject_agent_state=inject_agent_state)
270
+ + ', "agent_state": agent_state}\n'
271
+ )
272
+ code += (
273
+ f"{self.LOCAL_SANDBOX_RESULT_VAR_NAME} = base64.b64encode(pickle.dumps({self.LOCAL_SANDBOX_RESULT_VAR_NAME})).decode('utf-8')\n"
274
+ )
275
+ code += f"{self.LOCAL_SANDBOX_RESULT_VAR_NAME}\n"
276
+
277
+ return code
278
+
279
+ def initialize_param(self, name: str, raw_value: str) -> str:
280
+ params = self.tool.json_schema["parameters"]["properties"]
281
+ spec = params.get(name)
282
+ if spec is None:
283
+ # ignore extra params (like 'self') for now
284
+ return ""
285
+
286
+ param_type = spec.get("type")
287
+ if param_type is None and spec.get("parameters"):
288
+ param_type = spec["parameters"].get("type")
289
+
290
+ if param_type == "string":
291
+ value = '"' + raw_value + '"'
292
+ elif param_type == "integer" or param_type == "boolean":
293
+ value = raw_value
294
+ else:
295
+ raise TypeError(f"unsupported type: {param_type}")
296
+
297
+ return name + " = " + str(value) + "\n"
298
+
299
+ def invoke_function_call(self, inject_agent_state: bool) -> str:
300
+ """
301
+ Generate the code string to call the function.
302
+
303
+ Args:
304
+ inject_agent_state (bool): Whether to inject the agent's state as an input into the tool
305
+
306
+ Returns:
307
+ str: Generated code string for calling the tool
308
+ """
309
+ kwargs = []
310
+ for name in self.args:
311
+ if name in self.tool.json_schema["parameters"]["properties"]:
312
+ kwargs.append(name)
313
+
314
+ param_list = [f"{arg}={arg}" for arg in kwargs]
315
+ if inject_agent_state:
316
+ param_list.append("agent_state=agent_state")
317
+ params = ", ".join(param_list)
318
+ # if "agent_state" in kwargs:
319
+ # params += ", agent_state=agent_state"
320
+ # TODO: fix to figure out when to insert agent state or not
321
+ # params += "agent_state=agent_state"
322
+
323
+ func_call_str = self.tool.name + "(" + params + ")"
324
+ return func_call_str
325
+
326
+ #