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:
2026-06-04 06:03:28 +00:00
parent 9a7a5edd6e
commit f3a92a793e
11 changed files with 567 additions and 47 deletions
@@ -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 { 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() {
</div>
<div className="flex-1 overflow-y-auto p-8">
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
{innerAgentTab === 'persona' && <PersonaTemplateSettings />}
{innerAgentTab === 'providers' && <ProvidersSettings />}
{innerAgentTab === 'config' && <WorkflowConfigSettings />}
{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 defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
const sysNodesData = sysRes.data.system_nodes || [];
const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node'];
const defaultSysNodes = ['regulatory_node', 'consciousness_node'];
setSystemNodes(defaultSysNodes.map(nodeName => {
const found = sysNodesData.find((n: any) => n.node_name === nodeName);
return {
agent_id: nodeName, agent_name: nodeName, agent_type: 'System Node',
display_name: found?.display_name || '',
provider_title: found?.provider_title || defaultProvider,
model_id: found?.model_id || '',
tools: found?.tools ? JSON.stringify(found.tools) : '[]',
custom_system_prompt: found?.custom_system_prompt || '',
is_system: true
};
}));
@@ -109,7 +111,9 @@ export function WorkerIndividualSettings() {
individual_name: editData.agent_name,
provider_title: editData.provider_title,
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 {
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">
<Bot size={14} className="text-accent" />
</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>
</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">
<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.name')}</label>
<input type="text" required value={editData.agent_name || ''} onChange={(e) => setEditData({...editData, agent_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" disabled={(editData as any).is_system} />
<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={!(editData as any).is_system} value={(editData as any).is_system ? ((editData as any).display_name || '') : (editData.agent_name || '')} onChange={(e) => {
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>
<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>
{(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 && (
<>
<div>
+16 -1
View File
@@ -197,7 +197,22 @@
"system": "System",
"type.ordinary_individual": "Ordinary",
"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": {
"toolManagement": "Tool Management",
+16 -1
View File
@@ -197,7 +197,22 @@
"system": "系统",
"type.ordinary_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": {
"toolManagement": "工具管理",
+134 -13
View File
@@ -16,7 +16,7 @@
from typing import Union
from kilostar.utils.ray_hook import ray_actor_hook
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.core.postgres_database.model import AgentType
from fastapi import HTTPException
@@ -36,6 +36,8 @@ class AgentRegister(BaseModel):
model_id: str
individual_name: str
tools: Optional[List[str]] = None
custom_system_prompt: Optional[str] = None
display_name: Optional[str] = None
class AgentLocalRegister(BaseModel):
@@ -50,7 +52,7 @@ class AgentLocalRegister(BaseModel):
async def get_system_nodes(
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""返回大系统节点(regulatory/consciousness/control)当前的持久化配置。"""
"""返回大系统节点(regulatory/consciousness)当前的持久化配置。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
configs = await postgres_database.get_all_system_node_configs.remote()
return {"system_nodes": configs}
@@ -60,7 +62,7 @@ async def get_system_nodes(
async def load_agent(
agent_register: Union[AgentRegister, AgentLocalRegister],
request: Request,
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)),
):
"""加载/重载某个系统节点的 Agent:先持久化配置,再调用对应节点 Actor 的 ``create_agent``。"""
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.model_id,
agent_register.tools,
agent_register.custom_system_prompt,
agent_register.display_name,
)
scope = agent_register.individual_name
toolsets = await get_all_toolsets_for_scope(scope)
custom_prompt = agent_register.custom_system_prompt
match scope:
case "regulatory_node":
@@ -92,6 +97,7 @@ async def load_agent(
agent_register.tools,
toolsets,
accept_lang,
custom_prompt,
)
case "consciousness_node":
node = ray_actor_hook("consciousness_node").consciousness_node
@@ -102,16 +108,7 @@ async def load_agent(
agent_register.tools,
toolsets,
accept_lang,
)
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,
custom_prompt,
)
case _:
pass
@@ -122,6 +119,9 @@ async def load_agent(
return {"message": "创建成功"}
_VALID_AFFINITIES = {"cpu", "core", "gpu"}
class WorkerIndividualCreate(BaseModel):
"""``POST /worker`` 入参:创建一个 Worker Agent 所需的完整配置。"""
@@ -135,6 +135,14 @@ class WorkerIndividualCreate(BaseModel):
bound_skill: Dict[str, List[str]]
workspace: List[str]
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):
@@ -150,6 +158,14 @@ class WorkerIndividualUpdate(BaseModel):
bound_skill: Optional[Dict[str, List[str]]] = None
workspace: 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")
@@ -258,3 +274,108 @@ async def delete_worker_individual(
)
await postgres_database.delete_worker_individual.remote(agent_id=agent_id)
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.
from typing import List, Optional
from sqlalchemy import String
from sqlalchemy.dialects.postgresql import (
JSONB,
) # 针对 Postgres 优化,支持索引和高性能解析
from sqlalchemy import String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from .base import BaseDataModel
@@ -29,8 +27,14 @@ class SystemNodeConfigModel(BaseDataModel):
__tablename__ = "system_node_config"
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)
model_id: Mapped[str] = mapped_column(String(100), nullable=False)
tools: Mapped[Optional[List[str]]] = mapped_column(
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:
"""SystemNodeConfig 表的 DAO:管理 control/consciousness/regulatory 等系统节点的模型配置。"""
"""SystemNodeConfig 表的 DAO:管理 consciousness/regulatory 等系统节点的模型配置。"""
def __init__(self, async_session_maker):
self.async_session_maker = async_session_maker
@@ -31,8 +31,10 @@ class SystemNodeDatabase:
provider_title: str,
model_id: str,
tools: Optional[List[str]] = None,
custom_system_prompt: Optional[str] = None,
display_name: Optional[str] = None,
) -> SystemNodeConfigModel:
"""按 node_name 插入或更新一个系统节点的模型配置Provider + 模型 ID + 工具列表)"""
"""按 node_name 插入或更新一个系统节点的模型配置。"""
async with self.async_session_maker() as session:
statement = select(SystemNodeConfigModel).where(
SystemNodeConfigModel.node_name == node_name
@@ -44,12 +46,18 @@ class SystemNodeDatabase:
config.model_id = model_id
if tools is not None:
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:
config = SystemNodeConfigModel(
node_name=node_name,
provider_title=provider_title,
model_id=model_id,
tools=tools,
custom_system_prompt=custom_system_prompt,
display_name=display_name,
)
session.add(config)
await session.commit()
+32 -8
View File
@@ -16,6 +16,7 @@ import os
import asyncio
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.orm import sessionmaker
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.custom_toolset import CustomToolsetModel
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.user import AuthDatabase
@@ -53,6 +55,7 @@ from .module.mcp_server import MCPServerDatabase
from .module.tool_config import ToolConfigDatabase
from .module.custom_toolset import CustomToolsetDatabase
from .module.system_event_log import SystemEventLogDatabase
from .module.persona_template import PersonaTemplateDatabase
@actor_class
@@ -65,13 +68,10 @@ class PostgresDatabase:
"""
def __init__(self):
user = os.environ.get("POSTGRES_USER")
password = os.environ.get("POSTGRES_PASSWORD")
host = os.environ.get("POSTGRES_HOST")
port = os.environ.get("POSTGRES_PORT")
database = os.environ.get("POSTGRES_DB")
_s = get_settings().db
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_session_maker = sessionmaker(
@@ -88,6 +88,7 @@ class PostgresDatabase:
self._tool_config_database = ToolConfigDatabase(self.async_session_maker)
self._custom_toolset_database = CustomToolsetDatabase(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()
@@ -182,11 +183,13 @@ class PostgresDatabase:
provider_title: str,
model_id: str,
tools: list[str] = None,
custom_system_prompt: str = None,
display_name: str = None,
):
"""插入或更新某个系统节点(如 control/consciousness/regulatory)的模型配置。"""
"""插入或更新某个系统节点(如 consciousness/regulatory)的模型配置。"""
await self.ready_event.wait()
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):
@@ -410,3 +413,24 @@ class PostgresDatabase:
limit=limit,
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)
+19 -12
View File
@@ -41,7 +41,6 @@ from kilostar.core.global_state_machine import GlobalStateMachine
from kilostar.core.global_workflow_manager import GlobalWorkflowManager
from kilostar.core.individual.regulatory_node import RegulatoryNode
from kilostar.core.individual.consciousness_node import ConsciousnessNode
from kilostar.core.individual.control_node import ControlNode
if KILOSTAR_MODE != "standalone":
import ray
@@ -73,12 +72,13 @@ async def start_standalone():
consciousness_node = ConsciousnessNode()
register_standalone("consciousness_node", consciousness_node)
control_node = ControlNode()
register_standalone("control_node", control_node)
worker_cluster = WorkerCluster()
worker_cluster = WorkerCluster(node_type="cpu")
await worker_cluster.start()
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")
@@ -129,15 +129,22 @@ async def start_distributed():
RegulatoryNode.options(name="regulatory_node").remote()
ConsciousnessNode.options(name="consciousness_node").remote()
ControlNode.options(name="control_node").remote()
try:
WorkerCluster.options(
name="worker_cluster", lifetime="detached"
).remote()
print("✅ WorkerCluster 已成功启动并注册!")
except ValueError:
print("WorkerCluster 已经存在。")
for node_type in ("cpu", "core", "gpu"):
actor_name = f"worker_cluster_{node_type}"
resource_key = f"kilostar_node_{node_type}"
try:
WorkerCluster.options(
name=actor_name,
lifetime="detached",
resources={resource_key: 1},
).remote(node_type=node_type)
print(f"✅ WorkerCluster[{node_type}] 已成功启动并注册!")
except ValueError:
print(f"WorkerCluster[{node_type}] 已经存在。")
except Exception as e:
print(f"WorkerCluster 启动失败: {e}")
print("正在等待 GlobalWorkflowManager 初始化与恢复工作流...")
try: