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:
@@ -471,6 +471,41 @@ export default function AgentConsolePage() {
|
||||
canEdit={true}
|
||||
onUpdated={refresh}
|
||||
/>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="bg-red-500/10 rounded-xl p-6 border border-red-500/30">
|
||||
<h2 className="text-xl font-semibold text-red-400 mb-4 flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
Небезпечна зона
|
||||
</h2>
|
||||
<p className="text-white/50 mb-4">
|
||||
Видалення агента приховає його з усіх списків. Цю дію можна скасувати через адміністратора.
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
if (!confirm(`Ви впевнені, що хочете видалити агента "${dashboard.profile.display_name}"?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/city/agents/${dashboard.profile.agent_id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (res.ok) {
|
||||
window.location.href = '/agents';
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.detail || 'Failed to delete agent');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error deleting agent');
|
||||
}
|
||||
}}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Видалити агента
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
366
apps/web/src/app/agents/new/page.tsx
Normal file
366
apps/web/src/app/agents/new/page.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Bot, ChevronLeft, Save, Loader2 } from 'lucide-react';
|
||||
|
||||
// Districts list
|
||||
const DISTRICTS = [
|
||||
{ key: 'leadership', name: 'Leadership Hall' },
|
||||
{ key: 'system', name: 'System Control' },
|
||||
{ key: 'engineering', name: 'Engineering Lab' },
|
||||
{ key: 'marketing', name: 'Marketing Hub' },
|
||||
{ key: 'finance', name: 'Finance Office' },
|
||||
{ key: 'web3', name: 'Web3 District' },
|
||||
{ key: 'security', name: 'Security Bunker' },
|
||||
{ key: 'vision', name: 'Vision Studio' },
|
||||
{ key: 'rnd', name: 'R&D Laboratory' },
|
||||
{ key: 'memory', name: 'Memory Vault' },
|
||||
];
|
||||
|
||||
// Agent kinds
|
||||
const AGENT_KINDS = [
|
||||
{ key: 'assistant', name: 'Assistant', emoji: '🤖' },
|
||||
{ key: 'orchestrator', name: 'Orchestrator', emoji: '🎭' },
|
||||
{ key: 'specialist', name: 'Specialist', emoji: '🔧' },
|
||||
{ key: 'civic', name: 'Civic', emoji: '🏛️' },
|
||||
{ key: 'security', name: 'Security', emoji: '🛡️' },
|
||||
{ key: 'finance', name: 'Finance', emoji: '💰' },
|
||||
{ key: 'research', name: 'Research', emoji: '🔬' },
|
||||
{ key: 'marketing', name: 'Marketing', emoji: '📢' },
|
||||
{ key: 'builder', name: 'Builder', emoji: '🏗️' },
|
||||
];
|
||||
|
||||
// Nodes
|
||||
const NODES = [
|
||||
{ id: 'node-1-hetzner-gex44', name: 'NODE1 (Hetzner Production)' },
|
||||
{ id: 'node-2-macbook-m4max', name: 'NODE2 (MacBook Development)' },
|
||||
];
|
||||
|
||||
export default function NewAgentPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const prefilledMicrodao = searchParams.get('microdao');
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
display_name: '',
|
||||
slug: '',
|
||||
kind: 'assistant',
|
||||
role: '',
|
||||
model: '',
|
||||
node_id: 'node-2-macbook-m4max',
|
||||
home_microdao_id: prefilledMicrodao || '',
|
||||
district: '',
|
||||
avatar_url: '',
|
||||
color_hint: '#6366F1',
|
||||
is_public: false,
|
||||
is_orchestrator: false,
|
||||
priority: 'medium',
|
||||
});
|
||||
|
||||
// Auto-generate slug from display_name
|
||||
const handleNameChange = (name: string) => {
|
||||
const slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim();
|
||||
setFormData(prev => ({ ...prev, display_name: name, slug }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/city/agents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.detail || 'Failed to create agent');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
router.push(`/agents/${data.id}`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950">
|
||||
<div className="max-w-3xl mx-auto px-4 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href="/agents"
|
||||
className="inline-flex items-center gap-2 text-white/60 hover:text-white mb-4 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
Назад до агентів
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<Bot className="w-8 h-8 text-violet-400" />
|
||||
<h1 className="text-3xl font-bold text-white">Створити агента</h1>
|
||||
</div>
|
||||
<p className="text-white/60 mt-2">
|
||||
Створіть нового AI-агента для мережі DAARION
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4">
|
||||
<p className="text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic Info */}
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Основна інформація</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
Ім'я агента *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.display_name}
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-violet-500/50"
|
||||
placeholder="Наприклад: Helix"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
Slug (ID)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, slug: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-violet-500/50"
|
||||
placeholder="helix"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
Тип агента
|
||||
</label>
|
||||
<select
|
||||
value={formData.kind}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, kind: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-violet-500/50"
|
||||
>
|
||||
{AGENT_KINDS.map(kind => (
|
||||
<option key={kind.key} value={kind.key} className="bg-slate-800">
|
||||
{kind.emoji} {kind.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
Роль / Посада
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.role}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, role: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-violet-500/50"
|
||||
placeholder="CTO, Security Expert, etc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Розташування</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
Нода
|
||||
</label>
|
||||
<select
|
||||
value={formData.node_id}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, node_id: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-violet-500/50"
|
||||
>
|
||||
{NODES.map(node => (
|
||||
<option key={node.id} value={node.id} className="bg-slate-800">
|
||||
{node.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
Район міста
|
||||
</label>
|
||||
<select
|
||||
value={formData.district}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, district: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-violet-500/50"
|
||||
>
|
||||
<option value="" className="bg-slate-800">Не вибрано</option>
|
||||
{DISTRICTS.map(d => (
|
||||
<option key={d.key} value={d.key} className="bg-slate-800">
|
||||
{d.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
MicroDAO (опціонально)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.home_microdao_id}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, home_microdao_id: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-violet-500/50"
|
||||
placeholder="daarion, energy-union, etc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model & Appearance */}
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Модель та вигляд</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
LLM модель
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.model}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, model: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-violet-500/50"
|
||||
placeholder="gpt-oss:latest, qwen2.5-coder:32b"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
Колір
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={formData.color_hint}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, color_hint: e.target.value }))}
|
||||
className="w-12 h-12 rounded-xl cursor-pointer bg-transparent"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.color_hint}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, color_hint: e.target.value }))}
|
||||
className="flex-1 px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-violet-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/60 mb-2">
|
||||
URL аватара
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.avatar_url}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, avatar_url: e.target.value }))}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-violet-500/50"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visibility */}
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Видимість</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_public}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, is_public: e.target.checked }))}
|
||||
className="w-5 h-5 rounded bg-white/5 border-white/20 text-violet-500 focus:ring-violet-500"
|
||||
/>
|
||||
<span className="text-white">Публічний громадянин міста</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_orchestrator}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, is_orchestrator: e.target.checked }))}
|
||||
className="w-5 h-5 rounded bg-white/5 border-white/20 text-violet-500 focus:ring-violet-500"
|
||||
/>
|
||||
<span className="text-white">Оркестратор (керує іншими агентами)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Link
|
||||
href="/agents"
|
||||
className="px-6 py-3 bg-white/5 border border-white/10 rounded-xl text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
Скасувати
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !formData.display_name || !formData.slug}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-violet-500 to-purple-600 rounded-xl text-white font-semibold hover:from-violet-400 hover:to-purple-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Створення...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-5 h-5" />
|
||||
Створити агента
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Bot, Users, Building2, Server, ExternalLink, Shield } from 'lucide-react';
|
||||
import { Bot, Users, Building2, Server, ExternalLink, Shield, Plus } from 'lucide-react';
|
||||
import { useAgentList } from '@/hooks/useAgents';
|
||||
import { AgentSummary, getGovLevelBadge } from '@/lib/types/agents';
|
||||
import { AgentPresenceBadge } from '@/components/ui/AgentPresenceBadge';
|
||||
@@ -131,18 +131,29 @@ export default function AgentsPage() {
|
||||
<div className="max-w-7xl mx-auto px-4 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Bot className="w-8 h-8 text-violet-400" />
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-violet-400 to-purple-400 bg-clip-text text-transparent">
|
||||
Agent Console
|
||||
</h1>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Bot className="w-8 h-8 text-violet-400" />
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-violet-400 to-purple-400 bg-clip-text text-transparent">
|
||||
Agent Console
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-white/60 text-lg">
|
||||
Всі AI-агенти мережі DAARION
|
||||
</p>
|
||||
<p className="text-cyan-400 mt-2">
|
||||
Знайдено агентів: {total}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/agents/new"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-violet-500 to-purple-600 rounded-xl text-white font-semibold hover:from-violet-400 hover:to-purple-500 transition-all shadow-lg shadow-violet-500/25 hover:shadow-violet-500/40"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Новий агент
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-white/60 text-lg">
|
||||
Всі AI-агенти мережі DAARION
|
||||
</p>
|
||||
<p className="text-cyan-400 mt-2">
|
||||
Знайдено агентів: {total}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
|
||||
@@ -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