flowyml 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowyml/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
ChevronLeft,
|
|
16
16
|
ChevronRight,
|
|
17
17
|
Menu,
|
|
18
|
-
Activity
|
|
18
|
+
Activity,
|
|
19
|
+
Rocket,
|
|
20
|
+
Microscope
|
|
19
21
|
} from 'lucide-react';
|
|
20
22
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
21
23
|
|
|
@@ -33,6 +35,7 @@ const NAV_GROUPS = [
|
|
|
33
35
|
{ icon: PlayCircle, label: 'Pipelines', path: '/pipelines' },
|
|
34
36
|
{ icon: Calendar, label: 'Schedules', path: '/schedules' },
|
|
35
37
|
{ icon: PlayCircle, label: 'Runs', path: '/runs' },
|
|
38
|
+
{ icon: Rocket, label: 'Deployments', path: '/deployments' },
|
|
36
39
|
],
|
|
37
40
|
},
|
|
38
41
|
{
|
|
@@ -40,6 +43,7 @@ const NAV_GROUPS = [
|
|
|
40
43
|
items: [
|
|
41
44
|
{ icon: Trophy, label: 'Leaderboard', path: '/leaderboard' },
|
|
42
45
|
{ icon: FlaskConical, label: 'Experiments', path: '/experiments' },
|
|
46
|
+
{ icon: Microscope, label: 'Model Explorer', path: '/model-explorer' },
|
|
43
47
|
],
|
|
44
48
|
},
|
|
45
49
|
{
|
|
@@ -61,6 +65,8 @@ const SETTINGS_LINKS = [
|
|
|
61
65
|
export function Sidebar({ collapsed, setCollapsed }) {
|
|
62
66
|
const location = useLocation();
|
|
63
67
|
|
|
68
|
+
const [logoError, setLogoError] = useState(false);
|
|
69
|
+
|
|
64
70
|
return (
|
|
65
71
|
<motion.aside
|
|
66
72
|
initial={false}
|
|
@@ -68,20 +74,32 @@ export function Sidebar({ collapsed, setCollapsed }) {
|
|
|
68
74
|
className="h-screen bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex flex-col shadow-sm z-20 relative"
|
|
69
75
|
>
|
|
70
76
|
{/* Logo Section */}
|
|
71
|
-
<div className="p-
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
77
|
+
<div className="p-4 border-b border-slate-100 dark:border-slate-700 flex items-center gap-3 h-[73px]">
|
|
78
|
+
{logoError ? (
|
|
79
|
+
<div className="w-12 h-12 min-w-[48px] rounded-lg shadow-lg bg-gradient-to-br from-primary-500 to-indigo-600 flex items-center justify-center text-white font-bold text-xl select-none">
|
|
80
|
+
F
|
|
81
|
+
</div>
|
|
82
|
+
) : (
|
|
83
|
+
<img
|
|
84
|
+
src="/logo.png"
|
|
85
|
+
alt="FlowyML"
|
|
86
|
+
className="w-12 h-12 min-w-[48px] rounded-lg shadow-lg object-contain bg-white dark:bg-slate-800"
|
|
87
|
+
onError={() => setLogoError(true)}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
75
90
|
<AnimatePresence>
|
|
76
91
|
{!collapsed && (
|
|
77
|
-
<motion.
|
|
92
|
+
<motion.div
|
|
78
93
|
initial={{ opacity: 0, x: -10 }}
|
|
79
94
|
animate={{ opacity: 1, x: 0 }}
|
|
80
95
|
exit={{ opacity: 0, x: -10 }}
|
|
81
|
-
className="
|
|
96
|
+
className="flex flex-col"
|
|
82
97
|
>
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
<h1 className="text-xl font-bold text-slate-900 dark:text-white tracking-tight whitespace-nowrap overflow-hidden">
|
|
99
|
+
FlowyML
|
|
100
|
+
</h1>
|
|
101
|
+
<span className="text-[10px] text-slate-400 dark:text-slate-500">by UnicoLab</span>
|
|
102
|
+
</motion.div>
|
|
85
103
|
)}
|
|
86
104
|
</AnimatePresence>
|
|
87
105
|
</div>
|
|
@@ -123,14 +141,19 @@ export function Sidebar({ collapsed, setCollapsed }) {
|
|
|
123
141
|
|
|
124
142
|
{/* Footer */}
|
|
125
143
|
<div className="p-4 border-t border-slate-100 dark:border-slate-700">
|
|
126
|
-
<div className={`bg-slate-50 dark:
|
|
144
|
+
<div className={`bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 rounded-lg p-3 border border-slate-200 dark:border-slate-700 transition-all duration-200 ${collapsed ? 'p-2 flex justify-center' : ''}`}>
|
|
127
145
|
{!collapsed ? (
|
|
128
146
|
<>
|
|
129
|
-
<
|
|
130
|
-
|
|
147
|
+
<div className="flex items-center gap-2 mb-2">
|
|
148
|
+
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
|
149
|
+
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300 whitespace-nowrap">FlowyML v1.3.0</p>
|
|
150
|
+
</div>
|
|
151
|
+
<p className="text-[10px] text-slate-400 dark:text-slate-500 whitespace-nowrap">
|
|
152
|
+
Made with ❤️ by <span className="font-medium text-primary-500">UnicoLab</span>
|
|
153
|
+
</p>
|
|
131
154
|
</>
|
|
132
155
|
) : (
|
|
133
|
-
<div className="w-2 h-2 rounded-full bg-emerald-500" title="Online" />
|
|
156
|
+
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" title="Online" />
|
|
134
157
|
)}
|
|
135
158
|
</div>
|
|
136
159
|
</div>
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import * as webllm from '@mlc-ai/web-llm';
|
|
3
|
+
|
|
4
|
+
// FlowyML System Prompt - comprehensive knowledge base for the AI assistant
|
|
5
|
+
const FLOWYML_SYSTEM_PROMPT = `You are FlowyML Assistant, an expert AI advisor for the FlowyML ML pipeline framework. You help users optimize their pipelines, debug issues, and follow best practices.
|
|
6
|
+
|
|
7
|
+
## About FlowyML
|
|
8
|
+
FlowyML is an enterprise-grade ML pipeline framework that combines Python simplicity with powerful MLOps features:
|
|
9
|
+
- Zero-boilerplate orchestration with pure Python (no YAML/DSLs)
|
|
10
|
+
- Intelligent caching (code hash, input hash) to skip unchanged steps
|
|
11
|
+
- First-class Assets: Dataset, Model, Metrics, FeatureSet with auto-lineage
|
|
12
|
+
- Context auto-injection for parameters
|
|
13
|
+
- Step grouping for efficient execution
|
|
14
|
+
- Dynamic workflows with conditional logic (If/then/else)
|
|
15
|
+
- Human-in-the-loop approval gates
|
|
16
|
+
- Built-in experiment tracking and model leaderboard
|
|
17
|
+
- LLM/GenAI observability with @trace_llm decorator
|
|
18
|
+
- Data drift detection
|
|
19
|
+
- Pipeline scheduling and notifications
|
|
20
|
+
|
|
21
|
+
## Key Concepts
|
|
22
|
+
1. **Steps**: Use @step decorator with inputs/outputs. Cache with cache="input_hash" or cache="code_hash"
|
|
23
|
+
2. **Pipelines**: Create with Pipeline(name, context=ctx), add steps with .add_step()
|
|
24
|
+
3. **Context**: Define params with context(lr=0.01), auto-injected to steps
|
|
25
|
+
4. **Assets**: Dataset.create(), Model.create(), Metrics.create() - tracked automatically
|
|
26
|
+
5. **Execution Groups**: Group steps with execution_group="name" to share resources
|
|
27
|
+
|
|
28
|
+
## Best Practices
|
|
29
|
+
- Use caching strategically: input_hash for data processing, code_hash for model training
|
|
30
|
+
- Group related steps in execution_group for efficiency
|
|
31
|
+
- Use conditional execution (If/then/else) for dynamic workflows
|
|
32
|
+
- Track experiments with Experiment class
|
|
33
|
+
- Monitor data drift with detect_drift()
|
|
34
|
+
- Use @trace_llm for LLM observability
|
|
35
|
+
|
|
36
|
+
When users share logs, metrics, or pipeline context, analyze them and provide:
|
|
37
|
+
1. Specific, actionable recommendations
|
|
38
|
+
2. Code examples when helpful
|
|
39
|
+
3. Explanations of why certain optimizations work
|
|
40
|
+
4. Links to relevant FlowyML features
|
|
41
|
+
|
|
42
|
+
Be concise but thorough. Format responses with markdown for clarity.`;
|
|
43
|
+
|
|
44
|
+
const AIAssistantContext = createContext(null);
|
|
45
|
+
|
|
46
|
+
// Model configuration - using a smaller, faster model for WebGPU
|
|
47
|
+
const MODEL_ID = "Qwen2.5-1.5B-Instruct-q4f16_1-MLC"; // Fast, lightweight model
|
|
48
|
+
|
|
49
|
+
export function AIAssistantProvider({ children }) {
|
|
50
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
51
|
+
const [messages, setMessages] = useState([]);
|
|
52
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
53
|
+
const [isModelLoading, setIsModelLoading] = useState(false);
|
|
54
|
+
const [loadProgress, setLoadProgress] = useState(0);
|
|
55
|
+
const [loadStatus, setLoadStatus] = useState('');
|
|
56
|
+
const [error, setError] = useState(null);
|
|
57
|
+
const [isWebGPUSupported, setIsWebGPUSupported] = useState(null);
|
|
58
|
+
const [pipelineContext, setPipelineContext] = useState(null);
|
|
59
|
+
const [contextEnabled, setContextEnabled] = useState(true); // User toggle for context sharing
|
|
60
|
+
|
|
61
|
+
const engineRef = useRef(null);
|
|
62
|
+
const abortControllerRef = useRef(null);
|
|
63
|
+
|
|
64
|
+
// Check WebGPU support on mount
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const checkWebGPU = async () => {
|
|
67
|
+
try {
|
|
68
|
+
if (!navigator.gpu) {
|
|
69
|
+
setIsWebGPUSupported(false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
73
|
+
setIsWebGPUSupported(!!adapter);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.warn('WebGPU check failed:', e);
|
|
76
|
+
setIsWebGPUSupported(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
checkWebGPU();
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
// Initialize WebLLM engine
|
|
83
|
+
const initEngine = useCallback(async () => {
|
|
84
|
+
if (engineRef.current || !isWebGPUSupported) return;
|
|
85
|
+
|
|
86
|
+
setIsModelLoading(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
setLoadStatus('Initializing...');
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const engine = await webllm.CreateMLCEngine(MODEL_ID, {
|
|
92
|
+
initProgressCallback: (progress) => {
|
|
93
|
+
setLoadProgress(Math.round(progress.progress * 100));
|
|
94
|
+
setLoadStatus(progress.text || 'Loading model...');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
engineRef.current = engine;
|
|
99
|
+
setLoadStatus('Model ready!');
|
|
100
|
+
setIsModelLoading(false);
|
|
101
|
+
|
|
102
|
+
// Add welcome message
|
|
103
|
+
setMessages([{
|
|
104
|
+
role: 'assistant',
|
|
105
|
+
content: `👋 Hi! I'm your FlowyML AI Assistant, running **locally** in your browser using WebGPU.
|
|
106
|
+
|
|
107
|
+
I have access to your current pipeline context and can help you:
|
|
108
|
+
- 🔧 **Optimize** your pipelines for better performance
|
|
109
|
+
- 🐛 **Debug** failing steps and understand errors
|
|
110
|
+
- 📊 **Analyze** run metrics and suggest improvements
|
|
111
|
+
- 💡 **Learn** FlowyML best practices and features
|
|
112
|
+
|
|
113
|
+
What would you like help with today?`
|
|
114
|
+
}]);
|
|
115
|
+
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error('Failed to initialize WebLLM:', e);
|
|
118
|
+
setError(e.message || 'Failed to load AI model');
|
|
119
|
+
setIsModelLoading(false);
|
|
120
|
+
}
|
|
121
|
+
}, [isWebGPUSupported]);
|
|
122
|
+
|
|
123
|
+
// Send message and get streaming response
|
|
124
|
+
const sendMessage = useCallback(async (content) => {
|
|
125
|
+
if (!engineRef.current || isLoading) return;
|
|
126
|
+
|
|
127
|
+
// Add user message
|
|
128
|
+
const userMessage = { role: 'user', content };
|
|
129
|
+
setMessages(prev => [...prev, userMessage]);
|
|
130
|
+
setIsLoading(true);
|
|
131
|
+
|
|
132
|
+
// Create abort controller for cancellation
|
|
133
|
+
abortControllerRef.current = new AbortController();
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Build context-aware prompt (only if enabled and context exists)
|
|
137
|
+
let contextInfo = '';
|
|
138
|
+
if (contextEnabled && pipelineContext) {
|
|
139
|
+
contextInfo = `\n\n## Current Page Context\nThe user is currently viewing: ${pipelineContext.pageType || 'unknown page'}\n\n### Context Data\n${JSON.stringify(pipelineContext, null, 2)}\n\nUse this context to provide specific, actionable advice about the current pipeline run.\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const systemMessage = FLOWYML_SYSTEM_PROMPT + contextInfo;
|
|
143
|
+
|
|
144
|
+
// Build message history for context
|
|
145
|
+
const chatMessages = [
|
|
146
|
+
{ role: 'system', content: systemMessage },
|
|
147
|
+
...messages.slice(-6), // Keep last 6 messages for context
|
|
148
|
+
userMessage
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
// Add placeholder for assistant response
|
|
152
|
+
const assistantMessage = { role: 'assistant', content: '' };
|
|
153
|
+
setMessages(prev => [...prev, assistantMessage]);
|
|
154
|
+
|
|
155
|
+
// Stream the response
|
|
156
|
+
const asyncGenerator = await engineRef.current.chat.completions.create({
|
|
157
|
+
messages: chatMessages,
|
|
158
|
+
temperature: 0.7,
|
|
159
|
+
max_tokens: 1024,
|
|
160
|
+
stream: true
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
let fullContent = '';
|
|
164
|
+
for await (const chunk of asyncGenerator) {
|
|
165
|
+
if (abortControllerRef.current?.signal.aborted) break;
|
|
166
|
+
|
|
167
|
+
const delta = chunk.choices[0]?.delta?.content || '';
|
|
168
|
+
fullContent += delta;
|
|
169
|
+
|
|
170
|
+
// Update the last message with streamed content
|
|
171
|
+
setMessages(prev => {
|
|
172
|
+
const updated = [...prev];
|
|
173
|
+
updated[updated.length - 1] = { role: 'assistant', content: fullContent };
|
|
174
|
+
return updated;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (e.name !== 'AbortError') {
|
|
180
|
+
console.error('Chat error:', e);
|
|
181
|
+
setMessages(prev => [...prev, {
|
|
182
|
+
role: 'assistant',
|
|
183
|
+
content: `❌ Error: ${e.message || 'Failed to generate response'}`
|
|
184
|
+
}]);
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
setIsLoading(false);
|
|
188
|
+
abortControllerRef.current = null;
|
|
189
|
+
}
|
|
190
|
+
}, [messages, pipelineContext, contextEnabled, isLoading]);
|
|
191
|
+
|
|
192
|
+
// Cancel current generation
|
|
193
|
+
const cancelGeneration = useCallback(() => {
|
|
194
|
+
if (abortControllerRef.current) {
|
|
195
|
+
abortControllerRef.current.abort();
|
|
196
|
+
}
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
// Clear chat history
|
|
200
|
+
const clearChat = useCallback(() => {
|
|
201
|
+
setMessages([{
|
|
202
|
+
role: 'assistant',
|
|
203
|
+
content: '🗑️ Chat cleared. How can I help you with FlowyML?'
|
|
204
|
+
}]);
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
// Update pipeline context (called from other components)
|
|
208
|
+
const updateContext = useCallback((context) => {
|
|
209
|
+
setPipelineContext(context);
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
const value = {
|
|
213
|
+
isOpen,
|
|
214
|
+
setIsOpen,
|
|
215
|
+
messages,
|
|
216
|
+
isLoading,
|
|
217
|
+
isModelLoading,
|
|
218
|
+
loadProgress,
|
|
219
|
+
loadStatus,
|
|
220
|
+
error,
|
|
221
|
+
isWebGPUSupported,
|
|
222
|
+
initEngine,
|
|
223
|
+
sendMessage,
|
|
224
|
+
cancelGeneration,
|
|
225
|
+
clearChat,
|
|
226
|
+
updateContext,
|
|
227
|
+
pipelineContext,
|
|
228
|
+
contextEnabled,
|
|
229
|
+
setContextEnabled
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<AIAssistantContext.Provider value={value}>
|
|
234
|
+
{children}
|
|
235
|
+
</AIAssistantContext.Provider>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function useAIAssistant() {
|
|
240
|
+
const context = useContext(AIAssistantContext);
|
|
241
|
+
if (!context) {
|
|
242
|
+
throw new Error('useAIAssistant must be used within AIAssistantProvider');
|
|
243
|
+
}
|
|
244
|
+
return context;
|
|
245
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { createContext, useState, useContext, useEffect } from 'react';
|
|
2
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
const AuthContext = createContext(null);
|
|
5
|
+
|
|
6
|
+
export const AuthProvider = ({ children }) => {
|
|
7
|
+
const [user, setUser] = useState(null);
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const location = useLocation();
|
|
11
|
+
|
|
12
|
+
// On mount, check if user is already logged in via cookie
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
checkAuthStatus();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const checkAuthStatus = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch('/api/auth/me');
|
|
20
|
+
if (response.ok) {
|
|
21
|
+
const userData = await response.json();
|
|
22
|
+
setUser(userData);
|
|
23
|
+
} else {
|
|
24
|
+
// If 401/403, we are not logged in.
|
|
25
|
+
// If we get here, it means backend didn't auto-redirect or we are just verifying session.
|
|
26
|
+
setUser(null);
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Auth check failed:', error);
|
|
30
|
+
setUser(null);
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const login = async (username, password) => {
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch('/api/auth/login', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({ username, password }),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (response.ok) {
|
|
45
|
+
const data = await response.json();
|
|
46
|
+
await checkAuthStatus(); // Refresh user data
|
|
47
|
+
|
|
48
|
+
// Navigate to where they came from, or dashboard
|
|
49
|
+
const origin = location.state?.from?.pathname || '/';
|
|
50
|
+
navigate(origin);
|
|
51
|
+
return { success: true };
|
|
52
|
+
} else {
|
|
53
|
+
const errorData = await response.json();
|
|
54
|
+
return { success: false, error: errorData.detail || 'Login failed' };
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return { success: false, error: 'Network error' };
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const logout = async () => {
|
|
62
|
+
try {
|
|
63
|
+
await fetch('/api/auth/logout', { method: 'POST' });
|
|
64
|
+
setUser(null);
|
|
65
|
+
navigate('/login');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Logout failed:', error);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const value = {
|
|
72
|
+
user,
|
|
73
|
+
loading,
|
|
74
|
+
login,
|
|
75
|
+
logout,
|
|
76
|
+
checkAuthStatus
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const useAuth = () => {
|
|
83
|
+
return useContext(AuthContext);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Component to protect routes
|
|
87
|
+
export const RequireAuth = ({ children }) => {
|
|
88
|
+
const { user, loading } = useAuth();
|
|
89
|
+
const location = useLocation();
|
|
90
|
+
|
|
91
|
+
if (loading) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="flex h-screen w-screen items-center justify-center bg-gray-900 text-white">
|
|
94
|
+
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500"></div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!user) {
|
|
100
|
+
// Redirect to login page, but save the current location to redirect back after login
|
|
101
|
+
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return children;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Helper for redirect (imported from rrd usually, but defined here for context usage)
|
|
108
|
+
import { Navigate } from 'react-router-dom';
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useEffect, useCallback, useState } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router-dom';
|
|
3
|
+
import { useAIAssistant } from '../contexts/AIAssistantContext';
|
|
4
|
+
import { fetchApi } from '../utils/api';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to share page-specific context with the AI assistant.
|
|
8
|
+
* Fetches comprehensive context from the backend API.
|
|
9
|
+
* Automatically cleans up context when navigating away.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options - Configuration options
|
|
12
|
+
* @param {string} options.pageType - Type of page ('run', 'pipeline', 'experiment', etc.)
|
|
13
|
+
* @param {string} options.resourceId - The resource ID (run_id, pipeline_name, etc.)
|
|
14
|
+
* @param {boolean} options.includeLogs - Whether to include logs (default: true)
|
|
15
|
+
* @param {boolean} options.includeCode - Whether to include step code (default: true)
|
|
16
|
+
* @param {boolean} options.includeMetrics - Whether to include metrics (default: true)
|
|
17
|
+
*/
|
|
18
|
+
export function useAIContext({ pageType, resourceId, includeLogs = true, includeCode = true, includeMetrics = true } = {}) {
|
|
19
|
+
const location = useLocation();
|
|
20
|
+
const { updateContext, contextEnabled } = useAIAssistant();
|
|
21
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
|
|
24
|
+
const fetchContext = useCallback(async () => {
|
|
25
|
+
if (!pageType || !resourceId || !contextEnabled) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setIsLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Fetch comprehensive context from backend
|
|
34
|
+
const response = await fetchApi(`/api/ai/context/${pageType}/${resourceId}?include_logs=${includeLogs}&include_code=${includeCode}&include_metrics=${includeMetrics}`);
|
|
35
|
+
|
|
36
|
+
if (response.ok) {
|
|
37
|
+
const contextData = await response.json();
|
|
38
|
+
|
|
39
|
+
// Update context with backend data
|
|
40
|
+
updateContext({
|
|
41
|
+
pageType: contextData.page_type,
|
|
42
|
+
resourceId: contextData.resource_id,
|
|
43
|
+
...contextData.summary,
|
|
44
|
+
details: contextData.details,
|
|
45
|
+
suggestions: contextData.suggestions,
|
|
46
|
+
timestamp: new Date().toISOString()
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
// Fallback: Use basic context if API fails
|
|
50
|
+
console.warn('AI context API failed, using basic context');
|
|
51
|
+
updateContext({
|
|
52
|
+
pageType,
|
|
53
|
+
resourceId,
|
|
54
|
+
timestamp: new Date().toISOString()
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('Failed to fetch AI context:', err);
|
|
59
|
+
setError(err.message);
|
|
60
|
+
|
|
61
|
+
// Fallback to basic context
|
|
62
|
+
updateContext({
|
|
63
|
+
pageType,
|
|
64
|
+
resourceId,
|
|
65
|
+
timestamp: new Date().toISOString()
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}, [pageType, resourceId, contextEnabled, includeLogs, includeCode, includeMetrics, updateContext]);
|
|
71
|
+
|
|
72
|
+
// Fetch context when enabled or resource changes
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (contextEnabled && pageType && resourceId) {
|
|
75
|
+
fetchContext();
|
|
76
|
+
}
|
|
77
|
+
}, [contextEnabled, pageType, resourceId, fetchContext]);
|
|
78
|
+
|
|
79
|
+
// Cleanup when navigating away
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
return () => {
|
|
82
|
+
updateContext(null);
|
|
83
|
+
};
|
|
84
|
+
}, [location.pathname, updateContext]);
|
|
85
|
+
|
|
86
|
+
return { isLoading, error, refetch: fetchContext };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Formats run data for AI context - client-side fallback.
|
|
91
|
+
* Used when backend API is not available.
|
|
92
|
+
*/
|
|
93
|
+
export function formatRunContext(run, metrics = [], selectedStep = null, logs = {}) {
|
|
94
|
+
if (!run) return null;
|
|
95
|
+
|
|
96
|
+
// Summarize steps
|
|
97
|
+
const stepsSummary = run.steps ? Object.entries(run.steps).map(([name, step]) => ({
|
|
98
|
+
name,
|
|
99
|
+
status: step.success ? 'success' : (step.error ? 'failed' : 'pending'),
|
|
100
|
+
duration: step.duration?.toFixed(2) + 's',
|
|
101
|
+
cached: step.cached || false,
|
|
102
|
+
error: step.error || null,
|
|
103
|
+
inputs: step.inputs?.slice(0, 3),
|
|
104
|
+
outputs: step.outputs?.slice(0, 3)
|
|
105
|
+
})) : [];
|
|
106
|
+
|
|
107
|
+
// Format metrics - take top 10 most relevant
|
|
108
|
+
const metricsSummary = metrics.slice(0, 10).map(m => ({
|
|
109
|
+
name: m.name,
|
|
110
|
+
value: typeof m.value === 'number' ? m.value.toFixed(4) : m.value,
|
|
111
|
+
step: m.step
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// Format logs - truncate to last 500 chars per step
|
|
115
|
+
const logsSummary = {};
|
|
116
|
+
if (logs && typeof logs === 'object') {
|
|
117
|
+
Object.entries(logs).forEach(([stepName, stepLogs]) => {
|
|
118
|
+
if (typeof stepLogs === 'string') {
|
|
119
|
+
logsSummary[stepName] = stepLogs.slice(-500);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Selected step details
|
|
125
|
+
const selectedStepDetails = selectedStep && run.steps?.[selectedStep] ? {
|
|
126
|
+
name: selectedStep,
|
|
127
|
+
...run.steps[selectedStep],
|
|
128
|
+
fullError: run.steps[selectedStep].error,
|
|
129
|
+
sourceCode: run.steps[selectedStep].source_code?.slice(0, 1000)
|
|
130
|
+
} : null;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
pageType: 'run',
|
|
134
|
+
data: {
|
|
135
|
+
runId: run.run_id,
|
|
136
|
+
pipelineName: run.pipeline_name,
|
|
137
|
+
project: run.project,
|
|
138
|
+
status: run.status,
|
|
139
|
+
duration: run.duration?.toFixed(2) + 's',
|
|
140
|
+
startTime: run.start_time,
|
|
141
|
+
endTime: run.end_time,
|
|
142
|
+
totalSteps: stepsSummary.length,
|
|
143
|
+
successfulSteps: stepsSummary.filter(s => s.status === 'success').length,
|
|
144
|
+
failedSteps: stepsSummary.filter(s => s.status === 'failed').length,
|
|
145
|
+
cachedSteps: stepsSummary.filter(s => s.cached).length,
|
|
146
|
+
steps: stepsSummary,
|
|
147
|
+
metrics: metricsSummary,
|
|
148
|
+
selectedStep: selectedStepDetails,
|
|
149
|
+
recentLogs: logsSummary,
|
|
150
|
+
environment: run.environment || {},
|
|
151
|
+
context: run.context || {}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default useAIContext;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to detect WebGPU support in the browser.
|
|
5
|
+
* Returns loading state while checking and final support status.
|
|
6
|
+
*/
|
|
7
|
+
export function useWebGPU() {
|
|
8
|
+
const [isSupported, setIsSupported] = useState(null);
|
|
9
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
10
|
+
const [gpuInfo, setGpuInfo] = useState(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const checkWebGPU = async () => {
|
|
14
|
+
try {
|
|
15
|
+
// Check if WebGPU API is available
|
|
16
|
+
if (!navigator.gpu) {
|
|
17
|
+
setIsSupported(false);
|
|
18
|
+
setIsLoading(false);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Try to get an adapter
|
|
23
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
24
|
+
if (!adapter) {
|
|
25
|
+
setIsSupported(false);
|
|
26
|
+
setIsLoading(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get adapter info for debugging
|
|
31
|
+
const info = await adapter.requestAdapterInfo?.() || {};
|
|
32
|
+
setGpuInfo({
|
|
33
|
+
vendor: info.vendor || 'Unknown',
|
|
34
|
+
architecture: info.architecture || 'Unknown',
|
|
35
|
+
device: info.device || 'Unknown',
|
|
36
|
+
description: info.description || 'WebGPU Available'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
setIsSupported(true);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn('WebGPU detection error:', error);
|
|
42
|
+
setIsSupported(false);
|
|
43
|
+
} finally {
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
checkWebGPU();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
return { isSupported, isLoading, gpuInfo };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default useWebGPU;
|
|
@@ -4,6 +4,8 @@ import { Sidebar } from '../components/sidebar/Sidebar';
|
|
|
4
4
|
import { Header } from '../components/header/Header';
|
|
5
5
|
|
|
6
6
|
import { ErrorBoundary } from '../components/ui/ErrorBoundary';
|
|
7
|
+
import { AIAssistantButton } from '../components/ai/AIAssistantButton';
|
|
8
|
+
import { AIAssistantPanel } from '../components/ai/AIAssistantPanel';
|
|
7
9
|
|
|
8
10
|
export function MainLayout() {
|
|
9
11
|
const [collapsed, setCollapsed] = useState(false);
|
|
@@ -22,6 +24,10 @@ export function MainLayout() {
|
|
|
22
24
|
</div>
|
|
23
25
|
</main>
|
|
24
26
|
</div>
|
|
27
|
+
|
|
28
|
+
{/* AI Assistant - Floating button and slide-out panel */}
|
|
29
|
+
<AIAssistantButton />
|
|
30
|
+
<AIAssistantPanel />
|
|
25
31
|
</div>
|
|
26
32
|
);
|
|
27
33
|
}
|