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.
- flowyml/__init__.py +2 -1
- flowyml/assets/featureset.py +30 -5
- flowyml/assets/metrics.py +47 -4
- flowyml/cli/main.py +21 -0
- flowyml/cli/models.py +444 -0
- flowyml/cli/rich_utils.py +95 -0
- flowyml/core/checkpoint.py +6 -1
- flowyml/core/conditional.py +104 -0
- flowyml/core/display.py +525 -0
- flowyml/core/execution_status.py +1 -0
- flowyml/core/executor.py +201 -8
- flowyml/core/orchestrator.py +500 -7
- flowyml/core/pipeline.py +301 -11
- flowyml/core/project.py +4 -1
- flowyml/core/scheduler.py +225 -81
- flowyml/core/versioning.py +13 -4
- flowyml/registry/model_registry.py +1 -1
- flowyml/storage/sql.py +53 -13
- flowyml/ui/backend/main.py +2 -0
- flowyml/ui/backend/routers/assets.py +36 -0
- flowyml/ui/backend/routers/execution.py +2 -2
- flowyml/ui/backend/routers/runs.py +211 -0
- flowyml/ui/backend/routers/stats.py +2 -2
- flowyml/ui/backend/routers/websocket.py +121 -0
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +1 -0
- flowyml/ui/frontend/dist/assets/index-CX5RV2C9.js +630 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/package-lock.json +289 -0
- flowyml/ui/frontend/package.json +1 -0
- flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
- flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
- flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
- flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +69 -28
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/server_manager.py +181 -0
- flowyml/ui/utils.py +63 -1
- flowyml/utils/config.py +7 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/METADATA +5 -3
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/RECORD +49 -41
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/WHEEL +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.4.0.dist-info → flowyml-1.6.0.dist-info}/licenses/LICENSE +0 -0
flowyml/core/pipeline.py
CHANGED
|
@@ -134,17 +134,53 @@ class Pipeline:
|
|
|
134
134
|
>>> pipeline = Pipeline("my_pipeline", context=ctx)
|
|
135
135
|
>>> pipeline.add_step(train)
|
|
136
136
|
>>> result = pipeline.run()
|
|
137
|
+
|
|
138
|
+
# With project_name, automatically creates/attaches to project
|
|
139
|
+
>>> pipeline = Pipeline("my_pipeline", context=ctx, project_name="ml_project")
|
|
140
|
+
|
|
141
|
+
# With version parameter, automatically creates VersionedPipeline
|
|
142
|
+
>>> pipeline = Pipeline("my_pipeline", context=ctx, version="v1.0.1", project_name="ml_project")
|
|
137
143
|
"""
|
|
138
144
|
|
|
145
|
+
def __new__(
|
|
146
|
+
cls,
|
|
147
|
+
name: str,
|
|
148
|
+
version: str | None = None,
|
|
149
|
+
project_name: str | None = None,
|
|
150
|
+
project: str | None = None, # For backward compatibility
|
|
151
|
+
**kwargs,
|
|
152
|
+
):
|
|
153
|
+
"""Create a Pipeline or VersionedPipeline instance.
|
|
154
|
+
|
|
155
|
+
If version is provided, automatically returns a VersionedPipeline instance.
|
|
156
|
+
Otherwise, returns a regular Pipeline instance.
|
|
157
|
+
"""
|
|
158
|
+
if version is not None:
|
|
159
|
+
from flowyml.core.versioning import VersionedPipeline
|
|
160
|
+
|
|
161
|
+
# Pass project_name or project to VersionedPipeline
|
|
162
|
+
vp_kwargs = kwargs.copy()
|
|
163
|
+
if project_name:
|
|
164
|
+
vp_kwargs["project_name"] = project_name
|
|
165
|
+
elif project:
|
|
166
|
+
vp_kwargs["project"] = project
|
|
167
|
+
return VersionedPipeline(name=name, version=version, **vp_kwargs)
|
|
168
|
+
return super().__new__(cls)
|
|
169
|
+
|
|
139
170
|
def __init__(
|
|
140
171
|
self,
|
|
141
172
|
name: str,
|
|
142
173
|
context: Context | None = None,
|
|
143
174
|
executor: Executor | None = None,
|
|
144
175
|
enable_cache: bool = True,
|
|
176
|
+
enable_checkpointing: bool | None = None, # None means use config default
|
|
177
|
+
enable_experiment_tracking: bool | None = None, # None means use config default (True)
|
|
145
178
|
cache_dir: str | None = None,
|
|
146
179
|
stack: Any | None = None, # Stack instance
|
|
147
|
-
project: str | None = None, # Project name to attach to
|
|
180
|
+
project: str | None = None, # Project name to attach to (deprecated, use project_name)
|
|
181
|
+
project_name: str | None = None, # Project name to attach to (creates if doesn't exist)
|
|
182
|
+
version: str | None = None, # If provided, VersionedPipeline is created via __new__
|
|
183
|
+
**kwargs,
|
|
148
184
|
):
|
|
149
185
|
"""Initialize pipeline.
|
|
150
186
|
|
|
@@ -153,13 +189,35 @@ class Pipeline:
|
|
|
153
189
|
context: Optional context for parameter injection
|
|
154
190
|
executor: Optional executor (defaults to LocalExecutor)
|
|
155
191
|
enable_cache: Whether to enable caching
|
|
192
|
+
enable_checkpointing: Whether to enable checkpointing (defaults to config setting, True by default)
|
|
193
|
+
enable_experiment_tracking: Whether to enable automatic experiment tracking (defaults to config.auto_log_metrics, True by default)
|
|
156
194
|
cache_dir: Optional directory for cache
|
|
157
195
|
stack: Optional stack instance to run on
|
|
158
|
-
project: Optional project name to attach this pipeline to
|
|
196
|
+
project: Optional project name to attach this pipeline to (deprecated, use project_name)
|
|
197
|
+
project_name: Optional project name to attach this pipeline to.
|
|
198
|
+
If the project doesn't exist, it will be created automatically.
|
|
199
|
+
version: Optional version string. If provided, a VersionedPipeline
|
|
200
|
+
instance will be created instead of a regular Pipeline.
|
|
201
|
+
**kwargs: Additional keyword arguments passed to the pipeline.
|
|
202
|
+
instance is automatically created instead of a regular Pipeline.
|
|
159
203
|
"""
|
|
204
|
+
from flowyml.utils.config import get_config
|
|
205
|
+
|
|
160
206
|
self.name = name
|
|
161
207
|
self.context = context or Context()
|
|
162
208
|
self.enable_cache = enable_cache
|
|
209
|
+
|
|
210
|
+
# Set checkpointing (use config default if not specified)
|
|
211
|
+
config = get_config()
|
|
212
|
+
self.enable_checkpointing = (
|
|
213
|
+
enable_checkpointing if enable_checkpointing is not None else config.enable_checkpointing
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Set experiment tracking (use config default if not specified, default: True)
|
|
217
|
+
# Can be set via enable_experiment_tracking parameter or defaults to config.auto_log_metrics
|
|
218
|
+
self.enable_experiment_tracking = (
|
|
219
|
+
enable_experiment_tracking if enable_experiment_tracking is not None else config.auto_log_metrics
|
|
220
|
+
)
|
|
163
221
|
self.stack = None # Will be assigned via _apply_stack
|
|
164
222
|
self._stack_locked = stack is not None
|
|
165
223
|
self._provided_executor = executor
|
|
@@ -182,23 +240,28 @@ class Pipeline:
|
|
|
182
240
|
|
|
183
241
|
# Initialize components from stack or defaults
|
|
184
242
|
self.executor = executor or LocalExecutor()
|
|
185
|
-
# Metadata store for UI integration
|
|
243
|
+
# Metadata store for UI integration - use same store as UI
|
|
186
244
|
from flowyml.storage.metadata import SQLiteMetadataStore
|
|
245
|
+
from flowyml.utils.config import get_config
|
|
187
246
|
|
|
188
|
-
|
|
247
|
+
config = get_config()
|
|
248
|
+
# Use the same metadata database path as the UI to ensure visibility
|
|
249
|
+
self.metadata_store = SQLiteMetadataStore(db_path=str(config.metadata_db))
|
|
189
250
|
|
|
190
251
|
if stack:
|
|
191
252
|
self._apply_stack(stack, locked=True)
|
|
192
253
|
|
|
193
254
|
# Handle Project Attachment
|
|
194
|
-
|
|
255
|
+
# Support both project_name (preferred) and project (for backward compatibility)
|
|
256
|
+
project_to_use = project_name or project
|
|
257
|
+
if project_to_use:
|
|
195
258
|
from flowyml.core.project import ProjectManager
|
|
196
259
|
|
|
197
260
|
manager = ProjectManager()
|
|
198
261
|
# Get or create project
|
|
199
|
-
proj = manager.get_project(
|
|
262
|
+
proj = manager.get_project(project_to_use)
|
|
200
263
|
if not proj:
|
|
201
|
-
proj = manager.create_project(
|
|
264
|
+
proj = manager.create_project(project_to_use)
|
|
202
265
|
|
|
203
266
|
# Configure pipeline with project settings
|
|
204
267
|
self.runs_dir = proj.runs_dir
|
|
@@ -209,9 +272,15 @@ class Pipeline:
|
|
|
209
272
|
proj.metadata["pipelines"].append(name)
|
|
210
273
|
proj._save_metadata()
|
|
211
274
|
|
|
275
|
+
# Store project name for later use (e.g., in _save_run)
|
|
276
|
+
self.project_name = project_to_use
|
|
277
|
+
else:
|
|
278
|
+
self.project_name = None
|
|
279
|
+
|
|
212
280
|
# State
|
|
213
281
|
self._built = False
|
|
214
282
|
self.step_groups: list[Any] = [] # Will hold StepGroup objects
|
|
283
|
+
self.control_flows: list[Any] = [] # Store conditional control flows (If, Switch, etc.)
|
|
215
284
|
|
|
216
285
|
def _apply_stack(self, stack: Any | None, locked: bool) -> None:
|
|
217
286
|
"""Attach a stack and update executors/metadata."""
|
|
@@ -238,6 +307,32 @@ class Pipeline:
|
|
|
238
307
|
self._built = False
|
|
239
308
|
return self
|
|
240
309
|
|
|
310
|
+
def add_control_flow(self, control_flow: Any) -> "Pipeline":
|
|
311
|
+
"""Add conditional control flow to the pipeline.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
control_flow: Control flow object (If, Switch, etc.)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Self for chaining
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
```python
|
|
321
|
+
from flowyml import If
|
|
322
|
+
|
|
323
|
+
pipeline.add_control_flow(
|
|
324
|
+
If(
|
|
325
|
+
condition=lambda ctx: ctx.steps["evaluate_model"].outputs["accuracy"] > 0.9,
|
|
326
|
+
then_step=deploy_model,
|
|
327
|
+
else_step=retrain_model,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
"""
|
|
332
|
+
self.control_flows.append(control_flow)
|
|
333
|
+
self._built = False
|
|
334
|
+
return self
|
|
335
|
+
|
|
241
336
|
def build(self) -> None:
|
|
242
337
|
"""Build the execution DAG."""
|
|
243
338
|
if self._built:
|
|
@@ -277,9 +372,11 @@ class Pipeline:
|
|
|
277
372
|
inputs: dict[str, Any] | None = None,
|
|
278
373
|
debug: bool = False,
|
|
279
374
|
stack: Any | None = None, # Stack override
|
|
375
|
+
orchestrator: Any | None = None, # Orchestrator override (takes precedence over stack orchestrator)
|
|
280
376
|
resources: Any | None = None, # ResourceConfig
|
|
281
377
|
docker_config: Any | None = None, # DockerConfig
|
|
282
378
|
context: dict[str, Any] | None = None, # Context vars override
|
|
379
|
+
auto_start_ui: bool = True, # Auto-start UI server
|
|
283
380
|
**kwargs,
|
|
284
381
|
) -> PipelineResult:
|
|
285
382
|
"""Execute the pipeline.
|
|
@@ -287,19 +384,89 @@ class Pipeline:
|
|
|
287
384
|
Args:
|
|
288
385
|
inputs: Optional input data for the pipeline
|
|
289
386
|
debug: Enable debug mode with detailed logging
|
|
290
|
-
stack: Stack override (uses self.stack if not provided)
|
|
387
|
+
stack: Stack override (uses self.stack or active stack if not provided)
|
|
388
|
+
orchestrator: Orchestrator override (takes precedence over stack orchestrator)
|
|
291
389
|
resources: Resource configuration for execution
|
|
292
390
|
docker_config: Docker configuration for containerized execution
|
|
293
391
|
context: Context variables override
|
|
392
|
+
auto_start_ui: Automatically start UI server if not running and display URL
|
|
294
393
|
**kwargs: Additional arguments passed to the orchestrator
|
|
295
394
|
|
|
395
|
+
Note:
|
|
396
|
+
The orchestrator is determined in this priority order:
|
|
397
|
+
1. Explicit `orchestrator` parameter (if provided)
|
|
398
|
+
2. Stack's orchestrator (if stack is set/active)
|
|
399
|
+
3. Default LocalOrchestrator
|
|
400
|
+
|
|
401
|
+
When using a stack (e.g., GCPStack), the stack's orchestrator is automatically
|
|
402
|
+
used unless explicitly overridden. This is the recommended approach for
|
|
403
|
+
production deployments.
|
|
404
|
+
|
|
296
405
|
Returns:
|
|
297
406
|
PipelineResult with outputs and execution info
|
|
298
407
|
"""
|
|
299
408
|
import uuid
|
|
300
409
|
from flowyml.core.orchestrator import LocalOrchestrator
|
|
410
|
+
from flowyml.core.checkpoint import PipelineCheckpoint
|
|
411
|
+
from flowyml.utils.config import get_config
|
|
412
|
+
|
|
413
|
+
# Generate or use provided run_id
|
|
414
|
+
run_id = kwargs.pop("run_id", None) or str(uuid.uuid4())
|
|
415
|
+
|
|
416
|
+
# Initialize checkpointing if enabled
|
|
417
|
+
if self.enable_checkpointing:
|
|
418
|
+
config = get_config()
|
|
419
|
+
checkpoint = PipelineCheckpoint(
|
|
420
|
+
run_id=run_id,
|
|
421
|
+
checkpoint_dir=str(config.checkpoint_dir),
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Check if we should resume from checkpoint
|
|
425
|
+
if checkpoint.exists():
|
|
426
|
+
checkpoint_data = checkpoint.load()
|
|
427
|
+
completed_steps = checkpoint_data.get("completed_steps", [])
|
|
428
|
+
if completed_steps:
|
|
429
|
+
# Auto-resume: use checkpoint state
|
|
430
|
+
if hasattr(self, "_display") and self._display:
|
|
431
|
+
self._display.console.print(
|
|
432
|
+
f"[yellow]📦 Resuming from checkpoint: {len(completed_steps)} steps already completed[/yellow]",
|
|
433
|
+
)
|
|
434
|
+
# Store checkpoint info for orchestrator
|
|
435
|
+
self._checkpoint = checkpoint
|
|
436
|
+
self._resume_from_checkpoint = True
|
|
437
|
+
self._completed_steps_from_checkpoint = set(completed_steps)
|
|
438
|
+
else:
|
|
439
|
+
self._checkpoint = checkpoint
|
|
440
|
+
self._resume_from_checkpoint = False
|
|
441
|
+
self._completed_steps_from_checkpoint = set()
|
|
442
|
+
else:
|
|
443
|
+
self._checkpoint = checkpoint
|
|
444
|
+
self._resume_from_checkpoint = False
|
|
445
|
+
self._completed_steps_from_checkpoint = set()
|
|
446
|
+
else:
|
|
447
|
+
self._checkpoint = None
|
|
448
|
+
self._resume_from_checkpoint = False
|
|
449
|
+
self._completed_steps_from_checkpoint = set()
|
|
450
|
+
|
|
451
|
+
# Auto-start UI server if requested
|
|
452
|
+
ui_url = None
|
|
453
|
+
run_url = None
|
|
454
|
+
if auto_start_ui:
|
|
455
|
+
try:
|
|
456
|
+
from flowyml.ui.server_manager import UIServerManager
|
|
457
|
+
from flowyml.ui.utils import get_ui_host_port
|
|
458
|
+
|
|
459
|
+
ui_manager = UIServerManager.get_instance()
|
|
460
|
+
# Use config values for host/port
|
|
461
|
+
host, port = get_ui_host_port()
|
|
462
|
+
if ui_manager.ensure_running(host=host, port=port, auto_start=True):
|
|
463
|
+
ui_url = ui_manager.get_url()
|
|
464
|
+
run_url = ui_manager.get_run_url(run_id)
|
|
301
465
|
|
|
302
|
-
|
|
466
|
+
# UI URL will be shown in summary, no need to print here
|
|
467
|
+
except Exception:
|
|
468
|
+
# Silently fail if UI is not available
|
|
469
|
+
pass
|
|
303
470
|
|
|
304
471
|
# Determine stack for this run
|
|
305
472
|
if stack is not None:
|
|
@@ -316,9 +483,12 @@ class Pipeline:
|
|
|
316
483
|
self._apply_stack(active_stack, locked=False)
|
|
317
484
|
|
|
318
485
|
# Determine orchestrator
|
|
319
|
-
|
|
486
|
+
# Priority: 1) Explicit orchestrator parameter, 2) Stack orchestrator, 3) Default LocalOrchestrator
|
|
320
487
|
if orchestrator is None:
|
|
321
|
-
orchestrator
|
|
488
|
+
# Use orchestrator from stack if available
|
|
489
|
+
orchestrator = getattr(self.stack, "orchestrator", None) if self.stack else None
|
|
490
|
+
if orchestrator is None:
|
|
491
|
+
orchestrator = LocalOrchestrator()
|
|
322
492
|
|
|
323
493
|
# Update context with provided values
|
|
324
494
|
if context:
|
|
@@ -331,6 +501,26 @@ class Pipeline:
|
|
|
331
501
|
resource_config = self._coerce_resource_config(resources)
|
|
332
502
|
docker_cfg = self._coerce_docker_config(docker_config)
|
|
333
503
|
|
|
504
|
+
# Initialize display system for beautiful CLI output
|
|
505
|
+
display = None
|
|
506
|
+
try:
|
|
507
|
+
from flowyml.core.display import PipelineDisplay
|
|
508
|
+
|
|
509
|
+
display = PipelineDisplay(
|
|
510
|
+
pipeline_name=self.name,
|
|
511
|
+
steps=self.steps,
|
|
512
|
+
dag=self.dag,
|
|
513
|
+
verbose=True,
|
|
514
|
+
)
|
|
515
|
+
display.show_header()
|
|
516
|
+
display.show_execution_start()
|
|
517
|
+
except Exception:
|
|
518
|
+
# Silently fail if display system not available
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
# Store display on pipeline for orchestrator to use
|
|
522
|
+
self._display = display
|
|
523
|
+
|
|
334
524
|
# Run the pipeline via orchestrator
|
|
335
525
|
result = orchestrator.run_pipeline(
|
|
336
526
|
self,
|
|
@@ -342,6 +532,10 @@ class Pipeline:
|
|
|
342
532
|
**kwargs,
|
|
343
533
|
)
|
|
344
534
|
|
|
535
|
+
# Show summary (only if result is a PipelineResult, not a string)
|
|
536
|
+
if display and not isinstance(result, str):
|
|
537
|
+
display.show_summary(result, ui_url=ui_url, run_url=run_url)
|
|
538
|
+
|
|
345
539
|
# If result is just a job ID (remote execution), wrap it in a basic result
|
|
346
540
|
if isinstance(result, str):
|
|
347
541
|
# Create a submitted result wrapper
|
|
@@ -352,6 +546,10 @@ class Pipeline:
|
|
|
352
546
|
self._save_pipeline_definition()
|
|
353
547
|
return wrapper
|
|
354
548
|
|
|
549
|
+
# Ensure result has configs attached (in case orchestrator didn't do it)
|
|
550
|
+
if hasattr(result, "attach_configs") and not hasattr(result, "resource_config"):
|
|
551
|
+
result.attach_configs(resource_config, docker_cfg)
|
|
552
|
+
|
|
355
553
|
return result
|
|
356
554
|
|
|
357
555
|
def to_definition(self) -> dict:
|
|
@@ -368,6 +566,7 @@ class Pipeline:
|
|
|
368
566
|
"outputs": step.outputs,
|
|
369
567
|
"source_code": step.source_code,
|
|
370
568
|
"tags": step.tags,
|
|
569
|
+
"execution_group": step.execution_group,
|
|
371
570
|
}
|
|
372
571
|
for step in self.steps
|
|
373
572
|
],
|
|
@@ -425,6 +624,92 @@ class Pipeline:
|
|
|
425
624
|
return DockerConfig(**docker_config)
|
|
426
625
|
return docker_config
|
|
427
626
|
|
|
627
|
+
def _log_experiment_metrics(self, result: PipelineResult) -> None:
|
|
628
|
+
"""Automatically log Metrics to experiment tracking.
|
|
629
|
+
|
|
630
|
+
Extracts Metrics objects from pipeline outputs and logs them along with
|
|
631
|
+
context parameters to the experiment tracking system.
|
|
632
|
+
|
|
633
|
+
This is called automatically after each pipeline run if experiment tracking is enabled.
|
|
634
|
+
"""
|
|
635
|
+
from flowyml.utils.config import get_config
|
|
636
|
+
from flowyml.assets.metrics import Metrics
|
|
637
|
+
|
|
638
|
+
config = get_config()
|
|
639
|
+
|
|
640
|
+
# Check if experiment tracking is enabled (default: True)
|
|
641
|
+
# Can be disabled globally via config or per-pipeline via enable_experiment_tracking
|
|
642
|
+
enable_tracking = getattr(self, "enable_experiment_tracking", None)
|
|
643
|
+
if enable_tracking is None:
|
|
644
|
+
enable_tracking = getattr(config, "auto_log_metrics", True)
|
|
645
|
+
|
|
646
|
+
if not enable_tracking:
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
# Extract all Metrics from pipeline outputs
|
|
650
|
+
all_metrics = {}
|
|
651
|
+
for output_name, output_value in result.outputs.items():
|
|
652
|
+
if isinstance(output_value, Metrics):
|
|
653
|
+
# Extract metrics from Metrics object
|
|
654
|
+
metrics_dict = output_value.get_all_metrics() or output_value.data or {}
|
|
655
|
+
# Use output name as prefix to avoid conflicts, but simplify if output is "metrics"
|
|
656
|
+
for key, value in metrics_dict.items():
|
|
657
|
+
if output_name == "metrics" or output_name.endswith("/metrics"):
|
|
658
|
+
# Use metric key directly for cleaner names
|
|
659
|
+
all_metrics[key] = value
|
|
660
|
+
else:
|
|
661
|
+
# Prefix with output name to avoid conflicts
|
|
662
|
+
all_metrics[f"{output_name}.{key}"] = value
|
|
663
|
+
elif isinstance(output_value, dict):
|
|
664
|
+
# Check if dict contains Metrics objects
|
|
665
|
+
for key, val in output_value.items():
|
|
666
|
+
if isinstance(val, Metrics):
|
|
667
|
+
metrics_dict = val.get_all_metrics() or val.data or {}
|
|
668
|
+
for mkey, mval in metrics_dict.items():
|
|
669
|
+
all_metrics[f"{key}.{mkey}"] = mval
|
|
670
|
+
|
|
671
|
+
# Extract context parameters
|
|
672
|
+
context_params = {}
|
|
673
|
+
if self.context:
|
|
674
|
+
# Get all context parameters using to_dict() method
|
|
675
|
+
context_params = self.context.to_dict()
|
|
676
|
+
|
|
677
|
+
# Only log if we have metrics or parameters
|
|
678
|
+
if all_metrics or context_params:
|
|
679
|
+
try:
|
|
680
|
+
from flowyml.tracking.experiment import Experiment
|
|
681
|
+
from flowyml.tracking.runs import Run
|
|
682
|
+
|
|
683
|
+
# Create or get experiment (use pipeline name as experiment name)
|
|
684
|
+
experiment_name = self.name
|
|
685
|
+
experiment = Experiment(
|
|
686
|
+
name=experiment_name,
|
|
687
|
+
description=f"Auto-tracked experiment for pipeline: {self.name}",
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
# Log run to experiment
|
|
691
|
+
experiment.log_run(
|
|
692
|
+
run_id=result.run_id,
|
|
693
|
+
metrics=all_metrics,
|
|
694
|
+
parameters=context_params,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Also create/update Run object for compatibility
|
|
698
|
+
run = Run(
|
|
699
|
+
run_id=result.run_id,
|
|
700
|
+
pipeline_name=self.name,
|
|
701
|
+
parameters=context_params,
|
|
702
|
+
)
|
|
703
|
+
if all_metrics:
|
|
704
|
+
run.log_metrics(all_metrics)
|
|
705
|
+
run.complete(status="success" if result.success else "failed")
|
|
706
|
+
|
|
707
|
+
except Exception as e:
|
|
708
|
+
# Don't fail pipeline if experiment logging fails
|
|
709
|
+
import warnings
|
|
710
|
+
|
|
711
|
+
warnings.warn(f"Failed to log experiment metrics: {e}", stacklevel=2)
|
|
712
|
+
|
|
428
713
|
def _save_run(self, result: PipelineResult) -> None:
|
|
429
714
|
"""Save run results to disk and metadata database."""
|
|
430
715
|
# Save to JSON file
|
|
@@ -467,6 +752,7 @@ class Pipeline:
|
|
|
467
752
|
"inputs": step.inputs,
|
|
468
753
|
"outputs": step.outputs,
|
|
469
754
|
"tags": step.tags,
|
|
755
|
+
"execution_group": step.execution_group,
|
|
470
756
|
"resources": step.resources.to_dict() if hasattr(step.resources, "to_dict") else step.resources,
|
|
471
757
|
}
|
|
472
758
|
|
|
@@ -489,9 +775,13 @@ class Pipeline:
|
|
|
489
775
|
if hasattr(result.docker_config, "to_dict")
|
|
490
776
|
else result.docker_config,
|
|
491
777
|
"remote_job_id": result.remote_job_id,
|
|
778
|
+
"project": getattr(self, "project_name", None), # Include project for stats tracking
|
|
492
779
|
}
|
|
493
780
|
self.metadata_store.save_run(result.run_id, metadata)
|
|
494
781
|
|
|
782
|
+
# Automatic experiment tracking: Extract Metrics and log to experiments
|
|
783
|
+
self._log_experiment_metrics(result)
|
|
784
|
+
|
|
495
785
|
# Save artifacts and metrics
|
|
496
786
|
for step_name, step_result in result.step_results.items():
|
|
497
787
|
if step_result.success and step_result.output is not None:
|
flowyml/core/project.py
CHANGED
|
@@ -107,6 +107,9 @@ class Project:
|
|
|
107
107
|
# Use project metadata store
|
|
108
108
|
pipeline.metadata_store = self.metadata_store
|
|
109
109
|
|
|
110
|
+
# Set project name on pipeline for stats tracking
|
|
111
|
+
pipeline.project_name = self.name
|
|
112
|
+
|
|
110
113
|
# Register pipeline
|
|
111
114
|
if name not in self.metadata["pipelines"]:
|
|
112
115
|
self.metadata["pipelines"].append(name)
|
|
@@ -161,7 +164,7 @@ class Project:
|
|
|
161
164
|
|
|
162
165
|
def get_stats(self) -> dict[str, Any]:
|
|
163
166
|
"""Get project statistics."""
|
|
164
|
-
stats = self.metadata_store.get_statistics()
|
|
167
|
+
stats = self.metadata_store.get_statistics(project=self.name)
|
|
165
168
|
stats["project_name"] = self.name
|
|
166
169
|
stats["pipelines"] = len(self.metadata["pipelines"])
|
|
167
170
|
return stats
|