from typing import List, Optional from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query from pydantic import BaseModel from letta.schemas.agent import AgentState, InternalTemplateAgentCreate from letta.schemas.block import Block, InternalTemplateBlockCreate from letta.schemas.group import Group, InternalTemplateGroupCreate from letta.server.rest_api.utils import get_letta_server from letta.server.server import SyncServer router = APIRouter(prefix="/_internal_templates", tags=["_internal_templates"]) @router.post("/groups", response_model=Group, operation_id="create_internal_template_group") async def create_group( group: InternalTemplateGroupCreate = Body(...), server: "SyncServer" = Depends(get_letta_server), actor_id: Optional[str] = Header(None, alias="user_id"), ): """ Create a new multi-agent group with the specified configuration. """ try: actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id) return await server.group_manager.create_group_async(group, actor=actor) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/agents", response_model=AgentState, operation_id="create_internal_template_agent") async def create_agent( agent: InternalTemplateAgentCreate = Body(...), server: "SyncServer" = Depends(get_letta_server), actor_id: Optional[str] = Header(None, alias="user_id"), ): """ Create a new agent with template-related fields. """ try: actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id) return await server.agent_manager.create_agent_async(agent, actor=actor) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/blocks", response_model=Block, operation_id="create_internal_template_block") async def create_block( block: InternalTemplateBlockCreate = Body(...), server: "SyncServer" = Depends(get_letta_server), actor_id: Optional[str] = Header(None, alias="user_id"), ): """ Create a new block with template-related fields. """ try: actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id) block_obj = Block(**block.model_dump()) return await server.block_manager.create_or_update_block_async(block_obj, actor=actor) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) class DeploymentEntity(BaseModel): """A deployment entity.""" id: str type: str name: Optional[str] = None description: Optional[str] = None class ListDeploymentEntitiesResponse(BaseModel): """Response model for listing deployment entities.""" entities: List[DeploymentEntity] = [] total_count: int deployment_id: str message: str class DeleteDeploymentResponse(BaseModel): """Response model for delete deployment operation.""" deleted_blocks: List[str] = [] deleted_agents: List[str] = [] deleted_groups: List[str] = [] message: str @router.get("/deployment/{deployment_id}", response_model=ListDeploymentEntitiesResponse, operation_id="list_deployment_entities") async def list_deployment_entities( deployment_id: str, server: "SyncServer" = Depends(get_letta_server), actor_id: Optional[str] = Header(None, alias="user_id"), entity_types: Optional[List[str]] = Query(None, description="Filter by entity types (block, agent, group)"), ): """ List all entities (blocks, agents, groups) with the specified deployment_id. Optionally filter by entity types. """ try: actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id) entities = [] # Parse entity_types filter - support both array and comma-separated string allowed_types = {"block", "agent", "group"} if entity_types is None: # If no filter specified, include all types types_to_include = allowed_types else: # Handle comma-separated strings in a single item if len(entity_types) == 1 and "," in entity_types[0]: entity_types = [t.strip() for t in entity_types[0].split(",")] # Validate and filter types types_to_include = {t.lower() for t in entity_types if t.lower() in allowed_types} if not types_to_include: types_to_include = allowed_types # Default to all if invalid types provided # Query blocks if requested if "block" in types_to_include: from sqlalchemy import select from letta.orm.block import Block as BlockModel from letta.server.db import db_registry async with db_registry.async_session() as session: block_query = select(BlockModel).where( BlockModel.deployment_id == deployment_id, BlockModel.organization_id == actor.organization_id ) result = await session.execute(block_query) blocks = result.scalars().all() for block in blocks: entities.append( DeploymentEntity( id=block.id, type="block", name=getattr(block, "template_name", None) or getattr(block, "label", None), description=block.description, ) ) # Query agents if requested if "agent" in types_to_include: from letta.orm.agent import Agent as AgentModel async with db_registry.async_session() as session: agent_query = select(AgentModel).where( AgentModel.deployment_id == deployment_id, AgentModel.organization_id == actor.organization_id ) result = await session.execute(agent_query) agents = result.scalars().all() for agent in agents: entities.append(DeploymentEntity(id=agent.id, type="agent", name=agent.name, description=agent.description)) # Query groups if requested if "group" in types_to_include: from letta.orm.group import Group as GroupModel async with db_registry.async_session() as session: group_query = select(GroupModel).where( GroupModel.deployment_id == deployment_id, GroupModel.organization_id == actor.organization_id ) result = await session.execute(group_query) groups = result.scalars().all() for group in groups: entities.append( DeploymentEntity( id=group.id, type="group", name=None, # Groups don't have a name field description=group.description, ) ) message = f"Found {len(entities)} entities for deployment {deployment_id}" if entity_types: message += f" (filtered by types: {', '.join(types_to_include)})" return ListDeploymentEntitiesResponse(entities=entities, total_count=len(entities), deployment_id=deployment_id, message=message) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.delete("/deployment/{deployment_id}", response_model=DeleteDeploymentResponse, operation_id="delete_deployment") async def delete_deployment( deployment_id: str, server: "SyncServer" = Depends(get_letta_server), actor_id: Optional[str] = Header(None, alias="user_id"), ): """ Delete all entities (blocks, agents, groups) with the specified deployment_id. Deletion order: blocks -> agents -> groups to maintain referential integrity. """ try: actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id) deleted_blocks = [] deleted_agents = [] deleted_groups = [] # First delete blocks from sqlalchemy import select from letta.orm.block import Block as BlockModel from letta.server.db import db_registry async with db_registry.async_session() as session: # Get all blocks with the deployment_id block_query = select(BlockModel).where( BlockModel.deployment_id == deployment_id, BlockModel.organization_id == actor.organization_id ) result = await session.execute(block_query) blocks = result.scalars().all() for block in blocks: try: await server.block_manager.delete_block_async(block.id, actor) deleted_blocks.append(block.id) except Exception as e: # Continue deleting other blocks even if one fails print(f"Failed to delete block {block.id}: {e}") # Then delete agents from letta.orm.agent import Agent as AgentModel async with db_registry.async_session() as session: # Get all agents with the deployment_id agent_query = select(AgentModel).where( AgentModel.deployment_id == deployment_id, AgentModel.organization_id == actor.organization_id ) result = await session.execute(agent_query) agents = result.scalars().all() for agent in agents: try: await server.agent_manager.delete_agent_async(agent.id, actor) deleted_agents.append(agent.id) except Exception as e: # Continue deleting other agents even if one fails print(f"Failed to delete agent {agent.id}: {e}") # Finally delete groups from letta.orm.group import Group as GroupModel async with db_registry.async_session() as session: # Get all groups with the deployment_id group_query = select(GroupModel).where( GroupModel.deployment_id == deployment_id, GroupModel.organization_id == actor.organization_id ) result = await session.execute(group_query) groups = result.scalars().all() for group in groups: try: await server.group_manager.delete_group_async(group.id, actor) deleted_groups.append(group.id) except Exception as e: # Continue deleting other groups even if one fails print(f"Failed to delete group {group.id}: {e}") total_deleted = len(deleted_blocks) + len(deleted_agents) + len(deleted_groups) message = f"Successfully deleted {total_deleted} entities from deployment {deployment_id}" return DeleteDeploymentResponse( deleted_blocks=deleted_blocks, deleted_agents=deleted_agents, deleted_groups=deleted_groups, message=message ) except Exception as e: raise HTTPException(status_code=500, detail=str(e))