flowyml 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/dataset.py +570 -17
  3. flowyml/assets/metrics.py +5 -0
  4. flowyml/assets/model.py +1052 -15
  5. flowyml/cli/main.py +709 -0
  6. flowyml/cli/stack_cli.py +138 -25
  7. flowyml/core/__init__.py +17 -0
  8. flowyml/core/executor.py +231 -37
  9. flowyml/core/image_builder.py +129 -0
  10. flowyml/core/log_streamer.py +227 -0
  11. flowyml/core/orchestrator.py +59 -4
  12. flowyml/core/pipeline.py +65 -13
  13. flowyml/core/routing.py +558 -0
  14. flowyml/core/scheduler.py +88 -5
  15. flowyml/core/step.py +9 -1
  16. flowyml/core/step_grouping.py +49 -35
  17. flowyml/core/types.py +407 -0
  18. flowyml/integrations/keras.py +247 -82
  19. flowyml/monitoring/alerts.py +10 -0
  20. flowyml/monitoring/notifications.py +104 -25
  21. flowyml/monitoring/slack_blocks.py +323 -0
  22. flowyml/plugins/__init__.py +251 -0
  23. flowyml/plugins/alerters/__init__.py +1 -0
  24. flowyml/plugins/alerters/slack.py +168 -0
  25. flowyml/plugins/base.py +752 -0
  26. flowyml/plugins/config.py +478 -0
  27. flowyml/plugins/deployers/__init__.py +22 -0
  28. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  29. flowyml/plugins/deployers/sagemaker.py +306 -0
  30. flowyml/plugins/deployers/vertex.py +290 -0
  31. flowyml/plugins/integration.py +369 -0
  32. flowyml/plugins/manager.py +510 -0
  33. flowyml/plugins/model_registries/__init__.py +22 -0
  34. flowyml/plugins/model_registries/mlflow.py +159 -0
  35. flowyml/plugins/model_registries/sagemaker.py +489 -0
  36. flowyml/plugins/model_registries/vertex.py +386 -0
  37. flowyml/plugins/orchestrators/__init__.py +13 -0
  38. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  39. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  40. flowyml/plugins/registries/__init__.py +13 -0
  41. flowyml/plugins/registries/ecr.py +321 -0
  42. flowyml/plugins/registries/gcr.py +313 -0
  43. flowyml/plugins/registry.py +454 -0
  44. flowyml/plugins/stack.py +494 -0
  45. flowyml/plugins/stack_config.py +537 -0
  46. flowyml/plugins/stores/__init__.py +13 -0
  47. flowyml/plugins/stores/gcs.py +460 -0
  48. flowyml/plugins/stores/s3.py +453 -0
  49. flowyml/plugins/trackers/__init__.py +11 -0
  50. flowyml/plugins/trackers/mlflow.py +316 -0
  51. flowyml/plugins/validators/__init__.py +3 -0
  52. flowyml/plugins/validators/deepchecks.py +119 -0
  53. flowyml/registry/__init__.py +2 -1
  54. flowyml/registry/model_environment.py +109 -0
  55. flowyml/registry/model_registry.py +241 -96
  56. flowyml/serving/__init__.py +17 -0
  57. flowyml/serving/model_server.py +628 -0
  58. flowyml/stacks/__init__.py +60 -0
  59. flowyml/stacks/aws.py +93 -0
  60. flowyml/stacks/base.py +62 -0
  61. flowyml/stacks/components.py +12 -0
  62. flowyml/stacks/gcp.py +44 -9
  63. flowyml/stacks/plugins.py +115 -0
  64. flowyml/stacks/registry.py +2 -1
  65. flowyml/storage/sql.py +401 -12
  66. flowyml/tracking/experiment.py +8 -5
  67. flowyml/ui/backend/Dockerfile +87 -16
  68. flowyml/ui/backend/auth.py +12 -2
  69. flowyml/ui/backend/main.py +149 -5
  70. flowyml/ui/backend/routers/ai_context.py +226 -0
  71. flowyml/ui/backend/routers/assets.py +23 -4
  72. flowyml/ui/backend/routers/auth.py +96 -0
  73. flowyml/ui/backend/routers/deployments.py +660 -0
  74. flowyml/ui/backend/routers/model_explorer.py +597 -0
  75. flowyml/ui/backend/routers/plugins.py +103 -51
  76. flowyml/ui/backend/routers/projects.py +91 -8
  77. flowyml/ui/backend/routers/runs.py +132 -1
  78. flowyml/ui/backend/routers/schedules.py +54 -29
  79. flowyml/ui/backend/routers/templates.py +319 -0
  80. flowyml/ui/backend/routers/websocket.py +2 -2
  81. flowyml/ui/frontend/Dockerfile +55 -6
  82. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  83. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  84. flowyml/ui/frontend/dist/index.html +2 -2
  85. flowyml/ui/frontend/dist/logo.png +0 -0
  86. flowyml/ui/frontend/nginx.conf +65 -4
  87. flowyml/ui/frontend/package-lock.json +1415 -74
  88. flowyml/ui/frontend/package.json +4 -0
  89. flowyml/ui/frontend/public/logo.png +0 -0
  90. flowyml/ui/frontend/src/App.jsx +10 -7
  91. flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
  92. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  93. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  94. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  95. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  96. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  97. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  98. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
  99. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
  100. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  101. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  102. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
  103. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
  104. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
  105. flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
  106. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  107. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  108. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  109. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  110. flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
  111. flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
  112. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  113. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  114. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  115. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  116. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  117. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  118. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  119. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  120. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  121. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  122. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  123. flowyml/ui/frontend/src/router/index.jsx +47 -20
  124. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  125. flowyml/ui/server_manager.py +5 -5
  126. flowyml/ui/utils.py +157 -39
  127. flowyml/utils/config.py +37 -15
  128. flowyml/utils/model_introspection.py +123 -0
  129. flowyml/utils/observability.py +30 -0
  130. flowyml-1.8.0.dist-info/METADATA +174 -0
  131. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
  132. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  133. flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
  134. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
  135. flowyml-1.7.1.dist-info/METADATA +0 -477
  136. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  137. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,494 @@
1
+ """FlowyML Stack - Unified Interface for ML Operations.
2
+
3
+ This module provides a unified, intuitive API for common ML operations.
4
+ Just configure your stack in flowyml.yaml and use these functions -
5
+ FlowyML automatically routes to the configured plugins.
6
+
7
+ Usage:
8
+ # flowyml.yaml
9
+ plugins:
10
+ experiment_tracker:
11
+ type: mlflow
12
+ tracking_uri: http://localhost:5000
13
+ artifact_store:
14
+ type: gcs
15
+ bucket: my-ml-artifacts
16
+ container_registry:
17
+ type: gcr
18
+ project: my-gcp-project
19
+
20
+ # In code - intuitive API, no plugin knowledge needed
21
+ from flowyml.plugins.stack import (
22
+ start_run, end_run,
23
+ log_params, log_metrics,
24
+ save_artifact, load_artifact,
25
+ save_model, load_model,
26
+ )
27
+
28
+ # Just use - FlowyML routes to your configured plugins
29
+ start_run("training_v1")
30
+ log_params({"lr": 0.001, "epochs": 100})
31
+
32
+ # Train model...
33
+ log_metrics({"accuracy": 0.95})
34
+ save_artifact(test_data, "data/test.pkl")
35
+ save_model(model, "models/classifier")
36
+
37
+ end_run()
38
+ """
39
+
40
+ import logging
41
+ from typing import Any
42
+ from contextlib import contextmanager
43
+
44
+ from flowyml.plugins.config import (
45
+ get_config,
46
+ get_tracker,
47
+ get_artifact_store,
48
+ get_container_registry,
49
+ get_orchestrator,
50
+ get_alerter,
51
+ )
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+
56
+ # =============================================================================
57
+ # EXPERIMENT TRACKING (Auto-routes to configured tracker)
58
+ # =============================================================================
59
+
60
+ _current_run_id: str | None = None
61
+
62
+
63
+ def start_run(
64
+ run_name: str,
65
+ experiment_name: str = None,
66
+ tags: dict = None,
67
+ ) -> str | None:
68
+ """Start a new experiment run.
69
+
70
+ Uses the experiment_tracker configured in flowyml.yaml.
71
+
72
+ Args:
73
+ run_name: Name for this run.
74
+ experiment_name: Optional experiment name.
75
+ tags: Optional tags for the run.
76
+
77
+ Returns:
78
+ Run ID if successful, None otherwise.
79
+
80
+ Example:
81
+ start_run("training_v1", experiment_name="image_classification")
82
+ """
83
+ global _current_run_id
84
+ tracker = get_tracker()
85
+
86
+ if tracker:
87
+ _current_run_id = tracker.start_run(run_name, experiment_name, tags)
88
+ return _current_run_id
89
+ else:
90
+ logger.warning("No experiment_tracker configured in flowyml.yaml")
91
+ return None
92
+
93
+
94
+ def end_run(status: str = "FINISHED") -> None:
95
+ """End the current experiment run.
96
+
97
+ Args:
98
+ status: Final status (FINISHED, FAILED, etc.)
99
+ """
100
+ global _current_run_id
101
+ tracker = get_tracker()
102
+
103
+ if tracker:
104
+ tracker.end_run(status)
105
+ _current_run_id = None
106
+ else:
107
+ logger.warning("No experiment_tracker configured")
108
+
109
+
110
+ @contextmanager
111
+ def run(run_name: str, experiment_name: str = None, tags: dict = None):
112
+ """Context manager for experiment runs.
113
+
114
+ Usage:
115
+ with run("my_training"):
116
+ log_params({"lr": 0.001})
117
+ # Train model...
118
+ log_metrics({"accuracy": 0.95})
119
+ """
120
+ try:
121
+ start_run(run_name, experiment_name, tags)
122
+ yield
123
+ end_run("FINISHED")
124
+ except Exception:
125
+ end_run("FAILED")
126
+ raise
127
+
128
+
129
+ def log_params(params: dict[str, Any]) -> None:
130
+ """Log parameters to the current run.
131
+
132
+ Args:
133
+ params: Dictionary of parameter names and values.
134
+
135
+ Example:
136
+ log_params({"learning_rate": 0.001, "batch_size": 32})
137
+ """
138
+ tracker = get_tracker()
139
+ if tracker:
140
+ tracker.log_params(params)
141
+
142
+
143
+ def log_metrics(metrics: dict[str, float], step: int = None) -> None:
144
+ """Log metrics to the current run.
145
+
146
+ Args:
147
+ metrics: Dictionary of metric names and values.
148
+ step: Optional step number.
149
+
150
+ Example:
151
+ log_metrics({"accuracy": 0.95, "loss": 0.05}, step=100)
152
+ """
153
+ tracker = get_tracker()
154
+ if tracker:
155
+ tracker.log_metrics(metrics, step)
156
+
157
+
158
+ def log_artifact(local_path: str, artifact_path: str = None) -> None:
159
+ """Log an artifact file to the current run.
160
+
161
+ Args:
162
+ local_path: Path to the local file.
163
+ artifact_path: Optional subdirectory in artifacts.
164
+ """
165
+ tracker = get_tracker()
166
+ if tracker and hasattr(tracker, "log_artifact"):
167
+ tracker.log_artifact(local_path, artifact_path)
168
+
169
+
170
+ def set_tag(key: str, value: str) -> None:
171
+ """Set a tag on the current run."""
172
+ tracker = get_tracker()
173
+ if tracker and hasattr(tracker, "set_tag"):
174
+ tracker.set_tag(key, value)
175
+
176
+
177
+ def set_tags(tags: dict[str, str]) -> None:
178
+ """Set multiple tags on the current run."""
179
+ tracker = get_tracker()
180
+ if tracker and hasattr(tracker, "set_tags"):
181
+ tracker.set_tags(tags)
182
+
183
+
184
+ # =============================================================================
185
+ # ARTIFACT STORAGE (Auto-routes to configured store)
186
+ # =============================================================================
187
+
188
+
189
+ def save_artifact(artifact: Any, path: str) -> str | None:
190
+ """Save an artifact to the configured artifact store.
191
+
192
+ Uses the artifact_store configured in flowyml.yaml.
193
+
194
+ Args:
195
+ artifact: The artifact to save (model, data, etc.)
196
+ path: Path within the store.
197
+
198
+ Returns:
199
+ Full URI of the saved artifact.
200
+
201
+ Example:
202
+ save_artifact(processed_data, "data/processed.pkl")
203
+ save_artifact({"config": config}, "configs/training.json")
204
+ """
205
+ store = get_artifact_store()
206
+
207
+ if store:
208
+ return store.save(artifact, path)
209
+ else:
210
+ logger.warning("No artifact_store configured in flowyml.yaml")
211
+ return None
212
+
213
+
214
+ def load_artifact(path: str) -> Any | None:
215
+ """Load an artifact from the configured artifact store.
216
+
217
+ Args:
218
+ path: Path to the artifact.
219
+
220
+ Returns:
221
+ The loaded artifact.
222
+
223
+ Example:
224
+ data = load_artifact("data/processed.pkl")
225
+ """
226
+ store = get_artifact_store()
227
+
228
+ if store:
229
+ return store.load(path)
230
+ else:
231
+ logger.warning("No artifact_store configured")
232
+ return None
233
+
234
+
235
+ def artifact_exists(path: str) -> bool:
236
+ """Check if an artifact exists in the store.
237
+
238
+ Args:
239
+ path: Path to check.
240
+
241
+ Returns:
242
+ True if the artifact exists.
243
+ """
244
+ store = get_artifact_store()
245
+ return store.exists(path) if store else False
246
+
247
+
248
+ def list_artifacts(path: str = "") -> list[str]:
249
+ """List artifacts in a directory.
250
+
251
+ Args:
252
+ path: Directory path to list.
253
+
254
+ Returns:
255
+ List of artifact paths.
256
+ """
257
+ store = get_artifact_store()
258
+ return store.list(path) if store else []
259
+
260
+
261
+ def delete_artifact(path: str) -> bool:
262
+ """Delete an artifact from the store.
263
+
264
+ Args:
265
+ path: Path to delete.
266
+
267
+ Returns:
268
+ True if deletion was successful.
269
+ """
270
+ store = get_artifact_store()
271
+ return store.delete(path) if store else False
272
+
273
+
274
+ # =============================================================================
275
+ # MODEL MANAGEMENT (Uses both artifact store and tracker)
276
+ # =============================================================================
277
+
278
+
279
+ def save_model(
280
+ model: Any,
281
+ path: str,
282
+ model_type: str = None,
283
+ register: bool = True,
284
+ model_name: str = None,
285
+ ) -> str | None:
286
+ """Save a model to the artifact store with tracking.
287
+
288
+ This is the recommended way to save models - it automatically:
289
+ 1. Saves the model to the artifact store
290
+ 2. Logs it to the experiment tracker (if in a run)
291
+ 3. Optionally registers in model registry
292
+
293
+ Args:
294
+ model: The model object.
295
+ path: Path within the artifact store.
296
+ model_type: Type of model (sklearn, pytorch, tensorflow, etc.)
297
+ register: If True, register in model registry (if configured).
298
+ model_name: Name for model registry (uses path if not provided).
299
+
300
+ Returns:
301
+ Full URI of the saved model.
302
+
303
+ Example:
304
+ # Simple save
305
+ save_model(clf, "models/classifier")
306
+
307
+ # With explicit type
308
+ save_model(model, "models/neural_net", model_type="pytorch")
309
+ """
310
+ # Save to artifact store
311
+ store = get_artifact_store()
312
+ uri = None
313
+
314
+ if store:
315
+ uri = store.save(model, path)
316
+ logger.info(f"Model saved to: {uri}")
317
+
318
+ # Log to tracker if in a run
319
+ tracker = get_tracker()
320
+ if tracker and _current_run_id:
321
+ if hasattr(tracker, "log_model"):
322
+ tracker.log_model(model, path, model_type=model_type)
323
+ if uri:
324
+ set_tag("model_uri", uri)
325
+
326
+ return uri
327
+
328
+
329
+ def load_model(path: str) -> Any | None:
330
+ """Load a model from the artifact store.
331
+
332
+ Args:
333
+ path: Path to the model.
334
+
335
+ Returns:
336
+ The loaded model.
337
+
338
+ Example:
339
+ model = load_model("models/classifier.pkl")
340
+ """
341
+ return load_artifact(path)
342
+
343
+
344
+ # =============================================================================
345
+ # CONTAINER REGISTRY (Auto-routes to configured registry)
346
+ # =============================================================================
347
+
348
+
349
+ def push_image(image_name: str, tag: str = "latest", local_image: str = None) -> str | None:
350
+ """Push a Docker image to the configured registry.
351
+
352
+ Args:
353
+ image_name: Name for the image in the registry.
354
+ tag: Image tag.
355
+ local_image: Local image name to push.
356
+
357
+ Returns:
358
+ Full image URI.
359
+
360
+ Example:
361
+ push_image("ml-training", tag="v1.0", local_image="my-local-image")
362
+ """
363
+ registry = get_container_registry()
364
+
365
+ if registry:
366
+ return registry.push_image(image_name, tag, local_image)
367
+ else:
368
+ logger.warning("No container_registry configured in flowyml.yaml")
369
+ return None
370
+
371
+
372
+ def get_image_uri(image_name: str, tag: str = "latest") -> str | None:
373
+ """Get the full URI for an image.
374
+
375
+ Args:
376
+ image_name: Name of the image.
377
+ tag: Image tag.
378
+
379
+ Returns:
380
+ Full image URI.
381
+ """
382
+ registry = get_container_registry()
383
+ return registry.get_image_uri(image_name, tag) if registry else None
384
+
385
+
386
+ # =============================================================================
387
+ # PIPELINE ORCHESTRATION (Auto-routes to configured orchestrator)
388
+ # =============================================================================
389
+
390
+
391
+ def run_pipeline(
392
+ pipeline: Any,
393
+ run_id: str,
394
+ parameters: dict = None,
395
+ **kwargs,
396
+ ) -> Any | None:
397
+ """Run a pipeline on the configured orchestrator.
398
+
399
+ Args:
400
+ pipeline: The pipeline to run.
401
+ run_id: Unique identifier for this run.
402
+ parameters: Pipeline parameters.
403
+ **kwargs: Additional orchestrator-specific arguments.
404
+
405
+ Returns:
406
+ Run result/job object.
407
+
408
+ Example:
409
+ run_pipeline(my_training_pipeline, "training-001",
410
+ parameters={"epochs": 100})
411
+ """
412
+ orchestrator = get_orchestrator()
413
+
414
+ if orchestrator:
415
+ return orchestrator.run_pipeline(pipeline, run_id, parameters=parameters, **kwargs)
416
+ else:
417
+ logger.warning("No orchestrator configured in flowyml.yaml")
418
+ return None
419
+
420
+
421
+ # =============================================================================
422
+ # ALERTS (Auto-routes to configured alerter)
423
+ # =============================================================================
424
+
425
+
426
+ def send_alert(title: str, message: str, level: str = "info") -> bool:
427
+ """Send an alert notification.
428
+
429
+ Args:
430
+ title: Alert title.
431
+ message: Alert message.
432
+ level: Alert level (info, warning, error, critical).
433
+
434
+ Returns:
435
+ True if alert was sent successfully.
436
+
437
+ Example:
438
+ send_alert("Training Complete", "Model accuracy: 95%", level="success")
439
+ """
440
+ alerter = get_alerter()
441
+
442
+ if alerter:
443
+ return alerter.send_alert(title, message, level)
444
+ else:
445
+ logger.info(f"[{level.upper()}] {title}: {message}")
446
+ return True
447
+
448
+
449
+ # =============================================================================
450
+ # STACK INFO
451
+ # =============================================================================
452
+
453
+
454
+ def show_stack() -> dict[str, str]:
455
+ """Show the currently configured stack.
456
+
457
+ Returns:
458
+ Dictionary of configured plugins.
459
+
460
+ Example:
461
+ stack = show_stack()
462
+ print(stack)
463
+ # {'experiment_tracker': 'mlflow', 'artifact_store': 'gcs', ...}
464
+ """
465
+ config = get_config()
466
+ plugins_config = config.plugins_config
467
+
468
+ stack = {}
469
+ for role, conf in plugins_config.items():
470
+ if isinstance(conf, dict):
471
+ stack[role] = conf.get("type", "unknown")
472
+
473
+ return stack
474
+
475
+
476
+ def validate_stack() -> dict[str, bool]:
477
+ """Validate that all configured plugins are installed.
478
+
479
+ Returns:
480
+ Dictionary mapping plugin roles to installation status.
481
+ """
482
+ from flowyml.plugins import is_installed
483
+
484
+ config = get_config()
485
+ plugins_config = config.plugins_config
486
+
487
+ results = {}
488
+ for role, conf in plugins_config.items():
489
+ if isinstance(conf, dict):
490
+ plugin_type = conf.get("type")
491
+ if plugin_type:
492
+ results[role] = is_installed(plugin_type)
493
+
494
+ return results