letta-nightly 0.8.15.dev20250719104256__py3-none-any.whl → 0.8.16.dev20250721070720__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 (99) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +27 -11
  3. letta/agents/helpers.py +1 -1
  4. letta/agents/letta_agent.py +518 -322
  5. letta/agents/letta_agent_batch.py +1 -2
  6. letta/agents/voice_agent.py +15 -17
  7. letta/client/client.py +3 -3
  8. letta/constants.py +5 -0
  9. letta/embeddings.py +0 -2
  10. letta/errors.py +8 -0
  11. letta/functions/function_sets/base.py +3 -3
  12. letta/functions/helpers.py +2 -3
  13. letta/groups/sleeptime_multi_agent.py +0 -1
  14. letta/helpers/composio_helpers.py +2 -2
  15. letta/helpers/converters.py +1 -1
  16. letta/helpers/pinecone_utils.py +8 -0
  17. letta/helpers/tool_rule_solver.py +13 -18
  18. letta/llm_api/aws_bedrock.py +16 -2
  19. letta/llm_api/cohere.py +1 -1
  20. letta/llm_api/openai_client.py +1 -1
  21. letta/local_llm/grammars/gbnf_grammar_generator.py +1 -1
  22. letta/local_llm/llm_chat_completion_wrappers/zephyr.py +14 -14
  23. letta/local_llm/utils.py +1 -2
  24. letta/orm/agent.py +3 -3
  25. letta/orm/block.py +4 -4
  26. letta/orm/files_agents.py +0 -1
  27. letta/orm/identity.py +2 -0
  28. letta/orm/mcp_server.py +0 -2
  29. letta/orm/message.py +140 -14
  30. letta/orm/organization.py +5 -5
  31. letta/orm/passage.py +4 -4
  32. letta/orm/source.py +1 -1
  33. letta/orm/sqlalchemy_base.py +61 -39
  34. letta/orm/step.py +2 -0
  35. letta/otel/db_pool_monitoring.py +308 -0
  36. letta/otel/metric_registry.py +94 -1
  37. letta/otel/sqlalchemy_instrumentation.py +548 -0
  38. letta/otel/sqlalchemy_instrumentation_integration.py +124 -0
  39. letta/otel/tracing.py +37 -1
  40. letta/schemas/agent.py +0 -3
  41. letta/schemas/agent_file.py +283 -0
  42. letta/schemas/block.py +0 -3
  43. letta/schemas/file.py +28 -26
  44. letta/schemas/letta_message.py +15 -4
  45. letta/schemas/memory.py +1 -1
  46. letta/schemas/message.py +31 -26
  47. letta/schemas/openai/chat_completion_response.py +0 -1
  48. letta/schemas/providers.py +20 -0
  49. letta/schemas/source.py +11 -13
  50. letta/schemas/step.py +12 -0
  51. letta/schemas/tool.py +0 -4
  52. letta/serialize_schemas/marshmallow_agent.py +14 -1
  53. letta/serialize_schemas/marshmallow_block.py +23 -1
  54. letta/serialize_schemas/marshmallow_message.py +1 -3
  55. letta/serialize_schemas/marshmallow_tool.py +23 -1
  56. letta/server/db.py +110 -6
  57. letta/server/rest_api/app.py +85 -73
  58. letta/server/rest_api/routers/v1/agents.py +68 -53
  59. letta/server/rest_api/routers/v1/blocks.py +2 -2
  60. letta/server/rest_api/routers/v1/jobs.py +3 -0
  61. letta/server/rest_api/routers/v1/organizations.py +2 -2
  62. letta/server/rest_api/routers/v1/sources.py +18 -2
  63. letta/server/rest_api/routers/v1/tools.py +11 -12
  64. letta/server/rest_api/routers/v1/users.py +1 -1
  65. letta/server/rest_api/streaming_response.py +13 -5
  66. letta/server/rest_api/utils.py +8 -25
  67. letta/server/server.py +11 -4
  68. letta/server/ws_api/server.py +2 -2
  69. letta/services/agent_file_manager.py +616 -0
  70. letta/services/agent_manager.py +133 -46
  71. letta/services/block_manager.py +38 -17
  72. letta/services/file_manager.py +106 -21
  73. letta/services/file_processor/file_processor.py +93 -0
  74. letta/services/files_agents_manager.py +28 -0
  75. letta/services/group_manager.py +4 -5
  76. letta/services/helpers/agent_manager_helper.py +57 -9
  77. letta/services/identity_manager.py +22 -0
  78. letta/services/job_manager.py +210 -91
  79. letta/services/llm_batch_manager.py +9 -6
  80. letta/services/mcp/stdio_client.py +1 -2
  81. letta/services/mcp_manager.py +0 -1
  82. letta/services/message_manager.py +49 -26
  83. letta/services/passage_manager.py +0 -1
  84. letta/services/provider_manager.py +1 -1
  85. letta/services/source_manager.py +114 -5
  86. letta/services/step_manager.py +36 -4
  87. letta/services/telemetry_manager.py +9 -2
  88. letta/services/tool_executor/builtin_tool_executor.py +5 -1
  89. letta/services/tool_executor/core_tool_executor.py +3 -3
  90. letta/services/tool_manager.py +95 -20
  91. letta/services/user_manager.py +4 -12
  92. letta/settings.py +23 -6
  93. letta/system.py +1 -1
  94. letta/utils.py +26 -2
  95. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/METADATA +3 -2
  96. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/RECORD +99 -94
  97. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/LICENSE +0 -0
  98. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/WHEEL +0 -0
  99. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,308 @@
1
+ import time
2
+ from typing import Any
3
+
4
+ from sqlalchemy import Engine, PoolProxiedConnection, QueuePool, event
5
+ from sqlalchemy.engine.interfaces import DBAPIConnection
6
+ from sqlalchemy.ext.asyncio import AsyncEngine
7
+ from sqlalchemy.pool import ConnectionPoolEntry, Pool
8
+
9
+ from letta.helpers.datetime_helpers import get_utc_timestamp_ns, ns_to_ms
10
+ from letta.log import get_logger
11
+ from letta.otel.context import get_ctx_attributes
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class DatabasePoolMonitor:
17
+ """Monitor database connection pool metrics and events using SQLAlchemy event listeners."""
18
+
19
+ def __init__(self):
20
+ self._active_connections: dict[int, dict[str, Any]] = {}
21
+ self._pool_stats: dict[str, dict[str, Any]] = {}
22
+
23
+ def setup_monitoring(self, engine: Engine | AsyncEngine, engine_name: str = "default") -> None:
24
+ """Set up connection pool monitoring for the given engine."""
25
+ if not hasattr(engine, "pool"):
26
+ logger.warning(f"Engine {engine_name} does not have a pool attribute")
27
+ return
28
+
29
+ try:
30
+ self._setup_pool_listeners(engine.pool, engine_name)
31
+ logger.info(f"Database pool monitoring initialized for engine: {engine_name}")
32
+ except Exception as e:
33
+ logger.error(f"Failed to setup pool monitoring for {engine_name}: {e}")
34
+
35
+ def _setup_pool_listeners(self, pool: Pool, engine_name: str) -> None:
36
+ """Set up event listeners for the connection pool."""
37
+
38
+ @event.listens_for(pool, "connect")
39
+ def on_connect(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry):
40
+ """Called when a new connection is created."""
41
+ connection_id = id(connection_record)
42
+
43
+ self._active_connections[connection_id] = {
44
+ "engine_name": engine_name,
45
+ "created_at": time.time(),
46
+ "checked_out_at": None,
47
+ "checked_in_at": None,
48
+ "checkout_count": 0,
49
+ }
50
+
51
+ try:
52
+ from letta.otel.metric_registry import MetricRegistry
53
+
54
+ attrs = {
55
+ "engine_name": engine_name,
56
+ "event": "connect",
57
+ **get_ctx_attributes(),
58
+ }
59
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
60
+ except Exception as e:
61
+ logger.info(f"Failed to record connection event metric: {e}")
62
+
63
+ @event.listens_for(pool, "first_connect")
64
+ def on_first_connect(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry):
65
+ """Called when the first connection is created."""
66
+ try:
67
+ from letta.otel.metric_registry import MetricRegistry
68
+
69
+ attrs = {
70
+ "engine_name": engine_name,
71
+ "event": "first_connect",
72
+ **get_ctx_attributes(),
73
+ }
74
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
75
+ logger.info(f"First connection established for engine: {engine_name}")
76
+ except Exception as e:
77
+ logger.info(f"Failed to record first_connect event metric: {e}")
78
+
79
+ @event.listens_for(pool, "checkout")
80
+ def on_checkout(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry, connection_proxy: PoolProxiedConnection):
81
+ """Called when a connection is checked out from the pool."""
82
+ connection_id = id(connection_record)
83
+ checkout_start_ns = get_utc_timestamp_ns()
84
+
85
+ if connection_id in self._active_connections:
86
+ self._active_connections[connection_id]["checked_out_at_ns"] = checkout_start_ns
87
+ self._active_connections[connection_id]["checkout_count"] += 1
88
+
89
+ try:
90
+ from letta.otel.metric_registry import MetricRegistry
91
+
92
+ # Record current pool statistics
93
+ pool_stats = self._get_pool_stats(pool)
94
+ attrs = {
95
+ "engine_name": engine_name,
96
+ **get_ctx_attributes(),
97
+ }
98
+
99
+ MetricRegistry().db_pool_connections_checked_out_gauge.set(pool_stats["checked_out"], attributes=attrs)
100
+ MetricRegistry().db_pool_connections_available_gauge.set(pool_stats["available"], attributes=attrs)
101
+ MetricRegistry().db_pool_connections_total_gauge.set(pool_stats["total"], attributes=attrs)
102
+ if pool_stats["overflow"] is not None:
103
+ MetricRegistry().db_pool_connections_overflow_gauge.set(pool_stats["overflow"], attributes=attrs)
104
+
105
+ # Record checkout event
106
+ attrs["event"] = "checkout"
107
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
108
+
109
+ except Exception as e:
110
+ logger.info(f"Failed to record checkout event metric: {e}")
111
+
112
+ @event.listens_for(pool, "checkin")
113
+ def on_checkin(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry):
114
+ """Called when a connection is checked back into the pool."""
115
+ connection_id = id(connection_record)
116
+ checkin_time_ns = get_utc_timestamp_ns()
117
+
118
+ if connection_id in self._active_connections:
119
+ conn_info = self._active_connections[connection_id]
120
+ conn_info["checkin_time_ns"] = checkin_time_ns
121
+
122
+ # Calculate connection duration if we have checkout time
123
+ if conn_info["checked_out_at_ns"]:
124
+ duration_ms = ns_to_ms(checkin_time_ns - conn_info["checked_out_at_ns"])
125
+
126
+ try:
127
+ from letta.otel.metric_registry import MetricRegistry
128
+
129
+ attrs = {
130
+ "engine_name": engine_name,
131
+ **get_ctx_attributes(),
132
+ }
133
+ MetricRegistry().db_pool_connection_duration_ms_histogram.record(duration_ms, attributes=attrs)
134
+ except Exception as e:
135
+ logger.info(f"Failed to record connection duration metric: {e}")
136
+
137
+ try:
138
+ from letta.otel.metric_registry import MetricRegistry
139
+
140
+ # Record current pool statistics after checkin
141
+ pool_stats = self._get_pool_stats(pool)
142
+ attrs = {
143
+ "engine_name": engine_name,
144
+ **get_ctx_attributes(),
145
+ }
146
+
147
+ MetricRegistry().db_pool_connections_checked_out_gauge.set(pool_stats["checked_out"], attributes=attrs)
148
+ MetricRegistry().db_pool_connections_available_gauge.set(pool_stats["available"], attributes=attrs)
149
+
150
+ # Record checkin event
151
+ attrs["event"] = "checkin"
152
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
153
+
154
+ except Exception as e:
155
+ logger.info(f"Failed to record checkin event metric: {e}")
156
+
157
+ @event.listens_for(pool, "invalidate")
158
+ def on_invalidate(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry, exception):
159
+ """Called when a connection is invalidated."""
160
+ connection_id = id(connection_record)
161
+
162
+ if connection_id in self._active_connections:
163
+ del self._active_connections[connection_id]
164
+
165
+ try:
166
+ from letta.otel.metric_registry import MetricRegistry
167
+
168
+ attrs = {
169
+ "engine_name": engine_name,
170
+ "event": "invalidate",
171
+ "exception_type": type(exception).__name__ if exception else "unknown",
172
+ **get_ctx_attributes(),
173
+ }
174
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
175
+ MetricRegistry().db_pool_connection_errors_counter.add(1, attributes=attrs)
176
+ except Exception as e:
177
+ logger.info(f"Failed to record invalidate event metric: {e}")
178
+
179
+ @event.listens_for(pool, "soft_invalidate")
180
+ def on_soft_invalidate(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry, exception):
181
+ """Called when a connection is soft invalidated."""
182
+ try:
183
+ from letta.otel.metric_registry import MetricRegistry
184
+
185
+ attrs = {
186
+ "engine_name": engine_name,
187
+ "event": "soft_invalidate",
188
+ "exception_type": type(exception).__name__ if exception else "unknown",
189
+ **get_ctx_attributes(),
190
+ }
191
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
192
+ logger.debug(f"Connection soft invalidated for engine: {engine_name}")
193
+ except Exception as e:
194
+ logger.info(f"Failed to record soft_invalidate event metric: {e}")
195
+
196
+ @event.listens_for(pool, "close")
197
+ def on_close(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry):
198
+ """Called when a connection is closed."""
199
+ connection_id = id(connection_record)
200
+
201
+ if connection_id in self._active_connections:
202
+ del self._active_connections[connection_id]
203
+
204
+ try:
205
+ from letta.otel.metric_registry import MetricRegistry
206
+
207
+ attrs = {
208
+ "engine_name": engine_name,
209
+ "event": "close",
210
+ **get_ctx_attributes(),
211
+ }
212
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
213
+ except Exception as e:
214
+ logger.info(f"Failed to record close event metric: {e}")
215
+
216
+ @event.listens_for(pool, "close_detached")
217
+ def on_close_detached(dbapi_connection: DBAPIConnection):
218
+ """Called when a detached connection is closed."""
219
+ try:
220
+ from letta.otel.metric_registry import MetricRegistry
221
+
222
+ attrs = {
223
+ "engine_name": engine_name,
224
+ "event": "close_detached",
225
+ **get_ctx_attributes(),
226
+ }
227
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
228
+ logger.debug(f"Detached connection closed for engine: {engine_name}")
229
+ except Exception as e:
230
+ logger.info(f"Failed to record close_detached event metric: {e}")
231
+
232
+ @event.listens_for(pool, "detach")
233
+ def on_detach(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry):
234
+ """Called when a connection is detached from the pool."""
235
+ connection_id = id(connection_record)
236
+
237
+ if connection_id in self._active_connections:
238
+ self._active_connections[connection_id]["detached"] = True
239
+
240
+ try:
241
+ from letta.otel.metric_registry import MetricRegistry
242
+
243
+ attrs = {
244
+ "engine_name": engine_name,
245
+ "event": "detach",
246
+ **get_ctx_attributes(),
247
+ }
248
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
249
+ logger.debug(f"Connection detached from pool for engine: {engine_name}")
250
+ except Exception as e:
251
+ logger.info(f"Failed to record detach event metric: {e}")
252
+
253
+ @event.listens_for(pool, "reset")
254
+ def on_reset(dbapi_connection: DBAPIConnection, connection_record: ConnectionPoolEntry):
255
+ """Called when a connection is reset."""
256
+ try:
257
+ from letta.otel.metric_registry import MetricRegistry
258
+
259
+ attrs = {
260
+ "engine_name": engine_name,
261
+ "event": "reset",
262
+ **get_ctx_attributes(),
263
+ }
264
+ MetricRegistry().db_pool_connection_events_counter.add(1, attributes=attrs)
265
+ logger.debug(f"Connection reset for engine: {engine_name}")
266
+ except Exception as e:
267
+ logger.info(f"Failed to record reset event metric: {e}")
268
+
269
+ # Note: dispatch is not a listenable event, it's a method for custom events
270
+ # If you need to track custom dispatch events, you would need to implement them separately
271
+
272
+ # noinspection PyProtectedMember
273
+ @staticmethod
274
+ def _get_pool_stats(pool: Pool) -> dict[str, Any]:
275
+ """Get current pool statistics."""
276
+ stats = {
277
+ "total": 0,
278
+ "checked_out": 0,
279
+ "available": 0,
280
+ "overflow": None,
281
+ }
282
+
283
+ try:
284
+ if not isinstance(pool, QueuePool):
285
+ logger.info("Not currently supported for non-QueuePools")
286
+
287
+ stats["total"] = pool._pool.maxsize
288
+ stats["available"] = pool._pool.qsize()
289
+ stats["overflow"] = pool._overflow
290
+ stats["checked_out"] = stats["total"] - stats["available"]
291
+
292
+ except Exception as e:
293
+ logger.info(f"Failed to get pool stats: {e}")
294
+ return stats
295
+
296
+
297
+ # Global instance
298
+ _pool_monitor = DatabasePoolMonitor()
299
+
300
+
301
+ def get_pool_monitor() -> DatabasePoolMonitor:
302
+ """Get the global database pool monitor instance."""
303
+ return _pool_monitor
304
+
305
+
306
+ def setup_pool_monitoring(engine: Engine | AsyncEngine, engine_name: str = "default") -> None:
307
+ """Set up connection pool monitoring for the given engine."""
308
+ _pool_monitor.setup_monitoring(engine, engine_name)
@@ -3,6 +3,7 @@ from functools import partial
3
3
 
4
4
  from opentelemetry import metrics
5
5
  from opentelemetry.metrics import Counter, Histogram
6
+ from opentelemetry.metrics._internal import Gauge
6
7
 
7
8
  from letta.helpers.singleton import singleton
8
9
  from letta.otel.metrics import get_letta_meter
@@ -27,7 +28,7 @@ class MetricRegistry:
27
28
  agent_id -1:N -> tool_name
28
29
  """
29
30
 
30
- Instrument = Counter | Histogram
31
+ Instrument = Counter | Histogram | Gauge
31
32
  _metrics: dict[str, Instrument] = field(default_factory=dict, init=False)
32
33
  _meter: metrics.Meter = field(init=False)
33
34
 
@@ -180,3 +181,95 @@ class MetricRegistry:
180
181
  unit="By",
181
182
  ),
182
183
  )
184
+
185
+ # Database connection pool metrics
186
+ # (includes engine_name)
187
+ @property
188
+ def db_pool_connections_total_gauge(self) -> Gauge:
189
+ return self._get_or_create_metric(
190
+ "gauge_db_pool_connections_total",
191
+ partial(
192
+ self._meter.create_gauge,
193
+ name="gauge_db_pool_connections_total",
194
+ description="Total number of connections in the database pool",
195
+ unit="1",
196
+ ),
197
+ )
198
+
199
+ # (includes engine_name)
200
+ @property
201
+ def db_pool_connections_checked_out_gauge(self) -> Gauge:
202
+ return self._get_or_create_metric(
203
+ "gauge_db_pool_connections_checked_out",
204
+ partial(
205
+ self._meter.create_gauge,
206
+ name="gauge_db_pool_connections_checked_out",
207
+ description="Number of connections currently checked out from the pool",
208
+ unit="1",
209
+ ),
210
+ )
211
+
212
+ # (includes engine_name)
213
+ @property
214
+ def db_pool_connections_available_gauge(self) -> Gauge:
215
+ return self._get_or_create_metric(
216
+ "gauge_db_pool_connections_available",
217
+ partial(
218
+ self._meter.create_gauge,
219
+ name="gauge_db_pool_connections_available",
220
+ description="Number of available connections in the pool",
221
+ unit="1",
222
+ ),
223
+ )
224
+
225
+ # (includes engine_name)
226
+ @property
227
+ def db_pool_connections_overflow_gauge(self) -> Gauge:
228
+ return self._get_or_create_metric(
229
+ "gauge_db_pool_connections_overflow",
230
+ partial(
231
+ self._meter.create_gauge,
232
+ name="gauge_db_pool_connections_overflow",
233
+ description="Number of overflow connections in the pool",
234
+ unit="1",
235
+ ),
236
+ )
237
+
238
+ # (includes engine_name)
239
+ @property
240
+ def db_pool_connection_duration_ms_histogram(self) -> Histogram:
241
+ return self._get_or_create_metric(
242
+ "hist_db_pool_connection_duration_ms",
243
+ partial(
244
+ self._meter.create_histogram,
245
+ name="hist_db_pool_connection_duration_ms",
246
+ description="Duration of database connection usage in milliseconds",
247
+ unit="ms",
248
+ ),
249
+ )
250
+
251
+ # (includes engine_name, event)
252
+ @property
253
+ def db_pool_connection_events_counter(self) -> Counter:
254
+ return self._get_or_create_metric(
255
+ "count_db_pool_connection_events",
256
+ partial(
257
+ self._meter.create_counter,
258
+ name="count_db_pool_connection_events",
259
+ description="Count of database connection pool events (connect, checkout, checkin, invalidate)",
260
+ unit="1",
261
+ ),
262
+ )
263
+
264
+ # (includes engine_name, exception_type)
265
+ @property
266
+ def db_pool_connection_errors_counter(self) -> Counter:
267
+ return self._get_or_create_metric(
268
+ "count_db_pool_connection_errors",
269
+ partial(
270
+ self._meter.create_counter,
271
+ name="count_db_pool_connection_errors",
272
+ description="Count of database connection pool errors",
273
+ unit="1",
274
+ ),
275
+ )