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}
|
canEdit={true}
|
||||||
onUpdated={refresh}
|
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>
|
</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';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
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 { useAgentList } from '@/hooks/useAgents';
|
||||||
import { AgentSummary, getGovLevelBadge } from '@/lib/types/agents';
|
import { AgentSummary, getGovLevelBadge } from '@/lib/types/agents';
|
||||||
import { AgentPresenceBadge } from '@/components/ui/AgentPresenceBadge';
|
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">
|
<div className="max-w-7xl mx-auto px-4 py-12">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center justify-between">
|
||||||
<Bot className="w-8 h-8 text-violet-400" />
|
<div>
|
||||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-violet-400 to-purple-400 bg-clip-text text-transparent">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
Agent Console
|
<Bot className="w-8 h-8 text-violet-400" />
|
||||||
</h1>
|
<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>
|
</div>
|
||||||
<p className="text-white/60 text-lg">
|
|
||||||
Всі AI-агенти мережі DAARION
|
|
||||||
</p>
|
|
||||||
<p className="text-cyan-400 mt-2">
|
|
||||||
Знайдено агентів: {total}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
|
|||||||
@@ -579,6 +579,48 @@ class MicrodaoOption(BaseModel):
|
|||||||
is_active: bool = True
|
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)
|
# Visibility Updates (Task 029)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ from models_city import (
|
|||||||
MicrodaoRoomUpdate,
|
MicrodaoRoomUpdate,
|
||||||
AttachExistingRoomRequest,
|
AttachExistingRoomRequest,
|
||||||
SwapperModel,
|
SwapperModel,
|
||||||
NodeSwapperDetail
|
NodeSwapperDetail,
|
||||||
|
CreateAgentRequest,
|
||||||
|
CreateAgentResponse,
|
||||||
|
DeleteAgentResponse
|
||||||
)
|
)
|
||||||
import repo_city
|
import repo_city
|
||||||
from common.redis_client import PresenceRedis, get_redis
|
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")
|
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])
|
@router.get("/agents/online", response_model=List[AgentPresence])
|
||||||
async def get_online_agents():
|
async def get_online_agents():
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user