diff --git a/fern/openapi-overrides.yml b/fern/openapi-overrides.yml index 8431758d..f14d57c3 100644 --- a/fern/openapi-overrides.yml +++ b/fern/openapi-overrides.yml @@ -955,6 +955,18 @@ paths: - identities - properties x-fern-sdk-method-name: upsert + /v1/identities/{identity_id}/agents: + get: + x-fern-sdk-group-name: + - identities + - agents + x-fern-sdk-method-name: list + /v1/identities/{identity_id}/blocks: + get: + x-fern-sdk-group-name: + - identities + - blocks + x-fern-sdk-method-name: list /v1/groups/: get: x-fern-sdk-group-name: diff --git a/letta/server/rest_api/routers/v1/identities.py b/letta/server/rest_api/routers/v1/identities.py index 84264522..2a883304 100644 --- a/letta/server/rest_api/routers/v1/identities.py +++ b/letta/server/rest_api/routers/v1/identities.py @@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, List, Literal, Optional from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query from letta.orm.errors import NoResultFound, UniqueConstraintViolationError +from letta.schemas.agent import AgentState +from letta.schemas.block import Block from letta.schemas.identity import Identity, IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server @@ -192,3 +194,79 @@ async def delete_identity( raise HTTPException(status_code=404, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"{e}") + + +@router.get("/{identity_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_identity") +async def list_agents_for_identity( + identity_id: str, + before: Optional[str] = Query( + None, + description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + ), + after: Optional[str] = Query( + None, + description="Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + ), + limit: Optional[int] = Query(50, description="Maximum number of agents to return"), + order: Literal["asc", "desc"] = Query( + "desc", description="Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + ), + order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"), + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Get all agents associated with the specified identity. + """ + try: + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + return await server.identity_manager.list_agents_for_identity_async( + identity_id=identity_id, + before=before, + after=after, + limit=limit, + ascending=(order == "asc"), + actor=actor, + ) + except NoResultFound as e: + raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found") + except Exception as e: + raise HTTPException(status_code=500, detail=f"{e}") + + +@router.get("/{identity_id}/blocks", response_model=List[Block], operation_id="list_blocks_for_identity") +async def list_blocks_for_identity( + identity_id: str, + before: Optional[str] = Query( + None, + description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", + ), + after: Optional[str] = Query( + None, + description="Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order", + ), + limit: Optional[int] = Query(50, description="Maximum number of blocks to return"), + order: Literal["asc", "desc"] = Query( + "desc", description="Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first" + ), + order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"), + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Get all blocks associated with the specified identity. + """ + try: + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + return await server.identity_manager.list_blocks_for_identity_async( + identity_id=identity_id, + before=before, + after=after, + limit=limit, + ascending=(order == "asc"), + actor=actor, + ) + except NoResultFound as e: + raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found") + except Exception as e: + raise HTTPException(status_code=500, detail=f"{e}") diff --git a/letta/services/identity_manager.py b/letta/services/identity_manager.py index b2179b10..2a01263c 100644 --- a/letta/services/identity_manager.py +++ b/letta/services/identity_manager.py @@ -1,3 +1,4 @@ +import asyncio from typing import List, Optional from fastapi import HTTPException @@ -9,6 +10,8 @@ from letta.orm.block import Block as BlockModel from letta.orm.errors import UniqueConstraintViolationError from letta.orm.identity import Identity as IdentityModel from letta.otel.tracing import trace_method +from letta.schemas.agent import AgentState +from letta.schemas.block import Block from letta.schemas.identity import ( Identity as PydanticIdentity, IdentityCreate, @@ -274,3 +277,65 @@ class IdentityManager: current_ids = {item.id for item in current_relationship} new_items = [item for item in found_items if item.id not in current_ids] current_relationship.extend(new_items) + + @enforce_types + @trace_method + async def list_agents_for_identity_async( + self, + identity_id: str, + before: Optional[str] = None, + after: Optional[str] = None, + limit: Optional[int] = 50, + ascending: bool = False, + actor: PydanticUser = None, + ) -> List[AgentState]: + """ + Get all agents associated with the specified identity. + """ + async with db_registry.async_session() as session: + # First verify the identity exists and belongs to the user + identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor) + if identity is None: + raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found") + + # Get agents associated with this identity with pagination + agents = await AgentModel.list_async( + db_session=session, + before=before, + after=after, + limit=limit, + ascending=ascending, + identity_id=identity.id, + ) + return await asyncio.gather(*[agent.to_pydantic_async() for agent in agents]) + + @enforce_types + @trace_method + async def list_blocks_for_identity_async( + self, + identity_id: str, + before: Optional[str] = None, + after: Optional[str] = None, + limit: Optional[int] = 50, + ascending: bool = False, + actor: PydanticUser = None, + ) -> List[Block]: + """ + Get all blocks associated with the specified identity. + """ + async with db_registry.async_session() as session: + # First verify the identity exists and belongs to the user + identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor) + if identity is None: + raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found") + + # Get blocks associated with this identity with pagination + blocks = await BlockModel.list_async( + db_session=session, + before=before, + after=after, + limit=limit, + ascending=ascending, + identity_id=identity.id, + ) + return [block.to_pydantic() for block in blocks]