letta-nightly 0.5.4.dev20241121104201__py3-none-any.whl → 0.5.4.dev20241123104112__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 (40) hide show
  1. letta/agent.py +48 -25
  2. letta/agent_store/db.py +1 -1
  3. letta/client/client.py +361 -7
  4. letta/constants.py +5 -14
  5. letta/functions/helpers.py +5 -42
  6. letta/functions/schema_generator.py +24 -4
  7. letta/local_llm/utils.py +6 -3
  8. letta/log.py +7 -9
  9. letta/metadata.py +17 -4
  10. letta/orm/__init__.py +2 -0
  11. letta/orm/block.py +5 -2
  12. letta/orm/blocks_agents.py +29 -0
  13. letta/orm/mixins.py +8 -0
  14. letta/orm/organization.py +8 -1
  15. letta/orm/sandbox_config.py +56 -0
  16. letta/orm/sqlalchemy_base.py +9 -3
  17. letta/schemas/block.py +15 -1
  18. letta/schemas/blocks_agents.py +32 -0
  19. letta/schemas/letta_base.py +9 -0
  20. letta/schemas/memory.py +42 -8
  21. letta/schemas/sandbox_config.py +114 -0
  22. letta/schemas/tool.py +2 -45
  23. letta/server/rest_api/routers/v1/__init__.py +4 -9
  24. letta/server/rest_api/routers/v1/agents.py +85 -1
  25. letta/server/rest_api/routers/v1/sandbox_configs.py +108 -0
  26. letta/server/rest_api/routers/v1/tools.py +3 -5
  27. letta/server/rest_api/utils.py +6 -0
  28. letta/server/server.py +159 -12
  29. letta/services/block_manager.py +3 -1
  30. letta/services/blocks_agents_manager.py +84 -0
  31. letta/services/sandbox_config_manager.py +256 -0
  32. letta/services/tool_execution_sandbox.py +326 -0
  33. letta/services/tool_manager.py +10 -10
  34. letta/services/tool_sandbox_env/.gitkeep +0 -0
  35. letta/settings.py +4 -0
  36. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/METADATA +28 -27
  37. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/RECORD +40 -31
  38. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/LICENSE +0 -0
  39. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/WHEEL +0 -0
  40. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/entry_points.txt +0 -0
letta/schemas/memory.py CHANGED
@@ -5,8 +5,9 @@ from pydantic import BaseModel, Field
5
5
 
6
6
  # Forward referencing to avoid circular import with Agent -> Memory -> Agent
7
7
  if TYPE_CHECKING:
8
- from letta.agent import Agent
8
+ pass
9
9
 
10
+ from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
10
11
  from letta.schemas.block import Block
11
12
  from letta.schemas.message import Message
12
13
  from letta.schemas.openai.chat_completion_request import Tool
@@ -158,6 +159,13 @@ class Memory(BaseModel, validate_assignment=True):
158
159
 
159
160
  self.memory[block.label] = block
160
161
 
162
+ def unlink_block(self, block_label: str) -> Block:
163
+ """Unlink a block from the memory object"""
164
+ if block_label not in self.memory:
165
+ raise ValueError(f"Block with label {block_label} does not exist")
166
+
167
+ return self.memory.pop(block_label)
168
+
161
169
  def update_block_value(self, label: str, value: str):
162
170
  """Update the value of a block"""
163
171
  if label not in self.memory:
@@ -167,6 +175,32 @@ class Memory(BaseModel, validate_assignment=True):
167
175
 
168
176
  self.memory[label].value = value
169
177
 
178
+ def update_block_label(self, current_label: str, new_label: str):
179
+ """Update the label of a block"""
180
+ if current_label not in self.memory:
181
+ raise ValueError(f"Block with label {current_label} does not exist")
182
+ if not isinstance(new_label, str):
183
+ raise ValueError(f"Provided new label must be a string")
184
+
185
+ # First change the label of the block
186
+ self.memory[current_label].label = new_label
187
+
188
+ # Then swap the block to the new label
189
+ self.memory[new_label] = self.memory.pop(current_label)
190
+
191
+ def update_block_limit(self, label: str, limit: int):
192
+ """Update the limit of a block"""
193
+ if label not in self.memory:
194
+ raise ValueError(f"Block with label {label} does not exist")
195
+ if not isinstance(limit, int):
196
+ raise ValueError(f"Provided limit must be an integer")
197
+
198
+ # Check to make sure the new limit is greater than the current length of the block
199
+ if len(self.memory[label].value) > limit:
200
+ raise ValueError(f"New limit {limit} is less than the current length of the block {len(self.memory[label].value)}")
201
+
202
+ self.memory[label].limit = limit
203
+
170
204
 
171
205
  # TODO: ideally this is refactored into ChatMemory and the subclasses are given more specific names.
172
206
  class BasicBlockMemory(Memory):
@@ -196,7 +230,7 @@ class BasicBlockMemory(Memory):
196
230
  assert block.label is not None and block.label != "", "each existing chat block must have a name"
197
231
  self.link_block(block=block)
198
232
 
199
- def core_memory_append(self: "Agent", label: str, content: str) -> Optional[str]: # type: ignore
233
+ def core_memory_append(agent_state: "AgentState", label: str, content: str) -> Optional[str]: # type: ignore
200
234
  """
201
235
  Append to the contents of core memory.
202
236
 
@@ -207,12 +241,12 @@ class BasicBlockMemory(Memory):
207
241
  Returns:
208
242
  Optional[str]: None is always returned as this function does not produce a response.
209
243
  """
210
- current_value = str(self.memory.get_block(label).value)
244
+ current_value = str(agent_state.memory.get_block(label).value)
211
245
  new_value = current_value + "\n" + str(content)
212
- self.memory.update_block_value(label=label, value=new_value)
246
+ agent_state.memory.update_block_value(label=label, value=new_value)
213
247
  return None
214
248
 
215
- def core_memory_replace(self: "Agent", label: str, old_content: str, new_content: str) -> Optional[str]: # type: ignore
249
+ def core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]: # type: ignore
216
250
  """
217
251
  Replace the contents of core memory. To delete memories, use an empty string for new_content.
218
252
 
@@ -224,11 +258,11 @@ class BasicBlockMemory(Memory):
224
258
  Returns:
225
259
  Optional[str]: None is always returned as this function does not produce a response.
226
260
  """
227
- current_value = str(self.memory.get_block(label).value)
261
+ current_value = str(agent_state.memory.get_block(label).value)
228
262
  if old_content not in current_value:
229
263
  raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
230
264
  new_value = current_value.replace(str(old_content), str(new_content))
231
- self.memory.update_block_value(label=label, value=new_value)
265
+ agent_state.memory.update_block_value(label=label, value=new_value)
232
266
  return None
233
267
 
234
268
 
@@ -237,7 +271,7 @@ class ChatMemory(BasicBlockMemory):
237
271
  ChatMemory initializes a BaseChatMemory with two default blocks, `human` and `persona`.
238
272
  """
239
273
 
240
- def __init__(self, persona: str, human: str, limit: int = 2000):
274
+ def __init__(self, persona: str, human: str, limit: int = CORE_MEMORY_BLOCK_CHAR_LIMIT):
241
275
  """
242
276
  Initialize the ChatMemory object with a persona and human string.
243
277
 
@@ -0,0 +1,114 @@
1
+ import hashlib
2
+ import json
3
+ from enum import Enum
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from letta.schemas.agent import AgentState
9
+ from letta.schemas.letta_base import LettaBase, OrmMetadataBase
10
+
11
+
12
+ # Sandbox Config
13
+ class SandboxType(str, Enum):
14
+ E2B = "e2b"
15
+ LOCAL = "local"
16
+
17
+
18
+ class SandboxRunResult(BaseModel):
19
+ func_return: Optional[Any] = Field(None, description="The function return object")
20
+ agent_state: Optional[AgentState] = Field(None, description="The agent state")
21
+ stdout: Optional[List[str]] = Field(None, description="Captured stdout (e.g. prints, logs) from the function invocation")
22
+ sandbox_config_fingerprint: str = Field(None, description="The fingerprint of the config for the sandbox")
23
+
24
+
25
+ class LocalSandboxConfig(BaseModel):
26
+ sandbox_dir: str = Field(..., description="Directory for the sandbox environment.")
27
+
28
+ @property
29
+ def type(self) -> "SandboxType":
30
+ return SandboxType.LOCAL
31
+
32
+
33
+ class E2BSandboxConfig(BaseModel):
34
+ timeout: int = Field(5 * 60, description="Time limit for the sandbox (in seconds).")
35
+ template: Optional[str] = Field(None, description="The E2B template id (docker image).")
36
+ pip_requirements: Optional[List[str]] = Field(None, description="A list of pip packages to install on the E2B Sandbox")
37
+
38
+ @property
39
+ def type(self) -> "SandboxType":
40
+ return SandboxType.E2B
41
+
42
+
43
+ class SandboxConfigBase(OrmMetadataBase):
44
+ __id_prefix__ = "sandbox"
45
+
46
+
47
+ class SandboxConfig(SandboxConfigBase):
48
+ id: str = SandboxConfigBase.generate_id_field()
49
+ type: SandboxType = Field(None, description="The type of sandbox.")
50
+ organization_id: Optional[str] = Field(None, description="The unique identifier of the organization associated with the sandbox.")
51
+ config: Dict = Field(default_factory=lambda: {}, description="The JSON sandbox settings data.")
52
+
53
+ def get_e2b_config(self) -> E2BSandboxConfig:
54
+ return E2BSandboxConfig(**self.config)
55
+
56
+ def get_local_config(self) -> LocalSandboxConfig:
57
+ return LocalSandboxConfig(**self.config)
58
+
59
+ def fingerprint(self) -> str:
60
+ # Only take into account type, org_id, and the config items
61
+ # Canonicalize input data into JSON with sorted keys
62
+ hash_input = json.dumps(
63
+ {
64
+ "type": self.type.value,
65
+ "organization_id": self.organization_id,
66
+ "config": self.config,
67
+ },
68
+ sort_keys=True, # Ensure stable ordering
69
+ separators=(",", ":"), # Minimize serialization differences
70
+ )
71
+
72
+ # Compute SHA-256 hash
73
+ hash_digest = hashlib.sha256(hash_input.encode("utf-8")).digest()
74
+
75
+ # Convert the digest to an integer for compatibility with Python's hash requirements
76
+ return str(int.from_bytes(hash_digest, byteorder="big"))
77
+
78
+
79
+ class SandboxConfigCreate(LettaBase):
80
+ config: Union[LocalSandboxConfig, E2BSandboxConfig] = Field(..., description="The configuration for the sandbox.")
81
+
82
+
83
+ class SandboxConfigUpdate(LettaBase):
84
+ """Pydantic model for updating SandboxConfig fields."""
85
+
86
+ config: Union[LocalSandboxConfig, E2BSandboxConfig] = Field(None, description="The JSON configuration data for the sandbox.")
87
+
88
+
89
+ # Environment Variable
90
+ class SandboxEnvironmentVariableBase(OrmMetadataBase):
91
+ __id_prefix__ = "sandbox-env"
92
+
93
+
94
+ class SandboxEnvironmentVariable(SandboxEnvironmentVariableBase):
95
+ id: str = SandboxEnvironmentVariableBase.generate_id_field()
96
+ key: str = Field(..., description="The name of the environment variable.")
97
+ value: str = Field(..., description="The value of the environment variable.")
98
+ description: Optional[str] = Field(None, description="An optional description of the environment variable.")
99
+ sandbox_config_id: str = Field(..., description="The ID of the sandbox config this environment variable belongs to.")
100
+ organization_id: Optional[str] = Field(None, description="The ID of the organization this environment variable belongs to.")
101
+
102
+
103
+ class SandboxEnvironmentVariableCreate(LettaBase):
104
+ key: str = Field(..., description="The name of the environment variable.")
105
+ value: str = Field(..., description="The value of the environment variable.")
106
+ description: Optional[str] = Field(None, description="An optional description of the environment variable.")
107
+
108
+
109
+ class SandboxEnvironmentVariableUpdate(LettaBase):
110
+ """Pydantic model for updating SandboxEnvironmentVariable fields."""
111
+
112
+ key: Optional[str] = Field(None, description="The name of the environment variable.")
113
+ value: Optional[str] = Field(None, description="The value of the environment variable.")
114
+ description: Optional[str] = Field(None, description="An optional description of the environment variable.")
letta/schemas/tool.py CHANGED
@@ -4,13 +4,9 @@ from pydantic import Field
4
4
 
5
5
  from letta.functions.helpers import (
6
6
  generate_composio_tool_wrapper,
7
- generate_crewai_tool_wrapper,
8
7
  generate_langchain_tool_wrapper,
9
8
  )
10
- from letta.functions.schema_generator import (
11
- generate_schema_from_args_schema_v1,
12
- generate_schema_from_args_schema_v2,
13
- )
9
+ from letta.functions.schema_generator import generate_schema_from_args_schema_v2
14
10
  from letta.schemas.letta_base import LettaBase
15
11
  from letta.schemas.openai.chat_completions import ToolCall
16
12
 
@@ -132,37 +128,7 @@ class ToolCreate(LettaBase):
132
128
  tags = ["langchain"]
133
129
  # NOTE: langchain tools may come from different packages
134
130
  wrapper_func_name, wrapper_function_str = generate_langchain_tool_wrapper(langchain_tool, additional_imports_module_attr_map)
135
- json_schema = generate_schema_from_args_schema_v1(langchain_tool.args_schema, name=wrapper_func_name, description=description)
136
-
137
- return cls(
138
- name=wrapper_func_name,
139
- description=description,
140
- source_type=source_type,
141
- tags=tags,
142
- source_code=wrapper_function_str,
143
- json_schema=json_schema,
144
- )
145
-
146
- @classmethod
147
- def from_crewai(
148
- cls,
149
- crewai_tool: "CrewAIBaseTool",
150
- additional_imports_module_attr_map: dict[str, str] = None,
151
- ) -> "ToolCreate":
152
- """
153
- Class method to create an instance of Tool from a crewAI BaseTool object.
154
-
155
- Args:
156
- crewai_tool (CrewAIBaseTool): An instance of a crewAI BaseTool (BaseTool from crewai)
157
-
158
- Returns:
159
- Tool: A Letta Tool initialized with attributes derived from the provided crewAI BaseTool object.
160
- """
161
- description = crewai_tool.description
162
- source_type = "python"
163
- tags = ["crew-ai"]
164
- wrapper_func_name, wrapper_function_str = generate_crewai_tool_wrapper(crewai_tool, additional_imports_module_attr_map)
165
- json_schema = generate_schema_from_args_schema_v1(crewai_tool.args_schema, name=wrapper_func_name, description=description)
131
+ json_schema = generate_schema_from_args_schema_v2(langchain_tool.args_schema, name=wrapper_func_name, description=description)
166
132
 
167
133
  return cls(
168
134
  name=wrapper_func_name,
@@ -185,15 +151,6 @@ class ToolCreate(LettaBase):
185
151
 
186
152
  return [wikipedia_tool]
187
153
 
188
- @classmethod
189
- def load_default_crewai_tools(cls) -> List["ToolCreate"]:
190
- # For now, we only support scrape website tool
191
- from crewai_tools import ScrapeWebsiteTool
192
-
193
- web_scrape_tool = ToolCreate.from_crewai(ScrapeWebsiteTool())
194
-
195
- return [web_scrape_tool]
196
-
197
154
  @classmethod
198
155
  def load_default_composio_tools(cls) -> List["ToolCreate"]:
199
156
  from composio_langchain import Action
@@ -3,15 +3,10 @@ from letta.server.rest_api.routers.v1.blocks import router as blocks_router
3
3
  from letta.server.rest_api.routers.v1.health import router as health_router
4
4
  from letta.server.rest_api.routers.v1.jobs import router as jobs_router
5
5
  from letta.server.rest_api.routers.v1.llms import router as llm_router
6
+ from letta.server.rest_api.routers.v1.sandbox_configs import (
7
+ router as sandbox_configs_router,
8
+ )
6
9
  from letta.server.rest_api.routers.v1.sources import router as sources_router
7
10
  from letta.server.rest_api.routers.v1.tools import router as tools_router
8
11
 
9
- ROUTERS = [
10
- tools_router,
11
- sources_router,
12
- agents_router,
13
- llm_router,
14
- blocks_router,
15
- jobs_router,
16
- health_router,
17
- ]
12
+ ROUTERS = [tools_router, sources_router, agents_router, llm_router, blocks_router, jobs_router, health_router, sandbox_configs_router]
@@ -7,6 +7,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
7
7
 
8
8
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
9
9
  from letta.schemas.agent import AgentState, CreateAgent, UpdateAgentState
10
+ from letta.schemas.block import Block, BlockCreate, BlockLabelUpdate, BlockLimitUpdate
10
11
  from letta.schemas.enums import MessageStreamStatus
11
12
  from letta.schemas.letta_message import (
12
13
  LegacyLettaMessage,
@@ -217,7 +218,9 @@ def update_agent_memory(
217
218
  ):
218
219
  """
219
220
  Update the core memory of a specific agent.
220
- This endpoint accepts new memory contents (human and persona) and updates the core memory of the agent identified by the user ID and agent ID.
221
+ This endpoint accepts new memory contents (labels as keys, and values as values) and updates the core memory of the agent identified by the user ID and agent ID.
222
+ This endpoint accepts new memory contents to update the core memory of the agent.
223
+ This endpoint only supports modifying existing blocks; it does not support deleting/unlinking or creating/linking blocks.
221
224
  """
222
225
  actor = server.get_user_or_default(user_id=user_id)
223
226
 
@@ -225,6 +228,87 @@ def update_agent_memory(
225
228
  return memory
226
229
 
227
230
 
231
+ @router.patch("/{agent_id}/memory/label", response_model=Memory, operation_id="update_agent_memory_label")
232
+ def update_agent_memory_label(
233
+ agent_id: str,
234
+ update_label: BlockLabelUpdate = Body(...),
235
+ server: "SyncServer" = Depends(get_letta_server),
236
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
237
+ ):
238
+ """
239
+ Update the label of a block in an agent's memory.
240
+ """
241
+ actor = server.get_user_or_default(user_id=user_id)
242
+
243
+ memory = server.update_agent_memory_label(
244
+ user_id=actor.id, agent_id=agent_id, current_block_label=update_label.current_label, new_block_label=update_label.new_label
245
+ )
246
+ return memory
247
+
248
+
249
+ @router.post("/{agent_id}/memory/block", response_model=Memory, operation_id="add_agent_memory_block")
250
+ def add_agent_memory_block(
251
+ agent_id: str,
252
+ create_block: BlockCreate = Body(...),
253
+ server: "SyncServer" = Depends(get_letta_server),
254
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
255
+ ):
256
+ """
257
+ Creates a memory block and links it to the agent.
258
+ """
259
+ actor = server.get_user_or_default(user_id=user_id)
260
+
261
+ # Copied from POST /blocks
262
+ block_req = Block(**create_block.model_dump())
263
+ block = server.block_manager.create_or_update_block(actor=actor, block=block_req)
264
+
265
+ # Link the block to the agent
266
+ updated_memory = server.link_block_to_agent_memory(user_id=actor.id, agent_id=agent_id, block_id=block.id)
267
+
268
+ return updated_memory
269
+
270
+
271
+ @router.delete("/{agent_id}/memory/block/{block_label}", response_model=Memory, operation_id="remove_agent_memory_block")
272
+ def remove_agent_memory_block(
273
+ agent_id: str,
274
+ # TODO should this be block_id, or the label?
275
+ # I think label is OK since it's user-friendly + guaranteed to be unique within a Memory object
276
+ block_label: str,
277
+ server: "SyncServer" = Depends(get_letta_server),
278
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
279
+ ):
280
+ """
281
+ Removes a memory block from an agent by unlnking it. If the block is not linked to any other agent, it is deleted.
282
+ """
283
+ actor = server.get_user_or_default(user_id=user_id)
284
+
285
+ # Unlink the block from the agent
286
+ updated_memory = server.unlink_block_from_agent_memory(user_id=actor.id, agent_id=agent_id, block_label=block_label)
287
+
288
+ return updated_memory
289
+
290
+
291
+ @router.patch("/{agent_id}/memory/limit", response_model=Memory, operation_id="update_agent_memory_limit")
292
+ def update_agent_memory_limit(
293
+ agent_id: str,
294
+ update_label: BlockLimitUpdate = Body(...),
295
+ server: "SyncServer" = Depends(get_letta_server),
296
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
297
+ ):
298
+ """
299
+ Update the limit of a block in an agent's memory.
300
+ """
301
+ actor = server.get_user_or_default(user_id=user_id)
302
+
303
+ memory = server.update_agent_memory_limit(
304
+ user_id=actor.id,
305
+ agent_id=agent_id,
306
+ block_label=update_label.label,
307
+ limit=update_label.limit,
308
+ )
309
+ return memory
310
+
311
+
228
312
  @router.get("/{agent_id}/memory/recall", response_model=RecallMemorySummary, operation_id="get_agent_recall_memory_summary")
229
313
  def get_agent_recall_memory_summary(
230
314
  agent_id: str,
@@ -0,0 +1,108 @@
1
+ from typing import List, Optional
2
+
3
+ from fastapi import APIRouter, Depends, Query
4
+
5
+ from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
6
+ from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate
7
+ from letta.schemas.sandbox_config import SandboxEnvironmentVariable as PydanticEnvVar
8
+ from letta.schemas.sandbox_config import (
9
+ SandboxEnvironmentVariableCreate,
10
+ SandboxEnvironmentVariableUpdate,
11
+ )
12
+ from letta.server.rest_api.utils import get_letta_server, get_user_id
13
+ from letta.server.server import SyncServer
14
+
15
+ router = APIRouter(prefix="/sandbox-config", tags=["sandbox-config"])
16
+
17
+
18
+ ### Sandbox Config Routes
19
+
20
+
21
+ @router.post("/", response_model=PydanticSandboxConfig)
22
+ def create_sandbox_config(
23
+ config_create: SandboxConfigCreate,
24
+ server: SyncServer = Depends(get_letta_server),
25
+ user_id: str = Depends(get_user_id),
26
+ ):
27
+ actor = server.get_user_or_default(user_id=user_id)
28
+
29
+ return server.sandbox_config_manager.create_or_update_sandbox_config(config_create, actor)
30
+
31
+
32
+ @router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig)
33
+ def update_sandbox_config(
34
+ sandbox_config_id: str,
35
+ config_update: SandboxConfigUpdate,
36
+ server: SyncServer = Depends(get_letta_server),
37
+ user_id: str = Depends(get_user_id),
38
+ ):
39
+ actor = server.get_user_or_default(user_id=user_id)
40
+ return server.sandbox_config_manager.update_sandbox_config(sandbox_config_id, config_update, actor)
41
+
42
+
43
+ @router.delete("/{sandbox_config_id}", status_code=204)
44
+ def delete_sandbox_config(
45
+ sandbox_config_id: str,
46
+ server: SyncServer = Depends(get_letta_server),
47
+ user_id: str = Depends(get_user_id),
48
+ ):
49
+ actor = server.get_user_or_default(user_id=user_id)
50
+ server.sandbox_config_manager.delete_sandbox_config(sandbox_config_id, actor)
51
+
52
+
53
+ @router.get("/", response_model=List[PydanticSandboxConfig])
54
+ def list_sandbox_configs(
55
+ limit: int = Query(1000, description="Number of results to return"),
56
+ cursor: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
57
+ server: SyncServer = Depends(get_letta_server),
58
+ user_id: str = Depends(get_user_id),
59
+ ):
60
+ actor = server.get_user_or_default(user_id=user_id)
61
+ return server.sandbox_config_manager.list_sandbox_configs(actor, limit=limit, cursor=cursor)
62
+
63
+
64
+ ### Sandbox Environment Variable Routes
65
+
66
+
67
+ @router.post("/{sandbox_config_id}/environment-variable", response_model=PydanticEnvVar)
68
+ def create_sandbox_env_var(
69
+ sandbox_config_id: str,
70
+ env_var_create: SandboxEnvironmentVariableCreate,
71
+ server: SyncServer = Depends(get_letta_server),
72
+ user_id: str = Depends(get_user_id),
73
+ ):
74
+ actor = server.get_user_or_default(user_id=user_id)
75
+ return server.sandbox_config_manager.create_sandbox_env_var(env_var_create, sandbox_config_id, actor)
76
+
77
+
78
+ @router.patch("/environment-variable/{env_var_id}", response_model=PydanticEnvVar)
79
+ def update_sandbox_env_var(
80
+ env_var_id: str,
81
+ env_var_update: SandboxEnvironmentVariableUpdate,
82
+ server: SyncServer = Depends(get_letta_server),
83
+ user_id: str = Depends(get_user_id),
84
+ ):
85
+ actor = server.get_user_or_default(user_id=user_id)
86
+ return server.sandbox_config_manager.update_sandbox_env_var(env_var_id, env_var_update, actor)
87
+
88
+
89
+ @router.delete("/environment-variable/{env_var_id}", status_code=204)
90
+ def delete_sandbox_env_var(
91
+ env_var_id: str,
92
+ server: SyncServer = Depends(get_letta_server),
93
+ user_id: str = Depends(get_user_id),
94
+ ):
95
+ actor = server.get_user_or_default(user_id=user_id)
96
+ server.sandbox_config_manager.delete_sandbox_env_var(env_var_id, actor)
97
+
98
+
99
+ @router.get("/{sandbox_config_id}/environment-variable", response_model=List[PydanticEnvVar])
100
+ def list_sandbox_env_vars(
101
+ sandbox_config_id: str,
102
+ limit: int = Query(1000, description="Number of results to return"),
103
+ cursor: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
104
+ server: SyncServer = Depends(get_letta_server),
105
+ user_id: str = Depends(get_user_id),
106
+ ):
107
+ actor = server.get_user_or_default(user_id=user_id)
108
+ return server.sandbox_config_manager.list_sandbox_env_vars(sandbox_config_id, actor, limit=limit, cursor=cursor)
@@ -2,7 +2,6 @@ from typing import List, Optional
2
2
 
3
3
  from fastapi import APIRouter, Body, Depends, Header, HTTPException
4
4
 
5
- from letta.orm.errors import NoResultFound
6
5
  from letta.schemas.tool import Tool, ToolCreate, ToolUpdate
7
6
  from letta.server.rest_api.utils import get_letta_server
8
7
  from letta.server.server import SyncServer
@@ -49,11 +48,10 @@ def get_tool_id(
49
48
  Get a tool ID by name
50
49
  """
51
50
  actor = server.get_user_or_default(user_id=user_id)
52
-
53
- try:
54
- tool = server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
51
+ tool = server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
52
+ if tool:
55
53
  return tool.id
56
- except NoResultFound:
54
+ else:
57
55
  raise HTTPException(status_code=404, detail=f"Tool with name {tool_name} and organization id {actor.organization_id} not found.")
58
56
 
59
57
 
@@ -5,6 +5,7 @@ import warnings
5
5
  from enum import Enum
6
6
  from typing import AsyncGenerator, Optional, Union
7
7
 
8
+ from fastapi import Header
8
9
  from pydantic import BaseModel
9
10
 
10
11
  from letta.schemas.usage import LettaUsageStatistics
@@ -84,5 +85,10 @@ def get_letta_server() -> SyncServer:
84
85
  return server
85
86
 
86
87
 
88
+ # Dependency to get user_id from headers
89
+ def get_user_id(user_id: Optional[str] = Header(None, alias="user_id")) -> Optional[str]:
90
+ return user_id
91
+
92
+
87
93
  def get_current_interface() -> StreamingServerInterface:
88
94
  return StreamingServerInterface