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 +28 -37
- letta/functions/function_sets/base.py +3 -1
- letta/functions/schema_generator.py +1 -5
- letta/local_llm/function_parser.py +1 -1
- letta/orm/__init__.py +1 -1
- letta/orm/agent.py +19 -1
- letta/orm/file.py +3 -2
- letta/orm/mixins.py +3 -14
- letta/orm/organization.py +19 -3
- letta/orm/passage.py +59 -23
- letta/orm/source.py +4 -0
- letta/orm/sqlalchemy_base.py +2 -2
- letta/prompts/system/memgpt_modified_chat.txt +1 -1
- letta/prompts/system/memgpt_modified_o1.txt +1 -1
- letta/schemas/embedding_config.py +20 -2
- letta/schemas/passage.py +1 -1
- letta/server/rest_api/app.py +13 -0
- letta/server/rest_api/utils.py +24 -5
- letta/server/server.py +31 -114
- letta/server/ws_api/server.py +1 -1
- letta/services/agent_manager.py +341 -9
- letta/services/passage_manager.py +76 -100
- letta/settings.py +1 -1
- {letta_nightly-0.6.4.dev20241216104246.dist-info → letta_nightly-0.6.4.dev20241217104233.dist-info}/METADATA +6 -6
- {letta_nightly-0.6.4.dev20241216104246.dist-info → letta_nightly-0.6.4.dev20241217104233.dist-info}/RECORD +28 -28
- {letta_nightly-0.6.4.dev20241216104246.dist-info → letta_nightly-0.6.4.dev20241217104233.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.4.dev20241216104246.dist-info → letta_nightly-0.6.4.dev20241217104233.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.4.dev20241216104246.dist-info → letta_nightly-0.6.4.dev20241217104233.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
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"{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
|
1382
|
+
assert source is not None, f"Source {source_id} not found in user's organization ({user.organization_id})"
|
|
1391
1383
|
|
|
1392
|
-
#
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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.
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
from
|
|
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.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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")
|
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
letta/server/rest_api/app.py
CHANGED
|
@@ -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)})
|
letta/server/rest_api/utils.py
CHANGED
|
@@ -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"
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|