letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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 (151) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/letta_llm_request_adapter.py +0 -1
  4. letta/adapters/letta_llm_stream_adapter.py +7 -2
  5. letta/adapters/simple_llm_request_adapter.py +88 -0
  6. letta/adapters/simple_llm_stream_adapter.py +192 -0
  7. letta/agents/agent_loop.py +6 -0
  8. letta/agents/ephemeral_summary_agent.py +2 -1
  9. letta/agents/helpers.py +142 -6
  10. letta/agents/letta_agent.py +13 -33
  11. letta/agents/letta_agent_batch.py +2 -4
  12. letta/agents/letta_agent_v2.py +87 -77
  13. letta/agents/letta_agent_v3.py +927 -0
  14. letta/agents/voice_agent.py +2 -6
  15. letta/constants.py +8 -4
  16. letta/database_utils.py +161 -0
  17. letta/errors.py +40 -0
  18. letta/functions/function_sets/base.py +84 -4
  19. letta/functions/function_sets/multi_agent.py +0 -3
  20. letta/functions/schema_generator.py +113 -71
  21. letta/groups/dynamic_multi_agent.py +3 -2
  22. letta/groups/helpers.py +1 -2
  23. letta/groups/round_robin_multi_agent.py +3 -2
  24. letta/groups/sleeptime_multi_agent.py +3 -2
  25. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  26. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  27. letta/groups/supervisor_multi_agent.py +84 -80
  28. letta/helpers/converters.py +3 -0
  29. letta/helpers/message_helper.py +4 -0
  30. letta/helpers/tool_rule_solver.py +92 -5
  31. letta/interfaces/anthropic_streaming_interface.py +409 -0
  32. letta/interfaces/gemini_streaming_interface.py +296 -0
  33. letta/interfaces/openai_streaming_interface.py +752 -1
  34. letta/llm_api/anthropic_client.py +127 -16
  35. letta/llm_api/bedrock_client.py +4 -2
  36. letta/llm_api/deepseek_client.py +4 -1
  37. letta/llm_api/google_vertex_client.py +124 -42
  38. letta/llm_api/groq_client.py +4 -1
  39. letta/llm_api/llm_api_tools.py +11 -4
  40. letta/llm_api/llm_client_base.py +6 -2
  41. letta/llm_api/openai.py +32 -2
  42. letta/llm_api/openai_client.py +423 -18
  43. letta/llm_api/xai_client.py +4 -1
  44. letta/main.py +9 -5
  45. letta/memory.py +1 -0
  46. letta/orm/__init__.py +2 -1
  47. letta/orm/agent.py +10 -0
  48. letta/orm/block.py +7 -16
  49. letta/orm/blocks_agents.py +8 -2
  50. letta/orm/files_agents.py +2 -0
  51. letta/orm/job.py +7 -5
  52. letta/orm/mcp_oauth.py +1 -0
  53. letta/orm/message.py +21 -6
  54. letta/orm/organization.py +2 -0
  55. letta/orm/provider.py +6 -2
  56. letta/orm/run.py +71 -0
  57. letta/orm/run_metrics.py +82 -0
  58. letta/orm/sandbox_config.py +7 -1
  59. letta/orm/sqlalchemy_base.py +0 -306
  60. letta/orm/step.py +6 -5
  61. letta/orm/step_metrics.py +5 -5
  62. letta/otel/tracing.py +28 -3
  63. letta/plugins/defaults.py +4 -4
  64. letta/prompts/system_prompts/__init__.py +2 -0
  65. letta/prompts/system_prompts/letta_v1.py +25 -0
  66. letta/schemas/agent.py +3 -2
  67. letta/schemas/agent_file.py +9 -3
  68. letta/schemas/block.py +23 -10
  69. letta/schemas/enums.py +21 -2
  70. letta/schemas/job.py +17 -4
  71. letta/schemas/letta_message_content.py +71 -2
  72. letta/schemas/letta_stop_reason.py +5 -5
  73. letta/schemas/llm_config.py +53 -3
  74. letta/schemas/memory.py +1 -1
  75. letta/schemas/message.py +564 -117
  76. letta/schemas/openai/responses_request.py +64 -0
  77. letta/schemas/providers/__init__.py +2 -0
  78. letta/schemas/providers/anthropic.py +16 -0
  79. letta/schemas/providers/ollama.py +115 -33
  80. letta/schemas/providers/openrouter.py +52 -0
  81. letta/schemas/providers/vllm.py +2 -1
  82. letta/schemas/run.py +48 -42
  83. letta/schemas/run_metrics.py +21 -0
  84. letta/schemas/step.py +2 -2
  85. letta/schemas/step_metrics.py +1 -1
  86. letta/schemas/tool.py +15 -107
  87. letta/schemas/tool_rule.py +88 -5
  88. letta/serialize_schemas/marshmallow_agent.py +1 -0
  89. letta/server/db.py +79 -408
  90. letta/server/rest_api/app.py +61 -10
  91. letta/server/rest_api/dependencies.py +14 -0
  92. letta/server/rest_api/redis_stream_manager.py +19 -8
  93. letta/server/rest_api/routers/v1/agents.py +364 -292
  94. letta/server/rest_api/routers/v1/blocks.py +14 -20
  95. letta/server/rest_api/routers/v1/identities.py +45 -110
  96. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  97. letta/server/rest_api/routers/v1/jobs.py +23 -6
  98. letta/server/rest_api/routers/v1/messages.py +1 -1
  99. letta/server/rest_api/routers/v1/runs.py +149 -99
  100. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  101. letta/server/rest_api/routers/v1/tools.py +281 -594
  102. letta/server/rest_api/routers/v1/voice.py +1 -1
  103. letta/server/rest_api/streaming_response.py +29 -29
  104. letta/server/rest_api/utils.py +122 -64
  105. letta/server/server.py +160 -887
  106. letta/services/agent_manager.py +236 -919
  107. letta/services/agent_serialization_manager.py +16 -0
  108. letta/services/archive_manager.py +0 -100
  109. letta/services/block_manager.py +211 -168
  110. letta/services/context_window_calculator/token_counter.py +1 -1
  111. letta/services/file_manager.py +1 -1
  112. letta/services/files_agents_manager.py +24 -33
  113. letta/services/group_manager.py +0 -142
  114. letta/services/helpers/agent_manager_helper.py +7 -2
  115. letta/services/helpers/run_manager_helper.py +69 -0
  116. letta/services/job_manager.py +96 -411
  117. letta/services/lettuce/__init__.py +6 -0
  118. letta/services/lettuce/lettuce_client_base.py +86 -0
  119. letta/services/mcp_manager.py +38 -6
  120. letta/services/message_manager.py +165 -362
  121. letta/services/organization_manager.py +0 -36
  122. letta/services/passage_manager.py +0 -345
  123. letta/services/provider_manager.py +0 -80
  124. letta/services/run_manager.py +364 -0
  125. letta/services/sandbox_config_manager.py +0 -234
  126. letta/services/step_manager.py +62 -39
  127. letta/services/summarizer/summarizer.py +9 -7
  128. letta/services/telemetry_manager.py +0 -16
  129. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  130. letta/services/tool_executor/core_tool_executor.py +397 -2
  131. letta/services/tool_executor/files_tool_executor.py +3 -3
  132. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  133. letta/services/tool_executor/tool_execution_manager.py +6 -8
  134. letta/services/tool_executor/tool_executor_base.py +3 -3
  135. letta/services/tool_manager.py +85 -339
  136. letta/services/tool_sandbox/base.py +24 -13
  137. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  138. letta/services/tool_schema_generator.py +123 -0
  139. letta/services/user_manager.py +0 -99
  140. letta/settings.py +20 -4
  141. letta/system.py +5 -1
  142. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
  144. letta/agents/temporal/activities/__init__.py +0 -4
  145. letta/agents/temporal/activities/example_activity.py +0 -7
  146. letta/agents/temporal/activities/prepare_messages.py +0 -10
  147. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  148. letta/agents/temporal/types.py +0 -25
  149. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
  150. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
  151. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
letta/server/db.py CHANGED
@@ -1,433 +1,104 @@
1
- import asyncio
2
- import os
3
- import threading
4
- import time
5
1
  import uuid
6
- from contextlib import asynccontextmanager, contextmanager
7
- from typing import Any, AsyncGenerator, Generator
8
-
9
- from opentelemetry import trace
10
- from rich.console import Console
11
- from rich.panel import Panel
12
- from rich.text import Text
13
- from sqlalchemy import Engine, NullPool, QueuePool, create_engine, event
14
- from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
15
- from sqlalchemy.orm import sessionmaker
16
-
17
- from letta.config import LettaConfig
18
- from letta.log import get_logger
19
- from letta.otel.tracing import trace_method
20
- from letta.settings import DatabaseChoice, settings
21
-
22
- logger = get_logger(__name__)
23
-
24
-
25
- def print_sqlite_schema_error():
26
- """Print a formatted error message for SQLite schema issues"""
27
- console = Console()
28
- error_text = Text()
29
- error_text.append("Existing SQLite DB schema is invalid, and schema migrations are not supported for SQLite. ", style="bold red")
30
- error_text.append("To have migrations supported between Letta versions, please run Letta with Docker (", style="white")
31
- error_text.append("https://docs.letta.com/server/docker", style="blue underline")
32
- error_text.append(") or use Postgres by setting ", style="white")
33
- error_text.append("LETTA_PG_URI", style="yellow")
34
- error_text.append(".\n\n", style="white")
35
- error_text.append("If you wish to keep using SQLite, you can reset your database by removing the DB file with ", style="white")
36
- error_text.append("rm ~/.letta/sqlite.db", style="yellow")
37
- error_text.append(" or downgrade to your previous version of Letta.", style="white")
38
-
39
- console.print(Panel(error_text, border_style="red"))
40
-
41
-
42
- @event.listens_for(Engine, "connect")
43
- def enable_sqlite_foreign_keys(dbapi_connection, connection_record):
44
- """Enable foreign key constraints for SQLite connections."""
45
- if "sqlite" in str(dbapi_connection):
46
- cursor = dbapi_connection.cursor()
47
- cursor.execute("PRAGMA foreign_keys=ON")
48
- cursor.close()
49
-
50
-
51
- def on_connect(dbapi_connection, connection_record):
52
- cursor = dbapi_connection.cursor()
53
- cursor.execute("SELECT pg_backend_pid()")
54
- pid = cursor.fetchone()[0]
55
- connection_record.info["pid"] = pid
56
- connection_record.info["connect_spawn_time_ms"] = time.perf_counter() * 1000
57
- cursor.close()
58
-
59
-
60
- def on_close(dbapi_connection, connection_record):
61
- connection_record.info.get("pid")
62
- (time.perf_counter() * 1000) - connection_record.info.get("connect_spawn_time_ms")
63
- # print(f"Connection closed: {pid}, duration: {duration:.6f}s")
64
-
65
-
66
- def on_checkout(dbapi_connection, connection_record, connection_proxy):
67
- connection_record.info.get("pid")
68
- connection_record.info["connect_checkout_time_ms"] = time.perf_counter() * 1000
69
-
70
-
71
- def on_checkin(dbapi_connection, connection_record):
72
- pid = connection_record.info.get("pid")
73
- duration = (time.perf_counter() * 1000) - connection_record.info.get("connect_checkout_time_ms")
74
-
75
- tracer = trace.get_tracer("letta.db.connection")
76
- with tracer.start_as_current_span("connect_release") as span:
77
- span.set_attribute("db.connection.pid", pid)
78
- span.set_attribute("db.connection.duration_ms", duration)
79
- span.set_attribute("db.connection.operation", "checkin")
80
-
81
-
82
- @contextmanager
83
- def db_error_handler():
84
- """Context manager for handling database errors"""
85
- try:
86
- yield
87
- except Exception as e:
88
- # Handle other SQLAlchemy errors
89
- error_str = str(e)
90
-
91
- # Don't exit for expected constraint violations that should be handled by the application
92
- if "UNIQUE constraint failed" in error_str or "FOREIGN KEY constraint failed" in error_str:
93
- # These are application-level errors that should be handled by the ORM
94
- raise
95
-
96
- # For other database errors, print error and exit
97
- print(e)
98
- print_sqlite_schema_error()
99
- # raise ValueError(f"SQLite DB error: {str(e)}")
100
- exit(1)
101
-
102
-
103
- class DatabaseRegistry:
104
- """Registry for database connections and sessions.
105
-
106
- This class manages both synchronous and asynchronous database connections
107
- and provides context managers for session handling.
108
- """
109
-
110
- def __init__(self):
111
- self._engines: dict[str, Engine] = {}
112
- self._async_engines: dict[str, AsyncEngine] = {}
113
- self._session_factories: dict[str, sessionmaker] = {}
114
- self._async_session_factories: dict[str, async_sessionmaker] = {}
115
- self._initialized: dict[str, bool] = {"sync": False, "async": False}
116
- self._lock = threading.Lock()
117
- self.config = LettaConfig.load()
118
- self.logger = get_logger(__name__)
119
-
120
- if settings.db_max_concurrent_sessions:
121
- self._db_semaphore = asyncio.Semaphore(settings.db_max_concurrent_sessions)
122
- self.logger.info(f"Initialized database throttling with max {settings.db_max_concurrent_sessions} concurrent sessions")
123
- else:
124
- self.logger.info("Database throttling is disabled")
125
- self._db_semaphore = None
126
-
127
- def initialize_sync(self, force: bool = False) -> None:
128
- """Initialize the synchronous database engine if not already initialized."""
129
- with self._lock:
130
- if self._initialized.get("sync") and not force:
131
- return
132
-
133
- # Postgres engine
134
- if settings.database_engine is DatabaseChoice.POSTGRES:
135
- self.logger.info("Creating postgres engine")
136
- self.config.recall_storage_type = "postgres"
137
- self.config.recall_storage_uri = settings.letta_pg_uri_no_default
138
- self.config.archival_storage_type = "postgres"
139
- self.config.archival_storage_uri = settings.letta_pg_uri_no_default
140
-
141
- engine = create_engine(settings.letta_pg_uri, **self._build_sqlalchemy_engine_args(is_async=False))
142
-
143
- self._engines["default"] = engine
144
- # SQLite engine
145
- else:
146
- from letta.orm import Base
147
-
148
- # TODO: don't rely on config storage
149
- engine_path = "sqlite:///" + os.path.join(self.config.recall_storage_path, "sqlite.db")
150
- self.logger.info("Creating sqlite engine " + engine_path)
151
-
152
- engine = create_engine(engine_path)
153
-
154
- # Wrap the engine with error handling
155
- self._wrap_sqlite_engine(engine)
156
-
157
- Base.metadata.create_all(bind=engine)
158
- self._engines["default"] = engine
159
-
160
- # Set up connection monitoring
161
- if settings.sqlalchemy_tracing and settings.database_engine is DatabaseChoice.POSTGRES:
162
- event.listen(engine, "connect", on_connect)
163
- event.listen(engine, "close", on_close)
164
- event.listen(engine, "checkout", on_checkout)
165
- event.listen(engine, "checkin", on_checkin)
166
-
167
- self._setup_pool_monitoring(engine, "default")
168
-
169
- # Create session factory
170
- self._session_factories["default"] = sessionmaker(autocommit=False, autoflush=False, bind=self._engines["default"])
171
- self._initialized["sync"] = True
172
-
173
- def initialize_async(self, force: bool = False) -> None:
174
- """Initialize the asynchronous database engine if not already initialized."""
175
- with self._lock:
176
- if self._initialized.get("async") and not force:
177
- return
178
-
179
- if settings.database_engine is DatabaseChoice.POSTGRES:
180
- self.logger.info("Creating async postgres engine")
181
-
182
- # Create async engine - convert URI to async format
183
- pg_uri = settings.letta_pg_uri
184
- if pg_uri.startswith("postgresql://"):
185
- async_pg_uri = pg_uri.replace("postgresql://", "postgresql+asyncpg://")
186
- else:
187
- async_pg_uri = f"postgresql+asyncpg://{pg_uri.split('://', 1)[1]}" if "://" in pg_uri else pg_uri
188
- async_pg_uri = async_pg_uri.replace("sslmode=", "ssl=")
189
- async_engine = create_async_engine(async_pg_uri, **self._build_sqlalchemy_engine_args(is_async=True))
190
- else:
191
- # create sqlite async engine
192
- self._initialized["async"] = False
193
- # TODO: remove self.config
194
- engine_path = "sqlite+aiosqlite:///" + os.path.join(self.config.recall_storage_path, "sqlite.db")
195
- self.logger.info("Creating sqlite engine " + engine_path)
196
- async_engine = create_async_engine(engine_path, **self._build_sqlalchemy_engine_args(is_async=True))
197
-
198
- # Enable foreign keys for SQLite async connections
199
- @event.listens_for(async_engine.sync_engine, "connect")
200
- def enable_sqlite_foreign_keys_async(dbapi_connection, connection_record):
201
- cursor = dbapi_connection.cursor()
202
- cursor.execute("PRAGMA foreign_keys=ON")
203
- cursor.close()
204
-
205
- # Create async session factory
206
- self._async_engines["default"] = async_engine
207
-
208
- # Set up connection monitoring for async engine
209
- if settings.sqlalchemy_tracing and settings.database_engine is DatabaseChoice.POSTGRES:
210
- event.listen(async_engine.sync_engine, "connect", on_connect)
211
- event.listen(async_engine.sync_engine, "close", on_close)
212
- event.listen(async_engine.sync_engine, "checkout", on_checkout)
213
- event.listen(async_engine.sync_engine, "checkin", on_checkin)
214
-
215
- self._setup_pool_monitoring(async_engine, "default_async")
216
-
217
- self._async_session_factories["default"] = async_sessionmaker(
218
- expire_on_commit=False,
219
- close_resets_only=False,
220
- autocommit=False,
221
- autoflush=False,
222
- bind=self._async_engines["default"],
223
- class_=AsyncSession,
224
- )
225
- self._initialized["async"] = True
226
-
227
- def _build_sqlalchemy_engine_args(self, *, is_async: bool) -> dict:
228
- """Prepare keyword arguments for create_engine / create_async_engine."""
229
- # For async SQLite, always use NullPool to avoid cleanup issues during cancellation
230
- if is_async and settings.database_engine is DatabaseChoice.SQLITE:
231
- use_null_pool = True
232
- logger.info("Forcing NullPool for async SQLite to avoid cancellation cleanup issues")
233
- else:
234
- use_null_pool = settings.disable_sqlalchemy_pooling
235
-
236
- if use_null_pool:
237
- logger.info("Disabling pooling on SqlAlchemy")
238
- pool_cls = NullPool
239
- else:
240
- logger.info("Enabling pooling on SqlAlchemy")
241
- # AsyncAdaptedQueuePool will be the default if none is provided for async but setting this explicitly.
242
- from sqlalchemy import AsyncAdaptedQueuePool
243
-
244
- pool_cls = QueuePool if not is_async else AsyncAdaptedQueuePool
245
-
246
- base_args = {
247
- "echo": settings.pg_echo,
248
- "pool_pre_ping": settings.pool_pre_ping,
2
+ from contextlib import asynccontextmanager
3
+ from typing import AsyncGenerator
4
+
5
+ from sqlalchemy import NullPool
6
+ from sqlalchemy.ext.asyncio import (
7
+ AsyncEngine,
8
+ AsyncSession,
9
+ async_sessionmaker,
10
+ create_async_engine,
11
+ )
12
+
13
+ from letta.database_utils import get_database_uri_for_context
14
+ from letta.settings import settings
15
+
16
+ # Convert PostgreSQL URI to async format using common utility
17
+ async_pg_uri = get_database_uri_for_context(settings.letta_pg_uri, "async")
18
+
19
+ # Build engine configuration based on settings
20
+ engine_args = {
21
+ "echo": settings.pg_echo,
22
+ "pool_pre_ping": settings.pool_pre_ping,
23
+ }
24
+
25
+ # Configure pooling
26
+ if settings.disable_sqlalchemy_pooling:
27
+ engine_args["poolclass"] = NullPool
28
+ else:
29
+ # Use default AsyncAdaptedQueuePool with configured settings
30
+ engine_args.update(
31
+ {
32
+ "pool_size": settings.pg_pool_size,
33
+ "max_overflow": settings.pg_max_overflow,
34
+ "pool_timeout": settings.pg_pool_timeout,
35
+ "pool_recycle": settings.pg_pool_recycle,
249
36
  }
37
+ )
38
+
39
+ # Add asyncpg-specific settings for connection
40
+ if not settings.disable_sqlalchemy_pooling:
41
+ engine_args["connect_args"] = {
42
+ "timeout": settings.pg_pool_timeout,
43
+ "prepared_statement_name_func": lambda: f"__asyncpg_{uuid.uuid4()}__",
44
+ "statement_cache_size": 0,
45
+ "prepared_statement_cache_size": 0,
46
+ }
47
+
48
+ # Create the engine once at module level
49
+ engine: AsyncEngine = create_async_engine(async_pg_uri, **engine_args)
50
+
51
+ # Create session factory once at module level
52
+ async_session_factory = async_sessionmaker(
53
+ engine,
54
+ class_=AsyncSession,
55
+ expire_on_commit=False,
56
+ autocommit=False,
57
+ autoflush=False,
58
+ )
250
59
 
251
- if pool_cls:
252
- base_args["poolclass"] = pool_cls
253
-
254
- if not use_null_pool:
255
- base_args.update(
256
- {
257
- "pool_size": settings.pg_pool_size,
258
- "max_overflow": settings.pg_max_overflow,
259
- "pool_timeout": settings.pg_pool_timeout,
260
- "pool_recycle": settings.pg_pool_recycle,
261
- }
262
- )
263
- if not is_async:
264
- base_args.update(
265
- {
266
- "pool_use_lifo": settings.pool_use_lifo,
267
- }
268
- )
269
-
270
- elif is_async and settings.database_engine is DatabaseChoice.POSTGRES:
271
- # Invalid for SQLite, results in [0] TypeError: 'prepared_statement_name_func' is an invalid keyword argument for Connection()
272
- # For asyncpg, statement_cache_size should be in connect_args
273
- base_args.update(
274
- {
275
- "connect_args": {
276
- "timeout": settings.pg_pool_timeout,
277
- "prepared_statement_name_func": lambda: f"__asyncpg_{uuid.uuid4()}__",
278
- "statement_cache_size": 0,
279
- "prepared_statement_cache_size": 0,
280
- },
281
- }
282
- )
283
- return base_args
284
-
285
- def _wrap_sqlite_engine(self, engine: Engine) -> None:
286
- """Wrap SQLite engine with error handling."""
287
- original_connect = engine.connect
288
-
289
- def wrapped_connect(*args, **kwargs):
290
- with db_error_handler():
291
- connection = original_connect(*args, **kwargs)
292
- original_execute = connection.execute
293
-
294
- def wrapped_execute(*args, **kwargs):
295
- with db_error_handler():
296
- return original_execute(*args, **kwargs)
297
-
298
- connection.execute = wrapped_execute
299
- return connection
300
-
301
- engine.connect = wrapped_connect
302
-
303
- def _setup_pool_monitoring(self, engine: Engine | AsyncEngine, engine_name: str) -> None:
304
- """Set up database pool monitoring for the given engine."""
305
- if not settings.enable_db_pool_monitoring:
306
- return
307
-
308
- try:
309
- from letta.otel.db_pool_monitoring import setup_pool_monitoring
310
-
311
- setup_pool_monitoring(engine, engine_name)
312
- self.logger.info(f"Database pool monitoring enabled for {engine_name}")
313
- except ImportError:
314
- self.logger.warning("Database pool monitoring not available - missing dependencies")
315
- except Exception as e:
316
- self.logger.warning(f"Failed to setup pool monitoring for {engine_name}: {e}")
317
60
 
318
- def get_engine(self, name: str = "default") -> Engine:
319
- """Get a database engine by name."""
320
- self.initialize_sync()
321
- return self._engines.get(name)
322
-
323
- def get_async_engine(self, name: str = "default") -> Engine:
324
- """Get a database engine by name."""
325
- self.initialize_async()
326
- return self._async_engines.get(name)
327
-
328
- def get_session_factory(self, name: str = "default") -> sessionmaker:
329
- """Get a session factory by name."""
330
- self.initialize_sync()
331
- return self._session_factories.get(name)
332
-
333
- def get_async_session_factory(self, name: str = "default") -> async_sessionmaker:
334
- """Get an async session factory by name."""
335
- self.initialize_async()
336
- return self._async_session_factories.get(name)
337
-
338
- @trace_method
339
- @contextmanager
340
- def session(self, name: str = "default") -> Generator[Any, None, None]:
341
- """Context manager for database sessions."""
342
- caller_info = "unknown caller"
343
- try:
344
- import inspect
345
-
346
- frame = inspect.currentframe()
347
- stack = inspect.getouterframes(frame)
348
-
349
- for i, frame_info in enumerate(stack):
350
- module = inspect.getmodule(frame_info.frame)
351
- module_name = module.__name__ if module else "unknown"
352
-
353
- if module_name != "contextlib" and "db.py" not in frame_info.filename:
354
- caller_module = module_name
355
- caller_function = frame_info.function
356
- caller_lineno = frame_info.lineno
357
- caller_file = frame_info.filename.split("/")[-1]
358
-
359
- caller_info = f"{caller_module}.{caller_function}:{caller_lineno} ({caller_file})"
360
- break
361
- except:
362
- pass
363
- finally:
364
- del frame
365
-
366
- self.session_caller_trace(caller_info)
367
-
368
- session_factory = self.get_session_factory(name)
369
- if not session_factory:
370
- raise ValueError(f"No session factory found for '{name}'")
371
-
372
- session = session_factory()
373
- try:
374
- yield session
375
- finally:
376
- session.close()
61
+ class DatabaseRegistry:
62
+ """Dummy registry to maintain the existing interface."""
377
63
 
378
- @trace_method
379
64
  @asynccontextmanager
380
- async def async_session(self, name: str = "default") -> AsyncGenerator[AsyncSession, None]:
381
- """Async context manager for database sessions with throttling."""
382
- if self._db_semaphore:
383
- async with self._db_semaphore:
384
- session_factory = self.get_async_session_factory(name)
385
- if not session_factory:
386
- raise ValueError(f"No async session factory found for '{name}' or async database is not configured")
387
-
388
- session = session_factory()
389
- try:
390
- yield session
391
- finally:
392
- await session.close()
393
- else:
394
- session_factory = self.get_async_session_factory(name)
395
- if not session_factory:
396
- raise ValueError(f"No async session factory found for '{name}' or async database is not configured")
397
-
398
- session = session_factory()
65
+ async def async_session(self) -> AsyncGenerator[AsyncSession, None]:
66
+ """Get an async database session."""
67
+ async with async_session_factory() as session:
399
68
  try:
400
69
  yield session
70
+ await session.commit()
71
+ except Exception:
72
+ await session.rollback()
73
+ raise
401
74
  finally:
402
75
  await session.close()
403
76
 
404
- @trace_method
405
- def session_caller_trace(self, caller_info: str):
406
- """Trace sync db caller information for debugging purposes."""
407
- pass # wrapper used for otel tracing only
408
-
409
77
 
410
- # Create a singleton instance
78
+ # Create singleton instance to match existing interface
411
79
  db_registry = DatabaseRegistry()
412
80
 
413
81
 
82
+ # Backwards compatibility function
414
83
  def get_db_registry() -> DatabaseRegistry:
415
84
  """Get the global database registry instance."""
416
85
  return db_registry
417
86
 
418
87
 
419
- def get_db():
420
- """Get a database session."""
421
- with db_registry.session() as session:
422
- yield session
423
-
424
-
425
- async def get_db_async():
88
+ # FastAPI dependency helper
89
+ async def get_db_async() -> AsyncGenerator[AsyncSession, None]:
426
90
  """Get an async database session."""
427
91
  async with db_registry.async_session() as session:
428
92
  yield session
429
93
 
430
94
 
431
- # Prefer calling db_registry.session() or db_registry.async_session() directly
432
- # This is for backwards compatibility
433
- db_context = contextmanager(get_db)
95
+ # Optional: cleanup function for graceful shutdown
96
+ async def close_db() -> None:
97
+ """Close the database engine."""
98
+ await engine.dispose()
99
+
100
+
101
+ # Usage remains the same:
102
+ # async with db_registry.async_session() as session:
103
+ # result = await session.execute(select(User))
104
+ # users = result.scalars().all()
@@ -12,14 +12,26 @@ from typing import Optional
12
12
  import uvicorn
13
13
  from fastapi import FastAPI, Request
14
14
  from fastapi.responses import JSONResponse
15
+ from marshmallow import ValidationError
16
+ from sqlalchemy.exc import IntegrityError, OperationalError
15
17
  from starlette.middleware.cors import CORSMiddleware
16
18
 
17
19
  from letta.__init__ import __version__ as letta_version
18
20
  from letta.agents.exceptions import IncompatibleAgentType
19
21
  from letta.constants import ADMIN_PREFIX, API_PREFIX, OPENAI_API_PREFIX
20
22
  from letta.errors import (
23
+ AgentExportIdMappingError,
24
+ AgentExportProcessingError,
25
+ AgentFileImportError,
26
+ AgentNotFoundForExportError,
21
27
  BedrockPermissionError,
22
28
  LettaAgentNotFoundError,
29
+ LettaInvalidArgumentError,
30
+ LettaInvalidMCPSchemaError,
31
+ LettaMCPConnectionError,
32
+ LettaMCPTimeoutError,
33
+ LettaToolCreateError,
34
+ LettaToolNameConflictError,
23
35
  LettaUserNotFoundError,
24
36
  LLMAuthenticationError,
25
37
  LLMError,
@@ -44,7 +56,6 @@ from letta.server.db import db_registry
44
56
  from letta.server.rest_api.auth.index import setup_auth_router # TODO: probably remove right?
45
57
  from letta.server.rest_api.interface import StreamingServerInterface
46
58
  from letta.server.rest_api.middleware import CheckPasswordMiddleware, ProfilerContextMiddleware
47
- from letta.server.rest_api.routers.openai.chat_completions.chat_completions import router as openai_chat_completions_router
48
59
  from letta.server.rest_api.routers.v1 import ROUTERS as v1_routes
49
60
  from letta.server.rest_api.routers.v1.organizations import router as organizations_router
50
61
  from letta.server.rest_api.routers.v1.users import router as users_router # TODO: decide on admin
@@ -122,11 +133,10 @@ async def lifespan(app_: FastAPI):
122
133
  except Exception as exc:
123
134
  logger.info("Profiler not enabled: %", exc)
124
135
 
125
- logger.info(f"[Worker {worker_id}] Starting lifespan initialization")
126
- logger.info(f"[Worker {worker_id}] Initializing database connections")
127
- db_registry.initialize_sync()
128
- db_registry.initialize_async()
129
- logger.info(f"[Worker {worker_id}] Database connections initialized")
136
+ # logger.info(f"[Worker {worker_id}] Starting lifespan initialization")
137
+ # logger.info(f"[Worker {worker_id}] Initializing database connections")
138
+ # db_registry.initialize_async()
139
+ # logger.info(f"[Worker {worker_id}] Database connections initialized")
130
140
 
131
141
  if should_use_pinecone():
132
142
  if settings.upsert_pinecone_indices:
@@ -140,6 +150,7 @@ async def lifespan(app_: FastAPI):
140
150
 
141
151
  logger.info(f"[Worker {worker_id}] Starting scheduler with leader election")
142
152
  global server
153
+ await server.init_async()
143
154
  try:
144
155
  await start_scheduler_with_leader_election(server)
145
156
  logger.info(f"[Worker {worker_id}] Scheduler initialization completed")
@@ -180,6 +191,7 @@ def create_application() -> "FastAPI":
180
191
  if SENTRY_ENABLED:
181
192
  sentry_sdk.init(
182
193
  dsn=os.getenv("SENTRY_DSN"),
194
+ environment=os.getenv("LETTA_ENVIRONMENT", "undefined"),
183
195
  traces_sample_rate=1.0,
184
196
  _experiments={
185
197
  "continuous_profiling_auto_start": True,
@@ -232,14 +244,43 @@ def create_application() -> "FastAPI":
232
244
  _error_handler_404 = partial(error_handler_with_code, code=404)
233
245
  _error_handler_404_agent = partial(_error_handler_404, detail="Agent not found")
234
246
  _error_handler_404_user = partial(_error_handler_404, detail="User not found")
247
+ _error_handler_408 = partial(error_handler_with_code, code=408)
235
248
  _error_handler_409 = partial(error_handler_with_code, code=409)
236
-
249
+ _error_handler_422 = partial(error_handler_with_code, code=422)
250
+ _error_handler_500 = partial(error_handler_with_code, code=500)
251
+ _error_handler_503 = partial(error_handler_with_code, code=503)
252
+
253
+ # 400 Bad Request errors
254
+ app.add_exception_handler(LettaInvalidArgumentError, _error_handler_400)
255
+ app.add_exception_handler(LettaToolCreateError, _error_handler_400)
256
+ app.add_exception_handler(LettaToolNameConflictError, _error_handler_400)
257
+ app.add_exception_handler(AgentFileImportError, _error_handler_400)
237
258
  app.add_exception_handler(ValueError, _error_handler_400)
259
+
260
+ # 404 Not Found errors
238
261
  app.add_exception_handler(NoResultFound, _error_handler_404)
239
262
  app.add_exception_handler(LettaAgentNotFoundError, _error_handler_404_agent)
240
263
  app.add_exception_handler(LettaUserNotFoundError, _error_handler_404_user)
264
+ app.add_exception_handler(AgentNotFoundForExportError, _error_handler_404)
265
+
266
+ # 408 Timeout errors
267
+ app.add_exception_handler(LettaMCPTimeoutError, _error_handler_408)
268
+ app.add_exception_handler(LettaInvalidMCPSchemaError, _error_handler_400)
269
+
270
+ # 409 Conflict errors
241
271
  app.add_exception_handler(ForeignKeyConstraintViolationError, _error_handler_409)
242
272
  app.add_exception_handler(UniqueConstraintViolationError, _error_handler_409)
273
+ app.add_exception_handler(IntegrityError, _error_handler_409)
274
+
275
+ # 422 Validation errors
276
+ app.add_exception_handler(ValidationError, _error_handler_422)
277
+
278
+ # 500 Internal Server errors
279
+ app.add_exception_handler(AgentExportIdMappingError, _error_handler_500)
280
+ app.add_exception_handler(AgentExportProcessingError, _error_handler_500)
281
+
282
+ # 503 Service Unavailable errors
283
+ app.add_exception_handler(OperationalError, _error_handler_503)
243
284
 
244
285
  @app.exception_handler(IncompatibleAgentType)
245
286
  async def handle_incompatible_agent_type(request: Request, exc: IncompatibleAgentType):
@@ -323,6 +364,19 @@ def create_application() -> "FastAPI":
323
364
  },
324
365
  )
325
366
 
367
+ @app.exception_handler(LettaMCPConnectionError)
368
+ async def mcp_connection_error_handler(request: Request, exc: LettaMCPConnectionError):
369
+ return JSONResponse(
370
+ status_code=502,
371
+ content={
372
+ "error": {
373
+ "type": "mcp_connection_error",
374
+ "message": "Failed to connect to MCP server.",
375
+ "detail": str(exc),
376
+ }
377
+ },
378
+ )
379
+
326
380
  @app.exception_handler(LLMError)
327
381
  async def llm_error_handler(request: Request, exc: LLMError):
328
382
  return JSONResponse(
@@ -400,9 +454,6 @@ def create_application() -> "FastAPI":
400
454
  app.include_router(users_router, prefix=ADMIN_PREFIX)
401
455
  app.include_router(organizations_router, prefix=ADMIN_PREFIX)
402
456
 
403
- # openai
404
- app.include_router(openai_chat_completions_router, prefix=OPENAI_API_PREFIX)
405
-
406
457
  # /api/auth endpoints
407
458
  app.include_router(setup_auth_router(server, interface, random_password), prefix=API_PREFIX)
408
459