letta-nightly 0.11.7.dev20250909104137__py3-none-any.whl → 0.11.7.dev20250910104051__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 (70) hide show
  1. letta/adapters/letta_llm_adapter.py +81 -0
  2. letta/adapters/letta_llm_request_adapter.py +111 -0
  3. letta/adapters/letta_llm_stream_adapter.py +169 -0
  4. letta/agents/base_agent.py +4 -1
  5. letta/agents/base_agent_v2.py +68 -0
  6. letta/agents/helpers.py +3 -5
  7. letta/agents/letta_agent.py +23 -12
  8. letta/agents/letta_agent_v2.py +1220 -0
  9. letta/agents/voice_agent.py +2 -1
  10. letta/constants.py +1 -1
  11. letta/errors.py +12 -0
  12. letta/functions/function_sets/base.py +53 -12
  13. letta/functions/schema_generator.py +1 -1
  14. letta/groups/sleeptime_multi_agent_v3.py +231 -0
  15. letta/helpers/tool_rule_solver.py +4 -0
  16. letta/helpers/tpuf_client.py +607 -34
  17. letta/interfaces/anthropic_streaming_interface.py +64 -24
  18. letta/interfaces/openai_streaming_interface.py +80 -37
  19. letta/llm_api/openai_client.py +45 -4
  20. letta/orm/block.py +1 -0
  21. letta/orm/group.py +1 -0
  22. letta/orm/source.py +8 -1
  23. letta/orm/step_metrics.py +10 -0
  24. letta/schemas/block.py +4 -0
  25. letta/schemas/enums.py +1 -0
  26. letta/schemas/group.py +8 -0
  27. letta/schemas/letta_message.py +1 -1
  28. letta/schemas/letta_request.py +2 -2
  29. letta/schemas/mcp.py +9 -1
  30. letta/schemas/message.py +23 -0
  31. letta/schemas/providers/ollama.py +1 -1
  32. letta/schemas/providers.py +1 -2
  33. letta/schemas/source.py +6 -0
  34. letta/schemas/step_metrics.py +2 -0
  35. letta/server/rest_api/routers/v1/__init__.py +2 -0
  36. letta/server/rest_api/routers/v1/agents.py +100 -5
  37. letta/server/rest_api/routers/v1/blocks.py +6 -0
  38. letta/server/rest_api/routers/v1/folders.py +23 -5
  39. letta/server/rest_api/routers/v1/groups.py +6 -0
  40. letta/server/rest_api/routers/v1/internal_templates.py +218 -12
  41. letta/server/rest_api/routers/v1/messages.py +14 -19
  42. letta/server/rest_api/routers/v1/runs.py +43 -28
  43. letta/server/rest_api/routers/v1/sources.py +23 -5
  44. letta/server/rest_api/routers/v1/tools.py +42 -0
  45. letta/server/rest_api/streaming_response.py +9 -1
  46. letta/server/server.py +2 -1
  47. letta/services/agent_manager.py +39 -59
  48. letta/services/agent_serialization_manager.py +22 -8
  49. letta/services/archive_manager.py +60 -9
  50. letta/services/block_manager.py +5 -0
  51. letta/services/file_processor/embedder/base_embedder.py +5 -0
  52. letta/services/file_processor/embedder/openai_embedder.py +4 -0
  53. letta/services/file_processor/embedder/pinecone_embedder.py +5 -1
  54. letta/services/file_processor/embedder/turbopuffer_embedder.py +71 -0
  55. letta/services/file_processor/file_processor.py +9 -7
  56. letta/services/group_manager.py +74 -11
  57. letta/services/mcp_manager.py +132 -26
  58. letta/services/message_manager.py +229 -125
  59. letta/services/passage_manager.py +2 -1
  60. letta/services/source_manager.py +23 -1
  61. letta/services/summarizer/summarizer.py +2 -0
  62. letta/services/tool_executor/core_tool_executor.py +2 -120
  63. letta/services/tool_executor/files_tool_executor.py +133 -8
  64. letta/settings.py +6 -0
  65. letta/utils.py +34 -1
  66. {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/METADATA +2 -2
  67. {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/RECORD +70 -63
  68. {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/WHEEL +0 -0
  69. {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/entry_points.txt +0 -0
  70. {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
6
6
  from typing import Any, Dict, List, Optional, Tuple, Union
7
7
 
8
8
  from fastapi import HTTPException
9
- from sqlalchemy import delete, null
9
+ from sqlalchemy import delete, desc, null, select
10
10
  from starlette.requests import Request
11
11
 
12
12
  import letta.constants as constants
@@ -23,17 +23,19 @@ from letta.log import get_logger
23
23
  from letta.orm.errors import NoResultFound
24
24
  from letta.orm.mcp_oauth import MCPOAuth, OAuthSessionStatus
25
25
  from letta.orm.mcp_server import MCPServer as MCPServerModel
26
+ from letta.orm.tool import Tool as ToolModel
26
27
  from letta.schemas.mcp import (
27
28
  MCPOAuthSession,
28
29
  MCPOAuthSessionCreate,
29
30
  MCPOAuthSessionUpdate,
30
31
  MCPServer,
32
+ MCPServerResyncResult,
31
33
  UpdateMCPServer,
32
34
  UpdateSSEMCPServer,
33
35
  UpdateStdioMCPServer,
34
36
  UpdateStreamableHTTPMCPServer,
35
37
  )
36
- from letta.schemas.tool import Tool as PydanticTool, ToolCreate
38
+ from letta.schemas.tool import Tool as PydanticTool, ToolCreate, ToolUpdate
37
39
  from letta.schemas.user import User as PydanticUser
38
40
  from letta.server.db import db_registry
39
41
  from letta.services.mcp.sse_client import MCP_CONFIG_TOPLEVEL_KEY, AsyncSSEMCPClient
@@ -147,6 +149,117 @@ class MCPManager:
147
149
  # failed to add - handle error?
148
150
  return None
149
151
 
152
+ @enforce_types
153
+ async def resync_mcp_server_tools(
154
+ self, mcp_server_name: str, actor: PydanticUser, agent_id: Optional[str] = None
155
+ ) -> MCPServerResyncResult:
156
+ """
157
+ Resync tools for an MCP server by:
158
+ 1. Fetching current tools from the MCP server
159
+ 2. Deleting tools that no longer exist on the server
160
+ 3. Updating schemas for existing tools
161
+ 4. Adding new tools from the server
162
+
163
+ Returns a result with:
164
+ - deleted: List of deleted tool names
165
+ - updated: List of updated tool names
166
+ - added: List of added tool names
167
+ """
168
+ # Get the MCP server ID
169
+ mcp_server_id = await self.get_mcp_server_id_by_name(mcp_server_name, actor=actor)
170
+ if not mcp_server_id:
171
+ raise ValueError(f"MCP server '{mcp_server_name}' not found")
172
+
173
+ # Fetch current tools from MCP server
174
+ try:
175
+ current_mcp_tools = await self.list_mcp_server_tools(mcp_server_name, actor=actor, agent_id=agent_id)
176
+ except Exception as e:
177
+ logger.error(f"Failed to fetch tools from MCP server {mcp_server_name}: {e}")
178
+ raise HTTPException(
179
+ status_code=404,
180
+ detail={
181
+ "code": "MCPServerUnavailable",
182
+ "message": f"Could not connect to MCP server {mcp_server_name} to resync tools",
183
+ "error": str(e),
184
+ },
185
+ )
186
+
187
+ # Get all persisted tools for this MCP server
188
+ async with db_registry.async_session() as session:
189
+ # Query for tools with MCP metadata matching this server
190
+ # Using JSON path query to filter by metadata
191
+ persisted_tools = await ToolModel.list_async(
192
+ db_session=session,
193
+ organization_id=actor.organization_id,
194
+ )
195
+
196
+ # Filter tools that belong to this MCP server
197
+ mcp_tools = []
198
+ for tool in persisted_tools:
199
+ if tool.metadata_ and constants.MCP_TOOL_TAG_NAME_PREFIX in tool.metadata_:
200
+ if tool.metadata_[constants.MCP_TOOL_TAG_NAME_PREFIX].get("server_id") == mcp_server_id:
201
+ mcp_tools.append(tool)
202
+
203
+ # Create maps for easier comparison
204
+ current_tool_map = {tool.name: tool for tool in current_mcp_tools}
205
+ persisted_tool_map = {tool.name: tool for tool in mcp_tools}
206
+
207
+ deleted_tools = []
208
+ updated_tools = []
209
+ added_tools = []
210
+
211
+ # 1. Delete tools that no longer exist on the server
212
+ for tool_name, persisted_tool in persisted_tool_map.items():
213
+ if tool_name not in current_tool_map:
214
+ # Delete the tool (cascade will handle agent detachment)
215
+ await persisted_tool.hard_delete_async(db_session=session, actor=actor)
216
+ deleted_tools.append(tool_name)
217
+ logger.info(f"Deleted MCP tool {tool_name} as it no longer exists on server {mcp_server_name}")
218
+
219
+ # Commit deletions
220
+ await session.commit()
221
+
222
+ # 2. Update existing tools and add new tools
223
+ for tool_name, current_tool in current_tool_map.items():
224
+ if tool_name in persisted_tool_map:
225
+ # Update existing tool
226
+ persisted_tool = persisted_tool_map[tool_name]
227
+ tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=current_tool)
228
+
229
+ # Check if schema has changed
230
+ if persisted_tool.json_schema != tool_create.json_schema:
231
+ # Update the tool
232
+ update_data = ToolUpdate(
233
+ description=tool_create.description,
234
+ json_schema=tool_create.json_schema,
235
+ source_code=tool_create.source_code,
236
+ )
237
+
238
+ await self.tool_manager.update_tool_by_id_async(tool_id=persisted_tool.id, tool_update=update_data, actor=actor)
239
+ updated_tools.append(tool_name)
240
+ logger.info(f"Updated MCP tool {tool_name} with new schema from server {mcp_server_name}")
241
+ else:
242
+ # Add new tool
243
+ # Skip INVALID tools
244
+ if current_tool.health and current_tool.health.status == "INVALID":
245
+ logger.warning(
246
+ f"Skipping invalid tool {tool_name} from MCP server {mcp_server_name}: {', '.join(current_tool.health.reasons)}"
247
+ )
248
+ continue
249
+
250
+ tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=current_tool)
251
+ await self.tool_manager.create_mcp_tool_async(
252
+ tool_create=tool_create, mcp_server_name=mcp_server_name, mcp_server_id=mcp_server_id, actor=actor
253
+ )
254
+ added_tools.append(tool_name)
255
+ logger.info(f"Added new MCP tool {tool_name} from server {mcp_server_name}")
256
+
257
+ return MCPServerResyncResult(
258
+ deleted=deleted_tools,
259
+ updated=updated_tools,
260
+ added=added_tools,
261
+ )
262
+
150
263
  @enforce_types
151
264
  async def list_mcp_servers(self, actor: PydanticUser) -> List[MCPServer]:
152
265
  """List all MCP servers available"""
@@ -209,8 +322,6 @@ class MCPManager:
209
322
  # This ensures OAuth sessions created during testing get linked to the server
210
323
  server_url = getattr(mcp_server, "server_url", None)
211
324
  if server_url:
212
- from sqlalchemy import select
213
-
214
325
  result = await session.execute(
215
326
  select(MCPOAuth).where(
216
327
  MCPOAuth.server_url == server_url,
@@ -326,26 +437,9 @@ class MCPManager:
326
437
  )
327
438
  return mcp_server.to_pydantic()
328
439
 
329
- # @enforce_types
330
- # async def delete_mcp_server(self, mcp_server_name: str, actor: PydanticUser) -> None:
331
- # """Delete an existing tool."""
332
- # with db_registry.session() as session:
333
- # mcp_server_id = await self.get_mcp_server_id_by_name(mcp_server_name, actor)
334
- # mcp_server = await MCPServerModel.read_async(db_session=session, identifier=mcp_server_id, actor=actor)
335
- # if not mcp_server:
336
- # raise HTTPException(
337
- # status_code=404, # Not Found
338
- # detail={
339
- # "code": "MCPServerNotFoundError",
340
- # "message": f"MCP server {mcp_server_name} not found",
341
- # "mcp_server_name": mcp_server_name,
342
- # },
343
- # )
344
- # mcp_server.delete(session, actor=actor) # Re-raise other database-related errors
345
-
346
440
  @enforce_types
347
441
  async def delete_mcp_server_by_id(self, mcp_server_id: str, actor: PydanticUser) -> None:
348
- """Delete a MCP server by its ID."""
442
+ """Delete a MCP server by its ID and associated tools and OAuth sessions."""
349
443
  async with db_registry.async_session() as session:
350
444
  try:
351
445
  mcp_server = await MCPServerModel.read_async(db_session=session, identifier=mcp_server_id, actor=actor)
@@ -353,6 +447,22 @@ class MCPManager:
353
447
  raise NoResultFound(f"MCP server with id {mcp_server_id} not found.")
354
448
 
355
449
  server_url = getattr(mcp_server, "server_url", None)
450
+ # Get all tools with matching metadata
451
+ stmt = select(ToolModel).where(ToolModel.organization_id == actor.organization_id)
452
+ result = await session.execute(stmt)
453
+ all_tools = result.scalars().all()
454
+
455
+ # Filter and delete tools that belong to this MCP server
456
+ tools_deleted = 0
457
+ for tool in all_tools:
458
+ if tool.metadata_ and constants.MCP_TOOL_TAG_NAME_PREFIX in tool.metadata_:
459
+ if tool.metadata_[constants.MCP_TOOL_TAG_NAME_PREFIX].get("server_id") == mcp_server_id:
460
+ await tool.hard_delete_async(db_session=session, actor=actor)
461
+ tools_deleted = 1
462
+ logger.info(f"Deleted MCP tool {tool.name} associated with MCP server {mcp_server_id}")
463
+
464
+ if tools_deleted > 0:
465
+ logger.info(f"Deleted {tools_deleted} MCP tools associated with MCP server {mcp_server_id}")
356
466
 
357
467
  # Delete OAuth sessions for the same user and server URL in the same transaction
358
468
  # This handles orphaned sessions that were created during testing/connection
@@ -557,8 +667,6 @@ class MCPManager:
557
667
  @enforce_types
558
668
  async def get_oauth_session_by_server(self, server_url: str, actor: PydanticUser) -> Optional[MCPOAuthSession]:
559
669
  """Get the latest OAuth session by server URL, organization, and user."""
560
- from sqlalchemy import desc, select
561
-
562
670
  async with db_registry.async_session() as session:
563
671
  # Query for OAuth session matching organization, user, server URL, and status
564
672
  # Order by updated_at desc to get the most recent record
@@ -673,8 +781,6 @@ class MCPManager:
673
781
  cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
674
782
 
675
783
  async with db_registry.async_session() as session:
676
- from sqlalchemy import select
677
-
678
784
  # Find expired sessions
679
785
  result = await session.execute(select(MCPOAuth).where(MCPOAuth.created_at < cutoff_time))
680
786
  expired_sessions = result.scalars().all()