diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/__init__.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/__init__.py new file mode 100644 index 0000000000..29ea86d47d --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/__init__.py @@ -0,0 +1,8 @@ +from .model_garden import OpenAIFormatModelVersion, vertexai_name +from .modelgarden_plugin import VertexAIModelGarden + +__all__ = [ + OpenAIFormatModelVersion.__name__, + vertexai_name, + VertexAIModelGarden.__name__ +] \ No newline at end of file diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/model_garden.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/model_garden.py new file mode 100644 index 0000000000..a25aa696c0 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/model_garden.py @@ -0,0 +1,95 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 +import os + +from enum import StrEnum + + +from genkit.plugins.vertex_ai import constants as const +from genkit.core.typing import ( + ModelInfo, + Supports, +) +from genkit.veneer.registry import GenkitRegistry +from .openai_compatiblility import OpenAICompatibleModel, OpenAIConfig + + +def vertexai_name(name: str) -> str: + """Create a Vertex AI action name. + + Args: + name: Base name for the action. + + Returns: + The fully qualified Vertex AI action name. + """ + return f'vertexai/{name}' + +class OpenAIFormatModelVersion(StrEnum): + """Available versions of the llama model. + + This enum defines the available versions of the llama model that + can be used through Vertex AI. + """ + LLAMA_3_1 = 'llama-3.1' + LLAMA_3_2 = 'llama-3.2' + + +SUPPORTED_OPENAI_FORMAT_MODELS: dict[str, ModelInfo] = { + OpenAIFormatModelVersion.LLAMA_3_1: ModelInfo( + versions=['meta/llama3-405b-instruct-maas'], + label='llama-3.1', + supports=Supports( + multiturn=True, + media=False, + tools=True, + systemRole=True, + output=['text', 'json'] + ) + ), + OpenAIFormatModelVersion.LLAMA_3_2: ModelInfo( + versions=['meta/llama-3.2-90b-vision-instruct-maas'], + label='llama-3.2', + supports=Supports( + multiturn=True, + media=True, + tools=True, + systemRole=True, + output=['text', 'json'] + ) + ) +} + + +class ModelGarden: + @classmethod + def to_openai_compatible_model( + cls, + ai: GenkitRegistry, + model: str, + location: str, + project_id: str + ): + if model not in SUPPORTED_OPENAI_FORMAT_MODELS: + raise ValueError(f"Model '{model}' is not supported.") + model_version = SUPPORTED_OPENAI_FORMAT_MODELS[model].versions[0] + open_ai_compat = OpenAICompatibleModel( + model_version, + project_id, + location + ) + supports = SUPPORTED_OPENAI_FORMAT_MODELS[model].supports.model_dump() + + ai.define_model( + name=f'vertexai/{model}', + fn=open_ai_compat.generate, + config_schema=OpenAIConfig, + metadata={ + 'model': { + 'supports': supports + } + } + + ) + + diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/modelgarden_plugin.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/modelgarden_plugin.py new file mode 100644 index 0000000000..f9ef8800ef --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/modelgarden_plugin.py @@ -0,0 +1,75 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + + +"""ModelGarden API Compatible Plugin for Genkit.""" + +from pydantic import BaseModel, ConfigDict +from .model_garden import ( + SUPPORTED_OPENAI_FORMAT_MODELS, + ModelGarden +) +from genkit.veneer.plugin import Plugin +from genkit.veneer.registry import GenkitRegistry +from genkit.plugins.vertex_ai import constants as const +import pdb +import os +from pprint import pprint + + +class CommonPluginOptions(BaseModel): + model_config = ConfigDict(extra='forbid', populate_by_name=True) + + project_id: str | None = None + location: str | None = None + models: list[str] | None = None + + +def vertexai_name(name: str) -> str: + """Create a Vertex AI action name. + + Args: + name: Base name for the action. + + Returns: + The fully qualified Vertex AI action name. + """ + return f'vertexai/{name}' + +class VertexAIModelGarden(Plugin): + """Model Garden plugin for Genkit. + + This plugin provides integration with Google Cloud's Vertex AI platform, + enabling the use of Vertex AI models and services within the Genkit + framework. It handles initialization of the Model Garden client and + registration of model actions. + """ + + name = "modelgarden" + + def __init__(self, **kwargs): + """Initialize the plugin by registering actions with the registry.""" + self.plugin_options = CommonPluginOptions( + project_id=kwargs.get('project_id', os.getenv(const.GCLOUD_PROJECT)), + location=kwargs.get('location', const.DEFAULT_REGION), + models=kwargs.get('models') + ) + + def initialize(self, ai: GenkitRegistry) -> None: + """Handles actions for various openaicompatible models.""" + for model in self.plugin_options.models: + openai_model = next( + ( + key + for key, _ in SUPPORTED_OPENAI_FORMAT_MODELS.items() + if key == model + ), + None + ) + if openai_model: + ModelGarden.to_openai_compatible_model( + ai, + model=openai_model, + location=self.plugin_options.location, + project_id=self.plugin_options.project_id + ) diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/openai_compatiblility/__init__.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/openai_compatiblility/__init__.py new file mode 100644 index 0000000000..1b3280d287 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/openai_compatiblility/__init__.py @@ -0,0 +1,3 @@ +from .openai_compatibility import OpenAICompatibleModel + +__all__ = [OpenAICompatibleModel.__name__] \ No newline at end of file diff --git a/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/openai_compatiblility/openai_compatibility.py b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/openai_compatiblility/openai_compatibility.py new file mode 100644 index 0000000000..afcfbbed41 --- /dev/null +++ b/py/plugins/vertex-ai/src/genkit/plugins/vertex_ai/model_garden/openai_compatiblility/openai_compatibility.py @@ -0,0 +1,129 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +import pdb +from enum import StrEnum +from genkit.core.typing import ( + ModelInfo, + Supports, + GenerationCommonConfig, + GenerateRequest, + GenerateResponse, + Message, + Role, + TextPart, + GenerateResponseChunk +) +from genkit.core.action import ActionRunContext +from openai import OpenAI as OpenAIClient +from google.auth import default, transport +from typing import Annotated + +from pydantic import BaseModel, ConfigDict + +class ChatMessage(BaseModel): + model_config = ConfigDict(extra='forbid', populate_by_name=True) + + role: str + content: str + + +class OpenAIConfig(GenerationCommonConfig): + """Config for OpenAI model.""" + frequency_penalty: Annotated[float, range(-2, 2)] | None = None + logit_bias: dict[str, Annotated[float, range(-100, 100)]] | None = None + logprobs: bool | None = None + presence_penalty: Annotated[float, range(-2, 2)] | None = None + seed: int | None = None + top_logprobs: Annotated[int, range(0, 20)] | None = None + user: str | None = None + + +class ChatCompletionRole(StrEnum): + """Available roles supported by openai-compatible models.""" + USER = 'user' + ASSISTANT = 'assistant' + SYSTEM = 'system' + TOOL = 'tool' + + +class OpenAICompatibleModel: + "Handles openai compatible model support in model_garden""" + + def __init__(self, model: str, project_id: str, location: str): + self._model = model + self._client = self.client_factory(location, project_id) + + def client_factory(self, location: str, project_id: str) -> OpenAIClient: + """Initiates an openai compatible client object and return it.""" + if project_id: + credentials, _ = default() + else: + credentials, project_id = default() + + credentials.refresh(transport.requests.Request()) + base_url = f'https://{location}-aiplatform.googleapis.com/v1beta1/projects/{project_id}/locations/{location}/endpoints/openapi' + return OpenAIClient(api_key=credentials.token, base_url=base_url) + + + def to_openai_messages(self, messages: list[Message]) -> list[ChatMessage]: + if not messages: + raise ValueError('No messages provided in the request.') + return [ + ChatMessage( + role=OpenAICompatibleModel.to_openai_role(m.role.value), + content=''.join( + part.root.text + for part in m.content + if part.root.text is not None + ), + ) + for m in messages + ] + def generate( + self, request: GenerateRequest, ctx: ActionRunContext + ) -> GenerateResponse: + openai_config: dict = { + 'messages': self.to_openai_messages(request.messages), + 'model': self._model + } + if ctx.is_streaming: + openai_config['stream'] = True + stream = self._client.chat.completions.create(**openai_config) + for chunk in stream: + choice = chunk.choices[0] + if not choice.delta.content: + continue + + response_chunk = GenerateResponseChunk( + role=Role.MODEL, + index=choice.index, + content=[TextPart(text=choice.delta.content)], + ) + + ctx.send_chunk(response_chunk) + + else: + response = self._client.chat.completions.create(**openai_config) + return GenerateResponse( + request=request, + message=Message( + role=Role.MODEL, + content=[TextPart(text=response.choices[0].message.content)], + ), + ) + + @staticmethod + def to_openai_role(role: Role) -> ChatCompletionRole: + """Converts Role enum to corrosponding OpenAI Compatible role.""" + match role: + case Role.USER: + return ChatCompletionRole.USER + case Role.MODEL: + return ChatCompletionRole.ASSISTANT # "model" maps to "assistant" + case Role.SYSTEM: + return ChatCompletionRole.SYSTEM + case Role.TOOL: + return ChatCompletionRole.TOOL + case _: + raise ValueError(f"Role '{role}' doesn't map to an OpenAI role.") diff --git a/py/samples/model-garden/LICENSE b/py/samples/model-garden/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/samples/model-garden/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 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. diff --git a/py/samples/model-garden/README.md b/py/samples/model-garden/README.md new file mode 100644 index 0000000000..1cd735addf --- /dev/null +++ b/py/samples/model-garden/README.md @@ -0,0 +1,16 @@ +# Model Garden + +## Setup environment + +```bash +uv venv +source .venv/bin/activate +``` + +## Run the sample + +TODO + +```bash +genkit start -- uv run --directory py samples/model-garden/src/main.py +``` diff --git a/py/samples/model-garden/pyproject.toml b/py/samples/model-garden/pyproject.toml new file mode 100644 index 0000000000..49a63713d0 --- /dev/null +++ b/py/samples/model-garden/pyproject.toml @@ -0,0 +1,31 @@ +[project] +authors = [{ name = "Google" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "genkit", + "genkit-firebase-plugin", + "genkit-google-ai-plugin", + "genkit-google-cloud-plugin", + "genkit-ollama-plugin", + "genkit-vertex-ai-plugin", + "pydantic>=2.10.5", +] +description = "Model Garden sample" +license = { text = "Apache-2.0" } +name = "model-garden-example" +readme = "README.md" +requires-python = ">=3.12" +version = "0.1.0" diff --git a/py/samples/model-garden/src/main.py b/py/samples/model-garden/src/main.py new file mode 100644 index 0000000000..b4a8854f56 --- /dev/null +++ b/py/samples/model-garden/src/main.py @@ -0,0 +1,78 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +import pdb +import asyncio +from genkit.veneer.veneer import Genkit +from genkit.plugins.vertex_ai.model_garden import ( + OpenAIFormatModelVersion, + VertexAIModelGarden, + vertexai_name +) +from genkit.core.typing import ( + Role, + TextPart, + Message +) + +from openai import OpenAI +from google.auth import default, transport +# import google.auth.transport.requests + +# credentials, project_id = default() +# credentials.refresh(transport.requests.Request()) +# print(credentials) +# print(project_id) + + +# location = 'us-central1' +# base_url = f'https://{location}-aiplatform.googleapis.com/v1beta1/projects/{project_id}/locations/{location}/endpoints/openapi' + +# client = OpenAI( +# api_key=credentials.token, +# base_url=base_url +# ) + +# response = client.chat.completions.create( +# model="google/gemini-2.0-flash-001", +# messages=[{"role": "user", "content": "Why is the sky blue?"}], +# ) + +# print(response) + + +# pdb.set_trace() + +ai = Genkit( + plugins=[ + VertexAIModelGarden( + location='us-central1', + models=['llama-3.1'] + ) + ] +) +@ai.flow() +async def say_hi(name: str): + """Generate a greeting for the given name. + + Args: + name: The name of the person to greet. + + Returns: + The generated greeting response. + """ + return await ai.generate( + model=vertexai_name(OpenAIFormatModelVersion.LLAMA_3_1), + messages=[ + Message( + role=Role.USER, + content=[TextPart(text=f'Say hi to {name}')], + ), + ], + ) + +async def main() -> None: + print(await say_hi('John Doe')) + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file