chore(release): v0.1.1-alpha
##前端美化和bug修复 #### 💄 美化 - **前端美化**:对于整个前端效果进行了重新设计,现在的前端看起来会更立体。 #### 🐛 修复 - **前端演示**:修复了前端展示workflow列表的bug,但是workflow的具体条目显示由于序列化导致仍然有问题。 - **密钥修复**:对于secret_key现在在使用默认情况时,会强制生成一个安全的密钥。
This commit is contained in:
+1
-1
@@ -6,6 +6,6 @@ __pycache__
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
docker-compose.yml
|
||||
.env
|
||||
.env.template
|
||||
.env.example
|
||||
.idea
|
||||
+11
-1
@@ -1,9 +1,19 @@
|
||||
# ChangeLog
|
||||
|
||||
---
|
||||
## [v0.1.1Alpha] - 2026/5/4
|
||||
### 更新:
|
||||
#### 💄 美化
|
||||
- **前端美化**:对于整个前端效果进行了重新设计,现在的前端看起来会更立体。
|
||||
|
||||
#### 🐛 修复
|
||||
- **前端演示**:修复了前端展示workflow列表的bug,但是workflow的具体条目显示由于序列化导致仍然有问题。
|
||||
- **密钥修复**:对于secret_key现在在使用默认情况时,会强制生成一个安全的密钥。
|
||||
|
||||
---
|
||||
## [v0.1.0Alpha] - 2026/4/28
|
||||
### 更新:
|
||||
#### 🚀 新增功能 (Added)
|
||||
#### 🚀 新增功能
|
||||
- **分布式 Actor 骨架**:基于 Ray 框架构建了多智能体协作底座,支持节点跨进程通讯与资源调度。
|
||||
- **全局状态机 (GSM)**:实现了 `GlobalStateMachine` 模块,作为系统的“唯一真相来源”,管理所有 Individual、Skill 和 Provider 的注册信息。
|
||||
- **核心认知节点**:
|
||||
|
||||
Generated
+232
-2
@@ -8,6 +8,7 @@
|
||||
"name": "pretor-dashboard",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.15.1",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.4",
|
||||
@@ -1153,6 +1154,55 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1181,7 +1231,7 @@
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
@@ -1518,6 +1568,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz",
|
||||
"integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.76",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
|
||||
"integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -1724,6 +1806,12 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -1789,9 +1877,114 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -3461,6 +3654,15 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
||||
@@ -3607,6 +3809,34 @@
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.15.1",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.4",
|
||||
|
||||
+115
-34
@@ -1,25 +1,58 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Sidebar } from './components/Layout/Sidebar';
|
||||
import { TopBar } from './components/Layout/TopBar';
|
||||
import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar';
|
||||
import { SettingsLayout } from './components/Settings/SettingsLayout';
|
||||
import { AgentLayout } from './components/Agent/AgentLayout';
|
||||
import { ResourceLayout } from './components/Resource/ResourceLayout';
|
||||
import { PluginLayout } from './components/Plugin/PluginLayout'; // Will rename to PluginLayout soon
|
||||
import { LeftPanel } from './components/Chat/LeftPanel';
|
||||
import { ChatPanel } from './components/Chat/ChatPanel';
|
||||
import { RightPanel } from './components/Chat/RightPanel';
|
||||
import { WorkflowListView } from './components/Chat/WorkflowListView';
|
||||
import { AuthPage } from './components/Auth/AuthPage';
|
||||
|
||||
// For Chat Module State Persistence
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: Message[];
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('chats'); // For LeftPanel
|
||||
const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', 'agent', 'resource'
|
||||
const [settingsTab, setSettingsTab] = useState('users'); // For SettingsLayout
|
||||
const [agentTab, setAgentTab] = useState('worker'); // For AgentLayout
|
||||
const [resourceTab, setResourceTab] = useState('skill'); // For ResourceLayout
|
||||
|
||||
// Layout State
|
||||
const [mode, setMode] = useState<'work' | 'agent'>('work');
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
|
||||
// Module Sub-navigation States
|
||||
// Work Mode
|
||||
const [workTab, setWorkTab] = useState<'chat' | 'workflow'>('chat');
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
|
||||
|
||||
// Agent Mode
|
||||
const [agentTab, setAgentTab] = useState<'plugin' | 'agents'>('plugin');
|
||||
|
||||
// Settings Sub-tab
|
||||
const [settingsTab, setSettingsTab] = useState('users');
|
||||
|
||||
// Inner Agent Tab (temporary until full Agent layout rewrite)
|
||||
const [innerAgentTab, setInnerAgentTab] = useState('worker');
|
||||
const [resourceTab, setResourceTab] = useState('skill');
|
||||
|
||||
// Chat State Hoisted for Persistence
|
||||
const [chatSessions, setChatSessions] = useState<ChatSession[]>([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if token exists in localStorage on mount
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setIsAuthenticated(true);
|
||||
@@ -31,37 +64,85 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
|
||||
<div className="flex flex-col h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
|
||||
{/* 1. Top Bar */}
|
||||
<TopBar
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
showSettings={showSettings}
|
||||
setShowSettings={setShowSettings}
|
||||
/>
|
||||
|
||||
{/* 1. Sidebar (Leftmost) */}
|
||||
<Sidebar currentView={currentView} setCurrentView={setCurrentView} />
|
||||
{/* 2. Main Content Area */}
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
{showSettings ? (
|
||||
<SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} />
|
||||
) : (
|
||||
<>
|
||||
{/* Collapsible Main Sidebar */}
|
||||
<CollapsibleSidebar
|
||||
mode={mode}
|
||||
isOpen={isSidebarOpen}
|
||||
setIsOpen={setIsSidebarOpen}
|
||||
workTab={workTab}
|
||||
setWorkTab={setWorkTab}
|
||||
agentTab={agentTab}
|
||||
setAgentTab={setAgentTab}
|
||||
/>
|
||||
|
||||
{/* Main Content Area depending on view */}
|
||||
{currentView === 'agent' ? (
|
||||
<AgentLayout agentTab={agentTab} setAgentTab={setAgentTab} />
|
||||
) : currentView === 'resource' ? (
|
||||
<ResourceLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
|
||||
) : currentView === 'dashboard' ? (
|
||||
<>
|
||||
{/* 2. Left Panel - Cluster Status & Workflows/Chats */}
|
||||
<LeftPanel
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
selectedWorkflow={selectedWorkflow}
|
||||
setSelectedWorkflow={setSelectedWorkflow}
|
||||
/>
|
||||
{/* Dynamic View based on Mode and Tab */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{mode === 'work' && workTab === 'chat' && (
|
||||
<div className="flex-1 p-6 flex overflow-hidden">
|
||||
<div className="flex-1 flex bg-white rounded-3xl shadow-md border border-slate-200 overflow-hidden relative">
|
||||
<LeftPanel
|
||||
activeTab="chats"
|
||||
selectedWorkflow={null}
|
||||
setSelectedWorkflow={() => {}}
|
||||
// Pass hoisted state down
|
||||
chatSessions={chatSessions}
|
||||
setChatSessions={setChatSessions}
|
||||
activeSessionId={activeSessionId}
|
||||
setActiveSessionId={setActiveSessionId}
|
||||
/>
|
||||
<ChatPanel
|
||||
chatSessions={chatSessions}
|
||||
setChatSessions={setChatSessions}
|
||||
activeSessionId={activeSessionId}
|
||||
setActiveSessionId={setActiveSessionId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. Middle Panel - AI Chat */}
|
||||
<ChatPanel />
|
||||
{mode === 'work' && workTab === 'workflow' && (
|
||||
<>
|
||||
{selectedWorkflow ? (
|
||||
<>
|
||||
<LeftPanel
|
||||
activeTab="workflows"
|
||||
selectedWorkflow={selectedWorkflow}
|
||||
setSelectedWorkflow={setSelectedWorkflow}
|
||||
/>
|
||||
<RightPanel selectedWorkflow={selectedWorkflow} />
|
||||
</>
|
||||
) : (
|
||||
<WorkflowListView onSelectWorkflow={setSelectedWorkflow} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 4. Right Panel - Workflow Execution Status (Only show when viewing workflows) */}
|
||||
{activeTab === 'workflows' && <RightPanel selectedWorkflow={selectedWorkflow} />}
|
||||
</>
|
||||
) : (
|
||||
/* Settings View */
|
||||
<SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} />
|
||||
)}
|
||||
{mode === 'agent' && agentTab === 'agents' && (
|
||||
<AgentLayout agentTab={innerAgentTab} setAgentTab={setInnerAgentTab} />
|
||||
)}
|
||||
|
||||
{mode === 'agent' && agentTab === 'plugin' && (
|
||||
<PluginLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Bot, Key } from 'lucide-react';
|
||||
import { ProvidersSettings } from './ProvidersSettings';
|
||||
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||
|
||||
@@ -9,35 +8,32 @@ interface AgentLayoutProps {
|
||||
|
||||
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" />
|
||||
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>
|
||||
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
|
||||
{/* Top Tabs for Agent Module */}
|
||||
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shadow-sm z-10 shrink-0 space-x-6">
|
||||
<button
|
||||
onClick={() => setAgentTab('worker')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
agentTab === 'worker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Individual
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAgentTab('providers')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
agentTab === 'providers' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Provider Management
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Agent Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{agentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{agentTab === 'providers' && <ProvidersSettings />}
|
||||
</div>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{agentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{agentTab === 'providers' && <ProvidersSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ export function WorkerIndividualSettings() {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddNew}
|
||||
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Worker
|
||||
@@ -203,7 +203,7 @@ export function WorkerIndividualSettings() {
|
||||
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors" title="Edit">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
@@ -219,7 +219,7 @@ export function WorkerIndividualSettings() {
|
||||
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors" title="Edit">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(w.agent_id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete">
|
||||
@@ -256,7 +256,7 @@ export function WorkerIndividualSettings() {
|
||||
required
|
||||
value={editData.agent_name || ''}
|
||||
onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={(editData as any).is_system}
|
||||
/>
|
||||
</div>
|
||||
@@ -265,7 +265,7 @@ export function WorkerIndividualSettings() {
|
||||
<select
|
||||
value={editData.agent_type || 'ordinary_individual'}
|
||||
onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={(editData as any).is_system}
|
||||
>
|
||||
<option value="ordinary_individual">Ordinary Individual</option>
|
||||
@@ -285,7 +285,7 @@ export function WorkerIndividualSettings() {
|
||||
value={editData.provider_title || ''}
|
||||
onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="" disabled>Select Provider</option>
|
||||
{providers.map((p) => (
|
||||
@@ -303,7 +303,7 @@ export function WorkerIndividualSettings() {
|
||||
value={editData.model_id || ''}
|
||||
onChange={(e) => setEditData({...editData, model_id: e.target.value})}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="" disabled>Select a model</option>
|
||||
{models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
@@ -321,7 +321,7 @@ export function WorkerIndividualSettings() {
|
||||
value={editData.description || ''}
|
||||
onChange={(e) => setEditData({...editData, description: e.target.value})}
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -331,7 +331,7 @@ export function WorkerIndividualSettings() {
|
||||
value={editData.system_prompt || ''}
|
||||
onChange={(e) => setEditData({...editData, system_prompt: e.target.value})}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -342,7 +342,7 @@ export function WorkerIndividualSettings() {
|
||||
value={editData.output_template || '{}'}
|
||||
onChange={(e) => setEditData({...editData, output_template: e.target.value})}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -359,7 +359,7 @@ export function WorkerIndividualSettings() {
|
||||
const newSkill = val ? { [val]: [] } : {};
|
||||
setEditData({...editData, bound_skill: JSON.stringify(newSkill)});
|
||||
}}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={editData.agent_type !== 'skill_individual'}
|
||||
>
|
||||
<option value="">No Skill Bound</option>
|
||||
@@ -376,7 +376,7 @@ export function WorkerIndividualSettings() {
|
||||
value={editData.workspace || '[]'}
|
||||
onChange={(e) => setEditData({...editData, workspace: e.target.value})}
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -407,7 +407,7 @@ export function WorkerIndividualSettings() {
|
||||
}}
|
||||
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
|
||||
isSelected
|
||||
? 'bg-indigo-100 text-indigo-700 border border-indigo-200'
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-200'
|
||||
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
@@ -437,7 +437,7 @@ export function WorkerIndividualSettings() {
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
Save Worker
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { MessageSquare, Activity, Terminal, ChevronRight, Plus } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { ChatSession, Message } from '../../App';
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
sender: 'user' | 'ai';
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
eventId?: string;
|
||||
interface ChatPanelProps {
|
||||
chatSessions: ChatSession[];
|
||||
setChatSessions: React.Dispatch<React.SetStateAction<ChatSession[]>>;
|
||||
activeSessionId: string | null;
|
||||
setActiveSessionId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
export function ChatPanel() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
id: '1',
|
||||
sender: 'ai',
|
||||
text: "Hello! I am Pretor Assistant. How can I help you today?",
|
||||
timestamp: new Date()
|
||||
}
|
||||
]);
|
||||
export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId }: ChatPanelProps) {
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const activeSession = chatSessions.find((s) => s.id === activeSessionId) || null;
|
||||
const messages = activeSession ? activeSession.messages : [];
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const updateSessionMessages = (newMessages: Message[]) => {
|
||||
if (!activeSessionId) return;
|
||||
setChatSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === activeSessionId
|
||||
? { ...s, messages: newMessages, updatedAt: Date.now() }
|
||||
: s
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file || !activeSessionId) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
@@ -33,26 +49,24 @@ export function ChatPanel() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/adapter/client/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
const aiMessage: ChatMessage = {
|
||||
const aiMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
sender: 'ai',
|
||||
text: `已上传文件: ${response.data.filename}`,
|
||||
timestamp: new Date()
|
||||
role: 'assistant',
|
||||
content: `已上传文件: ${response.data.filename}`,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
updateSessionMessages([...messages, aiMessage]);
|
||||
} catch (error) {
|
||||
console.error("Error uploading file", error);
|
||||
const errorMessage: ChatMessage = {
|
||||
console.error('Error uploading file', error);
|
||||
const errorMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
sender: 'ai',
|
||||
text: "文件上传失败。",
|
||||
timestamp: new Date()
|
||||
role: 'assistant',
|
||||
content: '文件上传失败。',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
updateSessionMessages([...messages, errorMessage]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (fileInputRef.current) {
|
||||
@@ -62,77 +76,116 @@ export function ChatPanel() {
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!input.trim()) return;
|
||||
if (!input.trim() || !activeSessionId) return;
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
const userText = input;
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
sender: 'user',
|
||||
text: input,
|
||||
timestamp: new Date()
|
||||
role: 'user',
|
||||
content: userText,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
updateSessionMessages([...messages, userMessage]);
|
||||
setInput('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Assuming a token might be needed, apiClient should handle it if set
|
||||
const promptModifier = mode === 'deploy' ? '[DEPLOY TASK] ' : '';
|
||||
const response = await apiClient.post('/api/v1/adapter/client', {
|
||||
message: promptModifier + userMessage.text
|
||||
message: promptModifier + userMessage.content,
|
||||
});
|
||||
|
||||
const aiMessage: ChatMessage = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
sender: 'ai',
|
||||
text: typeof response.data.message === 'string' && response.data.message.includes('-')
|
||||
? "Task has been created." // It's an event ID
|
||||
: response.data.message || "I received your message.",
|
||||
eventId: typeof response.data.message === 'string' && response.data.message.includes('-') ? response.data.message : undefined,
|
||||
timestamp: new Date()
|
||||
};
|
||||
const responseData = response.data.message;
|
||||
let aiContent = responseData || 'I received your message.';
|
||||
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
|
||||
// If we got an event_id, we could potentially open a websocket to listen to its stream
|
||||
if (aiMessage.eventId) {
|
||||
console.log(`Open WS to track event: ${aiMessage.eventId}`);
|
||||
// Implement WS tracking if needed
|
||||
// Auto-update title if it's the first user message
|
||||
if (messages.length <= 1 && userText.length > 0) {
|
||||
setChatSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === activeSessionId
|
||||
? { ...s, title: userText.slice(0, 20) + (userText.length > 20 ? '...' : '') }
|
||||
: s
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error sending message", error);
|
||||
const errorMessage: ChatMessage = {
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
sender: 'ai',
|
||||
text: "Sorry, I encountered an error communicating with the server.",
|
||||
timestamp: new Date()
|
||||
role: 'assistant',
|
||||
content: aiContent,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
|
||||
updateSessionMessages([...messages, userMessage, aiMessage]);
|
||||
} catch (error) {
|
||||
console.error('Error sending message', error);
|
||||
const errorMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: 'Sorry, I encountered an error communicating with the server.',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
updateSessionMessages([...messages, userMessage, errorMessage]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
|
||||
if (!activeSessionId) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-white overflow-hidden items-center justify-center">
|
||||
<Activity size={48} className="text-slate-300 mb-4" />
|
||||
<h2 className="text-xl font-semibold text-slate-600">Pretor Assistant</h2>
|
||||
<p className="text-slate-400 mt-2">Select a chat history or create a new one to start.</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newSession: ChatSession = {
|
||||
id: Date.now().toString(),
|
||||
title: 'New Chat',
|
||||
messages: [
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
role: 'assistant',
|
||||
content: 'Hello! I am Pretor Assistant. How can I help you today?',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
setChatSessions([newSession, ...chatSessions]);
|
||||
setActiveSessionId(newSession.id);
|
||||
}}
|
||||
className="mt-6 px-6 py-2 bg-blue-200 text-slate-800 rounded-xl shadow-sm hover:bg-blue-300 transition-colors"
|
||||
>
|
||||
Start New Chat
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Notice we removed the outer padded div, since App.tsx is handling that layout now
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-slate-50">
|
||||
<div className="h-14 border-b border-slate-200 bg-white flex items-center justify-between px-6 shadow-sm z-10">
|
||||
<div className="flex-1 flex flex-col bg-white overflow-hidden relative">
|
||||
<div className="h-14 border-b border-slate-100 bg-white flex items-center justify-between px-6 z-10 shrink-0">
|
||||
<div className="flex items-center">
|
||||
<MessageSquare size={18} className="text-blue-600 mr-3" />
|
||||
<h1 className="font-semibold text-slate-800">Pretor Assistant</h1>
|
||||
<h1 className="font-semibold text-slate-800">{activeSession?.title || 'Chat'}</h1>
|
||||
</div>
|
||||
<div className="flex space-x-2 bg-slate-100 p-1 rounded-lg">
|
||||
<div className="flex space-x-2 bg-slate-50 p-1 rounded-lg">
|
||||
<button
|
||||
onClick={() => setMode('chat')}
|
||||
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${mode === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
|
||||
mode === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Chat
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('deploy')}
|
||||
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${mode === 'deploy' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
|
||||
mode === 'deploy' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Deploy Task
|
||||
</button>
|
||||
@@ -140,54 +193,52 @@ export function ChatPanel() {
|
||||
</div>
|
||||
|
||||
{/* Chat History */}
|
||||
<div className="flex-1 p-6 overflow-y-auto space-y-6">
|
||||
<div className="flex justify-center">
|
||||
<span className="text-xs text-slate-400 bg-slate-200/50 px-3 py-1 rounded-full">Today</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-6 overflow-y-auto space-y-6 bg-white">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
{msg.sender === 'ai' && (
|
||||
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
|
||||
<Activity size={16} className="text-blue-600" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`${msg.sender === 'user' ? 'bg-blue-600 text-white rounded-2xl rounded-tr-sm' : 'bg-white border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'} p-4 max-w-[80%] shadow-sm`}>
|
||||
<p className="text-sm leading-relaxed mb-3">{msg.text}</p>
|
||||
{msg.eventId && (
|
||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-3 flex items-center text-sm">
|
||||
<Terminal size={16} className="text-slate-400 mr-2" />
|
||||
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.eventId}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
msg.role === 'user'
|
||||
? 'bg-blue-100 text-slate-800 rounded-2xl rounded-tr-sm'
|
||||
: 'bg-slate-50 border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'
|
||||
} p-4 max-w-[80%] shadow-sm`}
|
||||
>
|
||||
<p className="text-sm leading-relaxed mb-1 whitespace-pre-wrap">{msg.content}</p>
|
||||
{typeof msg.content === 'string' && msg.content.includes('-') && msg.role === 'assistant' && (msg.content.length === 36 || msg.content.includes('任务已创建')) && (
|
||||
<div className="mt-2 bg-white border border-slate-100 rounded-lg p-3 flex items-center text-sm shadow-sm">
|
||||
<Terminal size={16} className="text-slate-400 mr-2" />
|
||||
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.content.substring(0, 36)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
|
||||
<Activity size={16} className="text-blue-600 animate-spin" />
|
||||
</div>
|
||||
<div className="bg-white border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
|
||||
<span className="flex space-x-1">
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
|
||||
<Activity size={16} className="text-blue-600 animate-spin" />
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
|
||||
<span className="flex space-x-1">
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Chat Input */}
|
||||
<div className="p-4 bg-white border-t border-slate-200">
|
||||
<div className="p-4 bg-white border-t border-slate-100 shrink-0">
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileUpload} className="hidden" />
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="absolute left-2 p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors z-10 cursor-pointer"
|
||||
@@ -201,12 +252,12 @@ export function ChatPanel() {
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||
placeholder="Ask Pretor to do something..."
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-xl pl-12 pr-12 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-inner"
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-2xl pl-12 pr-12 py-3.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={loading || !input.trim()}
|
||||
className="absolute right-2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-sm disabled:opacity-50 cursor-pointer"
|
||||
className="absolute right-2 p-1.5 bg-blue-200 text-slate-800 rounded-xl hover:bg-blue-300 transition-colors shadow-sm disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
|
||||
@@ -1,44 +1,32 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Server, Box, Cpu, HardDrive, List, MessageCircle } from 'lucide-react';
|
||||
import { useClusterState } from '../../hooks/useClusterState';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { Workflow } from '../../types';
|
||||
import type { ChatSession } from '../../App';
|
||||
|
||||
interface LeftPanelProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
selectedWorkflow: string | null;
|
||||
setSelectedWorkflow: (id: string | null) => void;
|
||||
// Hoisted state props (optional, since this panel is used for workflows too)
|
||||
chatSessions?: ChatSession[];
|
||||
setChatSessions?: React.Dispatch<React.SetStateAction<ChatSession[]>>;
|
||||
activeSessionId?: string | null;
|
||||
setActiveSessionId?: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelectedWorkflow }: LeftPanelProps) {
|
||||
const { nodes } = useClusterState();
|
||||
export function LeftPanel({
|
||||
activeTab,
|
||||
selectedWorkflow,
|
||||
setSelectedWorkflow,
|
||||
chatSessions,
|
||||
setChatSessions,
|
||||
activeSessionId,
|
||||
setActiveSessionId,
|
||||
}: LeftPanelProps) {
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loadingWorkflows, setLoadingWorkflows] = useState(false);
|
||||
|
||||
const totalNodes = nodes.length;
|
||||
const aliveNodes = nodes.filter(n => n.alive).length;
|
||||
|
||||
let totalCpu = 0;
|
||||
let usedCpu = 0;
|
||||
let totalMemory = 0;
|
||||
let usedMemory = 0;
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeTotalCpu = node.resources?.CPU || 0;
|
||||
const nodeRemainingCpu = node.remaining?.CPU || 0;
|
||||
totalCpu += nodeTotalCpu;
|
||||
usedCpu += (nodeTotalCpu - nodeRemainingCpu);
|
||||
|
||||
const nodeTotalMem = node.resources?.memory || 0;
|
||||
const nodeRemainingMem = node.remaining?.memory || 0;
|
||||
totalMemory += nodeTotalMem;
|
||||
usedMemory += (nodeTotalMem - nodeRemainingMem);
|
||||
});
|
||||
|
||||
const cpuPercent = totalCpu > 0 ? (usedCpu / totalCpu) * 100 : 0;
|
||||
const memPercent = totalMemory > 0 ? (usedMemory / totalMemory) * 100 : 0;
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: ReturnType<typeof setInterval>;
|
||||
|
||||
@@ -46,18 +34,16 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
|
||||
if (isInitial) setLoadingWorkflows(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/workflow/list');
|
||||
// Fallback parsing just in case it returns an object or array
|
||||
const data = response.data;
|
||||
let parsedWorkflows: Workflow[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
parsedWorkflows = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
// Suppose backend sends { workflows: [...] }
|
||||
parsedWorkflows = data.workflows || Object.values(data);
|
||||
}
|
||||
setWorkflows(parsedWorkflows);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch workflows", error);
|
||||
console.error('Failed to fetch workflows', error);
|
||||
setWorkflows([]);
|
||||
} finally {
|
||||
if (isInitial) setLoadingWorkflows(false);
|
||||
@@ -74,69 +60,56 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
|
||||
};
|
||||
}, [activeTab]);
|
||||
|
||||
const handleNewChat = () => {
|
||||
if (!setChatSessions || !setActiveSessionId) return;
|
||||
const newSession: ChatSession = {
|
||||
id: Date.now().toString(),
|
||||
title: 'New Chat',
|
||||
messages: [
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
role: 'assistant',
|
||||
content: 'Hello! I am Pretor Assistant. How can I help you today?',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
setChatSessions((prev) => [newSession, ...prev]);
|
||||
setActiveSessionId(newSession.id);
|
||||
};
|
||||
|
||||
const handleDeleteChat = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation();
|
||||
if (!setChatSessions || !setActiveSessionId || !chatSessions) return;
|
||||
const updated = chatSessions.filter((s) => s.id !== id);
|
||||
setChatSessions(updated);
|
||||
if (activeSessionId === id) {
|
||||
setActiveSessionId(updated.length > 0 ? updated[0].id : null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-72 bg-white border-r border-slate-200 flex flex-col z-0 shrink-0">
|
||||
{/* Top: Cluster Status */}
|
||||
<div className="h-1/3 p-4 border-b border-slate-100 flex flex-col">
|
||||
<h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-4 flex items-center">
|
||||
<Server size={16} className="mr-2" />
|
||||
Cluster Status
|
||||
</h2>
|
||||
<div className="space-y-4 flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-slate-600">
|
||||
<Box size={16} className="mr-2 text-blue-500" />
|
||||
<span className="text-sm">Active Nodes</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-800">{aliveNodes} / {totalNodes || 0}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-slate-600">
|
||||
<Cpu size={16} className="mr-2 text-indigo-500" />
|
||||
<span className="text-sm">Cluster CPU</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-800">{cpuPercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
||||
<div className="bg-indigo-500 h-1.5 rounded-full" style={{ width: `${cpuPercent}%` }}></div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center text-slate-600">
|
||||
<HardDrive size={16} className="mr-2 text-green-500" />
|
||||
<span className="text-sm">Cluster RAM</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-800">
|
||||
{(totalMemory > 0 ? usedMemory / (1024 ** 3) : 0).toFixed(1)} GB
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
||||
<div className="bg-green-500 h-1.5 rounded-full" style={{ width: `${memPercent}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom: Tabs for Workflows & Basic Chats */}
|
||||
<div className="w-72 bg-white border-r border-slate-100 flex flex-col z-0 shrink-0">
|
||||
{/* Bottom: Tab Selection */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex border-b border-slate-100">
|
||||
<button
|
||||
onClick={() => setActiveTab('chats')}
|
||||
className={`flex-1 py-3 text-xs font-medium text-center uppercase tracking-wider transition-colors ${activeTab === 'chats' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-slate-500 hover:bg-slate-50'}`}
|
||||
>
|
||||
<MessageCircle size={14} className="inline mr-1.5 -mt-0.5" />
|
||||
Chats
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('workflows')}
|
||||
className={`flex-1 py-3 text-xs font-medium text-center uppercase tracking-wider transition-colors ${activeTab === 'workflows' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-slate-500 hover:bg-slate-50'}`}
|
||||
>
|
||||
<List size={14} className="inline mr-1.5 -mt-0.5" />
|
||||
Workflows
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border-b border-slate-100 bg-slate-50">
|
||||
<span className="text-sm font-semibold text-slate-600 uppercase tracking-wider">
|
||||
{activeTab === 'chats' ? 'Chat History' : 'Workflows'}
|
||||
</span>
|
||||
{activeTab === 'chats' && (
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
|
||||
title="New Chat"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-4 overflow-y-auto">
|
||||
<div className="flex-1 p-3 overflow-y-auto">
|
||||
{activeTab === 'workflows' && (
|
||||
<div className="space-y-2">
|
||||
{loadingWorkflows ? (
|
||||
@@ -148,11 +121,29 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
|
||||
<div
|
||||
key={wf.event_id}
|
||||
onClick={() => setSelectedWorkflow(wf.event_id)}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-all ${selectedWorkflow === wf.event_id ? 'border-blue-200 bg-blue-50 shadow-sm' : 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'}`}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-all ${
|
||||
selectedWorkflow === wf.event_id
|
||||
? 'border-blue-300 bg-blue-50 shadow-sm'
|
||||
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className={`font-medium text-sm ${selectedWorkflow === wf.event_id ? 'text-blue-700' : 'text-slate-700'}`}>{wf.workflow_title || 'Unnamed Workflow'}</span>
|
||||
<span className={`flex h-2 w-2 rounded-full ${wf.status === 'llm_working' || wf.status === 'tool_working' ? 'bg-green-400 animate-pulse' : wf.status === 'failed' ? 'bg-red-400' : 'bg-slate-300'}`}></span>
|
||||
<span
|
||||
className={`font-medium text-sm ${
|
||||
selectedWorkflow === wf.event_id ? 'text-blue-700' : 'text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{wf.workflow_title || 'Unnamed Workflow'}
|
||||
</span>
|
||||
<span
|
||||
className={`flex h-2 w-2 rounded-full ${
|
||||
wf.status === 'llm_working' || wf.status === 'tool_working'
|
||||
? 'bg-green-400 animate-pulse'
|
||||
: wf.status === 'failed'
|
||||
? 'bg-red-400'
|
||||
: 'bg-slate-300'
|
||||
}`}
|
||||
></span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 font-mono line-clamp-1">ID: {wf.event_id}</p>
|
||||
</div>
|
||||
@@ -160,8 +151,42 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'chats' && (
|
||||
{activeTab === 'chats' && chatSessions && (
|
||||
<div className="space-y-2">
|
||||
{chatSessions.length === 0 ? (
|
||||
<div className="text-center text-slate-400 text-sm py-8">
|
||||
No chat history.<br/>Click + to start a new chat.
|
||||
</div>
|
||||
) : (
|
||||
chatSessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => setActiveSessionId?.(session.id)}
|
||||
className={`group flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-all ${
|
||||
activeSessionId === session.id
|
||||
? 'border-blue-300 bg-blue-50 shadow-sm'
|
||||
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0 mr-2">
|
||||
<h3 className={`font-medium text-sm truncate ${
|
||||
activeSessionId === session.id ? 'text-blue-700' : 'text-slate-700'
|
||||
}`}>
|
||||
{session.title}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{new Date(session.updatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => handleDeleteChat(e, session.id)}
|
||||
className="text-slate-400 opacity-0 group-hover:opacity-100 hover:text-red-500 transition-all"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Terminal, Activity, RefreshCw, CheckCircle2, Circle, XCircle, Clock, Loader2 } from 'lucide-react';
|
||||
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { WorkflowDetail, WorkflowStep } from '../../types';
|
||||
import type { WorkflowDetail } from '../../types';
|
||||
import { WorkflowDiagram } from './WorkflowDiagram';
|
||||
|
||||
interface RightPanelProps {
|
||||
selectedWorkflow: string | null;
|
||||
}
|
||||
|
||||
function stepStatusIcon(status: string) {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <CheckCircle2 size={14} className="text-green-500" />;
|
||||
case 'running':
|
||||
return <Loader2 size={14} className="text-blue-500 animate-spin" />;
|
||||
case 'failed':
|
||||
return <XCircle size={14} className="text-red-500" />;
|
||||
default:
|
||||
return <Circle size={14} className="text-slate-300" />;
|
||||
}
|
||||
}
|
||||
|
||||
export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
const [detail, setDetail] = useState<WorkflowDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [sseConnected, setSseConnected] = useState(false);
|
||||
const [replyText, setReplyText] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'chat' | 'diagram'>('chat');
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const logsEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchDetail = async (traceId: string) => {
|
||||
setLoading(true);
|
||||
setLogs([]);
|
||||
try {
|
||||
const response = await apiClient.get(`/api/v1/workflow/${traceId}`);
|
||||
setDetail(response.data);
|
||||
@@ -48,6 +38,7 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
}
|
||||
|
||||
fetchDetail(selectedWorkflow);
|
||||
setLogs([]); // Reset logs when changing workflow
|
||||
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
@@ -55,17 +46,13 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`);
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.onopen = () => {
|
||||
setSseConnected(true);
|
||||
};
|
||||
es.onopen = () => setSseConnected(true);
|
||||
|
||||
es.onmessage = (event) => {
|
||||
setLogs(prev => [...prev, event.data]);
|
||||
};
|
||||
|
||||
es.onerror = () => {
|
||||
setSseConnected(false);
|
||||
};
|
||||
es.onerror = () => setSseConnected(false);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDetail(selectedWorkflow);
|
||||
@@ -78,127 +65,127 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
};
|
||||
}, [selectedWorkflow]);
|
||||
|
||||
const isActive = detail?.status === 'llm_working' || detail?.status === 'tool_working';
|
||||
useEffect(() => {
|
||||
if (activeTab === 'chat' && logsEndRef.current) {
|
||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [logs, activeTab]);
|
||||
|
||||
if (!selectedWorkflow) {
|
||||
return (
|
||||
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0 justify-center items-center p-6 text-center">
|
||||
<Activity size={32} className="text-slate-300 mb-4" />
|
||||
<h3 className="text-sm font-semibold text-slate-600">No Workflow Selected</h3>
|
||||
<p className="text-xs text-slate-400 mt-2">Select a workflow from the left panel to view its details.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const handleReplySubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!replyText.trim() || !selectedWorkflow) return;
|
||||
|
||||
const message = replyText.trim();
|
||||
setReplyText('');
|
||||
setLogs(prev => [...prev, `[You]: ${message}`]);
|
||||
|
||||
try {
|
||||
await apiClient.post(`/api/v1/workflow/reply/${selectedWorkflow}`, { message });
|
||||
} catch (err) {
|
||||
console.error("Failed to send reply", err);
|
||||
setLogs(prev => [...prev, `[System Error]: Failed to send reply.`]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedWorkflow) return null;
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0">
|
||||
<div className="h-14 border-b border-slate-100 flex items-center px-4 justify-between bg-slate-50/50">
|
||||
<h2 className="font-semibold text-slate-800 text-sm flex items-center">
|
||||
<Terminal size={16} className="mr-2 text-slate-500" />
|
||||
Workflow Detail
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 text-xs rounded-md font-medium border ${sseConnected ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500 border-slate-200'}`}>
|
||||
{sseConnected ? 'Live' : '--'}
|
||||
<div className="flex-1 bg-white border-l border-slate-200 flex flex-col z-0 relative">
|
||||
<div className="h-14 border-b border-slate-100 flex items-center px-6 justify-between bg-white z-10 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<Terminal size={18} className="text-blue-500" />
|
||||
<span className="truncate max-w-[200px]" title={detail?.workflow_title || 'Loading...'}>
|
||||
{detail?.workflow_title || 'Workflow Details'}
|
||||
</span>
|
||||
</h2>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full font-medium ${sseConnected ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
|
||||
{sseConnected ? 'Live' : 'Disconnected'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => selectedWorkflow && fetchDetail(selectedWorkflow)}
|
||||
className="p-1 text-slate-400 hover:text-blue-600 rounded transition-colors"
|
||||
title="Refresh"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<LayoutList size={16} />
|
||||
交流日志
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('diagram')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'diagram' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<GitFork size={16} />
|
||||
流程图
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => fetchDetail(selectedWorkflow)}
|
||||
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Refresh Data"
|
||||
>
|
||||
<RefreshCw size={16} className={loading ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-4 overflow-y-auto">
|
||||
{loading && !detail ? (
|
||||
<div className="text-center text-slate-400 text-sm py-8">
|
||||
<Loader2 size={24} className="animate-spin mx-auto mb-2" />
|
||||
Loading...
|
||||
<div className="flex-1 flex overflow-hidden bg-slate-50 relative">
|
||||
{activeTab === 'diagram' ? (
|
||||
<div className="absolute inset-0">
|
||||
{detail?.steps && detail.steps.length > 0 ? (
|
||||
<WorkflowDiagram steps={detail.steps} currentStep={detail.current_step} status={detail.status} />
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">
|
||||
Workflow steps are not yet generated.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : !detail ? (
|
||||
<div className="text-center text-slate-400 text-sm py-8">Failed to load workflow details</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-base font-bold text-slate-800">
|
||||
{detail.workflow_title || 'Workflow'}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 font-mono mt-1">ID: {detail.event_id}</p>
|
||||
{detail.command && (
|
||||
<p className="text-xs text-slate-500 mt-1 truncate">Command: {detail.command}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
detail.status === 'failed' ? 'bg-red-100 text-red-700' :
|
||||
isActive ? 'bg-blue-100 text-blue-700' :
|
||||
detail.status === 'waiting_llm_working' || detail.status === 'waiting_tool_working' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{detail.status}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">
|
||||
Step {detail.current_step}/{detail.steps.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
{detail.steps.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Steps</h4>
|
||||
<div className="space-y-1.5">
|
||||
{detail.steps.map((step: WorkflowStep) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs border ${
|
||||
step.step === detail.current_step && isActive
|
||||
? 'border-blue-200 bg-blue-50'
|
||||
: step.status === 'completed'
|
||||
? 'border-green-100 bg-green-50/50'
|
||||
: step.status === 'failed'
|
||||
? 'border-red-100 bg-red-50/50'
|
||||
: 'border-slate-100 bg-white'
|
||||
}`}
|
||||
>
|
||||
{stepStatusIcon(step.status)}
|
||||
<span className="font-medium text-slate-700 w-5 text-right">{step.step}</span>
|
||||
<span className="text-slate-600 truncate flex-1">{step.name}</span>
|
||||
<span className="text-slate-400 text-[10px]">{step.node}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex-1 flex flex-col p-6 overflow-hidden">
|
||||
{/* Command Header */}
|
||||
{detail?.command && (
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4 mb-4 shadow-sm shrink-0">
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Original Command</h3>
|
||||
<p className="text-slate-700 text-sm">{detail.command}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{detail.steps.length === 0 && (
|
||||
<div className="text-center py-4">
|
||||
<Clock size={24} className="text-slate-300 mx-auto mb-2" />
|
||||
<p className="text-xs text-slate-400">Workflow is being generated...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SSE Logs */}
|
||||
{logs.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Live Logs</h4>
|
||||
<div className="relative border-l-2 border-slate-200 ml-3 pl-5 space-y-3">
|
||||
{logs.map((msg, idx) => (
|
||||
<div key={idx} className="relative">
|
||||
<div className={`absolute -left-[27px] top-1 w-3 h-3 rounded-full border-2 border-white shadow-sm ${idx === logs.length - 1 && sseConnected ? 'bg-blue-500 animate-pulse' : 'bg-green-500'}`} />
|
||||
<p className="text-[11px] font-mono text-slate-600 leading-relaxed break-all">{msg}</p>
|
||||
{/* Live Chat / Logs Area */}
|
||||
<div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-3 font-mono text-sm">
|
||||
{logs.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">
|
||||
Waiting for events...
|
||||
</div>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className={`p-3 rounded-lg ${log.startsWith('[You]') ? 'bg-blue-50 border border-blue-100 text-blue-800 self-end ml-12' : 'bg-slate-50 border border-slate-100 text-slate-700 mr-12'}`}>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
|
||||
{logs.length === 0 && sseConnected && isActive && (
|
||||
<div className="text-xs text-slate-400 italic mt-2">Waiting for live events...</div>
|
||||
)}
|
||||
</>
|
||||
{/* Input Area */}
|
||||
<form onSubmit={handleReplySubmit} className="relative shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder="Reply to the workflow..."
|
||||
className="w-full bg-white border border-slate-200 rounded-xl pl-4 pr-12 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!replyText.trim()}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<SendHorizontal size={16} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Background,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
MarkerType,
|
||||
BackgroundVariant
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import type { WorkflowStep } from '../../types';
|
||||
|
||||
interface WorkflowDiagramProps {
|
||||
steps: WorkflowStep[];
|
||||
currentStep: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function WorkflowDiagram({ steps, currentStep, status }: WorkflowDiagramProps) {
|
||||
const isWorkflowActive = status === 'llm_working' || status === 'tool_working';
|
||||
|
||||
const initialNodes = useMemo(() => {
|
||||
return steps.map((step, index) => {
|
||||
const isCurrent = step.step === currentStep && isWorkflowActive;
|
||||
const isCompleted = step.status === 'completed';
|
||||
const isFailed = step.status === 'failed';
|
||||
|
||||
let bgColor = '#ffffff';
|
||||
let borderColor = '#e2e8f0'; // slate-200
|
||||
let textColor = '#334155'; // slate-700
|
||||
|
||||
if (isCurrent) {
|
||||
bgColor = '#eff6ff'; // blue-50
|
||||
borderColor = '#3b82f6'; // blue-500
|
||||
textColor = '#1e40af'; // blue-800
|
||||
} else if (isFailed) {
|
||||
bgColor = '#fef2f2'; // red-50
|
||||
borderColor = '#ef4444'; // red-500
|
||||
textColor = '#991b1b'; // red-800
|
||||
} else if (isCompleted) {
|
||||
bgColor = '#f0fdf4'; // green-50
|
||||
borderColor = '#22c55e'; // green-500
|
||||
textColor = '#166534'; // green-800
|
||||
}
|
||||
|
||||
return {
|
||||
id: step.step.toString(),
|
||||
position: { x: 250, y: index * 120 + 50 },
|
||||
data: {
|
||||
label: (
|
||||
<div className="flex flex-col items-center p-2 min-w-[150px]">
|
||||
<div className="text-xs font-semibold mb-1 opacity-70 uppercase tracking-wider">{step.node}</div>
|
||||
<div className="text-sm font-medium">{step.name}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
style: {
|
||||
background: bgColor,
|
||||
border: `2px solid ${borderColor}`,
|
||||
borderRadius: '8px',
|
||||
color: textColor,
|
||||
boxShadow: isCurrent ? '0 4px 6px -1px rgba(59, 130, 246, 0.2)' : '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
padding: '4px',
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [steps, currentStep, isWorkflowActive]);
|
||||
|
||||
const initialEdges = useMemo(() => {
|
||||
const edges = [];
|
||||
for (let i = 0; i < steps.length - 1; i++) {
|
||||
edges.push({
|
||||
id: `e${steps[i].step}-${steps[i + 1].step}`,
|
||||
source: steps[i].step.toString(),
|
||||
target: steps[i + 1].step.toString(),
|
||||
animated: steps[i].step === currentStep && isWorkflowActive,
|
||||
style: { stroke: '#cbd5e1', strokeWidth: 2 },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: '#cbd5e1',
|
||||
},
|
||||
});
|
||||
}
|
||||
return edges;
|
||||
}, [steps, currentStep, isWorkflowActive]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
// Update nodes and edges if props change
|
||||
useEffect(() => {
|
||||
setNodes(initialNodes);
|
||||
setEdges(initialEdges);
|
||||
}, [initialNodes, initialEdges, setNodes, setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
fitView
|
||||
attributionPosition="bottom-right"
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#cbd5e1" />
|
||||
<Controls className="bg-white border-slate-200 fill-slate-500 shadow-sm rounded-md" />
|
||||
<MiniMap
|
||||
nodeColor={(n) => {
|
||||
if (n.style?.background) return n.style.background as string;
|
||||
return '#e2e8f0';
|
||||
}}
|
||||
maskColor="rgba(248, 250, 252, 0.7)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { Workflow } from '../../types';
|
||||
import { PlayCircle, CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||
|
||||
interface WorkflowListViewProps {
|
||||
onSelectWorkflow: (id: string) => void;
|
||||
}
|
||||
|
||||
export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/workflow/list');
|
||||
const data = response.data;
|
||||
let parsedWorkflows: Workflow[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
parsedWorkflows = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
parsedWorkflows = data.workflows || Object.values(data);
|
||||
}
|
||||
setWorkflows(parsedWorkflows);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch workflows', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchWorkflows();
|
||||
const intervalId = setInterval(fetchWorkflows, 5000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const getStatusIcon = (status?: string) => {
|
||||
if (status === 'completed') return <CheckCircle size={20} className="text-green-500" />;
|
||||
if (status === 'failed') return <XCircle size={20} className="text-red-500" />;
|
||||
if (status === 'llm_working' || status === 'tool_working' || status?.includes('working'))
|
||||
return <PlayCircle size={20} className="text-blue-500 animate-pulse" />;
|
||||
return <Clock size={20} className="text-slate-400" />;
|
||||
};
|
||||
|
||||
const getStatusBadge = (status?: string) => {
|
||||
let colorClass = "bg-slate-100 text-slate-600";
|
||||
let label = "Waiting";
|
||||
|
||||
if (status === 'completed') { colorClass = "bg-green-100 text-green-700"; label = "Completed"; }
|
||||
else if (status === 'failed') { colorClass = "bg-red-100 text-red-700"; label = "Failed"; }
|
||||
else if (status?.includes('working')) { colorClass = "bg-blue-100 text-blue-700 animate-pulse"; label = "Running"; }
|
||||
|
||||
return (
|
||||
<span className={`px-2.5 py-1 text-xs font-medium rounded-full ${colorClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center p-6 bg-slate-50">
|
||||
<div className="text-slate-400">Loading Workflows...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col p-8 bg-slate-50 overflow-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Workflows</h1>
|
||||
<p className="text-slate-500 mt-1">Manage and monitor your automated processes.</p>
|
||||
</div>
|
||||
|
||||
{workflows.length === 0 ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-slate-200 rounded-2xl bg-white p-12 text-center">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
|
||||
<PlayCircle size={32} className="text-slate-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-slate-800 mb-2">No Workflows Found</h3>
|
||||
<p className="text-slate-500 max-w-sm">
|
||||
Workflows created from your chats will appear here automatically.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{workflows.map((wf) => (
|
||||
<div
|
||||
key={wf.event_id}
|
||||
onClick={() => onSelectWorkflow(wf.event_id)}
|
||||
className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm hover:shadow-md hover:border-blue-300 transition-all cursor-pointer group flex flex-col h-full"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="p-2.5 bg-blue-50 text-blue-600 rounded-xl group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
{getStatusIcon(wf.status)}
|
||||
</div>
|
||||
{getStatusBadge(wf.status)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2 line-clamp-1" title={wf.workflow_title || 'Unnamed Workflow'}>
|
||||
{wf.workflow_title || 'Unnamed Workflow'}
|
||||
</h3>
|
||||
|
||||
<div className="mt-auto">
|
||||
{wf.message && (
|
||||
<div className="text-sm text-slate-500 line-clamp-2 mt-4 bg-slate-50 p-3 rounded-lg border border-slate-100">
|
||||
<span className="font-medium text-slate-600 block mb-1">Command:</span>
|
||||
"{wf.message}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mt-5 text-xs text-slate-400">
|
||||
<span className="font-mono bg-slate-100 px-2 py-1 rounded truncate max-w-[140px]" title={wf.event_id}>
|
||||
{wf.event_id}
|
||||
</span>
|
||||
{wf.create_time && (
|
||||
<span>{new Date(wf.create_time).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { ChevronLeft, ChevronRight, MessageSquare, Workflow, Box, Bot } from 'lucide-react';
|
||||
|
||||
interface CollapsibleSidebarProps {
|
||||
mode: 'work' | 'agent';
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
workTab: 'chat' | 'workflow';
|
||||
setWorkTab: (tab: 'chat' | 'workflow') => void;
|
||||
agentTab: 'plugin' | 'agents';
|
||||
setAgentTab: (tab: 'plugin' | 'agents') => void;
|
||||
}
|
||||
|
||||
export function CollapsibleSidebar({
|
||||
mode,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
workTab,
|
||||
setWorkTab,
|
||||
agentTab,
|
||||
setAgentTab,
|
||||
}: CollapsibleSidebarProps) {
|
||||
|
||||
const getWorkNav = () => (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setWorkTab('chat')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
workTab === 'chat'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<MessageSquare size={18} className="mr-3" />
|
||||
{isOpen && <span>Chat</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWorkTab('workflow')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
workTab === 'workflow'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Workflow size={18} className="mr-3" />
|
||||
{isOpen && <span>Workflow</span>}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
const getAgentNav = () => (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setAgentTab('plugin')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
agentTab === 'plugin'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Box size={18} className="mr-3" />
|
||||
{isOpen && <span>Plugin</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAgentTab('agents')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
agentTab === 'agents'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Bot size={18} className="mr-3" />
|
||||
{isOpen && <span>Agents</span>}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
// Background is slightly darker than the page background but lighter than the topbar
|
||||
// Let's make it the "main nav light blue" level. We will use bg-blue-50 for it,
|
||||
// and make the page background essentially white or extremely light slate,
|
||||
// but since the instruction says "page background is the next lightest",
|
||||
// let's use bg-blue-100 for Sidebar and bg-blue-50 for page background.
|
||||
return (
|
||||
<div
|
||||
className={`bg-blue-50 border-r border-blue-100 flex flex-col transition-all duration-300 relative z-10 ${
|
||||
isOpen ? 'w-64' : 'w-16'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 py-4 space-y-2 overflow-y-auto">
|
||||
{mode === 'work' ? getWorkNav() : getAgentNav()}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="h-10 w-10 absolute -right-5 top-1/2 -translate-y-1/2 bg-white border border-blue-200 rounded-full flex items-center justify-center text-slate-400 hover:text-blue-600 hover:border-blue-400 shadow-sm transition-all z-20"
|
||||
>
|
||||
{isOpen ? <ChevronLeft size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
import { Activity, MessageSquare, Settings, Bot, Box } from 'lucide-react';
|
||||
|
||||
interface SidebarProps {
|
||||
currentView: string;
|
||||
setCurrentView: (view: string) => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ currentView, setCurrentView }: SidebarProps) {
|
||||
return (
|
||||
<div className="w-12 bg-white border-r border-slate-200 flex flex-col items-center py-4 space-y-6 shadow-sm z-10 shrink-0">
|
||||
<div
|
||||
className="w-8 h-8 bg-blue-600 rounded-xl flex items-center justify-center text-white shadow-md shadow-blue-200 cursor-pointer hover:bg-blue-700 transition-colors"
|
||||
onClick={() => setCurrentView('dashboard')}
|
||||
>
|
||||
<Activity size={18} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-4 flex-1 mt-8">
|
||||
<button
|
||||
onClick={() => setCurrentView('dashboard')}
|
||||
className={`p-1.5 rounded-lg transition-colors ${currentView === 'dashboard' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
|
||||
title="Chat"
|
||||
>
|
||||
<MessageSquare size={18} />
|
||||
</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
|
||||
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'}`}
|
||||
title="Settings"
|
||||
>
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Settings, BrainCircuit } from 'lucide-react';
|
||||
|
||||
interface TopBarProps {
|
||||
mode: 'work' | 'agent';
|
||||
setMode: (mode: 'work' | 'agent') => void;
|
||||
showSettings: boolean;
|
||||
setShowSettings: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export function TopBar({ mode, setMode, showSettings, setShowSettings }: TopBarProps) {
|
||||
return (
|
||||
<div className="h-14 bg-blue-100 text-blue-900 flex items-center justify-between px-4 shrink-0 shadow-sm z-20 relative">
|
||||
{/* Left: Logo */}
|
||||
<div className="flex items-center space-x-2 font-bold text-xl tracking-tight text-blue-900">
|
||||
<BrainCircuit className="text-blue-600" size={24} />
|
||||
<span>Pretor</span>
|
||||
</div>
|
||||
|
||||
{/* Right Container: Mode Toggle Switch + Settings */}
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
{/* Mode Toggle Switch */}
|
||||
<div className="flex items-center space-x-1 bg-blue-200/50 p-1 rounded-full border border-blue-200">
|
||||
<button
|
||||
onClick={() => { setMode('work'); setShowSettings(false); }}
|
||||
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-all ${
|
||||
mode === 'work' && !showSettings
|
||||
? 'bg-white text-blue-700 shadow-sm'
|
||||
: 'text-blue-700 hover:text-blue-900 hover:bg-white/50'
|
||||
}`}
|
||||
>
|
||||
Work
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setMode('agent'); setShowSettings(false); }}
|
||||
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-all ${
|
||||
mode === 'agent' && !showSettings
|
||||
? 'bg-white text-blue-700 shadow-sm'
|
||||
: 'text-blue-700 hover:text-blue-900 hover:bg-white/50'
|
||||
}`}
|
||||
>
|
||||
Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
<button
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
showSettings ? 'bg-white text-blue-700 shadow-sm' : 'text-blue-700 hover:bg-white/50 hover:text-blue-900'
|
||||
}`}
|
||||
title="Settings"
|
||||
>
|
||||
<Settings size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { ToolSettings } from './ToolSettings';
|
||||
import { WorkflowTemplateSettings } from './WorkflowTemplateSettings';
|
||||
|
||||
interface PluginLayoutProps {
|
||||
resourceTab: string;
|
||||
setResourceTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export function PluginLayout({ resourceTab, setResourceTab }: PluginLayoutProps) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
|
||||
{/* Top Tabs for Plugin Module */}
|
||||
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shadow-sm z-10 shrink-0 space-x-6">
|
||||
<button
|
||||
onClick={() => setResourceTab('skill')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
resourceTab === 'skill' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Skills
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResourceTab('workflow_template')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
resourceTab === 'workflow_template' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Workflow Templates
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResourceTab('tool')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
resourceTab === 'tool' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Tools
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{resourceTab === 'skill' && <SkillSettings />}
|
||||
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
|
||||
{resourceTab === 'tool' && <ToolSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+4
-4
@@ -79,7 +79,7 @@ export function SkillSettings() {
|
||||
|
||||
<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">
|
||||
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center">
|
||||
<Download size={20} />
|
||||
</div>
|
||||
<div>
|
||||
@@ -98,7 +98,7 @@ export function SkillSettings() {
|
||||
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"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -108,7 +108,7 @@ export function SkillSettings() {
|
||||
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"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,7 +120,7 @@ export function SkillSettings() {
|
||||
<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"
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
{installing ? 'Installing...' : 'Install'}
|
||||
+3
-3
@@ -88,7 +88,7 @@ export function WorkflowTemplateSettings() {
|
||||
|
||||
<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">
|
||||
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center">
|
||||
<FileCode size={20} />
|
||||
</div>
|
||||
<div>
|
||||
@@ -105,7 +105,7 @@ export function WorkflowTemplateSettings() {
|
||||
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"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +116,7 @@ export function WorkflowTemplateSettings() {
|
||||
<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"
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
{creating ? 'Creating...' : 'Create Template'}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Wrench, Database, FileCode } from 'lucide-react';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { ToolSettings } from './ToolSettings';
|
||||
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('tool')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'tool' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||
>
|
||||
<Database size={18} className="mr-3" />
|
||||
Tools
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resource Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{resourceTab === 'skill' && <SkillSettings />}
|
||||
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
|
||||
{resourceTab === 'tool' && <ToolSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,6 +65,8 @@ export interface Workflow {
|
||||
event_id: string;
|
||||
workflow_title: string;
|
||||
status?: string;
|
||||
message?: string;
|
||||
create_time?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowStep {
|
||||
|
||||
@@ -4,6 +4,7 @@ from pretor.worker_individual.worker_cluster import WorkerCluster
|
||||
from pretor.utils.banner import print_banner
|
||||
from pretor.core.database.postgres import PostgresDatabase
|
||||
from pretor.core.global_state_machine.global_state_machine import GlobalStateMachine
|
||||
from pretor.core.global_state_machine.global_workflow_manager import GlobalWorkflowManager
|
||||
from pretor.core.individual.supervisory_node.supervisory_node import SupervisoryNode
|
||||
from pretor.core.individual.consciousness_node.consciousness_node import ConsciousnessNode
|
||||
from pretor.core.individual.control_node.control_node import ControlNode
|
||||
@@ -11,8 +12,13 @@ from pretor.core.workflow.workflow_runner import WorkflowRunningEngine
|
||||
from pretor.core.api import PretorGateway
|
||||
from ray import serve
|
||||
import os
|
||||
import secrets
|
||||
|
||||
|
||||
_secret_key = os.getenv("SECRET_KEY")
|
||||
if not _secret_key or _secret_key in {"secret", "114514"}:
|
||||
_secret_key = secrets.token_urlsafe(32)
|
||||
os.environ["SECRET_KEY"] = _secret_key
|
||||
print("⚠️ 警告: 未提供有效的 SECRET_KEY 或使用了不安全的默认值,已生成并设置随机密钥。")
|
||||
|
||||
async def start_system():
|
||||
env_vars = {
|
||||
@@ -21,7 +27,7 @@ async def start_system():
|
||||
"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"),
|
||||
"SECRET_KEY": os.getenv("SECRET_KEY"),
|
||||
}
|
||||
|
||||
ray.init(ignore_reinit_error=True,
|
||||
@@ -51,13 +57,19 @@ async def start_system():
|
||||
print(f"\n[致命错误] GlobalStateMachine 启动失败!真实报错如下:\n{e}\n")
|
||||
return
|
||||
|
||||
global_workflow_manager = GlobalWorkflowManager.options(
|
||||
name='global_workflow_manager',
|
||||
namespace='pretor',
|
||||
lifetime='detached'
|
||||
).remote()
|
||||
|
||||
# 4. 启动核心节点
|
||||
supervisory_node = SupervisoryNode.options(name='supervisory_node').remote()
|
||||
consciousness_node = ConsciousnessNode.options(name='consciousness_node').remote()
|
||||
control_node = ControlNode.options(name='control_node').remote()
|
||||
|
||||
try:
|
||||
worker_cluster_actor = WorkerCluster.options(
|
||||
WorkerCluster.options(
|
||||
name="worker_cluster",
|
||||
lifetime="detached" # 保证它在后台一直运行
|
||||
).remote()
|
||||
@@ -73,6 +85,14 @@ async def start_system():
|
||||
# 异步拉起 runner 协程群
|
||||
workflow_engine.run.remote()
|
||||
|
||||
print("正在等待 GlobalWorkflowManager 初始化与恢复工作流...")
|
||||
try:
|
||||
await global_workflow_manager.init_manager.remote()
|
||||
print("GlobalWorkflowManager 初始化成功!")
|
||||
except Exception as e:
|
||||
print(f"\n[致命错误] GlobalWorkflowManager 启动失败!真实报错如下:\n{e}\n")
|
||||
return
|
||||
|
||||
# 6. 启动 FastAPI 网关 (使用 Ray Serve)
|
||||
serve.start(http_options={"host": "0.0.0.0", "port": 8000})
|
||||
serve.run(PretorGateway.bind())
|
||||
|
||||
@@ -23,6 +23,8 @@ from pretor.utils.agent_model import ResponseModel, DepsModel
|
||||
from pretor.utils.error import ModelNotExistError
|
||||
|
||||
class AgentFactory:
|
||||
"""AgentFactory 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentFactory 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
def __init__(self):
|
||||
self._models_mapping = {"openai": (OpenAIChatModel, OpenAIProvider),
|
||||
"claude": (AnthropicModel, AnthropicProvider),
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import re
|
||||
import json
|
||||
from typing import Type, TypeVar, Any, Generic
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic_ai.run import AgentRunResult
|
||||
from pydantic_ai import Agent
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
class AgentRunResultProxy:
|
||||
"""AgentRunResultProxy 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentRunResultProxy 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
def __init__(self, original, parsed):
|
||||
self._original = original
|
||||
self._parsed = parsed
|
||||
def __getattr__(self, name):
|
||||
"""检索并获取特定的 getattr 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: name: 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if name == 'data':
|
||||
return self._parsed
|
||||
if name == 'output':
|
||||
@@ -78,6 +97,10 @@ class DeepSeekReasonerAgent(Generic[T]):
|
||||
)
|
||||
|
||||
def _parse_output(self, text: str) -> Any:
|
||||
"""执行与 parse output 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: text (str): 控制逻辑流向的具体字符串参数,指定了期望的 text 内容。
|
||||
Returns: (Any): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if not self.has_custom_output:
|
||||
return text
|
||||
|
||||
@@ -114,10 +137,18 @@ class DeepSeekReasonerAgent(Generic[T]):
|
||||
|
||||
def __getattr__(self, item):
|
||||
# Delegate any unknown attributes (like .system_prompt, .tool) to the underlying pydantic_ai Agent
|
||||
"""检索并获取特定的 getattr 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: item: 参与 getattr 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return getattr(self.agent, item)
|
||||
|
||||
async def run(self, user_prompt: str, deps: Any = None, message_history: list = None, **kwargs) -> Any:
|
||||
# Custom retry loop
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_prompt (str): 控制逻辑流向的具体字符串参数,指定了期望的 user prompt 内容。 deps (Any): 参与 run 逻辑运算或数据构建的上下文依赖对象。 message_history (list): 批量操作所需的列表集合,囊括了需要统一处理的多个 message history 元素。
|
||||
Returns: (Any): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
current_history = message_history or []
|
||||
last_exception = None
|
||||
|
||||
|
||||
@@ -27,18 +27,26 @@ from pretor.core.database.table.user import UserAuthority
|
||||
agent_router = APIRouter(prefix="/api/v1/agent", tags=["agent"])
|
||||
|
||||
class AgentRegister(BaseModel):
|
||||
"""AgentRegister 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
provider_title: str
|
||||
model_id: str
|
||||
individual_name: str
|
||||
tools: Optional[List[str]] = None
|
||||
|
||||
class AgentLocalRegister(BaseModel):
|
||||
"""AgentLocalRegister 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentLocalRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
path: str
|
||||
individual_name: str
|
||||
tools: Optional[List[str]] = None
|
||||
|
||||
@agent_router.get("")
|
||||
async def get_system_nodes(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 get system nodes 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get system nodes 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
configs = await postgres_database.get_all_system_node_configs.remote()
|
||||
return {"system_nodes": configs}
|
||||
@@ -46,6 +54,10 @@ async def get_system_nodes(_: TokenData = Depends(RoleChecker(allowed_roles=User
|
||||
@agent_router.post("")
|
||||
async def load_agent(agent_register: Union[AgentRegister, AgentLocalRegister],
|
||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 load agent 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_register (Union[AgentRegister, AgentLocalRegister]): 参与 load agent 逻辑运算或数据构建的上下文依赖对象。 _ (TokenData): 参与 load agent 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
|
||||
@@ -81,6 +93,8 @@ async def load_agent(agent_register: Union[AgentRegister, AgentLocalRegister],
|
||||
|
||||
|
||||
class WorkerIndividualCreate(BaseModel):
|
||||
"""WorkerIndividualCreate 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
|
||||
agent_name: str
|
||||
agent_type: AgentType
|
||||
description: str
|
||||
@@ -94,6 +108,8 @@ class WorkerIndividualCreate(BaseModel):
|
||||
|
||||
|
||||
class WorkerIndividualUpdate(BaseModel):
|
||||
"""WorkerIndividualUpdate 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
|
||||
agent_name: Optional[str] = None
|
||||
agent_type: Optional[AgentType] = None
|
||||
description: Optional[str] = None
|
||||
@@ -109,6 +125,10 @@ class WorkerIndividualUpdate(BaseModel):
|
||||
@agent_router.post("/worker")
|
||||
async def create_worker_individual(worker_data: WorkerIndividualCreate,
|
||||
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 create worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: worker_data (WorkerIndividualCreate): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
data_dict = worker_data.model_dump()
|
||||
data_dict["owner_id"] = token_data.user_id
|
||||
@@ -118,6 +138,10 @@ async def create_worker_individual(worker_data: WorkerIndividualCreate,
|
||||
|
||||
@agent_router.get("/worker")
|
||||
async def get_worker_individual_list(token_data: TokenData = Depends(Accessor.get_current_user)):
|
||||
"""处理针对 get worker individual list 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
workers = await postgres_database.get_worker_individual_list.remote( owner_id=token_data.user_id)
|
||||
return {"workers": workers}
|
||||
@@ -126,6 +150,10 @@ async def get_worker_individual_list(token_data: TokenData = Depends(Accessor.ge
|
||||
@agent_router.get("/worker/{agent_id}")
|
||||
async def get_worker_individual(agent_id: str,
|
||||
token_data: TokenData = Depends(Accessor.get_current_user)):
|
||||
"""处理针对 get worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id)
|
||||
if not worker:
|
||||
@@ -139,6 +167,10 @@ async def get_worker_individual(agent_id: str,
|
||||
async def update_worker_individual(agent_id: str,
|
||||
worker_data: WorkerIndividualUpdate,
|
||||
token_data: TokenData = Depends(Accessor.get_current_user)):
|
||||
"""处理针对 update worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 worker_data (WorkerIndividualUpdate): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id)
|
||||
if not worker:
|
||||
@@ -159,6 +191,10 @@ async def update_worker_individual(agent_id: str,
|
||||
|
||||
@agent_router.post("/worker/{agent_id}/reload")
|
||||
async def reload_worker_individual(agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)):
|
||||
"""处理针对 reload worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
|
||||
if not worker:
|
||||
@@ -175,6 +211,10 @@ async def reload_worker_individual(agent_id: str, token_data: TokenData = Depend
|
||||
@agent_router.delete("/worker/{agent_id}")
|
||||
async def delete_worker_individual(agent_id: str,
|
||||
token_data: TokenData = Depends(Accessor.get_current_user)):
|
||||
"""处理针对 delete worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id)
|
||||
if not worker:
|
||||
|
||||
@@ -25,22 +25,34 @@ from pretor.utils.error import UserNotExistError
|
||||
auth_router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
"""UserRegister 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 UserRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
user_name: str
|
||||
password: str
|
||||
|
||||
@auth_router.post("/register")
|
||||
async def create_user(user_register: UserRegister):
|
||||
"""处理针对 create user 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: user_register (UserRegister): 参与 create user 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
hashed_password = await run_in_threadpool(Accessor.hash_password, user_register.password)
|
||||
user = await postgres_database.add_user.remote( user_register.user_name, hashed_password)
|
||||
return {"message": "success", "user_id": user.user_id}
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""UserLogin 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 UserLogin 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
user_name: str
|
||||
password: str
|
||||
|
||||
@auth_router.post("/login")
|
||||
async def login_user(user_login: UserLogin):
|
||||
"""处理针对 login user 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: user_login (UserLogin): 参与 login user 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
user = await postgres_database.login_user.remote( user_login.user_name)
|
||||
if not user:
|
||||
@@ -49,6 +61,8 @@ async def login_user(user_login: UserLogin):
|
||||
return {"message":"success", "token":token}
|
||||
|
||||
class ChangeAuthorityRequest(BaseModel):
|
||||
"""ChangeAuthorityRequest 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ChangeAuthorityRequest 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
user_id: str
|
||||
new_authority: UserAuthority
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ from pretor.core.workflow.workflow import PretorWorkflow
|
||||
import asyncio
|
||||
|
||||
class PretorEvent(BaseModel):
|
||||
"""PretorEvent 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 PretorEvent 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
trace_id: str = Field(default_factory=lambda: str(ULID()), description="事件的唯一标识符")
|
||||
platform: str = Field(description="消息来源的平台")
|
||||
|
||||
@@ -18,18 +18,25 @@ from pretor.utils.access import Accessor, TokenData
|
||||
from pretor.api.platform.event import PretorEvent
|
||||
from pretor.utils.ray_hook import ray_actor_hook
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import anyio
|
||||
from pretor.utils.logger import get_logger
|
||||
|
||||
|
||||
logger = get_logger('frontend')
|
||||
client_router = APIRouter(prefix="/api/v1/adapter/client", tags=["client"])
|
||||
|
||||
class Message(BaseModel):
|
||||
"""Message 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 Message 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
message: str
|
||||
|
||||
@client_router.post("")
|
||||
async def create_message(message: Message,
|
||||
token_data: TokenData = Depends(Accessor.get_current_user)):
|
||||
"""处理针对 create message 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: message (Message): 参与 create message 逻辑运算或数据构建的上下文依赖对象。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
logger.info("收到消息,来源:客户端")
|
||||
logger.debug(f"消息内容:{message.message}")
|
||||
event = PretorEvent(platform="client",
|
||||
@@ -38,8 +45,8 @@ async def create_message(message: Message,
|
||||
message=message.message)
|
||||
supervisory_node = ray_actor_hook("supervisory_node").supervisory_node
|
||||
message = await supervisory_node.working.remote(event)
|
||||
if message == "任务已创建":
|
||||
return {"message": event.trace_id}
|
||||
if message.startswith("任务已创建"):
|
||||
return {"message": f"{event.trace_id}\n\n{message}"}
|
||||
elif message == "未知相应类型":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -50,12 +57,17 @@ async def create_message(message: Message,
|
||||
@client_router.post("/upload")
|
||||
async def upload_file(file: UploadFile = File(...),
|
||||
token_data: TokenData = Depends(Accessor.get_current_user)):
|
||||
"""处理针对 upload file 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: file (UploadFile): 参与 upload file 逻辑运算或数据构建的上下文依赖对象。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
try:
|
||||
upload_dir = "uploads"
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
file_path = os.path.join(upload_dir, file.filename)
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
async with await anyio.open_file(file_path, "wb") as buffer:
|
||||
while chunk := await file.read(64 * 1024): # 64KB chunks
|
||||
await buffer.write(chunk)
|
||||
logger.info(f"用户 {token_data.username} 上传了文件: {file.filename}")
|
||||
return {"filename": file.filename, "message": f"File {file.filename} uploaded successfully"}
|
||||
except Exception as e:
|
||||
|
||||
@@ -25,6 +25,8 @@ from pretor.utils.ray_hook import ray_actor_hook
|
||||
provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
|
||||
|
||||
class ProviderRegister(BaseModel):
|
||||
"""ProviderRegister 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
provider_type: Literal["openai", "claude", "deepseek"]
|
||||
provider_title: str
|
||||
provider_url: str
|
||||
@@ -33,6 +35,10 @@ class ProviderRegister(BaseModel):
|
||||
@provider_router.post("")
|
||||
async def create_provider(provider_register: ProviderRegister,
|
||||
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))) -> None:
|
||||
"""处理针对 create provider 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: provider_register (ProviderRegister): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_register 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (None): 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.add_provider_wrap.remote(provider_type=provider_register.provider_type,
|
||||
provider_title=provider_register.provider_title,
|
||||
@@ -43,12 +49,20 @@ async def create_provider(provider_register: ProviderRegister,
|
||||
|
||||
@provider_router.get("/list")
|
||||
async def get_provider_list(_: TokenData = Depends(Accessor.get_current_user)) -> Dict[str, Dict[str, Provider]]:
|
||||
"""处理针对 get provider list 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get provider list 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (Dict[str, Dict[str, Provider]]): 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
provider_list: Dict[str, Provider] = await global_state_machine.get_provider_list.remote()
|
||||
return {"provider_list": provider_list}
|
||||
|
||||
@provider_router.delete("/{provider_title}")
|
||||
async def delete_provider(provider_title: str, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR))) -> dict:
|
||||
"""处理针对 delete provider 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 _ (TokenData): 参与 delete provider 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (dict): 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.delete_provider.remote(provider_title=provider_title)
|
||||
return {"message": "success"}
|
||||
@@ -26,18 +26,30 @@ resource_router = APIRouter(prefix="/api/v1/resource")
|
||||
@resource_router.post("/workflow_template")
|
||||
async def create_workflow_template(workflow_template: WorkflowTemplate,
|
||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 create workflow template 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: workflow_template (WorkflowTemplate): 参与 create workflow template 逻辑运算或数据构建的上下文依赖对象。 _ (TokenData): 参与 create workflow template 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.add_workflow_template.remote( workflow_template.name, workflow_template)
|
||||
return {"message": "创建成功"}
|
||||
|
||||
@resource_router.get("/workflow_template")
|
||||
async def get_workflow_templates(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 get workflow templates 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get workflow templates 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
templates = await global_state_machine.get_all_workflow_templates.remote()
|
||||
return {"templates": templates}
|
||||
|
||||
@resource_router.delete("/workflow_template/{template_name}")
|
||||
async def delete_workflow_template(template_name: str, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR))):
|
||||
"""处理针对 delete workflow template 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: template_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 _ (TokenData): 参与 delete workflow template 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.delete_workflow_template.remote( template_name)
|
||||
return {"message": "success"}
|
||||
@@ -45,12 +57,18 @@ async def delete_workflow_template(template_name: str, _: TokenData = Depends(Ro
|
||||
|
||||
|
||||
class Skill(BaseModel):
|
||||
"""Skill 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 Skill 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
repo_url: str
|
||||
path: str | None
|
||||
|
||||
@resource_router.post("/skill")
|
||||
async def install_skill(skill: Skill,
|
||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 install skill 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: skill (Skill): 参与 install skill 逻辑运算或数据构建的上下文依赖对象。 _ (TokenData): 参与 install skill 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
# noinspection PyUnresolvedReferences
|
||||
import os
|
||||
@@ -68,12 +86,20 @@ async def install_skill(skill: Skill,
|
||||
|
||||
@resource_router.get("/skill")
|
||||
async def get_skills(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 get skills 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get skills 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
skills = await global_state_machine.get_skill_list.remote()
|
||||
return {"skills": skills}
|
||||
|
||||
@resource_router.delete("/skill/{skill_name}")
|
||||
async def delete_skill(skill_name: str, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR))):
|
||||
"""处理针对 delete skill 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 _ (TokenData): 参与 delete skill 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
# Note: this only removes it from the state machine manager.
|
||||
await global_state_machine.remove_skill.remote( skill_name)
|
||||
@@ -81,6 +107,10 @@ async def delete_skill(skill_name: str, _: TokenData = Depends(RoleChecker(allow
|
||||
|
||||
@resource_router.get("/tool")
|
||||
async def get_tools(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))):
|
||||
"""处理针对 get tools 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get tools 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
tool_mapper = await global_state_machine.get_tool_mapper.remote()
|
||||
all_tool_names = set()
|
||||
|
||||
+25
-8
@@ -22,15 +22,22 @@ workflow_router = APIRouter(prefix="/api/v1/workflow", tags=["workflow"])
|
||||
|
||||
@workflow_router.get("/list")
|
||||
async def get_workflow_list():
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
events = await global_state_machine.list_events.remote()
|
||||
"""处理针对 get workflow list 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
|
||||
events = await global_workflow_manager.list_events.remote()
|
||||
return events
|
||||
|
||||
|
||||
@workflow_router.get("/{trace_id}")
|
||||
async def get_workflow_detail(trace_id: str):
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
event = await global_state_machine.get_event.remote(trace_id)
|
||||
"""处理针对 get workflow detail 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: trace_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 trace 实例。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
|
||||
event = await global_workflow_manager.get_event.remote(trace_id)
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||
|
||||
@@ -71,9 +78,15 @@ async def get_workflow_detail(trace_id: str):
|
||||
|
||||
@workflow_router.get("/sse/{trace_id}")
|
||||
async def get_workflow_sse(trace_id: str, request: Request):
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
"""处理针对 get workflow sse 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: trace_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 trace 实例。 request (Request): FastAPI 框架注入的原生 HTTP 请求对象,包含了完整的 Header 标头、查询参数和正文流。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
|
||||
|
||||
async def event_generator():
|
||||
"""执行与 event generator 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。 """
|
||||
try:
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
@@ -81,7 +94,7 @@ async def get_workflow_sse(trace_id: str, request: Request):
|
||||
|
||||
# You might also want to send the workflow state periodically or when updated
|
||||
# Here we just wait for pending messages and send them
|
||||
message = await global_state_machine.get_pending.remote(trace_id)
|
||||
message = await global_workflow_manager.get_pending.remote(trace_id)
|
||||
# Ensure the message is formatted as SSE
|
||||
yield f"data: {message}\n\n"
|
||||
except asyncio.CancelledError:
|
||||
@@ -91,9 +104,13 @@ async def get_workflow_sse(trace_id: str, request: Request):
|
||||
|
||||
@workflow_router.post("/reply/{trace_id}")
|
||||
async def post_workflow_reply(trace_id: str, request: Request):
|
||||
"""处理针对 post workflow reply 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: trace_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 trace 实例。 request (Request): FastAPI 框架注入的原生 HTTP 请求对象,包含了完整的 Header 标头、查询参数和正文流。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
|
||||
data = await request.json()
|
||||
reply_msg = data.get("message", "")
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.put_received.remote(trace_id, reply_msg)
|
||||
global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
|
||||
await global_workflow_manager.put_received.remote(trace_id, reply_msg)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from pretor.api.provider import provider_router
|
||||
from pretor.api.resource import resource_router
|
||||
from pretor.api.cluster import cluster_router
|
||||
from pretor.api.agent import agent_router
|
||||
from pretor.api.workflow import workflow_router
|
||||
from pretor.utils.error import (
|
||||
DemandError, ModelNotExistError, UserError,
|
||||
UserNotExistError, UserPasswordError, ProviderError,
|
||||
@@ -40,6 +41,7 @@ app.include_router(provider_router) # 供应商路径
|
||||
app.include_router(resource_router) # 资源路径
|
||||
app.include_router(cluster_router) # 集群信息路径
|
||||
app.include_router(agent_router) # agent路径
|
||||
app.include_router(workflow_router) # workflow路径
|
||||
|
||||
@app.exception_handler(UserNotExistError)
|
||||
async def user_not_exist_handler(request: Request, exc: UserNotExistError):
|
||||
|
||||
@@ -19,7 +19,14 @@ from pretor.utils.error import UserNotExistError
|
||||
from pretor.utils.logger import get_logger
|
||||
logger = get_logger('database_exception')
|
||||
def database_exception(func):
|
||||
"""执行与 database exception 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: func: 参与 database exception 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async def wrapper(*args, **kwargs):
|
||||
"""执行与 wrapper 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except ValidationError as e:
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
from sqlmodel import select
|
||||
from typing import List, Optional
|
||||
from pretor.core.database.table.event import EventRecord
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
|
||||
|
||||
class EventDatabase:
|
||||
def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
async def upsert_event(self, trace_id: str, event_data_json: str) -> EventRecord:
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(EventRecord).where(EventRecord.trace_id == trace_id)
|
||||
results = await session.execute(statement)
|
||||
record = results.scalar_one_or_none()
|
||||
if record:
|
||||
record.event_data_json = event_data_json
|
||||
else:
|
||||
record = EventRecord(trace_id=trace_id, event_data_json=event_data_json)
|
||||
session.add(record)
|
||||
await session.commit()
|
||||
await session.refresh(record)
|
||||
return record
|
||||
|
||||
async def get_event(self, trace_id: str) -> Optional[EventRecord]:
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(EventRecord).where(EventRecord.trace_id == trace_id)
|
||||
results = await session.execute(statement)
|
||||
return results.scalar_one_or_none()
|
||||
|
||||
async def get_all_events(self) -> List[EventRecord]:
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(EventRecord)
|
||||
results = await session.execute(statement)
|
||||
return results.scalars().all()
|
||||
|
||||
async def delete_event(self, trace_id: str) -> bool:
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(EventRecord).where(EventRecord.trace_id == trace_id)
|
||||
results = await session.execute(statement)
|
||||
record = results.scalar_one_or_none()
|
||||
if record:
|
||||
await session.delete(record)
|
||||
await session.commit()
|
||||
return True
|
||||
return False
|
||||
@@ -20,11 +20,16 @@ from pretor.core.database.database_exception import database_exception
|
||||
from ulid import ULID
|
||||
|
||||
class IndividualDatabase:
|
||||
"""IndividualDatabase 核心组件类。
|
||||
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。 """
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
@database_exception
|
||||
async def add_worker_individual(self, **kwargs) -> WorkerIndividual:
|
||||
"""创建并持久化新的 worker individual 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Returns: (WorkerIndividual): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
agent_id = str(ULID())
|
||||
individual = WorkerIndividual(agent_id=agent_id, **kwargs)
|
||||
@@ -35,6 +40,10 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_worker_individual(self, agent_id: str) -> Optional[WorkerIndividual]:
|
||||
"""检索并获取特定的 worker individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: (Optional[WorkerIndividual]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id)
|
||||
results = await session.execute(statement)
|
||||
@@ -42,6 +51,10 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_worker_individual_list(self, owner_id: str) -> List[WorkerIndividual]:
|
||||
"""检索并获取特定的 worker individual list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: owner_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 owner 实例。
|
||||
Returns: (List[WorkerIndividual]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(WorkerIndividual).where(WorkerIndividual.owner_id == owner_id)
|
||||
results = await session.execute(statement)
|
||||
@@ -49,6 +62,10 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def update_worker_individual(self, agent_id: str, **kwargs) -> Optional[WorkerIndividual]:
|
||||
"""对现有的 worker individual 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: (Optional[WorkerIndividual]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id)
|
||||
results = await session.execute(statement)
|
||||
@@ -65,6 +82,10 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_worker_individual(self, agent_id: str) -> bool:
|
||||
"""安全地移除或注销 worker individual。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: (bool): 一个布尔型结果标志,明确返回 True 表示该操作成功应用或条件达成,False 则表示失败或被拒绝。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id)
|
||||
results = await session.execute(statement)
|
||||
@@ -77,6 +98,9 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_all_worker_individual(self) -> List[WorkerIndividual]:
|
||||
"""检索并获取特定的 all worker individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (List[WorkerIndividual]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(WorkerIndividual)
|
||||
results = await session.execute(statement)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from sqlmodel import SQLModel, Field, select
|
||||
from typing import Optional, List
|
||||
import json
|
||||
|
||||
class WorkflowRecord(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
workflow_id: str = Field(index=True)
|
||||
workflow_data_json: str
|
||||
|
||||
class MemoryRecord(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
memory_text: str
|
||||
embedding: List[float] = Field(sa_column_kwargs={"type_": "VECTOR"}) # Requires pgvector extension setup in DB
|
||||
|
||||
class MemoryRAG:
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
async def save_workflow(self, workflow_id: str, workflow_data: dict):
|
||||
async with self.async_session_maker() as session:
|
||||
record = WorkflowRecord(
|
||||
workflow_id=workflow_id,
|
||||
workflow_data_json=json.dumps(workflow_data)
|
||||
)
|
||||
session.add(record)
|
||||
await session.commit()
|
||||
await session.refresh(record)
|
||||
return record
|
||||
|
||||
async def get_workflow(self, workflow_id: str):
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(WorkflowRecord).where(WorkflowRecord.workflow_id == workflow_id)
|
||||
results = await session.execute(statement)
|
||||
record = results.scalar_one_or_none()
|
||||
if record:
|
||||
return json.loads(record.workflow_data_json)
|
||||
return None
|
||||
|
||||
async def add_memory(self, memory_text: str, embedding: List[float]):
|
||||
async with self.async_session_maker() as session:
|
||||
record = MemoryRecord(memory_text=memory_text, embedding=embedding)
|
||||
session.add(record)
|
||||
await session.commit()
|
||||
await session.refresh(record)
|
||||
return record
|
||||
|
||||
async def retrieve_memory(self, query_embedding: List[float], limit: int = 5):
|
||||
# Requires pgvector specific operations; simplified retrieval simulation here
|
||||
async with self.async_session_maker() as session:
|
||||
# A true pgvector query would use an ORDER BY using `<->` operator
|
||||
# e.g. statement = select(MemoryRecord).order_by(MemoryRecord.embedding.l2_distance(query_embedding)).limit(limit)
|
||||
statement = select(MemoryRecord).limit(limit)
|
||||
results = await session.execute(statement)
|
||||
return results.all()
|
||||
@@ -19,11 +19,16 @@ from sqlmodel import select
|
||||
from pretor.core.database.database_exception import database_exception
|
||||
|
||||
class ProviderDatabase:
|
||||
"""ProviderDatabase 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
@database_exception
|
||||
async def get_provider(self) -> List[Provider]:
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (List[Provider]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(Provider)
|
||||
results = await session.execute(statement)
|
||||
@@ -37,6 +42,9 @@ class ProviderDatabase:
|
||||
|
||||
@database_exception
|
||||
async def add_provider(self, **kwargs) -> None:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
provider = Provider(**kwargs)
|
||||
session.add(provider)
|
||||
@@ -44,6 +52,10 @@ class ProviderDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_provider(self, provider_id: str) -> None:
|
||||
"""安全地移除或注销 provider。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
provider = await session.get(Provider, provider_id)
|
||||
if provider is not None:
|
||||
@@ -52,6 +64,10 @@ class ProviderDatabase:
|
||||
|
||||
@database_exception
|
||||
async def update_provider(self, provider_id: str, **kwargs) -> Provider:
|
||||
"""对现有的 provider 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
provider = await session.get(Provider, provider_id)
|
||||
if provider is not None:
|
||||
|
||||
@@ -18,11 +18,17 @@ from typing import List, Optional
|
||||
from pretor.core.database.database_exception import database_exception
|
||||
|
||||
class SystemNodeDatabase:
|
||||
"""SystemNodeDatabase 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
@database_exception
|
||||
async def upsert_system_node_config(self, node_name: str, provider_title: str, model_id: str, tools: Optional[List[str]] = None) -> SystemNodeConfig:
|
||||
"""执行与 upsert system node config 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 model_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 model 实例。 tools (Optional[List[str]]): 控制逻辑流向的具体字符串参数,指定了期望的 tools 内容。
|
||||
Returns: (SystemNodeConfig): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(SystemNodeConfig).where(SystemNodeConfig.node_name == node_name)
|
||||
results = await session.execute(statement)
|
||||
@@ -41,6 +47,9 @@ class SystemNodeDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_all_system_node_configs(self) -> List[SystemNodeConfig]:
|
||||
"""检索并获取特定的 all system node configs 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (List[SystemNodeConfig]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(SystemNodeConfig)
|
||||
results = await session.execute(statement)
|
||||
@@ -48,6 +57,10 @@ class SystemNodeDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_system_node_config(self, node_name: str) -> Optional[SystemNodeConfig]:
|
||||
"""检索并获取特定的 system node config 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (Optional[SystemNodeConfig]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(SystemNodeConfig).where(SystemNodeConfig.node_name == node_name)
|
||||
results = await session.execute(statement)
|
||||
|
||||
@@ -20,11 +20,17 @@ from pretor.core.database.table.user import UserAuthority
|
||||
from pretor.utils.access import Accessor
|
||||
|
||||
class AuthDatabase:
|
||||
"""AuthDatabase 核心组件类。
|
||||
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。 """
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
@database_exception
|
||||
async def add_user(self, user_name: str, hashed_password: str) -> User:
|
||||
"""创建并持久化新的 user 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 hashed_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 hashed password 内容。
|
||||
Returns: (User): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
from ulid import ULID
|
||||
async with self.async_session_maker() as session:
|
||||
# Check if any users exist
|
||||
@@ -49,6 +55,10 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def change_password(self, user_name, old_password, new_password) -> User:
|
||||
"""执行与 change password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name: 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 old_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。 new_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (User): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User).where(User.user_name == user_name)
|
||||
results = await session.execute(statement)
|
||||
@@ -65,6 +75,10 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_user(self, user_name: str) -> None:
|
||||
"""安全地移除或注销 user。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User).where(User.user_name == user_name)
|
||||
results = await session.execute(statement)
|
||||
@@ -76,6 +90,10 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_user_by_id(self, user_id: str) -> None:
|
||||
"""安全地移除或注销 user by id。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if user is None:
|
||||
@@ -85,6 +103,10 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def login_user(self, user_name: str) -> str:
|
||||
"""执行与 login user 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User).where(User.user_name == user_name)
|
||||
results = await session.execute(statement)
|
||||
@@ -95,6 +117,9 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_all_users(self) -> list[User]:
|
||||
"""检索并获取特定的 all users 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (list[User]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User)
|
||||
results = await session.execute(statement)
|
||||
@@ -103,6 +128,10 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_user_authority(self, user_id: str) -> UserAuthority:
|
||||
"""检索并获取特定的 user authority 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: (UserAuthority): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
async with self.async_session_maker() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if user is None:
|
||||
|
||||
@@ -21,12 +21,15 @@ from sqlalchemy.orm import sessionmaker
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from pretor.core.database.module.individual import IndividualDatabase
|
||||
from pretor.core.database.module.event import EventDatabase
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
from pretor.core.database.module.provider import ProviderDatabase
|
||||
from pretor.core.database.module.system_node import SystemNodeDatabase
|
||||
|
||||
@ray.remote
|
||||
class PostgresDatabase:
|
||||
"""PostgresDatabase 核心组件类。
|
||||
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。 """
|
||||
def __init__(self):
|
||||
user = os.environ.get('POSTGRES_USER')
|
||||
password = os.environ.get('POSTGRES_PASSWORD')
|
||||
@@ -40,11 +43,15 @@ class PostgresDatabase:
|
||||
self._auth_database = AuthDatabase(self.async_session_maker)
|
||||
self._provider_database = ProviderDatabase(self.async_session_maker)
|
||||
self._individual_database = IndividualDatabase(self.async_session_maker)
|
||||
self._event_database = EventDatabase(self.async_session_maker)
|
||||
self._system_node_database = SystemNodeDatabase(self.async_session_maker)
|
||||
|
||||
self.ready_event = asyncio.Event()
|
||||
|
||||
async def init_db(self) -> None:
|
||||
"""完成 db 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
async with self.async_engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
@@ -57,84 +64,174 @@ class PostgresDatabase:
|
||||
|
||||
# Auth Database Methods
|
||||
async def add_user(self, user_name: str, hashed_password: str):
|
||||
"""创建并持久化新的 user 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 hashed_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 hashed password 内容。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.add_user(user_name, hashed_password)
|
||||
|
||||
async def change_password(self, user_name, old_password, new_password):
|
||||
"""执行与 change password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name: 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 old_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。 new_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.change_password(user_name, old_password, new_password)
|
||||
|
||||
async def delete_user(self, user_name: str):
|
||||
"""安全地移除或注销 user。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.delete_user(user_name)
|
||||
|
||||
async def delete_user_by_id(self, user_id: str):
|
||||
"""安全地移除或注销 user by id。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.delete_user_by_id(user_id)
|
||||
|
||||
async def login_user(self, user_name: str):
|
||||
"""执行与 login user 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.login_user(user_name)
|
||||
|
||||
async def get_all_users(self):
|
||||
"""检索并获取特定的 all users 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.get_all_users()
|
||||
|
||||
async def get_user_authority(self, user_id: str):
|
||||
"""检索并获取特定的 user authority 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.get_user_authority(user_id)
|
||||
|
||||
async def change_user_authority(self, user_id: str, new_authority):
|
||||
"""执行与 change user authority 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。 new_authority: 参与 change user authority 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.change_user_authority(user_id, new_authority)
|
||||
|
||||
# Provider Database Methods
|
||||
async def get_provider(self):
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.get_provider()
|
||||
|
||||
async def add_provider_db(self, **kwargs):
|
||||
"""创建并持久化新的 provider db 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.add_provider(**kwargs)
|
||||
|
||||
async def delete_provider_db(self, provider_id: str):
|
||||
"""安全地移除或注销 provider db。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.delete_provider(provider_id)
|
||||
|
||||
async def update_provider_db(self, provider_id: str, **kwargs):
|
||||
"""对现有的 provider db 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.update_provider(provider_id, **kwargs)
|
||||
|
||||
# System Node Database Methods
|
||||
async def upsert_system_node_config(self, node_name: str, provider_title: str, model_id: str, tools: list[str] = None):
|
||||
"""执行与 upsert system node config 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 model_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 model 实例。 tools (list[str]): 控制逻辑流向的具体字符串参数,指定了期望的 tools 内容。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._system_node_database.upsert_system_node_config(node_name, provider_title, model_id, tools)
|
||||
|
||||
async def get_all_system_node_configs(self):
|
||||
"""检索并获取特定的 all system node configs 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._system_node_database.get_all_system_node_configs()
|
||||
|
||||
# Individual Database Methods
|
||||
async def add_worker_individual(self, **kwargs):
|
||||
"""创建并持久化新的 worker individual 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.add_worker_individual(**kwargs)
|
||||
|
||||
async def get_worker_individual(self, agent_id: str):
|
||||
"""检索并获取特定的 worker individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.get_worker_individual(agent_id)
|
||||
|
||||
async def get_worker_individual_list(self, owner_id: str):
|
||||
"""检索并获取特定的 worker individual list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: owner_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 owner 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.get_worker_individual_list(owner_id)
|
||||
|
||||
async def update_worker_individual(self, agent_id: str, **kwargs):
|
||||
"""对现有的 worker individual 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.update_worker_individual(agent_id, **kwargs)
|
||||
|
||||
async def delete_worker_individual(self, agent_id: str):
|
||||
"""安全地移除或注销 worker individual。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.delete_worker_individual(agent_id)
|
||||
|
||||
async def get_all_worker_individual(self):
|
||||
"""检索并获取特定的 all worker individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.get_all_worker_individual()
|
||||
return await self._individual_database.get_all_worker_individual()
|
||||
# Event Database Methods
|
||||
async def upsert_event(self, trace_id: str, event_data_json: str):
|
||||
await self.ready_event.wait()
|
||||
return await self._event_database.upsert_event(trace_id, event_data_json)
|
||||
|
||||
async def get_event(self, trace_id: str):
|
||||
await self.ready_event.wait()
|
||||
return await self._event_database.get_event(trace_id)
|
||||
|
||||
async def get_all_events(self):
|
||||
await self.ready_event.wait()
|
||||
return await self._event_database.get_all_events()
|
||||
|
||||
async def delete_event(self, trace_id: str):
|
||||
await self.ready_event.wait()
|
||||
return await self._event_database.delete_event(trace_id)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
class EventRecord(SQLModel, table=True):
|
||||
trace_id: str = Field(primary_key=True, description="The unique trace ID of the PretorEvent")
|
||||
event_data_json: str = Field(description="The JSON serialized PretorEvent data")
|
||||
@@ -18,11 +18,15 @@ from sqlalchemy import Column, JSON
|
||||
from enum import Enum
|
||||
|
||||
class AgentType(str, Enum):
|
||||
"""AgentType 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentType 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
SKILL_INDIVIDUAL = "skill_individual"
|
||||
ORDINARY_INDIVIDUAL = "ordinary_individual"
|
||||
SPECIAL_INDIVIDUAL = "special_individual"
|
||||
|
||||
class WorkerIndividual(SQLModel, table=True):
|
||||
"""WorkerIndividual 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
|
||||
__tablename__ = "worker_individual"
|
||||
agent_id: str = Field(primary_key=True)
|
||||
agent_name: str = Field(index=True)
|
||||
|
||||
@@ -18,6 +18,8 @@ from sqlalchemy import Column, JSON
|
||||
from typing import Optional
|
||||
|
||||
class Provider(SQLModel, table=True):
|
||||
"""Provider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
__tablename__ = "provider"
|
||||
provider_id: str = Field(primary_key=True)
|
||||
provider_title: str = Field(index=True)
|
||||
|
||||
@@ -18,6 +18,8 @@ from typing import List, Optional
|
||||
from sqlalchemy import Column, JSON
|
||||
|
||||
class SystemNodeConfig(SQLModel, table=True):
|
||||
"""SystemNodeConfig 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
__tablename__ = "system_node_config"
|
||||
node_name: str = Field(primary_key=True)
|
||||
provider_title: str
|
||||
|
||||
@@ -16,6 +16,8 @@ from sqlmodel import SQLModel, Field
|
||||
from enum import IntEnum
|
||||
|
||||
class UserAuthority(IntEnum):
|
||||
"""UserAuthority 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 UserAuthority 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
SUPER_ADMINISTRATOR = 100
|
||||
ADMINISTRATOR = 50
|
||||
USER = 20
|
||||
@@ -23,6 +25,8 @@ class UserAuthority(IntEnum):
|
||||
GUEST = 0
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
"""User 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 User 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
__tablename__ = 'user'
|
||||
user_id: str = Field(primary_key=True)
|
||||
user_name: str = Field(index=True)
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
import ray
|
||||
from pretor.core.global_state_machine.provider_manager import ProviderManager
|
||||
from pretor.core.global_state_machine.tool_manager import GlobalToolManager
|
||||
from typing import Dict
|
||||
from pretor.core.database.postgres import PostgresDatabase
|
||||
from pretor.api.platform.event import PretorEvent
|
||||
import asyncio
|
||||
from pretor.core.workflow.workflow import PretorWorkflow
|
||||
from pretor.core.workflow.workflow_template_manager import WorkflowManager
|
||||
from pretor.core.global_state_machine.skill_manager import GlobalSkillManager
|
||||
from pretor.core.global_state_machine.individual_manager import GlobalIndividualManager
|
||||
@@ -27,10 +23,11 @@ from pretor.core.global_state_machine.individual_manager import GlobalIndividual
|
||||
|
||||
@ray.remote
|
||||
class GlobalStateMachine:
|
||||
"""GlobalStateMachine 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 GlobalStateMachine 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
def __init__(self, postgres_database: PostgresDatabase):
|
||||
import sys
|
||||
print("GSM __init__ START", file=sys.stderr, flush=True)
|
||||
self.event_dict: Dict[str, PretorEvent] = {}
|
||||
print(" event_dict done", file=sys.stderr, flush=True)
|
||||
self._global_provider_manager = ProviderManager(postgres_database)
|
||||
print(" provider_manager done", file=sys.stderr, flush=True)
|
||||
@@ -46,10 +43,16 @@ class GlobalStateMachine:
|
||||
print("GSM __init__ DONE", file=sys.stderr, flush=True)
|
||||
|
||||
async def init_state_machine(self):
|
||||
"""完成 state machine 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。 """
|
||||
await self._global_provider_manager.init_provider_register(self.postgres_database)
|
||||
await self._global_individual_manager.init_individual_register(self.postgres_database)
|
||||
|
||||
async def add_provider_wrap(self, provider_type, provider_title, provider_url, provider_apikey, provider_owner):
|
||||
"""创建并持久化新的 provider wrap 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_type: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_type 实例。 provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 provider_url: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_url 实例。 provider_apikey: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_apikey 实例。 provider_owner: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_owner 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return await self._global_provider_manager.add_provider(
|
||||
provider_type=provider_type,
|
||||
provider_title=provider_title,
|
||||
@@ -61,21 +64,39 @@ class GlobalStateMachine:
|
||||
|
||||
# Provider Manager Methods
|
||||
def get_provider_list(self):
|
||||
"""检索并获取特定的 provider list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_provider_manager.get_provider_list()
|
||||
|
||||
def get_provider(self, provider_title):
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_provider_manager.get_provider(provider_title)
|
||||
|
||||
async def delete_provider(self, provider_title: str):
|
||||
"""安全地移除或注销 provider。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return await self._global_provider_manager.delete_provider(provider_title, self.postgres_database)
|
||||
|
||||
# Tool Manager Methods
|
||||
def get_tool_mapper(self):
|
||||
"""检索并获取特定的 tool mapper 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_tool_manager.tool_mapper
|
||||
|
||||
def get_tool_list(self, agent_name: str):
|
||||
# get_tool_list didn't actually exist on tool_manager, let's implement it to return the tools
|
||||
# for a specific agent name (or scope)
|
||||
"""检索并获取特定的 tool list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: agent_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
tools = self._global_tool_manager.tool_mapper.get(agent_name, {})
|
||||
# also include default tools
|
||||
default_tools = self._global_tool_manager.tool_mapper.get("default", {})
|
||||
@@ -84,83 +105,78 @@ class GlobalStateMachine:
|
||||
|
||||
# Workflow Template Manager Methods
|
||||
def get_all_workflow_templates(self):
|
||||
"""检索并获取特定的 all workflow templates 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_workflow_template_manager.get_all_workflow_templates()
|
||||
|
||||
def add_workflow_template(self, template_name: str, workflow_template):
|
||||
"""创建并持久化新的 workflow template 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: template_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 workflow_template: 参与 add workflow template 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_workflow_template_manager.add_workflow_template(template_name, workflow_template)
|
||||
|
||||
def delete_workflow_template(self, template_name: str):
|
||||
"""安全地移除或注销 workflow template。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: template_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_workflow_template_manager.delete_workflow_template(template_name)
|
||||
|
||||
def generate_workflow_template(self, workflow_template):
|
||||
"""执行与 generate workflow template 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: workflow_template: 参与 generate workflow template 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_workflow_template_manager.generate_workflow_template(workflow_template)
|
||||
|
||||
# Skill Manager Methods
|
||||
def add_skill(self, skill_name: str):
|
||||
"""创建并持久化新的 skill 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_skill_manager.add_skill(skill_name)
|
||||
|
||||
def get_skill_list(self):
|
||||
"""检索并获取特定的 skill list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_skill_manager.get_skill_list()
|
||||
|
||||
def remove_skill(self, skill_name: str):
|
||||
"""安全地移除或注销 skill。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_skill_manager.remove_skill(skill_name)
|
||||
|
||||
# Individual Manager Methods
|
||||
def add_individual(self, agent_id: str, config):
|
||||
"""创建并持久化新的 individual 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 config: 驱动该模块运行的核心配置字典或 Pydantic 数据模型,定义了重试策略、超时时间及模型参数等选项。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_individual_manager.add_individual(agent_id, config)
|
||||
|
||||
def get_individual(self, agent_id: str):
|
||||
"""检索并获取特定的 individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_individual_manager.get_individual(agent_id)
|
||||
|
||||
def remove_individual(self, agent_id: str):
|
||||
"""安全地移除或注销 individual。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_individual_manager.remove_individual(agent_id)
|
||||
|
||||
def list_individuals(self):
|
||||
"""执行与 list individuals 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self._global_individual_manager.list_individuals()
|
||||
|
||||
###以下方法为event_dict方法
|
||||
def add_event(self, event: PretorEvent) -> None:
|
||||
event.pending_queue = asyncio.Queue()
|
||||
event.receive_queue = asyncio.Queue()
|
||||
self.event_dict[event.trace_id] = event
|
||||
|
||||
def delete_event(self, trace_id: str) -> None:
|
||||
del self.event_dict[trace_id]
|
||||
|
||||
def get_event(self, trace_id: str) -> PretorEvent:
|
||||
return self.event_dict.get(trace_id, None)
|
||||
|
||||
def update_attachment(self, trace_id: str, attachment: Dict[str, str]) -> None:
|
||||
self.event_dict[trace_id].attachment = attachment
|
||||
|
||||
def update_workflow(self, trace_id: str, workflow: PretorWorkflow) -> None:
|
||||
self.event_dict[trace_id].workflow = workflow
|
||||
|
||||
def get_workflow(self, trace_id: str) -> PretorWorkflow:
|
||||
return self.event_dict[trace_id].workflow
|
||||
|
||||
def list_events(self) -> list[dict]:
|
||||
result = []
|
||||
for trace_id, event in self.event_dict.items():
|
||||
workflow_title = event.workflow.title if event.workflow else None
|
||||
workflow_status = event.workflow.status.status if event.workflow and event.workflow.status else None
|
||||
result.append({
|
||||
"event_id": trace_id,
|
||||
"workflow_title": workflow_title,
|
||||
"status": workflow_status,
|
||||
"user_name": event.user_name,
|
||||
"message": event.message,
|
||||
})
|
||||
return result
|
||||
|
||||
async def put_pending(self, trace_id, item) -> None:
|
||||
await self.event_dict[trace_id].pending_queue.put(item)
|
||||
|
||||
async def get_pending(self, trace_id) -> str:
|
||||
return await self.event_dict[trace_id].pending_queue.get()
|
||||
|
||||
async def put_received(self, trace_id, item) -> None:
|
||||
await self.event_dict[trace_id].receive_queue.put(item)
|
||||
|
||||
async def get_received(self, trace_id) -> str:
|
||||
return await self.event_dict[trace_id].receive_queue.get()
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import ray
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
from pretor.api.platform.event import PretorEvent
|
||||
from pretor.core.workflow.workflow import PretorWorkflow
|
||||
from pretor.utils.ray_hook import ray_actor_hook
|
||||
from pretor.utils.logger import get_logger
|
||||
|
||||
@ray.remote
|
||||
class GlobalWorkflowManager:
|
||||
def __init__(self):
|
||||
self.event_dict: Dict[str, PretorEvent] = {}
|
||||
self.event_object_refs: Dict[str, ray.ObjectRef] = {}
|
||||
self.postgres_database = None
|
||||
self.logger = get_logger("GlobalWorkflowManager")
|
||||
|
||||
async def init_manager(self):
|
||||
self.postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
|
||||
# Load all events from database to memory
|
||||
try:
|
||||
records = await self.postgres_database.get_all_events.remote()
|
||||
for record in records:
|
||||
try:
|
||||
event = PretorEvent.model_validate_json(record.event_data_json)
|
||||
event.pending_queue = asyncio.Queue()
|
||||
event.receive_queue = asyncio.Queue()
|
||||
self.event_dict[event.trace_id] = event
|
||||
|
||||
# Store in ray object store for cache
|
||||
event_copy = event.model_copy()
|
||||
event_copy.pending_queue = None
|
||||
event_copy.receive_queue = None
|
||||
self.event_object_refs[event.trace_id] = ray.put(event_copy.model_dump_json())
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to load event {record.trace_id}: {e}")
|
||||
self.logger.info(f"Loaded {len(self.event_dict)} events from database")
|
||||
|
||||
# Trigger resumption of incomplete workflows
|
||||
workflow_running_engine = None
|
||||
for trace_id, event in self.event_dict.items():
|
||||
if event.workflow and event.workflow.status.status in ["waiting_llm_working", "waiting_tool_working", "llm_working", "tool_working"]:
|
||||
self.logger.info(f"Resuming incomplete workflow {trace_id}")
|
||||
if not workflow_running_engine:
|
||||
try:
|
||||
workflow_running_engine = ray_actor_hook("workflow_running_engine").workflow_running_engine
|
||||
except AttributeError:
|
||||
self.logger.warning("workflow_running_engine not found, cannot resume workflow")
|
||||
break
|
||||
await workflow_running_engine.resume_workflow.remote(event)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to fetch events from database: {e}")
|
||||
|
||||
async def _upsert_event_to_db(self, event: PretorEvent):
|
||||
try:
|
||||
# Create a copy and remove non-serializable queues
|
||||
event_copy = event.model_copy()
|
||||
event_copy.pending_queue = None
|
||||
event_copy.receive_queue = None
|
||||
|
||||
event_json = event_copy.model_dump_json()
|
||||
# Update cache
|
||||
self.event_object_refs[event.trace_id] = ray.put(event_json)
|
||||
|
||||
await self.postgres_database.upsert_event.remote(
|
||||
event.trace_id,
|
||||
event_json
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to upsert event {event.trace_id} to database: {e}")
|
||||
|
||||
async def add_event(self, event: PretorEvent) -> None:
|
||||
event.pending_queue = asyncio.Queue()
|
||||
event.receive_queue = asyncio.Queue()
|
||||
self.event_dict[event.trace_id] = event
|
||||
await self._upsert_event_to_db(event)
|
||||
|
||||
async def delete_event(self, trace_id: str) -> None:
|
||||
if trace_id in self.event_dict:
|
||||
del self.event_dict[trace_id]
|
||||
if trace_id in self.event_object_refs:
|
||||
del self.event_object_refs[trace_id]
|
||||
try:
|
||||
await self.postgres_database.delete_event.remote(trace_id)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to delete event {trace_id} from database: {e}")
|
||||
|
||||
async def get_event(self, trace_id: str) -> PretorEvent | None:
|
||||
# First check memory dict
|
||||
if trace_id in self.event_dict:
|
||||
return self.event_dict[trace_id]
|
||||
|
||||
# Then check Ray object store cache
|
||||
if trace_id in self.event_object_refs:
|
||||
try:
|
||||
event_json = ray.get(self.event_object_refs[trace_id])
|
||||
return PretorEvent.model_validate_json(event_json)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to fetch event from cache for trace {trace_id}: {e}")
|
||||
|
||||
# Fallback to database
|
||||
try:
|
||||
record = await self.postgres_database.get_event.remote(trace_id)
|
||||
if record:
|
||||
event = PretorEvent.model_validate_json(record.event_data_json)
|
||||
|
||||
# Restore to memory dict with missing transient queues
|
||||
event.pending_queue = asyncio.Queue()
|
||||
event.receive_queue = asyncio.Queue()
|
||||
self.event_dict[trace_id] = event
|
||||
|
||||
# Restore to cache
|
||||
event_copy = event.model_copy()
|
||||
event_copy.pending_queue = None
|
||||
event_copy.receive_queue = None
|
||||
self.event_object_refs[trace_id] = ray.put(event_copy.model_dump_json())
|
||||
|
||||
return event
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to fetch event {trace_id} from database fallback: {e}")
|
||||
|
||||
return None
|
||||
|
||||
async def update_attachment(self, trace_id: str, attachment: Dict[str, str]) -> None:
|
||||
if trace_id in self.event_dict:
|
||||
self.event_dict[trace_id].attachment = attachment
|
||||
await self._upsert_event_to_db(self.event_dict[trace_id])
|
||||
|
||||
async def update_workflow(self, trace_id: str, workflow: PretorWorkflow) -> None:
|
||||
if trace_id in self.event_dict:
|
||||
self.event_dict[trace_id].workflow = workflow
|
||||
await self._upsert_event_to_db(self.event_dict[trace_id])
|
||||
|
||||
async def get_workflow(self, trace_id: str) -> PretorWorkflow | None:
|
||||
event = await self.get_event(trace_id)
|
||||
return event.workflow if event else None
|
||||
|
||||
async def list_events(self) -> list[dict]:
|
||||
result = []
|
||||
|
||||
# Read strictly from the database to ensure we get all events,
|
||||
# and ignore the cache to prevent frontend missing items.
|
||||
try:
|
||||
records = await self.postgres_database.get_all_events.remote()
|
||||
for record in records:
|
||||
try:
|
||||
event = PretorEvent.model_validate_json(record.event_data_json)
|
||||
workflow_title = event.workflow.title if event.workflow else None
|
||||
workflow_status = event.workflow.status.status if event.workflow and event.workflow.status else None
|
||||
result.append({
|
||||
"event_id": event.trace_id,
|
||||
"workflow_title": workflow_title,
|
||||
"status": workflow_status,
|
||||
"user_name": event.user_name,
|
||||
"message": event.message,
|
||||
"create_time": event.create_time,
|
||||
})
|
||||
# Best-effort cache population
|
||||
self.event_object_refs[event.trace_id] = ray.put(record.event_data_json)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to list_events from DB: {e}")
|
||||
|
||||
return result
|
||||
|
||||
async def put_pending(self, trace_id, item) -> None:
|
||||
if trace_id in self.event_dict and self.event_dict[trace_id].pending_queue:
|
||||
await self.event_dict[trace_id].pending_queue.put(item)
|
||||
|
||||
async def get_pending(self, trace_id) -> str:
|
||||
if trace_id in self.event_dict and self.event_dict[trace_id].pending_queue:
|
||||
return await self.event_dict[trace_id].pending_queue.get()
|
||||
await asyncio.sleep(1) # Prevent CPU spinning if not found
|
||||
return ""
|
||||
|
||||
async def put_received(self, trace_id, item) -> None:
|
||||
if trace_id in self.event_dict and self.event_dict[trace_id].receive_queue:
|
||||
await self.event_dict[trace_id].receive_queue.put(item)
|
||||
|
||||
async def get_received(self, trace_id) -> str:
|
||||
if trace_id in self.event_dict and self.event_dict[trace_id].receive_queue:
|
||||
return await self.event_dict[trace_id].receive_queue.get()
|
||||
await asyncio.sleep(1) # Prevent CPU spinning if not found
|
||||
return ""
|
||||
@@ -17,10 +17,16 @@ from pretor.utils.logger import get_logger
|
||||
logger = get_logger('individual_manager')
|
||||
|
||||
class GlobalIndividualManager:
|
||||
"""GlobalIndividualManager 核心组件类。
|
||||
这是一个管理器类,职责集中在维护整个系统内有关 GlobalIndividual 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
|
||||
def __init__(self):
|
||||
self._individuals: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def init_individual_register(self, postgres) -> None:
|
||||
"""完成 individual register 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
|
||||
Args: postgres: 参与 init individual register 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
try:
|
||||
individuals = await postgres.get_all_worker_individual.remote()
|
||||
@@ -55,8 +61,15 @@ class GlobalIndividualManager:
|
||||
return self._individuals.get(agent_id, None)
|
||||
|
||||
def remove_individual(self, agent_id: str) -> None:
|
||||
"""安全地移除或注销 individual。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if agent_id in self._individuals:
|
||||
del self._individuals[agent_id]
|
||||
|
||||
def list_individuals(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""执行与 list individuals 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: (Dict[str, Dict[str, Any]]): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
|
||||
return self._individuals
|
||||
|
||||
@@ -18,10 +18,14 @@ from typing import List
|
||||
from enum import Enum
|
||||
|
||||
class ProviderStatus(str, Enum):
|
||||
"""ProviderStatus 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
UP = "up"
|
||||
DOWN = "down"
|
||||
|
||||
class Provider(BaseModel):
|
||||
"""Provider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
provider_title: str
|
||||
provider_url: str
|
||||
provider_apikey: str
|
||||
@@ -31,12 +35,16 @@ class Provider(BaseModel):
|
||||
provider_status: ProviderStatus = ProviderStatus.UP
|
||||
|
||||
class ProviderArgs(BaseModel):
|
||||
"""ProviderArgs 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
provider_title: str
|
||||
provider_url: str
|
||||
provider_apikey: str
|
||||
provider_owner: str
|
||||
|
||||
class BaseProvider(ABC):
|
||||
"""BaseProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from pretor.utils.retry import retry_on_retryable_error
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -13,13 +12,21 @@ from pretor.utils.retry import retry_on_retryable_error
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pretor.utils.retry import retry_on_retryable_error
|
||||
|
||||
from pretor.core.global_state_machine.model_provider.base_provider import BaseProvider, Provider, ProviderArgs
|
||||
import httpx
|
||||
from typing import List
|
||||
|
||||
class ClaudeProvider(BaseProvider):
|
||||
"""ClaudeProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
@staticmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
provider_models: List[str] = await ClaudeProvider._load_models(provider_args)
|
||||
provider: Provider = ClaudeProvider._return_provider(provider_args, provider_models)
|
||||
return provider
|
||||
@@ -28,6 +35,10 @@ class ClaudeProvider(BaseProvider):
|
||||
@retry_on_retryable_error()
|
||||
async def _load_models(provider_args: ProviderArgs) -> List[str]:
|
||||
# Anthropic 官方需要 version 头
|
||||
"""执行与 load models 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
headers = {
|
||||
"x-api-key": provider_args.provider_apikey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
@@ -53,6 +64,10 @@ class ClaudeProvider(BaseProvider):
|
||||
|
||||
@staticmethod
|
||||
def _return_provider(provider_args: ProviderArgs, provider_models: List[str]) -> Provider:
|
||||
"""执行与 return provider 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。 provider_models (List[str]): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_models 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return Provider(provider_title=provider_args.provider_title,
|
||||
provider_apikey=provider_args.provider_apikey,
|
||||
provider_url=provider_args.provider_url,
|
||||
|
||||
@@ -18,8 +18,14 @@ import httpx
|
||||
from typing import List
|
||||
|
||||
class DeepseekProvider(BaseProvider):
|
||||
"""DeepseekProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
@staticmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
provider_models: List[str] = await DeepseekProvider._load_models(provider_args)
|
||||
provider: Provider = DeepseekProvider._return_provider(provider_args, provider_models)
|
||||
return provider
|
||||
@@ -27,6 +33,10 @@ class DeepseekProvider(BaseProvider):
|
||||
@staticmethod
|
||||
@retry_on_retryable_error()
|
||||
async def _load_models(provider_args: ProviderArgs) -> List[str]:
|
||||
"""执行与 load models 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
headers = {
|
||||
"Authorization": f"Bearer {provider_args.provider_apikey}",
|
||||
"Content-Type": "application/json"
|
||||
@@ -52,6 +62,10 @@ class DeepseekProvider(BaseProvider):
|
||||
|
||||
@staticmethod
|
||||
def _return_provider(provider_args: ProviderArgs, provider_models: List[str]) -> Provider:
|
||||
"""执行与 return provider 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。 provider_models (List[str]): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_models 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return Provider(provider_title=provider_args.provider_title,
|
||||
provider_apikey=provider_args.provider_apikey,
|
||||
provider_url=provider_args.provider_url,
|
||||
|
||||
@@ -18,8 +18,14 @@ import httpx
|
||||
from typing import List
|
||||
|
||||
class OpenAIProvider(BaseProvider):
|
||||
"""OpenAIProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
@staticmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
provider_models: List[str] = await OpenAIProvider._load_models(provider_args)
|
||||
provider: Provider = OpenAIProvider._return_provider(provider_args, provider_models)
|
||||
return provider
|
||||
@@ -27,6 +33,10 @@ class OpenAIProvider(BaseProvider):
|
||||
@staticmethod
|
||||
@retry_on_retryable_error()
|
||||
async def _load_models(provider_args: ProviderArgs) -> List[str]:
|
||||
"""执行与 load models 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
headers = {
|
||||
"Authorization": f"Bearer {provider_args.provider_apikey}",
|
||||
"Content-Type": "application/json"
|
||||
@@ -52,6 +62,10 @@ class OpenAIProvider(BaseProvider):
|
||||
|
||||
@staticmethod
|
||||
def _return_provider(provider_args: ProviderArgs, provider_models: List[str]) -> Provider:
|
||||
"""执行与 return provider 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。 provider_models (List[str]): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_models 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return Provider(provider_title=provider_args.provider_title,
|
||||
provider_apikey=provider_args.provider_apikey,
|
||||
provider_url=provider_args.provider_url,
|
||||
|
||||
@@ -33,11 +33,19 @@ class ProviderManager:
|
||||
self.provider_register = {}
|
||||
|
||||
async def init_provider_register(self, postgres) -> None:
|
||||
"""完成 provider register 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
|
||||
Args: postgres: 参与 init provider register 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
providers = await postgres.get_provider.remote()
|
||||
for provider in providers:
|
||||
self.provider_register[provider.provider_title] = provider
|
||||
|
||||
async def add_provider(self, provider_type, provider_title, provider_url, provider_apikey, provider_owner, postgres_database) -> None:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_type: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_type 实例。 provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 provider_url: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_url 实例。 provider_apikey: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_apikey 实例。 provider_owner: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_owner 实例。 postgres_database: 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
from pretor.core.global_state_machine.model_provider import ProviderArgs
|
||||
from pretor.utils.logger import get_logger
|
||||
logger = get_logger('provider_manager')
|
||||
@@ -74,12 +82,23 @@ class ProviderManager:
|
||||
logger.warning(f"[{provider_args.provider_title}] 解析模型列表时发生错误: {e}")
|
||||
|
||||
def get_provider_list(self):
|
||||
"""检索并获取特定的 provider list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self.provider_register
|
||||
|
||||
def get_provider(self, provider_title):
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return self.provider_register.get(provider_title)
|
||||
|
||||
async def delete_provider(self, provider_title: str, postgres_database) -> None:
|
||||
"""安全地移除或注销 provider。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 postgres_database: 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if provider_title in self.provider_register:
|
||||
provider = self.provider_register[provider_title]
|
||||
await postgres_database.delete_provider_db.remote( provider_id=provider.provider_id)
|
||||
|
||||
@@ -18,6 +18,8 @@ import pathlib
|
||||
import json
|
||||
|
||||
class GlobalSkillManager:
|
||||
"""GlobalSkillManager 核心组件类。
|
||||
这是一个管理器类,职责集中在维护整个系统内有关 GlobalSkill 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
|
||||
skill_mapper = Dict[str,Tuple[str]]
|
||||
"""skill的存储表"""
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ from pretor.utils.logger import get_logger
|
||||
logger = get_logger('tool_manager')
|
||||
|
||||
class GlobalToolManager:
|
||||
"""GlobalToolManager 核心组件类。
|
||||
这是一个管理器类,职责集中在维护整个系统内有关 GlobalTool 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
|
||||
tool_mapper: Dict[str, Dict[str, Type[BaseToolData]]]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -25,6 +25,8 @@ from pretor.adapter.model_adapter.agent_factory import AgentFactory
|
||||
|
||||
@ray.remote
|
||||
class ConsciousnessNode:
|
||||
"""ConsciousnessNode 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
def __init__(self) -> None:
|
||||
from pretor.utils.logger import get_logger
|
||||
self.logger = get_logger('consciousness_node')
|
||||
@@ -70,6 +72,10 @@ class ConsciousnessNode:
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]):
|
||||
"""执行与 dynamic prompt 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: ctx (RunContext[ConsciousnessNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += (
|
||||
f"=== 当前任务上下文 ===\n"
|
||||
@@ -87,6 +93,10 @@ class ConsciousnessNode:
|
||||
return prompt
|
||||
|
||||
async def working(self, payload: Union[ForWorkflowEngineInput, ForWorkflowInput, ForSupervisoryInput]) -> Union[ForWorkflowEngine, ForWorkflow, ForSupervisoryNode, None]:
|
||||
"""执行与 working 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: payload (Union[ForWorkflowEngineInput, ForWorkflowInput, ForSupervisoryInput]): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (Union[ForWorkflowEngine, ForWorkflow, ForSupervisoryNode, None]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
result = await self._run(payload)
|
||||
if isinstance(result, (ForWorkflowEngine, ForWorkflow, ForSupervisoryNode)):
|
||||
@@ -139,6 +149,10 @@ class ConsciousnessNode:
|
||||
pass
|
||||
|
||||
async def _run(self, payload: Union[ForSupervisoryInput, ForWorkflowInput, ForWorkflowEngineInput]) -> Union[ForSupervisoryNode, ForWorkflow, ForWorkflowEngine]:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: payload (Union[ForSupervisoryInput, ForWorkflowInput, ForWorkflowEngineInput]): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (Union[ForSupervisoryNode, ForWorkflow, ForWorkflowEngine]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
self.agent.retries = 3
|
||||
if isinstance(payload, ForWorkflowEngineInput):
|
||||
|
||||
@@ -41,6 +41,8 @@ class ForSupervisoryNode(ConsciousnessNodeResponse):
|
||||
|
||||
|
||||
class ConsciousnessNodeDeps(DepsModel):
|
||||
"""ConsciousnessNodeDeps 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
original_command: str
|
||||
workflow_template: str | None = None
|
||||
command: str
|
||||
@@ -48,20 +50,28 @@ class ConsciousnessNodeDeps(DepsModel):
|
||||
|
||||
|
||||
class ConsciousnessNodeInput(InputModel):
|
||||
"""ConsciousnessNodeInput 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
pass
|
||||
|
||||
|
||||
class ForWorkflowEngineInput(ConsciousnessNodeInput):
|
||||
"""ForWorkflowEngineInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflowEngineInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
workflow_template: str | None = None
|
||||
original_command: str
|
||||
available_skills: list[dict] | None = None
|
||||
|
||||
|
||||
class ForWorkflowInput(ConsciousnessNodeInput):
|
||||
"""ForWorkflowInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflowInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
workflow_step: WorkStep
|
||||
original_command: str
|
||||
|
||||
|
||||
class ForSupervisoryInput(ConsciousnessNodeInput):
|
||||
"""ForSupervisoryInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForSupervisoryInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
workflow: PretorWorkflow
|
||||
original_command: str
|
||||
|
||||
@@ -23,6 +23,8 @@ from pretor.core.individual.control_node.template import ForWorkflow, ForWorkflo
|
||||
|
||||
@ray.remote
|
||||
class ControlNode:
|
||||
"""ControlNode 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
def __init__(self):
|
||||
from pretor.utils.logger import get_logger
|
||||
self.logger = get_logger('control_node')
|
||||
@@ -67,6 +69,10 @@ class ControlNode:
|
||||
tools=callables)
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]):
|
||||
"""执行与 dynamic prompt 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: ctx (RunContext[ControlNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += (
|
||||
f"=== 当前任务步骤上下文 ===\n"
|
||||
@@ -77,6 +83,10 @@ class ControlNode:
|
||||
return prompt
|
||||
|
||||
async def working(self, payload: ForWorkflowInput) -> str:
|
||||
"""执行与 working 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: payload (ForWorkflowInput): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
|
||||
try:
|
||||
result: ForWorkflow = await self._run(payload)
|
||||
return result
|
||||
@@ -85,6 +95,10 @@ class ControlNode:
|
||||
return None
|
||||
|
||||
async def _run(self, payload: ForWorkflowInput) -> ForWorkflow:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: payload (ForWorkflowInput): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (ForWorkflow): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
self.agent.retries = 3
|
||||
deps = ControlNodeDeps(
|
||||
|
||||
@@ -23,17 +23,25 @@ class ControlNodeResponse(ResponseModel):
|
||||
|
||||
|
||||
class ControlNodeInput(InputModel):
|
||||
"""ControlNodeInput 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
pass
|
||||
|
||||
|
||||
class ControlNodeDeps(DepsModel):
|
||||
"""ControlNodeDeps 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
workflow_step: WorkStep
|
||||
# In the future, this can be dynamically populated with tools specific to the current task execution
|
||||
|
||||
|
||||
class ForWorkflow(ControlNodeResponse):
|
||||
"""ForWorkflow 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflow 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
output: str = Field(..., description="控制节点执行特定工作流步骤的结果。包含执行细节和输出数据。")
|
||||
|
||||
|
||||
class ForWorkflowInput(ControlNodeInput):
|
||||
"""ForWorkflowInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflowInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
workflow_step: WorkStep
|
||||
|
||||
@@ -26,6 +26,8 @@ from pretor.utils.ray_hook import ray_actor_hook
|
||||
|
||||
@ray.remote
|
||||
class SupervisoryNode:
|
||||
"""SupervisoryNode 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
def __init__(self) -> None:
|
||||
from pretor.utils.logger import get_logger
|
||||
self.logger = get_logger('supervisory_node')
|
||||
@@ -71,6 +73,10 @@ class SupervisoryNode:
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[SupervisoryNodeDeps]):
|
||||
"""执行与 dynamic prompt 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: ctx (RunContext[SupervisoryNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += (
|
||||
f"=== 当前上下文 ===\n"
|
||||
@@ -111,8 +117,8 @@ class SupervisoryNode:
|
||||
if isinstance(payload, PretorEvent):
|
||||
payload.context["workflow_template"] = result.workflow_template
|
||||
try:
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.add_event.remote(payload)
|
||||
global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
|
||||
await global_workflow_manager.add_event.remote(payload)
|
||||
workflow_running_engine = ray_actor_hook("workflow_running_engine").workflow_running_engine
|
||||
await workflow_running_engine.put_event.remote(payload)
|
||||
except Exception as e:
|
||||
|
||||
@@ -17,21 +17,31 @@ from pretor.utils.agent_model import ResponseModel, DepsModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
class SupervisoryNodeResponse(ResponseModel):
|
||||
"""SupervisoryNodeResponse 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
pass
|
||||
|
||||
class ForUser(SupervisoryNodeResponse):
|
||||
"""ForUser 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForUser 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
context: str = Field(..., description="对用户的回复,应当使用和蔼的语气进行回复。用于直接解答简单问题或返回最终报告。")
|
||||
|
||||
class ForConsciousnessNode(SupervisoryNodeResponse):
|
||||
"""ForConsciousnessNode 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
workflow_template: str | None = Field(default=None, description="选择的工作流模板的名称,用于处理复杂任务。若无需模板则为 None。")
|
||||
reasoning: str = Field(..., description="选择将任务移交意识节点并选用该模板的简短原因。")
|
||||
|
||||
class TerminationMessage(BaseModel):
|
||||
"""TerminationMessage 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 TerminationMessage 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
platform: str
|
||||
user_name: str
|
||||
message: str
|
||||
|
||||
class SupervisoryNodeDeps(DepsModel):
|
||||
"""SupervisoryNodeDeps 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
|
||||
platform: str
|
||||
user_name: str
|
||||
time: str
|
||||
|
||||
@@ -22,14 +22,20 @@ NodeType = Literal[
|
||||
]
|
||||
|
||||
class EventInfo(BaseModel):
|
||||
"""EventInfo 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 EventInfo 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
platform: str
|
||||
user_name: str
|
||||
|
||||
class LogicGate(BaseModel):
|
||||
"""LogicGate 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 LogicGate 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
if_fail: str = Field(..., description="失败跳转目标,如 'jump_to_step_1'")
|
||||
if_pass: Literal["continue", "exit"] = Field(default="continue", description="成功后的动作")
|
||||
|
||||
class WorkStep(BaseModel):
|
||||
"""WorkStep 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkStep 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
step: int = Field(..., gt=0, description="步骤序号,严格自增")
|
||||
name: str = Field(..., description="步骤名称")
|
||||
node: NodeType = Field(..., description="负责执行的节点类型")
|
||||
@@ -46,6 +52,8 @@ class WorkStep(BaseModel):
|
||||
|
||||
|
||||
class WorkflowStatus(BaseModel):
|
||||
"""WorkflowStatus 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowStatus 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
step: int = Field(default=1, gt=0, description="当前运行到的工作流步数")
|
||||
status: Literal["waiting_llm_working", "waiting_tool_working", "llm_working", "tool_working"] = Field(
|
||||
default="waiting_llm_working",
|
||||
@@ -53,6 +61,8 @@ class WorkflowStatus(BaseModel):
|
||||
)
|
||||
|
||||
class PretorWorkflow(BaseModel):
|
||||
"""PretorWorkflow 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 PretorWorkflow 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
title: str = Field(..., description="工作流的标题")
|
||||
work_link: List[WorkStep] = Field(..., description="工作链逻辑定义")
|
||||
# ---------------- 以下为系统级管控字段,LLM 无需关心 ---------------- #
|
||||
@@ -66,6 +76,9 @@ class PretorWorkflow(BaseModel):
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_workflow_integrity(self) -> 'PretorWorkflow':
|
||||
"""执行与 validate workflow integrity 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: ('PretorWorkflow'): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
steps = [s.step for s in self.work_link]
|
||||
expected = list(range(1, len(steps) + 1))
|
||||
if steps != expected:
|
||||
|
||||
@@ -34,6 +34,10 @@ import pathlib
|
||||
|
||||
|
||||
def get_workflow_template(workflow_name: str) -> str:
|
||||
"""检索并获取特定的 workflow template 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: workflow_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
|
||||
workflow_template = pathlib.Path(__file__).parent.parent.parent / "workflow_template" / (workflow_name + "_workflow_template.json")
|
||||
with open(workflow_template, "r", encoding="utf-8") as workflow_template_file:
|
||||
workflow_template = workflow_template_file.read()
|
||||
@@ -41,6 +45,8 @@ def get_workflow_template(workflow_name: str) -> str:
|
||||
|
||||
|
||||
class WorkflowEngine:
|
||||
"""WorkflowEngine 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowEngine 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
def __init__(self,
|
||||
workflow: PretorWorkflow,
|
||||
consciousness_node=None,
|
||||
@@ -58,11 +64,15 @@ class WorkflowEngine:
|
||||
"""控制节点"""
|
||||
self.supervisory_node = supervisory_node
|
||||
"""监督节点"""
|
||||
self._gsm = ray_actor_hook("global_state_machine").global_state_machine
|
||||
self._gwm = ray_actor_hook("global_workflow_manager").global_workflow_manager
|
||||
|
||||
async def _push_sse(self, msg: str) -> None:
|
||||
"""执行与 push sse 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: msg (str): 控制逻辑流向的具体字符串参数,指定了期望的 msg 内容。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
await self._gsm.put_pending.remote(self.workflow.trace_id, msg)
|
||||
await self._gwm.put_pending.remote(self.workflow.trace_id, msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -273,6 +283,8 @@ class WorkflowEngine:
|
||||
|
||||
@ray.remote
|
||||
class WorkflowRunningEngine:
|
||||
"""WorkflowRunningEngine 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowRunningEngine 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
def __init__(self, consciousness_node=None, control_node=None, supervisory_node=None):
|
||||
from pretor.utils.logger import get_logger
|
||||
self.logger = get_logger('workflow_runner')
|
||||
@@ -285,6 +297,8 @@ class WorkflowRunningEngine:
|
||||
|
||||
async def run(self):
|
||||
# Move actor hook to async start so we don't race during __init__ across cluster
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。 """
|
||||
self.global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
self.workflow_queue = asyncio.Queue()
|
||||
self.runner_engine = {
|
||||
@@ -293,8 +307,22 @@ class WorkflowRunningEngine:
|
||||
}
|
||||
|
||||
async def put_event(self, event: PretorEvent) -> None:
|
||||
"""执行与 put event 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: event (PretorEvent): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
await self.workflow_queue.put(event)
|
||||
|
||||
async def resume_workflow(self, event: PretorEvent) -> None:
|
||||
"""Resume an incomplete workflow that was loaded from the database."""
|
||||
self.logger.info(f"Resuming workflow {event.trace_id}")
|
||||
workflow_engine = WorkflowEngine(event.workflow,
|
||||
self.consciousness_node,
|
||||
self.control_node,
|
||||
self.supervisory_node)
|
||||
# Assuming you want to schedule it via a task
|
||||
asyncio.create_task(workflow_engine.run())
|
||||
|
||||
async def runner(self, i: int) -> None:
|
||||
"""
|
||||
runner方法,从self.workflow_queue中不断取出任务并执行
|
||||
@@ -347,7 +375,8 @@ class WorkflowRunningEngine:
|
||||
self.logger.info(
|
||||
f"WorkflowRunningEngine: runner_{i} 成功生成工作流 {workflow.trace_id}:{workflow.title}")
|
||||
|
||||
await self.global_state_machine.update_workflow.remote(event.trace_id, workflow)
|
||||
global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
|
||||
await global_workflow_manager.update_workflow.remote(event.trace_id, workflow)
|
||||
|
||||
workflow_engine = WorkflowEngine(workflow,
|
||||
self.consciousness_node,
|
||||
|
||||
@@ -16,6 +16,8 @@ from pydantic import BaseModel, model_validator
|
||||
from typing import Dict,List
|
||||
|
||||
class WorkflowTemplateStep(BaseModel):
|
||||
"""WorkflowTemplateStep 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowTemplateStep 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
step: int
|
||||
node: str
|
||||
action: str
|
||||
@@ -25,12 +27,17 @@ class WorkflowTemplateStep(BaseModel):
|
||||
logic_gate: Dict[str, str]
|
||||
|
||||
class WorkflowTemplate(BaseModel):
|
||||
"""WorkflowTemplate 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowTemplate 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
name: str
|
||||
desc: str
|
||||
work_link: list[WorkflowTemplateStep]
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_steps(self) -> 'WorkflowTemplate':
|
||||
"""执行与 validate steps 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: ('WorkflowTemplate'): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
steps = [s.step for s in self.work_link]
|
||||
if len(steps) != len(set(steps)):
|
||||
raise ValueError("Step numbers in work_link must be unique")
|
||||
|
||||
@@ -16,8 +16,14 @@ from pathlib import Path
|
||||
from pretor.core.workflow.workflow_template_generator.workflow_template import WorkflowTemplate
|
||||
|
||||
class WorkflowTemplateGenerator:
|
||||
"""WorkflowTemplateGenerator 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowTemplateGenerator 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
@staticmethod
|
||||
def generate_workflow_template(workflow_template: WorkflowTemplate) -> WorkflowTemplate:
|
||||
"""执行与 generate workflow template 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: workflow_template (WorkflowTemplate): 参与 generate workflow template 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (WorkflowTemplate): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
output_dir = Path("pretor") / "workflow_template"
|
||||
if not output_dir.exists():
|
||||
output_dir.mkdir(parents=True)
|
||||
|
||||
@@ -21,6 +21,8 @@ from pretor.utils.logger import get_logger
|
||||
logger = get_logger('workflow_template_manager')
|
||||
|
||||
class WorkflowManager:
|
||||
"""WorkflowManager 核心组件类。
|
||||
这是一个管理器类,职责集中在维护整个系统内有关 Workflow 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
|
||||
def __init__(self):
|
||||
self.workflow_template_generator = WorkflowTemplateGenerator()
|
||||
self.workflow_templates_registry = {}
|
||||
@@ -28,6 +30,9 @@ class WorkflowManager:
|
||||
self._load_workflow_template()
|
||||
|
||||
def _load_workflow_template(self) -> None:
|
||||
"""执行与 load workflow template 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
for workflow_template_file in self.template_path.glob("*_workflow_template.json"):
|
||||
with workflow_template_file.open("r",encoding="utf-8") as f:
|
||||
try:
|
||||
@@ -39,6 +44,10 @@ class WorkflowManager:
|
||||
logger.warning(f"{workflow_template_file}不符合workflow_template格式")
|
||||
|
||||
def generate_workflow_template(self, workflow_template: WorkflowTemplate) -> None:
|
||||
"""执行与 generate workflow template 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: workflow_template (WorkflowTemplate): 参与 generate workflow template 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
workflow_template = self.workflow_template_generator.generate_workflow_template(workflow_template=workflow_template)
|
||||
self.workflow_templates_registry[workflow_template.name] = workflow_template.desc
|
||||
@@ -46,11 +55,22 @@ class WorkflowManager:
|
||||
logger.exception("Failed to generate workflow template")
|
||||
|
||||
def add_workflow_template(self, template_name: str, workflow_template: WorkflowTemplate) -> None:
|
||||
"""创建并持久化新的 workflow template 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: template_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 workflow_template (WorkflowTemplate): 参与 add workflow template 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
self.generate_workflow_template(workflow_template)
|
||||
|
||||
def get_all_workflow_templates(self) -> dict:
|
||||
"""检索并获取特定的 all workflow templates 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
|
||||
return self.workflow_templates_registry
|
||||
|
||||
def delete_workflow_template(self, template_name: str) -> None:
|
||||
"""安全地移除或注销 workflow template。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: template_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if template_name in self.workflow_templates_registry:
|
||||
del self.workflow_templates_registry[template_name]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .approval import ApprovalToolData, approval
|
||||
__all__ = ["ApprovalToolData", "approval"]
|
||||
|
||||
@@ -17,6 +17,8 @@ from pretor.utils.ray_hook import ray_actor_hook
|
||||
from typing import List, Literal, Dict
|
||||
|
||||
class ApprovalToolData(BaseToolData):
|
||||
"""ApprovalToolData 核心组件类。
|
||||
这是一个可被智能体动态调用的外部工具组件类。它定义了清晰的输入参数 Schema 与执行契约,赋予智能体与外界真实系统(如文件、网页、API)进行交互的能力。 """
|
||||
is_system: bool = True
|
||||
action_scope: List[Literal["control_node", "consciousness_node", "supervisory_node", "growth_node", "", ""]] = [
|
||||
"control_node", "consciousness_node"]
|
||||
|
||||
@@ -17,6 +17,8 @@ from typing import List, Literal, Dict
|
||||
from pydantic import ConfigDict
|
||||
|
||||
class BaseToolData(BaseModel):
|
||||
"""BaseToolData 核心组件类。
|
||||
这是一个可被智能体动态调用的外部工具组件类。它定义了清晰的输入参数 Schema 与执行契约,赋予智能体与外界真实系统(如文件、网页、API)进行交互的能力。 """
|
||||
model_config = ConfigDict(extra="allow")
|
||||
is_system: bool
|
||||
action_scope: List[Literal["control_node", "consciousness_node", "supervisory_node", "growth_node", "", ""]] = []
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .file_reader import FileReaderData, file_reader
|
||||
|
||||
__all__ = ["FileReaderData", "file_reader"]
|
||||
|
||||
@@ -17,6 +17,8 @@ from pretor.plugin.tool_plugin.base_tool import BaseToolData
|
||||
import os
|
||||
|
||||
class FileReaderData(BaseToolData):
|
||||
"""FileReaderData 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 FileReaderData 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
is_system: bool = True
|
||||
name: str = "file_reader"
|
||||
description: str = "读取本地文件的内容"
|
||||
|
||||
@@ -23,6 +23,8 @@ from pwdlib import PasswordHash
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
"""TokenData 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 TokenData 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
user_id: str
|
||||
username: Optional[str] = None
|
||||
exp: Optional[int] = None
|
||||
@@ -31,12 +33,21 @@ SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
|
||||
|
||||
if not SECRET_KEY or SECRET_KEY in {"secret", "114514"}:
|
||||
raise RuntimeError("未提供有效的 SECRET_KEY 或使用了不安全的默认值")
|
||||
|
||||
password_hasher = PasswordHash.recommended()
|
||||
|
||||
|
||||
class Accessor:
|
||||
"""Accessor 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 Accessor 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
@staticmethod
|
||||
def _decode_token(token: str) -> TokenData:
|
||||
"""执行与 decode token 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: token (str): 由认证中心颁发的 JWT 或长期访问令牌,用于跨服务调用时的身份自证与权限校验。
|
||||
Returns: (TokenData): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
@@ -57,6 +68,10 @@ class Accessor:
|
||||
|
||||
@staticmethod
|
||||
def _create_access_token(data: dict) -> str:
|
||||
"""创建并持久化新的 access token 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: data (dict): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": int(expire.timestamp())})
|
||||
@@ -64,10 +79,18 @@ class Accessor:
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""执行与 verify password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: plain_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 plain password 内容。 hashed_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 hashed password 内容。
|
||||
Returns: (bool): 一个布尔型结果标志,明确返回 True 表示该操作成功应用或条件达成,False 则表示失败或被拒绝。 """
|
||||
return password_hasher.verify(plain_password, hashed_password)
|
||||
|
||||
@staticmethod
|
||||
def get_current_user(request: Request) -> TokenData:
|
||||
"""检索并获取特定的 current user 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: request (Request): FastAPI 框架注入的原生 HTTP 请求对象,包含了完整的 Header 标头、查询参数和正文流。
|
||||
Returns: (TokenData): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
@@ -79,6 +102,10 @@ class Accessor:
|
||||
|
||||
@staticmethod
|
||||
def login_hashed_password(user: User, password: str) -> str:
|
||||
"""执行与 login hashed password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user (User): 当前已通过鉴权流程的访问者实体对象,内部包含用户角色、权限层级及租户归属等核心元信息。 password (str): 控制逻辑流向的具体字符串参数,指定了期望的 password 内容。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -97,6 +124,10 @@ class Accessor:
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
"""执行与 hash password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: password (str): 控制逻辑流向的具体字符串参数,指定了期望的 password 内容。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
|
||||
if not password:
|
||||
raise ValueError("密码不能为空")
|
||||
if len(password) < 6:
|
||||
|
||||
@@ -16,10 +16,16 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ResponseModel(BaseModel):
|
||||
"""ResponseModel 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ResponseModel 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
pass
|
||||
|
||||
class DepsModel(BaseModel):
|
||||
"""DepsModel 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 DepsModel 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
pass
|
||||
|
||||
class InputModel(BaseModel):
|
||||
"""InputModel 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 InputModel 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
pass
|
||||
@@ -1,7 +1,24 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
import yaml
|
||||
def print_banner() -> None:
|
||||
"""执行与 print banner 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
with open("config/config.yml","r") as config:
|
||||
config = yaml.load(config, Loader=yaml.FullLoader)
|
||||
version = config.get("version", "unknown")
|
||||
|
||||
@@ -18,6 +18,10 @@ from pretor.core.database.table.user import UserAuthority
|
||||
from pretor.utils.ray_hook import ray_actor_hook
|
||||
|
||||
async def get_authority(user_id: str) -> UserAuthority:
|
||||
"""检索并获取特定的 authority 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: (UserAuthority): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
from pretor.utils.error import UserNotExistError
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
try:
|
||||
@@ -38,11 +42,17 @@ async def get_authority(user_id: str) -> UserAuthority:
|
||||
raise
|
||||
|
||||
class RoleChecker:
|
||||
"""RoleChecker 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 RoleChecker 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
def __init__(self, **kwargs):
|
||||
self.allowed_roles = kwargs.get("allowed_roles", )
|
||||
|
||||
async def __call__(self,
|
||||
token_data: Annotated[TokenData, Depends(Accessor.get_current_user)]):
|
||||
"""执行与 call 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: token_data (Annotated[TokenData, Depends(Accessor.get_current_user)]): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
user_authority = await get_authority(token_data.user_id)
|
||||
if user_authority < self.allowed_roles:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -21,30 +21,48 @@ class NonRetryableError(Exception):
|
||||
pass
|
||||
|
||||
class DemandError(NonRetryableError):
|
||||
"""DemandError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 Demand 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
|
||||
pass
|
||||
|
||||
class ModelNotExistError(Exception):
|
||||
"""ModelNotExistError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 ModelNotExist 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
|
||||
pass
|
||||
|
||||
class UserError(Exception):
|
||||
"""UserError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 User 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
|
||||
pass
|
||||
|
||||
class UserNotExistError(UserError):
|
||||
"""UserNotExistError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 UserNotExist 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
|
||||
pass
|
||||
|
||||
class UserPasswordError(UserError):
|
||||
"""UserPasswordError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 UserPassword 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
|
||||
pass
|
||||
|
||||
class ProviderError(Exception):
|
||||
"""ProviderError 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
pass
|
||||
|
||||
class ProviderNotExistError(ProviderError):
|
||||
"""ProviderNotExistError 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
|
||||
pass
|
||||
|
||||
class WorkflowError(Exception):
|
||||
|
||||
"""WorkflowError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 Workflow 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
|
||||
pass
|
||||
|
||||
class WorkflowExit(WorkflowError):
|
||||
|
||||
"""WorkflowExit 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowExit 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
pass
|
||||
|
||||
@@ -16,8 +16,6 @@ import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from typing import Callable, Dict, List
|
||||
import pathlib
|
||||
from pretor.utils.ray_hook import ray_actor_hook
|
||||
|
||||
from pretor.utils.logger import get_logger
|
||||
logger = get_logger('get_tool')
|
||||
@@ -25,6 +23,10 @@ _tool_cache: Dict[str, Callable] = {}
|
||||
|
||||
|
||||
def _get_tool_func(tool_name: str) -> Callable | None:
|
||||
"""检索并获取特定的 tool func 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: tool_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (Callable | None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
func = _tool_cache.get(tool_name, None)
|
||||
if func:
|
||||
return func
|
||||
@@ -64,10 +66,18 @@ def _get_tool_func(tool_name: str) -> Callable | None:
|
||||
return None
|
||||
|
||||
def del_tool_cache(tool_name: str) -> None:
|
||||
"""执行与 del tool cache 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: tool_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if tool_name in _tool_cache:
|
||||
del _tool_cache[tool_name]
|
||||
|
||||
def load_tools_from_list(tool_names: List[str] | None) -> List[Callable]:
|
||||
"""执行与 load tools from list 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: tool_names (List[str] | None): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (List[Callable]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
|
||||
if not tool_names:
|
||||
return []
|
||||
|
||||
|
||||
@@ -17,10 +17,17 @@ from rich.logging import RichHandler
|
||||
from loguru._logger import Logger
|
||||
|
||||
def setup_logger() -> Logger:
|
||||
"""对现有的 setup logger 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Returns: (Logger): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
logger.remove()
|
||||
|
||||
def format_record(record):
|
||||
# Format string for rich handler
|
||||
"""执行与 format record 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: record: 参与 format record 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
actor = record["extra"].get("actor_name", "System")
|
||||
trace_id = record["extra"].get("trace_id", "")
|
||||
|
||||
@@ -41,4 +48,8 @@ def setup_logger() -> Logger:
|
||||
global_logger = setup_logger()
|
||||
|
||||
def get_logger(actor_name: str, trace_id: str = "") -> Logger:
|
||||
"""检索并获取特定的 logger 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: actor_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 trace_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 trace 实例。
|
||||
Returns: (Logger): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return global_logger.bind(actor_name=actor_name, trace_id=trace_id)
|
||||
|
||||
@@ -29,6 +29,9 @@ def pickle(cls: T) -> T:
|
||||
"""
|
||||
def __reduce__(self):
|
||||
# 1. 序列化:触发 Pydantic-core (Rust) 的极速序列化
|
||||
"""执行与 reduce 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
data = self.model_dump_json()
|
||||
# 2. 反序列化:告诉 Pickle 重建时调用 cls.model_validate_json
|
||||
return cls.model_validate_json, (data,)
|
||||
|
||||
@@ -15,18 +15,30 @@ import ray
|
||||
from functools import lru_cache
|
||||
|
||||
class ActorList:
|
||||
"""ActorList 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ActorList 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
|
||||
def __init__(self):
|
||||
super().__setattr__('dict', {})
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
"""对现有的 setattr 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: key: 参与 setattr 逻辑运算或数据构建的上下文依赖对象。 value: 参与 setattr 逻辑运算或数据构建的上下文依赖对象。 """
|
||||
self.dict[key] = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
"""检索并获取特定的 getattr 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: key: 参与 getattr 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if key in self.dict:
|
||||
return self.dict[key]
|
||||
raise AttributeError(f"ActorList 对象没有属性 '{key}'")
|
||||
|
||||
def __delattr__(self, key):
|
||||
"""执行与 delattr 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: key: 参与 delattr 逻辑运算或数据构建的上下文依赖对象。 """
|
||||
if key in self.dict:
|
||||
del self.dict[key]
|
||||
else:
|
||||
@@ -42,6 +54,9 @@ def clear_actor_cache():
|
||||
_get_cached_actor_handle.cache_clear()
|
||||
|
||||
def ray_actor_hook(*actor_names: str):
|
||||
"""执行与 ray actor hook 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
actor_list = ActorList()
|
||||
for actor_name in actor_names:
|
||||
handle = _get_cached_actor_handle(actor_name)
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from pretor.utils.error import RetryableError
|
||||
|
||||
def retry_on_retryable_error(max_retries=3, base_delay=1):
|
||||
"""执行与 retry on retryable error 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: max_retries: 参与 retry on retryable error 逻辑运算或数据构建的上下文依赖对象。 base_delay: 参与 retry on retryable error 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
def decorator(func):
|
||||
"""执行与 decorator 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: func: 参与 decorator 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
"""执行与 async wrapper 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
@@ -19,6 +44,9 @@ def retry_on_retryable_error(max_retries=3, base_delay=1):
|
||||
else:
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
"""执行与 sync wrapper 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
import time
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# Copyright 2026 zhaoxi826
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pretor.worker_individual.base_individual import BaseIndividual
|
||||
from pretor.worker_individual.skill_individual import SkillIndividual
|
||||
from pretor.worker_individual.ordinary_individual import OrdinaryIndividual
|
||||
|
||||
@@ -23,12 +23,18 @@ from pretor.utils.logger import get_logger
|
||||
logger = get_logger('worker_individual')
|
||||
|
||||
class WorkerIndividualResponse(ResponseModel):
|
||||
"""WorkerIndividualResponse 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
|
||||
output: str = Field(..., description="Worker执行任务的输出结果")
|
||||
|
||||
class WorkerIndividualDeps(DepsModel):
|
||||
"""WorkerIndividualDeps 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
|
||||
task_event: dict
|
||||
|
||||
class WorkerIndividualInput(InputModel):
|
||||
"""WorkerIndividualInput 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
|
||||
task_event: dict
|
||||
|
||||
class BaseIndividual:
|
||||
@@ -42,6 +48,10 @@ class BaseIndividual:
|
||||
self.agent: Agent | None = None
|
||||
|
||||
async def _init_agent(self, agent_name: str, system_prompt: str):
|
||||
"""完成 agent 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
|
||||
Args: agent_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 system_prompt (str): 控制逻辑流向的具体字符串参数,指定了期望的 system prompt 内容。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
from pretor.utils.get_tool import load_tools_from_list
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
provider_title = self.agent_config.get("provider_title", "openai") # default fallback
|
||||
@@ -65,6 +75,10 @@ class BaseIndividual:
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[WorkerIndividualDeps]):
|
||||
"""执行与 dynamic prompt 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: ctx (RunContext[WorkerIndividualDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += (
|
||||
f"=== 当前任务上下文 ===\n"
|
||||
@@ -73,4 +87,8 @@ class BaseIndividual:
|
||||
return prompt
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
|
||||
raise NotImplementedError("子类必须实现 run 方法")
|
||||
|
||||
@@ -26,6 +26,10 @@ class OrdinaryIndividual(BaseIndividual):
|
||||
super().__init__(agent_config)
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get("prompt", "你是一个普通的AI助手,请尽力完成给定的任务。")
|
||||
await self._init_agent("ordinary_individual", system_prompt)
|
||||
|
||||
@@ -88,6 +88,10 @@ class SkillIndividual(BaseIndividual):
|
||||
return tools
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get("prompt",
|
||||
"你是一个拥有专业技能的专家级AI助手,请利用你的专业知识完成给定的任务。")
|
||||
|
||||
@@ -26,6 +26,10 @@ class SpecialIndividual(BaseIndividual):
|
||||
super().__init__(agent_config)
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get("prompt", "你是一个特殊的AI助手,负责处理特殊类型的任务。")
|
||||
await self._init_agent("special_individual", system_prompt)
|
||||
|
||||
@@ -45,6 +45,8 @@ class WorkerCluster:
|
||||
self.logger = get_logger('worker_cluster')
|
||||
|
||||
async def start(self):
|
||||
"""执行与 start 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。 """
|
||||
if self.task_queue is None:
|
||||
self.task_queue = Queue()
|
||||
self.runners = [asyncio.create_task(self._runner(i)) for i in range(self.num_runners)]
|
||||
@@ -78,6 +80,9 @@ class WorkerCluster:
|
||||
return worker
|
||||
|
||||
async def _runner(self, runner_id: int):
|
||||
"""执行与 runner 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: runner_id (int): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 runner 实例。 """
|
||||
while True:
|
||||
try:
|
||||
if self.task_queue is None:
|
||||
@@ -119,6 +124,10 @@ class WorkerCluster:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def submit_task(self, task_id: str, agent_id: str, task_event: dict):
|
||||
"""执行与 submit task 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 task 实例。 agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
if not self.runners:
|
||||
await self.start()
|
||||
|
||||
@@ -140,6 +149,9 @@ class WorkerCluster:
|
||||
self.results_futures.pop(task_id, None)
|
||||
|
||||
def get_cluster_metrics(self):
|
||||
"""检索并获取特定的 cluster metrics 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
|
||||
return {
|
||||
"active_worker_count": len(self._active_workers),
|
||||
"max_capacity": self.max_capacity,
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
import json
|
||||
import sys
|
||||
import builtins
|
||||
|
||||
real_import = builtins.__import__
|
||||
|
||||
|
||||
def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
if name == 'sqlmodel':
|
||||
mock_sqlmodel = MagicMock()
|
||||
|
||||
class DummySQLModel:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
pass
|
||||
|
||||
mock_sqlmodel.SQLModel = DummySQLModel
|
||||
mock_sqlmodel.Field = MagicMock(return_value=None)
|
||||
mock_sqlmodel.select = MagicMock()
|
||||
return mock_sqlmodel
|
||||
return real_import(name, globals, locals, fromlist, level)
|
||||
|
||||
|
||||
builtins.__import__ = mock_import
|
||||
for mod in list(sys.modules.keys()):
|
||||
if 'pretor.core.database.module.memory' in mod or 'sqlmodel' in mod:
|
||||
del sys.modules[mod]
|
||||
from pretor.core.database.module.memory import MemoryRAG
|
||||
|
||||
builtins.__import__ = real_import
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_dependencies():
|
||||
with patch("pretor.core.database.module.memory.WorkflowRecord") as mock_workflow_record:
|
||||
with patch("pretor.core.database.module.memory.MemoryRecord") as mock_memory_record:
|
||||
with patch("pretor.core.database.module.memory.select") as mock_select:
|
||||
yield mock_workflow_record, mock_memory_record, mock_select
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session_maker():
|
||||
maker = MagicMock()
|
||||
session = AsyncMock()
|
||||
session.add = MagicMock()
|
||||
maker.return_value.__aenter__.return_value = session
|
||||
maker.__aenter__.return_value = session
|
||||
maker.__aexit__ = AsyncMock()
|
||||
return maker, session
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_workflow(mock_session_maker, mock_dependencies):
|
||||
mock_workflow_record, _, _ = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_record = MagicMock()
|
||||
mock_workflow_record.return_value = mock_record
|
||||
|
||||
workflow_data = {"key": "value"}
|
||||
record = await rag.save_workflow("wf_123", workflow_data)
|
||||
|
||||
mock_workflow_record.assert_called_once_with(
|
||||
workflow_id="wf_123",
|
||||
workflow_data_json=json.dumps(workflow_data)
|
||||
)
|
||||
session.add.assert_called_once_with(mock_record)
|
||||
session.commit.assert_called_once()
|
||||
session.refresh.assert_called_once_with(mock_record)
|
||||
assert record == mock_record
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_workflow_success(mock_session_maker, mock_dependencies):
|
||||
_, _, mock_select = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_statement = MagicMock()
|
||||
mock_select.return_value.where.return_value = mock_statement
|
||||
|
||||
mock_record = MagicMock()
|
||||
mock_record.workflow_data_json = '{"key": "value"}'
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_record
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
data = await rag.get_workflow("wf_123")
|
||||
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert data == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_workflow_not_found(mock_session_maker, mock_dependencies):
|
||||
_, _, mock_select = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
data = await rag.get_workflow("wf_123")
|
||||
assert data is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_memory(mock_session_maker, mock_dependencies):
|
||||
_, mock_memory_record, _ = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_record = MagicMock()
|
||||
mock_memory_record.return_value = mock_record
|
||||
|
||||
record = await rag.add_memory("text", [0.1, 0.2])
|
||||
|
||||
mock_memory_record.assert_called_once_with(memory_text="text", embedding=[0.1, 0.2])
|
||||
session.add.assert_called_once_with(mock_record)
|
||||
session.commit.assert_called_once()
|
||||
session.refresh.assert_called_once_with(mock_record)
|
||||
assert record == mock_record
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieve_memory(mock_session_maker, mock_dependencies):
|
||||
_, _, mock_select = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_statement = MagicMock()
|
||||
mock_select.return_value.limit.return_value = mock_statement
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.all.return_value = ["res1", "res2"]
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
results = await rag.retrieve_memory([0.1, 0.2], 5)
|
||||
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert results == ["res1", "res2"]
|
||||
@@ -1,8 +1,6 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
import sys
|
||||
|
||||
import builtins
|
||||
|
||||
real_import = builtins.__import__
|
||||
@@ -31,8 +29,6 @@ for mod in list(sys.modules.keys()):
|
||||
del sys.modules[mod]
|
||||
|
||||
from pretor.core.global_state_machine.global_state_machine import GlobalStateMachine
|
||||
from pretor.api.platform.event import PretorEvent
|
||||
from pretor.core.workflow.workflow import PretorWorkflow
|
||||
|
||||
builtins.__import__ = real_import
|
||||
|
||||
@@ -48,53 +44,6 @@ def gsm(mock_postgres):
|
||||
return manager
|
||||
|
||||
|
||||
def test_add_delete_get_event(gsm):
|
||||
event = MagicMock(spec=PretorEvent)
|
||||
event.trace_id = "123"
|
||||
|
||||
gsm.add_event(event)
|
||||
|
||||
assert getattr(event, 'pending_queue', None) is not None
|
||||
assert getattr(event, 'receive_queue', None) is not None
|
||||
|
||||
retrieved = gsm.get_event("123")
|
||||
assert retrieved == event
|
||||
|
||||
gsm.delete_event("123")
|
||||
assert gsm.get_event("123") is None
|
||||
|
||||
|
||||
def test_update_attachment_and_workflow(gsm):
|
||||
event = MagicMock(spec=PretorEvent)
|
||||
event.trace_id = "abc"
|
||||
gsm.add_event(event)
|
||||
|
||||
gsm.update_attachment("abc", {"k": "v"})
|
||||
assert event.attachment == {"k": "v"}
|
||||
|
||||
wf = MagicMock(spec=PretorWorkflow)
|
||||
gsm.update_workflow("abc", wf)
|
||||
assert event.workflow == wf
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_queues(gsm):
|
||||
event = MagicMock(spec=PretorEvent)
|
||||
event.trace_id = "q_event"
|
||||
# To use await put/get, we must actually use real asyncio queues for the mock event
|
||||
event.pending_queue = asyncio.Queue()
|
||||
event.receive_queue = asyncio.Queue()
|
||||
gsm.event_dict["q_event"] = event
|
||||
|
||||
await gsm.put_pending("q_event", "item1")
|
||||
res1 = await gsm.get_pending("q_event")
|
||||
assert res1 == "item1"
|
||||
|
||||
await gsm.put_received("q_event", "item2")
|
||||
res2 = await gsm.get_received("q_event")
|
||||
assert res2 == "item2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_provider_success(gsm, mock_postgres):
|
||||
mock_provider_class = AsyncMock()
|
||||
|
||||
@@ -147,16 +147,21 @@ async def test_workflow_running_engine_runner():
|
||||
|
||||
# Mock the global_state_machine get_skill_list.remote method properly
|
||||
mock_gsm = MagicMock()
|
||||
mock_gsm.get_skill_list.remote = AsyncMock(return_value={"test_skill": ("description", "instructions")})
|
||||
mock_gsm.list_individuals.remote = AsyncMock(return_value={"test_skill": {"agent_type": "skill_individual", "agent_name": "TestSkill", "description": "desc"}})
|
||||
engine.global_state_machine = mock_gsm
|
||||
|
||||
with patch("pretor.core.workflow.workflow_runner.WorkflowEngine") as mock_wf_engine_cls, patch("builtins.open", new_callable=MagicMock) as mock_open:
|
||||
with patch("pretor.core.workflow.workflow_runner.WorkflowEngine") as mock_wf_engine_cls, patch("builtins.open", new_callable=MagicMock) as mock_open, \
|
||||
patch("pretor.core.workflow.workflow_runner.ray_actor_hook") as mock_hook:
|
||||
|
||||
# Instead of patching hook, we inject it directly
|
||||
engine.global_state_machine = AsyncMock()
|
||||
# engine.global_state_machine = AsyncMock()
|
||||
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = '{}'
|
||||
|
||||
mock_gwm = MagicMock()
|
||||
mock_gwm.update_workflow.remote = AsyncMock()
|
||||
mock_hook.return_value.global_workflow_manager = mock_gwm
|
||||
|
||||
mock_engine_instance = MagicMock()
|
||||
mock_engine_instance.run = AsyncMock()
|
||||
mock_wf_engine_cls.return_value = mock_engine_instance
|
||||
|
||||
Reference in New Issue
Block a user