Compare commits
4 Commits
dcf53524b2
...
e6fcb08ce3
| Author | SHA1 | Date |
|---|---|---|
|
|
e6fcb08ce3 | |
|
|
1df416ac4d | |
|
|
9ace4d4996 | |
|
|
3a96f287c7 |
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
|
||||||
|
|
|
||||||
|
|
@ -33,5 +33,3 @@ services:
|
||||||
- POSTGRES_DB=pretor
|
- POSTGRES_DB=pretor
|
||||||
- SECRET_KEY=changethiskey12345
|
- SECRET_KEY=changethiskey12345
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
|
|
|
||||||
|
|
@ -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,18 +1,36 @@
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Sidebar } from './components/Layout/Sidebar';
|
import { Sidebar } from './components/Layout/Sidebar';
|
||||||
import { MonitoringLayout } from './components/Monitoring/MonitoringLayout';
|
import { MonitoringLayout } from './components/Monitoring/MonitoringLayout';
|
||||||
import { SettingsLayout } from './components/Settings/SettingsLayout';
|
import { SettingsLayout } from './components/Settings/SettingsLayout';
|
||||||
|
import { AgentLayout } from './components/Agent/AgentLayout';
|
||||||
|
import { ResourceLayout } from './components/Resource/ResourceLayout';
|
||||||
import { LeftPanel } from './components/Chat/LeftPanel';
|
import { LeftPanel } from './components/Chat/LeftPanel';
|
||||||
import { ChatPanel } from './components/Chat/ChatPanel';
|
import { ChatPanel } from './components/Chat/ChatPanel';
|
||||||
import { RightPanel } from './components/Chat/RightPanel';
|
import { RightPanel } from './components/Chat/RightPanel';
|
||||||
|
import { AuthPage } from './components/Auth/AuthPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState('chats'); // For LeftPanel
|
const [activeTab, setActiveTab] = useState('chats'); // For LeftPanel
|
||||||
const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', or 'monitoring'
|
const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', 'monitoring', 'agent', 'resource'
|
||||||
const [settingsTab, setSettingsTab] = useState('users'); // For SettingsLayout
|
const [settingsTab, setSettingsTab] = useState('users'); // For SettingsLayout
|
||||||
|
const [agentTab, setAgentTab] = useState('worker'); // For AgentLayout
|
||||||
|
const [resourceTab, setResourceTab] = useState('skill'); // For ResourceLayout
|
||||||
|
|
||||||
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
|
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if token exists in localStorage on mount
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <AuthPage onLoginSuccess={() => setIsAuthenticated(true)} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
|
<div className="flex h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
|
||||||
|
|
||||||
|
|
@ -22,6 +40,10 @@ function App() {
|
||||||
{/* Main Content Area depending on view */}
|
{/* Main Content Area depending on view */}
|
||||||
{currentView === 'monitoring' ? (
|
{currentView === 'monitoring' ? (
|
||||||
<MonitoringLayout />
|
<MonitoringLayout />
|
||||||
|
) : currentView === 'agent' ? (
|
||||||
|
<AgentLayout agentTab={agentTab} setAgentTab={setAgentTab} />
|
||||||
|
) : currentView === 'resource' ? (
|
||||||
|
<ResourceLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
|
||||||
) : currentView === 'dashboard' ? (
|
) : currentView === 'dashboard' ? (
|
||||||
<>
|
<>
|
||||||
{/* 2. Left Panel - Cluster Status & Workflows/Chats */}
|
{/* 2. Left Panel - Cluster Status & Workflows/Chats */}
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,18 @@ apiClient.interceptors.request.use((config) => {
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Interceptor to catch 401 Unauthorized errors and force login
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
// Clear token
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
// Reload the page to force the user back to the Auth view
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default apiClient;
|
export default apiClient;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Bot, Key } from 'lucide-react';
|
||||||
|
import { ProvidersSettings } from './ProvidersSettings';
|
||||||
|
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||||
|
|
||||||
|
interface AgentLayoutProps {
|
||||||
|
agentTab: string;
|
||||||
|
setAgentTab: (tab: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex bg-slate-50 overflow-hidden">
|
||||||
|
{/* Agent Inner Sidebar */}
|
||||||
|
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-0">
|
||||||
|
<div className="p-6 border-b border-slate-100">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800">Agents</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-4 space-y-2 overflow-y-auto">
|
||||||
|
<button
|
||||||
|
onClick={() => setAgentTab('worker')}
|
||||||
|
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${agentTab === 'worker' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||||
|
>
|
||||||
|
<Bot size={18} className="mr-3" />
|
||||||
|
Worker Individual
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setAgentTab('providers')}
|
||||||
|
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${agentTab === 'providers' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||||
|
>
|
||||||
|
<Key size={18} className="mr-3" />
|
||||||
|
Provider Management
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Main Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
|
{agentTab === 'worker' && <WorkerIndividualSettings />}
|
||||||
|
{agentTab === 'providers' && <ProvidersSettings />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import apiClient from '../../api/client';
|
||||||
|
import { Bot, Save } from 'lucide-react';
|
||||||
|
import type { Provider } from '../../types';
|
||||||
|
|
||||||
|
function WorkerIndividualForm({ providers }: { providers: Provider[] }) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
agent_name: '',
|
||||||
|
agent_type: 'OrdinaryIndividual',
|
||||||
|
description: '',
|
||||||
|
provider_title: providers.length > 0 ? providers[0].provider_title : '',
|
||||||
|
model_id: '',
|
||||||
|
system_prompt: '',
|
||||||
|
output_template: '{}',
|
||||||
|
bound_skill: '{}',
|
||||||
|
workspace: '[]'
|
||||||
|
});
|
||||||
|
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 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setMessage('');
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
output_template: JSON.parse(formData.output_template),
|
||||||
|
bound_skill: JSON.parse(formData.bound_skill),
|
||||||
|
workspace: JSON.parse(formData.workspace)
|
||||||
|
};
|
||||||
|
await apiClient.post('/api/v1/agent/worker', payload);
|
||||||
|
setMessage('Successfully created worker individual');
|
||||||
|
setFormData({
|
||||||
|
agent_name: '',
|
||||||
|
agent_type: 'OrdinaryIndividual',
|
||||||
|
description: '',
|
||||||
|
provider_title: '',
|
||||||
|
model_id: '',
|
||||||
|
system_prompt: '',
|
||||||
|
output_template: '{}',
|
||||||
|
bound_skill: '{}',
|
||||||
|
workspace: '[]'
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
setMessage(err.response?.data?.detail || 'Failed to create worker individual. Ensure JSON fields are valid.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} 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">Agent Name</label>
|
||||||
|
<input required type="text" name="agent_name" value={formData.agent_name} 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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Type</label>
|
||||||
|
<select name="agent_type" value={formData.agent_type} 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">
|
||||||
|
<option value="OrdinaryIndividual">OrdinaryIndividual</option>
|
||||||
|
<option value="SkillIndividual">SkillIndividual</option>
|
||||||
|
<option value="SpecialIndividual">SpecialIndividual</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Description</label>
|
||||||
|
<input required type="text" name="description" value={formData.description} 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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
|
||||||
|
<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>
|
||||||
|
<input required type="text" name="model_id" value={formData.model_id} 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" />
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">System Prompt</label>
|
||||||
|
<textarea required name="system_prompt" value={formData.system_prompt} onChange={handleChange} rows={3} 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">Output Template (JSON dict)</label>
|
||||||
|
<input required type="text" name="output_template" value={formData.output_template} 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 font-mono text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Bound Skill (JSON dict)</label>
|
||||||
|
<input required type="text" name="bound_skill" value={formData.bound_skill} 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 font-mono text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Workspace (JSON list)</label>
|
||||||
|
<input required type="text" name="workspace" value={formData.workspace} 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 font-mono text-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div className={`p-3 rounded-lg text-sm ${message.includes('Success') ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-2">
|
||||||
|
<button type="submit" disabled={loading} className="flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50">
|
||||||
|
<Save size={16} />
|
||||||
|
<span>{loading ? 'Creating...' : 'Create Worker'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkerIndividualSettings() {
|
||||||
|
const [nodeType, setNodeType] = useState('supervisory_node');
|
||||||
|
const [providerTitle, setProviderTitle] = useState('');
|
||||||
|
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();
|
||||||
|
setLoading(true);
|
||||||
|
setMessage('');
|
||||||
|
try {
|
||||||
|
await apiClient.post('/api/v1/agent', {
|
||||||
|
provider_title: providerTitle,
|
||||||
|
model_id: modelId,
|
||||||
|
individual_name: nodeType
|
||||||
|
});
|
||||||
|
setMessage(`Successfully loaded ${nodeType}`);
|
||||||
|
setProviderTitle('');
|
||||||
|
setModelId('');
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
setMessage(err.response?.data?.detail || 'Failed to load agent node');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl space-y-6">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-800">Worker Individual Settings</h1>
|
||||||
|
<p className="text-slate-500 mt-1">Configure your system agents and custom workers.</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 justify-between">
|
||||||
|
<div className="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">
|
||||||
|
<Bot size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800">System Nodes</h2>
|
||||||
|
<p className="text-sm text-slate-500">Initialize core system agents</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<form onSubmit={handleCreateNode} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Node Type</label>
|
||||||
|
<select
|
||||||
|
value={nodeType}
|
||||||
|
onChange={(e) => setNodeType(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"
|
||||||
|
>
|
||||||
|
<option value="supervisory_node">Supervisory Node</option>
|
||||||
|
<option value="consciousness_node">Consciousness Node</option>
|
||||||
|
<option value="control_node">Control Node</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
|
||||||
|
<select
|
||||||
|
value={providerTitle}
|
||||||
|
onChange={(e) => setProviderTitle(e.target.value)}
|
||||||
|
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>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={modelId}
|
||||||
|
onChange={(e) => setModelId(e.target.value)}
|
||||||
|
placeholder="e.g. gpt-4"
|
||||||
|
required
|
||||||
|
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={`p-3 rounded-lg text-sm ${message.includes('Success') ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
|
<span>{loading ? 'Saving...' : 'Load Node'}</span>
|
||||||
|
</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">Create Worker Individual</h2>
|
||||||
|
<p className="text-sm text-slate-500">Add a new custom worker to the system.</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<WorkerIndividualForm providers={providers} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import apiClient from '../../api/client';
|
||||||
|
import { Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
interface AuthPageProps {
|
||||||
|
onLoginSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||||
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
|
const [userName, setUserName] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isLogin) {
|
||||||
|
// Login
|
||||||
|
const response = await apiClient.post('/api/v1/auth/login', {
|
||||||
|
user_name: userName,
|
||||||
|
password: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.token) {
|
||||||
|
localStorage.setItem('token', response.data.token);
|
||||||
|
onLoginSuccess();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Register
|
||||||
|
const response = await apiClient.post('/api/v1/auth/register', {
|
||||||
|
user_name: userName,
|
||||||
|
password: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// After successful register, we can automatically log them in
|
||||||
|
// or just show a message and switch to login. Let's switch to login.
|
||||||
|
if (response.data.message === 'success') {
|
||||||
|
setIsLogin(true);
|
||||||
|
setError('Registration successful. Please log in.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
setError(err.response?.data?.detail || err.response?.data?.message || 'Authentication failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen w-full items-center justify-center bg-slate-50">
|
||||||
|
<div className="w-full max-w-md bg-white rounded-xl shadow-lg p-8 border border-slate-100">
|
||||||
|
<div className="flex flex-col items-center mb-8">
|
||||||
|
<div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center text-white mb-4 shadow-md shadow-blue-200">
|
||||||
|
<Activity size={24} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-slate-800">
|
||||||
|
{isLogin ? 'Welcome Back' : 'Create Account'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-500 mt-2 text-sm">
|
||||||
|
{isLogin ? 'Enter your credentials to access your account' : 'Sign up to start using the platform'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className={`mb-4 p-3 rounded-lg text-sm ${error.includes('successful') ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={userName}
|
||||||
|
onChange={(e) => setUserName(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Processing...' : (isLogin ? 'Sign In' : 'Sign Up')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-slate-500">
|
||||||
|
{isLogin ? "Don't have an account? " : "Already have an account? "}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsLogin(!isLogin);
|
||||||
|
setError('');
|
||||||
|
}}
|
||||||
|
className="text-blue-600 font-medium hover:text-blue-700 focus:outline-none"
|
||||||
|
>
|
||||||
|
{isLogin ? 'Sign up' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import { Activity, MessageSquare, MonitorPlay, Settings } from 'lucide-react';
|
import { Activity, MessageSquare, MonitorPlay, Settings, Bot, Box } from 'lucide-react';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
currentView: string;
|
currentView: string;
|
||||||
|
|
@ -31,6 +31,20 @@ export function Sidebar({ currentView, setCurrentView }: SidebarProps) {
|
||||||
>
|
>
|
||||||
<MonitorPlay size={18} />
|
<MonitorPlay size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentView('agent')}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors ${currentView === 'agent' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
|
||||||
|
title="Agents"
|
||||||
|
>
|
||||||
|
<Bot size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentView('resource')}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors ${currentView === 'resource' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
|
||||||
|
title="Resources"
|
||||||
|
>
|
||||||
|
<Box size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentView('settings')}
|
onClick={() => setCurrentView('settings')}
|
||||||
className={`p-1.5 rounded-lg transition-colors ${currentView === 'settings' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
|
className={`p-1.5 rounded-lg transition-colors ${currentView === 'settings' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Wrench, Database, FileCode } from 'lucide-react';
|
||||||
|
import { SkillSettings } from './SkillSettings';
|
||||||
|
import { ResourceSettings } from './ResourceSettings';
|
||||||
|
import { WorkflowTemplateSettings } from './WorkflowTemplateSettings';
|
||||||
|
|
||||||
|
interface ResourceLayoutProps {
|
||||||
|
resourceTab: string;
|
||||||
|
setResourceTab: (tab: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourceLayout({ resourceTab, setResourceTab }: ResourceLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex bg-slate-50 overflow-hidden">
|
||||||
|
{/* Resource Inner Sidebar */}
|
||||||
|
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-0">
|
||||||
|
<div className="p-6 border-b border-slate-100">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800">Resources</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-4 space-y-2 overflow-y-auto">
|
||||||
|
<button
|
||||||
|
onClick={() => setResourceTab('skill')}
|
||||||
|
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'skill' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||||
|
>
|
||||||
|
<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'}`}
|
||||||
|
>
|
||||||
|
<Database size={18} className="mr-3" />
|
||||||
|
Resources
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resource Main Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
|
{resourceTab === 'skill' && <SkillSettings />}
|
||||||
|
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
|
||||||
|
{resourceTab === 'resource' && <ResourceSettings />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function ResourceSettings() {
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl space-y-6">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-800">Resource Management</h1>
|
||||||
|
<p className="text-slate-500 mt-1">Manage external and internal resources.</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden p-6 text-slate-500 text-sm">
|
||||||
|
Resource management configuration coming soon...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
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">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
import { Users, Key, Sliders } from 'lucide-react';
|
import { Users, Sliders } from 'lucide-react';
|
||||||
import { UsersSettings } from './UsersSettings';
|
import { UsersSettings } from './UsersSettings';
|
||||||
import { ProvidersSettings } from './ProvidersSettings';
|
|
||||||
import { SystemSettings } from './SystemSettings';
|
import { SystemSettings } from './SystemSettings';
|
||||||
|
|
||||||
interface SettingsLayoutProps {
|
interface SettingsLayoutProps {
|
||||||
|
|
@ -25,13 +24,6 @@ export function SettingsLayout({ settingsTab, setSettingsTab }: SettingsLayoutPr
|
||||||
<Users size={18} className="mr-3" />
|
<Users size={18} className="mr-3" />
|
||||||
User Management
|
User Management
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setSettingsTab('providers')}
|
|
||||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === 'providers' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
|
||||||
>
|
|
||||||
<Key size={18} className="mr-3" />
|
|
||||||
Provider Management
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setSettingsTab('system')}
|
onClick={() => setSettingsTab('system')}
|
||||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === 'system' ? '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 ${settingsTab === 'system' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||||
|
|
@ -45,7 +37,6 @@ export function SettingsLayout({ settingsTab, setSettingsTab }: SettingsLayoutPr
|
||||||
{/* Settings Main Content */}
|
{/* Settings Main Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
{settingsTab === 'users' && <UsersSettings />}
|
{settingsTab === 'users' && <UsersSettings />}
|
||||||
{settingsTab === 'providers' && <ProvidersSettings />}
|
|
||||||
{settingsTab === 'system' && <SystemSettings />}
|
{settingsTab === 'system' && <SystemSettings />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,36 @@
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { Globe, Server, Save } from 'lucide-react';
|
import { Globe, Server, Save } from 'lucide-react';
|
||||||
|
|
||||||
export function SystemSettings() {
|
export function SystemSettings() {
|
||||||
|
const [language, setLanguage] = useState(localStorage.getItem('language') || 'English');
|
||||||
|
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'Light');
|
||||||
|
const [debugMode, setDebugMode] = useState(true);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
localStorage.setItem('language', language);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
|
||||||
|
// Apply theme
|
||||||
|
if (theme === 'Dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real app, you would dispatch a language change event or context update here
|
||||||
|
alert(`Settings saved!\nLanguage: ${language}\nTheme: ${theme}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize theme on mount if needed
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme === 'Dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -18,17 +47,25 @@ export function SystemSettings() {
|
||||||
<div className="space-y-4 max-w-md">
|
<div className="space-y-4 max-w-md">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">System Language</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1">System Language</label>
|
||||||
<select className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all">
|
<select
|
||||||
<option>English</option>
|
value={language}
|
||||||
<option>简体中文</option>
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||||
|
>
|
||||||
|
<option value="English">English</option>
|
||||||
|
<option value="简体中文">简体中文</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Theme</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1">Theme</label>
|
||||||
<select className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all">
|
<select
|
||||||
<option>Light</option>
|
value={theme}
|
||||||
<option>Dark</option>
|
onChange={(e) => setTheme(e.target.value)}
|
||||||
<option>System Default</option>
|
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||||
|
>
|
||||||
|
<option value="Light">Light</option>
|
||||||
|
<option value="Dark">Dark</option>
|
||||||
|
<option value="System Default">System Default</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -40,12 +77,14 @@ export function SystemSettings() {
|
||||||
Cluster & Runtime
|
Cluster & Runtime
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-4 max-w-md">
|
<div className="space-y-4 max-w-md">
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Max Concurrent Workflows</label>
|
|
||||||
<input type="number" defaultValue={10} className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center mt-4">
|
<div className="flex items-center mt-4">
|
||||||
<input type="checkbox" id="debug_mode" defaultChecked className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="debug_mode"
|
||||||
|
checked={debugMode}
|
||||||
|
onChange={(e) => setDebugMode(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
<label htmlFor="debug_mode" className="ml-2 block text-sm text-slate-700">
|
<label htmlFor="debug_mode" className="ml-2 block text-sm text-slate-700">
|
||||||
Enable debug logging
|
Enable debug logging
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -54,7 +93,10 @@ export function SystemSettings() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button className="flex items-center px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm">
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="flex items-center px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm cursor-pointer"
|
||||||
|
>
|
||||||
<Save size={16} className="mr-2" />
|
<Save size={16} className="mr-2" />
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,5 @@ async def update_cluster_state(websocket: WebSocket):
|
||||||
]
|
]
|
||||||
await websocket.send_json(payload)
|
await websocket.send_json(payload)
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
except WebSocketDisconnect:
|
except (WebSocketDisconnect, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
@ -29,6 +29,6 @@ async def get_workflow(websocket: WebSocket, event_id: str):
|
||||||
await websocket.send_text(await global_state_machine.get_pending.remote(event_id))
|
await websocket.send_text(await global_state_machine.get_pending.remote(event_id))
|
||||||
response = await websocket.receive_text()
|
response = await websocket.receive_text()
|
||||||
await global_state_machine.put_received(event_id, response)
|
await global_state_machine.put_received(event_id, response)
|
||||||
except WebSocketDisconnect:
|
except (WebSocketDisconnect, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ class WorkflowEngine:
|
||||||
consciousness_node=None,
|
consciousness_node=None,
|
||||||
control_node=None,
|
control_node=None,
|
||||||
supervisory_node=None):
|
supervisory_node=None):
|
||||||
|
from pretor.utils.logger import get_logger
|
||||||
|
self.logger = get_logger('workflow_runner')
|
||||||
self.workflow: PretorWorkflow = workflow
|
self.workflow: PretorWorkflow = workflow
|
||||||
"""工作流:当前WorkflowEngine待执行的workflow"""
|
"""工作流:当前WorkflowEngine待执行的workflow"""
|
||||||
self._steps_by_id: Dict[int, WorkStep] = {step.step: step for step in self.workflow.work_link}
|
self._steps_by_id: Dict[int, WorkStep] = {step.step: step for step in self.workflow.work_link}
|
||||||
|
|
|
||||||
|
|
@ -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