letta-nightly 0.6.2.dev20241210104242__py3-none-any.whl → 0.6.2.dev20241211031658__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.

Files changed (42) hide show
  1. letta/agent.py +32 -43
  2. letta/agent_store/db.py +12 -54
  3. letta/agent_store/storage.py +10 -9
  4. letta/cli/cli.py +1 -0
  5. letta/client/client.py +3 -2
  6. letta/config.py +2 -2
  7. letta/data_sources/connectors.py +4 -3
  8. letta/embeddings.py +29 -9
  9. letta/functions/function_sets/base.py +36 -11
  10. letta/metadata.py +13 -2
  11. letta/o1_agent.py +2 -3
  12. letta/offline_memory_agent.py +2 -1
  13. letta/orm/__init__.py +1 -0
  14. letta/orm/file.py +1 -0
  15. letta/orm/mixins.py +12 -2
  16. letta/orm/organization.py +3 -0
  17. letta/orm/passage.py +72 -0
  18. letta/orm/sqlalchemy_base.py +36 -7
  19. letta/orm/sqlite_functions.py +140 -0
  20. letta/orm/user.py +1 -1
  21. letta/schemas/agent.py +4 -3
  22. letta/schemas/letta_message.py +5 -1
  23. letta/schemas/letta_request.py +3 -3
  24. letta/schemas/passage.py +6 -4
  25. letta/schemas/sandbox_config.py +1 -0
  26. letta/schemas/tool_rule.py +0 -3
  27. letta/server/rest_api/app.py +34 -12
  28. letta/server/rest_api/routers/v1/agents.py +19 -6
  29. letta/server/server.py +118 -44
  30. letta/server/static_files/assets/{index-4848e3d7.js → index-048c9598.js} +1 -1
  31. letta/server/static_files/assets/{index-43ab4d62.css → index-0e31b727.css} +1 -1
  32. letta/server/static_files/index.html +2 -2
  33. letta/services/passage_manager.py +225 -0
  34. letta/services/source_manager.py +2 -1
  35. letta/services/tool_execution_sandbox.py +18 -6
  36. letta/settings.py +2 -0
  37. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.2.dev20241211031658.dist-info}/METADATA +10 -15
  38. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.2.dev20241211031658.dist-info}/RECORD +41 -39
  39. letta/agent_store/chroma.py +0 -297
  40. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.2.dev20241211031658.dist-info}/LICENSE +0 -0
  41. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.2.dev20241211031658.dist-info}/WHEEL +0 -0
  42. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.2.dev20241211031658.dist-info}/entry_points.txt +0 -0
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 ArchivalMemory, EmbeddingArchivalMemory, summarize_messages
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
- archival_memory: Optional[ArchivalMemory] = None,
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"{archival_memory.count() if archival_memory else 0} total memories you created are stored in archival memory (use functions to access them)",
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
- archival_memory: Optional[ArchivalMemory] = None,
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
- archival_memory=archival_memory,
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
- archival_memory: Optional[ArchivalMemory] = None,
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, archival_memory=archival_memory, recall_memory=recall_memory
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
- archival_memory=archival_memory,
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.archival_memory = EmbeddingArchivalMemory(agent_state)
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
- archival_memory=None,
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
- archival_memory=None,
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
- archival_memory=self.archival_memory,
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, source_connector: StorageConnector, ms: MetadataStore):
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
- generator = source_connector.get_all_paginated(filters=filters, page_size=page_size) # yields List[Passage]
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
- # need to associated passage with agent (for filtering)
1387
- for passage in passages:
1388
- assert isinstance(passage, Passage), f"Generate yielded bad non-Passage type: {type(passage)}"
1389
- passage.agent_id = self.agent_state.id
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
- # regenerate passage ID (avoid duplicates)
1392
- # TODO: need to find another solution to the text duplication issue
1393
- # passage.id = create_uuid_from_string(f"{source_id}_{str(passage.agent_id)}_{passage.text}")
1394
-
1395
- # insert into agent archival memory
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 = SourceManager().get_source_by_id(source_id=source_id, actor=user)
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
- total_agent_passages = self.archival_memory.storage.size()
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(all_passages)}. Agent now has {total_agent_passages} embeddings in archival memory.",
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
- num_archival_memory = self.archival_memory.storage.size()
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
- archival_memory=self.archival_memory,
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=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.schemas.passage import Passage
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
- with self.session_maker() as session:
324
- session.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) # Enables the vector extension
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
- raise ValueError(f"Table type {table_type} not implemented")
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:
@@ -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.PASSAGES or self.table_type == TableType.FILES:
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
- agent_id=None,
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
- assert len(agents) == 1, f"Multiple agents with the same name: {agents}"
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, before=before, after=after, limit=limit)
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 = "chroma" # local, db
66
- archival_storage_path: str = os.path.join(LETTA_DIR, "chroma")
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
@@ -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
- user_id=source.created_by_id,
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
- def default_embedding_model():
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
- os.environ["TOKENIZERS_PARALLELISM"] = "False"
150
- model = "BAAI/bge-small-en-v1.5"
151
- return HuggingFaceEmbedding(model_name=model)
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
- return default_embedding_model()
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.archival_memory.insert(content)
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
- results, total = self.archival_memory.search(query, count=count, start=page * count)
195
- num_pages = math.ceil(total / count) - 1 # 0 index
196
- if len(results) == 0:
197
- results_str = f"No results found."
198
- else:
199
- results_pref = f"Showing {len(results)} of {total} results (page {page}/{num_pages}):"
200
- results_formatted = [f"timestamp: {d['timestamp']}, memory: {d['content']}" for d in results]
201
- results_str = f"{results_pref} {json_dumps(results_formatted)}"
202
- return results_str
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.add(AgentSourceMappingModel(id=mapping_id, user_id=user_id, agent_id=agent_id, source_id=source_id))
367
- session.commit()
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
- # This is hacky but we need to do this for now
71
- for m in next_input_message:
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
@@ -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(String, ForeignKey("files.id"))
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):
letta/orm/organization.py CHANGED
@@ -33,7 +33,10 @@ class Organization(SqlalchemyBase):
33
33
  sandbox_environment_variables: Mapped[List["SandboxEnvironmentVariable"]] = relationship(
34
34
  "SandboxEnvironmentVariable", back_populates="organization", cascade="all, delete-orphan"
35
35
  )
36
+
37
+ # relationships
36
38
  messages: Mapped[List["Message"]] = relationship("Message", back_populates="organization", cascade="all, delete-orphan")
39
+ passages: Mapped[List["Passage"]] = relationship("Passage", back_populates="organization", cascade="all, delete-orphan")
37
40
 
38
41
  # TODO: Map these relationships later when we actually make these models
39
42
  # below is just a suggestion