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.

Files changed (32) hide show
  1. letta/agent.py +2 -2
  2. letta/agents/ephemeral_memory_agent.py +114 -0
  3. letta/agents/{low_latency_agent.py → voice_agent.py} +133 -79
  4. letta/client/client.py +1 -1
  5. letta/embeddings.py +3 -14
  6. letta/functions/function_sets/multi_agent.py +46 -1
  7. letta/functions/helpers.py +10 -57
  8. letta/functions/mcp_client/base_client.py +7 -9
  9. letta/functions/mcp_client/exceptions.py +6 -0
  10. letta/helpers/tool_execution_helper.py +9 -7
  11. letta/llm_api/anthropic.py +1 -19
  12. letta/llm_api/aws_bedrock.py +2 -2
  13. letta/llm_api/azure_openai.py +22 -46
  14. letta/llm_api/llm_api_tools.py +15 -4
  15. letta/orm/sqlalchemy_base.py +106 -7
  16. letta/schemas/openai/chat_completion_request.py +20 -1
  17. letta/schemas/providers.py +251 -0
  18. letta/schemas/tool.py +4 -1
  19. letta/server/rest_api/app.py +1 -11
  20. letta/server/rest_api/optimistic_json_parser.py +5 -5
  21. letta/server/rest_api/routers/v1/tools.py +34 -2
  22. letta/server/rest_api/routers/v1/voice.py +5 -5
  23. letta/server/server.py +6 -0
  24. letta/services/agent_manager.py +1 -1
  25. letta/services/block_manager.py +8 -6
  26. letta/services/message_manager.py +65 -2
  27. letta/settings.py +3 -3
  28. {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/METADATA +4 -4
  29. {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/RECORD +32 -30
  30. {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/LICENSE +0 -0
  31. {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/WHEEL +0 -0
  32. {letta_nightly-0.6.43.dev20250319104146.dist-info → letta_nightly-0.6.43.dev20250321104124.dist-info}/entry_points.txt +0 -0
@@ -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
+ )
@@ -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, password), prefix=API_PREFIX)
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
- pass
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
- return json.loads(f'"{input_str[1:]}"'), ""
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
- available_tools = server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
384
- # See if the tool is in the avaialable list
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.low_latency_agent import LowLatencyAgent
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 = LowLatencyAgent(
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=10,
72
- message_buffer_min=4,
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
@@ -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
- block = self.block_manager.update_block(block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=actor)
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(
@@ -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
- # TODO: We can do this much more efficiently by listing, instead of executing individual queries per block_id
110
- blocks = []
111
- for block_id in block_ids:
112
- block = self.get_block_by_id(block_id, actor=actor)
113
- blocks.append(block)
114
- return blocks
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
- """Create multiple messages."""
67
- return [self.create_message(m, actor=actor) for m in pydantic_msgs]
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 = 15.0
23
- mcp_list_tools_timeout: float = 10.0
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.dev20250319104146
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)