letta-nightly 0.8.0.dev20250606104326__py3-none-any.whl → 0.8.2.dev20250606215616__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.
- letta/__init__.py +1 -1
- letta/agent.py +1 -1
- letta/agents/letta_agent.py +49 -29
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +19 -13
- letta/agents/voice_sleeptime_agent.py +11 -3
- letta/constants.py +18 -0
- letta/data_sources/__init__.py +0 -0
- letta/data_sources/redis_client.py +282 -0
- letta/errors.py +0 -4
- letta/functions/function_sets/files.py +58 -0
- letta/functions/schema_generator.py +18 -1
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/helpers/datetime_helpers.py +47 -3
- letta/helpers/decorators.py +69 -0
- letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
- letta/interfaces/anthropic_streaming_interface.py +43 -24
- letta/interfaces/openai_streaming_interface.py +21 -19
- letta/llm_api/anthropic.py +1 -1
- letta/llm_api/anthropic_client.py +22 -14
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +36 -30
- letta/llm_api/llm_api_tools.py +1 -1
- letta/llm_api/llm_client_base.py +29 -1
- letta/llm_api/openai.py +1 -1
- letta/llm_api/openai_client.py +6 -8
- letta/local_llm/chat_completion_proxy.py +1 -1
- letta/memory.py +1 -1
- letta/orm/enums.py +1 -0
- letta/orm/file.py +80 -3
- letta/orm/files_agents.py +13 -0
- letta/orm/sqlalchemy_base.py +34 -11
- letta/otel/__init__.py +0 -0
- letta/otel/context.py +25 -0
- letta/otel/events.py +0 -0
- letta/otel/metric_registry.py +122 -0
- letta/otel/metrics.py +66 -0
- letta/otel/resource.py +26 -0
- letta/{tracing.py → otel/tracing.py} +55 -78
- letta/plugins/README.md +22 -0
- letta/plugins/__init__.py +0 -0
- letta/plugins/defaults.py +11 -0
- letta/plugins/plugins.py +72 -0
- letta/schemas/enums.py +8 -0
- letta/schemas/file.py +12 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +7 -7
- letta/server/rest_api/app.py +8 -6
- letta/server/rest_api/routers/v1/agents.py +37 -36
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/sources.py +26 -3
- letta/server/rest_api/utils.py +9 -6
- letta/server/server.py +18 -12
- letta/services/agent_manager.py +185 -193
- letta/services/block_manager.py +1 -1
- letta/services/context_window_calculator/token_counter.py +3 -2
- letta/services/file_processor/chunker/line_chunker.py +34 -0
- letta/services/file_processor/file_processor.py +40 -11
- letta/services/file_processor/parser/mistral_parser.py +11 -1
- letta/services/files_agents_manager.py +96 -7
- letta/services/group_manager.py +6 -6
- letta/services/helpers/agent_manager_helper.py +373 -3
- letta/services/identity_manager.py +1 -1
- letta/services/job_manager.py +1 -1
- letta/services/llm_batch_manager.py +1 -1
- letta/services/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +1 -1
- letta/services/per_agent_lock_manager.py +1 -1
- letta/services/provider_manager.py +1 -1
- letta/services/sandbox_config_manager.py +1 -1
- letta/services/source_manager.py +178 -19
- letta/services/step_manager.py +2 -2
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/telemetry_manager.py +1 -1
- letta/services/tool_executor/builtin_tool_executor.py +117 -0
- letta/services/tool_executor/composio_tool_executor.py +53 -0
- letta/services/tool_executor/core_tool_executor.py +474 -0
- letta/services/tool_executor/files_tool_executor.py +131 -0
- letta/services/tool_executor/mcp_tool_executor.py +45 -0
- letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
- letta/services/tool_executor/tool_execution_manager.py +34 -14
- letta/services/tool_executor/tool_execution_sandbox.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -802
- letta/services/tool_executor/tool_executor_base.py +43 -0
- letta/services/tool_manager.py +55 -59
- letta/services/tool_sandbox/e2b_sandbox.py +1 -1
- letta/services/tool_sandbox/local_sandbox.py +6 -3
- letta/services/user_manager.py +6 -3
- letta/settings.py +21 -1
- letta/utils.py +7 -2
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/METADATA +4 -2
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/RECORD +96 -74
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.0.dev20250606104326.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/entry_points.txt +0 -0
@@ -9,6 +9,16 @@ from letta.settings import settings
|
|
9
9
|
logger = get_logger(__name__)
|
10
10
|
|
11
11
|
|
12
|
+
SIMPLE_TEXT_MIME_TYPES = {
|
13
|
+
"text/plain",
|
14
|
+
"text/markdown",
|
15
|
+
"text/x-markdown",
|
16
|
+
"application/json",
|
17
|
+
"application/jsonl",
|
18
|
+
"application/x-jsonlines",
|
19
|
+
}
|
20
|
+
|
21
|
+
|
12
22
|
class MistralFileParser(FileParser):
|
13
23
|
"""Mistral-based OCR extraction"""
|
14
24
|
|
@@ -23,7 +33,7 @@ class MistralFileParser(FileParser):
|
|
23
33
|
|
24
34
|
# TODO: Kind of hacky...we try to exit early here?
|
25
35
|
# TODO: Create our internal file parser representation we return instead of OCRResponse
|
26
|
-
if mime_type
|
36
|
+
if mime_type in SIMPLE_TEXT_MIME_TYPES or mime_type.startswith("text/"):
|
27
37
|
text = content.decode("utf-8", errors="replace")
|
28
38
|
return OCRResponse(
|
29
39
|
model=self.model,
|
@@ -5,10 +5,11 @@ from sqlalchemy import and_, func, select, update
|
|
5
5
|
|
6
6
|
from letta.orm.errors import NoResultFound
|
7
7
|
from letta.orm.files_agents import FileAgent as FileAgentModel
|
8
|
+
from letta.otel.tracing import trace_method
|
9
|
+
from letta.schemas.block import Block as PydanticBlock
|
8
10
|
from letta.schemas.file import FileAgent as PydanticFileAgent
|
9
11
|
from letta.schemas.user import User as PydanticUser
|
10
12
|
from letta.server.db import db_registry
|
11
|
-
from letta.tracing import trace_method
|
12
13
|
from letta.utils import enforce_types
|
13
14
|
|
14
15
|
|
@@ -22,6 +23,7 @@ class FileAgentManager:
|
|
22
23
|
*,
|
23
24
|
agent_id: str,
|
24
25
|
file_id: str,
|
26
|
+
file_name: str,
|
25
27
|
actor: PydanticUser,
|
26
28
|
is_open: bool = True,
|
27
29
|
visible_content: Optional[str] = None,
|
@@ -38,6 +40,7 @@ class FileAgentManager:
|
|
38
40
|
and_(
|
39
41
|
FileAgentModel.agent_id == agent_id,
|
40
42
|
FileAgentModel.file_id == file_id,
|
43
|
+
FileAgentModel.file_name == file_name,
|
41
44
|
FileAgentModel.organization_id == actor.organization_id,
|
42
45
|
)
|
43
46
|
)
|
@@ -61,6 +64,7 @@ class FileAgentManager:
|
|
61
64
|
assoc = FileAgentModel(
|
62
65
|
agent_id=agent_id,
|
63
66
|
file_id=file_id,
|
67
|
+
file_name=file_name,
|
64
68
|
organization_id=actor.organization_id,
|
65
69
|
is_open=is_open,
|
66
70
|
visible_content=visible_content,
|
@@ -71,7 +75,7 @@ class FileAgentManager:
|
|
71
75
|
|
72
76
|
@enforce_types
|
73
77
|
@trace_method
|
74
|
-
async def
|
78
|
+
async def update_file_agent_by_id(
|
75
79
|
self,
|
76
80
|
*,
|
77
81
|
agent_id: str,
|
@@ -82,7 +86,33 @@ class FileAgentManager:
|
|
82
86
|
) -> PydanticFileAgent:
|
83
87
|
"""Patch an existing association row."""
|
84
88
|
async with db_registry.async_session() as session:
|
85
|
-
assoc = await self.
|
89
|
+
assoc = await self._get_association_by_file_id(session, agent_id, file_id, actor)
|
90
|
+
|
91
|
+
if is_open is not None:
|
92
|
+
assoc.is_open = is_open
|
93
|
+
if visible_content is not None:
|
94
|
+
assoc.visible_content = visible_content
|
95
|
+
|
96
|
+
# touch timestamp
|
97
|
+
assoc.last_accessed_at = datetime.now(timezone.utc)
|
98
|
+
|
99
|
+
await assoc.update_async(session, actor=actor)
|
100
|
+
return assoc.to_pydantic()
|
101
|
+
|
102
|
+
@enforce_types
|
103
|
+
@trace_method
|
104
|
+
async def update_file_agent_by_name(
|
105
|
+
self,
|
106
|
+
*,
|
107
|
+
agent_id: str,
|
108
|
+
file_name: str,
|
109
|
+
actor: PydanticUser,
|
110
|
+
is_open: Optional[bool] = None,
|
111
|
+
visible_content: Optional[str] = None,
|
112
|
+
) -> PydanticFileAgent:
|
113
|
+
"""Patch an existing association row."""
|
114
|
+
async with db_registry.async_session() as session:
|
115
|
+
assoc = await self._get_association_by_file_name(session, agent_id, file_name, actor)
|
86
116
|
|
87
117
|
if is_open is not None:
|
88
118
|
assoc.is_open = is_open
|
@@ -100,15 +130,61 @@ class FileAgentManager:
|
|
100
130
|
async def detach_file(self, *, agent_id: str, file_id: str, actor: PydanticUser) -> None:
|
101
131
|
"""Hard-delete the association."""
|
102
132
|
async with db_registry.async_session() as session:
|
103
|
-
assoc = await self.
|
133
|
+
assoc = await self._get_association_by_file_id(session, agent_id, file_id, actor)
|
104
134
|
await assoc.hard_delete_async(session, actor=actor)
|
105
135
|
|
106
136
|
@enforce_types
|
107
137
|
@trace_method
|
108
|
-
async def
|
138
|
+
async def get_file_agent_by_id(self, *, agent_id: str, file_id: str, actor: PydanticUser) -> Optional[PydanticFileAgent]:
|
139
|
+
async with db_registry.async_session() as session:
|
140
|
+
try:
|
141
|
+
assoc = await self._get_association_by_file_id(session, agent_id, file_id, actor)
|
142
|
+
return assoc.to_pydantic()
|
143
|
+
except NoResultFound:
|
144
|
+
return None
|
145
|
+
|
146
|
+
@enforce_types
|
147
|
+
@trace_method
|
148
|
+
async def get_all_file_blocks_by_name(
|
149
|
+
self,
|
150
|
+
*,
|
151
|
+
file_names: List[str],
|
152
|
+
actor: PydanticUser,
|
153
|
+
) -> List[PydanticBlock]:
|
154
|
+
"""
|
155
|
+
Retrieve multiple FileAgent associations by their IDs in a single query.
|
156
|
+
|
157
|
+
Args:
|
158
|
+
file_names: List of file names to retrieve
|
159
|
+
actor: The user making the request
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
List of PydanticFileAgent objects found (may be fewer than requested if some IDs don't exist)
|
163
|
+
"""
|
164
|
+
if not file_names:
|
165
|
+
return []
|
166
|
+
|
167
|
+
async with db_registry.async_session() as session:
|
168
|
+
# Use IN clause for efficient bulk retrieval
|
169
|
+
query = select(FileAgentModel).where(
|
170
|
+
and_(
|
171
|
+
FileAgentModel.file_name.in_(file_names),
|
172
|
+
FileAgentModel.organization_id == actor.organization_id,
|
173
|
+
)
|
174
|
+
)
|
175
|
+
|
176
|
+
# Execute query and get all results
|
177
|
+
rows = (await session.execute(query)).scalars().all()
|
178
|
+
|
179
|
+
# Convert to Pydantic models
|
180
|
+
return [row.to_pydantic_block() for row in rows]
|
181
|
+
|
182
|
+
@enforce_types
|
183
|
+
@trace_method
|
184
|
+
async def get_file_agent_by_file_name(self, *, agent_id: str, file_name: str, actor: PydanticUser) -> Optional[PydanticFileAgent]:
|
109
185
|
async with db_registry.async_session() as session:
|
110
186
|
try:
|
111
|
-
assoc = await self.
|
187
|
+
assoc = await self._get_association_by_file_name(session, agent_id, file_name, actor)
|
112
188
|
return assoc.to_pydantic()
|
113
189
|
except NoResultFound:
|
114
190
|
return None
|
@@ -170,7 +246,7 @@ class FileAgentManager:
|
|
170
246
|
await session.execute(stmt)
|
171
247
|
await session.commit()
|
172
248
|
|
173
|
-
async def
|
249
|
+
async def _get_association_by_file_id(self, session, agent_id: str, file_id: str, actor: PydanticUser) -> FileAgentModel:
|
174
250
|
q = select(FileAgentModel).where(
|
175
251
|
and_(
|
176
252
|
FileAgentModel.agent_id == agent_id,
|
@@ -182,3 +258,16 @@ class FileAgentManager:
|
|
182
258
|
if not assoc:
|
183
259
|
raise NoResultFound(f"FileAgent(agent_id={agent_id}, file_id={file_id}) not found in org {actor.organization_id}")
|
184
260
|
return assoc
|
261
|
+
|
262
|
+
async def _get_association_by_file_name(self, session, agent_id: str, file_name: str, actor: PydanticUser) -> FileAgentModel:
|
263
|
+
q = select(FileAgentModel).where(
|
264
|
+
and_(
|
265
|
+
FileAgentModel.agent_id == agent_id,
|
266
|
+
FileAgentModel.file_name == file_name,
|
267
|
+
FileAgentModel.organization_id == actor.organization_id,
|
268
|
+
)
|
269
|
+
)
|
270
|
+
assoc = await session.scalar(q)
|
271
|
+
if not assoc:
|
272
|
+
raise NoResultFound(f"FileAgent(agent_id={agent_id}, file_name={file_name}) not found in org {actor.organization_id}")
|
273
|
+
return assoc
|
letta/services/group_manager.py
CHANGED
@@ -7,13 +7,13 @@ from letta.orm.agent import Agent as AgentModel
|
|
7
7
|
from letta.orm.errors import NoResultFound
|
8
8
|
from letta.orm.group import Group as GroupModel
|
9
9
|
from letta.orm.message import Message as MessageModel
|
10
|
+
from letta.otel.tracing import trace_method
|
10
11
|
from letta.schemas.group import Group as PydanticGroup
|
11
12
|
from letta.schemas.group import GroupCreate, GroupUpdate, ManagerType
|
12
13
|
from letta.schemas.letta_message import LettaMessage
|
13
14
|
from letta.schemas.message import Message as PydanticMessage
|
14
15
|
from letta.schemas.user import User as PydanticUser
|
15
16
|
from letta.server.db import db_registry
|
16
|
-
from letta.tracing import trace_method
|
17
17
|
from letta.utils import enforce_types
|
18
18
|
|
19
19
|
|
@@ -152,9 +152,9 @@ class GroupManager:
|
|
152
152
|
|
153
153
|
@trace_method
|
154
154
|
@enforce_types
|
155
|
-
def
|
156
|
-
with db_registry.
|
157
|
-
group = GroupModel.
|
155
|
+
async def modify_group_async(self, group_id: str, group_update: GroupUpdate, actor: PydanticUser) -> PydanticGroup:
|
156
|
+
async with db_registry.async_session() as session:
|
157
|
+
group = await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor)
|
158
158
|
|
159
159
|
sleeptime_agent_frequency = None
|
160
160
|
max_message_buffer_length = None
|
@@ -206,11 +206,11 @@ class GroupManager:
|
|
206
206
|
if group_update.description:
|
207
207
|
group.description = group_update.description
|
208
208
|
if group_update.agent_ids:
|
209
|
-
self.
|
209
|
+
await self._process_agent_relationship_async(
|
210
210
|
session=session, group=group, agent_ids=group_update.agent_ids, allow_partial=False, replace=True
|
211
211
|
)
|
212
212
|
|
213
|
-
group.
|
213
|
+
await group.update_async(session, actor=actor)
|
214
214
|
return group.to_pydantic()
|
215
215
|
|
216
216
|
@trace_method
|
@@ -1,27 +1,33 @@
|
|
1
1
|
import datetime
|
2
2
|
from typing import List, Literal, Optional
|
3
3
|
|
4
|
-
|
4
|
+
import numpy as np
|
5
|
+
from sqlalchemy import Select, and_, asc, desc, func, literal, or_, select, union_all
|
5
6
|
from sqlalchemy.sql.expression import exists
|
6
7
|
|
7
8
|
from letta import system
|
8
|
-
from letta.constants import IN_CONTEXT_MEMORY_KEYWORD, STRUCTURED_OUTPUT_MODELS
|
9
|
+
from letta.constants import IN_CONTEXT_MEMORY_KEYWORD, MAX_EMBEDDING_DIM, STRUCTURED_OUTPUT_MODELS
|
10
|
+
from letta.embeddings import embedding_model
|
9
11
|
from letta.helpers import ToolRulesSolver
|
10
12
|
from letta.helpers.datetime_helpers import get_local_time, get_local_time_fast
|
13
|
+
from letta.orm import AgentPassage, SourcePassage, SourcesAgents
|
11
14
|
from letta.orm.agent import Agent as AgentModel
|
12
15
|
from letta.orm.agents_tags import AgentsTags
|
13
16
|
from letta.orm.errors import NoResultFound
|
14
17
|
from letta.orm.identity import Identity
|
18
|
+
from letta.orm.sqlite_functions import adapt_array
|
19
|
+
from letta.otel.tracing import trace_method
|
15
20
|
from letta.prompts import gpt_system
|
16
21
|
from letta.schemas.agent import AgentState, AgentType
|
22
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
17
23
|
from letta.schemas.enums import MessageRole
|
18
24
|
from letta.schemas.letta_message_content import TextContent
|
19
25
|
from letta.schemas.memory import Memory
|
20
26
|
from letta.schemas.message import Message, MessageCreate
|
21
27
|
from letta.schemas.tool_rule import ToolRule
|
22
28
|
from letta.schemas.user import User
|
29
|
+
from letta.settings import settings
|
23
30
|
from letta.system import get_initial_boot_messages, get_login_event, package_function_response
|
24
|
-
from letta.tracing import trace_method
|
25
31
|
|
26
32
|
|
27
33
|
# Static methods
|
@@ -566,3 +572,367 @@ def _apply_filters(
|
|
566
572
|
if base_template_id:
|
567
573
|
query = query.where(AgentModel.base_template_id == base_template_id)
|
568
574
|
return query
|
575
|
+
|
576
|
+
|
577
|
+
def build_passage_query(
|
578
|
+
actor: User,
|
579
|
+
agent_id: Optional[str] = None,
|
580
|
+
file_id: Optional[str] = None,
|
581
|
+
query_text: Optional[str] = None,
|
582
|
+
start_date: Optional[datetime] = None,
|
583
|
+
end_date: Optional[datetime] = None,
|
584
|
+
before: Optional[str] = None,
|
585
|
+
after: Optional[str] = None,
|
586
|
+
source_id: Optional[str] = None,
|
587
|
+
embed_query: bool = False,
|
588
|
+
ascending: bool = True,
|
589
|
+
embedding_config: Optional[EmbeddingConfig] = None,
|
590
|
+
agent_only: bool = False,
|
591
|
+
) -> Select:
|
592
|
+
"""Helper function to build the base passage query with all filters applied.
|
593
|
+
Supports both before and after pagination across merged source and agent passages.
|
594
|
+
|
595
|
+
Returns the query before any limit or count operations are applied.
|
596
|
+
"""
|
597
|
+
embedded_text = None
|
598
|
+
if embed_query:
|
599
|
+
assert embedding_config is not None, "embedding_config must be specified for vector search"
|
600
|
+
assert query_text is not None, "query_text must be specified for vector search"
|
601
|
+
embedded_text = embedding_model(embedding_config).get_text_embedding(query_text)
|
602
|
+
embedded_text = np.array(embedded_text)
|
603
|
+
embedded_text = np.pad(embedded_text, (0, MAX_EMBEDDING_DIM - embedded_text.shape[0]), mode="constant").tolist()
|
604
|
+
|
605
|
+
# Start with base query for source passages
|
606
|
+
source_passages = None
|
607
|
+
if not agent_only: # Include source passages
|
608
|
+
if agent_id is not None:
|
609
|
+
source_passages = (
|
610
|
+
select(SourcePassage, literal(None).label("agent_id"))
|
611
|
+
.join(SourcesAgents, SourcesAgents.source_id == SourcePassage.source_id)
|
612
|
+
.where(SourcesAgents.agent_id == agent_id)
|
613
|
+
.where(SourcePassage.organization_id == actor.organization_id)
|
614
|
+
)
|
615
|
+
else:
|
616
|
+
source_passages = select(SourcePassage, literal(None).label("agent_id")).where(
|
617
|
+
SourcePassage.organization_id == actor.organization_id
|
618
|
+
)
|
619
|
+
|
620
|
+
if source_id:
|
621
|
+
source_passages = source_passages.where(SourcePassage.source_id == source_id)
|
622
|
+
if file_id:
|
623
|
+
source_passages = source_passages.where(SourcePassage.file_id == file_id)
|
624
|
+
|
625
|
+
# Add agent passages query
|
626
|
+
agent_passages = None
|
627
|
+
if agent_id is not None:
|
628
|
+
agent_passages = (
|
629
|
+
select(
|
630
|
+
AgentPassage.id,
|
631
|
+
AgentPassage.text,
|
632
|
+
AgentPassage.embedding_config,
|
633
|
+
AgentPassage.metadata_,
|
634
|
+
AgentPassage.embedding,
|
635
|
+
AgentPassage.created_at,
|
636
|
+
AgentPassage.updated_at,
|
637
|
+
AgentPassage.is_deleted,
|
638
|
+
AgentPassage._created_by_id,
|
639
|
+
AgentPassage._last_updated_by_id,
|
640
|
+
AgentPassage.organization_id,
|
641
|
+
literal(None).label("file_id"),
|
642
|
+
literal(None).label("source_id"),
|
643
|
+
AgentPassage.agent_id,
|
644
|
+
)
|
645
|
+
.where(AgentPassage.agent_id == agent_id)
|
646
|
+
.where(AgentPassage.organization_id == actor.organization_id)
|
647
|
+
)
|
648
|
+
|
649
|
+
# Combine queries
|
650
|
+
if source_passages is not None and agent_passages is not None:
|
651
|
+
combined_query = union_all(source_passages, agent_passages).cte("combined_passages")
|
652
|
+
elif agent_passages is not None:
|
653
|
+
combined_query = agent_passages.cte("combined_passages")
|
654
|
+
elif source_passages is not None:
|
655
|
+
combined_query = source_passages.cte("combined_passages")
|
656
|
+
else:
|
657
|
+
raise ValueError("No passages found")
|
658
|
+
|
659
|
+
# Build main query from combined CTE
|
660
|
+
main_query = select(combined_query)
|
661
|
+
|
662
|
+
# Apply filters
|
663
|
+
if start_date:
|
664
|
+
main_query = main_query.where(combined_query.c.created_at >= start_date)
|
665
|
+
if end_date:
|
666
|
+
main_query = main_query.where(combined_query.c.created_at <= end_date)
|
667
|
+
if source_id:
|
668
|
+
main_query = main_query.where(combined_query.c.source_id == source_id)
|
669
|
+
if file_id:
|
670
|
+
main_query = main_query.where(combined_query.c.file_id == file_id)
|
671
|
+
|
672
|
+
# Vector search
|
673
|
+
if embedded_text:
|
674
|
+
if settings.letta_pg_uri_no_default:
|
675
|
+
# PostgreSQL with pgvector
|
676
|
+
main_query = main_query.order_by(combined_query.c.embedding.cosine_distance(embedded_text).asc())
|
677
|
+
else:
|
678
|
+
# SQLite with custom vector type
|
679
|
+
query_embedding_binary = adapt_array(embedded_text)
|
680
|
+
main_query = main_query.order_by(
|
681
|
+
func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
|
682
|
+
combined_query.c.created_at.asc() if ascending else combined_query.c.created_at.desc(),
|
683
|
+
combined_query.c.id.asc(),
|
684
|
+
)
|
685
|
+
else:
|
686
|
+
if query_text:
|
687
|
+
main_query = main_query.where(func.lower(combined_query.c.text).contains(func.lower(query_text)))
|
688
|
+
|
689
|
+
# Handle pagination
|
690
|
+
if before or after:
|
691
|
+
# Create reference CTEs
|
692
|
+
if before:
|
693
|
+
before_ref = select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == before).cte("before_ref")
|
694
|
+
if after:
|
695
|
+
after_ref = select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == after).cte("after_ref")
|
696
|
+
|
697
|
+
if before and after:
|
698
|
+
# Window-based query (get records between before and after)
|
699
|
+
main_query = main_query.where(
|
700
|
+
or_(
|
701
|
+
combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
|
702
|
+
and_(
|
703
|
+
combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
|
704
|
+
combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
|
705
|
+
),
|
706
|
+
)
|
707
|
+
)
|
708
|
+
main_query = main_query.where(
|
709
|
+
or_(
|
710
|
+
combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
|
711
|
+
and_(
|
712
|
+
combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
|
713
|
+
combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
|
714
|
+
),
|
715
|
+
)
|
716
|
+
)
|
717
|
+
else:
|
718
|
+
# Pure pagination (only before or only after)
|
719
|
+
if before:
|
720
|
+
main_query = main_query.where(
|
721
|
+
or_(
|
722
|
+
combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
|
723
|
+
and_(
|
724
|
+
combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
|
725
|
+
combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
|
726
|
+
),
|
727
|
+
)
|
728
|
+
)
|
729
|
+
if after:
|
730
|
+
main_query = main_query.where(
|
731
|
+
or_(
|
732
|
+
combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
|
733
|
+
and_(
|
734
|
+
combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
|
735
|
+
combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
|
736
|
+
),
|
737
|
+
)
|
738
|
+
)
|
739
|
+
|
740
|
+
# Add ordering if not already ordered by similarity
|
741
|
+
if not embed_query:
|
742
|
+
if ascending:
|
743
|
+
main_query = main_query.order_by(
|
744
|
+
combined_query.c.created_at.asc(),
|
745
|
+
combined_query.c.id.asc(),
|
746
|
+
)
|
747
|
+
else:
|
748
|
+
main_query = main_query.order_by(
|
749
|
+
combined_query.c.created_at.desc(),
|
750
|
+
combined_query.c.id.asc(),
|
751
|
+
)
|
752
|
+
|
753
|
+
return main_query
|
754
|
+
|
755
|
+
|
756
|
+
def build_source_passage_query(
|
757
|
+
actor: User,
|
758
|
+
agent_id: Optional[str] = None,
|
759
|
+
file_id: Optional[str] = None,
|
760
|
+
query_text: Optional[str] = None,
|
761
|
+
start_date: Optional[datetime] = None,
|
762
|
+
end_date: Optional[datetime] = None,
|
763
|
+
before: Optional[str] = None,
|
764
|
+
after: Optional[str] = None,
|
765
|
+
source_id: Optional[str] = None,
|
766
|
+
embed_query: bool = False,
|
767
|
+
ascending: bool = True,
|
768
|
+
embedding_config: Optional[EmbeddingConfig] = None,
|
769
|
+
) -> Select:
|
770
|
+
"""Build query for source passages with all filters applied."""
|
771
|
+
|
772
|
+
# Handle embedding for vector search
|
773
|
+
embedded_text = None
|
774
|
+
if embed_query:
|
775
|
+
assert embedding_config is not None, "embedding_config must be specified for vector search"
|
776
|
+
assert query_text is not None, "query_text must be specified for vector search"
|
777
|
+
embedded_text = embedding_model(embedding_config).get_text_embedding(query_text)
|
778
|
+
embedded_text = np.array(embedded_text)
|
779
|
+
embedded_text = np.pad(embedded_text, (0, MAX_EMBEDDING_DIM - embedded_text.shape[0]), mode="constant").tolist()
|
780
|
+
|
781
|
+
# Base query for source passages
|
782
|
+
query = select(SourcePassage).where(SourcePassage.organization_id == actor.organization_id)
|
783
|
+
|
784
|
+
# If agent_id is specified, join with SourcesAgents to get only passages linked to that agent
|
785
|
+
if agent_id is not None:
|
786
|
+
query = query.join(SourcesAgents, SourcesAgents.source_id == SourcePassage.source_id)
|
787
|
+
query = query.where(SourcesAgents.agent_id == agent_id)
|
788
|
+
|
789
|
+
# Apply filters
|
790
|
+
if source_id:
|
791
|
+
query = query.where(SourcePassage.source_id == source_id)
|
792
|
+
if file_id:
|
793
|
+
query = query.where(SourcePassage.file_id == file_id)
|
794
|
+
if start_date:
|
795
|
+
query = query.where(SourcePassage.created_at >= start_date)
|
796
|
+
if end_date:
|
797
|
+
query = query.where(SourcePassage.created_at <= end_date)
|
798
|
+
|
799
|
+
# Handle text search or vector search
|
800
|
+
if embedded_text:
|
801
|
+
if settings.letta_pg_uri_no_default:
|
802
|
+
# PostgreSQL with pgvector
|
803
|
+
query = query.order_by(SourcePassage.embedding.cosine_distance(embedded_text).asc())
|
804
|
+
else:
|
805
|
+
# SQLite with custom vector type
|
806
|
+
query_embedding_binary = adapt_array(embedded_text)
|
807
|
+
query = query.order_by(
|
808
|
+
func.cosine_distance(SourcePassage.embedding, query_embedding_binary).asc(),
|
809
|
+
SourcePassage.created_at.asc() if ascending else SourcePassage.created_at.desc(),
|
810
|
+
SourcePassage.id.asc(),
|
811
|
+
)
|
812
|
+
else:
|
813
|
+
if query_text:
|
814
|
+
query = query.where(func.lower(SourcePassage.text).contains(func.lower(query_text)))
|
815
|
+
|
816
|
+
# Handle pagination
|
817
|
+
if before or after:
|
818
|
+
if before:
|
819
|
+
# Get the reference record
|
820
|
+
before_subq = select(SourcePassage.created_at, SourcePassage.id).where(SourcePassage.id == before).subquery()
|
821
|
+
query = query.where(
|
822
|
+
or_(
|
823
|
+
SourcePassage.created_at < before_subq.c.created_at,
|
824
|
+
and_(
|
825
|
+
SourcePassage.created_at == before_subq.c.created_at,
|
826
|
+
SourcePassage.id < before_subq.c.id,
|
827
|
+
),
|
828
|
+
)
|
829
|
+
)
|
830
|
+
|
831
|
+
if after:
|
832
|
+
# Get the reference record
|
833
|
+
after_subq = select(SourcePassage.created_at, SourcePassage.id).where(SourcePassage.id == after).subquery()
|
834
|
+
query = query.where(
|
835
|
+
or_(
|
836
|
+
SourcePassage.created_at > after_subq.c.created_at,
|
837
|
+
and_(
|
838
|
+
SourcePassage.created_at == after_subq.c.created_at,
|
839
|
+
SourcePassage.id > after_subq.c.id,
|
840
|
+
),
|
841
|
+
)
|
842
|
+
)
|
843
|
+
|
844
|
+
# Apply ordering if not already ordered by similarity
|
845
|
+
if not embed_query:
|
846
|
+
if ascending:
|
847
|
+
query = query.order_by(SourcePassage.created_at.asc(), SourcePassage.id.asc())
|
848
|
+
else:
|
849
|
+
query = query.order_by(SourcePassage.created_at.desc(), SourcePassage.id.asc())
|
850
|
+
|
851
|
+
return query
|
852
|
+
|
853
|
+
|
854
|
+
def build_agent_passage_query(
|
855
|
+
actor: User,
|
856
|
+
agent_id: str, # Required for agent passages
|
857
|
+
query_text: Optional[str] = None,
|
858
|
+
start_date: Optional[datetime] = None,
|
859
|
+
end_date: Optional[datetime] = None,
|
860
|
+
before: Optional[str] = None,
|
861
|
+
after: Optional[str] = None,
|
862
|
+
embed_query: bool = False,
|
863
|
+
ascending: bool = True,
|
864
|
+
embedding_config: Optional[EmbeddingConfig] = None,
|
865
|
+
) -> Select:
|
866
|
+
"""Build query for agent passages with all filters applied."""
|
867
|
+
|
868
|
+
# Handle embedding for vector search
|
869
|
+
embedded_text = None
|
870
|
+
if embed_query:
|
871
|
+
assert embedding_config is not None, "embedding_config must be specified for vector search"
|
872
|
+
assert query_text is not None, "query_text must be specified for vector search"
|
873
|
+
embedded_text = embedding_model(embedding_config).get_text_embedding(query_text)
|
874
|
+
embedded_text = np.array(embedded_text)
|
875
|
+
embedded_text = np.pad(embedded_text, (0, MAX_EMBEDDING_DIM - embedded_text.shape[0]), mode="constant").tolist()
|
876
|
+
|
877
|
+
# Base query for agent passages
|
878
|
+
query = select(AgentPassage).where(AgentPassage.agent_id == agent_id, AgentPassage.organization_id == actor.organization_id)
|
879
|
+
|
880
|
+
# Apply filters
|
881
|
+
if start_date:
|
882
|
+
query = query.where(AgentPassage.created_at >= start_date)
|
883
|
+
if end_date:
|
884
|
+
query = query.where(AgentPassage.created_at <= end_date)
|
885
|
+
|
886
|
+
# Handle text search or vector search
|
887
|
+
if embedded_text:
|
888
|
+
if settings.letta_pg_uri_no_default:
|
889
|
+
# PostgreSQL with pgvector
|
890
|
+
query = query.order_by(AgentPassage.embedding.cosine_distance(embedded_text).asc())
|
891
|
+
else:
|
892
|
+
# SQLite with custom vector type
|
893
|
+
query_embedding_binary = adapt_array(embedded_text)
|
894
|
+
query = query.order_by(
|
895
|
+
func.cosine_distance(AgentPassage.embedding, query_embedding_binary).asc(),
|
896
|
+
AgentPassage.created_at.asc() if ascending else AgentPassage.created_at.desc(),
|
897
|
+
AgentPassage.id.asc(),
|
898
|
+
)
|
899
|
+
else:
|
900
|
+
if query_text:
|
901
|
+
query = query.where(func.lower(AgentPassage.text).contains(func.lower(query_text)))
|
902
|
+
|
903
|
+
# Handle pagination
|
904
|
+
if before or after:
|
905
|
+
if before:
|
906
|
+
# Get the reference record
|
907
|
+
before_subq = select(AgentPassage.created_at, AgentPassage.id).where(AgentPassage.id == before).subquery()
|
908
|
+
query = query.where(
|
909
|
+
or_(
|
910
|
+
AgentPassage.created_at < before_subq.c.created_at,
|
911
|
+
and_(
|
912
|
+
AgentPassage.created_at == before_subq.c.created_at,
|
913
|
+
AgentPassage.id < before_subq.c.id,
|
914
|
+
),
|
915
|
+
)
|
916
|
+
)
|
917
|
+
|
918
|
+
if after:
|
919
|
+
# Get the reference record
|
920
|
+
after_subq = select(AgentPassage.created_at, AgentPassage.id).where(AgentPassage.id == after).subquery()
|
921
|
+
query = query.where(
|
922
|
+
or_(
|
923
|
+
AgentPassage.created_at > after_subq.c.created_at,
|
924
|
+
and_(
|
925
|
+
AgentPassage.created_at == after_subq.c.created_at,
|
926
|
+
AgentPassage.id > after_subq.c.id,
|
927
|
+
),
|
928
|
+
)
|
929
|
+
)
|
930
|
+
|
931
|
+
# Apply ordering if not already ordered by similarity
|
932
|
+
if not embed_query:
|
933
|
+
if ascending:
|
934
|
+
query = query.order_by(AgentPassage.created_at.asc(), AgentPassage.id.asc())
|
935
|
+
else:
|
936
|
+
query = query.order_by(AgentPassage.created_at.desc(), AgentPassage.id.asc())
|
937
|
+
|
938
|
+
return query
|
@@ -7,11 +7,11 @@ from sqlalchemy.exc import NoResultFound
|
|
7
7
|
from letta.orm.agent import Agent as AgentModel
|
8
8
|
from letta.orm.block import Block as BlockModel
|
9
9
|
from letta.orm.identity import Identity as IdentityModel
|
10
|
+
from letta.otel.tracing import trace_method
|
10
11
|
from letta.schemas.identity import Identity as PydanticIdentity
|
11
12
|
from letta.schemas.identity import IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert
|
12
13
|
from letta.schemas.user import User as PydanticUser
|
13
14
|
from letta.server.db import db_registry
|
14
|
-
from letta.tracing import trace_method
|
15
15
|
from letta.utils import enforce_types
|
16
16
|
|
17
17
|
|
letta/services/job_manager.py
CHANGED
@@ -14,6 +14,7 @@ from letta.orm.message import Message as MessageModel
|
|
14
14
|
from letta.orm.sqlalchemy_base import AccessType
|
15
15
|
from letta.orm.step import Step
|
16
16
|
from letta.orm.step import Step as StepModel
|
17
|
+
from letta.otel.tracing import trace_method
|
17
18
|
from letta.schemas.enums import JobStatus, MessageRole
|
18
19
|
from letta.schemas.job import BatchJob as PydanticBatchJob
|
19
20
|
from letta.schemas.job import Job as PydanticJob
|
@@ -25,7 +26,6 @@ from letta.schemas.step import Step as PydanticStep
|
|
25
26
|
from letta.schemas.usage import LettaUsageStatistics
|
26
27
|
from letta.schemas.user import User as PydanticUser
|
27
28
|
from letta.server.db import db_registry
|
28
|
-
from letta.tracing import trace_method
|
29
29
|
from letta.utils import enforce_types
|
30
30
|
|
31
31
|
|
@@ -9,6 +9,7 @@ from letta.log import get_logger
|
|
9
9
|
from letta.orm import Message as MessageModel
|
10
10
|
from letta.orm.llm_batch_items import LLMBatchItem
|
11
11
|
from letta.orm.llm_batch_job import LLMBatchJob
|
12
|
+
from letta.otel.tracing import trace_method
|
12
13
|
from letta.schemas.agent import AgentStepState
|
13
14
|
from letta.schemas.enums import AgentStepStatus, JobStatus, ProviderType
|
14
15
|
from letta.schemas.llm_batch_job import LLMBatchItem as PydanticLLMBatchItem
|
@@ -17,7 +18,6 @@ from letta.schemas.llm_config import LLMConfig
|
|
17
18
|
from letta.schemas.message import Message as PydanticMessage
|
18
19
|
from letta.schemas.user import User as PydanticUser
|
19
20
|
from letta.server.db import db_registry
|
20
|
-
from letta.tracing import trace_method
|
21
21
|
from letta.utils import enforce_types
|
22
22
|
|
23
23
|
logger = get_logger(__name__)
|