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.
- letta/agent.py +48 -25
- letta/agent_store/db.py +1 -1
- letta/client/client.py +361 -7
- letta/constants.py +5 -14
- letta/functions/helpers.py +5 -42
- letta/functions/schema_generator.py +24 -4
- letta/local_llm/utils.py +6 -3
- letta/log.py +7 -9
- letta/metadata.py +17 -4
- 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/block.py +15 -1
- letta/schemas/blocks_agents.py +32 -0
- letta/schemas/letta_base.py +9 -0
- letta/schemas/memory.py +42 -8
- letta/schemas/sandbox_config.py +114 -0
- letta/schemas/tool.py +2 -45
- letta/server/rest_api/routers/v1/__init__.py +4 -9
- letta/server/rest_api/routers/v1/agents.py +85 -1
- 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 +159 -12
- letta/services/block_manager.py +3 -1
- 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.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/METADATA +28 -27
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/RECORD +40 -31
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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(
|
|
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(
|
|
244
|
+
current_value = str(agent_state.memory.get_block(label).value)
|
|
211
245
|
new_value = current_value + "\n" + str(content)
|
|
212
|
-
|
|
246
|
+
agent_state.memory.update_block_value(label=label, value=new_value)
|
|
213
247
|
return None
|
|
214
248
|
|
|
215
|
-
def core_memory_replace(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|