letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251025104015__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.dev20251025104015.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.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.dev20251025104015.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/entry_points.txt +0 -0
  159. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/licenses/LICENSE +0 -0
@@ -5,8 +5,8 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query
5
5
  from pydantic import Field
6
6
 
7
7
  from letta.data_sources.redis_client import NoopAsyncRedisClient, get_redis_client
8
+ from letta.errors import LettaExpiredError, LettaInvalidArgumentError
8
9
  from letta.helpers.datetime_helpers import get_utc_time
9
- from letta.orm.errors import NoResultFound
10
10
  from letta.schemas.enums import RunStatus
11
11
  from letta.schemas.letta_message import LettaMessageUnion
12
12
  from letta.schemas.letta_request import RetrieveStreamRequest
@@ -151,28 +151,25 @@ async def retrieve_run(
151
151
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
152
152
  runs_manager = RunManager()
153
153
 
154
- try:
155
- run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
156
-
157
- use_lettuce = run.metadata and run.metadata.get("lettuce")
158
- if use_lettuce and run.status not in [RunStatus.completed, RunStatus.failed, RunStatus.cancelled]:
159
- lettuce_client = await LettuceClient.create()
160
- status = await lettuce_client.get_status()
161
-
162
- # Map the status to our enum
163
- run_status = run.status
164
- if status == "RUNNING":
165
- run_status = RunStatus.running
166
- elif status == "COMPLETED":
167
- run_status = RunStatus.completed
168
- elif status == "FAILED":
169
- run_status = RunStatus.failed
170
- elif status == "CANCELLED":
171
- run_status = RunStatus.cancelled
172
- run.status = run_status
173
- return run
174
- except NoResultFound:
175
- raise HTTPException(status_code=404, detail="Run not found")
154
+ run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
155
+
156
+ use_lettuce = run.metadata and run.metadata.get("lettuce")
157
+ if use_lettuce and run.status not in [RunStatus.completed, RunStatus.failed, RunStatus.cancelled]:
158
+ lettuce_client = await LettuceClient.create()
159
+ status = await lettuce_client.get_status(run_id=run_id)
160
+
161
+ # Map the status to our enum
162
+ run_status = run.status
163
+ if status == "RUNNING":
164
+ run_status = RunStatus.running
165
+ elif status == "COMPLETED":
166
+ run_status = RunStatus.completed
167
+ elif status == "FAILED":
168
+ run_status = RunStatus.failed
169
+ elif status == "CANCELLED":
170
+ run_status = RunStatus.cancelled
171
+ run.status = run_status
172
+ return run
176
173
 
177
174
 
178
175
  RunMessagesResponse = Annotated[
@@ -218,11 +215,7 @@ async def retrieve_run_usage(
218
215
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
219
216
  runs_manager = RunManager()
220
217
 
221
- try:
222
- usage = await runs_manager.get_run_usage(run_id=run_id, actor=actor)
223
- return usage
224
- except NoResultFound:
225
- raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
218
+ return await runs_manager.get_run_usage(run_id=run_id, actor=actor)
226
219
 
227
220
 
228
221
  @router.get("/{run_id}/metrics", response_model=RunMetrics, operation_id="retrieve_metrics_for_run")
@@ -234,12 +227,9 @@ async def retrieve_metrics_for_run(
234
227
  """
235
228
  Get run metrics by run ID.
236
229
  """
237
- try:
238
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
239
- runs_manager = RunManager()
240
- return await runs_manager.get_run_metrics_async(run_id=run_id, actor=actor)
241
- except NoResultFound:
242
- raise HTTPException(status_code=404, detail="Run metrics not found")
230
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
231
+ runs_manager = RunManager()
232
+ return await runs_manager.get_run_metrics_async(run_id=run_id, actor=actor)
243
233
 
244
234
 
245
235
  @router.get(
@@ -275,7 +265,7 @@ async def list_run_steps(
275
265
  )
276
266
 
277
267
 
278
- @router.delete("/{run_id}", response_model=Run, operation_id="delete_run")
268
+ @router.delete("/{run_id}", response_model=None, operation_id="delete_run")
279
269
  async def delete_run(
280
270
  run_id: str,
281
271
  headers: HeaderParams = Depends(get_headers),
@@ -286,7 +276,7 @@ async def delete_run(
286
276
  """
287
277
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
288
278
  runs_manager = RunManager()
289
- return await runs_manager.delete_run(run_id=run_id, actor=actor)
279
+ return await runs_manager.delete_run_by_id(run_id=run_id, actor=actor)
290
280
 
291
281
 
292
282
  @router.post(
@@ -330,16 +320,13 @@ async def retrieve_stream(
330
320
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
331
321
  runs_manager = RunManager()
332
322
 
333
- try:
334
- run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
335
- except NoResultFound:
336
- raise HTTPException(status_code=404, detail="Run not found")
323
+ run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
337
324
 
338
325
  if not run.background:
339
- raise HTTPException(status_code=400, detail="Run was not created in background mode, so it cannot be retrieved.")
326
+ raise LettaInvalidArgumentError("Run was not created in background mode, so it cannot be retrieved.")
340
327
 
341
328
  if run.created_at < get_utc_time() - timedelta(hours=3):
342
- raise HTTPException(status_code=410, detail="Run was created more than 3 hours ago, and is now expired.")
329
+ raise LettaExpiredError("Run was created more than 3 hours ago, and is now expired.")
343
330
 
344
331
  redis_client = await get_redis_client()
345
332
 
@@ -370,7 +357,7 @@ async def retrieve_stream(
370
357
  )
371
358
 
372
359
  if request.include_pings and settings.enable_keepalive:
373
- stream = add_keepalive_to_stream(stream, keepalive_interval=settings.keepalive_interval)
360
+ stream = add_keepalive_to_stream(stream, keepalive_interval=settings.keepalive_interval, run_id=run_id)
374
361
 
375
362
  return StreamingResponseWithStatusCode(
376
363
  stream,
@@ -15,12 +15,14 @@ from letta.schemas.environment_variables import (
15
15
  from letta.schemas.sandbox_config import (
16
16
  LocalSandboxConfig,
17
17
  SandboxConfig as PydanticSandboxConfig,
18
+ SandboxConfigBase,
18
19
  SandboxConfigCreate,
19
20
  SandboxConfigUpdate,
20
21
  )
21
22
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
22
23
  from letta.server.server import SyncServer
23
24
  from letta.services.helpers.tool_execution_helper import create_venv_for_local_sandbox, install_pip_requirements_for_sandbox
25
+ from letta.validators import SandboxConfigId
24
26
 
25
27
  router = APIRouter(prefix="/sandbox-config", tags=["sandbox-config"])
26
28
 
@@ -87,8 +89,8 @@ async def create_custom_local_sandbox_config(
87
89
 
88
90
  @router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig)
89
91
  async def update_sandbox_config(
90
- sandbox_config_id: str,
91
92
  config_update: SandboxConfigUpdate,
93
+ sandbox_config_id: SandboxConfigId,
92
94
  server: SyncServer = Depends(get_letta_server),
93
95
  headers: HeaderParams = Depends(get_headers),
94
96
  ):
@@ -98,7 +100,7 @@ async def update_sandbox_config(
98
100
 
99
101
  @router.delete("/{sandbox_config_id}", status_code=204)
100
102
  async def delete_sandbox_config(
101
- sandbox_config_id: str,
103
+ sandbox_config_id: SandboxConfigId,
102
104
  server: SyncServer = Depends(get_letta_server),
103
105
  headers: HeaderParams = Depends(get_headers),
104
106
  ):
@@ -157,8 +159,8 @@ async def force_recreate_local_sandbox_venv(
157
159
 
158
160
  @router.post("/{sandbox_config_id}/environment-variable", response_model=PydanticEnvVar)
159
161
  async def create_sandbox_env_var(
160
- sandbox_config_id: str,
161
162
  env_var_create: SandboxEnvironmentVariableCreate,
163
+ sandbox_config_id: SandboxConfigId,
162
164
  server: SyncServer = Depends(get_letta_server),
163
165
  headers: HeaderParams = Depends(get_headers),
164
166
  ):
@@ -189,7 +191,7 @@ async def delete_sandbox_env_var(
189
191
 
190
192
  @router.get("/{sandbox_config_id}/environment-variable", response_model=List[PydanticEnvVar])
191
193
  async def list_sandbox_env_vars(
192
- sandbox_config_id: str,
194
+ sandbox_config_id: SandboxConfigId,
193
195
  limit: int = Query(1000, description="Number of results to return"),
194
196
  after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
195
197
  server: SyncServer = Depends(get_letta_server),
@@ -9,6 +9,7 @@ from starlette import status
9
9
  from starlette.responses import Response
10
10
 
11
11
  import letta.constants as constants
12
+ from letta.errors import LettaInvalidArgumentError, LettaUnsupportedFileUploadError
12
13
  from letta.helpers.pinecone_utils import (
13
14
  delete_file_records_from_pinecone_index,
14
15
  delete_source_records_from_pinecone_index,
@@ -20,9 +21,9 @@ from letta.otel.tracing import trace_method
20
21
  from letta.schemas.agent import AgentState
21
22
  from letta.schemas.embedding_config import EmbeddingConfig
22
23
  from letta.schemas.enums import DuplicateFileHandling, FileProcessingStatus
23
- from letta.schemas.file import FileMetadata
24
+ from letta.schemas.file import FileMetadata, FileMetadataBase
24
25
  from letta.schemas.passage import Passage
25
- from letta.schemas.source import Source, SourceCreate, SourceUpdate
26
+ from letta.schemas.source import BaseSource, Source, SourceCreate, SourceUpdate
26
27
  from letta.schemas.source_metadata import OrganizationSourcesStats
27
28
  from letta.schemas.user import User
28
29
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
@@ -35,6 +36,7 @@ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFil
35
36
  from letta.services.file_processor.parser.mistral_parser import MistralFileParser
36
37
  from letta.settings import settings
37
38
  from letta.utils import safe_create_file_processing_task, safe_create_task, sanitize_filename
39
+ from letta.validators import FileId, SourceId
38
40
 
39
41
  logger = get_logger(__name__)
40
42
 
@@ -58,7 +60,7 @@ async def count_sources(
58
60
 
59
61
  @router.get("/{source_id}", response_model=Source, operation_id="retrieve_source", deprecated=True)
60
62
  async def retrieve_source(
61
- source_id: str,
63
+ source_id: SourceId,
62
64
  server: "SyncServer" = Depends(get_letta_server),
63
65
  headers: HeaderParams = Depends(get_headers),
64
66
  ):
@@ -66,10 +68,7 @@ async def retrieve_source(
66
68
  Get all sources
67
69
  """
68
70
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
69
-
70
71
  source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
71
- if not source:
72
- raise HTTPException(status_code=404, detail=f"Source with id={source_id} not found.")
73
72
  return source
74
73
 
75
74
 
@@ -85,8 +84,6 @@ async def get_source_id_by_name(
85
84
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
86
85
 
87
86
  source = await server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
88
- if not source:
89
- raise HTTPException(status_code=404, detail=f"Source with name={source_name} not found.")
90
87
  return source.id
91
88
 
92
89
 
@@ -138,8 +135,9 @@ async def create_source(
138
135
  if not source_create.embedding_config:
139
136
  if not source_create.embedding:
140
137
  if settings.default_embedding_handle is None:
141
- # TODO: modify error type
142
- raise ValueError("Must specify either embedding or embedding_config in request")
138
+ raise LettaInvalidArgumentError(
139
+ "Must specify either embedding or embedding_config in request", argument_name="default_embedding_handle"
140
+ )
143
141
  else:
144
142
  source_create.embedding = settings.default_embedding_handle
145
143
  source_create.embedding_config = await server.get_embedding_config_from_handle_async(
@@ -159,8 +157,8 @@ async def create_source(
159
157
 
160
158
  @router.patch("/{source_id}", response_model=Source, operation_id="modify_source", deprecated=True)
161
159
  async def modify_source(
162
- source_id: str,
163
160
  source: SourceUpdate,
161
+ source_id: SourceId,
164
162
  server: "SyncServer" = Depends(get_letta_server),
165
163
  headers: HeaderParams = Depends(get_headers),
166
164
  ):
@@ -169,14 +167,13 @@ async def modify_source(
169
167
  """
170
168
  # TODO: allow updating the handle/embedding config
171
169
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
172
- if not await server.source_manager.get_source_by_id(source_id=source_id, actor=actor):
173
- raise HTTPException(status_code=404, detail=f"Source with id={source_id} does not exist.")
170
+ await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
174
171
  return await server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
175
172
 
176
173
 
177
174
  @router.delete("/{source_id}", response_model=None, operation_id="delete_source", deprecated=True)
178
175
  async def delete_source(
179
- source_id: str,
176
+ source_id: SourceId,
180
177
  server: "SyncServer" = Depends(get_letta_server),
181
178
  headers: HeaderParams = Depends(get_headers),
182
179
  ):
@@ -203,18 +200,16 @@ async def delete_source(
203
200
  await server.remove_files_from_context_window(agent_state=agent_state, file_ids=file_ids, actor=actor)
204
201
 
205
202
  if agent_state.enable_sleeptime:
206
- try:
207
- block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=source.name, actor=actor)
203
+ block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=source.name, actor=actor)
204
+ if block:
208
205
  await server.block_manager.delete_block_async(block.id, actor)
209
- except:
210
- pass
211
206
  await server.delete_source(source_id=source_id, actor=actor)
212
207
 
213
208
 
214
209
  @router.post("/{source_id}/upload", response_model=FileMetadata, operation_id="upload_file_to_source", deprecated=True)
215
210
  async def upload_file_to_source(
216
211
  file: UploadFile,
217
- source_id: str,
212
+ source_id: SourceId,
218
213
  duplicate_handling: DuplicateFileHandling = Query(DuplicateFileHandling.SUFFIX, description="How to handle duplicate filenames"),
219
214
  name: Optional[str] = Query(None, description="Optional custom name to override the uploaded file's name"),
220
215
  server: "SyncServer" = Depends(get_letta_server),
@@ -246,9 +241,8 @@ async def upload_file_to_source(
246
241
 
247
242
  # If still not allowed, reject with 415.
248
243
  if media_type not in allowed_media_types:
249
- raise HTTPException(
250
- status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
251
- detail=(
244
+ raise LettaUnsupportedFileUploadError(
245
+ message=(
252
246
  f"Unsupported file type: {media_type or 'unknown'} "
253
247
  f"(filename: {file.filename}). "
254
248
  f"Supported types: PDF, text files (.txt, .md), JSON, and code files (.py, .js, .java, etc.)."
@@ -258,8 +252,6 @@ async def upload_file_to_source(
258
252
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
259
253
 
260
254
  source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
261
- if source is None:
262
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Source with id={source_id} not found.")
263
255
 
264
256
  content = await file.read()
265
257
 
@@ -278,8 +270,9 @@ async def upload_file_to_source(
278
270
  if existing_file:
279
271
  # Duplicate found, handle based on strategy
280
272
  if duplicate_handling == DuplicateFileHandling.ERROR:
281
- raise HTTPException(
282
- status_code=status.HTTP_409_CONFLICT, detail=f"File '{original_filename}' already exists in source '{source.name}'"
273
+ raise LettaInvalidArgumentError(
274
+ message=f"File '{original_filename}' already exists in source '{source.name}'",
275
+ argument_name="duplicate_handling",
283
276
  )
284
277
  elif duplicate_handling == DuplicateFileHandling.SKIP:
285
278
  # Return existing file metadata with custom header to indicate it was skipped
@@ -331,7 +324,7 @@ async def upload_file_to_source(
331
324
 
332
325
  @router.get("/{source_id}/agents", response_model=List[str], operation_id="get_agents_for_source", deprecated=True)
333
326
  async def get_agents_for_source(
334
- source_id: str,
327
+ source_id: SourceId,
335
328
  server: SyncServer = Depends(get_letta_server),
336
329
  headers: HeaderParams = Depends(get_headers),
337
330
  ):
@@ -344,7 +337,7 @@ async def get_agents_for_source(
344
337
 
345
338
  @router.get("/{source_id}/passages", response_model=List[Passage], operation_id="list_source_passages", deprecated=True)
346
339
  async def list_source_passages(
347
- source_id: str,
340
+ source_id: SourceId,
348
341
  after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."),
349
342
  before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."),
350
343
  limit: int = Query(100, description="Maximum number of messages to retrieve."),
@@ -366,7 +359,7 @@ async def list_source_passages(
366
359
 
367
360
  @router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_source_files", deprecated=True)
368
361
  async def list_source_files(
369
- source_id: str,
362
+ source_id: SourceId,
370
363
  limit: int = Query(1000, description="Number of files to return"),
371
364
  after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
372
365
  include_content: bool = Query(False, description="Whether to include full file content"),
@@ -394,8 +387,8 @@ async def list_source_files(
394
387
 
395
388
  @router.get("/{source_id}/files/{file_id}", response_model=FileMetadata, operation_id="get_file_metadata", deprecated=True)
396
389
  async def get_file_metadata(
397
- source_id: str,
398
- file_id: str,
390
+ source_id: SourceId,
391
+ file_id: FileId,
399
392
  include_content: bool = Query(False, description="Whether to include full file content"),
400
393
  server: "SyncServer" = Depends(get_letta_server),
401
394
  headers: HeaderParams = Depends(get_headers),
@@ -410,13 +403,6 @@ async def get_file_metadata(
410
403
  file_id=file_id, actor=actor, include_content=include_content, strip_directory_prefix=True
411
404
  )
412
405
 
413
- if not file_metadata:
414
- raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
415
-
416
- # Verify the file belongs to the specified source
417
- if file_metadata.source_id != source_id:
418
- raise HTTPException(status_code=404, detail=f"File with id={file_id} not found in source {source_id}.")
419
-
420
406
  # Check and update file status (timeout check and pinecone embedding sync)
421
407
  file_metadata = await server.file_manager.check_and_update_file_status(file_metadata, actor)
422
408
 
@@ -427,8 +413,8 @@ async def get_file_metadata(
427
413
  # it's still good practice to return a status indicating the success or failure of the deletion
428
414
  @router.delete("/{source_id}/{file_id}", status_code=204, operation_id="delete_file_from_source", deprecated=True)
429
415
  async def delete_file_from_source(
430
- source_id: str,
431
- file_id: str,
416
+ source_id: SourceId,
417
+ file_id: FileId,
432
418
  server: "SyncServer" = Depends(get_letta_server),
433
419
  headers: HeaderParams = Depends(get_headers),
434
420
  ):
@@ -452,8 +438,6 @@ async def delete_file_from_source(
452
438
  await delete_file_records_from_pinecone_index(file_id=file_id, actor=actor)
453
439
 
454
440
  safe_create_task(sleeptime_document_ingest_async(server, source_id, actor, clear_history=True), label="document_ingest_after_delete")
455
- if deleted_file is None:
456
- raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
457
441
 
458
442
 
459
443
  async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, filename: str, bytes: bytes, actor: User):
@@ -1,19 +1,19 @@
1
1
  from datetime import datetime
2
2
  from typing import List, Literal, Optional
3
3
 
4
- from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
4
+ from fastapi import APIRouter, Body, Depends, Header, Query
5
5
  from pydantic import BaseModel, Field
6
6
 
7
- from letta.orm.errors import NoResultFound
8
7
  from letta.schemas.letta_message import LettaMessageUnion
9
8
  from letta.schemas.message import Message
10
9
  from letta.schemas.provider_trace import ProviderTrace
11
- from letta.schemas.step import Step
10
+ from letta.schemas.step import Step, StepBase
12
11
  from letta.schemas.step_metrics import StepMetrics
13
12
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
14
13
  from letta.server.server import SyncServer
15
14
  from letta.services.step_manager import FeedbackType
16
15
  from letta.settings import settings
16
+ from letta.validators import StepId
17
17
 
18
18
  router = APIRouter(prefix="/steps", tags=["steps"])
19
19
 
@@ -70,39 +70,33 @@ async def list_steps(
70
70
 
71
71
  @router.get("/{step_id}", response_model=Step, operation_id="retrieve_step")
72
72
  async def retrieve_step(
73
- step_id: str,
73
+ step_id: StepId,
74
74
  headers: HeaderParams = Depends(get_headers),
75
75
  server: SyncServer = Depends(get_letta_server),
76
76
  ):
77
77
  """
78
78
  Get a step by ID.
79
79
  """
80
- try:
81
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
82
- return await server.step_manager.get_step_async(step_id=step_id, actor=actor)
83
- except NoResultFound:
84
- raise HTTPException(status_code=404, detail="Step not found")
80
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
81
+ return await server.step_manager.get_step_async(step_id=step_id, actor=actor)
85
82
 
86
83
 
87
84
  @router.get("/{step_id}/metrics", response_model=StepMetrics, operation_id="retrieve_metrics_for_step")
88
85
  async def retrieve_metrics_for_step(
89
- step_id: str,
86
+ step_id: StepId,
90
87
  headers: HeaderParams = Depends(get_headers),
91
88
  server: SyncServer = Depends(get_letta_server),
92
89
  ):
93
90
  """
94
91
  Get step metrics by step ID.
95
92
  """
96
- try:
97
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
98
- return await server.step_manager.get_step_metrics_async(step_id=step_id, actor=actor)
99
- except NoResultFound:
100
- raise HTTPException(status_code=404, detail="Step metrics not found")
93
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
94
+ return await server.step_manager.get_step_metrics_async(step_id=step_id, actor=actor)
101
95
 
102
96
 
103
97
  @router.get("/{step_id}/trace", response_model=Optional[ProviderTrace], operation_id="retrieve_trace_for_step")
104
98
  async def retrieve_trace_for_step(
105
- step_id: str,
99
+ step_id: StepId,
106
100
  server: SyncServer = Depends(get_letta_server),
107
101
  headers: HeaderParams = Depends(get_headers),
108
102
  ):
@@ -125,7 +119,7 @@ class ModifyFeedbackRequest(BaseModel):
125
119
 
126
120
  @router.patch("/{step_id}/feedback", response_model=Step, operation_id="modify_feedback_for_step")
127
121
  async def modify_feedback_for_step(
128
- step_id: str,
122
+ step_id: StepId,
129
123
  request: ModifyFeedbackRequest = Body(...),
130
124
  headers: HeaderParams = Depends(get_headers),
131
125
  server: SyncServer = Depends(get_letta_server),
@@ -133,16 +127,13 @@ async def modify_feedback_for_step(
133
127
  """
134
128
  Modify feedback for a given step.
135
129
  """
136
- try:
137
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
138
- return await server.step_manager.add_feedback_async(step_id=step_id, feedback=request.feedback, tags=request.tags, actor=actor)
139
- except NoResultFound:
140
- raise HTTPException(status_code=404, detail="Step not found")
130
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
131
+ return await server.step_manager.add_feedback_async(step_id=step_id, feedback=request.feedback, tags=request.tags, actor=actor)
141
132
 
142
133
 
143
134
  @router.get("/{step_id}/messages", response_model=List[LettaMessageUnion], operation_id="list_messages_for_step")
144
135
  async def list_messages_for_step(
145
- step_id: str,
136
+ step_id: StepId,
146
137
  headers: HeaderParams = Depends(get_headers),
147
138
  server: SyncServer = Depends(get_letta_server),
148
139
  before: Optional[str] = Query(
@@ -169,8 +160,8 @@ async def list_messages_for_step(
169
160
 
170
161
  @router.patch("/{step_id}/transaction/{transaction_id}", response_model=Step, operation_id="update_step_transaction_id")
171
162
  async def update_step_transaction_id(
172
- step_id: str,
173
163
  transaction_id: str,
164
+ step_id: StepId,
174
165
  headers: HeaderParams = Depends(get_headers),
175
166
  server: SyncServer = Depends(get_letta_server),
176
167
  ):
@@ -178,8 +169,4 @@ async def update_step_transaction_id(
178
169
  Update the transaction ID for a step.
179
170
  """
180
171
  actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
181
-
182
- try:
183
- return await server.step_manager.update_step_transaction_id(actor=actor, step_id=step_id, transaction_id=transaction_id)
184
- except NoResultFound:
185
- raise HTTPException(status_code=404, detail="Step not found")
172
+ return await server.step_manager.update_step_transaction_id(actor=actor, step_id=step_id, transaction_id=transaction_id)
@@ -30,14 +30,17 @@ from letta.schemas.letta_message_content import TextContent
30
30
  from letta.schemas.mcp import UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamableHTTPMCPServer
31
31
  from letta.schemas.message import Message
32
32
  from letta.schemas.pip_requirement import PipRequirement
33
- from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
33
+ from letta.schemas.tool import BaseTool, Tool, ToolCreate, ToolRunFromSource, ToolUpdate
34
34
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
35
35
  from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode
36
36
  from letta.server.server import SyncServer
37
37
  from letta.services.mcp.oauth_utils import MCPOAuthSession, drill_down_exception, oauth_stream_event
38
38
  from letta.services.mcp.stdio_client import AsyncStdioMCPClient
39
39
  from letta.services.mcp.types import OauthStreamEvent
40
+ from letta.services.summarizer.summarizer import traceback
40
41
  from letta.settings import tool_settings
42
+ from letta.utils import asyncio
43
+ from letta.validators import ToolId
41
44
 
42
45
  router = APIRouter(prefix="/tools", tags=["tools"])
43
46
 
@@ -46,7 +49,7 @@ logger = get_logger(__name__)
46
49
 
47
50
  @router.delete("/{tool_id}", operation_id="delete_tool")
48
51
  async def delete_tool(
49
- tool_id: str,
52
+ tool_id: ToolId,
50
53
  server: SyncServer = Depends(get_letta_server),
51
54
  headers: HeaderParams = Depends(get_headers),
52
55
  ):
@@ -149,7 +152,7 @@ async def count_tools(
149
152
 
150
153
  @router.get("/{tool_id}", response_model=Tool, operation_id="retrieve_tool")
151
154
  async def retrieve_tool(
152
- tool_id: str,
155
+ tool_id: ToolId,
153
156
  server: SyncServer = Depends(get_letta_server),
154
157
  headers: HeaderParams = Depends(get_headers),
155
158
  ):
@@ -297,7 +300,7 @@ async def upsert_tool(
297
300
 
298
301
  @router.patch("/{tool_id}", response_model=Tool, operation_id="modify_tool")
299
302
  async def modify_tool(
300
- tool_id: str,
303
+ tool_id: ToolId,
301
304
  request: ToolUpdate = Body(...),
302
305
  server: SyncServer = Depends(get_letta_server),
303
306
  headers: HeaderParams = Depends(get_headers),
@@ -443,14 +446,11 @@ async def add_mcp_tool(
443
446
  argument_name="mcp_tool_name",
444
447
  )
445
448
 
446
- # Check tool health - reject only INVALID tools
447
- if mcp_tool.health:
448
- if mcp_tool.health.status == "INVALID":
449
- raise LettaInvalidMCPSchemaError(
450
- server_name=mcp_server_name,
451
- mcp_tool_name=mcp_tool_name,
452
- reasons=mcp_tool.health.reasons,
453
- )
449
+ # Log warning if tool has invalid schema but allow attachment
450
+ if mcp_tool.health and mcp_tool.health.status == "INVALID":
451
+ logger.warning(
452
+ f"Attaching MCP tool {mcp_tool_name} from server {mcp_server_name} with invalid schema. Reasons: {mcp_tool.health.reasons}"
453
+ )
454
454
 
455
455
  tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
456
456
  # For config-based servers, use the server name as ID since they don't have database IDs
@@ -666,7 +666,11 @@ async def connect_mcp_server(
666
666
  detailed_error = drill_down_exception(e)
667
667
  logger.error(f"Error in OAuth stream:\n{detailed_error}")
668
668
  yield oauth_stream_event(OauthStreamEvent.ERROR, message=f"Internal error: {detailed_error}")
669
-
669
+ # TODO: investigate cancelled by cancel scope errors here during oauth exchange flow
670
+ except asyncio.CancelledError as e:
671
+ logger.error(f"CancelledError: {e!r}")
672
+ tb = "".join(traceback.format_stack())
673
+ logger.error(f"Stack trace at cancellation:\n{tb}")
670
674
  finally:
671
675
  if client:
672
676
  try:
@@ -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.user import User, UserCreate, UserUpdate
6
6
  from letta.server.rest_api.dependencies import get_letta_server
@@ -22,13 +22,7 @@ async def list_users(
22
22
  """
23
23
  Get a list of all users in the database
24
24
  """
25
- try:
26
- users = await server.user_manager.list_actors_async(after=after, limit=limit)
27
- except HTTPException:
28
- raise
29
- except Exception as e:
30
- raise HTTPException(status_code=500, detail=f"{e}")
31
- return users
25
+ return await server.user_manager.list_actors_async(after=after, limit=limit)
32
26
 
33
27
 
34
28
  @router.post("/", tags=["admin"], response_model=User, operation_id="create_user")
@@ -62,13 +56,7 @@ async def delete_user(
62
56
  server: "SyncServer" = Depends(get_letta_server),
63
57
  ):
64
58
  # TODO make a soft deletion, instead of a hard deletion
65
- try:
66
- user = await server.user_manager.get_actor_by_id_async(actor_id=user_id)
67
- if user is None:
68
- raise HTTPException(status_code=404, detail="User does not exist")
69
- await server.user_manager.delete_actor_by_id_async(user_id=user_id)
70
- except HTTPException:
71
- raise
72
- except Exception as e:
73
- raise HTTPException(status_code=500, detail=f"{e}")
59
+ # Get the user first so we can return it after deletion
60
+ user = await server.user_manager.get_actor_by_id_async(actor_id=user_id)
61
+ await server.user_manager.delete_actor_by_id_async(user_id=user_id)
74
62
  return user