letta-nightly 0.5.4.dev20241122104229__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.
- letta/agent.py +23 -3
- letta/agent_store/db.py +1 -1
- letta/client/client.py +290 -0
- letta/constants.py +5 -14
- letta/functions/helpers.py +0 -4
- letta/functions/schema_generator.py +24 -4
- letta/local_llm/utils.py +6 -3
- letta/log.py +7 -9
- letta/orm/__init__.py +2 -0
- letta/orm/block.py +5 -2
- letta/orm/blocks_agents.py +29 -0
- letta/orm/mixins.py +8 -0
- letta/orm/organization.py +8 -1
- letta/orm/sandbox_config.py +56 -0
- letta/orm/sqlalchemy_base.py +9 -3
- letta/schemas/blocks_agents.py +32 -0
- letta/schemas/letta_base.py +9 -0
- letta/schemas/memory.py +9 -8
- letta/schemas/sandbox_config.py +114 -0
- letta/server/rest_api/routers/v1/__init__.py +4 -9
- letta/server/rest_api/routers/v1/sandbox_configs.py +108 -0
- letta/server/rest_api/routers/v1/tools.py +3 -5
- letta/server/rest_api/utils.py +6 -0
- letta/server/server.py +10 -5
- letta/services/block_manager.py +4 -2
- letta/services/blocks_agents_manager.py +84 -0
- letta/services/sandbox_config_manager.py +256 -0
- letta/services/tool_execution_sandbox.py +326 -0
- letta/services/tool_manager.py +10 -10
- letta/services/tool_sandbox_env/.gitkeep +0 -0
- letta/settings.py +4 -0
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/METADATA +3 -1
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/RECORD +36 -27
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.4.dev20241122104229.dist-info → letta_nightly-0.5.4.dev20241123104112.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")
|
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -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
|
-
|
|
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.")
|
letta/schemas/letta_base.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
244
|
+
current_value = str(agent_state.memory.get_block(label).value)
|
|
244
245
|
new_value = current_value + "\n" + str(content)
|
|
245
|
-
|
|
246
|
+
agent_state.memory.update_block_value(label=label, value=new_value)
|
|
246
247
|
return None
|
|
247
248
|
|
|
248
|
-
def core_memory_replace(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
letta/server/rest_api/utils.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
841
|
-
|
|
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
|
-
|
|
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]
|
letta/services/block_manager.py
CHANGED
|
@@ -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
|
|
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
|
|
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)
|