chore: initial commit for Pretor v0.1.0-alpha

正式发布 Pretor 平台的首个 alpha 版本。本项目旨在构建一个基于分布式架构的多智能体协同工作流水线。

核心功能实现:
1. 建立基于 BaseIndividual 的动态插件加载机制。
2. 实现三类核心 worker_individual 子个体。
3. 集成 Ray 框架支持分布式集群调度。
4. 基于 PostgreSQL 的全量持久化存储方案。
5. 提供完整的 FastAPI 后端与 React 前端交互界面。
This commit is contained in:
2026-04-29 10:09:07 +08:00
commit d84212f780
163 changed files with 19251 additions and 0 deletions
@@ -0,0 +1,452 @@
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import { Save, Plus, Edit2, Trash2, X } from 'lucide-react';
import type { Provider } from '../../types';
interface WorkerIndividual {
agent_id: string;
agent_name: string;
agent_type: string;
description?: string;
provider_title: string;
model_id: string;
system_prompt?: string;
output_template?: string; // Change to string for the form state
bound_skill?: string; // Change to string for the form state
workspace?: string; // Change to string for the form state
tools?: string; // Form state for tools JSON array
}
export function WorkerIndividualSettings() {
const [providers, setProviders] = useState<Provider[]>([]);
const [workers, setWorkers] = useState<WorkerIndividual[]>([]);
const [systemNodes, setSystemNodes] = useState<any[]>([]);
const [availableSkills, setAvailableSkills] = useState<string[]>([]);
const [availableTools, setAvailableTools] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<Partial<WorkerIndividual>>({});
const [isNew, setIsNew] = useState(false);
const [modalMessage, setModalMessage] = useState('');
const fetchData = async () => {
setLoading(true);
try {
const [provRes, workRes, sysRes, toolsRes, skillsRes] = await Promise.all([
apiClient.get('/api/v1/provider/list'),
apiClient.get('/api/v1/agent/worker'),
apiClient.get('/api/v1/agent'),
apiClient.get('/api/v1/resource/tool'),
apiClient.get('/api/v1/resource/skill')
]);
setProviders(Object.values(provRes.data.provider_list || {}));
setWorkers(workRes.data.workers || []);
const allTools = toolsRes.data.tools || [];
setAvailableTools(allTools);
setAvailableSkills(Object.keys(skillsRes.data.skills || {}));
const sysNodesData = sysRes.data.system_nodes || [];
const defaultSysNodes = ['supervisory_node', 'consciousness_node', 'control_node'];
const providersList = Object.values(provRes.data.provider_list || {}) as Provider[];
const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
const formattedSysNodes = defaultSysNodes.map(nodeName => {
const found = sysNodesData.find((n: any) => n.node_name === nodeName);
return {
agent_id: nodeName,
agent_name: nodeName,
agent_type: 'System Node',
provider_title: found && found.provider_title ? found.provider_title : defaultProvider,
model_id: found && found.model_id ? found.model_id : '',
tools: found && found.tools ? JSON.stringify(found.tools) : '[]',
is_system: true
};
});
setSystemNodes(formattedSysNodes);
} catch (err: any) {
console.error(err);
setError('Failed to load data');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleEdit = (worker: any) => { // Accept the backend object which might have objects instead of strings
setEditData({
...worker,
output_template: typeof worker.output_template === 'string' ? worker.output_template : JSON.stringify(worker.output_template || {}),
bound_skill: typeof worker.bound_skill === 'string' ? worker.bound_skill : JSON.stringify(worker.bound_skill || {}),
workspace: typeof worker.workspace === 'string' ? worker.workspace : JSON.stringify(worker.workspace || []),
tools: typeof worker.tools === 'string' ? worker.tools : JSON.stringify(worker.tools || [])
});
setIsNew(false);
setIsEditing(true);
setModalMessage('');
};
const handleAddNew = () => {
setEditData({
agent_name: '',
agent_type: 'ordinary_individual',
description: '',
provider_title: providers.length > 0 ? providers[0].provider_title : '',
model_id: '',
system_prompt: '',
output_template: '{}',
bound_skill: '{}',
workspace: '[]',
tools: '[]'
});
setIsNew(true);
setIsEditing(true);
setModalMessage('');
};
const handleDelete = async (agent_id: string) => {
if (!confirm('Are you sure you want to delete this agent?')) return;
try {
await apiClient.delete(`/api/v1/agent/worker/${agent_id}`);
fetchData();
} catch (err: any) {
console.error(err);
alert('Failed to delete agent');
}
};
const handleModalSave = async (e: React.FormEvent) => {
e.preventDefault();
setModalMessage('');
try {
if ((editData as any).is_system) {
const payload = {
individual_name: editData.agent_name,
provider_title: editData.provider_title,
model_id: editData.model_id,
tools: JSON.parse(editData.tools || '[]')
};
await apiClient.post('/api/v1/agent', payload);
} else {
const payload = {
...editData,
output_template: JSON.parse(editData.output_template || '{}'),
bound_skill: JSON.parse(editData.bound_skill || '{}'),
workspace: JSON.parse(editData.workspace || '[]'),
tools: JSON.parse(editData.tools || '[]')
};
if (isNew) {
await apiClient.post('/api/v1/agent/worker', payload);
} else {
await apiClient.put(`/api/v1/agent/worker/${editData.agent_id}`, payload);
}
}
setIsEditing(false);
fetchData();
} catch (err: any) {
console.error(err);
setModalMessage(err.response?.data?.detail || err.message || 'Failed to save');
}
};
return (
<div className="max-w-5xl space-y-6 relative">
<div className="mb-8 flex justify-between items-end">
<div>
<h1 className="text-2xl font-bold text-slate-800">Individual</h1>
<p className="text-slate-500 mt-1">Manage all system nodes and custom workers.</p>
</div>
<button
onClick={handleAddNew}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
<Plus size={16} className="mr-2" />
Add Worker
</button>
</div>
{error && <div className="text-red-600">{error}</div>}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-0">
{loading ? (
<div className="p-6 text-slate-500">Loading...</div>
) : (workers.length === 0 && systemNodes.length === 0) ? (
<div className="p-6 text-slate-500">No individuals found.</div>
) : (
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 border-b border-slate-200 text-slate-600 text-sm">
<th className="p-4 font-semibold">Name</th>
<th className="p-4 font-semibold">Type</th>
<th className="p-4 font-semibold">Provider / Model ID</th>
<th className="p-4 font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody>
{systemNodes.map((w) => (
<tr key={w.agent_id} className="border-b border-slate-100 bg-slate-50 hover:bg-slate-100 transition-colors">
<td className="p-4 font-medium text-slate-800">{w.agent_name}</td>
<td className="p-4 text-slate-600">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{w.agent_type}</span>
</td>
<td className="p-4 text-slate-600 text-sm">
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
</td>
<td className="p-4 text-right space-x-2">
<button onClick={() => handleEdit(w)} className="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors" title="Edit">
<Edit2 size={16} />
</button>
</td>
</tr>
))}
{workers.map((w) => (
<tr key={w.agent_id} className="border-b border-slate-100 hover:bg-slate-50 transition-colors">
<td className="p-4 font-medium text-slate-800">{w.agent_name}</td>
<td className="p-4 text-slate-600">
<span className="px-2 py-1 bg-slate-100 rounded text-xs">{w.agent_type}</span>
</td>
<td className="p-4 text-slate-600 text-sm">
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
</td>
<td className="p-4 text-right space-x-2">
<button onClick={() => handleEdit(w)} className="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors" title="Edit">
<Edit2 size={16} />
</button>
<button onClick={() => handleDelete(w.agent_id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete">
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
{/* Edit/Create Modal */}
{isEditing && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-slate-100 sticky top-0 bg-white z-10">
<h2 className="text-xl font-bold text-slate-800">
{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}
</h2>
<button onClick={() => setIsEditing(false)} className="text-slate-400 hover:text-slate-600">
<X size={24} />
</button>
</div>
<form onSubmit={handleModalSave} className="p-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Name</label>
<input
type="text"
required
value={editData.agent_name || ''}
onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
disabled={(editData as any).is_system}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Type</label>
<select
value={editData.agent_type || 'ordinary_individual'}
onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
disabled={(editData as any).is_system}
>
<option value="ordinary_individual">Ordinary Individual</option>
<option value="skill_individual">Skill Individual</option>
<option value="special_individual">Special Individual</option>
{(editData as any).is_system && (
<option value="System Node">System Node</option>
)}
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
<select
value={editData.provider_title || ''}
onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
required
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
>
<option value="" disabled>Select Provider</option>
{providers.map((p) => (
<option key={p.provider_title} value={p.provider_title}>{p.provider_title}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Model ID</label>
{(() => {
const selectedProvider = providers.find(p => p.provider_title === editData.provider_title);
const models = selectedProvider?.provider_models || [];
return (
<select
value={editData.model_id || ''}
onChange={(e) => setEditData({...editData, model_id: e.target.value})}
required
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
>
<option value="" disabled>Select a model</option>
{models.map(m => <option key={m} value={m}>{m}</option>)}
</select>
);
})()}
</div>
</div>
{!(editData as any).is_system && (
<>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Description</label>
<textarea
value={editData.description || ''}
onChange={(e) => setEditData({...editData, description: e.target.value})}
rows={2}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">System Prompt</label>
<textarea
value={editData.system_prompt || ''}
onChange={(e) => setEditData({...editData, system_prompt: e.target.value})}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Output Template (JSON)</label>
<textarea
value={editData.output_template || '{}'}
onChange={(e) => setEditData({...editData, output_template: e.target.value})}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Bound Skill (Select)</label>
<select
value={(() => {
try {
const parsed = JSON.parse(editData.bound_skill || '{}');
return Object.keys(parsed)[0] || '';
} catch { return ''; }
})()}
onChange={(e) => {
const val = e.target.value;
const newSkill = val ? { [val]: [] } : {};
setEditData({...editData, bound_skill: JSON.stringify(newSkill)});
}}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
disabled={editData.agent_type !== 'skill_individual'}
>
<option value="">No Skill Bound</option>
{availableSkills.map(skill => (
<option key={skill} value={skill}>{skill}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Workspace (JSON Array)</label>
<textarea
value={editData.workspace || '[]'}
onChange={(e) => setEditData({...editData, workspace: e.target.value})}
rows={2}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
/>
</div>
</>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Tools (Select Multiple)</label>
<div className="flex flex-wrap gap-2 p-4 border border-slate-200 rounded-lg max-h-48 overflow-y-auto">
{availableTools.map(tool => {
let currentTools: string[] = [];
try {
currentTools = JSON.parse(editData.tools || '[]');
} catch { currentTools = []; }
const isSelected = currentTools.includes(tool);
return (
<button
key={tool}
type="button"
onClick={() => {
let updatedTools = [...currentTools];
if (isSelected) {
updatedTools = updatedTools.filter(t => t !== tool);
} else {
updatedTools.push(tool);
}
setEditData({...editData, tools: JSON.stringify(updatedTools)});
}}
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
isSelected
? 'bg-indigo-100 text-indigo-700 border border-indigo-200'
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100'
}`}
>
{tool}
</button>
);
})}
{availableTools.length === 0 && (
<span className="text-sm text-slate-500">No tools available</span>
)}
</div>
</div>
{modalMessage && (
<div className="p-3 bg-red-50 text-red-700 text-sm rounded-lg">
{modalMessage}
</div>
)}
<div className="pt-4 flex justify-end space-x-3 border-t border-slate-100">
<button
type="button"
onClick={() => setIsEditing(false)}
className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
<Save size={16} className="mr-2" />
Save Worker
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}