letta-nightly 0.4.1.dev20241013104006__py3-none-any.whl → 0.5.0.dev20241015014828__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 +2 -2
- letta/agent.py +51 -65
- letta/agent_store/db.py +18 -7
- letta/agent_store/lancedb.py +2 -2
- letta/agent_store/milvus.py +1 -1
- letta/agent_store/qdrant.py +1 -1
- letta/agent_store/storage.py +12 -10
- letta/cli/cli_load.py +1 -1
- letta/client/client.py +51 -0
- letta/data_sources/connectors.py +124 -124
- letta/data_sources/connectors_helper.py +97 -0
- letta/llm_api/mistral.py +47 -0
- letta/main.py +19 -9
- letta/metadata.py +58 -0
- letta/providers.py +44 -0
- letta/schemas/file.py +31 -0
- letta/schemas/job.py +1 -1
- letta/schemas/letta_request.py +3 -3
- letta/schemas/llm_config.py +1 -0
- letta/schemas/message.py +6 -2
- letta/schemas/passage.py +3 -3
- letta/schemas/source.py +2 -2
- letta/server/rest_api/routers/v1/agents.py +10 -16
- letta/server/rest_api/routers/v1/jobs.py +17 -1
- letta/server/rest_api/routers/v1/sources.py +7 -9
- letta/server/server.py +137 -24
- letta/server/static_files/assets/{index-9a9c449b.js → index-dc228d4a.js} +4 -4
- letta/server/static_files/index.html +1 -1
- {letta_nightly-0.4.1.dev20241013104006.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/METADATA +1 -1
- {letta_nightly-0.4.1.dev20241013104006.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/RECORD +33 -31
- letta/schemas/document.py +0 -21
- {letta_nightly-0.4.1.dev20241013104006.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/LICENSE +0 -0
- {letta_nightly-0.4.1.dev20241013104006.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/WHEEL +0 -0
- {letta_nightly-0.4.1.dev20241013104006.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.5.0"
|
|
2
2
|
|
|
3
3
|
# import clients
|
|
4
4
|
from letta.client.admin import Admin
|
|
@@ -7,9 +7,9 @@ from letta.client.client import LocalClient, RESTClient, create_client
|
|
|
7
7
|
# imports for easier access
|
|
8
8
|
from letta.schemas.agent import AgentState
|
|
9
9
|
from letta.schemas.block import Block
|
|
10
|
-
from letta.schemas.document import Document
|
|
11
10
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
12
11
|
from letta.schemas.enums import JobStatus
|
|
12
|
+
from letta.schemas.file import FileMetadata
|
|
13
13
|
from letta.schemas.job import Job
|
|
14
14
|
from letta.schemas.letta_message import LettaMessage
|
|
15
15
|
from letta.schemas.llm_config import LLMConfig
|
letta/agent.py
CHANGED
|
@@ -39,6 +39,7 @@ from letta.system import (
|
|
|
39
39
|
get_login_event,
|
|
40
40
|
package_function_response,
|
|
41
41
|
package_summarize_message,
|
|
42
|
+
package_user_message,
|
|
42
43
|
)
|
|
43
44
|
from letta.utils import (
|
|
44
45
|
count_tokens,
|
|
@@ -200,16 +201,7 @@ class BaseAgent(ABC):
|
|
|
200
201
|
@abstractmethod
|
|
201
202
|
def step(
|
|
202
203
|
self,
|
|
203
|
-
messages: Union[Message, List[Message]
|
|
204
|
-
first_message: bool = False,
|
|
205
|
-
first_message_retry_limit: int = FIRST_MESSAGE_ATTEMPTS,
|
|
206
|
-
skip_verify: bool = False,
|
|
207
|
-
return_dicts: bool = True, # if True, return dicts, if False, return Message objects
|
|
208
|
-
recreate_message_timestamp: bool = True, # if True, when input is a Message type, recreated the 'created_at' field
|
|
209
|
-
stream: bool = False, # TODO move to config?
|
|
210
|
-
timestamp: Optional[datetime.datetime] = None,
|
|
211
|
-
inner_thoughts_in_kwargs_option: OptionState = OptionState.DEFAULT,
|
|
212
|
-
ms: Optional[MetadataStore] = None,
|
|
204
|
+
messages: Union[Message, List[Message]],
|
|
213
205
|
) -> AgentStepResponse:
|
|
214
206
|
"""
|
|
215
207
|
Top-level event message handler for the agent.
|
|
@@ -730,14 +722,13 @@ class Agent(BaseAgent):
|
|
|
730
722
|
|
|
731
723
|
def step(
|
|
732
724
|
self,
|
|
733
|
-
|
|
725
|
+
messages: Union[Message, List[Message]],
|
|
734
726
|
first_message: bool = False,
|
|
735
727
|
first_message_retry_limit: int = FIRST_MESSAGE_ATTEMPTS,
|
|
736
728
|
skip_verify: bool = False,
|
|
737
729
|
return_dicts: bool = True,
|
|
738
|
-
recreate_message_timestamp: bool = True, # if True, when input is a Message type, recreated the 'created_at' field
|
|
730
|
+
# recreate_message_timestamp: bool = True, # if True, when input is a Message type, recreated the 'created_at' field
|
|
739
731
|
stream: bool = False, # TODO move to config?
|
|
740
|
-
timestamp: Optional[datetime.datetime] = None,
|
|
741
732
|
inner_thoughts_in_kwargs_option: OptionState = OptionState.DEFAULT,
|
|
742
733
|
ms: Optional[MetadataStore] = None,
|
|
743
734
|
) -> AgentStepResponse:
|
|
@@ -760,50 +751,13 @@ class Agent(BaseAgent):
|
|
|
760
751
|
self.rebuild_memory(force=True, ms=ms)
|
|
761
752
|
|
|
762
753
|
# Step 1: add user message
|
|
763
|
-
if
|
|
764
|
-
|
|
765
|
-
assert user_message.text is not None
|
|
766
|
-
|
|
767
|
-
# Validate JSON via save/load
|
|
768
|
-
user_message_text = validate_json(user_message.text)
|
|
769
|
-
cleaned_user_message_text, name = strip_name_field_from_user_message(user_message_text)
|
|
770
|
-
|
|
771
|
-
if name is not None:
|
|
772
|
-
# Update Message object
|
|
773
|
-
user_message.text = cleaned_user_message_text
|
|
774
|
-
user_message.name = name
|
|
754
|
+
if isinstance(messages, Message):
|
|
755
|
+
messages = [messages]
|
|
775
756
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
user_message.created_at = get_utc_time()
|
|
757
|
+
if not all(isinstance(m, Message) for m in messages):
|
|
758
|
+
raise ValueError(f"messages should be a Message or a list of Message, got {type(messages)}")
|
|
779
759
|
|
|
780
|
-
|
|
781
|
-
# Validate JSON via save/load
|
|
782
|
-
user_message = validate_json(user_message)
|
|
783
|
-
cleaned_user_message_text, name = strip_name_field_from_user_message(user_message)
|
|
784
|
-
|
|
785
|
-
# If user_message['name'] is not None, it will be handled properly by dict_to_message
|
|
786
|
-
# So no need to run strip_name_field_from_user_message
|
|
787
|
-
|
|
788
|
-
# Create the associated Message object (in the database)
|
|
789
|
-
user_message = Message.dict_to_message(
|
|
790
|
-
agent_id=self.agent_state.id,
|
|
791
|
-
user_id=self.agent_state.user_id,
|
|
792
|
-
model=self.model,
|
|
793
|
-
openai_message_dict={"role": "user", "content": cleaned_user_message_text, "name": name},
|
|
794
|
-
created_at=timestamp,
|
|
795
|
-
)
|
|
796
|
-
|
|
797
|
-
else:
|
|
798
|
-
raise ValueError(f"Bad type for user_message: {type(user_message)}")
|
|
799
|
-
|
|
800
|
-
self.interface.user_message(user_message.text, msg_obj=user_message)
|
|
801
|
-
|
|
802
|
-
input_message_sequence = self._messages + [user_message]
|
|
803
|
-
|
|
804
|
-
# Alternatively, the requestor can send an empty user message
|
|
805
|
-
else:
|
|
806
|
-
input_message_sequence = self._messages
|
|
760
|
+
input_message_sequence = self._messages + messages
|
|
807
761
|
|
|
808
762
|
if len(input_message_sequence) > 1 and input_message_sequence[-1].role != "user":
|
|
809
763
|
printd(f"{CLI_WARNING_PREFIX}Attempting to run ChatCompletion without user as the last message in the queue")
|
|
@@ -846,11 +800,8 @@ class Agent(BaseAgent):
|
|
|
846
800
|
)
|
|
847
801
|
|
|
848
802
|
# Step 6: extend the message history
|
|
849
|
-
if
|
|
850
|
-
|
|
851
|
-
all_new_messages = [user_message] + all_response_messages
|
|
852
|
-
else:
|
|
853
|
-
raise ValueError(type(user_message))
|
|
803
|
+
if len(messages) > 0:
|
|
804
|
+
all_new_messages = messages + all_response_messages
|
|
854
805
|
else:
|
|
855
806
|
all_new_messages = all_response_messages
|
|
856
807
|
|
|
@@ -897,7 +848,7 @@ class Agent(BaseAgent):
|
|
|
897
848
|
)
|
|
898
849
|
|
|
899
850
|
except Exception as e:
|
|
900
|
-
printd(f"step() failed\
|
|
851
|
+
printd(f"step() failed\nmessages = {messages}\nerror = {e}")
|
|
901
852
|
|
|
902
853
|
# If we got a context alert, try trimming the messages length, then try again
|
|
903
854
|
if is_context_overflow_error(e):
|
|
@@ -906,14 +857,14 @@ class Agent(BaseAgent):
|
|
|
906
857
|
|
|
907
858
|
# Try step again
|
|
908
859
|
return self.step(
|
|
909
|
-
|
|
860
|
+
messages=messages,
|
|
910
861
|
first_message=first_message,
|
|
911
862
|
first_message_retry_limit=first_message_retry_limit,
|
|
912
863
|
skip_verify=skip_verify,
|
|
913
864
|
return_dicts=return_dicts,
|
|
914
|
-
recreate_message_timestamp=recreate_message_timestamp,
|
|
865
|
+
# recreate_message_timestamp=recreate_message_timestamp,
|
|
915
866
|
stream=stream,
|
|
916
|
-
timestamp=timestamp,
|
|
867
|
+
# timestamp=timestamp,
|
|
917
868
|
inner_thoughts_in_kwargs_option=inner_thoughts_in_kwargs_option,
|
|
918
869
|
ms=ms,
|
|
919
870
|
)
|
|
@@ -922,6 +873,40 @@ class Agent(BaseAgent):
|
|
|
922
873
|
printd(f"step() failed with an unrecognized exception: '{str(e)}'")
|
|
923
874
|
raise e
|
|
924
875
|
|
|
876
|
+
def step_user_message(self, user_message_str: str, **kwargs) -> AgentStepResponse:
|
|
877
|
+
"""Takes a basic user message string, turns it into a stringified JSON with extra metadata, then sends it to the agent
|
|
878
|
+
|
|
879
|
+
Example:
|
|
880
|
+
-> user_message_str = 'hi'
|
|
881
|
+
-> {'message': 'hi', 'type': 'user_message', ...}
|
|
882
|
+
-> json.dumps(...)
|
|
883
|
+
-> agent.step(messages=[Message(role='user', text=...)])
|
|
884
|
+
"""
|
|
885
|
+
# Wrap with metadata, dumps to JSON
|
|
886
|
+
assert user_message_str and isinstance(
|
|
887
|
+
user_message_str, str
|
|
888
|
+
), f"user_message_str should be a non-empty string, got {type(user_message_str)}"
|
|
889
|
+
user_message_json_str = package_user_message(user_message_str)
|
|
890
|
+
|
|
891
|
+
# Validate JSON via save/load
|
|
892
|
+
user_message = validate_json(user_message_json_str)
|
|
893
|
+
cleaned_user_message_text, name = strip_name_field_from_user_message(user_message)
|
|
894
|
+
|
|
895
|
+
# Turn into a dict
|
|
896
|
+
openai_message_dict = {"role": "user", "content": cleaned_user_message_text, "name": name}
|
|
897
|
+
|
|
898
|
+
# Create the associated Message object (in the database)
|
|
899
|
+
assert self.agent_state.user_id is not None, "User ID is not set"
|
|
900
|
+
user_message = Message.dict_to_message(
|
|
901
|
+
agent_id=self.agent_state.id,
|
|
902
|
+
user_id=self.agent_state.user_id,
|
|
903
|
+
model=self.model,
|
|
904
|
+
openai_message_dict=openai_message_dict,
|
|
905
|
+
# created_at=timestamp,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
return self.step(messages=[user_message], **kwargs)
|
|
909
|
+
|
|
925
910
|
def summarize_messages_inplace(self, cutoff=None, preserve_last_N_messages=True, disallow_tool_as_first=True):
|
|
926
911
|
assert self.messages[0]["role"] == "system", f"self.messages[0] should be system (instead got {self.messages[0]})"
|
|
927
912
|
|
|
@@ -1340,7 +1325,8 @@ class Agent(BaseAgent):
|
|
|
1340
1325
|
|
|
1341
1326
|
self.pop_until_user()
|
|
1342
1327
|
user_message = self.pop_message(count=1)[0]
|
|
1343
|
-
|
|
1328
|
+
assert user_message.text is not None, "User message text is None"
|
|
1329
|
+
step_response = self.step_user_message(user_message_str=user_message.text, return_dicts=False)
|
|
1344
1330
|
messages = step_response.messages
|
|
1345
1331
|
|
|
1346
1332
|
assert messages is not None
|
letta/agent_store/db.py
CHANGED
|
@@ -28,7 +28,7 @@ from letta.agent_store.storage import StorageConnector, TableType
|
|
|
28
28
|
from letta.base import Base
|
|
29
29
|
from letta.config import LettaConfig
|
|
30
30
|
from letta.constants import MAX_EMBEDDING_DIM
|
|
31
|
-
from letta.metadata import EmbeddingConfigColumn, ToolCallColumn
|
|
31
|
+
from letta.metadata import EmbeddingConfigColumn, FileMetadataModel, ToolCallColumn
|
|
32
32
|
|
|
33
33
|
# from letta.schemas.message import Message, Passage, Record, RecordType, ToolCall
|
|
34
34
|
from letta.schemas.message import Message
|
|
@@ -141,7 +141,7 @@ class PassageModel(Base):
|
|
|
141
141
|
id = Column(String, primary_key=True)
|
|
142
142
|
user_id = Column(String, nullable=False)
|
|
143
143
|
text = Column(String)
|
|
144
|
-
|
|
144
|
+
file_id = Column(String)
|
|
145
145
|
agent_id = Column(String)
|
|
146
146
|
source_id = Column(String)
|
|
147
147
|
|
|
@@ -160,7 +160,7 @@ class PassageModel(Base):
|
|
|
160
160
|
# Add a datetime column, with default value as the current time
|
|
161
161
|
created_at = Column(DateTime(timezone=True))
|
|
162
162
|
|
|
163
|
-
Index("passage_idx_user", user_id, agent_id,
|
|
163
|
+
Index("passage_idx_user", user_id, agent_id, file_id),
|
|
164
164
|
|
|
165
165
|
def __repr__(self):
|
|
166
166
|
return f"<Passage(passage_id='{self.id}', text='{self.text}', embedding='{self.embedding})>"
|
|
@@ -170,7 +170,7 @@ class PassageModel(Base):
|
|
|
170
170
|
text=self.text,
|
|
171
171
|
embedding=self.embedding,
|
|
172
172
|
embedding_config=self.embedding_config,
|
|
173
|
-
|
|
173
|
+
file_id=self.file_id,
|
|
174
174
|
user_id=self.user_id,
|
|
175
175
|
id=self.id,
|
|
176
176
|
source_id=self.source_id,
|
|
@@ -365,12 +365,17 @@ class PostgresStorageConnector(SQLStorageConnector):
|
|
|
365
365
|
self.uri = self.config.archival_storage_uri
|
|
366
366
|
self.db_model = PassageModel
|
|
367
367
|
if self.config.archival_storage_uri is None:
|
|
368
|
-
raise ValueError(f"Must
|
|
368
|
+
raise ValueError(f"Must specify archival_storage_uri in config {self.config.config_path}")
|
|
369
369
|
elif table_type == TableType.RECALL_MEMORY:
|
|
370
370
|
self.uri = self.config.recall_storage_uri
|
|
371
371
|
self.db_model = MessageModel
|
|
372
372
|
if self.config.recall_storage_uri is None:
|
|
373
|
-
raise ValueError(f"Must
|
|
373
|
+
raise ValueError(f"Must specify recall_storage_uri in config {self.config.config_path}")
|
|
374
|
+
elif table_type == TableType.FILES:
|
|
375
|
+
self.uri = self.config.metadata_storage_uri
|
|
376
|
+
self.db_model = FileMetadataModel
|
|
377
|
+
if self.config.metadata_storage_uri is None:
|
|
378
|
+
raise ValueError(f"Must specify metadata_storage_uri in config {self.config.config_path}")
|
|
374
379
|
else:
|
|
375
380
|
raise ValueError(f"Table type {table_type} not implemented")
|
|
376
381
|
|
|
@@ -487,8 +492,14 @@ class SQLLiteStorageConnector(SQLStorageConnector):
|
|
|
487
492
|
# TODO: eventually implement URI option
|
|
488
493
|
self.path = self.config.recall_storage_path
|
|
489
494
|
if self.path is None:
|
|
490
|
-
raise ValueError(f"Must
|
|
495
|
+
raise ValueError(f"Must specify recall_storage_path in config.")
|
|
491
496
|
self.db_model = MessageModel
|
|
497
|
+
elif table_type == TableType.FILES:
|
|
498
|
+
self.path = self.config.metadata_storage_path
|
|
499
|
+
if self.path is None:
|
|
500
|
+
raise ValueError(f"Must specify metadata_storage_path in config.")
|
|
501
|
+
self.db_model = FileMetadataModel
|
|
502
|
+
|
|
492
503
|
else:
|
|
493
504
|
raise ValueError(f"Table type {table_type} not implemented")
|
|
494
505
|
|
letta/agent_store/lancedb.py
CHANGED
|
@@ -24,7 +24,7 @@ def get_db_model(table_name: str, table_type: TableType):
|
|
|
24
24
|
id: uuid.UUID
|
|
25
25
|
user_id: str
|
|
26
26
|
text: str
|
|
27
|
-
|
|
27
|
+
file_id: str
|
|
28
28
|
agent_id: str
|
|
29
29
|
data_source: str
|
|
30
30
|
embedding: Vector(config.default_embedding_config.embedding_dim)
|
|
@@ -37,7 +37,7 @@ def get_db_model(table_name: str, table_type: TableType):
|
|
|
37
37
|
return Passage(
|
|
38
38
|
text=self.text,
|
|
39
39
|
embedding=self.embedding,
|
|
40
|
-
|
|
40
|
+
file_id=self.file_id,
|
|
41
41
|
user_id=self.user_id,
|
|
42
42
|
id=self.id,
|
|
43
43
|
data_source=self.data_source,
|
letta/agent_store/milvus.py
CHANGED
|
@@ -26,7 +26,7 @@ class MilvusStorageConnector(StorageConnector):
|
|
|
26
26
|
raise ValueError("Please set `archival_storage_uri` in the config file when using Milvus.")
|
|
27
27
|
|
|
28
28
|
# need to be converted to strings
|
|
29
|
-
self.uuid_fields = ["id", "user_id", "agent_id", "source_id", "
|
|
29
|
+
self.uuid_fields = ["id", "user_id", "agent_id", "source_id", "file_id"]
|
|
30
30
|
|
|
31
31
|
def _create_collection(self):
|
|
32
32
|
schema = MilvusClient.create_schema(
|
letta/agent_store/qdrant.py
CHANGED
|
@@ -38,7 +38,7 @@ class QdrantStorageConnector(StorageConnector):
|
|
|
38
38
|
distance=models.Distance.COSINE,
|
|
39
39
|
),
|
|
40
40
|
)
|
|
41
|
-
self.uuid_fields = ["id", "user_id", "agent_id", "source_id", "
|
|
41
|
+
self.uuid_fields = ["id", "user_id", "agent_id", "source_id", "file_id"]
|
|
42
42
|
|
|
43
43
|
def get_all_paginated(self, filters: Optional[Dict] = {}, page_size: int = 10) -> Iterator[List[RecordType]]:
|
|
44
44
|
from qdrant_client import grpc
|
letta/agent_store/storage.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Tuple, Type, Union
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
12
|
from letta.config import LettaConfig
|
|
13
|
-
from letta.schemas.
|
|
13
|
+
from letta.schemas.file import FileMetadata
|
|
14
14
|
from letta.schemas.message import Message
|
|
15
15
|
from letta.schemas.passage import Passage
|
|
16
16
|
from letta.utils import printd
|
|
@@ -22,7 +22,7 @@ class TableType:
|
|
|
22
22
|
ARCHIVAL_MEMORY = "archival_memory" # recall memory table: letta_agent_{agent_id}
|
|
23
23
|
RECALL_MEMORY = "recall_memory" # archival memory table: letta_agent_recall_{agent_id}
|
|
24
24
|
PASSAGES = "passages" # TODO
|
|
25
|
-
|
|
25
|
+
FILES = "files"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
# table names used by Letta
|
|
@@ -33,17 +33,17 @@ ARCHIVAL_TABLE_NAME = "letta_archival_memory_agent" # agent memory
|
|
|
33
33
|
|
|
34
34
|
# external data source tables
|
|
35
35
|
PASSAGE_TABLE_NAME = "letta_passages" # chunked/embedded passages (from source)
|
|
36
|
-
|
|
36
|
+
FILE_TABLE_NAME = "letta_files" # original files (from source)
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class StorageConnector:
|
|
40
|
-
"""Defines a DB connection that is user-specific to access data:
|
|
40
|
+
"""Defines a DB connection that is user-specific to access data: files, Passages, Archival/Recall Memory"""
|
|
41
41
|
|
|
42
42
|
type: Type[BaseModel]
|
|
43
43
|
|
|
44
44
|
def __init__(
|
|
45
45
|
self,
|
|
46
|
-
table_type: Union[TableType.ARCHIVAL_MEMORY, TableType.RECALL_MEMORY, TableType.PASSAGES, TableType.
|
|
46
|
+
table_type: Union[TableType.ARCHIVAL_MEMORY, TableType.RECALL_MEMORY, TableType.PASSAGES, TableType.FILES],
|
|
47
47
|
config: LettaConfig,
|
|
48
48
|
user_id,
|
|
49
49
|
agent_id=None,
|
|
@@ -59,9 +59,9 @@ class StorageConnector:
|
|
|
59
59
|
elif table_type == TableType.RECALL_MEMORY:
|
|
60
60
|
self.type = Message
|
|
61
61
|
self.table_name = RECALL_TABLE_NAME
|
|
62
|
-
elif table_type == TableType.
|
|
63
|
-
self.type =
|
|
64
|
-
self.table_name
|
|
62
|
+
elif table_type == TableType.FILES:
|
|
63
|
+
self.type = FileMetadata
|
|
64
|
+
self.table_name = FILE_TABLE_NAME
|
|
65
65
|
elif table_type == TableType.PASSAGES:
|
|
66
66
|
self.type = Passage
|
|
67
67
|
self.table_name = PASSAGE_TABLE_NAME
|
|
@@ -74,7 +74,7 @@ class StorageConnector:
|
|
|
74
74
|
# agent-specific table
|
|
75
75
|
assert agent_id is not None, "Agent ID must be provided for agent-specific tables"
|
|
76
76
|
self.filters = {"user_id": self.user_id, "agent_id": self.agent_id}
|
|
77
|
-
elif self.table_type == TableType.PASSAGES or self.table_type == TableType.
|
|
77
|
+
elif self.table_type == TableType.PASSAGES or self.table_type == TableType.FILES:
|
|
78
78
|
# setup base filters for user-specific tables
|
|
79
79
|
assert agent_id is None, "Agent ID must not be provided for user-specific tables"
|
|
80
80
|
self.filters = {"user_id": self.user_id}
|
|
@@ -83,7 +83,7 @@ class StorageConnector:
|
|
|
83
83
|
|
|
84
84
|
@staticmethod
|
|
85
85
|
def get_storage_connector(
|
|
86
|
-
table_type: Union[TableType.ARCHIVAL_MEMORY, TableType.RECALL_MEMORY, TableType.PASSAGES, TableType.
|
|
86
|
+
table_type: Union[TableType.ARCHIVAL_MEMORY, TableType.RECALL_MEMORY, TableType.PASSAGES, TableType.FILES],
|
|
87
87
|
config: LettaConfig,
|
|
88
88
|
user_id,
|
|
89
89
|
agent_id=None,
|
|
@@ -92,6 +92,8 @@ class StorageConnector:
|
|
|
92
92
|
storage_type = config.archival_storage_type
|
|
93
93
|
elif table_type == TableType.RECALL_MEMORY:
|
|
94
94
|
storage_type = config.recall_storage_type
|
|
95
|
+
elif table_type == TableType.FILES:
|
|
96
|
+
storage_type = config.metadata_storage_type
|
|
95
97
|
else:
|
|
96
98
|
raise ValueError(f"Table type {table_type} not implemented")
|
|
97
99
|
|
letta/cli/cli_load.py
CHANGED
|
@@ -106,7 +106,7 @@ def load_vector_database(
|
|
|
106
106
|
# document_store=None,
|
|
107
107
|
# passage_store=passage_storage,
|
|
108
108
|
# )
|
|
109
|
-
# print(f"Loaded {num_passages} passages and {num_documents}
|
|
109
|
+
# print(f"Loaded {num_passages} passages and {num_documents} files from {name}")
|
|
110
110
|
# except Exception as e:
|
|
111
111
|
# typer.secho(f"Failed to load data from provided information.\n{e}", fg=typer.colors.RED)
|
|
112
112
|
# ms.delete_source(source_id=source.id)
|
letta/client/client.py
CHANGED
|
@@ -25,6 +25,7 @@ from letta.schemas.embedding_config import EmbeddingConfig
|
|
|
25
25
|
|
|
26
26
|
# new schemas
|
|
27
27
|
from letta.schemas.enums import JobStatus, MessageRole
|
|
28
|
+
from letta.schemas.file import FileMetadata
|
|
28
29
|
from letta.schemas.job import Job
|
|
29
30
|
from letta.schemas.letta_request import LettaRequest
|
|
30
31
|
from letta.schemas.letta_response import LettaResponse, LettaStreamingResponse
|
|
@@ -232,6 +233,9 @@ class AbstractClient(object):
|
|
|
232
233
|
def list_attached_sources(self, agent_id: str) -> List[Source]:
|
|
233
234
|
raise NotImplementedError
|
|
234
235
|
|
|
236
|
+
def list_files_from_source(self, source_id: str, limit: int = 1000, cursor: Optional[str] = None) -> List[FileMetadata]:
|
|
237
|
+
raise NotImplementedError
|
|
238
|
+
|
|
235
239
|
def update_source(self, source_id: str, name: Optional[str] = None) -> Source:
|
|
236
240
|
raise NotImplementedError
|
|
237
241
|
|
|
@@ -1016,6 +1020,12 @@ class RESTClient(AbstractClient):
|
|
|
1016
1020
|
raise ValueError(f"Failed to get job: {response.text}")
|
|
1017
1021
|
return Job(**response.json())
|
|
1018
1022
|
|
|
1023
|
+
def delete_job(self, job_id: str) -> Job:
|
|
1024
|
+
response = requests.delete(f"{self.base_url}/{self.api_prefix}/jobs/{job_id}", headers=self.headers)
|
|
1025
|
+
if response.status_code != 200:
|
|
1026
|
+
raise ValueError(f"Failed to delete job: {response.text}")
|
|
1027
|
+
return Job(**response.json())
|
|
1028
|
+
|
|
1019
1029
|
def list_jobs(self):
|
|
1020
1030
|
response = requests.get(f"{self.base_url}/{self.api_prefix}/jobs", headers=self.headers)
|
|
1021
1031
|
return [Job(**job) for job in response.json()]
|
|
@@ -1088,6 +1098,30 @@ class RESTClient(AbstractClient):
|
|
|
1088
1098
|
raise ValueError(f"Failed to list attached sources: {response.text}")
|
|
1089
1099
|
return [Source(**source) for source in response.json()]
|
|
1090
1100
|
|
|
1101
|
+
def list_files_from_source(self, source_id: str, limit: int = 1000, cursor: Optional[str] = None) -> List[FileMetadata]:
|
|
1102
|
+
"""
|
|
1103
|
+
List files from source with pagination support.
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
source_id (str): ID of the source
|
|
1107
|
+
limit (int): Number of files to return
|
|
1108
|
+
cursor (Optional[str]): Pagination cursor for fetching the next page
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
List[FileMetadata]: List of files
|
|
1112
|
+
"""
|
|
1113
|
+
# Prepare query parameters for pagination
|
|
1114
|
+
params = {"limit": limit, "cursor": cursor}
|
|
1115
|
+
|
|
1116
|
+
# Make the request to the FastAPI endpoint
|
|
1117
|
+
response = requests.get(f"{self.base_url}/{self.api_prefix}/sources/{source_id}/files", headers=self.headers, params=params)
|
|
1118
|
+
|
|
1119
|
+
if response.status_code != 200:
|
|
1120
|
+
raise ValueError(f"Failed to list files with source id {source_id}: [{response.status_code}] {response.text}")
|
|
1121
|
+
|
|
1122
|
+
# Parse the JSON response
|
|
1123
|
+
return [FileMetadata(**metadata) for metadata in response.json()]
|
|
1124
|
+
|
|
1091
1125
|
def update_source(self, source_id: str, name: Optional[str] = None) -> Source:
|
|
1092
1126
|
"""
|
|
1093
1127
|
Update a source
|
|
@@ -2162,6 +2196,9 @@ class LocalClient(AbstractClient):
|
|
|
2162
2196
|
def get_job(self, job_id: str):
|
|
2163
2197
|
return self.server.get_job(job_id=job_id)
|
|
2164
2198
|
|
|
2199
|
+
def delete_job(self, job_id: str):
|
|
2200
|
+
return self.server.delete_job(job_id)
|
|
2201
|
+
|
|
2165
2202
|
def list_jobs(self):
|
|
2166
2203
|
return self.server.list_jobs(user_id=self.user_id)
|
|
2167
2204
|
|
|
@@ -2261,6 +2298,20 @@ class LocalClient(AbstractClient):
|
|
|
2261
2298
|
"""
|
|
2262
2299
|
return self.server.list_attached_sources(agent_id=agent_id)
|
|
2263
2300
|
|
|
2301
|
+
def list_files_from_source(self, source_id: str, limit: int = 1000, cursor: Optional[str] = None) -> List[FileMetadata]:
|
|
2302
|
+
"""
|
|
2303
|
+
List files from source.
|
|
2304
|
+
|
|
2305
|
+
Args:
|
|
2306
|
+
source_id (str): ID of the source
|
|
2307
|
+
limit (int): The # of items to return
|
|
2308
|
+
cursor (str): The cursor for fetching the next page
|
|
2309
|
+
|
|
2310
|
+
Returns:
|
|
2311
|
+
files (List[FileMetadata]): List of files
|
|
2312
|
+
"""
|
|
2313
|
+
return self.server.list_files_from_source(source_id=source_id, limit=limit, cursor=cursor)
|
|
2314
|
+
|
|
2264
2315
|
def update_source(self, source_id: str, name: Optional[str] = None) -> Source:
|
|
2265
2316
|
"""
|
|
2266
2317
|
Update a source
|