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)""" """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 = ( user_agent = (
db.query(UserAgent) 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() .first()
) )

View File

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

View File

@@ -29,10 +29,16 @@ def create_user_agent(
) -> dict | None: ) -> dict | None:
"""Create a new user agent configuration""" """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 = ( existing = (
db.query(UserAgent) 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() .first()
) )
@@ -55,9 +61,13 @@ def create_user_agent(
def get_user_agents(db: Session, user_id: UUID) -> list[dict]: 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] return [_format_user_agent(agent, db) for agent in user_agents]
@@ -69,7 +79,13 @@ def update_user_agent(
user_agent = ( user_agent = (
db.query(UserAgent) 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() .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: def get_user_agent_instances(db: Session, agent_id: UUID, user_id: UUID) -> list | None:
"""Get all instances for a specific user agent""" """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 = ( user_agent = (
db.query(UserAgent) 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() .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: 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 = ( user_agent = (
db.query(UserAgent) 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() .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() 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: for instance in agent_instances:
# Delete messages
db.query(Message).filter(Message.agent_instance_id == instance.id).delete() db.query(Message).filter(Message.agent_instance_id == instance.id).delete()
# Delete all agent instances # Mark all agent instances as DELETED
db.query(AgentInstance).filter(AgentInstance.user_agent_id == agent_id).delete() 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() db.commit()
return True 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: 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 # Normalize name to lowercase for consistent storage
normalized_name = name.lower() normalized_name = name.lower()
# Only look for non-deleted user agents
user_agent = ( user_agent = (
db.query(UserAgent) 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() .first()
) )
if not user_agent: 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, name=normalized_name,
user_id=UUID(user_id), user_id=UUID(user_id),
is_active=True, is_active=True,
is_deleted=False, # Explicitly set to False for new agents
) )
db.add(user_agent) db.add(user_agent)
db.flush() # Flush to get the user_agent ID 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 uuid import UUID, uuid4
from typing import TYPE_CHECKING 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.dialects.postgresql import UUID as PostgresUUID, JSONB
from sqlalchemy.orm import ( from sqlalchemy.orm import (
DeclarativeBase, # type: ignore[attr-defined] DeclarativeBase, # type: ignore[attr-defined]
@@ -77,7 +77,14 @@ class User(Base):
class UserAgent(Base): class UserAgent(Base):
__tablename__ = "user_agents" __tablename__ = "user_agents"
__table_args__ = ( __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"), 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_url: Mapped[str | None] = mapped_column(Text, default=None)
webhook_api_key: Mapped[str | None] = mapped_column(Text, default=None) # Encrypted webhook_api_key: Mapped[str | None] = mapped_column(Text, default=None) # Encrypted
is_active: Mapped[bool] = mapped_column(default=True) 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( created_at: Mapped[datetime] = mapped_column(
default=lambda: datetime.now(timezone.utc) default=lambda: datetime.now(timezone.utc)
) )