diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5405cc8ff1..12ad9fb7c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,6 @@ repos: hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 3.9.2 + rev: 6.1.0 hooks: - id: flake8 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9c26098f62..57a1ffd7da 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.28.1" + ".": "1.29.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a09446d7..96186f54e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ # Changelog +## [1.29.0](https://github.com/googleapis/python-aiplatform/compare/v1.28.1...v1.29.0) (2023-08-02) + + +### Features + +* Add preview CustomJob which can be run on persistent resource ([56906b0](https://github.com/googleapis/python-aiplatform/commit/56906b08d80bee64334f6ba0c713e30dae39cef4)) +* LLM - Support for Batch Prediction for the `textembedding` models (preview) ([a368538](https://github.com/googleapis/python-aiplatform/commit/a36853869e627aabf3dc563400d184f44c8ae876)) +* LLM - Support tuning for the code-bison model (preview) ([e4b23a2](https://github.com/googleapis/python-aiplatform/commit/e4b23a254aadfae821e326b238555cee2ecb463a)) +* LVM - Large Vision Models SDK (preview release). Support for image captioning and image QnA (`imagetext` model) and multi modal embedding (`multimodelembedding` model) (preview) ([9bbf1ea](https://github.com/googleapis/python-aiplatform/commit/9bbf1eaa02dda0723303cd39e9f6bdffab32ec21)) + + +### Bug Fixes + +* LLM - Fixed `get_tuned_model` for the future models that are not `text-bison` ([1adf72b](https://github.com/googleapis/python-aiplatform/commit/1adf72b866021b9e857166778dbddf83fd808fb7)) + + +### Documentation + +* Fix auto-generated pydoc for language_models ([7d72bd1](https://github.com/googleapis/python-aiplatform/commit/7d72bd1c3740039d7c63d1042aa6bcadbd3e4946)) +* LLM - Made it possible to provide message history to `CodeChatModel` when starting chat. ([cf46145](https://github.com/googleapis/python-aiplatform/commit/cf46145b3de8de794d4295f59d8af3ea9dd57826)) + ## [1.28.1](https://github.com/googleapis/python-aiplatform/compare/v1.28.0...v1.28.1) (2023-07-18) diff --git a/docs/vertexai/services.rst b/docs/vertexai/services.rst index 92ad7aacbe..12e47a0ffb 100644 --- a/docs/vertexai/services.rst +++ b/docs/vertexai/services.rst @@ -11,6 +11,11 @@ Vertex AI SDK :show-inheritance: :inherited-members: +.. automodule:: vertexai.preview + :members: + :show-inheritance: + :inherited-members: + .. automodule:: vertexai.preview.language_models :members: :show-inheritance: diff --git a/google/cloud/aiplatform/constants/prediction.py b/google/cloud/aiplatform/constants/prediction.py index f5e3b748d8..a5467fb910 100644 --- a/google/cloud/aiplatform/constants/prediction.py +++ b/google/cloud/aiplatform/constants/prediction.py @@ -34,6 +34,9 @@ XGBOOST = "xgboost" XGBOOST_CONTAINER_URIS = [ + "us-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-7:latest", + "europe-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-7:latest", + "asia-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-7:latest", "us-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-6:latest", "europe-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-6:latest", "asia-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-6:latest", @@ -61,6 +64,9 @@ ] SKLEARN_CONTAINER_URIS = [ + "us-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-2:latest", + "europe-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-2:latest", + "asia-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-2:latest", "us-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-0:latest", "europe-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-0:latest", "asia-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-0:latest", @@ -79,6 +85,12 @@ ] TF_CONTAINER_URIS = [ + "us-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-12:latest", + "europe-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-12:latest", + "asia-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-12:latest", + "us-docker.pkg.dev/vertex-ai/prediction/tf2-gpu.2-12:latest", + "europe-docker.pkg.dev/vertex-ai/prediction/tf2-gpu.2-12:latest", + "asia-docker.pkg.dev/vertex-ai/prediction/tf2-gpu.2-12:latest", "us-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-11:latest", "europe-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-11:latest", "asia-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-11:latest", @@ -154,6 +166,12 @@ ] PYTORCH_CONTAINER_URIS = [ + "us-docker.pkg.dev/vertex-ai/prediction/pytorch-cpu.2-0:latest", + "europe-docker.pkg.dev/vertex-ai/prediction/pytorch-cpu.2-0:latest", + "asia-docker.pkg.dev/vertex-ai/prediction/pytorch-cpu.2-0:latest", + "us-docker.pkg.dev/vertex-ai/prediction/pytorch-gpu.2-0:latest", + "europe-docker.pkg.dev/vertex-ai/prediction/pytorch-gpu.2-0:latest", + "asia-docker.pkg.dev/vertex-ai/prediction/pytorch-gpu.2-0:latest", "us-docker.pkg.dev/vertex-ai/prediction/pytorch-cpu.1-13:latest", "europe-docker.pkg.dev/vertex-ai/prediction/pytorch-cpu.1-13:latest", "asia-docker.pkg.dev/vertex-ai/prediction/pytorch-cpu.1-13:latest", diff --git a/google/cloud/aiplatform/gapic_version.py b/google/cloud/aiplatform/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/gapic_version.py +++ b/google/cloud/aiplatform/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/jobs.py b/google/cloud/aiplatform/jobs.py index 7862800270..ef23f86275 100644 --- a/google/cloud/aiplatform/jobs.py +++ b/google/cloud/aiplatform/jobs.py @@ -685,7 +685,7 @@ def create( else: input_config.instances_format = instances_format input_config.gcs_source = gca_io_compat.GcsSource( - uris=gcs_source if type(gcs_source) == list else [gcs_source] + uris=gcs_source if isinstance(gcs_source, list) else [gcs_source] ) if bigquery_destination_prefix: @@ -1154,7 +1154,7 @@ class DataLabelingJob(_Job): pass -class CustomJob(_RunnableJob): +class CustomJob(_RunnableJob, base.PreviewMixin): """Vertex AI Custom Job.""" _resource_noun = "customJobs" @@ -1165,6 +1165,7 @@ class CustomJob(_RunnableJob): _parse_resource_name_method = "parse_custom_job_path" _format_resource_name_method = "custom_job_path" _job_type = "training" + _preview_class = "google.cloud.aiplatform.aiplatform.preview.jobs.CustomJob" def __init__( self, diff --git a/google/cloud/aiplatform/preview/jobs.py b/google/cloud/aiplatform/preview/jobs.py new file mode 100644 index 0000000000..35e611f802 --- /dev/null +++ b/google/cloud/aiplatform/preview/jobs.py @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from typing import Dict, List, Optional, Union +import uuid + +from google.api_core import retry +from google.auth import credentials as auth_credentials +from google.cloud import aiplatform +from google.cloud.aiplatform import base +from google.cloud.aiplatform import compat +from google.cloud.aiplatform import initializer +from google.cloud.aiplatform import jobs +from google.cloud.aiplatform import utils +from google.cloud.aiplatform.compat.types import ( + custom_job_v1beta1 as gca_custom_job_compat, +) +from google.cloud.aiplatform.compat.types import ( + execution_v1beta1 as gcs_execution_compat, +) +from google.cloud.aiplatform.compat.types import io_v1beta1 as gca_io_compat +from google.cloud.aiplatform.metadata import constants as metadata_constants +from google.cloud.aiplatform.utils import console_utils +import proto + +from google.protobuf import duration_pb2 # type: ignore + + +_LOGGER = base.Logger(__name__) +_DEFAULT_RETRY = retry.Retry() + + +class CustomJob(jobs.CustomJob): + """Vertex AI Custom Job.""" + + def __init__( + self, + # TODO(b/223262536): Make display_name parameter fully optional in next major release + display_name: str, + worker_pool_specs: Union[ + List[Dict], List[gca_custom_job_compat.WorkerPoolSpec] + ], + base_output_dir: Optional[str] = None, + project: Optional[str] = None, + location: Optional[str] = None, + credentials: Optional[auth_credentials.Credentials] = None, + labels: Optional[Dict[str, str]] = None, + encryption_spec_key_name: Optional[str] = None, + staging_bucket: Optional[str] = None, + persistent_resource_id: Optional[str] = None, + ): + """Constructs a Custom Job with Worker Pool Specs. + + ``` + Example usage: + worker_pool_specs = [ + { + "machine_spec": { + "machine_type": "n1-standard-4", + "accelerator_type": "NVIDIA_TESLA_K80", + "accelerator_count": 1, + }, + "replica_count": 1, + "container_spec": { + "image_uri": container_image_uri, + "command": [], + "args": [], + }, + } + ] + + my_job = aiplatform.CustomJob( + display_name='my_job', + worker_pool_specs=worker_pool_specs, + labels={'my_key': 'my_value'}, + ) + + my_job.run() + ``` + + + For more information on configuring worker pool specs please visit: + https://cloud.google.com/ai-platform-unified/docs/training/create-custom-job + + + Args: + display_name (str): + Required. The user-defined name of the HyperparameterTuningJob. + The name can be up to 128 characters long and can be consist + of any UTF-8 characters. + worker_pool_specs (Union[List[Dict], List[aiplatform.gapic.WorkerPoolSpec]]): + Required. The spec of the worker pools including machine type and Docker image. + Can provided as a list of dictionaries or list of WorkerPoolSpec proto messages. + base_output_dir (str): + Optional. GCS output directory of job. If not provided a + timestamped directory in the staging directory will be used. + project (str): + Optional.Project to run the custom job in. Overrides project set in aiplatform.init. + location (str): + Optional.Location to run the custom job in. Overrides location set in aiplatform.init. + credentials (auth_credentials.Credentials): + Optional.Custom credentials to use to run call custom job service. Overrides + credentials set in aiplatform.init. + labels (Dict[str, str]): + Optional. The labels with user-defined metadata to + organize CustomJobs. + Label keys and values can be no longer than 64 + characters (Unicode codepoints), can only + contain lowercase letters, numeric characters, + underscores and dashes. International characters + are allowed. + See https://goo.gl/xmQnxf for more information + and examples of labels. + encryption_spec_key_name (str): + Optional.Customer-managed encryption key name for a + CustomJob. If this is set, then all resources + created by the CustomJob will be encrypted with + the provided encryption key. + staging_bucket (str): + Optional. Bucket for produced custom job artifacts. Overrides + staging_bucket set in aiplatform.init. + persistent_resource_id (str): + Optional. The ID of the PersistentResource in the same Project + and Location. If this is specified, the job will be run on + existing machines held by the PersistentResource instead of + on-demand short-live machines. The network and CMEK configs on + the job should be consistent with those on the PersistentResource, + otherwise, the job will be rejected. + + Raises: + RuntimeError: If staging bucket was not set using aiplatform.init + and a staging bucket was not passed in. + """ + + super().__init__( + display_name=display_name, + worker_pool_specs=worker_pool_specs, + base_output_dir=base_output_dir, + project=project, + location=location, + credentials=credentials, + labels=labels, + encryption_spec_key_name=encryption_spec_key_name, + staging_bucket=staging_bucket, + ) + + staging_bucket = staging_bucket or initializer.global_config.staging_bucket + + if not staging_bucket: + raise RuntimeError( + "staging_bucket should be passed to CustomJob constructor or " + "should be set using aiplatform.init(staging_bucket='gs://my-bucket')" + ) + + if labels: + utils.validate_labels(labels) + + # default directory if not given + base_output_dir = base_output_dir or utils._timestamped_gcs_dir( + staging_bucket, "aiplatform-custom-job" + ) + + if not display_name: + display_name = self.__class__._generate_display_name() + + self._gca_resource = gca_custom_job_compat.CustomJob( + display_name=display_name, + job_spec=gca_custom_job_compat.CustomJobSpec( + worker_pool_specs=worker_pool_specs, + base_output_directory=gca_io_compat.GcsDestination( + output_uri_prefix=base_output_dir + ), + persistent_resource_id=persistent_resource_id, + ), + labels=labels, + encryption_spec=initializer.global_config.get_encryption_spec( + encryption_spec_key_name=encryption_spec_key_name, + select_version=compat.V1BETA1, + ), + ) + + self._experiment = None + self._experiment_run = None + self._enable_autolog = False + + def _get_gca_resource( + self, + resource_name: str, + parent_resource_name_fields: Optional[Dict[str, str]] = None, + ) -> proto.Message: + """Returns GAPIC service representation of client class resource. + + Args: + resource_name (str): Required. A fully-qualified resource name or ID. + parent_resource_name_fields (Dict[str,str]): + Optional. Mapping of parent resource name key to values. These + will be used to compose the resource name if only resource ID is given. + Should not include project and location. + """ + resource_name = utils.full_resource_name( + resource_name=resource_name, + resource_noun=self._resource_noun, + parse_resource_name_method=self._parse_resource_name, + format_resource_name_method=self._format_resource_name, + project=self.project, + location=self.location, + parent_resource_name_fields=parent_resource_name_fields, + resource_id_validator=self._resource_id_validator, + ) + + return getattr(self.api_client.select_version("v1beta1"), self._getter_method)( + name=resource_name, retry=_DEFAULT_RETRY + ) + + def submit( + self, + *, + service_account: Optional[str] = None, + network: Optional[str] = None, + timeout: Optional[int] = None, + restart_job_on_worker_restart: bool = False, + enable_web_access: bool = False, + experiment: Optional[Union["aiplatform.Experiment", str]] = None, + experiment_run: Optional[Union["aiplatform.ExperimentRun", str]] = None, + tensorboard: Optional[str] = None, + create_request_timeout: Optional[float] = None, + ) -> None: + """Submit the configured CustomJob. + + Args: + service_account (str): + Optional. Specifies the service account for workload run-as account. + Users submitting jobs must have act-as permission on this run-as account. + network (str): + Optional. The full name of the Compute Engine network to which the job + should be peered. For example, projects/12345/global/networks/myVPC. + Private services access must already be configured for the network. + timeout (int): + The maximum job running time in seconds. The default is 7 days. + restart_job_on_worker_restart (bool): + Restarts the entire CustomJob if a worker + gets restarted. This feature can be used by + distributed training jobs that are not resilient + to workers leaving and joining a job. + enable_web_access (bool): + Whether you want Vertex AI to enable interactive shell access + to training containers. + https://cloud.google.com/vertex-ai/docs/training/monitor-debug-interactive-shell + experiment (Union[aiplatform.Experiment, str]): + Optional. The instance or name of an Experiment resource to which + this CustomJob will upload training parameters and metrics. + + `service_account` is required with provided `experiment`. + For more information on configuring your service account please visit: + https://cloud.google.com/vertex-ai/docs/experiments/tensorboard-training + experiment_run (Union[aiplatform.ExperimentRun, str]): + Optional. The instance or name of an ExperimentRun resource to which + this CustomJob will upload training parameters and metrics. + This arg can only be set when `experiment` is set. If 'experiment' + is set but 'experiment_run` is not, an ExperimentRun resource + will still be auto-generated. + tensorboard (str): + Optional. The name of a Vertex AI + [Tensorboard][google.cloud.aiplatform.v1beta1.Tensorboard] + resource to which this CustomJob will upload Tensorboard + logs. Format: + ``projects/{project}/locations/{location}/tensorboards/{tensorboard}`` + + The training script should write Tensorboard to following Vertex AI environment + variable: + + AIP_TENSORBOARD_LOG_DIR + + `service_account` is required with provided `tensorboard`. + For more information on configuring your service account please visit: + https://cloud.google.com/vertex-ai/docs/experiments/tensorboard-training + create_request_timeout (float): + Optional. The timeout for the create request in seconds. + + Raises: + ValueError: + If both `experiment` and `tensorboard` are specified or if + `enable_autolog` is True in `CustomJob.from_local_script` but + `experiment` is not specified or the specified experiment + doesn't have a backing tensorboard. + """ + if experiment and tensorboard: + raise ValueError("'experiment' and 'tensorboard' cannot be set together.") + if self._enable_autolog and (not experiment): + raise ValueError( + "'experiment' is required since you've enabled autolog in 'from_local_script'." + ) + if service_account: + self._gca_resource.job_spec.service_account = service_account + + if network: + self._gca_resource.job_spec.network = network + + if timeout or restart_job_on_worker_restart: + timeout = duration_pb2.Duration(seconds=timeout) if timeout else None + self._gca_resource.job_spec.scheduling = gca_custom_job_compat.Scheduling( + timeout=timeout, + restart_job_on_worker_restart=restart_job_on_worker_restart, + ) + + if enable_web_access: + self._gca_resource.job_spec.enable_web_access = enable_web_access + + if tensorboard: + self._gca_resource.job_spec.tensorboard = tensorboard + + # TODO(b/275105711) Update implementation after experiment/run in the proto + if experiment: + # short-term solution to set experiment/experimentRun in SDK + if isinstance(experiment, aiplatform.Experiment): + self._experiment = experiment + # convert the Experiment instance to string to be passed to env + experiment = experiment.name + else: + self._experiment = aiplatform.Experiment.get(experiment_name=experiment) + if not self._experiment: + raise ValueError( + f"Experiment '{experiment}' doesn't exist. " + "Please call aiplatform.init(experiment='my-exp') to create an experiment." + ) + elif ( + not self._experiment.backing_tensorboard_resource_name + and self._enable_autolog + ): + raise ValueError( + f"Experiment '{experiment}' doesn't have a backing tensorboard resource, " + "which is required by the experiment autologging feature. " + "Please call Experiment.assign_backing_tensorboard('my-tb-resource-name')." + ) + + # if run name is not specified, auto-generate one + if not experiment_run: + experiment_run = ( + # TODO(b/223262536)Once display_name is optional this run name + # might be invalid as well. + f"{self._gca_resource.display_name}-{uuid.uuid4().hex[0:5]}" + ) + + # get or create the experiment run for the job + if isinstance(experiment_run, aiplatform.ExperimentRun): + self._experiment_run = experiment_run + # convert the ExperimentRun instance to string to be passed to env + experiment_run = experiment_run.name + else: + self._experiment_run = aiplatform.ExperimentRun.get( + run_name=experiment_run, + experiment=self._experiment, + ) + if not self._experiment_run: + self._experiment_run = aiplatform.ExperimentRun.create( + run_name=experiment_run, + experiment=self._experiment, + ) + self._experiment_run.update_state( + gcs_execution_compat.Execution.State.RUNNING + ) + + worker_pool_specs = self._gca_resource.job_spec.worker_pool_specs + for spec in worker_pool_specs: + if not spec: + continue + + if "python_package_spec" in spec: + container_spec = spec.python_package_spec + else: + container_spec = spec.container_spec + + experiment_env = [ + { + "name": metadata_constants.ENV_EXPERIMENT_KEY, + "value": experiment, + }, + { + "name": metadata_constants.ENV_EXPERIMENT_RUN_KEY, + "value": experiment_run, + }, + ] + if "env" in container_spec: + container_spec.env.extend(experiment_env) + else: + container_spec.env = experiment_env + + _LOGGER.log_create_with_lro(self.__class__) + + self._gca_resource = self.api_client.select_version( + "v1beta1" + ).create_custom_job( + parent=self._parent, + custom_job=self._gca_resource, + timeout=create_request_timeout, + ) + + _LOGGER.log_create_complete_with_getter( + self.__class__, self._gca_resource, "custom_job" + ) + + _LOGGER.info("View Custom Job:\n%s" % self._dashboard_uri()) + + if tensorboard: + _LOGGER.info( + "View Tensorboard:\n%s" + % console_utils.custom_job_tensorboard_console_uri( + tensorboard, self.resource_name + ) + ) + + if experiment: + custom_job = { + metadata_constants._CUSTOM_JOB_RESOURCE_NAME: self.resource_name, + metadata_constants._CUSTOM_JOB_CONSOLE_URI: self._dashboard_uri(), + } + + run_context = self._experiment_run._metadata_node + custom_jobs = run_context._gca_resource.metadata.get( + metadata_constants._CUSTOM_JOB_KEY + ) + if custom_jobs: + custom_jobs.append(custom_job) + else: + custom_jobs = [custom_job] + run_context.update({metadata_constants._CUSTOM_JOB_KEY: custom_jobs}) diff --git a/google/cloud/aiplatform/v1/schema/predict/instance/gapic_version.py b/google/cloud/aiplatform/v1/schema/predict/instance/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/predict/instance/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/predict/instance/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1/schema/predict/instance_v1/gapic_version.py b/google/cloud/aiplatform/v1/schema/predict/instance_v1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/predict/instance_v1/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/predict/instance_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1/schema/predict/params/gapic_version.py b/google/cloud/aiplatform/v1/schema/predict/params/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/predict/params/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/predict/params/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1/schema/predict/params_v1/gapic_version.py b/google/cloud/aiplatform/v1/schema/predict/params_v1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/predict/params_v1/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/predict/params_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1/schema/predict/prediction/gapic_version.py b/google/cloud/aiplatform/v1/schema/predict/prediction/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/predict/prediction/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/predict/prediction/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1/schema/predict/prediction_v1/gapic_version.py b/google/cloud/aiplatform/v1/schema/predict/prediction_v1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/predict/prediction_v1/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/predict/prediction_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1/schema/trainingjob/definition/gapic_version.py b/google/cloud/aiplatform/v1/schema/trainingjob/definition/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/trainingjob/definition/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/trainingjob/definition/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1/schema/trainingjob/definition_v1/gapic_version.py b/google/cloud/aiplatform/v1/schema/trainingjob/definition_v1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1/schema/trainingjob/definition_v1/gapic_version.py +++ b/google/cloud/aiplatform/v1/schema/trainingjob/definition_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/predict/instance/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/predict/instance/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/predict/instance/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/predict/instance/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/predict/instance_v1beta1/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/predict/instance_v1beta1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/predict/instance_v1beta1/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/predict/instance_v1beta1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/predict/params/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/predict/params/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/predict/params/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/predict/params/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/predict/params_v1beta1/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/predict/params_v1beta1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/predict/params_v1beta1/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/predict/params_v1beta1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/predict/prediction/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/predict/prediction/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/predict/prediction/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/predict/prediction/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/predict/prediction_v1beta1/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/predict/prediction_v1beta1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/predict/prediction_v1beta1/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/predict/prediction_v1beta1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition_v1beta1/gapic_version.py b/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition_v1beta1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition_v1beta1/gapic_version.py +++ b/google/cloud/aiplatform/v1beta1/schema/trainingjob/definition_v1beta1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform/version.py b/google/cloud/aiplatform/version.py index ccff07328b..507af32a89 100644 --- a/google/cloud/aiplatform/version.py +++ b/google/cloud/aiplatform/version.py @@ -15,4 +15,4 @@ # limitations under the License. # -__version__ = "1.28.1" +__version__ = "1.29.0" diff --git a/google/cloud/aiplatform_v1/gapic_version.py b/google/cloud/aiplatform_v1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform_v1/gapic_version.py +++ b/google/cloud/aiplatform_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/google/cloud/aiplatform_v1beta1/gapic_version.py b/google/cloud/aiplatform_v1beta1/gapic_version.py index 7be5a59b62..ed6cb05766 100644 --- a/google/cloud/aiplatform_v1beta1/gapic_version.py +++ b/google/cloud/aiplatform_v1beta1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.28.1" # {x-release-please-version} +__version__ = "1.29.0" # {x-release-please-version} diff --git a/noxfile.py b/noxfile.py index babd58a573..87ec893504 100644 --- a/noxfile.py +++ b/noxfile.py @@ -25,6 +25,7 @@ import nox +FLAKE8_VERSION = "flake8==6.1.0" BLACK_VERSION = "black==22.3.0" ISORT_VERSION = "isort==5.10.1" LINT_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] @@ -85,7 +86,7 @@ def lint(session): Returns a failure if the linters find linting errors or sufficiently serious code quality issues. """ - session.install("flake8", BLACK_VERSION) + session.install(FLAKE8_VERSION, BLACK_VERSION) session.run( "black", "--check", diff --git a/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1.json b/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1.json index 4ed85d0200..78aaf3e5fd 100644 --- a/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1.json +++ b/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-aiplatform", - "version": "1.28.1" + "version": "1.29.0" }, "snippets": [ { diff --git a/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1beta1.json b/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1beta1.json index e98be78548..05a52b9c3b 100644 --- a/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1beta1.json +++ b/samples/generated_samples/snippet_metadata_google.cloud.aiplatform.v1beta1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-aiplatform", - "version": "1.28.1" + "version": "1.29.0" }, "snippets": [ { diff --git a/samples/model-builder/experiment_tracking/list_artifact_sample.py b/samples/model-builder/experiment_tracking/list_artifact_sample.py index 1414470c51..d254fb4d10 100644 --- a/samples/model-builder/experiment_tracking/list_artifact_sample.py +++ b/samples/model-builder/experiment_tracking/list_artifact_sample.py @@ -21,13 +21,13 @@ def list_artifact_sample( project: str, location: str, - display_name_fitler: Optional[str] = "display_name=\"my_model_*\"", + display_name_filter: Optional[str] = "display_name=\"my_model_*\"", create_date_filter: Optional[str] = "create_time>\"2022-06-11\"", order_by: Optional[str] = None, ): aiplatform.init(project=project, location=location) - combined_filters = f"{display_name_fitler} AND {create_date_filter}" + combined_filters = f"{display_name_filter} AND {create_date_filter}" return aiplatform.Artifact.list( filter=combined_filters, order_by=order_by, diff --git a/samples/model-builder/experiment_tracking/list_artifact_sample_test.py b/samples/model-builder/experiment_tracking/list_artifact_sample_test.py index 2785d24bc5..874adcd266 100644 --- a/samples/model-builder/experiment_tracking/list_artifact_sample_test.py +++ b/samples/model-builder/experiment_tracking/list_artifact_sample_test.py @@ -20,7 +20,7 @@ def test_list_artifact_with_sdk_sample(mock_artifact, mock_list_artifact): artifacts = list_artifact_sample.list_artifact_sample( project=constants.PROJECT, location=constants.LOCATION, - display_name_fitler=constants.DISPLAY_NAME, + display_name_filter=constants.DISPLAY_NAME, create_date_filter=constants.CREATE_DATE, order_by=constants.ORDER_BY, ) diff --git a/samples/model-builder/experiment_tracking/list_execution_sample.py b/samples/model-builder/experiment_tracking/list_execution_sample.py index c5539ccd15..5581098839 100644 --- a/samples/model-builder/experiment_tracking/list_execution_sample.py +++ b/samples/model-builder/experiment_tracking/list_execution_sample.py @@ -21,14 +21,14 @@ def list_execution_sample( project: str, location: str, - display_name_fitler: Optional[str] = "display_name=\"my_execution_*\"", + display_name_filter: Optional[str] = "display_name=\"my_execution_*\"", create_date_filter: Optional[str] = "create_time>\"2022-06-11T12:30:00-08:00\"", ): aiplatform.init( project=project, location=location) - combined_filters = f"{display_name_fitler} AND {create_date_filter}" + combined_filters = f"{display_name_filter} AND {create_date_filter}" return aiplatform.Execution.list(filter=combined_filters) diff --git a/samples/model-builder/experiment_tracking/list_execution_sample_test.py b/samples/model-builder/experiment_tracking/list_execution_sample_test.py index cbec80fc84..ca0fb353a6 100644 --- a/samples/model-builder/experiment_tracking/list_execution_sample_test.py +++ b/samples/model-builder/experiment_tracking/list_execution_sample_test.py @@ -20,7 +20,7 @@ def test_list_execution_sample(mock_execution, mock_list_execution): executions = list_execution_sample.list_execution_sample( project=constants.PROJECT, location=constants.LOCATION, - display_name_fitler=constants.DISPLAY_NAME, + display_name_filter=constants.DISPLAY_NAME, create_date_filter=constants.CREATE_DATE, ) diff --git a/samples/snippets/conftest.py b/samples/snippets/conftest.py index db8bfaf73b..b18d6839e9 100644 --- a/samples/snippets/conftest.py +++ b/samples/snippets/conftest.py @@ -282,7 +282,9 @@ def teardown_batch_read_feature_values(shared_state, bigquery_client): def create_endpoint(shared_state, endpoint_client): def create(project, location, test_name="temp_deploy_model_test"): parent = f"projects/{project}/locations/{location}" - endpoint = aiplatform.gapic.Endpoint(display_name=f"{test_name}_{uuid4()}",) + endpoint = aiplatform.gapic.Endpoint( + display_name=f"{test_name}_{uuid4()}", + ) create_endpoint_response = endpoint_client.create_endpoint( parent=parent, endpoint=endpoint ) diff --git a/samples/snippets/helpers.py b/samples/snippets/helpers.py index 4a9b50719f..dc04f9ca8e 100644 --- a/samples/snippets/helpers.py +++ b/samples/snippets/helpers.py @@ -24,7 +24,7 @@ def get_name(out, key="name"): - pattern = re.compile(fr'{key}:\s*"([\-a-zA-Z0-9/]+)"') + pattern = re.compile(rf'{key}:\s*"([\-a-zA-Z0-9/]+)"') name = re.search(pattern, out).group(1) return name @@ -38,7 +38,7 @@ def get_state(out): def get_featurestore_resource_name(out, key="name"): - pattern = re.compile(fr'{key}:\s*"([\_\-a-zA-Z0-9/]+)"') + pattern = re.compile(rf'{key}:\s*"([\_\-a-zA-Z0-9/]+)"') name = re.search(pattern, out).group(1) return name @@ -51,7 +51,7 @@ def wait_for_job_state( timeout: int = 90, freq: float = 1.5, ) -> None: - """ Waits until the Job state of provided resource name is a particular state. + """Waits until the Job state of provided resource name is a particular state. Args: get_job_method: Callable[[str], "proto.Message"] @@ -91,12 +91,12 @@ def flaky_test_diagnostic(file_name, test_name, N=20): timing_dict = collections.defaultdict(list) for ri in range(N): start = timer() - result = pytest.main(['-s', f'{file_name}::{test_name}']) + result = pytest.main(["-s", f"{file_name}::{test_name}"]) end = timer() - delta = end-start + delta = end - start if result == pytest.ExitCode.OK: - timing_dict['SUCCESS'].append(delta) + timing_dict["SUCCESS"].append(delta) else: - timing_dict['FAILURE'].append(delta) + timing_dict["FAILURE"].append(delta) return timing_dict diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 7c8a63994c..1224cbe212 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -160,6 +160,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +188,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +212,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +225,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +257,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/samples/snippets/noxfile_config.py b/samples/snippets/noxfile_config.py index d1502d6bf6..8f520451ef 100644 --- a/samples/snippets/noxfile_config.py +++ b/samples/snippets/noxfile_config.py @@ -30,6 +30,6 @@ # secrets here. These values will override predefined values. "envs": { "DATA_LABELING_API_ENDPOINT": "us-central1-autopush-aiplatform.sandbox.googleapis.com", - "PYTEST_ADDOPTS": "-n=auto" # Run tests parallel using all available CPUs + "PYTEST_ADDOPTS": "-n=auto", # Run tests parallel using all available CPUs }, } diff --git a/samples/snippets/prediction_service/explain_tabular_sample_test.py b/samples/snippets/prediction_service/explain_tabular_sample_test.py index 910107ac37..385d80602c 100644 --- a/samples/snippets/prediction_service/explain_tabular_sample_test.py +++ b/samples/snippets/prediction_service/explain_tabular_sample_test.py @@ -34,4 +34,4 @@ def test_ucaip_generated_explain_tabular_sample(capsys): ) out, _ = capsys.readouterr() - assert 'attribution' in out + assert "attribution" in out diff --git a/samples/snippets/prediction_service/predict_custom_trained_model_sample.py b/samples/snippets/prediction_service/predict_custom_trained_model_sample.py index 5d04fc2400..cf7a6d2102 100644 --- a/samples/snippets/prediction_service/predict_custom_trained_model_sample.py +++ b/samples/snippets/prediction_service/predict_custom_trained_model_sample.py @@ -37,7 +37,7 @@ def predict_custom_trained_model_sample( # This client only needs to be created once, and can be reused for multiple requests. client = aiplatform.gapic.PredictionServiceClient(client_options=client_options) # The format of each instance should conform to the deployed model's prediction input schema. - instances = instances if type(instances) == list else [instances] + instances = instances if isinstance(instances, list) else [instances] instances = [ json_format.ParseDict(instance_dict, Value()) for instance_dict in instances ] diff --git a/samples/snippets/prediction_service/predict_image_classification_sample.py b/samples/snippets/prediction_service/predict_image_classification_sample.py index 48a88bd256..d8d0adcb37 100644 --- a/samples/snippets/prediction_service/predict_image_classification_sample.py +++ b/samples/snippets/prediction_service/predict_image_classification_sample.py @@ -42,7 +42,8 @@ def predict_image_classification_sample( instances = [instance] # See gs://google-cloud-aiplatform/schema/predict/params/image_classification_1.0.0.yaml for the format of the parameters. parameters = predict.params.ImageClassificationPredictionParams( - confidence_threshold=0.5, max_predictions=5, + confidence_threshold=0.5, + max_predictions=5, ).to_value() endpoint = client.endpoint_path( project=project, location=location, endpoint=endpoint_id diff --git a/samples/snippets/prediction_service/predict_image_classification_sample_test.py b/samples/snippets/prediction_service/predict_image_classification_sample_test.py index f771af99a4..8a0f25a749 100644 --- a/samples/snippets/prediction_service/predict_image_classification_sample_test.py +++ b/samples/snippets/prediction_service/predict_image_classification_sample_test.py @@ -31,4 +31,4 @@ def test_ucaip_generated_predict_image_classification_sample(capsys): ) out, _ = capsys.readouterr() - assert 'deployed_model_id:' in out + assert "deployed_model_id:" in out diff --git a/samples/snippets/prediction_service/predict_image_object_detection_sample.py b/samples/snippets/prediction_service/predict_image_object_detection_sample.py index 1975b06a33..03c053f663 100644 --- a/samples/snippets/prediction_service/predict_image_object_detection_sample.py +++ b/samples/snippets/prediction_service/predict_image_object_detection_sample.py @@ -42,7 +42,8 @@ def predict_image_object_detection_sample( instances = [instance] # See gs://google-cloud-aiplatform/schema/predict/params/image_object_detection_1.0.0.yaml for the format of the parameters. parameters = predict.params.ImageObjectDetectionPredictionParams( - confidence_threshold=0.5, max_predictions=5, + confidence_threshold=0.5, + max_predictions=5, ).to_value() endpoint = client.endpoint_path( project=project, location=location, endpoint=endpoint_id diff --git a/samples/snippets/prediction_service/predict_image_object_detection_sample_test.py b/samples/snippets/prediction_service/predict_image_object_detection_sample_test.py index 8c2eb2e99f..4d028bb541 100644 --- a/samples/snippets/prediction_service/predict_image_object_detection_sample_test.py +++ b/samples/snippets/prediction_service/predict_image_object_detection_sample_test.py @@ -31,4 +31,4 @@ def test_ucaip_generated_predict_image_object_detection_sample(capsys): ) out, _ = capsys.readouterr() - assert 'Salad' in out + assert "Salad" in out diff --git a/samples/snippets/prediction_service/predict_tabular_classification_sample_test.py b/samples/snippets/prediction_service/predict_tabular_classification_sample_test.py index bc85447ddb..4debb804c9 100644 --- a/samples/snippets/prediction_service/predict_tabular_classification_sample_test.py +++ b/samples/snippets/prediction_service/predict_tabular_classification_sample_test.py @@ -35,4 +35,4 @@ def test_ucaip_generated_predict_tabular_classification_sample(capsys): ) out, _ = capsys.readouterr() - assert 'setosa' in out + assert "setosa" in out diff --git a/tests/system/aiplatform/test_featurestore.py b/tests/system/aiplatform/test_featurestore.py index 1e5702f89a..56013aef6b 100644 --- a/tests/system/aiplatform/test_featurestore.py +++ b/tests/system/aiplatform/test_featurestore.py @@ -583,7 +583,7 @@ def test_batch_serve_to_df(self, shared_state, caplog): "average_rating", ] - assert type(df) == pd.DataFrame + assert isinstance(df, pd.DataFrame) assert list(df.columns) == expected_df_columns assert df.size == 54 assert "Featurestore feature values served." in caplog.text @@ -699,16 +699,16 @@ def test_online_reads(self, shared_state): movie_entity_type = shared_state["movie_entity_type"] user_entity_views = user_entity_type.read(entity_ids="alice") - assert type(user_entity_views) == pd.DataFrame + assert isinstance(user_entity_views, pd.DataFrame) movie_entity_views = movie_entity_type.read( entity_ids=["movie_01", "movie_04"], feature_ids=[_TEST_MOVIE_TITLE_FEATURE_ID, _TEST_MOVIE_GENRES_FEATURE_ID], ) - assert type(movie_entity_views) == pd.DataFrame + assert isinstance(movie_entity_views, pd.DataFrame) movie_entity_views = movie_entity_type.read( entity_ids="movie_01", feature_ids=[_TEST_MOVIE_TITLE_FEATURE_ID, _TEST_MOVIE_GENRES_FEATURE_ID], ) - assert type(movie_entity_views) == pd.DataFrame + assert isinstance(movie_entity_views, pd.DataFrame) diff --git a/tests/system/aiplatform/test_language_models.py b/tests/system/aiplatform/test_language_models.py index d4e205c944..1a281d671c 100644 --- a/tests/system/aiplatform/test_language_models.py +++ b/tests/system/aiplatform/test_language_models.py @@ -19,7 +19,7 @@ from google.cloud import aiplatform from google.cloud.aiplatform.compat.types import ( - job_state_v1beta1 as gca_job_state_v1beta1, + job_state as gca_job_state, ) from tests.system.aiplatform import e2e_base from vertexai.preview.language_models import ( @@ -160,7 +160,7 @@ def test_tuning(self, shared_state): ) assert tuned_model_response.text - def test_batch_prediction(self): + def test_batch_prediction_for_text_generation(self): source_uri = "gs://ucaip-samples-us-central1/model/llm/batch_prediction/batch_prediction_prompts1.jsonl" destination_uri_prefix = "gs://ucaip-samples-us-central1/model/llm/batch_prediction/predictions/text-bison@001_" @@ -178,4 +178,24 @@ def test_batch_prediction(self): gapic_job = job._gca_resource job.delete() - assert gapic_job.state == gca_job_state_v1beta1.JobState.JOB_STATE_SUCCEEDED + assert gapic_job.state == gca_job_state.JobState.JOB_STATE_SUCCEEDED + + def test_batch_prediction_for_textembedding(self): + source_uri = "gs://ucaip-samples-us-central1/model/llm/batch_prediction/batch_prediction_prompts1.jsonl" + destination_uri_prefix = "gs://ucaip-samples-us-central1/model/llm/batch_prediction/predictions/textembedding-gecko@001_" + + aiplatform.init(project=e2e_base._PROJECT, location=e2e_base._LOCATION) + + model = TextEmbeddingModel.from_pretrained("textembedding-gecko") + job = model.batch_predict( + dataset=source_uri, + destination_uri_prefix=destination_uri_prefix, + model_parameters={}, + ) + + job.wait_for_resource_creation() + job.wait() + gapic_job = job._gca_resource + job.delete() + + assert gapic_job.state == gca_job_state.JobState.JOB_STATE_SUCCEEDED diff --git a/tests/system/aiplatform/test_vision_models.py b/tests/system/aiplatform/test_vision_models.py new file mode 100644 index 0000000000..ddf7cf7168 --- /dev/null +++ b/tests/system/aiplatform/test_vision_models.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# pylint: disable=protected-access + +import os +import tempfile + +from google.cloud import aiplatform +from tests.system.aiplatform import e2e_base +from vertexai.preview import vision_models +from PIL import Image as PIL_Image + + +def _create_blank_image( + width: int = 100, + height: int = 100, +) -> vision_models.Image: + with tempfile.TemporaryDirectory() as temp_dir: + image_path = os.path.join(temp_dir, "image.png") + pil_image = PIL_Image.new(mode="RGB", size=(width, height)) + pil_image.save(image_path, format="PNG") + return vision_models.Image.load_from_file(image_path) + + +class VisionModelTestSuite(e2e_base.TestEndToEnd): + """System tests for vision models.""" + + _temp_prefix = "temp_vision_models_test_" + + def test_image_captioning_model_get_captions(self): + aiplatform.init(project=e2e_base._PROJECT, location=e2e_base._LOCATION) + + model = vision_models.ImageCaptioningModel.from_pretrained("imagetext") + image = _create_blank_image() + captions = model.get_captions( + image=image, + # Optional: + number_of_results=2, + language="en", + ) + assert len(captions) == 2 + + def test_image_q_and_a_model_ask_question(self): + aiplatform.init(project=e2e_base._PROJECT, location=e2e_base._LOCATION) + + model = vision_models.ImageQnAModel.from_pretrained("imagetext") + image = _create_blank_image() + answers = model.ask_question( + image=image, + question="What color is the car in this image?", + # Optional: + number_of_results=2, + ) + assert len(answers) == 2 + + def test_multi_modal_embedding_model(self): + aiplatform.init(project=e2e_base._PROJECT, location=e2e_base._LOCATION) + + model = vision_models.MultiModalEmbeddingModel.from_pretrained( + "multimodalembedding@001" + ) + image = _create_blank_image() + embeddings = model.get_embeddings( + image=image, + # Optional: + contextual_text="this is a car", + ) + # The service is expected to return the embeddings of size 1408 + assert len(embeddings.image_embedding) == 1408 + assert len(embeddings.text_embedding) == 1408 diff --git a/tests/unit/aiplatform/test_autologging.py b/tests/unit/aiplatform/test_autologging.py index a0516a61e8..71a5c06190 100644 --- a/tests/unit/aiplatform/test_autologging.py +++ b/tests/unit/aiplatform/test_autologging.py @@ -35,7 +35,6 @@ from google.cloud.aiplatform import initializer from google.cloud.aiplatform import base from google.cloud.aiplatform_v1 import ( - AddContextArtifactsAndExecutionsResponse, Artifact as GapicArtifact, Context as GapicContext, Execution as GapicExecution, @@ -395,17 +394,6 @@ def add_context_children_mock(): yield add_context_children_mock -@pytest.fixture -def add_context_artifacts_and_executions_mock(): - with patch.object( - MetadataServiceClient, "add_context_artifacts_and_executions" - ) as add_context_artifacts_and_executions_mock: - add_context_artifacts_and_executions_mock.return_value = ( - AddContextArtifactsAndExecutionsResponse() - ) - yield add_context_artifacts_and_executions_mock - - @pytest.fixture def get_tensorboard_run_not_found_mock(): with patch.object( diff --git a/tests/unit/aiplatform/test_custom_job_persistent_resource.py b/tests/unit/aiplatform/test_custom_job_persistent_resource.py new file mode 100644 index 0000000000..3405feb9da --- /dev/null +++ b/tests/unit/aiplatform/test_custom_job_persistent_resource.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import copy +from importlib import reload +from unittest import mock +from unittest.mock import patch + +from google.cloud import aiplatform +from google.cloud.aiplatform.compat.services import ( + job_service_client_v1beta1, +) +from google.cloud.aiplatform.compat.types import custom_job_v1beta1 +from google.cloud.aiplatform.compat.types import encryption_spec_v1beta1 +from google.cloud.aiplatform.compat.types import io_v1beta1 +from google.cloud.aiplatform.compat.types import ( + job_state_v1beta1 as gca_job_state_compat, +) +from google.cloud.aiplatform.preview import jobs +import constants as test_constants +import pytest + +from google.protobuf import duration_pb2 + + +_TEST_PROJECT = test_constants.ProjectConstants._TEST_PROJECT +_TEST_LOCATION = test_constants.ProjectConstants._TEST_LOCATION +_TEST_ID = "1028944691210842416" +_TEST_DISPLAY_NAME = test_constants.TrainingJobConstants._TEST_DISPLAY_NAME + +_TEST_PARENT = test_constants.ProjectConstants._TEST_PARENT + +_TEST_CUSTOM_JOB_NAME = f"{_TEST_PARENT}/customJobs/{_TEST_ID}" + +_TEST_PREBUILT_CONTAINER_IMAGE = "gcr.io/cloud-aiplatform/container:image" + +_TEST_RUN_ARGS = test_constants.TrainingJobConstants._TEST_RUN_ARGS +_TEST_EXPERIMENT = "test-experiment" +_TEST_EXPERIMENT_RUN = "test-experiment-run" + +_TEST_WORKER_POOL_SPEC = test_constants.TrainingJobConstants._TEST_WORKER_POOL_SPEC + +_TEST_STAGING_BUCKET = test_constants.TrainingJobConstants._TEST_STAGING_BUCKET +_TEST_BASE_OUTPUT_DIR = test_constants.TrainingJobConstants._TEST_BASE_OUTPUT_DIR + +# CMEK encryption +_TEST_DEFAULT_ENCRYPTION_KEY_NAME = "key_1234" +_TEST_DEFAULT_ENCRYPTION_SPEC = encryption_spec_v1beta1.EncryptionSpec( + kms_key_name=_TEST_DEFAULT_ENCRYPTION_KEY_NAME +) + +_TEST_SERVICE_ACCOUNT = test_constants.ProjectConstants._TEST_SERVICE_ACCOUNT + + +_TEST_NETWORK = test_constants.TrainingJobConstants._TEST_NETWORK + +_TEST_TIMEOUT = test_constants.TrainingJobConstants._TEST_TIMEOUT +_TEST_RESTART_JOB_ON_WORKER_RESTART = ( + test_constants.TrainingJobConstants._TEST_RESTART_JOB_ON_WORKER_RESTART +) + +_TEST_LABELS = test_constants.ProjectConstants._TEST_LABELS + + +# Persistent Resource +_TEST_PERSISTENT_RESOURCE_ID = "test-persistent-resource-1" +_TEST_CUSTOM_JOB_WITH_PERSISTENT_RESOURCE_PROTO = custom_job_v1beta1.CustomJob( + display_name=_TEST_DISPLAY_NAME, + job_spec=custom_job_v1beta1.CustomJobSpec( + worker_pool_specs=_TEST_WORKER_POOL_SPEC, + base_output_directory=io_v1beta1.GcsDestination( + output_uri_prefix=_TEST_BASE_OUTPUT_DIR + ), + scheduling=custom_job_v1beta1.Scheduling( + timeout=duration_pb2.Duration(seconds=_TEST_TIMEOUT), + restart_job_on_worker_restart=_TEST_RESTART_JOB_ON_WORKER_RESTART, + ), + service_account=_TEST_SERVICE_ACCOUNT, + network=_TEST_NETWORK, + persistent_resource_id=_TEST_PERSISTENT_RESOURCE_ID, + ), + labels=_TEST_LABELS, + encryption_spec=_TEST_DEFAULT_ENCRYPTION_SPEC, +) + + +def _get_custom_job_proto(state=None, name=None, error=None): + custom_job_proto = copy.deepcopy(_TEST_CUSTOM_JOB_WITH_PERSISTENT_RESOURCE_PROTO) + custom_job_proto.name = name + custom_job_proto.state = state + custom_job_proto.error = error + return custom_job_proto + + +@pytest.fixture +def create_preview_custom_job_mock(): + with mock.patch.object( + job_service_client_v1beta1.JobServiceClient, "create_custom_job" + ) as create_preview_custom_job_mock: + create_preview_custom_job_mock.return_value = _get_custom_job_proto( + name=_TEST_CUSTOM_JOB_NAME, + state=gca_job_state_compat.JobState.JOB_STATE_PENDING, + ) + yield create_preview_custom_job_mock + + +@pytest.fixture +def get_custom_job_mock(): + with patch.object( + job_service_client_v1beta1.JobServiceClient, "get_custom_job" + ) as get_custom_job_mock: + get_custom_job_mock.side_effect = [ + _get_custom_job_proto( + name=_TEST_CUSTOM_JOB_NAME, + state=gca_job_state_compat.JobState.JOB_STATE_PENDING, + ), + _get_custom_job_proto( + name=_TEST_CUSTOM_JOB_NAME, + state=gca_job_state_compat.JobState.JOB_STATE_RUNNING, + ), + _get_custom_job_proto( + name=_TEST_CUSTOM_JOB_NAME, + state=gca_job_state_compat.JobState.JOB_STATE_SUCCEEDED, + ), + ] + yield get_custom_job_mock + + +@pytest.mark.usefixtures("google_auth_mock") +class TestCustomJobPersistentResource: + def setup_method(self): + reload(aiplatform.initializer) + reload(aiplatform) + + def teardown_method(self): + aiplatform.initializer.global_pool.shutdown(wait=True) + + @pytest.mark.parametrize("sync", [True, False]) + def test_create_custom_job_with_persistent_resource( + self, create_preview_custom_job_mock, get_custom_job_mock, sync + ): + + aiplatform.init( + project=_TEST_PROJECT, + location=_TEST_LOCATION, + staging_bucket=_TEST_STAGING_BUCKET, + encryption_spec_key_name=_TEST_DEFAULT_ENCRYPTION_KEY_NAME, + ) + + job = jobs.CustomJob( + display_name=_TEST_DISPLAY_NAME, + worker_pool_specs=_TEST_WORKER_POOL_SPEC, + base_output_dir=_TEST_BASE_OUTPUT_DIR, + labels=_TEST_LABELS, + persistent_resource_id=_TEST_PERSISTENT_RESOURCE_ID, + ) + + job.run( + service_account=_TEST_SERVICE_ACCOUNT, + network=_TEST_NETWORK, + timeout=_TEST_TIMEOUT, + restart_job_on_worker_restart=_TEST_RESTART_JOB_ON_WORKER_RESTART, + sync=sync, + create_request_timeout=None, + ) + + job.wait_for_resource_creation() + + assert job.resource_name == _TEST_CUSTOM_JOB_NAME + + job.wait() + + expected_custom_job = _get_custom_job_proto() + + create_preview_custom_job_mock.assert_called_once_with( + parent=_TEST_PARENT, + custom_job=expected_custom_job, + timeout=None, + ) + + assert job.job_spec == expected_custom_job.job_spec + assert ( + job._gca_resource.state == gca_job_state_compat.JobState.JOB_STATE_SUCCEEDED + ) + assert job.network == _TEST_NETWORK + + def test_submit_custom_job_with_persistent_resource( + self, create_preview_custom_job_mock, get_custom_job_mock + ): + + aiplatform.init( + project=_TEST_PROJECT, + location=_TEST_LOCATION, + staging_bucket=_TEST_STAGING_BUCKET, + encryption_spec_key_name=_TEST_DEFAULT_ENCRYPTION_KEY_NAME, + ) + + job = jobs.CustomJob( + display_name=_TEST_DISPLAY_NAME, + worker_pool_specs=_TEST_WORKER_POOL_SPEC, + base_output_dir=_TEST_BASE_OUTPUT_DIR, + labels=_TEST_LABELS, + persistent_resource_id=_TEST_PERSISTENT_RESOURCE_ID, + ) + + job.submit( + service_account=_TEST_SERVICE_ACCOUNT, + network=_TEST_NETWORK, + timeout=_TEST_TIMEOUT, + restart_job_on_worker_restart=_TEST_RESTART_JOB_ON_WORKER_RESTART, + create_request_timeout=None, + ) + + job.wait_for_resource_creation() + + assert job.resource_name == _TEST_CUSTOM_JOB_NAME + + job.wait() + + expected_custom_job = _get_custom_job_proto() + + create_preview_custom_job_mock.assert_called_once_with( + parent=_TEST_PARENT, + custom_job=expected_custom_job, + timeout=None, + ) + + assert job.job_spec == expected_custom_job.job_spec + assert ( + job._gca_resource.state == gca_job_state_compat.JobState.JOB_STATE_PENDING + ) + assert job.network == _TEST_NETWORK diff --git a/tests/unit/aiplatform/test_datasets.py b/tests/unit/aiplatform/test_datasets.py index ccc9a8e9dc..86618bb789 100644 --- a/tests/unit/aiplatform/test_datasets.py +++ b/tests/unit/aiplatform/test_datasets.py @@ -1444,7 +1444,7 @@ def test_list_dataset(self, list_datasets_mock): assert len(ds_list) < len(_TEST_DATASET_LIST) for ds in ds_list: - assert type(ds) == aiplatform.TabularDataset + assert isinstance(ds, aiplatform.TabularDataset) def test_list_dataset_no_order_or_filter(self, list_datasets_mock): @@ -1456,7 +1456,7 @@ def test_list_dataset_no_order_or_filter(self, list_datasets_mock): assert len(ds_list) < len(_TEST_DATASET_LIST) for ds in ds_list: - assert type(ds) == aiplatform.TabularDataset + assert isinstance(ds, aiplatform.TabularDataset) @pytest.mark.usefixtures("get_dataset_tabular_missing_metadata_mock") def test_tabular_dataset_column_name_missing_metadata(self): diff --git a/tests/unit/aiplatform/test_deployment_resource_pools.py b/tests/unit/aiplatform/test_deployment_resource_pools.py index f0c319f52f..24347562c2 100644 --- a/tests/unit/aiplatform/test_deployment_resource_pools.py +++ b/tests/unit/aiplatform/test_deployment_resource_pools.py @@ -324,7 +324,7 @@ def test_list(self, list_drp_mock): list_drp_mock.assert_called_once() for drp in drp_list: - assert type(drp) == models.DeploymentResourcePool + assert isinstance(drp, models.DeploymentResourcePool) @pytest.mark.usefixtures("delete_drp_mock", "get_drp_mock") @pytest.mark.parametrize("sync", [True, False]) diff --git a/tests/unit/aiplatform/test_endpoints.py b/tests/unit/aiplatform/test_endpoints.py index 52df7f5d5d..8e39790676 100644 --- a/tests/unit/aiplatform/test_endpoints.py +++ b/tests/unit/aiplatform/test_endpoints.py @@ -1999,7 +1999,7 @@ def test_list_endpoint_order_by_time(self, list_endpoints_mock): assert len(ep_list) == len(_TEST_ENDPOINT_LIST) for ep in ep_list: - assert type(ep) == aiplatform.Endpoint + assert isinstance(ep, aiplatform.Endpoint) assert ep_list[0].create_time > ep_list[1].create_time > ep_list[2].create_time @@ -2018,7 +2018,7 @@ def test_list_endpoint_order_by_display_name(self, list_endpoints_mock): assert len(ep_list) == len(_TEST_ENDPOINT_LIST) for ep in ep_list: - assert type(ep) == aiplatform.Endpoint + assert isinstance(ep, aiplatform.Endpoint) assert ( ep_list[0].display_name < ep_list[1].display_name < ep_list[2].display_name diff --git a/tests/unit/aiplatform/test_featurestores.py b/tests/unit/aiplatform/test_featurestores.py index 3eccdb401f..6c2f8d19f9 100644 --- a/tests/unit/aiplatform/test_featurestores.py +++ b/tests/unit/aiplatform/test_featurestores.py @@ -937,7 +937,7 @@ def test_get_entity_type(self, get_entity_type_mock): get_entity_type_mock.assert_called_once_with( name=_TEST_ENTITY_TYPE_NAME, retry=base._DEFAULT_RETRY ) - assert type(my_entity_type) == aiplatform.EntityType + assert isinstance(my_entity_type, aiplatform.EntityType) @pytest.mark.usefixtures("get_featurestore_mock") def test_update_featurestore(self, update_featurestore_mock): @@ -1049,7 +1049,7 @@ def test_list_featurestores(self, list_featurestores_mock): ) assert len(my_featurestore_list) == len(_TEST_FEATURESTORE_LIST) for my_featurestore in my_featurestore_list: - assert type(my_featurestore) == aiplatform.Featurestore + assert isinstance(my_featurestore, aiplatform.Featurestore) @pytest.mark.parametrize( "force, sync", @@ -1093,7 +1093,7 @@ def test_list_entity_types(self, list_entity_types_mock): ) assert len(my_entity_type_list) == len(_TEST_ENTITY_TYPE_LIST) for my_entity_type in my_entity_type_list: - assert type(my_entity_type) == aiplatform.EntityType + assert isinstance(my_entity_type, aiplatform.EntityType) @pytest.mark.usefixtures("get_featurestore_mock") def test_list_entity_types_with_no_init(self, list_entity_types_mock): @@ -1109,7 +1109,7 @@ def test_list_entity_types_with_no_init(self, list_entity_types_mock): ) assert len(my_entity_type_list) == len(_TEST_ENTITY_TYPE_LIST) for my_entity_type in my_entity_type_list: - assert type(my_entity_type) == aiplatform.EntityType + assert isinstance(my_entity_type, aiplatform.EntityType) @pytest.mark.parametrize( "force, sync", @@ -2071,7 +2071,7 @@ def test_get_featurestore(self, get_featurestore_mock): get_featurestore_mock.assert_called_once_with( name=my_featurestore.resource_name, retry=base._DEFAULT_RETRY ) - assert type(my_featurestore) == aiplatform.Featurestore + assert isinstance(my_featurestore, aiplatform.Featurestore) @pytest.mark.usefixtures("get_entity_type_mock") def test_get_feature(self, get_feature_mock): @@ -2083,7 +2083,7 @@ def test_get_feature(self, get_feature_mock): get_feature_mock.assert_called_once_with( name=my_feature.resource_name, retry=base._DEFAULT_RETRY ) - assert type(my_feature) == aiplatform.Feature + assert isinstance(my_feature, aiplatform.Feature) @pytest.mark.usefixtures("get_entity_type_mock") def test_update_entity_type(self, update_entity_type_mock): @@ -2123,7 +2123,7 @@ def test_list_entity_type(self, featurestore_name, list_entity_types_mock): ) assert len(my_entity_type_list) == len(_TEST_ENTITY_TYPE_LIST) for my_entity_type in my_entity_type_list: - assert type(my_entity_type) == aiplatform.EntityType + assert isinstance(my_entity_type, aiplatform.EntityType) @pytest.mark.usefixtures("get_entity_type_mock") def test_list_features(self, list_features_mock): @@ -2137,7 +2137,7 @@ def test_list_features(self, list_features_mock): ) assert len(my_feature_list) == len(_TEST_FEATURE_LIST) for my_feature in my_feature_list: - assert type(my_feature) == aiplatform.Feature + assert isinstance(my_feature, aiplatform.Feature) @pytest.mark.usefixtures("get_entity_type_mock") def test_list_features_with_no_init(self, list_features_mock): @@ -2154,7 +2154,7 @@ def test_list_features_with_no_init(self, list_features_mock): ) assert len(my_feature_list) == len(_TEST_FEATURE_LIST) for my_feature in my_feature_list: - assert type(my_feature) == aiplatform.Feature + assert isinstance(my_feature, aiplatform.Feature) @pytest.mark.parametrize("sync", [True, False]) @pytest.mark.usefixtures("get_entity_type_mock", "get_feature_mock") @@ -2646,7 +2646,7 @@ def test_read_single_entity(self, read_feature_values_mock): metadata=_TEST_REQUEST_METADATA, timeout=None, ) - assert type(result) == pd.DataFrame + assert isinstance(result, pd.DataFrame) assert len(result) == 1 assert result.entity_id[0] == _TEST_READ_ENTITY_ID assert result.get(_TEST_FEATURE_ID)[0] == _TEST_FEATURE_VALUE @@ -2697,7 +2697,7 @@ def test_read_multiple_entities(self, streaming_read_feature_values_mock): metadata=_TEST_REQUEST_METADATA, timeout=None, ) - assert type(result) == pd.DataFrame + assert isinstance(result, pd.DataFrame) assert len(result) == 1 assert result.entity_id[0] == _TEST_READ_ENTITY_ID assert result.get(_TEST_FEATURE_ID)[0] == _TEST_FEATURE_VALUE @@ -3674,7 +3674,7 @@ def test_get_featurestore(self, get_featurestore_mock): get_featurestore_mock.assert_called_once_with( name=my_featurestore.resource_name, retry=base._DEFAULT_RETRY ) - assert type(my_featurestore) == aiplatform.Featurestore + assert isinstance(my_featurestore, aiplatform.Featurestore) @pytest.mark.usefixtures("get_feature_mock") def test_get_entity_type(self, get_entity_type_mock): @@ -3686,7 +3686,7 @@ def test_get_entity_type(self, get_entity_type_mock): get_entity_type_mock.assert_called_once_with( name=my_entity_type.resource_name, retry=base._DEFAULT_RETRY ) - assert type(my_entity_type) == aiplatform.EntityType + assert isinstance(my_entity_type, aiplatform.EntityType) @pytest.mark.usefixtures("get_feature_mock") def test_update_feature(self, update_feature_mock): @@ -3728,7 +3728,7 @@ def test_list_features(self, entity_type_name, featurestore_id, list_features_mo ) assert len(my_feature_list) == len(_TEST_FEATURE_LIST) for my_feature in my_feature_list: - assert type(my_feature) == aiplatform.Feature + assert isinstance(my_feature, aiplatform.Feature) @pytest.mark.usefixtures("get_feature_mock") def test_search_features(self, search_features_mock): @@ -3741,7 +3741,7 @@ def test_search_features(self, search_features_mock): ) assert len(my_feature_list) == len(_TEST_FEATURE_LIST) for my_feature in my_feature_list: - assert type(my_feature) == aiplatform.Feature + assert isinstance(my_feature, aiplatform.Feature) @pytest.mark.usefixtures("get_feature_mock") @pytest.mark.parametrize("sync", [True, False]) diff --git a/tests/unit/aiplatform/test_language_models.py b/tests/unit/aiplatform/test_language_models.py index 8a1ed1bb00..7da36fbea3 100644 --- a/tests/unit/aiplatform/test_language_models.py +++ b/tests/unit/aiplatform/test_language_models.py @@ -141,7 +141,7 @@ "version_id": "001", "open_source_category": "PROPRIETARY", "launch_stage": gca_publisher_model.PublisherModel.LaunchStage.GA, - "publisher_model_template": "projects/{user-project}/locations/{location}/publishers/google/models/chat-bison@001", + "publisher_model_template": "projects/{user-project}/locations/{location}/publishers/google/models/textembedding-gecko@001", "predict_schemata": { "instance_schema_uri": "gs://google-cloud-aiplatform/schema/predict/instance/text_embedding_1.0.0.yaml", "parameters_schema_uri": "gs://google-cloud-aiplatfrom/schema/predict/params/text_generation_1.0.0.yaml", @@ -672,6 +672,7 @@ def test_tune_model( "pipeline_job" ].runtime_config.parameter_values assert pipeline_arguments["learning_rate"] == 0.1 + assert pipeline_arguments["large_model_reference"] == "text-bison@001" assert ( call_kwargs["pipeline_job"].encryption_spec.kms_key_name == _TEST_ENCRYPTION_KEY_NAME @@ -1322,3 +1323,37 @@ def test_batch_prediction(self): gcs_destination_prefix="gs://test-bucket/results/", model_parameters={"temperature": 0.1}, ) + + def test_batch_prediction_for_text_embedding(self): + """Tests batch prediction.""" + aiplatform.init( + project=_TEST_PROJECT, + location=_TEST_LOCATION, + ) + with mock.patch.object( + target=model_garden_service_client.ModelGardenServiceClient, + attribute="get_publisher_model", + return_value=gca_publisher_model.PublisherModel( + _TEXT_EMBEDDING_GECKO_PUBLISHER_MODEL_DICT + ), + ): + model = preview_language_models.TextEmbeddingModel.from_pretrained( + "textembedding-gecko@001" + ) + + with mock.patch.object( + target=aiplatform.BatchPredictionJob, + attribute="create", + ) as mock_create: + model.batch_predict( + dataset="gs://test-bucket/test_table.jsonl", + destination_uri_prefix="gs://test-bucket/results/", + model_parameters={}, + ) + mock_create.assert_called_once_with( + model_name="publishers/google/models/textembedding-gecko@001", + job_display_name=None, + gcs_source="gs://test-bucket/test_table.jsonl", + gcs_destination_prefix="gs://test-bucket/results/", + model_parameters={}, + ) diff --git a/tests/unit/aiplatform/test_matching_engine_index.py b/tests/unit/aiplatform/test_matching_engine_index.py index b1fe99db1f..a4693dc809 100644 --- a/tests/unit/aiplatform/test_matching_engine_index.py +++ b/tests/unit/aiplatform/test_matching_engine_index.py @@ -256,7 +256,7 @@ def test_list_indexes(self, list_indexes_mock): list_indexes_mock.assert_called_once_with(request={"parent": _TEST_PARENT}) assert len(my_indexes_list) == len(_TEST_INDEX_LIST) for my_index in my_indexes_list: - assert type(my_index) == aiplatform.MatchingEngineIndex + assert isinstance(my_index, aiplatform.MatchingEngineIndex) @pytest.mark.parametrize("sync", [True, False]) @pytest.mark.usefixtures("get_index_mock") diff --git a/tests/unit/aiplatform/test_matching_engine_index_endpoint.py b/tests/unit/aiplatform/test_matching_engine_index_endpoint.py index 3590c3e5e1..87ab5a9c5f 100644 --- a/tests/unit/aiplatform/test_matching_engine_index_endpoint.py +++ b/tests/unit/aiplatform/test_matching_engine_index_endpoint.py @@ -586,7 +586,7 @@ def test_list_index_endpoints(self, list_index_endpoints_mock): ) assert len(my_index_endpoints_list) == len(_TEST_INDEX_ENDPOINT_LIST) for my_index_endpoint in my_index_endpoints_list: - assert type(my_index_endpoint) == aiplatform.MatchingEngineIndexEndpoint + assert isinstance(my_index_endpoint, aiplatform.MatchingEngineIndexEndpoint) @pytest.mark.parametrize("sync", [True, False]) @pytest.mark.usefixtures("get_index_endpoint_mock") diff --git a/tests/unit/aiplatform/test_vision_models.py b/tests/unit/aiplatform/test_vision_models.py new file mode 100644 index 0000000000..f42228e7e8 --- /dev/null +++ b/tests/unit/aiplatform/test_vision_models.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Unit tests for the vision models.""" + +# pylint: disable=protected-access,bad-continuation + +import importlib +import os +import tempfile +from unittest import mock + +from google.cloud import aiplatform +from google.cloud.aiplatform import base +from google.cloud.aiplatform import initializer +from google.cloud.aiplatform.compat.services import ( + model_garden_service_client, +) +from google.cloud.aiplatform.compat.services import prediction_service_client +from google.cloud.aiplatform.compat.types import ( + prediction_service as gca_prediction_service, +) +from google.cloud.aiplatform.compat.types import ( + publisher_model as gca_publisher_model, +) +from vertexai.preview import vision_models + +from PIL import Image as PIL_Image +import pytest + +_TEST_PROJECT = "test-project" +_TEST_LOCATION = "us-central1" + +_IMAGE_TEXT_PUBLISHER_MODEL_DICT = { + "name": "publishers/google/models/imagetext", + "version_id": "001", + "open_source_category": "PROPRIETARY", + "launch_stage": gca_publisher_model.PublisherModel.LaunchStage.GA, + "publisher_model_template": "projects/{project}/locations/{location}/publishers/google/models/imagetext@001", + "predict_schemata": { + "instance_schema_uri": "gs://google-cloud-aiplatform/schema/predict/instance/vision_reasoning_model_1.0.0.yaml", + "parameters_schema_uri": "gs://google-cloud-aiplatfrom/schema/predict/params/vision_reasoning_model_1.0.0.yaml", + "prediction_schema_uri": "gs://google-cloud-aiplatform/schema/predict/prediction/vision_reasoning_model_1.0.0.yaml", + }, +} + +_IMAGE_EMBEDDING_PUBLISHER_MODEL_DICT = { + "name": "publishers/google/models/multimodalembedding", + "version_id": "001", + "open_source_category": "PROPRIETARY", + "launch_stage": gca_publisher_model.PublisherModel.LaunchStage.GA, + "publisher_model_template": "projects/{project}/locations/{location}/publishers/google/models/multimodalembedding@001", + "predict_schemata": { + "instance_schema_uri": "gs://google-cloud-aiplatform/schema/predict/instance/vision_embedding_model_1.0.0.yaml", + "parameters_schema_uri": "gs://google-cloud-aiplatfrom/schema/predict/params/vision_embedding_model_1.0.0.yaml", + "prediction_schema_uri": "gs://google-cloud-aiplatform/schema/predict/prediction/vision_embedding_model_1.0.0.yaml", + }, +} + + +def generate_image_from_file( + width: int = 100, height: int = 100 +) -> vision_models.Image: + with tempfile.TemporaryDirectory() as temp_dir: + image_path = os.path.join(temp_dir, "image.png") + pil_image = PIL_Image.new(mode="RGB", size=(width, height)) + pil_image.save(image_path, format="PNG") + return vision_models.Image.load_from_file(image_path) + + +@pytest.mark.usefixtures("google_auth_mock") +class ImageCaptioningModelTests: + """Unit tests for the image captioning models.""" + + def setup_method(self): + importlib.reload(initializer) + importlib.reload(aiplatform) + + def teardown_method(self): + initializer.global_pool.shutdown(wait=True) + + def test_get_captions(self): + """Tests the image captioning model.""" + aiplatform.init( + project=_TEST_PROJECT, + location=_TEST_LOCATION, + ) + with mock.patch.object( + target=model_garden_service_client.ModelGardenServiceClient, + attribute="get_publisher_model", + return_value=gca_publisher_model(_IMAGE_TEXT_PUBLISHER_MODEL_DICT), + ): + model = vision_models.ImageCaptioningModel.from_pretrained("imagetext@001") + + image_captions = [ + "Caption 1", + "Caption 2", + ] + gca_predict_response = gca_prediction_service.PredictResponse() + gca_predict_response.predictions.extend(image_captions) + + with tempfile.TemporaryDirectory() as temp_dir: + image_path = os.path.join(temp_dir, "image.png") + pil_image = PIL_Image.new(mode="RGB", size=(100, 100)) + pil_image.save(image_path, format="PNG") + image = vision_models.Image.load_from_file(image_path) + + with mock.patch.object( + target=prediction_service_client.PredictionServiceClient, + attribute="predict", + return_value=gca_predict_response, + ): + actual_captions = model.get_captions(image=image, number_of_results=2) + assert actual_captions == image_captions + + +@pytest.mark.usefixtures("google_auth_mock") +class ImageQnAModelTests: + """Unit tests for the image to text models.""" + + def setup_method(self): + importlib.reload(initializer) + importlib.reload(aiplatform) + + def teardown_method(self): + initializer.global_pool.shutdown(wait=True) + + def test_get_captions(self): + """Tests the image captioning model.""" + aiplatform.init( + project=_TEST_PROJECT, + location=_TEST_LOCATION, + ) + with mock.patch.object( + target=model_garden_service_client.ModelGardenServiceClient, + attribute="get_publisher_model", + return_value=gca_publisher_model.PublisherModel( + _IMAGE_TEXT_PUBLISHER_MODEL_DICT + ), + ) as mock_get_publisher_model: + model = vision_models.ImageQnAModel.from_pretrained("imagetext@001") + + mock_get_publisher_model.assert_called_once_with( + name="publishers/google/models/imagetext@001", + retry=base._DEFAULT_RETRY, + ) + + image_answers = [ + "Black square", + "Black Square by Malevich", + ] + gca_predict_response = gca_prediction_service.PredictResponse() + gca_predict_response.predictions.extend(image_answers) + + image = generate_image_from_file() + + with mock.patch.object( + target=prediction_service_client.PredictionServiceClient, + attribute="predict", + return_value=gca_predict_response, + ): + actual_answers = model.ask_question( + image=image, + question="What is this painting?", + number_of_results=2, + ) + assert actual_answers == image_answers + + +@pytest.mark.usefixtures("google_auth_mock") +class TestMultiModalEmbeddingModels: + """Unit tests for the image generation models.""" + + def setup_method(self): + importlib.reload(initializer) + importlib.reload(aiplatform) + + def teardown_method(self): + initializer.global_pool.shutdown(wait=True) + + def test_image_embedding_model_with_only_image(self): + aiplatform.init( + project=_TEST_PROJECT, + location=_TEST_LOCATION, + ) + with mock.patch.object( + target=model_garden_service_client.ModelGardenServiceClient, + attribute="get_publisher_model", + return_value=gca_publisher_model.PublisherModel( + _IMAGE_EMBEDDING_PUBLISHER_MODEL_DICT + ), + ) as mock_get_publisher_model: + model = vision_models.MultiModalEmbeddingModel.from_pretrained( + "multimodalembedding@001" + ) + + mock_get_publisher_model.assert_called_once_with( + name="publishers/google/models/multimodalembedding@001", + retry=base._DEFAULT_RETRY, + ) + + test_image_embeddings = [0, 0] + gca_predict_response = gca_prediction_service.PredictResponse() + gca_predict_response.predictions.append( + {"imageEmbedding": test_image_embeddings} + ) + + image = generate_image_from_file() + + with mock.patch.object( + target=prediction_service_client.PredictionServiceClient, + attribute="predict", + return_value=gca_predict_response, + ): + embedding_response = model.get_embeddings(image=image) + + assert embedding_response.image_embedding == test_image_embeddings + assert not embedding_response.text_embedding + + def test_image_embedding_model_with_image_and_text(self): + aiplatform.init( + project=_TEST_PROJECT, + location=_TEST_LOCATION, + ) + with mock.patch.object( + target=model_garden_service_client.ModelGardenServiceClient, + attribute="get_publisher_model", + return_value=gca_publisher_model.PublisherModel( + _IMAGE_EMBEDDING_PUBLISHER_MODEL_DICT + ), + ): + model = vision_models.MultiModalEmbeddingModel.from_pretrained( + "multimodalembedding@001" + ) + + test_embeddings = [0, 0] + gca_predict_response = gca_prediction_service.PredictResponse() + gca_predict_response.predictions.append( + {"imageEmbedding": test_embeddings, "textEmbedding": test_embeddings} + ) + + image = generate_image_from_file() + + with mock.patch.object( + target=prediction_service_client.PredictionServiceClient, + attribute="predict", + return_value=gca_predict_response, + ): + embedding_response = model.get_embeddings( + image=image, contextual_text="hello world" + ) + + assert embedding_response.image_embedding == test_embeddings + assert embedding_response.text_embedding == test_embeddings diff --git a/tests/unit/aiplatform/test_vizier.py b/tests/unit/aiplatform/test_vizier.py index 27bd751a42..60c95a5baf 100644 --- a/tests/unit/aiplatform/test_vizier.py +++ b/tests/unit/aiplatform/test_vizier.py @@ -292,7 +292,7 @@ def test_create_study(self, create_study_mock): create_study_mock.assert_called_once_with( parent=_TEST_PARENT, study=_TEST_STUDY ) - assert type(study) == Study + assert isinstance(study, Study) @pytest.mark.usefixtures("get_study_mock") def test_create_study_already_exists( @@ -320,7 +320,7 @@ def test_create_study_already_exists( lookup_study_mock.assert_called_once_with( request={"parent": _TEST_PARENT, "display_name": _TEST_DISPLAY_NAME} ) - assert type(study) == Study + assert isinstance(study, Study) @pytest.mark.usefixtures("get_study_mock") def test_materialize_study_config(self, create_study_mock): @@ -347,7 +347,7 @@ def test_materialize_study_config(self, create_study_mock): create_study_mock.assert_called_once_with( parent=_TEST_PARENT, study=_TEST_STUDY ) - assert type(study_config) == pyvizier.StudyConfig + assert isinstance(study_config, pyvizier.StudyConfig) @pytest.mark.usefixtures("get_study_mock", "get_trial_mock") def test_suggest(self, create_study_mock, suggest_trials_mock): @@ -378,7 +378,7 @@ def test_suggest(self, create_study_mock, suggest_trials_mock): "client_id": "test_worker", } ) - assert type(trials[0]) == Trial + assert isinstance(trials[0], Trial) @pytest.mark.usefixtures("get_study_mock") def test_from_uid(self): @@ -386,7 +386,7 @@ def test_from_uid(self): study = Study.from_uid(uid=_TEST_STUDY_ID) - assert type(study) == Study + assert isinstance(study, Study) assert study.name == _TEST_STUDY_ID @pytest.mark.usefixtures("get_study_mock") @@ -438,7 +438,7 @@ def test_optimal_trials(self, list_optimal_trials_mock): list_optimal_trials_mock.assert_called_once_with( request={"parent": _TEST_STUDY_NAME} ) - assert type(trials[0]) == Trial + assert isinstance(trials[0], Trial) @pytest.mark.usefixtures("get_study_mock", "create_study_mock", "get_trial_mock") def test_list_trials(self, list_trials_mock): @@ -463,7 +463,7 @@ def test_list_trials(self, list_trials_mock): trials = study.trials() list_trials_mock.assert_called_once_with(request={"parent": _TEST_STUDY_NAME}) - assert type(trials[0]) == Trial + assert isinstance(trials[0], Trial) @pytest.mark.usefixtures("get_study_mock", "create_study_mock") def test_get_trial(self, get_trial_mock): @@ -488,7 +488,7 @@ def test_get_trial(self, get_trial_mock): trial = study.get_trial(1) get_trial_mock.assert_called_once_with(name=_TEST_TRIAL_NAME, retry=ANY) - assert type(trial) == Trial + assert isinstance(trial, Trial) @pytest.mark.usefixtures("google_auth_mock") @@ -508,7 +508,7 @@ def test_delete(self, delete_trial_mock): trial.delete() delete_trial_mock.assert_called_once_with(name=_TEST_TRIAL_NAME) - assert type(trial) == Trial + assert isinstance(trial, Trial) @pytest.mark.usefixtures("get_trial_mock") def test_complete(self, complete_trial_mock): @@ -532,7 +532,7 @@ def test_complete(self, complete_trial_mock): ), } ) - assert type(measurement) == pyvizier.Measurement + assert isinstance(measurement, pyvizier.Measurement) @pytest.mark.usefixtures("get_trial_mock") def test_complete_empty_measurement(self, complete_trial_empty_measurement_mock): diff --git a/vertexai/_model_garden/_model_garden_models.py b/vertexai/_model_garden/_model_garden_models.py index 5634ced5e4..30f71398e6 100644 --- a/vertexai/_model_garden/_model_garden_models.py +++ b/vertexai/_model_garden/_model_garden_models.py @@ -31,7 +31,8 @@ _SUPPORTED_PUBLISHERS = ["google"] _SHORT_MODEL_ID_TO_TUNING_PIPELINE_MAP = { - "text-bison": "https://us-kfp.pkg.dev/ml-pipeline/large-language-model-pipelines/tune-large-model/v2.0.0" + "text-bison": "https://us-kfp.pkg.dev/ml-pipeline/large-language-model-pipelines/tune-large-model/v2.0.0", + "code-bison": "https://us-kfp.pkg.dev/ml-pipeline/large-language-model-pipelines/tune-large-model/v3.0.0", } _SDK_PRIVATE_PREVIEW_LAUNCH_STAGE = frozenset( @@ -117,7 +118,7 @@ def _get_model_info( if short_model_id in _SHORT_MODEL_ID_TO_TUNING_PIPELINE_MAP: tuning_pipeline_uri = _SHORT_MODEL_ID_TO_TUNING_PIPELINE_MAP[short_model_id] - tuning_model_id = short_model_id + "-" + publisher_model_res.version_id + tuning_model_id = publisher_model_template.rsplit("/", 1)[-1] else: tuning_pipeline_uri = None tuning_model_id = None diff --git a/vertexai/language_models/_language_models.py b/vertexai/language_models/_language_models.py index c1e7524968..378006a891 100644 --- a/vertexai/language_models/_language_models.py +++ b/vertexai/language_models/_language_models.py @@ -42,7 +42,7 @@ def _get_model_id_from_tuning_model_id(tuning_model_id: str) -> str: """Gets the base model ID for the model ID labels used the tuned models. Args: - tuning_model_id: The model ID used in tuning + tuning_model_id: The model ID used in tuning. E.g. `text-bison-001` Returns: The publisher model ID @@ -50,11 +50,9 @@ def _get_model_id_from_tuning_model_id(tuning_model_id: str) -> str: Raises: ValueError: If tuning model ID is unsupported """ - if tuning_model_id.startswith("text-bison-"): - return tuning_model_id.replace( - "text-bison-", "publishers/google/models/text-bison@" - ) - raise ValueError(f"Unsupported tuning model ID {tuning_model_id}") + model_name, _, version = tuning_model_id.rpartition("-") + # "publishers/google/models/text-bison@001" + return f"publishers/google/models/{model_name}@{version}" class _LanguageModel(_model_garden_models._ModelGardenModel): @@ -151,7 +149,8 @@ def tune_model( Args: training_data: A Pandas DataFrame or a URI pointing to data in JSON lines format. - The dataset must have the "input_text" and "output_text" columns. + The dataset schema is model-specific. + See https://cloud.google.com/vertex-ai/docs/generative-ai/models/tune-models#dataset_format train_steps: Number of training batches to tune on (batch size is 8 samples). learning_rate: Learning rate for the tuning tuning_job_location: GCP location where the tuning job should be run. @@ -201,6 +200,7 @@ def tune_model( tuned_model = job.result() # The UXR study attendees preferred to tune model in place self._endpoint = tuned_model._endpoint + self._endpoint_name = tuned_model._endpoint_name @dataclasses.dataclass @@ -586,9 +586,7 @@ def get_embeddings(self, texts: List[str]) -> List["TextEmbedding"]: ] -class _PreviewTextEmbeddingModel(TextEmbeddingModel): - """Preview text embedding model.""" - +class _PreviewTextEmbeddingModel(TextEmbeddingModel, _ModelWithBatchPredict): _LAUNCH_STAGE = _model_garden_models._SDK_PUBLIC_PREVIEW_LAUNCH_STAGE @@ -726,6 +724,7 @@ def start_chat( *, max_output_tokens: int = _DEFAULT_MAX_OUTPUT_TOKENS, temperature: float = _DEFAULT_TEMPERATURE, + message_history: Optional[List[ChatMessage]] = None, ) -> "CodeChatSession": """Starts a chat session with the code chat model. @@ -740,6 +739,7 @@ def start_chat( model=self, max_output_tokens=max_output_tokens, temperature=temperature, + message_history=message_history ) @@ -914,12 +914,14 @@ def __init__( model: CodeChatModel, max_output_tokens: int = CodeChatModel._DEFAULT_MAX_OUTPUT_TOKENS, temperature: float = CodeChatModel._DEFAULT_TEMPERATURE, + message_history: Optional[List[ChatMessage]] = None, ): super().__init__( model=model, max_output_tokens=max_output_tokens, temperature=temperature, is_code_chat_session=True, + message_history=message_history, ) def send_message( @@ -1007,6 +1009,10 @@ def predict( ) +class _PreviewCodeGenerationModel(CodeGenerationModel, _TunableModelMixin): + _LAUNCH_STAGE = _model_garden_models._SDK_PUBLIC_PREVIEW_LAUNCH_STAGE + + ###### Model tuning # Currently, tuning can only work in this location _TUNING_LOCATIONS = ("europe-west4", "us-central1") @@ -1102,7 +1108,6 @@ def _launch_tuning_job( dataset_uri = training_data elif pandas and isinstance(training_data, pandas.DataFrame): dataset_uri = _uri_join(output_dir_uri, "training_data.jsonl") - training_data = training_data[["input_text", "output_text"]] gcs_utils._upload_pandas_df_to_gcs( df=training_data, upload_gcs_path=dataset_uri diff --git a/vertexai/preview/language_models.py b/vertexai/preview/language_models.py index aee71e2aaa..057d73fdcd 100644 --- a/vertexai/preview/language_models.py +++ b/vertexai/preview/language_models.py @@ -16,6 +16,7 @@ from vertexai.language_models._language_models import ( _PreviewChatModel, + _PreviewCodeGenerationModel, _PreviewTextEmbeddingModel, _PreviewTextGenerationModel, ChatMessage, @@ -23,13 +24,13 @@ ChatSession, CodeChatModel, CodeChatSession, - CodeGenerationModel, InputOutputTextPair, TextEmbedding, TextGenerationResponse, ) ChatModel = _PreviewChatModel +CodeGenerationModel = _PreviewCodeGenerationModel TextGenerationModel = _PreviewTextGenerationModel TextEmbeddingModel = _PreviewTextEmbeddingModel diff --git a/vertexai/preview/vision_models.py b/vertexai/preview/vision_models.py new file mode 100644 index 0000000000..fb3a32fd5f --- /dev/null +++ b/vertexai/preview/vision_models.py @@ -0,0 +1,31 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Classes for working with vision models.""" + +from vertexai.vision_models._vision_models import ( + Image, + ImageCaptioningModel, + ImageQnAModel, + MultiModalEmbeddingModel, + MultiModalEmbeddingResponse, +) + +__all__ = [ + "Image", + "ImageCaptioningModel", + "ImageQnAModel", + "MultiModalEmbeddingModel", + "MultiModalEmbeddingResponse", +] diff --git a/vertexai/vision_models/_vision_models.py b/vertexai/vision_models/_vision_models.py new file mode 100644 index 0000000000..a9a7150076 --- /dev/null +++ b/vertexai/vision_models/_vision_models.py @@ -0,0 +1,291 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Classes for working with vision models.""" + +import base64 +import dataclasses +import io +import pathlib +from typing import Any, List, Optional + +from vertexai._model_garden import _model_garden_models + +# pylint: disable=g-import-not-at-top +try: + from IPython import display as IPython_display +except ImportError: + IPython_display = None + +try: + from PIL import Image as PIL_Image +except ImportError: + PIL_Image = None + + +class Image: + """Image.""" + + _image_bytes: bytes + _loaded_image: Optional["PIL_Image.Image"] = None + + def __init__(self, image_bytes: bytes): + """Creates an `Image` object. + + Args: + image_bytes: Image file bytes. Image can be in PNG or JPEG format. + """ + self._image_bytes = image_bytes + + @staticmethod + def load_from_file(location: str) -> "Image": + """Loads image from file. + + Args: + location: Local path from where to load the image. + + Returns: + Loaded image as an `Image` object. + """ + image_bytes = pathlib.Path(location).read_bytes() + image = Image(image_bytes=image_bytes) + return image + + @property + def _pil_image(self) -> "PIL_Image.Image": + if self._loaded_image is None: + self._loaded_image = PIL_Image.open(io.BytesIO(self._image_bytes)) + return self._loaded_image + + @property + def _size(self): + return self._pil_image.size + + def show(self): + """Shows the image. + + This method only works when in a notebook environment. + """ + if PIL_Image and IPython_display: + IPython_display.display(self._pil_image) + + def save(self, location: str): + """Saves image to a file. + + Args: + location: Local path where to save the image. + """ + pathlib.Path(location).write_bytes(self._image_bytes) + + def _as_base64_string(self) -> str: + """Encodes image using the base64 encoding. + + Returns: + Base64 encoding of the image as a string. + """ + # ! b64encode returns `bytes` object, not ``str. + # We need to convert `bytes` to `str`, otherwise we get service error: + # "received initial metadata size exceeds limit" + return base64.b64encode(self._image_bytes).decode("ascii") + + +class ImageCaptioningModel( + _model_garden_models._ModelGardenModel # pylint: disable=protected-access +): + """Generates captions from image. + + Examples:: + + model = ImageCaptioningModel.from_pretrained("imagetext@001") + image = Image.load_from_file("image.png") + captions = model.get_captions( + image=image, + # Optional: + number_of_results=1, + language="en", + ) + """ + + _INSTANCE_SCHEMA_URI = "gs://google-cloud-aiplatform/schema/predict/instance/vision_reasoning_model_1.0.0.yaml" + _LAUNCH_STAGE = ( + _model_garden_models._SDK_GA_LAUNCH_STAGE # pylint: disable=protected-access + ) + + def get_captions( + self, + image: Image, + *, + number_of_results: int = 1, + language: str = "en", + ) -> List[str]: + """Generates captions for a given image. + + Args: + image: The image to get captions for. Size limit: 10 MB. + number_of_results: Number of captions to produce. Range: 1-3. + language: Language to use for captions. + Supported languages: "en", "fr", "de", "it", "es" + + Returns: + A list of image caption strings. + """ + instance = { + "image": { + "bytesBase64Encoded": image._as_base64_string() # pylint: disable=protected-access + } + } + parameters = { + "sampleCount": number_of_results, + "language": language, + } + response = self._endpoint.predict( + instances=[instance], + parameters=parameters, + ) + return response.predictions + + +class ImageQnAModel( + _model_garden_models._ModelGardenModel # pylint: disable=protected-access +): + """Answers questions about an image. + + Examples:: + + model = ImageQnAModel.from_pretrained("imagetext@001") + image = Image.load_from_file("image.png") + answers = model.ask_question( + image=image, + question="What color is the car in this image?", + # Optional: + number_of_results=1, + ) + """ + + _INSTANCE_SCHEMA_URI = "gs://google-cloud-aiplatform/schema/predict/instance/vision_reasoning_model_1.0.0.yaml" + _LAUNCH_STAGE = ( + _model_garden_models._SDK_GA_LAUNCH_STAGE # pylint: disable=protected-access + ) + + def ask_question( + self, + image: Image, + question: str, + *, + number_of_results: int = 1, + ) -> List[str]: + """Answers questions about an image. + + Args: + image: The image to get captions for. Size limit: 10 MB. + question: Question to ask about the image. + number_of_results: Number of captions to produce. Range: 1-3. + + Returns: + A list of answers. + """ + instance = { + "prompt": question, + "image": { + "bytesBase64Encoded": image._as_base64_string() # pylint: disable=protected-access + }, + } + parameters = { + "sampleCount": number_of_results, + } + response = self._endpoint.predict( + instances=[instance], + parameters=parameters, + ) + return response.predictions + + +class MultiModalEmbeddingModel(_model_garden_models._ModelGardenModel): + """Generates embedding vectors from images. + + Examples:: + + model = MultiModalEmbeddingModel.from_pretrained("multimodalembedding@001") + image = Image.load_from_file("image.png") + + embeddings = model.get_embeddings( + image=image, + contextual_text="Hello world", + ) + image_embedding = embeddings.image_embedding + text_embedding = embeddings.text_embedding + """ + + _INSTANCE_SCHEMA_URI = "gs://google-cloud-aiplatform/schema/predict/instance/vision_embedding_model_1.0.0.yaml" + + _LAUNCH_STAGE = ( + _model_garden_models._SDK_GA_LAUNCH_STAGE # pylint: disable=protected-access + ) + + def get_embeddings( + self, image: Image, contextual_text: Optional[str] = None + ) -> "MultiModalEmbeddingResponse": + """Gets embedding vectors from the provided image. + + Args: + image (Image): + The image to generate embeddings for. + contextual_text (str): + Optional. Contextual text for your input image. If provided, the model will also + generate an embedding vector for the provided contextual text. The returned image + and text embedding vectors are in the same semantic space with the same dimensionality, + and the vectors can be used interchangeably for use cases like searching image by text + or searching text by image. + + Returns: + ImageEmbeddingResponse: + The image and text embedding vectors. + """ + + instance = { + "image": {"bytesBase64Encoded": image._as_base64_string()}, + "features": [{"type": "IMAGE_EMBEDDING"}], + } + + if contextual_text: + instance["text"] = contextual_text + + response = self._endpoint.predict(instances=[instance]) + image_embedding = response.predictions[0].get("imageEmbedding") + text_embedding = ( + response.predictions[0].get("textEmbedding") + if "textEmbedding" in response.predictions[0] + else None + ) + return MultiModalEmbeddingResponse( + image_embedding=image_embedding, + _prediction_response=response, + text_embedding=text_embedding, + ) + + +@dataclasses.dataclass +class MultiModalEmbeddingResponse: + """The image embedding response. + + Attributes: + image_embedding (List[float]): + The emebedding vector generated from your image. + text_embedding (List[float]): + Optional. The embedding vector generated from the contextual text provided for your image. + """ + + image_embedding: List[float] + _prediction_response: Any + text_embedding: Optional[List[float]] = None