letta-nightly 0.7.20.dev20250521104258__py3-none-any.whl → 0.7.21.dev20250521233415__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 (66) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +290 -3
  3. letta/agents/base_agent.py +0 -55
  4. letta/agents/helpers.py +5 -0
  5. letta/agents/letta_agent.py +314 -64
  6. letta/agents/letta_agent_batch.py +102 -55
  7. letta/agents/voice_agent.py +5 -5
  8. letta/client/client.py +9 -18
  9. letta/constants.py +55 -1
  10. letta/functions/function_sets/builtin.py +27 -0
  11. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  12. letta/interfaces/anthropic_streaming_interface.py +10 -1
  13. letta/interfaces/openai_streaming_interface.py +9 -2
  14. letta/llm_api/anthropic.py +21 -2
  15. letta/llm_api/anthropic_client.py +33 -6
  16. letta/llm_api/google_ai_client.py +136 -423
  17. letta/llm_api/google_vertex_client.py +173 -22
  18. letta/llm_api/llm_api_tools.py +27 -0
  19. letta/llm_api/llm_client.py +1 -1
  20. letta/llm_api/llm_client_base.py +32 -21
  21. letta/llm_api/openai.py +57 -0
  22. letta/llm_api/openai_client.py +7 -11
  23. letta/memory.py +0 -1
  24. letta/orm/__init__.py +1 -0
  25. letta/orm/enums.py +1 -0
  26. letta/orm/provider_trace.py +26 -0
  27. letta/orm/step.py +1 -0
  28. letta/schemas/provider_trace.py +43 -0
  29. letta/schemas/providers.py +210 -65
  30. letta/schemas/step.py +1 -0
  31. letta/schemas/tool.py +4 -0
  32. letta/server/db.py +37 -19
  33. letta/server/rest_api/routers/v1/__init__.py +2 -0
  34. letta/server/rest_api/routers/v1/agents.py +57 -34
  35. letta/server/rest_api/routers/v1/blocks.py +3 -3
  36. letta/server/rest_api/routers/v1/identities.py +24 -26
  37. letta/server/rest_api/routers/v1/jobs.py +3 -3
  38. letta/server/rest_api/routers/v1/llms.py +13 -8
  39. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -6
  40. letta/server/rest_api/routers/v1/tags.py +3 -3
  41. letta/server/rest_api/routers/v1/telemetry.py +18 -0
  42. letta/server/rest_api/routers/v1/tools.py +6 -6
  43. letta/server/rest_api/streaming_response.py +105 -0
  44. letta/server/rest_api/utils.py +4 -0
  45. letta/server/server.py +140 -1
  46. letta/services/agent_manager.py +251 -18
  47. letta/services/block_manager.py +52 -37
  48. letta/services/helpers/noop_helper.py +10 -0
  49. letta/services/identity_manager.py +43 -38
  50. letta/services/job_manager.py +29 -0
  51. letta/services/message_manager.py +111 -0
  52. letta/services/sandbox_config_manager.py +36 -0
  53. letta/services/step_manager.py +146 -0
  54. letta/services/telemetry_manager.py +58 -0
  55. letta/services/tool_executor/tool_execution_manager.py +49 -5
  56. letta/services/tool_executor/tool_execution_sandbox.py +47 -0
  57. letta/services/tool_executor/tool_executor.py +236 -7
  58. letta/services/tool_manager.py +160 -1
  59. letta/services/tool_sandbox/e2b_sandbox.py +65 -3
  60. letta/settings.py +10 -2
  61. letta/tracing.py +5 -5
  62. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/METADATA +3 -2
  63. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/RECORD +66 -59
  64. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/LICENSE +0 -0
  65. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/WHEEL +0 -0
  66. {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250521233415.dist-info}/entry_points.txt +0 -0
@@ -47,12 +47,21 @@ class Provider(ProviderBase):
47
47
  def list_llm_models(self) -> List[LLMConfig]:
48
48
  return []
49
49
 
50
+ async def list_llm_models_async(self) -> List[LLMConfig]:
51
+ return []
52
+
50
53
  def list_embedding_models(self) -> List[EmbeddingConfig]:
51
54
  return []
52
55
 
56
+ async def list_embedding_models_async(self) -> List[EmbeddingConfig]:
57
+ return []
58
+
53
59
  def get_model_context_window(self, model_name: str) -> Optional[int]:
54
60
  raise NotImplementedError
55
61
 
62
+ async def get_model_context_window_async(self, model_name: str) -> Optional[int]:
63
+ raise NotImplementedError
64
+
56
65
  def provider_tag(self) -> str:
57
66
  """String representation of the provider for display purposes"""
58
67
  raise NotImplementedError
@@ -140,6 +149,19 @@ class LettaProvider(Provider):
140
149
  )
141
150
  ]
142
151
 
152
+ async def list_llm_models_async(self) -> List[LLMConfig]:
153
+ return [
154
+ LLMConfig(
155
+ model="letta-free", # NOTE: renamed
156
+ model_endpoint_type="openai",
157
+ model_endpoint=LETTA_MODEL_ENDPOINT,
158
+ context_window=8192,
159
+ handle=self.get_handle("letta-free"),
160
+ provider_name=self.name,
161
+ provider_category=self.provider_category,
162
+ )
163
+ ]
164
+
143
165
  def list_embedding_models(self):
144
166
  return [
145
167
  EmbeddingConfig(
@@ -189,9 +211,40 @@ class OpenAIProvider(Provider):
189
211
 
190
212
  return data
191
213
 
214
+ async def _get_models_async(self) -> List[dict]:
215
+ from letta.llm_api.openai import openai_get_model_list_async
216
+
217
+ # Some hardcoded support for OpenRouter (so that we only get models with tool calling support)...
218
+ # See: https://openrouter.ai/docs/requests
219
+ extra_params = {"supported_parameters": "tools"} if "openrouter.ai" in self.base_url else None
220
+
221
+ # Similar to Nebius
222
+ extra_params = {"verbose": True} if "nebius.com" in self.base_url else None
223
+
224
+ response = await openai_get_model_list_async(
225
+ self.base_url,
226
+ api_key=self.api_key,
227
+ extra_params=extra_params,
228
+ # fix_url=True, # NOTE: make sure together ends with /v1
229
+ )
230
+
231
+ if "data" in response:
232
+ data = response["data"]
233
+ else:
234
+ # TogetherAI's response is missing the 'data' field
235
+ data = response
236
+
237
+ return data
238
+
192
239
  def list_llm_models(self) -> List[LLMConfig]:
193
240
  data = self._get_models()
241
+ return self._list_llm_models(data)
194
242
 
243
+ async def list_llm_models_async(self) -> List[LLMConfig]:
244
+ data = await self._get_models_async()
245
+ return self._list_llm_models(data)
246
+
247
+ def _list_llm_models(self, data) -> List[LLMConfig]:
195
248
  configs = []
196
249
  for model in data:
197
250
  assert "id" in model, f"OpenAI model missing 'id' field: {model}"
@@ -279,7 +332,6 @@ class OpenAIProvider(Provider):
279
332
  return configs
280
333
 
281
334
  def list_embedding_models(self) -> List[EmbeddingConfig]:
282
-
283
335
  if self.base_url == "https://api.openai.com/v1":
284
336
  # TODO: actually automatically list models for OpenAI
285
337
  return [
@@ -312,55 +364,92 @@ class OpenAIProvider(Provider):
312
364
  else:
313
365
  # Actually attempt to list
314
366
  data = self._get_models()
367
+ return self._list_embedding_models(data)
315
368
 
316
- configs = []
317
- for model in data:
318
- assert "id" in model, f"Model missing 'id' field: {model}"
319
- model_name = model["id"]
369
+ async def list_embedding_models_async(self) -> List[EmbeddingConfig]:
370
+ if self.base_url == "https://api.openai.com/v1":
371
+ # TODO: actually automatically list models for OpenAI
372
+ return [
373
+ EmbeddingConfig(
374
+ embedding_model="text-embedding-ada-002",
375
+ embedding_endpoint_type="openai",
376
+ embedding_endpoint=self.base_url,
377
+ embedding_dim=1536,
378
+ embedding_chunk_size=300,
379
+ handle=self.get_handle("text-embedding-ada-002", is_embedding=True),
380
+ ),
381
+ EmbeddingConfig(
382
+ embedding_model="text-embedding-3-small",
383
+ embedding_endpoint_type="openai",
384
+ embedding_endpoint=self.base_url,
385
+ embedding_dim=2000,
386
+ embedding_chunk_size=300,
387
+ handle=self.get_handle("text-embedding-3-small", is_embedding=True),
388
+ ),
389
+ EmbeddingConfig(
390
+ embedding_model="text-embedding-3-large",
391
+ embedding_endpoint_type="openai",
392
+ embedding_endpoint=self.base_url,
393
+ embedding_dim=2000,
394
+ embedding_chunk_size=300,
395
+ handle=self.get_handle("text-embedding-3-large", is_embedding=True),
396
+ ),
397
+ ]
320
398
 
321
- if "context_length" in model:
322
- # Context length is returned in Nebius as "context_length"
323
- context_window_size = model["context_length"]
324
- else:
325
- context_window_size = self.get_model_context_window_size(model_name)
399
+ else:
400
+ # Actually attempt to list
401
+ data = await self._get_models_async()
402
+ return self._list_embedding_models(data)
326
403
 
327
- # We need the context length for embeddings too
328
- if not context_window_size:
329
- continue
404
+ def _list_embedding_models(self, data) -> List[EmbeddingConfig]:
405
+ configs = []
406
+ for model in data:
407
+ assert "id" in model, f"Model missing 'id' field: {model}"
408
+ model_name = model["id"]
330
409
 
331
- if "nebius.com" in self.base_url:
332
- # Nebius includes the type, which we can use to filter for embedidng models
333
- try:
334
- model_type = model["architecture"]["modality"]
335
- if model_type not in ["text->embedding"]:
336
- # print(f"Skipping model w/ modality {model_type}:\n{model}")
337
- continue
338
- except KeyError:
339
- print(f"Couldn't access architecture type field, skipping model:\n{model}")
340
- continue
410
+ if "context_length" in model:
411
+ # Context length is returned in Nebius as "context_length"
412
+ context_window_size = model["context_length"]
413
+ else:
414
+ context_window_size = self.get_model_context_window_size(model_name)
415
+
416
+ # We need the context length for embeddings too
417
+ if not context_window_size:
418
+ continue
341
419
 
342
- elif "together.ai" in self.base_url or "together.xyz" in self.base_url:
343
- # TogetherAI includes the type, which we can use to filter for embedding models
344
- if "type" in model and model["type"] not in ["embedding"]:
420
+ if "nebius.com" in self.base_url:
421
+ # Nebius includes the type, which we can use to filter for embedidng models
422
+ try:
423
+ model_type = model["architecture"]["modality"]
424
+ if model_type not in ["text->embedding"]:
345
425
  # print(f"Skipping model w/ modality {model_type}:\n{model}")
346
426
  continue
427
+ except KeyError:
428
+ print(f"Couldn't access architecture type field, skipping model:\n{model}")
429
+ continue
347
430
 
348
- else:
349
- # For other providers we should skip by default, since we don't want to assume embeddings are supported
431
+ elif "together.ai" in self.base_url or "together.xyz" in self.base_url:
432
+ # TogetherAI includes the type, which we can use to filter for embedding models
433
+ if "type" in model and model["type"] not in ["embedding"]:
434
+ # print(f"Skipping model w/ modality {model_type}:\n{model}")
350
435
  continue
351
436
 
352
- configs.append(
353
- EmbeddingConfig(
354
- embedding_model=model_name,
355
- embedding_endpoint_type=self.provider_type,
356
- embedding_endpoint=self.base_url,
357
- embedding_dim=context_window_size,
358
- embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
359
- handle=self.get_handle(model, is_embedding=True),
360
- )
437
+ else:
438
+ # For other providers we should skip by default, since we don't want to assume embeddings are supported
439
+ continue
440
+
441
+ configs.append(
442
+ EmbeddingConfig(
443
+ embedding_model=model_name,
444
+ embedding_endpoint_type=self.provider_type,
445
+ embedding_endpoint=self.base_url,
446
+ embedding_dim=context_window_size,
447
+ embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
448
+ handle=self.get_handle(model, is_embedding=True),
361
449
  )
450
+ )
362
451
 
363
- return configs
452
+ return configs
364
453
 
365
454
  def get_model_context_window_size(self, model_name: str):
366
455
  if model_name in LLM_MAX_TOKENS:
@@ -647,26 +736,19 @@ class AnthropicProvider(Provider):
647
736
  anthropic_check_valid_api_key(self.api_key)
648
737
 
649
738
  def list_llm_models(self) -> List[LLMConfig]:
650
- from letta.llm_api.anthropic import MODEL_LIST, anthropic_get_model_list
739
+ from letta.llm_api.anthropic import anthropic_get_model_list
651
740
 
652
- models = anthropic_get_model_list(self.base_url, api_key=self.api_key)
741
+ models = anthropic_get_model_list(api_key=self.api_key)
742
+ return self._list_llm_models(models)
653
743
 
654
- """
655
- Example response:
656
- {
657
- "data": [
658
- {
659
- "type": "model",
660
- "id": "claude-3-5-sonnet-20241022",
661
- "display_name": "Claude 3.5 Sonnet (New)",
662
- "created_at": "2024-10-22T00:00:00Z"
663
- }
664
- ],
665
- "has_more": true,
666
- "first_id": "<string>",
667
- "last_id": "<string>"
668
- }
669
- """
744
+ async def list_llm_models_async(self) -> List[LLMConfig]:
745
+ from letta.llm_api.anthropic import anthropic_get_model_list_async
746
+
747
+ models = await anthropic_get_model_list_async(api_key=self.api_key)
748
+ return self._list_llm_models(models)
749
+
750
+ def _list_llm_models(self, models) -> List[LLMConfig]:
751
+ from letta.llm_api.anthropic import MODEL_LIST
670
752
 
671
753
  configs = []
672
754
  for model in models:
@@ -724,9 +806,6 @@ class AnthropicProvider(Provider):
724
806
  )
725
807
  return configs
726
808
 
727
- def list_embedding_models(self) -> List[EmbeddingConfig]:
728
- return []
729
-
730
809
 
731
810
  class MistralProvider(Provider):
732
811
  provider_type: Literal[ProviderType.mistral] = Field(ProviderType.mistral, description="The type of the provider.")
@@ -948,14 +1027,24 @@ class TogetherProvider(OpenAIProvider):
948
1027
  def list_llm_models(self) -> List[LLMConfig]:
949
1028
  from letta.llm_api.openai import openai_get_model_list
950
1029
 
951
- response = openai_get_model_list(self.base_url, api_key=self.api_key)
1030
+ models = openai_get_model_list(self.base_url, api_key=self.api_key)
1031
+ return self._list_llm_models(models)
1032
+
1033
+ async def list_llm_models_async(self) -> List[LLMConfig]:
1034
+ from letta.llm_api.openai import openai_get_model_list_async
1035
+
1036
+ models = await openai_get_model_list_async(self.base_url, api_key=self.api_key)
1037
+ return self._list_llm_models(models)
1038
+
1039
+ def _list_llm_models(self, models) -> List[LLMConfig]:
1040
+ pass
952
1041
 
953
1042
  # TogetherAI's response is missing the 'data' field
954
1043
  # assert "data" in response, f"OpenAI model query response missing 'data' field: {response}"
955
- if "data" in response:
956
- data = response["data"]
1044
+ if "data" in models:
1045
+ data = models["data"]
957
1046
  else:
958
- data = response
1047
+ data = models
959
1048
 
960
1049
  configs = []
961
1050
  for model in data:
@@ -1057,7 +1146,6 @@ class GoogleAIProvider(Provider):
1057
1146
  from letta.llm_api.google_ai_client import google_ai_get_model_list
1058
1147
 
1059
1148
  model_options = google_ai_get_model_list(base_url=self.base_url, api_key=self.api_key)
1060
- # filter by 'generateContent' models
1061
1149
  model_options = [mo for mo in model_options if "generateContent" in mo["supportedGenerationMethods"]]
1062
1150
  model_options = [str(m["name"]) for m in model_options]
1063
1151
 
@@ -1081,6 +1169,42 @@ class GoogleAIProvider(Provider):
1081
1169
  provider_category=self.provider_category,
1082
1170
  )
1083
1171
  )
1172
+
1173
+ return configs
1174
+
1175
+ async def list_llm_models_async(self):
1176
+ import asyncio
1177
+
1178
+ from letta.llm_api.google_ai_client import google_ai_get_model_list_async
1179
+
1180
+ # Get and filter the model list
1181
+ model_options = await google_ai_get_model_list_async(base_url=self.base_url, api_key=self.api_key)
1182
+ model_options = [mo for mo in model_options if "generateContent" in mo["supportedGenerationMethods"]]
1183
+ model_options = [str(m["name"]) for m in model_options]
1184
+
1185
+ # filter by model names
1186
+ model_options = [mo[len("models/") :] if mo.startswith("models/") else mo for mo in model_options]
1187
+
1188
+ # Add support for all gemini models
1189
+ model_options = [mo for mo in model_options if str(mo).startswith("gemini-")]
1190
+
1191
+ # Prepare tasks for context window lookups in parallel
1192
+ async def create_config(model):
1193
+ context_window = await self.get_model_context_window_async(model)
1194
+ return LLMConfig(
1195
+ model=model,
1196
+ model_endpoint_type="google_ai",
1197
+ model_endpoint=self.base_url,
1198
+ context_window=context_window,
1199
+ handle=self.get_handle(model),
1200
+ max_tokens=8192,
1201
+ provider_name=self.name,
1202
+ provider_category=self.provider_category,
1203
+ )
1204
+
1205
+ # Execute all config creation tasks concurrently
1206
+ configs = await asyncio.gather(*[create_config(model) for model in model_options])
1207
+
1084
1208
  return configs
1085
1209
 
1086
1210
  def list_embedding_models(self):
@@ -1088,6 +1212,16 @@ class GoogleAIProvider(Provider):
1088
1212
 
1089
1213
  # TODO: use base_url instead
1090
1214
  model_options = google_ai_get_model_list(base_url=self.base_url, api_key=self.api_key)
1215
+ return self._list_embedding_models(model_options)
1216
+
1217
+ async def list_embedding_models_async(self):
1218
+ from letta.llm_api.google_ai_client import google_ai_get_model_list_async
1219
+
1220
+ # TODO: use base_url instead
1221
+ model_options = await google_ai_get_model_list_async(base_url=self.base_url, api_key=self.api_key)
1222
+ return self._list_embedding_models(model_options)
1223
+
1224
+ def _list_embedding_models(self, model_options):
1091
1225
  # filter by 'generateContent' models
1092
1226
  model_options = [mo for mo in model_options if "embedContent" in mo["supportedGenerationMethods"]]
1093
1227
  model_options = [str(m["name"]) for m in model_options]
@@ -1110,7 +1244,18 @@ class GoogleAIProvider(Provider):
1110
1244
  def get_model_context_window(self, model_name: str) -> Optional[int]:
1111
1245
  from letta.llm_api.google_ai_client import google_ai_get_model_context_window
1112
1246
 
1113
- return google_ai_get_model_context_window(self.base_url, self.api_key, model_name)
1247
+ if model_name in LLM_MAX_TOKENS:
1248
+ return LLM_MAX_TOKENS[model_name]
1249
+ else:
1250
+ return google_ai_get_model_context_window(self.base_url, self.api_key, model_name)
1251
+
1252
+ async def get_model_context_window_async(self, model_name: str) -> Optional[int]:
1253
+ from letta.llm_api.google_ai_client import google_ai_get_model_context_window_async
1254
+
1255
+ if model_name in LLM_MAX_TOKENS:
1256
+ return LLM_MAX_TOKENS[model_name]
1257
+ else:
1258
+ return await google_ai_get_model_context_window_async(self.base_url, self.api_key, model_name)
1114
1259
 
1115
1260
 
1116
1261
  class GoogleVertexProvider(Provider):
letta/schemas/step.py CHANGED
@@ -20,6 +20,7 @@ class Step(StepBase):
20
20
  )
21
21
  agent_id: Optional[str] = Field(None, description="The ID of the agent that performed the step.")
22
22
  provider_name: Optional[str] = Field(None, description="The name of the provider used for this step.")
23
+ provider_category: Optional[str] = Field(None, description="The category of the provider used for this step.")
23
24
  model: Optional[str] = Field(None, description="The name of the model used for this step.")
24
25
  model_endpoint: Optional[str] = Field(None, description="The model endpoint url used for this step.")
25
26
  context_window_limit: Optional[int] = Field(None, description="The context window limit configured for this step.")
letta/schemas/tool.py CHANGED
@@ -5,6 +5,7 @@ from pydantic import Field, model_validator
5
5
  from letta.constants import (
6
6
  COMPOSIO_TOOL_TAG_NAME,
7
7
  FUNCTION_RETURN_CHAR_LIMIT,
8
+ LETTA_BUILTIN_TOOL_MODULE_NAME,
8
9
  LETTA_CORE_TOOL_MODULE_NAME,
9
10
  LETTA_MULTI_AGENT_TOOL_MODULE_NAME,
10
11
  LETTA_VOICE_TOOL_MODULE_NAME,
@@ -104,6 +105,9 @@ class Tool(BaseTool):
104
105
  elif self.tool_type in {ToolType.LETTA_VOICE_SLEEPTIME_CORE}:
105
106
  # If it's letta voice tool, we generate the json_schema on the fly here
106
107
  self.json_schema = get_json_schema_from_module(module_name=LETTA_VOICE_TOOL_MODULE_NAME, function_name=self.name)
108
+ elif self.tool_type in {ToolType.LETTA_BUILTIN}:
109
+ # If it's letta voice tool, we generate the json_schema on the fly here
110
+ self.json_schema = get_json_schema_from_module(module_name=LETTA_BUILTIN_TOOL_MODULE_NAME, function_name=self.name)
107
111
 
108
112
  # At this point, we need to validate that at least json_schema is populated
109
113
  if not self.json_schema:
letta/server/db.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any, AsyncGenerator, Generator
6
6
  from rich.console import Console
7
7
  from rich.panel import Panel
8
8
  from rich.text import Text
9
- from sqlalchemy import Engine, create_engine
9
+ from sqlalchemy import Engine, NullPool, QueuePool, create_engine
10
10
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
11
11
  from sqlalchemy.orm import sessionmaker
12
12
 
@@ -14,6 +14,8 @@ from letta.config import LettaConfig
14
14
  from letta.log import get_logger
15
15
  from letta.settings import settings
16
16
 
17
+ logger = get_logger(__name__)
18
+
17
19
 
18
20
  def print_sqlite_schema_error():
19
21
  """Print a formatted error message for SQLite schema issues"""
@@ -76,16 +78,7 @@ class DatabaseRegistry:
76
78
  self.config.archival_storage_type = "postgres"
77
79
  self.config.archival_storage_uri = settings.letta_pg_uri_no_default
78
80
 
79
- engine = create_engine(
80
- settings.letta_pg_uri,
81
- # f"{settings.letta_pg_uri}?options=-c%20client_encoding=UTF8",
82
- pool_size=settings.pg_pool_size,
83
- max_overflow=settings.pg_max_overflow,
84
- pool_timeout=settings.pg_pool_timeout,
85
- pool_recycle=settings.pg_pool_recycle,
86
- echo=settings.pg_echo,
87
- # connect_args={"client_encoding": "utf8"},
88
- )
81
+ engine = create_engine(settings.letta_pg_uri, **self._build_sqlalchemy_engine_args(is_async=False))
89
82
 
90
83
  self._engines["default"] = engine
91
84
  # SQLite engine
@@ -125,14 +118,7 @@ class DatabaseRegistry:
125
118
  async_pg_uri = f"postgresql+asyncpg://{pg_uri.split('://', 1)[1]}" if "://" in pg_uri else pg_uri
126
119
  async_pg_uri = async_pg_uri.replace("sslmode=", "ssl=")
127
120
 
128
- async_engine = create_async_engine(
129
- async_pg_uri,
130
- pool_size=settings.pg_pool_size,
131
- max_overflow=settings.pg_max_overflow,
132
- pool_timeout=settings.pg_pool_timeout,
133
- pool_recycle=settings.pg_pool_recycle,
134
- echo=settings.pg_echo,
135
- )
121
+ async_engine = create_async_engine(async_pg_uri, **self._build_sqlalchemy_engine_args(is_async=True))
136
122
 
137
123
  self._async_engines["default"] = async_engine
138
124
 
@@ -146,6 +132,38 @@ class DatabaseRegistry:
146
132
  # TODO (cliandy): unclear around async sqlite support in sqlalchemy, we will not currently support this
147
133
  self._initialized["async"] = False
148
134
 
135
+ def _build_sqlalchemy_engine_args(self, *, is_async: bool) -> dict:
136
+ """Prepare keyword arguments for create_engine / create_async_engine."""
137
+ use_null_pool = settings.disable_sqlalchemy_pooling
138
+
139
+ if use_null_pool:
140
+ logger.info("Disabling pooling on SqlAlchemy")
141
+ pool_cls = NullPool
142
+ else:
143
+ logger.info("Enabling pooling on SqlAlchemy")
144
+ pool_cls = QueuePool if not is_async else None
145
+
146
+ base_args = {
147
+ "echo": settings.pg_echo,
148
+ "pool_pre_ping": settings.pool_pre_ping,
149
+ }
150
+
151
+ if pool_cls:
152
+ base_args["poolclass"] = pool_cls
153
+
154
+ if not use_null_pool and not is_async:
155
+ base_args.update(
156
+ {
157
+ "pool_size": settings.pg_pool_size,
158
+ "max_overflow": settings.pg_max_overflow,
159
+ "pool_timeout": settings.pg_pool_timeout,
160
+ "pool_recycle": settings.pg_pool_recycle,
161
+ "pool_use_lifo": settings.pool_use_lifo,
162
+ }
163
+ )
164
+
165
+ return base_args
166
+
149
167
  def _wrap_sqlite_engine(self, engine: Engine) -> None:
150
168
  """Wrap SQLite engine with error handling."""
151
169
  original_connect = engine.connect
@@ -13,6 +13,7 @@ from letta.server.rest_api.routers.v1.sandbox_configs import router as sandbox_c
13
13
  from letta.server.rest_api.routers.v1.sources import router as sources_router
14
14
  from letta.server.rest_api.routers.v1.steps import router as steps_router
15
15
  from letta.server.rest_api.routers.v1.tags import router as tags_router
16
+ from letta.server.rest_api.routers.v1.telemetry import router as telemetry_router
16
17
  from letta.server.rest_api.routers.v1.tools import router as tools_router
17
18
  from letta.server.rest_api.routers.v1.voice import router as voice_router
18
19
 
@@ -31,6 +32,7 @@ ROUTERS = [
31
32
  runs_router,
32
33
  steps_router,
33
34
  tags_router,
35
+ telemetry_router,
34
36
  messages_router,
35
37
  voice_router,
36
38
  embeddings_router,