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.

Files changed (159) hide show
  1. letta/__init__.py +2 -3
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/simple_llm_request_adapter.py +8 -5
  4. letta/adapters/simple_llm_stream_adapter.py +22 -6
  5. letta/agents/agent_loop.py +10 -3
  6. letta/agents/base_agent.py +4 -1
  7. letta/agents/helpers.py +41 -9
  8. letta/agents/letta_agent.py +11 -10
  9. letta/agents/letta_agent_v2.py +47 -37
  10. letta/agents/letta_agent_v3.py +395 -300
  11. letta/agents/voice_agent.py +8 -6
  12. letta/agents/voice_sleeptime_agent.py +3 -3
  13. letta/constants.py +30 -7
  14. letta/errors.py +20 -0
  15. letta/functions/function_sets/base.py +55 -3
  16. letta/functions/mcp_client/types.py +33 -57
  17. letta/functions/schema_generator.py +135 -23
  18. letta/groups/sleeptime_multi_agent_v3.py +6 -11
  19. letta/groups/sleeptime_multi_agent_v4.py +227 -0
  20. letta/helpers/converters.py +78 -4
  21. letta/helpers/crypto_utils.py +6 -2
  22. letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
  23. letta/interfaces/anthropic_streaming_interface.py +3 -4
  24. letta/interfaces/gemini_streaming_interface.py +4 -6
  25. letta/interfaces/openai_streaming_interface.py +63 -28
  26. letta/llm_api/anthropic_client.py +7 -4
  27. letta/llm_api/deepseek_client.py +6 -4
  28. letta/llm_api/google_ai_client.py +3 -12
  29. letta/llm_api/google_vertex_client.py +1 -1
  30. letta/llm_api/helpers.py +90 -61
  31. letta/llm_api/llm_api_tools.py +4 -1
  32. letta/llm_api/openai.py +12 -12
  33. letta/llm_api/openai_client.py +53 -16
  34. letta/local_llm/constants.py +4 -3
  35. letta/local_llm/json_parser.py +5 -2
  36. letta/local_llm/utils.py +2 -3
  37. letta/log.py +171 -7
  38. letta/orm/agent.py +43 -9
  39. letta/orm/archive.py +4 -0
  40. letta/orm/custom_columns.py +15 -0
  41. letta/orm/identity.py +11 -11
  42. letta/orm/mcp_server.py +9 -0
  43. letta/orm/message.py +6 -1
  44. letta/orm/run_metrics.py +7 -2
  45. letta/orm/sqlalchemy_base.py +2 -2
  46. letta/orm/tool.py +3 -0
  47. letta/otel/tracing.py +2 -0
  48. letta/prompts/prompt_generator.py +7 -2
  49. letta/schemas/agent.py +41 -10
  50. letta/schemas/agent_file.py +3 -0
  51. letta/schemas/archive.py +4 -2
  52. letta/schemas/block.py +2 -1
  53. letta/schemas/enums.py +36 -3
  54. letta/schemas/file.py +3 -3
  55. letta/schemas/folder.py +2 -1
  56. letta/schemas/group.py +2 -1
  57. letta/schemas/identity.py +18 -9
  58. letta/schemas/job.py +3 -1
  59. letta/schemas/letta_message.py +71 -12
  60. letta/schemas/letta_request.py +7 -3
  61. letta/schemas/letta_stop_reason.py +0 -25
  62. letta/schemas/llm_config.py +8 -2
  63. letta/schemas/mcp.py +80 -83
  64. letta/schemas/mcp_server.py +349 -0
  65. letta/schemas/memory.py +20 -8
  66. letta/schemas/message.py +212 -67
  67. letta/schemas/providers/anthropic.py +13 -6
  68. letta/schemas/providers/azure.py +6 -4
  69. letta/schemas/providers/base.py +8 -4
  70. letta/schemas/providers/bedrock.py +6 -2
  71. letta/schemas/providers/cerebras.py +7 -3
  72. letta/schemas/providers/deepseek.py +2 -1
  73. letta/schemas/providers/google_gemini.py +15 -6
  74. letta/schemas/providers/groq.py +2 -1
  75. letta/schemas/providers/lmstudio.py +9 -6
  76. letta/schemas/providers/mistral.py +2 -1
  77. letta/schemas/providers/openai.py +7 -2
  78. letta/schemas/providers/together.py +9 -3
  79. letta/schemas/providers/xai.py +7 -3
  80. letta/schemas/run.py +7 -2
  81. letta/schemas/run_metrics.py +2 -1
  82. letta/schemas/sandbox_config.py +2 -2
  83. letta/schemas/secret.py +3 -158
  84. letta/schemas/source.py +2 -2
  85. letta/schemas/step.py +2 -2
  86. letta/schemas/tool.py +24 -1
  87. letta/schemas/usage.py +0 -1
  88. letta/server/rest_api/app.py +123 -7
  89. letta/server/rest_api/dependencies.py +3 -0
  90. letta/server/rest_api/interface.py +7 -4
  91. letta/server/rest_api/redis_stream_manager.py +16 -1
  92. letta/server/rest_api/routers/v1/__init__.py +7 -0
  93. letta/server/rest_api/routers/v1/agents.py +332 -322
  94. letta/server/rest_api/routers/v1/archives.py +127 -40
  95. letta/server/rest_api/routers/v1/blocks.py +54 -6
  96. letta/server/rest_api/routers/v1/chat_completions.py +146 -0
  97. letta/server/rest_api/routers/v1/folders.py +27 -35
  98. letta/server/rest_api/routers/v1/groups.py +23 -35
  99. letta/server/rest_api/routers/v1/identities.py +24 -10
  100. letta/server/rest_api/routers/v1/internal_runs.py +107 -0
  101. letta/server/rest_api/routers/v1/internal_templates.py +162 -179
  102. letta/server/rest_api/routers/v1/jobs.py +15 -27
  103. letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
  104. letta/server/rest_api/routers/v1/messages.py +23 -34
  105. letta/server/rest_api/routers/v1/organizations.py +6 -27
  106. letta/server/rest_api/routers/v1/providers.py +35 -62
  107. letta/server/rest_api/routers/v1/runs.py +30 -43
  108. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
  109. letta/server/rest_api/routers/v1/sources.py +26 -42
  110. letta/server/rest_api/routers/v1/steps.py +16 -29
  111. letta/server/rest_api/routers/v1/tools.py +17 -13
  112. letta/server/rest_api/routers/v1/users.py +5 -17
  113. letta/server/rest_api/routers/v1/voice.py +18 -27
  114. letta/server/rest_api/streaming_response.py +5 -2
  115. letta/server/rest_api/utils.py +187 -25
  116. letta/server/server.py +27 -22
  117. letta/server/ws_api/server.py +5 -4
  118. letta/services/agent_manager.py +148 -26
  119. letta/services/agent_serialization_manager.py +6 -1
  120. letta/services/archive_manager.py +168 -15
  121. letta/services/block_manager.py +14 -4
  122. letta/services/file_manager.py +33 -29
  123. letta/services/group_manager.py +10 -0
  124. letta/services/helpers/agent_manager_helper.py +65 -11
  125. letta/services/identity_manager.py +105 -4
  126. letta/services/job_manager.py +11 -1
  127. letta/services/mcp/base_client.py +2 -2
  128. letta/services/mcp/oauth_utils.py +33 -8
  129. letta/services/mcp_manager.py +174 -78
  130. letta/services/mcp_server_manager.py +1331 -0
  131. letta/services/message_manager.py +109 -4
  132. letta/services/organization_manager.py +4 -4
  133. letta/services/passage_manager.py +9 -25
  134. letta/services/provider_manager.py +91 -15
  135. letta/services/run_manager.py +72 -15
  136. letta/services/sandbox_config_manager.py +45 -3
  137. letta/services/source_manager.py +15 -8
  138. letta/services/step_manager.py +24 -1
  139. letta/services/streaming_service.py +581 -0
  140. letta/services/summarizer/summarizer.py +1 -1
  141. letta/services/tool_executor/core_tool_executor.py +111 -0
  142. letta/services/tool_executor/files_tool_executor.py +5 -3
  143. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  144. letta/services/tool_executor/tool_execution_manager.py +1 -1
  145. letta/services/tool_manager.py +10 -3
  146. letta/services/tool_sandbox/base.py +61 -1
  147. letta/services/tool_sandbox/local_sandbox.py +1 -3
  148. letta/services/user_manager.py +2 -2
  149. letta/settings.py +49 -5
  150. letta/system.py +14 -5
  151. letta/utils.py +73 -1
  152. letta/validators.py +105 -0
  153. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
  155. letta/schemas/letta_ping.py +0 -28
  156. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  157. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
  159. {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 HTTPException(status_code=413, detail=f"Request too large ({length} bytes). Max is {max_bytes} bytes.")
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
- try:
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
- try:
166
- job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
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
- try:
191
- job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
192
- job = await server.job_manager.update_job_by_id_async(job_id=job.id, job_update=JobUpdate(status=JobStatus.cancelled), actor=actor)
193
-
194
- # Get related llm batch jobs
195
- llm_batch_jobs = await server.batch_manager.list_llm_batch_jobs_async(letta_batch_id=job.id, actor=actor)
196
- for llm_batch_job in llm_batch_jobs:
197
- if llm_batch_job.status in {JobStatus.running, JobStatus.created}:
198
- # TODO: Extend to providers beyond anthropic
199
- # TODO: For now, we only support anthropic
200
- # Cancel the job
201
- anthropic_batch_id = llm_batch_job.create_batch_response.id
202
- await server.anthropic_async_client.messages.batches.cancel(anthropic_batch_id)
203
-
204
- # Update all the batch_job statuses
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, HTTPException, Query
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
- try:
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
- try:
53
- org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
54
- if org is None:
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
- try:
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, HTTPException, Query, status
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
- try:
42
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
43
- providers = await server.provider_manager.list_providers_async(
44
- before=before, after=after, limit=limit, actor=actor, name=name, provider_type=provider_type, ascending=(order == "asc")
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: str,
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: str,
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
- try:
110
- if request.base_url and len(request.base_url) == 0:
111
- # set to null if empty string
112
- request.base_url = None
113
- await server.provider_manager.check_provider_api_key(provider_check=request)
114
- return JSONResponse(
115
- status_code=status.HTTP_200_OK, content={"message": f"Valid api key for provider_type={request.provider_type.value}"}
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: str,
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
- try:
133
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
134
- provider = await server.provider_manager.get_provider_async(provider_id=provider_id, actor=actor)
135
-
136
- # Create a ProviderCheck from the existing provider
137
- provider_check = ProviderCheck(
138
- provider_type=provider.provider_type,
139
- api_key=provider.api_key,
140
- base_url=provider.base_url,
141
- )
142
-
143
- await server.provider_manager.check_provider_api_key(provider_check=provider_check)
144
- return JSONResponse(
145
- status_code=status.HTTP_200_OK, content={"message": f"Valid api key for provider_type={provider.provider_type.value}"}
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: str,
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
- try:
167
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
168
- await server.provider_manager.delete_provider_by_id_async(provider_id=provider_id, actor=actor)
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"})