letta-nightly 0.8.0.dev20250606195656__py3-none-any.whl → 0.8.3.dev20250607000559__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 (105) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +16 -12
  3. letta/agents/base_agent.py +1 -1
  4. letta/agents/helpers.py +13 -2
  5. letta/agents/letta_agent.py +72 -34
  6. letta/agents/letta_agent_batch.py +1 -2
  7. letta/agents/voice_agent.py +19 -13
  8. letta/agents/voice_sleeptime_agent.py +23 -6
  9. letta/constants.py +18 -0
  10. letta/data_sources/__init__.py +0 -0
  11. letta/data_sources/redis_client.py +282 -0
  12. letta/errors.py +0 -4
  13. letta/functions/function_sets/files.py +58 -0
  14. letta/functions/schema_generator.py +18 -1
  15. letta/groups/sleeptime_multi_agent_v2.py +13 -3
  16. letta/helpers/datetime_helpers.py +47 -3
  17. letta/helpers/decorators.py +69 -0
  18. letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
  19. letta/interfaces/anthropic_streaming_interface.py +43 -24
  20. letta/interfaces/openai_streaming_interface.py +21 -19
  21. letta/llm_api/anthropic.py +1 -1
  22. letta/llm_api/anthropic_client.py +30 -16
  23. letta/llm_api/google_vertex_client.py +1 -1
  24. letta/llm_api/helpers.py +36 -30
  25. letta/llm_api/llm_api_tools.py +1 -1
  26. letta/llm_api/llm_client_base.py +29 -1
  27. letta/llm_api/openai.py +1 -1
  28. letta/llm_api/openai_client.py +6 -8
  29. letta/local_llm/chat_completion_proxy.py +1 -1
  30. letta/memory.py +1 -1
  31. letta/orm/enums.py +1 -0
  32. letta/orm/file.py +80 -3
  33. letta/orm/files_agents.py +13 -0
  34. letta/orm/passage.py +2 -0
  35. letta/orm/sqlalchemy_base.py +34 -11
  36. letta/otel/__init__.py +0 -0
  37. letta/otel/context.py +25 -0
  38. letta/otel/events.py +0 -0
  39. letta/otel/metric_registry.py +122 -0
  40. letta/otel/metrics.py +66 -0
  41. letta/otel/resource.py +26 -0
  42. letta/{tracing.py → otel/tracing.py} +55 -78
  43. letta/plugins/README.md +22 -0
  44. letta/plugins/__init__.py +0 -0
  45. letta/plugins/defaults.py +11 -0
  46. letta/plugins/plugins.py +72 -0
  47. letta/schemas/enums.py +8 -0
  48. letta/schemas/file.py +12 -0
  49. letta/schemas/letta_request.py +6 -0
  50. letta/schemas/passage.py +1 -0
  51. letta/schemas/tool.py +4 -0
  52. letta/server/db.py +7 -7
  53. letta/server/rest_api/app.py +8 -6
  54. letta/server/rest_api/routers/v1/agents.py +46 -37
  55. letta/server/rest_api/routers/v1/groups.py +3 -3
  56. letta/server/rest_api/routers/v1/sources.py +26 -3
  57. letta/server/rest_api/routers/v1/tools.py +7 -2
  58. letta/server/rest_api/utils.py +9 -6
  59. letta/server/server.py +25 -13
  60. letta/services/agent_manager.py +186 -194
  61. letta/services/block_manager.py +1 -1
  62. letta/services/context_window_calculator/context_window_calculator.py +1 -1
  63. letta/services/context_window_calculator/token_counter.py +3 -2
  64. letta/services/file_processor/chunker/line_chunker.py +34 -0
  65. letta/services/file_processor/file_processor.py +43 -12
  66. letta/services/file_processor/parser/mistral_parser.py +11 -1
  67. letta/services/files_agents_manager.py +96 -7
  68. letta/services/group_manager.py +6 -6
  69. letta/services/helpers/agent_manager_helper.py +404 -3
  70. letta/services/identity_manager.py +1 -1
  71. letta/services/job_manager.py +1 -1
  72. letta/services/llm_batch_manager.py +1 -1
  73. letta/services/mcp/stdio_client.py +5 -1
  74. letta/services/mcp_manager.py +4 -4
  75. letta/services/message_manager.py +1 -1
  76. letta/services/organization_manager.py +1 -1
  77. letta/services/passage_manager.py +604 -19
  78. letta/services/per_agent_lock_manager.py +1 -1
  79. letta/services/provider_manager.py +1 -1
  80. letta/services/sandbox_config_manager.py +1 -1
  81. letta/services/source_manager.py +178 -19
  82. letta/services/step_manager.py +2 -2
  83. letta/services/summarizer/summarizer.py +1 -1
  84. letta/services/telemetry_manager.py +1 -1
  85. letta/services/tool_executor/builtin_tool_executor.py +117 -0
  86. letta/services/tool_executor/composio_tool_executor.py +53 -0
  87. letta/services/tool_executor/core_tool_executor.py +474 -0
  88. letta/services/tool_executor/files_tool_executor.py +138 -0
  89. letta/services/tool_executor/mcp_tool_executor.py +45 -0
  90. letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
  91. letta/services/tool_executor/tool_execution_manager.py +34 -14
  92. letta/services/tool_executor/tool_execution_sandbox.py +1 -1
  93. letta/services/tool_executor/tool_executor.py +3 -802
  94. letta/services/tool_executor/tool_executor_base.py +43 -0
  95. letta/services/tool_manager.py +55 -59
  96. letta/services/tool_sandbox/e2b_sandbox.py +1 -1
  97. letta/services/tool_sandbox/local_sandbox.py +6 -3
  98. letta/services/user_manager.py +6 -3
  99. letta/settings.py +23 -2
  100. letta/utils.py +7 -2
  101. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/METADATA +4 -2
  102. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/RECORD +105 -83
  103. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/LICENSE +0 -0
  104. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/WHEEL +0 -0
  105. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/entry_points.txt +0 -0
@@ -3,9 +3,8 @@ import os
3
3
  from datetime import datetime, timezone
4
4
  from typing import Dict, List, Optional, Set, Tuple
5
5
 
6
- import numpy as np
7
6
  import sqlalchemy as sa
8
- from sqlalchemy import Select, and_, delete, func, insert, literal, or_, select, union_all
7
+ from sqlalchemy import delete, func, insert, literal, or_, select
9
8
  from sqlalchemy.dialects.postgresql import insert as pg_insert
10
9
 
11
10
  from letta.constants import (
@@ -17,10 +16,9 @@ from letta.constants import (
17
16
  BASE_VOICE_SLEEPTIME_CHAT_TOOLS,
18
17
  BASE_VOICE_SLEEPTIME_TOOLS,
19
18
  DATA_SOURCE_ATTACH_ALERT,
20
- MAX_EMBEDDING_DIM,
19
+ FILES_TOOLS,
21
20
  MULTI_AGENT_TOOLS,
22
21
  )
23
- from letta.embeddings import embedding_model
24
22
  from letta.helpers.datetime_helpers import get_utc_time
25
23
  from letta.llm_api.llm_client import LLMClient
26
24
  from letta.log import get_logger
@@ -39,7 +37,7 @@ from letta.orm.errors import NoResultFound
39
37
  from letta.orm.sandbox_config import AgentEnvironmentVariable
40
38
  from letta.orm.sandbox_config import AgentEnvironmentVariable as AgentEnvironmentVariableModel
41
39
  from letta.orm.sqlalchemy_base import AccessType
42
- from letta.orm.sqlite_functions import adapt_array
40
+ from letta.otel.tracing import trace_method
43
41
  from letta.schemas.agent import AgentState as PydanticAgentState
44
42
  from letta.schemas.agent import AgentType, CreateAgent, UpdateAgent, get_prompt_template_for_agent_type
45
43
  from letta.schemas.block import DEFAULT_BLOCKS
@@ -66,6 +64,7 @@ from letta.server.db import db_registry
66
64
  from letta.services.block_manager import BlockManager
67
65
  from letta.services.context_window_calculator.context_window_calculator import ContextWindowCalculator
68
66
  from letta.services.context_window_calculator.token_counter import AnthropicTokenCounter, TiktokenCounter
67
+ from letta.services.files_agents_manager import FileAgentManager
69
68
  from letta.services.helpers.agent_manager_helper import (
70
69
  _apply_filters,
71
70
  _apply_identity_filters,
@@ -74,6 +73,9 @@ from letta.services.helpers.agent_manager_helper import (
74
73
  _apply_tag_filter,
75
74
  _process_relationship,
76
75
  _process_relationship_async,
76
+ build_agent_passage_query,
77
+ build_passage_query,
78
+ build_source_passage_query,
77
79
  check_supports_structured_output,
78
80
  compile_system_message,
79
81
  derive_system_message,
@@ -85,8 +87,6 @@ from letta.services.message_manager import MessageManager
85
87
  from letta.services.passage_manager import PassageManager
86
88
  from letta.services.source_manager import SourceManager
87
89
  from letta.services.tool_manager import ToolManager
88
- from letta.settings import settings
89
- from letta.tracing import trace_method
90
90
  from letta.utils import enforce_types, united_diff
91
91
 
92
92
  logger = get_logger(__name__)
@@ -102,6 +102,7 @@ class AgentManager:
102
102
  self.message_manager = MessageManager()
103
103
  self.passage_manager = PassageManager()
104
104
  self.identity_manager = IdentityManager()
105
+ self.file_agent_manager = FileAgentManager()
105
106
 
106
107
  @staticmethod
107
108
  def _resolve_tools(session, names: Set[str], ids: Set[str], org_id: str) -> Tuple[Dict[str, str], Dict[str, str]]:
@@ -1384,6 +1385,11 @@ class AgentManager:
1384
1385
  curr_system_message = self.get_system_message(
1385
1386
  agent_id=agent_id, actor=actor
1386
1387
  ) # this is the system + memory bank, not just the system prompt
1388
+
1389
+ if curr_system_message is None:
1390
+ logger.warning(f"No system message found for agent {agent_state.id} and user {actor}")
1391
+ return agent_state
1392
+
1387
1393
  curr_system_message_openai = curr_system_message.to_openai_dict()
1388
1394
 
1389
1395
  # note: we only update the system prompt if the core memory is changed
@@ -1451,6 +1457,11 @@ class AgentManager:
1451
1457
  curr_system_message = await self.get_system_message_async(
1452
1458
  agent_id=agent_id, actor=actor
1453
1459
  ) # this is the system + memory bank, not just the system prompt
1460
+
1461
+ if curr_system_message is None:
1462
+ logger.warning(f"No system message found for agent {agent_state.id} and user {actor}")
1463
+ return agent_state
1464
+
1454
1465
  curr_system_message_openai = curr_system_message.to_openai_dict()
1455
1466
 
1456
1467
  # note: we only update the system prompt if the core memory is changed
@@ -1472,7 +1483,7 @@ class AgentManager:
1472
1483
  memory_edit_timestamp = curr_system_message.created_at
1473
1484
 
1474
1485
  num_messages = await self.message_manager.size_async(actor=actor, agent_id=agent_id)
1475
- num_archival_memories = await self.passage_manager.size_async(actor=actor, agent_id=agent_id)
1486
+ num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_id)
1476
1487
 
1477
1488
  # update memory (TODO: potentially update recall/archival stats separately)
1478
1489
  new_system_message_str = compile_system_message(
@@ -1650,12 +1661,18 @@ class AgentManager:
1650
1661
  @trace_method
1651
1662
  @enforce_types
1652
1663
  async def refresh_memory_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
1664
+ # TODO: This will NOT work for new blocks/file blocks added intra-step
1653
1665
  block_ids = [b.id for b in agent_state.memory.blocks]
1654
- if not block_ids:
1655
- return agent_state
1666
+ file_block_names = [b.label for b in agent_state.memory.file_blocks]
1667
+
1668
+ if block_ids:
1669
+ blocks = await self.block_manager.get_all_blocks_by_ids_async(block_ids=[b.id for b in agent_state.memory.blocks], actor=actor)
1670
+ agent_state.memory.blocks = [b for b in blocks if b is not None]
1671
+
1672
+ if file_block_names:
1673
+ file_blocks = await self.file_agent_manager.get_all_file_blocks_by_name(file_names=file_block_names, actor=actor)
1674
+ agent_state.memory.file_blocks = [b for b in file_blocks if b is not None]
1656
1675
 
1657
- blocks = await self.block_manager.get_all_blocks_by_ids_async(block_ids=[b.id for b in agent_state.memory.blocks], actor=actor)
1658
- agent_state.memory.blocks = [b for b in blocks if b is not None]
1659
1676
  return agent_state
1660
1677
 
1661
1678
  # ======================================================================================================================
@@ -2006,184 +2023,6 @@ class AgentManager:
2006
2023
  # ======================================================================================================================
2007
2024
  # Passage Management
2008
2025
  # ======================================================================================================================
2009
- def _build_passage_query(
2010
- self,
2011
- actor: PydanticUser,
2012
- agent_id: Optional[str] = None,
2013
- file_id: Optional[str] = None,
2014
- query_text: Optional[str] = None,
2015
- start_date: Optional[datetime] = None,
2016
- end_date: Optional[datetime] = None,
2017
- before: Optional[str] = None,
2018
- after: Optional[str] = None,
2019
- source_id: Optional[str] = None,
2020
- embed_query: bool = False,
2021
- ascending: bool = True,
2022
- embedding_config: Optional[EmbeddingConfig] = None,
2023
- agent_only: bool = False,
2024
- ) -> Select:
2025
- """Helper function to build the base passage query with all filters applied.
2026
- Supports both before and after pagination across merged source and agent passages.
2027
-
2028
- Returns the query before any limit or count operations are applied.
2029
- """
2030
- embedded_text = None
2031
- if embed_query:
2032
- assert embedding_config is not None, "embedding_config must be specified for vector search"
2033
- assert query_text is not None, "query_text must be specified for vector search"
2034
- embedded_text = embedding_model(embedding_config).get_text_embedding(query_text)
2035
- embedded_text = np.array(embedded_text)
2036
- embedded_text = np.pad(embedded_text, (0, MAX_EMBEDDING_DIM - embedded_text.shape[0]), mode="constant").tolist()
2037
-
2038
- # Start with base query for source passages
2039
- source_passages = None
2040
- if not agent_only: # Include source passages
2041
- if agent_id is not None:
2042
- source_passages = (
2043
- select(SourcePassage, literal(None).label("agent_id"))
2044
- .join(SourcesAgents, SourcesAgents.source_id == SourcePassage.source_id)
2045
- .where(SourcesAgents.agent_id == agent_id)
2046
- .where(SourcePassage.organization_id == actor.organization_id)
2047
- )
2048
- else:
2049
- source_passages = select(SourcePassage, literal(None).label("agent_id")).where(
2050
- SourcePassage.organization_id == actor.organization_id
2051
- )
2052
-
2053
- if source_id:
2054
- source_passages = source_passages.where(SourcePassage.source_id == source_id)
2055
- if file_id:
2056
- source_passages = source_passages.where(SourcePassage.file_id == file_id)
2057
-
2058
- # Add agent passages query
2059
- agent_passages = None
2060
- if agent_id is not None:
2061
- agent_passages = (
2062
- select(
2063
- AgentPassage.id,
2064
- AgentPassage.text,
2065
- AgentPassage.embedding_config,
2066
- AgentPassage.metadata_,
2067
- AgentPassage.embedding,
2068
- AgentPassage.created_at,
2069
- AgentPassage.updated_at,
2070
- AgentPassage.is_deleted,
2071
- AgentPassage._created_by_id,
2072
- AgentPassage._last_updated_by_id,
2073
- AgentPassage.organization_id,
2074
- literal(None).label("file_id"),
2075
- literal(None).label("source_id"),
2076
- AgentPassage.agent_id,
2077
- )
2078
- .where(AgentPassage.agent_id == agent_id)
2079
- .where(AgentPassage.organization_id == actor.organization_id)
2080
- )
2081
-
2082
- # Combine queries
2083
- if source_passages is not None and agent_passages is not None:
2084
- combined_query = union_all(source_passages, agent_passages).cte("combined_passages")
2085
- elif agent_passages is not None:
2086
- combined_query = agent_passages.cte("combined_passages")
2087
- elif source_passages is not None:
2088
- combined_query = source_passages.cte("combined_passages")
2089
- else:
2090
- raise ValueError("No passages found")
2091
-
2092
- # Build main query from combined CTE
2093
- main_query = select(combined_query)
2094
-
2095
- # Apply filters
2096
- if start_date:
2097
- main_query = main_query.where(combined_query.c.created_at >= start_date)
2098
- if end_date:
2099
- main_query = main_query.where(combined_query.c.created_at <= end_date)
2100
- if source_id:
2101
- main_query = main_query.where(combined_query.c.source_id == source_id)
2102
- if file_id:
2103
- main_query = main_query.where(combined_query.c.file_id == file_id)
2104
-
2105
- # Vector search
2106
- if embedded_text:
2107
- if settings.letta_pg_uri_no_default:
2108
- # PostgreSQL with pgvector
2109
- main_query = main_query.order_by(combined_query.c.embedding.cosine_distance(embedded_text).asc())
2110
- else:
2111
- # SQLite with custom vector type
2112
- query_embedding_binary = adapt_array(embedded_text)
2113
- main_query = main_query.order_by(
2114
- func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
2115
- combined_query.c.created_at.asc() if ascending else combined_query.c.created_at.desc(),
2116
- combined_query.c.id.asc(),
2117
- )
2118
- else:
2119
- if query_text:
2120
- main_query = main_query.where(func.lower(combined_query.c.text).contains(func.lower(query_text)))
2121
-
2122
- # Handle pagination
2123
- if before or after:
2124
- # Create reference CTEs
2125
- if before:
2126
- before_ref = select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == before).cte("before_ref")
2127
- if after:
2128
- after_ref = select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == after).cte("after_ref")
2129
-
2130
- if before and after:
2131
- # Window-based query (get records between before and after)
2132
- main_query = main_query.where(
2133
- or_(
2134
- combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
2135
- and_(
2136
- combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
2137
- combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
2138
- ),
2139
- )
2140
- )
2141
- main_query = main_query.where(
2142
- or_(
2143
- combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
2144
- and_(
2145
- combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
2146
- combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
2147
- ),
2148
- )
2149
- )
2150
- else:
2151
- # Pure pagination (only before or only after)
2152
- if before:
2153
- main_query = main_query.where(
2154
- or_(
2155
- combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
2156
- and_(
2157
- combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
2158
- combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
2159
- ),
2160
- )
2161
- )
2162
- if after:
2163
- main_query = main_query.where(
2164
- or_(
2165
- combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
2166
- and_(
2167
- combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
2168
- combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
2169
- ),
2170
- )
2171
- )
2172
-
2173
- # Add ordering if not already ordered by similarity
2174
- if not embed_query:
2175
- if ascending:
2176
- main_query = main_query.order_by(
2177
- combined_query.c.created_at.asc(),
2178
- combined_query.c.id.asc(),
2179
- )
2180
- else:
2181
- main_query = main_query.order_by(
2182
- combined_query.c.created_at.desc(),
2183
- combined_query.c.id.asc(),
2184
- )
2185
-
2186
- return main_query
2187
2026
 
2188
2027
  @trace_method
2189
2028
  @enforce_types
@@ -2206,7 +2045,7 @@ class AgentManager:
2206
2045
  ) -> List[PydanticPassage]:
2207
2046
  """Lists all passages attached to an agent."""
2208
2047
  with db_registry.session() as session:
2209
- main_query = self._build_passage_query(
2048
+ main_query = build_passage_query(
2210
2049
  actor=actor,
2211
2050
  agent_id=agent_id,
2212
2051
  file_id=file_id,
@@ -2236,6 +2075,7 @@ class AgentManager:
2236
2075
  # This is an AgentPassage - remove source fields
2237
2076
  data.pop("source_id", None)
2238
2077
  data.pop("file_id", None)
2078
+ data.pop("file_name", None)
2239
2079
  passage = AgentPassage(**data)
2240
2080
  else:
2241
2081
  # This is a SourcePassage - remove agent field
@@ -2266,7 +2106,7 @@ class AgentManager:
2266
2106
  ) -> List[PydanticPassage]:
2267
2107
  """Lists all passages attached to an agent."""
2268
2108
  async with db_registry.async_session() as session:
2269
- main_query = self._build_passage_query(
2109
+ main_query = build_passage_query(
2270
2110
  actor=actor,
2271
2111
  agent_id=agent_id,
2272
2112
  file_id=file_id,
@@ -2296,6 +2136,7 @@ class AgentManager:
2296
2136
  # This is an AgentPassage - remove source fields
2297
2137
  data.pop("source_id", None)
2298
2138
  data.pop("file_id", None)
2139
+ data.pop("file_name", None)
2299
2140
  passage = AgentPassage(**data)
2300
2141
  else:
2301
2142
  # This is a SourcePassage - remove agent field
@@ -2305,6 +2146,98 @@ class AgentManager:
2305
2146
 
2306
2147
  return [p.to_pydantic() for p in passages]
2307
2148
 
2149
+ @trace_method
2150
+ @enforce_types
2151
+ async def list_source_passages_async(
2152
+ self,
2153
+ actor: PydanticUser,
2154
+ agent_id: Optional[str] = None,
2155
+ file_id: Optional[str] = None,
2156
+ limit: Optional[int] = 50,
2157
+ query_text: Optional[str] = None,
2158
+ start_date: Optional[datetime] = None,
2159
+ end_date: Optional[datetime] = None,
2160
+ before: Optional[str] = None,
2161
+ after: Optional[str] = None,
2162
+ source_id: Optional[str] = None,
2163
+ embed_query: bool = False,
2164
+ ascending: bool = True,
2165
+ embedding_config: Optional[EmbeddingConfig] = None,
2166
+ ) -> List[PydanticPassage]:
2167
+ """Lists all passages attached to an agent."""
2168
+ async with db_registry.async_session() as session:
2169
+ main_query = build_source_passage_query(
2170
+ actor=actor,
2171
+ agent_id=agent_id,
2172
+ file_id=file_id,
2173
+ query_text=query_text,
2174
+ start_date=start_date,
2175
+ end_date=end_date,
2176
+ before=before,
2177
+ after=after,
2178
+ source_id=source_id,
2179
+ embed_query=embed_query,
2180
+ ascending=ascending,
2181
+ embedding_config=embedding_config,
2182
+ )
2183
+
2184
+ # Add limit
2185
+ if limit:
2186
+ main_query = main_query.limit(limit)
2187
+
2188
+ # Execute query
2189
+ result = await session.execute(main_query)
2190
+
2191
+ # Get ORM objects directly using scalars()
2192
+ passages = result.scalars().all()
2193
+
2194
+ # Convert to Pydantic models
2195
+ return [p.to_pydantic() for p in passages]
2196
+
2197
+ @trace_method
2198
+ @enforce_types
2199
+ async def list_agent_passages_async(
2200
+ self,
2201
+ actor: PydanticUser,
2202
+ agent_id: Optional[str] = None,
2203
+ limit: Optional[int] = 50,
2204
+ query_text: Optional[str] = None,
2205
+ start_date: Optional[datetime] = None,
2206
+ end_date: Optional[datetime] = None,
2207
+ before: Optional[str] = None,
2208
+ after: Optional[str] = None,
2209
+ embed_query: bool = False,
2210
+ ascending: bool = True,
2211
+ embedding_config: Optional[EmbeddingConfig] = None,
2212
+ ) -> List[PydanticPassage]:
2213
+ """Lists all passages attached to an agent."""
2214
+ async with db_registry.async_session() as session:
2215
+ main_query = build_agent_passage_query(
2216
+ actor=actor,
2217
+ agent_id=agent_id,
2218
+ query_text=query_text,
2219
+ start_date=start_date,
2220
+ end_date=end_date,
2221
+ before=before,
2222
+ after=after,
2223
+ embed_query=embed_query,
2224
+ ascending=ascending,
2225
+ embedding_config=embedding_config,
2226
+ )
2227
+
2228
+ # Add limit
2229
+ if limit:
2230
+ main_query = main_query.limit(limit)
2231
+
2232
+ # Execute query
2233
+ result = await session.execute(main_query)
2234
+
2235
+ # Get ORM objects directly using scalars()
2236
+ passages = result.scalars().all()
2237
+
2238
+ # Convert to Pydantic models
2239
+ return [p.to_pydantic() for p in passages]
2240
+
2308
2241
  @trace_method
2309
2242
  @enforce_types
2310
2243
  def passage_size(
@@ -2325,7 +2258,7 @@ class AgentManager:
2325
2258
  ) -> int:
2326
2259
  """Returns the count of passages matching the given criteria."""
2327
2260
  with db_registry.session() as session:
2328
- main_query = self._build_passage_query(
2261
+ main_query = build_passage_query(
2329
2262
  actor=actor,
2330
2263
  agent_id=agent_id,
2331
2264
  file_id=file_id,
@@ -2363,7 +2296,7 @@ class AgentManager:
2363
2296
  agent_only: bool = False,
2364
2297
  ) -> int:
2365
2298
  async with db_registry.async_session() as session:
2366
- main_query = self._build_passage_query(
2299
+ main_query = build_passage_query(
2367
2300
  actor=actor,
2368
2301
  agent_id=agent_id,
2369
2302
  file_id=file_id,
@@ -2458,6 +2391,65 @@ class AgentManager:
2458
2391
  await agent.update_async(session, actor=actor)
2459
2392
  return await agent.to_pydantic_async()
2460
2393
 
2394
+ @trace_method
2395
+ @enforce_types
2396
+ async def attach_missing_files_tools_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
2397
+ """
2398
+ Attaches missing core file tools to an agent.
2399
+
2400
+ Args:
2401
+ agent_id: ID of the agent to attach the tools to.
2402
+ actor: User performing the action.
2403
+
2404
+ Raises:
2405
+ NoResultFound: If the agent or tool is not found.
2406
+
2407
+ Returns:
2408
+ PydanticAgentState: The updated agent state.
2409
+ """
2410
+ # Check if the agent is missing any files tools
2411
+ core_tool_names = {tool.name for tool in agent_state.tools if tool.tool_type == ToolType.LETTA_FILES_CORE}
2412
+ missing_tool_names = set(FILES_TOOLS).difference(core_tool_names)
2413
+
2414
+ for tool_name in missing_tool_names:
2415
+ tool_id = await self.tool_manager.get_tool_id_by_name_async(tool_name=tool_name, actor=actor)
2416
+
2417
+ # TODO: This is hacky and deserves a rethink - how do we keep all the base tools available in every org always?
2418
+ if not tool_id:
2419
+ await self.tool_manager.upsert_base_tools_async(actor=actor, allowed_types={ToolType.LETTA_FILES_CORE})
2420
+
2421
+ # TODO: Inefficient - I think this re-retrieves the agent_state?
2422
+ agent_state = await self.attach_tool_async(agent_id=agent_state.id, tool_id=tool_id, actor=actor)
2423
+
2424
+ return agent_state
2425
+
2426
+ @trace_method
2427
+ @enforce_types
2428
+ async def detach_all_files_tools_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
2429
+ """
2430
+ Detach all core file tools from an agent.
2431
+
2432
+ Args:
2433
+ agent_id: ID of the agent to detach the tools from.
2434
+ actor: User performing the action.
2435
+
2436
+ Raises:
2437
+ NoResultFound: If the agent or tool is not found.
2438
+
2439
+ Returns:
2440
+ PydanticAgentState: The updated agent state.
2441
+ """
2442
+ # Check if the agent is missing any files tools
2443
+ core_tool_names = {tool.name for tool in agent_state.tools if tool.tool_type == ToolType.LETTA_FILES_CORE}
2444
+
2445
+ for tool_name in core_tool_names:
2446
+ tool_id = await self.tool_manager.get_tool_id_by_name_async(tool_name=tool_name, actor=actor)
2447
+
2448
+ # TODO: Inefficient - I think this re-retrieves the agent_state?
2449
+ agent_state = await self.detach_tool_async(agent_id=agent_state.id, tool_id=tool_id, actor=actor)
2450
+
2451
+ return agent_state
2452
+
2461
2453
  @trace_method
2462
2454
  @enforce_types
2463
2455
  def detach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
@@ -9,12 +9,12 @@ from letta.orm.block import Block as BlockModel
9
9
  from letta.orm.block_history import BlockHistory
10
10
  from letta.orm.enums import ActorType
11
11
  from letta.orm.errors import NoResultFound
12
+ from letta.otel.tracing import trace_method
12
13
  from letta.schemas.agent import AgentState as PydanticAgentState
13
14
  from letta.schemas.block import Block as PydanticBlock
14
15
  from letta.schemas.block import BlockUpdate
15
16
  from letta.schemas.user import User as PydanticUser
16
17
  from letta.server.db import db_registry
17
- from letta.tracing import trace_method
18
18
  from letta.utils import enforce_types
19
19
 
20
20
  logger = get_logger(__name__)
@@ -63,7 +63,7 @@ class ContextWindowCalculator:
63
63
  # Fetch data concurrently
64
64
  (in_context_messages, passage_manager_size, message_manager_size) = await asyncio.gather(
65
65
  message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor),
66
- passage_manager.size_async(actor=actor, agent_id=agent_state.id),
66
+ passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_state.id),
67
67
  message_manager.size_async(actor=actor, agent_id=agent_state.id),
68
68
  )
69
69
 
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from typing import Any, Dict, List
3
3
 
4
4
  from letta.llm_api.anthropic_client import AnthropicClient
5
+ from letta.schemas.openai.chat_completion_request import Tool as OpenAITool
5
6
  from letta.utils import count_tokens
6
7
 
7
8
 
@@ -42,7 +43,7 @@ class AnthropicTokenCounter(TokenCounter):
42
43
  return 0
43
44
  return await self.client.count_tokens(model=self.model, messages=messages)
44
45
 
45
- async def count_tool_tokens(self, tools: List[Any]) -> int:
46
+ async def count_tool_tokens(self, tools: List[OpenAITool]) -> int:
46
47
  if not tools:
47
48
  return 0
48
49
  return await self.client.count_tokens(model=self.model, tools=tools)
@@ -69,7 +70,7 @@ class TiktokenCounter(TokenCounter):
69
70
 
70
71
  return num_tokens_from_messages(messages=messages, model=self.model)
71
72
 
72
- async def count_tool_tokens(self, tools: List[Any]) -> int:
73
+ async def count_tool_tokens(self, tools: List[OpenAITool]) -> int:
73
74
  if not tools:
74
75
  return 0
75
76
  from letta.local_llm.utils import num_tokens_from_functions
@@ -0,0 +1,34 @@
1
+ from typing import List, Optional
2
+
3
+ from letta.log import get_logger
4
+
5
+ logger = get_logger(__name__)
6
+
7
+
8
+ class LineChunker:
9
+ """Newline chunker"""
10
+
11
+ def __init__(self):
12
+ pass
13
+
14
+ # TODO: Make this more general beyond Mistral
15
+ def chunk_text(self, text: str, start: Optional[int] = None, end: Optional[int] = None) -> List[str]:
16
+ """Split lines"""
17
+ content_lines = [line.strip() for line in text.split("\n") if line.strip()]
18
+ total_lines = len(content_lines)
19
+
20
+ if start and end:
21
+ content_lines = content_lines[start:end]
22
+ line_offset = start
23
+ else:
24
+ line_offset = 0
25
+
26
+ content_lines = [f"Line {i + line_offset}: {line}" for i, line in enumerate(content_lines)]
27
+
28
+ # Add metadata about total lines
29
+ if start and end:
30
+ content_lines.insert(0, f"[Viewing lines {start} to {end} (out of {total_lines} lines)]")
31
+ else:
32
+ content_lines.insert(0, f"[Viewing file start (out of {total_lines} lines)]")
33
+
34
+ return content_lines