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,306 @@
1
+ """SageMaker Endpoint Deployer - Native FlowyML Plugin.
2
+
3
+ This plugin provides direct integration with AWS SageMaker
4
+ endpoints for model serving and inference.
5
+
6
+ Usage:
7
+ from flowyml.plugins import get_plugin
8
+
9
+ deployer = get_plugin("sagemaker_endpoint", region="us-east-1")
10
+ endpoint = deployer.deploy(
11
+ model_name="my-model",
12
+ endpoint_name="my-endpoint",
13
+ instance_type="ml.m5.large"
14
+ )
15
+ """
16
+
17
+ import logging
18
+ from typing import Any
19
+ import time
20
+
21
+ from flowyml.plugins.base import ModelDeployerPlugin, PluginMetadata, PluginType
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class SageMakerEndpointDeployer(ModelDeployerPlugin):
27
+ """Native SageMaker Endpoint deployer for FlowyML.
28
+
29
+ This plugin deploys models to SageMaker Endpoints for real-time inference.
30
+
31
+ Args:
32
+ region: AWS region.
33
+ role_arn: IAM role ARN for SageMaker.
34
+ """
35
+
36
+ metadata = PluginMetadata(
37
+ name="sagemaker_endpoint",
38
+ version="1.0.0",
39
+ description="AWS SageMaker Endpoint Deployer",
40
+ author="FlowyML Team",
41
+ plugin_type=PluginType.CUSTOM,
42
+ )
43
+
44
+ def __init__(
45
+ self,
46
+ region: str = None,
47
+ role_arn: str = None,
48
+ **kwargs,
49
+ ):
50
+ """Initialize the SageMaker Endpoint deployer.
51
+
52
+ Args:
53
+ region: AWS region (uses default if not specified).
54
+ role_arn: IAM role ARN for SageMaker operations.
55
+ **kwargs: Additional plugin arguments.
56
+ """
57
+ super().__init__(**kwargs)
58
+ self.region = region
59
+ self.role_arn = role_arn
60
+ self._boto_session = None
61
+ self._sm_client = None
62
+ self._runtime_client = None
63
+ self._initialized = False
64
+
65
+ @property
66
+ def plugin_type(self) -> PluginType:
67
+ return PluginType.CUSTOM
68
+
69
+ def initialize(self) -> None:
70
+ """Initialize connection to SageMaker."""
71
+ if self._initialized:
72
+ return
73
+
74
+ try:
75
+ import boto3
76
+
77
+ self._boto_session = boto3.Session(region_name=self.region)
78
+ self._sm_client = self._boto_session.client("sagemaker")
79
+ self._runtime_client = self._boto_session.client("sagemaker-runtime")
80
+
81
+ self._initialized = True
82
+ logger.info(f"SageMaker Endpoint Deployer initialized in region {self.region}")
83
+ except ImportError:
84
+ raise ImportError(
85
+ "boto3 is required. Install with: pip install boto3",
86
+ )
87
+
88
+ def _ensure_initialized(self) -> None:
89
+ """Ensure SageMaker is initialized."""
90
+ if not self._initialized:
91
+ self.initialize()
92
+
93
+ def deploy(
94
+ self,
95
+ model_uri: str,
96
+ endpoint_name: str,
97
+ instance_type: str = "ml.m5.large",
98
+ instance_count: int = 1,
99
+ inference_image_uri: str = None,
100
+ wait: bool = False,
101
+ **kwargs,
102
+ ) -> str:
103
+ """Deploy a model to a SageMaker endpoint.
104
+
105
+ Args:
106
+ model_uri: S3 URI to model artifacts or model package ARN.
107
+ endpoint_name: Name for the endpoint.
108
+ instance_type: Instance type (e.g., ml.m5.large).
109
+ instance_count: Number of instances.
110
+ inference_image_uri: Docker image for inference.
111
+ wait: Whether to wait for deployment to complete.
112
+ **kwargs: Additional deployment arguments.
113
+
114
+ Returns:
115
+ Endpoint ARN.
116
+ """
117
+ self._ensure_initialized()
118
+
119
+ try:
120
+ model_name = f"{endpoint_name}-model"
121
+ config_name = f"{endpoint_name}-config"
122
+
123
+ # Create model (if model_uri is S3 path, need image)
124
+ if model_uri.startswith("s3://"):
125
+ if not inference_image_uri:
126
+ raise ValueError("inference_image_uri required for S3 model")
127
+
128
+ try:
129
+ self._sm_client.create_model(
130
+ ModelName=model_name,
131
+ PrimaryContainer={
132
+ "Image": inference_image_uri,
133
+ "ModelDataUrl": model_uri,
134
+ },
135
+ ExecutionRoleArn=self.role_arn,
136
+ )
137
+ except self._sm_client.exceptions.ResourceInUse:
138
+ logger.info(f"Model {model_name} already exists")
139
+ elif model_uri.startswith("arn:"):
140
+ # Model package ARN
141
+ try:
142
+ self._sm_client.create_model(
143
+ ModelName=model_name,
144
+ PrimaryContainer={
145
+ "ModelPackageName": model_uri,
146
+ },
147
+ ExecutionRoleArn=self.role_arn,
148
+ )
149
+ except self._sm_client.exceptions.ResourceInUse:
150
+ logger.info(f"Model {model_name} already exists")
151
+ else:
152
+ raise ValueError(f"Invalid model_uri format: {model_uri}")
153
+
154
+ # Create endpoint config
155
+ try:
156
+ self._sm_client.create_endpoint_config(
157
+ EndpointConfigName=config_name,
158
+ ProductionVariants=[
159
+ {
160
+ "VariantName": "primary",
161
+ "ModelName": model_name,
162
+ "InstanceType": instance_type,
163
+ "InitialInstanceCount": instance_count,
164
+ },
165
+ ],
166
+ )
167
+ except self._sm_client.exceptions.ResourceInUse:
168
+ logger.info(f"Endpoint config {config_name} already exists")
169
+
170
+ # Create or update endpoint
171
+ try:
172
+ self._sm_client.create_endpoint(
173
+ EndpointName=endpoint_name,
174
+ EndpointConfigName=config_name,
175
+ )
176
+ logger.info(f"Creating endpoint: {endpoint_name}")
177
+ except self._sm_client.exceptions.ResourceInUse:
178
+ self._sm_client.update_endpoint(
179
+ EndpointName=endpoint_name,
180
+ EndpointConfigName=config_name,
181
+ )
182
+ logger.info(f"Updating endpoint: {endpoint_name}")
183
+
184
+ if wait:
185
+ self._wait_for_endpoint(endpoint_name)
186
+
187
+ # Get endpoint ARN
188
+ endpoint_desc = self._sm_client.describe_endpoint(
189
+ EndpointName=endpoint_name,
190
+ )
191
+
192
+ logger.info(f"Deployed to endpoint: {endpoint_name}")
193
+ return endpoint_desc["EndpointArn"]
194
+
195
+ except Exception as e:
196
+ logger.error(f"Failed to deploy model: {e}")
197
+ raise
198
+
199
+ def _wait_for_endpoint(self, endpoint_name: str, timeout: int = 600) -> None:
200
+ """Wait for endpoint to be in service."""
201
+ start = time.time()
202
+ while time.time() - start < timeout:
203
+ desc = self._sm_client.describe_endpoint(EndpointName=endpoint_name)
204
+ status = desc["EndpointStatus"]
205
+
206
+ if status == "InService":
207
+ logger.info(f"Endpoint {endpoint_name} is InService")
208
+ return
209
+ elif status == "Failed":
210
+ raise RuntimeError(f"Endpoint deployment failed: {desc.get('FailureReason')}")
211
+
212
+ logger.debug(f"Waiting for endpoint, status: {status}")
213
+ time.sleep(30)
214
+
215
+ raise TimeoutError(f"Endpoint deployment timed out after {timeout}s")
216
+
217
+ def undeploy(
218
+ self,
219
+ endpoint_name: str,
220
+ ) -> bool:
221
+ """Delete an endpoint.
222
+
223
+ Args:
224
+ endpoint_name: Endpoint name.
225
+
226
+ Returns:
227
+ True if successful.
228
+ """
229
+ self._ensure_initialized()
230
+
231
+ try:
232
+ self._sm_client.delete_endpoint(EndpointName=endpoint_name)
233
+ logger.info(f"Deleted endpoint: {endpoint_name}")
234
+ return True
235
+
236
+ except Exception as e:
237
+ logger.error(f"Failed to delete endpoint: {e}")
238
+ return False
239
+
240
+ def get_endpoint(
241
+ self,
242
+ endpoint_name: str,
243
+ ) -> dict | None:
244
+ """Get endpoint details.
245
+
246
+ Args:
247
+ endpoint_name: Endpoint name.
248
+
249
+ Returns:
250
+ Endpoint details dictionary.
251
+ """
252
+ self._ensure_initialized()
253
+
254
+ try:
255
+ desc = self._sm_client.describe_endpoint(EndpointName=endpoint_name)
256
+
257
+ return {
258
+ "name": endpoint_name,
259
+ "arn": desc["EndpointArn"],
260
+ "status": desc["EndpointStatus"],
261
+ "config_name": desc["EndpointConfigName"],
262
+ "created": str(desc.get("CreationTime", "")),
263
+ "last_modified": str(desc.get("LastModifiedTime", "")),
264
+ }
265
+
266
+ except self._sm_client.exceptions.ResourceNotFoundException:
267
+ return None
268
+ except Exception as e:
269
+ logger.error(f"Failed to get endpoint: {e}")
270
+ return None
271
+
272
+ def predict(
273
+ self,
274
+ endpoint_name: str,
275
+ data: Any,
276
+ content_type: str = "application/json",
277
+ ) -> Any:
278
+ """Make predictions using a deployed endpoint.
279
+
280
+ Args:
281
+ endpoint_name: Endpoint name.
282
+ data: Input data (will be JSON serialized if dict/list).
283
+ content_type: Content type of the request.
284
+
285
+ Returns:
286
+ Prediction result.
287
+ """
288
+ self._ensure_initialized()
289
+
290
+ try:
291
+ import json
292
+
293
+ body = json.dumps(data) if isinstance(data, (dict, list)) else str(data)
294
+
295
+ response = self._runtime_client.invoke_endpoint(
296
+ EndpointName=endpoint_name,
297
+ ContentType=content_type,
298
+ Body=body.encode("utf-8"),
299
+ )
300
+
301
+ result = json.loads(response["Body"].read().decode("utf-8"))
302
+ return result
303
+
304
+ except Exception as e:
305
+ logger.error(f"Failed to predict: {e}")
306
+ raise
@@ -0,0 +1,290 @@
1
+ """Vertex AI Endpoint Deployer - Native FlowyML Plugin.
2
+
3
+ This plugin provides direct integration with Google Cloud Vertex AI
4
+ endpoints for model serving and inference.
5
+
6
+ Usage:
7
+ from flowyml.plugins import get_plugin
8
+
9
+ deployer = get_plugin("vertex_endpoint", project="my-project")
10
+ endpoint = deployer.deploy(
11
+ model_name="my-model",
12
+ endpoint_name="my-endpoint",
13
+ machine_type="n1-standard-4"
14
+ )
15
+ """
16
+
17
+ import logging
18
+
19
+ from flowyml.plugins.base import ModelDeployerPlugin, PluginMetadata, PluginType
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class VertexEndpointDeployer(ModelDeployerPlugin):
25
+ """Native Vertex AI Endpoint deployer for FlowyML.
26
+
27
+ This plugin deploys models to Vertex AI Endpoints for real-time inference.
28
+
29
+ Args:
30
+ project: GCP project ID.
31
+ location: GCP region (default: us-central1).
32
+ staging_bucket: GCS bucket for staging artifacts.
33
+ """
34
+
35
+ metadata = PluginMetadata(
36
+ name="vertex_endpoint",
37
+ version="1.0.0",
38
+ description="Google Cloud Vertex AI Endpoint Deployer",
39
+ author="FlowyML Team",
40
+ plugin_type=PluginType.CUSTOM,
41
+ )
42
+
43
+ def __init__(
44
+ self,
45
+ project: str,
46
+ location: str = "us-central1",
47
+ staging_bucket: str = None,
48
+ **kwargs,
49
+ ):
50
+ """Initialize the Vertex Endpoint deployer.
51
+
52
+ Args:
53
+ project: GCP project ID.
54
+ location: GCP region.
55
+ staging_bucket: GCS bucket for artifacts.
56
+ **kwargs: Additional plugin arguments.
57
+ """
58
+ super().__init__(**kwargs)
59
+ self.project = project
60
+ self.location = location
61
+ self.staging_bucket = staging_bucket
62
+ self._aiplatform = None
63
+ self._initialized = False
64
+
65
+ @property
66
+ def plugin_type(self) -> PluginType:
67
+ return PluginType.CUSTOM
68
+
69
+ def initialize(self) -> None:
70
+ """Initialize connection to Vertex AI."""
71
+ if self._initialized:
72
+ return
73
+
74
+ try:
75
+ from google.cloud import aiplatform
76
+
77
+ aiplatform.init(
78
+ project=self.project,
79
+ location=self.location,
80
+ staging_bucket=self.staging_bucket,
81
+ )
82
+ self._aiplatform = aiplatform
83
+ self._initialized = True
84
+ logger.info(f"Vertex Endpoint Deployer initialized for project {self.project}")
85
+ except ImportError:
86
+ raise ImportError(
87
+ "google-cloud-aiplatform is required. " "Install with: pip install google-cloud-aiplatform",
88
+ )
89
+
90
+ def _ensure_initialized(self) -> None:
91
+ """Ensure Vertex AI is initialized."""
92
+ if not self._initialized:
93
+ self.initialize()
94
+
95
+ def deploy(
96
+ self,
97
+ model_uri: str,
98
+ endpoint_name: str,
99
+ machine_type: str = "n1-standard-4",
100
+ min_replica_count: int = 1,
101
+ max_replica_count: int = 1,
102
+ accelerator_type: str = None,
103
+ accelerator_count: int = 0,
104
+ service_account: str = None,
105
+ **kwargs,
106
+ ) -> str:
107
+ """Deploy a model to a Vertex AI endpoint.
108
+
109
+ Args:
110
+ model_uri: Model resource name or GCS URI.
111
+ endpoint_name: Name for the endpoint.
112
+ machine_type: Machine type (e.g., n1-standard-4).
113
+ min_replica_count: Minimum replicas.
114
+ max_replica_count: Maximum replicas.
115
+ accelerator_type: GPU type (NVIDIA_TESLA_T4, etc.).
116
+ accelerator_count: Number of GPUs.
117
+ service_account: Service account for endpoint.
118
+ **kwargs: Additional deployment arguments.
119
+
120
+ Returns:
121
+ Endpoint resource name.
122
+ """
123
+ self._ensure_initialized()
124
+
125
+ try:
126
+ # Get or create endpoint
127
+ endpoints = self._aiplatform.Endpoint.list(
128
+ filter=f'display_name="{endpoint_name}"',
129
+ )
130
+
131
+ if endpoints:
132
+ endpoint = endpoints[0]
133
+ logger.info(f"Using existing endpoint: {endpoint_name}")
134
+ else:
135
+ endpoint = self._aiplatform.Endpoint.create(
136
+ display_name=endpoint_name,
137
+ )
138
+ logger.info(f"Created endpoint: {endpoint_name}")
139
+
140
+ # Get the model
141
+ if model_uri.startswith("projects/"):
142
+ model = self._aiplatform.Model(model_name=model_uri)
143
+ else:
144
+ # Search by display name
145
+ models = self._aiplatform.Model.list(
146
+ filter=f'display_name="{model_uri}"',
147
+ order_by="create_time desc",
148
+ )
149
+ if not models:
150
+ raise ValueError(f"Model '{model_uri}' not found")
151
+ model = models[0]
152
+
153
+ # Build deployment config
154
+ deploy_config = {
155
+ "machine_type": machine_type,
156
+ "min_replica_count": min_replica_count,
157
+ "max_replica_count": max_replica_count,
158
+ }
159
+
160
+ if accelerator_type and accelerator_count > 0:
161
+ deploy_config["accelerator_type"] = accelerator_type
162
+ deploy_config["accelerator_count"] = accelerator_count
163
+
164
+ if service_account:
165
+ deploy_config["service_account"] = service_account
166
+
167
+ # Deploy model to endpoint
168
+ model.deploy(endpoint=endpoint, **deploy_config)
169
+
170
+ logger.info(f"Deployed model to endpoint: {endpoint.resource_name}")
171
+ return endpoint.resource_name
172
+
173
+ except Exception as e:
174
+ logger.error(f"Failed to deploy model: {e}")
175
+ raise
176
+
177
+ def undeploy(
178
+ self,
179
+ endpoint_name: str,
180
+ deployed_model_id: str = None,
181
+ ) -> bool:
182
+ """Undeploy a model from an endpoint.
183
+
184
+ Args:
185
+ endpoint_name: Endpoint name or resource name.
186
+ deployed_model_id: Specific deployed model ID (undeployes all if None).
187
+
188
+ Returns:
189
+ True if successful.
190
+ """
191
+ self._ensure_initialized()
192
+
193
+ try:
194
+ if endpoint_name.startswith("projects/"):
195
+ endpoint = self._aiplatform.Endpoint(endpoint_name=endpoint_name)
196
+ else:
197
+ endpoints = self._aiplatform.Endpoint.list(
198
+ filter=f'display_name="{endpoint_name}"',
199
+ )
200
+ if not endpoints:
201
+ logger.warning(f"Endpoint '{endpoint_name}' not found")
202
+ return False
203
+ endpoint = endpoints[0]
204
+
205
+ if deployed_model_id:
206
+ endpoint.undeploy(deployed_model_id=deployed_model_id)
207
+ else:
208
+ endpoint.undeploy_all()
209
+
210
+ logger.info(f"Undeployed model(s) from endpoint: {endpoint_name}")
211
+ return True
212
+
213
+ except Exception as e:
214
+ logger.error(f"Failed to undeploy: {e}")
215
+ return False
216
+
217
+ def get_endpoint(
218
+ self,
219
+ endpoint_name: str,
220
+ ) -> dict | None:
221
+ """Get endpoint details.
222
+
223
+ Args:
224
+ endpoint_name: Endpoint name or resource name.
225
+
226
+ Returns:
227
+ Endpoint details dictionary.
228
+ """
229
+ self._ensure_initialized()
230
+
231
+ try:
232
+ if endpoint_name.startswith("projects/"):
233
+ endpoint = self._aiplatform.Endpoint(endpoint_name=endpoint_name)
234
+ else:
235
+ endpoints = self._aiplatform.Endpoint.list(
236
+ filter=f'display_name="{endpoint_name}"',
237
+ )
238
+ if not endpoints:
239
+ return None
240
+ endpoint = endpoints[0]
241
+
242
+ return {
243
+ "name": endpoint.display_name,
244
+ "resource_name": endpoint.resource_name,
245
+ "deployed_models": [
246
+ {
247
+ "id": dm.id,
248
+ "model": dm.model,
249
+ }
250
+ for dm in endpoint.deployed_models
251
+ ],
252
+ }
253
+
254
+ except Exception as e:
255
+ logger.error(f"Failed to get endpoint: {e}")
256
+ return None
257
+
258
+ def predict(
259
+ self,
260
+ endpoint_name: str,
261
+ instances: list[dict],
262
+ ) -> list[dict]:
263
+ """Make predictions using a deployed endpoint.
264
+
265
+ Args:
266
+ endpoint_name: Endpoint name or resource name.
267
+ instances: List of input instances.
268
+
269
+ Returns:
270
+ List of predictions.
271
+ """
272
+ self._ensure_initialized()
273
+
274
+ try:
275
+ if endpoint_name.startswith("projects/"):
276
+ endpoint = self._aiplatform.Endpoint(endpoint_name=endpoint_name)
277
+ else:
278
+ endpoints = self._aiplatform.Endpoint.list(
279
+ filter=f'display_name="{endpoint_name}"',
280
+ )
281
+ if not endpoints:
282
+ raise ValueError(f"Endpoint '{endpoint_name}' not found")
283
+ endpoint = endpoints[0]
284
+
285
+ response = endpoint.predict(instances=instances)
286
+ return response.predictions
287
+
288
+ except Exception as e:
289
+ logger.error(f"Failed to predict: {e}")
290
+ raise