letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251024223017__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/__init__.py +2 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, Request
|
|
4
|
+
from httpx import HTTPStatusError
|
|
5
|
+
from starlette.responses import StreamingResponse
|
|
6
|
+
|
|
7
|
+
from letta.functions.mcp_client.types import SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig
|
|
8
|
+
from letta.log import get_logger
|
|
9
|
+
from letta.schemas.letta_message import ToolReturnMessage
|
|
10
|
+
from letta.schemas.mcp_server import (
|
|
11
|
+
CreateMCPServerUnion,
|
|
12
|
+
MCPServerUnion,
|
|
13
|
+
MCPToolExecuteRequest,
|
|
14
|
+
UpdateMCPServerUnion,
|
|
15
|
+
convert_generic_to_union,
|
|
16
|
+
convert_update_to_internal,
|
|
17
|
+
)
|
|
18
|
+
from letta.schemas.tool import Tool
|
|
19
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
|
20
|
+
from letta.server.rest_api.dependencies import (
|
|
21
|
+
HeaderParams,
|
|
22
|
+
get_headers,
|
|
23
|
+
get_letta_server,
|
|
24
|
+
)
|
|
25
|
+
from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode
|
|
26
|
+
from letta.server.server import SyncServer
|
|
27
|
+
from letta.services.mcp.oauth_utils import drill_down_exception, oauth_stream_event
|
|
28
|
+
from letta.services.mcp.stdio_client import AsyncStdioMCPClient
|
|
29
|
+
from letta.services.mcp.types import OauthStreamEvent
|
|
30
|
+
from letta.settings import tool_settings
|
|
31
|
+
|
|
32
|
+
router = APIRouter(prefix="/mcp-servers", tags=["mcp-servers"])
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.post(
|
|
38
|
+
"/",
|
|
39
|
+
response_model=MCPServerUnion,
|
|
40
|
+
operation_id="mcp_create_mcp_server",
|
|
41
|
+
)
|
|
42
|
+
async def create_mcp_server(
|
|
43
|
+
request: CreateMCPServerUnion = Body(...),
|
|
44
|
+
server: SyncServer = Depends(get_letta_server),
|
|
45
|
+
headers: HeaderParams = Depends(get_headers),
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Add a new MCP server to the Letta MCP server config
|
|
49
|
+
"""
|
|
50
|
+
# TODO: add the tools to the MCP server table we made.
|
|
51
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
52
|
+
new_server = await server.mcp_server_manager.create_mcp_server_from_config_with_tools(request, actor=actor)
|
|
53
|
+
return convert_generic_to_union(new_server)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get(
|
|
57
|
+
"/",
|
|
58
|
+
response_model=List[MCPServerUnion],
|
|
59
|
+
operation_id="mcp_list_mcp_servers",
|
|
60
|
+
)
|
|
61
|
+
async def list_mcp_servers(
|
|
62
|
+
server: SyncServer = Depends(get_letta_server),
|
|
63
|
+
headers: HeaderParams = Depends(get_headers),
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Get a list of all configured MCP servers
|
|
67
|
+
"""
|
|
68
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
69
|
+
mcp_servers = await server.mcp_server_manager.list_mcp_servers(actor=actor)
|
|
70
|
+
return [convert_generic_to_union(mcp_server) for mcp_server in mcp_servers]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.get(
|
|
74
|
+
"/{mcp_server_id}",
|
|
75
|
+
response_model=MCPServerUnion,
|
|
76
|
+
operation_id="mcp_get_mcp_server",
|
|
77
|
+
)
|
|
78
|
+
async def get_mcp_server(
|
|
79
|
+
mcp_server_id: str,
|
|
80
|
+
server: SyncServer = Depends(get_letta_server),
|
|
81
|
+
headers: HeaderParams = Depends(get_headers),
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Get a specific MCP server
|
|
85
|
+
"""
|
|
86
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
87
|
+
current_server = await server.mcp_server_manager.get_mcp_server_by_id_async(mcp_server_id=mcp_server_id, actor=actor)
|
|
88
|
+
return convert_generic_to_union(current_server)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.delete(
|
|
92
|
+
"/{mcp_server_id}",
|
|
93
|
+
status_code=204,
|
|
94
|
+
operation_id="mcp_delete_mcp_server",
|
|
95
|
+
)
|
|
96
|
+
async def delete_mcp_server(
|
|
97
|
+
mcp_server_id: str,
|
|
98
|
+
server: SyncServer = Depends(get_letta_server),
|
|
99
|
+
headers: HeaderParams = Depends(get_headers),
|
|
100
|
+
):
|
|
101
|
+
"""
|
|
102
|
+
Delete an MCP server by its ID
|
|
103
|
+
"""
|
|
104
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
105
|
+
await server.mcp_server_manager.delete_mcp_server_by_id(mcp_server_id, actor=actor)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.patch(
|
|
109
|
+
"/{mcp_server_id}",
|
|
110
|
+
response_model=MCPServerUnion,
|
|
111
|
+
operation_id="mcp_update_mcp_server",
|
|
112
|
+
)
|
|
113
|
+
async def update_mcp_server(
|
|
114
|
+
mcp_server_id: str,
|
|
115
|
+
request: UpdateMCPServerUnion = Body(...),
|
|
116
|
+
server: SyncServer = Depends(get_letta_server),
|
|
117
|
+
headers: HeaderParams = Depends(get_headers),
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Update an existing MCP server configuration
|
|
121
|
+
"""
|
|
122
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
123
|
+
# Convert external update payload to internal manager union
|
|
124
|
+
internal_update = convert_update_to_internal(request)
|
|
125
|
+
updated_server = await server.mcp_server_manager.update_mcp_server_by_id(
|
|
126
|
+
mcp_server_id=mcp_server_id, mcp_server_update=internal_update, actor=actor
|
|
127
|
+
)
|
|
128
|
+
return convert_generic_to_union(updated_server)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@router.get("/{mcp_server_id}/tools", response_model=List[Tool], operation_id="mcp_list_mcp_tools_by_server")
|
|
132
|
+
async def list_mcp_tools_by_server(
|
|
133
|
+
mcp_server_id: str,
|
|
134
|
+
server: SyncServer = Depends(get_letta_server),
|
|
135
|
+
headers: HeaderParams = Depends(get_headers),
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Get a list of all tools for a specific MCP server
|
|
139
|
+
"""
|
|
140
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
141
|
+
# Use the new efficient method that queries from the database using MCPTools mapping
|
|
142
|
+
tools = await server.mcp_server_manager.list_tools_by_mcp_server_from_db(mcp_server_id, actor=actor)
|
|
143
|
+
return tools
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.get("/{mcp_server_id}/tools/{tool_id}", response_model=Tool, operation_id="mcp_get_mcp_tool")
|
|
147
|
+
async def get_mcp_tool(
|
|
148
|
+
mcp_server_id: str,
|
|
149
|
+
tool_id: str,
|
|
150
|
+
server: SyncServer = Depends(get_letta_server),
|
|
151
|
+
headers: HeaderParams = Depends(get_headers),
|
|
152
|
+
):
|
|
153
|
+
"""
|
|
154
|
+
Get a specific MCP tool by its ID
|
|
155
|
+
"""
|
|
156
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
157
|
+
tool = await server.mcp_server_manager.get_tool_by_mcp_server(mcp_server_id, tool_id, actor=actor)
|
|
158
|
+
return tool
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.post("/{mcp_server_id}/tools/{tool_id}/run", response_model=ToolExecutionResult, operation_id="mcp_run_tool")
|
|
162
|
+
async def run_mcp_tool(
|
|
163
|
+
mcp_server_id: str,
|
|
164
|
+
tool_id: str,
|
|
165
|
+
server: SyncServer = Depends(get_letta_server),
|
|
166
|
+
headers: HeaderParams = Depends(get_headers),
|
|
167
|
+
request: MCPToolExecuteRequest = Body(default=MCPToolExecuteRequest()),
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Execute a specific MCP tool
|
|
171
|
+
|
|
172
|
+
The request body should contain the tool arguments in the MCPToolExecuteRequest format.
|
|
173
|
+
"""
|
|
174
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
175
|
+
|
|
176
|
+
# Execute the tool
|
|
177
|
+
result, success = await server.mcp_server_manager.execute_mcp_server_tool(
|
|
178
|
+
mcp_server_id=mcp_server_id,
|
|
179
|
+
tool_id=tool_id,
|
|
180
|
+
tool_args=request.args,
|
|
181
|
+
environment_variables={}, # TODO: Get environment variables from somewhere if needed
|
|
182
|
+
actor=actor,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Create a ToolExecutionResult
|
|
186
|
+
return ToolExecutionResult(
|
|
187
|
+
status="success" if success else "error",
|
|
188
|
+
func_return=result,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.patch("/{mcp_server_id}/refresh", operation_id="mcp_refresh_mcp_server_tools")
|
|
193
|
+
async def refresh_mcp_server_tools(
|
|
194
|
+
mcp_server_id: str,
|
|
195
|
+
server: SyncServer = Depends(get_letta_server),
|
|
196
|
+
headers: HeaderParams = Depends(get_headers),
|
|
197
|
+
agent_id: Optional[str] = None,
|
|
198
|
+
):
|
|
199
|
+
"""
|
|
200
|
+
Refresh tools for an MCP server by:
|
|
201
|
+
1. Fetching current tools from the MCP server
|
|
202
|
+
2. Deleting tools that no longer exist on the server
|
|
203
|
+
3. Updating schemas for existing tools
|
|
204
|
+
4. Adding new tools from the server
|
|
205
|
+
|
|
206
|
+
Returns a summary of changes made.
|
|
207
|
+
"""
|
|
208
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
209
|
+
result = await server.mcp_server_manager.resync_mcp_server_tools(mcp_server_id, actor=actor, agent_id=agent_id)
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@router.get(
|
|
214
|
+
"/connect/{mcp_server_id}",
|
|
215
|
+
response_model=None,
|
|
216
|
+
# TODO: make this into a model?
|
|
217
|
+
responses={
|
|
218
|
+
200: {
|
|
219
|
+
"description": "Successful response",
|
|
220
|
+
"content": {
|
|
221
|
+
"text/event-stream": {"description": "Server-Sent Events stream"},
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
operation_id="mcp_connect_mcp_server",
|
|
226
|
+
)
|
|
227
|
+
async def connect_mcp_server(
|
|
228
|
+
mcp_server_id: str,
|
|
229
|
+
request: Request,
|
|
230
|
+
server: SyncServer = Depends(get_letta_server),
|
|
231
|
+
headers: HeaderParams = Depends(get_headers),
|
|
232
|
+
) -> StreamingResponse:
|
|
233
|
+
"""
|
|
234
|
+
Connect to an MCP server with support for OAuth via SSE.
|
|
235
|
+
Returns a stream of events handling authorization state and exchange if OAuth is required.
|
|
236
|
+
"""
|
|
237
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
238
|
+
mcp_server = await server.mcp_server_manager.get_mcp_server_by_id_async(mcp_server_id=mcp_server_id, actor=actor)
|
|
239
|
+
|
|
240
|
+
# Convert the MCP server to the appropriate config type
|
|
241
|
+
config = mcp_server.to_config(resolve_variables=False)
|
|
242
|
+
|
|
243
|
+
async def oauth_stream_generator(
|
|
244
|
+
mcp_config: Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig],
|
|
245
|
+
http_request: Request,
|
|
246
|
+
) -> AsyncGenerator[str, None]:
|
|
247
|
+
client = None
|
|
248
|
+
|
|
249
|
+
oauth_flow_attempted = False
|
|
250
|
+
try:
|
|
251
|
+
# Acknowledge connection attempt
|
|
252
|
+
yield oauth_stream_event(OauthStreamEvent.CONNECTION_ATTEMPT, server_name=mcp_config.server_name)
|
|
253
|
+
|
|
254
|
+
# Create MCP client with respective transport type
|
|
255
|
+
try:
|
|
256
|
+
mcp_config.resolve_environment_variables()
|
|
257
|
+
client = await server.mcp_server_manager.get_mcp_client(mcp_config, actor)
|
|
258
|
+
except ValueError as e:
|
|
259
|
+
yield oauth_stream_event(OauthStreamEvent.ERROR, message=str(e))
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Try normal connection first for flows that don't require OAuth
|
|
263
|
+
try:
|
|
264
|
+
await client.connect_to_server()
|
|
265
|
+
tools = await client.list_tools(serialize=True)
|
|
266
|
+
yield oauth_stream_event(OauthStreamEvent.SUCCESS, tools=tools)
|
|
267
|
+
return
|
|
268
|
+
except ConnectionError:
|
|
269
|
+
# TODO: jnjpng make this connection error check more specific to the 401 unauthorized error
|
|
270
|
+
if isinstance(client, AsyncStdioMCPClient):
|
|
271
|
+
logger.warning("OAuth not supported for stdio")
|
|
272
|
+
yield oauth_stream_event(OauthStreamEvent.ERROR, message="OAuth not supported for stdio")
|
|
273
|
+
return
|
|
274
|
+
# Continue to OAuth flow
|
|
275
|
+
logger.info(f"Attempting OAuth flow for {mcp_config}...")
|
|
276
|
+
except Exception as e:
|
|
277
|
+
yield oauth_stream_event(OauthStreamEvent.ERROR, message=f"Connection failed: {str(e)}")
|
|
278
|
+
return
|
|
279
|
+
finally:
|
|
280
|
+
if client:
|
|
281
|
+
try:
|
|
282
|
+
await client.cleanup()
|
|
283
|
+
# This is a workaround to catch the expected 401 Unauthorized from the official MCP SDK, see their streamable_http.py
|
|
284
|
+
# For SSE transport types, we catch the ConnectionError above, but Streamable HTTP doesn't bubble up the exception
|
|
285
|
+
except HTTPStatusError:
|
|
286
|
+
oauth_flow_attempted = True
|
|
287
|
+
async for event in server.mcp_server_manager.handle_oauth_flow(
|
|
288
|
+
request=mcp_config, actor=actor, http_request=http_request
|
|
289
|
+
):
|
|
290
|
+
yield event
|
|
291
|
+
|
|
292
|
+
# Failsafe to make sure we don't try to handle OAuth flow twice
|
|
293
|
+
if not oauth_flow_attempted:
|
|
294
|
+
async for event in server.mcp_server_manager.handle_oauth_flow(request=mcp_config, actor=actor, http_request=http_request):
|
|
295
|
+
yield event
|
|
296
|
+
return
|
|
297
|
+
except Exception as e:
|
|
298
|
+
detailed_error = drill_down_exception(e)
|
|
299
|
+
logger.error(f"Error in OAuth stream:\n{detailed_error}")
|
|
300
|
+
yield oauth_stream_event(OauthStreamEvent.ERROR, message=f"Internal error: {detailed_error}")
|
|
301
|
+
|
|
302
|
+
finally:
|
|
303
|
+
if client:
|
|
304
|
+
try:
|
|
305
|
+
await client.cleanup()
|
|
306
|
+
except Exception as cleanup_error:
|
|
307
|
+
logger.warning(f"Error during temp MCP client cleanup: {cleanup_error}")
|
|
308
|
+
|
|
309
|
+
return StreamingResponseWithStatusCode(oauth_stream_generator(config, request), media_type="text/event-stream")
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from typing import List, Literal, Optional
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, Body, Depends, Query
|
|
4
|
-
from fastapi.exceptions import HTTPException
|
|
5
4
|
from starlette.requests import Request
|
|
6
5
|
|
|
7
6
|
from letta.agents.letta_agent_batch import LettaAgentBatch
|
|
7
|
+
from letta.errors import LettaInvalidArgumentError
|
|
8
8
|
from letta.log import get_logger
|
|
9
|
-
from letta.orm.errors import NoResultFound
|
|
10
9
|
from letta.schemas.job import BatchJob, JobStatus, JobType, JobUpdate
|
|
11
10
|
from letta.schemas.letta_request import CreateBatch
|
|
12
11
|
from letta.schemas.letta_response import LettaBatchMessages
|
|
@@ -42,7 +41,9 @@ async def create_batch(
|
|
|
42
41
|
if content_length:
|
|
43
42
|
length = int(content_length)
|
|
44
43
|
if length > max_bytes:
|
|
45
|
-
raise
|
|
44
|
+
raise LettaInvalidArgumentError(
|
|
45
|
+
message=f"Request too large ({length} bytes). Max is {max_bytes} bytes.", argument_name="content-length"
|
|
46
|
+
)
|
|
46
47
|
|
|
47
48
|
if not settings.enable_batch_job_polling:
|
|
48
49
|
logger.warning("Batch job polling is disabled. Enable batch processing by setting LETTA_ENABLE_BATCH_JOB_POLLING to True.")
|
|
@@ -93,12 +94,8 @@ async def retrieve_batch(
|
|
|
93
94
|
Retrieve the status and details of a batch run.
|
|
94
95
|
"""
|
|
95
96
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
|
|
99
|
-
return BatchJob.from_job(job)
|
|
100
|
-
except NoResultFound:
|
|
101
|
-
raise HTTPException(status_code=404, detail="Batch not found")
|
|
97
|
+
job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
|
|
98
|
+
return BatchJob.from_job(job)
|
|
102
99
|
|
|
103
100
|
|
|
104
101
|
@router.get("/batches", response_model=List[BatchJob], operation_id="list_batches")
|
|
@@ -162,11 +159,8 @@ async def list_messages_for_batch(
|
|
|
162
159
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
163
160
|
|
|
164
161
|
# Verify the batch job exists and the user has access to it
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
BatchJob.from_job(job)
|
|
168
|
-
except NoResultFound:
|
|
169
|
-
raise HTTPException(status_code=404, detail="Batch not found")
|
|
162
|
+
job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
|
|
163
|
+
BatchJob.from_job(job)
|
|
170
164
|
|
|
171
165
|
# Get messages directly using our efficient method
|
|
172
166
|
messages = await server.batch_manager.get_messages_for_letta_batch_async(
|
|
@@ -187,23 +181,18 @@ async def cancel_batch(
|
|
|
187
181
|
"""
|
|
188
182
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
189
183
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
await server.batch_manager.update_llm_batch_status_async(
|
|
206
|
-
llm_batch_id=llm_batch_job.id, status=JobStatus.cancelled, actor=actor
|
|
207
|
-
)
|
|
208
|
-
except NoResultFound:
|
|
209
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
184
|
+
job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
|
|
185
|
+
job = await server.job_manager.update_job_by_id_async(job_id=job.id, job_update=JobUpdate(status=JobStatus.cancelled), actor=actor)
|
|
186
|
+
|
|
187
|
+
# Get related llm batch jobs
|
|
188
|
+
llm_batch_jobs = await server.batch_manager.list_llm_batch_jobs_async(letta_batch_id=job.id, actor=actor)
|
|
189
|
+
for llm_batch_job in llm_batch_jobs:
|
|
190
|
+
if llm_batch_job.status in {JobStatus.running, JobStatus.created}:
|
|
191
|
+
# TODO: Extend to providers beyond anthropic
|
|
192
|
+
# TODO: For now, we only support anthropic
|
|
193
|
+
# Cancel the job
|
|
194
|
+
anthropic_batch_id = llm_batch_job.create_batch_response.id
|
|
195
|
+
await server.anthropic_async_client.messages.batches.cancel(anthropic_batch_id)
|
|
196
|
+
|
|
197
|
+
# Update all the batch_job statuses
|
|
198
|
+
await server.batch_manager.update_llm_batch_status_async(llm_batch_id=llm_batch_job.id, status=JobStatus.cancelled, actor=actor)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, List, Optional
|
|
2
2
|
|
|
3
|
-
from fastapi import APIRouter, Body, Depends,
|
|
3
|
+
from fastapi import APIRouter, Body, Depends, Query
|
|
4
4
|
|
|
5
5
|
from letta.schemas.organization import Organization, OrganizationCreate, OrganizationUpdate
|
|
6
6
|
from letta.server.rest_api.dependencies import get_letta_server
|
|
@@ -21,13 +21,7 @@ async def get_all_orgs(
|
|
|
21
21
|
"""
|
|
22
22
|
Get a list of all orgs in the database
|
|
23
23
|
"""
|
|
24
|
-
|
|
25
|
-
orgs = await server.organization_manager.list_organizations_async(after=after, limit=limit)
|
|
26
|
-
except HTTPException:
|
|
27
|
-
raise
|
|
28
|
-
except Exception as e:
|
|
29
|
-
raise HTTPException(status_code=500, detail=f"{e}")
|
|
30
|
-
return orgs
|
|
24
|
+
return await server.organization_manager.list_organizations_async(after=after, limit=limit)
|
|
31
25
|
|
|
32
26
|
|
|
33
27
|
@router.post("/", tags=["admin"], response_model=Organization, operation_id="create_organization")
|
|
@@ -49,15 +43,9 @@ async def delete_org(
|
|
|
49
43
|
server: "SyncServer" = Depends(get_letta_server),
|
|
50
44
|
):
|
|
51
45
|
# TODO make a soft deletion, instead of a hard deletion
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
raise HTTPException(status_code=404, detail="Organization does not exist")
|
|
56
|
-
await server.organization_manager.delete_organization_by_id_async(org_id=org_id)
|
|
57
|
-
except HTTPException:
|
|
58
|
-
raise
|
|
59
|
-
except Exception as e:
|
|
60
|
-
raise HTTPException(status_code=500, detail=f"{e}")
|
|
46
|
+
# Get the org first so we can return it after deletion
|
|
47
|
+
org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
|
|
48
|
+
await server.organization_manager.delete_organization_by_id_async(org_id=org_id)
|
|
61
49
|
return org
|
|
62
50
|
|
|
63
51
|
|
|
@@ -67,13 +55,4 @@ async def update_org(
|
|
|
67
55
|
request: OrganizationUpdate = Body(...),
|
|
68
56
|
server: "SyncServer" = Depends(get_letta_server),
|
|
69
57
|
):
|
|
70
|
-
|
|
71
|
-
org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
|
|
72
|
-
if org is None:
|
|
73
|
-
raise HTTPException(status_code=404, detail="Organization does not exist")
|
|
74
|
-
org = await server.organization_manager.update_organization_async(org_id=org_id, name=request.name)
|
|
75
|
-
except HTTPException:
|
|
76
|
-
raise
|
|
77
|
-
except Exception as e:
|
|
78
|
-
raise HTTPException(status_code=500, detail=f"{e}")
|
|
79
|
-
return org
|
|
58
|
+
return await server.organization_manager.update_organization_async(org_id=org_id, org_update=request)
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, List, Literal, Optional
|
|
2
2
|
|
|
3
|
-
from fastapi import APIRouter, Body, Depends,
|
|
3
|
+
from fastapi import APIRouter, Body, Depends, Query, status
|
|
4
4
|
from fastapi.responses import JSONResponse
|
|
5
5
|
|
|
6
|
-
from letta.errors import LLMAuthenticationError
|
|
7
|
-
from letta.orm.errors import NoResultFound
|
|
8
6
|
from letta.schemas.enums import ProviderType
|
|
9
|
-
from letta.schemas.providers import Provider, ProviderCheck, ProviderCreate, ProviderUpdate
|
|
7
|
+
from letta.schemas.providers import Provider, ProviderBase, ProviderCheck, ProviderCreate, ProviderUpdate
|
|
10
8
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
9
|
+
from letta.validators import ProviderId
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
13
12
|
from letta.server.server import SyncServer
|
|
@@ -38,21 +37,16 @@ async def list_providers(
|
|
|
38
37
|
"""
|
|
39
38
|
Get a list of all custom providers.
|
|
40
39
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
except HTTPException:
|
|
47
|
-
raise
|
|
48
|
-
except Exception as e:
|
|
49
|
-
raise HTTPException(status_code=500, detail=f"{e}")
|
|
40
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
41
|
+
providers = await server.provider_manager.list_providers_async(
|
|
42
|
+
before=before, after=after, limit=limit, actor=actor, name=name, provider_type=provider_type, ascending=(order == "asc")
|
|
43
|
+
)
|
|
50
44
|
return providers
|
|
51
45
|
|
|
52
46
|
|
|
53
47
|
@router.get("/{provider_id}", response_model=Provider, operation_id="retrieve_provider")
|
|
54
48
|
async def retrieve_provider(
|
|
55
|
-
provider_id:
|
|
49
|
+
provider_id: ProviderId,
|
|
56
50
|
headers: HeaderParams = Depends(get_headers),
|
|
57
51
|
server: "SyncServer" = Depends(get_letta_server),
|
|
58
52
|
):
|
|
@@ -86,7 +80,7 @@ async def create_provider(
|
|
|
86
80
|
|
|
87
81
|
@router.patch("/{provider_id}", response_model=Provider, operation_id="modify_provider")
|
|
88
82
|
async def modify_provider(
|
|
89
|
-
provider_id:
|
|
83
|
+
provider_id: ProviderId,
|
|
90
84
|
request: ProviderUpdate = Body(...),
|
|
91
85
|
headers: HeaderParams = Depends(get_headers),
|
|
92
86
|
server: "SyncServer" = Depends(get_letta_server),
|
|
@@ -106,70 +100,49 @@ async def check_provider(
|
|
|
106
100
|
"""
|
|
107
101
|
Verify the API key and additional parameters for a provider.
|
|
108
102
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
except LLMAuthenticationError as e:
|
|
118
|
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"{e.message}")
|
|
119
|
-
except Exception as e:
|
|
120
|
-
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"{e}")
|
|
103
|
+
if request.base_url and len(request.base_url) == 0:
|
|
104
|
+
# set to null if empty string
|
|
105
|
+
request.base_url = None
|
|
106
|
+
await server.provider_manager.check_provider_api_key(provider_check=request)
|
|
107
|
+
return JSONResponse(
|
|
108
|
+
status_code=status.HTTP_200_OK, content={"message": f"Valid api key for provider_type={request.provider_type.value}"}
|
|
109
|
+
)
|
|
121
110
|
|
|
122
111
|
|
|
123
112
|
@router.post("/{provider_id}/check", response_model=None, operation_id="check_existing_provider")
|
|
124
113
|
async def check_existing_provider(
|
|
125
|
-
provider_id:
|
|
114
|
+
provider_id: ProviderId,
|
|
126
115
|
headers: HeaderParams = Depends(get_headers),
|
|
127
116
|
server: "SyncServer" = Depends(get_letta_server),
|
|
128
117
|
):
|
|
129
118
|
"""
|
|
130
119
|
Verify the API key and additional parameters for an existing provider.
|
|
131
120
|
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
except LLMAuthenticationError as e:
|
|
148
|
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"{e.message}")
|
|
149
|
-
except NoResultFound:
|
|
150
|
-
raise HTTPException(status_code=404, detail=f"Provider provider_id={provider_id} not found for user_id={actor.id}.")
|
|
151
|
-
except HTTPException:
|
|
152
|
-
raise
|
|
153
|
-
except Exception as e:
|
|
154
|
-
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"{e}")
|
|
121
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
122
|
+
provider = await server.provider_manager.get_provider_async(provider_id=provider_id, actor=actor)
|
|
123
|
+
|
|
124
|
+
# Create a ProviderCheck from the existing provider
|
|
125
|
+
provider_check = ProviderCheck(
|
|
126
|
+
provider_type=provider.provider_type,
|
|
127
|
+
api_key=provider.api_key,
|
|
128
|
+
base_url=provider.base_url,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
await server.provider_manager.check_provider_api_key(provider_check=provider_check)
|
|
132
|
+
return JSONResponse(
|
|
133
|
+
status_code=status.HTTP_200_OK, content={"message": f"Valid api key for provider_type={provider.provider_type.value}"}
|
|
134
|
+
)
|
|
155
135
|
|
|
156
136
|
|
|
157
137
|
@router.delete("/{provider_id}", response_model=None, operation_id="delete_provider")
|
|
158
138
|
async def delete_provider(
|
|
159
|
-
provider_id:
|
|
139
|
+
provider_id: ProviderId,
|
|
160
140
|
headers: HeaderParams = Depends(get_headers),
|
|
161
141
|
server: "SyncServer" = Depends(get_letta_server),
|
|
162
142
|
):
|
|
163
143
|
"""
|
|
164
144
|
Delete an existing custom provider.
|
|
165
145
|
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Provider id={provider_id} successfully deleted"})
|
|
170
|
-
except NoResultFound:
|
|
171
|
-
raise HTTPException(status_code=404, detail=f"Provider provider_id={provider_id} not found for user_id={actor.id}.")
|
|
172
|
-
except HTTPException:
|
|
173
|
-
raise
|
|
174
|
-
except Exception as e:
|
|
175
|
-
raise HTTPException(status_code=500, detail=f"{e}")
|
|
146
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
147
|
+
await server.provider_manager.delete_provider_by_id_async(provider_id=provider_id, actor=actor)
|
|
148
|
+
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Provider id={provider_id} successfully deleted"})
|