feat: workflow和chat分离

1,增加了创建workflow的页面
2.删除了event
This commit is contained in:
2026-05-14 15:51:28 +00:00
parent c0e4fd34ae
commit 78bd6adc48
30 changed files with 1196 additions and 760 deletions
+45 -18
View File
@@ -1,16 +1,18 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { TopBar } from './components/Layout/TopBar'; import { TopBar } from './components/Layout/TopBar';
import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar'; 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 { PluginLayout } from './components/Plugin/PluginLayout'; // Will rename to PluginLayout soon import { PluginLayout } from './components/Plugin/PluginLayout';
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 { WorkflowListView } from './components/Chat/WorkflowListView';
import { NewWorkflowDialog } from './components/Chat/NewWorkflowDialog';
import { AuthPage } from './components/Auth/AuthPage'; import { AuthPage } from './components/Auth/AuthPage';
import apiClient from './api/client';
import type { ChatSessionDB } from './types';
// For Chat Module State Persistence
export interface Message { export interface Message {
id: string; id: string;
role: 'user' | 'assistant' | 'system'; role: 'user' | 'assistant' | 'system';
@@ -25,33 +27,43 @@ export interface ChatSession {
updatedAt: number; updatedAt: number;
} }
function mapSessionFromDB(s: ChatSessionDB): ChatSession {
return {
id: s.chat_id,
title: s.title,
messages: [],
updatedAt: new Date(s.updated_at).getTime(),
};
}
function App() { function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
// Layout State
const [mode, setMode] = useState<'work' | 'agent'>('work'); const [mode, setMode] = useState<'work' | 'agent'>('work');
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(true);
// Module Sub-navigation States
// Work Mode
const [workTab, setWorkTab] = useState<'chat' | 'workflow'>('chat'); 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'); const [agentTab, setAgentTab] = useState<'plugin' | 'agents'>('plugin');
// Settings Sub-tab
const [settingsTab, setSettingsTab] = useState('users'); const [settingsTab, setSettingsTab] = useState('users');
// Inner Agent Tab (temporary until full Agent layout rewrite)
const [innerAgentTab, setInnerAgentTab] = useState('worker'); const [innerAgentTab, setInnerAgentTab] = useState('worker');
const [resourceTab, setResourceTab] = useState('skill'); const [resourceTab, setResourceTab] = useState('skill');
// Chat State Hoisted for Persistence
const [chatSessions, setChatSessions] = useState<ChatSession[]>([]); const [chatSessions, setChatSessions] = useState<ChatSession[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null); const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const loadChatSessions = useCallback(async () => {
try {
const response = await apiClient.get('/api/v1/chat');
const sessions: ChatSessionDB[] = response.data?.sessions || [];
setChatSessions(sessions.map(mapSessionFromDB));
} catch (error) {
console.error('Failed to load chat sessions', error);
}
}, []);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
@@ -59,13 +71,18 @@ function App() {
} }
}, []); }, []);
useEffect(() => {
if (isAuthenticated) {
loadChatSessions();
}
}, [isAuthenticated, loadChatSessions]);
if (!isAuthenticated) { if (!isAuthenticated) {
return <AuthPage onLoginSuccess={() => setIsAuthenticated(true)} />; return <AuthPage onLoginSuccess={() => setIsAuthenticated(true)} />;
} }
return ( return (
<div className="flex flex-col 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 <TopBar
mode={mode} mode={mode}
setMode={setMode} setMode={setMode}
@@ -73,13 +90,11 @@ function App() {
setShowSettings={setShowSettings} setShowSettings={setShowSettings}
/> />
{/* 2. Main Content Area */}
<div className="flex flex-1 overflow-hidden relative"> <div className="flex flex-1 overflow-hidden relative">
{showSettings ? ( {showSettings ? (
<SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} /> <SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} />
) : ( ) : (
<> <>
{/* Collapsible Main Sidebar */}
<CollapsibleSidebar <CollapsibleSidebar
mode={mode} mode={mode}
isOpen={isSidebarOpen} isOpen={isSidebarOpen}
@@ -90,7 +105,6 @@ function App() {
setAgentTab={setAgentTab} setAgentTab={setAgentTab}
/> />
{/* Dynamic View based on Mode and Tab */}
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
{mode === 'work' && workTab === 'chat' && ( {mode === 'work' && workTab === 'chat' && (
<div className="flex-1 p-6 flex overflow-hidden"> <div className="flex-1 p-6 flex overflow-hidden">
@@ -99,17 +113,18 @@ function App() {
activeTab="chats" activeTab="chats"
selectedWorkflow={null} selectedWorkflow={null}
setSelectedWorkflow={() => {}} setSelectedWorkflow={() => {}}
// Pass hoisted state down
chatSessions={chatSessions} chatSessions={chatSessions}
setChatSessions={setChatSessions} setChatSessions={setChatSessions}
activeSessionId={activeSessionId} activeSessionId={activeSessionId}
setActiveSessionId={setActiveSessionId} setActiveSessionId={setActiveSessionId}
onSessionsChanged={loadChatSessions}
/> />
<ChatPanel <ChatPanel
chatSessions={chatSessions} chatSessions={chatSessions}
setChatSessions={setChatSessions} setChatSessions={setChatSessions}
activeSessionId={activeSessionId} activeSessionId={activeSessionId}
setActiveSessionId={setActiveSessionId} setActiveSessionId={setActiveSessionId}
onSessionsChanged={loadChatSessions}
/> />
</div> </div>
</div> </div>
@@ -117,7 +132,19 @@ function App() {
{mode === 'work' && workTab === 'workflow' && ( {mode === 'work' && workTab === 'workflow' && (
<> <>
{selectedWorkflow ? ( {selectedWorkflow === 'new' ? (
<>
<LeftPanel
activeTab="workflows"
selectedWorkflow={selectedWorkflow}
setSelectedWorkflow={setSelectedWorkflow}
/>
<NewWorkflowDialog
onClose={() => setSelectedWorkflow(null)}
onSuccess={(traceId: string) => setSelectedWorkflow(traceId)}
/>
</>
) : selectedWorkflow ? (
<> <>
<LeftPanel <LeftPanel
activeTab="workflows" activeTab="workflows"
+95 -87
View File
@@ -1,19 +1,22 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { MessageSquare, Activity, Terminal, ChevronRight, Plus } from 'lucide-react'; import { MessageSquare, Activity, ChevronRight, Plus } from 'lucide-react';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
import type { ChatSession, Message } from '../../App'; import type { ChatSession, Message } from '../../App';
import type { ChatMessageDB } from '../../types';
interface ChatPanelProps { interface ChatPanelProps {
chatSessions: ChatSession[]; chatSessions: ChatSession[];
setChatSessions: React.Dispatch<React.SetStateAction<ChatSession[]>>; setChatSessions: React.Dispatch<React.SetStateAction<ChatSession[]>>;
activeSessionId: string | null; activeSessionId: string | null;
setActiveSessionId: React.Dispatch<React.SetStateAction<string | null>>; setActiveSessionId: React.Dispatch<React.SetStateAction<string | null>>;
onSessionsChanged?: () => void;
} }
export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId }: ChatPanelProps) { export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId, onSessionsChanged }: ChatPanelProps) {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [mode, setMode] = useState<'chat' | 'deploy'>('chat'); const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
const [loadingMessages, setLoadingMessages] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -28,6 +31,36 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
scrollToBottom(); scrollToBottom();
}, [messages]); }, [messages]);
const loadMessages = useCallback(async (chatId: string) => {
setLoadingMessages(true);
try {
const response = await apiClient.get(`/api/v1/chat/${chatId}`);
const dbMessages: ChatMessageDB[] = response.data?.messages || [];
const mapped: Message[] = dbMessages.map((m) => ({
id: m.message_id,
role: m.message_owner === 'user' ? 'user' : 'assistant',
content: m.message,
timestamp: new Date(m.created_at).getTime(),
}));
setChatSessions((prev) =>
prev.map((s) => (s.id === chatId ? { ...s, messages: mapped } : s))
);
} catch (error) {
console.error('Failed to load messages', error);
} finally {
setLoadingMessages(false);
}
}, [setChatSessions]);
useEffect(() => {
if (activeSessionId) {
const session = chatSessions.find((s) => s.id === activeSessionId);
if (session && session.messages.length === 0) {
loadMessages(activeSessionId);
}
}
}, [activeSessionId]);
const updateSessionMessages = (newMessages: Message[]) => { const updateSessionMessages = (newMessages: Message[]) => {
if (!activeSessionId) return; if (!activeSessionId) return;
setChatSessions((prev) => setChatSessions((prev) =>
@@ -39,39 +72,30 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
); );
}; };
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleNewChat = async () => {
const file = e.target.files?.[0]; if (!onSessionsChanged) return;
if (!file || !activeSessionId) return;
const formData = new FormData();
formData.append('file', file);
setLoading(true);
try { try {
const response = await apiClient.post('/api/v1/adapter/client/upload', formData, { const response = await apiClient.post('/api/v1/chat', {
headers: { 'Content-Type': 'multipart/form-data' }, title: '新对话',
initial_message: '你好',
}); });
const aiMessage: Message = { const chatId: string = response.data.chat_id;
id: Date.now().toString(), const reply: string = response.data.reply || '你好!我是 kilostar 助手,有什么可以帮你的吗?';
role: 'assistant',
content: `已上传文件: ${response.data.filename}`, const newSession: ChatSession = {
timestamp: Date.now(), id: chatId,
title: '新对话',
messages: [
{ id: chatId + '_user', role: 'user', content: '你好', timestamp: Date.now() },
{ id: chatId + '_ai', role: 'assistant', content: reply, timestamp: Date.now() },
],
updatedAt: Date.now(),
}; };
updateSessionMessages([...messages, aiMessage]); setChatSessions((prev) => [newSession, ...prev]);
setActiveSessionId(chatId);
onSessionsChanged();
} catch (error) { } catch (error) {
console.error('Error uploading file', error); console.error('Failed to create chat session', error);
const errorMessage: Message = {
id: Date.now().toString(),
role: 'assistant',
content: '文件上传失败。',
timestamp: Date.now(),
};
updateSessionMessages([...messages, errorMessage]);
} finally {
setLoading(false);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} }
}; };
@@ -86,21 +110,19 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
timestamp: Date.now(), timestamp: Date.now(),
}; };
updateSessionMessages([...messages, userMessage]); const currentMessages = activeSession?.messages || [];
updateSessionMessages([...currentMessages, userMessage]);
setInput(''); setInput('');
setLoading(true); setLoading(true);
try { try {
const promptModifier = mode === 'deploy' ? '[DEPLOY TASK] ' : ''; const response = await apiClient.post(`/api/v1/chat/${activeSessionId}/reply`, {
const response = await apiClient.post('/api/v1/adapter/client', { message: userText,
message: promptModifier + userMessage.content,
}); });
const responseData = response.data.message; const replyContent: string = response.data?.reply || '收到你的消息。';
let aiContent = responseData || 'I received your message.';
// Auto-update title if it's the first user message if (currentMessages.length <= 1 && userText.length > 0) {
if (messages.length <= 1 && userText.length > 0) {
setChatSessions((prev) => setChatSessions((prev) =>
prev.map((s) => prev.map((s) =>
s.id === activeSessionId s.id === activeSessionId
@@ -113,20 +135,21 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
const aiMessage: Message = { const aiMessage: Message = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
role: 'assistant', role: 'assistant',
content: aiContent, content: replyContent,
timestamp: Date.now(), timestamp: Date.now(),
}; };
updateSessionMessages([...messages, userMessage, aiMessage]); const updatedMessages = [...(currentMessages.length > 0 ? currentMessages : []), userMessage, aiMessage];
updateSessionMessages(updatedMessages);
} catch (error) { } catch (error) {
console.error('Error sending message', error); console.error('Error sending message', error);
const errorMessage: Message = { const errorMessage: Message = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
role: 'assistant', role: 'assistant',
content: 'Sorry, I encountered an error communicating with the server.', content: '抱歉,与服务器通信时出错。',
timestamp: Date.now(), timestamp: Date.now(),
}; };
updateSessionMessages([...messages, userMessage, errorMessage]); updateSessionMessages([...currentMessages, userMessage, errorMessage]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -139,23 +162,7 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
<h2 className="text-xl font-semibold text-slate-600">kilostar Assistant</h2> <h2 className="text-xl font-semibold text-slate-600">kilostar Assistant</h2>
<p className="text-slate-400 mt-2">Select a chat history or create a new one to start.</p> <p className="text-slate-400 mt-2">Select a chat history or create a new one to start.</p>
<button <button
onClick={() => { onClick={handleNewChat}
const newSession: ChatSession = {
id: Date.now().toString(),
title: 'New Chat',
messages: [
{
id: Date.now().toString(),
role: 'assistant',
content: 'Hello! I am kilostar 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" 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 Start New Chat
@@ -164,7 +171,6 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
); );
} }
// 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-white overflow-hidden relative"> <div className="flex-1 flex flex-col bg-white overflow-hidden relative">
<div className="h-14 border-b border-slate-100 bg-white flex items-center justify-between px-6 z-10 shrink-0"> <div className="h-14 border-b border-slate-100 bg-white flex items-center justify-between px-6 z-10 shrink-0">
@@ -192,32 +198,35 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
</div> </div>
</div> </div>
{/* Chat History */}
<div className="flex-1 p-6 overflow-y-auto space-y-6 bg-white"> <div className="flex-1 p-6 overflow-y-auto space-y-6 bg-white">
{messages.map((msg) => ( {loadingMessages ? (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}> <div className="flex justify-center items-center h-full">
{msg.role === 'assistant' && ( <div className="flex space-x-2">
<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"> <span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
<Activity size={16} className="text-blue-600" /> <span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
</div> <span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
)}
<div
className={`${
msg.role === 'user'
? 'bg-blue-100 text-slate-800 rounded-2xl rounded-tr-sm'
: 'bg-slate-50 border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'
} p-4 max-w-[80%] shadow-sm`}
>
<p className="text-sm leading-relaxed mb-1 whitespace-pre-wrap">{msg.content}</p>
{typeof msg.content === 'string' && msg.content.includes('-') && msg.role === 'assistant' && (msg.content.length === 36 || msg.content.includes('任务已创建')) && (
<div className="mt-2 bg-white border border-slate-100 rounded-lg p-3 flex items-center text-sm shadow-sm">
<Terminal size={16} className="text-slate-400 mr-2" />
<span className="font-mono text-slate-600 text-xs">Task ID: {msg.content.substring(0, 36)}</span>
</div>
)}
</div> </div>
</div> </div>
))} ) : (
messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.role === 'assistant' && (
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
<Activity size={16} className="text-blue-600" />
</div>
)}
<div
className={`${
msg.role === 'user'
? 'bg-blue-100 text-slate-800 rounded-2xl rounded-tr-sm'
: 'bg-slate-50 border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'
} p-4 max-w-[80%] shadow-sm`}
>
<p className="text-sm leading-relaxed mb-1 whitespace-pre-wrap">{msg.content}</p>
</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">
@@ -235,10 +244,9 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
{/* Chat Input */}
<div className="p-4 bg-white border-t border-slate-100 shrink-0"> <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 type="file" ref={fileInputRef} onChange={handleFileUpload} className="hidden" /> <input type="file" ref={fileInputRef} 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"
+37 -27
View File
@@ -8,11 +8,11 @@ interface LeftPanelProps {
activeTab: string; activeTab: string;
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[]; chatSessions?: ChatSession[];
setChatSessions?: React.Dispatch<React.SetStateAction<ChatSession[]>>; setChatSessions?: React.Dispatch<React.SetStateAction<ChatSession[]>>;
activeSessionId?: string | null; activeSessionId?: string | null;
setActiveSessionId?: React.Dispatch<React.SetStateAction<string | null>>; setActiveSessionId?: React.Dispatch<React.SetStateAction<string | null>>;
onSessionsChanged?: () => void;
} }
export function LeftPanel({ export function LeftPanel({
@@ -23,6 +23,7 @@ export function LeftPanel({
setChatSessions, setChatSessions,
activeSessionId, activeSessionId,
setActiveSessionId, setActiveSessionId,
onSessionsChanged,
}: LeftPanelProps) { }: LeftPanelProps) {
const [workflows, setWorkflows] = useState<Workflow[]>([]); const [workflows, setWorkflows] = useState<Workflow[]>([]);
const [loadingWorkflows, setLoadingWorkflows] = useState(false); const [loadingWorkflows, setLoadingWorkflows] = useState(false);
@@ -60,23 +61,31 @@ export function LeftPanel({
}; };
}, [activeTab]); }, [activeTab]);
const handleNewChat = () => { const handleNewChat = async () => {
if (!setChatSessions || !setActiveSessionId) return; if (!setChatSessions || !setActiveSessionId || !onSessionsChanged) return;
const newSession: ChatSession = { try {
id: Date.now().toString(), const response = await apiClient.post('/api/v1/chat', {
title: 'New Chat', title: '新对话',
messages: [ initial_message: '你好',
{ });
id: Date.now().toString(), const chatId: string = response.data.chat_id;
role: 'assistant', const reply: string = response.data.reply || '你好!我是 kilostar 助手,有什么可以帮你的吗?';
content: 'Hello! I am kilostar Assistant. How can I help you today?',
timestamp: Date.now(), const newSession: ChatSession = {
}, id: chatId,
], title: '新对话',
updatedAt: Date.now(), messages: [
}; { id: chatId + '_user', role: 'user', content: '你好', timestamp: Date.now() },
setChatSessions((prev) => [newSession, ...prev]); { id: chatId + '_ai', role: 'assistant', content: reply, timestamp: Date.now() },
setActiveSessionId(newSession.id); ],
updatedAt: Date.now(),
};
setChatSessions((prev) => [newSession, ...prev]);
setActiveSessionId(chatId);
onSessionsChanged();
} catch (error) {
console.error('Failed to create chat session', error);
}
}; };
const handleDeleteChat = (e: React.MouseEvent, id: string) => { const handleDeleteChat = (e: React.MouseEvent, id: string) => {
@@ -91,7 +100,6 @@ export function LeftPanel({
return ( return (
<div className="w-72 bg-white border-r border-slate-100 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">
{/* Bottom: Tab Selection */}
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-slate-100 bg-slate-50"> <div className="flex items-center justify-between p-3 border-b border-slate-100 bg-slate-50">
@@ -103,7 +111,7 @@ export function LeftPanel({
if (activeTab === 'chats') { if (activeTab === 'chats') {
handleNewChat(); handleNewChat();
} else { } else {
setSelectedWorkflow('new'); // 设置为一个特殊值,表示进入新建工作流向导 setSelectedWorkflow('new');
} }
}} }}
className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors" className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
@@ -123,10 +131,10 @@ export function LeftPanel({
) : ( ) : (
workflows.map((wf) => ( workflows.map((wf) => (
<div <div
key={wf.event_id} key={wf.trace_id}
onClick={() => setSelectedWorkflow(wf.event_id)} onClick={() => setSelectedWorkflow(wf.trace_id)}
className={`p-3 rounded-lg border cursor-pointer transition-all ${ className={`p-3 rounded-lg border cursor-pointer transition-all ${
selectedWorkflow === wf.event_id selectedWorkflow === wf.trace_id
? 'border-blue-300 bg-blue-50 shadow-sm' ? 'border-blue-300 bg-blue-50 shadow-sm'
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50' : 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
}`} }`}
@@ -134,22 +142,24 @@ export function LeftPanel({
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">
<span <span
className={`font-medium text-sm ${ className={`font-medium text-sm ${
selectedWorkflow === wf.event_id ? 'text-blue-700' : 'text-slate-700' selectedWorkflow === wf.trace_id ? 'text-blue-700' : 'text-slate-700'
}`} }`}
> >
{wf.workflow_title || 'Unnamed Workflow'} {wf.title || 'Unnamed Workflow'}
</span> </span>
<span <span
className={`flex h-2 w-2 rounded-full ${ className={`flex h-2 w-2 rounded-full ${
wf.status === 'llm_working' || wf.status === 'tool_working' wf.status && (wf.status.includes('working'))
? 'bg-green-400 animate-pulse' ? 'bg-green-400 animate-pulse'
: wf.status === 'failed' : wf.status === 'failed'
? 'bg-red-400' ? 'bg-red-400'
: wf.status === 'completed'
? 'bg-green-500'
: 'bg-slate-300' : 'bg-slate-300'
}`} }`}
></span> ></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.trace_id}</p>
</div> </div>
)) ))
)} )}
@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Terminal, X, ArrowRight } from 'lucide-react'; import { Terminal, Sparkles, ArrowLeft } from 'lucide-react';
import apiClient from '../../api/client'; import apiClient from '../../api/client';
interface NewWorkflowDialogProps { interface NewWorkflowDialogProps {
@@ -13,10 +13,14 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e?: React.FormEvent) => {
e.preventDefault(); if (e) e.preventDefault();
if (!command.trim() || !title.trim()) { if (!title.trim()) {
setError('标题和需求描述不能为空'); setError('请输入工作流标题');
return;
}
if (!command.trim()) {
setError('请输入具体需求描述');
return; return;
} }
@@ -25,8 +29,8 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
try { try {
const response = await apiClient.post('/api/v1/workflow', { const response = await apiClient.post('/api/v1/workflow', {
title: title, title: title.trim(),
command: command command: command.trim()
}); });
if (response.data && response.data.trace_id) { if (response.data && response.data.trace_id) {
onSuccess(response.data.trace_id); onSuccess(response.data.trace_id);
@@ -38,67 +42,110 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
} }
}; };
return ( const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
<div className="absolute inset-0 bg-white/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"> if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
<div className="bg-white rounded-2xl shadow-xl border border-slate-200 w-full max-w-md overflow-hidden"> e.preventDefault();
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-100 bg-slate-50"> handleSubmit();
<div className="flex items-center gap-2"> }
<Terminal size={20} className="text-blue-600" /> };
<h2 className="font-semibold text-slate-800"></h2>
</div>
<button onClick={onClose} className="p-1 text-slate-400 hover:bg-slate-200 rounded-lg transition-colors">
<X size={18} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4"> return (
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shrink-0">
<button
onClick={onClose}
className="flex items-center gap-1.5 text-sm text-slate-500 hover:text-blue-600 transition-colors"
>
<ArrowLeft size={16} />
</button>
<div className="flex-1 flex items-center justify-center gap-2">
<Terminal size={18} className="text-blue-600" />
<h2 className="font-semibold text-slate-800"></h2>
</div>
<div className="w-28" />
</div>
<form onSubmit={handleSubmit} className="flex-1 p-6 overflow-y-auto">
<div className="max-w-3xl mx-auto h-full flex flex-col gap-6">
{error && ( {error && (
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-100 rounded-lg"> <div className="flex items-center gap-2 p-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-xl shrink-0">
<span className="text-base"></span>
{error} {error}
</div> </div>
)} )}
<div> <div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 shrink-0">
<label className="block text-sm font-medium text-slate-700 mb-1"></label> <label className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-3">
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 text-xs font-bold">1</span>
</label>
<p className="text-xs text-slate-400 mb-3"></p>
<input <input
type="text" type="text"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="例如:爬取最新技术新闻" placeholder="例如:爬取最新技术新闻并生成摘要"
className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" className="w-full px-4 py-3 text-lg font-medium text-slate-800 bg-slate-50 border border-slate-200 rounded-xl placeholder:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 focus:bg-white transition-all"
autoFocus autoFocus
/> />
</div> </div>
<div> <div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 flex flex-col flex-1 min-h-0">
<label className="block text-sm font-medium text-slate-700 mb-1"></label> <label className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-3 shrink-0">
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 text-xs font-bold">2</span>
</label>
<p className="text-xs text-slate-400 mb-3 shrink-0"> AI </p>
<textarea <textarea
value={command} value={command}
onChange={(e) => setCommand(e.target.value)} onChange={(e) => setCommand(e.target.value)}
placeholder="详细描述您希望工作流完成的任务..." onKeyDown={handleKeyDown}
className="w-full px-3 py-2 border border-slate-200 rounded-lg h-32 resize-none focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" placeholder="例如:请帮我自动执行以下任务:
/>
</div>
<div className="pt-2 flex justify-end gap-2"> 1. 访问指定的技术新闻网站
<button 2. 抓取过去24小时内发布的所有文章
type="button" 3. 对每篇文章进行智能摘要
onClick={onClose} 4. 将结果整理为结构化表格
className="px-4 py-2 text-sm font-medium text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors" 5. 通过邮件发送给我
>
提示:按 Ctrl+Enter 快速提交"
</button> className="flex-1 w-full px-4 py-3 text-sm text-slate-700 bg-slate-50 border border-slate-200 rounded-xl resize-none placeholder:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 focus:bg-white transition-all min-h-[200px]"
<button />
type="submit" <div className="flex items-center justify-between mt-3 shrink-0">
disabled={loading} <span className="text-xs text-slate-400">
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50" {command.length > 0 ? `已输入 ${command.length} 个字符` : 'Ctrl + Enter 快速提交'}
> </span>
{loading ? '创建中...' : '开始创建'} <div className="flex items-center gap-2">
{!loading && <ArrowRight size={16} />} <button
</button> type="button"
onClick={onClose}
className="px-5 py-2.5 text-sm font-medium text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-xl transition-colors"
>
</button>
<button
type="submit"
disabled={loading}
className="flex items-center gap-2 px-6 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-xl transition-all shadow-sm hover:shadow-md disabled:opacity-50 disabled:hover:bg-blue-600"
>
{loading ? (
<>
<Sparkles size={16} className="animate-spin" />
...
</>
) : (
<>
<Sparkles size={16} />
</>
)}
</button>
</div>
</div>
</div> </div>
</form> </div>
</div> </form>
</div> </div>
); );
} }
+6 -9
View File
@@ -23,7 +23,8 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
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);
} catch { } catch (err) {
console.error('Failed to fetch workflow detail', err);
setDetail(null); setDetail(null);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -38,7 +39,7 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
} }
fetchDetail(selectedWorkflow); fetchDetail(selectedWorkflow);
setLogs([]); // Reset logs when changing workflow setLogs([]);
const protocol = window.location.protocol; const protocol = window.location.protocol;
const host = window.location.host; const host = window.location.host;
@@ -95,8 +96,8 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<h2 className="font-semibold text-slate-800 flex items-center gap-2"> <h2 className="font-semibold text-slate-800 flex items-center gap-2">
<Terminal size={18} className="text-blue-500" /> <Terminal size={18} className="text-blue-500" />
<span className="truncate max-w-[200px]" title={detail?.workflow_title || 'Loading...'}> <span className="truncate max-w-[200px]" title={detail?.title || 'Loading...'}>
{detail?.workflow_title || 'Workflow Details'} {detail?.title || 'Workflow Details'}
</span> </span>
</h2> </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'}`}> <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'}`}>
@@ -104,7 +105,6 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
</span> </span>
</div> </div>
{/* Navigation Tabs */}
<div className="flex items-center bg-slate-100 rounded-lg p-1"> <div className="flex items-center bg-slate-100 rounded-lg p-1">
<button <button
onClick={() => setActiveTab('chat')} onClick={() => setActiveTab('chat')}
@@ -135,7 +135,7 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
{activeTab === 'diagram' ? ( {activeTab === 'diagram' ? (
<div className="absolute inset-0"> <div className="absolute inset-0">
{detail?.steps && detail.steps.length > 0 ? ( {detail?.steps && detail.steps.length > 0 ? (
<WorkflowDiagram steps={detail.steps} currentStep={detail.current_step} status={detail.status} /> <WorkflowDiagram steps={detail.steps} currentStep={0} status={detail.status} />
) : ( ) : (
<div className="h-full flex items-center justify-center text-slate-400"> <div className="h-full flex items-center justify-center text-slate-400">
Workflow steps are not yet generated. Workflow steps are not yet generated.
@@ -144,7 +144,6 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
</div> </div>
) : ( ) : (
<div className="flex-1 flex flex-col p-6 overflow-hidden"> <div className="flex-1 flex flex-col p-6 overflow-hidden">
{/* Command Header */}
{detail?.command && ( {detail?.command && (
<div className="bg-white border border-slate-200 rounded-xl p-4 mb-4 shadow-sm shrink-0"> <div className="bg-white border border-slate-200 rounded-xl p-4 mb-4 shadow-sm shrink-0">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Original Command</h3> <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Original Command</h3>
@@ -152,7 +151,6 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
</div> </div>
)} )}
{/* Live Chat / Logs Area */}
<div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-3 font-mono text-sm"> <div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-3 font-mono text-sm">
{logs.length === 0 ? ( {logs.length === 0 ? (
<div className="h-full flex items-center justify-center text-slate-400"> <div className="h-full flex items-center justify-center text-slate-400">
@@ -168,7 +166,6 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
<div ref={logsEndRef} /> <div ref={logsEndRef} />
</div> </div>
{/* Input Area */}
<form onSubmit={handleReplySubmit} className="relative shrink-0"> <form onSubmit={handleReplySubmit} className="relative shrink-0">
<input <input
type="text" type="text"
@@ -38,7 +38,7 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
const getStatusIcon = (status?: string) => { const getStatusIcon = (status?: string) => {
if (status === 'completed') return <CheckCircle size={20} className="text-green-500" />; if (status === 'completed') return <CheckCircle size={20} className="text-green-500" />;
if (status === 'failed') return <XCircle size={20} className="text-red-500" />; if (status === 'failed') return <XCircle size={20} className="text-red-500" />;
if (status === 'llm_working' || status === 'tool_working' || status?.includes('working')) if (status && (status.includes('working')))
return <PlayCircle size={20} className="text-blue-500 animate-pulse" />; return <PlayCircle size={20} className="text-blue-500 animate-pulse" />;
return <Clock size={20} className="text-slate-400" />; return <Clock size={20} className="text-slate-400" />;
}; };
@@ -49,7 +49,7 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
if (status === 'completed') { colorClass = "bg-green-100 text-green-700"; label = "Completed"; } 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 === '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"; } else if (status && status.includes('working')) { colorClass = "bg-blue-100 text-blue-700 animate-pulse"; label = "Running"; }
return ( return (
<span className={`px-2.5 py-1 text-xs font-medium rounded-full ${colorClass}`}> <span className={`px-2.5 py-1 text-xs font-medium rounded-full ${colorClass}`}>
@@ -95,8 +95,8 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{workflows.map((wf) => ( {workflows.map((wf) => (
<div <div
key={wf.event_id} key={wf.trace_id}
onClick={() => onSelectWorkflow(wf.event_id)} onClick={() => onSelectWorkflow(wf.trace_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" 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="flex justify-between items-start mb-4">
@@ -106,33 +106,24 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
{getStatusBadge(wf.status)} {getStatusBadge(wf.status)}
</div> </div>
<h3 className="text-lg font-semibold text-slate-800 mb-2 line-clamp-1" title={wf.workflow_title || 'Unnamed Workflow'}> <h3 className="text-lg font-semibold text-slate-800 mb-2 line-clamp-1" title={wf.title || 'Unnamed Workflow'}>
{wf.workflow_title || 'Unnamed Workflow'} {wf.title || 'Unnamed Workflow'}
</h3> </h3>
<div className="mt-auto"> <div className="mt-auto">
{(wf as any).command && ( {wf.command && (
<div className="text-sm text-slate-500 line-clamp-2 mt-4 bg-slate-50 p-3 rounded-lg border border-slate-100"> <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> <span className="font-medium text-slate-600 block mb-1">Command:</span>
"{(wf as any).command}" "{wf.command}"
</div>
)}
{wf.message && !(wf as any).command && (
<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>
)} )}
<div className="flex justify-between items-center mt-5 text-xs text-slate-400"> <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 as any).trace_id}> <span className="font-mono bg-slate-100 px-2 py-1 rounded truncate max-w-[140px]" title={wf.trace_id}>
{wf.event_id || (wf as any).trace_id} {wf.trace_id}
</span> </span>
{wf.create_time && ( {wf.created_at && (
<span>{new Date(wf.create_time).toLocaleDateString()}</span> <span>{new Date(wf.created_at).toLocaleDateString()}</span>
)}
{(wf as any).created_at && !wf.create_time && (
<span>{new Date((wf as any).created_at).toLocaleDateString()}</span>
)} )}
</div> </div>
</div> </div>
+24 -19
View File
@@ -19,7 +19,6 @@ export interface Provider {
provider_url?: string; provider_url?: string;
provider_owner?: string; provider_owner?: string;
provider_models?: string[]; provider_models?: string[];
// Based on your UI needs we might infer some local status fields
status?: string; status?: string;
model?: string; model?: string;
} }
@@ -51,22 +50,31 @@ export interface ClusterNode {
remaining: ClusterResources; remaining: ClusterResources;
} }
// Chat types // Chat types (from DB)
export interface ChatMessageRequest { export interface ChatSessionDB {
chat_id: string;
user_id: string;
title: string;
created_at: string;
updated_at: string;
}
export interface ChatMessageDB {
message_id: string;
chat_id: string;
message: string; message: string;
message_owner: string;
created_at: string;
} }
export interface ChatMessageResponse { // Workflow types (matches backend API response)
message: string; // Either event_id or text
}
// Workflow types
export interface Workflow { export interface Workflow {
event_id: string; trace_id: string;
workflow_title: string; title: string;
command: string;
status?: string; status?: string;
message?: string; created_at?: string;
create_time?: string; updated_at?: string;
} }
export interface WorkflowStep { export interface WorkflowStep {
@@ -80,22 +88,19 @@ export interface WorkflowStep {
} }
export interface WorkflowDetail { export interface WorkflowDetail {
event_id: string; trace_id: string;
workflow_title: string | null; title: string;
status: string; status: string;
command?: string; command?: string;
current_step: number;
user_name: string;
message: string;
create_time: string;
steps: WorkflowStep[]; steps: WorkflowStep[];
context_blackboard?: Record<string, unknown>;
} }
// Workflow Template Validation // Workflow Template Validation
export interface WorkStep { export interface WorkStep {
name: string; name: string;
desc?: string; desc?: string;
actor: string; // the name of the worker individual actor: string;
inputs?: string[]; inputs?: string[];
outputs?: string[]; outputs?: string[];
} }
+1 -1
View File
@@ -98,7 +98,7 @@ async def workflow_error_handler(request: Request, exc: WorkflowError):
base_dir = os.path.dirname( base_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
) )
frontend_dir = os.path.join(base_dir, "frontend", "dist") frontend_dir = os.path.join(base_dir, "frontend", "dist")
-52
View File
@@ -1,52 +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.
import datetime
from pydantic import BaseModel, Field, ConfigDict
from ulid import ULID
from typing import Any, Dict
import asyncio
from kilostar.core.work.workflow.workflow import KiloStarWorkflow
class kilostarEvent(BaseModel):
"""kilostarEvent 核心组件类。
这是一个领域数据模型或功能封装类,承载了 kilostarEvent 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
model_config = ConfigDict(arbitrary_types_allowed=True)
trace_id: str = Field(
default_factory=lambda: str(ULID()), description="事件的唯一标识符"
)
platform: str = Field(description="消息来源的平台")
user_id: str = Field(description="用户id")
user_name: str = Field(description="用户名")
create_time: str = Field(
default_factory=lambda: str(
datetime.datetime.now(datetime.timezone.utc).isoformat()
),
description="事件创建时间",
)
message: str = Field(description="用户发来的消息")
attachment: Dict[str, str] | None = Field(default=None, description="附件")
# --------------------------------------------------------------------------------------------------------------
context: Dict[str, Any] = Field(
default_factory=dict, description="事件上下文内容,可包含工作流模板等信息"
)
workflow: KiloStarWorkflow | None = Field(default=None, description="工作流")
pending_queue: asyncio.Queue[str] | None = Field(
default=None, description="待处理队列"
)
receive_queue: asyncio.Queue[str] | None = Field(
default=None, description="待接收队列"
)
+5 -15
View File
@@ -12,10 +12,9 @@
# 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 fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel from pydantic import BaseModel
from kilostar.utils.access import Accessor, TokenData from kilostar.utils.access import Accessor, TokenData
from kilostar.api.platform.event import kilostarEvent
from kilostar.utils.ray_hook import ray_actor_hook from kilostar.utils.ray_hook import ray_actor_hook
import os import os
import anyio import anyio
@@ -43,22 +42,13 @@ async def create_message(
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。""" Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
logger.info("收到消息,来源:客户端") logger.info("收到消息,来源:客户端")
logger.debug(f"消息内容:{message.message}") logger.debug(f"消息内容:{message.message}")
event = kilostarEvent( regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
platform="client", reply = await regulatory_node.handle_client_message.remote(
user_id=str(token_data.user_id), user_id=token_data.user_id,
user_name=token_data.username, user_name=token_data.username,
message=message.message, message=message.message,
) )
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node return {"message": reply}
message = await regulatory_node.working.remote(event)
if message.startswith("任务已创建"):
return {"message": f"{event.trace_id}\n\n{message}"}
elif message == "未知相应类型":
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="模型回复错误"
)
else:
return {"message": message}
@client_router.post("/upload") @client_router.post("/upload")
+38 -41
View File
@@ -42,9 +42,12 @@ async def create_workflow(
command=request.command, command=request.command,
) )
# 将需求发送给意识节点去处理构建 global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
await global_workflow_manager.create_trace.remote(trace_id)
consciousness_node = ray_actor_hook("consciousness_node").consciousness_node consciousness_node = ray_actor_hook("consciousness_node").consciousness_node
# 可以异步通知意识节点开始与用户在特定 Trace ID 下对话
consciousness_node.start_workflow_design.remote(trace_id, request.command) consciousness_node.start_workflow_design.remote(trace_id, request.command)
return {"trace_id": trace_id, "status": "creating"} return {"trace_id": trace_id, "status": "creating"}
@@ -59,6 +62,39 @@ async def get_workflow_list(token_data: TokenData = Depends(Accessor.get_current
return {"workflows": workflows} return {"workflows": workflows}
@workflow_router.get("/sse/{trace_id}")
async def get_workflow_sse(trace_id: str, request: Request):
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
async def event_generator():
try:
while True:
if await request.is_disconnected():
break
message = await global_workflow_manager.get_pending.remote(trace_id)
if message:
yield f"data: {message}\n\n"
else:
await asyncio.sleep(0.5)
except asyncio.CancelledError:
pass
return StreamingResponse(event_generator(), media_type="text/event-stream")
@workflow_router.post("/reply/{trace_id}")
async def post_workflow_reply(trace_id: str, request: Request):
data = await request.json()
reply_msg = data.get("message", "")
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
await global_workflow_manager.put_received.remote(trace_id, reply_msg)
return {"status": "ok"}
@workflow_router.get("/{trace_id}") @workflow_router.get("/{trace_id}")
async def get_workflow_detail( async def get_workflow_detail(
trace_id: str, token_data: TokenData = Depends(Accessor.get_current_user) trace_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
@@ -80,42 +116,3 @@ async def get_workflow_detail(
"steps": steps, "steps": steps,
"context_blackboard": context.blackboard if context else {}, "context_blackboard": context.blackboard if context else {},
} }
@workflow_router.get("/sse/{trace_id}")
async def get_workflow_sse(trace_id: str, request: Request):
"""
用于与意识节点交互,获取工作流状态或设计阶段的问答消息
"""
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
async def event_generator():
try:
while True:
if await request.is_disconnected():
break
message = await global_workflow_manager.get_pending.remote(trace_id)
if message:
yield f"data: {message}\n\n"
else:
await asyncio.sleep(0.5)
except asyncio.CancelledError:
pass
return StreamingResponse(event_generator(), media_type="text/event-stream")
@workflow_router.post("/reply/{trace_id}")
async def post_workflow_reply(trace_id: str, request: Request):
"""
用于用户回复意识节点的提问(设计阶段或运行中的中断确认)
"""
data = await request.json()
reply_msg = data.get("message", "")
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
await global_workflow_manager.put_received.remote(trace_id, reply_msg)
return {"status": "ok"}
@@ -1,212 +1,48 @@
import ray import ray
import asyncio import asyncio
from typing import Dict from typing import Dict
from kilostar.api.platform.event import kilostarEvent
from kilostar.core.work.workflow.workflow import KiloStarWorkflow
from kilostar.utils.ray_hook import ray_actor_hook from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.logger import get_logger from kilostar.utils.logger import get_logger
class TraceQueues:
def __init__(self):
self.pending: asyncio.Queue[str] = asyncio.Queue()
self.receive: asyncio.Queue[str] = asyncio.Queue()
@ray.remote @ray.remote
class GlobalWorkflowManager: class GlobalWorkflowManager:
def __init__(self): def __init__(self):
self.event_dict: Dict[str, kilostarEvent] = {} self._traces: Dict[str, TraceQueues] = {}
self.event_object_refs: Dict[str, ray.ObjectRef] = {}
self.postgres_database = None
self.logger = get_logger("GlobalWorkflowManager") self.logger = get_logger("GlobalWorkflowManager")
async def init_manager(self): async def init_manager(self):
self.postgres_database = ray_actor_hook("postgres_database").postgres_database self.logger.info("GlobalWorkflowManager initialized")
# Load all events from database to memory async def create_trace(self, trace_id: str) -> None:
try: if trace_id not in self._traces:
records = await self.postgres_database.get_all_events.remote() self._traces[trace_id] = TraceQueues()
for record in records:
try:
event = kilostarEvent.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 async def delete_trace(self, trace_id: str) -> None:
event_copy = event.model_copy() self._traces.pop(trace_id, None)
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: kilostarEvent):
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: kilostarEvent) -> 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) -> kilostarEvent | 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 kilostarEvent.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 = kilostarEvent.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: KiloStarWorkflow) -> 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) -> KiloStarWorkflow | 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 = kilostarEvent.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: async def put_pending(self, trace_id, item) -> None:
if trace_id in self.event_dict and self.event_dict[trace_id].pending_queue: if trace_id in self._traces:
await self.event_dict[trace_id].pending_queue.put(item) await self._traces[trace_id].pending.put(item)
async def get_pending(self, trace_id) -> str: async def get_pending(self, trace_id) -> str:
if trace_id in self.event_dict and self.event_dict[trace_id].pending_queue: if trace_id in self._traces:
return await self.event_dict[trace_id].pending_queue.get() return await self._traces[trace_id].pending.get()
await asyncio.sleep(1) # Prevent CPU spinning if not found await asyncio.sleep(1)
return "" return ""
async def put_received(self, trace_id, item) -> None: async def put_received(self, trace_id, item) -> None:
if trace_id in self.event_dict and self.event_dict[trace_id].receive_queue: if trace_id in self._traces:
await self.event_dict[trace_id].receive_queue.put(item) await self._traces[trace_id].receive.put(item)
async def get_received(self, trace_id) -> str: async def get_received(self, trace_id) -> str:
if trace_id in self.event_dict and self.event_dict[trace_id].receive_queue: if trace_id in self._traces:
return await self.event_dict[trace_id].receive_queue.get() return await self._traces[trace_id].receive.get()
await asyncio.sleep(1) # Prevent CPU spinning if not found await asyncio.sleep(1)
return "" return ""
@@ -12,6 +12,6 @@
# 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 .regulatory_node import regulatoryNode from .regulatory_node import RegulatoryNode
__all__ = ["regulatoryNode"] __all__ = ["RegulatoryNode"]
@@ -15,7 +15,6 @@
import datetime import datetime
import ray import ray
from typing import Union, overload from typing import Union, overload
from kilostar.api.platform.event import kilostarEvent
from kilostar.adapter.model_adapter.agent_factory import AgentFactory from kilostar.adapter.model_adapter.agent_factory import AgentFactory
from kilostar.core.global_state_machine.global_state_machine import GlobalStateMachine from kilostar.core.global_state_machine.global_state_machine import GlobalStateMachine
from kilostar.core.global_state_machine.model_provider import Provider from kilostar.core.global_state_machine.model_provider import Provider
@@ -29,6 +28,14 @@ from pydantic_ai import RunContext, Agent
from kilostar.utils.ray_hook import ray_actor_hook from kilostar.utils.ray_hook import ray_actor_hook
class ClientMessage:
def __init__(self, user_id: str, user_name: str, message: str):
self.user_id = user_id
self.user_name = user_name
self.message = message
self.platform = "client"
@ray.remote @ray.remote
class RegulatoryNode: class RegulatoryNode:
"""regulatoryNode 核心组件类。 """regulatoryNode 核心组件类。
@@ -116,35 +123,43 @@ class RegulatoryNode:
) )
return prompt return prompt
###工作函数 async def handle_chat_message(
async def working(self, payload: Union[kilostarEvent, TerminationMessage]) -> str: self, user_id: str, chat_id: str, message: str
""" ) -> str:
working方法,是节点唯一的调用方法,对于_run函数的结果进行判断并实现最终回复 payload = ClientMessage(
user_id=user_id, user_name="", message=message
)
return await self._process(payload)
async def handle_client_message(
self, user_id: str, user_name: str, message: str
) -> str:
payload = ClientMessage(
user_id=user_id, user_name=user_name, message=message
)
return await self._process(payload)
async def working(self, payload: Union[ClientMessage, TerminationMessage]) -> str:
"""working方法,是节点唯一的调用方法,对于_run函数的结果进行判断并实现最终回复
Args: Args:
payload: 消息载荷,包含所有信息 payload: 消息载荷,包含所有信息
Returns: Returns:
str,监控节点对于用户的回复 str,监控节点对于用户的回复
""" """
return await self._process(payload)
async def _process(
self, payload: Union[ClientMessage, TerminationMessage]
) -> str:
try: try:
result = await self._run(payload) result = await self._run(payload)
if isinstance(result, ForConsciousnessNode): if isinstance(result, ForConsciousnessNode):
self.logger.info("regulatoryNode: 任务已分配给工作流引擎处理") self.logger.info("regulatoryNode: 任务需交意识节点处理")
if isinstance(payload, kilostarEvent): workflow_running_engine = ray_actor_hook(
try: "workflow_running_engine"
global_workflow_manager = ray_actor_hook( ).workflow_running_engine
"global_workflow_manager" await workflow_running_engine.put_event.remote(payload)
).global_workflow_manager
await global_workflow_manager.add_event.remote(payload)
workflow_running_engine = ray_actor_hook(
"workflow_running_engine"
).workflow_running_engine
await workflow_running_engine.put_event.remote(payload)
except Exception as e:
self.logger.error(
f"regulatoryNode: 无法将事件放入 WorkflowRunningEngine: {e}"
)
return "抱歉,任务提交失败,系统内部错误。"
return f"任务已创建,准备创建工作流。原因:{result.reasoning}" return f"任务已创建,准备创建工作流。原因:{result.reasoning}"
elif isinstance(result, ForUser): elif isinstance(result, ForUser):
self.logger.info("regulatoryNode: 直接向用户返回简单回复。") self.logger.info("regulatoryNode: 直接向用户返回简单回复。")
@@ -158,43 +173,15 @@ class RegulatoryNode:
@overload @overload
async def _run( async def _run(
self, payload: kilostarEvent self, payload: ClientMessage
) -> Union[ForConsciousnessNode, ForUser]: ) -> Union[ForConsciousnessNode, ForUser]: ...
"""
_run方法
Args:
payload: kilostarEvent的实例,是用户输入时对于消息的封装
Returns:
ForUser对象,监控节点对于用户进行的简单回答
ForConsciousnessNode对象,监控节点将用户的请求判断为复杂任务,将kilostarEvent传递给意识节点
"""
...
@overload @overload
async def _run(self, payload: TerminationMessage) -> ForUser: async def _run(self, payload: TerminationMessage) -> ForUser: ...
"""
_run方法
Args:
payload: Termination的实例,是工作流结束后到达监控节点的最后结果
Returns:
ForUser对象,工作流结束后给用户的返回
"""
...
async def _run( async def _run(
self, payload: Union[kilostarEvent, TerminationMessage] self, payload: Union[ClientMessage, TerminationMessage]
) -> Union[ForConsciousnessNode, ForUser]: ) -> Union[ForConsciousnessNode, ForUser]:
"""
_run方法,将payload转化为对llm发送的消息并发送
Args:
payload: 消息载荷
Returns:
ForConsciousnessNode对象,对意识节点发送的消息
ForUser对象,对用户发送到消息
"""
platform = payload.platform platform = payload.platform
user_name = payload.user_name user_name = payload.user_name
message = payload.message message = payload.message
@@ -44,6 +44,7 @@ def database_exception(func):
raise e raise e
except UserNotExistError as e: except UserNotExistError as e:
logger.error(f"更改密码失败,用户不存在:{e}") logger.error(f"更改密码失败,用户不存在:{e}")
raise e
except Exception as e: except Exception as e:
logger.exception(f"未预期的数据库错误: {e}") logger.exception(f"未预期的数据库错误: {e}")
raise e raise e
@@ -12,9 +12,14 @@
# 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 kilostar.core.postgres_database.model.user import User from kilostar.core.postgres_database.model.user import User, UserAuthority
from kilostar.core.postgres_database.model.provider import Provider from kilostar.core.postgres_database.model.provider import ProviderModel
from kilostar.core.postgres_database.model.individual import WorkerIndividual from kilostar.core.postgres_database.model.individual import (
BaseIndividualModel,
SpecialistIndividualModel,
OrdinaryIndividualModel,
SpecialIndividualModel,
)
from kilostar.core.postgres_database.model.workflow import ( from kilostar.core.postgres_database.model.workflow import (
Workflow, Workflow,
WorkflowContextModel, WorkflowContextModel,
@@ -23,13 +28,30 @@ from kilostar.core.postgres_database.model.chat_history import (
ChatHistoryRegister, ChatHistoryRegister,
ChatHistoryMessage, ChatHistoryMessage,
) )
from kilostar.core.postgres_database.model.system_node import SystemNodeConfigModel
# 兼容旧代码的别名
Provider = ProviderModel
SystemNodeConfig = SystemNodeConfigModel
# AgentType: 与 BaseIndividualModel 的 polymorphic_on 保持一致
from typing import Literal
AgentType = Literal["base", "specialist", "ordinary", "special"]
__all__ = [ __all__ = [
"User", "User",
"UserAuthority",
"ProviderModel",
"Provider", "Provider",
"WorkerIndividual", "BaseIndividualModel",
"SpecialistIndividualModel",
"OrdinaryIndividualModel",
"SpecialIndividualModel",
"Workflow", "Workflow",
"WorkflowContextModel", "WorkflowContextModel",
"ChatHistoryRegister", "ChatHistoryRegister",
"ChatHistoryMessage", "ChatHistoryMessage",
"SystemNodeConfigModel",
"SystemNodeConfig",
"AgentType",
] ]
@@ -26,13 +26,13 @@ class ChatHistoryRegister(BaseDataModel):
__tablename__ = "chat_history_register" __tablename__ = "chat_history_register"
chat_id: Mapped[str] = mapped_column( chat_id: Mapped[str] = mapped_column(
String(64), primary_key=True, description="聊天会话ID" String(64), primary_key=True, comment="聊天会话ID"
) )
user_id: Mapped[str] = mapped_column( user_id: Mapped[str] = mapped_column(
String(64), index=True, description="归属的用户ID" String(64), index=True, comment="归属的用户ID"
) )
title: Mapped[str] = mapped_column( title: Mapped[str] = mapped_column(
String(255), default="新对话", description="对话标题" String(255), default="新对话", comment="对话标题"
) )
created_at: Mapped[str] = mapped_column( created_at: Mapped[str] = mapped_column(
DateTime(timezone=True), server_default=func.now() DateTime(timezone=True), server_default=func.now()
@@ -50,15 +50,15 @@ class ChatHistoryMessage(BaseDataModel):
__tablename__ = "chat_history_message" __tablename__ = "chat_history_message"
message_id: Mapped[str] = mapped_column( message_id: Mapped[str] = mapped_column(
String(64), primary_key=True, description="消息ID" String(64), primary_key=True, comment="消息ID"
) )
chat_id: Mapped[str] = mapped_column( chat_id: Mapped[str] = mapped_column(
String(64), index=True, description="所属会话ID" String(64), index=True, comment="所属会话ID"
) )
message: Mapped[str] = mapped_column(String, description="消息体内容") message: Mapped[str] = mapped_column(String, comment="消息体内容")
message_owner: Mapped[str] = mapped_column( message_owner: Mapped[str] = mapped_column(
String(50), String(50),
description="消息发送方,例如 'user', 'regulatory_node', 'consciousness_node'", comment="消息发送方,例如 'user', 'regulatory_node', 'consciousness_node'",
) )
created_at: Mapped[str] = mapped_column( created_at: Mapped[str] = mapped_column(
DateTime(timezone=True), server_default=func.now() DateTime(timezone=True), server_default=func.now()
@@ -14,7 +14,7 @@
from enum import Enum from enum import Enum
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from sqlalchemy import String, Text, text, ForeignKey from sqlalchemy import String, Text, text, ForeignKey, Enum as SAEnum
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -119,7 +119,8 @@ class SpecialIndividualModel(BaseIndividualModel):
ForeignKey("base_individual.agent_id", ondelete="CASCADE"), primary_key=True ForeignKey("base_individual.agent_id", ondelete="CASCADE"), primary_key=True
) )
modality_type: Mapped[ModalityType] = mapped_column( modality_type: Mapped[ModalityType] = mapped_column(
default=ModalityType.MULTIMODAL, server_default=text("'multimodal'") SAEnum(ModalityType),
default=ModalityType.MULTIMODAL,
) )
multimodal_config: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSONB) multimodal_config: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSONB)
@@ -22,22 +22,22 @@ class Workflow(BaseDataModel):
__tablename__ = "workflow" __tablename__ = "workflow"
trace_id: Mapped[str] = mapped_column( trace_id: Mapped[str] = mapped_column(
String(64), primary_key=True, description="工作流唯一ID (Trace ID)" String(64), primary_key=True, comment="工作流唯一ID (Trace ID)"
) )
user_id: Mapped[str] = mapped_column( user_id: Mapped[str] = mapped_column(
String(64), index=True, description="创建该工作流的用户ID" String(64), index=True, comment="创建该工作流的用户ID"
) )
title: Mapped[str] = mapped_column(String(255), description="工作流标题/简短描述") title: Mapped[str] = mapped_column(String(255), comment="工作流标题/简短描述")
command: Mapped[str] = mapped_column( command: Mapped[str] = mapped_column(
String, description="创建工作流的原始用户命令文本" String, comment="创建工作流的原始用户命令文本"
) )
status: Mapped[str] = mapped_column( status: Mapped[str] = mapped_column(
String(50), String(50),
default="creating", default="creating",
description="工作流的总体状态 (例如: creating, running, pending, completed, failed等)", comment="工作流的总体状态 (例如: creating, running, pending, completed, failed等)",
) )
version: Mapped[str] = mapped_column( version: Mapped[str] = mapped_column(
String(50), default="v1.0", description="系统协议版本号" String(50), default="v1.0", comment="系统协议版本号"
) )
created_at: Mapped[str] = mapped_column( created_at: Mapped[str] = mapped_column(
DateTime(timezone=True), server_default=func.now() DateTime(timezone=True), server_default=func.now()
@@ -51,27 +51,27 @@ class WorkflowContextModel(BaseDataModel):
__tablename__ = "workflow_context" __tablename__ = "workflow_context"
trace_id: Mapped[str] = mapped_column( trace_id: Mapped[str] = mapped_column(
String(64), primary_key=True, description="对应的工作流 Trace ID" String(64), primary_key=True, comment="对应的工作流 Trace ID"
) )
workflow_status: Mapped[dict] = mapped_column( workflow_status: Mapped[dict] = mapped_column(
JSONB, default=dict, description="工作流状态变更历史" JSONB, default=dict, comment="工作流状态变更历史"
) )
blackboard: Mapped[dict] = mapped_column( blackboard: Mapped[dict] = mapped_column(
JSONB, default=dict, description="大模型输出的存储区 (共享黑板)" JSONB, default=dict, comment="大模型输出的存储区 (共享黑板)"
) )
work_step_status: Mapped[dict] = mapped_column( work_step_status: Mapped[dict] = mapped_column(
JSONB, nullable=True, description="工作流运行步骤状态" JSONB, nullable=True, comment="工作流运行步骤状态"
) )
workflow_pointer: Mapped[int] = mapped_column( workflow_pointer: Mapped[int] = mapped_column(
nullable=True, description="工作流指针,指向具体运行步骤位置" nullable=True, comment="工作流指针,指向具体运行步骤位置"
) )
workflow_log: Mapped[list] = mapped_column( workflow_log: Mapped[list] = mapped_column(
JSONB, default=list, description="工作流运行日志" JSONB, default=list, comment="工作流运行日志"
) )
work_link: Mapped[list] = mapped_column( work_link: Mapped[list] = mapped_column(
JSONB, JSONB,
default=list, default=list,
description="工作链(即 WorkflowStep 的定义列表,包含图结构和原子动作)", comment="工作链(即 WorkflowStep 的定义列表,包含图结构和原子动作)",
) )
created_at: Mapped[str] = mapped_column( created_at: Mapped[str] = mapped_column(
DateTime(timezone=True), server_default=func.now() DateTime(timezone=True), server_default=func.now()
@@ -20,12 +20,14 @@ from kilostar.core.postgres_database.model.chat_history import (
) )
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
from ulid import ULID from ulid import ULID
from kilostar.core.postgres_database.database_exception import database_exception
class ChatHistoryDatabase: class ChatHistoryDatabase:
def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]): def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]):
self.async_session_maker = async_session_maker self.async_session_maker = async_session_maker
@database_exception
async def create_chat_session( async def create_chat_session(
self, user_id: str, title: str = "新对话" self, user_id: str, title: str = "新对话"
) -> ChatHistoryRegister: ) -> ChatHistoryRegister:
@@ -37,6 +39,7 @@ class ChatHistoryDatabase:
await session.refresh(chat) await session.refresh(chat)
return chat return chat
@database_exception
async def list_chat_sessions(self, user_id: str) -> List[ChatHistoryRegister]: async def list_chat_sessions(self, user_id: str) -> List[ChatHistoryRegister]:
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = ( statement = (
@@ -47,6 +50,7 @@ class ChatHistoryDatabase:
results = await session.execute(statement) results = await session.execute(statement)
return results.scalars().all() return results.scalars().all()
@database_exception
async def add_chat_message( async def add_chat_message(
self, chat_id: str, message: str, message_owner: str self, chat_id: str, message: str, message_owner: str
) -> ChatHistoryMessage: ) -> ChatHistoryMessage:
@@ -71,6 +75,7 @@ class ChatHistoryDatabase:
await session.refresh(msg) await session.refresh(msg)
return msg return msg
@database_exception
async def list_chat_messages(self, chat_id: str) -> List[ChatHistoryMessage]: async def list_chat_messages(self, chat_id: str) -> List[ChatHistoryMessage]:
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = ( statement = (
@@ -1,46 +0,0 @@
from sqlalchemy import select
from typing import List, Optional
from kilostar.core.postgres_database.model.workflow 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
@@ -12,13 +12,24 @@
# 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 kilostar.core.postgres_database.model.individual import WorkerIndividual from kilostar.core.postgres_database.model.individual import (
BaseIndividualModel,
SpecialistIndividualModel,
OrdinaryIndividualModel,
SpecialIndividualModel,
)
from sqlalchemy import select from sqlalchemy import select
from typing import List, Optional from typing import List, Optional
from kilostar.core.postgres_database.database_exception import database_exception from kilostar.core.postgres_database.database_exception import database_exception
from ulid import ULID from ulid import ULID
_AGENT_TYPE_MODEL_MAP = {
"specialist": SpecialistIndividualModel,
"ordinary": OrdinaryIndividualModel,
"special": SpecialIndividualModel,
}
class IndividualDatabase: class IndividualDatabase:
"""IndividualDatabase 核心组件类。 """IndividualDatabase 核心组件类。
@@ -27,56 +38,50 @@ class IndividualDatabase:
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
@staticmethod
def _select_model(agent_type: str):
return _AGENT_TYPE_MODEL_MAP.get(agent_type, BaseIndividualModel)
@database_exception @database_exception
async def add_worker_individual(self, **kwargs) -> WorkerIndividual: async def add_worker_individual(self, **kwargs):
"""创建并持久化新的 worker individual 实体。 """创建并持久化新的 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) agent_type = kwargs.get("agent_type", "base")
model_cls = self._select_model(agent_type)
individual = model_cls(agent_id=agent_id, **kwargs)
session.add(individual) session.add(individual)
await session.commit() await session.commit()
await session.refresh(individual) await session.refresh(individual)
return individual return individual
@database_exception @database_exception
async def get_worker_individual(self, agent_id: str) -> Optional[WorkerIndividual]: async def get_worker_individual(self, agent_id: str):
"""检索并获取特定的 worker individual 数据集合或实例对象。 """检索并获取特定的 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( statement = select(BaseIndividualModel).where(
WorkerIndividual.agent_id == agent_id BaseIndividualModel.agent_id == agent_id
) )
results = await session.execute(statement) results = await session.execute(statement)
return results.scalar_one_or_none() return results.scalar_one_or_none()
@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):
"""检索并获取特定的 worker individual list 数据集合或实例对象。 """检索并获取特定的 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( statement = select(BaseIndividualModel).where(
WorkerIndividual.owner_id == owner_id BaseIndividualModel.owner_id == owner_id
) )
results = await session.execute(statement) results = await session.execute(statement)
return list(results.scalars().all()) return list(results.scalars().all())
@database_exception @database_exception
async def update_worker_individual( async def update_worker_individual(self, agent_id: str, **kwargs):
self, agent_id: str, **kwargs """对现有的 worker individual 进行状态更新或属性覆盖。"""
) -> 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( statement = select(BaseIndividualModel).where(
WorkerIndividual.agent_id == agent_id BaseIndividualModel.agent_id == agent_id
) )
results = await session.execute(statement) results = await session.execute(statement)
individual = results.scalar_one_or_none() individual = results.scalar_one_or_none()
@@ -92,13 +97,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。 """安全地移除或注销 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( statement = select(BaseIndividualModel).where(
WorkerIndividual.agent_id == agent_id BaseIndividualModel.agent_id == agent_id
) )
results = await session.execute(statement) results = await session.execute(statement)
individual = results.scalar_one_or_none() individual = results.scalar_one_or_none()
@@ -109,11 +111,9 @@ class IndividualDatabase:
return True return True
@database_exception @database_exception
async def get_all_worker_individual(self) -> List[WorkerIndividual]: async def get_all_worker_individual(self):
"""检索并获取特定的 all worker individual 数据集合或实例对象。 """检索并获取特定的 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(BaseIndividualModel)
results = await session.execute(statement) results = await session.execute(statement)
return list(results.scalars().all()) return list(results.scalars().all())
@@ -14,7 +14,7 @@
from typing import List from typing import List
from kilostar.core.postgres_database.model.provider import Provider from kilostar.core.postgres_database.model.provider import ProviderModel
from sqlalchemy import select from sqlalchemy import select
from kilostar.core.postgres_database.database_exception import database_exception from kilostar.core.postgres_database.database_exception import database_exception
@@ -27,21 +27,25 @@ class ProviderDatabase:
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[ProviderModel]:
"""检索并获取特定的 provider 数据集合或实例对象。 """检索并获取特定的 provider 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。 根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: (List[Provider]): 经过筛选、排序或分页处理后的实体对象列表集合。""" Returns: (List[ProviderModel]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(Provider) statement = select(ProviderModel)
results = await session.execute(statement) results = await session.execute(statement)
results = results.scalars().all() results = results.scalars().all()
providers = [ providers = [
Provider( ProviderModel(
provider_id=provider.provider_id,
provider_title=provider.provider_title, provider_title=provider.provider_title,
provider_url=provider.provider_url, provider_url=provider.provider_url,
provider_apikey=provider.provider_apikey, provider_apikey=provider.provider_apikey,
provider_models=provider.provider_models, provider_models=provider.provider_models,
provider_type=provider.provider_type, provider_type=provider.provider_type,
provider_owner=provider.provider_owner,
provider_status=provider.provider_status,
is_active=provider.is_active,
) )
for provider in results for provider in results
] ]
@@ -53,7 +57,7 @@ class ProviderDatabase:
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。 接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。""" Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
provider = Provider(**kwargs) provider = ProviderModel(**kwargs)
session.add(provider) session.add(provider)
await session.commit() await session.commit()
@@ -64,19 +68,19 @@ class ProviderDatabase:
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。 Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。""" 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(ProviderModel, provider_id)
if provider is not None: if provider is not None:
session.delete(provider) session.delete(provider)
await session.commit() await session.commit()
@database_exception @database_exception
async def update_provider(self, provider_id: str, **kwargs) -> Provider: async def update_provider(self, provider_id: str, **kwargs) -> None:
"""对现有的 provider 进行状态更新或属性覆盖。 """对现有的 provider 进行状态更新或属性覆盖。
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。 基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。 Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
Returns: (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(ProviderModel, provider_id)
if provider is not None: if provider is not None:
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(provider, key, value) setattr(provider, key, value)
@@ -12,7 +12,7 @@
# 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 kilostar.core.postgres_database.model.system_node import SystemNodeConfig from kilostar.core.postgres_database.model.system_node import SystemNodeConfigModel
from sqlalchemy import select from sqlalchemy import select
from typing import List, Optional from typing import List, Optional
from kilostar.core.postgres_database.database_exception import database_exception from kilostar.core.postgres_database.database_exception import database_exception
@@ -32,14 +32,14 @@ class SystemNodeDatabase:
provider_title: str, provider_title: str,
model_id: str, model_id: str,
tools: Optional[List[str]] = None, tools: Optional[List[str]] = None,
) -> SystemNodeConfig: ) -> SystemNodeConfigModel:
"""执行与 upsert system node config 相关的核心业务流转操作。 """执行与 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 内容。 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): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。""" Returns: (SystemNodeConfigModel): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(SystemNodeConfig).where( statement = select(SystemNodeConfigModel).where(
SystemNodeConfig.node_name == node_name SystemNodeConfigModel.node_name == node_name
) )
results = await session.execute(statement) results = await session.execute(statement)
config = results.scalar_one_or_none() config = results.scalar_one_or_none()
@@ -49,7 +49,7 @@ class SystemNodeDatabase:
if tools is not None: if tools is not None:
config.tools = tools config.tools = tools
else: else:
config = SystemNodeConfig( config = SystemNodeConfigModel(
node_name=node_name, node_name=node_name,
provider_title=provider_title, provider_title=provider_title,
model_id=model_id, model_id=model_id,
@@ -61,26 +61,26 @@ class SystemNodeDatabase:
return config return config
@database_exception @database_exception
async def get_all_system_node_configs(self) -> List[SystemNodeConfig]: async def get_all_system_node_configs(self) -> List[SystemNodeConfigModel]:
"""检索并获取特定的 all system node configs 数据集合或实例对象。 """检索并获取特定的 all system node configs 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。 根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Returns: (List[SystemNodeConfig]): 经过筛选、排序或分页处理后的实体对象列表集合。""" Returns: (List[SystemNodeConfigModel]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(SystemNodeConfig) statement = select(SystemNodeConfigModel)
results = await session.execute(statement) results = await session.execute(statement)
return list(results.scalars().all()) return list(results.scalars().all())
@database_exception @database_exception
async def get_system_node_config( async def get_system_node_config(
self, node_name: str self, node_name: str
) -> Optional[SystemNodeConfig]: ) -> Optional[SystemNodeConfigModel]:
"""检索并获取特定的 system node config 数据集合或实例对象。 """检索并获取特定的 system node config 数据集合或实例对象。
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。 根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
Returns: (Optional[SystemNodeConfig]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。""" Returns: (Optional[SystemNodeConfigModel]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(SystemNodeConfig).where( statement = select(SystemNodeConfigModel).where(
SystemNodeConfig.node_name == node_name SystemNodeConfigModel.node_name == node_name
) )
results = await session.execute(statement) results = await session.execute(statement)
return results.scalar_one_or_none() return results.scalar_one_or_none()
@@ -70,7 +70,7 @@ class AuthDatabase:
raise UserNotExistError() raise UserNotExistError()
if not Accessor.verify_password(old_password, user.hashed_password): if not Accessor.verify_password(old_password, user.hashed_password):
raise UserPasswordError() raise UserPasswordError()
user.hashed_password = new_password user.hashed_password = Accessor.hash_password(new_password)
session.add(user) session.add(user)
await session.commit() await session.commit()
await session.refresh(user) await session.refresh(user)
@@ -19,12 +19,14 @@ from kilostar.core.postgres_database.model.workflow import (
WorkflowContextModel, WorkflowContextModel,
) )
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
from kilostar.core.postgres_database.database_exception import database_exception
class WorkflowDatabase: class WorkflowDatabase:
def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]): def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]):
self.async_session_maker = async_session_maker self.async_session_maker = async_session_maker
@database_exception
async def create_workflow( async def create_workflow(
self, trace_id: str, user_id: str, title: str, command: str self, trace_id: str, user_id: str, title: str, command: str
) -> Workflow: ) -> Workflow:
@@ -41,12 +43,14 @@ class WorkflowDatabase:
await session.refresh(wf) await session.refresh(wf)
return wf return wf
@database_exception
async def get_workflow(self, trace_id: str) -> Optional[Workflow]: async def get_workflow(self, trace_id: str) -> Optional[Workflow]:
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(Workflow).where(Workflow.trace_id == trace_id) statement = select(Workflow).where(Workflow.trace_id == trace_id)
results = await session.execute(statement) results = await session.execute(statement)
return results.scalar_one_or_none() return results.scalar_one_or_none()
@database_exception
async def update_workflow_status( async def update_workflow_status(
self, trace_id: str, status: str self, trace_id: str, status: str
) -> Optional[Workflow]: ) -> Optional[Workflow]:
@@ -60,12 +64,14 @@ class WorkflowDatabase:
await session.refresh(record) await session.refresh(record)
return record return record
@database_exception
async def list_workflows(self, user_id: str) -> List[Workflow]: async def list_workflows(self, user_id: str) -> List[Workflow]:
async with self.async_session_maker() as session: async with self.async_session_maker() as session:
statement = select(Workflow).where(Workflow.user_id == user_id) statement = select(Workflow).where(Workflow.user_id == user_id)
results = await session.execute(statement) results = await session.execute(statement)
return results.scalars().all() return results.scalars().all()
@database_exception
async def upsert_workflow_context( async def upsert_workflow_context(
self, trace_id: str, **kwargs self, trace_id: str, **kwargs
) -> WorkflowContextModel: ) -> WorkflowContextModel:
@@ -85,6 +91,7 @@ class WorkflowDatabase:
await session.refresh(record) await session.refresh(record)
return record return record
@database_exception
async def get_workflow_context( async def get_workflow_context(
self, trace_id: str self, trace_id: str
) -> Optional[WorkflowContextModel]: ) -> Optional[WorkflowContextModel]:
+22 -25
View File
@@ -20,8 +20,26 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from kilostar.core.postgres_database.model.base import BaseDataModel from kilostar.core.postgres_database.model.base import BaseDataModel
# 在 create_all 前显式导入所有 ORM 模型类,确保它们注册到 metadata
from kilostar.core.postgres_database.model.provider import ProviderModel
from kilostar.core.postgres_database.model.user import User
from kilostar.core.postgres_database.model.individual import (
BaseIndividualModel,
SpecialistIndividualModel,
OrdinaryIndividualModel,
SpecialIndividualModel,
)
from kilostar.core.postgres_database.model.workflow import (
Workflow,
WorkflowContextModel,
)
from kilostar.core.postgres_database.model.chat_history import (
ChatHistoryRegister,
ChatHistoryMessage,
)
from kilostar.core.postgres_database.model.system_node import SystemNodeConfigModel
from .module.individual import IndividualDatabase from .module.individual import IndividualDatabase
from .module.event import EventDatabase
from .module.user import AuthDatabase from .module.user import AuthDatabase
from .module.provider import ProviderDatabase from .module.provider import ProviderDatabase
from .module.system_node import SystemNodeDatabase from .module.system_node import SystemNodeDatabase
@@ -51,7 +69,6 @@ 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._workflow_database = WorkflowDatabase(self.async_session_maker) self._workflow_database = WorkflowDatabase(self.async_session_maker)
self._chat_history_database = ChatHistoryDatabase(self.async_session_maker) self._chat_history_database = ChatHistoryDatabase(self.async_session_maker)
@@ -59,16 +76,13 @@ class PostgresDatabase:
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(BaseDataModel.metadata.create_all) await conn.run_sync(BaseDataModel.metadata.create_all)
print("✅ 数据库表创建/验证完成")
except Exception as e: except Exception as e:
# Provide a warning if the database is not accessible, allowing print(f"❌ 数据库初始化失败: {e}")
# the app to start up for development/UI tests without crashing immediately. raise
print(f"Warning: Failed to initialize PostgreSQL database: {e}")
finally: finally:
self.ready_event.set() self.ready_event.set()
@@ -242,23 +256,6 @@ class PostgresDatabase:
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)
# Workflow Database Methods # Workflow Database Methods
async def create_workflow( async def create_workflow(
self, trace_id: str, user_id: str, title: str, command: str self, trace_id: str, user_id: str, title: str, command: str
+2 -1
View File
@@ -12,13 +12,14 @@
# 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 __future__ import annotations
import jwt import jwt
import os import os
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
from fastapi import HTTPException, status, Request from fastapi import HTTPException, status, Request
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from kilostar.core.postgres_database.model import User
from pwdlib import PasswordHash from pwdlib import PasswordHash
+3 -3
View File
@@ -5,10 +5,10 @@ from kilostar.utils.banner import print_banner
from kilostar.core.postgres_database import PostgresDatabase from kilostar.core.postgres_database import PostgresDatabase
from kilostar.core.global_state_machine import GlobalStateMachine from kilostar.core.global_state_machine import GlobalStateMachine
from kilostar.core.global_workflow_manager import GlobalWorkflowManager from kilostar.core.global_workflow_manager import GlobalWorkflowManager
from kilostar.core.individual.center_node import regulatoryNode from kilostar.core.individual.regulatory_node import RegulatoryNode
from kilostar.core.individual.consciousness_node import ConsciousnessNode from kilostar.core.individual.consciousness_node import ConsciousnessNode
from kilostar.core.individual.control_node import ControlNode from kilostar.core.individual.control_node import ControlNode
from kilostar.core.workflow_running_engine import WorkflowRunningEngine from kilostar.core.work.workflow.workflow_engine import WorkflowRunningEngine
from kilostar.api import kilostarGateway from kilostar.api import kilostarGateway
from ray import serve from ray import serve
import os import os
@@ -64,7 +64,7 @@ async def start_system():
).remote() ).remote()
# 4. 启动核心节点 # 4. 启动核心节点
regulatory_node = regulatoryNode.options(name="regulatory_node").remote() regulatory_node = RegulatoryNode.options(name="regulatory_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()
Generated
+616 -15
View File
File diff suppressed because it is too large Load Diff