# Conflicts:
#	frontend/src/components/Agent/WorkerIndividualSettings.tsx
#	frontend/src/components/Resource/ResourceLayout.tsx
#	frontend/src/components/Resource/SkillSettings.tsx
This commit is contained in:
朝夕 2026-04-24 23:50:27 +08:00
commit 1df416ac4d
12 changed files with 396 additions and 59 deletions

2
.env
View File

@ -1,5 +1,5 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PASSWORD=postgrespassword
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_DB=pretor

View File

@ -2,23 +2,12 @@
---
## 问题栏
#### 🔴 核心缺陷与修复 (Bug Fixes & Stability)
- [x] /pretor/core/individual每个template进行优化
- [x] /pretor/worker_individual待完善复合子个体和基础子个体
#### 🛡️ 安全与合规 (Security & Auth)
- [ ] 优化安全架构防止模型注入
- [ ] 设计workflowEngine的自动扩缩容设计
- [ ] 完善错误捕获和日志系统
#### ⚡ 性能与资源优化 (Performance & Scalability)
- [ ] 增加对应全workflow的情况追踪使得在任务运行中人机交互更加自然方便
- [ ] 优化import
#### 🏗️ 架构演进 (Architecture & Refactoring)
- [x] ~~使用fastapi-users完善用户系统~~(2026/4/19 fastapi-users会严重摧毁代码的优雅性)
- [x] 升级auth功能
- [x] /pretor/api的接口函数进行重构
- [x] /dockerfile待完善
- [ ] 完善沙箱功能
- [ ] 完善爬虫功能
- [ ] 对接更多的provider

View File

@ -1,13 +1,5 @@
## Pretor项目开发
#项目规划
---
#### 全局规划:
- [ ] 实现监管模型的资源调度
- [ ] 实现子个体的工作传递
- [ ] 实现用户交互接口与ray集群的交互
- [ ] 实现监管模型调度ray资源的接口
- [ ] 实现由监管模型理解并发布,子个体向下布置任务,完成任务向上传递,监管模型检查的全工作流
---
#### 简介
**Pretor**是一款python开发实现将小模型进行微调后整理为一个大型集群从而实现低算力情况下高复杂度任务的实现。
系统模型分为以下部分:
@ -15,13 +7,6 @@
- **管控节点**:负责调度系统资源;
- **意识节点**:负责复杂任务的处理;
- **生长节点**:负责获取资源并且将基础模型训练为特化模型;
- **感知模块**与外界交互的模型如embedding模型tts模型等
- **复合子个体**:将监管节点的任务领取并进行专业的拆解任务并进行分配;
- **生产子个体**:领取任务最小单位并执行;
---
#### 短期规划
v0.1版本
- [ ] **workflow构建**:构建任务的工作流
- [ ] **接口构建**:对接vllmopenai接口和gemini接口
- [ ] **工具构建**:配置供模型调用的爬虫工具箱docker接口
- [ ] **平台对接构建**:对接telegram等消息平台
- **特殊子个体**与外界交互的模型如embedding模型tts模型等
- **专家子个体**
- **基础子个体**普通的agent对象

View File

@ -1,13 +1,14 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import { Bot, Save } from 'lucide-react';
import type { Provider } from '../../types';
function WorkerIndividualForm() {
function WorkerIndividualForm({ providers }: { providers: Provider[] }) {
const [formData, setFormData] = useState({
agent_name: '',
agent_type: 'OrdinaryIndividual',
description: '',
provider_title: '',
provider_title: providers.length > 0 ? providers[0].provider_title : '',
model_id: '',
system_prompt: '',
output_template: '{}',
@ -17,6 +18,13 @@ function WorkerIndividualForm() {
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
// Update initial provider_title when providers load
useEffect(() => {
if (providers.length > 0 && !formData.provider_title) {
setFormData(prev => ({ ...prev, provider_title: providers[0].provider_title }));
}
}, [providers, formData.provider_title]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
@ -74,7 +82,23 @@ function WorkerIndividualForm() {
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
<input required type="text" name="provider_title" value={formData.provider_title} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
<select
required
name="provider_title"
value={formData.provider_title}
onChange={handleChange}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{providers.length === 0 ? (
<option value="" disabled>No providers available. Create one first.</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>
@ -120,6 +144,25 @@ export function WorkerIndividualSettings() {
const [modelId, setModelId] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [providers, setProviders] = useState<Provider[]>([]);
useEffect(() => {
const fetchProviders = async () => {
try {
const response = await apiClient.get('/api/v1/provider/list');
const data = response.data.provider_list || {};
const providerArray: Provider[] = Object.values(data);
setProviders(providerArray);
if (providerArray.length > 0) {
setProviderTitle(providerArray[0].provider_title);
}
} catch (error) {
console.error("Failed to fetch providers", error);
setProviders([]);
}
};
fetchProviders();
}, []);
const handleCreateNode = async (e: React.FormEvent) => {
e.preventDefault();
@ -178,14 +221,22 @@ export function WorkerIndividualSettings() {
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
<input
type="text"
<select
value={providerTitle}
onChange={(e) => setProviderTitle(e.target.value)}
placeholder="e.g. openai"
required
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
>
{providers.length === 0 ? (
<option value="" disabled>No providers available. Create one first.</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>
@ -226,7 +277,7 @@ export function WorkerIndividualSettings() {
<p className="text-sm text-slate-500">Add a new custom worker to the system.</p>
</div>
<div className="p-6">
<WorkerIndividualForm />
<WorkerIndividualForm providers={providers} />
</div>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { Wrench, Database } from 'lucide-react';
import { Wrench, Database, FileCode } from 'lucide-react';
import { SkillSettings } from './SkillSettings';
import { ResourceSettings } from './ResourceSettings';
import { WorkflowTemplateSettings } from './WorkflowTemplateSettings';
interface ResourceLayoutProps {
resourceTab: string;
@ -23,6 +24,13 @@ export function ResourceLayout({ resourceTab, setResourceTab }: ResourceLayoutPr
<Wrench size={18} className="mr-3" />
Skills
</button>
<button
onClick={() => setResourceTab('workflow_template')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'workflow_template' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<FileCode size={18} className="mr-3" />
Workflow Templates
</button>
<button
onClick={() => setResourceTab('resource')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'resource' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
@ -36,6 +44,7 @@ export function ResourceLayout({ resourceTab, setResourceTab }: ResourceLayoutPr
{/* Resource Main Content */}
<div className="flex-1 overflow-y-auto p-8">
{resourceTab === 'skill' && <SkillSettings />}
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
{resourceTab === 'resource' && <ResourceSettings />}
</div>
</div>

View File

@ -1,12 +1,159 @@
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import { Download, Trash2, Plus, Box } from 'lucide-react';
export function SkillSettings() {
const [skills, setSkills] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [repoUrl, setRepoUrl] = useState('');
const [path, setPath] = useState('');
const [installing, setInstalling] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const fetchSkills = async () => {
setLoading(true);
try {
const response = await apiClient.get('/api/v1/resource/skill');
setSkills(response.data.skills || []);
} catch (err) {
console.error('Failed to fetch skills:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSkills();
}, []);
const handleInstall = async (e: React.FormEvent) => {
e.preventDefault();
if (!repoUrl) return;
setInstalling(true);
setMessage('');
setError('');
try {
await apiClient.post('/api/v1/resource/skill', {
repo_url: repoUrl,
path: path || null
});
setMessage('Skill installed successfully');
setRepoUrl('');
setPath('');
fetchSkills();
} catch (err: any) {
console.error(err);
setError(err.response?.data?.message || 'Failed to install skill');
} finally {
setInstalling(false);
}
};
const handleDelete = async (skillName: string) => {
if (!confirm(`Are you sure you want to delete ${skillName}?`)) return;
try {
await apiClient.delete(`/api/v1/resource/skill/${skillName}`);
fetchSkills();
} catch (err: any) {
console.error('Failed to delete skill:', err);
alert('Failed to delete skill');
}
};
return (
<div className="max-w-4xl space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Skill Management</h1>
<p className="text-slate-500 mt-1">Manage agent skills and functions.</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden p-6 text-slate-500 text-sm">
Skill management configuration coming soon...
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
<Download size={20} />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800">Install Skill</h2>
<p className="text-sm text-slate-500">Install a new skill from a repository.</p>
</div>
</div>
<div className="p-6">
<form onSubmit={handleInstall} className="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">Repository URL</label>
<input
type="text"
required
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/user/repo"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Path (Optional)</label>
<input
type="text"
value={path}
onChange={(e) => setPath(e.target.value)}
placeholder="e.g. subfolder/path"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
</div>
{message && <div className="text-green-600 text-sm">{message}</div>}
{error && <div className="text-red-600 text-sm">{error}</div>}
<div className="flex justify-end">
<button
type="submit"
disabled={installing}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
<Plus size={16} className="mr-2" />
{installing ? 'Installing...' : 'Install'}
</button>
</div>
</form>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Installed Skills</h2>
</div>
<div className="p-6">
{loading ? (
<div className="text-slate-500 text-sm">Loading skills...</div>
) : skills.length === 0 ? (
<div className="text-slate-500 text-sm">No skills installed yet.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{skills.map((skill) => (
<div key={skill} className="p-4 border border-slate-200 rounded-xl flex items-center justify-between hover:shadow-sm transition-shadow">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
<Box size={16} />
</div>
<span className="font-medium text-slate-800">{skill}</span>
</div>
<button
onClick={() => handleDelete(skill)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Skill"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
);

View File

@ -0,0 +1,150 @@
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import { FileCode, Trash2, Plus, LayoutTemplate } from 'lucide-react';
interface WorkflowTemplate {
name: string;
[key: string]: any;
}
export function WorkflowTemplateSettings() {
const [templates, setTemplates] = useState<Record<string, WorkflowTemplate>>({});
const [loading, setLoading] = useState(true);
const [templateJson, setTemplateJson] = useState('{\n "name": "my_template"\n}');
const [creating, setCreating] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const fetchTemplates = async () => {
setLoading(true);
try {
const response = await apiClient.get('/api/v1/resource/workflow_template');
setTemplates(response.data.templates || {});
} catch (err) {
console.error('Failed to fetch templates:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTemplates();
}, []);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setCreating(true);
setMessage('');
setError('');
try {
const parsedJson = JSON.parse(templateJson);
await apiClient.post('/api/v1/resource/workflow_template', parsedJson);
setMessage('Workflow template created successfully');
setTemplateJson('{\n "name": "my_template"\n}');
fetchTemplates();
} catch (err: any) {
console.error(err);
if (err instanceof SyntaxError) {
setError('Invalid JSON format');
} else {
setError(err.response?.data?.message || 'Failed to create workflow template');
}
} finally {
setCreating(false);
}
};
const handleDelete = async (templateName: string) => {
if (!confirm(`Are you sure you want to delete ${templateName}?`)) return;
try {
await apiClient.delete(`/api/v1/resource/workflow_template/${templateName}`);
fetchTemplates();
} catch (err: any) {
console.error('Failed to delete template:', err);
alert('Failed to delete template');
}
};
return (
<div className="max-w-4xl space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Workflow Templates</h1>
<p className="text-slate-500 mt-1">Manage and create reusable workflow templates.</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
<FileCode size={20} />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800">Create Template</h2>
<p className="text-sm text-slate-500">Provide the JSON definition for a new workflow template.</p>
</div>
</div>
<div className="p-6">
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Template JSON Definition</label>
<textarea
required
rows={8}
value={templateJson}
onChange={(e) => setTemplateJson(e.target.value)}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
/>
</div>
{message && <div className="text-green-600 text-sm">{message}</div>}
{error && <div className="text-red-600 text-sm">{error}</div>}
<div className="flex justify-end">
<button
type="submit"
disabled={creating}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
<Plus size={16} className="mr-2" />
{creating ? 'Creating...' : 'Create Template'}
</button>
</div>
</form>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Available Templates</h2>
</div>
<div className="p-6">
{loading ? (
<div className="text-slate-500 text-sm">Loading templates...</div>
) : Object.keys(templates).length === 0 ? (
<div className="text-slate-500 text-sm">No workflow templates created yet.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.keys(templates).map((name) => (
<div key={name} className="p-4 border border-slate-200 rounded-xl flex items-center justify-between hover:shadow-sm transition-shadow">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
<LayoutTemplate size={16} />
</div>
<span className="font-medium text-slate-800">{name}</span>
</div>
<button
onClick={() => handleDelete(name)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Template"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}

14
main.py
View File

@ -15,15 +15,13 @@ import os
async def start_system():
# 1. 初始化 Ray
db_host = os.getenv("POSTGRES_HOST", "db")
env_vars = {
"POSTGRES_USER": "postgres",
"POSTGRES_PASSWORD": "postgres",
"POSTGRES_HOST": db_host,
"POSTGRES_PORT": "5432",
"POSTGRES_DB": "postgres",
"SECRET_KEY": "yoursecretkey"
"POSTGRES_USER": os.getenv("POSTGRES_USER", "postgres"),
"POSTGRES_PASSWORD": os.getenv("POSTGRES_PASSWORD", ""),
"POSTGRES_HOST": os.getenv("POSTGRES_HOST", "db"),
"POSTGRES_PORT": os.getenv("POSTGRES_PORT", "5432"),
"POSTGRES_DB": os.getenv("POSTGRES_DB", "postgres"),
"SECRET_KEY": os.getenv("SECRET_KEY", "secret"),
}
ray.init(ignore_reinit_error=True,

View File

@ -13,6 +13,7 @@
# limitations under the License.
import os
import asyncio
import ray
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
@ -39,6 +40,8 @@ class PostgresDatabase:
self._provider_database = ProviderDatabase(self.async_session_maker)
self._individual_database = IndividualDatabase(self.async_session_maker)
self.ready_event = asyncio.Event()
async def init_db(self) -> None:
try:
async with self.async_engine.begin() as conn:
@ -47,15 +50,20 @@ class PostgresDatabase:
# Provide a warning if the database is not accessible, allowing
# the app to start up for development/UI tests without crashing immediately.
print(f"Warning: Failed to initialize PostgreSQL database: {e}")
finally:
self.ready_event.set()
async def auth_database(self, method_name: str, *args, **kwargs):
await self.ready_event.wait()
method = getattr(self._auth_database, method_name)
return await method(*args, **kwargs)
async def provider_database(self, method_name: str, *args, **kwargs):
await self.ready_event.wait()
method = getattr(self._provider_database, method_name)
return await method(*args, **kwargs)
async def individual_database(self, method_name: str, *args, **kwargs):
await self.ready_event.wait()
method = getattr(self._individual_database, method_name)
return await method(*args, **kwargs)

View File

@ -28,5 +28,5 @@ class Provider(SQLModel, table=True):
provider_models: List[str] = Field(sa_column=Column(JSON))
provider_owner: int
provider_owner: str
is_active: bool = Field(default=True, description="该服务商节点是否在线/启用")

View File

@ -27,14 +27,14 @@ class Provider(BaseModel):
provider_apikey: str
provider_models: List[str]
provider_type: str
provider_owner: int | None = None
provider_owner: str | None = None
provider_status: ProviderStatus = ProviderStatus.UP
class ProviderArgs(BaseModel):
provider_title: str
provider_url: str
provider_apikey: str
provider_owner: int
provider_owner: str
class BaseProvider(ABC):
@staticmethod

View File

@ -10,7 +10,7 @@ def test_provider_args():
provider_title="title",
provider_url="url",
provider_apikey="key",
provider_owner=1
provider_owner="1"
)
assert args.provider_title == "title"