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
@@ -1,7 +1,7 @@
1
1
  import mimetypes
2
2
  import os
3
3
  import tempfile
4
- from pathlib import Path
4
+ from pathlib import Path as PathLibPath
5
5
  from typing import List, Literal, Optional
6
6
 
7
7
  from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile
@@ -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,10 +21,10 @@ 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.folder import Folder
24
+ from letta.schemas.file import FileMetadata, FileMetadataBase
25
+ from letta.schemas.folder import BaseFolder, Folder
25
26
  from letta.schemas.passage import Passage
26
- from letta.schemas.source import Source, SourceCreate, SourceUpdate
27
+ from letta.schemas.source import BaseSource, Source, SourceCreate, SourceUpdate
27
28
  from letta.schemas.source_metadata import OrganizationSourcesStats
28
29
  from letta.schemas.user import User
29
30
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
@@ -36,6 +37,7 @@ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFil
36
37
  from letta.services.file_processor.parser.mistral_parser import MistralFileParser
37
38
  from letta.settings import settings
38
39
  from letta.utils import safe_create_file_processing_task, safe_create_task, sanitize_filename
40
+ from letta.validators import FileId, FolderId
39
41
 
40
42
  logger = get_logger(__name__)
41
43
 
@@ -60,7 +62,7 @@ async def count_folders(
60
62
 
61
63
  @router.get("/{folder_id}", response_model=Folder, operation_id="retrieve_folder")
62
64
  async def retrieve_folder(
63
- folder_id: str,
65
+ folder_id: FolderId,
64
66
  server: "SyncServer" = Depends(get_letta_server),
65
67
  headers: HeaderParams = Depends(get_headers),
66
68
  ):
@@ -70,8 +72,6 @@ async def retrieve_folder(
70
72
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
71
73
 
72
74
  folder = await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor)
73
- if not folder:
74
- raise HTTPException(status_code=404, detail=f"Folder with id={folder_id} not found.")
75
75
  return folder
76
76
 
77
77
 
@@ -90,8 +90,6 @@ async def get_folder_by_name(
90
90
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
91
91
 
92
92
  folder = await server.source_manager.get_source_by_name(source_name=folder_name, actor=actor)
93
- if not folder:
94
- raise HTTPException(status_code=404, detail=f"Folder with name={folder_name} not found.")
95
93
  return folder.id
96
94
 
97
95
 
@@ -157,8 +155,9 @@ async def create_folder(
157
155
  if not folder_create.embedding_config:
158
156
  if not folder_create.embedding:
159
157
  if settings.default_embedding_handle is None:
160
- # TODO: modify error type
161
- raise ValueError("Must specify either embedding or embedding_config in request")
158
+ raise LettaInvalidArgumentError(
159
+ "Must specify either embedding or embedding_config in request", argument_name="default_embedding_handle"
160
+ )
162
161
  else:
163
162
  folder_create.embedding = settings.default_embedding_handle
164
163
  folder_create.embedding_config = await server.get_embedding_config_from_handle_async(
@@ -178,8 +177,8 @@ async def create_folder(
178
177
 
179
178
  @router.patch("/{folder_id}", response_model=Folder, operation_id="modify_folder")
180
179
  async def modify_folder(
181
- folder_id: str,
182
180
  folder: SourceUpdate,
181
+ folder_id: FolderId,
183
182
  server: "SyncServer" = Depends(get_letta_server),
184
183
  headers: HeaderParams = Depends(get_headers),
185
184
  ):
@@ -188,14 +187,13 @@ async def modify_folder(
188
187
  """
189
188
  # TODO: allow updating the handle/embedding config
190
189
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
191
- if not await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor):
192
- raise HTTPException(status_code=404, detail=f"Folder with id={folder_id} does not exist.")
190
+ await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor)
193
191
  return await server.source_manager.update_source(source_id=folder_id, source_update=folder, actor=actor)
194
192
 
195
193
 
196
194
  @router.delete("/{folder_id}", response_model=None, operation_id="delete_folder")
197
195
  async def delete_folder(
198
- folder_id: str,
196
+ folder_id: FolderId,
199
197
  server: "SyncServer" = Depends(get_letta_server),
200
198
  headers: HeaderParams = Depends(get_headers),
201
199
  ):
@@ -222,18 +220,16 @@ async def delete_folder(
222
220
  await server.remove_files_from_context_window(agent_state=agent_state, file_ids=file_ids, actor=actor)
223
221
 
224
222
  if agent_state.enable_sleeptime:
225
- try:
226
- block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=folder.name, actor=actor)
223
+ block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=folder.name, actor=actor)
224
+ if block:
227
225
  await server.block_manager.delete_block_async(block.id, actor)
228
- except:
229
- pass
230
226
  await server.delete_source(source_id=folder_id, actor=actor)
231
227
 
232
228
 
233
229
  @router.post("/{folder_id}/upload", response_model=FileMetadata, operation_id="upload_file_to_folder")
234
230
  async def upload_file_to_folder(
235
231
  file: UploadFile,
236
- folder_id: str,
232
+ folder_id: FolderId,
237
233
  duplicate_handling: DuplicateFileHandling = Query(DuplicateFileHandling.SUFFIX, description="How to handle duplicate filenames"),
238
234
  name: Optional[str] = Query(None, description="Optional custom name to override the uploaded file's name"),
239
235
  server: "SyncServer" = Depends(get_letta_server),
@@ -259,15 +255,14 @@ async def upload_file_to_folder(
259
255
  media_type = (guessed or "").lower()
260
256
 
261
257
  if media_type not in allowed_media_types:
262
- ext = Path(file.filename).suffix.lower()
258
+ ext = PathLibPath(file.filename).suffix.lower()
263
259
  ext_map = get_extension_to_mime_type_map()
264
260
  media_type = ext_map.get(ext, media_type)
265
261
 
266
262
  # If still not allowed, reject with 415.
267
263
  if media_type not in allowed_media_types:
268
- raise HTTPException(
269
- status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
270
- detail=(
264
+ raise LettaUnsupportedFileUploadError(
265
+ message=(
271
266
  f"Unsupported file type: {media_type or 'unknown'} "
272
267
  f"(filename: {file.filename}). "
273
268
  f"Supported types: PDF, text files (.txt, .md), JSON, and code files (.py, .js, .java, etc.)."
@@ -277,8 +272,6 @@ async def upload_file_to_folder(
277
272
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
278
273
 
279
274
  folder = await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor)
280
- if folder is None:
281
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Folder with id={folder_id} not found.")
282
275
 
283
276
  content = await file.read()
284
277
 
@@ -297,8 +290,9 @@ async def upload_file_to_folder(
297
290
  if existing_file:
298
291
  # Duplicate found, handle based on strategy
299
292
  if duplicate_handling == DuplicateFileHandling.ERROR:
300
- raise HTTPException(
301
- status_code=status.HTTP_409_CONFLICT, detail=f"File '{original_filename}' already exists in folder '{folder.name}'"
293
+ raise LettaInvalidArgumentError(
294
+ message=f"File '{original_filename}' already exists in folder '{folder.name}'",
295
+ argument_name="duplicate_handling",
302
296
  )
303
297
  elif duplicate_handling == DuplicateFileHandling.SKIP:
304
298
  # Return existing file metadata with custom header to indicate it was skipped
@@ -350,7 +344,7 @@ async def upload_file_to_folder(
350
344
 
351
345
  @router.get("/{folder_id}/agents", response_model=List[str], operation_id="list_agents_for_folder")
352
346
  async def list_agents_for_folder(
353
- folder_id: str,
347
+ folder_id: FolderId,
354
348
  before: Optional[str] = Query(
355
349
  None,
356
350
  description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order",
@@ -383,7 +377,7 @@ async def list_agents_for_folder(
383
377
 
384
378
  @router.get("/{folder_id}/passages", response_model=List[Passage], operation_id="list_folder_passages")
385
379
  async def list_folder_passages(
386
- folder_id: str,
380
+ folder_id: FolderId,
387
381
  before: Optional[str] = Query(
388
382
  None,
389
383
  description="Passage ID cursor for pagination. Returns passages that come before this passage ID in the specified sort order",
@@ -416,7 +410,7 @@ async def list_folder_passages(
416
410
 
417
411
  @router.get("/{folder_id}/files", response_model=List[FileMetadata], operation_id="list_folder_files")
418
412
  async def list_folder_files(
419
- folder_id: str,
413
+ folder_id: FolderId,
420
414
  before: Optional[str] = Query(
421
415
  None,
422
416
  description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order",
@@ -503,8 +497,8 @@ async def list_folder_files(
503
497
  # it's still good practice to return a status indicating the success or failure of the deletion
504
498
  @router.delete("/{folder_id}/{file_id}", status_code=204, operation_id="delete_file_from_folder")
505
499
  async def delete_file_from_folder(
506
- folder_id: str,
507
- file_id: str,
500
+ folder_id: FolderId,
501
+ file_id: FileId,
508
502
  server: "SyncServer" = Depends(get_letta_server),
509
503
  headers: HeaderParams = Depends(get_headers),
510
504
  ):
@@ -528,8 +522,6 @@ async def delete_file_from_folder(
528
522
  await delete_file_records_from_pinecone_index(file_id=file_id, actor=actor)
529
523
 
530
524
  safe_create_task(sleeptime_document_ingest_async(server, folder_id, actor, clear_history=True), label="document_ingest_after_delete")
531
- if deleted_file is None:
532
- raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
533
525
 
534
526
 
535
527
  async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, filename: str, bytes: bytes, actor: User):
@@ -1,17 +1,18 @@
1
1
  from typing import Annotated, List, Literal, Optional
2
2
 
3
- from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, status
3
+ from fastapi import APIRouter, Body, Depends, Header, Query, status
4
4
  from fastapi.responses import JSONResponse
5
5
  from pydantic import Field
6
6
 
7
7
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
8
- from letta.orm.errors import NoResultFound
9
- from letta.schemas.group import Group, GroupCreate, GroupUpdate, ManagerType
8
+ from letta.schemas.group import Group, GroupBase, GroupCreate, GroupUpdate, ManagerType
10
9
  from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion
11
10
  from letta.schemas.letta_request import LettaRequest, LettaStreamingRequest
12
11
  from letta.schemas.letta_response import LettaResponse
12
+ from letta.schemas.message import BaseMessage
13
13
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
14
14
  from letta.server.server import SyncServer
15
+ from letta.validators import GroupId, MessageId
15
16
 
16
17
  router = APIRouter(prefix="/groups", tags=["groups"])
17
18
 
@@ -69,7 +70,7 @@ async def count_groups(
69
70
 
70
71
  @router.get("/{group_id}", response_model=Group, operation_id="retrieve_group")
71
72
  async def retrieve_group(
72
- group_id: str,
73
+ group_id: GroupId,
73
74
  server: "SyncServer" = Depends(get_letta_server),
74
75
  headers: HeaderParams = Depends(get_headers),
75
76
  ):
@@ -77,11 +78,7 @@ async def retrieve_group(
77
78
  Retrieve the group by id.
78
79
  """
79
80
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
80
-
81
- try:
82
- return await server.group_manager.retrieve_group_async(group_id=group_id, actor=actor)
83
- except NoResultFound as e:
84
- raise HTTPException(status_code=404, detail=str(e))
81
+ return await server.group_manager.retrieve_group_async(group_id=group_id, actor=actor)
85
82
 
86
83
 
87
84
  @router.post("/", response_model=Group, operation_id="create_group")
@@ -96,16 +93,13 @@ async def create_group(
96
93
  """
97
94
  Create a new multi-agent group with the specified configuration.
98
95
  """
99
- try:
100
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
101
- return await server.group_manager.create_group_async(group, actor=actor)
102
- except Exception as e:
103
- raise HTTPException(status_code=500, detail=str(e))
96
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
97
+ return await server.group_manager.create_group_async(group, actor=actor)
104
98
 
105
99
 
106
100
  @router.patch("/{group_id}", response_model=Group, operation_id="modify_group")
107
101
  async def modify_group(
108
- group_id: str,
102
+ group_id: GroupId,
109
103
  group: GroupUpdate = Body(...),
110
104
  server: "SyncServer" = Depends(get_letta_server),
111
105
  headers: HeaderParams = Depends(get_headers),
@@ -116,16 +110,13 @@ async def modify_group(
116
110
  """
117
111
  Create a new multi-agent group with the specified configuration.
118
112
  """
119
- try:
120
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
121
- return await server.group_manager.modify_group_async(group_id=group_id, group_update=group, actor=actor)
122
- except Exception as e:
123
- raise HTTPException(status_code=500, detail=str(e))
113
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
114
+ return await server.group_manager.modify_group_async(group_id=group_id, group_update=group, actor=actor)
124
115
 
125
116
 
126
117
  @router.delete("/{group_id}", response_model=None, operation_id="delete_group")
127
118
  async def delete_group(
128
- group_id: str,
119
+ group_id: GroupId,
129
120
  server: "SyncServer" = Depends(get_letta_server),
130
121
  headers: HeaderParams = Depends(get_headers),
131
122
  ):
@@ -133,11 +124,8 @@ async def delete_group(
133
124
  Delete a multi-agent group.
134
125
  """
135
126
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
136
- try:
137
- await server.group_manager.delete_group_async(group_id=group_id, actor=actor)
138
- return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Group id={group_id} successfully deleted"})
139
- except NoResultFound:
140
- raise HTTPException(status_code=404, detail=f"Group id={group_id} not found for user_id={actor.id}.")
127
+ await server.group_manager.delete_group_async(group_id=group_id, actor=actor)
128
+ return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Group id={group_id} successfully deleted"})
141
129
 
142
130
 
143
131
  @router.post(
@@ -146,7 +134,7 @@ async def delete_group(
146
134
  operation_id="send_group_message",
147
135
  )
148
136
  async def send_group_message(
149
- group_id: str,
137
+ group_id: GroupId,
150
138
  server: SyncServer = Depends(get_letta_server),
151
139
  request: LettaRequest = Body(...),
152
140
  headers: HeaderParams = Depends(get_headers),
@@ -184,7 +172,7 @@ async def send_group_message(
184
172
  },
185
173
  )
186
174
  async def send_group_message_streaming(
187
- group_id: str,
175
+ group_id: GroupId,
188
176
  server: SyncServer = Depends(get_letta_server),
189
177
  request: LettaStreamingRequest = Body(...),
190
178
  headers: HeaderParams = Depends(get_headers),
@@ -216,8 +204,8 @@ GroupMessagesResponse = Annotated[
216
204
 
217
205
  @router.patch("/{group_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_group_message")
218
206
  async def modify_group_message(
219
- group_id: str,
220
- message_id: str,
207
+ group_id: GroupId,
208
+ message_id: MessageId,
221
209
  request: LettaMessageUpdateUnion = Body(...),
222
210
  server: "SyncServer" = Depends(get_letta_server),
223
211
  headers: HeaderParams = Depends(get_headers),
@@ -232,7 +220,7 @@ async def modify_group_message(
232
220
 
233
221
  @router.get("/{group_id}/messages", response_model=GroupMessagesResponse, operation_id="list_group_messages")
234
222
  async def list_group_messages(
235
- group_id: str,
223
+ group_id: GroupId,
236
224
  before: Optional[str] = Query(
237
225
  None,
238
226
  description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order",
@@ -246,9 +234,9 @@ async def list_group_messages(
246
234
  "desc", description="Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first"
247
235
  ),
248
236
  order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
249
- use_assistant_message: bool = Query(True, description="Whether to use assistant messages"),
250
- assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool."),
251
- assistant_message_tool_kwarg: str = Query(DEFAULT_MESSAGE_TOOL_KWARG, description="The name of the message argument."),
237
+ use_assistant_message: bool = Query(True, description="Whether to use assistant messages", deprecated=True),
238
+ assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool.", deprecated=True),
239
+ assistant_message_tool_kwarg: str = Query(DEFAULT_MESSAGE_TOOL_KWARG, description="The name of the message argument.", deprecated=True),
252
240
  server: "SyncServer" = Depends(get_letta_server),
253
241
  headers: HeaderParams = Depends(get_headers),
254
242
  ):
@@ -287,7 +275,7 @@ async def list_group_messages(
287
275
 
288
276
  @router.patch("/{group_id}/reset-messages", response_model=None, operation_id="reset_group_messages")
289
277
  async def reset_group_messages(
290
- group_id: str,
278
+ group_id: GroupId,
291
279
  server: "SyncServer" = Depends(get_letta_server),
292
280
  headers: HeaderParams = Depends(get_headers),
293
281
  ):
@@ -1,12 +1,20 @@
1
- from typing import TYPE_CHECKING, List, Literal, Optional
1
+ from typing import TYPE_CHECKING, List, Literal, Optional, Union
2
2
 
3
3
  from fastapi import APIRouter, Body, Depends, Header, Query
4
4
 
5
5
  from letta.orm.errors import NoResultFound, UniqueConstraintViolationError
6
- from letta.schemas.agent import AgentState
6
+ from letta.schemas.agent import AgentRelationships, AgentState
7
7
  from letta.schemas.block import Block
8
- from letta.schemas.identity import Identity, IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert
8
+ from letta.schemas.identity import (
9
+ Identity,
10
+ IdentityCreate,
11
+ IdentityProperty,
12
+ IdentityType,
13
+ IdentityUpdate,
14
+ IdentityUpsert,
15
+ )
9
16
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
17
+ from letta.validators import IdentityId
10
18
 
11
19
  if TYPE_CHECKING:
12
20
  from letta.server.server import SyncServer
@@ -41,7 +49,7 @@ async def list_identities(
41
49
  """
42
50
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
43
51
 
44
- identities = await server.identity_manager.list_identities_async(
52
+ identities, next_cursor, has_more = await server.identity_manager.list_identities_async(
45
53
  name=name,
46
54
  project_id=project_id,
47
55
  identifier_key=identifier_key,
@@ -52,6 +60,7 @@ async def list_identities(
52
60
  ascending=(order == "asc"),
53
61
  actor=actor,
54
62
  )
63
+
55
64
  return identities
56
65
 
57
66
 
@@ -72,7 +81,7 @@ async def count_identities(
72
81
 
73
82
  @router.get("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="retrieve_identity")
74
83
  async def retrieve_identity(
75
- identity_id: str,
84
+ identity_id: IdentityId,
76
85
  server: "SyncServer" = Depends(get_letta_server),
77
86
  headers: HeaderParams = Depends(get_headers),
78
87
  ):
@@ -108,7 +117,7 @@ async def upsert_identity(
108
117
 
109
118
  @router.patch("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="update_identity")
110
119
  async def modify_identity(
111
- identity_id: str,
120
+ identity_id: IdentityId,
112
121
  identity: IdentityUpdate = Body(...),
113
122
  server: "SyncServer" = Depends(get_letta_server),
114
123
  headers: HeaderParams = Depends(get_headers),
@@ -119,7 +128,7 @@ async def modify_identity(
119
128
 
120
129
  @router.put("/{identity_id}/properties", tags=["identities"], operation_id="upsert_identity_properties")
121
130
  async def upsert_identity_properties(
122
- identity_id: str,
131
+ identity_id: IdentityId,
123
132
  properties: List[IdentityProperty] = Body(...),
124
133
  server: "SyncServer" = Depends(get_letta_server),
125
134
  headers: HeaderParams = Depends(get_headers),
@@ -130,7 +139,7 @@ async def upsert_identity_properties(
130
139
 
131
140
  @router.delete("/{identity_id}", tags=["identities"], operation_id="delete_identity")
132
141
  async def delete_identity(
133
- identity_id: str,
142
+ identity_id: IdentityId,
134
143
  server: "SyncServer" = Depends(get_letta_server),
135
144
  headers: HeaderParams = Depends(get_headers),
136
145
  ):
@@ -143,7 +152,7 @@ async def delete_identity(
143
152
 
144
153
  @router.get("/{identity_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_identity")
145
154
  async def list_agents_for_identity(
146
- identity_id: str,
155
+ identity_id: IdentityId,
147
156
  before: Optional[str] = Query(
148
157
  None,
149
158
  description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order",
@@ -157,6 +166,10 @@ async def list_agents_for_identity(
157
166
  "desc", description="Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first"
158
167
  ),
159
168
  order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
169
+ include: List[AgentRelationships] = Query(
170
+ [],
171
+ description=("Specify which relational fields to include in the response. No relationships are included by default."),
172
+ ),
160
173
  server: "SyncServer" = Depends(get_letta_server),
161
174
  headers: HeaderParams = Depends(get_headers),
162
175
  ):
@@ -170,13 +183,14 @@ async def list_agents_for_identity(
170
183
  after=after,
171
184
  limit=limit,
172
185
  ascending=(order == "asc"),
186
+ include=include,
173
187
  actor=actor,
174
188
  )
175
189
 
176
190
 
177
191
  @router.get("/{identity_id}/blocks", response_model=List[Block], operation_id="list_blocks_for_identity")
178
192
  async def list_blocks_for_identity(
179
- identity_id: str,
193
+ identity_id: IdentityId,
180
194
  before: Optional[str] = Query(
181
195
  None,
182
196
  description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order",
@@ -0,0 +1,107 @@
1
+ from typing import List, Literal, Optional
2
+
3
+ from fastapi import APIRouter, Depends, Query
4
+
5
+ from letta.schemas.enums import ComparisonOperator, RunStatus
6
+ from letta.schemas.letta_stop_reason import StopReasonType
7
+ from letta.schemas.run import Run
8
+ from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
9
+ from letta.server.server import SyncServer
10
+ from letta.services.run_manager import RunManager
11
+
12
+ router = APIRouter(prefix="/_internal_runs", tags=["_internal_runs"])
13
+
14
+
15
+ def convert_statuses_to_enum(statuses: Optional[List[str]]) -> Optional[List[RunStatus]]:
16
+ """Convert a list of status strings to RunStatus enum values.
17
+
18
+ Args:
19
+ statuses: List of status strings or None
20
+
21
+ Returns:
22
+ List of RunStatus enum values or None if input is None
23
+ """
24
+ if statuses is None:
25
+ return None
26
+ return [RunStatus(status) for status in statuses]
27
+
28
+
29
+ @router.get("/", response_model=List[Run], operation_id="list_internal_runs")
30
+ async def list_runs(
31
+ server: "SyncServer" = Depends(get_letta_server),
32
+ agent_id: Optional[str] = Query(None, description="The unique identifier of the agent associated with the run."),
33
+ agent_ids: Optional[List[str]] = Query(
34
+ None,
35
+ description="The unique identifiers of the agents associated with the run. Deprecated in favor of agent_id field.",
36
+ deprecated=True,
37
+ ),
38
+ statuses: Optional[List[str]] = Query(None, description="Filter runs by status. Can specify multiple statuses."),
39
+ background: Optional[bool] = Query(None, description="If True, filters for runs that were created in background mode."),
40
+ stop_reason: Optional[StopReasonType] = Query(None, description="Filter runs by stop reason."),
41
+ template_family: Optional[str] = Query(None, description="Filter runs by template family (base_template_id)."),
42
+ step_count: Optional[int] = Query(None, description="Filter runs by step count. Must be provided with step_count_operator."),
43
+ step_count_operator: ComparisonOperator = Query(
44
+ ComparisonOperator.EQ,
45
+ description="Operator for step_count filter: 'eq' for equals, 'gte' for greater than or equal, 'lte' for less than or equal.",
46
+ ),
47
+ tools_used: Optional[List[str]] = Query(None, description="Filter runs that used any of the specified tools."),
48
+ before: Optional[str] = Query(
49
+ None, description="Run ID cursor for pagination. Returns runs that come before this run ID in the specified sort order"
50
+ ),
51
+ after: Optional[str] = Query(
52
+ None, description="Run ID cursor for pagination. Returns runs that come after this run ID in the specified sort order"
53
+ ),
54
+ limit: Optional[int] = Query(100, description="Maximum number of runs to return"),
55
+ order: Literal["asc", "desc"] = Query(
56
+ "desc", description="Sort order for runs by creation time. 'asc' for oldest first, 'desc' for newest first"
57
+ ),
58
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
59
+ active: bool = Query(False, description="Filter for active runs."),
60
+ ascending: bool = Query(
61
+ False,
62
+ description="Whether to sort agents oldest to newest (True) or newest to oldest (False, default). Deprecated in favor of order field.",
63
+ deprecated=True,
64
+ ),
65
+ headers: HeaderParams = Depends(get_headers),
66
+ ):
67
+ """
68
+ List all runs.
69
+ """
70
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
71
+ runs_manager = server.run_manager
72
+
73
+ # Handle backwards compatibility: if statuses not provided but active=True, filter by active statuses
74
+ if statuses is None and active:
75
+ statuses = [RunStatus.created, RunStatus.running]
76
+
77
+ if agent_id:
78
+ # NOTE: we are deprecating agent_ids so this will the primary path soon
79
+ agent_ids = [agent_id]
80
+
81
+ # Handle backward compatibility: if ascending is explicitly set, use it; otherwise use order
82
+ if ascending is not False:
83
+ # ascending was explicitly set to True
84
+ sort_ascending = ascending
85
+ else:
86
+ # Use the new order parameter
87
+ sort_ascending = order == "asc"
88
+
89
+ # Convert string statuses to RunStatus enum
90
+ parsed_statuses = convert_statuses_to_enum(statuses)
91
+
92
+ runs = await runs_manager.list_runs(
93
+ actor=actor,
94
+ agent_ids=agent_ids,
95
+ statuses=parsed_statuses,
96
+ limit=limit,
97
+ before=before,
98
+ after=after,
99
+ ascending=sort_ascending,
100
+ stop_reason=stop_reason,
101
+ background=background,
102
+ template_family=template_family,
103
+ step_count=step_count,
104
+ step_count_operator=step_count_operator,
105
+ tools_used=tools_used,
106
+ )
107
+ return runs