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.
- letta/__init__.py +2 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/entry_points.txt +0 -0
- {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:
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
226
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
269
|
-
|
|
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
|
|
301
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
507
|
-
file_id:
|
|
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,
|
|
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.
|
|
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:
|
|
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
|
-
|
|
100
|
-
|
|
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:
|
|
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
|
-
|
|
120
|
-
|
|
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:
|
|
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
|
-
|
|
137
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
220
|
-
message_id:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|