Skip to content

Commit d7ff12e

Browse files
authored
Merge branch 'main' into patch-1
2 parents 8d76d2a + 11e5378 commit d7ff12e

File tree

10 files changed

+324
-1
lines changed

10 files changed

+324
-1
lines changed

google/cloud/aiplatform/compat/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@
226226
types.explanation_metadata = types.explanation_metadata_v1
227227
types.feature = types.feature_v1
228228
types.feature_group = types.feature_group_v1
229+
# TODO(b/293184410): Temporary code. Switch to v1 once v1 is available.
230+
types.feature_monitor = types.feature_monitor_v1beta1
231+
types.feature_monitor_job = types.feature_monitor_job_v1beta1
229232
types.feature_monitoring_stats = types.feature_monitoring_stats_v1
230233
types.feature_online_store = types.feature_online_store_v1
231234
types.feature_online_store_admin_service = (

google/cloud/aiplatform/utils/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,12 @@ def parse_feature_path(path: str) -> Dict[str, str]:
763763
)
764764

765765

766+
class FeatureRegistryClientV1Beta1WithOverride(FeatureRegistryClientWithOverride):
767+
"""Adds function override for v1beta1 client classes to support new Feature Store."""
768+
769+
_default_version = compat.V1BETA1
770+
771+
766772
class FeaturestoreClientWithOverride(ClientWithOverride):
767773
_is_temporary = True
768774
_default_version = compat.DEFAULT_VERSION

tests/unit/vertexai/conftest.py

+15
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
from google.cloud.aiplatform.compat.services import (
3535
feature_registry_service_client,
3636
)
37+
from google.cloud.aiplatform_v1beta1.services.feature_registry_service import (
38+
FeatureRegistryServiceClient,
39+
)
3740
from google.cloud.aiplatform.compat.services import job_service_client
3841
from google.cloud.aiplatform.compat.types import (
3942
custom_job as gca_custom_job_compat,
@@ -59,6 +62,7 @@
5962
_TEST_FG1,
6063
_TEST_FG1_F1,
6164
_TEST_FG1_F2,
65+
_TEST_FG1_FM1,
6266
_TEST_FV1,
6367
_TEST_OPTIMIZED_EMBEDDING_FV,
6468
_TEST_OPTIMIZED_FV1,
@@ -67,6 +71,7 @@
6771
)
6872
import pytest
6973

74+
7075
_TEST_PROJECT = "test-project"
7176
_TEST_PROJECT_NUMBER = "12345678"
7277
_TEST_LOCATION = "us-central1"
@@ -519,3 +524,13 @@ def get_feature_with_version_column_mock():
519524
) as get_fg_mock:
520525
get_fg_mock.return_value = _TEST_FG1_F2
521526
yield get_fg_mock
527+
528+
529+
@pytest.fixture
530+
def get_feature_monitor_mock():
531+
with patch.object(
532+
FeatureRegistryServiceClient,
533+
"get_feature_monitor",
534+
) as get_fg_mock:
535+
get_fg_mock.return_value = _TEST_FG1_FM1
536+
yield get_fg_mock

tests/unit/vertexai/feature_store_constants.py

+12
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,15 @@
352352
)
353353

354354
_TEST_FG1_FEATURE_LIST = [_TEST_FG1_F1, _TEST_FG1_F2]
355+
356+
_TEST_FG1_FM1_ID = "my_fg1_fm1"
357+
_TEST_FG1_FM1_PATH = (
358+
f"{_TEST_PARENT}/featureGroups/{_TEST_FG1_ID}/featureMonitors/{_TEST_FG1_FM1_ID}"
359+
)
360+
_TEST_FG1_FM1_DESCRIPTION = "My feature monitor 1 in feature group 1"
361+
_TEST_FG1_FM1_LABELS = {"my_fg1_feature_monitor": "fm1"}
362+
_TEST_FG1_FM1 = types.feature_monitor.FeatureMonitor(
363+
name=_TEST_FG1_FM1_PATH,
364+
description=_TEST_FG1_FM1_DESCRIPTION,
365+
labels=_TEST_FG1_FM1_LABELS,
366+
)
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright 2024 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import re
19+
from typing import Dict
20+
21+
from google.cloud import aiplatform
22+
from google.cloud.aiplatform import base
23+
from feature_store_constants import _TEST_FG1_FM1_DESCRIPTION
24+
from feature_store_constants import _TEST_FG1_FM1_ID
25+
from feature_store_constants import _TEST_FG1_FM1_LABELS
26+
from feature_store_constants import _TEST_FG1_FM1_PATH
27+
from feature_store_constants import _TEST_FG1_ID
28+
from feature_store_constants import _TEST_LOCATION
29+
from feature_store_constants import _TEST_PROJECT
30+
from vertexai.resources.preview import FeatureMonitor
31+
import pytest
32+
33+
34+
pytestmark = pytest.mark.usefixtures("google_auth_mock")
35+
36+
37+
def feature_monitor_eq(
38+
feature_monitor_to_check: FeatureMonitor,
39+
name: str,
40+
resource_name: str,
41+
project: str,
42+
location: str,
43+
description: str,
44+
labels: Dict[str, str],
45+
):
46+
"""Check if a Feature Monitor has the appropriate values set."""
47+
assert feature_monitor_to_check.name == name
48+
assert feature_monitor_to_check.resource_name == resource_name
49+
assert feature_monitor_to_check.project == project
50+
assert feature_monitor_to_check.location == location
51+
assert feature_monitor_to_check.description == description
52+
assert feature_monitor_to_check.labels == labels
53+
54+
55+
def test_init_with_feature_monitor_id_and_no_fg_id_raises_error():
56+
aiplatform.init(project=_TEST_PROJECT, location=_TEST_LOCATION)
57+
58+
with pytest.raises(
59+
ValueError,
60+
match=re.escape(
61+
"Since feature monitor 'my_fg1_fm1' is not provided as a path, please"
62+
" specify feature_group_id."
63+
),
64+
):
65+
FeatureMonitor(_TEST_FG1_FM1_ID)
66+
67+
68+
def test_init_with_feature_monitor_path_and_fg_id_raises_error():
69+
aiplatform.init(project=_TEST_PROJECT, location=_TEST_LOCATION)
70+
71+
with pytest.raises(
72+
ValueError,
73+
match=re.escape(
74+
"Since feature monitor 'projects/test-project/locations/us-central1/"
75+
"featureGroups/my_fg1/featureMonitors/my_fg1_fm1' is provided as a "
76+
"path, feature_group_id should not be specified."
77+
),
78+
):
79+
FeatureMonitor(_TEST_FG1_FM1_PATH, feature_group_id=_TEST_FG1_ID)
80+
81+
82+
def test_init_with_feature_monitor_id(get_feature_monitor_mock):
83+
aiplatform.init(project=_TEST_PROJECT, location=_TEST_LOCATION)
84+
85+
feature_monitor = FeatureMonitor(_TEST_FG1_FM1_ID, feature_group_id=_TEST_FG1_ID)
86+
87+
get_feature_monitor_mock.assert_called_once_with(
88+
name=_TEST_FG1_FM1_PATH,
89+
retry=base._DEFAULT_RETRY,
90+
)
91+
92+
feature_monitor_eq(
93+
feature_monitor,
94+
name=_TEST_FG1_FM1_ID,
95+
resource_name=_TEST_FG1_FM1_PATH,
96+
project=_TEST_PROJECT,
97+
location=_TEST_LOCATION,
98+
description=_TEST_FG1_FM1_DESCRIPTION,
99+
labels=_TEST_FG1_FM1_LABELS,
100+
)
101+
102+
103+
def test_init_with_feature_path(get_feature_monitor_mock):
104+
aiplatform.init(project=_TEST_PROJECT, location=_TEST_LOCATION)
105+
106+
feature_monitor = FeatureMonitor(_TEST_FG1_FM1_PATH)
107+
108+
get_feature_monitor_mock.assert_called_once_with(
109+
name=_TEST_FG1_FM1_PATH,
110+
retry=base._DEFAULT_RETRY,
111+
)
112+
113+
feature_monitor_eq(
114+
feature_monitor,
115+
name=_TEST_FG1_FM1_ID,
116+
resource_name=_TEST_FG1_FM1_PATH,
117+
project=_TEST_PROJECT,
118+
location=_TEST_LOCATION,
119+
description=_TEST_FG1_FM1_DESCRIPTION,
120+
labels=_TEST_FG1_FM1_LABELS,
121+
)

vertexai/generative_models/_generative_models.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ def _prepare_request(
484484
self,
485485
contents: ContentsType,
486486
*,
487+
model: Optional[str] = None,
487488
generation_config: Optional[GenerationConfigType] = None,
488489
safety_settings: Optional[SafetySettingsType] = None,
489490
tools: Optional[List["Tool"]] = None,
@@ -495,6 +496,7 @@ def _prepare_request(
495496
if not contents:
496497
raise TypeError("contents must not be empty")
497498

499+
model = model or self._prediction_resource_name
498500
generation_config = generation_config or self._generation_config
499501
safety_settings = safety_settings or self._safety_settings
500502
tools = tools or self._tools
@@ -563,7 +565,7 @@ def _prepare_request(
563565
# The `model` parameter now needs to be set for the vision models.
564566
# Always need to pass the resource via the `model` parameter.
565567
# Even when resource is an endpoint.
566-
model=self._prediction_resource_name,
568+
model=model,
567569
contents=contents,
568570
generation_config=gapic_generation_config,
569571
safety_settings=gapic_safety_settings,
@@ -2072,11 +2074,26 @@ def __init__(
20722074
)
20732075
)
20742076

2077+
def __repr__(self) -> str:
2078+
return self._gapic_function_calling_config.__repr__()
2079+
20752080
def __init__(self, function_calling_config: "ToolConfig.FunctionCallingConfig"):
20762081
self._gapic_tool_config = gapic_tool_types.ToolConfig(
20772082
function_calling_config=function_calling_config._gapic_function_calling_config
20782083
)
20792084

2085+
@classmethod
2086+
def _from_gapic(
2087+
cls,
2088+
gapic_tool_config: gapic_tool_types.ToolConfig,
2089+
) -> "ToolConfig":
2090+
response = cls.__new__(cls)
2091+
response._gapic_tool_config = gapic_tool_config
2092+
return response
2093+
2094+
def __repr__(self) -> str:
2095+
return self._gapic_tool_config.__repr__()
2096+
20802097

20812098
class FunctionDeclaration:
20822099
r"""A representation of a function declaration.
@@ -3249,6 +3266,7 @@ def _prepare_request(
32493266
self,
32503267
contents: ContentsType,
32513268
*,
3269+
model: Optional[str] = None,
32523270
generation_config: Optional[GenerationConfigType] = None,
32533271
safety_settings: Optional[SafetySettingsType] = None,
32543272
tools: Optional[List["Tool"]] = None,
@@ -3259,6 +3277,7 @@ def _prepare_request(
32593277
"""Prepares a GAPIC GenerateContentRequest."""
32603278
request_v1beta1 = super()._prepare_request(
32613279
contents=contents,
3280+
model=model,
32623281
generation_config=generation_config,
32633282
safety_settings=safety_settings,
32643283
tools=tools,

vertexai/resources/preview/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
Feature,
4141
FeatureGroup,
4242
FeatureGroupBigQuerySource,
43+
FeatureMonitor,
4344
FeatureOnlineStore,
4445
FeatureOnlineStoreType,
4546
FeatureView,
@@ -70,6 +71,7 @@
7071
"Feature",
7172
"FeatureGroup",
7273
"FeatureGroupBigQuerySource",
74+
"FeatureMonitor",
7375
"FeatureOnlineStoreType",
7476
"FeatureOnlineStore",
7577
"FeatureView",

vertexai/resources/preview/feature_store/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
FeatureGroup,
2525
)
2626

27+
from vertexai.resources.preview.feature_store.feature_monitor import (
28+
FeatureMonitor,
29+
)
30+
2731
from vertexai.resources.preview.feature_store.feature_online_store import (
2832
FeatureOnlineStore,
2933
FeatureOnlineStoreType,
@@ -48,6 +52,7 @@
4852
Feature,
4953
FeatureGroup,
5054
FeatureGroupBigQuerySource,
55+
FeatureMonitor,
5156
FeatureOnlineStoreType,
5257
FeatureOnlineStore,
5358
FeatureView,

vertexai/resources/preview/feature_store/feature_group.py

+30
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
from vertexai.resources.preview.feature_store import (
3737
Feature,
3838
)
39+
from vertexai.resources.preview.feature_store.feature_monitor import (
40+
FeatureMonitor,
41+
)
3942

4043

4144
_LOGGER = base.Logger(__name__)
@@ -398,6 +401,33 @@ def list_features(
398401
credentials=credentials,
399402
)
400403

404+
def get_feature_monitor(
405+
self,
406+
feature_monitor_id: str,
407+
credentials: Optional[auth_credentials.Credentials] = None,
408+
) -> FeatureMonitor:
409+
"""Retrieves an existing feature monitor.
410+
411+
Args:
412+
feature_monitor_id: The ID of the feature monitor.
413+
credentials:
414+
Custom credentials to use to retrieve the feature monitor under this
415+
feature group. The order of which credentials are used is as
416+
follows: (1) this parameter (2) credentials passed to FeatureGroup
417+
constructor (3) credentials set in aiplatform.init.
418+
419+
Returns:
420+
FeatureMonitor - the Feature Monitor resource object under this
421+
feature group.
422+
"""
423+
credentials = (
424+
credentials or self.credentials or initializer.global_config.credentials
425+
)
426+
return FeatureMonitor(
427+
f"{self.resource_name}/featureMonitors/{feature_monitor_id}",
428+
credentials=credentials,
429+
)
430+
401431
@property
402432
def source(self) -> FeatureGroupBigQuerySource:
403433
return FeatureGroupBigQuerySource(

0 commit comments

Comments
 (0)