delete agent type (#66)

* delete agent type

* more filters

---------

Co-authored-by: Kartik Sarangmath <kartiksarangmath@Kartiks-MacBook-Air.local>
This commit is contained in:
ksarangmath
2025-08-11 00:25:35 -07:00
committed by GitHub
parent 23b37cc7d3
commit 804d458583
6 changed files with 151 additions and 27 deletions

View File

@@ -106,10 +106,16 @@ async def create_agent_instance(
):
"""Create a new instance of a user agent (trigger webhook if applicable)"""
# Get the user agent
# Get the user agent (excluding soft-deleted ones)
user_agent = (
db.query(UserAgent)
.filter(and_(UserAgent.id == agent_id, UserAgent.user_id == current_user.id))
.filter(
and_(
UserAgent.id == agent_id,
UserAgent.user_id == current_user.id,
UserAgent.is_deleted.is_(False),
)
)
.first()
)

View File

@@ -61,11 +61,11 @@ def _format_instance(instance: AgentInstance) -> AgentInstanceResponse:
def get_all_agent_types_with_instances(
db: Session, user_id: UUID
) -> list[AgentTypeOverview]:
"""Get all user agents with their instances for a specific user - OPTIMIZED"""
# Get all user agents for this user with instances in a single query
"""Get all non-deleted user agents with their instances for a specific user - OPTIMIZED"""
# Get all non-deleted user agents for this user with instances in a single query
user_agents = (
db.query(UserAgent)
.filter(UserAgent.user_id == user_id)
.filter(UserAgent.user_id == user_id, UserAgent.is_deleted.is_(False))
.options(subqueryload(UserAgent.instances))
.all()
)
@@ -267,7 +267,9 @@ def get_agent_summary(db: Session, user_id: UUID) -> dict:
)
.join(AgentInstance, AgentInstance.user_agent_id == UserAgent.id)
.filter(
UserAgent.user_id == user_id, AgentInstance.status != AgentStatus.DELETED
UserAgent.user_id == user_id,
UserAgent.is_deleted.is_(False),
AgentInstance.status != AgentStatus.DELETED,
)
.group_by(UserAgent.id, UserAgent.name, AgentInstance.status)
.all()
@@ -304,7 +306,11 @@ def get_agent_type_instances(
user_agent = (
db.query(UserAgent)
.filter(UserAgent.id == agent_type_id, UserAgent.user_id == user_id)
.filter(
UserAgent.id == agent_type_id,
UserAgent.user_id == user_id,
UserAgent.is_deleted.is_(False),
)
.first()
)
if not user_agent:

View File

@@ -29,10 +29,16 @@ def create_user_agent(
) -> dict | None:
"""Create a new user agent configuration"""
# Check if agent with same name already exists for this user
# Check if non-deleted agent with same name already exists for this user
existing = (
db.query(UserAgent)
.filter(and_(UserAgent.user_id == user_id, UserAgent.name == request.name))
.filter(
and_(
UserAgent.user_id == user_id,
UserAgent.name == request.name,
UserAgent.is_deleted.is_(False),
)
)
.first()
)
@@ -55,9 +61,13 @@ def create_user_agent(
def get_user_agents(db: Session, user_id: UUID) -> list[dict]:
"""Get all user agents for a specific user"""
"""Get all non-deleted user agents for a specific user"""
user_agents = db.query(UserAgent).filter(UserAgent.user_id == user_id).all()
user_agents = (
db.query(UserAgent)
.filter(and_(UserAgent.user_id == user_id, UserAgent.is_deleted.is_(False)))
.all()
)
return [_format_user_agent(agent, db) for agent in user_agents]
@@ -69,7 +79,13 @@ def update_user_agent(
user_agent = (
db.query(UserAgent)
.filter(and_(UserAgent.id == agent_id, UserAgent.user_id == user_id))
.filter(
and_(
UserAgent.id == agent_id,
UserAgent.user_id == user_id,
UserAgent.is_deleted.is_(False),
)
)
.first()
)
@@ -266,10 +282,16 @@ async def trigger_webhook_agent(
def get_user_agent_instances(db: Session, agent_id: UUID, user_id: UUID) -> list | None:
"""Get all instances for a specific user agent"""
# Verify the user agent exists and belongs to the user
# Verify the user agent exists, belongs to the user, and is not deleted
user_agent = (
db.query(UserAgent)
.filter(and_(UserAgent.id == agent_id, UserAgent.user_id == user_id))
.filter(
and_(
UserAgent.id == agent_id,
UserAgent.user_id == user_id,
UserAgent.is_deleted.is_(False),
)
)
.first()
)
@@ -293,12 +315,18 @@ def get_user_agent_instances(db: Session, agent_id: UUID, user_id: UUID) -> list
def delete_user_agent(db: Session, agent_id: UUID, user_id: UUID) -> bool:
"""Delete a user agent and all its associated instances and related data"""
"""Soft delete a user agent and mark its instances as deleted, while removing messages"""
# First verify the user agent exists and belongs to the user
# First verify the user agent exists, belongs to the user, and is not already deleted
user_agent = (
db.query(UserAgent)
.filter(and_(UserAgent.id == agent_id, UserAgent.user_id == user_id))
.filter(
and_(
UserAgent.id == agent_id,
UserAgent.user_id == user_id,
UserAgent.is_deleted.is_(False),
)
)
.first()
)
@@ -310,16 +338,19 @@ def delete_user_agent(db: Session, agent_id: UUID, user_id: UUID) -> bool:
db.query(AgentInstance).filter(AgentInstance.user_agent_id == agent_id).all()
)
# For each agent instance, delete all related data
# For each agent instance, delete all messages (for privacy/storage)
for instance in agent_instances:
# Delete messages
db.query(Message).filter(Message.agent_instance_id == instance.id).delete()
# Delete all agent instances
db.query(AgentInstance).filter(AgentInstance.user_agent_id == agent_id).delete()
# Mark all agent instances as DELETED
db.query(AgentInstance).filter(AgentInstance.user_agent_id == agent_id).update(
{"status": AgentStatus.DELETED}
)
# Soft delete the user agent
user_agent.is_deleted = True
user_agent.updated_at = datetime.now(timezone.utc)
# Delete the user agent
db.delete(user_agent)
db.commit()
return True

View File

@@ -20,13 +20,18 @@ logger = logging.getLogger(__name__)
def create_or_get_user_agent(db: Session, name: str, user_id: str) -> UserAgent:
"""Create or get a user agent by name for a specific user"""
"""Create or get a non-deleted user agent by name for a specific user"""
# Normalize name to lowercase for consistent storage
normalized_name = name.lower()
# Only look for non-deleted user agents
user_agent = (
db.query(UserAgent)
.filter(UserAgent.name == normalized_name, UserAgent.user_id == UUID(user_id))
.filter(
UserAgent.name == normalized_name,
UserAgent.user_id == UUID(user_id),
UserAgent.is_deleted.is_(False),
)
.first()
)
if not user_agent:
@@ -34,6 +39,7 @@ def create_or_get_user_agent(db: Session, name: str, user_id: str) -> UserAgent:
name=normalized_name,
user_id=UUID(user_id),
is_active=True,
is_deleted=False, # Explicitly set to False for new agents
)
db.add(user_agent)
db.flush() # Flush to get the user_agent ID

View File

@@ -0,0 +1,67 @@
"""Add soft delete to user agents
Revision ID: 9f61865b8ba8
Revises: 2e2f1b18e835
Create Date: 2025-08-10 22:22:28.432623
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "9f61865b8ba8"
down_revision: Union[str, None] = "2e2f1b18e835"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Add column as nullable first with server default
op.add_column(
"user_agents",
sa.Column(
"is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=True
),
)
# Set default value for existing rows (in case any are NULL)
op.execute("UPDATE user_agents SET is_deleted = FALSE WHERE is_deleted IS NULL")
# Now make the column NOT NULL
op.alter_column(
"user_agents", "is_deleted", nullable=False, server_default=sa.text("FALSE")
)
op.drop_constraint(
op.f("uq_user_agents_user_id_name"), "user_agents", type_="unique"
)
op.create_index(
"uq_user_agents_user_id_name",
"user_agents",
["user_id", "name"],
unique=True,
postgresql_where=sa.text("is_deleted = FALSE"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
"uq_user_agents_user_id_name",
table_name="user_agents",
postgresql_where=sa.text("is_deleted = FALSE"),
)
op.create_unique_constraint(
op.f("uq_user_agents_user_id_name"),
"user_agents",
["user_id", "name"],
postgresql_nulls_not_distinct=False,
)
op.drop_column("user_agents", "is_deleted")
# ### end Alembic commands ###

View File

@@ -2,7 +2,7 @@ from datetime import datetime, timezone
from uuid import UUID, uuid4
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Index, String, Text, UniqueConstraint
from sqlalchemy import ForeignKey, Index, String, Text, text
from sqlalchemy.dialects.postgresql import UUID as PostgresUUID, JSONB
from sqlalchemy.orm import (
DeclarativeBase, # type: ignore[attr-defined]
@@ -77,7 +77,14 @@ class User(Base):
class UserAgent(Base):
__tablename__ = "user_agents"
__table_args__ = (
UniqueConstraint("user_id", "name", name="uq_user_agents_user_id_name"),
# Partial unique index: only enforce uniqueness for non-deleted agents
Index(
"uq_user_agents_user_id_name",
"user_id",
"name",
unique=True,
postgresql_where=text("is_deleted = FALSE"),
),
Index("ix_user_agents_user_id", "user_id"),
)
@@ -91,6 +98,7 @@ class UserAgent(Base):
webhook_url: Mapped[str | None] = mapped_column(Text, default=None)
webhook_api_key: Mapped[str | None] = mapped_column(Text, default=None) # Encrypted
is_active: Mapped[bool] = mapped_column(default=True)
is_deleted: Mapped[bool] = mapped_column(default=False) # Soft delete flag
created_at: Mapped[datetime] = mapped_column(
default=lambda: datetime.now(timezone.utc)
)