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