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:
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user