letta-nightly 0.6.43.dev20250319104146__py3-none-any.whl → 0.6.43.dev20250321104124__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/agent.py +2 -2
- letta/agents/ephemeral_memory_agent.py +114 -0
- letta/agents/{low_latency_agent.py → voice_agent.py} +133 -79
- letta/client/client.py +1 -1
- letta/embeddings.py +3 -14
- letta/functions/function_sets/multi_agent.py +46 -1
- letta/functions/helpers.py +10 -57
- letta/functions/mcp_client/base_client.py +7 -9
- letta/functions/mcp_client/exceptions.py +6 -0
- letta/helpers/tool_execution_helper.py +9 -7
- letta/llm_api/anthropic.py +1 -19
- letta/llm_api/aws_bedrock.py +2 -2
- letta/llm_api/azure_openai.py +22 -46
- letta/llm_api/llm_api_tools.py +15 -4
- letta/orm/sqlalchemy_base.py +106 -7
- letta/schemas/openai/chat_completion_request.py +20 -1
- letta/schemas/providers.py +251 -0
- letta/schemas/tool.py +4 -1
- letta/server/rest_api/app.py +1 -11
- letta/server/rest_api/optimistic_json_parser.py +5 -5
- letta/server/rest_api/routers/v1/tools.py +34 -2
- letta/server/rest_api/routers/v1/voice.py +5 -5
- letta/server/server.py +6 -0
- letta/services/agent_manager.py +1 -1
- letta/services/block_manager.py +8 -6
- letta/services/message_manager.py +65 -2
- letta/settings.py +3 -3
- {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/METADATA +4 -4
- {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/RECORD +32 -30
- {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/entry_points.txt +0 -0
letta/schemas/providers.py
CHANGED
|
@@ -210,6 +210,257 @@ class OpenAIProvider(Provider):
|
|
|
210
210
|
else:
|
|
211
211
|
return LLM_MAX_TOKENS["DEFAULT"]
|
|
212
212
|
|
|
213
|
+
|
|
214
|
+
class xAIProvider(OpenAIProvider):
|
|
215
|
+
"""https://docs.x.ai/docs/api-reference"""
|
|
216
|
+
|
|
217
|
+
name: str = "xai"
|
|
218
|
+
api_key: str = Field(..., description="API key for the xAI/Grok API.")
|
|
219
|
+
base_url: str = Field("https://api.x.ai/v1", description="Base URL for the xAI/Grok API.")
|
|
220
|
+
|
|
221
|
+
def get_model_context_window_size(self, model_name: str) -> Optional[int]:
|
|
222
|
+
# xAI doesn't return context window in the model listing,
|
|
223
|
+
# so these are hardcoded from their website
|
|
224
|
+
if model_name == "grok-2-1212":
|
|
225
|
+
return 131072
|
|
226
|
+
else:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def list_llm_models(self) -> List[LLMConfig]:
|
|
230
|
+
from letta.llm_api.openai import openai_get_model_list
|
|
231
|
+
|
|
232
|
+
response = openai_get_model_list(self.base_url, api_key=self.api_key)
|
|
233
|
+
|
|
234
|
+
if "data" in response:
|
|
235
|
+
data = response["data"]
|
|
236
|
+
else:
|
|
237
|
+
data = response
|
|
238
|
+
|
|
239
|
+
configs = []
|
|
240
|
+
for model in data:
|
|
241
|
+
assert "id" in model, f"xAI/Grok model missing 'id' field: {model}"
|
|
242
|
+
model_name = model["id"]
|
|
243
|
+
|
|
244
|
+
# In case xAI starts supporting it in the future:
|
|
245
|
+
if "context_length" in model:
|
|
246
|
+
context_window_size = model["context_length"]
|
|
247
|
+
else:
|
|
248
|
+
context_window_size = self.get_model_context_window_size(model_name)
|
|
249
|
+
|
|
250
|
+
if not context_window_size:
|
|
251
|
+
warnings.warn(f"Couldn't find context window size for model {model_name}")
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
configs.append(
|
|
255
|
+
LLMConfig(
|
|
256
|
+
model=model_name,
|
|
257
|
+
model_endpoint_type="xai",
|
|
258
|
+
model_endpoint=self.base_url,
|
|
259
|
+
context_window=context_window_size,
|
|
260
|
+
handle=self.get_handle(model_name),
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return configs
|
|
265
|
+
|
|
266
|
+
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
|
267
|
+
# No embeddings supported
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class DeepSeekProvider(OpenAIProvider):
|
|
272
|
+
"""
|
|
273
|
+
DeepSeek ChatCompletions API is similar to OpenAI's reasoning API,
|
|
274
|
+
but with slight differences:
|
|
275
|
+
* For example, DeepSeek's API requires perfect interleaving of user/assistant
|
|
276
|
+
* It also does not support native function calling
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
name: str = "deepseek"
|
|
280
|
+
base_url: str = Field("https://api.deepseek.com/v1", description="Base URL for the DeepSeek API.")
|
|
281
|
+
api_key: str = Field(..., description="API key for the DeepSeek API.")
|
|
282
|
+
|
|
283
|
+
def get_model_context_window_size(self, model_name: str) -> Optional[int]:
|
|
284
|
+
# DeepSeek doesn't return context window in the model listing,
|
|
285
|
+
# so these are hardcoded from their website
|
|
286
|
+
if model_name == "deepseek-reasoner":
|
|
287
|
+
return 64000
|
|
288
|
+
elif model_name == "deepseek-chat":
|
|
289
|
+
return 64000
|
|
290
|
+
else:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def list_llm_models(self) -> List[LLMConfig]:
|
|
294
|
+
from letta.llm_api.openai import openai_get_model_list
|
|
295
|
+
|
|
296
|
+
response = openai_get_model_list(self.base_url, api_key=self.api_key)
|
|
297
|
+
|
|
298
|
+
if "data" in response:
|
|
299
|
+
data = response["data"]
|
|
300
|
+
else:
|
|
301
|
+
data = response
|
|
302
|
+
|
|
303
|
+
configs = []
|
|
304
|
+
for model in data:
|
|
305
|
+
assert "id" in model, f"DeepSeek model missing 'id' field: {model}"
|
|
306
|
+
model_name = model["id"]
|
|
307
|
+
|
|
308
|
+
# In case DeepSeek starts supporting it in the future:
|
|
309
|
+
if "context_length" in model:
|
|
310
|
+
# Context length is returned in OpenRouter as "context_length"
|
|
311
|
+
context_window_size = model["context_length"]
|
|
312
|
+
else:
|
|
313
|
+
context_window_size = self.get_model_context_window_size(model_name)
|
|
314
|
+
|
|
315
|
+
if not context_window_size:
|
|
316
|
+
warnings.warn(f"Couldn't find context window size for model {model_name}")
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Not used for deepseek-reasoner, but otherwise is true
|
|
320
|
+
put_inner_thoughts_in_kwargs = False if model_name == "deepseek-reasoner" else True
|
|
321
|
+
|
|
322
|
+
configs.append(
|
|
323
|
+
LLMConfig(
|
|
324
|
+
model=model_name,
|
|
325
|
+
model_endpoint_type="deepseek",
|
|
326
|
+
model_endpoint=self.base_url,
|
|
327
|
+
context_window=context_window_size,
|
|
328
|
+
handle=self.get_handle(model_name),
|
|
329
|
+
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return configs
|
|
334
|
+
|
|
335
|
+
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
|
336
|
+
# No embeddings supported
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class LMStudioOpenAIProvider(OpenAIProvider):
|
|
341
|
+
name: str = "lmstudio-openai"
|
|
342
|
+
base_url: str = Field(..., description="Base URL for the LMStudio OpenAI API.")
|
|
343
|
+
api_key: Optional[str] = Field(None, description="API key for the LMStudio API.")
|
|
344
|
+
|
|
345
|
+
def list_llm_models(self) -> List[LLMConfig]:
|
|
346
|
+
from letta.llm_api.openai import openai_get_model_list
|
|
347
|
+
|
|
348
|
+
# For LMStudio, we want to hit 'GET /api/v0/models' instead of 'GET /v1/models'
|
|
349
|
+
MODEL_ENDPOINT_URL = f"{self.base_url.strip('/v1')}/api/v0"
|
|
350
|
+
response = openai_get_model_list(MODEL_ENDPOINT_URL)
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
Example response:
|
|
354
|
+
|
|
355
|
+
{
|
|
356
|
+
"object": "list",
|
|
357
|
+
"data": [
|
|
358
|
+
{
|
|
359
|
+
"id": "qwen2-vl-7b-instruct",
|
|
360
|
+
"object": "model",
|
|
361
|
+
"type": "vlm",
|
|
362
|
+
"publisher": "mlx-community",
|
|
363
|
+
"arch": "qwen2_vl",
|
|
364
|
+
"compatibility_type": "mlx",
|
|
365
|
+
"quantization": "4bit",
|
|
366
|
+
"state": "not-loaded",
|
|
367
|
+
"max_context_length": 32768
|
|
368
|
+
},
|
|
369
|
+
...
|
|
370
|
+
"""
|
|
371
|
+
if "data" not in response:
|
|
372
|
+
warnings.warn(f"LMStudio OpenAI model query response missing 'data' field: {response}")
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
configs = []
|
|
376
|
+
for model in response["data"]:
|
|
377
|
+
assert "id" in model, f"Model missing 'id' field: {model}"
|
|
378
|
+
model_name = model["id"]
|
|
379
|
+
|
|
380
|
+
if "type" not in model:
|
|
381
|
+
warnings.warn(f"LMStudio OpenAI model missing 'type' field: {model}")
|
|
382
|
+
continue
|
|
383
|
+
elif model["type"] not in ["vlm", "llm"]:
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
if "max_context_length" in model:
|
|
387
|
+
context_window_size = model["max_context_length"]
|
|
388
|
+
else:
|
|
389
|
+
warnings.warn(f"LMStudio OpenAI model missing 'max_context_length' field: {model}")
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
configs.append(
|
|
393
|
+
LLMConfig(
|
|
394
|
+
model=model_name,
|
|
395
|
+
model_endpoint_type="openai",
|
|
396
|
+
model_endpoint=self.base_url,
|
|
397
|
+
context_window=context_window_size,
|
|
398
|
+
handle=self.get_handle(model_name),
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return configs
|
|
403
|
+
|
|
404
|
+
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
|
405
|
+
from letta.llm_api.openai import openai_get_model_list
|
|
406
|
+
|
|
407
|
+
# For LMStudio, we want to hit 'GET /api/v0/models' instead of 'GET /v1/models'
|
|
408
|
+
MODEL_ENDPOINT_URL = f"{self.base_url.strip('/v1')}/api/v0"
|
|
409
|
+
response = openai_get_model_list(MODEL_ENDPOINT_URL)
|
|
410
|
+
|
|
411
|
+
"""
|
|
412
|
+
Example response:
|
|
413
|
+
{
|
|
414
|
+
"object": "list",
|
|
415
|
+
"data": [
|
|
416
|
+
{
|
|
417
|
+
"id": "text-embedding-nomic-embed-text-v1.5",
|
|
418
|
+
"object": "model",
|
|
419
|
+
"type": "embeddings",
|
|
420
|
+
"publisher": "nomic-ai",
|
|
421
|
+
"arch": "nomic-bert",
|
|
422
|
+
"compatibility_type": "gguf",
|
|
423
|
+
"quantization": "Q4_0",
|
|
424
|
+
"state": "not-loaded",
|
|
425
|
+
"max_context_length": 2048
|
|
426
|
+
}
|
|
427
|
+
...
|
|
428
|
+
"""
|
|
429
|
+
if "data" not in response:
|
|
430
|
+
warnings.warn(f"LMStudio OpenAI model query response missing 'data' field: {response}")
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
configs = []
|
|
434
|
+
for model in response["data"]:
|
|
435
|
+
assert "id" in model, f"Model missing 'id' field: {model}"
|
|
436
|
+
model_name = model["id"]
|
|
437
|
+
|
|
438
|
+
if "type" not in model:
|
|
439
|
+
warnings.warn(f"LMStudio OpenAI model missing 'type' field: {model}")
|
|
440
|
+
continue
|
|
441
|
+
elif model["type"] not in ["embeddings"]:
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
if "max_context_length" in model:
|
|
445
|
+
context_window_size = model["max_context_length"]
|
|
446
|
+
else:
|
|
447
|
+
warnings.warn(f"LMStudio OpenAI model missing 'max_context_length' field: {model}")
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
configs.append(
|
|
451
|
+
EmbeddingConfig(
|
|
452
|
+
embedding_model=model_name,
|
|
453
|
+
embedding_endpoint_type="openai",
|
|
454
|
+
embedding_endpoint=self.base_url,
|
|
455
|
+
embedding_dim=context_window_size,
|
|
456
|
+
embedding_chunk_size=300, # NOTE: max is 2048
|
|
457
|
+
handle=self.get_handle(model_name),
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return configs
|
|
462
|
+
|
|
463
|
+
|
|
213
464
|
class xAIProvider(OpenAIProvider):
|
|
214
465
|
"""https://docs.x.ai/docs/api-reference"""
|
|
215
466
|
|
letta/schemas/tool.py
CHANGED
|
@@ -171,7 +171,7 @@ class ToolCreate(LettaBase):
|
|
|
171
171
|
from composio import LogLevel
|
|
172
172
|
from composio_langchain import ComposioToolSet
|
|
173
173
|
|
|
174
|
-
composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR)
|
|
174
|
+
composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR, lock=False)
|
|
175
175
|
composio_action_schemas = composio_toolset.get_action_schemas(actions=[action_name], check_connected_accounts=False)
|
|
176
176
|
|
|
177
177
|
assert len(composio_action_schemas) > 0, "User supplied parameters do not match any Composio tools"
|
|
@@ -250,3 +250,6 @@ class ToolRunFromSource(LettaBase):
|
|
|
250
250
|
name: Optional[str] = Field(None, description="The name of the tool to run.")
|
|
251
251
|
source_type: Optional[str] = Field(None, description="The type of the source code.")
|
|
252
252
|
args_json_schema: Optional[Dict] = Field(None, description="The args JSON schema of the function.")
|
|
253
|
+
json_schema: Optional[Dict] = Field(
|
|
254
|
+
None, description="The JSON schema of the function (auto-generated from source_code if not provided)"
|
|
255
|
+
)
|
letta/server/rest_api/app.py
CHANGED
|
@@ -43,16 +43,6 @@ interface: StreamingServerInterface = StreamingServerInterface
|
|
|
43
43
|
server = SyncServer(default_interface_factory=lambda: interface())
|
|
44
44
|
logger = get_logger(__name__)
|
|
45
45
|
|
|
46
|
-
# TODO: remove
|
|
47
|
-
password = None
|
|
48
|
-
## TODO(ethan): eventuall remove
|
|
49
|
-
# if password := settings.server_pass:
|
|
50
|
-
# # if the pass was specified in the environment, use it
|
|
51
|
-
# print(f"Using existing admin server password from environment.")
|
|
52
|
-
# else:
|
|
53
|
-
# # Autogenerate a password for this session and dump it to stdout
|
|
54
|
-
# password = secrets.token_urlsafe(16)
|
|
55
|
-
# #typer.secho(f"Generated admin server password for this session: {password}", fg=typer.colors.GREEN)
|
|
56
46
|
|
|
57
47
|
import logging
|
|
58
48
|
import platform
|
|
@@ -287,7 +277,7 @@ def create_application() -> "FastAPI":
|
|
|
287
277
|
app.include_router(openai_chat_completions_router, prefix=OPENAI_API_PREFIX)
|
|
288
278
|
|
|
289
279
|
# /api/auth endpoints
|
|
290
|
-
app.include_router(setup_auth_router(server, interface,
|
|
280
|
+
app.include_router(setup_auth_router(server, interface, random_password), prefix=API_PREFIX)
|
|
291
281
|
|
|
292
282
|
# / static files
|
|
293
283
|
mount_static_files(app)
|
|
@@ -32,7 +32,7 @@ class OptimisticJSONParser:
|
|
|
32
32
|
self.on_extra_token = self.default_on_extra_token
|
|
33
33
|
|
|
34
34
|
def default_on_extra_token(self, text, data, reminding):
|
|
35
|
-
|
|
35
|
+
print(f"Parsed JSON with extra tokens: {data}, remaining: {reminding}")
|
|
36
36
|
|
|
37
37
|
def parse(self, input_str):
|
|
38
38
|
"""
|
|
@@ -130,8 +130,8 @@ class OptimisticJSONParser:
|
|
|
130
130
|
if end == -1:
|
|
131
131
|
# Incomplete string
|
|
132
132
|
if not self.strict:
|
|
133
|
-
return input_str[1:], ""
|
|
134
|
-
|
|
133
|
+
return input_str[1:], "" # Lenient mode returns partial string
|
|
134
|
+
raise decode_error # Raise error for incomplete string in strict mode
|
|
135
135
|
|
|
136
136
|
str_val = input_str[: end + 1]
|
|
137
137
|
input_str = input_str[end + 1 :]
|
|
@@ -152,8 +152,8 @@ class OptimisticJSONParser:
|
|
|
152
152
|
num_str = input_str[:idx]
|
|
153
153
|
remainder = input_str[idx:]
|
|
154
154
|
|
|
155
|
-
# If it's only a sign or just '.', return as-is with empty remainder
|
|
156
|
-
if not num_str or num_str in {"-", "."}:
|
|
155
|
+
# If not strict, and it's only a sign or just '.', return as-is with empty remainder
|
|
156
|
+
if not self.strict and (not num_str or num_str in {"-", "."}):
|
|
157
157
|
return num_str, ""
|
|
158
158
|
|
|
159
159
|
try:
|
|
@@ -12,6 +12,7 @@ from composio.exceptions import (
|
|
|
12
12
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException
|
|
13
13
|
|
|
14
14
|
from letta.errors import LettaToolCreateError
|
|
15
|
+
from letta.functions.mcp_client.exceptions import MCPTimeoutError
|
|
15
16
|
from letta.functions.mcp_client.types import MCPTool, SSEServerConfig, StdioServerConfig
|
|
16
17
|
from letta.helpers.composio_helpers import get_composio_api_key
|
|
17
18
|
from letta.log import get_logger
|
|
@@ -192,6 +193,7 @@ def run_tool_from_source(
|
|
|
192
193
|
tool_env_vars=request.env_vars,
|
|
193
194
|
tool_name=request.name,
|
|
194
195
|
tool_args_json_schema=request.args_json_schema,
|
|
196
|
+
tool_json_schema=request.json_schema,
|
|
195
197
|
actor=actor,
|
|
196
198
|
)
|
|
197
199
|
except LettaToolCreateError as e:
|
|
@@ -366,6 +368,15 @@ def list_mcp_tools_by_server(
|
|
|
366
368
|
"mcp_server_name": mcp_server_name,
|
|
367
369
|
},
|
|
368
370
|
)
|
|
371
|
+
except MCPTimeoutError as e:
|
|
372
|
+
raise HTTPException(
|
|
373
|
+
status_code=408, # Timeout
|
|
374
|
+
detail={
|
|
375
|
+
"code": "MCPTimeoutError",
|
|
376
|
+
"message": str(e),
|
|
377
|
+
"mcp_server_name": mcp_server_name,
|
|
378
|
+
},
|
|
379
|
+
)
|
|
369
380
|
|
|
370
381
|
|
|
371
382
|
@router.post("/mcp/servers/{mcp_server_name}/{mcp_tool_name}", response_model=Tool, operation_id="add_mcp_tool")
|
|
@@ -380,8 +391,29 @@ def add_mcp_tool(
|
|
|
380
391
|
"""
|
|
381
392
|
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
|
382
393
|
|
|
383
|
-
|
|
384
|
-
|
|
394
|
+
try:
|
|
395
|
+
available_tools = server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
|
|
396
|
+
except ValueError as e:
|
|
397
|
+
# ValueError means that the MCP server name doesn't exist
|
|
398
|
+
raise HTTPException(
|
|
399
|
+
status_code=400, # Bad Request
|
|
400
|
+
detail={
|
|
401
|
+
"code": "MCPServerNotFoundError",
|
|
402
|
+
"message": str(e),
|
|
403
|
+
"mcp_server_name": mcp_server_name,
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
except MCPTimeoutError as e:
|
|
407
|
+
raise HTTPException(
|
|
408
|
+
status_code=408, # Timeout
|
|
409
|
+
detail={
|
|
410
|
+
"code": "MCPTimeoutError",
|
|
411
|
+
"message": str(e),
|
|
412
|
+
"mcp_server_name": mcp_server_name,
|
|
413
|
+
},
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# See if the tool is in the available list
|
|
385
417
|
mcp_tool = None
|
|
386
418
|
for tool in available_tools:
|
|
387
419
|
if tool.name == mcp_tool_name:
|
|
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Body, Depends, Header
|
|
|
6
6
|
from fastapi.responses import StreamingResponse
|
|
7
7
|
from openai.types.chat.completion_create_params import CompletionCreateParams
|
|
8
8
|
|
|
9
|
-
from letta.agents.
|
|
9
|
+
from letta.agents.voice_agent import VoiceAgent
|
|
10
10
|
from letta.log import get_logger
|
|
11
11
|
from letta.schemas.openai.chat_completions import UserMessage
|
|
12
12
|
from letta.server.rest_api.utils import get_letta_server, get_messages_from_completion_request
|
|
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from letta.server.server import SyncServer
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
router = APIRouter(prefix="/voice", tags=["voice"])
|
|
19
|
+
router = APIRouter(prefix="/voice-beta", tags=["voice"])
|
|
20
20
|
|
|
21
21
|
logger = get_logger(__name__)
|
|
22
22
|
|
|
@@ -61,15 +61,15 @@ async def create_voice_chat_completions(
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
# Instantiate our LowLatencyAgent
|
|
64
|
-
agent =
|
|
64
|
+
agent = VoiceAgent(
|
|
65
65
|
agent_id=agent_id,
|
|
66
66
|
openai_client=client,
|
|
67
67
|
message_manager=server.message_manager,
|
|
68
68
|
agent_manager=server.agent_manager,
|
|
69
69
|
block_manager=server.block_manager,
|
|
70
70
|
actor=actor,
|
|
71
|
-
message_buffer_limit=
|
|
72
|
-
message_buffer_min=
|
|
71
|
+
message_buffer_limit=50,
|
|
72
|
+
message_buffer_min=10,
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
# Return the streaming generator
|
letta/server/server.py
CHANGED
|
@@ -1202,6 +1202,7 @@ class SyncServer(Server):
|
|
|
1202
1202
|
tool_source_type: Optional[str] = None,
|
|
1203
1203
|
tool_name: Optional[str] = None,
|
|
1204
1204
|
tool_args_json_schema: Optional[Dict[str, Any]] = None,
|
|
1205
|
+
tool_json_schema: Optional[Dict[str, Any]] = None,
|
|
1205
1206
|
) -> ToolReturnMessage:
|
|
1206
1207
|
"""Run a tool from source code"""
|
|
1207
1208
|
if tool_source_type is not None and tool_source_type != "python":
|
|
@@ -1213,6 +1214,11 @@ class SyncServer(Server):
|
|
|
1213
1214
|
source_code=tool_source,
|
|
1214
1215
|
args_json_schema=tool_args_json_schema,
|
|
1215
1216
|
)
|
|
1217
|
+
|
|
1218
|
+
# If tools_json_schema is explicitly passed in, override it on the created Tool object
|
|
1219
|
+
if tool_json_schema:
|
|
1220
|
+
tool.json_schema = tool_json_schema
|
|
1221
|
+
|
|
1216
1222
|
assert tool.name is not None, "Failed to create tool object"
|
|
1217
1223
|
|
|
1218
1224
|
# TODO eventually allow using agent state in tools
|
letta/services/agent_manager.py
CHANGED
|
@@ -755,7 +755,7 @@ class AgentManager:
|
|
|
755
755
|
if updated_value != agent_state.memory.get_block(label).value:
|
|
756
756
|
# update the block if it's changed
|
|
757
757
|
block_id = agent_state.memory.get_block(label).id
|
|
758
|
-
|
|
758
|
+
self.block_manager.update_block(block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=actor)
|
|
759
759
|
|
|
760
760
|
# refresh memory from DB (using block ids)
|
|
761
761
|
agent_state.memory = Memory(
|
letta/services/block_manager.py
CHANGED
|
@@ -106,12 +106,14 @@ class BlockManager:
|
|
|
106
106
|
|
|
107
107
|
@enforce_types
|
|
108
108
|
def get_all_blocks_by_ids(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
"""Retrieve blocks by their names."""
|
|
110
|
+
with self.session_maker() as session:
|
|
111
|
+
blocks = list(
|
|
112
|
+
map(lambda obj: obj.to_pydantic(), BlockModel.read_multiple(db_session=session, identifiers=block_ids, actor=actor))
|
|
113
|
+
)
|
|
114
|
+
# backwards compatibility. previous implementation added None for every block not found.
|
|
115
|
+
blocks.extend([None for _ in range(len(block_ids) - len(blocks))])
|
|
116
|
+
return blocks
|
|
115
117
|
|
|
116
118
|
@enforce_types
|
|
117
119
|
def add_default_blocks(self, actor: PydanticUser):
|
|
@@ -63,8 +63,71 @@ class MessageManager:
|
|
|
63
63
|
|
|
64
64
|
@enforce_types
|
|
65
65
|
def create_many_messages(self, pydantic_msgs: List[PydanticMessage], actor: PydanticUser) -> List[PydanticMessage]:
|
|
66
|
-
"""
|
|
67
|
-
|
|
66
|
+
"""
|
|
67
|
+
Create multiple messages in a single database transaction.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
pydantic_msgs: List of Pydantic message models to create
|
|
71
|
+
actor: User performing the action
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of created Pydantic message models
|
|
75
|
+
"""
|
|
76
|
+
if not pydantic_msgs:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
# Create ORM model instances for all messages
|
|
80
|
+
orm_messages = []
|
|
81
|
+
for pydantic_msg in pydantic_msgs:
|
|
82
|
+
# Set the organization id of the Pydantic message
|
|
83
|
+
pydantic_msg.organization_id = actor.organization_id
|
|
84
|
+
msg_data = pydantic_msg.model_dump(to_orm=True)
|
|
85
|
+
orm_messages.append(MessageModel(**msg_data))
|
|
86
|
+
|
|
87
|
+
# Use the batch_create method for efficient creation
|
|
88
|
+
with self.session_maker() as session:
|
|
89
|
+
created_messages = MessageModel.batch_create(orm_messages, session, actor=actor)
|
|
90
|
+
|
|
91
|
+
# Convert back to Pydantic models
|
|
92
|
+
return [msg.to_pydantic() for msg in created_messages]
|
|
93
|
+
|
|
94
|
+
@enforce_types
|
|
95
|
+
def update_message_by_letta_message(
|
|
96
|
+
self, message_id: str, letta_message_update: LettaMessageUpdateUnion, actor: PydanticUser
|
|
97
|
+
) -> PydanticMessage:
|
|
98
|
+
"""
|
|
99
|
+
Updated the underlying messages table giving an update specified to the user-facing LettaMessage
|
|
100
|
+
"""
|
|
101
|
+
message = self.get_message_by_id(message_id=message_id, actor=actor)
|
|
102
|
+
if letta_message_update.message_type == "assistant_message":
|
|
103
|
+
# modify the tool call for send_message
|
|
104
|
+
# TODO: fix this if we add parallel tool calls
|
|
105
|
+
# TODO: note this only works if the AssistantMessage is generated by the standard send_message
|
|
106
|
+
assert (
|
|
107
|
+
message.tool_calls[0].function.name == "send_message"
|
|
108
|
+
), f"Expected the first tool call to be send_message, but got {message.tool_calls[0].function.name}"
|
|
109
|
+
original_args = json.loads(message.tool_calls[0].function.arguments)
|
|
110
|
+
original_args["message"] = letta_message_update.content # override the assistant message
|
|
111
|
+
update_tool_call = message.tool_calls[0].__deepcopy__()
|
|
112
|
+
update_tool_call.function.arguments = json.dumps(original_args)
|
|
113
|
+
|
|
114
|
+
update_message = MessageUpdate(tool_calls=[update_tool_call])
|
|
115
|
+
elif letta_message_update.message_type == "reasoning_message":
|
|
116
|
+
update_message = MessageUpdate(content=letta_message_update.reasoning)
|
|
117
|
+
elif letta_message_update.message_type == "user_message" or letta_message_update.message_type == "system_message":
|
|
118
|
+
update_message = MessageUpdate(content=letta_message_update.content)
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError(f"Unsupported message type for modification: {letta_message_update.message_type}")
|
|
121
|
+
|
|
122
|
+
message = self.update_message_by_id(message_id=message_id, message_update=update_message, actor=actor)
|
|
123
|
+
|
|
124
|
+
# convert back to LettaMessage
|
|
125
|
+
for letta_msg in message.to_letta_message(use_assistant_message=True):
|
|
126
|
+
if letta_msg.message_type == letta_message_update.message_type:
|
|
127
|
+
return letta_msg
|
|
128
|
+
|
|
129
|
+
# raise error if message type got modified
|
|
130
|
+
raise ValueError(f"Message type got modified: {letta_message_update.message_type}")
|
|
68
131
|
|
|
69
132
|
@enforce_types
|
|
70
133
|
def update_message_by_letta_message(
|
letta/settings.py
CHANGED
|
@@ -19,8 +19,8 @@ class ToolSettings(BaseSettings):
|
|
|
19
19
|
local_sandbox_dir: Optional[str] = None
|
|
20
20
|
|
|
21
21
|
# MCP settings
|
|
22
|
-
mcp_connect_to_server_timeout: float =
|
|
23
|
-
mcp_list_tools_timeout: float =
|
|
22
|
+
mcp_connect_to_server_timeout: float = 30.0
|
|
23
|
+
mcp_list_tools_timeout: float = 30.0
|
|
24
24
|
mcp_execute_tool_timeout: float = 60.0
|
|
25
25
|
mcp_read_from_config: bool = True # if False, will throw if attempting to read/write from file
|
|
26
26
|
|
|
@@ -179,7 +179,7 @@ class Settings(BaseSettings):
|
|
|
179
179
|
|
|
180
180
|
# telemetry logging
|
|
181
181
|
verbose_telemetry_logging: bool = False
|
|
182
|
-
otel_exporter_otlp_endpoint: str = "http://localhost:4317"
|
|
182
|
+
otel_exporter_otlp_endpoint: Optional[str] = None # otel default: "http://localhost:4317"
|
|
183
183
|
disable_tracing: bool = False
|
|
184
184
|
|
|
185
185
|
# uvicorn settings
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: letta-nightly
|
|
3
|
-
Version: 0.6.43.
|
|
3
|
+
Version: 0.6.43.dev20250321104124
|
|
4
4
|
Summary: Create LLM agents with long-term memory and custom tools
|
|
5
5
|
License: Apache License
|
|
6
6
|
Author: Letta Team
|
|
@@ -31,7 +31,7 @@ Requires-Dist: brotli (>=1.1.0,<2.0.0)
|
|
|
31
31
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
32
32
|
Requires-Dist: composio-core (>=0.7.7,<0.8.0)
|
|
33
33
|
Requires-Dist: composio-langchain (>=0.7.7,<0.8.0)
|
|
34
|
-
Requires-Dist: datamodel-code-generator[http] (>=0.25.0,<0.26.0)
|
|
34
|
+
Requires-Dist: datamodel-code-generator[http] (>=0.25.0,<0.26.0) ; extra == "desktop" or extra == "all"
|
|
35
35
|
Requires-Dist: demjson3 (>=3.0.6,<4.0.0)
|
|
36
36
|
Requires-Dist: docker (>=7.1.0,<8.0.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
|
37
37
|
Requires-Dist: docstring-parser (>=0.16,<0.17)
|
|
@@ -49,12 +49,12 @@ Requires-Dist: isort (>=5.13.2,<6.0.0) ; extra == "dev" or extra == "all"
|
|
|
49
49
|
Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
|
|
50
50
|
Requires-Dist: langchain (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
|
51
51
|
Requires-Dist: langchain-community (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
|
52
|
-
Requires-Dist: letta_client (>=0.1.65,<0.2.0)
|
|
52
|
+
Requires-Dist: letta_client (>=0.1.65,<0.2.0) ; extra == "desktop"
|
|
53
53
|
Requires-Dist: llama-index (>=0.12.2,<0.13.0)
|
|
54
54
|
Requires-Dist: llama-index-embeddings-openai (>=0.3.1,<0.4.0)
|
|
55
55
|
Requires-Dist: locust (>=2.31.5,<3.0.0) ; extra == "dev" or extra == "desktop" or extra == "all"
|
|
56
56
|
Requires-Dist: marshmallow-sqlalchemy (>=1.4.1,<2.0.0)
|
|
57
|
-
Requires-Dist: mcp (>=1.3.0,<2.0.0)
|
|
57
|
+
Requires-Dist: mcp (>=1.3.0,<2.0.0) ; extra == "desktop"
|
|
58
58
|
Requires-Dist: nltk (>=3.8.1,<4.0.0)
|
|
59
59
|
Requires-Dist: numpy (>=1.26.2,<2.0.0)
|
|
60
60
|
Requires-Dist: openai (>=1.60.0,<2.0.0)
|