letta-nightly 0.7.20.dev20250521104258__py3-none-any.whl → 0.7.21.dev20250522104246__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.
- letta/__init__.py +1 -1
- letta/agent.py +290 -3
- letta/agents/base_agent.py +0 -55
- letta/agents/helpers.py +5 -0
- letta/agents/letta_agent.py +314 -64
- letta/agents/letta_agent_batch.py +102 -55
- letta/agents/voice_agent.py +5 -5
- letta/client/client.py +9 -18
- letta/constants.py +55 -1
- letta/functions/function_sets/builtin.py +27 -0
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +10 -1
- letta/interfaces/openai_streaming_interface.py +9 -2
- letta/llm_api/anthropic.py +21 -2
- letta/llm_api/anthropic_client.py +33 -6
- letta/llm_api/google_ai_client.py +136 -423
- letta/llm_api/google_vertex_client.py +173 -22
- letta/llm_api/llm_api_tools.py +27 -0
- letta/llm_api/llm_client.py +1 -1
- letta/llm_api/llm_client_base.py +32 -21
- letta/llm_api/openai.py +57 -0
- letta/llm_api/openai_client.py +7 -11
- letta/memory.py +0 -1
- letta/orm/__init__.py +1 -0
- letta/orm/enums.py +1 -0
- letta/orm/provider_trace.py +26 -0
- letta/orm/step.py +1 -0
- letta/schemas/provider_trace.py +43 -0
- letta/schemas/providers.py +210 -65
- letta/schemas/step.py +1 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +37 -19
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +57 -34
- letta/server/rest_api/routers/v1/blocks.py +3 -3
- letta/server/rest_api/routers/v1/identities.py +24 -26
- letta/server/rest_api/routers/v1/jobs.py +3 -3
- letta/server/rest_api/routers/v1/llms.py +13 -8
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -6
- letta/server/rest_api/routers/v1/tags.py +3 -3
- letta/server/rest_api/routers/v1/telemetry.py +18 -0
- letta/server/rest_api/routers/v1/tools.py +6 -6
- letta/server/rest_api/streaming_response.py +105 -0
- letta/server/rest_api/utils.py +4 -0
- letta/server/server.py +140 -1
- letta/services/agent_manager.py +251 -18
- letta/services/block_manager.py +52 -37
- letta/services/helpers/noop_helper.py +10 -0
- letta/services/identity_manager.py +43 -38
- letta/services/job_manager.py +29 -0
- letta/services/message_manager.py +111 -0
- letta/services/sandbox_config_manager.py +36 -0
- letta/services/step_manager.py +146 -0
- letta/services/telemetry_manager.py +58 -0
- letta/services/tool_executor/tool_execution_manager.py +49 -5
- letta/services/tool_executor/tool_execution_sandbox.py +47 -0
- letta/services/tool_executor/tool_executor.py +236 -7
- letta/services/tool_manager.py +160 -1
- letta/services/tool_sandbox/e2b_sandbox.py +65 -3
- letta/settings.py +10 -2
- letta/tracing.py +5 -5
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/METADATA +3 -2
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/RECORD +66 -59
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.20.dev20250521104258.dist-info → letta_nightly-0.7.21.dev20250522104246.dist-info}/entry_points.txt +0 -0
letta/schemas/providers.py
CHANGED
@@ -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
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
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
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
328
|
-
|
329
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
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
|
-
|
343
|
-
|
344
|
-
|
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
|
-
|
349
|
-
|
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
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
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
|
-
|
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
|
739
|
+
from letta.llm_api.anthropic import anthropic_get_model_list
|
651
740
|
|
652
|
-
models = anthropic_get_model_list(
|
741
|
+
models = anthropic_get_model_list(api_key=self.api_key)
|
742
|
+
return self._list_llm_models(models)
|
653
743
|
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
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
|
-
|
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
|
956
|
-
data =
|
1044
|
+
if "data" in models:
|
1045
|
+
data = models["data"]
|
957
1046
|
else:
|
958
|
-
data =
|
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
|
-
|
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,
|