letta-nightly 0.7.29.dev20250602104315__py3-none-any.whl → 0.8.0.dev20250604104349__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 (138) hide show
  1. letta/__init__.py +7 -1
  2. letta/agent.py +16 -9
  3. letta/agents/base_agent.py +1 -0
  4. letta/agents/ephemeral_summary_agent.py +104 -0
  5. letta/agents/helpers.py +35 -3
  6. letta/agents/letta_agent.py +492 -176
  7. letta/agents/letta_agent_batch.py +22 -16
  8. letta/agents/prompts/summary_system_prompt.txt +62 -0
  9. letta/agents/voice_agent.py +22 -7
  10. letta/agents/voice_sleeptime_agent.py +13 -8
  11. letta/constants.py +33 -1
  12. letta/data_sources/connectors.py +52 -36
  13. letta/errors.py +4 -0
  14. letta/functions/ast_parsers.py +13 -30
  15. letta/functions/function_sets/base.py +3 -1
  16. letta/functions/functions.py +2 -0
  17. letta/functions/mcp_client/base_client.py +151 -97
  18. letta/functions/mcp_client/sse_client.py +49 -31
  19. letta/functions/mcp_client/stdio_client.py +107 -106
  20. letta/functions/schema_generator.py +22 -22
  21. letta/groups/helpers.py +3 -4
  22. letta/groups/sleeptime_multi_agent.py +4 -4
  23. letta/groups/sleeptime_multi_agent_v2.py +22 -0
  24. letta/helpers/composio_helpers.py +16 -0
  25. letta/helpers/converters.py +20 -0
  26. letta/helpers/datetime_helpers.py +1 -6
  27. letta/helpers/tool_rule_solver.py +2 -1
  28. letta/interfaces/anthropic_streaming_interface.py +17 -2
  29. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
  30. letta/interfaces/openai_streaming_interface.py +18 -2
  31. letta/jobs/llm_batch_job_polling.py +1 -1
  32. letta/jobs/scheduler.py +1 -1
  33. letta/llm_api/anthropic_client.py +24 -3
  34. letta/llm_api/google_ai_client.py +0 -15
  35. letta/llm_api/google_vertex_client.py +6 -5
  36. letta/llm_api/llm_client_base.py +15 -0
  37. letta/llm_api/openai.py +2 -2
  38. letta/llm_api/openai_client.py +60 -8
  39. letta/orm/__init__.py +2 -0
  40. letta/orm/agent.py +45 -43
  41. letta/orm/base.py +0 -2
  42. letta/orm/block.py +1 -0
  43. letta/orm/custom_columns.py +13 -0
  44. letta/orm/enums.py +5 -0
  45. letta/orm/file.py +3 -1
  46. letta/orm/files_agents.py +68 -0
  47. letta/orm/mcp_server.py +48 -0
  48. letta/orm/message.py +1 -0
  49. letta/orm/organization.py +11 -2
  50. letta/orm/passage.py +25 -10
  51. letta/orm/sandbox_config.py +5 -2
  52. letta/orm/sqlalchemy_base.py +171 -110
  53. letta/prompts/system/memgpt_base.txt +6 -1
  54. letta/prompts/system/memgpt_v2_chat.txt +57 -0
  55. letta/prompts/system/sleeptime.txt +2 -0
  56. letta/prompts/system/sleeptime_v2.txt +28 -0
  57. letta/schemas/agent.py +87 -20
  58. letta/schemas/block.py +7 -1
  59. letta/schemas/file.py +57 -0
  60. letta/schemas/mcp.py +74 -0
  61. letta/schemas/memory.py +5 -2
  62. letta/schemas/message.py +9 -0
  63. letta/schemas/openai/openai.py +0 -6
  64. letta/schemas/providers.py +33 -4
  65. letta/schemas/tool.py +26 -21
  66. letta/schemas/tool_execution_result.py +5 -0
  67. letta/server/db.py +23 -8
  68. letta/server/rest_api/app.py +73 -56
  69. letta/server/rest_api/interface.py +4 -4
  70. letta/server/rest_api/routers/v1/agents.py +132 -47
  71. letta/server/rest_api/routers/v1/blocks.py +3 -2
  72. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  73. letta/server/rest_api/routers/v1/groups.py +3 -3
  74. letta/server/rest_api/routers/v1/jobs.py +14 -17
  75. letta/server/rest_api/routers/v1/organizations.py +10 -10
  76. letta/server/rest_api/routers/v1/providers.py +12 -10
  77. letta/server/rest_api/routers/v1/runs.py +3 -3
  78. letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
  79. letta/server/rest_api/routers/v1/sources.py +108 -43
  80. letta/server/rest_api/routers/v1/steps.py +8 -6
  81. letta/server/rest_api/routers/v1/tools.py +134 -95
  82. letta/server/rest_api/utils.py +12 -1
  83. letta/server/server.py +272 -73
  84. letta/services/agent_manager.py +246 -313
  85. letta/services/block_manager.py +30 -9
  86. letta/services/context_window_calculator/__init__.py +0 -0
  87. letta/services/context_window_calculator/context_window_calculator.py +150 -0
  88. letta/services/context_window_calculator/token_counter.py +82 -0
  89. letta/services/file_processor/__init__.py +0 -0
  90. letta/services/file_processor/chunker/__init__.py +0 -0
  91. letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
  92. letta/services/file_processor/embedder/__init__.py +0 -0
  93. letta/services/file_processor/embedder/openai_embedder.py +84 -0
  94. letta/services/file_processor/file_processor.py +123 -0
  95. letta/services/file_processor/parser/__init__.py +0 -0
  96. letta/services/file_processor/parser/base_parser.py +9 -0
  97. letta/services/file_processor/parser/mistral_parser.py +54 -0
  98. letta/services/file_processor/types.py +0 -0
  99. letta/services/files_agents_manager.py +184 -0
  100. letta/services/group_manager.py +118 -0
  101. letta/services/helpers/agent_manager_helper.py +76 -21
  102. letta/services/helpers/tool_execution_helper.py +3 -0
  103. letta/services/helpers/tool_parser_helper.py +100 -0
  104. letta/services/identity_manager.py +44 -42
  105. letta/services/job_manager.py +21 -10
  106. letta/services/mcp/base_client.py +5 -2
  107. letta/services/mcp/sse_client.py +3 -5
  108. letta/services/mcp/stdio_client.py +3 -5
  109. letta/services/mcp_manager.py +281 -0
  110. letta/services/message_manager.py +40 -26
  111. letta/services/organization_manager.py +55 -19
  112. letta/services/passage_manager.py +211 -13
  113. letta/services/provider_manager.py +48 -2
  114. letta/services/sandbox_config_manager.py +105 -0
  115. letta/services/source_manager.py +4 -5
  116. letta/services/step_manager.py +9 -6
  117. letta/services/summarizer/summarizer.py +50 -23
  118. letta/services/telemetry_manager.py +7 -0
  119. letta/services/tool_executor/tool_execution_manager.py +11 -52
  120. letta/services/tool_executor/tool_execution_sandbox.py +4 -34
  121. letta/services/tool_executor/tool_executor.py +107 -105
  122. letta/services/tool_manager.py +56 -17
  123. letta/services/tool_sandbox/base.py +39 -92
  124. letta/services/tool_sandbox/e2b_sandbox.py +16 -11
  125. letta/services/tool_sandbox/local_sandbox.py +51 -23
  126. letta/services/user_manager.py +36 -3
  127. letta/settings.py +10 -3
  128. letta/templates/__init__.py +0 -0
  129. letta/templates/sandbox_code_file.py.j2 +47 -0
  130. letta/templates/template_helper.py +16 -0
  131. letta/tracing.py +30 -1
  132. letta/types/__init__.py +7 -0
  133. letta/utils.py +25 -1
  134. {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
  135. {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +138 -112
  136. {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
  137. {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
  138. {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -12,7 +12,7 @@ router = APIRouter(prefix="/steps", tags=["steps"])
12
12
 
13
13
 
14
14
  @router.get("/", response_model=List[Step], operation_id="list_steps")
15
- def list_steps(
15
+ async def list_steps(
16
16
  before: Optional[str] = Query(None, description="Return steps before this step ID"),
17
17
  after: Optional[str] = Query(None, description="Return steps after this step ID"),
18
18
  limit: Optional[int] = Query(50, description="Maximum number of steps to return"),
@@ -21,6 +21,7 @@ def list_steps(
21
21
  end_date: Optional[str] = Query(None, description='Return steps before this ISO datetime (e.g. "2025-01-29T15:01:19-08:00")'),
22
22
  model: Optional[str] = Query(None, description="Filter by the name of the model used for the step"),
23
23
  agent_id: Optional[str] = Query(None, description="Filter by the ID of the agent that performed the step"),
24
+ trace_ids: Optional[list[str]] = Query(None, description="Filter by trace ids returned by the server"),
24
25
  server: SyncServer = Depends(get_letta_server),
25
26
  actor_id: Optional[str] = Header(None, alias="user_id"),
26
27
  ):
@@ -28,13 +29,13 @@ def list_steps(
28
29
  List steps with optional pagination and date filters.
29
30
  Dates should be provided in ISO 8601 format (e.g. 2025-01-29T15:01:19-08:00)
30
31
  """
31
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
32
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
32
33
 
33
34
  # Convert ISO strings to datetime objects if provided
34
35
  start_dt = datetime.fromisoformat(start_date) if start_date else None
35
36
  end_dt = datetime.fromisoformat(end_date) if end_date else None
36
37
 
37
- return server.step_manager.list_steps(
38
+ return await server.step_manager.list_steps_async(
38
39
  actor=actor,
39
40
  before=before,
40
41
  after=after,
@@ -44,11 +45,12 @@ def list_steps(
44
45
  order=order,
45
46
  model=model,
46
47
  agent_id=agent_id,
48
+ trace_ids=trace_ids,
47
49
  )
48
50
 
49
51
 
50
52
  @router.get("/{step_id}", response_model=Step, operation_id="retrieve_step")
51
- def retrieve_step(
53
+ async def retrieve_step(
52
54
  step_id: str,
53
55
  actor_id: Optional[str] = Header(None, alias="user_id"),
54
56
  server: SyncServer = Depends(get_letta_server),
@@ -57,8 +59,8 @@ def retrieve_step(
57
59
  Get a step by ID.
58
60
  """
59
61
  try:
60
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
61
- return server.step_manager.get_step(step_id=step_id, actor=actor)
62
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
63
+ return await server.step_manager.get_step_async(step_id=step_id, actor=actor)
62
64
  except NoResultFound:
63
65
  raise HTTPException(status_code=404, detail="Step not found")
64
66
 
@@ -21,6 +21,7 @@ from letta.schemas.letta_message import ToolReturnMessage
21
21
  from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
22
22
  from letta.server.rest_api.utils import get_letta_server
23
23
  from letta.server.server import SyncServer
24
+ from letta.settings import tool_settings
24
25
 
25
26
  router = APIRouter(prefix="/tools", tags=["tools"])
26
27
 
@@ -28,7 +29,7 @@ logger = get_logger(__name__)
28
29
 
29
30
 
30
31
  @router.delete("/{tool_id}", operation_id="delete_tool")
31
- def delete_tool(
32
+ async def delete_tool(
32
33
  tool_id: str,
33
34
  server: SyncServer = Depends(get_letta_server),
34
35
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -36,12 +37,12 @@ def delete_tool(
36
37
  """
37
38
  Delete a tool by name
38
39
  """
39
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
40
- server.tool_manager.delete_tool_by_id(tool_id=tool_id, actor=actor)
40
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
41
+ await server.tool_manager.delete_tool_by_id_async(tool_id=tool_id, actor=actor)
41
42
 
42
43
 
43
44
  @router.get("/count", response_model=int, operation_id="count_tools")
44
- def count_tools(
45
+ async def count_tools(
45
46
  server: SyncServer = Depends(get_letta_server),
46
47
  actor_id: Optional[str] = Header(None, alias="user_id"),
47
48
  include_base_tools: Optional[bool] = Query(False, description="Include built-in Letta tools in the count"),
@@ -50,9 +51,8 @@ def count_tools(
50
51
  Get a count of all tools available to agents belonging to the org of the user.
51
52
  """
52
53
  try:
53
- return server.tool_manager.size(
54
- actor=server.user_manager.get_user_or_default(user_id=actor_id), include_base_tools=include_base_tools
55
- )
54
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
55
+ return await server.tool_manager.size_async(actor=actor, include_base_tools=include_base_tools)
56
56
  except Exception as e:
57
57
  print(f"Error occurred: {e}")
58
58
  raise HTTPException(status_code=500, detail=str(e))
@@ -114,7 +114,7 @@ def count_tools(
114
114
 
115
115
 
116
116
  @router.post("/", response_model=Tool, operation_id="create_tool")
117
- def create_tool(
117
+ async def create_tool(
118
118
  request: ToolCreate = Body(...),
119
119
  server: SyncServer = Depends(get_letta_server),
120
120
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -123,9 +123,9 @@ def create_tool(
123
123
  Create a new tool
124
124
  """
125
125
  try:
126
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
126
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
127
127
  tool = Tool(**request.model_dump())
128
- return server.tool_manager.create_tool(pydantic_tool=tool, actor=actor)
128
+ return await server.tool_manager.create_tool_async(pydantic_tool=tool, actor=actor)
129
129
  except UniqueConstraintViolationError as e:
130
130
  # Log or print the full exception here for debugging
131
131
  print(f"Error occurred: {e}")
@@ -146,7 +146,7 @@ def create_tool(
146
146
 
147
147
 
148
148
  @router.put("/", response_model=Tool, operation_id="upsert_tool")
149
- def upsert_tool(
149
+ async def upsert_tool(
150
150
  request: ToolCreate = Body(...),
151
151
  server: SyncServer = Depends(get_letta_server),
152
152
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -155,8 +155,8 @@ def upsert_tool(
155
155
  Create or update a tool
156
156
  """
157
157
  try:
158
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
159
- tool = server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**request.model_dump()), actor=actor)
158
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
159
+ tool = await server.tool_manager.create_or_update_tool_async(pydantic_tool=Tool(**request.model_dump()), actor=actor)
160
160
  return tool
161
161
  except UniqueConstraintViolationError as e:
162
162
  # Log the error and raise a conflict exception
@@ -173,7 +173,7 @@ def upsert_tool(
173
173
 
174
174
 
175
175
  @router.patch("/{tool_id}", response_model=Tool, operation_id="modify_tool")
176
- def modify_tool(
176
+ async def modify_tool(
177
177
  tool_id: str,
178
178
  request: ToolUpdate = Body(...),
179
179
  server: SyncServer = Depends(get_letta_server),
@@ -183,8 +183,8 @@ def modify_tool(
183
183
  Update an existing tool
184
184
  """
185
185
  try:
186
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
187
- return server.tool_manager.update_tool_by_id(tool_id=tool_id, tool_update=request, actor=actor)
186
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
187
+ return await server.tool_manager.update_tool_by_id_async(tool_id=tool_id, tool_update=request, actor=actor)
188
188
  except LettaToolCreateError as e:
189
189
  # HTTP 400 == Bad Request
190
190
  print(f"Error occurred during tool update: {e}")
@@ -208,7 +208,7 @@ async def upsert_base_tools(
208
208
 
209
209
 
210
210
  @router.post("/run", response_model=ToolReturnMessage, operation_id="run_tool_from_source")
211
- def run_tool_from_source(
211
+ async def run_tool_from_source(
212
212
  server: SyncServer = Depends(get_letta_server),
213
213
  request: ToolRunFromSource = Body(...),
214
214
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -216,10 +216,10 @@ def run_tool_from_source(
216
216
  """
217
217
  Attempt to build a tool from source, then run it on the provided arguments
218
218
  """
219
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
219
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
220
220
 
221
221
  try:
222
- return server.run_tool_from_source(
222
+ return await server.run_tool_from_source(
223
223
  tool_source=request.source_code,
224
224
  tool_source_type=request.source_type,
225
225
  tool_args=request.args,
@@ -280,7 +280,7 @@ def list_composio_actions_by_app(
280
280
 
281
281
 
282
282
  @router.post("/composio/{composio_action_name}", response_model=Tool, operation_id="add_composio_tool")
283
- def add_composio_tool(
283
+ async def add_composio_tool(
284
284
  composio_action_name: str,
285
285
  server: SyncServer = Depends(get_letta_server),
286
286
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -288,11 +288,11 @@ def add_composio_tool(
288
288
  """
289
289
  Add a new Composio tool by action name (Composio refers to each tool as an `Action`)
290
290
  """
291
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
291
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
292
292
 
293
293
  try:
294
294
  tool_create = ToolCreate.from_composio(action_name=composio_action_name)
295
- return server.tool_manager.create_or_update_composio_tool(tool_create=tool_create, actor=actor)
295
+ return await server.tool_manager.create_or_update_composio_tool_async(tool_create=tool_create, actor=actor)
296
296
  except ConnectedAccountNotFoundError as e:
297
297
  raise HTTPException(
298
298
  status_code=400, # Bad Request
@@ -369,18 +369,22 @@ def add_composio_tool(
369
369
 
370
370
  # Specific routes for MCP
371
371
  @router.get("/mcp/servers", response_model=dict[str, Union[SSEServerConfig, StdioServerConfig]], operation_id="list_mcp_servers")
372
- def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
372
+ async def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
373
373
  """
374
374
  Get a list of all configured MCP servers
375
375
  """
376
- actor = server.user_manager.get_user_or_default(user_id=user_id)
377
- return server.get_mcp_servers()
376
+ if tool_settings.mcp_read_from_config:
377
+ return server.get_mcp_servers()
378
+ else:
379
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=user_id)
380
+ mcp_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
381
+ return {server.server_name: server.to_config() for server in mcp_servers}
378
382
 
379
383
 
380
384
  # NOTE: async because the MCP client/session calls are async
381
385
  # TODO: should we make the return type MCPTool, not Tool (since we don't have ID)?
382
386
  @router.get("/mcp/servers/{mcp_server_name}/tools", response_model=List[MCPTool], operation_id="list_mcp_tools_by_server")
383
- def list_mcp_tools_by_server(
387
+ async def list_mcp_tools_by_server(
384
388
  mcp_server_name: str,
385
389
  server: SyncServer = Depends(get_letta_server),
386
390
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -388,32 +392,36 @@ def list_mcp_tools_by_server(
388
392
  """
389
393
  Get a list of all tools for a specific MCP server
390
394
  """
391
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
392
- try:
393
- return server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
394
- except ValueError as e:
395
- # ValueError means that the MCP server name doesn't exist
396
- raise HTTPException(
397
- status_code=400, # Bad Request
398
- detail={
399
- "code": "MCPServerNotFoundError",
400
- "message": str(e),
401
- "mcp_server_name": mcp_server_name,
402
- },
403
- )
404
- except MCPTimeoutError as e:
405
- raise HTTPException(
406
- status_code=408, # Timeout
407
- detail={
408
- "code": "MCPTimeoutError",
409
- "message": str(e),
410
- "mcp_server_name": mcp_server_name,
411
- },
412
- )
395
+ if tool_settings.mcp_read_from_config:
396
+ try:
397
+ return await server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
398
+ except ValueError as e:
399
+ # ValueError means that the MCP server name doesn't exist
400
+ raise HTTPException(
401
+ status_code=400, # Bad Request
402
+ detail={
403
+ "code": "MCPServerNotFoundError",
404
+ "message": str(e),
405
+ "mcp_server_name": mcp_server_name,
406
+ },
407
+ )
408
+ except MCPTimeoutError as e:
409
+ raise HTTPException(
410
+ status_code=408, # Timeout
411
+ detail={
412
+ "code": "MCPTimeoutError",
413
+ "message": str(e),
414
+ "mcp_server_name": mcp_server_name,
415
+ },
416
+ )
417
+ else:
418
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
419
+ mcp_tools = await server.mcp_manager.list_mcp_server_tools(mcp_server_name=mcp_server_name, actor=actor)
420
+ return mcp_tools
413
421
 
414
422
 
415
423
  @router.post("/mcp/servers/{mcp_server_name}/{mcp_tool_name}", response_model=Tool, operation_id="add_mcp_tool")
416
- def add_mcp_tool(
424
+ async def add_mcp_tool(
417
425
  mcp_server_name: str,
418
426
  mcp_tool_name: str,
419
427
  server: SyncServer = Depends(get_letta_server),
@@ -424,50 +432,55 @@ def add_mcp_tool(
424
432
  """
425
433
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
426
434
 
427
- try:
428
- available_tools = server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
429
- except ValueError as e:
430
- # ValueError means that the MCP server name doesn't exist
431
- raise HTTPException(
432
- status_code=400, # Bad Request
433
- detail={
434
- "code": "MCPServerNotFoundError",
435
- "message": str(e),
436
- "mcp_server_name": mcp_server_name,
437
- },
438
- )
439
- except MCPTimeoutError as e:
440
- raise HTTPException(
441
- status_code=408, # Timeout
442
- detail={
443
- "code": "MCPTimeoutError",
444
- "message": str(e),
445
- "mcp_server_name": mcp_server_name,
446
- },
447
- )
448
-
449
- # See if the tool is in the available list
450
- mcp_tool = None
451
- for tool in available_tools:
452
- if tool.name == mcp_tool_name:
453
- mcp_tool = tool
454
- break
455
- if not mcp_tool:
456
- raise HTTPException(
457
- status_code=400, # Bad Request
458
- detail={
459
- "code": "MCPToolNotFoundError",
460
- "message": f"Tool {mcp_tool_name} not found in MCP server {mcp_server_name} - available tools: {', '.join([tool.name for tool in available_tools])}",
461
- "mcp_tool_name": mcp_tool_name,
462
- },
463
- )
464
-
465
- tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
466
- return server.tool_manager.create_or_update_mcp_tool(tool_create=tool_create, mcp_server_name=mcp_server_name, actor=actor)
435
+ if tool_settings.mcp_read_from_config:
436
+
437
+ try:
438
+ available_tools = await server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
439
+ except ValueError as e:
440
+ # ValueError means that the MCP server name doesn't exist
441
+ raise HTTPException(
442
+ status_code=400, # Bad Request
443
+ detail={
444
+ "code": "MCPServerNotFoundError",
445
+ "message": str(e),
446
+ "mcp_server_name": mcp_server_name,
447
+ },
448
+ )
449
+ except MCPTimeoutError as e:
450
+ raise HTTPException(
451
+ status_code=408, # Timeout
452
+ detail={
453
+ "code": "MCPTimeoutError",
454
+ "message": str(e),
455
+ "mcp_server_name": mcp_server_name,
456
+ },
457
+ )
458
+
459
+ # See if the tool is in the available list
460
+ mcp_tool = None
461
+ for tool in available_tools:
462
+ if tool.name == mcp_tool_name:
463
+ mcp_tool = tool
464
+ break
465
+ if not mcp_tool:
466
+ raise HTTPException(
467
+ status_code=400, # Bad Request
468
+ detail={
469
+ "code": "MCPToolNotFoundError",
470
+ "message": f"Tool {mcp_tool_name} not found in MCP server {mcp_server_name} - available tools: {', '.join([tool.name for tool in available_tools])}",
471
+ "mcp_tool_name": mcp_tool_name,
472
+ },
473
+ )
474
+
475
+ tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
476
+ return await server.tool_manager.create_mcp_tool_async(tool_create=tool_create, mcp_server_name=mcp_server_name, actor=actor)
477
+
478
+ else:
479
+ return await server.mcp_manager.add_tool_from_mcp_server(mcp_server_name=mcp_server_name, mcp_tool_name=mcp_tool_name, actor=actor)
467
480
 
468
481
 
469
482
  @router.put("/mcp/servers", response_model=List[Union[StdioServerConfig, SSEServerConfig]], operation_id="add_mcp_server")
470
- def add_mcp_server_to_config(
483
+ async def add_mcp_server_to_config(
471
484
  request: Union[StdioServerConfig, SSEServerConfig] = Body(...),
472
485
  server: SyncServer = Depends(get_letta_server),
473
486
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -475,14 +488,34 @@ def add_mcp_server_to_config(
475
488
  """
476
489
  Add a new MCP server to the Letta MCP server config
477
490
  """
478
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
479
- return server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
491
+
492
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
493
+
494
+ if tool_settings.mcp_read_from_config:
495
+ # write to config file
496
+ return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
497
+ else:
498
+ # log to DB
499
+ from letta.schemas.mcp import MCPServer
500
+
501
+ if isinstance(request, StdioServerConfig):
502
+ mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, stdio_config=request)
503
+ # don't allow stdio servers
504
+ raise HTTPException(status_code=400, detail="StdioServerConfig is not supported")
505
+ elif isinstance(request, SSEServerConfig):
506
+ mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, server_url=request.server_url)
507
+ # TODO: add HTTP streaming
508
+ mcp_server = await server.mcp_manager.create_or_update_mcp_server(mapped_request, actor=actor)
509
+
510
+ # TODO: don't do this in the future (just return MCPServer)
511
+ all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
512
+ return [server.to_config() for server in all_servers]
480
513
 
481
514
 
482
515
  @router.delete(
483
516
  "/mcp/servers/{mcp_server_name}", response_model=List[Union[StdioServerConfig, SSEServerConfig]], operation_id="delete_mcp_server"
484
517
  )
485
- def delete_mcp_server_from_config(
518
+ async def delete_mcp_server_from_config(
486
519
  mcp_server_name: str,
487
520
  server: SyncServer = Depends(get_letta_server),
488
521
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -490,5 +523,11 @@ def delete_mcp_server_from_config(
490
523
  """
491
524
  Add a new MCP server to the Letta MCP server config
492
525
  """
493
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
494
- return server.delete_mcp_server_from_config(server_name=mcp_server_name)
526
+ if tool_settings.mcp_read_from_config:
527
+ # write to config file
528
+ return server.delete_mcp_server_from_config(server_name=mcp_server_name)
529
+ else:
530
+ # log to DB
531
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
532
+ mcp_server_id = await server.mcp_manager.get_mcp_server_id_by_name(mcp_server_name, actor)
533
+ return server.mcp_manager.delete_mcp_server_by_id(mcp_server_id, actor=actor)
@@ -21,7 +21,8 @@ from letta.log import get_logger
21
21
  from letta.schemas.enums import MessageRole
22
22
  from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
23
23
  from letta.schemas.llm_config import LLMConfig
24
- from letta.schemas.message import Message, MessageCreate
24
+ from letta.schemas.message import Message, MessageCreate, ToolReturn
25
+ from letta.schemas.tool_execution_result import ToolExecutionResult
25
26
  from letta.schemas.usage import LettaUsageStatistics
26
27
  from letta.schemas.user import User
27
28
  from letta.server.rest_api.interface import StreamingServerInterface
@@ -181,6 +182,7 @@ def create_letta_messages_from_llm_response(
181
182
  model: str,
182
183
  function_name: str,
183
184
  function_arguments: Dict,
185
+ tool_execution_result: ToolExecutionResult,
184
186
  tool_call_id: str,
185
187
  function_call_success: bool,
186
188
  function_response: Optional[str],
@@ -234,6 +236,14 @@ def create_letta_messages_from_llm_response(
234
236
  created_at=get_utc_time(),
235
237
  name=function_name,
236
238
  batch_item_id=llm_batch_item_id,
239
+ tool_returns=[
240
+ ToolReturn(
241
+ status=tool_execution_result.status,
242
+ stderr=tool_execution_result.stderr,
243
+ stdout=tool_execution_result.stdout,
244
+ # func_return=tool_execution_result.func_return,
245
+ )
246
+ ],
237
247
  )
238
248
  if pre_computed_tool_message_id:
239
249
  tool_message.id = pre_computed_tool_message_id
@@ -286,6 +296,7 @@ def create_assistant_messages_from_openai_response(
286
296
  model=model,
287
297
  function_name=DEFAULT_MESSAGE_TOOL,
288
298
  function_arguments={DEFAULT_MESSAGE_TOOL_KWARG: response_text}, # Avoid raw string manipulation
299
+ tool_execution_result=ToolExecutionResult(status="success"),
289
300
  tool_call_id=tool_call_id,
290
301
  function_call_success=True,
291
302
  function_response=None,