feat(agent): 移除control_node实例化,新增系统节点命名与人设管理前端
当前阶段只保留regulatory+consciousness两个系统节点,control_node代码保留但不再实例化。 系统节点新增display_name字段支持自定义显示名称,前端新增人设管理Tab支持模板CRUD。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||||
@@ -4,6 +4,7 @@ import { ProvidersSettings } from './ProvidersSettings';
|
|||||||
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||||
import { WorkflowConfigSettings } from './WorkflowConfigSettings';
|
import { WorkflowConfigSettings } from './WorkflowConfigSettings';
|
||||||
import { SystemLogsView } from './SystemLogsView';
|
import { SystemLogsView } from './SystemLogsView';
|
||||||
|
import { PersonaTemplateSettings } from './PersonaTemplateSettings';
|
||||||
|
|
||||||
export function AgentLayout() {
|
export function AgentLayout() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -11,6 +12,7 @@ export function AgentLayout() {
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'worker', label: t('agent.individual') },
|
{ key: 'worker', label: t('agent.individual') },
|
||||||
|
{ key: 'persona', label: t('agent.personaManagement') },
|
||||||
{ key: 'providers', label: t('agent.providerManagement') },
|
{ key: 'providers', label: t('agent.providerManagement') },
|
||||||
{ key: 'config', label: t('agent.config') },
|
{ key: 'config', label: t('agent.config') },
|
||||||
{ key: 'logs', label: t('agent.systemLogs') },
|
{ key: 'logs', label: t('agent.systemLogs') },
|
||||||
@@ -35,6 +37,7 @@ export function AgentLayout() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
|
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
|
||||||
|
{innerAgentTab === 'persona' && <PersonaTemplateSettings />}
|
||||||
{innerAgentTab === 'providers' && <ProvidersSettings />}
|
{innerAgentTab === 'providers' && <ProvidersSettings />}
|
||||||
{innerAgentTab === 'config' && <WorkflowConfigSettings />}
|
{innerAgentTab === 'config' && <WorkflowConfigSettings />}
|
||||||
{innerAgentTab === 'logs' && <SystemLogsView />}
|
{innerAgentTab === 'logs' && <SystemLogsView />}
|
||||||
|
|||||||
@@ -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<PersonaTemplate[]>([]);
|
||||||
|
const [providers, setProviders] = useState<Provider[]>([]);
|
||||||
|
const [availableTools, setAvailableTools] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editData, setEditData] = useState<Partial<PersonaTemplate>>({});
|
||||||
|
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<string, string> = {
|
||||||
|
ordinary_individual: 'bg-bg-secondary text-text-muted',
|
||||||
|
skill_individual: 'bg-success-bg text-success',
|
||||||
|
special_individual: 'bg-warning-bg text-warning',
|
||||||
|
};
|
||||||
|
return <span className={`px-2 py-0.5 rounded-md text-[10px] font-medium ${colors[type] || colors.ordinary_individual}`}>{t(`agent.type.${type}`, type)}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl space-y-6">
|
||||||
|
<div className="flex justify-between items-end">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-bold text-text-primary">{t('agent.personaManagement')}</h1>
|
||||||
|
<p className="text-sm text-text-muted mt-0.5">{t('agent.personaManagementDesc')}</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleAddNew} className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
|
||||||
|
<Plus size={14} /> {t('agent.addTemplate')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="text-sm text-danger bg-danger-bg border border-danger/20 rounded-xl p-3">{error}</div>}
|
||||||
|
|
||||||
|
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
|
||||||
|
<Loader2 size={24} className="animate-spin mb-3" />
|
||||||
|
<span className="text-sm">{t('common.loading')}</span>
|
||||||
|
</div>
|
||||||
|
) : templates.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
|
||||||
|
<FileText size={32} className="mb-3 opacity-40" />
|
||||||
|
<span className="text-sm">{t('agent.noTemplates')}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className="w-full text-left text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-bg-secondary border-b border-border-primary text-text-muted text-xs uppercase tracking-wider">
|
||||||
|
<th className="px-5 py-3 font-semibold">{t('agent.templateName')}</th>
|
||||||
|
<th className="px-5 py-3 font-semibold">{t('agent.type')}</th>
|
||||||
|
<th className="px-5 py-3 font-semibold">{t('agent.tags')}</th>
|
||||||
|
<th className="px-5 py-3 font-semibold text-right">{t('common.actions')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border-secondary">
|
||||||
|
{templates.map((tpl) => (
|
||||||
|
<tr key={tpl.template_id} className="hover:bg-bg-hover transition-colors">
|
||||||
|
<td className="px-5 py-3">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="w-7 h-7 rounded-lg bg-bg-secondary border border-border-primary flex items-center justify-center">
|
||||||
|
<FileText size={14} className="text-text-muted" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-text-primary text-xs">{tpl.name}</span>
|
||||||
|
{tpl.is_builtin && <span className="text-[10px] text-accent">{t('agent.builtin')}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-3">{getTypeBadge(tpl.agent_type)}</td>
|
||||||
|
<td className="px-5 py-3">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{(tpl.tags || []).map(tag => (
|
||||||
|
<span key={tag} className="px-1.5 py-0.5 rounded text-[10px] bg-bg-secondary text-text-muted">{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-3 text-right">
|
||||||
|
<button onClick={() => handleCreateWorker(tpl.template_id)} title={t('agent.createFromTemplate')} className="p-1.5 text-text-muted hover:text-success hover:bg-success-bg rounded-lg transition-all mr-0.5"><Copy size={14} /></button>
|
||||||
|
{!tpl.is_builtin && <button onClick={() => handleEdit(tpl)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all mr-0.5"><Edit2 size={14} /></button>}
|
||||||
|
{!tpl.is_builtin && <button onClick={() => handleDelete(tpl.template_id)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all"><Trash2 size={14} /></button>}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-border-primary animate-fade-in-scale">
|
||||||
|
<div className="flex justify-between items-center p-5 border-b border-border-primary sticky top-0 bg-bg-card z-10">
|
||||||
|
<h2 className="text-base font-bold text-text-primary">{isNew ? t('agent.addTemplate') : t('agent.editTemplate')}</h2>
|
||||||
|
<button onClick={() => setIsEditing(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={20} /></button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleModalSave} className="p-5 space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.templateName')}</label>
|
||||||
|
<input type="text" required value={editData.name || ''} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.type')}</label>
|
||||||
|
<select value={editData.agent_type || 'ordinary_individual'} onChange={(e) => setEditData({...editData, agent_type: 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">
|
||||||
|
<option value="ordinary_individual">{t('agent.type.ordinary_individual')}</option>
|
||||||
|
<option value="skill_individual">{t('agent.type.skill_individual')}</option>
|
||||||
|
<option value="special_individual">{t('agent.type.special_individual')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.provider')}</label>
|
||||||
|
<select value={editData.provider_title || ''} onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
|
||||||
|
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">
|
||||||
|
<option value="">{t('common.none')}</option>
|
||||||
|
{providers.map((p) => (<option key={p.provider_title} value={p.provider_title}>{p.provider_title}</option>))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.model')}</label>
|
||||||
|
{(() => {
|
||||||
|
const sp = providers.find(p => p.provider_title === editData.provider_title);
|
||||||
|
const models = sp?.provider_models || [];
|
||||||
|
return (
|
||||||
|
<select value={editData.model_id || ''} onChange={(e) => setEditData({...editData, model_id: 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">
|
||||||
|
<option value="">{t('common.none')}</option>
|
||||||
|
{models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.description')}</label>
|
||||||
|
<textarea value={editData.description || ''} onChange={(e) => setEditData({...editData, description: e.target.value})} rows={2}
|
||||||
|
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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.systemPrompt')}</label>
|
||||||
|
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={4}
|
||||||
|
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.tags')}</label>
|
||||||
|
<input type="text" value={(editData.tags || []).join(', ')}
|
||||||
|
onChange={(e) => setEditData({...editData, tags: e.target.value.split(',').map(s => s.trim()).filter(Boolean)})}
|
||||||
|
placeholder={t('agent.tagsPlaceholder')}
|
||||||
|
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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.tools')}</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5 p-3 bg-bg-input border border-border-primary rounded-xl max-h-40 overflow-y-auto">
|
||||||
|
{availableTools.map(tool => {
|
||||||
|
const currentTools: string[] = editData.tools || [];
|
||||||
|
const isSelected = currentTools.includes(tool);
|
||||||
|
return (
|
||||||
|
<button key={tool} type="button" onClick={() => {
|
||||||
|
const updated = isSelected ? currentTools.filter(x => x !== tool) : [...currentTools, tool];
|
||||||
|
setEditData({...editData, tools: updated});
|
||||||
|
}}
|
||||||
|
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${isSelected ? 'bg-accent-light text-accent border border-accent/20' : 'bg-bg-secondary text-text-muted border border-border-primary hover:border-text-muted'}`}>
|
||||||
|
{tool}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{availableTools.length === 0 && <span className="text-xs text-text-muted">{t('agent.noTools')}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{modalMessage && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{modalMessage}</div>}
|
||||||
|
<div className="pt-3 flex justify-end gap-2 border-t border-border-primary">
|
||||||
|
<button type="button" onClick={() => setIsEditing(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
|
||||||
|
<button type="submit" disabled={submitLoading} className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
|
||||||
|
<Save size={14} /> {submitLoading ? t('common.saving') : t('common.save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -51,15 +51,17 @@ export function WorkerIndividualSettings() {
|
|||||||
const providersList = Object.values(provRes.data.provider_list || {}) as Provider[];
|
const providersList = Object.values(provRes.data.provider_list || {}) as Provider[];
|
||||||
const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
|
const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
|
||||||
const sysNodesData = sysRes.data.system_nodes || [];
|
const sysNodesData = sysRes.data.system_nodes || [];
|
||||||
const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node'];
|
const defaultSysNodes = ['regulatory_node', 'consciousness_node'];
|
||||||
|
|
||||||
setSystemNodes(defaultSysNodes.map(nodeName => {
|
setSystemNodes(defaultSysNodes.map(nodeName => {
|
||||||
const found = sysNodesData.find((n: any) => n.node_name === nodeName);
|
const found = sysNodesData.find((n: any) => n.node_name === nodeName);
|
||||||
return {
|
return {
|
||||||
agent_id: nodeName, agent_name: nodeName, agent_type: 'System Node',
|
agent_id: nodeName, agent_name: nodeName, agent_type: 'System Node',
|
||||||
|
display_name: found?.display_name || '',
|
||||||
provider_title: found?.provider_title || defaultProvider,
|
provider_title: found?.provider_title || defaultProvider,
|
||||||
model_id: found?.model_id || '',
|
model_id: found?.model_id || '',
|
||||||
tools: found?.tools ? JSON.stringify(found.tools) : '[]',
|
tools: found?.tools ? JSON.stringify(found.tools) : '[]',
|
||||||
|
custom_system_prompt: found?.custom_system_prompt || '',
|
||||||
is_system: true
|
is_system: true
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
@@ -109,7 +111,9 @@ export function WorkerIndividualSettings() {
|
|||||||
individual_name: editData.agent_name,
|
individual_name: editData.agent_name,
|
||||||
provider_title: editData.provider_title,
|
provider_title: editData.provider_title,
|
||||||
model_id: editData.model_id,
|
model_id: editData.model_id,
|
||||||
tools: JSON.parse(editData.tools || '[]')
|
tools: JSON.parse(editData.tools || '[]'),
|
||||||
|
custom_system_prompt: (editData as any).custom_system_prompt || null,
|
||||||
|
display_name: (editData as any).display_name || null
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -184,7 +188,10 @@ export function WorkerIndividualSettings() {
|
|||||||
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
||||||
<Bot size={14} className="text-accent" />
|
<Bot size={14} className="text-accent" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-text-primary text-xs">{w.agent_name}</span>
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-text-primary text-xs">{w.display_name || w.agent_name}</span>
|
||||||
|
{w.display_name && <span className="text-[10px] text-text-muted">{w.agent_name}</span>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3">{getTypeBadge(w.agent_type, true)}</td>
|
<td className="px-5 py-3">{getTypeBadge(w.agent_type, true)}</td>
|
||||||
@@ -228,9 +235,13 @@ export function WorkerIndividualSettings() {
|
|||||||
<form onSubmit={handleModalSave} className="p-5 space-y-4">
|
<form onSubmit={handleModalSave} className="p-5 space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.name')}</label>
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{(editData as any).is_system ? t('agent.displayName') : t('agent.name')}</label>
|
||||||
<input type="text" required value={editData.agent_name || ''} onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
|
<input type="text" required={!(editData as any).is_system} value={(editData as any).is_system ? ((editData as any).display_name || '') : (editData.agent_name || '')} onChange={(e) => {
|
||||||
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" disabled={(editData as any).is_system} />
|
if ((editData as any).is_system) setEditData({...editData, display_name: e.target.value} as any);
|
||||||
|
else setEditData({...editData, agent_name: e.target.value});
|
||||||
|
}}
|
||||||
|
placeholder={(editData as any).is_system ? editData.agent_name : ''}
|
||||||
|
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" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.type')}</label>
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.type')}</label>
|
||||||
@@ -267,6 +278,14 @@ export function WorkerIndividualSettings() {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{(editData as any).is_system && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.customPrompt')}</label>
|
||||||
|
<textarea value={(editData as any).custom_system_prompt || ''} onChange={(e) => setEditData({...editData, custom_system_prompt: e.target.value} as any)} rows={3}
|
||||||
|
placeholder={t('agent.customPromptPlaceholder')}
|
||||||
|
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!(editData as any).is_system && (
|
{!(editData as any).is_system && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -197,7 +197,22 @@
|
|||||||
"system": "System",
|
"system": "System",
|
||||||
"type.ordinary_individual": "Ordinary",
|
"type.ordinary_individual": "Ordinary",
|
||||||
"type.skill_individual": "Skill",
|
"type.skill_individual": "Skill",
|
||||||
"type.special_individual": "Special"
|
"type.special_individual": "Special",
|
||||||
|
"personaManagement": "Persona Management",
|
||||||
|
"personaManagementDesc": "Manage persona templates for quick worker creation",
|
||||||
|
"addTemplate": "Add Template",
|
||||||
|
"editTemplate": "Edit Template",
|
||||||
|
"deleteTemplateConfirm": "Delete this template?",
|
||||||
|
"noTemplates": "No persona templates yet",
|
||||||
|
"templateName": "Template Name",
|
||||||
|
"builtin": "Built-in",
|
||||||
|
"createFromTemplate": "Create worker from template",
|
||||||
|
"workerCreatedFromTemplate": "Worker created from template successfully",
|
||||||
|
"tags": "Tags",
|
||||||
|
"tagsPlaceholder": "Comma-separated, e.g.: assistant, translator, coder",
|
||||||
|
"customPrompt": "Custom Prompt",
|
||||||
|
"customPromptPlaceholder": "Additional content appended after the default system prompt...",
|
||||||
|
"displayName": "Display Name"
|
||||||
},
|
},
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"toolManagement": "Tool Management",
|
"toolManagement": "Tool Management",
|
||||||
|
|||||||
@@ -197,7 +197,22 @@
|
|||||||
"system": "系统",
|
"system": "系统",
|
||||||
"type.ordinary_individual": "普通",
|
"type.ordinary_individual": "普通",
|
||||||
"type.skill_individual": "技能",
|
"type.skill_individual": "技能",
|
||||||
"type.special_individual": "特殊"
|
"type.special_individual": "特殊",
|
||||||
|
"personaManagement": "人设管理",
|
||||||
|
"personaManagementDesc": "管理人设模板,快速创建工作者",
|
||||||
|
"addTemplate": "添加模板",
|
||||||
|
"editTemplate": "编辑模板",
|
||||||
|
"deleteTemplateConfirm": "确定要删除此模板吗?",
|
||||||
|
"noTemplates": "暂无人设模板",
|
||||||
|
"templateName": "模板名称",
|
||||||
|
"builtin": "内置",
|
||||||
|
"createFromTemplate": "从模板创建工作者",
|
||||||
|
"workerCreatedFromTemplate": "已从模板成功创建工作者",
|
||||||
|
"tags": "标签",
|
||||||
|
"tagsPlaceholder": "用逗号分隔,如:助手, 翻译, 代码",
|
||||||
|
"customPrompt": "附加人设",
|
||||||
|
"customPromptPlaceholder": "在默认系统提示词之后追加的自定义内容...",
|
||||||
|
"displayName": "显示名称"
|
||||||
},
|
},
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"toolManagement": "工具管理",
|
"toolManagement": "工具管理",
|
||||||
|
|||||||
+134
-13
@@ -16,7 +16,7 @@
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
from kilostar.utils.ray_hook import ray_actor_hook
|
from kilostar.utils.ray_hook import ray_actor_hook
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, field_validator
|
||||||
from kilostar.utils.access import Accessor, TokenData
|
from kilostar.utils.access import Accessor, TokenData
|
||||||
from kilostar.core.postgres_database.model import AgentType
|
from kilostar.core.postgres_database.model import AgentType
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -36,6 +36,8 @@ class AgentRegister(BaseModel):
|
|||||||
model_id: str
|
model_id: str
|
||||||
individual_name: str
|
individual_name: str
|
||||||
tools: Optional[List[str]] = None
|
tools: Optional[List[str]] = None
|
||||||
|
custom_system_prompt: Optional[str] = None
|
||||||
|
display_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class AgentLocalRegister(BaseModel):
|
class AgentLocalRegister(BaseModel):
|
||||||
@@ -50,7 +52,7 @@ class AgentLocalRegister(BaseModel):
|
|||||||
async def get_system_nodes(
|
async def get_system_nodes(
|
||||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||||
):
|
):
|
||||||
"""返回三大系统节点(regulatory/consciousness/control)当前的持久化配置。"""
|
"""返回两大系统节点(regulatory/consciousness)当前的持久化配置。"""
|
||||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||||
configs = await postgres_database.get_all_system_node_configs.remote()
|
configs = await postgres_database.get_all_system_node_configs.remote()
|
||||||
return {"system_nodes": configs}
|
return {"system_nodes": configs}
|
||||||
@@ -60,7 +62,7 @@ async def get_system_nodes(
|
|||||||
async def load_agent(
|
async def load_agent(
|
||||||
agent_register: Union[AgentRegister, AgentLocalRegister],
|
agent_register: Union[AgentRegister, AgentLocalRegister],
|
||||||
request: Request,
|
request: Request,
|
||||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
|
||||||
):
|
):
|
||||||
"""加载/重载某个系统节点的 Agent:先持久化配置,再调用对应节点 Actor 的 ``create_agent``。"""
|
"""加载/重载某个系统节点的 Agent:先持久化配置,再调用对应节点 Actor 的 ``create_agent``。"""
|
||||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||||
@@ -77,10 +79,13 @@ async def load_agent(
|
|||||||
agent_register.provider_title,
|
agent_register.provider_title,
|
||||||
agent_register.model_id,
|
agent_register.model_id,
|
||||||
agent_register.tools,
|
agent_register.tools,
|
||||||
|
agent_register.custom_system_prompt,
|
||||||
|
agent_register.display_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
scope = agent_register.individual_name
|
scope = agent_register.individual_name
|
||||||
toolsets = await get_all_toolsets_for_scope(scope)
|
toolsets = await get_all_toolsets_for_scope(scope)
|
||||||
|
custom_prompt = agent_register.custom_system_prompt
|
||||||
|
|
||||||
match scope:
|
match scope:
|
||||||
case "regulatory_node":
|
case "regulatory_node":
|
||||||
@@ -92,6 +97,7 @@ async def load_agent(
|
|||||||
agent_register.tools,
|
agent_register.tools,
|
||||||
toolsets,
|
toolsets,
|
||||||
accept_lang,
|
accept_lang,
|
||||||
|
custom_prompt,
|
||||||
)
|
)
|
||||||
case "consciousness_node":
|
case "consciousness_node":
|
||||||
node = ray_actor_hook("consciousness_node").consciousness_node
|
node = ray_actor_hook("consciousness_node").consciousness_node
|
||||||
@@ -102,16 +108,7 @@ async def load_agent(
|
|||||||
agent_register.tools,
|
agent_register.tools,
|
||||||
toolsets,
|
toolsets,
|
||||||
accept_lang,
|
accept_lang,
|
||||||
)
|
custom_prompt,
|
||||||
case "control_node":
|
|
||||||
node = ray_actor_hook("control_node").control_node
|
|
||||||
await node.create_agent.remote(
|
|
||||||
global_state_machine,
|
|
||||||
agent_register.provider_title,
|
|
||||||
agent_register.model_id,
|
|
||||||
agent_register.tools,
|
|
||||||
toolsets,
|
|
||||||
accept_lang,
|
|
||||||
)
|
)
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
@@ -122,6 +119,9 @@ async def load_agent(
|
|||||||
return {"message": "创建成功"}
|
return {"message": "创建成功"}
|
||||||
|
|
||||||
|
|
||||||
|
_VALID_AFFINITIES = {"cpu", "core", "gpu"}
|
||||||
|
|
||||||
|
|
||||||
class WorkerIndividualCreate(BaseModel):
|
class WorkerIndividualCreate(BaseModel):
|
||||||
"""``POST /worker`` 入参:创建一个 Worker Agent 所需的完整配置。"""
|
"""``POST /worker`` 入参:创建一个 Worker Agent 所需的完整配置。"""
|
||||||
|
|
||||||
@@ -135,6 +135,14 @@ class WorkerIndividualCreate(BaseModel):
|
|||||||
bound_skill: Dict[str, List[str]]
|
bound_skill: Dict[str, List[str]]
|
||||||
workspace: List[str]
|
workspace: List[str]
|
||||||
tools: Optional[List[str]] = None
|
tools: Optional[List[str]] = None
|
||||||
|
node_affinity: str = "cpu"
|
||||||
|
|
||||||
|
@field_validator("node_affinity")
|
||||||
|
@classmethod
|
||||||
|
def _check_affinity(cls, v: str) -> str:
|
||||||
|
if v not in _VALID_AFFINITIES:
|
||||||
|
raise ValueError(f"node_affinity 必须是 cpu/core/gpu,收到: {v}")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
class WorkerIndividualUpdate(BaseModel):
|
class WorkerIndividualUpdate(BaseModel):
|
||||||
@@ -150,6 +158,14 @@ class WorkerIndividualUpdate(BaseModel):
|
|||||||
bound_skill: Optional[Dict[str, List[str]]] = None
|
bound_skill: Optional[Dict[str, List[str]]] = None
|
||||||
workspace: Optional[List[str]] = None
|
workspace: Optional[List[str]] = None
|
||||||
tools: Optional[List[str]] = None
|
tools: Optional[List[str]] = None
|
||||||
|
node_affinity: Optional[str] = None
|
||||||
|
|
||||||
|
@field_validator("node_affinity")
|
||||||
|
@classmethod
|
||||||
|
def _check_affinity(cls, v: Optional[str]) -> Optional[str]:
|
||||||
|
if v is not None and v not in _VALID_AFFINITIES:
|
||||||
|
raise ValueError(f"node_affinity 必须是 cpu/core/gpu,收到: {v}")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
@agent_router.post("/worker")
|
@agent_router.post("/worker")
|
||||||
@@ -258,3 +274,108 @@ async def delete_worker_individual(
|
|||||||
)
|
)
|
||||||
await postgres_database.delete_worker_individual.remote(agent_id=agent_id)
|
await postgres_database.delete_worker_individual.remote(agent_id=agent_id)
|
||||||
return {"message": "success"}
|
return {"message": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────── Persona Template ────────────────────────────
|
||||||
|
|
||||||
|
class PersonaTemplateCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str = ""
|
||||||
|
system_prompt: str = ""
|
||||||
|
agent_type: AgentType = "ordinary"
|
||||||
|
provider_title: Optional[str] = None
|
||||||
|
model_id: Optional[str] = None
|
||||||
|
tools: Optional[List[str]] = None
|
||||||
|
tags: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PersonaTemplateUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
system_prompt: Optional[str] = None
|
||||||
|
agent_type: Optional[AgentType] = None
|
||||||
|
provider_title: Optional[str] = None
|
||||||
|
model_id: Optional[str] = None
|
||||||
|
tools: Optional[List[str]] = None
|
||||||
|
tags: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@agent_router.get("/template")
|
||||||
|
async def list_templates(
|
||||||
|
include_builtin: bool = True,
|
||||||
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
||||||
|
):
|
||||||
|
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||||
|
templates = await postgres_database.list_templates.remote(
|
||||||
|
owner_id=token_data.user_id, include_builtin=include_builtin
|
||||||
|
)
|
||||||
|
return {"templates": templates}
|
||||||
|
|
||||||
|
|
||||||
|
@agent_router.post("/template")
|
||||||
|
async def create_template(
|
||||||
|
data: PersonaTemplateCreate,
|
||||||
|
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||||
|
):
|
||||||
|
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||||
|
tpl = await postgres_database.add_template.remote(
|
||||||
|
**data.model_dump(), owner_id=token_data.user_id, is_builtin=False
|
||||||
|
)
|
||||||
|
return {"message": "success", "template_id": tpl.template_id}
|
||||||
|
|
||||||
|
|
||||||
|
@agent_router.put("/template/{template_id}")
|
||||||
|
async def update_template(
|
||||||
|
template_id: str,
|
||||||
|
data: PersonaTemplateUpdate,
|
||||||
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
||||||
|
):
|
||||||
|
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||||
|
tpl = await postgres_database.get_template.remote(template_id)
|
||||||
|
if not tpl:
|
||||||
|
raise HTTPException(status_code=404, detail="Template not found")
|
||||||
|
if not tpl.is_builtin and tpl.owner_id != token_data.user_id:
|
||||||
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
updated = await postgres_database.update_template.remote(
|
||||||
|
template_id, **data.model_dump(exclude_unset=True)
|
||||||
|
)
|
||||||
|
return {"message": "success", "template": updated}
|
||||||
|
|
||||||
|
|
||||||
|
@agent_router.delete("/template/{template_id}")
|
||||||
|
async def delete_template(
|
||||||
|
template_id: str,
|
||||||
|
token_data: TokenData = Depends(Accessor.get_current_user),
|
||||||
|
):
|
||||||
|
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||||
|
tpl = await postgres_database.get_template.remote(template_id)
|
||||||
|
if not tpl:
|
||||||
|
raise HTTPException(status_code=404, detail="Template not found")
|
||||||
|
if tpl.is_builtin or tpl.owner_id != token_data.user_id:
|
||||||
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
await postgres_database.delete_template.remote(template_id)
|
||||||
|
return {"message": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@agent_router.post("/worker/from-template/{template_id}")
|
||||||
|
async def create_worker_from_template(
|
||||||
|
template_id: str,
|
||||||
|
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||||
|
):
|
||||||
|
"""从人设模板快速创建一个 Worker Agent,字段直接从模板复制。"""
|
||||||
|
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||||
|
tpl = await postgres_database.get_template.remote(template_id)
|
||||||
|
if not tpl:
|
||||||
|
raise HTTPException(status_code=404, detail="Template not found")
|
||||||
|
worker = await postgres_database.add_worker_individual.remote(
|
||||||
|
agent_name=tpl.name,
|
||||||
|
agent_type=tpl.agent_type,
|
||||||
|
description=tpl.description,
|
||||||
|
system_prompt=tpl.system_prompt,
|
||||||
|
provider_title=tpl.provider_title or "",
|
||||||
|
model_id=tpl.model_id or "",
|
||||||
|
tools=tpl.tools or [],
|
||||||
|
owner_id=token_data.user_id,
|
||||||
|
template_origin_id=template_id,
|
||||||
|
)
|
||||||
|
return {"message": "success", "agent_id": worker.agent_id}
|
||||||
|
|||||||
@@ -13,10 +13,8 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String, Text
|
||||||
from sqlalchemy.dialects.postgresql import (
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
JSONB,
|
|
||||||
) # 针对 Postgres 优化,支持索引和高性能解析
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from .base import BaseDataModel
|
from .base import BaseDataModel
|
||||||
|
|
||||||
@@ -29,8 +27,14 @@ class SystemNodeConfigModel(BaseDataModel):
|
|||||||
|
|
||||||
__tablename__ = "system_node_config"
|
__tablename__ = "system_node_config"
|
||||||
node_name: Mapped[str] = mapped_column(String(100), primary_key=True)
|
node_name: Mapped[str] = mapped_column(String(100), primary_key=True)
|
||||||
|
display_name: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(100), nullable=True, comment="管理员可自定义的显示名称,用于前端展示"
|
||||||
|
)
|
||||||
provider_title: Mapped[str] = mapped_column(String(50), nullable=False)
|
provider_title: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
model_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
model_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
tools: Mapped[Optional[List[str]]] = mapped_column(
|
tools: Mapped[Optional[List[str]]] = mapped_column(
|
||||||
JSONB, default=list, comment="节点可调用的工具标识列表"
|
JSONB, default=list, comment="节点可调用的工具标识列表"
|
||||||
)
|
)
|
||||||
|
custom_system_prompt: Mapped[Optional[str]] = mapped_column(
|
||||||
|
Text, nullable=True, comment="管理员自定义追加的提示词,拼接在默认 system prompt 之后"
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from kilostar.core.postgres_database.database_exception import database_exceptio
|
|||||||
|
|
||||||
|
|
||||||
class SystemNodeDatabase:
|
class SystemNodeDatabase:
|
||||||
"""SystemNodeConfig 表的 DAO:管理 control/consciousness/regulatory 等系统节点的模型配置。"""
|
"""SystemNodeConfig 表的 DAO:管理 consciousness/regulatory 等系统节点的模型配置。"""
|
||||||
|
|
||||||
def __init__(self, async_session_maker):
|
def __init__(self, async_session_maker):
|
||||||
self.async_session_maker = async_session_maker
|
self.async_session_maker = async_session_maker
|
||||||
@@ -31,8 +31,10 @@ class SystemNodeDatabase:
|
|||||||
provider_title: str,
|
provider_title: str,
|
||||||
model_id: str,
|
model_id: str,
|
||||||
tools: Optional[List[str]] = None,
|
tools: Optional[List[str]] = None,
|
||||||
|
custom_system_prompt: Optional[str] = None,
|
||||||
|
display_name: Optional[str] = None,
|
||||||
) -> SystemNodeConfigModel:
|
) -> SystemNodeConfigModel:
|
||||||
"""按 node_name 插入或更新一个系统节点的模型配置(Provider + 模型 ID + 工具列表)。"""
|
"""按 node_name 插入或更新一个系统节点的模型配置。"""
|
||||||
async with self.async_session_maker() as session:
|
async with self.async_session_maker() as session:
|
||||||
statement = select(SystemNodeConfigModel).where(
|
statement = select(SystemNodeConfigModel).where(
|
||||||
SystemNodeConfigModel.node_name == node_name
|
SystemNodeConfigModel.node_name == node_name
|
||||||
@@ -44,12 +46,18 @@ class SystemNodeDatabase:
|
|||||||
config.model_id = model_id
|
config.model_id = model_id
|
||||||
if tools is not None:
|
if tools is not None:
|
||||||
config.tools = tools
|
config.tools = tools
|
||||||
|
if custom_system_prompt is not None:
|
||||||
|
config.custom_system_prompt = custom_system_prompt
|
||||||
|
if display_name is not None:
|
||||||
|
config.display_name = display_name
|
||||||
else:
|
else:
|
||||||
config = SystemNodeConfigModel(
|
config = SystemNodeConfigModel(
|
||||||
node_name=node_name,
|
node_name=node_name,
|
||||||
provider_title=provider_title,
|
provider_title=provider_title,
|
||||||
model_id=model_id,
|
model_id=model_id,
|
||||||
tools=tools,
|
tools=tools,
|
||||||
|
custom_system_prompt=custom_system_prompt,
|
||||||
|
display_name=display_name,
|
||||||
)
|
)
|
||||||
session.add(config)
|
session.add(config)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import os
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from kilostar.utils.standalone_proxy import actor_class
|
from kilostar.utils.standalone_proxy import actor_class
|
||||||
|
from kilostar.utils.settings import get_settings
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from kilostar.core.postgres_database.model.base import BaseDataModel
|
from kilostar.core.postgres_database.model.base import BaseDataModel
|
||||||
@@ -42,6 +43,7 @@ from kilostar.core.postgres_database.model.mcp_server import MCPServerModel
|
|||||||
from kilostar.core.postgres_database.model.tool_config import ToolConfigModel
|
from kilostar.core.postgres_database.model.tool_config import ToolConfigModel
|
||||||
from kilostar.core.postgres_database.model.custom_toolset import CustomToolsetModel
|
from kilostar.core.postgres_database.model.custom_toolset import CustomToolsetModel
|
||||||
from kilostar.core.postgres_database.model.system_event_log import SystemEventLog
|
from kilostar.core.postgres_database.model.system_event_log import SystemEventLog
|
||||||
|
from kilostar.core.postgres_database.model.persona_template import PersonaTemplate
|
||||||
|
|
||||||
from .module.individual import IndividualDatabase
|
from .module.individual import IndividualDatabase
|
||||||
from .module.user import AuthDatabase
|
from .module.user import AuthDatabase
|
||||||
@@ -53,6 +55,7 @@ from .module.mcp_server import MCPServerDatabase
|
|||||||
from .module.tool_config import ToolConfigDatabase
|
from .module.tool_config import ToolConfigDatabase
|
||||||
from .module.custom_toolset import CustomToolsetDatabase
|
from .module.custom_toolset import CustomToolsetDatabase
|
||||||
from .module.system_event_log import SystemEventLogDatabase
|
from .module.system_event_log import SystemEventLogDatabase
|
||||||
|
from .module.persona_template import PersonaTemplateDatabase
|
||||||
|
|
||||||
|
|
||||||
@actor_class
|
@actor_class
|
||||||
@@ -65,13 +68,10 @@ class PostgresDatabase:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
user = os.environ.get("POSTGRES_USER")
|
_s = get_settings().db
|
||||||
password = os.environ.get("POSTGRES_PASSWORD")
|
|
||||||
host = os.environ.get("POSTGRES_HOST")
|
|
||||||
port = os.environ.get("POSTGRES_PORT")
|
|
||||||
database = os.environ.get("POSTGRES_DB")
|
|
||||||
database_url = (
|
database_url = (
|
||||||
f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{database}"
|
f"postgresql+asyncpg://{_s.postgres_user}:{_s.postgres_password}"
|
||||||
|
f"@{_s.postgres_host}:{_s.postgres_port}/{_s.postgres_db}"
|
||||||
)
|
)
|
||||||
self.async_engine = create_async_engine(database_url, echo=True)
|
self.async_engine = create_async_engine(database_url, echo=True)
|
||||||
self.async_session_maker = sessionmaker(
|
self.async_session_maker = sessionmaker(
|
||||||
@@ -88,6 +88,7 @@ class PostgresDatabase:
|
|||||||
self._tool_config_database = ToolConfigDatabase(self.async_session_maker)
|
self._tool_config_database = ToolConfigDatabase(self.async_session_maker)
|
||||||
self._custom_toolset_database = CustomToolsetDatabase(self.async_session_maker)
|
self._custom_toolset_database = CustomToolsetDatabase(self.async_session_maker)
|
||||||
self._system_event_log_database = SystemEventLogDatabase(self.async_session_maker)
|
self._system_event_log_database = SystemEventLogDatabase(self.async_session_maker)
|
||||||
|
self._persona_template_database = PersonaTemplateDatabase(self.async_session_maker)
|
||||||
|
|
||||||
self.ready_event = asyncio.Event()
|
self.ready_event = asyncio.Event()
|
||||||
|
|
||||||
@@ -182,11 +183,13 @@ class PostgresDatabase:
|
|||||||
provider_title: str,
|
provider_title: str,
|
||||||
model_id: str,
|
model_id: str,
|
||||||
tools: list[str] = None,
|
tools: list[str] = None,
|
||||||
|
custom_system_prompt: str = None,
|
||||||
|
display_name: str = None,
|
||||||
):
|
):
|
||||||
"""插入或更新某个系统节点(如 control/consciousness/regulatory)的模型配置。"""
|
"""插入或更新某个系统节点(如 consciousness/regulatory)的模型配置。"""
|
||||||
await self.ready_event.wait()
|
await self.ready_event.wait()
|
||||||
return await self._system_node_database.upsert_system_node_config(
|
return await self._system_node_database.upsert_system_node_config(
|
||||||
node_name, provider_title, model_id, tools
|
node_name, provider_title, model_id, tools, custom_system_prompt, display_name
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_all_system_node_configs(self):
|
async def get_all_system_node_configs(self):
|
||||||
@@ -410,3 +413,24 @@ class PostgresDatabase:
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Persona Template Database Methods
|
||||||
|
async def add_template(self, **kwargs):
|
||||||
|
await self.ready_event.wait()
|
||||||
|
return await self._persona_template_database.add_template(**kwargs)
|
||||||
|
|
||||||
|
async def get_template(self, template_id: str):
|
||||||
|
await self.ready_event.wait()
|
||||||
|
return await self._persona_template_database.get_template(template_id)
|
||||||
|
|
||||||
|
async def list_templates(self, owner_id: str = None, include_builtin: bool = True):
|
||||||
|
await self.ready_event.wait()
|
||||||
|
return await self._persona_template_database.list_templates(owner_id, include_builtin)
|
||||||
|
|
||||||
|
async def update_template(self, template_id: str, **kwargs):
|
||||||
|
await self.ready_event.wait()
|
||||||
|
return await self._persona_template_database.update_template(template_id, **kwargs)
|
||||||
|
|
||||||
|
async def delete_template(self, template_id: str):
|
||||||
|
await self.ready_event.wait()
|
||||||
|
return await self._persona_template_database.delete_template(template_id)
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ from kilostar.core.global_state_machine import GlobalStateMachine
|
|||||||
from kilostar.core.global_workflow_manager import GlobalWorkflowManager
|
from kilostar.core.global_workflow_manager import GlobalWorkflowManager
|
||||||
from kilostar.core.individual.regulatory_node import RegulatoryNode
|
from kilostar.core.individual.regulatory_node import RegulatoryNode
|
||||||
from kilostar.core.individual.consciousness_node import ConsciousnessNode
|
from kilostar.core.individual.consciousness_node import ConsciousnessNode
|
||||||
from kilostar.core.individual.control_node import ControlNode
|
|
||||||
|
|
||||||
if KILOSTAR_MODE != "standalone":
|
if KILOSTAR_MODE != "standalone":
|
||||||
import ray
|
import ray
|
||||||
@@ -73,12 +72,13 @@ async def start_standalone():
|
|||||||
consciousness_node = ConsciousnessNode()
|
consciousness_node = ConsciousnessNode()
|
||||||
register_standalone("consciousness_node", consciousness_node)
|
register_standalone("consciousness_node", consciousness_node)
|
||||||
|
|
||||||
control_node = ControlNode()
|
worker_cluster = WorkerCluster(node_type="cpu")
|
||||||
register_standalone("control_node", control_node)
|
|
||||||
|
|
||||||
worker_cluster = WorkerCluster()
|
|
||||||
await worker_cluster.start()
|
await worker_cluster.start()
|
||||||
register_standalone("worker_cluster", worker_cluster)
|
register_standalone("worker_cluster", worker_cluster)
|
||||||
|
# 单机模式三个标签共用同一实例
|
||||||
|
register_standalone("worker_cluster_cpu", worker_cluster)
|
||||||
|
register_standalone("worker_cluster_core", worker_cluster)
|
||||||
|
register_standalone("worker_cluster_gpu", worker_cluster)
|
||||||
|
|
||||||
print(f"✅ KiloStar 单机模式启动完成,监听 0.0.0.0:8000")
|
print(f"✅ KiloStar 单机模式启动完成,监听 0.0.0.0:8000")
|
||||||
|
|
||||||
@@ -129,15 +129,22 @@ async def start_distributed():
|
|||||||
|
|
||||||
RegulatoryNode.options(name="regulatory_node").remote()
|
RegulatoryNode.options(name="regulatory_node").remote()
|
||||||
ConsciousnessNode.options(name="consciousness_node").remote()
|
ConsciousnessNode.options(name="consciousness_node").remote()
|
||||||
ControlNode.options(name="control_node").remote()
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
for node_type in ("cpu", "core", "gpu"):
|
||||||
|
actor_name = f"worker_cluster_{node_type}"
|
||||||
|
resource_key = f"kilostar_node_{node_type}"
|
||||||
try:
|
try:
|
||||||
WorkerCluster.options(
|
WorkerCluster.options(
|
||||||
name="worker_cluster", lifetime="detached"
|
name=actor_name,
|
||||||
).remote()
|
lifetime="detached",
|
||||||
print("✅ WorkerCluster 已成功启动并注册!")
|
resources={resource_key: 1},
|
||||||
|
).remote(node_type=node_type)
|
||||||
|
print(f"✅ WorkerCluster[{node_type}] 已成功启动并注册!")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("WorkerCluster 已经存在。")
|
print(f"WorkerCluster[{node_type}] 已经存在。")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WorkerCluster 启动失败: {e}")
|
||||||
|
|
||||||
print("正在等待 GlobalWorkflowManager 初始化与恢复工作流...")
|
print("正在等待 GlobalWorkflowManager 初始化与恢复工作流...")
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user