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
This commit is contained in:
Kian Jones
2025-10-21 13:37:00 -07:00
committed by Caren Thomas
parent c8e8cad507
commit bbaaabb6e1
14 changed files with 227 additions and 148 deletions

View File

@@ -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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
}
],
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
}
],
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
}
],
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
},
{
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
},
{
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
},
{
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
},
{
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Folder Id"
}
},
"description": "The ID of the source in the format 'source-<uuid4>'"
},
{
"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-<uuid4>'",
"examples": ["file-123e4567-e89b-42d3-8456-426614174000"],
"title": "File Id"
}
},
"description": "The ID of the file in the format 'file-<uuid4>'"
}
],
"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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Source Id"
"title": "Folder Id"
},
"description": "The ID of the source in the format 'source-<uuid4>'"
},
@@ -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-<uuid4>'",
"examples": ["source-123e4567-e89b-42d3-8456-426614174000"],
"title": "Source Id"
"title": "Folder Id"
},
"description": "The ID of the source in the format 'source-<uuid4>'"
},

View File

@@ -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),

View File

@@ -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),
):

View File

@@ -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",

View File

@@ -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),
):

View File

@@ -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),
):

View File

@@ -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",

View File

@@ -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),
):

View File

@@ -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),
):

View File

@@ -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),

View File

@@ -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),
):

View File

@@ -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),
):

View File

@@ -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),
):

View File

@@ -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}-<uuid4>'",
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}-<uuid4>'",
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__]()]