From bbaaabb6e1b5c8b0df1d4b12442ad0c736a2e646 Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:37:00 -0700 Subject: [PATCH] fix: path validator had weird fastapi shared object memory bug (#5594) * fix weird path param conflict * move to factory model * openapi * use type hinting and import annotations * re add after mc resolution --- fern/openapi.json | 71 +++++++++--- letta/server/rest_api/routers/v1/agents.py | 102 +++++++++--------- letta/server/rest_api/routers/v1/archives.py | 4 +- letta/server/rest_api/routers/v1/blocks.py | 10 +- letta/server/rest_api/routers/v1/folders.py | 26 ++--- letta/server/rest_api/routers/v1/groups.py | 20 ++-- .../server/rest_api/routers/v1/identities.py | 14 +-- letta/server/rest_api/routers/v1/jobs.py | 8 +- letta/server/rest_api/routers/v1/providers.py | 10 +- .../rest_api/routers/v1/sandbox_configs.py | 10 +- letta/server/rest_api/routers/v1/sources.py | 24 ++--- letta/server/rest_api/routers/v1/steps.py | 14 +-- letta/server/rest_api/routers/v1/tools.py | 8 +- letta/validators.py | 54 ++++++++-- 14 files changed, 227 insertions(+), 148 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index cc54195c..f8f4234c 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -2521,9 +2521,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" } ], "responses": { @@ -2561,9 +2566,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" } ], "requestBody": { @@ -2611,9 +2621,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" } ], "responses": { @@ -2910,9 +2925,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" }, { "name": "duplicate_handling", @@ -2991,9 +3011,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" }, { "name": "before", @@ -3118,9 +3143,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" }, { "name": "before", @@ -3245,9 +3275,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" }, { "name": "before", @@ -3384,9 +3419,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the source in the format 'source-'" }, { "name": "file_id", @@ -3394,9 +3434,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], "title": "File Id" - } + }, + "description": "The ID of the file in the format 'file-'" } ], "responses": { @@ -4587,7 +4632,7 @@ "operationId": "attach_folder_to_agent", "parameters": [ { - "name": "source_id", + "name": "folder_id", "in": "path", "required": true, "schema": { @@ -4597,7 +4642,7 @@ "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", "description": "The ID of the source in the format 'source-'", "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], - "title": "Source Id" + "title": "Folder Id" }, "description": "The ID of the source in the format 'source-'" }, @@ -4712,7 +4757,7 @@ "operationId": "detach_folder_from_agent", "parameters": [ { - "name": "source_id", + "name": "folder_id", "in": "path", "required": true, "schema": { @@ -4722,7 +4767,7 @@ "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", "description": "The ID of the source in the format 'source-'", "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], - "title": "Source Id" + "title": "Folder Id" }, "description": "The ID of the source in the format 'source-'" }, diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index 9638f602..11d4aeb7 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -59,7 +59,7 @@ from letta.services.lettuce import LettuceClient from letta.services.run_manager import RunManager from letta.settings import settings from letta.utils import safe_create_shielded_task, safe_create_task, truncate_file_visible_content -from letta.validators import PATH_VALIDATORS +from letta.validators import AgentId, BlockId, FileId, MessageId, SourceId, ToolId # These can be forward refs, but because Fastapi needs them at runtime the must be imported normally @@ -170,7 +170,7 @@ class IndentedORJSONResponse(Response): @router.get("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent") async def export_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: str = AgentId, max_steps: int = Query(100, deprecated=True), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -345,7 +345,7 @@ async def import_agent( @router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="retrieve_agent_context_window", deprecated=True) async def retrieve_agent_context_window( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -385,7 +385,7 @@ async def create_agent( @router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent") async def modify_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, update_agent: UpdateAgent = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -397,7 +397,7 @@ async def modify_agent( @router.get("/{agent_id}/tools", response_model=list[Tool], operation_id="list_tools_for_agent") async def list_tools_for_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), before: Optional[str] = Query( @@ -426,8 +426,8 @@ async def list_tools_for_agent( @router.patch("/{agent_id}/tools/attach/{tool_id}", response_model=AgentState, operation_id="attach_tool_to_agent") async def attach_tool_to_agent( - tool_id: str = PATH_VALIDATORS[BaseTool.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + tool_id: ToolId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -442,8 +442,8 @@ async def attach_tool_to_agent( @router.patch("/{agent_id}/tools/detach/{tool_id}", response_model=AgentState, operation_id="detach_tool_from_agent") async def detach_tool_from_agent( - tool_id: str = PATH_VALIDATORS[BaseTool.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + tool_id: ToolId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -460,7 +460,7 @@ async def detach_tool_from_agent( async def modify_approval_for_tool( tool_name: str, requires_approval: bool, - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -477,8 +477,8 @@ async def modify_approval_for_tool( @router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent", deprecated=True) async def attach_source( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + source_id: SourceId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -504,8 +504,8 @@ async def attach_source( @router.patch("/{agent_id}/folders/attach/{folder_id}", response_model=AgentState, operation_id="attach_folder_to_agent") async def attach_folder_to_agent( - folder_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + folder_id: SourceId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -531,8 +531,8 @@ async def attach_folder_to_agent( @router.patch("/{agent_id}/sources/detach/{source_id}", response_model=AgentState, operation_id="detach_source_from_agent", deprecated=True) async def detach_source( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + source_id: SourceId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -561,8 +561,8 @@ async def detach_source( @router.patch("/{agent_id}/folders/detach/{folder_id}", response_model=AgentState, operation_id="detach_folder_from_agent") async def detach_folder_from_agent( - folder_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + folder_id: SourceId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -591,7 +591,7 @@ async def detach_folder_from_agent( @router.patch("/{agent_id}/files/close-all", response_model=List[str], operation_id="close_all_files_for_agent") async def close_all_files_for_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -608,8 +608,8 @@ async def close_all_files_for_agent( @router.patch("/{agent_id}/files/{file_id}/open", response_model=List[str], operation_id="open_file_for_agent") async def open_file_for_agent( - file_id: str = PATH_VALIDATORS[FileMetadataBase.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + file_id: FileId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -657,8 +657,8 @@ async def open_file_for_agent( @router.patch("/{agent_id}/files/{file_id}/close", response_model=None, operation_id="close_file_for_agent") async def close_file_for_agent( - file_id: str = PATH_VALIDATORS[FileMetadataBase.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + file_id: FileId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -682,7 +682,7 @@ async def close_file_for_agent( @router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent") async def retrieve_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, include_relationships: list[str] | None = Query( None, description=( @@ -705,7 +705,7 @@ async def retrieve_agent( @router.delete("/{agent_id}", response_model=None, operation_id="delete_agent") async def delete_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -719,7 +719,7 @@ async def delete_agent( @router.get("/{agent_id}/sources", response_model=list[Source], operation_id="list_agent_sources", deprecated=True) async def list_agent_sources( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), before: Optional[str] = Query( @@ -750,7 +750,7 @@ async def list_agent_sources( @router.get("/{agent_id}/folders", response_model=list[Source], operation_id="list_folders_for_agent") async def list_folders_for_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), before: Optional[str] = Query( @@ -781,7 +781,7 @@ async def list_folders_for_agent( @router.get("/{agent_id}/files", response_model=PaginatedAgentFiles, operation_id="list_files_for_agent") async def list_files_for_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, before: Optional[str] = Query( None, description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order" ), @@ -846,7 +846,7 @@ async def list_files_for_agent( # TODO: remove? can also get with agent blocks @router.get("/{agent_id}/core-memory", response_model=Memory, operation_id="retrieve_agent_memory", deprecated=True) async def retrieve_agent_memory( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -862,7 +862,7 @@ async def retrieve_agent_memory( @router.get("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="retrieve_core_memory_block") async def retrieve_block_for_agent( block_label: str, - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -876,7 +876,7 @@ async def retrieve_block_for_agent( @router.get("/{agent_id}/core-memory/blocks", response_model=list[Block], operation_id="list_core_memory_blocks") async def list_blocks_for_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), before: Optional[str] = Query( @@ -909,7 +909,7 @@ async def list_blocks_for_agent( @router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_core_memory_block") async def modify_block_for_agent( block_label: str, - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, block_update: BlockUpdate = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -931,8 +931,8 @@ async def modify_block_for_agent( @router.patch("/{agent_id}/core-memory/blocks/attach/{block_id}", response_model=AgentState, operation_id="attach_core_memory_block") async def attach_block_to_agent( - block_id: str = PATH_VALIDATORS[BaseBlock.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + block_id: BlockId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -945,8 +945,8 @@ async def attach_block_to_agent( @router.patch("/{agent_id}/core-memory/blocks/detach/{block_id}", response_model=AgentState, operation_id="detach_core_memory_block") async def detach_block_from_agent( - block_id: str = PATH_VALIDATORS[BaseBlock.__id_prefix__], - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + block_id: BlockId, + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -959,7 +959,7 @@ async def detach_block_from_agent( @router.get("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="list_passages", deprecated=True) async def list_passages( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), after: str | None = Query(None, description="Unique ID of the memory to start the query range at."), before: str | None = Query(None, description="Unique ID of the memory to end the query range at."), @@ -988,7 +988,7 @@ async def list_passages( @router.post("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="create_passage", deprecated=True) async def create_passage( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, request: CreateArchivalMemory = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -1010,7 +1010,7 @@ async def create_passage( deprecated=True, ) async def search_archival_memory( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, query: str = Query(..., description="String to search for using semantic similarity"), tags: Optional[List[str]] = Query(None, description="Optional list of tags to filter search results"), tag_match_mode: Literal["any", "all"] = Query( @@ -1058,7 +1058,7 @@ async def search_archival_memory( @router.delete("/{agent_id}/archival-memory/{memory_id}", response_model=None, operation_id="delete_passage", deprecated=True) async def delete_passage( memory_id: str, - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, # memory_id: str = Query(..., description="Unique ID of the memory to be deleted."), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -1079,7 +1079,7 @@ AgentMessagesResponse = Annotated[ @router.get("/{agent_id}/messages", response_model=AgentMessagesResponse, operation_id="list_messages") async def list_messages( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), before: Optional[str] = Query( None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" @@ -1124,8 +1124,8 @@ async def list_messages( @router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_message") async def modify_message( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], # backwards compatible. Consider removing for v1 - message_id: str = PATH_VALIDATORS[BaseMessage.__id_prefix__], + agent_id: AgentId, # backwards compatible. Consider removing for v1 + message_id: MessageId, request: LettaMessageUpdateUnion = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -1148,7 +1148,7 @@ async def modify_message( ) async def send_message( request_obj: Request, # FastAPI Request - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: SyncServer = Depends(get_letta_server), request: LettaRequest = Body(...), headers: HeaderParams = Depends(get_headers), @@ -1275,7 +1275,7 @@ async def send_message( ) async def send_message_streaming( request_obj: Request, # FastAPI Request - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: SyncServer = Depends(get_letta_server), request: LettaStreamingRequest = Body(...), headers: HeaderParams = Depends(get_headers), @@ -1308,7 +1308,7 @@ class CancelAgentRunRequest(BaseModel): @router.post("/{agent_id}/messages/cancel", operation_id="cancel_message") async def cancel_message( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, request: CancelAgentRunRequest = Body(None), server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -1484,7 +1484,7 @@ async def _process_message_background( operation_id="create_agent_message_async", ) async def send_message_async( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, server: SyncServer = Depends(get_letta_server), request: LettaAsyncRequest = Body(...), headers: HeaderParams = Depends(get_headers), @@ -1588,7 +1588,7 @@ async def send_message_async( @router.patch("/{agent_id}/reset-messages", response_model=AgentState, operation_id="reset_messages") async def reset_messages( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, add_default_initial_messages: bool = Query(default=False, description="If true, adds the default initial messages after resetting."), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -1602,7 +1602,7 @@ async def reset_messages( @router.get("/{agent_id}/groups", response_model=list[Group], operation_id="list_groups_for_agent") async def list_groups_for_agent( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, manager_type: str | None = Query(None, description="Manager type to filter groups by"), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -1638,7 +1638,7 @@ async def list_groups_for_agent( operation_id="preview_raw_payload", ) async def preview_raw_payload( - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, request: Union[LettaRequest, LettaStreamingRequest] = Body(...), server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -1684,7 +1684,7 @@ async def preview_raw_payload( @router.post("/{agent_id}/summarize", status_code=204, operation_id="summarize_agent_conversation") async def summarize_agent_conversation( request_obj: Request, # FastAPI Request - agent_id: str = PATH_VALIDATORS[AgentState.__id_prefix__], + agent_id: AgentId, max_message_length: int = Query(..., description="Maximum number of messages to retain after summarization."), server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), diff --git a/letta/server/rest_api/routers/v1/archives.py b/letta/server/rest_api/routers/v1/archives.py index f41aa89b..0f5e0392 100644 --- a/letta/server/rest_api/routers/v1/archives.py +++ b/letta/server/rest_api/routers/v1/archives.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from letta.schemas.archive import Archive as PydanticArchive, ArchiveBase from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer -from letta.validators import PATH_VALIDATORS +from letta.validators import ArchiveId router = APIRouter(prefix="/archives", tags=["archives"]) @@ -85,8 +85,8 @@ async def list_archives( @router.patch("/{archive_id}", response_model=PydanticArchive, operation_id="modify_archive") async def modify_archive( + archive_id: ArchiveId, archive: ArchiveUpdateRequest = Body(...), - archive_id: str = PATH_VALIDATORS[ArchiveBase.__id_prefix__], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): diff --git a/letta/server/rest_api/routers/v1/blocks.py b/letta/server/rest_api/routers/v1/blocks.py index 4ccb2dc3..c0a4ad3a 100644 --- a/letta/server/rest_api/routers/v1/blocks.py +++ b/letta/server/rest_api/routers/v1/blocks.py @@ -7,7 +7,7 @@ from letta.schemas.agent import AgentState from letta.schemas.block import BaseBlock, Block, BlockUpdate, CreateBlock from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer -from letta.validators import PATH_VALIDATORS +from letta.validators import BlockId if TYPE_CHECKING: pass @@ -129,7 +129,7 @@ async def create_block( @router.patch("/{block_id}", response_model=Block, operation_id="modify_block") async def modify_block( - block_id: str = PATH_VALIDATORS[BaseBlock.__id_prefix__], + block_id: BlockId, block_update: BlockUpdate = Body(...), server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -140,7 +140,7 @@ async def modify_block( @router.delete("/{block_id}", operation_id="delete_block") async def delete_block( - block_id: str = PATH_VALIDATORS[BaseBlock.__id_prefix__], + block_id: BlockId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -150,7 +150,7 @@ async def delete_block( @router.get("/{block_id}", response_model=Block, operation_id="retrieve_block") async def retrieve_block( - block_id: str = PATH_VALIDATORS[BaseBlock.__id_prefix__], + block_id: BlockId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -163,7 +163,7 @@ async def retrieve_block( @router.get("/{block_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_block") async def list_agents_for_block( - block_id: str = PATH_VALIDATORS[BaseBlock.__id_prefix__], + block_id: BlockId, before: Optional[str] = Query( None, description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", diff --git a/letta/server/rest_api/routers/v1/folders.py b/letta/server/rest_api/routers/v1/folders.py index 2a768d08..73c5bd91 100644 --- a/letta/server/rest_api/routers/v1/folders.py +++ b/letta/server/rest_api/routers/v1/folders.py @@ -1,10 +1,10 @@ import mimetypes import os import tempfile -from pathlib import Path +from pathlib import Path as PathLibPath from typing import List, Literal, Optional -from fastapi import APIRouter, Depends, HTTPException, Path as PathParam, Query, UploadFile +from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile from starlette import status from starlette.responses import Response @@ -37,7 +37,7 @@ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFil from letta.services.file_processor.parser.mistral_parser import MistralFileParser from letta.settings import settings from letta.utils import safe_create_file_processing_task, safe_create_task, sanitize_filename -from letta.validators import PATH_VALIDATORS, PRIMITIVE_ID_PATTERNS +from letta.validators import FileId, FolderId logger = get_logger(__name__) @@ -62,7 +62,7 @@ async def count_folders( @router.get("/{folder_id}", response_model=Folder, operation_id="retrieve_folder") async def retrieve_folder( - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), + folder_id: FolderId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -178,7 +178,7 @@ async def create_folder( @router.patch("/{folder_id}", response_model=Folder, operation_id="modify_folder") async def modify_folder( folder: SourceUpdate, - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), + folder_id: FolderId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -193,7 +193,7 @@ async def modify_folder( @router.delete("/{folder_id}", response_model=None, operation_id="delete_folder") async def delete_folder( - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), + folder_id: FolderId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -229,7 +229,7 @@ async def delete_folder( @router.post("/{folder_id}/upload", response_model=FileMetadata, operation_id="upload_file_to_folder") async def upload_file_to_folder( file: UploadFile, - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), + folder_id: FolderId, duplicate_handling: DuplicateFileHandling = Query(DuplicateFileHandling.SUFFIX, description="How to handle duplicate filenames"), name: Optional[str] = Query(None, description="Optional custom name to override the uploaded file's name"), server: "SyncServer" = Depends(get_letta_server), @@ -255,7 +255,7 @@ async def upload_file_to_folder( media_type = (guessed or "").lower() if media_type not in allowed_media_types: - ext = Path(file.filename).suffix.lower() + ext = PathLibPath(file.filename).suffix.lower() ext_map = get_extension_to_mime_type_map() media_type = ext_map.get(ext, media_type) @@ -344,7 +344,7 @@ async def upload_file_to_folder( @router.get("/{folder_id}/agents", response_model=List[str], operation_id="list_agents_for_folder") async def list_agents_for_folder( - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), + folder_id: FolderId, before: Optional[str] = Query( None, description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", @@ -377,7 +377,7 @@ async def list_agents_for_folder( @router.get("/{folder_id}/passages", response_model=List[Passage], operation_id="list_folder_passages") async def list_folder_passages( - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), + folder_id: FolderId, before: Optional[str] = Query( None, description="Passage ID cursor for pagination. Returns passages that come before this passage ID in the specified sort order", @@ -410,7 +410,7 @@ async def list_folder_passages( @router.get("/{folder_id}/files", response_model=List[FileMetadata], operation_id="list_folder_files") async def list_folder_files( - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), + folder_id: FolderId, before: Optional[str] = Query( None, description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order", @@ -497,8 +497,8 @@ async def list_folder_files( # it's still good practice to return a status indicating the success or failure of the deletion @router.delete("/{folder_id}/{file_id}", status_code=204, operation_id="delete_file_from_folder") async def delete_file_from_folder( - folder_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[BaseFolder.__id_prefix__].pattern), - file_id: str = PathParam(..., pattern=PRIMITIVE_ID_PATTERNS[FileMetadataBase.__id_prefix__].pattern), + folder_id: FolderId, + file_id: FileId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): diff --git a/letta/server/rest_api/routers/v1/groups.py b/letta/server/rest_api/routers/v1/groups.py index f800e477..fe9f4766 100644 --- a/letta/server/rest_api/routers/v1/groups.py +++ b/letta/server/rest_api/routers/v1/groups.py @@ -12,7 +12,7 @@ from letta.schemas.letta_response import LettaResponse from letta.schemas.message import BaseMessage from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer -from letta.validators import PATH_VALIDATORS +from letta.validators import GroupId, MessageId router = APIRouter(prefix="/groups", tags=["groups"]) @@ -70,7 +70,7 @@ async def count_groups( @router.get("/{group_id}", response_model=Group, operation_id="retrieve_group") async def retrieve_group( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], + group_id: GroupId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -99,7 +99,7 @@ async def create_group( @router.patch("/{group_id}", response_model=Group, operation_id="modify_group") async def modify_group( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], + group_id: GroupId, group: GroupUpdate = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -116,7 +116,7 @@ async def modify_group( @router.delete("/{group_id}", response_model=None, operation_id="delete_group") async def delete_group( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], + group_id: GroupId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -134,7 +134,7 @@ async def delete_group( operation_id="send_group_message", ) async def send_group_message( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], + group_id: GroupId, server: SyncServer = Depends(get_letta_server), request: LettaRequest = Body(...), headers: HeaderParams = Depends(get_headers), @@ -172,7 +172,7 @@ async def send_group_message( }, ) async def send_group_message_streaming( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], + group_id: GroupId, server: SyncServer = Depends(get_letta_server), request: LettaStreamingRequest = Body(...), headers: HeaderParams = Depends(get_headers), @@ -204,8 +204,8 @@ GroupMessagesResponse = Annotated[ @router.patch("/{group_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_group_message") async def modify_group_message( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], - message_id: str = PATH_VALIDATORS[BaseMessage.__id_prefix__], + group_id: GroupId, + message_id: MessageId, request: LettaMessageUpdateUnion = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -220,7 +220,7 @@ async def modify_group_message( @router.get("/{group_id}/messages", response_model=GroupMessagesResponse, operation_id="list_group_messages") async def list_group_messages( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], + group_id: GroupId, before: Optional[str] = Query( None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", @@ -275,7 +275,7 @@ async def list_group_messages( @router.patch("/{group_id}/reset-messages", response_model=None, operation_id="reset_group_messages") async def reset_group_messages( - group_id: str = PATH_VALIDATORS[GroupBase.__id_prefix__], + group_id: GroupId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): diff --git a/letta/server/rest_api/routers/v1/identities.py b/letta/server/rest_api/routers/v1/identities.py index d5875af2..e74c5090 100644 --- a/letta/server/rest_api/routers/v1/identities.py +++ b/letta/server/rest_api/routers/v1/identities.py @@ -7,7 +7,7 @@ from letta.schemas.agent import AgentState from letta.schemas.block import Block from letta.schemas.identity import Identity, IdentityBase, IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server -from letta.validators import PATH_VALIDATORS +from letta.validators import IdentityId if TYPE_CHECKING: from letta.server.server import SyncServer @@ -73,7 +73,7 @@ async def count_identities( @router.get("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="retrieve_identity") async def retrieve_identity( - identity_id: str = PATH_VALIDATORS[IdentityBase.__id_prefix__], + identity_id: IdentityId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -109,7 +109,7 @@ async def upsert_identity( @router.patch("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="update_identity") async def modify_identity( - identity_id: str = PATH_VALIDATORS[IdentityBase.__id_prefix__], + identity_id: IdentityId, identity: IdentityUpdate = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -120,7 +120,7 @@ async def modify_identity( @router.put("/{identity_id}/properties", tags=["identities"], operation_id="upsert_identity_properties") async def upsert_identity_properties( - identity_id: str = PATH_VALIDATORS[IdentityBase.__id_prefix__], + identity_id: IdentityId, properties: List[IdentityProperty] = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -131,7 +131,7 @@ async def upsert_identity_properties( @router.delete("/{identity_id}", tags=["identities"], operation_id="delete_identity") async def delete_identity( - identity_id: str = PATH_VALIDATORS[IdentityBase.__id_prefix__], + identity_id: IdentityId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -144,7 +144,7 @@ async def delete_identity( @router.get("/{identity_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_identity") async def list_agents_for_identity( - identity_id: str = PATH_VALIDATORS[IdentityBase.__id_prefix__], + identity_id: IdentityId, before: Optional[str] = Query( None, description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", @@ -177,7 +177,7 @@ async def list_agents_for_identity( @router.get("/{identity_id}/blocks", response_model=List[Block], operation_id="list_blocks_for_identity") async def list_blocks_for_identity( - identity_id: str = PATH_VALIDATORS[IdentityBase.__id_prefix__], + identity_id: IdentityId, before: Optional[str] = Query( None, description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", diff --git a/letta/server/rest_api/routers/v1/jobs.py b/letta/server/rest_api/routers/v1/jobs.py index dfec9f68..82207a8b 100644 --- a/letta/server/rest_api/routers/v1/jobs.py +++ b/letta/server/rest_api/routers/v1/jobs.py @@ -8,7 +8,7 @@ from letta.schemas.job import Job, JobBase from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer from letta.settings import settings -from letta.validators import PATH_VALIDATORS +from letta.validators import JobId router = APIRouter(prefix="/jobs", tags=["jobs"]) @@ -89,7 +89,7 @@ async def list_active_jobs( @router.get("/{job_id}", response_model=Job, operation_id="retrieve_job") async def retrieve_job( - job_id: str = PATH_VALIDATORS[JobBase.__id_prefix__], + job_id: JobId, headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -102,7 +102,7 @@ async def retrieve_job( @router.patch("/{job_id}/cancel", response_model=Job, operation_id="cancel_job") async def cancel_job( - job_id: str = PATH_VALIDATORS[JobBase.__id_prefix__], + job_id: JobId, headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -127,7 +127,7 @@ async def cancel_job( @router.delete("/{job_id}", response_model=Job, operation_id="delete_job") async def delete_job( - job_id: str = PATH_VALIDATORS[JobBase.__id_prefix__], + job_id: JobId, headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): diff --git a/letta/server/rest_api/routers/v1/providers.py b/letta/server/rest_api/routers/v1/providers.py index 031ce128..39cdd004 100644 --- a/letta/server/rest_api/routers/v1/providers.py +++ b/letta/server/rest_api/routers/v1/providers.py @@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse from letta.schemas.enums import ProviderType from letta.schemas.providers import Provider, ProviderBase, ProviderCheck, ProviderCreate, ProviderUpdate from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server -from letta.validators import PATH_VALIDATORS +from letta.validators import ProviderId if TYPE_CHECKING: from letta.server.server import SyncServer @@ -46,7 +46,7 @@ async def list_providers( @router.get("/{provider_id}", response_model=Provider, operation_id="retrieve_provider") async def retrieve_provider( - provider_id: str = PATH_VALIDATORS[ProviderBase.__id_prefix__], + provider_id: ProviderId, headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -80,8 +80,8 @@ async def create_provider( @router.patch("/{provider_id}", response_model=Provider, operation_id="modify_provider") async def modify_provider( + provider_id: ProviderId, request: ProviderUpdate = Body(...), - provider_id: str = PATH_VALIDATORS[ProviderBase.__id_prefix__], headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -111,7 +111,7 @@ async def check_provider( @router.post("/{provider_id}/check", response_model=None, operation_id="check_existing_provider") async def check_existing_provider( - provider_id: str = PATH_VALIDATORS[ProviderBase.__id_prefix__], + provider_id: ProviderId, headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -136,7 +136,7 @@ async def check_existing_provider( @router.delete("/{provider_id}", response_model=None, operation_id="delete_provider") async def delete_provider( - provider_id: str = PATH_VALIDATORS[ProviderBase.__id_prefix__], + provider_id: ProviderId, headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): diff --git a/letta/server/rest_api/routers/v1/sandbox_configs.py b/letta/server/rest_api/routers/v1/sandbox_configs.py index c4d100d9..5e51fa33 100644 --- a/letta/server/rest_api/routers/v1/sandbox_configs.py +++ b/letta/server/rest_api/routers/v1/sandbox_configs.py @@ -22,7 +22,7 @@ from letta.schemas.sandbox_config import ( from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer from letta.services.helpers.tool_execution_helper import create_venv_for_local_sandbox, install_pip_requirements_for_sandbox -from letta.validators import PATH_VALIDATORS +from letta.validators import SandboxConfigId router = APIRouter(prefix="/sandbox-config", tags=["sandbox-config"]) @@ -90,7 +90,7 @@ async def create_custom_local_sandbox_config( @router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig) async def update_sandbox_config( config_update: SandboxConfigUpdate, - sandbox_config_id: str = PATH_VALIDATORS[SandboxConfigBase.__id_prefix__], + sandbox_config_id: SandboxConfigId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -100,7 +100,7 @@ async def update_sandbox_config( @router.delete("/{sandbox_config_id}", status_code=204) async def delete_sandbox_config( - sandbox_config_id: str = PATH_VALIDATORS[SandboxConfigBase.__id_prefix__], + sandbox_config_id: SandboxConfigId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -160,7 +160,7 @@ async def force_recreate_local_sandbox_venv( @router.post("/{sandbox_config_id}/environment-variable", response_model=PydanticEnvVar) async def create_sandbox_env_var( env_var_create: SandboxEnvironmentVariableCreate, - sandbox_config_id: str = PATH_VALIDATORS[SandboxConfigBase.__id_prefix__], + sandbox_config_id: SandboxConfigId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -191,7 +191,7 @@ async def delete_sandbox_env_var( @router.get("/{sandbox_config_id}/environment-variable", response_model=List[PydanticEnvVar]) async def list_sandbox_env_vars( - sandbox_config_id: str = PATH_VALIDATORS[SandboxConfigBase.__id_prefix__], + sandbox_config_id: SandboxConfigId, limit: int = Query(1000, description="Number of results to return"), after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"), server: SyncServer = Depends(get_letta_server), diff --git a/letta/server/rest_api/routers/v1/sources.py b/letta/server/rest_api/routers/v1/sources.py index 771285a3..d723e355 100644 --- a/letta/server/rest_api/routers/v1/sources.py +++ b/letta/server/rest_api/routers/v1/sources.py @@ -36,7 +36,7 @@ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFil from letta.services.file_processor.parser.mistral_parser import MistralFileParser from letta.settings import settings from letta.utils import safe_create_file_processing_task, safe_create_task, sanitize_filename -from letta.validators import PATH_VALIDATORS +from letta.validators import FileId, SourceId logger = get_logger(__name__) @@ -60,7 +60,7 @@ async def count_sources( @router.get("/{source_id}", response_model=Source, operation_id="retrieve_source", deprecated=True) async def retrieve_source( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], + source_id: SourceId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -158,7 +158,7 @@ async def create_source( @router.patch("/{source_id}", response_model=Source, operation_id="modify_source", deprecated=True) async def modify_source( source: SourceUpdate, - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], + source_id: SourceId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -173,7 +173,7 @@ async def modify_source( @router.delete("/{source_id}", response_model=None, operation_id="delete_source", deprecated=True) async def delete_source( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], + source_id: SourceId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -209,7 +209,7 @@ async def delete_source( @router.post("/{source_id}/upload", response_model=FileMetadata, operation_id="upload_file_to_source", deprecated=True) async def upload_file_to_source( file: UploadFile, - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], + source_id: SourceId, duplicate_handling: DuplicateFileHandling = Query(DuplicateFileHandling.SUFFIX, description="How to handle duplicate filenames"), name: Optional[str] = Query(None, description="Optional custom name to override the uploaded file's name"), server: "SyncServer" = Depends(get_letta_server), @@ -324,7 +324,7 @@ async def upload_file_to_source( @router.get("/{source_id}/agents", response_model=List[str], operation_id="get_agents_for_source", deprecated=True) async def get_agents_for_source( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], + source_id: SourceId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -337,7 +337,7 @@ async def get_agents_for_source( @router.get("/{source_id}/passages", response_model=List[Passage], operation_id="list_source_passages", deprecated=True) async def list_source_passages( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], + source_id: SourceId, after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."), before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."), limit: int = Query(100, description="Maximum number of messages to retrieve."), @@ -359,7 +359,7 @@ async def list_source_passages( @router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_source_files", deprecated=True) async def list_source_files( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], + source_id: SourceId, limit: int = Query(1000, description="Number of files to return"), after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"), include_content: bool = Query(False, description="Whether to include full file content"), @@ -387,8 +387,8 @@ async def list_source_files( @router.get("/{source_id}/files/{file_id}", response_model=FileMetadata, operation_id="get_file_metadata", deprecated=True) async def get_file_metadata( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], - file_id: str = PATH_VALIDATORS[FileMetadataBase.__id_prefix__], + source_id: SourceId, + file_id: FileId, include_content: bool = Query(False, description="Whether to include full file content"), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -413,8 +413,8 @@ async def get_file_metadata( # it's still good practice to return a status indicating the success or failure of the deletion @router.delete("/{source_id}/{file_id}", status_code=204, operation_id="delete_file_from_source", deprecated=True) async def delete_file_from_source( - source_id: str = PATH_VALIDATORS[BaseSource.__id_prefix__], - file_id: str = PATH_VALIDATORS[FileMetadataBase.__id_prefix__], + source_id: SourceId, + file_id: FileId, server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): diff --git a/letta/server/rest_api/routers/v1/steps.py b/letta/server/rest_api/routers/v1/steps.py index 310a061e..0b28a949 100644 --- a/letta/server/rest_api/routers/v1/steps.py +++ b/letta/server/rest_api/routers/v1/steps.py @@ -13,7 +13,7 @@ from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_le from letta.server.server import SyncServer from letta.services.step_manager import FeedbackType from letta.settings import settings -from letta.validators import PATH_VALIDATORS +from letta.validators import StepId router = APIRouter(prefix="/steps", tags=["steps"]) @@ -70,7 +70,7 @@ async def list_steps( @router.get("/{step_id}", response_model=Step, operation_id="retrieve_step") async def retrieve_step( - step_id: str = PATH_VALIDATORS[StepBase.__id_prefix__], + step_id: StepId, headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): @@ -83,7 +83,7 @@ async def retrieve_step( @router.get("/{step_id}/metrics", response_model=StepMetrics, operation_id="retrieve_metrics_for_step") async def retrieve_metrics_for_step( - step_id: str = PATH_VALIDATORS[StepBase.__id_prefix__], + step_id: StepId, headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): @@ -96,7 +96,7 @@ async def retrieve_metrics_for_step( @router.get("/{step_id}/trace", response_model=Optional[ProviderTrace], operation_id="retrieve_trace_for_step") async def retrieve_trace_for_step( - step_id: str = PATH_VALIDATORS[StepBase.__id_prefix__], + step_id: StepId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -119,8 +119,8 @@ class ModifyFeedbackRequest(BaseModel): @router.patch("/{step_id}/feedback", response_model=Step, operation_id="modify_feedback_for_step") async def modify_feedback_for_step( + step_id: StepId, request: ModifyFeedbackRequest = Body(...), - step_id: str = PATH_VALIDATORS[StepBase.__id_prefix__], headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): @@ -133,7 +133,7 @@ async def modify_feedback_for_step( @router.get("/{step_id}/messages", response_model=List[LettaMessageUnion], operation_id="list_messages_for_step") async def list_messages_for_step( - step_id: str = PATH_VALIDATORS[StepBase.__id_prefix__], + step_id: StepId, headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), before: Optional[str] = Query( @@ -161,7 +161,7 @@ async def list_messages_for_step( @router.patch("/{step_id}/transaction/{transaction_id}", response_model=Step, operation_id="update_step_transaction_id") async def update_step_transaction_id( transaction_id: str, - step_id: str = PATH_VALIDATORS[StepBase.__id_prefix__], + step_id: StepId, headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): diff --git a/letta/server/rest_api/routers/v1/tools.py b/letta/server/rest_api/routers/v1/tools.py index 91d68c66..937fe24a 100644 --- a/letta/server/rest_api/routers/v1/tools.py +++ b/letta/server/rest_api/routers/v1/tools.py @@ -38,7 +38,7 @@ from letta.services.mcp.oauth_utils import MCPOAuthSession, drill_down_exception from letta.services.mcp.stdio_client import AsyncStdioMCPClient from letta.services.mcp.types import OauthStreamEvent from letta.settings import tool_settings -from letta.validators import PATH_VALIDATORS +from letta.validators import ToolId router = APIRouter(prefix="/tools", tags=["tools"]) @@ -47,7 +47,7 @@ logger = get_logger(__name__) @router.delete("/{tool_id}", operation_id="delete_tool") async def delete_tool( - tool_id: str = PATH_VALIDATORS[BaseTool.__id_prefix__], + tool_id: ToolId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -150,7 +150,7 @@ async def count_tools( @router.get("/{tool_id}", response_model=Tool, operation_id="retrieve_tool") async def retrieve_tool( - tool_id: str = PATH_VALIDATORS[BaseTool.__id_prefix__], + tool_id: ToolId, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -298,8 +298,8 @@ async def upsert_tool( @router.patch("/{tool_id}", response_model=Tool, operation_id="modify_tool") async def modify_tool( + tool_id: ToolId, request: ToolUpdate = Body(...), - tool_id: str = PATH_VALIDATORS[BaseTool.__id_prefix__], server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): diff --git a/letta/validators.py b/letta/validators.py index 1c927e68..5f297946 100644 --- a/letta/validators.py +++ b/letta/validators.py @@ -1,4 +1,5 @@ import re +from typing import Annotated from fastapi import Path @@ -44,17 +45,50 @@ PRIMITIVE_ID_PATTERNS = { for primitive in primitives } -PATH_VALIDATORS = {} -for primitive in primitives: - PATH_VALIDATORS[primitive] = Path( - description=f"The ID of the {primitive} in the format '{primitive}-'", - pattern=PRIMITIVE_ID_PATTERNS[primitive].pattern, - examples=[f"{primitive}-123e4567-e89b-42d3-8456-426614174000"], - # len(agent) + len("-") + len(uuid4) - min_length=len(primitive) + 1 + 36, - max_length=len(primitive) + 1 + 36, - ) + +def _create_path_validator_factory(primitive: str): + """ + Creates a factory function that returns a fresh Path validator. + + This avoids shared state issues when the same validator is used + across multiple endpoints with different parameter names. + """ + + def factory(): + return Path( + description=f"The ID of the {primitive} in the format '{primitive}-'", + pattern=PRIMITIVE_ID_PATTERNS[primitive].pattern, + examples=[f"{primitive}-123e4567-e89b-42d3-8456-426614174000"], + min_length=len(primitive) + 1 + 36, + max_length=len(primitive) + 1 + 36, + ) + + return factory + + +# PATH_VALIDATORS now contains factory functions, not Path objects +# Usage: folder_id: str = PATH_VALIDATORS[BaseFolder.__id_prefix__]() +PATH_VALIDATORS = {primitive: _create_path_validator_factory(primitive) for primitive in primitives} def is_valid_id(primitive: str, id: str) -> bool: return PRIMITIVE_ID_PATTERNS[primitive].match(id) is not None + + +# Type aliases for common ID types +# These can be used directly in route handler signatures for cleaner code +AgentId = Annotated[str, PATH_VALIDATORS[AgentState.__id_prefix__]()] +ToolId = Annotated[str, PATH_VALIDATORS[BaseTool.__id_prefix__]()] +SourceId = Annotated[str, PATH_VALIDATORS[BaseSource.__id_prefix__]()] +BlockId = Annotated[str, PATH_VALIDATORS[BaseBlock.__id_prefix__]()] +MessageId = Annotated[str, PATH_VALIDATORS[BaseMessage.__id_prefix__]()] +RunId = Annotated[str, PATH_VALIDATORS[RunBase.__id_prefix__]()] +JobId = Annotated[str, PATH_VALIDATORS[JobBase.__id_prefix__]()] +GroupId = Annotated[str, PATH_VALIDATORS[GroupBase.__id_prefix__]()] +FileId = Annotated[str, PATH_VALIDATORS[FileMetadataBase.__id_prefix__]()] +FolderId = Annotated[str, PATH_VALIDATORS[BaseFolder.__id_prefix__]()] +ArchiveId = Annotated[str, PATH_VALIDATORS[ArchiveBase.__id_prefix__]()] +ProviderId = Annotated[str, PATH_VALIDATORS[ProviderBase.__id_prefix__]()] +SandboxConfigId = Annotated[str, PATH_VALIDATORS[SandboxConfigBase.__id_prefix__]()] +StepId = Annotated[str, PATH_VALIDATORS[StepBase.__id_prefix__]()] +IdentityId = Annotated[str, PATH_VALIDATORS[IdentityBase.__id_prefix__]()]