feat(agents): Add Create/Delete Agent functionality

Backend:
- Added POST /city/agents endpoint for creating agents
- Added DELETE /city/agents/{id} endpoint for soft-deleting agents
- Added CreateAgentRequest, CreateAgentResponse, DeleteAgentResponse models

Frontend:
- Added '+ Новий агент' button on /agents page
- Created /agents/new page with full agent creation form
- Added 'Видалити агента' button in agent Identity tab (Danger Zone)

Features:
- Auto-generate slug from display_name
- Support for all agent fields: kind, role, model, node, district, microdao
- Color picker for agent color
- Visibility toggles (is_public, is_orchestrator)
- Soft delete with confirmation dialog
This commit is contained in:
Apple
2025-12-01 09:29:42 -08:00
parent 649d07ee29
commit 6cd8148872
5 changed files with 584 additions and 13 deletions

View File

@@ -579,6 +579,48 @@ class MicrodaoOption(BaseModel):
is_active: bool = True
# =============================================================================
# Agent Management (Create/Delete)
# =============================================================================
class CreateAgentRequest(BaseModel):
"""Request to create a new agent"""
slug: str
display_name: str
kind: str = "assistant" # assistant, orchestrator, specialist, civic
role: Optional[str] = None
model: Optional[str] = None
node_id: Optional[str] = None
home_node_id: Optional[str] = None
home_microdao_id: Optional[str] = None
district: Optional[str] = None
primary_room_slug: Optional[str] = None
avatar_url: Optional[str] = None
color_hint: Optional[str] = None
is_public: bool = False
is_orchestrator: bool = False
priority: str = "medium"
class CreateAgentResponse(BaseModel):
"""Response after creating an agent"""
id: str
slug: str
display_name: str
kind: str
node_id: Optional[str] = None
home_microdao_id: Optional[str] = None
district: Optional[str] = None
created_at: datetime
class DeleteAgentResponse(BaseModel):
"""Response after deleting an agent"""
ok: bool
message: str
agent_id: str
# =============================================================================
# Visibility Updates (Task 029)
# =============================================================================

View File

@@ -47,7 +47,10 @@ from models_city import (
MicrodaoRoomUpdate,
AttachExistingRoomRequest,
SwapperModel,
NodeSwapperDetail
NodeSwapperDetail,
CreateAgentRequest,
CreateAgentResponse,
DeleteAgentResponse
)
import repo_city
from common.redis_client import PresenceRedis, get_redis
@@ -2535,6 +2538,120 @@ async def get_agents():
raise HTTPException(status_code=500, detail="Failed to get agents")
@router.post("/agents", response_model=CreateAgentResponse)
async def create_agent(body: CreateAgentRequest):
"""
Створити нового агента
"""
try:
pool = await repo_city.get_pool()
# Check if slug already exists
existing = await pool.fetchrow(
"SELECT id FROM agents WHERE id = $1 OR slug = $1",
body.slug
)
if existing:
raise HTTPException(status_code=400, detail=f"Agent with slug '{body.slug}' already exists")
# Generate ID from slug
agent_id = body.slug
# Insert agent
row = await pool.fetchrow("""
INSERT INTO agents (
id, slug, display_name, kind, role, model,
node_id, home_node_id, home_microdao_id, district,
primary_room_slug, avatar_url, color_hint,
is_public, is_orchestrator, priority,
created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10,
$11, $12, $13,
$14, $15, $16,
NOW(), NOW()
)
RETURNING id, slug, display_name, kind, node_id, home_microdao_id, district, created_at
""",
agent_id,
body.slug,
body.display_name,
body.kind,
body.role,
body.model,
body.node_id,
body.home_node_id or body.node_id,
body.home_microdao_id,
body.district,
body.primary_room_slug,
body.avatar_url,
body.color_hint,
body.is_public,
body.is_orchestrator,
body.priority
)
logger.info(f"Created agent: {agent_id}")
return CreateAgentResponse(
id=row["id"],
slug=row["slug"],
display_name=row["display_name"],
kind=row["kind"],
node_id=row["node_id"],
home_microdao_id=row["home_microdao_id"],
district=row["district"],
created_at=row["created_at"]
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to create agent: {e}")
raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}")
@router.delete("/agents/{agent_id}", response_model=DeleteAgentResponse)
async def delete_agent(agent_id: str):
"""
Видалити агента (soft delete - встановлює is_archived=true, deleted_at=now())
"""
try:
pool = await repo_city.get_pool()
# Check if agent exists
existing = await pool.fetchrow(
"SELECT id, display_name FROM agents WHERE id = $1 AND deleted_at IS NULL",
agent_id
)
if not existing:
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
# Soft delete
await pool.execute("""
UPDATE agents
SET is_archived = true,
deleted_at = NOW(),
updated_at = NOW()
WHERE id = $1
""", agent_id)
logger.info(f"Deleted (archived) agent: {agent_id}")
return DeleteAgentResponse(
ok=True,
message=f"Agent '{existing['display_name']}' has been archived",
agent_id=agent_id
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete agent: {e}")
raise HTTPException(status_code=500, detail=f"Failed to delete agent: {str(e)}")
@router.get("/agents/online", response_model=List[AgentPresence])
async def get_online_agents():
"""