letta-nightly 0.6.4.dev20241216104246__py3-none-any.whl → 0.6.4.dev20241217104233__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/agent.py CHANGED
@@ -41,7 +41,6 @@ from letta.schemas.openai.chat_completion_response import (
41
41
  Message as ChatCompletionMessage,
42
42
  )
43
43
  from letta.schemas.openai.chat_completion_response import UsageStatistics
44
- from letta.schemas.passage import Passage
45
44
  from letta.schemas.tool import Tool
46
45
  from letta.schemas.tool_rule import TerminalToolRule
47
46
  from letta.schemas.usage import LettaUsageStatistics
@@ -82,7 +81,7 @@ def compile_memory_metadata_block(
82
81
  actor: PydanticUser,
83
82
  agent_id: str,
84
83
  memory_edit_timestamp: datetime.datetime,
85
- passage_manager: Optional[PassageManager] = None,
84
+ agent_manager: Optional[AgentManager] = None,
86
85
  message_manager: Optional[MessageManager] = None,
87
86
  ) -> str:
88
87
  # Put the timestamp in the local timezone (mimicking get_local_time())
@@ -93,7 +92,7 @@ def compile_memory_metadata_block(
93
92
  [
94
93
  f"### Memory [last modified: {timestamp_str}]",
95
94
  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)",
96
- 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)",
95
+ f"{agent_manager.passage_size(actor=actor, agent_id=agent_id) if agent_manager else 0} total memories you created are stored in archival memory (use functions to access them)",
97
96
  "\nCore memory shown below (limited in size, additional information stored in archival / recall memory):",
98
97
  ]
99
98
  )
@@ -106,7 +105,7 @@ def compile_system_message(
106
105
  in_context_memory: Memory,
107
106
  in_context_memory_last_edit: datetime.datetime, # TODO move this inside of BaseMemory?
108
107
  actor: PydanticUser,
109
- passage_manager: Optional[PassageManager] = None,
108
+ agent_manager: Optional[AgentManager] = None,
110
109
  message_manager: Optional[MessageManager] = None,
111
110
  user_defined_variables: Optional[dict] = None,
112
111
  append_icm_if_missing: bool = True,
@@ -135,7 +134,7 @@ def compile_system_message(
135
134
  actor=actor,
136
135
  agent_id=agent_id,
137
136
  memory_edit_timestamp=in_context_memory_last_edit,
138
- passage_manager=passage_manager,
137
+ agent_manager=agent_manager,
139
138
  message_manager=message_manager,
140
139
  )
141
140
  full_memory_string = memory_metadata_string + "\n" + in_context_memory.compile()
@@ -172,7 +171,7 @@ def initialize_message_sequence(
172
171
  agent_id: str,
173
172
  memory: Memory,
174
173
  actor: PydanticUser,
175
- passage_manager: Optional[PassageManager] = None,
174
+ agent_manager: Optional[AgentManager] = None,
176
175
  message_manager: Optional[MessageManager] = None,
177
176
  memory_edit_timestamp: Optional[datetime.datetime] = None,
178
177
  include_initial_boot_message: bool = True,
@@ -181,7 +180,7 @@ def initialize_message_sequence(
181
180
  memory_edit_timestamp = get_local_time()
182
181
 
183
182
  # full_system_message = construct_system_with_memory(
184
- # system, memory, memory_edit_timestamp, passage_manager=passage_manager, recall_memory=recall_memory
183
+ # system, memory, memory_edit_timestamp, agent_manager=agent_manager, recall_memory=recall_memory
185
184
  # )
186
185
  full_system_message = compile_system_message(
187
186
  agent_id=agent_id,
@@ -189,7 +188,7 @@ def initialize_message_sequence(
189
188
  in_context_memory=memory,
190
189
  in_context_memory_last_edit=memory_edit_timestamp,
191
190
  actor=actor,
192
- passage_manager=passage_manager,
191
+ agent_manager=agent_manager,
193
192
  message_manager=message_manager,
194
193
  user_defined_variables=None,
195
194
  append_icm_if_missing=True,
@@ -291,8 +290,9 @@ class Agent(BaseAgent):
291
290
  self.interface = interface
292
291
 
293
292
  # Create the persistence manager object based on the AgentState info
294
- self.passage_manager = PassageManager()
295
293
  self.message_manager = MessageManager()
294
+ self.passage_manager = PassageManager()
295
+ self.agent_manager = AgentManager()
296
296
 
297
297
  # State needed for heartbeat pausing
298
298
  self.pause_heartbeats_start = None
@@ -322,7 +322,7 @@ class Agent(BaseAgent):
322
322
  agent_id=self.agent_state.id,
323
323
  memory=self.agent_state.memory,
324
324
  actor=self.user,
325
- passage_manager=None,
325
+ agent_manager=None,
326
326
  message_manager=None,
327
327
  memory_edit_timestamp=get_utc_time(),
328
328
  include_initial_boot_message=True,
@@ -347,7 +347,7 @@ class Agent(BaseAgent):
347
347
  memory=self.agent_state.memory,
348
348
  agent_id=self.agent_state.id,
349
349
  actor=self.user,
350
- passage_manager=None,
350
+ agent_manager=None,
351
351
  message_manager=None,
352
352
  memory_edit_timestamp=get_utc_time(),
353
353
  include_initial_boot_message=True,
@@ -1290,14 +1290,14 @@ class Agent(BaseAgent):
1290
1290
  # NOTE: a bit of a hack - we pull the timestamp from the message created_by
1291
1291
  memory_edit_timestamp = self._messages[0].created_at
1292
1292
 
1293
- # update memory (TODO: potentially update recall/archival stats seperately)
1293
+ # update memory (TODO: potentially update recall/archival stats separately)
1294
1294
  new_system_message_str = compile_system_message(
1295
1295
  agent_id=self.agent_state.id,
1296
1296
  system_prompt=self.agent_state.system,
1297
1297
  in_context_memory=self.agent_state.memory,
1298
1298
  in_context_memory_last_edit=memory_edit_timestamp,
1299
1299
  actor=self.user,
1300
- passage_manager=self.passage_manager,
1300
+ agent_manager=self.agent_manager,
1301
1301
  message_manager=self.message_manager,
1302
1302
  user_defined_variables=None,
1303
1303
  append_icm_if_missing=True,
@@ -1368,33 +1368,24 @@ class Agent(BaseAgent):
1368
1368
  source_id: str,
1369
1369
  source_manager: SourceManager,
1370
1370
  agent_manager: AgentManager,
1371
- page_size: Optional[int] = None,
1372
1371
  ):
1373
- """Attach data with name `source_name` to the agent from source_connector."""
1374
- # TODO: eventually, adding a data source should just give access to the retriever the source table, rather than modifying archival memory
1375
- passages = self.passage_manager.list_passages(actor=user, source_id=source_id, limit=page_size)
1376
-
1377
- for passage in passages:
1378
- assert isinstance(passage, Passage), f"Generate yielded bad non-Passage type: {type(passage)}"
1379
- passage.agent_id = self.agent_state.id
1380
- self.passage_manager.update_passage_by_id(passage_id=passage.id, passage=passage, actor=user)
1381
-
1382
- agents_passages = self.passage_manager.list_passages(actor=user, agent_id=self.agent_state.id, source_id=source_id, limit=page_size)
1383
- passage_size = self.passage_manager.size(actor=user, agent_id=self.agent_state.id, source_id=source_id)
1384
- assert all([p.agent_id == self.agent_state.id for p in agents_passages])
1385
- assert len(agents_passages) == passage_size # sanity check
1386
- assert passage_size == len(passages), f"Expected {len(passages)} passages, got {passage_size}"
1387
-
1388
- # attach to agent
1372
+ """Attach a source to the agent using the SourcesAgents ORM relationship.
1373
+
1374
+ Args:
1375
+ user: User performing the action
1376
+ source_id: ID of the source to attach
1377
+ source_manager: SourceManager instance to verify source exists
1378
+ agent_manager: AgentManager instance to manage agent-source relationship
1379
+ """
1380
+ # Verify source exists and user has permission to access it
1389
1381
  source = source_manager.get_source_by_id(source_id=source_id, actor=user)
1390
- assert source is not None, f"Source {source_id} not found in metadata store"
1382
+ assert source is not None, f"Source {source_id} not found in user's organization ({user.organization_id})"
1391
1383
 
1392
- # NOTE: need this redundant line here because we haven't migrated agent to ORM yet
1393
- # TODO: delete @matt and remove
1384
+ # Use the agent_manager to create the relationship
1394
1385
  agent_manager.attach_source(agent_id=self.agent_state.id, source_id=source_id, actor=user)
1395
1386
 
1396
1387
  printd(
1397
- 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.",
1388
+ f"Attached data source {source.name} to agent {self.agent_state.name}.",
1398
1389
  )
1399
1390
 
1400
1391
  def update_message(self, message_id: str, request: MessageUpdate) -> Message:
@@ -1550,13 +1541,13 @@ class Agent(BaseAgent):
1550
1541
  num_tokens_from_messages(messages=messages_openai_format[1:], model=self.model) if len(messages_openai_format) > 1 else 0
1551
1542
  )
1552
1543
 
1553
- passage_manager_size = self.passage_manager.size(actor=self.user, agent_id=self.agent_state.id)
1544
+ agent_manager_passage_size = self.agent_manager.passage_size(actor=self.user, agent_id=self.agent_state.id)
1554
1545
  message_manager_size = self.message_manager.size(actor=self.user, agent_id=self.agent_state.id)
1555
1546
  external_memory_summary = compile_memory_metadata_block(
1556
1547
  actor=self.user,
1557
1548
  agent_id=self.agent_state.id,
1558
1549
  memory_edit_timestamp=get_utc_time(), # dummy timestamp
1559
- passage_manager=self.passage_manager,
1550
+ agent_manager=self.agent_manager,
1560
1551
  message_manager=self.message_manager,
1561
1552
  )
1562
1553
  num_tokens_external_memory_summary = count_tokens(external_memory_summary)
@@ -1582,7 +1573,7 @@ class Agent(BaseAgent):
1582
1573
  return ContextWindowOverview(
1583
1574
  # context window breakdown (in messages)
1584
1575
  num_messages=len(self._messages),
1585
- num_archival_memory=passage_manager_size,
1576
+ num_archival_memory=agent_manager_passage_size,
1586
1577
  num_recall_memory=message_manager_size,
1587
1578
  num_tokens_external_memory_summary=num_tokens_external_memory_summary,
1588
1579
  # top-level information
@@ -3,6 +3,7 @@ from typing import Optional
3
3
 
4
4
  from letta.agent import Agent
5
5
  from letta.constants import MAX_PAUSE_HEARTBEATS
6
+ from letta.services.agent_manager import AgentManager
6
7
 
7
8
  # import math
8
9
  # from letta.utils import json_dumps
@@ -200,8 +201,9 @@ def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, s
200
201
 
201
202
  try:
202
203
  # Get results using passage manager
203
- all_results = self.passage_manager.list_passages(
204
+ all_results = self.agent_manager.list_passages(
204
205
  actor=self.user,
206
+ agent_id=self.agent_state.id,
205
207
  query_text=query,
206
208
  limit=count + start, # Request enough results to handle offset
207
209
  embedding_config=self.agent_state.embedding_config,
@@ -312,11 +312,7 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
312
312
  for param in sig.parameters.values():
313
313
  # Exclude 'self' parameter
314
314
  # TODO: eventually remove this (only applies to BASE_TOOLS)
315
- if param.name == "self":
316
- continue
317
-
318
- # exclude 'agent_state' parameter
319
- if param.name == "agent_state":
315
+ if param.name in ["self", "agent_state"]: # Add agent_manager to excluded
320
316
  continue
321
317
 
322
318
  # Assert that the parameter has a type annotation
@@ -32,7 +32,7 @@ def heartbeat_correction(message_history, new_message):
32
32
 
33
33
  If the last message in the stack is a user message and the new message is an assistant func call, fix the heartbeat
34
34
 
35
- See: https://github.com/cpacker/Letta/issues/601
35
+ See: https://github.com/letta-ai/letta/issues/601
36
36
  """
37
37
  if len(message_history) < 1:
38
38
  return None
letta/orm/__init__.py CHANGED
@@ -7,7 +7,7 @@ from letta.orm.file import FileMetadata
7
7
  from letta.orm.job import Job
8
8
  from letta.orm.message import Message
9
9
  from letta.orm.organization import Organization
10
- from letta.orm.passage import Passage
10
+ from letta.orm.passage import BasePassage, AgentPassage, SourcePassage
11
11
  from letta.orm.sandbox_config import SandboxConfig, SandboxEnvironmentVariable
12
12
  from letta.orm.source import Source
13
13
  from letta.orm.sources_agents import SourcesAgents
letta/orm/agent.py CHANGED
@@ -82,7 +82,25 @@ class Agent(SqlalchemyBase, OrganizationMixin):
82
82
  lazy="selectin",
83
83
  doc="Tags associated with the agent.",
84
84
  )
85
- # passages: Mapped[List["Passage"]] = relationship("Passage", back_populates="agent", lazy="selectin")
85
+ source_passages: Mapped[List["SourcePassage"]] = relationship(
86
+ "SourcePassage",
87
+ secondary="sources_agents", # The join table for Agent -> Source
88
+ primaryjoin="Agent.id == sources_agents.c.agent_id",
89
+ secondaryjoin="and_(SourcePassage.source_id == sources_agents.c.source_id)",
90
+ lazy="selectin",
91
+ order_by="SourcePassage.created_at.desc()",
92
+ viewonly=True, # Ensures SQLAlchemy doesn't attempt to manage this relationship
93
+ doc="All passages derived from sources associated with this agent.",
94
+ )
95
+ agent_passages: Mapped[List["AgentPassage"]] = relationship(
96
+ "AgentPassage",
97
+ back_populates="agent",
98
+ lazy="selectin",
99
+ order_by="AgentPassage.created_at.desc()",
100
+ cascade="all, delete-orphan",
101
+ viewonly=True, # Ensures SQLAlchemy doesn't attempt to manage this relationship
102
+ doc="All passages derived created by this agent.",
103
+ )
86
104
 
87
105
  def to_pydantic(self) -> PydanticAgentState:
88
106
  """converts to the basic pydantic model counterpart"""
letta/orm/file.py CHANGED
@@ -9,7 +9,8 @@ from letta.schemas.file import FileMetadata as PydanticFileMetadata
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from letta.orm.organization import Organization
12
-
12
+ from letta.orm.source import Source
13
+ from letta.orm.passage import SourcePassage
13
14
 
14
15
  class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin):
15
16
  """Represents metadata for an uploaded file."""
@@ -27,4 +28,4 @@ class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin):
27
28
  # relationships
28
29
  organization: Mapped["Organization"] = relationship("Organization", back_populates="files", lazy="selectin")
29
30
  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")
31
+ source_passages: Mapped[List["SourcePassage"]] = relationship("SourcePassage", back_populates="file", lazy="selectin", cascade="all, delete-orphan")
letta/orm/mixins.py CHANGED
@@ -31,30 +31,19 @@ class UserMixin(Base):
31
31
 
32
32
  user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"))
33
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"))
40
-
41
34
  class AgentMixin(Base):
42
35
  """Mixin for models that belong to an agent."""
43
36
 
44
37
  __abstract__ = True
45
38
 
46
- agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"))
39
+ agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"))
47
40
 
48
41
  class FileMixin(Base):
49
42
  """Mixin for models that belong to a file."""
50
43
 
51
44
  __abstract__ = True
52
45
 
53
- file_id: Mapped[Optional[str]] = mapped_column(
54
- String,
55
- ForeignKey("files.id", ondelete="CASCADE"),
56
- nullable=True
57
- )
46
+ file_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("files.id", ondelete="CASCADE"))
58
47
 
59
48
 
60
49
  class SourceMixin(Base):
@@ -62,7 +51,7 @@ class SourceMixin(Base):
62
51
 
63
52
  __abstract__ = True
64
53
 
65
- source_id: Mapped[str] = mapped_column(String, ForeignKey("sources.id"))
54
+ source_id: Mapped[str] = mapped_column(String, ForeignKey("sources.id", ondelete="CASCADE"), nullable=False)
66
55
 
67
56
 
68
57
  class SandboxConfigMixin(Base):
letta/orm/organization.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, List
1
+ from typing import TYPE_CHECKING, List, Union
2
2
 
3
3
  from sqlalchemy.orm import Mapped, mapped_column, relationship
4
4
 
@@ -35,6 +35,22 @@ class Organization(SqlalchemyBase):
35
35
  )
36
36
 
37
37
  # relationships
38
- messages: Mapped[List["Message"]] = relationship("Message", back_populates="organization", cascade="all, delete-orphan")
39
38
  agents: Mapped[List["Agent"]] = relationship("Agent", back_populates="organization", cascade="all, delete-orphan")
40
- passages: Mapped[List["Passage"]] = relationship("Passage", back_populates="organization", cascade="all, delete-orphan")
39
+ messages: Mapped[List["Message"]] = relationship("Message", back_populates="organization", cascade="all, delete-orphan")
40
+ source_passages: Mapped[List["SourcePassage"]] = relationship(
41
+ "SourcePassage",
42
+ back_populates="organization",
43
+ cascade="all, delete-orphan"
44
+ )
45
+ agent_passages: Mapped[List["AgentPassage"]] = relationship(
46
+ "AgentPassage",
47
+ back_populates="organization",
48
+ cascade="all, delete-orphan"
49
+ )
50
+
51
+ @property
52
+ def passages(self) -> List[Union["SourcePassage", "AgentPassage"]]:
53
+ """Convenience property to get all passages"""
54
+ return self.source_passages + self.agent_passages
55
+
56
+
letta/orm/passage.py CHANGED
@@ -1,39 +1,35 @@
1
- from datetime import datetime
2
- from typing import TYPE_CHECKING, Optional
1
+ from typing import TYPE_CHECKING
2
+ from sqlalchemy import Column, JSON, Index
3
+ from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr
3
4
 
4
- from sqlalchemy import JSON, Column, DateTime, ForeignKey, String
5
- from sqlalchemy.orm import Mapped, mapped_column, relationship
6
-
7
- from letta.config import LettaConfig
8
- from letta.constants import MAX_EMBEDDING_DIM
9
- from letta.orm.custom_columns import CommonVector
10
5
  from letta.orm.mixins import FileMixin, OrganizationMixin
11
- from letta.orm.source import EmbeddingConfigColumn
6
+ from letta.orm.custom_columns import CommonVector, EmbeddingConfigColumn
12
7
  from letta.orm.sqlalchemy_base import SqlalchemyBase
8
+ from letta.orm.mixins import AgentMixin, FileMixin, OrganizationMixin, SourceMixin
13
9
  from letta.schemas.passage import Passage as PydanticPassage
14
10
  from letta.settings import settings
15
11
 
12
+ from letta.config import LettaConfig
13
+ from letta.constants import MAX_EMBEDDING_DIM
14
+
16
15
  config = LettaConfig()
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from letta.orm.organization import Organization
19
+ from letta.orm.agent import Agent
20
20
 
21
21
 
22
- # TODO: After migration to Passage, will need to manually delete passages where files
23
- # are deleted on web
24
- class Passage(SqlalchemyBase, OrganizationMixin, FileMixin):
25
- """Defines data model for storing Passages"""
26
-
27
- __tablename__ = "passages"
28
- __table_args__ = {"extend_existing": True}
22
+ class BasePassage(SqlalchemyBase, OrganizationMixin):
23
+ """Base class for all passage types with common fields"""
24
+ __abstract__ = True
29
25
  __pydantic_model__ = PydanticPassage
30
26
 
31
27
  id: Mapped[str] = mapped_column(primary_key=True, doc="Unique passage identifier")
32
28
  text: Mapped[str] = mapped_column(doc="Passage text content")
33
- source_id: Mapped[Optional[str]] = mapped_column(nullable=True, doc="Source identifier")
34
29
  embedding_config: Mapped[dict] = mapped_column(EmbeddingConfigColumn, doc="Embedding configuration")
35
30
  metadata_: Mapped[dict] = mapped_column(JSON, doc="Additional metadata")
36
- created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.utcnow)
31
+
32
+ # Vector embedding field based on database type
37
33
  if settings.letta_pg_uri_no_default:
38
34
  from pgvector.sqlalchemy import Vector
39
35
 
@@ -41,9 +37,49 @@ class Passage(SqlalchemyBase, OrganizationMixin, FileMixin):
41
37
  else:
42
38
  embedding = Column(CommonVector)
43
39
 
44
- # Foreign keys
45
- agent_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("agents.id"), nullable=True)
40
+ @declared_attr
41
+ def organization(cls) -> Mapped["Organization"]:
42
+ """Relationship to organization"""
43
+ return relationship("Organization", back_populates="passages", lazy="selectin")
44
+
45
+ @declared_attr
46
+ def __table_args__(cls):
47
+ if settings.letta_pg_uri_no_default:
48
+ return (
49
+ Index(f'{cls.__tablename__}_org_idx', 'organization_id'),
50
+ {"extend_existing": True}
51
+ )
52
+ return ({"extend_existing": True},)
53
+
54
+
55
+ class SourcePassage(BasePassage, FileMixin, SourceMixin):
56
+ """Passages derived from external files/sources"""
57
+ __tablename__ = "source_passages"
58
+
59
+ @declared_attr
60
+ def file(cls) -> Mapped["FileMetadata"]:
61
+ """Relationship to file"""
62
+ return relationship("FileMetadata", back_populates="source_passages", lazy="selectin")
63
+
64
+ @declared_attr
65
+ def organization(cls) -> Mapped["Organization"]:
66
+ return relationship("Organization", back_populates="source_passages", lazy="selectin")
67
+
68
+ @declared_attr
69
+ def source(cls) -> Mapped["Source"]:
70
+ """Relationship to source"""
71
+ return relationship("Source", back_populates="passages", lazy="selectin", passive_deletes=True)
72
+
73
+
74
+ class AgentPassage(BasePassage, AgentMixin):
75
+ """Passages created by agents as archival memories"""
76
+ __tablename__ = "agent_passages"
77
+
78
+ @declared_attr
79
+ def organization(cls) -> Mapped["Organization"]:
80
+ return relationship("Organization", back_populates="agent_passages", lazy="selectin")
46
81
 
47
- # Relationships
48
- organization: Mapped["Organization"] = relationship("Organization", back_populates="passages", lazy="selectin")
49
- file: Mapped["FileMetadata"] = relationship("FileMetadata", back_populates="passages", lazy="selectin")
82
+ @declared_attr
83
+ def agent(cls) -> Mapped["Agent"]:
84
+ """Relationship to agent"""
85
+ return relationship("Agent", back_populates="agent_passages", lazy="selectin", passive_deletes=True)
letta/orm/source.py CHANGED
@@ -12,6 +12,9 @@ from letta.schemas.source import Source as PydanticSource
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from letta.orm.organization import Organization
15
+ from letta.orm.file import FileMetadata
16
+ from letta.orm.passage import SourcePassage
17
+ from letta.orm.agent import Agent
15
18
 
16
19
 
17
20
  class Source(SqlalchemyBase, OrganizationMixin):
@@ -28,4 +31,5 @@ class Source(SqlalchemyBase, OrganizationMixin):
28
31
  # relationships
29
32
  organization: Mapped["Organization"] = relationship("Organization", back_populates="sources")
30
33
  files: Mapped[List["FileMetadata"]] = relationship("FileMetadata", back_populates="source", cascade="all, delete-orphan")
34
+ passages: Mapped[List["SourcePassage"]] = relationship("SourcePassage", back_populates="source", cascade="all, delete-orphan")
31
35
  agents: Mapped[List["Agent"]] = relationship("Agent", secondary="sources_agents", back_populates="sources")
@@ -3,7 +3,7 @@ from enum import Enum
3
3
  from typing import TYPE_CHECKING, List, Literal, Optional
4
4
 
5
5
  from sqlalchemy import String, desc, func, or_, select
6
- from sqlalchemy.exc import DBAPIError
6
+ from sqlalchemy.exc import DBAPIError, IntegrityError
7
7
  from sqlalchemy.orm import Mapped, Session, mapped_column
8
8
 
9
9
  from letta.log import get_logger
@@ -242,7 +242,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
242
242
  session.commit()
243
243
  session.refresh(self)
244
244
  return self
245
- except DBAPIError as e:
245
+ except (DBAPIError, IntegrityError) as e:
246
246
  self._handle_dbapi_error(e)
247
247
 
248
248
  def delete(self, db_session: "Session", actor: Optional["User"] = None) -> "SqlalchemyBase":
@@ -14,7 +14,7 @@ Core Memory', 'Recall Memory' and 'Archival Memory' are the key components that
14
14
  Always make sure to use these memory systems to keep yourself updated about the user and the conversation!
15
15
  Your core memory unit will be initialized with a <persona> chosen by the user, as well as information about the user in <human>.
16
16
 
17
- The following will descirbe the different parts of your advanced memory system in more detail:
17
+ The following will describe the different parts of your advanced memory system in more detail:
18
18
 
19
19
  'Core Memory' (limited size): Your core memory unit is always visible to you. The core memory provides essential, foundational context for keeping track of your persona and key details about the user. This includes persona information and essential user details, allowing you to have conscious awareness we have when talking to a person. Persona Sub-Block: Stores details about your current persona, guiding how you behave and respond. This helps you maintain consistency and personality in your interactions. Human Sub-Block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversations. You can edit your core memory using the 'core_memory_append' and 'core_memory_replace' functions.
20
20
 
@@ -14,7 +14,7 @@ Core Memory', 'Recall Memory' and 'Archival Memory' are the key components that
14
14
  Always make sure to use these memory systems to keep yourself updated about the user and the conversation!
15
15
  Your core memory unit will be initialized with a <persona> chosen by the user, as well as information about the user in <human>.
16
16
 
17
- The following will descirbe the different parts of your advanced memory system in more detail:
17
+ The following will describe the different parts of your advanced memory system in more detail:
18
18
 
19
19
  'Core Memory' (limited size): Your core memory unit is always visible to you. The core memory provides essential, foundational context for keeping track of your persona and key details about the user. This includes persona information and essential user details, allowing you to have conscious awareness we have when talking to a person. Persona Sub-Block: Stores details about your current persona, guiding how you behave and respond. This helps you maintain consistency and personality in your interactions. Human Sub-Block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversations. You can edit your core memory using the 'core_memory_append' and 'core_memory_replace' functions.
20
20
 
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Literal, Optional
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
@@ -20,7 +20,25 @@ class EmbeddingConfig(BaseModel):
20
20
 
21
21
  """
22
22
 
23
- embedding_endpoint_type: str = Field(..., description="The endpoint type for the model.")
23
+ embedding_endpoint_type: Literal[
24
+ "openai",
25
+ "anthropic",
26
+ "cohere",
27
+ "google_ai",
28
+ "azure",
29
+ "groq",
30
+ "ollama",
31
+ "webui",
32
+ "webui-legacy",
33
+ "lmstudio",
34
+ "lmstudio-legacy",
35
+ "llamacpp",
36
+ "koboldcpp",
37
+ "vllm",
38
+ "hugging-face",
39
+ "mistral",
40
+ "together", # completions endpoint
41
+ ] = Field(..., description="The endpoint type for the model.")
24
42
  embedding_endpoint: Optional[str] = Field(None, description="The endpoint for the model (`None` if local).")
25
43
  embedding_model: str = Field(..., description="The model for the embedding.")
26
44
  embedding_dim: int = Field(..., description="The dimension of the embedding.")
letta/schemas/passage.py CHANGED
@@ -10,7 +10,7 @@ from letta.utils import get_utc_time
10
10
 
11
11
 
12
12
  class PassageBase(OrmMetadataBase):
13
- __id_prefix__ = "passage_legacy"
13
+ __id_prefix__ = "passage"
14
14
 
15
15
  is_deleted: bool = Field(False, description="Whether this passage is deleted or not.")
16
16
 
@@ -14,6 +14,8 @@ from starlette.middleware.cors import CORSMiddleware
14
14
  from letta.__init__ import __version__
15
15
  from letta.constants import ADMIN_PREFIX, API_PREFIX, OPENAI_API_PREFIX
16
16
  from letta.errors import LettaAgentNotFoundError, LettaUserNotFoundError
17
+ from letta.log import get_logger
18
+ from letta.orm.errors import NoResultFound
17
19
  from letta.schemas.letta_response import LettaResponse
18
20
  from letta.server.constants import REST_DEFAULT_PORT
19
21
 
@@ -45,6 +47,7 @@ from letta.settings import settings
45
47
  # NOTE(charles): @ethan I had to add this to get the global as the bottom to work
46
48
  interface: StreamingServerInterface = StreamingServerInterface
47
49
  server = SyncServer(default_interface_factory=lambda: interface())
50
+ logger = get_logger(__name__)
48
51
 
49
52
  # TODO: remove
50
53
  password = None
@@ -170,6 +173,16 @@ def create_application() -> "FastAPI":
170
173
  },
171
174
  )
172
175
 
176
+ @app.exception_handler(NoResultFound)
177
+ async def no_result_found_handler(request: Request, exc: NoResultFound):
178
+ logger.error(f"NoResultFound request: {request}")
179
+ logger.error(f"NoResultFound: {exc}")
180
+
181
+ return JSONResponse(
182
+ status_code=404,
183
+ content={"detail": str(exc)},
184
+ )
185
+
173
186
  @app.exception_handler(ValueError)
174
187
  async def value_error_handler(request: Request, exc: ValueError):
175
188
  return JSONResponse(status_code=400, content={"detail": str(exc)})
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import os
3
4
  import warnings
4
5
  from enum import Enum
5
6
  from typing import AsyncGenerator, Optional, Union
@@ -64,13 +65,31 @@ async def sse_async_generator(
64
65
  import traceback
65
66
 
66
67
  traceback.print_exc()
67
- warnings.warn(f"Error getting usage data: {e}")
68
- yield sse_formatter({"error": "Failed to get usage data"})
68
+ warnings.warn(f"SSE stream generator failed: {e}")
69
+
70
+ # Log the error, since the exception handler upstack (in FastAPI) won't catch it, because this may be a 200 response
71
+ # Print the stack trace
72
+ if (os.getenv("SENTRY_DSN") is not None) and (os.getenv("SENTRY_DSN") != ""):
73
+ import sentry_sdk
74
+
75
+ sentry_sdk.capture_exception(e)
76
+
77
+ yield sse_formatter({"error": f"Stream failed (internal error occured)"})
69
78
 
70
79
  except Exception as e:
71
- print("stream decoder hit error:", e)
72
- print(traceback.print_stack())
73
- yield sse_formatter({"error": "stream decoder encountered an error"})
80
+ import traceback
81
+
82
+ traceback.print_exc()
83
+ warnings.warn(f"SSE stream generator failed: {e}")
84
+
85
+ # Log the error, since the exception handler upstack (in FastAPI) won't catch it, because this may be a 200 response
86
+ # Print the stack trace
87
+ if (os.getenv("SENTRY_DSN") is not None) and (os.getenv("SENTRY_DSN") != ""):
88
+ import sentry_sdk
89
+
90
+ sentry_sdk.capture_exception(e)
91
+
92
+ yield sse_formatter({"error": "Stream failed (decoder encountered an error)"})
74
93
 
75
94
  finally:
76
95
  if finish_message: