flowyml 1.7.2__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/metrics.py +5 -0
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +161 -26
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +22 -2
- flowyml/core/pipeline.py +34 -10
- flowyml/core/routing.py +558 -0
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- 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 +20 -1
- flowyml/ui/backend/routers/schedules.py +22 -17
- 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 +1404 -74
- flowyml/ui/frontend/package.json +3 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- 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/runs/[runId]/page.jsx +36 -24
- 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/AssetDetailsPanel.jsx +29 -7
- 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/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.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
- flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
- flowyml-1.7.2.dist-info/METADATA +0 -477
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
flowyml/core/routing.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""FlowyML Artifact Routing - Automatic Type-Based Artifact Routing.
|
|
2
|
+
|
|
3
|
+
This module provides automatic routing of step outputs to appropriate
|
|
4
|
+
infrastructure based on their Python types. When a step returns a
|
|
5
|
+
`Model`, `Dataset`, `Metrics`, or other artifact type, the runtime
|
|
6
|
+
automatically routes it to the configured stores and registries.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from flowyml.core.routing import route_artifact
|
|
10
|
+
|
|
11
|
+
# After step execution
|
|
12
|
+
result = step.func(**inputs)
|
|
13
|
+
|
|
14
|
+
# Route based on type and stack config
|
|
15
|
+
artifact_info = route_artifact(
|
|
16
|
+
output=result,
|
|
17
|
+
step_name="train_model",
|
|
18
|
+
run_id="run-123",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
The routing is configured via flowyml.yaml:
|
|
22
|
+
stacks:
|
|
23
|
+
gcp-prod:
|
|
24
|
+
artifact_routing:
|
|
25
|
+
Model: { store: gcs, register: true }
|
|
26
|
+
Dataset: { store: gcs }
|
|
27
|
+
Metrics: { log_to_tracker: true }
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
from typing import Any, get_type_hints
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class RoutingResult:
|
|
39
|
+
"""Result of artifact routing.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
artifact_type: Name of the artifact type (Model, Dataset, etc.)
|
|
43
|
+
store_uri: URI where the artifact was stored
|
|
44
|
+
registered: Whether the artifact was registered (e.g., in model registry)
|
|
45
|
+
deployed: Whether the artifact was deployed (e.g., to endpoint)
|
|
46
|
+
endpoint_uri: URI of the deployment endpoint
|
|
47
|
+
logged: Whether the artifact was logged (e.g., metrics to tracker)
|
|
48
|
+
metadata: Additional metadata from routing
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
artifact_type: str | None = None
|
|
52
|
+
store_uri: str | None = None
|
|
53
|
+
registered: bool = False
|
|
54
|
+
deployed: bool = False
|
|
55
|
+
endpoint_uri: str | None = None
|
|
56
|
+
logged: bool = False
|
|
57
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_step_return_type(step_func: callable) -> type | None:
|
|
61
|
+
"""Get the return type annotation from a step function.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
step_func: The step function to inspect.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The return type annotation, or None if not annotated.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
hints = get_type_hints(step_func)
|
|
71
|
+
return hints.get("return")
|
|
72
|
+
except Exception:
|
|
73
|
+
# Fallback to __annotations__ if get_type_hints fails
|
|
74
|
+
try:
|
|
75
|
+
return step_func.__annotations__.get("return")
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def detect_artifact_type(output: Any) -> str | None:
|
|
81
|
+
"""Detect the artifact type from an output value.
|
|
82
|
+
|
|
83
|
+
This checks if the output is an instance of one of our artifact types
|
|
84
|
+
or if it matches specific patterns (like dict for Metrics).
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
output: The step output value.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Type name string or None.
|
|
91
|
+
"""
|
|
92
|
+
# Import types here to avoid circular imports
|
|
93
|
+
from flowyml.core.types import Artifact, Model, Dataset, Metrics, Parameters
|
|
94
|
+
|
|
95
|
+
if isinstance(output, Model):
|
|
96
|
+
return "Model"
|
|
97
|
+
elif isinstance(output, Dataset):
|
|
98
|
+
return "Dataset"
|
|
99
|
+
elif isinstance(output, Metrics):
|
|
100
|
+
return "Metrics"
|
|
101
|
+
elif isinstance(output, Parameters):
|
|
102
|
+
return "Parameters"
|
|
103
|
+
elif isinstance(output, Artifact):
|
|
104
|
+
return type(output).__name__
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def route_artifact(
|
|
110
|
+
output: Any,
|
|
111
|
+
step_name: str,
|
|
112
|
+
run_id: str,
|
|
113
|
+
return_type: type | None = None,
|
|
114
|
+
project_name: str = "default",
|
|
115
|
+
) -> RoutingResult:
|
|
116
|
+
"""Route a step output to appropriate infrastructure based on type.
|
|
117
|
+
|
|
118
|
+
This is the main entry point for type-based artifact routing.
|
|
119
|
+
It inspects the output type and routes to configured stores/registries.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
output: The step output to route.
|
|
123
|
+
step_name: Name of the step that produced this output.
|
|
124
|
+
run_id: Current run identifier.
|
|
125
|
+
return_type: Optional return type annotation (if known).
|
|
126
|
+
project_name: Project name for namespacing.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
RoutingResult with routing information.
|
|
130
|
+
"""
|
|
131
|
+
result = RoutingResult()
|
|
132
|
+
|
|
133
|
+
# Skip None outputs
|
|
134
|
+
if output is None:
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
# Detect artifact type
|
|
138
|
+
artifact_type = detect_artifact_type(output)
|
|
139
|
+
|
|
140
|
+
# If not detected from value, try from type annotation
|
|
141
|
+
if artifact_type is None and return_type is not None:
|
|
142
|
+
try:
|
|
143
|
+
type_name = return_type.__name__ if hasattr(return_type, "__name__") else str(return_type)
|
|
144
|
+
if type_name in ("Model", "Dataset", "Metrics", "Parameters"):
|
|
145
|
+
artifact_type = type_name
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
if artifact_type is None:
|
|
150
|
+
# Not a routable artifact type
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
result.artifact_type = artifact_type
|
|
154
|
+
logger.debug(f"Routing {artifact_type} artifact from step '{step_name}'")
|
|
155
|
+
|
|
156
|
+
# Get routing configuration from active stack
|
|
157
|
+
try:
|
|
158
|
+
from flowyml.plugins.stack_config import get_routing_for_type, get_active_stack
|
|
159
|
+
|
|
160
|
+
routing_rule = get_routing_for_type(artifact_type)
|
|
161
|
+
stack = get_active_stack()
|
|
162
|
+
|
|
163
|
+
if routing_rule is None:
|
|
164
|
+
logger.debug(f"No routing rule for {artifact_type}, using defaults")
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
# Route to artifact store
|
|
168
|
+
if routing_rule.store:
|
|
169
|
+
result.store_uri = _save_to_store(
|
|
170
|
+
output=output,
|
|
171
|
+
artifact_type=artifact_type,
|
|
172
|
+
store_name=routing_rule.store,
|
|
173
|
+
path=routing_rule.format_path(
|
|
174
|
+
run_id=run_id,
|
|
175
|
+
step_name=step_name,
|
|
176
|
+
artifact_name=artifact_type.lower(),
|
|
177
|
+
),
|
|
178
|
+
stack=stack,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Register model if configured
|
|
182
|
+
if routing_rule.register and artifact_type == "Model":
|
|
183
|
+
result.registered = _register_model(
|
|
184
|
+
output=output,
|
|
185
|
+
step_name=step_name,
|
|
186
|
+
run_id=run_id,
|
|
187
|
+
stack=stack,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Deploy model if configured and conditions are met
|
|
191
|
+
# Note: deploy=True just enables deployment - actual deployment depends on deploy_condition
|
|
192
|
+
if routing_rule.deploy and artifact_type == "Model":
|
|
193
|
+
# Get metrics from model metadata for conditional deployment
|
|
194
|
+
model_metrics = None
|
|
195
|
+
if hasattr(output, "metadata") and output.metadata:
|
|
196
|
+
model_metrics = output.metadata.get("metrics", {})
|
|
197
|
+
|
|
198
|
+
# Check if auto-deployment should proceed
|
|
199
|
+
if routing_rule.should_auto_deploy(model_metrics):
|
|
200
|
+
endpoint_name = routing_rule.endpoint_name or f"{step_name}-endpoint"
|
|
201
|
+
result.deployed, result.endpoint_uri = _deploy_model(
|
|
202
|
+
output=output,
|
|
203
|
+
step_name=step_name,
|
|
204
|
+
run_id=run_id,
|
|
205
|
+
endpoint_name=endpoint_name,
|
|
206
|
+
stack=stack,
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
# Log that deployment is pending approval/manual action
|
|
210
|
+
condition = routing_rule.deploy_condition
|
|
211
|
+
if condition == "manual":
|
|
212
|
+
logger.info(
|
|
213
|
+
f"Model registered but not deployed (deploy_condition='manual'). "
|
|
214
|
+
f"Use 'flowyml model deploy {output.name}' to deploy.",
|
|
215
|
+
)
|
|
216
|
+
elif condition == "on_approval":
|
|
217
|
+
logger.info("Model registered, awaiting approval for deployment.")
|
|
218
|
+
elif condition == "auto" and routing_rule.deploy_min_metrics:
|
|
219
|
+
logger.info(
|
|
220
|
+
f"Model not deployed - metrics did not meet thresholds: " f"{routing_rule.deploy_min_metrics}",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Log metrics if configured
|
|
224
|
+
if routing_rule.log_to_tracker and artifact_type == "Metrics":
|
|
225
|
+
result.logged = _log_metrics(
|
|
226
|
+
output=output,
|
|
227
|
+
step_name=step_name,
|
|
228
|
+
run_id=run_id,
|
|
229
|
+
stack=stack,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Log parameters if configured
|
|
233
|
+
if routing_rule.log_to_tracker and artifact_type == "Parameters":
|
|
234
|
+
result.logged = _log_parameters(
|
|
235
|
+
output=output,
|
|
236
|
+
step_name=step_name,
|
|
237
|
+
run_id=run_id,
|
|
238
|
+
stack=stack,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Add routing metadata
|
|
242
|
+
result.metadata = {
|
|
243
|
+
"store": routing_rule.store,
|
|
244
|
+
"path": routing_rule.path,
|
|
245
|
+
"registered": result.registered,
|
|
246
|
+
"deployed": result.deployed,
|
|
247
|
+
"logged": result.logged,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
except ImportError:
|
|
251
|
+
logger.debug("Stack config not available, skipping routing")
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.warning(f"Error during artifact routing: {e}")
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _save_to_store(
|
|
259
|
+
output: Any,
|
|
260
|
+
artifact_type: str,
|
|
261
|
+
store_name: str,
|
|
262
|
+
path: str,
|
|
263
|
+
stack: Any,
|
|
264
|
+
) -> str | None:
|
|
265
|
+
"""Save artifact to the configured store.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
output: The artifact to save.
|
|
269
|
+
artifact_type: Type of the artifact.
|
|
270
|
+
store_name: Name of the store (gcs, s3, local).
|
|
271
|
+
path: Path within the store.
|
|
272
|
+
stack: Stack configuration.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
URI of the saved artifact or None.
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
# Get artifact store from stack
|
|
279
|
+
if store_name and stack and stack.artifact_stores:
|
|
280
|
+
store_config = stack.artifact_stores.get(store_name)
|
|
281
|
+
if store_config:
|
|
282
|
+
# Instantiate and use the store
|
|
283
|
+
from flowyml.plugins.config import get_artifact_store
|
|
284
|
+
|
|
285
|
+
store = get_artifact_store()
|
|
286
|
+
if store:
|
|
287
|
+
# Extract data if it's an Artifact wrapper
|
|
288
|
+
from flowyml.core.types import Artifact
|
|
289
|
+
|
|
290
|
+
data = output.data if isinstance(output, Artifact) else output
|
|
291
|
+
return store.save(data, path)
|
|
292
|
+
|
|
293
|
+
# Fallback to default artifact store
|
|
294
|
+
from flowyml.plugins.config import get_artifact_store
|
|
295
|
+
|
|
296
|
+
store = get_artifact_store()
|
|
297
|
+
if store:
|
|
298
|
+
from flowyml.core.types import Artifact
|
|
299
|
+
|
|
300
|
+
data = output.data if isinstance(output, Artifact) else output
|
|
301
|
+
return store.save(data, path)
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning(f"Failed to save artifact to store: {e}")
|
|
305
|
+
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _register_model(
|
|
310
|
+
output: Any,
|
|
311
|
+
step_name: str,
|
|
312
|
+
run_id: str,
|
|
313
|
+
stack: Any,
|
|
314
|
+
) -> bool:
|
|
315
|
+
"""Register a model in the model registry.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
output: The Model artifact.
|
|
319
|
+
step_name: Step that produced the model.
|
|
320
|
+
run_id: Current run ID.
|
|
321
|
+
stack: Stack configuration.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
True if registration was successful.
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
from flowyml.core.types import Model
|
|
328
|
+
|
|
329
|
+
if not isinstance(output, Model):
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
# Get model registry from plugins
|
|
333
|
+
from flowyml.plugins.config import get_config
|
|
334
|
+
|
|
335
|
+
config = get_config()
|
|
336
|
+
registry = config._get_plugin("model_registry")
|
|
337
|
+
|
|
338
|
+
if registry:
|
|
339
|
+
model_name = output.name or f"{step_name}_model"
|
|
340
|
+
model_uri = output.uri or f"runs/{run_id}/models/{step_name}"
|
|
341
|
+
|
|
342
|
+
registry.register_model(
|
|
343
|
+
name=model_name,
|
|
344
|
+
model_uri=model_uri,
|
|
345
|
+
version=output.version,
|
|
346
|
+
metadata={
|
|
347
|
+
"framework": output.framework,
|
|
348
|
+
"step_name": step_name,
|
|
349
|
+
"run_id": run_id,
|
|
350
|
+
**output.metadata,
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
logger.info(f"Registered model '{model_name}' to registry")
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
logger.warning(f"Failed to register model: {e}")
|
|
358
|
+
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _deploy_model(
|
|
363
|
+
output: Any,
|
|
364
|
+
step_name: str,
|
|
365
|
+
run_id: str,
|
|
366
|
+
endpoint_name: str,
|
|
367
|
+
stack: Any,
|
|
368
|
+
) -> tuple[bool, str | None]:
|
|
369
|
+
"""Deploy a model to an endpoint.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
output: The Model artifact.
|
|
373
|
+
step_name: Step that produced the model.
|
|
374
|
+
run_id: Current run ID.
|
|
375
|
+
endpoint_name: Name for the endpoint.
|
|
376
|
+
stack: Stack configuration.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Tuple of (success, endpoint_uri).
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
from flowyml.core.types import Model
|
|
383
|
+
|
|
384
|
+
if not isinstance(output, Model):
|
|
385
|
+
return False, None
|
|
386
|
+
|
|
387
|
+
# Get model deployer from stack config
|
|
388
|
+
if stack and stack.model_deployer:
|
|
389
|
+
deployer_config = stack.model_deployer
|
|
390
|
+
deployer_type = deployer_config.get("type", "")
|
|
391
|
+
|
|
392
|
+
deployer = None
|
|
393
|
+
|
|
394
|
+
# Instantiate the appropriate deployer
|
|
395
|
+
if "vertex" in deployer_type:
|
|
396
|
+
from flowyml.plugins.deployers.vertex import VertexEndpointDeployer
|
|
397
|
+
|
|
398
|
+
deployer = VertexEndpointDeployer(
|
|
399
|
+
project=deployer_config.get("project"),
|
|
400
|
+
location=deployer_config.get("location", "us-central1"),
|
|
401
|
+
)
|
|
402
|
+
elif "sagemaker" in deployer_type:
|
|
403
|
+
from flowyml.plugins.deployers.sagemaker import SageMakerEndpointDeployer
|
|
404
|
+
|
|
405
|
+
deployer = SageMakerEndpointDeployer(
|
|
406
|
+
region=deployer_config.get("region"),
|
|
407
|
+
role_arn=deployer_config.get("role_arn"),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if deployer:
|
|
411
|
+
deployer.initialize()
|
|
412
|
+
|
|
413
|
+
# Get model URI (from artifact store or output)
|
|
414
|
+
model_uri = output.uri or f"runs/{run_id}/models/{step_name}"
|
|
415
|
+
|
|
416
|
+
endpoint_uri = deployer.deploy(
|
|
417
|
+
model_uri=model_uri,
|
|
418
|
+
endpoint_name=endpoint_name,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
logger.info(f"Deployed model to endpoint: {endpoint_uri}")
|
|
422
|
+
return True, endpoint_uri
|
|
423
|
+
|
|
424
|
+
# No deployer configured
|
|
425
|
+
logger.debug("No model deployer configured in stack")
|
|
426
|
+
return False, None
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.warning(f"Failed to deploy model: {e}")
|
|
430
|
+
|
|
431
|
+
return False, None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _log_metrics(
|
|
435
|
+
output: Any,
|
|
436
|
+
step_name: str,
|
|
437
|
+
run_id: str,
|
|
438
|
+
stack: Any,
|
|
439
|
+
) -> bool:
|
|
440
|
+
"""Log metrics to the experiment tracker.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
output: The Metrics artifact (dict-like).
|
|
444
|
+
step_name: Step that produced the metrics.
|
|
445
|
+
run_id: Current run ID.
|
|
446
|
+
stack: Stack configuration.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
True if logging was successful.
|
|
450
|
+
"""
|
|
451
|
+
try:
|
|
452
|
+
from flowyml.core.types import Metrics
|
|
453
|
+
from flowyml.plugins.config import get_tracker
|
|
454
|
+
|
|
455
|
+
tracker = get_tracker()
|
|
456
|
+
if tracker:
|
|
457
|
+
# Get metrics values
|
|
458
|
+
if isinstance(output, Metrics):
|
|
459
|
+
metrics_dict = dict(output)
|
|
460
|
+
step_num = output._step
|
|
461
|
+
else:
|
|
462
|
+
metrics_dict = dict(output)
|
|
463
|
+
step_num = None
|
|
464
|
+
|
|
465
|
+
tracker.log_metrics(metrics_dict, step=step_num)
|
|
466
|
+
logger.debug(f"Logged metrics from step '{step_name}': {list(metrics_dict.keys())}")
|
|
467
|
+
return True
|
|
468
|
+
|
|
469
|
+
except Exception as e:
|
|
470
|
+
logger.warning(f"Failed to log metrics: {e}")
|
|
471
|
+
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _log_parameters(
|
|
476
|
+
output: Any,
|
|
477
|
+
step_name: str,
|
|
478
|
+
run_id: str,
|
|
479
|
+
stack: Any,
|
|
480
|
+
) -> bool:
|
|
481
|
+
"""Log parameters to the experiment tracker.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
output: The Parameters artifact (dict-like).
|
|
485
|
+
step_name: Step that uses the parameters.
|
|
486
|
+
run_id: Current run ID.
|
|
487
|
+
stack: Stack configuration.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
True if logging was successful.
|
|
491
|
+
"""
|
|
492
|
+
try:
|
|
493
|
+
from flowyml.core.types import Parameters
|
|
494
|
+
from flowyml.plugins.config import get_tracker
|
|
495
|
+
|
|
496
|
+
tracker = get_tracker()
|
|
497
|
+
if tracker:
|
|
498
|
+
# Get parameter values
|
|
499
|
+
if isinstance(output, Parameters):
|
|
500
|
+
params_dict = dict(output)
|
|
501
|
+
else:
|
|
502
|
+
params_dict = dict(output)
|
|
503
|
+
|
|
504
|
+
# Log parameters (with step prefix for clarity)
|
|
505
|
+
prefixed_params = {f"{step_name}/{k}": v for k, v in params_dict.items()}
|
|
506
|
+
tracker.log_params(prefixed_params)
|
|
507
|
+
logger.debug(f"Logged parameters from step '{step_name}': {list(params_dict.keys())}")
|
|
508
|
+
return True
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.warning(f"Failed to log parameters: {e}")
|
|
512
|
+
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def should_route(output: Any) -> bool:
|
|
517
|
+
"""Check if an output should be routed.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
output: The step output.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
True if the output should be routed.
|
|
524
|
+
"""
|
|
525
|
+
if output is None:
|
|
526
|
+
return False
|
|
527
|
+
|
|
528
|
+
from flowyml.core.types import is_artifact_type
|
|
529
|
+
|
|
530
|
+
return is_artifact_type(output)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def auto_route_metrics_and_params(
|
|
534
|
+
output: Any,
|
|
535
|
+
step_name: str,
|
|
536
|
+
run_id: str,
|
|
537
|
+
) -> bool:
|
|
538
|
+
"""Automatically route Metrics and Parameters without explicit config.
|
|
539
|
+
|
|
540
|
+
This is a convenience function that can be called to log Metrics
|
|
541
|
+
and Parameters even when no routing rule is configured.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
output: The step output.
|
|
545
|
+
step_name: Step name.
|
|
546
|
+
run_id: Run ID.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
True if logging was successful.
|
|
550
|
+
"""
|
|
551
|
+
from flowyml.core.types import Metrics, Parameters
|
|
552
|
+
|
|
553
|
+
if isinstance(output, Metrics):
|
|
554
|
+
return _log_metrics(output, step_name, run_id, None)
|
|
555
|
+
elif isinstance(output, Parameters):
|
|
556
|
+
return _log_parameters(output, step_name, run_id, None)
|
|
557
|
+
|
|
558
|
+
return False
|
flowyml/core/step.py
CHANGED
|
@@ -31,6 +31,8 @@ class StepConfig:
|
|
|
31
31
|
tags: dict[str, str] = field(default_factory=dict)
|
|
32
32
|
condition: Callable | None = None
|
|
33
33
|
execution_group: str | None = None
|
|
34
|
+
source_file: str | None = None
|
|
35
|
+
source_line: int | None = None
|
|
34
36
|
|
|
35
37
|
def __hash__(self):
|
|
36
38
|
"""Make StepConfig hashable."""
|
|
@@ -84,11 +86,15 @@ class Step:
|
|
|
84
86
|
self.condition = condition
|
|
85
87
|
self.execution_group = execution_group
|
|
86
88
|
|
|
87
|
-
# Capture source code for UI display
|
|
89
|
+
# Capture source code and location for UI display
|
|
88
90
|
try:
|
|
89
91
|
self.source_code = inspect.getsource(func)
|
|
92
|
+
self.source_file = inspect.getsourcefile(func)
|
|
93
|
+
_, self.source_line = inspect.getsourcelines(func)
|
|
90
94
|
except (OSError, TypeError):
|
|
91
95
|
self.source_code = "# Source code not available"
|
|
96
|
+
self.source_file = None
|
|
97
|
+
self.source_line = None
|
|
92
98
|
|
|
93
99
|
self.config = StepConfig(
|
|
94
100
|
name=self.name,
|
|
@@ -102,6 +108,8 @@ class Step:
|
|
|
102
108
|
tags=self.tags,
|
|
103
109
|
condition=self.condition,
|
|
104
110
|
execution_group=self.execution_group,
|
|
111
|
+
source_file=self.source_file,
|
|
112
|
+
source_line=self.source_line,
|
|
105
113
|
)
|
|
106
114
|
|
|
107
115
|
def __call__(self, *args, **kwargs):
|
flowyml/core/step_grouping.py
CHANGED
|
@@ -172,26 +172,23 @@ class StepGroupAnalyzer:
|
|
|
172
172
|
Returns:
|
|
173
173
|
True if steps can execute consecutively
|
|
174
174
|
"""
|
|
175
|
-
# Get
|
|
176
|
-
|
|
175
|
+
# Get ALL transitively producing and consuming nodes between step1 and step2
|
|
176
|
+
# Steps are consecutive if there are no intermediate steps NOT in this group
|
|
177
|
+
# that must execute between step1 and step2.
|
|
178
|
+
all_deps_of_s2 = dag.get_all_dependencies(step2.name)
|
|
177
179
|
|
|
178
|
-
# If
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
if not group_deps:
|
|
182
|
-
# No dependencies from this group, consecutive is OK
|
|
183
|
-
return True
|
|
180
|
+
# If step1 is not even a dependency of step2, they are independent.
|
|
181
|
+
# They can be grouped as long as there is no path from step1 to step2
|
|
182
|
+
# through an external step.
|
|
184
183
|
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
intermediate = group_deps - {step1.name}
|
|
184
|
+
# All nodes on any path from step1 to step2:
|
|
185
|
+
all_successors_of_s1 = dag.get_all_dependents(step1.name)
|
|
186
|
+
intermediate_nodes = all_successors_of_s1 & all_deps_of_s2
|
|
189
187
|
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
# If any node on a path from s1 to s2 is NOT in the group, they are not consecutive
|
|
189
|
+
external_intermediates = intermediate_nodes - group_step_names
|
|
192
190
|
|
|
193
|
-
|
|
194
|
-
return False
|
|
191
|
+
return len(external_intermediates) == 0
|
|
195
192
|
|
|
196
193
|
def _get_execution_order(self, steps: list[Step], dag: DAG) -> list[str]:
|
|
197
194
|
"""Get topological execution order for steps in a group.
|
|
@@ -264,29 +261,46 @@ def get_execution_units(dag: DAG, steps: list[Step]) -> list[Step | StepGroup]:
|
|
|
264
261
|
for step in group.steps:
|
|
265
262
|
step_to_group[step.name] = group
|
|
266
263
|
|
|
267
|
-
#
|
|
268
|
-
|
|
264
|
+
# To correctly determine execution order of units (which may have changed due to grouping),
|
|
265
|
+
# we build a new DAG where each node is an execution unit (Step or StepGroup).
|
|
266
|
+
from flowyml.core.graph import Node as DAGNode
|
|
269
267
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
processed_groups: set[str] = set()
|
|
268
|
+
units_dag = DAG()
|
|
269
|
+
unit_map: dict[str, Step | StepGroup] = {}
|
|
273
270
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if
|
|
271
|
+
# Add units as nodes
|
|
272
|
+
processed_steps = set()
|
|
273
|
+
for step in steps:
|
|
274
|
+
if step.name in processed_steps:
|
|
278
275
|
continue
|
|
279
276
|
|
|
280
|
-
|
|
277
|
+
unit: Step | StepGroup
|
|
281
278
|
if step.name in step_to_group:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
#
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
279
|
+
unit = step_to_group[step.name]
|
|
280
|
+
unit_name = f"group:{unit.group_name}"
|
|
281
|
+
# Extract names for inputs/outputs
|
|
282
|
+
u_inputs_set = set()
|
|
283
|
+
u_outputs_set = set()
|
|
284
|
+
for s in unit.steps:
|
|
285
|
+
u_inputs_set.update(s.inputs)
|
|
286
|
+
u_outputs_set.update(s.outputs)
|
|
287
|
+
processed_steps.add(s.name)
|
|
288
|
+
|
|
289
|
+
# External inputs are those not produced within the group
|
|
290
|
+
u_inputs = list(u_inputs_set - u_outputs_set)
|
|
291
|
+
u_outputs = list(u_outputs_set)
|
|
288
292
|
else:
|
|
289
|
-
|
|
290
|
-
|
|
293
|
+
unit = step
|
|
294
|
+
unit_name = step.name
|
|
295
|
+
u_inputs = step.inputs
|
|
296
|
+
u_outputs = step.outputs
|
|
297
|
+
processed_steps.add(step.name)
|
|
298
|
+
|
|
299
|
+
unit_map[unit_name] = unit
|
|
300
|
+
units_dag.add_node(DAGNode(name=unit_name, step=unit, inputs=u_inputs, outputs=u_outputs))
|
|
301
|
+
|
|
302
|
+
# Build edges and sort
|
|
303
|
+
units_dag.build_edges()
|
|
304
|
+
sorted_unit_nodes = units_dag.topological_sort()
|
|
291
305
|
|
|
292
|
-
return
|
|
306
|
+
return [unit_map[node.name] for node in sorted_unit_nodes]
|