From 01dd36742ab8302178d7c2445317f0c5d8d6f574 Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 29 Jul 2026 15:35:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(orchestrator):=20=E5=A4=9A=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E7=BC=96=E6=8E=92P0/P1=E9=87=8D=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83=E7=BB=9F=E4=B8=80?= =?UTF-8?q?PostgreSQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0: 任务取消全链路(注册表+部分计费)、pydantic强校验+修复循环、稳定key依赖、评审策略分层(4档)+评分制rework P1: SubAgentTemplate蓝图、per-subtask AgentState持久化、三档权限模式、team_tools增量编排、BillingPolicy抽象 AgentScope 2.0对齐: ContextConfig/MCP构造注入、AgentState外部注入、中间件栈; 新增pytest测试体系(7个模块); 开发环境切换PostgreSQL(BREAKING: 旧SQLite数据不自动迁移) --- .gitignore | 4 +- README.md | 4 +- README_EN.md | 4 +- backend/.env.example | 9 +- backend/admin/src/app/admin/teams/page.tsx | 487 +++++++++ .../admin/src/app/admin/templates/page.tsx | 285 +++++ .../src/components/admin/AdminLayout.tsx | 2 + .../admin/agents/AgentForm/LeaderConfig.tsx | 113 ++ .../admin/agents/AgentForm/SystemPrompt.tsx | 16 +- .../admin/agents/AgentForm/index.tsx | 16 +- .../admin/agents/AgentForm/schema.ts | 15 + backend/admin/src/i18n/locales/en-US.json | 124 ++- backend/admin/src/i18n/locales/zh-CN.json | 124 ++- backend/admin/src/types/index.ts | 8 + backend/admin/tsconfig.tsbuildinfo | 2 +- backend/agents.py | 76 +- backend/config.py | 6 +- backend/database.py | 100 +- backend/main.py | 7 + backend/middlewares.py | 214 ++++ .../g4h5i6j7k8l9_add_agent_review_policy.py | 48 + .../h5i6j7k8l9m0_add_agent_permission_mode.py | 46 + .../i6j7k8l9m0n1_add_sub_agent_templates.py | 140 +++ ...8l9m0n1o2_add_agent_orchestration_style.py | 35 + backend/models.py | 49 + backend/pytest.ini | 11 + backend/requirements-dev.txt | 8 + backend/requirements.txt | 2 +- backend/routers/admin_debug.py | 3 +- backend/routers/admin_sub_agent_templates.py | 143 +++ backend/routers/admin_tools.py | 4 +- backend/routers/chats.py | 1 + backend/routers/orchestrate.py | 24 +- backend/schemas.py | 51 + backend/scripts/probe_db.py | 74 ++ backend/scripts/prompt_templates.json | 144 +++ backend/scripts/seed_db.py | 6 +- backend/security/__init__.py | 20 +- backend/security/permission.py | 128 +++ backend/services/agent_executor.py | 166 ++- backend/services/billing_policy.py | 201 ++++ backend/services/chat_generation.py | 41 +- backend/services/chat_multi_agent.py | 159 ++- backend/services/chat_tool_dispatch.py | 15 + backend/services/media_canvas_bridge.py | 38 +- backend/services/orchestrator.py | 975 +++++++++++++++--- backend/services/team_events.py | 83 ++ backend/services/team_tools.py | 393 +++++++ backend/services/tool_execution_logger.py | 6 +- backend/services/tool_manager/context.py | 8 + backend/services/tool_manager/manager.py | 22 +- .../services/tool_manager/providers/canvas.py | 11 +- .../tool_manager/providers/image_edit.py | 3 +- .../tool_manager/providers/music_gen.py | 3 +- .../tool_manager/providers/video_edit.py | 3 +- .../tool_manager/providers/video_gen.py | 5 +- .../Makeup_Beauty_Design/SKILL.md | 264 +++++ .../references/full_makeup_guide.md | 152 +++ backend/tests/__init__.py | 1 + backend/tests/services/__init__.py | 1 + .../services/test_agent_executor_subtask.py | 255 +++++ backend/tests/services/test_billing_policy.py | 337 ++++++ backend/tests/services/test_orchestrator.py | 475 +++++++++ backend/tests/services/test_permission.py | 183 ++++ .../services/test_sub_agent_templates.py | 201 ++++ backend/tests/services/test_team_tools.py | 100 ++ .../services/test_tool_manager_permission.py | 126 +++ deploy/docker-compose.dev.yml | 70 ++ dev.py | 96 +- .../theater/[id]/hooks/useTheaterLoading.ts | 12 +- .../components/ai-assistant/ChatMessage.tsx | 4 + .../ai-assistant/MultiAgentPanel.tsx | 147 ++- .../components/ai-assistant/ThinkPanel.tsx | 16 +- .../ai-assistant/hooks/useSSEHandler.ts | 308 +++++- .../ai-assistant/hooks/useSessionManager.ts | 7 +- .../components/canvas/AIAssistantPanel.tsx | 7 + .../src/components/canvas/MultiAgentSteps.tsx | 245 +++-- frontend/src/store/useAIAssistantStore.ts | 14 + frontend/src/store/useCanvasStore.ts | 58 ++ 79 files changed, 7360 insertions(+), 404 deletions(-) create mode 100644 backend/admin/src/app/admin/teams/page.tsx create mode 100644 backend/admin/src/app/admin/templates/page.tsx create mode 100644 backend/middlewares.py create mode 100644 backend/migrations/versions/g4h5i6j7k8l9_add_agent_review_policy.py create mode 100644 backend/migrations/versions/h5i6j7k8l9m0_add_agent_permission_mode.py create mode 100644 backend/migrations/versions/i6j7k8l9m0n1_add_sub_agent_templates.py create mode 100644 backend/migrations/versions/j7k8l9m0n1o2_add_agent_orchestration_style.py create mode 100644 backend/pytest.ini create mode 100644 backend/requirements-dev.txt create mode 100644 backend/routers/admin_sub_agent_templates.py create mode 100644 backend/scripts/probe_db.py create mode 100644 backend/security/permission.py create mode 100644 backend/services/billing_policy.py create mode 100644 backend/services/team_events.py create mode 100644 backend/services/team_tools.py create mode 100644 backend/skills/active_skills/Makeup_Beauty_Design/SKILL.md create mode 100644 backend/skills/active_skills/Makeup_Beauty_Design/references/full_makeup_guide.md create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/services/__init__.py create mode 100644 backend/tests/services/test_agent_executor_subtask.py create mode 100644 backend/tests/services/test_billing_policy.py create mode 100644 backend/tests/services/test_orchestrator.py create mode 100644 backend/tests/services/test_permission.py create mode 100644 backend/tests/services/test_sub_agent_templates.py create mode 100644 backend/tests/services/test_team_tools.py create mode 100644 backend/tests/services/test_tool_manager_permission.py create mode 100644 deploy/docker-compose.dev.yml diff --git a/.gitignore b/.gitignore index d57e4338..25c1005d 100644 --- a/.gitignore +++ b/.gitignore @@ -109,4 +109,6 @@ frontend/public/*.jpeg # 统一使用 npm,忽略其他锁定文件 pnpm-lock.yaml -yarn.lock \ No newline at end of file +yarn.lock + +docs diff --git a/README.md b/README.md index 340adab6..56feed11 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ KunFlix 专为**影视广告与短剧创作**打造的开放式AI内容创作生 数据库 -SQLite (开发) / PostgreSQL (生产) + SQLAlchemy +PostgreSQL + SQLAlchemy(开发与生产统一) 前端框架 @@ -132,7 +132,7 @@ KunFlix 专为**影视广告与短剧创作**打造的开放式AI内容创作生 - **Rust 1.85+**(仅当从源码编译 ripgrep 时需要;首次安装会触发,参见 [UPGRADE.md](./UPGRADE.md)) - Node.js 20+ - Docker 24+(Docker 部署时需要) -- SQLite(开发)/ PostgreSQL(生产) +- PostgreSQL 18(开发与生产统一;本地可用 `docker compose -f deploy/docker-compose.dev.yml up -d` 一键启动 PostgreSQL + Redis) > 从 1.0 升级到 2.0 的开发者请先阅读 [UPGRADE.md](./UPGRADE.md)。 diff --git a/README_EN.md b/README_EN.md index 5edd90c6..41ce0b62 100644 --- a/README_EN.md +++ b/README_EN.md @@ -108,7 +108,7 @@ KunFlix is an open AI content creation ecosystem designed for **film advertising Database -SQLite (Dev) / PostgreSQL (Prod) + SQLAlchemy +PostgreSQL + SQLAlchemy (unified for dev & prod) Frontend @@ -132,7 +132,7 @@ KunFlix is an open AI content creation ecosystem designed for **film advertising - **Rust 1.85+** (only when compiling ripgrep from source; triggered on first install, see [UPGRADE.md](./UPGRADE.md)) - Node.js 20+ - Docker 24+ (required for Docker deployment) -- SQLite (Development) / PostgreSQL (Production) +- PostgreSQL 18 (unified for dev & prod; start locally with `docker compose -f deploy/docker-compose.dev.yml up -d` for PostgreSQL + Redis) > If you are upgrading from 1.0 to 2.0, please read [UPGRADE.md](./UPGRADE.md) first. diff --git a/backend/.env.example b/backend/.env.example index c7726822..730e79db 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,13 +1,16 @@ # ============================================================================= # KunFlix Backend - 环境变量样例 -# 复制为 .env 后按需覆盖;保持默认值即可在本地 SQLite + 内存缓存运行。 +# 复制为 .env 后按需覆盖;推荐搭配 deploy/docker-compose.dev.yml 一键启动 +# PostgreSQL + Redis 依赖: +# docker compose -f deploy/docker-compose.dev.yml up -d # ============================================================================= # ----------------------------------------------------------------------------- # Database # ----------------------------------------------------------------------------- -# 默认使用 backend/kunflix.db SQLite 文件,生产建议切 PostgreSQL: -# DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/kunflix +# 推荐 PostgreSQL(与 docker-compose.dev.yml 默认账号一致);若想保留旧版本的 +# SQLite 数据库方便升级无损过渡,把下方行换回: +# DATABASE_URL=sqlite+aiosqlite:///./kunflix.db DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost/kunflix_db # 连接池(PostgreSQL 生产建议值:POOL_SIZE=20, MAX_OVERFLOW=30) diff --git a/backend/admin/src/app/admin/teams/page.tsx b/backend/admin/src/app/admin/teams/page.tsx new file mode 100644 index 00000000..dbcf36d2 --- /dev/null +++ b/backend/admin/src/app/admin/teams/page.tsx @@ -0,0 +1,487 @@ +'use client'; + +/** + * 智能体团队管理页 —— 完整 CRUD + * + * 操作流程: + * 1. "创建团队"按钮 → Dialog → 选 Leader + 选成员 + 配置参数 → PUT agent + * 2. 卡片"编辑" → 编辑 Dialog → 修改成员/参数 → PUT agent + * 3. 卡片"解散" → 确认 → PUT agent (is_leader=false, member_agent_ids=[]) + * + * 所有操作复用 PUT /api/agents/{id} 接口,不需要独立的 team CRUD API。 + */ +import React, { useMemo, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useTranslation } from 'react-i18next'; +import { useAgents } from '@/hooks/useAgents'; +import { useLLMProviders } from '@/hooks/useLLMProviders'; +import { Agent } from '@/types'; +import api from '@/lib/axios'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Switch } from '@/components/ui/switch'; +import { Slider } from '@/components/ui/slider'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, +} from '@/components/ui/dialog'; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select'; +import { + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { + Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useToast } from '@/components/ui/use-toast'; +import { + Users2, Search, Crown, ShieldCheck, Gauge, Sparkles, ArrowRight, Plus, +} from 'lucide-react'; + +// 评审策略选项 +const REVIEW_POLICIES = ['final_only', 'per_subtask', 'threshold_based', 'disabled'] as const; +const PERMISSION_MODES = ['default', 'explore', 'bypass'] as const; + +// --------------------------------------------------------------------------- +// 团队表单默认值 +// --------------------------------------------------------------------------- +interface TeamFormData { + leaderId: string; + memberIds: string[]; + maxSubtasks: number; + reviewPolicy: string; + permissionMode: string; + enableAutoReview: boolean; +} + +const DEFAULT_FORM: TeamFormData = { + leaderId: '', + memberIds: [], + maxSubtasks: 10, + reviewPolicy: 'final_only', + permissionMode: 'default', + enableAutoReview: true, +}; + +// --------------------------------------------------------------------------- +// 主页面 +// --------------------------------------------------------------------------- + +export default function TeamsPage() { + const router = useRouter(); + const { t } = useTranslation(); + const { toast } = useToast(); + const [searchText, setSearchText] = useState(''); + const [createOpen, setCreateOpen] = useState(false); + const [editOpen, setEditOpen] = useState(false); + const [form, setForm] = useState(DEFAULT_FORM); + + const { agents, isLoading, isError, mutate } = useAgents('', 1, 100); + const { providers } = useLLMProviders(); + + // id -> Agent 映射 + const agentById = useMemo(() => { + const m = new Map(); + (agents || []).forEach((a) => a.id && m.set(a.id, a)); + return m; + }, [agents]); + + // leaders 列表 + const leaders = useMemo(() => { + const all = (agents || []).filter((a) => a.is_leader); + if (!searchText.trim()) return all; + const kw = searchText.trim().toLowerCase(); + return all.filter((a) => + a.name.toLowerCase().includes(kw) || (a.description || '').toLowerCase().includes(kw) + ); + }, [agents, searchText]); + + // 非 leader 的 agents(可作为 leader 候选) + const nonLeaderAgents = useMemo( + () => (agents || []).filter((a) => !a.is_leader), + [agents], + ); + + // 统计 + const stats = useMemo(() => { + const totalTeams = leaders.length; + const totalMembers = leaders.reduce((sum, l) => sum + (l.member_agent_ids?.length || 0), 0); + const avgMembers = totalTeams > 0 ? (totalMembers / totalTeams).toFixed(1) : '0'; + return { totalTeams, totalMembers, avgMembers }; + }, [leaders]); + + // ----- 创建团队 ----- + const openCreate = () => { + setForm(DEFAULT_FORM); + setCreateOpen(true); + }; + + const handleCreate = async () => { + if (!form.leaderId) return; + try { + await api.put(`/agents/${form.leaderId}`, { + is_leader: true, + member_agent_ids: form.memberIds, + max_subtasks: form.maxSubtasks, + review_policy: form.reviewPolicy, + permission_mode: form.permissionMode, + enable_auto_review: form.enableAutoReview, + }); + toast({ title: t('teams.toast.createSuccess') }); + setCreateOpen(false); + mutate(); + } catch (err: any) { + toast({ variant: 'destructive', title: t('teams.toast.error'), description: err?.response?.data?.detail || '' }); + } + }; + + // ----- 编辑团队 ----- + const openEdit = (leader: Agent) => { + setForm({ + leaderId: leader.id || '', + memberIds: leader.member_agent_ids || [], + maxSubtasks: leader.max_subtasks || 10, + reviewPolicy: leader.review_policy || 'final_only', + permissionMode: leader.permission_mode || 'default', + enableAutoReview: leader.enable_auto_review !== false, + }); + setEditOpen(true); + }; + + const handleEdit = async () => { + if (!form.leaderId) return; + try { + await api.put(`/agents/${form.leaderId}`, { + member_agent_ids: form.memberIds, + max_subtasks: form.maxSubtasks, + review_policy: form.reviewPolicy, + permission_mode: form.permissionMode, + enable_auto_review: form.enableAutoReview, + }); + toast({ title: t('teams.toast.editSuccess') }); + setEditOpen(false); + mutate(); + } catch (err: any) { + toast({ variant: 'destructive', title: t('teams.toast.error'), description: err?.response?.data?.detail || '' }); + } + }; + + // ----- 解散团队 ----- + const handleDissolve = async (leaderId: string) => { + try { + await api.put(`/agents/${leaderId}`, { + is_leader: false, + member_agent_ids: [], + }); + toast({ title: t('teams.toast.dissolveSuccess') }); + mutate(); + } catch (err: any) { + toast({ variant: 'destructive', title: t('teams.toast.error'), description: err?.response?.data?.detail || '' }); + } + }; + + // ----- 成员多选 toggle ----- + const toggleMember = (agentId: string) => { + setForm((prev) => ({ + ...prev, + memberIds: prev.memberIds.includes(agentId) + ? prev.memberIds.filter((id) => id !== agentId) + : [...prev.memberIds, agentId], + })); + }; + + // 当前表单中可选的成员(排除自己) + const availableMembers = useMemo( + () => (agents || []).filter((a) => a.id !== form.leaderId), + [agents, form.leaderId], + ); + + // --------------------------------------------------------------------------- + // 创建/编辑 Dialog 内的表单内容(复用) + // --------------------------------------------------------------------------- + const TeamFormContent = ({ isCreate }: { isCreate: boolean }) => ( +
+ {/* Leader 选择(仅创建时) */} + {isCreate && ( +
+ +

{t('teams.create.selectLeaderDesc')}

+ +
+ )} + + {/* 成员选择 */} + {form.leaderId && ( +
+ +

{t('teams.create.selectMembersDesc')}

+
+ {availableMembers.length > 0 ? availableMembers.map((a) => ( + + )) : ( +

{t('teams.create.noAvailableAgents')}

+ )} +
+
+ )} + + {/* 参数配置 */} + {form.leaderId && ( +
+

{t('teams.create.settings')}

+ + {/* max_subtasks */} +
+
+ {t('teams.create.maxSubtasks')} + {form.maxSubtasks} +
+ setForm({ ...form, maxSubtasks: v[0] })} + min={1} max={20} step={1} + /> +
+ + {/* review_policy */} +
+ + +
+ + {/* permission_mode */} +
+ + +
+ + {/* auto_review */} +
+
+ {t('teams.create.autoReview')} +

{t('teams.create.autoReviewDesc')}

+
+ setForm({ ...form, enableAutoReview: v })} + /> +
+
+ )} +
+ ); + + return ( +
+ {/* Header */} +
+
+

{t('teams.title')}

+

{t('teams.subtitle')}

+
+
+
+ + setSearchText(e.target.value)} + /> +
+ +
+
+ + {/* 统计条 */} +
+ {[ + { label: t('teams.stats.totalTeams'), value: stats.totalTeams }, + { label: t('teams.stats.totalMembers'), value: stats.totalMembers }, + { label: t('teams.stats.avgMembers'), value: stats.avgMembers }, + ].map((s) => ( +
+
{s.label}
+
{s.value}
+
+ ))} +
+ + {/* 卡片列表 */} +
+ {isLoading ? ( +
{t('teams.loading')}
+ ) : isError ? ( +
{t('teams.error')}
+ ) : leaders.length === 0 ? ( +
+
+ +
+

{t('teams.empty.title')}

+

{t('teams.empty.desc')}

+ +
+ ) : ( +
+ {leaders.map((leader) => { + const members = (leader.member_agent_ids || []) + .map((id) => agentById.get(id)) + .filter((a): a is Agent => Boolean(a)); + const provider = providers?.find((p) => p.id === leader.provider_id); + return ( + + +
+
+ +
+
+
+ {leader.name} + {t('teams.card.leader')} +
+

{leader.description || '—'}

+
+ {provider && {provider.name}} + {leader.model} +
+
+
+
+ + {/* 成员 */} +
+
+ + {t('teams.card.members')} + {members.length} +
+ {members.length > 0 ? ( +
+ {members.map((m) => ( + + {m.name} + + ))} +
+ ) : ( +

{t('teams.card.noMembers')}

+ )} +
+ {/* 配置摘要 */} +
+ {t('teams.card.maxSubtasks')} + {leader.max_subtasks || 10} + {t('teams.card.reviewPolicy')} + + {t(`agents.form.leader.reviewPolicyOptions.${leader.review_policy || 'final_only'}`)} + +
+ {/* 操作按钮 */} +
+ + + + + + + + {t('teams.dissolve.title')} + {t('teams.dissolve.desc')} + + + {t('common.buttons.cancel')} + leader.id && handleDissolve(leader.id)}> + {t('teams.dissolve.confirm')} + + + + +
+
+
+ ); + })} +
+ )} +
+ + {/* 创建团队 Dialog */} + + + + {t('teams.create.title')} + + + + + + + + + + {/* 编辑团队 Dialog */} + + + + {t('teams.edit.title')} — {agentById.get(form.leaderId)?.name} + + + + + + + + +
+ ); +} diff --git a/backend/admin/src/app/admin/templates/page.tsx b/backend/admin/src/app/admin/templates/page.tsx new file mode 100644 index 00000000..9d5e7538 --- /dev/null +++ b/backend/admin/src/app/admin/templates/page.tsx @@ -0,0 +1,285 @@ +'use client'; + +/** + * P1-1: SubAgentTemplate 蓝图管理页 + * + * 简洁的表格视图 + 新建 Dialog。编辑/删除通过行操作按钮触发。 + * 页面数据来自 GET /api/admin/sub-agent-templates。 + */ +import React, { useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import useSWR from 'swr'; +import { fetcher } from '@/lib/api-utils'; +import api from '@/lib/axios'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { useToast } from '@/components/ui/use-toast'; +import { + Table, TableBody, TableCell, TableHead, TableHeader, TableRow, +} from '@/components/ui/table'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, +} from '@/components/ui/dialog'; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select'; +import { + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { Plus, Pencil, Trash2, Puzzle } from 'lucide-react'; + +interface Template { + id: string; + type: string; + description: string; + system_prompt_template: string; + permission_mode: string; + tools: string[]; + max_tool_rounds: number; + context_config: any; + created_at: string; +} + +const PERMISSION_MODES = ['default', 'explore', 'bypass'] as const; + +export default function TemplatesPage() { + const { t } = useTranslation(); + const { toast } = useToast(); + const { data: templates, mutate } = useSWR( + '/admin/sub-agent-templates', + fetcher, + ); + + const [dialogOpen, setDialogOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [form, setForm] = useState({ + type: '', + description: '', + system_prompt_template: '', + permission_mode: 'default', + tools: '', + max_tool_rounds: 50, + }); + + const resetForm = () => setForm({ + type: '', description: '', system_prompt_template: '', + permission_mode: 'default', tools: '', max_tool_rounds: 50, + }); + + const openCreate = () => { + resetForm(); + setEditingId(null); + setDialogOpen(true); + }; + + const openEdit = (tpl: Template) => { + setForm({ + type: tpl.type, + description: tpl.description, + system_prompt_template: tpl.system_prompt_template, + permission_mode: tpl.permission_mode || 'default', + tools: (tpl.tools || []).join(', '), + max_tool_rounds: tpl.max_tool_rounds || 50, + }); + setEditingId(tpl.id); + setDialogOpen(true); + }; + + const handleSave = async () => { + const payload = { + type: form.type.trim(), + description: form.description.trim(), + system_prompt_template: form.system_prompt_template, + permission_mode: form.permission_mode, + tools: form.tools.split(',').map(s => s.trim()).filter(Boolean), + max_tool_rounds: form.max_tool_rounds, + }; + try { + if (editingId) { + await api.put(`/admin/sub-agent-templates/${editingId}`, payload); + toast({ title: 'Template updated' }); + } else { + await api.post('/admin/sub-agent-templates', payload); + toast({ title: 'Template created' }); + } + mutate(); + setDialogOpen(false); + } catch (err: any) { + toast({ variant: 'destructive', title: 'Error', description: err?.response?.data?.detail || 'Failed' }); + } + }; + + const handleDelete = async (id: string) => { + try { + await api.delete(`/admin/sub-agent-templates/${id}`); + toast({ title: 'Template deleted' }); + mutate(); + } catch (err: any) { + toast({ variant: 'destructive', title: 'Error', description: err?.response?.data?.detail || 'Failed' }); + } + }; + + return ( +
+
+
+

+ + {t('templates.title')} +

+

+ {t('templates.subtitle')} +

+
+ +
+ +
+ + + + Type + Description + Mode + Tools + Rounds + Actions + + + + {templates?.map((tpl) => ( + + {tpl.type} + + {tpl.description} + + + + {tpl.permission_mode} + + + + {(tpl.tools || []).join(', ') || '—'} + + {tpl.max_tool_rounds} + +
+ + + + + + + + Delete "{tpl.type}"? + + This action cannot be undone. Leaders referencing this template must be updated first. + + + + Cancel + handleDelete(tpl.id)}>Delete + + + +
+
+
+ ))} + {(!templates || templates.length === 0) && ( + + + No blueprints yet. Click "New Blueprint" to create one. + + + )} +
+
+
+ + {/* Create/Edit Dialog */} + + + + {editingId ? 'Edit Blueprint' : 'New Blueprint'} + +
+
+ + setForm({ ...form, type: e.target.value })} + placeholder={t('templates.form.typePlaceholder')} + disabled={!!editingId} + /> +
+
+ + setForm({ ...form, description: e.target.value })} + placeholder={t('templates.form.descriptionPlaceholder')} + /> +
+
+ +