letta-nightly 0.8.0.dev20250606195656__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.
Files changed (96) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +1 -1
  3. letta/agents/letta_agent.py +49 -29
  4. letta/agents/letta_agent_batch.py +1 -2
  5. letta/agents/voice_agent.py +19 -13
  6. letta/agents/voice_sleeptime_agent.py +11 -3
  7. letta/constants.py +18 -0
  8. letta/data_sources/__init__.py +0 -0
  9. letta/data_sources/redis_client.py +282 -0
  10. letta/errors.py +0 -4
  11. letta/functions/function_sets/files.py +58 -0
  12. letta/functions/schema_generator.py +18 -1
  13. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  14. letta/helpers/datetime_helpers.py +47 -3
  15. letta/helpers/decorators.py +69 -0
  16. letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
  17. letta/interfaces/anthropic_streaming_interface.py +43 -24
  18. letta/interfaces/openai_streaming_interface.py +21 -19
  19. letta/llm_api/anthropic.py +1 -1
  20. letta/llm_api/anthropic_client.py +22 -14
  21. letta/llm_api/google_vertex_client.py +1 -1
  22. letta/llm_api/helpers.py +36 -30
  23. letta/llm_api/llm_api_tools.py +1 -1
  24. letta/llm_api/llm_client_base.py +29 -1
  25. letta/llm_api/openai.py +1 -1
  26. letta/llm_api/openai_client.py +6 -8
  27. letta/local_llm/chat_completion_proxy.py +1 -1
  28. letta/memory.py +1 -1
  29. letta/orm/enums.py +1 -0
  30. letta/orm/file.py +80 -3
  31. letta/orm/files_agents.py +13 -0
  32. letta/orm/sqlalchemy_base.py +34 -11
  33. letta/otel/__init__.py +0 -0
  34. letta/otel/context.py +25 -0
  35. letta/otel/events.py +0 -0
  36. letta/otel/metric_registry.py +122 -0
  37. letta/otel/metrics.py +66 -0
  38. letta/otel/resource.py +26 -0
  39. letta/{tracing.py → otel/tracing.py} +55 -78
  40. letta/plugins/README.md +22 -0
  41. letta/plugins/__init__.py +0 -0
  42. letta/plugins/defaults.py +11 -0
  43. letta/plugins/plugins.py +72 -0
  44. letta/schemas/enums.py +8 -0
  45. letta/schemas/file.py +12 -0
  46. letta/schemas/tool.py +4 -0
  47. letta/server/db.py +7 -7
  48. letta/server/rest_api/app.py +8 -6
  49. letta/server/rest_api/routers/v1/agents.py +37 -36
  50. letta/server/rest_api/routers/v1/groups.py +3 -3
  51. letta/server/rest_api/routers/v1/sources.py +26 -3
  52. letta/server/rest_api/utils.py +9 -6
  53. letta/server/server.py +18 -12
  54. letta/services/agent_manager.py +185 -193
  55. letta/services/block_manager.py +1 -1
  56. letta/services/context_window_calculator/token_counter.py +3 -2
  57. letta/services/file_processor/chunker/line_chunker.py +34 -0
  58. letta/services/file_processor/file_processor.py +40 -11
  59. letta/services/file_processor/parser/mistral_parser.py +11 -1
  60. letta/services/files_agents_manager.py +96 -7
  61. letta/services/group_manager.py +6 -6
  62. letta/services/helpers/agent_manager_helper.py +373 -3
  63. letta/services/identity_manager.py +1 -1
  64. letta/services/job_manager.py +1 -1
  65. letta/services/llm_batch_manager.py +1 -1
  66. letta/services/message_manager.py +1 -1
  67. letta/services/organization_manager.py +1 -1
  68. letta/services/passage_manager.py +1 -1
  69. letta/services/per_agent_lock_manager.py +1 -1
  70. letta/services/provider_manager.py +1 -1
  71. letta/services/sandbox_config_manager.py +1 -1
  72. letta/services/source_manager.py +178 -19
  73. letta/services/step_manager.py +2 -2
  74. letta/services/summarizer/summarizer.py +1 -1
  75. letta/services/telemetry_manager.py +1 -1
  76. letta/services/tool_executor/builtin_tool_executor.py +117 -0
  77. letta/services/tool_executor/composio_tool_executor.py +53 -0
  78. letta/services/tool_executor/core_tool_executor.py +474 -0
  79. letta/services/tool_executor/files_tool_executor.py +131 -0
  80. letta/services/tool_executor/mcp_tool_executor.py +45 -0
  81. letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
  82. letta/services/tool_executor/tool_execution_manager.py +34 -14
  83. letta/services/tool_executor/tool_execution_sandbox.py +1 -1
  84. letta/services/tool_executor/tool_executor.py +3 -802
  85. letta/services/tool_executor/tool_executor_base.py +43 -0
  86. letta/services/tool_manager.py +55 -59
  87. letta/services/tool_sandbox/e2b_sandbox.py +1 -1
  88. letta/services/tool_sandbox/local_sandbox.py +6 -3
  89. letta/services/user_manager.py +6 -3
  90. letta/settings.py +21 -1
  91. letta/utils.py +7 -2
  92. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/METADATA +4 -2
  93. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/RECORD +96 -74
  94. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/LICENSE +0 -0
  95. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/WHEEL +0 -0
  96. {letta_nightly-0.8.0.dev20250606195656.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 == "text/plain":
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 update_file_agent(
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._get_association(session, agent_id, file_id, actor)
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._get_association(session, agent_id, file_id, actor)
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 get_file_agent(self, *, agent_id: str, file_id: str, actor: PydanticUser) -> Optional[PydanticFileAgent]:
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._get_association(session, agent_id, file_id, actor)
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 _get_association(self, session, agent_id: str, file_id: str, actor: PydanticUser) -> FileAgentModel:
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
@@ -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 modify_group(self, group_id: str, group_update: GroupUpdate, actor: PydanticUser) -> PydanticGroup:
156
- with db_registry.session() as session:
157
- group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
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._process_agent_relationship(
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.update(session, actor=actor)
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
- from sqlalchemy import and_, asc, desc, or_, select
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
 
@@ -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__)