chore(release): v0.1.1-alpha

##前端美化和bug修复
#### 💄 美化
- **前端美化**:对于整个前端效果进行了重新设计,现在的前端看起来会更立体。

#### 🐛 修复
- **前端演示**:修复了前端展示workflow列表的bug,但是workflow的具体条目显示由于序列化导致仍然有问题。 
- **密钥修复**:对于secret_key现在在使用默认情况时,会强制生成一个安全的密钥。
This commit is contained in:
2026-05-04 16:38:21 +08:00
committed by GitHub
parent d84212f780
commit d30c7e37a6
92 changed files with 2449 additions and 863 deletions
+1 -1
View File
@@ -6,6 +6,6 @@ __pycache__
frontend/node_modules frontend/node_modules
frontend/dist frontend/dist
docker-compose.yml docker-compose.yml
.env .env.template
.env.example .env.example
.idea .idea
View File
+11 -1
View File
@@ -1,9 +1,19 @@
# ChangeLog # ChangeLog
---
## [v0.1.1Alpha] - 2026/5/4
### 更新:
#### 💄 美化
- **前端美化**:对于整个前端效果进行了重新设计,现在的前端看起来会更立体。
#### 🐛 修复
- **前端演示**:修复了前端展示workflow列表的bug,但是workflow的具体条目显示由于序列化导致仍然有问题。
- **密钥修复**:对于secret_key现在在使用默认情况时,会强制生成一个安全的密钥。
--- ---
## [v0.1.0Alpha] - 2026/4/28 ## [v0.1.0Alpha] - 2026/4/28
### 更新: ### 更新:
#### 🚀 新增功能 (Added) #### 🚀 新增功能
- **分布式 Actor 骨架**:基于 Ray 框架构建了多智能体协作底座,支持节点跨进程通讯与资源调度。 - **分布式 Actor 骨架**:基于 Ray 框架构建了多智能体协作底座,支持节点跨进程通讯与资源调度。
- **全局状态机 (GSM)**:实现了 `GlobalStateMachine` 模块,作为系统的“唯一真相来源”,管理所有 Individual、Skill 和 Provider 的注册信息。 - **全局状态机 (GSM)**:实现了 `GlobalStateMachine` 模块,作为系统的“唯一真相来源”,管理所有 Individual、Skill 和 Provider 的注册信息。
- **核心认知节点** - **核心认知节点**
+232 -2
View File
@@ -8,6 +8,7 @@
"name": "pretor-dashboard", "name": "pretor-dashboard",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@xyflow/react": "^12.10.2",
"axios": "^1.15.1", "axios": "^1.15.1",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react": "^19.2.4", "react": "^19.2.4",
@@ -1153,6 +1154,55 @@
"tslib": "^2.4.0" "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": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1181,7 +1231,7 @@
"version": "19.2.14", "version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "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": { "node_modules/acorn": {
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -1724,6 +1806,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1789,9 +1877,114 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -3461,6 +3654,15 @@
"punycode": "^2.1.0" "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": { "node_modules/vite": {
"version": "8.0.8", "version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
@@ -3607,6 +3809,34 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "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
}
}
} }
} }
} }
+1
View File
@@ -10,6 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@xyflow/react": "^12.10.2",
"axios": "^1.15.1", "axios": "^1.15.1",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react": "^19.2.4", "react": "^19.2.4",
+115 -34
View File
@@ -1,25 +1,58 @@
import { useState, useEffect } from 'react'; 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 { SettingsLayout } from './components/Settings/SettingsLayout';
import { AgentLayout } from './components/Agent/AgentLayout'; 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 { LeftPanel } from './components/Chat/LeftPanel';
import { ChatPanel } from './components/Chat/ChatPanel'; import { ChatPanel } from './components/Chat/ChatPanel';
import { RightPanel } from './components/Chat/RightPanel'; import { RightPanel } from './components/Chat/RightPanel';
import { WorkflowListView } from './components/Chat/WorkflowListView';
import { AuthPage } from './components/Auth/AuthPage'; 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() { function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false); 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); 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(() => { useEffect(() => {
// Check if token exists in localStorage on mount
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
setIsAuthenticated(true); setIsAuthenticated(true);
@@ -31,37 +64,85 @@ function App() {
} }
return ( 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) */} {/* 2. Main Content Area */}
<Sidebar currentView={currentView} setCurrentView={setCurrentView} /> <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 */} {/* Dynamic View based on Mode and Tab */}
{currentView === 'agent' ? ( <div className="flex-1 flex overflow-hidden">
<AgentLayout agentTab={agentTab} setAgentTab={setAgentTab} /> {mode === 'work' && workTab === 'chat' && (
) : currentView === 'resource' ? ( <div className="flex-1 p-6 flex overflow-hidden">
<ResourceLayout resourceTab={resourceTab} setResourceTab={setResourceTab} /> <div className="flex-1 flex bg-white rounded-3xl shadow-md border border-slate-200 overflow-hidden relative">
) : currentView === 'dashboard' ? ( <LeftPanel
<> activeTab="chats"
{/* 2. Left Panel - Cluster Status & Workflows/Chats */} selectedWorkflow={null}
<LeftPanel setSelectedWorkflow={() => {}}
activeTab={activeTab} // Pass hoisted state down
setActiveTab={setActiveTab} chatSessions={chatSessions}
selectedWorkflow={selectedWorkflow} setChatSessions={setChatSessions}
setSelectedWorkflow={setSelectedWorkflow} activeSessionId={activeSessionId}
/> setActiveSessionId={setActiveSessionId}
/>
<ChatPanel
chatSessions={chatSessions}
setChatSessions={setChatSessions}
activeSessionId={activeSessionId}
setActiveSessionId={setActiveSessionId}
/>
</div>
</div>
)}
{/* 3. Middle Panel - AI Chat */} {mode === 'work' && workTab === 'workflow' && (
<ChatPanel /> <>
{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) */} {mode === 'agent' && agentTab === 'agents' && (
{activeTab === 'workflows' && <RightPanel selectedWorkflow={selectedWorkflow} />} <AgentLayout agentTab={innerAgentTab} setAgentTab={setInnerAgentTab} />
</> )}
) : (
/* Settings View */
<SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} />
)}
{mode === 'agent' && agentTab === 'plugin' && (
<PluginLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
)}
</div>
</>
)}
</div>
</div> </div>
); );
} }
+25 -29
View File
@@ -1,4 +1,3 @@
import { Bot, Key } from 'lucide-react';
import { ProvidersSettings } from './ProvidersSettings'; import { ProvidersSettings } from './ProvidersSettings';
import { WorkerIndividualSettings } from './WorkerIndividualSettings'; import { WorkerIndividualSettings } from './WorkerIndividualSettings';
@@ -9,35 +8,32 @@ interface AgentLayoutProps {
export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) { export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) {
return ( return (
<div className="flex-1 flex bg-slate-50 overflow-hidden"> <div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
{/* Agent Inner Sidebar */} {/* Top Tabs for Agent Module */}
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-0"> <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">
<div className="p-6 border-b border-slate-100"> <button
<h2 className="text-lg font-semibold text-slate-800">Agents</h2> onClick={() => setAgentTab('worker')}
</div> className={`py-4 text-sm font-medium border-b-2 transition-colors ${
<div className="flex-1 p-4 space-y-2 overflow-y-auto"> agentTab === 'worker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
<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'}`} Individual
> </button>
<Bot size={18} className="mr-3" /> <button
Individual onClick={() => setAgentTab('providers')}
</button> className={`py-4 text-sm font-medium border-b-2 transition-colors ${
<button agentTab === 'providers' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
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'}`} >
> Provider Management
<Key size={18} className="mr-3" /> </button>
Provider Management </div>
</button>
</div>
</div>
{/* Agent Main Content */} {/* Main Content */}
<div className="flex-1 overflow-y-auto p-8"> <div className="flex-1 overflow-y-auto p-8">
{agentTab === 'worker' && <WorkerIndividualSettings />} {agentTab === 'worker' && <WorkerIndividualSettings />}
{agentTab === 'providers' && <ProvidersSettings />} {agentTab === 'providers' && <ProvidersSettings />}
</div> </div>
</div> </div>
); );
} }
@@ -167,7 +167,7 @@ export function WorkerIndividualSettings() {
</div> </div>
<button <button
onClick={handleAddNew} 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" /> <Plus size={16} className="mr-2" />
Add Worker Add Worker
@@ -203,7 +203,7 @@ export function WorkerIndividualSettings() {
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id} {w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
</td> </td>
<td className="p-4 text-right space-x-2"> <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} /> <Edit2 size={16} />
</button> </button>
</td> </td>
@@ -219,7 +219,7 @@ export function WorkerIndividualSettings() {
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id} {w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
</td> </td>
<td className="p-4 text-right space-x-2"> <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} /> <Edit2 size={16} />
</button> </button>
<button onClick={() => handleDelete(w.agent_id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete"> <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 required
value={editData.agent_name || ''} value={editData.agent_name || ''}
onChange={(e) => setEditData({...editData, agent_name: e.target.value})} 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} disabled={(editData as any).is_system}
/> />
</div> </div>
@@ -265,7 +265,7 @@ export function WorkerIndividualSettings() {
<select <select
value={editData.agent_type || 'ordinary_individual'} value={editData.agent_type || 'ordinary_individual'}
onChange={(e) => setEditData({...editData, agent_type: e.target.value})} 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} disabled={(editData as any).is_system}
> >
<option value="ordinary_individual">Ordinary Individual</option> <option value="ordinary_individual">Ordinary Individual</option>
@@ -285,7 +285,7 @@ export function WorkerIndividualSettings() {
value={editData.provider_title || ''} value={editData.provider_title || ''}
onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})} onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
required 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> <option value="" disabled>Select Provider</option>
{providers.map((p) => ( {providers.map((p) => (
@@ -303,7 +303,7 @@ export function WorkerIndividualSettings() {
value={editData.model_id || ''} value={editData.model_id || ''}
onChange={(e) => setEditData({...editData, model_id: e.target.value})} onChange={(e) => setEditData({...editData, model_id: e.target.value})}
required 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> <option value="" disabled>Select a model</option>
{models.map(m => <option key={m} value={m}>{m}</option>)} {models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -321,7 +321,7 @@ export function WorkerIndividualSettings() {
value={editData.description || ''} value={editData.description || ''}
onChange={(e) => setEditData({...editData, description: e.target.value})} onChange={(e) => setEditData({...editData, description: e.target.value})}
rows={2} 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> </div>
@@ -331,7 +331,7 @@ export function WorkerIndividualSettings() {
value={editData.system_prompt || ''} value={editData.system_prompt || ''}
onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})}
rows={3} 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>
@@ -342,7 +342,7 @@ export function WorkerIndividualSettings() {
value={editData.output_template || '{}'} value={editData.output_template || '{}'}
onChange={(e) => setEditData({...editData, output_template: e.target.value})} onChange={(e) => setEditData({...editData, output_template: e.target.value})}
rows={3} 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>
<div> <div>
@@ -359,7 +359,7 @@ export function WorkerIndividualSettings() {
const newSkill = val ? { [val]: [] } : {}; const newSkill = val ? { [val]: [] } : {};
setEditData({...editData, bound_skill: JSON.stringify(newSkill)}); 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'} disabled={editData.agent_type !== 'skill_individual'}
> >
<option value="">No Skill Bound</option> <option value="">No Skill Bound</option>
@@ -376,7 +376,7 @@ export function WorkerIndividualSettings() {
value={editData.workspace || '[]'} value={editData.workspace || '[]'}
onChange={(e) => setEditData({...editData, workspace: e.target.value})} onChange={(e) => setEditData({...editData, workspace: e.target.value})}
rows={2} 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> </div>
</> </>
@@ -407,7 +407,7 @@ export function WorkerIndividualSettings() {
}} }}
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${ className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
isSelected 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' : 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100'
}`} }`}
> >
@@ -437,7 +437,7 @@ export function WorkerIndividualSettings() {
</button> </button>
<button <button
type="submit" 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 size={16} className="mr-2" />
Save Worker Save Worker
+153 -102
View File
@@ -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 { MessageSquare, Activity, Terminal, ChevronRight, Plus } from 'lucide-react';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
import type { ChatSession, Message } from '../../App';
interface ChatMessage { interface ChatPanelProps {
id: string; chatSessions: ChatSession[];
sender: 'user' | 'ai'; setChatSessions: React.Dispatch<React.SetStateAction<ChatSession[]>>;
text: string; activeSessionId: string | null;
timestamp: Date; setActiveSessionId: React.Dispatch<React.SetStateAction<string | null>>;
eventId?: string;
} }
export function ChatPanel() { export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId }: ChatPanelProps) {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
sender: 'ai',
text: "Hello! I am Pretor Assistant. How can I help you today?",
timestamp: new Date()
}
]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [loading, setLoading] = useState(false); 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 handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file || !activeSessionId) return;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
@@ -33,26 +49,24 @@ export function ChatPanel() {
setLoading(true); setLoading(true);
try { try {
const response = await apiClient.post('/api/v1/adapter/client/upload', formData, { const response = await apiClient.post('/api/v1/adapter/client/upload', formData, {
headers: { headers: { 'Content-Type': 'multipart/form-data' },
'Content-Type': 'multipart/form-data'
}
}); });
const aiMessage: ChatMessage = { const aiMessage: Message = {
id: Date.now().toString(), id: Date.now().toString(),
sender: 'ai', role: 'assistant',
text: `已上传文件: ${response.data.filename}`, content: `已上传文件: ${response.data.filename}`,
timestamp: new Date() timestamp: Date.now(),
}; };
setMessages(prev => [...prev, aiMessage]); updateSessionMessages([...messages, aiMessage]);
} catch (error) { } catch (error) {
console.error("Error uploading file", error); console.error('Error uploading file', error);
const errorMessage: ChatMessage = { const errorMessage: Message = {
id: Date.now().toString(), id: Date.now().toString(),
sender: 'ai', role: 'assistant',
text: "文件上传失败。", content: '文件上传失败。',
timestamp: new Date() timestamp: Date.now(),
}; };
setMessages(prev => [...prev, errorMessage]); updateSessionMessages([...messages, errorMessage]);
} finally { } finally {
setLoading(false); setLoading(false);
if (fileInputRef.current) { if (fileInputRef.current) {
@@ -62,77 +76,116 @@ export function ChatPanel() {
}; };
const handleSendMessage = async () => { 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(), id: Date.now().toString(),
sender: 'user', role: 'user',
text: input, content: userText,
timestamp: new Date() timestamp: Date.now(),
}; };
setMessages(prev => [...prev, userMessage]); updateSessionMessages([...messages, userMessage]);
setInput(''); setInput('');
setLoading(true); setLoading(true);
try { try {
// Assuming a token might be needed, apiClient should handle it if set
const promptModifier = mode === 'deploy' ? '[DEPLOY TASK] ' : ''; const promptModifier = mode === 'deploy' ? '[DEPLOY TASK] ' : '';
const response = await apiClient.post('/api/v1/adapter/client', { const response = await apiClient.post('/api/v1/adapter/client', {
message: promptModifier + userMessage.text message: promptModifier + userMessage.content,
}); });
const aiMessage: ChatMessage = { const responseData = response.data.message;
id: (Date.now() + 1).toString(), let aiContent = responseData || 'I received your message.';
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()
};
setMessages(prev => [...prev, aiMessage]); // Auto-update title if it's the first user message
if (messages.length <= 1 && userText.length > 0) {
// If we got an event_id, we could potentially open a websocket to listen to its stream setChatSessions((prev) =>
if (aiMessage.eventId) { prev.map((s) =>
console.log(`Open WS to track event: ${aiMessage.eventId}`); s.id === activeSessionId
// Implement WS tracking if needed ? { ...s, title: userText.slice(0, 20) + (userText.length > 20 ? '...' : '') }
: s
)
);
} }
} catch (error) { const aiMessage: Message = {
console.error("Error sending message", error);
const errorMessage: ChatMessage = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
sender: 'ai', role: 'assistant',
text: "Sorry, I encountered an error communicating with the server.", content: aiContent,
timestamp: new Date() 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 { } finally {
setLoading(false); 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 ( return (
<div className="flex-1 flex flex-col bg-slate-50"> <div className="flex-1 flex flex-col bg-white overflow-hidden relative">
<div className="h-14 border-b border-slate-200 bg-white flex items-center justify-between px-6 shadow-sm z-10"> <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"> <div className="flex items-center">
<MessageSquare size={18} className="text-blue-600 mr-3" /> <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>
<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 <button
onClick={() => setMode('chat')} 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 Chat
</button> </button>
<button <button
onClick={() => setMode('deploy')} 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 Deploy Task
</button> </button>
@@ -140,54 +193,52 @@ export function ChatPanel() {
</div> </div>
{/* Chat History */} {/* Chat History */}
<div className="flex-1 p-6 overflow-y-auto space-y-6"> <div className="flex-1 p-6 overflow-y-auto space-y-6 bg-white">
<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>
{messages.map((msg) => ( {messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}> <div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.sender === 'ai' && ( {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"> <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" /> <Activity size={16} className="text-blue-600" />
</div> </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`}> <div
<p className="text-sm leading-relaxed mb-3">{msg.text}</p> className={`${
{msg.eventId && ( msg.role === 'user'
<div className="bg-slate-50 border border-slate-100 rounded-lg p-3 flex items-center text-sm"> ? 'bg-blue-100 text-slate-800 rounded-2xl rounded-tr-sm'
<Terminal size={16} className="text-slate-400 mr-2" /> : 'bg-slate-50 border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.eventId}</span> } p-4 max-w-[80%] shadow-sm`}
</div> >
<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>
</div> </div>
))} ))}
{loading && ( {loading && (
<div className="flex justify-start"> <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"> <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" /> <Activity size={16} className="text-blue-600 animate-spin" />
</div> </div>
<div className="bg-white border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm"> <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="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"></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-75"></span>
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span> <span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
</span> </span>
</div> </div>
</div> </div>
)} )}
<div ref={messagesEndRef} />
</div> </div>
{/* Chat Input */} {/* 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"> <div className="relative flex items-center">
<input <input type="file" ref={fileInputRef} onChange={handleFileUpload} className="hidden" />
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
className="hidden"
/>
<button <button
onClick={() => fileInputRef.current?.click()} 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" 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)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Ask Pretor to do something..." 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 <button
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={loading || !input.trim()} 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} /> <ChevronRight size={18} />
</button> </button>
+119 -94
View File
@@ -1,44 +1,32 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Server, Box, Cpu, HardDrive, List, MessageCircle } from 'lucide-react'; import { Plus, Trash2 } from 'lucide-react';
import { useClusterState } from '../../hooks/useClusterState';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
import type { Workflow } from '../../types'; import type { Workflow } from '../../types';
import type { ChatSession } from '../../App';
interface LeftPanelProps { interface LeftPanelProps {
activeTab: string; activeTab: string;
setActiveTab: (tab: string) => void;
selectedWorkflow: string | null; selectedWorkflow: string | null;
setSelectedWorkflow: (id: string | null) => void; 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) { export function LeftPanel({
const { nodes } = useClusterState(); activeTab,
selectedWorkflow,
setSelectedWorkflow,
chatSessions,
setChatSessions,
activeSessionId,
setActiveSessionId,
}: LeftPanelProps) {
const [workflows, setWorkflows] = useState<Workflow[]>([]); const [workflows, setWorkflows] = useState<Workflow[]>([]);
const [loadingWorkflows, setLoadingWorkflows] = useState(false); 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(() => { useEffect(() => {
let intervalId: ReturnType<typeof setInterval>; let intervalId: ReturnType<typeof setInterval>;
@@ -46,18 +34,16 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
if (isInitial) setLoadingWorkflows(true); if (isInitial) setLoadingWorkflows(true);
try { try {
const response = await apiClient.get('/api/v1/workflow/list'); const response = await apiClient.get('/api/v1/workflow/list');
// Fallback parsing just in case it returns an object or array
const data = response.data; const data = response.data;
let parsedWorkflows: Workflow[] = []; let parsedWorkflows: Workflow[] = [];
if (Array.isArray(data)) { if (Array.isArray(data)) {
parsedWorkflows = data; parsedWorkflows = data;
} else if (data && typeof data === 'object') { } else if (data && typeof data === 'object') {
// Suppose backend sends { workflows: [...] }
parsedWorkflows = data.workflows || Object.values(data); parsedWorkflows = data.workflows || Object.values(data);
} }
setWorkflows(parsedWorkflows); setWorkflows(parsedWorkflows);
} catch (error) { } catch (error) {
console.error("Failed to fetch workflows", error); console.error('Failed to fetch workflows', error);
setWorkflows([]); setWorkflows([]);
} finally { } finally {
if (isInitial) setLoadingWorkflows(false); if (isInitial) setLoadingWorkflows(false);
@@ -74,69 +60,56 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
}; };
}, [activeTab]); }, [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 ( return (
<div className="w-72 bg-white border-r border-slate-200 flex flex-col z-0 shrink-0"> <div className="w-72 bg-white border-r border-slate-100 flex flex-col z-0 shrink-0">
{/* Top: Cluster Status */} {/* Bottom: Tab Selection */}
<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="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<div className="flex border-b border-slate-100">
<button <div className="flex items-center justify-between p-3 border-b border-slate-100 bg-slate-50">
onClick={() => setActiveTab('chats')} <span className="text-sm font-semibold text-slate-600 uppercase tracking-wider">
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'}`} {activeTab === 'chats' ? 'Chat History' : 'Workflows'}
> </span>
<MessageCircle size={14} className="inline mr-1.5 -mt-0.5" /> {activeTab === 'chats' && (
Chats <button
</button> onClick={handleNewChat}
<button className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
onClick={() => setActiveTab('workflows')} title="New Chat"
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'}`} >
> <Plus size={16} />
<List size={14} className="inline mr-1.5 -mt-0.5" /> </button>
Workflows )}
</button>
</div> </div>
<div className="flex-1 p-4 overflow-y-auto"> <div className="flex-1 p-3 overflow-y-auto">
{activeTab === 'workflows' && ( {activeTab === 'workflows' && (
<div className="space-y-2"> <div className="space-y-2">
{loadingWorkflows ? ( {loadingWorkflows ? (
@@ -148,11 +121,29 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
<div <div
key={wf.event_id} key={wf.event_id}
onClick={() => setSelectedWorkflow(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"> <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
<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> 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> </div>
<p className="text-xs text-slate-500 font-mono line-clamp-1">ID: {wf.event_id}</p> <p className="text-xs text-slate-500 font-mono line-clamp-1">ID: {wf.event_id}</p>
</div> </div>
@@ -160,8 +151,42 @@ export function LeftPanel({ activeTab, setActiveTab, selectedWorkflow, setSelect
)} )}
</div> </div>
)} )}
{activeTab === 'chats' && ( {activeTab === 'chats' && chatSessions && (
<div className="space-y-2"> <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>
)} )}
</div> </div>
+117 -130
View File
@@ -1,35 +1,25 @@
import { useState, useEffect, useRef } from 'react'; 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 apiClient from '../../api/client';
import type { WorkflowDetail, WorkflowStep } from '../../types'; import type { WorkflowDetail } from '../../types';
import { WorkflowDiagram } from './WorkflowDiagram';
interface RightPanelProps { interface RightPanelProps {
selectedWorkflow: string | null; 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) { export function RightPanel({ selectedWorkflow }: RightPanelProps) {
const [detail, setDetail] = useState<WorkflowDetail | null>(null); const [detail, setDetail] = useState<WorkflowDetail | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState<string[]>([]); const [logs, setLogs] = useState<string[]>([]);
const [sseConnected, setSseConnected] = useState(false); const [sseConnected, setSseConnected] = useState(false);
const [replyText, setReplyText] = useState('');
const [activeTab, setActiveTab] = useState<'chat' | 'diagram'>('chat');
const eventSourceRef = useRef<EventSource | null>(null); const eventSourceRef = useRef<EventSource | null>(null);
const logsEndRef = useRef<HTMLDivElement>(null);
const fetchDetail = async (traceId: string) => { const fetchDetail = async (traceId: string) => {
setLoading(true); setLoading(true);
setLogs([]);
try { try {
const response = await apiClient.get(`/api/v1/workflow/${traceId}`); const response = await apiClient.get(`/api/v1/workflow/${traceId}`);
setDetail(response.data); setDetail(response.data);
@@ -48,6 +38,7 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
} }
fetchDetail(selectedWorkflow); fetchDetail(selectedWorkflow);
setLogs([]); // Reset logs when changing workflow
const protocol = window.location.protocol; const protocol = window.location.protocol;
const host = window.location.host; const host = window.location.host;
@@ -55,17 +46,13 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`); const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`);
eventSourceRef.current = es; eventSourceRef.current = es;
es.onopen = () => { es.onopen = () => setSseConnected(true);
setSseConnected(true);
};
es.onmessage = (event) => { es.onmessage = (event) => {
setLogs(prev => [...prev, event.data]); setLogs(prev => [...prev, event.data]);
}; };
es.onerror = () => { es.onerror = () => setSseConnected(false);
setSseConnected(false);
};
const interval = setInterval(() => { const interval = setInterval(() => {
fetchDetail(selectedWorkflow); fetchDetail(selectedWorkflow);
@@ -78,127 +65,127 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
}; };
}, [selectedWorkflow]); }, [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) { const handleReplySubmit = async (e: React.FormEvent) => {
return ( e.preventDefault();
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0 justify-center items-center p-6 text-center"> if (!replyText.trim() || !selectedWorkflow) return;
<Activity size={32} className="text-slate-300 mb-4" />
<h3 className="text-sm font-semibold text-slate-600">No Workflow Selected</h3> const message = replyText.trim();
<p className="text-xs text-slate-400 mt-2">Select a workflow from the left panel to view its details.</p> setReplyText('');
</div> 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 ( return (
<div className="w-80 bg-white border-l border-slate-200 flex flex-col z-0"> <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-4 justify-between bg-slate-50/50"> <div className="h-14 border-b border-slate-100 flex items-center px-6 justify-between bg-white z-10 shrink-0">
<h2 className="font-semibold text-slate-800 text-sm flex items-center"> <div className="flex items-center gap-4">
<Terminal size={16} className="mr-2 text-slate-500" /> <h2 className="font-semibold text-slate-800 flex items-center gap-2">
Workflow Detail <Terminal size={18} className="text-blue-500" />
</h2> <span className="truncate max-w-[200px]" title={detail?.workflow_title || 'Loading...'}>
<div className="flex items-center gap-2"> {detail?.workflow_title || 'Workflow Details'}
<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'}`}> </span>
{sseConnected ? 'Live' : '--'} </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> </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> </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>
<div className="flex-1 p-4 overflow-y-auto"> <div className="flex-1 flex overflow-hidden bg-slate-50 relative">
{loading && !detail ? ( {activeTab === 'diagram' ? (
<div className="text-center text-slate-400 text-sm py-8"> <div className="absolute inset-0">
<Loader2 size={24} className="animate-spin mx-auto mb-2" /> {detail?.steps && detail.steps.length > 0 ? (
Loading... <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> </div>
) : !detail ? (
<div className="text-center text-slate-400 text-sm py-8">Failed to load workflow details</div>
) : ( ) : (
<> <div className="flex-1 flex flex-col p-6 overflow-hidden">
{/* Header */} {/* Command Header */}
<div className="mb-4"> {detail?.command && (
<h3 className="text-base font-bold text-slate-800"> <div className="bg-white border border-slate-200 rounded-xl p-4 mb-4 shadow-sm shrink-0">
{detail.workflow_title || 'Workflow'} <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Original Command</h3>
</h3> <p className="text-slate-700 text-sm">{detail.command}</p>
<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> </div>
</div> )}
)}
{detail.steps.length === 0 && ( {/* Live Chat / Logs Area */}
<div className="text-center py-4"> <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">
<Clock size={24} className="text-slate-300 mx-auto mb-2" /> {logs.length === 0 ? (
<p className="text-xs text-slate-400">Workflow is being generated...</p> <div className="h-full flex items-center justify-center text-slate-400">
</div> Waiting for events...
)} </div>
) : (
{/* SSE Logs */} logs.map((log, index) => (
{logs.length > 0 && ( <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'}`}>
<div> {log}
<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>
</div> </div>
))} ))
</div> )}
</div> <div ref={logsEndRef} />
)} </div>
{logs.length === 0 && sseConnected && isActive && ( {/* Input Area */}
<div className="text-xs text-slate-400 italic mt-2">Waiting for live events...</div> <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>
</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>
);
}
+59
View File
@@ -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>
);
}
@@ -79,7 +79,7 @@ export function SkillSettings() {
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"> <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="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} /> <Download size={20} />
</div> </div>
<div> <div>
@@ -98,7 +98,7 @@ export function SkillSettings() {
value={repoUrl} value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)} onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/user/repo" 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>
<div> <div>
@@ -108,7 +108,7 @@ export function SkillSettings() {
value={path} value={path}
onChange={(e) => setPath(e.target.value)} onChange={(e) => setPath(e.target.value)}
placeholder="e.g. subfolder/path" 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>
</div> </div>
@@ -120,7 +120,7 @@ export function SkillSettings() {
<button <button
type="submit" type="submit"
disabled={installing} 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" /> <Plus size={16} className="mr-2" />
{installing ? 'Installing...' : 'Install'} {installing ? 'Installing...' : 'Install'}
@@ -88,7 +88,7 @@ export function WorkflowTemplateSettings() {
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"> <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="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} /> <FileCode size={20} />
</div> </div>
<div> <div>
@@ -105,7 +105,7 @@ export function WorkflowTemplateSettings() {
rows={8} rows={8}
value={templateJson} value={templateJson}
onChange={(e) => setTemplateJson(e.target.value)} 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> </div>
@@ -116,7 +116,7 @@ export function WorkflowTemplateSettings() {
<button <button
type="submit" type="submit"
disabled={creating} 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" /> <Plus size={16} className="mr-2" />
{creating ? 'Creating...' : 'Create Template'} {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>
);
}
+2
View File
@@ -65,6 +65,8 @@ export interface Workflow {
event_id: string; event_id: string;
workflow_title: string; workflow_title: string;
status?: string; status?: string;
message?: string;
create_time?: string;
} }
export interface WorkflowStep { export interface WorkflowStep {
+23 -3
View File
@@ -4,6 +4,7 @@ from pretor.worker_individual.worker_cluster import WorkerCluster
from pretor.utils.banner import print_banner from pretor.utils.banner import print_banner
from pretor.core.database.postgres import PostgresDatabase 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_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.supervisory_node.supervisory_node import SupervisoryNode
from pretor.core.individual.consciousness_node.consciousness_node import ConsciousnessNode from pretor.core.individual.consciousness_node.consciousness_node import ConsciousnessNode
from pretor.core.individual.control_node.control_node import ControlNode 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 pretor.core.api import PretorGateway
from ray import serve from ray import serve
import os 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(): async def start_system():
env_vars = { env_vars = {
@@ -21,7 +27,7 @@ async def start_system():
"POSTGRES_HOST": os.getenv("POSTGRES_HOST", "db"), "POSTGRES_HOST": os.getenv("POSTGRES_HOST", "db"),
"POSTGRES_PORT": os.getenv("POSTGRES_PORT", "5432"), "POSTGRES_PORT": os.getenv("POSTGRES_PORT", "5432"),
"POSTGRES_DB": os.getenv("POSTGRES_DB", "postgres"), "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, ray.init(ignore_reinit_error=True,
@@ -51,13 +57,19 @@ async def start_system():
print(f"\n[致命错误] GlobalStateMachine 启动失败!真实报错如下:\n{e}\n") print(f"\n[致命错误] GlobalStateMachine 启动失败!真实报错如下:\n{e}\n")
return return
global_workflow_manager = GlobalWorkflowManager.options(
name='global_workflow_manager',
namespace='pretor',
lifetime='detached'
).remote()
# 4. 启动核心节点 # 4. 启动核心节点
supervisory_node = SupervisoryNode.options(name='supervisory_node').remote() supervisory_node = SupervisoryNode.options(name='supervisory_node').remote()
consciousness_node = ConsciousnessNode.options(name='consciousness_node').remote() consciousness_node = ConsciousnessNode.options(name='consciousness_node').remote()
control_node = ControlNode.options(name='control_node').remote() control_node = ControlNode.options(name='control_node').remote()
try: try:
worker_cluster_actor = WorkerCluster.options( WorkerCluster.options(
name="worker_cluster", name="worker_cluster",
lifetime="detached" # 保证它在后台一直运行 lifetime="detached" # 保证它在后台一直运行
).remote() ).remote()
@@ -73,6 +85,14 @@ async def start_system():
# 异步拉起 runner 协程群 # 异步拉起 runner 协程群
workflow_engine.run.remote() 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) # 6. 启动 FastAPI 网关 (使用 Ray Serve)
serve.start(http_options={"host": "0.0.0.0", "port": 8000}) serve.start(http_options={"host": "0.0.0.0", "port": 8000})
serve.run(PretorGateway.bind()) serve.run(PretorGateway.bind())
@@ -23,6 +23,8 @@ from pretor.utils.agent_model import ResponseModel, DepsModel
from pretor.utils.error import ModelNotExistError from pretor.utils.error import ModelNotExistError
class AgentFactory: class AgentFactory:
"""AgentFactory 核心组件类。
这是一个领域数据模型或功能封装类,承载了 AgentFactory 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
def __init__(self): def __init__(self):
self._models_mapping = {"openai": (OpenAIChatModel, OpenAIProvider), self._models_mapping = {"openai": (OpenAIChatModel, OpenAIProvider),
"claude": (AnthropicModel, AnthropicProvider), "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 re
import json import json
from typing import Type, TypeVar, Any, Generic from typing import Type, TypeVar, Any, Generic
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from pydantic_ai import Agent, RunContext from pydantic_ai import Agent
from pydantic_ai.run import AgentRunResult
T = TypeVar('T', bound=BaseModel) T = TypeVar('T', bound=BaseModel)
class AgentRunResultProxy: class AgentRunResultProxy:
"""AgentRunResultProxy 核心组件类。
这是一个领域数据模型或功能封装类,承载了 AgentRunResultProxy 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
def __init__(self, original, parsed): def __init__(self, original, parsed):
self._original = original self._original = original
self._parsed = parsed self._parsed = parsed
def __getattr__(self, name): def __getattr__(self, name):
"""检索并获取特定的 getattr 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: name: 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
if name == 'data': if name == 'data':
return self._parsed return self._parsed
if name == 'output': if name == 'output':
@@ -78,6 +97,10 @@ class DeepSeekReasonerAgent(Generic[T]):
) )
def _parse_output(self, text: str) -> Any: def _parse_output(self, text: str) -> Any:
"""执行与 parse output 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: text (str): 控制逻辑流向的具体字符串参数,指定了期望的 text 内容。
Returns: (Any): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
if not self.has_custom_output: if not self.has_custom_output:
return text return text
@@ -114,10 +137,18 @@ class DeepSeekReasonerAgent(Generic[T]):
def __getattr__(self, item): def __getattr__(self, item):
# Delegate any unknown attributes (like .system_prompt, .tool) to the underlying pydantic_ai Agent # Delegate any unknown attributes (like .system_prompt, .tool) to the underlying pydantic_ai Agent
"""检索并获取特定的 getattr 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: item: 参与 getattr 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return getattr(self.agent, item) return getattr(self.agent, item)
async def run(self, user_prompt: str, deps: Any = None, message_history: list = None, **kwargs) -> Any: async def run(self, user_prompt: str, deps: Any = None, message_history: list = None, **kwargs) -> Any:
# Custom retry loop # 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 [] current_history = message_history or []
last_exception = None last_exception = None
+40
View File
@@ -27,18 +27,26 @@ from pretor.core.database.table.user import UserAuthority
agent_router = APIRouter(prefix="/api/v1/agent", tags=["agent"]) agent_router = APIRouter(prefix="/api/v1/agent", tags=["agent"])
class AgentRegister(BaseModel): class AgentRegister(BaseModel):
"""AgentRegister 核心组件类。
这是一个领域数据模型或功能封装类,承载了 AgentRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
provider_title: str provider_title: str
model_id: str model_id: str
individual_name: str individual_name: str
tools: Optional[List[str]] = None tools: Optional[List[str]] = None
class AgentLocalRegister(BaseModel): class AgentLocalRegister(BaseModel):
"""AgentLocalRegister 核心组件类。
这是一个领域数据模型或功能封装类,承载了 AgentLocalRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
path: str path: str
individual_name: str individual_name: str
tools: Optional[List[str]] = None tools: Optional[List[str]] = None
@agent_router.get("") @agent_router.get("")
async def get_system_nodes(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
configs = await postgres_database.get_all_system_node_configs.remote() configs = await postgres_database.get_all_system_node_configs.remote()
return {"system_nodes": configs} return {"system_nodes": configs}
@@ -46,6 +54,10 @@ async def get_system_nodes(_: TokenData = Depends(RoleChecker(allowed_roles=User
@agent_router.post("") @agent_router.post("")
async def load_agent(agent_register: Union[AgentRegister, AgentLocalRegister], async def load_agent(agent_register: Union[AgentRegister, AgentLocalRegister],
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): _: 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
postgres_database = ray_actor_hook("postgres_database").postgres_database 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): class WorkerIndividualCreate(BaseModel):
"""WorkerIndividualCreate 核心组件类。
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
agent_name: str agent_name: str
agent_type: AgentType agent_type: AgentType
description: str description: str
@@ -94,6 +108,8 @@ class WorkerIndividualCreate(BaseModel):
class WorkerIndividualUpdate(BaseModel): class WorkerIndividualUpdate(BaseModel):
"""WorkerIndividualUpdate 核心组件类。
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
agent_name: Optional[str] = None agent_name: Optional[str] = None
agent_type: Optional[AgentType] = None agent_type: Optional[AgentType] = None
description: Optional[str] = None description: Optional[str] = None
@@ -109,6 +125,10 @@ class WorkerIndividualUpdate(BaseModel):
@agent_router.post("/worker") @agent_router.post("/worker")
async def create_worker_individual(worker_data: WorkerIndividualCreate, async def create_worker_individual(worker_data: WorkerIndividualCreate,
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
data_dict = worker_data.model_dump() data_dict = worker_data.model_dump()
data_dict["owner_id"] = token_data.user_id data_dict["owner_id"] = token_data.user_id
@@ -118,6 +138,10 @@ async def create_worker_individual(worker_data: WorkerIndividualCreate,
@agent_router.get("/worker") @agent_router.get("/worker")
async def get_worker_individual_list(token_data: TokenData = Depends(Accessor.get_current_user)): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
workers = await postgres_database.get_worker_individual_list.remote( owner_id=token_data.user_id) workers = await postgres_database.get_worker_individual_list.remote( owner_id=token_data.user_id)
return {"workers": workers} 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}") @agent_router.get("/worker/{agent_id}")
async def get_worker_individual(agent_id: str, async def get_worker_individual(agent_id: str,
token_data: TokenData = Depends(Accessor.get_current_user)): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id) worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id)
if not worker: if not worker:
@@ -139,6 +167,10 @@ async def get_worker_individual(agent_id: str,
async def update_worker_individual(agent_id: str, async def update_worker_individual(agent_id: str,
worker_data: WorkerIndividualUpdate, worker_data: WorkerIndividualUpdate,
token_data: TokenData = Depends(Accessor.get_current_user)): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id) worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id)
if not worker: if not worker:
@@ -159,6 +191,10 @@ async def update_worker_individual(agent_id: str,
@agent_router.post("/worker/{agent_id}/reload") @agent_router.post("/worker/{agent_id}/reload")
async def reload_worker_individual(agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id) worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
if not worker: 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}") @agent_router.delete("/worker/{agent_id}")
async def delete_worker_individual(agent_id: str, async def delete_worker_individual(agent_id: str,
token_data: TokenData = Depends(Accessor.get_current_user)): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id) worker = await postgres_database.get_worker_individual.remote( agent_id=agent_id)
if not worker: if not worker:
+14
View File
@@ -25,22 +25,34 @@ from pretor.utils.error import UserNotExistError
auth_router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) auth_router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
class UserRegister(BaseModel): class UserRegister(BaseModel):
"""UserRegister 核心组件类。
这是一个领域数据模型或功能封装类,承载了 UserRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
user_name: str user_name: str
password: str password: str
@auth_router.post("/register") @auth_router.post("/register")
async def create_user(user_register: UserRegister): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
hashed_password = await run_in_threadpool(Accessor.hash_password, user_register.password) 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) user = await postgres_database.add_user.remote( user_register.user_name, hashed_password)
return {"message": "success", "user_id": user.user_id} return {"message": "success", "user_id": user.user_id}
class UserLogin(BaseModel): class UserLogin(BaseModel):
"""UserLogin 核心组件类。
这是一个领域数据模型或功能封装类,承载了 UserLogin 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
user_name: str user_name: str
password: str password: str
@auth_router.post("/login") @auth_router.post("/login")
async def login_user(user_login: UserLogin): 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 postgres_database = ray_actor_hook("postgres_database").postgres_database
user = await postgres_database.login_user.remote( user_login.user_name) user = await postgres_database.login_user.remote( user_login.user_name)
if not user: if not user:
@@ -49,6 +61,8 @@ async def login_user(user_login: UserLogin):
return {"message":"success", "token":token} return {"message":"success", "token":token}
class ChangeAuthorityRequest(BaseModel): class ChangeAuthorityRequest(BaseModel):
"""ChangeAuthorityRequest 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ChangeAuthorityRequest 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
user_id: str user_id: str
new_authority: UserAuthority new_authority: UserAuthority
+2
View File
@@ -20,6 +20,8 @@ from pretor.core.workflow.workflow import PretorWorkflow
import asyncio import asyncio
class PretorEvent(BaseModel): class PretorEvent(BaseModel):
"""PretorEvent 核心组件类。
这是一个领域数据模型或功能封装类,承载了 PretorEvent 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
model_config = ConfigDict(arbitrary_types_allowed=True) model_config = ConfigDict(arbitrary_types_allowed=True)
trace_id: str = Field(default_factory=lambda: str(ULID()), description="事件的唯一标识符") trace_id: str = Field(default_factory=lambda: str(ULID()), description="事件的唯一标识符")
platform: str = Field(description="消息来源的平台") platform: str = Field(description="消息来源的平台")
+18 -6
View File
@@ -18,18 +18,25 @@ from pretor.utils.access import Accessor, TokenData
from pretor.api.platform.event import PretorEvent from pretor.api.platform.event import PretorEvent
from pretor.utils.ray_hook import ray_actor_hook from pretor.utils.ray_hook import ray_actor_hook
import os import os
import shutil import anyio
from pretor.utils.logger import get_logger from pretor.utils.logger import get_logger
logger = get_logger('frontend') logger = get_logger('frontend')
client_router = APIRouter(prefix="/api/v1/adapter/client", tags=["client"]) client_router = APIRouter(prefix="/api/v1/adapter/client", tags=["client"])
class Message(BaseModel): class Message(BaseModel):
"""Message 核心组件类。
这是一个领域数据模型或功能封装类,承载了 Message 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
message: str message: str
@client_router.post("") @client_router.post("")
async def create_message(message: Message, async def create_message(message: Message,
token_data: TokenData = Depends(Accessor.get_current_user)): 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.info("收到消息,来源:客户端")
logger.debug(f"消息内容:{message.message}") logger.debug(f"消息内容:{message.message}")
event = PretorEvent(platform="client", event = PretorEvent(platform="client",
@@ -38,8 +45,8 @@ async def create_message(message: Message,
message=message.message) message=message.message)
supervisory_node = ray_actor_hook("supervisory_node").supervisory_node supervisory_node = ray_actor_hook("supervisory_node").supervisory_node
message = await supervisory_node.working.remote(event) message = await supervisory_node.working.remote(event)
if message == "任务已创建": if message.startswith("任务已创建"):
return {"message": event.trace_id} return {"message": f"{event.trace_id}\n\n{message}"}
elif message == "未知相应类型": elif message == "未知相应类型":
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -50,12 +57,17 @@ async def create_message(message: Message,
@client_router.post("/upload") @client_router.post("/upload")
async def upload_file(file: UploadFile = File(...), async def upload_file(file: UploadFile = File(...),
token_data: TokenData = Depends(Accessor.get_current_user)): token_data: TokenData = Depends(Accessor.get_current_user)):
"""处理针对 upload file 相关的 HTTP API 请求。
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
Args: file (UploadFile): 参与 upload file 逻辑运算或数据构建的上下文依赖对象。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。 """
try: try:
upload_dir = "uploads" upload_dir = "uploads"
os.makedirs(upload_dir, exist_ok=True) os.makedirs(upload_dir, exist_ok=True)
file_path = os.path.join(upload_dir, file.filename) file_path = os.path.join(upload_dir, file.filename)
with open(file_path, "wb") as buffer: async with await anyio.open_file(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer) while chunk := await file.read(64 * 1024): # 64KB chunks
await buffer.write(chunk)
logger.info(f"用户 {token_data.username} 上传了文件: {file.filename}") logger.info(f"用户 {token_data.username} 上传了文件: {file.filename}")
return {"filename": file.filename, "message": f"File {file.filename} uploaded successfully"} return {"filename": file.filename, "message": f"File {file.filename} uploaded successfully"}
except Exception as e: except Exception as e:
+14
View File
@@ -25,6 +25,8 @@ from pretor.utils.ray_hook import ray_actor_hook
provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"]) provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
class ProviderRegister(BaseModel): class ProviderRegister(BaseModel):
"""ProviderRegister 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
provider_type: Literal["openai", "claude", "deepseek"] provider_type: Literal["openai", "claude", "deepseek"]
provider_title: str provider_title: str
provider_url: str provider_url: str
@@ -33,6 +35,10 @@ class ProviderRegister(BaseModel):
@provider_router.post("") @provider_router.post("")
async def create_provider(provider_register: ProviderRegister, async def create_provider(provider_register: ProviderRegister,
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))) -> None: 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 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, await global_state_machine.add_provider_wrap.remote(provider_type=provider_register.provider_type,
provider_title=provider_register.provider_title, provider_title=provider_register.provider_title,
@@ -43,12 +49,20 @@ async def create_provider(provider_register: ProviderRegister,
@provider_router.get("/list") @provider_router.get("/list")
async def get_provider_list(_: TokenData = Depends(Accessor.get_current_user)) -> Dict[str, Dict[str, Provider]]: 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 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() provider_list: Dict[str, Provider] = await global_state_machine.get_provider_list.remote()
return {"provider_list": provider_list} return {"provider_list": provider_list}
@provider_router.delete("/{provider_title}") @provider_router.delete("/{provider_title}")
async def delete_provider(provider_title: str, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR))) -> dict: 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
await global_state_machine.delete_provider.remote(provider_title=provider_title) await global_state_machine.delete_provider.remote(provider_title=provider_title)
return {"message": "success"} return {"message": "success"}
+30
View File
@@ -26,18 +26,30 @@ resource_router = APIRouter(prefix="/api/v1/resource")
@resource_router.post("/workflow_template") @resource_router.post("/workflow_template")
async def create_workflow_template(workflow_template: WorkflowTemplate, async def create_workflow_template(workflow_template: WorkflowTemplate,
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): _: 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 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) await global_state_machine.add_workflow_template.remote( workflow_template.name, workflow_template)
return {"message": "创建成功"} return {"message": "创建成功"}
@resource_router.get("/workflow_template") @resource_router.get("/workflow_template")
async def get_workflow_templates(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
templates = await global_state_machine.get_all_workflow_templates.remote() templates = await global_state_machine.get_all_workflow_templates.remote()
return {"templates": templates} return {"templates": templates}
@resource_router.delete("/workflow_template/{template_name}") @resource_router.delete("/workflow_template/{template_name}")
async def delete_workflow_template(template_name: str, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR))): 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
await global_state_machine.delete_workflow_template.remote( template_name) await global_state_machine.delete_workflow_template.remote( template_name)
return {"message": "success"} return {"message": "success"}
@@ -45,12 +57,18 @@ async def delete_workflow_template(template_name: str, _: TokenData = Depends(Ro
class Skill(BaseModel): class Skill(BaseModel):
"""Skill 核心组件类。
这是一个领域数据模型或功能封装类,承载了 Skill 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
repo_url: str repo_url: str
path: str | None path: str | None
@resource_router.post("/skill") @resource_router.post("/skill")
async def install_skill(skill: Skill, async def install_skill(skill: Skill,
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): _: 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import os import os
@@ -68,12 +86,20 @@ async def install_skill(skill: Skill,
@resource_router.get("/skill") @resource_router.get("/skill")
async def get_skills(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
skills = await global_state_machine.get_skill_list.remote() skills = await global_state_machine.get_skill_list.remote()
return {"skills": skills} return {"skills": skills}
@resource_router.delete("/skill/{skill_name}") @resource_router.delete("/skill/{skill_name}")
async def delete_skill(skill_name: str, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR))): 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
# Note: this only removes it from the state machine manager. # Note: this only removes it from the state machine manager.
await global_state_machine.remove_skill.remote( skill_name) 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") @resource_router.get("/tool")
async def get_tools(_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))): 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 global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
tool_mapper = await global_state_machine.get_tool_mapper.remote() tool_mapper = await global_state_machine.get_tool_mapper.remote()
all_tool_names = set() all_tool_names = set()
+25 -8
View File
@@ -22,15 +22,22 @@ workflow_router = APIRouter(prefix="/api/v1/workflow", tags=["workflow"])
@workflow_router.get("/list") @workflow_router.get("/list")
async def get_workflow_list(): async def get_workflow_list():
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine """处理针对 get workflow list 相关的 HTTP API 请求。
events = await global_state_machine.list_events.remote() 该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 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 return events
@workflow_router.get("/{trace_id}") @workflow_router.get("/{trace_id}")
async def get_workflow_detail(trace_id: str): async def get_workflow_detail(trace_id: str):
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine """处理针对 get workflow detail 相关的 HTTP API 请求。
event = await global_state_machine.get_event.remote(trace_id) 该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 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: if not event:
raise HTTPException(status_code=404, detail="Workflow not found") 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}") @workflow_router.get("/sse/{trace_id}")
async def get_workflow_sse(trace_id: str, request: Request): 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(): async def event_generator():
"""执行与 event generator 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。 """
try: try:
while True: while True:
if await request.is_disconnected(): 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 # You might also want to send the workflow state periodically or when updated
# Here we just wait for pending messages and send them # 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 # Ensure the message is formatted as SSE
yield f"data: {message}\n\n" yield f"data: {message}\n\n"
except asyncio.CancelledError: except asyncio.CancelledError:
@@ -91,9 +104,13 @@ async def get_workflow_sse(trace_id: str, request: Request):
@workflow_router.post("/reply/{trace_id}") @workflow_router.post("/reply/{trace_id}")
async def post_workflow_reply(trace_id: str, request: Request): 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() data = await request.json()
reply_msg = data.get("message", "") reply_msg = data.get("message", "")
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
await global_state_machine.put_received.remote(trace_id, reply_msg) await global_workflow_manager.put_received.remote(trace_id, reply_msg)
return {"status": "ok"} return {"status": "ok"}
+2
View File
@@ -24,6 +24,7 @@ from pretor.api.provider import provider_router
from pretor.api.resource import resource_router from pretor.api.resource import resource_router
from pretor.api.cluster import cluster_router from pretor.api.cluster import cluster_router
from pretor.api.agent import agent_router from pretor.api.agent import agent_router
from pretor.api.workflow import workflow_router
from pretor.utils.error import ( from pretor.utils.error import (
DemandError, ModelNotExistError, UserError, DemandError, ModelNotExistError, UserError,
UserNotExistError, UserPasswordError, ProviderError, UserNotExistError, UserPasswordError, ProviderError,
@@ -40,6 +41,7 @@ app.include_router(provider_router) # 供应商路径
app.include_router(resource_router) # 资源路径 app.include_router(resource_router) # 资源路径
app.include_router(cluster_router) # 集群信息路径 app.include_router(cluster_router) # 集群信息路径
app.include_router(agent_router) # agent路径 app.include_router(agent_router) # agent路径
app.include_router(workflow_router) # workflow路径
@app.exception_handler(UserNotExistError) @app.exception_handler(UserNotExistError)
async def user_not_exist_handler(request: Request, exc: 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 from pretor.utils.logger import get_logger
logger = get_logger('database_exception') logger = get_logger('database_exception')
def database_exception(func): def database_exception(func):
"""执行与 database exception 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: func: 参与 database exception 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
"""执行与 wrapper 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
try: try:
return await func(*args, **kwargs) return await func(*args, **kwargs)
except ValidationError as e: except ValidationError as e:
+45
View File
@@ -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
+24
View File
@@ -20,11 +20,16 @@ from pretor.core.database.database_exception import database_exception
from ulid import ULID from ulid import ULID
class IndividualDatabase: class IndividualDatabase:
"""IndividualDatabase 核心组件类。
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。 """
def __init__(self, async_session_maker): def __init__(self, async_session_maker):
self.async_session_maker = async_session_maker self.async_session_maker = async_session_maker
@database_exception @database_exception
async def add_worker_individual(self, **kwargs) -> WorkerIndividual: async def add_worker_individual(self, **kwargs) -> WorkerIndividual:
"""创建并持久化新的 worker individual 实体。
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
Returns: (WorkerIndividual): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
agent_id = str(ULID()) agent_id = str(ULID())
individual = WorkerIndividual(agent_id=agent_id, **kwargs) individual = WorkerIndividual(agent_id=agent_id, **kwargs)
@@ -35,6 +40,10 @@ class IndividualDatabase:
@database_exception @database_exception
async def get_worker_individual(self, agent_id: str) -> Optional[WorkerIndividual]: 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: async with self.async_session_maker() as session:
statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id) statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id)
results = await session.execute(statement) results = await session.execute(statement)
@@ -42,6 +51,10 @@ class IndividualDatabase:
@database_exception @database_exception
async def get_worker_individual_list(self, owner_id: str) -> List[WorkerIndividual]: 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: async with self.async_session_maker() as session:
statement = select(WorkerIndividual).where(WorkerIndividual.owner_id == owner_id) statement = select(WorkerIndividual).where(WorkerIndividual.owner_id == owner_id)
results = await session.execute(statement) results = await session.execute(statement)
@@ -49,6 +62,10 @@ class IndividualDatabase:
@database_exception @database_exception
async def update_worker_individual(self, agent_id: str, **kwargs) -> Optional[WorkerIndividual]: 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: async with self.async_session_maker() as session:
statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id) statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id)
results = await session.execute(statement) results = await session.execute(statement)
@@ -65,6 +82,10 @@ class IndividualDatabase:
@database_exception @database_exception
async def delete_worker_individual(self, agent_id: str) -> bool: 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: async with self.async_session_maker() as session:
statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id) statement = select(WorkerIndividual).where(WorkerIndividual.agent_id == agent_id)
results = await session.execute(statement) results = await session.execute(statement)
@@ -77,6 +98,9 @@ class IndividualDatabase:
@database_exception @database_exception
async def get_all_worker_individual(self) -> List[WorkerIndividual]: async def get_all_worker_individual(self) -> List[WorkerIndividual]:
"""检索并获取特定的 all worker individual 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: (List[WorkerIndividual]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(WorkerIndividual) statement = select(WorkerIndividual)
results = await session.execute(statement) results = await session.execute(statement)
-68
View File
@@ -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()
+16
View File
@@ -19,11 +19,16 @@ from sqlmodel import select
from pretor.core.database.database_exception import database_exception from pretor.core.database.database_exception import database_exception
class ProviderDatabase: class ProviderDatabase:
"""ProviderDatabase 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
def __init__(self, async_session_maker): def __init__(self, async_session_maker):
self.async_session_maker = async_session_maker self.async_session_maker = async_session_maker
@database_exception @database_exception
async def get_provider(self) -> List[Provider]: async def get_provider(self) -> List[Provider]:
"""检索并获取特定的 provider 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: (List[Provider]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(Provider) statement = select(Provider)
results = await session.execute(statement) results = await session.execute(statement)
@@ -37,6 +42,9 @@ class ProviderDatabase:
@database_exception @database_exception
async def add_provider(self, **kwargs) -> None: async def add_provider(self, **kwargs) -> None:
"""创建并持久化新的 provider 实体。
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
provider = Provider(**kwargs) provider = Provider(**kwargs)
session.add(provider) session.add(provider)
@@ -44,6 +52,10 @@ class ProviderDatabase:
@database_exception @database_exception
async def delete_provider(self, provider_id: str) -> None: 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: async with self.async_session_maker() as session:
provider = await session.get(Provider, provider_id) provider = await session.get(Provider, provider_id)
if provider is not None: if provider is not None:
@@ -52,6 +64,10 @@ class ProviderDatabase:
@database_exception @database_exception
async def update_provider(self, provider_id: str, **kwargs) -> Provider: 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: async with self.async_session_maker() as session:
provider = await session.get(Provider, provider_id) provider = await session.get(Provider, provider_id)
if provider is not None: if provider is not None:
@@ -18,11 +18,17 @@ from typing import List, Optional
from pretor.core.database.database_exception import database_exception from pretor.core.database.database_exception import database_exception
class SystemNodeDatabase: class SystemNodeDatabase:
"""SystemNodeDatabase 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
def __init__(self, async_session_maker): def __init__(self, async_session_maker):
self.async_session_maker = async_session_maker self.async_session_maker = async_session_maker
@database_exception @database_exception
async def upsert_system_node_config(self, node_name: str, provider_title: str, model_id: str, tools: Optional[List[str]] = None) -> SystemNodeConfig: 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: async with self.async_session_maker() as session:
statement = select(SystemNodeConfig).where(SystemNodeConfig.node_name == node_name) statement = select(SystemNodeConfig).where(SystemNodeConfig.node_name == node_name)
results = await session.execute(statement) results = await session.execute(statement)
@@ -41,6 +47,9 @@ class SystemNodeDatabase:
@database_exception @database_exception
async def get_all_system_node_configs(self) -> List[SystemNodeConfig]: 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: async with self.async_session_maker() as session:
statement = select(SystemNodeConfig) statement = select(SystemNodeConfig)
results = await session.execute(statement) results = await session.execute(statement)
@@ -48,6 +57,10 @@ class SystemNodeDatabase:
@database_exception @database_exception
async def get_system_node_config(self, node_name: str) -> Optional[SystemNodeConfig]: 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: async with self.async_session_maker() as session:
statement = select(SystemNodeConfig).where(SystemNodeConfig.node_name == node_name) statement = select(SystemNodeConfig).where(SystemNodeConfig.node_name == node_name)
results = await session.execute(statement) results = await session.execute(statement)
+29
View File
@@ -20,11 +20,17 @@ from pretor.core.database.table.user import UserAuthority
from pretor.utils.access import Accessor from pretor.utils.access import Accessor
class AuthDatabase: class AuthDatabase:
"""AuthDatabase 核心组件类。
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。 """
def __init__(self, async_session_maker): def __init__(self, async_session_maker):
self.async_session_maker = async_session_maker self.async_session_maker = async_session_maker
@database_exception @database_exception
async def add_user(self, user_name: str, hashed_password: str) -> User: 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 from ulid import ULID
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
# Check if any users exist # Check if any users exist
@@ -49,6 +55,10 @@ class AuthDatabase:
@database_exception @database_exception
async def change_password(self, user_name, old_password, new_password) -> User: 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: async with self.async_session_maker() as session:
statement = select(User).where(User.user_name == user_name) statement = select(User).where(User.user_name == user_name)
results = await session.execute(statement) results = await session.execute(statement)
@@ -65,6 +75,10 @@ class AuthDatabase:
@database_exception @database_exception
async def delete_user(self, user_name: str) -> None: 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: async with self.async_session_maker() as session:
statement = select(User).where(User.user_name == user_name) statement = select(User).where(User.user_name == user_name)
results = await session.execute(statement) results = await session.execute(statement)
@@ -76,6 +90,10 @@ class AuthDatabase:
@database_exception @database_exception
async def delete_user_by_id(self, user_id: str) -> None: 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: async with self.async_session_maker() as session:
user = await session.get(User, user_id) user = await session.get(User, user_id)
if user is None: if user is None:
@@ -85,6 +103,10 @@ class AuthDatabase:
@database_exception @database_exception
async def login_user(self, user_name: str) -> str: 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: async with self.async_session_maker() as session:
statement = select(User).where(User.user_name == user_name) statement = select(User).where(User.user_name == user_name)
results = await session.execute(statement) results = await session.execute(statement)
@@ -95,6 +117,9 @@ class AuthDatabase:
@database_exception @database_exception
async def get_all_users(self) -> list[User]: async def get_all_users(self) -> list[User]:
"""检索并获取特定的 all users 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: (list[User]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(User) statement = select(User)
results = await session.execute(statement) results = await session.execute(statement)
@@ -103,6 +128,10 @@ class AuthDatabase:
@database_exception @database_exception
async def get_user_authority(self, user_id: str) -> UserAuthority: 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: async with self.async_session_maker() as session:
user = await session.get(User, user_id) user = await session.get(User, user_id)
if user is None: if user is None:
+98 -1
View File
@@ -21,12 +21,15 @@ from sqlalchemy.orm import sessionmaker
from sqlmodel import SQLModel from sqlmodel import SQLModel
from pretor.core.database.module.individual import IndividualDatabase 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.user import AuthDatabase
from pretor.core.database.module.provider import ProviderDatabase from pretor.core.database.module.provider import ProviderDatabase
from pretor.core.database.module.system_node import SystemNodeDatabase from pretor.core.database.module.system_node import SystemNodeDatabase
@ray.remote @ray.remote
class PostgresDatabase: class PostgresDatabase:
"""PostgresDatabase 核心组件类。
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。 """
def __init__(self): def __init__(self):
user = os.environ.get('POSTGRES_USER') user = os.environ.get('POSTGRES_USER')
password = os.environ.get('POSTGRES_PASSWORD') password = os.environ.get('POSTGRES_PASSWORD')
@@ -40,11 +43,15 @@ class PostgresDatabase:
self._auth_database = AuthDatabase(self.async_session_maker) self._auth_database = AuthDatabase(self.async_session_maker)
self._provider_database = ProviderDatabase(self.async_session_maker) self._provider_database = ProviderDatabase(self.async_session_maker)
self._individual_database = IndividualDatabase(self.async_session_maker) self._individual_database = IndividualDatabase(self.async_session_maker)
self._event_database = EventDatabase(self.async_session_maker)
self._system_node_database = SystemNodeDatabase(self.async_session_maker) self._system_node_database = SystemNodeDatabase(self.async_session_maker)
self.ready_event = asyncio.Event() self.ready_event = asyncio.Event()
async def init_db(self) -> None: async def init_db(self) -> None:
"""完成 db 模块的启动与依赖初始化。
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
try: try:
async with self.async_engine.begin() as conn: async with self.async_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all) await conn.run_sync(SQLModel.metadata.create_all)
@@ -57,84 +64,174 @@ class PostgresDatabase:
# Auth Database Methods # Auth Database Methods
async def add_user(self, user_name: str, hashed_password: str): 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() await self.ready_event.wait()
return await self._auth_database.add_user(user_name, hashed_password) return await self._auth_database.add_user(user_name, hashed_password)
async def change_password(self, user_name, old_password, new_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() await self.ready_event.wait()
return await self._auth_database.change_password(user_name, old_password, new_password) return await self._auth_database.change_password(user_name, old_password, new_password)
async def delete_user(self, user_name: str): async def delete_user(self, user_name: str):
"""安全地移除或注销 user。
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._auth_database.delete_user(user_name) return await self._auth_database.delete_user(user_name)
async def delete_user_by_id(self, user_id: str): 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() await self.ready_event.wait()
return await self._auth_database.delete_user_by_id(user_id) return await self._auth_database.delete_user_by_id(user_id)
async def login_user(self, user_name: str): async def login_user(self, user_name: str):
"""执行与 login user 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._auth_database.login_user(user_name) return await self._auth_database.login_user(user_name)
async def get_all_users(self): async def get_all_users(self):
"""检索并获取特定的 all users 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._auth_database.get_all_users() return await self._auth_database.get_all_users()
async def get_user_authority(self, user_id: str): async def get_user_authority(self, user_id: str):
"""检索并获取特定的 user authority 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._auth_database.get_user_authority(user_id) return await self._auth_database.get_user_authority(user_id)
async def change_user_authority(self, user_id: str, new_authority): 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() await self.ready_event.wait()
return await self._auth_database.change_user_authority(user_id, new_authority) return await self._auth_database.change_user_authority(user_id, new_authority)
# Provider Database Methods # Provider Database Methods
async def get_provider(self): async def get_provider(self):
"""检索并获取特定的 provider 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._provider_database.get_provider() return await self._provider_database.get_provider()
async def add_provider_db(self, **kwargs): async def add_provider_db(self, **kwargs):
"""创建并持久化新的 provider db 实体。
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._provider_database.add_provider(**kwargs) return await self._provider_database.add_provider(**kwargs)
async def delete_provider_db(self, provider_id: str): async def delete_provider_db(self, provider_id: str):
"""安全地移除或注销 provider db。
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._provider_database.delete_provider(provider_id) return await self._provider_database.delete_provider(provider_id)
async def update_provider_db(self, provider_id: str, **kwargs): 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() await self.ready_event.wait()
return await self._provider_database.update_provider(provider_id, **kwargs) return await self._provider_database.update_provider(provider_id, **kwargs)
# System Node Database Methods # System Node Database Methods
async def upsert_system_node_config(self, node_name: str, provider_title: str, model_id: str, tools: list[str] = None): 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() await self.ready_event.wait()
return await self._system_node_database.upsert_system_node_config(node_name, provider_title, model_id, tools) 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): async def get_all_system_node_configs(self):
"""检索并获取特定的 all system node configs 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._system_node_database.get_all_system_node_configs() return await self._system_node_database.get_all_system_node_configs()
# Individual Database Methods # Individual Database Methods
async def add_worker_individual(self, **kwargs): async def add_worker_individual(self, **kwargs):
"""创建并持久化新的 worker individual 实体。
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._individual_database.add_worker_individual(**kwargs) return await self._individual_database.add_worker_individual(**kwargs)
async def get_worker_individual(self, agent_id: str): async def get_worker_individual(self, agent_id: str):
"""检索并获取特定的 worker individual 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._individual_database.get_worker_individual(agent_id) return await self._individual_database.get_worker_individual(agent_id)
async def get_worker_individual_list(self, owner_id: str): 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() await self.ready_event.wait()
return await self._individual_database.get_worker_individual_list(owner_id) return await self._individual_database.get_worker_individual_list(owner_id)
async def update_worker_individual(self, agent_id: str, **kwargs): 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() await self.ready_event.wait()
return await self._individual_database.update_worker_individual(agent_id, **kwargs) return await self._individual_database.update_worker_individual(agent_id, **kwargs)
async def delete_worker_individual(self, agent_id: str): async def delete_worker_individual(self, agent_id: str):
"""安全地移除或注销 worker individual。
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() await self.ready_event.wait()
return await self._individual_database.delete_worker_individual(agent_id) return await self._individual_database.delete_worker_individual(agent_id)
async def get_all_worker_individual(self): async def get_all_worker_individual(self):
"""检索并获取特定的 all worker individual 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.ready_event.wait() 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)
+5
View File
@@ -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")
+4
View File
@@ -18,11 +18,15 @@ from sqlalchemy import Column, JSON
from enum import Enum from enum import Enum
class AgentType(str, Enum): class AgentType(str, Enum):
"""AgentType 核心组件类。
这是一个领域数据模型或功能封装类,承载了 AgentType 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
SKILL_INDIVIDUAL = "skill_individual" SKILL_INDIVIDUAL = "skill_individual"
ORDINARY_INDIVIDUAL = "ordinary_individual" ORDINARY_INDIVIDUAL = "ordinary_individual"
SPECIAL_INDIVIDUAL = "special_individual" SPECIAL_INDIVIDUAL = "special_individual"
class WorkerIndividual(SQLModel, table=True): class WorkerIndividual(SQLModel, table=True):
"""WorkerIndividual 核心组件类。
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
__tablename__ = "worker_individual" __tablename__ = "worker_individual"
agent_id: str = Field(primary_key=True) agent_id: str = Field(primary_key=True)
agent_name: str = Field(index=True) agent_name: str = Field(index=True)
+2
View File
@@ -18,6 +18,8 @@ from sqlalchemy import Column, JSON
from typing import Optional from typing import Optional
class Provider(SQLModel, table=True): class Provider(SQLModel, table=True):
"""Provider 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
__tablename__ = "provider" __tablename__ = "provider"
provider_id: str = Field(primary_key=True) provider_id: str = Field(primary_key=True)
provider_title: str = Field(index=True) provider_title: str = Field(index=True)
@@ -18,6 +18,8 @@ from typing import List, Optional
from sqlalchemy import Column, JSON from sqlalchemy import Column, JSON
class SystemNodeConfig(SQLModel, table=True): class SystemNodeConfig(SQLModel, table=True):
"""SystemNodeConfig 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
__tablename__ = "system_node_config" __tablename__ = "system_node_config"
node_name: str = Field(primary_key=True) node_name: str = Field(primary_key=True)
provider_title: str provider_title: str
+4
View File
@@ -16,6 +16,8 @@ from sqlmodel import SQLModel, Field
from enum import IntEnum from enum import IntEnum
class UserAuthority(IntEnum): class UserAuthority(IntEnum):
"""UserAuthority 核心组件类。
这是一个领域数据模型或功能封装类,承载了 UserAuthority 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
SUPER_ADMINISTRATOR = 100 SUPER_ADMINISTRATOR = 100
ADMINISTRATOR = 50 ADMINISTRATOR = 50
USER = 20 USER = 20
@@ -23,6 +25,8 @@ class UserAuthority(IntEnum):
GUEST = 0 GUEST = 0
class User(SQLModel, table=True): class User(SQLModel, table=True):
"""User 核心组件类。
这是一个领域数据模型或功能封装类,承载了 User 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
__tablename__ = 'user' __tablename__ = 'user'
user_id: str = Field(primary_key=True) user_id: str = Field(primary_key=True)
user_name: str = Field(index=True) user_name: str = Field(index=True)
@@ -15,11 +15,7 @@
import ray import ray
from pretor.core.global_state_machine.provider_manager import ProviderManager from pretor.core.global_state_machine.provider_manager import ProviderManager
from pretor.core.global_state_machine.tool_manager import GlobalToolManager from pretor.core.global_state_machine.tool_manager import GlobalToolManager
from typing import Dict
from pretor.core.database.postgres import PostgresDatabase 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.workflow.workflow_template_manager import WorkflowManager
from pretor.core.global_state_machine.skill_manager import GlobalSkillManager from pretor.core.global_state_machine.skill_manager import GlobalSkillManager
from pretor.core.global_state_machine.individual_manager import GlobalIndividualManager 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 @ray.remote
class GlobalStateMachine: class GlobalStateMachine:
"""GlobalStateMachine 核心组件类。
这是一个领域数据模型或功能封装类,承载了 GlobalStateMachine 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
def __init__(self, postgres_database: PostgresDatabase): def __init__(self, postgres_database: PostgresDatabase):
import sys import sys
print("GSM __init__ START", file=sys.stderr, flush=True) print("GSM __init__ START", file=sys.stderr, flush=True)
self.event_dict: Dict[str, PretorEvent] = {}
print(" event_dict done", file=sys.stderr, flush=True) print(" event_dict done", file=sys.stderr, flush=True)
self._global_provider_manager = ProviderManager(postgres_database) self._global_provider_manager = ProviderManager(postgres_database)
print(" provider_manager done", file=sys.stderr, flush=True) print(" provider_manager done", file=sys.stderr, flush=True)
@@ -46,10 +43,16 @@ class GlobalStateMachine:
print("GSM __init__ DONE", file=sys.stderr, flush=True) print("GSM __init__ DONE", file=sys.stderr, flush=True)
async def init_state_machine(self): async def init_state_machine(self):
"""完成 state machine 模块的启动与依赖初始化。
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。 """
await self._global_provider_manager.init_provider_register(self.postgres_database) await self._global_provider_manager.init_provider_register(self.postgres_database)
await self._global_individual_manager.init_individual_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): 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( return await self._global_provider_manager.add_provider(
provider_type=provider_type, provider_type=provider_type,
provider_title=provider_title, provider_title=provider_title,
@@ -61,21 +64,39 @@ class GlobalStateMachine:
# Provider Manager Methods # Provider Manager Methods
def get_provider_list(self): def get_provider_list(self):
"""检索并获取特定的 provider list 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_provider_manager.get_provider_list() return self._global_provider_manager.get_provider_list()
def get_provider(self, provider_title): def get_provider(self, provider_title):
"""检索并获取特定的 provider 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_provider_manager.get_provider(provider_title) return self._global_provider_manager.get_provider(provider_title)
async def delete_provider(self, provider_title: str): 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) return await self._global_provider_manager.delete_provider(provider_title, self.postgres_database)
# Tool Manager Methods # Tool Manager Methods
def get_tool_mapper(self): def get_tool_mapper(self):
"""检索并获取特定的 tool mapper 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_tool_manager.tool_mapper return self._global_tool_manager.tool_mapper
def get_tool_list(self, agent_name: str): 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 # 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) # 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, {}) tools = self._global_tool_manager.tool_mapper.get(agent_name, {})
# also include default tools # also include default tools
default_tools = self._global_tool_manager.tool_mapper.get("default", {}) default_tools = self._global_tool_manager.tool_mapper.get("default", {})
@@ -84,83 +105,78 @@ class GlobalStateMachine:
# Workflow Template Manager Methods # Workflow Template Manager Methods
def get_all_workflow_templates(self): def get_all_workflow_templates(self):
"""检索并获取特定的 all workflow templates 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_workflow_template_manager.get_all_workflow_templates() return self._global_workflow_template_manager.get_all_workflow_templates()
def add_workflow_template(self, template_name: str, workflow_template): 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) return self._global_workflow_template_manager.add_workflow_template(template_name, workflow_template)
def delete_workflow_template(self, template_name: str): 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) return self._global_workflow_template_manager.delete_workflow_template(template_name)
def generate_workflow_template(self, workflow_template): 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) return self._global_workflow_template_manager.generate_workflow_template(workflow_template)
# Skill Manager Methods # Skill Manager Methods
def add_skill(self, skill_name: str): def add_skill(self, skill_name: str):
"""创建并持久化新的 skill 实体。
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_skill_manager.add_skill(skill_name) return self._global_skill_manager.add_skill(skill_name)
def get_skill_list(self): def get_skill_list(self):
"""检索并获取特定的 skill list 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_skill_manager.get_skill_list() return self._global_skill_manager.get_skill_list()
def remove_skill(self, skill_name: str): def remove_skill(self, skill_name: str):
"""安全地移除或注销 skill。
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_skill_manager.remove_skill(skill_name) return self._global_skill_manager.remove_skill(skill_name)
# Individual Manager Methods # Individual Manager Methods
def add_individual(self, agent_id: str, config): 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) return self._global_individual_manager.add_individual(agent_id, config)
def get_individual(self, agent_id: str): 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) return self._global_individual_manager.get_individual(agent_id)
def remove_individual(self, agent_id: str): 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) return self._global_individual_manager.remove_individual(agent_id)
def list_individuals(self): def list_individuals(self):
"""执行与 list individuals 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self._global_individual_manager.list_individuals() 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') logger = get_logger('individual_manager')
class GlobalIndividualManager: class GlobalIndividualManager:
"""GlobalIndividualManager 核心组件类。
这是一个管理器类,职责集中在维护整个系统内有关 GlobalIndividual 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
def __init__(self): def __init__(self):
self._individuals: Dict[str, Dict[str, Any]] = {} self._individuals: Dict[str, Dict[str, Any]] = {}
async def init_individual_register(self, postgres) -> None: async def init_individual_register(self, postgres) -> None:
"""完成 individual register 模块的启动与依赖初始化。
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
Args: postgres: 参与 init individual register 逻辑运算或数据构建的上下文依赖对象。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
try: try:
try: try:
individuals = await postgres.get_all_worker_individual.remote() individuals = await postgres.get_all_worker_individual.remote()
@@ -55,8 +61,15 @@ class GlobalIndividualManager:
return self._individuals.get(agent_id, None) return self._individuals.get(agent_id, None)
def remove_individual(self, agent_id: str) -> 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: if agent_id in self._individuals:
del self._individuals[agent_id] del self._individuals[agent_id]
def list_individuals(self) -> Dict[str, Dict[str, Any]]: def list_individuals(self) -> Dict[str, Dict[str, Any]]:
"""执行与 list individuals 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: (Dict[str, Dict[str, Any]]): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
return self._individuals return self._individuals
@@ -18,10 +18,14 @@ from typing import List
from enum import Enum from enum import Enum
class ProviderStatus(str, Enum): class ProviderStatus(str, Enum):
"""ProviderStatus 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
UP = "up" UP = "up"
DOWN = "down" DOWN = "down"
class Provider(BaseModel): class Provider(BaseModel):
"""Provider 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
provider_title: str provider_title: str
provider_url: str provider_url: str
provider_apikey: str provider_apikey: str
@@ -31,12 +35,16 @@ class Provider(BaseModel):
provider_status: ProviderStatus = ProviderStatus.UP provider_status: ProviderStatus = ProviderStatus.UP
class ProviderArgs(BaseModel): class ProviderArgs(BaseModel):
"""ProviderArgs 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
provider_title: str provider_title: str
provider_url: str provider_url: str
provider_apikey: str provider_apikey: str
provider_owner: str provider_owner: str
class BaseProvider(ABC): class BaseProvider(ABC):
"""BaseProvider 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
@staticmethod @staticmethod
@abstractmethod @abstractmethod
async def create_provider(provider_args: ProviderArgs) -> Provider: async def create_provider(provider_args: ProviderArgs) -> Provider:
@@ -1,4 +1,3 @@
from pretor.utils.retry import retry_on_retryable_error
# Copyright 2026 zhaoxi826 # Copyright 2026 zhaoxi826
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # 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 # See the License for the specific language governing permissions and
# limitations under the License. # 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 from pretor.core.global_state_machine.model_provider.base_provider import BaseProvider, Provider, ProviderArgs
import httpx import httpx
from typing import List from typing import List
class ClaudeProvider(BaseProvider): class ClaudeProvider(BaseProvider):
"""ClaudeProvider 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
@staticmethod @staticmethod
async def create_provider(provider_args: ProviderArgs) -> Provider: 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_models: List[str] = await ClaudeProvider._load_models(provider_args)
provider: Provider = ClaudeProvider._return_provider(provider_args, provider_models) provider: Provider = ClaudeProvider._return_provider(provider_args, provider_models)
return provider return provider
@@ -28,6 +35,10 @@ class ClaudeProvider(BaseProvider):
@retry_on_retryable_error() @retry_on_retryable_error()
async def _load_models(provider_args: ProviderArgs) -> List[str]: async def _load_models(provider_args: ProviderArgs) -> List[str]:
# Anthropic 官方需要 version 头 # Anthropic 官方需要 version 头
"""执行与 load models 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
headers = { headers = {
"x-api-key": provider_args.provider_apikey, "x-api-key": provider_args.provider_apikey,
"anthropic-version": "2023-06-01", "anthropic-version": "2023-06-01",
@@ -53,6 +64,10 @@ class ClaudeProvider(BaseProvider):
@staticmethod @staticmethod
def _return_provider(provider_args: ProviderArgs, provider_models: List[str]) -> Provider: 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, return Provider(provider_title=provider_args.provider_title,
provider_apikey=provider_args.provider_apikey, provider_apikey=provider_args.provider_apikey,
provider_url=provider_args.provider_url, provider_url=provider_args.provider_url,
@@ -18,8 +18,14 @@ import httpx
from typing import List from typing import List
class DeepseekProvider(BaseProvider): class DeepseekProvider(BaseProvider):
"""DeepseekProvider 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
@staticmethod @staticmethod
async def create_provider(provider_args: ProviderArgs) -> Provider: 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_models: List[str] = await DeepseekProvider._load_models(provider_args)
provider: Provider = DeepseekProvider._return_provider(provider_args, provider_models) provider: Provider = DeepseekProvider._return_provider(provider_args, provider_models)
return provider return provider
@@ -27,6 +33,10 @@ class DeepseekProvider(BaseProvider):
@staticmethod @staticmethod
@retry_on_retryable_error() @retry_on_retryable_error()
async def _load_models(provider_args: ProviderArgs) -> List[str]: async def _load_models(provider_args: ProviderArgs) -> List[str]:
"""执行与 load models 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
headers = { headers = {
"Authorization": f"Bearer {provider_args.provider_apikey}", "Authorization": f"Bearer {provider_args.provider_apikey}",
"Content-Type": "application/json" "Content-Type": "application/json"
@@ -52,6 +62,10 @@ class DeepseekProvider(BaseProvider):
@staticmethod @staticmethod
def _return_provider(provider_args: ProviderArgs, provider_models: List[str]) -> Provider: 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, return Provider(provider_title=provider_args.provider_title,
provider_apikey=provider_args.provider_apikey, provider_apikey=provider_args.provider_apikey,
provider_url=provider_args.provider_url, provider_url=provider_args.provider_url,
@@ -18,8 +18,14 @@ import httpx
from typing import List from typing import List
class OpenAIProvider(BaseProvider): class OpenAIProvider(BaseProvider):
"""OpenAIProvider 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
@staticmethod @staticmethod
async def create_provider(provider_args: ProviderArgs) -> Provider: 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_models: List[str] = await OpenAIProvider._load_models(provider_args)
provider: Provider = OpenAIProvider._return_provider(provider_args, provider_models) provider: Provider = OpenAIProvider._return_provider(provider_args, provider_models)
return provider return provider
@@ -27,6 +33,10 @@ class OpenAIProvider(BaseProvider):
@staticmethod @staticmethod
@retry_on_retryable_error() @retry_on_retryable_error()
async def _load_models(provider_args: ProviderArgs) -> List[str]: async def _load_models(provider_args: ProviderArgs) -> List[str]:
"""执行与 load models 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。 """
headers = { headers = {
"Authorization": f"Bearer {provider_args.provider_apikey}", "Authorization": f"Bearer {provider_args.provider_apikey}",
"Content-Type": "application/json" "Content-Type": "application/json"
@@ -52,6 +62,10 @@ class OpenAIProvider(BaseProvider):
@staticmethod @staticmethod
def _return_provider(provider_args: ProviderArgs, provider_models: List[str]) -> Provider: 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, return Provider(provider_title=provider_args.provider_title,
provider_apikey=provider_args.provider_apikey, provider_apikey=provider_args.provider_apikey,
provider_url=provider_args.provider_url, provider_url=provider_args.provider_url,
@@ -33,11 +33,19 @@ class ProviderManager:
self.provider_register = {} self.provider_register = {}
async def init_provider_register(self, postgres) -> None: async def init_provider_register(self, postgres) -> None:
"""完成 provider register 模块的启动与依赖初始化。
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
Args: postgres: 参与 init provider register 逻辑运算或数据构建的上下文依赖对象。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
providers = await postgres.get_provider.remote() providers = await postgres.get_provider.remote()
for provider in providers: for provider in providers:
self.provider_register[provider.provider_title] = provider 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: 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.core.global_state_machine.model_provider import ProviderArgs
from pretor.utils.logger import get_logger from pretor.utils.logger import get_logger
logger = get_logger('provider_manager') logger = get_logger('provider_manager')
@@ -74,12 +82,23 @@ class ProviderManager:
logger.warning(f"[{provider_args.provider_title}] 解析模型列表时发生错误: {e}") logger.warning(f"[{provider_args.provider_title}] 解析模型列表时发生错误: {e}")
def get_provider_list(self): def get_provider_list(self):
"""检索并获取特定的 provider list 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self.provider_register return self.provider_register
def get_provider(self, provider_title): def get_provider(self, provider_title):
"""检索并获取特定的 provider 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return self.provider_register.get(provider_title) return self.provider_register.get(provider_title)
async def delete_provider(self, provider_title: str, postgres_database) -> None: 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: if provider_title in self.provider_register:
provider = self.provider_register[provider_title] provider = self.provider_register[provider_title]
await postgres_database.delete_provider_db.remote( provider_id=provider.provider_id) await postgres_database.delete_provider_db.remote( provider_id=provider.provider_id)
@@ -18,6 +18,8 @@ import pathlib
import json import json
class GlobalSkillManager: class GlobalSkillManager:
"""GlobalSkillManager 核心组件类。
这是一个管理器类,职责集中在维护整个系统内有关 GlobalSkill 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
skill_mapper = Dict[str,Tuple[str]] skill_mapper = Dict[str,Tuple[str]]
"""skill的存储表""" """skill的存储表"""
@@ -22,6 +22,8 @@ from pretor.utils.logger import get_logger
logger = get_logger('tool_manager') logger = get_logger('tool_manager')
class GlobalToolManager: class GlobalToolManager:
"""GlobalToolManager 核心组件类。
这是一个管理器类,职责集中在维护整个系统内有关 GlobalTool 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
tool_mapper: Dict[str, Dict[str, Type[BaseToolData]]] tool_mapper: Dict[str, Dict[str, Type[BaseToolData]]]
def __init__(self): def __init__(self):
@@ -25,6 +25,8 @@ from pretor.adapter.model_adapter.agent_factory import AgentFactory
@ray.remote @ray.remote
class ConsciousnessNode: class ConsciousnessNode:
"""ConsciousnessNode 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
def __init__(self) -> None: def __init__(self) -> None:
from pretor.utils.logger import get_logger from pretor.utils.logger import get_logger
self.logger = get_logger('consciousness_node') self.logger = get_logger('consciousness_node')
@@ -70,6 +72,10 @@ class ConsciousnessNode:
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]): async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]):
"""执行与 dynamic prompt 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: ctx (RunContext[ConsciousnessNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
prompt = system_prompt + "\n\n" prompt = system_prompt + "\n\n"
prompt += ( prompt += (
f"=== 当前任务上下文 ===\n" f"=== 当前任务上下文 ===\n"
@@ -87,6 +93,10 @@ class ConsciousnessNode:
return prompt return prompt
async def working(self, payload: Union[ForWorkflowEngineInput, ForWorkflowInput, ForSupervisoryInput]) -> Union[ForWorkflowEngine, ForWorkflow, ForSupervisoryNode, None]: 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: try:
result = await self._run(payload) result = await self._run(payload)
if isinstance(result, (ForWorkflowEngine, ForWorkflow, ForSupervisoryNode)): if isinstance(result, (ForWorkflowEngine, ForWorkflow, ForSupervisoryNode)):
@@ -139,6 +149,10 @@ class ConsciousnessNode:
pass pass
async def _run(self, payload: Union[ForSupervisoryInput, ForWorkflowInput, ForWorkflowEngineInput]) -> Union[ForSupervisoryNode, ForWorkflow, ForWorkflowEngine]: 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: try:
self.agent.retries = 3 self.agent.retries = 3
if isinstance(payload, ForWorkflowEngineInput): if isinstance(payload, ForWorkflowEngineInput):
@@ -41,6 +41,8 @@ class ForSupervisoryNode(ConsciousnessNodeResponse):
class ConsciousnessNodeDeps(DepsModel): class ConsciousnessNodeDeps(DepsModel):
"""ConsciousnessNodeDeps 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
original_command: str original_command: str
workflow_template: str | None = None workflow_template: str | None = None
command: str command: str
@@ -48,20 +50,28 @@ class ConsciousnessNodeDeps(DepsModel):
class ConsciousnessNodeInput(InputModel): class ConsciousnessNodeInput(InputModel):
"""ConsciousnessNodeInput 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
pass pass
class ForWorkflowEngineInput(ConsciousnessNodeInput): class ForWorkflowEngineInput(ConsciousnessNodeInput):
"""ForWorkflowEngineInput 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ForWorkflowEngineInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
workflow_template: str | None = None workflow_template: str | None = None
original_command: str original_command: str
available_skills: list[dict] | None = None available_skills: list[dict] | None = None
class ForWorkflowInput(ConsciousnessNodeInput): class ForWorkflowInput(ConsciousnessNodeInput):
"""ForWorkflowInput 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ForWorkflowInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
workflow_step: WorkStep workflow_step: WorkStep
original_command: str original_command: str
class ForSupervisoryInput(ConsciousnessNodeInput): class ForSupervisoryInput(ConsciousnessNodeInput):
"""ForSupervisoryInput 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ForSupervisoryInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
workflow: PretorWorkflow workflow: PretorWorkflow
original_command: str original_command: str
@@ -23,6 +23,8 @@ from pretor.core.individual.control_node.template import ForWorkflow, ForWorkflo
@ray.remote @ray.remote
class ControlNode: class ControlNode:
"""ControlNode 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
def __init__(self): def __init__(self):
from pretor.utils.logger import get_logger from pretor.utils.logger import get_logger
self.logger = get_logger('control_node') self.logger = get_logger('control_node')
@@ -67,6 +69,10 @@ class ControlNode:
tools=callables) tools=callables)
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]): async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]):
"""执行与 dynamic prompt 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: ctx (RunContext[ControlNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
prompt = system_prompt + "\n\n" prompt = system_prompt + "\n\n"
prompt += ( prompt += (
f"=== 当前任务步骤上下文 ===\n" f"=== 当前任务步骤上下文 ===\n"
@@ -77,6 +83,10 @@ class ControlNode:
return prompt return prompt
async def working(self, payload: ForWorkflowInput) -> str: async def working(self, payload: ForWorkflowInput) -> str:
"""执行与 working 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: payload (ForWorkflowInput): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
try: try:
result: ForWorkflow = await self._run(payload) result: ForWorkflow = await self._run(payload)
return result return result
@@ -85,6 +95,10 @@ class ControlNode:
return None return None
async def _run(self, payload: ForWorkflowInput) -> ForWorkflow: async def _run(self, payload: ForWorkflowInput) -> ForWorkflow:
"""执行与 run 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: payload (ForWorkflowInput): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
Returns: (ForWorkflow): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
try: try:
self.agent.retries = 3 self.agent.retries = 3
deps = ControlNodeDeps( deps = ControlNodeDeps(
@@ -23,17 +23,25 @@ class ControlNodeResponse(ResponseModel):
class ControlNodeInput(InputModel): class ControlNodeInput(InputModel):
"""ControlNodeInput 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
pass pass
class ControlNodeDeps(DepsModel): class ControlNodeDeps(DepsModel):
"""ControlNodeDeps 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
workflow_step: WorkStep workflow_step: WorkStep
# In the future, this can be dynamically populated with tools specific to the current task execution # In the future, this can be dynamically populated with tools specific to the current task execution
class ForWorkflow(ControlNodeResponse): class ForWorkflow(ControlNodeResponse):
"""ForWorkflow 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ForWorkflow 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
output: str = Field(..., description="控制节点执行特定工作流步骤的结果。包含执行细节和输出数据。") output: str = Field(..., description="控制节点执行特定工作流步骤的结果。包含执行细节和输出数据。")
class ForWorkflowInput(ControlNodeInput): class ForWorkflowInput(ControlNodeInput):
"""ForWorkflowInput 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ForWorkflowInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
workflow_step: WorkStep workflow_step: WorkStep
@@ -26,6 +26,8 @@ from pretor.utils.ray_hook import ray_actor_hook
@ray.remote @ray.remote
class SupervisoryNode: class SupervisoryNode:
"""SupervisoryNode 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
def __init__(self) -> None: def __init__(self) -> None:
from pretor.utils.logger import get_logger from pretor.utils.logger import get_logger
self.logger = get_logger('supervisory_node') self.logger = get_logger('supervisory_node')
@@ -71,6 +73,10 @@ class SupervisoryNode:
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[SupervisoryNodeDeps]): async def dynamic_prompt(ctx: RunContext[SupervisoryNodeDeps]):
"""执行与 dynamic prompt 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: ctx (RunContext[SupervisoryNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
prompt = system_prompt + "\n\n" prompt = system_prompt + "\n\n"
prompt += ( prompt += (
f"=== 当前上下文 ===\n" f"=== 当前上下文 ===\n"
@@ -111,8 +117,8 @@ class SupervisoryNode:
if isinstance(payload, PretorEvent): if isinstance(payload, PretorEvent):
payload.context["workflow_template"] = result.workflow_template payload.context["workflow_template"] = result.workflow_template
try: try:
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine global_workflow_manager = ray_actor_hook("global_workflow_manager").global_workflow_manager
await global_state_machine.add_event.remote(payload) await global_workflow_manager.add_event.remote(payload)
workflow_running_engine = ray_actor_hook("workflow_running_engine").workflow_running_engine workflow_running_engine = ray_actor_hook("workflow_running_engine").workflow_running_engine
await workflow_running_engine.put_event.remote(payload) await workflow_running_engine.put_event.remote(payload)
except Exception as e: except Exception as e:
@@ -17,21 +17,31 @@ from pretor.utils.agent_model import ResponseModel, DepsModel
from pydantic import BaseModel from pydantic import BaseModel
class SupervisoryNodeResponse(ResponseModel): class SupervisoryNodeResponse(ResponseModel):
"""SupervisoryNodeResponse 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
pass pass
class ForUser(SupervisoryNodeResponse): class ForUser(SupervisoryNodeResponse):
"""ForUser 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ForUser 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
context: str = Field(..., description="对用户的回复,应当使用和蔼的语气进行回复。用于直接解答简单问题或返回最终报告。") context: str = Field(..., description="对用户的回复,应当使用和蔼的语气进行回复。用于直接解答简单问题或返回最终报告。")
class ForConsciousnessNode(SupervisoryNodeResponse): class ForConsciousnessNode(SupervisoryNodeResponse):
"""ForConsciousnessNode 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
workflow_template: str | None = Field(default=None, description="选择的工作流模板的名称,用于处理复杂任务。若无需模板则为 None。") workflow_template: str | None = Field(default=None, description="选择的工作流模板的名称,用于处理复杂任务。若无需模板则为 None。")
reasoning: str = Field(..., description="选择将任务移交意识节点并选用该模板的简短原因。") reasoning: str = Field(..., description="选择将任务移交意识节点并选用该模板的简短原因。")
class TerminationMessage(BaseModel): class TerminationMessage(BaseModel):
"""TerminationMessage 核心组件类。
这是一个领域数据模型或功能封装类,承载了 TerminationMessage 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
platform: str platform: str
user_name: str user_name: str
message: str message: str
class SupervisoryNodeDeps(DepsModel): class SupervisoryNodeDeps(DepsModel):
"""SupervisoryNodeDeps 核心组件类。
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。 """
platform: str platform: str
user_name: str user_name: str
time: str time: str
+13
View File
@@ -22,14 +22,20 @@ NodeType = Literal[
] ]
class EventInfo(BaseModel): class EventInfo(BaseModel):
"""EventInfo 核心组件类。
这是一个领域数据模型或功能封装类,承载了 EventInfo 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
platform: str platform: str
user_name: str user_name: str
class LogicGate(BaseModel): class LogicGate(BaseModel):
"""LogicGate 核心组件类。
这是一个领域数据模型或功能封装类,承载了 LogicGate 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
if_fail: str = Field(..., description="失败跳转目标,如 'jump_to_step_1'") if_fail: str = Field(..., description="失败跳转目标,如 'jump_to_step_1'")
if_pass: Literal["continue", "exit"] = Field(default="continue", description="成功后的动作") if_pass: Literal["continue", "exit"] = Field(default="continue", description="成功后的动作")
class WorkStep(BaseModel): class WorkStep(BaseModel):
"""WorkStep 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkStep 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
step: int = Field(..., gt=0, description="步骤序号,严格自增") step: int = Field(..., gt=0, description="步骤序号,严格自增")
name: str = Field(..., description="步骤名称") name: str = Field(..., description="步骤名称")
node: NodeType = Field(..., description="负责执行的节点类型") node: NodeType = Field(..., description="负责执行的节点类型")
@@ -46,6 +52,8 @@ class WorkStep(BaseModel):
class WorkflowStatus(BaseModel): class WorkflowStatus(BaseModel):
"""WorkflowStatus 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkflowStatus 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
step: int = Field(default=1, gt=0, description="当前运行到的工作流步数") step: int = Field(default=1, gt=0, description="当前运行到的工作流步数")
status: Literal["waiting_llm_working", "waiting_tool_working", "llm_working", "tool_working"] = Field( status: Literal["waiting_llm_working", "waiting_tool_working", "llm_working", "tool_working"] = Field(
default="waiting_llm_working", default="waiting_llm_working",
@@ -53,6 +61,8 @@ class WorkflowStatus(BaseModel):
) )
class PretorWorkflow(BaseModel): class PretorWorkflow(BaseModel):
"""PretorWorkflow 核心组件类。
这是一个领域数据模型或功能封装类,承载了 PretorWorkflow 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
title: str = Field(..., description="工作流的标题") title: str = Field(..., description="工作流的标题")
work_link: List[WorkStep] = Field(..., description="工作链逻辑定义") work_link: List[WorkStep] = Field(..., description="工作链逻辑定义")
# ---------------- 以下为系统级管控字段,LLM 无需关心 ---------------- # # ---------------- 以下为系统级管控字段,LLM 无需关心 ---------------- #
@@ -66,6 +76,9 @@ class PretorWorkflow(BaseModel):
@model_validator(mode='after') @model_validator(mode='after')
def validate_workflow_integrity(self) -> 'PretorWorkflow': def validate_workflow_integrity(self) -> 'PretorWorkflow':
"""执行与 validate workflow integrity 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: ('PretorWorkflow'): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
steps = [s.step for s in self.work_link] steps = [s.step for s in self.work_link]
expected = list(range(1, len(steps) + 1)) expected = list(range(1, len(steps) + 1))
if steps != expected: if steps != expected:
+32 -3
View File
@@ -34,6 +34,10 @@ import pathlib
def get_workflow_template(workflow_name: str) -> str: 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") 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: with open(workflow_template, "r", encoding="utf-8") as workflow_template_file:
workflow_template = workflow_template_file.read() workflow_template = workflow_template_file.read()
@@ -41,6 +45,8 @@ def get_workflow_template(workflow_name: str) -> str:
class WorkflowEngine: class WorkflowEngine:
"""WorkflowEngine 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkflowEngine 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
def __init__(self, def __init__(self,
workflow: PretorWorkflow, workflow: PretorWorkflow,
consciousness_node=None, consciousness_node=None,
@@ -58,11 +64,15 @@ class WorkflowEngine:
"""控制节点""" """控制节点"""
self.supervisory_node = supervisory_node 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: async def _push_sse(self, msg: str) -> None:
"""执行与 push sse 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: msg (str): 控制逻辑流向的具体字符串参数,指定了期望的 msg 内容。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
try: 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: except Exception:
pass pass
@@ -273,6 +283,8 @@ class WorkflowEngine:
@ray.remote @ray.remote
class WorkflowRunningEngine: class WorkflowRunningEngine:
"""WorkflowRunningEngine 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkflowRunningEngine 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
def __init__(self, consciousness_node=None, control_node=None, supervisory_node=None): def __init__(self, consciousness_node=None, control_node=None, supervisory_node=None):
from pretor.utils.logger import get_logger from pretor.utils.logger import get_logger
self.logger = get_logger('workflow_runner') self.logger = get_logger('workflow_runner')
@@ -285,6 +297,8 @@ class WorkflowRunningEngine:
async def run(self): async def run(self):
# Move actor hook to async start so we don't race during __init__ across cluster # 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.global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
self.workflow_queue = asyncio.Queue() self.workflow_queue = asyncio.Queue()
self.runner_engine = { self.runner_engine = {
@@ -293,8 +307,22 @@ class WorkflowRunningEngine:
} }
async def put_event(self, event: PretorEvent) -> None: async def put_event(self, event: PretorEvent) -> None:
"""执行与 put event 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: event (PretorEvent): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
await self.workflow_queue.put(event) 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: async def runner(self, i: int) -> None:
""" """
runner方法,从self.workflow_queue中不断取出任务并执行 runner方法,从self.workflow_queue中不断取出任务并执行
@@ -347,7 +375,8 @@ class WorkflowRunningEngine:
self.logger.info( self.logger.info(
f"WorkflowRunningEngine: runner_{i} 成功生成工作流 {workflow.trace_id}:{workflow.title}") 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, workflow_engine = WorkflowEngine(workflow,
self.consciousness_node, self.consciousness_node,
@@ -16,6 +16,8 @@ from pydantic import BaseModel, model_validator
from typing import Dict,List from typing import Dict,List
class WorkflowTemplateStep(BaseModel): class WorkflowTemplateStep(BaseModel):
"""WorkflowTemplateStep 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkflowTemplateStep 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
step: int step: int
node: str node: str
action: str action: str
@@ -25,12 +27,17 @@ class WorkflowTemplateStep(BaseModel):
logic_gate: Dict[str, str] logic_gate: Dict[str, str]
class WorkflowTemplate(BaseModel): class WorkflowTemplate(BaseModel):
"""WorkflowTemplate 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkflowTemplate 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
name: str name: str
desc: str desc: str
work_link: list[WorkflowTemplateStep] work_link: list[WorkflowTemplateStep]
@model_validator(mode='after') @model_validator(mode='after')
def validate_steps(self) -> 'WorkflowTemplate': def validate_steps(self) -> 'WorkflowTemplate':
"""执行与 validate steps 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: ('WorkflowTemplate'): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
steps = [s.step for s in self.work_link] steps = [s.step for s in self.work_link]
if len(steps) != len(set(steps)): if len(steps) != len(set(steps)):
raise ValueError("Step numbers in work_link must be unique") 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 from pretor.core.workflow.workflow_template_generator.workflow_template import WorkflowTemplate
class WorkflowTemplateGenerator: class WorkflowTemplateGenerator:
"""WorkflowTemplateGenerator 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkflowTemplateGenerator 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
@staticmethod @staticmethod
def generate_workflow_template(workflow_template: WorkflowTemplate) -> WorkflowTemplate: 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" output_dir = Path("pretor") / "workflow_template"
if not output_dir.exists(): if not output_dir.exists():
output_dir.mkdir(parents=True) output_dir.mkdir(parents=True)
@@ -21,6 +21,8 @@ from pretor.utils.logger import get_logger
logger = get_logger('workflow_template_manager') logger = get_logger('workflow_template_manager')
class WorkflowManager: class WorkflowManager:
"""WorkflowManager 核心组件类。
这是一个管理器类,职责集中在维护整个系统内有关 Workflow 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。 """
def __init__(self): def __init__(self):
self.workflow_template_generator = WorkflowTemplateGenerator() self.workflow_template_generator = WorkflowTemplateGenerator()
self.workflow_templates_registry = {} self.workflow_templates_registry = {}
@@ -28,6 +30,9 @@ class WorkflowManager:
self._load_workflow_template() self._load_workflow_template()
def _load_workflow_template(self) -> None: def _load_workflow_template(self) -> None:
"""执行与 load workflow template 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
for workflow_template_file in self.template_path.glob("*_workflow_template.json"): for workflow_template_file in self.template_path.glob("*_workflow_template.json"):
with workflow_template_file.open("r",encoding="utf-8") as f: with workflow_template_file.open("r",encoding="utf-8") as f:
try: try:
@@ -39,6 +44,10 @@ class WorkflowManager:
logger.warning(f"{workflow_template_file}不符合workflow_template格式") logger.warning(f"{workflow_template_file}不符合workflow_template格式")
def generate_workflow_template(self, workflow_template: WorkflowTemplate) -> None: def generate_workflow_template(self, workflow_template: WorkflowTemplate) -> None:
"""执行与 generate workflow template 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: workflow_template (WorkflowTemplate): 参与 generate workflow template 逻辑运算或数据构建的上下文依赖对象。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
try: try:
workflow_template = self.workflow_template_generator.generate_workflow_template(workflow_template=workflow_template) workflow_template = self.workflow_template_generator.generate_workflow_template(workflow_template=workflow_template)
self.workflow_templates_registry[workflow_template.name] = workflow_template.desc self.workflow_templates_registry[workflow_template.name] = workflow_template.desc
@@ -46,11 +55,22 @@ class WorkflowManager:
logger.exception("Failed to generate workflow template") logger.exception("Failed to generate workflow template")
def add_workflow_template(self, template_name: str, workflow_template: WorkflowTemplate) -> None: 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) self.generate_workflow_template(workflow_template)
def get_all_workflow_templates(self) -> dict: def get_all_workflow_templates(self) -> dict:
"""检索并获取特定的 all workflow templates 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
return self.workflow_templates_registry return self.workflow_templates_registry
def delete_workflow_template(self, template_name: str) -> None: 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: if template_name in self.workflow_templates_registry:
del self.workflow_templates_registry[template_name] del self.workflow_templates_registry[template_name]
+13
View File
@@ -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 from .approval import ApprovalToolData, approval
__all__ = ["ApprovalToolData", "approval"] __all__ = ["ApprovalToolData", "approval"]
@@ -17,6 +17,8 @@ from pretor.utils.ray_hook import ray_actor_hook
from typing import List, Literal, Dict from typing import List, Literal, Dict
class ApprovalToolData(BaseToolData): class ApprovalToolData(BaseToolData):
"""ApprovalToolData 核心组件类。
这是一个可被智能体动态调用的外部工具组件类。它定义了清晰的输入参数 Schema 与执行契约,赋予智能体与外界真实系统(如文件、网页、API)进行交互的能力。 """
is_system: bool = True is_system: bool = True
action_scope: List[Literal["control_node", "consciousness_node", "supervisory_node", "growth_node", "", ""]] = [ action_scope: List[Literal["control_node", "consciousness_node", "supervisory_node", "growth_node", "", ""]] = [
"control_node", "consciousness_node"] "control_node", "consciousness_node"]
+2
View File
@@ -17,6 +17,8 @@ from typing import List, Literal, Dict
from pydantic import ConfigDict from pydantic import ConfigDict
class BaseToolData(BaseModel): class BaseToolData(BaseModel):
"""BaseToolData 核心组件类。
这是一个可被智能体动态调用的外部工具组件类。它定义了清晰的输入参数 Schema 与执行契约,赋予智能体与外界真实系统(如文件、网页、API)进行交互的能力。 """
model_config = ConfigDict(extra="allow") model_config = ConfigDict(extra="allow")
is_system: bool is_system: bool
action_scope: List[Literal["control_node", "consciousness_node", "supervisory_node", "growth_node", "", ""]] = [] 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 from .file_reader import FileReaderData, file_reader
__all__ = ["FileReaderData", "file_reader"] __all__ = ["FileReaderData", "file_reader"]
@@ -17,6 +17,8 @@ from pretor.plugin.tool_plugin.base_tool import BaseToolData
import os import os
class FileReaderData(BaseToolData): class FileReaderData(BaseToolData):
"""FileReaderData 核心组件类。
这是一个领域数据模型或功能封装类,承载了 FileReaderData 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
is_system: bool = True is_system: bool = True
name: str = "file_reader" name: str = "file_reader"
description: str = "读取本地文件的内容" description: str = "读取本地文件的内容"
+31
View File
@@ -23,6 +23,8 @@ from pwdlib import PasswordHash
class TokenData(BaseModel): class TokenData(BaseModel):
"""TokenData 核心组件类。
这是一个领域数据模型或功能封装类,承载了 TokenData 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
user_id: str user_id: str
username: Optional[str] = None username: Optional[str] = None
exp: Optional[int] = None exp: Optional[int] = None
@@ -31,12 +33,21 @@ SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
if not SECRET_KEY or SECRET_KEY in {"secret", "114514"}:
raise RuntimeError("未提供有效的 SECRET_KEY 或使用了不安全的默认值")
password_hasher = PasswordHash.recommended() password_hasher = PasswordHash.recommended()
class Accessor: class Accessor:
"""Accessor 核心组件类。
这是一个领域数据模型或功能封装类,承载了 Accessor 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
@staticmethod @staticmethod
def _decode_token(token: str) -> TokenData: def _decode_token(token: str) -> TokenData:
"""执行与 decode token 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: token (str): 由认证中心颁发的 JWT 或长期访问令牌,用于跨服务调用时的身份自证与权限校验。
Returns: (TokenData): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
try: try:
payload = jwt.decode( payload = jwt.decode(
token, token,
@@ -57,6 +68,10 @@ class Accessor:
@staticmethod @staticmethod
def _create_access_token(data: dict) -> str: def _create_access_token(data: dict) -> str:
"""创建并持久化新的 access token 实体。
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
Args: data (dict): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
to_encode = data.copy() to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": int(expire.timestamp())}) to_encode.update({"exp": int(expire.timestamp())})
@@ -64,10 +79,18 @@ class Accessor:
@staticmethod @staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool: 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) return password_hasher.verify(plain_password, hashed_password)
@staticmethod @staticmethod
def get_current_user(request: Request) -> TokenData: def get_current_user(request: Request) -> TokenData:
"""检索并获取特定的 current user 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: request (Request): FastAPI 框架注入的原生 HTTP 请求对象,包含了完整的 Header 标头、查询参数和正文流。
Returns: (TokenData): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "): if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException( raise HTTPException(
@@ -79,6 +102,10 @@ class Accessor:
@staticmethod @staticmethod
def login_hashed_password(user: User, password: str) -> str: 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: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@@ -97,6 +124,10 @@ class Accessor:
@staticmethod @staticmethod
def hash_password(password: str) -> str: def hash_password(password: str) -> str:
"""执行与 hash password 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: password (str): 控制逻辑流向的具体字符串参数,指定了期望的 password 内容。
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。 """
if not password: if not password:
raise ValueError("密码不能为空") raise ValueError("密码不能为空")
if len(password) < 6: if len(password) < 6:
+6
View File
@@ -16,10 +16,16 @@
from pydantic import BaseModel from pydantic import BaseModel
class ResponseModel(BaseModel): class ResponseModel(BaseModel):
"""ResponseModel 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ResponseModel 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
pass pass
class DepsModel(BaseModel): class DepsModel(BaseModel):
"""DepsModel 核心组件类。
这是一个领域数据模型或功能封装类,承载了 DepsModel 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
pass pass
class InputModel(BaseModel): class InputModel(BaseModel):
"""InputModel 核心组件类。
这是一个领域数据模型或功能封装类,承载了 InputModel 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
pass pass
+17
View File
@@ -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.console import Console
from rich.text import Text from rich.text import Text
import yaml import yaml
def print_banner() -> None: def print_banner() -> None:
"""执行与 print banner 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
with open("config/config.yml","r") as config: with open("config/config.yml","r") as config:
config = yaml.load(config, Loader=yaml.FullLoader) config = yaml.load(config, Loader=yaml.FullLoader)
version = config.get("version", "unknown") version = config.get("version", "unknown")
+10
View File
@@ -18,6 +18,10 @@ from pretor.core.database.table.user import UserAuthority
from pretor.utils.ray_hook import ray_actor_hook from pretor.utils.ray_hook import ray_actor_hook
async def get_authority(user_id: str) -> UserAuthority: async def get_authority(user_id: str) -> UserAuthority:
"""检索并获取特定的 authority 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
Returns: (UserAuthority): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
from pretor.utils.error import UserNotExistError from pretor.utils.error import UserNotExistError
postgres_database = ray_actor_hook("postgres_database").postgres_database postgres_database = ray_actor_hook("postgres_database").postgres_database
try: try:
@@ -38,11 +42,17 @@ async def get_authority(user_id: str) -> UserAuthority:
raise raise
class RoleChecker: class RoleChecker:
"""RoleChecker 核心组件类。
这是一个领域数据模型或功能封装类,承载了 RoleChecker 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.allowed_roles = kwargs.get("allowed_roles", ) self.allowed_roles = kwargs.get("allowed_roles", )
async def __call__(self, async def __call__(self,
token_data: Annotated[TokenData, Depends(Accessor.get_current_user)]): 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) user_authority = await get_authority(token_data.user_id)
if user_authority < self.allowed_roles: if user_authority < self.allowed_roles:
raise HTTPException( raise HTTPException(
+18
View File
@@ -21,30 +21,48 @@ class NonRetryableError(Exception):
pass pass
class DemandError(NonRetryableError): class DemandError(NonRetryableError):
"""DemandError 核心组件类。
这是一个自定义异常类,专门用于在 Demand 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
pass pass
class ModelNotExistError(Exception): class ModelNotExistError(Exception):
"""ModelNotExistError 核心组件类。
这是一个自定义异常类,专门用于在 ModelNotExist 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
pass pass
class UserError(Exception): class UserError(Exception):
"""UserError 核心组件类。
这是一个自定义异常类,专门用于在 User 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
pass pass
class UserNotExistError(UserError): class UserNotExistError(UserError):
"""UserNotExistError 核心组件类。
这是一个自定义异常类,专门用于在 UserNotExist 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
pass pass
class UserPasswordError(UserError): class UserPasswordError(UserError):
"""UserPasswordError 核心组件类。
这是一个自定义异常类,专门用于在 UserPassword 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
pass pass
class ProviderError(Exception): class ProviderError(Exception):
"""ProviderError 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
pass pass
class ProviderNotExistError(ProviderError): class ProviderNotExistError(ProviderError):
"""ProviderNotExistError 核心组件类。
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。 """
pass pass
class WorkflowError(Exception): class WorkflowError(Exception):
"""WorkflowError 核心组件类。
这是一个自定义异常类,专门用于在 Workflow 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。 """
pass pass
class WorkflowExit(WorkflowError): class WorkflowExit(WorkflowError):
"""WorkflowExit 核心组件类。
这是一个领域数据模型或功能封装类,承载了 WorkflowExit 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
pass pass
+12 -2
View File
@@ -16,8 +16,6 @@ import importlib.util
import os import os
import sys import sys
from typing import Callable, Dict, List from typing import Callable, Dict, List
import pathlib
from pretor.utils.ray_hook import ray_actor_hook
from pretor.utils.logger import get_logger from pretor.utils.logger import get_logger
logger = get_logger('get_tool') logger = get_logger('get_tool')
@@ -25,6 +23,10 @@ _tool_cache: Dict[str, Callable] = {}
def _get_tool_func(tool_name: str) -> Callable | None: 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) func = _tool_cache.get(tool_name, None)
if func: if func:
return func return func
@@ -64,10 +66,18 @@ def _get_tool_func(tool_name: str) -> Callable | None:
return None return None
def del_tool_cache(tool_name: str) -> 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: if tool_name in _tool_cache:
del _tool_cache[tool_name] del _tool_cache[tool_name]
def load_tools_from_list(tool_names: List[str] | None) -> List[Callable]: 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: if not tool_names:
return [] return []
+11
View File
@@ -17,10 +17,17 @@ from rich.logging import RichHandler
from loguru._logger import Logger from loguru._logger import Logger
def setup_logger() -> Logger: def setup_logger() -> Logger:
"""对现有的 setup logger 进行状态更新或属性覆盖。
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
Returns: (Logger): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
logger.remove() logger.remove()
def format_record(record): def format_record(record):
# Format string for rich handler # Format string for rich handler
"""执行与 format record 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: record: 参与 format record 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
actor = record["extra"].get("actor_name", "System") actor = record["extra"].get("actor_name", "System")
trace_id = record["extra"].get("trace_id", "") trace_id = record["extra"].get("trace_id", "")
@@ -41,4 +48,8 @@ def setup_logger() -> Logger:
global_logger = setup_logger() global_logger = setup_logger()
def get_logger(actor_name: str, trace_id: str = "") -> 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) return global_logger.bind(actor_name=actor_name, trace_id=trace_id)
+3
View File
@@ -29,6 +29,9 @@ def pickle(cls: T) -> T:
""" """
def __reduce__(self): def __reduce__(self):
# 1. 序列化:触发 Pydantic-core (Rust) 的极速序列化 # 1. 序列化:触发 Pydantic-core (Rust) 的极速序列化
"""执行与 reduce 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
data = self.model_dump_json() data = self.model_dump_json()
# 2. 反序列化:告诉 Pickle 重建时调用 cls.model_validate_json # 2. 反序列化:告诉 Pickle 重建时调用 cls.model_validate_json
return cls.model_validate_json, (data,) return cls.model_validate_json, (data,)
+15
View File
@@ -15,18 +15,30 @@ import ray
from functools import lru_cache from functools import lru_cache
class ActorList: class ActorList:
"""ActorList 核心组件类。
这是一个领域数据模型或功能封装类,承载了 ActorList 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。 """
def __init__(self): def __init__(self):
super().__setattr__('dict', {}) super().__setattr__('dict', {})
def __setattr__(self, key, value): def __setattr__(self, key, value):
"""对现有的 setattr 进行状态更新或属性覆盖。
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
Args: key: 参与 setattr 逻辑运算或数据构建的上下文依赖对象。 value: 参与 setattr 逻辑运算或数据构建的上下文依赖对象。 """
self.dict[key] = value self.dict[key] = value
def __getattr__(self, key): def __getattr__(self, key):
"""检索并获取特定的 getattr 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: key: 参与 getattr 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
if key in self.dict: if key in self.dict:
return self.dict[key] return self.dict[key]
raise AttributeError(f"ActorList 对象没有属性 '{key}'") raise AttributeError(f"ActorList 对象没有属性 '{key}'")
def __delattr__(self, key): def __delattr__(self, key):
"""执行与 delattr 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: key: 参与 delattr 逻辑运算或数据构建的上下文依赖对象。 """
if key in self.dict: if key in self.dict:
del self.dict[key] del self.dict[key]
else: else:
@@ -42,6 +54,9 @@ def clear_actor_cache():
_get_cached_actor_handle.cache_clear() _get_cached_actor_handle.cache_clear()
def ray_actor_hook(*actor_names: str): def ray_actor_hook(*actor_names: str):
"""执行与 ray actor hook 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
actor_list = ActorList() actor_list = ActorList()
for actor_name in actor_names: for actor_name in actor_names:
handle = _get_cached_actor_handle(actor_name) handle = _get_cached_actor_handle(actor_name)
+28
View File
@@ -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 import asyncio
from functools import wraps from functools import wraps
from pretor.utils.error import RetryableError from pretor.utils.error import RetryableError
def retry_on_retryable_error(max_retries=3, base_delay=1): 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): def decorator(func):
"""执行与 decorator 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: func: 参与 decorator 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
if asyncio.iscoroutinefunction(func): if asyncio.iscoroutinefunction(func):
@wraps(func) @wraps(func)
async def async_wrapper(*args, **kwargs): async def async_wrapper(*args, **kwargs):
"""执行与 async wrapper 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
return await func(*args, **kwargs) return await func(*args, **kwargs)
@@ -19,6 +44,9 @@ def retry_on_retryable_error(max_retries=3, base_delay=1):
else: else:
@wraps(func) @wraps(func)
def sync_wrapper(*args, **kwargs): def sync_wrapper(*args, **kwargs):
"""执行与 sync wrapper 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
import time import time
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
+14
View File
@@ -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.base_individual import BaseIndividual
from pretor.worker_individual.skill_individual import SkillIndividual from pretor.worker_individual.skill_individual import SkillIndividual
from pretor.worker_individual.ordinary_individual import OrdinaryIndividual from pretor.worker_individual.ordinary_individual import OrdinaryIndividual
@@ -23,12 +23,18 @@ from pretor.utils.logger import get_logger
logger = get_logger('worker_individual') logger = get_logger('worker_individual')
class WorkerIndividualResponse(ResponseModel): class WorkerIndividualResponse(ResponseModel):
"""WorkerIndividualResponse 核心组件类。
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
output: str = Field(..., description="Worker执行任务的输出结果") output: str = Field(..., description="Worker执行任务的输出结果")
class WorkerIndividualDeps(DepsModel): class WorkerIndividualDeps(DepsModel):
"""WorkerIndividualDeps 核心组件类。
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
task_event: dict task_event: dict
class WorkerIndividualInput(InputModel): class WorkerIndividualInput(InputModel):
"""WorkerIndividualInput 核心组件类。
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。 """
task_event: dict task_event: dict
class BaseIndividual: class BaseIndividual:
@@ -42,6 +48,10 @@ class BaseIndividual:
self.agent: Agent | None = None self.agent: Agent | None = None
async def _init_agent(self, agent_name: str, system_prompt: str): 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 from pretor.utils.get_tool import load_tools_from_list
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
provider_title = self.agent_config.get("provider_title", "openai") # default fallback provider_title = self.agent_config.get("provider_title", "openai") # default fallback
@@ -65,6 +75,10 @@ class BaseIndividual:
@self.agent.system_prompt @self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[WorkerIndividualDeps]): async def dynamic_prompt(ctx: RunContext[WorkerIndividualDeps]):
"""执行与 dynamic prompt 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: ctx (RunContext[WorkerIndividualDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
prompt = system_prompt + "\n\n" prompt = system_prompt + "\n\n"
prompt += ( prompt += (
f"=== 当前任务上下文 ===\n" f"=== 当前任务上下文 ===\n"
@@ -73,4 +87,8 @@ class BaseIndividual:
return prompt return prompt
async def run(self, task_event: dict) -> dict: async def run(self, task_event: dict) -> dict:
"""执行与 run 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
raise NotImplementedError("子类必须实现 run 方法") raise NotImplementedError("子类必须实现 run 方法")
@@ -26,6 +26,10 @@ class OrdinaryIndividual(BaseIndividual):
super().__init__(agent_config) super().__init__(agent_config)
async def run(self, task_event: dict) -> dict: async def run(self, task_event: dict) -> dict:
"""执行与 run 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
if self.agent is None: if self.agent is None:
system_prompt = self.agent_config.get("prompt", "你是一个普通的AI助手,请尽力完成给定的任务。") system_prompt = self.agent_config.get("prompt", "你是一个普通的AI助手,请尽力完成给定的任务。")
await self._init_agent("ordinary_individual", system_prompt) await self._init_agent("ordinary_individual", system_prompt)
@@ -88,6 +88,10 @@ class SkillIndividual(BaseIndividual):
return tools return tools
async def run(self, task_event: dict) -> dict: async def run(self, task_event: dict) -> dict:
"""执行与 run 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
if self.agent is None: if self.agent is None:
system_prompt = self.agent_config.get("prompt", system_prompt = self.agent_config.get("prompt",
"你是一个拥有专业技能的专家级AI助手,请利用你的专业知识完成给定的任务。") "你是一个拥有专业技能的专家级AI助手,请利用你的专业知识完成给定的任务。")
@@ -26,6 +26,10 @@ class SpecialIndividual(BaseIndividual):
super().__init__(agent_config) super().__init__(agent_config)
async def run(self, task_event: dict) -> dict: async def run(self, task_event: dict) -> dict:
"""执行与 run 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。 """
if self.agent is None: if self.agent is None:
system_prompt = self.agent_config.get("prompt", "你是一个特殊的AI助手,负责处理特殊类型的任务。") system_prompt = self.agent_config.get("prompt", "你是一个特殊的AI助手,负责处理特殊类型的任务。")
await self._init_agent("special_individual", system_prompt) await self._init_agent("special_individual", system_prompt)
@@ -45,6 +45,8 @@ class WorkerCluster:
self.logger = get_logger('worker_cluster') self.logger = get_logger('worker_cluster')
async def start(self): async def start(self):
"""执行与 start 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。 """
if self.task_queue is None: if self.task_queue is None:
self.task_queue = Queue() self.task_queue = Queue()
self.runners = [asyncio.create_task(self._runner(i)) for i in range(self.num_runners)] self.runners = [asyncio.create_task(self._runner(i)) for i in range(self.num_runners)]
@@ -78,6 +80,9 @@ class WorkerCluster:
return worker return worker
async def _runner(self, runner_id: int): async def _runner(self, runner_id: int):
"""执行与 runner 相关的核心业务流转操作。
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
Args: runner_id (int): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 runner 实例。 """
while True: while True:
try: try:
if self.task_queue is None: if self.task_queue is None:
@@ -119,6 +124,10 @@ class WorkerCluster:
await asyncio.sleep(1) await asyncio.sleep(1)
async def submit_task(self, task_id: str, agent_id: str, task_event: dict): 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: if not self.runners:
await self.start() await self.start()
@@ -140,6 +149,9 @@ class WorkerCluster:
self.results_futures.pop(task_id, None) self.results_futures.pop(task_id, None)
def get_cluster_metrics(self): def get_cluster_metrics(self):
"""检索并获取特定的 cluster metrics 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。 """
return { return {
"active_worker_count": len(self._active_workers), "active_worker_count": len(self._active_workers),
"max_capacity": self.max_capacity, "max_capacity": self.max_capacity,
-145
View File
@@ -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 pytest
import asyncio
from unittest.mock import MagicMock, AsyncMock, patch from unittest.mock import MagicMock, AsyncMock, patch
import sys import sys
import builtins import builtins
real_import = builtins.__import__ real_import = builtins.__import__
@@ -31,8 +29,6 @@ for mod in list(sys.modules.keys()):
del sys.modules[mod] del sys.modules[mod]
from pretor.core.global_state_machine.global_state_machine import GlobalStateMachine 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 builtins.__import__ = real_import
@@ -48,53 +44,6 @@ def gsm(mock_postgres):
return manager 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 @pytest.mark.asyncio
async def test_add_provider_success(gsm, mock_postgres): async def test_add_provider_success(gsm, mock_postgres):
mock_provider_class = AsyncMock() mock_provider_class = AsyncMock()
+8 -3
View File
@@ -147,16 +147,21 @@ async def test_workflow_running_engine_runner():
# Mock the global_state_machine get_skill_list.remote method properly # Mock the global_state_machine get_skill_list.remote method properly
mock_gsm = MagicMock() 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 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 # 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_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 = MagicMock()
mock_engine_instance.run = AsyncMock() mock_engine_instance.run = AsyncMock()
mock_wf_engine_cls.return_value = mock_engine_instance mock_wf_engine_cls.return_value = mock_engine_instance