flowyml 1.1.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/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Experiment tracking and run management."""
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Experiment Tracking - Track ML experiments and compare results."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ExperimentConfig:
|
|
12
|
+
"""Configuration for an experiment."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
description: str
|
|
16
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
17
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, Any]:
|
|
21
|
+
"""Convert to dictionary."""
|
|
22
|
+
return {
|
|
23
|
+
"name": self.name,
|
|
24
|
+
"description": self.description,
|
|
25
|
+
"tags": self.tags,
|
|
26
|
+
"parameters": self.parameters,
|
|
27
|
+
"created_at": self.created_at.isoformat(),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Experiment:
|
|
32
|
+
"""Experiment for tracking multiple pipeline runs.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> exp = Experiment(name="baseline_training", description="Baseline model training")
|
|
36
|
+
>>> exp.log_run(run_id, metrics={"accuracy": 0.95})
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
name: str,
|
|
42
|
+
description: str = "",
|
|
43
|
+
tags: dict[str, str] | None = None,
|
|
44
|
+
parameters: dict[str, Any] | None = None,
|
|
45
|
+
experiment_dir: str = ".flowyml/experiments",
|
|
46
|
+
):
|
|
47
|
+
self.name = name
|
|
48
|
+
self.description = description
|
|
49
|
+
self.config = ExperimentConfig(
|
|
50
|
+
name=name,
|
|
51
|
+
description=description,
|
|
52
|
+
tags=tags or {},
|
|
53
|
+
parameters=parameters or {},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Storage
|
|
57
|
+
self.experiment_dir = Path(experiment_dir) / name
|
|
58
|
+
self.experiment_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# Metadata store for UI
|
|
61
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
62
|
+
|
|
63
|
+
self.metadata_store = SQLiteMetadataStore()
|
|
64
|
+
|
|
65
|
+
# Save experiment to DB
|
|
66
|
+
self.metadata_store.save_experiment(
|
|
67
|
+
experiment_id=self.name,
|
|
68
|
+
name=self.name,
|
|
69
|
+
description=self.description,
|
|
70
|
+
tags=tags,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Runs
|
|
74
|
+
self.runs: list[str] = [] # run IDs
|
|
75
|
+
self.run_metrics: dict[str, dict[str, Any]] = {}
|
|
76
|
+
|
|
77
|
+
self._load_experiment()
|
|
78
|
+
|
|
79
|
+
def log_run(
|
|
80
|
+
self,
|
|
81
|
+
run_id: str,
|
|
82
|
+
metrics: dict[str, Any] | None = None,
|
|
83
|
+
parameters: dict[str, Any] | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Log a pipeline run to this experiment.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
run_id: Run identifier
|
|
89
|
+
metrics: Metrics from the run
|
|
90
|
+
parameters: Parameters used in the run
|
|
91
|
+
"""
|
|
92
|
+
if run_id not in self.runs:
|
|
93
|
+
self.runs.append(run_id)
|
|
94
|
+
|
|
95
|
+
self.run_metrics[run_id] = {
|
|
96
|
+
"metrics": metrics or {},
|
|
97
|
+
"parameters": parameters or {},
|
|
98
|
+
"timestamp": datetime.now().isoformat(),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
self._save_experiment()
|
|
102
|
+
|
|
103
|
+
# Log to DB
|
|
104
|
+
self.metadata_store.log_experiment_run(
|
|
105
|
+
experiment_id=self.name,
|
|
106
|
+
run_id=run_id,
|
|
107
|
+
metrics=metrics,
|
|
108
|
+
parameters=parameters,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def get_run_metrics(self, run_id: str) -> dict[str, Any] | None:
|
|
112
|
+
"""Get metrics for a specific run."""
|
|
113
|
+
return self.run_metrics.get(run_id)
|
|
114
|
+
|
|
115
|
+
def list_runs(self) -> list[str]:
|
|
116
|
+
"""List all runs in this experiment."""
|
|
117
|
+
return self.runs
|
|
118
|
+
|
|
119
|
+
def compare_runs(
|
|
120
|
+
self,
|
|
121
|
+
run_ids: list[str] | None = None,
|
|
122
|
+
metric: str | None = None,
|
|
123
|
+
) -> dict[str, Any]:
|
|
124
|
+
"""Compare runs in this experiment.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
run_ids: Specific runs to compare (or all if None)
|
|
128
|
+
metric: Specific metric to compare (or all if None)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Comparison results
|
|
132
|
+
"""
|
|
133
|
+
runs_to_compare = run_ids or self.runs
|
|
134
|
+
|
|
135
|
+
comparison = {
|
|
136
|
+
"experiment": self.name,
|
|
137
|
+
"runs": {},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for run_id in runs_to_compare:
|
|
141
|
+
if run_id not in self.run_metrics:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
run_data = self.run_metrics[run_id]
|
|
145
|
+
metrics = run_data.get("metrics", {})
|
|
146
|
+
|
|
147
|
+
if metric:
|
|
148
|
+
comparison["runs"][run_id] = {metric: metrics.get(metric)}
|
|
149
|
+
else:
|
|
150
|
+
comparison["runs"][run_id] = metrics
|
|
151
|
+
|
|
152
|
+
return comparison
|
|
153
|
+
|
|
154
|
+
def get_best_run(self, metric: str, maximize: bool = True) -> str | None:
|
|
155
|
+
"""Get the best run based on a metric.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
metric: Metric to optimize
|
|
159
|
+
maximize: Whether to maximize (True) or minimize (False)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Best run ID
|
|
163
|
+
"""
|
|
164
|
+
best_run = None
|
|
165
|
+
best_value = float("-inf") if maximize else float("inf")
|
|
166
|
+
|
|
167
|
+
for run_id, run_data in self.run_metrics.items():
|
|
168
|
+
metrics = run_data.get("metrics", {})
|
|
169
|
+
value = metrics.get(metric)
|
|
170
|
+
|
|
171
|
+
if value is None:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if maximize and value > best_value or not maximize and value < best_value:
|
|
175
|
+
best_value = value
|
|
176
|
+
best_run = run_id
|
|
177
|
+
|
|
178
|
+
return best_run
|
|
179
|
+
|
|
180
|
+
def _save_experiment(self) -> None:
|
|
181
|
+
"""Save experiment data to disk."""
|
|
182
|
+
experiment_file = self.experiment_dir / "experiment.json"
|
|
183
|
+
|
|
184
|
+
data = {
|
|
185
|
+
"config": self.config.to_dict(),
|
|
186
|
+
"runs": self.runs,
|
|
187
|
+
"run_metrics": self.run_metrics,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
with open(experiment_file, "w") as f:
|
|
191
|
+
json.dump(data, f, indent=2)
|
|
192
|
+
|
|
193
|
+
def _load_experiment(self) -> None:
|
|
194
|
+
"""Load experiment data from disk."""
|
|
195
|
+
experiment_file = self.experiment_dir / "experiment.json"
|
|
196
|
+
|
|
197
|
+
if not experiment_file.exists():
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
with open(experiment_file) as f:
|
|
202
|
+
data = json.load(f)
|
|
203
|
+
|
|
204
|
+
self.runs = data.get("runs", [])
|
|
205
|
+
self.run_metrics = data.get("run_metrics", {})
|
|
206
|
+
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
def __repr__(self) -> str:
|
|
211
|
+
return f"Experiment(name='{self.name}', runs={len(self.runs)})"
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Model comparison and leaderboard functionality."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ModelScore:
|
|
10
|
+
"""A model's score on a leaderboard."""
|
|
11
|
+
|
|
12
|
+
model_name: str
|
|
13
|
+
run_id: str
|
|
14
|
+
metric_value: float
|
|
15
|
+
timestamp: str
|
|
16
|
+
metadata: dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelLeaderboard:
|
|
20
|
+
"""Compare and rank models based on metrics.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> leaderboard = ModelLeaderboard(metric="accuracy")
|
|
24
|
+
>>> # Add model scores
|
|
25
|
+
>>> leaderboard.add_score(model_name="bert-base", run_id="run_123", score=0.92)
|
|
26
|
+
>>> # Get top models
|
|
27
|
+
>>> top_models = leaderboard.get_top(n=5)
|
|
28
|
+
>>> leaderboard.display()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
metric: str,
|
|
34
|
+
higher_is_better: bool = True,
|
|
35
|
+
metadata_store: SQLiteMetadataStore | None = None,
|
|
36
|
+
):
|
|
37
|
+
"""Args:
|
|
38
|
+
metric: Metric to compare on
|
|
39
|
+
higher_is_better: Whether higher values are better
|
|
40
|
+
metadata_store: Optional metadata store.
|
|
41
|
+
"""
|
|
42
|
+
self.metric = metric
|
|
43
|
+
self.higher_is_better = higher_is_better
|
|
44
|
+
self.metadata_store = metadata_store or SQLiteMetadataStore()
|
|
45
|
+
|
|
46
|
+
def add_score(
|
|
47
|
+
self,
|
|
48
|
+
model_name: str,
|
|
49
|
+
run_id: str,
|
|
50
|
+
score: float,
|
|
51
|
+
metadata: dict[str, Any] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Add a model score to the leaderboard."""
|
|
54
|
+
# Save to metadata store
|
|
55
|
+
self.metadata_store.save_metric(
|
|
56
|
+
run_id=run_id,
|
|
57
|
+
name=f"leaderboard_{self.metric}",
|
|
58
|
+
value=score,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Also save model info as artifact
|
|
62
|
+
self.metadata_store.save_artifact(
|
|
63
|
+
artifact_id=f"{run_id}_leaderboard",
|
|
64
|
+
metadata={
|
|
65
|
+
"name": model_name,
|
|
66
|
+
"type": "leaderboard_entry",
|
|
67
|
+
"run_id": run_id,
|
|
68
|
+
"value": str(score),
|
|
69
|
+
"metric": self.metric,
|
|
70
|
+
"metadata": metadata or {},
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def get_top(self, n: int = 10) -> list[ModelScore]:
|
|
75
|
+
"""Get top N models."""
|
|
76
|
+
# Query all leaderboard entries
|
|
77
|
+
artifacts = self.metadata_store.list_assets(type="leaderboard_entry")
|
|
78
|
+
|
|
79
|
+
# Filter by metric
|
|
80
|
+
entries = [a for a in artifacts if a.get("metric") == self.metric]
|
|
81
|
+
|
|
82
|
+
# Sort
|
|
83
|
+
entries.sort(
|
|
84
|
+
key=lambda x: float(x.get("value", 0)),
|
|
85
|
+
reverse=self.higher_is_better,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Convert to ModelScore
|
|
89
|
+
scores = []
|
|
90
|
+
for entry in entries[:n]:
|
|
91
|
+
scores.append(
|
|
92
|
+
ModelScore(
|
|
93
|
+
model_name=entry.get("name"),
|
|
94
|
+
run_id=entry.get("run_id"),
|
|
95
|
+
metric_value=float(entry.get("value", 0)),
|
|
96
|
+
timestamp=entry.get("created_at", ""),
|
|
97
|
+
metadata=entry.get("metadata", {}),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return scores
|
|
102
|
+
|
|
103
|
+
def display(self, n: int = 10) -> None:
|
|
104
|
+
"""Display the leaderboard."""
|
|
105
|
+
scores = self.get_top(n)
|
|
106
|
+
|
|
107
|
+
if not scores:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
for _i, _score in enumerate(scores, 1):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def compare_models(self, run_ids: list[str]) -> dict[str, Any]:
|
|
114
|
+
"""Compare specific models side-by-side."""
|
|
115
|
+
comparison = {
|
|
116
|
+
"metric": self.metric,
|
|
117
|
+
"models": [],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for run_id in run_ids:
|
|
121
|
+
# Get metrics for this run
|
|
122
|
+
metrics = self.metadata_store.get_metrics(run_id)
|
|
123
|
+
metric_val = next(
|
|
124
|
+
(m["value"] for m in metrics if m["name"] == self.metric),
|
|
125
|
+
None,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Get run details
|
|
129
|
+
run = self.metadata_store.load_run(run_id)
|
|
130
|
+
|
|
131
|
+
comparison["models"].append(
|
|
132
|
+
{
|
|
133
|
+
"run_id": run_id,
|
|
134
|
+
"metric_value": metric_val,
|
|
135
|
+
"pipeline": run.get("pipeline_name") if run else None,
|
|
136
|
+
"status": run.get("status") if run else None,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Sort by metric value
|
|
141
|
+
comparison["models"].sort(
|
|
142
|
+
key=lambda x: x.get("metric_value", 0) or 0,
|
|
143
|
+
reverse=self.higher_is_better,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return comparison
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compare_runs(
|
|
150
|
+
run_ids: list[str],
|
|
151
|
+
metrics: list[str] | None = None,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""Compare multiple runs across metrics.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
run_ids: List of run IDs to compare
|
|
157
|
+
metrics: Optional list of specific metrics to compare
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Comparison dictionary
|
|
161
|
+
"""
|
|
162
|
+
store = SQLiteMetadataStore()
|
|
163
|
+
|
|
164
|
+
comparison = {
|
|
165
|
+
"runs": {},
|
|
166
|
+
"metrics": {},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Collect all metrics across runs
|
|
170
|
+
all_metric_names = set()
|
|
171
|
+
|
|
172
|
+
for run_id in run_ids:
|
|
173
|
+
run_metrics = store.get_metrics(run_id)
|
|
174
|
+
run_data = {
|
|
175
|
+
"metrics": {m["name"]: m["value"] for m in run_metrics},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
all_metric_names.update(run_data["metrics"].keys())
|
|
179
|
+
comparison["runs"][run_id] = run_data
|
|
180
|
+
|
|
181
|
+
# Filter metrics if specified
|
|
182
|
+
if metrics:
|
|
183
|
+
all_metric_names = set(metrics)
|
|
184
|
+
|
|
185
|
+
# Organize by metric
|
|
186
|
+
for metric_name in all_metric_names:
|
|
187
|
+
comparison["metrics"][metric_name] = {
|
|
188
|
+
run_id: comparison["runs"][run_id]["metrics"].get(metric_name) for run_id in run_ids
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return comparison
|
flowyml/tracking/runs.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Run Management - Track individual pipeline runs."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class RunMetadata:
|
|
12
|
+
"""Metadata for a pipeline run."""
|
|
13
|
+
|
|
14
|
+
run_id: str
|
|
15
|
+
pipeline_name: str
|
|
16
|
+
started_at: datetime
|
|
17
|
+
ended_at: datetime | None = None
|
|
18
|
+
status: str = "running" # running, success, failed
|
|
19
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, Any]:
|
|
24
|
+
"""Convert to dictionary."""
|
|
25
|
+
return {
|
|
26
|
+
"run_id": self.run_id,
|
|
27
|
+
"pipeline_name": self.pipeline_name,
|
|
28
|
+
"started_at": self.started_at.isoformat(),
|
|
29
|
+
"ended_at": self.ended_at.isoformat() if self.ended_at else None,
|
|
30
|
+
"status": self.status,
|
|
31
|
+
"parameters": self.parameters,
|
|
32
|
+
"metrics": self.metrics,
|
|
33
|
+
"tags": self.tags,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Run:
|
|
38
|
+
"""Represents a single pipeline execution run.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> run = Run(run_id="training_20240115_120000", pipeline_name="training_pipeline")
|
|
42
|
+
>>> run.log_metric("accuracy", 0.95)
|
|
43
|
+
>>> run.complete(status="success")
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
run_id: str,
|
|
49
|
+
pipeline_name: str,
|
|
50
|
+
parameters: dict[str, Any] | None = None,
|
|
51
|
+
tags: dict[str, str] | None = None,
|
|
52
|
+
runs_dir: str = ".flowyml/runs",
|
|
53
|
+
):
|
|
54
|
+
self.run_id = run_id
|
|
55
|
+
self.pipeline_name = pipeline_name
|
|
56
|
+
|
|
57
|
+
self.metadata = RunMetadata(
|
|
58
|
+
run_id=run_id,
|
|
59
|
+
pipeline_name=pipeline_name,
|
|
60
|
+
started_at=datetime.now(),
|
|
61
|
+
parameters=parameters or {},
|
|
62
|
+
tags=tags or {},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Storage
|
|
66
|
+
self.runs_dir = Path(runs_dir)
|
|
67
|
+
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
self._save()
|
|
70
|
+
|
|
71
|
+
def log_metric(self, name: str, value: Any) -> None:
|
|
72
|
+
"""Log a metric for this run."""
|
|
73
|
+
self.metadata.metrics[name] = value
|
|
74
|
+
self._save()
|
|
75
|
+
|
|
76
|
+
def log_metrics(self, metrics: dict[str, Any]) -> None:
|
|
77
|
+
"""Log multiple metrics."""
|
|
78
|
+
self.metadata.metrics.update(metrics)
|
|
79
|
+
self._save()
|
|
80
|
+
|
|
81
|
+
def log_parameter(self, name: str, value: Any) -> None:
|
|
82
|
+
"""Log a parameter."""
|
|
83
|
+
self.metadata.parameters[name] = value
|
|
84
|
+
self._save()
|
|
85
|
+
|
|
86
|
+
def add_tag(self, key: str, value: str) -> None:
|
|
87
|
+
"""Add a tag to the run."""
|
|
88
|
+
self.metadata.tags[key] = value
|
|
89
|
+
self._save()
|
|
90
|
+
|
|
91
|
+
def complete(self, status: str = "success") -> None:
|
|
92
|
+
"""Mark run as complete.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
status: Final status ("success" or "failed")
|
|
96
|
+
"""
|
|
97
|
+
self.metadata.status = status
|
|
98
|
+
self.metadata.ended_at = datetime.now()
|
|
99
|
+
self._save()
|
|
100
|
+
|
|
101
|
+
def get_duration(self) -> float | None:
|
|
102
|
+
"""Get run duration in seconds."""
|
|
103
|
+
if self.metadata.ended_at:
|
|
104
|
+
return (self.metadata.ended_at - self.metadata.started_at).total_seconds()
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def _save(self) -> None:
|
|
108
|
+
"""Save run metadata to disk."""
|
|
109
|
+
run_file = self.runs_dir / f"{self.run_id}.json"
|
|
110
|
+
|
|
111
|
+
with open(run_file, "w") as f:
|
|
112
|
+
json.dump(self.metadata.to_dict(), f, indent=2)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def load(cls, run_id: str, runs_dir: str = ".flowyml/runs") -> Optional["Run"]:
|
|
116
|
+
"""Load a run from disk."""
|
|
117
|
+
run_file = Path(runs_dir) / f"{run_id}.json"
|
|
118
|
+
|
|
119
|
+
if not run_file.exists():
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with open(run_file) as f:
|
|
124
|
+
data = json.load(f)
|
|
125
|
+
|
|
126
|
+
run = cls(
|
|
127
|
+
run_id=data["run_id"],
|
|
128
|
+
pipeline_name=data["pipeline_name"],
|
|
129
|
+
parameters=data.get("parameters", {}),
|
|
130
|
+
tags=data.get("tags", {}),
|
|
131
|
+
runs_dir=runs_dir,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
run.metadata.status = data["status"]
|
|
135
|
+
run.metadata.metrics = data.get("metrics", {})
|
|
136
|
+
if data.get("ended_at"):
|
|
137
|
+
run.metadata.ended_at = datetime.fromisoformat(data["ended_at"])
|
|
138
|
+
|
|
139
|
+
return run
|
|
140
|
+
|
|
141
|
+
except Exception:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def __repr__(self) -> str:
|
|
145
|
+
return f"Run(id='{self.run_id}', status='{self.metadata.status}')"
|
flowyml/ui/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""flowyml UI module - Real-time UI for pipeline monitoring."""
|
|
2
|
+
|
|
3
|
+
from flowyml.ui.utils import (
|
|
4
|
+
is_ui_running,
|
|
5
|
+
get_ui_url,
|
|
6
|
+
get_run_url,
|
|
7
|
+
get_pipeline_url,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"is_ui_running",
|
|
12
|
+
"get_ui_url",
|
|
13
|
+
"get_run_url",
|
|
14
|
+
"get_pipeline_url",
|
|
15
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
FROM python:3.10-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install system dependencies
|
|
6
|
+
RUN apt-get update && apt-get install -y \
|
|
7
|
+
build-essential \
|
|
8
|
+
curl \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
|
|
11
|
+
# Install poetry
|
|
12
|
+
RUN curl -sSL https://install.python-poetry.org | python3 -
|
|
13
|
+
ENV PATH="/root/.local/bin:$PATH"
|
|
14
|
+
|
|
15
|
+
# Copy project files
|
|
16
|
+
COPY pyproject.toml poetry.lock ./
|
|
17
|
+
COPY README.md ./
|
|
18
|
+
COPY flowyml ./flowyml
|
|
19
|
+
|
|
20
|
+
# Install dependencies
|
|
21
|
+
RUN poetry config virtualenvs.create false \
|
|
22
|
+
&& poetry install --no-interaction --no-ansi --no-root
|
|
23
|
+
|
|
24
|
+
# Install the package itself
|
|
25
|
+
RUN pip install .
|
|
26
|
+
|
|
27
|
+
# Expose port
|
|
28
|
+
EXPOSE 8000
|
|
29
|
+
|
|
30
|
+
# Run the application
|
|
31
|
+
CMD ["uvicorn", "flowyml.ui.backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
File without changes
|