letta-nightly 0.5.2.dev20241118104226__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 +13 -7
- letta/agent_store/db.py +4 -3
- letta/cli/cli.py +2 -1
- letta/client/client.py +33 -30
- letta/constants.py +4 -1
- letta/functions/functions.py +2 -1
- letta/llm_api/llm_api_tools.py +38 -0
- letta/llm_api/openai.py +0 -1
- letta/local_llm/utils.py +12 -2
- 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/providers.py +141 -3
- letta/schemas/block.py +31 -26
- letta/schemas/letta_base.py +1 -1
- letta/schemas/llm_config.py +1 -0
- letta/schemas/openai/chat_completion_response.py +1 -0
- letta/server/rest_api/routers/v1/blocks.py +18 -22
- letta/server/rest_api/routers/v1/sources.py +9 -3
- letta/server/server.py +20 -85
- letta/services/block_manager.py +103 -0
- letta/services/tool_manager.py +4 -0
- letta/settings.py +3 -0
- letta/utils.py +39 -0
- {letta_nightly-0.5.2.dev20241118104226.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/METADATA +2 -1
- {letta_nightly-0.5.2.dev20241118104226.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/RECORD +32 -30
- {letta_nightly-0.5.2.dev20241118104226.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.2.dev20241118104226.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.2.dev20241118104226.dist-info → letta_nightly-0.5.3.dev20241120010849.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
@@ -49,18 +49,12 @@ from letta.providers import (
|
|
|
49
49
|
OllamaProvider,
|
|
50
50
|
OpenAIProvider,
|
|
51
51
|
Provider,
|
|
52
|
+
TogetherProvider,
|
|
52
53
|
VLLMChatCompletionsProvider,
|
|
53
54
|
VLLMCompletionsProvider,
|
|
54
55
|
)
|
|
55
56
|
from letta.schemas.agent import AgentState, AgentType, CreateAgent, UpdateAgentState
|
|
56
57
|
from letta.schemas.api_key import APIKey, APIKeyCreate
|
|
57
|
-
from letta.schemas.block import (
|
|
58
|
-
Block,
|
|
59
|
-
CreateBlock,
|
|
60
|
-
CreateHuman,
|
|
61
|
-
CreatePersona,
|
|
62
|
-
UpdateBlock,
|
|
63
|
-
)
|
|
64
58
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
65
59
|
|
|
66
60
|
# openai schemas
|
|
@@ -82,6 +76,7 @@ from letta.schemas.tool import Tool, ToolCreate
|
|
|
82
76
|
from letta.schemas.usage import LettaUsageStatistics
|
|
83
77
|
from letta.schemas.user import User
|
|
84
78
|
from letta.services.agents_tags_manager import AgentsTagsManager
|
|
79
|
+
from letta.services.block_manager import BlockManager
|
|
85
80
|
from letta.services.organization_manager import OrganizationManager
|
|
86
81
|
from letta.services.source_manager import SourceManager
|
|
87
82
|
from letta.services.tool_manager import ToolManager
|
|
@@ -249,6 +244,7 @@ class SyncServer(Server):
|
|
|
249
244
|
self.organization_manager = OrganizationManager()
|
|
250
245
|
self.user_manager = UserManager()
|
|
251
246
|
self.tool_manager = ToolManager()
|
|
247
|
+
self.block_manager = BlockManager()
|
|
252
248
|
self.source_manager = SourceManager()
|
|
253
249
|
self.agents_tags_manager = AgentsTagsManager()
|
|
254
250
|
|
|
@@ -256,7 +252,7 @@ class SyncServer(Server):
|
|
|
256
252
|
if init_with_default_org_and_user:
|
|
257
253
|
self.default_org = self.organization_manager.create_default_organization()
|
|
258
254
|
self.default_user = self.user_manager.create_default_user()
|
|
259
|
-
self.add_default_blocks(self.default_user
|
|
255
|
+
self.block_manager.add_default_blocks(actor=self.default_user)
|
|
260
256
|
self.tool_manager.add_base_tools(actor=self.default_user)
|
|
261
257
|
|
|
262
258
|
# If there is a default org/user
|
|
@@ -303,7 +299,18 @@ class SyncServer(Server):
|
|
|
303
299
|
)
|
|
304
300
|
)
|
|
305
301
|
if model_settings.groq_api_key:
|
|
306
|
-
self._enabled_providers.append(
|
|
302
|
+
self._enabled_providers.append(
|
|
303
|
+
GroqProvider(
|
|
304
|
+
api_key=model_settings.groq_api_key,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
if model_settings.together_api_key:
|
|
308
|
+
self._enabled_providers.append(
|
|
309
|
+
TogetherProvider(
|
|
310
|
+
api_key=model_settings.together_api_key,
|
|
311
|
+
default_prompt_formatter=model_settings.default_prompt_formatter,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
307
314
|
if model_settings.vllm_api_base:
|
|
308
315
|
# vLLM exposes both a /chat/completions and a /completions endpoint
|
|
309
316
|
self._enabled_providers.append(
|
|
@@ -321,15 +328,6 @@ class SyncServer(Server):
|
|
|
321
328
|
)
|
|
322
329
|
)
|
|
323
330
|
|
|
324
|
-
def save_agents(self):
|
|
325
|
-
"""Saves all the agents that are in the in-memory object store"""
|
|
326
|
-
for agent_d in self.active_agents:
|
|
327
|
-
try:
|
|
328
|
-
save_agent(agent_d["agent"], self.ms)
|
|
329
|
-
logger.debug(f"Saved agent {agent_d['agent_id']}")
|
|
330
|
-
except Exception as e:
|
|
331
|
-
logger.exception(f"Error occurred while trying to save agent {agent_d['agent_id']}:\n{e}")
|
|
332
|
-
|
|
333
331
|
def _get_agent(self, user_id: str, agent_id: str) -> Union[Agent, None]:
|
|
334
332
|
"""Get the agent object from the in-memory object store"""
|
|
335
333
|
for d in self.active_agents:
|
|
@@ -387,9 +385,9 @@ class SyncServer(Server):
|
|
|
387
385
|
assert isinstance(agent_state.memory, Memory)
|
|
388
386
|
|
|
389
387
|
if agent_state.agent_type == AgentType.memgpt_agent:
|
|
390
|
-
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)
|
|
391
389
|
elif agent_state.agent_type == AgentType.o1_agent:
|
|
392
|
-
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)
|
|
393
391
|
else:
|
|
394
392
|
raise NotImplementedError("Not a supported agent type")
|
|
395
393
|
|
|
@@ -872,6 +870,7 @@ class SyncServer(Server):
|
|
|
872
870
|
first_message_verify_mono=(
|
|
873
871
|
True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
|
|
874
872
|
),
|
|
873
|
+
user=actor,
|
|
875
874
|
initial_message_sequence=request.initial_message_sequence,
|
|
876
875
|
)
|
|
877
876
|
elif request.agent_type == AgentType.o1_agent:
|
|
@@ -883,6 +882,7 @@ class SyncServer(Server):
|
|
|
883
882
|
first_message_verify_mono=(
|
|
884
883
|
True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
|
|
885
884
|
),
|
|
885
|
+
user=actor,
|
|
886
886
|
)
|
|
887
887
|
# rebuilding agent memory on agent create in case shared memory blocks
|
|
888
888
|
# were specified in the new agent's memory config. we're doing this for two reasons:
|
|
@@ -1118,56 +1118,6 @@ class SyncServer(Server):
|
|
|
1118
1118
|
|
|
1119
1119
|
return [self.get_agent_state(user_id=user.id, agent_id=agent_id) for agent_id in agent_ids]
|
|
1120
1120
|
|
|
1121
|
-
def get_blocks(
|
|
1122
|
-
self,
|
|
1123
|
-
user_id: Optional[str] = None,
|
|
1124
|
-
label: Optional[str] = None,
|
|
1125
|
-
template: Optional[bool] = None,
|
|
1126
|
-
name: Optional[str] = None,
|
|
1127
|
-
id: Optional[str] = None,
|
|
1128
|
-
) -> Optional[List[Block]]:
|
|
1129
|
-
|
|
1130
|
-
return self.ms.get_blocks(user_id=user_id, label=label, template=template, template_name=name, id=id)
|
|
1131
|
-
|
|
1132
|
-
def get_block(self, block_id: str):
|
|
1133
|
-
|
|
1134
|
-
blocks = self.get_blocks(id=block_id)
|
|
1135
|
-
if blocks is None or len(blocks) == 0:
|
|
1136
|
-
raise ValueError("Block does not exist")
|
|
1137
|
-
if len(blocks) > 1:
|
|
1138
|
-
raise ValueError("Multiple blocks with the same id")
|
|
1139
|
-
return blocks[0]
|
|
1140
|
-
|
|
1141
|
-
def create_block(self, request: CreateBlock, user_id: str, update: bool = False) -> Block:
|
|
1142
|
-
existing_blocks = self.ms.get_blocks(
|
|
1143
|
-
template_name=request.template_name, user_id=user_id, template=request.template, label=request.label
|
|
1144
|
-
)
|
|
1145
|
-
|
|
1146
|
-
# for templates, update existing block template if exists
|
|
1147
|
-
if existing_blocks is not None and request.template:
|
|
1148
|
-
existing_block = existing_blocks[0]
|
|
1149
|
-
assert len(existing_blocks) == 1
|
|
1150
|
-
if update:
|
|
1151
|
-
return self.update_block(UpdateBlock(id=existing_block.id, **vars(request)))
|
|
1152
|
-
else:
|
|
1153
|
-
raise ValueError(f"Block with name {request.template_name} already exists")
|
|
1154
|
-
block = Block(**vars(request))
|
|
1155
|
-
self.ms.create_block(block)
|
|
1156
|
-
return block
|
|
1157
|
-
|
|
1158
|
-
def update_block(self, request: UpdateBlock) -> Block:
|
|
1159
|
-
block = self.get_block(request.id)
|
|
1160
|
-
block.limit = request.limit if request.limit is not None else block.limit
|
|
1161
|
-
block.value = request.value if request.value is not None else block.value
|
|
1162
|
-
block.template_name = request.template_name if request.template_name is not None else block.template_name
|
|
1163
|
-
self.ms.update_block(block=block)
|
|
1164
|
-
return self.ms.get_block(block_id=request.id)
|
|
1165
|
-
|
|
1166
|
-
def delete_block(self, block_id: str):
|
|
1167
|
-
block = self.get_block(block_id)
|
|
1168
|
-
self.ms.delete_block(block_id)
|
|
1169
|
-
return block
|
|
1170
|
-
|
|
1171
1121
|
# convert name->id
|
|
1172
1122
|
|
|
1173
1123
|
def get_agent_id(self, name: str, user_id: str):
|
|
@@ -1778,21 +1728,6 @@ class SyncServer(Server):
|
|
|
1778
1728
|
|
|
1779
1729
|
return success
|
|
1780
1730
|
|
|
1781
|
-
def add_default_blocks(self, user_id: str):
|
|
1782
|
-
from letta.utils import list_human_files, list_persona_files
|
|
1783
|
-
|
|
1784
|
-
assert user_id is not None, "User ID must be provided"
|
|
1785
|
-
|
|
1786
|
-
for persona_file in list_persona_files():
|
|
1787
|
-
text = open(persona_file, "r", encoding="utf-8").read()
|
|
1788
|
-
name = os.path.basename(persona_file).replace(".txt", "")
|
|
1789
|
-
self.create_block(CreatePersona(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
|
|
1790
|
-
|
|
1791
|
-
for human_file in list_human_files():
|
|
1792
|
-
text = open(human_file, "r", encoding="utf-8").read()
|
|
1793
|
-
name = os.path.basename(human_file).replace(".txt", "")
|
|
1794
|
-
self.create_block(CreateHuman(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
|
|
1795
|
-
|
|
1796
1731
|
def get_agent_message(self, agent_id: str, message_id: str) -> Optional[Message]:
|
|
1797
1732
|
"""Get a single message from the agent's memory"""
|
|
1798
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/services/tool_manager.py
CHANGED
|
@@ -70,6 +70,10 @@ class ToolManager:
|
|
|
70
70
|
pydantic_tool.organization_id = actor.organization_id
|
|
71
71
|
tool_data = pydantic_tool.model_dump()
|
|
72
72
|
tool = ToolModel(**tool_data)
|
|
73
|
+
# The description is most likely auto-generated via the json_schema,
|
|
74
|
+
# so copy it over into the top-level description field
|
|
75
|
+
if tool.description is None:
|
|
76
|
+
tool.description = tool.json_schema.get("description", None)
|
|
73
77
|
tool.create(session, actor=actor)
|
|
74
78
|
|
|
75
79
|
return tool.to_pydantic()
|
letta/settings.py
CHANGED
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"
|