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.

@@ -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.schemas.block import Block, CreateBlock, UpdateBlock
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: CreateBlock = Body(...),
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
- create_block.user_id = actor.id
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: UpdateBlock = Body(...),
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
- # actor = server.get_current_user()
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
- block = server.get_block(block_id=block_id)
73
- if block is None:
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, file=file, bytes=bytes)
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
- # write the file to a temporary directory (deleted after the context manager exits)
231
+ # Create a temporary directory (deleted after the context manager exits)
231
232
  with tempfile.TemporaryDirectory() as tmpdirname:
232
- file_path = os.path.join(str(tmpdirname), str(file.filename))
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.id)
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(GroqProvider(api_key=model_settings.groq_api_key))
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)
@@ -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
@@ -43,6 +43,9 @@ class ModelSettings(BaseSettings):
43
43
  # google ai
44
44
  gemini_api_key: Optional[str] = None
45
45
 
46
+ # together
47
+ together_api_key: Optional[str] = None
48
+
46
49
  # vLLM
47
50
  vllm_api_base: Optional[str] = None
48
51
 
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.2.dev20241118104226
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"