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.
- letta/adapters/letta_llm_adapter.py +81 -0
- letta/adapters/letta_llm_request_adapter.py +111 -0
- letta/adapters/letta_llm_stream_adapter.py +169 -0
- letta/agents/base_agent.py +4 -1
- letta/agents/base_agent_v2.py +68 -0
- letta/agents/helpers.py +3 -5
- letta/agents/letta_agent.py +23 -12
- letta/agents/letta_agent_v2.py +1220 -0
- letta/agents/voice_agent.py +2 -1
- letta/constants.py +1 -1
- letta/errors.py +12 -0
- letta/functions/function_sets/base.py +53 -12
- letta/functions/schema_generator.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +231 -0
- letta/helpers/tool_rule_solver.py +4 -0
- letta/helpers/tpuf_client.py +607 -34
- letta/interfaces/anthropic_streaming_interface.py +64 -24
- letta/interfaces/openai_streaming_interface.py +80 -37
- letta/llm_api/openai_client.py +45 -4
- letta/orm/block.py +1 -0
- letta/orm/group.py +1 -0
- letta/orm/source.py +8 -1
- letta/orm/step_metrics.py +10 -0
- letta/schemas/block.py +4 -0
- letta/schemas/enums.py +1 -0
- letta/schemas/group.py +8 -0
- letta/schemas/letta_message.py +1 -1
- letta/schemas/letta_request.py +2 -2
- letta/schemas/mcp.py +9 -1
- letta/schemas/message.py +23 -0
- letta/schemas/providers/ollama.py +1 -1
- letta/schemas/providers.py +1 -2
- letta/schemas/source.py +6 -0
- letta/schemas/step_metrics.py +2 -0
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +100 -5
- letta/server/rest_api/routers/v1/blocks.py +6 -0
- letta/server/rest_api/routers/v1/folders.py +23 -5
- letta/server/rest_api/routers/v1/groups.py +6 -0
- letta/server/rest_api/routers/v1/internal_templates.py +218 -12
- letta/server/rest_api/routers/v1/messages.py +14 -19
- letta/server/rest_api/routers/v1/runs.py +43 -28
- letta/server/rest_api/routers/v1/sources.py +23 -5
- letta/server/rest_api/routers/v1/tools.py +42 -0
- letta/server/rest_api/streaming_response.py +9 -1
- letta/server/server.py +2 -1
- letta/services/agent_manager.py +39 -59
- letta/services/agent_serialization_manager.py +22 -8
- letta/services/archive_manager.py +60 -9
- letta/services/block_manager.py +5 -0
- letta/services/file_processor/embedder/base_embedder.py +5 -0
- letta/services/file_processor/embedder/openai_embedder.py +4 -0
- letta/services/file_processor/embedder/pinecone_embedder.py +5 -1
- letta/services/file_processor/embedder/turbopuffer_embedder.py +71 -0
- letta/services/file_processor/file_processor.py +9 -7
- letta/services/group_manager.py +74 -11
- letta/services/mcp_manager.py +132 -26
- letta/services/message_manager.py +229 -125
- letta/services/passage_manager.py +2 -1
- letta/services/source_manager.py +23 -1
- letta/services/summarizer/summarizer.py +2 -0
- letta/services/tool_executor/core_tool_executor.py +2 -120
- letta/services/tool_executor/files_tool_executor.py +133 -8
- letta/settings.py +6 -0
- letta/utils.py +34 -1
- {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/METADATA +2 -2
- {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/RECORD +70 -63
- {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250909104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/licenses/LICENSE +0 -0
letta/services/mcp_manager.py
CHANGED
@@ -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()
|