letta-nightly 0.6.2.dev20241210104242__py3-none-any.whl → 0.6.3.dev20241211050151__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 +32 -43
- letta/agent_store/db.py +12 -54
- letta/agent_store/storage.py +10 -9
- letta/cli/cli.py +1 -0
- letta/client/client.py +3 -2
- letta/config.py +2 -2
- letta/data_sources/connectors.py +4 -3
- letta/embeddings.py +29 -9
- letta/functions/function_sets/base.py +36 -11
- letta/metadata.py +13 -2
- letta/o1_agent.py +2 -3
- letta/offline_memory_agent.py +2 -1
- letta/orm/__init__.py +1 -0
- letta/orm/file.py +1 -0
- letta/orm/mixins.py +12 -2
- letta/orm/organization.py +3 -0
- letta/orm/passage.py +72 -0
- letta/orm/sqlalchemy_base.py +36 -7
- letta/orm/sqlite_functions.py +140 -0
- letta/orm/user.py +1 -1
- letta/schemas/agent.py +4 -3
- letta/schemas/letta_message.py +5 -1
- letta/schemas/letta_request.py +3 -3
- letta/schemas/passage.py +6 -4
- letta/schemas/sandbox_config.py +1 -0
- letta/schemas/tool_rule.py +0 -3
- letta/server/rest_api/app.py +34 -12
- letta/server/rest_api/routers/v1/agents.py +19 -6
- letta/server/server.py +182 -43
- letta/server/static_files/assets/{index-4848e3d7.js → index-048c9598.js} +1 -1
- letta/server/static_files/assets/{index-43ab4d62.css → index-0e31b727.css} +1 -1
- letta/server/static_files/index.html +2 -2
- letta/services/passage_manager.py +225 -0
- letta/services/source_manager.py +2 -1
- letta/services/tool_execution_sandbox.py +18 -6
- letta/settings.py +2 -0
- letta_nightly-0.6.3.dev20241211050151.dist-info/METADATA +375 -0
- {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/RECORD +42 -40
- letta/agent_store/chroma.py +0 -297
- letta_nightly-0.6.2.dev20241210104242.dist-info/METADATA +0 -212
- {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
letta/agent.py
CHANGED
|
@@ -28,7 +28,7 @@ from letta.interface import AgentInterface
|
|
|
28
28
|
from letta.llm_api.helpers import is_context_overflow_error
|
|
29
29
|
from letta.llm_api.llm_api_tools import create
|
|
30
30
|
from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_messages
|
|
31
|
-
from letta.memory import
|
|
31
|
+
from letta.memory import summarize_messages
|
|
32
32
|
from letta.metadata import MetadataStore
|
|
33
33
|
from letta.orm import User
|
|
34
34
|
from letta.schemas.agent import AgentState, AgentStepResponse
|
|
@@ -52,6 +52,7 @@ from letta.schemas.usage import LettaUsageStatistics
|
|
|
52
52
|
from letta.schemas.user import User as PydanticUser
|
|
53
53
|
from letta.services.block_manager import BlockManager
|
|
54
54
|
from letta.services.message_manager import MessageManager
|
|
55
|
+
from letta.services.passage_manager import PassageManager
|
|
55
56
|
from letta.services.source_manager import SourceManager
|
|
56
57
|
from letta.services.tool_execution_sandbox import ToolExecutionSandbox
|
|
57
58
|
from letta.services.user_manager import UserManager
|
|
@@ -85,7 +86,7 @@ def compile_memory_metadata_block(
|
|
|
85
86
|
actor: PydanticUser,
|
|
86
87
|
agent_id: str,
|
|
87
88
|
memory_edit_timestamp: datetime.datetime,
|
|
88
|
-
|
|
89
|
+
passage_manager: Optional[PassageManager] = None,
|
|
89
90
|
message_manager: Optional[MessageManager] = None,
|
|
90
91
|
) -> str:
|
|
91
92
|
# Put the timestamp in the local timezone (mimicking get_local_time())
|
|
@@ -96,7 +97,7 @@ def compile_memory_metadata_block(
|
|
|
96
97
|
[
|
|
97
98
|
f"### Memory [last modified: {timestamp_str}]",
|
|
98
99
|
f"{message_manager.size(actor=actor, agent_id=agent_id) if message_manager else 0} previous messages between you and the user are stored in recall memory (use functions to access them)",
|
|
99
|
-
f"{
|
|
100
|
+
f"{passage_manager.size(actor=actor, agent_id=agent_id) if passage_manager else 0} total memories you created are stored in archival memory (use functions to access them)",
|
|
100
101
|
"\nCore memory shown below (limited in size, additional information stored in archival / recall memory):",
|
|
101
102
|
]
|
|
102
103
|
)
|
|
@@ -109,7 +110,7 @@ def compile_system_message(
|
|
|
109
110
|
in_context_memory: Memory,
|
|
110
111
|
in_context_memory_last_edit: datetime.datetime, # TODO move this inside of BaseMemory?
|
|
111
112
|
actor: PydanticUser,
|
|
112
|
-
|
|
113
|
+
passage_manager: Optional[PassageManager] = None,
|
|
113
114
|
message_manager: Optional[MessageManager] = None,
|
|
114
115
|
user_defined_variables: Optional[dict] = None,
|
|
115
116
|
append_icm_if_missing: bool = True,
|
|
@@ -138,7 +139,7 @@ def compile_system_message(
|
|
|
138
139
|
actor=actor,
|
|
139
140
|
agent_id=agent_id,
|
|
140
141
|
memory_edit_timestamp=in_context_memory_last_edit,
|
|
141
|
-
|
|
142
|
+
passage_manager=passage_manager,
|
|
142
143
|
message_manager=message_manager,
|
|
143
144
|
)
|
|
144
145
|
full_memory_string = memory_metadata_string + "\n" + in_context_memory.compile()
|
|
@@ -175,7 +176,7 @@ def initialize_message_sequence(
|
|
|
175
176
|
agent_id: str,
|
|
176
177
|
memory: Memory,
|
|
177
178
|
actor: PydanticUser,
|
|
178
|
-
|
|
179
|
+
passage_manager: Optional[PassageManager] = None,
|
|
179
180
|
message_manager: Optional[MessageManager] = None,
|
|
180
181
|
memory_edit_timestamp: Optional[datetime.datetime] = None,
|
|
181
182
|
include_initial_boot_message: bool = True,
|
|
@@ -184,7 +185,7 @@ def initialize_message_sequence(
|
|
|
184
185
|
memory_edit_timestamp = get_local_time()
|
|
185
186
|
|
|
186
187
|
# full_system_message = construct_system_with_memory(
|
|
187
|
-
# system, memory, memory_edit_timestamp,
|
|
188
|
+
# system, memory, memory_edit_timestamp, passage_manager=passage_manager, recall_memory=recall_memory
|
|
188
189
|
# )
|
|
189
190
|
full_system_message = compile_system_message(
|
|
190
191
|
agent_id=agent_id,
|
|
@@ -192,7 +193,7 @@ def initialize_message_sequence(
|
|
|
192
193
|
in_context_memory=memory,
|
|
193
194
|
in_context_memory_last_edit=memory_edit_timestamp,
|
|
194
195
|
actor=actor,
|
|
195
|
-
|
|
196
|
+
passage_manager=passage_manager,
|
|
196
197
|
message_manager=message_manager,
|
|
197
198
|
user_defined_variables=None,
|
|
198
199
|
append_icm_if_missing=True,
|
|
@@ -294,7 +295,7 @@ class Agent(BaseAgent):
|
|
|
294
295
|
self.interface = interface
|
|
295
296
|
|
|
296
297
|
# Create the persistence manager object based on the AgentState info
|
|
297
|
-
self.
|
|
298
|
+
self.passage_manager = PassageManager()
|
|
298
299
|
self.message_manager = MessageManager()
|
|
299
300
|
|
|
300
301
|
# State needed for heartbeat pausing
|
|
@@ -325,7 +326,7 @@ class Agent(BaseAgent):
|
|
|
325
326
|
agent_id=self.agent_state.id,
|
|
326
327
|
memory=self.agent_state.memory,
|
|
327
328
|
actor=self.user,
|
|
328
|
-
|
|
329
|
+
passage_manager=None,
|
|
329
330
|
message_manager=None,
|
|
330
331
|
memory_edit_timestamp=get_utc_time(),
|
|
331
332
|
include_initial_boot_message=True,
|
|
@@ -350,7 +351,7 @@ class Agent(BaseAgent):
|
|
|
350
351
|
memory=self.agent_state.memory,
|
|
351
352
|
agent_id=self.agent_state.id,
|
|
352
353
|
actor=self.user,
|
|
353
|
-
|
|
354
|
+
passage_manager=None,
|
|
354
355
|
message_manager=None,
|
|
355
356
|
memory_edit_timestamp=get_utc_time(),
|
|
356
357
|
include_initial_boot_message=True,
|
|
@@ -1306,7 +1307,7 @@ class Agent(BaseAgent):
|
|
|
1306
1307
|
in_context_memory=self.agent_state.memory,
|
|
1307
1308
|
in_context_memory_last_edit=memory_edit_timestamp,
|
|
1308
1309
|
actor=self.user,
|
|
1309
|
-
|
|
1310
|
+
passage_manager=self.passage_manager,
|
|
1310
1311
|
message_manager=self.message_manager,
|
|
1311
1312
|
user_defined_variables=None,
|
|
1312
1313
|
append_icm_if_missing=True,
|
|
@@ -1371,45 +1372,33 @@ class Agent(BaseAgent):
|
|
|
1371
1372
|
# TODO: recall memory
|
|
1372
1373
|
raise NotImplementedError()
|
|
1373
1374
|
|
|
1374
|
-
def attach_source(self, source_id: str,
|
|
1375
|
+
def attach_source(self, user: PydanticUser, source_id: str, source_manager: SourceManager, ms: MetadataStore):
|
|
1375
1376
|
"""Attach data with name `source_name` to the agent from source_connector."""
|
|
1376
1377
|
# TODO: eventually, adding a data source should just give access to the retriever the source table, rather than modifying archival memory
|
|
1377
|
-
user = UserManager().get_user_by_id(self.agent_state.user_id)
|
|
1378
|
-
filters = {"user_id": self.agent_state.user_id, "source_id": source_id}
|
|
1379
|
-
size = source_connector.size(filters)
|
|
1380
1378
|
page_size = 100
|
|
1381
|
-
|
|
1382
|
-
all_passages = []
|
|
1383
|
-
for i in tqdm(range(0, size, page_size)):
|
|
1384
|
-
passages = next(generator)
|
|
1379
|
+
passages = self.passage_manager.list_passages(actor=user, source_id=source_id, limit=page_size)
|
|
1385
1380
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1381
|
+
for passage in passages:
|
|
1382
|
+
assert isinstance(passage, Passage), f"Generate yielded bad non-Passage type: {type(passage)}"
|
|
1383
|
+
passage.agent_id = self.agent_state.id
|
|
1384
|
+
self.passage_manager.update_passage_by_id(passage_id=passage.id, passage=passage, actor=user)
|
|
1390
1385
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
self.archival_memory.storage.insert_many(passages)
|
|
1397
|
-
all_passages += passages
|
|
1398
|
-
|
|
1399
|
-
assert size == len(all_passages), f"Expected {size} passages, but only got {len(all_passages)}"
|
|
1400
|
-
|
|
1401
|
-
# save destination storage
|
|
1402
|
-
self.archival_memory.storage.save()
|
|
1386
|
+
agents_passages = self.passage_manager.list_passages(actor=user, agent_id=self.agent_state.id, source_id=source_id, limit=page_size)
|
|
1387
|
+
passage_size = self.passage_manager.size(actor=user, agent_id=self.agent_state.id, source_id=source_id)
|
|
1388
|
+
assert all([p.agent_id == self.agent_state.id for p in agents_passages])
|
|
1389
|
+
assert len(agents_passages) == passage_size # sanity check
|
|
1390
|
+
assert passage_size == len(passages), f"Expected {len(passages)} passages, got {passage_size}"
|
|
1403
1391
|
|
|
1404
1392
|
# attach to agent
|
|
1405
|
-
source =
|
|
1393
|
+
source = source_manager.get_source_by_id(source_id=source_id, actor=user)
|
|
1406
1394
|
assert source is not None, f"Source {source_id} not found in metadata store"
|
|
1407
|
-
ms.attach_source(agent_id=self.agent_state.id, source_id=source_id, user_id=self.agent_state.user_id)
|
|
1408
1395
|
|
|
1409
|
-
|
|
1396
|
+
# NOTE: need this redundant line here because we haven't migrated agent to ORM yet
|
|
1397
|
+
# TODO: delete @matt and remove
|
|
1398
|
+
ms.attach_source(agent_id=self.agent_state.id, source_id=source_id, user_id=self.agent_state.user_id)
|
|
1410
1399
|
|
|
1411
1400
|
printd(
|
|
1412
|
-
f"Attached data source {source.name} to agent {self.agent_state.name}, consisting of {len(
|
|
1401
|
+
f"Attached data source {source.name} to agent {self.agent_state.name}, consisting of {len(passages)}. Agent now has {passage_size} embeddings in archival memory.",
|
|
1413
1402
|
)
|
|
1414
1403
|
|
|
1415
1404
|
def update_message(self, message_id: str, request: MessageUpdate) -> Message:
|
|
@@ -1565,13 +1554,13 @@ class Agent(BaseAgent):
|
|
|
1565
1554
|
num_tokens_from_messages(messages=messages_openai_format[1:], model=self.model) if len(messages_openai_format) > 1 else 0
|
|
1566
1555
|
)
|
|
1567
1556
|
|
|
1568
|
-
|
|
1557
|
+
passage_manager_size = self.passage_manager.size(actor=self.user, agent_id=self.agent_state.id)
|
|
1569
1558
|
message_manager_size = self.message_manager.size(actor=self.user, agent_id=self.agent_state.id)
|
|
1570
1559
|
external_memory_summary = compile_memory_metadata_block(
|
|
1571
1560
|
actor=self.user,
|
|
1572
1561
|
agent_id=self.agent_state.id,
|
|
1573
1562
|
memory_edit_timestamp=get_utc_time(), # dummy timestamp
|
|
1574
|
-
|
|
1563
|
+
passage_manager=self.passage_manager,
|
|
1575
1564
|
message_manager=self.message_manager,
|
|
1576
1565
|
)
|
|
1577
1566
|
num_tokens_external_memory_summary = count_tokens(external_memory_summary)
|
|
@@ -1597,7 +1586,7 @@ class Agent(BaseAgent):
|
|
|
1597
1586
|
return ContextWindowOverview(
|
|
1598
1587
|
# context window breakdown (in messages)
|
|
1599
1588
|
num_messages=len(self._messages),
|
|
1600
|
-
num_archival_memory=
|
|
1589
|
+
num_archival_memory=passage_manager_size,
|
|
1601
1590
|
num_recall_memory=message_manager_size,
|
|
1602
1591
|
num_tokens_external_memory_summary=num_tokens_external_memory_summary,
|
|
1603
1592
|
# top-level information
|
letta/agent_store/db.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from typing import Dict, List, Optional
|
|
@@ -32,7 +33,7 @@ from letta.orm.base import Base
|
|
|
32
33
|
from letta.orm.file import FileMetadata as FileMetadataModel
|
|
33
34
|
|
|
34
35
|
# from letta.schemas.message import Message, Passage, Record, RecordType, ToolCall
|
|
35
|
-
from letta.
|
|
36
|
+
from letta.orm.passage import Passage as PassageModel
|
|
36
37
|
from letta.settings import settings
|
|
37
38
|
|
|
38
39
|
config = LettaConfig()
|
|
@@ -66,56 +67,6 @@ class CommonVector(TypeDecorator):
|
|
|
66
67
|
# For PostgreSQL, value is already in bytes
|
|
67
68
|
return np.frombuffer(value, dtype=np.float32)
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
class PassageModel(Base):
|
|
71
|
-
"""Defines data model for storing Passages (consisting of text, embedding)"""
|
|
72
|
-
|
|
73
|
-
__tablename__ = "passages"
|
|
74
|
-
__table_args__ = {"extend_existing": True}
|
|
75
|
-
|
|
76
|
-
# Assuming passage_id is the primary key
|
|
77
|
-
id = Column(String, primary_key=True)
|
|
78
|
-
user_id = Column(String, nullable=False)
|
|
79
|
-
text = Column(String)
|
|
80
|
-
file_id = Column(String)
|
|
81
|
-
agent_id = Column(String)
|
|
82
|
-
source_id = Column(String)
|
|
83
|
-
|
|
84
|
-
# vector storage
|
|
85
|
-
if settings.letta_pg_uri_no_default:
|
|
86
|
-
from pgvector.sqlalchemy import Vector
|
|
87
|
-
|
|
88
|
-
embedding = mapped_column(Vector(MAX_EMBEDDING_DIM))
|
|
89
|
-
elif config.archival_storage_type == "sqlite" or config.archival_storage_type == "chroma":
|
|
90
|
-
embedding = Column(CommonVector)
|
|
91
|
-
else:
|
|
92
|
-
raise ValueError(f"Unsupported archival_storage_type: {config.archival_storage_type}")
|
|
93
|
-
embedding_config = Column(EmbeddingConfigColumn)
|
|
94
|
-
metadata_ = Column(MutableJson)
|
|
95
|
-
|
|
96
|
-
# Add a datetime column, with default value as the current time
|
|
97
|
-
created_at = Column(DateTime(timezone=True))
|
|
98
|
-
|
|
99
|
-
Index("passage_idx_user", user_id, agent_id, file_id),
|
|
100
|
-
|
|
101
|
-
def __repr__(self):
|
|
102
|
-
return f"<Passage(passage_id='{self.id}', text='{self.text}', embedding='{self.embedding})>"
|
|
103
|
-
|
|
104
|
-
def to_record(self):
|
|
105
|
-
return Passage(
|
|
106
|
-
text=self.text,
|
|
107
|
-
embedding=self.embedding,
|
|
108
|
-
embedding_config=self.embedding_config,
|
|
109
|
-
file_id=self.file_id,
|
|
110
|
-
user_id=self.user_id,
|
|
111
|
-
id=self.id,
|
|
112
|
-
source_id=self.source_id,
|
|
113
|
-
agent_id=self.agent_id,
|
|
114
|
-
metadata_=self.metadata_,
|
|
115
|
-
created_at=self.created_at,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
70
|
class SQLStorageConnector(StorageConnector):
|
|
120
71
|
def __init__(self, table_type: str, config: LettaConfig, user_id, agent_id=None):
|
|
121
72
|
super().__init__(table_type=table_type, config=config, user_id=user_id, agent_id=agent_id)
|
|
@@ -320,8 +271,9 @@ class PostgresStorageConnector(SQLStorageConnector):
|
|
|
320
271
|
self.session_maker = db_context
|
|
321
272
|
|
|
322
273
|
# TODO: move to DB init
|
|
323
|
-
|
|
324
|
-
|
|
274
|
+
if settings.pg_uri:
|
|
275
|
+
with self.session_maker() as session:
|
|
276
|
+
session.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) # Enables the vector extension
|
|
325
277
|
|
|
326
278
|
def query(self, query: str, query_vec: List[float], top_k: int = 10, filters: Optional[Dict] = {}):
|
|
327
279
|
filters = self.get_filters(filters)
|
|
@@ -419,7 +371,13 @@ class SQLLiteStorageConnector(SQLStorageConnector):
|
|
|
419
371
|
|
|
420
372
|
# get storage URI
|
|
421
373
|
if table_type == TableType.ARCHIVAL_MEMORY or table_type == TableType.PASSAGES:
|
|
422
|
-
|
|
374
|
+
self.db_model = PassageModel
|
|
375
|
+
if settings.letta_pg_uri_no_default:
|
|
376
|
+
self.uri = settings.letta_pg_uri_no_default
|
|
377
|
+
else:
|
|
378
|
+
# For SQLite, use the archival storage path
|
|
379
|
+
self.path = config.archival_storage_path
|
|
380
|
+
self.uri = f"sqlite:///{os.path.join(config.archival_storage_path, 'letta.db')}"
|
|
423
381
|
elif table_type == TableType.FILES:
|
|
424
382
|
self.path = self.config.metadata_storage_path
|
|
425
383
|
if self.path is None:
|
letta/agent_store/storage.py
CHANGED
|
@@ -45,11 +45,13 @@ class StorageConnector:
|
|
|
45
45
|
self,
|
|
46
46
|
table_type: Union[TableType.ARCHIVAL_MEMORY, TableType.RECALL_MEMORY, TableType.PASSAGES, TableType.FILES],
|
|
47
47
|
config: LettaConfig,
|
|
48
|
-
user_id,
|
|
49
|
-
agent_id=None,
|
|
48
|
+
user_id: str,
|
|
49
|
+
agent_id: Optional[str] = None,
|
|
50
|
+
organization_id: Optional[str] = None,
|
|
50
51
|
):
|
|
51
52
|
self.user_id = user_id
|
|
52
53
|
self.agent_id = agent_id
|
|
54
|
+
self.organization_id = organization_id
|
|
53
55
|
self.table_type = table_type
|
|
54
56
|
|
|
55
57
|
# get object type
|
|
@@ -74,10 +76,12 @@ class StorageConnector:
|
|
|
74
76
|
# agent-specific table
|
|
75
77
|
assert agent_id is not None, "Agent ID must be provided for agent-specific tables"
|
|
76
78
|
self.filters = {"user_id": self.user_id, "agent_id": self.agent_id}
|
|
77
|
-
elif self.table_type == TableType.
|
|
79
|
+
elif self.table_type == TableType.FILES:
|
|
78
80
|
# setup base filters for user-specific tables
|
|
79
81
|
assert agent_id is None, "Agent ID must not be provided for user-specific tables"
|
|
80
82
|
self.filters = {"user_id": self.user_id}
|
|
83
|
+
elif self.table_type == TableType.PASSAGES:
|
|
84
|
+
self.filters = {"organization_id": self.organization_id}
|
|
81
85
|
else:
|
|
82
86
|
raise ValueError(f"Table type {table_type} not implemented")
|
|
83
87
|
|
|
@@ -85,8 +89,9 @@ class StorageConnector:
|
|
|
85
89
|
def get_storage_connector(
|
|
86
90
|
table_type: Union[TableType.ARCHIVAL_MEMORY, TableType.RECALL_MEMORY, TableType.PASSAGES, TableType.FILES],
|
|
87
91
|
config: LettaConfig,
|
|
88
|
-
user_id,
|
|
89
|
-
|
|
92
|
+
user_id: str,
|
|
93
|
+
organization_id: Optional[str] = None,
|
|
94
|
+
agent_id: Optional[str] = None,
|
|
90
95
|
):
|
|
91
96
|
if table_type == TableType.ARCHIVAL_MEMORY or table_type == TableType.PASSAGES:
|
|
92
97
|
storage_type = config.archival_storage_type
|
|
@@ -101,10 +106,6 @@ class StorageConnector:
|
|
|
101
106
|
from letta.agent_store.db import PostgresStorageConnector
|
|
102
107
|
|
|
103
108
|
return PostgresStorageConnector(table_type, config, user_id, agent_id)
|
|
104
|
-
elif storage_type == "chroma":
|
|
105
|
-
from letta.agent_store.chroma import ChromaStorageConnector
|
|
106
|
-
|
|
107
|
-
return ChromaStorageConnector(table_type, config, user_id, agent_id)
|
|
108
109
|
|
|
109
110
|
elif storage_type == "qdrant":
|
|
110
111
|
from letta.agent_store.qdrant import QdrantStorageConnector
|
letta/cli/cli.py
CHANGED
|
@@ -53,6 +53,7 @@ def server(
|
|
|
53
53
|
debug: Annotated[bool, typer.Option(help="Turn debugging output on")] = False,
|
|
54
54
|
ade: Annotated[bool, typer.Option(help="Allows remote access")] = False, # NOTE: deprecated
|
|
55
55
|
secure: Annotated[bool, typer.Option(help="Adds simple security access")] = False,
|
|
56
|
+
localhttps: Annotated[bool, typer.Option(help="Setup local https")] = False,
|
|
56
57
|
):
|
|
57
58
|
"""Launch a Letta server process"""
|
|
58
59
|
if type == ServerChoice.rest_api:
|
letta/client/client.py
CHANGED
|
@@ -742,7 +742,8 @@ class RESTClient(AbstractClient):
|
|
|
742
742
|
agents = [AgentState(**agent) for agent in response.json()]
|
|
743
743
|
if len(agents) == 0:
|
|
744
744
|
return None
|
|
745
|
-
|
|
745
|
+
agents = [agents[0]] # TODO: @matt monkeypatched
|
|
746
|
+
assert len(agents) == 1, f"Multiple agents with the same name: {[(agents.name, agents.id) for agents in agents]}"
|
|
746
747
|
return agents[0].id
|
|
747
748
|
|
|
748
749
|
# memory
|
|
@@ -3107,7 +3108,7 @@ class LocalClient(AbstractClient):
|
|
|
3107
3108
|
passages (List[Passage]): List of passages
|
|
3108
3109
|
"""
|
|
3109
3110
|
|
|
3110
|
-
return self.server.get_agent_archival_cursor(user_id=self.user_id, agent_id=agent_id,
|
|
3111
|
+
return self.server.get_agent_archival_cursor(user_id=self.user_id, agent_id=agent_id, limit=limit)
|
|
3111
3112
|
|
|
3112
3113
|
# recall memory
|
|
3113
3114
|
|
letta/config.py
CHANGED
|
@@ -62,8 +62,8 @@ class LettaConfig:
|
|
|
62
62
|
# @norton120 these are the metdadatastore
|
|
63
63
|
|
|
64
64
|
# database configs: archival
|
|
65
|
-
archival_storage_type: str = "
|
|
66
|
-
archival_storage_path: str =
|
|
65
|
+
archival_storage_type: str = "sqlite" # local, db
|
|
66
|
+
archival_storage_path: str = LETTA_DIR
|
|
67
67
|
archival_storage_uri: str = None # TODO: eventually allow external vector DB
|
|
68
68
|
|
|
69
69
|
# database configs: recall
|
letta/data_sources/connectors.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, Iterator, List, Tuple
|
|
1
|
+
from typing import Dict, Iterator, List, Tuple, Optional
|
|
2
2
|
|
|
3
3
|
import typer
|
|
4
4
|
|
|
@@ -42,7 +42,7 @@ class DataConnector:
|
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def load_data(connector: DataConnector, source: Source, passage_store: StorageConnector, source_manager: SourceManager, actor: "User"):
|
|
45
|
+
def load_data(connector: DataConnector, source: Source, passage_store: StorageConnector, source_manager: SourceManager, actor: "User", agent_id: Optional[str] = None):
|
|
46
46
|
"""Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id."""
|
|
47
47
|
embedding_config = source.embedding_config
|
|
48
48
|
|
|
@@ -82,9 +82,10 @@ def load_data(connector: DataConnector, source: Source, passage_store: StorageCo
|
|
|
82
82
|
id=create_uuid_from_string(f"{str(source.id)}_{passage_text}"),
|
|
83
83
|
text=passage_text,
|
|
84
84
|
file_id=file_metadata.id,
|
|
85
|
+
agent_id=agent_id,
|
|
85
86
|
source_id=source.id,
|
|
86
87
|
metadata_=passage_metadata,
|
|
87
|
-
|
|
88
|
+
organization_id=source.organization_id,
|
|
88
89
|
embedding_config=source.embedding_config,
|
|
89
90
|
embedding=embedding,
|
|
90
91
|
)
|
letta/embeddings.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import uuid
|
|
3
2
|
from typing import Any, List, Optional
|
|
4
3
|
|
|
@@ -141,14 +140,35 @@ class AzureOpenAIEmbedding:
|
|
|
141
140
|
return embeddings
|
|
142
141
|
|
|
143
142
|
|
|
144
|
-
|
|
145
|
-
# default to hugging face model running local
|
|
146
|
-
# warning: this is a terrible model
|
|
147
|
-
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
|
143
|
+
class OllamaEmbeddings:
|
|
148
144
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
# Format:
|
|
146
|
+
# curl http://localhost:11434/api/embeddings -d '{
|
|
147
|
+
# "model": "mxbai-embed-large",
|
|
148
|
+
# "prompt": "Llamas are members of the camelid family"
|
|
149
|
+
# }'
|
|
150
|
+
|
|
151
|
+
def __init__(self, model: str, base_url: str, ollama_additional_kwargs: dict):
|
|
152
|
+
self.model = model
|
|
153
|
+
self.base_url = base_url
|
|
154
|
+
self.ollama_additional_kwargs = ollama_additional_kwargs
|
|
155
|
+
|
|
156
|
+
def get_text_embedding(self, text: str):
|
|
157
|
+
import httpx
|
|
158
|
+
|
|
159
|
+
headers = {"Content-Type": "application/json"}
|
|
160
|
+
json_data = {"model": self.model, "prompt": text}
|
|
161
|
+
json_data.update(self.ollama_additional_kwargs)
|
|
162
|
+
|
|
163
|
+
with httpx.Client() as client:
|
|
164
|
+
response = client.post(
|
|
165
|
+
f"{self.base_url}/api/embeddings",
|
|
166
|
+
headers=headers,
|
|
167
|
+
json=json_data,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
response_json = response.json()
|
|
171
|
+
return response_json["embedding"]
|
|
152
172
|
|
|
153
173
|
|
|
154
174
|
def query_embedding(embedding_model, query_text: str):
|
|
@@ -228,4 +248,4 @@ def embedding_model(config: EmbeddingConfig, user_id: Optional[uuid.UUID] = None
|
|
|
228
248
|
return model
|
|
229
249
|
|
|
230
250
|
else:
|
|
231
|
-
|
|
251
|
+
raise ValueError(f"Unknown endpoint type {endpoint_type}")
|
|
@@ -164,17 +164,23 @@ def archival_memory_insert(self: "Agent", content: str) -> Optional[str]:
|
|
|
164
164
|
Returns:
|
|
165
165
|
Optional[str]: None is always returned as this function does not produce a response.
|
|
166
166
|
"""
|
|
167
|
-
self.
|
|
167
|
+
self.passage_manager.insert_passage(
|
|
168
|
+
agent_state=self.agent_state,
|
|
169
|
+
agent_id=self.agent_state.id,
|
|
170
|
+
text=content,
|
|
171
|
+
actor=self.user,
|
|
172
|
+
)
|
|
168
173
|
return None
|
|
169
174
|
|
|
170
175
|
|
|
171
|
-
def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0) -> Optional[str]:
|
|
176
|
+
def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, start: Optional[int] = 0) -> Optional[str]:
|
|
172
177
|
"""
|
|
173
178
|
Search archival memory using semantic (embedding-based) search.
|
|
174
179
|
|
|
175
180
|
Args:
|
|
176
181
|
query (str): String to search for.
|
|
177
182
|
page (Optional[int]): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).
|
|
183
|
+
start (Optional[int]): Starting index for the search results. Defaults to 0.
|
|
178
184
|
|
|
179
185
|
Returns:
|
|
180
186
|
str: Query result string
|
|
@@ -191,15 +197,34 @@ def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0) -
|
|
|
191
197
|
except:
|
|
192
198
|
raise ValueError(f"'page' argument must be an integer")
|
|
193
199
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
# Get results using passage manager
|
|
203
|
+
all_results = self.passage_manager.list_passages(
|
|
204
|
+
actor=self.user,
|
|
205
|
+
query_text=query,
|
|
206
|
+
limit=count + start, # Request enough results to handle offset
|
|
207
|
+
embedding_config=self.agent_state.embedding_config,
|
|
208
|
+
embed_query=True
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Apply pagination
|
|
212
|
+
end = min(count + start, len(all_results))
|
|
213
|
+
paged_results = all_results[start:end]
|
|
214
|
+
|
|
215
|
+
# Format results to match previous implementation
|
|
216
|
+
formatted_results = [
|
|
217
|
+
{
|
|
218
|
+
"timestamp": str(result.created_at),
|
|
219
|
+
"content": result.text
|
|
220
|
+
}
|
|
221
|
+
for result in paged_results
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
return formatted_results, len(formatted_results)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
raise e
|
|
203
228
|
|
|
204
229
|
|
|
205
230
|
def core_memory_append(agent_state: "AgentState", label: str, content: str) -> Optional[str]: # type: ignore
|
letta/metadata.py
CHANGED
|
@@ -363,8 +363,19 @@ class MetadataStore:
|
|
|
363
363
|
with self.session_maker() as session:
|
|
364
364
|
# TODO: remove this (is a hack)
|
|
365
365
|
mapping_id = f"{user_id}-{agent_id}-{source_id}"
|
|
366
|
-
session.
|
|
367
|
-
|
|
366
|
+
existing = session.query(AgentSourceMappingModel).filter(
|
|
367
|
+
AgentSourceMappingModel.id == mapping_id
|
|
368
|
+
).first()
|
|
369
|
+
|
|
370
|
+
if existing is None:
|
|
371
|
+
# Only create if it doesn't exist
|
|
372
|
+
session.add(AgentSourceMappingModel(
|
|
373
|
+
id=mapping_id,
|
|
374
|
+
user_id=user_id,
|
|
375
|
+
agent_id=agent_id,
|
|
376
|
+
source_id=source_id
|
|
377
|
+
))
|
|
378
|
+
session.commit()
|
|
368
379
|
|
|
369
380
|
@enforce_types
|
|
370
381
|
def list_attached_source_ids(self, agent_id: str) -> List[str]:
|
letta/o1_agent.py
CHANGED
|
@@ -67,9 +67,8 @@ class O1Agent(Agent):
|
|
|
67
67
|
total_usage = UsageStatistics()
|
|
68
68
|
step_count = 0
|
|
69
69
|
while step_count < self.max_thinking_steps:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
m.id = m._generate_id()
|
|
70
|
+
if counter > 0:
|
|
71
|
+
next_input_message = []
|
|
73
72
|
|
|
74
73
|
kwargs["ms"] = ms
|
|
75
74
|
kwargs["first_message"] = False
|
letta/offline_memory_agent.py
CHANGED
|
@@ -130,8 +130,9 @@ class OfflineMemoryAgent(Agent):
|
|
|
130
130
|
# extras
|
|
131
131
|
first_message_verify_mono: bool = False,
|
|
132
132
|
max_memory_rethinks: int = 10,
|
|
133
|
+
initial_message_sequence: Optional[List[Message]] = None,
|
|
133
134
|
):
|
|
134
|
-
super().__init__(interface, agent_state, user)
|
|
135
|
+
super().__init__(interface, agent_state, user, initial_message_sequence=initial_message_sequence)
|
|
135
136
|
self.first_message_verify_mono = first_message_verify_mono
|
|
136
137
|
self.max_memory_rethinks = max_memory_rethinks
|
|
137
138
|
|
letta/orm/__init__.py
CHANGED
|
@@ -6,6 +6,7 @@ from letta.orm.file import FileMetadata
|
|
|
6
6
|
from letta.orm.job import Job
|
|
7
7
|
from letta.orm.message import Message
|
|
8
8
|
from letta.orm.organization import Organization
|
|
9
|
+
from letta.orm.passage import Passage
|
|
9
10
|
from letta.orm.sandbox_config import SandboxConfig, SandboxEnvironmentVariable
|
|
10
11
|
from letta.orm.source import Source
|
|
11
12
|
from letta.orm.tool import Tool
|
letta/orm/file.py
CHANGED
|
@@ -27,3 +27,4 @@ class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin):
|
|
|
27
27
|
# relationships
|
|
28
28
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="files", lazy="selectin")
|
|
29
29
|
source: Mapped["Source"] = relationship("Source", back_populates="files", lazy="selectin")
|
|
30
|
+
passages: Mapped[List["Passage"]] = relationship("Passage", back_populates="file", lazy="selectin", cascade="all, delete-orphan")
|
letta/orm/mixins.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Optional
|
|
1
2
|
from uuid import UUID
|
|
2
3
|
|
|
3
4
|
from sqlalchemy import ForeignKey, String
|
|
@@ -30,6 +31,12 @@ class UserMixin(Base):
|
|
|
30
31
|
|
|
31
32
|
user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"))
|
|
32
33
|
|
|
34
|
+
class FileMixin(Base):
|
|
35
|
+
"""Mixin for models that belong to a file."""
|
|
36
|
+
|
|
37
|
+
__abstract__ = True
|
|
38
|
+
|
|
39
|
+
file_id: Mapped[str] = mapped_column(String, ForeignKey("files.id"))
|
|
33
40
|
|
|
34
41
|
class AgentMixin(Base):
|
|
35
42
|
"""Mixin for models that belong to an agent."""
|
|
@@ -38,13 +45,16 @@ class AgentMixin(Base):
|
|
|
38
45
|
|
|
39
46
|
agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"))
|
|
40
47
|
|
|
41
|
-
|
|
42
48
|
class FileMixin(Base):
|
|
43
49
|
"""Mixin for models that belong to a file."""
|
|
44
50
|
|
|
45
51
|
__abstract__ = True
|
|
46
52
|
|
|
47
|
-
file_id: Mapped[str] = mapped_column(
|
|
53
|
+
file_id: Mapped[Optional[str]] = mapped_column(
|
|
54
|
+
String,
|
|
55
|
+
ForeignKey("files.id", ondelete="CASCADE"),
|
|
56
|
+
nullable=True
|
|
57
|
+
)
|
|
48
58
|
|
|
49
59
|
|
|
50
60
|
class SourceMixin(Base):
|