flowyml 1.4.0__py3-none-any.whl → 1.6.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.
Files changed (51) hide show
  1. flowyml/__init__.py +2 -1
  2. flowyml/assets/featureset.py +30 -5
  3. flowyml/assets/metrics.py +47 -4
  4. flowyml/cli/main.py +21 -0
  5. flowyml/cli/models.py +444 -0
  6. flowyml/cli/rich_utils.py +95 -0
  7. flowyml/core/checkpoint.py +6 -1
  8. flowyml/core/conditional.py +104 -0
  9. flowyml/core/display.py +525 -0
  10. flowyml/core/execution_status.py +1 -0
  11. flowyml/core/executor.py +201 -8
  12. flowyml/core/orchestrator.py +500 -7
  13. flowyml/core/pipeline.py +301 -11
  14. flowyml/core/project.py +4 -1
  15. flowyml/core/scheduler.py +225 -81
  16. flowyml/core/versioning.py +13 -4
  17. flowyml/registry/model_registry.py +1 -1
  18. flowyml/storage/sql.py +53 -13
  19. flowyml/ui/backend/main.py +2 -0
  20. flowyml/ui/backend/routers/assets.py +36 -0
  21. flowyml/ui/backend/routers/execution.py +2 -2
  22. flowyml/ui/backend/routers/runs.py +211 -0
  23. flowyml/ui/backend/routers/stats.py +2 -2
  24. flowyml/ui/backend/routers/websocket.py +121 -0
  25. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
  26. flowyml/ui/frontend/dist/assets/index-CX5RV2C9.js +630 -0
  27. flowyml/ui/frontend/dist/index.html +2 -2
  28. flowyml/ui/frontend/package-lock.json +289 -0
  29. flowyml/ui/frontend/package.json +1 -0
  30. flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
  31. flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
  32. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
  33. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
  34. flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
  35. flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
  36. flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
  37. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
  38. flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
  39. flowyml/ui/frontend/src/components/PipelineGraph.jsx +69 -28
  40. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
  41. flowyml/ui/frontend/src/router/index.jsx +4 -0
  42. flowyml/ui/server_manager.py +181 -0
  43. flowyml/ui/utils.py +63 -1
  44. flowyml/utils/config.py +7 -0
  45. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/METADATA +5 -3
  46. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/RECORD +49 -41
  47. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
  48. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
  49. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/WHEEL +0 -0
  50. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/entry_points.txt +0 -0
  51. {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,289 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useSearchParams, Link } from 'react-router-dom';
3
+ import { fetchApi } from '../../../utils/api';
4
+ import { ArrowLeft, FlaskConical, Calendar, CheckCircle, XCircle, AlertTriangle, Activity, Clock, Layout } from 'lucide-react';
5
+ import { Card } from '../../../components/ui/Card';
6
+ import { Badge } from '../../../components/ui/Badge';
7
+ import { Button } from '../../../components/ui/Button';
8
+ import { format } from 'date-fns';
9
+
10
+ export function ExperimentComparisonPage() {
11
+ const [searchParams] = useSearchParams();
12
+ const [experiments, setExperiments] = useState([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [error, setError] = useState(null);
15
+
16
+ const idsParam = searchParams.get('ids');
17
+ const ids = idsParam ? idsParam.split(',').filter(Boolean) : [];
18
+
19
+ useEffect(() => {
20
+ if (ids.length < 2) {
21
+ setError('Please select at least two experiments to compare.');
22
+ setLoading(false);
23
+ return;
24
+ }
25
+ fetchData();
26
+ }, [idsParam]);
27
+
28
+ const fetchData = async () => {
29
+ setLoading(true);
30
+ try {
31
+ // 1. Fetch Experiments Details
32
+ // Since we don't have a bulk API, we fetch individually
33
+ // Ideally we'd have /api/experiments?ids=... or similar
34
+ // But we can just iterate.
35
+
36
+ // Note: Experiment API might not return run stats directly.
37
+ // We might need to fetch runs for each experiment to calculate stats if they aren't provided.
38
+ // Let's assume /api/experiments/{id} returns basic details.
39
+
40
+ // To get stats, we might need to fetch runs.
41
+ // Let's parallelize fetching experiment details.
42
+ // CAUTION: If /api/experiments/{id} doesn't exist, we might need to filter from the list API.
43
+ // Let's assume filtering from list API is safer if individual endpoint isn't guaranteed.
44
+ // Actually, usually detail endpoint exists. Let's try to map the list first since current API usage in Experiments page used list.
45
+
46
+ // Let's use the list endpoint with a filter if possible, or just fetch all and filter client side (not efficient but safe if list is small).
47
+ // Better: Fetch all experiments (like in main page) and find the ones we need.
48
+ // This is safer given we know /api/experiments/ endpoint works.
49
+
50
+ const res = await fetchApi('/api/experiments/?limit=1000'); // Fetch enough
51
+ const data = await res.json();
52
+ const allExperiments = data.experiments || [];
53
+
54
+ const selectedExperiments = allExperiments.filter(e => ids.includes(e.experiment_id));
55
+
56
+ // Now for each experiment, we need run stats.
57
+ // The list object might have run_count. But for success rate we need runs.
58
+ // Let's fetch runs for each selected experiment.
59
+ // This could be heavy, but it's "Comparison" page, user expects detail.
60
+
61
+ const enrichedExperiments = await Promise.all(selectedExperiments.map(async (exp) => {
62
+ try {
63
+ // Fetch runs for this experiment.
64
+ // Assuming we can filter by pipeline or if the backend supports filter by experiment
65
+ // Looking at `ExperimentDetailsPanel`, it filters by pipeline_name AND run_id list.
66
+ // Let's replicate that logic.
67
+
68
+ const runsUrl = exp.pipeline_name
69
+ ? `/api/runs?pipeline=${encodeURIComponent(exp.pipeline_name)}&limit=100`
70
+ : `/api/runs?limit=100`;
71
+
72
+ const runsRes = await fetchApi(runsUrl);
73
+ const runsData = await runsRes.json();
74
+
75
+ // Filter relevant runs
76
+ const relevantRuns = (runsData.runs || []).filter(r =>
77
+ r.pipeline_name === exp.pipeline_name ||
78
+ (exp.runs && exp.runs.includes(r.run_id))
79
+ );
80
+
81
+ // Calculate stats
82
+ const total = relevantRuns.length;
83
+ const success = relevantRuns.filter(r => r.status === 'completed').length;
84
+ const failed = relevantRuns.filter(r => r.status === 'failed').length;
85
+ const durations = relevantRuns.map(r => r.duration || 0).filter(d => d > 0);
86
+ const avgDuration = durations.length > 0
87
+ ? durations.reduce((a, b) => a + b, 0) / durations.length
88
+ : 0;
89
+
90
+ return {
91
+ ...exp,
92
+ stats: {
93
+ total,
94
+ success,
95
+ failed,
96
+ avgDuration,
97
+ successRate: total > 0 ? (success / total) * 100 : 0
98
+ }
99
+ };
100
+ } catch (e) {
101
+ console.error(`Failed to fetch stats for experiment ${exp.name}`, e);
102
+ return { ...exp, stats: { total: 0, success: 0, failed: 0, avgDuration: 0, successRate: 0 } };
103
+ }
104
+ }));
105
+
106
+ // Sort by order of IDs in param to maintain user selection order
107
+ const ordered = ids.map(id => enrichedExperiments.find(e => e.experiment_id === id)).filter(Boolean);
108
+
109
+ setExperiments(ordered);
110
+ } catch (err) {
111
+ setError(err.message);
112
+ } finally {
113
+ setLoading(false);
114
+ }
115
+ };
116
+
117
+ if (loading) {
118
+ return (
119
+ <div className="flex flex-col items-center justify-center h-screen bg-slate-50 dark:bg-slate-900">
120
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
121
+ <p className="text-slate-500">Loading experiments...</p>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ if (error) {
127
+ return (
128
+ <div className="flex flex-col items-center justify-center h-screen bg-slate-50 dark:bg-slate-900">
129
+ <div className="bg-red-50 p-6 rounded-xl border border-red-100 max-w-md text-center">
130
+ <AlertTriangle className="mx-auto text-red-500 mb-2" size={32} />
131
+ <h2 className="text-xl font-bold text-slate-800 mb-2">Error</h2>
132
+ <p className="text-slate-600 mb-4">{error}</p>
133
+ <Link to="/experiments">
134
+ <Button>Back to Experiments</Button>
135
+ </Link>
136
+ </div>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ const highestSuccessRate = Math.max(...experiments.map(e => e.stats.successRate));
142
+ const lowestAvgDuration = Math.min(...experiments.map(e => e.stats.avgDuration).filter(d => d > 0));
143
+
144
+ return (
145
+ <div className="min-h-screen bg-slate-50 dark:bg-slate-900 overflow-y-auto">
146
+ {/* Header */}
147
+ <div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 sticky top-0 z-10 w-full overflow-x-auto">
148
+ <div className="w-full min-w-max px-6 py-4">
149
+ <div className="flex items-center gap-4 mb-4">
150
+ <Link to="/experiments" className="p-2 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-full transition-colors">
151
+ <ArrowLeft size={20} className="text-slate-500" />
152
+ </Link>
153
+ <h1 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
154
+ Experiment Comparison
155
+ <Badge variant="secondary" className="ml-2">({experiments.length} selected)</Badge>
156
+ </h1>
157
+ </div>
158
+
159
+ <div className="grid gap-4" style={{ gridTemplateColumns: `repeat(${experiments.length}, minmax(300px, 1fr))` }}>
160
+ {experiments.map(exp => (
161
+ <div key={exp.experiment_id} className="flex items-center gap-3 p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700">
162
+ <div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg text-purple-600">
163
+ <FlaskConical size={20} />
164
+ </div>
165
+ <div className="min-w-0">
166
+ <h3 className="font-bold text-slate-900 dark:text-white truncate" title={exp.name}>{exp.name}</h3>
167
+ <p className="text-xs text-slate-500">{format(new Date(exp.created_at || Date.now()), 'MMM d, yyyy')}</p>
168
+ </div>
169
+ </div>
170
+ ))}
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <div className="w-full overflow-x-auto">
176
+ <div className="min-w-max px-6 py-8 space-y-8">
177
+ {/* Overview Comparison */}
178
+ <section>
179
+ <h2 className="text-lg font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
180
+ <Layout size={18} /> Overview
181
+ </h2>
182
+ <Card className="overflow-hidden">
183
+ <table className="w-full text-sm text-left">
184
+ <thead className="bg-slate-50 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 text-xs uppercase text-slate-500 font-medium">
185
+ <tr>
186
+ <th className="px-6 py-3 w-48 sticky left-0 bg-slate-50 dark:bg-slate-800 z-10 border-r border-slate-200 dark:border-slate-700">Attribute</th>
187
+ {experiments.map((exp, i) => (
188
+ <th key={exp.experiment_id} className={`px-6 py-3 min-w-[300px] ${i < experiments.length - 1 ? 'border-r border-slate-200 dark:border-slate-700' : ''}`}>
189
+ {exp.name}
190
+ </th>
191
+ ))}
192
+ </tr>
193
+ </thead>
194
+ <tbody className="divide-y divide-slate-100 dark:divide-slate-700">
195
+ <tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
196
+ <td className="px-6 py-3 font-medium text-slate-700 dark:text-slate-300 sticky left-0 bg-white dark:bg-slate-900 border-r border-slate-100 dark:border-slate-700">Description</td>
197
+ {experiments.map((exp, i) => (
198
+ <td key={exp.experiment_id} className={`px-6 py-3 text-slate-600 ${i < experiments.length - 1 ? 'border-r border-slate-100 dark:border-slate-700' : ''}`}>
199
+ {exp.description || '-'}
200
+ </td>
201
+ ))}
202
+ </tr>
203
+ <tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
204
+ <td className="px-6 py-3 font-medium text-slate-700 dark:text-slate-300 sticky left-0 bg-white dark:bg-slate-900 border-r border-slate-100 dark:border-slate-700">Project</td>
205
+ {experiments.map((exp, i) => (
206
+ <td key={exp.experiment_id} className={`px-6 py-3 text-slate-600 ${i < experiments.length - 1 ? 'border-r border-slate-100 dark:border-slate-700' : ''}`}>
207
+ {exp.project || '-'}
208
+ </td>
209
+ ))}
210
+ </tr>
211
+ <tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
212
+ <td className="px-6 py-3 font-medium text-slate-700 dark:text-slate-300 sticky left-0 bg-white dark:bg-slate-900 border-r border-slate-100 dark:border-slate-700">Pipeline Base</td>
213
+ {experiments.map((exp, i) => (
214
+ <td key={exp.experiment_id} className={`px-6 py-3 text-slate-600 ${i < experiments.length - 1 ? 'border-r border-slate-100 dark:border-slate-700' : ''}`}>
215
+ {exp.pipeline_name || '-'}
216
+ {exp.pipeline_version && <Badge variant="outline" className="ml-2">v{exp.pipeline_version}</Badge>}
217
+ </td>
218
+ ))}
219
+ </tr>
220
+ </tbody>
221
+ </table>
222
+ </Card>
223
+ </section>
224
+
225
+ {/* Stats Comparison */}
226
+ <section>
227
+ <h2 className="text-lg font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
228
+ <Activity size={18} /> Performance Stats
229
+ </h2>
230
+ <Card className="overflow-hidden">
231
+ <table className="w-full text-sm text-left">
232
+ <thead className="bg-slate-50 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 text-xs uppercase text-slate-500 font-medium">
233
+ <tr>
234
+ <th className="px-6 py-3 w-48 sticky left-0 bg-slate-50 dark:bg-slate-800 z-10 border-r border-slate-200 dark:border-slate-700">Metric</th>
235
+ {experiments.map((exp, i) => (
236
+ <th key={exp.experiment_id} className={`px-6 py-3 min-w-[300px] ${i < experiments.length - 1 ? 'border-r border-slate-200 dark:border-slate-700' : ''}`}>
237
+ {exp.name}
238
+ </th>
239
+ ))}
240
+ </tr>
241
+ </thead>
242
+ <tbody className="divide-y divide-slate-100 dark:divide-slate-700">
243
+ <tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
244
+ <td className="px-6 py-3 font-medium text-slate-700 dark:text-slate-300 sticky left-0 bg-white dark:bg-slate-900 border-r border-slate-100 dark:border-slate-700">Success Rate</td>
245
+ {experiments.map((exp, i) => (
246
+ <td key={exp.experiment_id} className={`px-6 py-3 ${i < experiments.length - 1 ? 'border-r border-slate-100 dark:border-slate-700' : ''} ${exp.stats.successRate === highestSuccessRate && exp.stats.successRate > 0 ? 'text-emerald-600 font-bold' : 'text-slate-600'}`}>
247
+ {exp.stats.successRate.toFixed(1)}%
248
+ {exp.stats.successRate === highestSuccessRate && exp.stats.successRate > 0 && experiments.length > 1 &&
249
+ <span className="ml-2 text-xs bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 px-1.5 py-0.5 rounded-full">Best</span>
250
+ }
251
+ </td>
252
+ ))}
253
+ </tr>
254
+ <tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
255
+ <td className="px-6 py-3 font-medium text-slate-700 dark:text-slate-300 sticky left-0 bg-white dark:bg-slate-900 border-r border-slate-100 dark:border-slate-700">Avg Duration</td>
256
+ {experiments.map((exp, i) => (
257
+ <td key={exp.experiment_id} className={`px-6 py-3 ${i < experiments.length - 1 ? 'border-r border-slate-100 dark:border-slate-700' : ''} ${exp.stats.avgDuration === lowestAvgDuration && exp.stats.avgDuration > 0 ? 'text-emerald-600 font-bold' : 'text-slate-600'}`}>
258
+ {exp.stats.avgDuration.toFixed(1)}s
259
+ {exp.stats.avgDuration === lowestAvgDuration && exp.stats.avgDuration > 0 && experiments.length > 1 &&
260
+ <span className="ml-2 text-xs bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 px-1.5 py-0.5 rounded-full">Fastest</span>
261
+ }
262
+ </td>
263
+ ))}
264
+ </tr>
265
+ <tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
266
+ <td className="px-6 py-3 font-medium text-slate-700 dark:text-slate-300 sticky left-0 bg-white dark:bg-slate-900 border-r border-slate-100 dark:border-slate-700">Total Runs</td>
267
+ {experiments.map((exp, i) => (
268
+ <td key={exp.experiment_id} className={`px-6 py-3 text-slate-600 ${i < experiments.length - 1 ? 'border-r border-slate-100 dark:border-slate-700' : ''}`}>
269
+ {exp.stats.total}
270
+ </td>
271
+ ))}
272
+ </tr>
273
+ <tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
274
+ <td className="px-6 py-3 font-medium text-slate-700 dark:text-slate-300 sticky left-0 bg-white dark:bg-slate-900 border-r border-slate-100 dark:border-slate-700">Failures</td>
275
+ {experiments.map((exp, i) => (
276
+ <td key={exp.experiment_id} className={`px-6 py-3 ${i < experiments.length - 1 ? 'border-r border-slate-100 dark:border-slate-700' : ''} ${exp.stats.failed > 0 ? 'text-rose-600' : 'text-emerald-600'}`}>
277
+ {exp.stats.failed}
278
+ </td>
279
+ ))}
280
+ </tr>
281
+ </tbody>
282
+ </table>
283
+ </Card>
284
+ </section>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ );
289
+ }
@@ -19,6 +19,10 @@ export function Experiments() {
19
19
  const { selectedProject } = useProject();
20
20
  const navigate = useNavigate();
21
21
 
22
+ // Selection & Comparison State
23
+ const [selectionMode, setSelectionMode] = useState('single');
24
+ const [selectedIds, setSelectedIds] = useState([]);
25
+
22
26
  useEffect(() => {
23
27
  const fetchExperiments = async () => {
24
28
  setLoading(true);
@@ -39,7 +43,33 @@ export function Experiments() {
39
43
  }, [selectedProject]);
40
44
 
41
45
  const handleExperimentSelect = (experiment) => {
42
- setSelectedExperiment(experiment);
46
+ if (selectionMode === 'single') {
47
+ setSelectedExperiment(experiment);
48
+ }
49
+ };
50
+
51
+ const toggleSelectionMode = () => {
52
+ if (selectionMode === 'single') {
53
+ setSelectionMode('multi');
54
+ setSelectedIds([]);
55
+ } else {
56
+ setSelectionMode('single');
57
+ setSelectedIds([]);
58
+ }
59
+ };
60
+
61
+ const handleMultiSelect = (id, checked) => {
62
+ if (checked) {
63
+ setSelectedIds(prev => [...prev, id]);
64
+ } else {
65
+ setSelectedIds(prev => prev.filter(i => i !== id));
66
+ }
67
+ };
68
+
69
+ const handleCompare = () => {
70
+ if (selectedIds.length >= 2) {
71
+ navigate(`/experiments/compare?ids=${selectedIds.join(',')}`);
72
+ }
43
73
  };
44
74
 
45
75
  return (
@@ -56,6 +86,33 @@ export function Experiments() {
56
86
  Manage and track your ML experiments
57
87
  </p>
58
88
  </div>
89
+ <div className="flex items-center gap-3">
90
+ {selectionMode === 'multi' ? (
91
+ <>
92
+ <span className="text-sm text-slate-500 mr-2">
93
+ {selectedIds.length} selected
94
+ </span>
95
+ <Button
96
+ size="sm"
97
+ variant="primary"
98
+ disabled={selectedIds.length < 2}
99
+ onClick={handleCompare}
100
+ >
101
+ <Activity size={16} className="mr-2" />
102
+ Compare
103
+ </Button>
104
+ <Button variant="ghost" size="sm" onClick={toggleSelectionMode}>
105
+ <Layout size={16} className="mr-2" />
106
+ Cancel
107
+ </Button>
108
+ </>
109
+ ) : (
110
+ <Button variant="outline" size="sm" onClick={toggleSelectionMode}>
111
+ <Activity size={16} className="mr-2" />
112
+ Select to Compare
113
+ </Button>
114
+ )}
115
+ </div>
59
116
  </div>
60
117
  </div>
61
118
 
@@ -74,6 +131,9 @@ export function Experiments() {
74
131
  projectId={selectedProject}
75
132
  onSelect={handleExperimentSelect}
76
133
  selectedId={selectedExperiment?.experiment_id}
134
+ selectionMode={selectionMode}
135
+ selectedIds={selectedIds}
136
+ onMultiSelect={handleMultiSelect}
77
137
  />
78
138
  </div>
79
139
  </div>