Merge branch 'dev' of https://github.com/zhaoxi826/Pretor into dev
# Conflicts: # frontend/src/components/Agent/WorkerIndividualSettings.tsx # frontend/src/components/Resource/ResourceLayout.tsx # frontend/src/components/Resource/SkillSettings.tsx
This commit is contained in:
commit
1df416ac4d
2
.env
2
.env
|
|
@ -1,5 +1,5 @@
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=postgrespassword
|
||||||
POSTGRES_HOST=127.0.0.1
|
POSTGRES_HOST=127.0.0.1
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_DB=pretor
|
POSTGRES_DB=pretor
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,12 @@
|
||||||
---
|
---
|
||||||
## 问题栏
|
## 问题栏
|
||||||
#### 🔴 核心缺陷与修复 (Bug Fixes & Stability)
|
#### 🔴 核心缺陷与修复 (Bug Fixes & Stability)
|
||||||
- [x] /pretor/core/individual每个template进行优化
|
|
||||||
- [x] /pretor/worker_individual待完善复合子个体和基础子个体
|
|
||||||
|
|
||||||
#### 🛡️ 安全与合规 (Security & Auth)
|
#### 🛡️ 安全与合规 (Security & Auth)
|
||||||
- [ ] 优化安全架构防止模型注入
|
|
||||||
- [ ] 设计workflowEngine的自动扩缩容设计
|
|
||||||
- [ ] 完善错误捕获和日志系统
|
|
||||||
|
|
||||||
#### ⚡ 性能与资源优化 (Performance & Scalability)
|
#### ⚡ 性能与资源优化 (Performance & Scalability)
|
||||||
- [ ] 增加对应全workflow的情况追踪,使得在任务运行中人机交互更加自然方便
|
|
||||||
- [ ] 优化import
|
|
||||||
|
|
||||||
#### 🏗️ 架构演进 (Architecture & Refactoring)
|
#### 🏗️ 架构演进 (Architecture & Refactoring)
|
||||||
- [x] ~~使用fastapi-users完善用户系统~~(2026/4/19 fastapi-users会严重摧毁代码的优雅性)
|
|
||||||
- [x] 升级auth功能
|
|
||||||
- [x] /pretor/api的接口函数进行重构
|
|
||||||
- [x] /dockerfile待完善
|
|
||||||
- [ ] 完善沙箱功能
|
|
||||||
- [ ] 完善爬虫功能
|
|
||||||
- [ ] 对接更多的provider
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
## Pretor项目开发
|
## Pretor项目开发
|
||||||
#项目规划
|
|
||||||
---
|
|
||||||
#### 全局规划:
|
|
||||||
- [ ] 实现监管模型的资源调度
|
|
||||||
- [ ] 实现子个体的工作传递
|
|
||||||
- [ ] 实现用户交互接口与ray集群的交互
|
|
||||||
- [ ] 实现监管模型调度ray资源的接口
|
|
||||||
- [ ] 实现由监管模型理解并发布,子个体向下布置任务,完成任务向上传递,监管模型检查的全工作流
|
|
||||||
---
|
|
||||||
#### 简介
|
#### 简介
|
||||||
**Pretor**是一款python开发,实现将小模型进行微调后整理为一个大型集群,从而实现低算力情况下高复杂度任务的实现。
|
**Pretor**是一款python开发,实现将小模型进行微调后整理为一个大型集群,从而实现低算力情况下高复杂度任务的实现。
|
||||||
系统模型分为以下部分:
|
系统模型分为以下部分:
|
||||||
|
|
@ -15,13 +7,6 @@
|
||||||
- **管控节点**:负责调度系统资源;
|
- **管控节点**:负责调度系统资源;
|
||||||
- **意识节点**:负责复杂任务的处理;
|
- **意识节点**:负责复杂任务的处理;
|
||||||
- **生长节点**:负责获取资源并且将基础模型训练为特化模型;
|
- **生长节点**:负责获取资源并且将基础模型训练为特化模型;
|
||||||
- **感知模块**:与外界交互的模型,如embedding模型,tts模型等;
|
- **特殊子个体**:与外界交互的模型,如embedding模型,tts模型等;
|
||||||
- **复合子个体**:将监管节点的任务领取并进行专业的拆解任务并进行分配;
|
- **专家子个体**:;
|
||||||
- **生产子个体**:领取任务最小单位并执行;
|
- **基础子个体**:普通的agent对象;
|
||||||
---
|
|
||||||
#### 短期规划
|
|
||||||
v0.1版本
|
|
||||||
- [ ] **workflow构建**:构建任务的工作流
|
|
||||||
- [ ] **接口构建**:对接vllm,openai接口和gemini接口
|
|
||||||
- [ ] **工具构建**:配置供模型调用的爬虫工具箱,docker接口
|
|
||||||
- [ ] **平台对接构建**:对接telegram等消息平台
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import apiClient from '../../api/client';
|
import apiClient from '../../api/client';
|
||||||
import { Bot, Save } from 'lucide-react';
|
import { Bot, Save } from 'lucide-react';
|
||||||
|
import type { Provider } from '../../types';
|
||||||
|
|
||||||
function WorkerIndividualForm() {
|
function WorkerIndividualForm({ providers }: { providers: Provider[] }) {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
agent_name: '',
|
agent_name: '',
|
||||||
agent_type: 'OrdinaryIndividual',
|
agent_type: 'OrdinaryIndividual',
|
||||||
description: '',
|
description: '',
|
||||||
provider_title: '',
|
provider_title: providers.length > 0 ? providers[0].provider_title : '',
|
||||||
model_id: '',
|
model_id: '',
|
||||||
system_prompt: '',
|
system_prompt: '',
|
||||||
output_template: '{}',
|
output_template: '{}',
|
||||||
|
|
@ -17,6 +18,13 @@ function WorkerIndividualForm() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
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>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
@ -74,7 +82,23 @@ function WorkerIndividualForm() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Model ID</label>
|
<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 [modelId, setModelId] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
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) => {
|
const handleCreateNode = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -178,14 +221,22 @@ export function WorkerIndividualSettings() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
value={providerTitle}
|
value={providerTitle}
|
||||||
onChange={(e) => setProviderTitle(e.target.value)}
|
onChange={(e) => setProviderTitle(e.target.value)}
|
||||||
placeholder="e.g. openai"
|
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Model ID</label>
|
<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>
|
<p className="text-sm text-slate-500">Add a new custom worker to the system.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<WorkerIndividualForm />
|
<WorkerIndividualForm providers={providers} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Wrench, Database } from 'lucide-react';
|
import { Wrench, Database, FileCode } from 'lucide-react';
|
||||||
import { SkillSettings } from './SkillSettings';
|
import { SkillSettings } from './SkillSettings';
|
||||||
import { ResourceSettings } from './ResourceSettings';
|
import { ResourceSettings } from './ResourceSettings';
|
||||||
|
import { WorkflowTemplateSettings } from './WorkflowTemplateSettings';
|
||||||
|
|
||||||
interface ResourceLayoutProps {
|
interface ResourceLayoutProps {
|
||||||
resourceTab: string;
|
resourceTab: string;
|
||||||
|
|
@ -23,6 +24,13 @@ export function ResourceLayout({ resourceTab, setResourceTab }: ResourceLayoutPr
|
||||||
<Wrench size={18} className="mr-3" />
|
<Wrench size={18} className="mr-3" />
|
||||||
Skills
|
Skills
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setResourceTab('resource')}
|
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'}`}
|
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 */}
|
{/* Resource Main Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
{resourceTab === 'skill' && <SkillSettings />}
|
{resourceTab === 'skill' && <SkillSettings />}
|
||||||
|
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
|
||||||
{resourceTab === 'resource' && <ResourceSettings />}
|
{resourceTab === 'resource' && <ResourceSettings />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
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 (
|
return (
|
||||||
<div className="max-w-4xl space-y-6">
|
<div className="max-w-4xl space-y-6">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-slate-800">Skill Management</h1>
|
<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>
|
<p className="text-slate-500 mt-1">Manage agent skills and functions.</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
14
main.py
|
|
@ -15,15 +15,13 @@ import os
|
||||||
|
|
||||||
|
|
||||||
async def start_system():
|
async def start_system():
|
||||||
# 1. 初始化 Ray
|
|
||||||
db_host = os.getenv("POSTGRES_HOST", "db")
|
|
||||||
env_vars = {
|
env_vars = {
|
||||||
"POSTGRES_USER": "postgres",
|
"POSTGRES_USER": os.getenv("POSTGRES_USER", "postgres"),
|
||||||
"POSTGRES_PASSWORD": "postgres",
|
"POSTGRES_PASSWORD": os.getenv("POSTGRES_PASSWORD", ""),
|
||||||
"POSTGRES_HOST": db_host,
|
"POSTGRES_HOST": os.getenv("POSTGRES_HOST", "db"),
|
||||||
"POSTGRES_PORT": "5432",
|
"POSTGRES_PORT": os.getenv("POSTGRES_PORT", "5432"),
|
||||||
"POSTGRES_DB": "postgres",
|
"POSTGRES_DB": os.getenv("POSTGRES_DB", "postgres"),
|
||||||
"SECRET_KEY": "yoursecretkey"
|
"SECRET_KEY": os.getenv("SECRET_KEY", "secret"),
|
||||||
}
|
}
|
||||||
|
|
||||||
ray.init(ignore_reinit_error=True,
|
ray.init(ignore_reinit_error=True,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import asyncio
|
||||||
|
|
||||||
import ray
|
import ray
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||||
|
|
@ -39,6 +40,8 @@ class PostgresDatabase:
|
||||||
self._provider_database = ProviderDatabase(self.async_session_maker)
|
self._provider_database = ProviderDatabase(self.async_session_maker)
|
||||||
self._individual_database = IndividualDatabase(self.async_session_maker)
|
self._individual_database = IndividualDatabase(self.async_session_maker)
|
||||||
|
|
||||||
|
self.ready_event = asyncio.Event()
|
||||||
|
|
||||||
async def init_db(self) -> None:
|
async def init_db(self) -> None:
|
||||||
try:
|
try:
|
||||||
async with self.async_engine.begin() as conn:
|
async with self.async_engine.begin() as conn:
|
||||||
|
|
@ -47,15 +50,20 @@ class PostgresDatabase:
|
||||||
# Provide a warning if the database is not accessible, allowing
|
# Provide a warning if the database is not accessible, allowing
|
||||||
# the app to start up for development/UI tests without crashing immediately.
|
# the app to start up for development/UI tests without crashing immediately.
|
||||||
print(f"Warning: Failed to initialize PostgreSQL database: {e}")
|
print(f"Warning: Failed to initialize PostgreSQL database: {e}")
|
||||||
|
finally:
|
||||||
|
self.ready_event.set()
|
||||||
|
|
||||||
async def auth_database(self, method_name: str, *args, **kwargs):
|
async def auth_database(self, method_name: str, *args, **kwargs):
|
||||||
|
await self.ready_event.wait()
|
||||||
method = getattr(self._auth_database, method_name)
|
method = getattr(self._auth_database, method_name)
|
||||||
return await method(*args, **kwargs)
|
return await method(*args, **kwargs)
|
||||||
|
|
||||||
async def provider_database(self, method_name: str, *args, **kwargs):
|
async def provider_database(self, method_name: str, *args, **kwargs):
|
||||||
|
await self.ready_event.wait()
|
||||||
method = getattr(self._provider_database, method_name)
|
method = getattr(self._provider_database, method_name)
|
||||||
return await method(*args, **kwargs)
|
return await method(*args, **kwargs)
|
||||||
|
|
||||||
async def individual_database(self, method_name: str, *args, **kwargs):
|
async def individual_database(self, method_name: str, *args, **kwargs):
|
||||||
|
await self.ready_event.wait()
|
||||||
method = getattr(self._individual_database, method_name)
|
method = getattr(self._individual_database, method_name)
|
||||||
return await method(*args, **kwargs)
|
return await method(*args, **kwargs)
|
||||||
|
|
@ -28,5 +28,5 @@ class Provider(SQLModel, table=True):
|
||||||
|
|
||||||
provider_models: List[str] = Field(sa_column=Column(JSON))
|
provider_models: List[str] = Field(sa_column=Column(JSON))
|
||||||
|
|
||||||
provider_owner: int
|
provider_owner: str
|
||||||
is_active: bool = Field(default=True, description="该服务商节点是否在线/启用")
|
is_active: bool = Field(default=True, description="该服务商节点是否在线/启用")
|
||||||
|
|
@ -27,14 +27,14 @@ class Provider(BaseModel):
|
||||||
provider_apikey: str
|
provider_apikey: str
|
||||||
provider_models: List[str]
|
provider_models: List[str]
|
||||||
provider_type: str
|
provider_type: str
|
||||||
provider_owner: int | None = None
|
provider_owner: str | None = None
|
||||||
provider_status: ProviderStatus = ProviderStatus.UP
|
provider_status: ProviderStatus = ProviderStatus.UP
|
||||||
|
|
||||||
class ProviderArgs(BaseModel):
|
class ProviderArgs(BaseModel):
|
||||||
provider_title: str
|
provider_title: str
|
||||||
provider_url: str
|
provider_url: str
|
||||||
provider_apikey: str
|
provider_apikey: str
|
||||||
provider_owner: int
|
provider_owner: str
|
||||||
|
|
||||||
class BaseProvider(ABC):
|
class BaseProvider(ABC):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ def test_provider_args():
|
||||||
provider_title="title",
|
provider_title="title",
|
||||||
provider_url="url",
|
provider_url="url",
|
||||||
provider_apikey="key",
|
provider_apikey="key",
|
||||||
provider_owner=1
|
provider_owner="1"
|
||||||
)
|
)
|
||||||
assert args.provider_title == "title"
|
assert args.provider_title == "title"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue