letta-nightly 0.5.2.dev20241119104253__py3-none-any.whl → 0.5.3.dev20241120010849__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/__init__.py +1 -1
- letta/agent.py +11 -6
- letta/agent_store/db.py +4 -3
- letta/cli/cli.py +2 -1
- letta/client/client.py +33 -30
- letta/constants.py +3 -0
- letta/metadata.py +1 -155
- letta/o1_agent.py +3 -1
- letta/orm/__init__.py +1 -0
- letta/orm/block.py +44 -0
- letta/orm/organization.py +1 -0
- letta/schemas/block.py +31 -26
- letta/schemas/letta_base.py +1 -1
- letta/server/rest_api/routers/v1/blocks.py +18 -22
- letta/server/rest_api/routers/v1/sources.py +9 -3
- letta/server/server.py +7 -84
- letta/services/block_manager.py +103 -0
- letta/utils.py +39 -0
- {letta_nightly-0.5.2.dev20241119104253.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/METADATA +2 -1
- {letta_nightly-0.5.2.dev20241119104253.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/RECORD +23 -21
- {letta_nightly-0.5.2.dev20241119104253.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.2.dev20241119104253.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.2.dev20241119104253.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/entry_points.txt +0 -0
letta/schemas/block.py
CHANGED
|
@@ -14,36 +14,30 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
14
14
|
__id_prefix__ = "block"
|
|
15
15
|
|
|
16
16
|
# data value
|
|
17
|
-
value:
|
|
17
|
+
value: str = Field(..., description="Value of the block.")
|
|
18
18
|
limit: int = Field(2000, description="Character limit of the block.")
|
|
19
19
|
|
|
20
20
|
# template data (optional)
|
|
21
21
|
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
|
|
22
|
-
|
|
22
|
+
is_template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
|
|
23
23
|
|
|
24
24
|
# context window label
|
|
25
|
-
label: str = Field(None, description="Label of the block (e.g. 'human', 'persona') in the context window.")
|
|
25
|
+
label: Optional[str] = Field(None, description="Label of the block (e.g. 'human', 'persona') in the context window.")
|
|
26
26
|
|
|
27
27
|
# metadata
|
|
28
28
|
description: Optional[str] = Field(None, description="Description of the block.")
|
|
29
29
|
metadata_: Optional[dict] = Field({}, description="Metadata of the block.")
|
|
30
30
|
|
|
31
|
-
# associated user/agent
|
|
32
|
-
user_id: Optional[str] = Field(None, description="The unique identifier of the user associated with the block.")
|
|
33
|
-
|
|
34
31
|
@model_validator(mode="after")
|
|
35
32
|
def verify_char_limit(self) -> Self:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
except AssertionError:
|
|
39
|
-
error_msg = f"Edit failed: Exceeds {self.limit} character limit (requested {len(self)}) - {str(self)}."
|
|
33
|
+
if len(self.value) > self.limit:
|
|
34
|
+
error_msg = f"Edit failed: Exceeds {self.limit} character limit (requested {len(self.value)}) - {str(self)}."
|
|
40
35
|
raise ValueError(error_msg)
|
|
41
|
-
|
|
42
|
-
raise e
|
|
36
|
+
|
|
43
37
|
return self
|
|
44
38
|
|
|
45
|
-
def __len__(self):
|
|
46
|
-
|
|
39
|
+
# def __len__(self):
|
|
40
|
+
# return len(self.value)
|
|
47
41
|
|
|
48
42
|
def __setattr__(self, name, value):
|
|
49
43
|
"""Run validation if self.value is updated"""
|
|
@@ -52,6 +46,9 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
52
46
|
# run validation
|
|
53
47
|
self.__class__.model_validate(self.model_dump(exclude_unset=True))
|
|
54
48
|
|
|
49
|
+
class Config:
|
|
50
|
+
extra = "ignore" # Ignores extra fields
|
|
51
|
+
|
|
55
52
|
|
|
56
53
|
class Block(BaseBlock):
|
|
57
54
|
"""
|
|
@@ -61,15 +58,22 @@ class Block(BaseBlock):
|
|
|
61
58
|
label (str): The label of the block (e.g. 'human', 'persona'). This defines a category for the block.
|
|
62
59
|
value (str): The value of the block. This is the string that is represented in the context window.
|
|
63
60
|
limit (int): The character limit of the block.
|
|
61
|
+
is_template (bool): Whether the block is a template (e.g. saved human/persona options). Non-template blocks are not stored in the database and are ephemeral, while templated blocks are stored in the database.
|
|
62
|
+
label (str): The label of the block (e.g. 'human', 'persona'). This defines a category for the block.
|
|
64
63
|
template_name (str): The name of the block template (if it is a template).
|
|
65
|
-
template (bool): Whether the block is a template (e.g. saved human/persona options). Non-template blocks are not stored in the database and are ephemeral, while templated blocks are stored in the database.
|
|
66
64
|
description (str): Description of the block.
|
|
67
65
|
metadata_ (Dict): Metadata of the block.
|
|
68
66
|
user_id (str): The unique identifier of the user associated with the block.
|
|
69
67
|
"""
|
|
70
68
|
|
|
71
69
|
id: str = BaseBlock.generate_id_field()
|
|
72
|
-
|
|
70
|
+
|
|
71
|
+
# associated user/agent
|
|
72
|
+
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization associated with the block.")
|
|
73
|
+
|
|
74
|
+
# default orm fields
|
|
75
|
+
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Block.")
|
|
76
|
+
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that last updated this Block.")
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
class Human(Block):
|
|
@@ -84,41 +88,42 @@ class Persona(Block):
|
|
|
84
88
|
label: str = "persona"
|
|
85
89
|
|
|
86
90
|
|
|
87
|
-
class
|
|
91
|
+
class BlockCreate(BaseBlock):
|
|
88
92
|
"""Create a block"""
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
is_template: bool = True
|
|
91
95
|
label: str = Field(..., description="Label of the block.")
|
|
92
96
|
|
|
93
97
|
|
|
94
|
-
class CreatePersona(
|
|
98
|
+
class CreatePersona(BlockCreate):
|
|
95
99
|
"""Create a persona block"""
|
|
96
100
|
|
|
97
|
-
template: bool = True
|
|
98
101
|
label: str = "persona"
|
|
99
102
|
|
|
100
103
|
|
|
101
|
-
class CreateHuman(
|
|
104
|
+
class CreateHuman(BlockCreate):
|
|
102
105
|
"""Create a human block"""
|
|
103
106
|
|
|
104
|
-
template: bool = True
|
|
105
107
|
label: str = "human"
|
|
106
108
|
|
|
107
109
|
|
|
108
|
-
class
|
|
110
|
+
class BlockUpdate(BaseBlock):
|
|
109
111
|
"""Update a block"""
|
|
110
112
|
|
|
111
|
-
id: str = Field(..., description="The unique identifier of the block.")
|
|
112
113
|
limit: Optional[int] = Field(2000, description="Character limit of the block.")
|
|
114
|
+
value: Optional[str] = Field(None, description="Value of the block.")
|
|
115
|
+
|
|
116
|
+
class Config:
|
|
117
|
+
extra = "ignore" # Ignores extra fields
|
|
113
118
|
|
|
114
119
|
|
|
115
|
-
class UpdatePersona(
|
|
120
|
+
class UpdatePersona(BlockUpdate):
|
|
116
121
|
"""Update a persona block"""
|
|
117
122
|
|
|
118
123
|
label: str = "persona"
|
|
119
124
|
|
|
120
125
|
|
|
121
|
-
class UpdateHuman(
|
|
126
|
+
class UpdateHuman(BlockUpdate):
|
|
122
127
|
"""Update a human block"""
|
|
123
128
|
|
|
124
129
|
label: str = "human"
|
letta/schemas/letta_base.py
CHANGED
|
@@ -77,6 +77,6 @@ class LettaBase(BaseModel):
|
|
|
77
77
|
"""
|
|
78
78
|
_ = values # for SCA
|
|
79
79
|
if isinstance(v, UUID):
|
|
80
|
-
logger.warning("Bare UUIDs are deprecated, please use the full prefixed id!")
|
|
80
|
+
logger.warning(f"Bare UUIDs are deprecated, please use the full prefixed id ({cls.__id_prefix__})!")
|
|
81
81
|
return f"{cls.__id_prefix__}-{v}"
|
|
82
82
|
return v
|
|
@@ -2,7 +2,8 @@ from typing import TYPE_CHECKING, List, Optional
|
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
|
4
4
|
|
|
5
|
-
from letta.
|
|
5
|
+
from letta.orm.errors import NoResultFound
|
|
6
|
+
from letta.schemas.block import Block, BlockCreate, BlockUpdate
|
|
6
7
|
from letta.server.rest_api.utils import get_letta_server
|
|
7
8
|
from letta.server.server import SyncServer
|
|
8
9
|
|
|
@@ -22,54 +23,49 @@ def list_blocks(
|
|
|
22
23
|
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
23
24
|
):
|
|
24
25
|
actor = server.get_user_or_default(user_id=user_id)
|
|
25
|
-
|
|
26
|
-
blocks = server.get_blocks(user_id=actor.id, label=label, template=templates_only, name=name)
|
|
27
|
-
if blocks is None:
|
|
28
|
-
return []
|
|
29
|
-
return blocks
|
|
26
|
+
return server.block_manager.get_blocks(actor=actor, label=label, is_template=templates_only, template_name=name)
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
@router.post("/", response_model=Block, operation_id="create_memory_block")
|
|
33
30
|
def create_block(
|
|
34
|
-
create_block:
|
|
31
|
+
create_block: BlockCreate = Body(...),
|
|
35
32
|
server: SyncServer = Depends(get_letta_server),
|
|
36
33
|
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
37
34
|
):
|
|
38
35
|
actor = server.get_user_or_default(user_id=user_id)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return server.create_block(user_id=actor.id, request=create_block)
|
|
36
|
+
block = Block(**create_block.model_dump())
|
|
37
|
+
return server.block_manager.create_or_update_block(actor=actor, block=block)
|
|
42
38
|
|
|
43
39
|
|
|
44
40
|
@router.patch("/{block_id}", response_model=Block, operation_id="update_memory_block")
|
|
45
41
|
def update_block(
|
|
46
42
|
block_id: str,
|
|
47
|
-
updated_block:
|
|
43
|
+
updated_block: BlockUpdate = Body(...),
|
|
48
44
|
server: SyncServer = Depends(get_letta_server),
|
|
45
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
49
46
|
):
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
updated_block.id = block_id
|
|
53
|
-
return server.update_block(request=updated_block)
|
|
47
|
+
actor = server.get_user_or_default(user_id=user_id)
|
|
48
|
+
return server.block_manager.update_block(block_id=block_id, block_update=updated_block, actor=actor)
|
|
54
49
|
|
|
55
50
|
|
|
56
|
-
# TODO: delete should not return anything
|
|
57
51
|
@router.delete("/{block_id}", response_model=Block, operation_id="delete_memory_block")
|
|
58
52
|
def delete_block(
|
|
59
53
|
block_id: str,
|
|
60
54
|
server: SyncServer = Depends(get_letta_server),
|
|
55
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
61
56
|
):
|
|
62
|
-
|
|
63
|
-
return server.delete_block(block_id=block_id)
|
|
57
|
+
actor = server.get_user_or_default(user_id=user_id)
|
|
58
|
+
return server.block_manager.delete_block(block_id=block_id, actor=actor)
|
|
64
59
|
|
|
65
60
|
|
|
66
61
|
@router.get("/{block_id}", response_model=Block, operation_id="get_memory_block")
|
|
67
62
|
def get_block(
|
|
68
63
|
block_id: str,
|
|
69
64
|
server: SyncServer = Depends(get_letta_server),
|
|
65
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
70
66
|
):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
actor = server.get_user_or_default(user_id=user_id)
|
|
68
|
+
try:
|
|
69
|
+
return server.block_manager.get_block_by_id(block_id=block_id, actor=actor)
|
|
70
|
+
except NoResultFound:
|
|
74
71
|
raise HTTPException(status_code=404, detail="Block not found")
|
|
75
|
-
return block
|
|
@@ -18,6 +18,7 @@ from letta.schemas.passage import Passage
|
|
|
18
18
|
from letta.schemas.source import Source, SourceCreate, SourceUpdate
|
|
19
19
|
from letta.server.rest_api.utils import get_letta_server
|
|
20
20
|
from letta.server.server import SyncServer
|
|
21
|
+
from letta.utils import sanitize_filename
|
|
21
22
|
|
|
22
23
|
# These can be forward refs, but because Fastapi needs them at runtime the must be imported normally
|
|
23
24
|
|
|
@@ -170,7 +171,7 @@ def upload_file_to_source(
|
|
|
170
171
|
server.ms.create_job(job)
|
|
171
172
|
|
|
172
173
|
# create background task
|
|
173
|
-
background_tasks.add_task(load_file_to_source_async, server, source_id=source.id, job_id=job.id,
|
|
174
|
+
background_tasks.add_task(load_file_to_source_async, server, source_id=source.id, file=file, job_id=job.id, bytes=bytes)
|
|
174
175
|
|
|
175
176
|
# return job information
|
|
176
177
|
job = server.ms.get_job(job_id=job_id)
|
|
@@ -227,10 +228,15 @@ def delete_file_from_source(
|
|
|
227
228
|
|
|
228
229
|
|
|
229
230
|
def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, file: UploadFile, bytes: bytes):
|
|
230
|
-
#
|
|
231
|
+
# Create a temporary directory (deleted after the context manager exits)
|
|
231
232
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
|
232
|
-
|
|
233
|
+
# Sanitize the filename
|
|
234
|
+
sanitized_filename = sanitize_filename(file.filename)
|
|
235
|
+
file_path = os.path.join(tmpdirname, sanitized_filename)
|
|
236
|
+
|
|
237
|
+
# Write the file to the sanitized path
|
|
233
238
|
with open(file_path, "wb") as buffer:
|
|
234
239
|
buffer.write(bytes)
|
|
235
240
|
|
|
241
|
+
# Pass the file to load_file_to_source
|
|
236
242
|
server.load_file_to_source(source_id, file_path, job_id)
|
letta/server/server.py
CHANGED
|
@@ -55,13 +55,6 @@ from letta.providers import (
|
|
|
55
55
|
)
|
|
56
56
|
from letta.schemas.agent import AgentState, AgentType, CreateAgent, UpdateAgentState
|
|
57
57
|
from letta.schemas.api_key import APIKey, APIKeyCreate
|
|
58
|
-
from letta.schemas.block import (
|
|
59
|
-
Block,
|
|
60
|
-
CreateBlock,
|
|
61
|
-
CreateHuman,
|
|
62
|
-
CreatePersona,
|
|
63
|
-
UpdateBlock,
|
|
64
|
-
)
|
|
65
58
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
66
59
|
|
|
67
60
|
# openai schemas
|
|
@@ -83,6 +76,7 @@ from letta.schemas.tool import Tool, ToolCreate
|
|
|
83
76
|
from letta.schemas.usage import LettaUsageStatistics
|
|
84
77
|
from letta.schemas.user import User
|
|
85
78
|
from letta.services.agents_tags_manager import AgentsTagsManager
|
|
79
|
+
from letta.services.block_manager import BlockManager
|
|
86
80
|
from letta.services.organization_manager import OrganizationManager
|
|
87
81
|
from letta.services.source_manager import SourceManager
|
|
88
82
|
from letta.services.tool_manager import ToolManager
|
|
@@ -250,6 +244,7 @@ class SyncServer(Server):
|
|
|
250
244
|
self.organization_manager = OrganizationManager()
|
|
251
245
|
self.user_manager = UserManager()
|
|
252
246
|
self.tool_manager = ToolManager()
|
|
247
|
+
self.block_manager = BlockManager()
|
|
253
248
|
self.source_manager = SourceManager()
|
|
254
249
|
self.agents_tags_manager = AgentsTagsManager()
|
|
255
250
|
|
|
@@ -257,7 +252,7 @@ class SyncServer(Server):
|
|
|
257
252
|
if init_with_default_org_and_user:
|
|
258
253
|
self.default_org = self.organization_manager.create_default_organization()
|
|
259
254
|
self.default_user = self.user_manager.create_default_user()
|
|
260
|
-
self.add_default_blocks(self.default_user
|
|
255
|
+
self.block_manager.add_default_blocks(actor=self.default_user)
|
|
261
256
|
self.tool_manager.add_base_tools(actor=self.default_user)
|
|
262
257
|
|
|
263
258
|
# If there is a default org/user
|
|
@@ -333,15 +328,6 @@ class SyncServer(Server):
|
|
|
333
328
|
)
|
|
334
329
|
)
|
|
335
330
|
|
|
336
|
-
def save_agents(self):
|
|
337
|
-
"""Saves all the agents that are in the in-memory object store"""
|
|
338
|
-
for agent_d in self.active_agents:
|
|
339
|
-
try:
|
|
340
|
-
save_agent(agent_d["agent"], self.ms)
|
|
341
|
-
logger.debug(f"Saved agent {agent_d['agent_id']}")
|
|
342
|
-
except Exception as e:
|
|
343
|
-
logger.exception(f"Error occurred while trying to save agent {agent_d['agent_id']}:\n{e}")
|
|
344
|
-
|
|
345
331
|
def _get_agent(self, user_id: str, agent_id: str) -> Union[Agent, None]:
|
|
346
332
|
"""Get the agent object from the in-memory object store"""
|
|
347
333
|
for d in self.active_agents:
|
|
@@ -399,9 +385,9 @@ class SyncServer(Server):
|
|
|
399
385
|
assert isinstance(agent_state.memory, Memory)
|
|
400
386
|
|
|
401
387
|
if agent_state.agent_type == AgentType.memgpt_agent:
|
|
402
|
-
letta_agent = Agent(agent_state=agent_state, interface=interface, tools=tool_objs)
|
|
388
|
+
letta_agent = Agent(agent_state=agent_state, interface=interface, tools=tool_objs, user=actor)
|
|
403
389
|
elif agent_state.agent_type == AgentType.o1_agent:
|
|
404
|
-
letta_agent = O1Agent(agent_state=agent_state, interface=interface, tools=tool_objs)
|
|
390
|
+
letta_agent = O1Agent(agent_state=agent_state, interface=interface, tools=tool_objs, user=actor)
|
|
405
391
|
else:
|
|
406
392
|
raise NotImplementedError("Not a supported agent type")
|
|
407
393
|
|
|
@@ -884,6 +870,7 @@ class SyncServer(Server):
|
|
|
884
870
|
first_message_verify_mono=(
|
|
885
871
|
True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
|
|
886
872
|
),
|
|
873
|
+
user=actor,
|
|
887
874
|
initial_message_sequence=request.initial_message_sequence,
|
|
888
875
|
)
|
|
889
876
|
elif request.agent_type == AgentType.o1_agent:
|
|
@@ -895,6 +882,7 @@ class SyncServer(Server):
|
|
|
895
882
|
first_message_verify_mono=(
|
|
896
883
|
True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
|
|
897
884
|
),
|
|
885
|
+
user=actor,
|
|
898
886
|
)
|
|
899
887
|
# rebuilding agent memory on agent create in case shared memory blocks
|
|
900
888
|
# were specified in the new agent's memory config. we're doing this for two reasons:
|
|
@@ -1130,56 +1118,6 @@ class SyncServer(Server):
|
|
|
1130
1118
|
|
|
1131
1119
|
return [self.get_agent_state(user_id=user.id, agent_id=agent_id) for agent_id in agent_ids]
|
|
1132
1120
|
|
|
1133
|
-
def get_blocks(
|
|
1134
|
-
self,
|
|
1135
|
-
user_id: Optional[str] = None,
|
|
1136
|
-
label: Optional[str] = None,
|
|
1137
|
-
template: Optional[bool] = None,
|
|
1138
|
-
name: Optional[str] = None,
|
|
1139
|
-
id: Optional[str] = None,
|
|
1140
|
-
) -> Optional[List[Block]]:
|
|
1141
|
-
|
|
1142
|
-
return self.ms.get_blocks(user_id=user_id, label=label, template=template, template_name=name, id=id)
|
|
1143
|
-
|
|
1144
|
-
def get_block(self, block_id: str):
|
|
1145
|
-
|
|
1146
|
-
blocks = self.get_blocks(id=block_id)
|
|
1147
|
-
if blocks is None or len(blocks) == 0:
|
|
1148
|
-
raise ValueError("Block does not exist")
|
|
1149
|
-
if len(blocks) > 1:
|
|
1150
|
-
raise ValueError("Multiple blocks with the same id")
|
|
1151
|
-
return blocks[0]
|
|
1152
|
-
|
|
1153
|
-
def create_block(self, request: CreateBlock, user_id: str, update: bool = False) -> Block:
|
|
1154
|
-
existing_blocks = self.ms.get_blocks(
|
|
1155
|
-
template_name=request.template_name, user_id=user_id, template=request.template, label=request.label
|
|
1156
|
-
)
|
|
1157
|
-
|
|
1158
|
-
# for templates, update existing block template if exists
|
|
1159
|
-
if existing_blocks is not None and request.template:
|
|
1160
|
-
existing_block = existing_blocks[0]
|
|
1161
|
-
assert len(existing_blocks) == 1
|
|
1162
|
-
if update:
|
|
1163
|
-
return self.update_block(UpdateBlock(id=existing_block.id, **vars(request)))
|
|
1164
|
-
else:
|
|
1165
|
-
raise ValueError(f"Block with name {request.template_name} already exists")
|
|
1166
|
-
block = Block(**vars(request))
|
|
1167
|
-
self.ms.create_block(block)
|
|
1168
|
-
return block
|
|
1169
|
-
|
|
1170
|
-
def update_block(self, request: UpdateBlock) -> Block:
|
|
1171
|
-
block = self.get_block(request.id)
|
|
1172
|
-
block.limit = request.limit if request.limit is not None else block.limit
|
|
1173
|
-
block.value = request.value if request.value is not None else block.value
|
|
1174
|
-
block.template_name = request.template_name if request.template_name is not None else block.template_name
|
|
1175
|
-
self.ms.update_block(block=block)
|
|
1176
|
-
return self.ms.get_block(block_id=request.id)
|
|
1177
|
-
|
|
1178
|
-
def delete_block(self, block_id: str):
|
|
1179
|
-
block = self.get_block(block_id)
|
|
1180
|
-
self.ms.delete_block(block_id)
|
|
1181
|
-
return block
|
|
1182
|
-
|
|
1183
1121
|
# convert name->id
|
|
1184
1122
|
|
|
1185
1123
|
def get_agent_id(self, name: str, user_id: str):
|
|
@@ -1790,21 +1728,6 @@ class SyncServer(Server):
|
|
|
1790
1728
|
|
|
1791
1729
|
return success
|
|
1792
1730
|
|
|
1793
|
-
def add_default_blocks(self, user_id: str):
|
|
1794
|
-
from letta.utils import list_human_files, list_persona_files
|
|
1795
|
-
|
|
1796
|
-
assert user_id is not None, "User ID must be provided"
|
|
1797
|
-
|
|
1798
|
-
for persona_file in list_persona_files():
|
|
1799
|
-
text = open(persona_file, "r", encoding="utf-8").read()
|
|
1800
|
-
name = os.path.basename(persona_file).replace(".txt", "")
|
|
1801
|
-
self.create_block(CreatePersona(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
|
|
1802
|
-
|
|
1803
|
-
for human_file in list_human_files():
|
|
1804
|
-
text = open(human_file, "r", encoding="utf-8").read()
|
|
1805
|
-
name = os.path.basename(human_file).replace(".txt", "")
|
|
1806
|
-
self.create_block(CreateHuman(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
|
|
1807
|
-
|
|
1808
1731
|
def get_agent_message(self, agent_id: str, message_id: str) -> Optional[Message]:
|
|
1809
1732
|
"""Get a single message from the agent's memory"""
|
|
1810
1733
|
# Get the agent object (loaded in memory)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from letta.orm.block import Block as BlockModel
|
|
5
|
+
from letta.orm.errors import NoResultFound
|
|
6
|
+
from letta.schemas.block import Block
|
|
7
|
+
from letta.schemas.block import Block as PydanticBlock
|
|
8
|
+
from letta.schemas.block import BlockUpdate, Human, Persona
|
|
9
|
+
from letta.schemas.user import User as PydanticUser
|
|
10
|
+
from letta.utils import enforce_types, list_human_files, list_persona_files
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BlockManager:
|
|
14
|
+
"""Manager class to handle business logic related to Blocks."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
# Fetching the db_context similarly as in ToolManager
|
|
18
|
+
from letta.server.server import db_context
|
|
19
|
+
|
|
20
|
+
self.session_maker = db_context
|
|
21
|
+
|
|
22
|
+
@enforce_types
|
|
23
|
+
def create_or_update_block(self, block: Block, actor: PydanticUser) -> PydanticBlock:
|
|
24
|
+
"""Create a new block based on the Block schema."""
|
|
25
|
+
db_block = self.get_block_by_id(block.id, actor)
|
|
26
|
+
if db_block:
|
|
27
|
+
update_data = BlockUpdate(**block.model_dump(exclude_none=True))
|
|
28
|
+
self.update_block(block.id, update_data, actor)
|
|
29
|
+
else:
|
|
30
|
+
with self.session_maker() as session:
|
|
31
|
+
data = block.model_dump(exclude_none=True)
|
|
32
|
+
block = BlockModel(**data, organization_id=actor.organization_id)
|
|
33
|
+
block.create(session, actor=actor)
|
|
34
|
+
return block.to_pydantic()
|
|
35
|
+
|
|
36
|
+
@enforce_types
|
|
37
|
+
def update_block(self, block_id: str, block_update: BlockUpdate, actor: PydanticUser) -> PydanticBlock:
|
|
38
|
+
"""Update a block by its ID with the given BlockUpdate object."""
|
|
39
|
+
with self.session_maker() as session:
|
|
40
|
+
block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
|
41
|
+
update_data = block_update.model_dump(exclude_unset=True, exclude_none=True)
|
|
42
|
+
for key, value in update_data.items():
|
|
43
|
+
setattr(block, key, value)
|
|
44
|
+
block.update(db_session=session, actor=actor)
|
|
45
|
+
return block.to_pydantic()
|
|
46
|
+
|
|
47
|
+
@enforce_types
|
|
48
|
+
def delete_block(self, block_id: str, actor: PydanticUser) -> PydanticBlock:
|
|
49
|
+
"""Delete a block by its ID."""
|
|
50
|
+
with self.session_maker() as session:
|
|
51
|
+
block = BlockModel.read(db_session=session, identifier=block_id)
|
|
52
|
+
block.hard_delete(db_session=session, actor=actor)
|
|
53
|
+
return block.to_pydantic()
|
|
54
|
+
|
|
55
|
+
@enforce_types
|
|
56
|
+
def get_blocks(
|
|
57
|
+
self,
|
|
58
|
+
actor: PydanticUser,
|
|
59
|
+
label: Optional[str] = None,
|
|
60
|
+
is_template: Optional[bool] = None,
|
|
61
|
+
template_name: Optional[str] = None,
|
|
62
|
+
id: Optional[str] = None,
|
|
63
|
+
cursor: Optional[str] = None,
|
|
64
|
+
limit: Optional[int] = 50,
|
|
65
|
+
) -> List[PydanticBlock]:
|
|
66
|
+
"""Retrieve blocks based on various optional filters."""
|
|
67
|
+
with self.session_maker() as session:
|
|
68
|
+
# Prepare filters
|
|
69
|
+
filters = {"organization_id": actor.organization_id}
|
|
70
|
+
if label:
|
|
71
|
+
filters["label"] = label
|
|
72
|
+
if is_template is not None:
|
|
73
|
+
filters["is_template"] = is_template
|
|
74
|
+
if template_name:
|
|
75
|
+
filters["template_name"] = template_name
|
|
76
|
+
if id:
|
|
77
|
+
filters["id"] = id
|
|
78
|
+
|
|
79
|
+
blocks = BlockModel.list(db_session=session, cursor=cursor, limit=limit, **filters)
|
|
80
|
+
|
|
81
|
+
return [block.to_pydantic() for block in blocks]
|
|
82
|
+
|
|
83
|
+
@enforce_types
|
|
84
|
+
def get_block_by_id(self, block_id, actor: PydanticUser) -> Optional[PydanticBlock]:
|
|
85
|
+
"""Retrieve a block by its name."""
|
|
86
|
+
with self.session_maker() as session:
|
|
87
|
+
try:
|
|
88
|
+
block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
|
89
|
+
return block.to_pydantic()
|
|
90
|
+
except NoResultFound:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
@enforce_types
|
|
94
|
+
def add_default_blocks(self, actor: PydanticUser):
|
|
95
|
+
for persona_file in list_persona_files():
|
|
96
|
+
text = open(persona_file, "r", encoding="utf-8").read()
|
|
97
|
+
name = os.path.basename(persona_file).replace(".txt", "")
|
|
98
|
+
self.create_or_update_block(Persona(template_name=name, value=text, is_template=True), actor=actor)
|
|
99
|
+
|
|
100
|
+
for human_file in list_human_files():
|
|
101
|
+
text = open(human_file, "r", encoding="utf-8").read()
|
|
102
|
+
name = os.path.basename(human_file).replace(".txt", "")
|
|
103
|
+
self.create_or_update_block(Human(template_name=name, value=text, is_template=True), actor=actor)
|
letta/utils.py
CHANGED
|
@@ -21,6 +21,7 @@ from urllib.parse import urljoin, urlparse
|
|
|
21
21
|
import demjson3 as demjson
|
|
22
22
|
import pytz
|
|
23
23
|
import tiktoken
|
|
24
|
+
from pathvalidate import sanitize_filename as pathvalidate_sanitize_filename
|
|
24
25
|
|
|
25
26
|
import letta
|
|
26
27
|
from letta.constants import (
|
|
@@ -29,6 +30,7 @@ from letta.constants import (
|
|
|
29
30
|
CORE_MEMORY_PERSONA_CHAR_LIMIT,
|
|
30
31
|
FUNCTION_RETURN_CHAR_LIMIT,
|
|
31
32
|
LETTA_DIR,
|
|
33
|
+
MAX_FILENAME_LENGTH,
|
|
32
34
|
TOOL_CALL_ID_MAX_LEN,
|
|
33
35
|
)
|
|
34
36
|
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
|
@@ -1071,3 +1073,40 @@ def json_dumps(data, indent=2):
|
|
|
1071
1073
|
|
|
1072
1074
|
def json_loads(data):
|
|
1073
1075
|
return json.loads(data, strict=False)
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def sanitize_filename(filename: str) -> str:
|
|
1079
|
+
"""
|
|
1080
|
+
Sanitize the given filename to prevent directory traversal, invalid characters,
|
|
1081
|
+
and reserved names while ensuring it fits within the maximum length allowed by the filesystem.
|
|
1082
|
+
|
|
1083
|
+
Parameters:
|
|
1084
|
+
filename (str): The user-provided filename.
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
str: A sanitized filename that is unique and safe for use.
|
|
1088
|
+
"""
|
|
1089
|
+
# Extract the base filename to avoid directory components
|
|
1090
|
+
filename = os.path.basename(filename)
|
|
1091
|
+
|
|
1092
|
+
# Split the base and extension
|
|
1093
|
+
base, ext = os.path.splitext(filename)
|
|
1094
|
+
|
|
1095
|
+
# External sanitization library
|
|
1096
|
+
base = pathvalidate_sanitize_filename(base)
|
|
1097
|
+
|
|
1098
|
+
# Cannot start with a period
|
|
1099
|
+
if base.startswith("."):
|
|
1100
|
+
raise ValueError(f"Invalid filename - derived file name {base} cannot start with '.'")
|
|
1101
|
+
|
|
1102
|
+
# Truncate the base name to fit within the maximum allowed length
|
|
1103
|
+
max_base_length = MAX_FILENAME_LENGTH - len(ext) - 33 # 32 for UUID + 1 for `_`
|
|
1104
|
+
if len(base) > max_base_length:
|
|
1105
|
+
base = base[:max_base_length]
|
|
1106
|
+
|
|
1107
|
+
# Append a unique UUID suffix for uniqueness
|
|
1108
|
+
unique_suffix = uuid.uuid4().hex
|
|
1109
|
+
sanitized_filename = f"{base}_{unique_suffix}{ext}"
|
|
1110
|
+
|
|
1111
|
+
# Return the sanitized filename
|
|
1112
|
+
return sanitized_filename
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: letta-nightly
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3.dev20241120010849
|
|
4
4
|
Summary: Create LLM agents with long-term memory and custom tools
|
|
5
5
|
License: Apache License
|
|
6
6
|
Author: Letta Team
|
|
@@ -47,6 +47,7 @@ Requires-Dist: llama-index-embeddings-openai (>=0.2.5,<0.3.0)
|
|
|
47
47
|
Requires-Dist: locust (>=2.31.5,<3.0.0)
|
|
48
48
|
Requires-Dist: nltk (>=3.8.1,<4.0.0)
|
|
49
49
|
Requires-Dist: numpy (>=1.26.2,<2.0.0)
|
|
50
|
+
Requires-Dist: pathvalidate (>=3.2.1,<4.0.0)
|
|
50
51
|
Requires-Dist: pexpect (>=4.9.0,<5.0.0) ; extra == "dev"
|
|
51
52
|
Requires-Dist: pg8000 (>=1.30.3,<2.0.0) ; extra == "postgres"
|
|
52
53
|
Requires-Dist: pgvector (>=0.2.3,<0.3.0) ; extra == "postgres"
|