feat: add pagination to agent files endpoint (#5086)

* feat: add pagination to agent files endpoint
This commit is contained in:
cthomas
2025-10-06 15:24:02 -07:00
committed by Caren Thomas
parent 416af626cc
commit 757bbcac37
3 changed files with 133 additions and 24 deletions

View File

@@ -4961,6 +4961,87 @@
"title": "Agent Id"
}
},
{
"name": "before",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "File ID cursor for pagination. Returns files that come before this file ID in the specified sort order",
"title": "Before"
},
"description": "File ID cursor for pagination. Returns files that come before this file ID in the specified sort order"
},
{
"name": "after",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "File ID cursor for pagination. Returns files that come after this file ID in the specified sort order",
"title": "After"
},
"description": "File ID cursor for pagination. Returns files that come after this file ID in the specified sort order"
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Maximum number of files to return",
"default": 100,
"title": "Limit"
},
"description": "Maximum number of files to return"
},
{
"name": "order",
"in": "query",
"required": false,
"schema": {
"enum": ["asc", "desc"],
"type": "string",
"description": "Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first",
"default": "desc",
"title": "Order"
},
"description": "Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first"
},
{
"name": "order_by",
"in": "query",
"required": false,
"schema": {
"const": "created_at",
"type": "string",
"description": "Field to sort by",
"default": "created_at",
"title": "Order By"
},
"description": "Field to sort by"
},
{
"name": "cursor",
"in": "query",
@@ -4974,24 +5055,12 @@
"type": "null"
}
],
"description": "Pagination cursor from previous response",
"description": "Pagination cursor from previous response (deprecated, use before/after)",
"deprecated": true,
"title": "Cursor"
},
"description": "Pagination cursor from previous response"
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"maximum": 100,
"minimum": 1,
"description": "Number of items to return (1-100)",
"default": 20,
"title": "Limit"
},
"description": "Number of items to return (1-100)"
"description": "Pagination cursor from previous response (deprecated, use before/after)",
"deprecated": true
},
{
"name": "is_open",

View File

@@ -794,8 +794,20 @@ async def list_agent_folders(
@router.get("/{agent_id}/files", response_model=PaginatedAgentFiles, operation_id="list_agent_files")
async def list_agent_files(
agent_id: str,
cursor: Optional[str] = Query(None, description="Pagination cursor from previous response"),
limit: int = Query(20, ge=1, le=100, description="Number of items to return (1-100)"),
before: Optional[str] = Query(
None, description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order"
),
after: Optional[str] = Query(
None, description="File ID cursor for pagination. Returns files that come after this file ID in the specified sort order"
),
limit: Optional[int] = Query(100, description="Maximum number of files to return"),
order: Literal["asc", "desc"] = Query(
"desc", description="Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first"
),
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
cursor: Optional[str] = Query(
None, description="Pagination cursor from previous response (deprecated, use before/after)", deprecated=True
),
is_open: Optional[bool] = Query(None, description="Filter by open status (true for open files, false for closed files)"),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -805,9 +817,18 @@ async def list_agent_files(
"""
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
effective_limit = limit or 20
# get paginated file-agent relationships for this agent
file_agents, next_cursor, has_more = await server.file_agent_manager.list_files_for_agent_paginated(
agent_id=agent_id, actor=actor, cursor=cursor, limit=limit, is_open=is_open
agent_id=agent_id,
actor=actor,
cursor=cursor, # keep for backwards compatibility
limit=effective_limit,
is_open=is_open,
before=before,
after=after,
ascending=(order == "asc"),
)
# enrich with file and source metadata

View File

@@ -300,6 +300,9 @@ class FileAgentManager:
cursor: Optional[str] = None,
limit: int = 20,
is_open: Optional[bool] = None,
before: Optional[str] = None,
after: Optional[str] = None,
ascending: bool = False,
) -> tuple[List[PydanticFileAgent], Optional[str], bool]:
"""
Return paginated file associations for an agent.
@@ -307,9 +310,12 @@ class FileAgentManager:
Args:
agent_id: The agent ID to get files for
actor: User performing the action
cursor: Pagination cursor (file-agent ID to start after)
cursor: Pagination cursor (file-agent ID to start after) - deprecated, use before/after
limit: Maximum number of results to return
is_open: Optional filter for open/closed status (None = all, True = open only, False = closed only)
before: File-agent ID cursor for pagination. Returns files that come before this ID in the specified sort order
after: File-agent ID cursor for pagination. Returns files that come after this ID in the specified sort order
ascending: Sort order (True = ascending by created_at/id, False = descending)
Returns:
Tuple of (file_agents, next_cursor, has_more)
@@ -325,14 +331,27 @@ class FileAgentManager:
if is_open is not None:
conditions.append(FileAgentModel.is_open == is_open)
# apply cursor if provided (get records after this ID)
if cursor:
# handle pagination cursors (support both old and new style)
if before:
conditions.append(FileAgentModel.id < before)
elif after:
conditions.append(FileAgentModel.id > after)
elif cursor:
# fallback to old cursor behavior for backwards compatibility
conditions.append(FileAgentModel.id > cursor)
query = select(FileAgentModel).where(and_(*conditions))
# order by ID for stable pagination
query = query.order_by(FileAgentModel.id)
# apply sorting based on pagination method
if before or after:
# For new cursor-based pagination, use created_at + id ordering
if ascending:
query = query.order_by(FileAgentModel.created_at.asc(), FileAgentModel.id.asc())
else:
query = query.order_by(FileAgentModel.created_at.desc(), FileAgentModel.id.desc())
else:
# For old cursor compatibility, maintain original behavior (ascending by ID)
query = query.order_by(FileAgentModel.id)
# fetch limit + 1 to check if there are more results
query = query.limit(limit + 1)