feat: add the ability to find/delete by deployment id [PRO-1150] (#4421)

* feat: add the ability to find/delete by deployment id

* fix: add ability to delete by deployment id

---------

Co-authored-by: Shubham Naik <shub@memgpt.ai>
This commit is contained in:
Shubham Naik
2025-09-04 11:08:54 -07:00
committed by GitHub
parent eb21668f35
commit f6ec6dc3a0

View File

@@ -1,6 +1,7 @@
from typing import Optional
from typing import List, Optional
from fastapi import APIRouter, Body, Depends, Header, HTTPException
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
@@ -57,3 +58,216 @@ async def create_block(
return await server.block_manager.create_or_update_block_async(block, 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))