diff --git a/alembic/versions/2026_06_04_0002-0005_system_node_display_name.py b/alembic/versions/2026_06_04_0002-0005_system_node_display_name.py new file mode 100644 index 0000000..ecdb5a3 --- /dev/null +++ b/alembic/versions/2026_06_04_0002-0005_system_node_display_name.py @@ -0,0 +1,28 @@ +"""system_node_display_name + +Revision ID: 0005_system_node_display_name +Revises: 0004_system_node_custom_prompt +Create Date: 2026-06-04 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = "0005_system_node_display_name" +down_revision: Union[str, None] = "0004_system_node_custom_prompt" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "system_node_config", + sa.Column("display_name", sa.String(100), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("system_node_config", "display_name") diff --git a/frontend/src/components/Agent/AgentLayout.tsx b/frontend/src/components/Agent/AgentLayout.tsx index 04aac09..b22156e 100644 --- a/frontend/src/components/Agent/AgentLayout.tsx +++ b/frontend/src/components/Agent/AgentLayout.tsx @@ -4,6 +4,7 @@ import { ProvidersSettings } from './ProvidersSettings'; import { WorkerIndividualSettings } from './WorkerIndividualSettings'; import { WorkflowConfigSettings } from './WorkflowConfigSettings'; import { SystemLogsView } from './SystemLogsView'; +import { PersonaTemplateSettings } from './PersonaTemplateSettings'; export function AgentLayout() { const { t } = useTranslation(); @@ -11,6 +12,7 @@ export function AgentLayout() { const tabs = [ { key: 'worker', label: t('agent.individual') }, + { key: 'persona', label: t('agent.personaManagement') }, { key: 'providers', label: t('agent.providerManagement') }, { key: 'config', label: t('agent.config') }, { key: 'logs', label: t('agent.systemLogs') }, @@ -35,6 +37,7 @@ export function AgentLayout() {
{innerAgentTab === 'worker' && } + {innerAgentTab === 'persona' && } {innerAgentTab === 'providers' && } {innerAgentTab === 'config' && } {innerAgentTab === 'logs' && } diff --git a/frontend/src/components/Agent/PersonaTemplateSettings.tsx b/frontend/src/components/Agent/PersonaTemplateSettings.tsx new file mode 100644 index 0000000..59c3d40 --- /dev/null +++ b/frontend/src/components/Agent/PersonaTemplateSettings.tsx @@ -0,0 +1,276 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import apiClient from '../../api/client'; +import { Plus, Edit2, Trash2, X, Save, Loader2, FileText, Copy } from 'lucide-react'; +import type { Provider } from '../../types'; + +interface PersonaTemplate { + template_id: string; + name: string; + description: string; + system_prompt: string; + agent_type: string; + provider_title: string | null; + model_id: string | null; + tools: string[]; + tags: string[]; + is_builtin: boolean; + owner_id: string | null; +} + +export function PersonaTemplateSettings() { + const { t } = useTranslation(); + const [templates, setTemplates] = useState([]); + const [providers, setProviders] = useState([]); + const [availableTools, setAvailableTools] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const [editData, setEditData] = useState>({}); + const [isNew, setIsNew] = useState(false); + const [modalMessage, setModalMessage] = useState(''); + const [submitLoading, setSubmitLoading] = useState(false); + + const fetchData = async () => { + setLoading(true); + try { + const [tplRes, provRes, toolsRes] = await Promise.all([ + apiClient.get('/api/v1/agent/template'), + apiClient.get('/api/v1/provider/list'), + apiClient.get('/api/v1/resource/tool') + ]); + setTemplates(tplRes.data.templates || []); + setProviders(Object.values(provRes.data.provider_list || {})); + setAvailableTools(toolsRes.data.tools || []); + } catch { setError(t('agent.loadFailed')); } + finally { setLoading(false); } + }; + + useEffect(() => { fetchData(); }, []); + + const handleAddNew = () => { + setEditData({ name: '', description: '', system_prompt: '', agent_type: 'ordinary_individual', + provider_title: providers.length > 0 ? providers[0].provider_title : '', + model_id: '', tools: [], tags: [] }); + setIsNew(true); + setIsEditing(true); + setModalMessage(''); + }; + + const handleEdit = (tpl: PersonaTemplate) => { + setEditData({ ...tpl }); + setIsNew(false); + setIsEditing(true); + setModalMessage(''); + }; + + const handleDelete = async (id: string) => { + if (!confirm(t('agent.deleteTemplateConfirm'))) return; + try { await apiClient.delete(`/api/v1/agent/template/${id}`); fetchData(); } + catch { alert(t('common.deleteFailed')); } + }; + + const handleCreateWorker = async (id: string) => { + try { + await apiClient.post(`/api/v1/agent/worker/from-template/${id}`); + alert(t('agent.workerCreatedFromTemplate')); + } catch (err: any) { + alert(err.response?.data?.detail || t('common.saveFailed')); + } + }; + + const handleModalSave = async (e: React.FormEvent) => { + e.preventDefault(); + setModalMessage(''); + setSubmitLoading(true); + try { + const payload = { name: editData.name, description: editData.description, + system_prompt: editData.system_prompt, agent_type: editData.agent_type, + provider_title: editData.provider_title || null, + model_id: editData.model_id || null, + tools: editData.tools || [], tags: editData.tags || [] }; + if (isNew) await apiClient.post('/api/v1/agent/template', payload); + else await apiClient.put(`/api/v1/agent/template/${editData.template_id}`, payload); + setIsEditing(false); + fetchData(); + } catch (err: any) { + setModalMessage(err.response?.data?.detail || t('common.saveFailed')); + } finally { setSubmitLoading(false); } + }; + + const getTypeBadge = (type: string) => { + const colors: Record = { + ordinary_individual: 'bg-bg-secondary text-text-muted', + skill_individual: 'bg-success-bg text-success', + special_individual: 'bg-warning-bg text-warning', + }; + return {t(`agent.type.${type}`, type)}; + }; + + return ( +
+
+
+

{t('agent.personaManagement')}

+

{t('agent.personaManagementDesc')}

+
+ +
+ + {error &&
{error}
} + +
+ {loading ? ( +
+ + {t('common.loading')} +
+ ) : templates.length === 0 ? ( +
+ + {t('agent.noTemplates')} +
+ ) : ( + + + + + + + + + + + {templates.map((tpl) => ( + + + + + + + ))} + +
{t('agent.templateName')}{t('agent.type')}{t('agent.tags')}{t('common.actions')}
+
+
+ +
+
+ {tpl.name} + {tpl.is_builtin && {t('agent.builtin')}} +
+
+
{getTypeBadge(tpl.agent_type)} +
+ {(tpl.tags || []).map(tag => ( + {tag} + ))} +
+
+ + {!tpl.is_builtin && } + {!tpl.is_builtin && } +
+ )} +
+ + {isEditing && ( +
+
+
+

{isNew ? t('agent.addTemplate') : t('agent.editTemplate')}

+ +
+
+
+
+ + setEditData({...editData, name: e.target.value})} + className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" /> +
+
+ + +
+
+
+
+ + +
+
+ + {(() => { + const sp = providers.find(p => p.provider_title === editData.provider_title); + const models = sp?.provider_models || []; + return ( + + ); + })()} +
+
+
+ +