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
letta/orm/organization.py CHANGED
@@ -2,12 +2,12 @@ from typing import TYPE_CHECKING, List
2
2
 
3
3
  from sqlalchemy.orm import Mapped, mapped_column, relationship
4
4
 
5
- from letta.orm.file import FileMetadata
6
5
  from letta.orm.sqlalchemy_base import SqlalchemyBase
7
6
  from letta.schemas.organization import Organization as PydanticOrganization
8
7
 
9
8
  if TYPE_CHECKING:
10
9
 
10
+ from letta.orm.file import FileMetadata
11
11
  from letta.orm.tool import Tool
12
12
  from letta.orm.user import User
13
13
 
@@ -27,6 +27,13 @@ class Organization(SqlalchemyBase):
27
27
  sources: Mapped[List["Source"]] = relationship("Source", back_populates="organization", cascade="all, delete-orphan")
28
28
  agents_tags: Mapped[List["AgentsTags"]] = relationship("AgentsTags", back_populates="organization", cascade="all, delete-orphan")
29
29
  files: Mapped[List["FileMetadata"]] = relationship("FileMetadata", back_populates="organization", cascade="all, delete-orphan")
30
+ sandbox_configs: Mapped[List["SandboxConfig"]] = relationship(
31
+ "SandboxConfig", back_populates="organization", cascade="all, delete-orphan"
32
+ )
33
+ sandbox_environment_variables: Mapped[List["SandboxEnvironmentVariable"]] = relationship(
34
+ "SandboxEnvironmentVariable", back_populates="organization", cascade="all, delete-orphan"
35
+ )
36
+
30
37
  # TODO: Map these relationships later when we actually make these models
31
38
  # below is just a suggestion
32
39
  # agents: Mapped[List["Agent"]] = relationship("Agent", back_populates="organization", cascade="all, delete-orphan")
@@ -0,0 +1,56 @@
1
+ from typing import TYPE_CHECKING, Dict, List, Optional
2
+
3
+ from sqlalchemy import JSON
4
+ from sqlalchemy import Enum as SqlEnum
5
+ from sqlalchemy import String, UniqueConstraint
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+
8
+ from letta.orm.mixins import OrganizationMixin, SandboxConfigMixin
9
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
10
+ from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
11
+ from letta.schemas.sandbox_config import (
12
+ SandboxEnvironmentVariable as PydanticSandboxEnvironmentVariable,
13
+ )
14
+ from letta.schemas.sandbox_config import SandboxType
15
+
16
+ if TYPE_CHECKING:
17
+ from letta.orm.organization import Organization
18
+
19
+
20
+ class SandboxConfig(SqlalchemyBase, OrganizationMixin):
21
+ """ORM model for sandbox configurations with JSON storage for arbitrary config data."""
22
+
23
+ __tablename__ = "sandbox_configs"
24
+ __pydantic_model__ = PydanticSandboxConfig
25
+
26
+ # For now, we only allow one type of sandbox config per organization
27
+ __table_args__ = (UniqueConstraint("type", "organization_id", name="uix_type_organization"),)
28
+
29
+ id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False)
30
+ type: Mapped[SandboxType] = mapped_column(SqlEnum(SandboxType), nullable=False, doc="The type of sandbox.")
31
+ config: Mapped[Dict] = mapped_column(JSON, nullable=False, doc="The JSON configuration data.")
32
+
33
+ # relationships
34
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="sandbox_configs")
35
+ sandbox_environment_variables: Mapped[List["SandboxEnvironmentVariable"]] = relationship(
36
+ "SandboxEnvironmentVariable", back_populates="sandbox_config", cascade="all, delete-orphan"
37
+ )
38
+
39
+
40
+ class SandboxEnvironmentVariable(SqlalchemyBase, OrganizationMixin, SandboxConfigMixin):
41
+ """ORM model for environment variables associated with sandboxes."""
42
+
43
+ __tablename__ = "sandbox_environment_variables"
44
+ __pydantic_model__ = PydanticSandboxEnvironmentVariable
45
+
46
+ # We cannot have duplicate key names in the same sandbox, the env var would get overwritten
47
+ __table_args__ = (UniqueConstraint("key", "sandbox_config_id", name="uix_key_sandbox_config"),)
48
+
49
+ id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False)
50
+ key: Mapped[str] = mapped_column(String, nullable=False, doc="The name of the environment variable.")
51
+ value: Mapped[str] = mapped_column(String, nullable=False, doc="The value of the environment variable.")
52
+ description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="An optional description of the environment variable.")
53
+
54
+ # relationships
55
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="sandbox_environment_variables")
56
+ sandbox_config: Mapped["SandboxConfig"] = relationship("SandboxConfig", back_populates="sandbox_environment_variables")
@@ -11,7 +11,6 @@ if TYPE_CHECKING:
11
11
  from pydantic import BaseModel
12
12
  from sqlalchemy.orm import Session
13
13
 
14
- # from letta.orm.user import User
15
14
 
16
15
  logger = get_logger(__name__)
17
16
 
@@ -28,6 +27,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
28
27
  cls, *, db_session: "Session", cursor: Optional[str] = None, limit: Optional[int] = 50, **kwargs
29
28
  ) -> List[Type["SqlalchemyBase"]]:
30
29
  """List records with optional cursor (for pagination) and limit."""
30
+ logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
31
31
  with db_session as session:
32
32
  # Start with the base query filtered by kwargs
33
33
  query = select(cls).filter_by(**kwargs)
@@ -67,6 +67,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
67
67
  Raises:
68
68
  NoResultFound: if the object is not found
69
69
  """
70
+ logger.debug(f"Reading {cls.__name__} with ID: {identifier} with actor={actor}")
71
+
70
72
  # Start the query
71
73
  query = select(cls)
72
74
  # Collect query conditions for better error reporting
@@ -96,6 +98,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
96
98
  raise NoResultFound(f"{cls.__name__} not found with {conditions_str}")
97
99
 
98
100
  def create(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
101
+ logger.debug(f"Creating {self.__class__.__name__} with ID: {self.id} with actor={actor}")
102
+
99
103
  if actor:
100
104
  self._set_created_and_updated_by_fields(actor.id)
101
105
 
@@ -106,6 +110,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
106
110
  return self
107
111
 
108
112
  def delete(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
113
+ logger.debug(f"Soft deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
114
+
109
115
  if actor:
110
116
  self._set_created_and_updated_by_fields(actor.id)
111
117
 
@@ -114,8 +120,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
114
120
 
115
121
  def hard_delete(self, db_session: "Session", actor: Optional["User"] = None) -> None:
116
122
  """Permanently removes the record from the database."""
117
- if actor:
118
- logger.info(f"User {actor.id} requested hard deletion of {self.__class__.__name__} with ID {self.id}")
123
+ logger.debug(f"Hard deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
119
124
 
120
125
  with db_session as session:
121
126
  try:
@@ -129,6 +134,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
129
134
  logger.info(f"{self.__class__.__name__} with ID {self.id} successfully hard deleted")
130
135
 
131
136
  def update(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
137
+ logger.debug(f"Updating {self.__class__.__name__} with ID: {self.id} with actor={actor}")
132
138
  if actor:
133
139
  self._set_created_and_updated_by_fields(actor.id)
134
140
 
@@ -0,0 +1,32 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from pydantic import Field
5
+
6
+ from letta.schemas.letta_base import LettaBase
7
+
8
+
9
+ class BlocksAgentsBase(LettaBase):
10
+ __id_prefix__ = "blocks_agents"
11
+
12
+
13
+ class BlocksAgents(BlocksAgentsBase):
14
+ """
15
+ Schema representing the relationship between blocks and agents.
16
+
17
+ Parameters:
18
+ agent_id (str): The ID of the associated agent.
19
+ block_id (str): The ID of the associated block.
20
+ block_label (str): The label of the block.
21
+ created_at (datetime): The date this relationship was created.
22
+ updated_at (datetime): The date this relationship was last updated.
23
+ is_deleted (bool): Whether this block-agent relationship is deleted or not.
24
+ """
25
+
26
+ id: str = BlocksAgentsBase.generate_id_field()
27
+ agent_id: str = Field(..., description="The ID of the associated agent.")
28
+ block_id: str = Field(..., description="The ID of the associated block.")
29
+ block_label: str = Field(..., description="The label of the block.")
30
+ created_at: Optional[datetime] = Field(None, description="The creation date of the association.")
31
+ updated_at: Optional[datetime] = Field(None, description="The update date of the association.")
32
+ is_deleted: bool = Field(False, description="Whether this block-agent relationship is deleted or not.")
@@ -1,4 +1,5 @@
1
1
  import uuid
2
+ from datetime import datetime
2
3
  from logging import getLogger
3
4
  from typing import Optional
4
5
  from uuid import UUID
@@ -80,3 +81,11 @@ class LettaBase(BaseModel):
80
81
  logger.warning(f"Bare UUIDs are deprecated, please use the full prefixed id ({cls.__id_prefix__})!")
81
82
  return f"{cls.__id_prefix__}-{v}"
82
83
  return v
84
+
85
+
86
+ class OrmMetadataBase(LettaBase):
87
+ # metadata fields
88
+ created_by_id: Optional[str] = Field(None, description="The id of the user that made this object.")
89
+ last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this object.")
90
+ created_at: Optional[datetime] = Field(None, description="The timestamp when the object was created.")
91
+ updated_at: Optional[datetime] = Field(None, description="The timestamp when the object was last updated.")
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
@@ -229,7 +230,7 @@ class BasicBlockMemory(Memory):
229
230
  assert block.label is not None and block.label != "", "each existing chat block must have a name"
230
231
  self.link_block(block=block)
231
232
 
232
- 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
233
234
  """
234
235
  Append to the contents of core memory.
235
236
 
@@ -240,12 +241,12 @@ class BasicBlockMemory(Memory):
240
241
  Returns:
241
242
  Optional[str]: None is always returned as this function does not produce a response.
242
243
  """
243
- current_value = str(self.memory.get_block(label).value)
244
+ current_value = str(agent_state.memory.get_block(label).value)
244
245
  new_value = current_value + "\n" + str(content)
245
- self.memory.update_block_value(label=label, value=new_value)
246
+ agent_state.memory.update_block_value(label=label, value=new_value)
246
247
  return None
247
248
 
248
- 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
249
250
  """
250
251
  Replace the contents of core memory. To delete memories, use an empty string for new_content.
251
252
 
@@ -257,11 +258,11 @@ class BasicBlockMemory(Memory):
257
258
  Returns:
258
259
  Optional[str]: None is always returned as this function does not produce a response.
259
260
  """
260
- current_value = str(self.memory.get_block(label).value)
261
+ current_value = str(agent_state.memory.get_block(label).value)
261
262
  if old_content not in current_value:
262
263
  raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
263
264
  new_value = current_value.replace(str(old_content), str(new_content))
264
- self.memory.update_block_value(label=label, value=new_value)
265
+ agent_state.memory.update_block_value(label=label, value=new_value)
265
266
  return None
266
267
 
267
268
 
@@ -270,7 +271,7 @@ class ChatMemory(BasicBlockMemory):
270
271
  ChatMemory initializes a BaseChatMemory with two default blocks, `human` and `persona`.
271
272
  """
272
273
 
273
- 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):
274
275
  """
275
276
  Initialize the ChatMemory object with a persona and human string.
276
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.")
@@ -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]
@@ -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
letta/server/server.py CHANGED
@@ -77,7 +77,9 @@ from letta.schemas.usage import LettaUsageStatistics
77
77
  from letta.schemas.user import User
78
78
  from letta.services.agents_tags_manager import AgentsTagsManager
79
79
  from letta.services.block_manager import BlockManager
80
+ from letta.services.blocks_agents_manager import BlocksAgentsManager
80
81
  from letta.services.organization_manager import OrganizationManager
82
+ from letta.services.sandbox_config_manager import SandboxConfigManager
81
83
  from letta.services.source_manager import SourceManager
82
84
  from letta.services.tool_manager import ToolManager
83
85
  from letta.services.user_manager import UserManager
@@ -247,6 +249,8 @@ class SyncServer(Server):
247
249
  self.block_manager = BlockManager()
248
250
  self.source_manager = SourceManager()
249
251
  self.agents_tags_manager = AgentsTagsManager()
252
+ self.blocks_agents_manager = BlocksAgentsManager()
253
+ self.sandbox_config_manager = SandboxConfigManager(tool_settings)
250
254
 
251
255
  # Make default user and org
252
256
  if init_with_default_org_and_user:
@@ -381,10 +385,11 @@ class SyncServer(Server):
381
385
  tool_objs = []
382
386
  for name in agent_state.tools:
383
387
  # TODO: This should be a hard failure, but for migration reasons, we patch it for now
384
- try:
388
+ tool_obj = self.tool_manager.get_tool_by_name(tool_name=name, actor=actor)
389
+ if tool_obj:
385
390
  tool_obj = self.tool_manager.get_tool_by_name(tool_name=name, actor=actor)
386
391
  tool_objs.append(tool_obj)
387
- except NoResultFound:
392
+ else:
388
393
  warnings.warn(f"Tried to retrieve a tool with name {name} from the agent_state, but does not exist in tool db.")
389
394
 
390
395
  # set agent_state tools to only the names of the available tools
@@ -837,10 +842,10 @@ class SyncServer(Server):
837
842
  tool_objs = []
838
843
  if request.tools:
839
844
  for tool_name in request.tools:
840
- try:
841
- tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
845
+ tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
846
+ if tool_obj:
842
847
  tool_objs.append(tool_obj)
843
- except NoResultFound:
848
+ else:
844
849
  warnings.warn(f"Attempted to add a nonexistent tool {tool_name} to agent {request.name}, skipping.")
845
850
  # reset the request.tools to only valid tools
846
851
  request.tools = [t.name for t in tool_objs]
@@ -28,13 +28,15 @@ class BlockManager:
28
28
  self.update_block(block.id, update_data, actor)
29
29
  else:
30
30
  with self.session_maker() as session:
31
+ # Always write the organization_id
32
+ block.organization_id = actor.organization_id
31
33
  data = block.model_dump(exclude_none=True)
32
- block = BlockModel(**data, organization_id=actor.organization_id)
34
+ block = BlockModel(**data)
33
35
  block.create(session, actor=actor)
34
36
  return block.to_pydantic()
35
37
 
36
38
  @enforce_types
37
- def update_block(self, block_id: str, block_update: BlockUpdate, actor: PydanticUser, limit: Optional[int] = None) -> PydanticBlock:
39
+ def update_block(self, block_id: str, block_update: BlockUpdate, actor: PydanticUser) -> PydanticBlock:
38
40
  """Update a block by its ID with the given BlockUpdate object."""
39
41
  with self.session_maker() as session:
40
42
  block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)