diff --git a/py/plugins/anthropic/LICENSE b/py/plugins/anthropic/LICENSE new file mode 100644 index 0000000000..2205396735 --- /dev/null +++ b/py/plugins/anthropic/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/plugins/anthropic/README.md b/py/plugins/anthropic/README.md new file mode 100644 index 0000000000..d8c4455f8c --- /dev/null +++ b/py/plugins/anthropic/README.md @@ -0,0 +1,3 @@ +# Genkit Anthropic model provider Plugin + +This Genkit plugin provides a set of tools and utilities for working with Anthropic. \ No newline at end of file diff --git a/py/plugins/anthropic/pyproject.toml b/py/plugins/anthropic/pyproject.toml new file mode 100644 index 0000000000..e21fada3c8 --- /dev/null +++ b/py/plugins/anthropic/pyproject.toml @@ -0,0 +1,32 @@ +[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 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries", +] +dependencies = ["genkit", "anthropic>=0.40.0"] +description = "Genkit Anthropic Plugin" +license = { text = "Apache-2.0" } +name = "genkit-plugin-anthropic" +readme = "README.md" +requires-python = ">=3.10" +version = "0.4.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["src/genkit", "src/genkit/plugins"] diff --git a/py/plugins/anthropic/src/genkit/plugins/anthropic/__init__.py b/py/plugins/anthropic/src/genkit/plugins/anthropic/__init__.py new file mode 100644 index 0000000000..d117e483fc --- /dev/null +++ b/py/plugins/anthropic/src/genkit/plugins/anthropic/__init__.py @@ -0,0 +1,21 @@ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Anthropic plugin for Genkit.""" + +from genkit.plugins.anthropic.plugin import Anthropic, anthropic_name + +__all__ = ['Anthropic', 'anthropic_name'] diff --git a/py/plugins/anthropic/src/genkit/plugins/anthropic/model_info.py b/py/plugins/anthropic/src/genkit/plugins/anthropic/model_info.py new file mode 100644 index 0000000000..3e4a994fb8 --- /dev/null +++ b/py/plugins/anthropic/src/genkit/plugins/anthropic/model_info.py @@ -0,0 +1,135 @@ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Anthropic Models for Genkit.""" + +from genkit.types import ( + ModelInfo, + Supports, +) + +# Model definitions +CLAUDE_3_HAIKU = ModelInfo( + label='Anthropic - Claude 3 Haiku', + versions=['claude-3-haiku-20240307'], + supports=Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, + ), +) + +CLAUDE_3_5_HAIKU = ModelInfo( + label='Anthropic - Claude 3.5 Haiku', + versions=['claude-3-5-haiku-20241022'], + supports=Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, + ), +) + +CLAUDE_SONNET_4 = ModelInfo( + label='Anthropic - Claude Sonnet 4', + versions=['claude-sonnet-4-20250514'], + supports=Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, + ), +) + +CLAUDE_OPUS_4 = ModelInfo( + label='Anthropic - Claude Opus 4', + versions=['claude-opus-4-20250514'], + supports=Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, + ), +) + +CLAUDE_SONNET_4_5 = ModelInfo( + label='Anthropic - Claude Sonnet 4.5', + versions=['claude-sonnet-4-5-20250915'], + supports=Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, + ), +) + +CLAUDE_HAIKU_4_5 = ModelInfo( + label='Anthropic - Claude Haiku 4.5', + versions=['claude-haiku-4-5-20250915'], + supports=Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, + ), +) + +CLAUDE_OPUS_4_1 = ModelInfo( + label='Anthropic - Claude Opus 4.1', + versions=['claude-opus-4-1-20260115'], + supports=Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, + ), +) + +SUPPORTED_ANTHROPIC_MODELS: dict[str, ModelInfo] = { + 'claude-3-haiku': CLAUDE_3_HAIKU, + 'claude-3-5-haiku': CLAUDE_3_5_HAIKU, + 'claude-sonnet-4': CLAUDE_SONNET_4, + 'claude-opus-4': CLAUDE_OPUS_4, + 'claude-sonnet-4-5': CLAUDE_SONNET_4_5, + 'claude-haiku-4-5': CLAUDE_HAIKU_4_5, + 'claude-opus-4-1': CLAUDE_OPUS_4_1, +} + +DEFAULT_SUPPORTS = Supports( + multiturn=True, + media=True, + tools=True, + system_role=True, +) + + +def get_model_info(name: str) -> ModelInfo: + """Get model info for a given model name. + + Args: + name: Model name. + + Returns: + Model information. + """ + return SUPPORTED_ANTHROPIC_MODELS.get( + name, + ModelInfo( + label=f'Anthropic - {name}', + supports=DEFAULT_SUPPORTS, + ), + ) diff --git a/py/plugins/anthropic/src/genkit/plugins/anthropic/models.py b/py/plugins/anthropic/src/genkit/plugins/anthropic/models.py new file mode 100644 index 0000000000..e9c6185cba --- /dev/null +++ b/py/plugins/anthropic/src/genkit/plugins/anthropic/models.py @@ -0,0 +1,237 @@ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Anthropic model implementations.""" + +from typing import Any + +from anthropic import AsyncAnthropic +from genkit.ai import ActionRunContext +from genkit.blocks.model import get_basic_usage_stats +from genkit.plugins.anthropic.model_info import get_model_info +from genkit.types import ( + GenerateRequest, + GenerateResponse, + GenerateResponseChunk, + GenerationUsage, + MediaPart, + Message, + Part, + Role, + TextPart, + ToolRequestPart, + ToolResponsePart, +) + +DEFAULT_MAX_OUTPUT_TOKENS = 4096 + + +class AnthropicModel: + """Represents an Anthropic language model for use with Genkit. + + Encapsulates interaction logic for a specific Claude model + enabling its use within Genkit for generative tasks. + """ + + def __init__(self, model_name: str, client: AsyncAnthropic) -> None: + """Initialize Anthropic model. + + Sets up the client for communicating with the Anthropic API + and stores the model name. + + Args: + model_name: Name of the Anthropic model. + client: AsyncAnthropic client instance. + """ + model_info = get_model_info(model_name) + self.model_name = model_info.versions[0] if model_info.versions else model_name + self.client = client + + async def generate(self, request: GenerateRequest, ctx: ActionRunContext | None = None) -> GenerateResponse: + """Generate response from Anthropic. + + Args: + request: Generation request. + ctx: Action run context for streaming. + + Returns: + Generated response. + """ + params = self._build_params(request) + streaming = ctx and ctx.is_streaming + + if streaming: + response = await self._generate_streaming(params, ctx) + content = [] + else: + response = await self.client.messages.create(**params) + content = self._to_genkit_content(response.content) + + response_message = Message(role=Role.MODEL, content=content) + basic_usage = get_basic_usage_stats(input_=request.messages, response=response_message) + + finish_reason_map = { + 'end_turn': 'stop', + 'max_tokens': 'length', + 'stop_sequence': 'stop', + 'tool_use': 'stop', + } + finish_reason = finish_reason_map.get(response.stop_reason, 'unknown') + + return GenerateResponse( + message=response_message, + usage=GenerationUsage( + input_tokens=response.usage.input_tokens, + output_tokens=response.usage.output_tokens, + total_tokens=response.usage.input_tokens + response.usage.output_tokens, + input_characters=basic_usage.input_characters, + output_characters=basic_usage.output_characters, + input_images=basic_usage.input_images, + output_images=basic_usage.output_images, + ), + finish_reason=finish_reason, + ) + + def _build_params(self, request: GenerateRequest) -> dict[str, Any]: + """Build Anthropic API parameters.""" + config = request.config + if isinstance(config, dict): + max_tokens = config.get('max_output_tokens') or DEFAULT_MAX_OUTPUT_TOKENS + temperature = config.get('temperature') + top_p = config.get('top_p') + stop_sequences = config.get('stop_sequences') + else: + max_tokens = config.max_output_tokens if config and config.max_output_tokens else DEFAULT_MAX_OUTPUT_TOKENS + temperature = config.temperature if config else None + top_p = config.top_p if config else None + stop_sequences = config.stop_sequences if config else None + + params: dict[str, Any] = { + 'model': self.model_name, + 'messages': self._to_anthropic_messages(request.messages), + 'max_tokens': int(max_tokens), + } + + system = self._extract_system(request.messages) + if system: + params['system'] = system + if temperature is not None: + params['temperature'] = temperature + if top_p is not None: + params['top_p'] = top_p + if stop_sequences: + params['stop_sequences'] = stop_sequences + if request.tools: + params['tools'] = [ + { + 'name': t.name, + 'description': t.description, + 'input_schema': t.input_schema, + } + for t in request.tools + ] + + return params + + async def _generate_streaming(self, params: dict[str, Any], ctx: ActionRunContext): + """Handle streaming generation.""" + async with self.client.messages.stream(**params) as stream: + async for chunk in stream: + if chunk.type == 'content_block_delta' and hasattr(chunk.delta, 'text'): + ctx.send_chunk( + GenerateResponseChunk( + role=Role.MODEL, + index=0, + content=[TextPart(text=chunk.delta.text)], + ) + ) + return await stream.get_final_message() + + def _extract_system(self, messages: list[Message]) -> str | None: + """Extract system prompt from messages.""" + for msg in messages: + if msg.role == Role.SYSTEM: + texts = [] + for part in msg.content: + actual_part = part.root if isinstance(part, Part) else part + if isinstance(actual_part, TextPart): + texts.append(actual_part.text) + return ''.join(texts) if texts else None + return None + + def _to_anthropic_messages(self, messages: list[Message]) -> list[dict[str, Any]]: + """Convert Genkit messages to Anthropic format.""" + result = [] + for msg in messages: + if msg.role == Role.SYSTEM: + continue + role = 'assistant' if msg.role == Role.MODEL else 'user' + content = [] + for part in msg.content: + actual_part = part.root if isinstance(part, Part) else part + if isinstance(actual_part, TextPart): + content.append({'type': 'text', 'text': actual_part.text}) + elif isinstance(actual_part, MediaPart): + content.append(self._to_anthropic_media(actual_part)) + elif isinstance(actual_part, ToolRequestPart): + content.append({ + 'type': 'tool_use', + 'id': actual_part.tool_request.ref, + 'name': actual_part.tool_request.name, + 'input': actual_part.tool_request.input, + }) + elif isinstance(actual_part, ToolResponsePart): + content.append({ + 'type': 'tool_result', + 'tool_use_id': actual_part.tool_response.ref, + 'content': str(actual_part.tool_response.output), + }) + result.append({'role': role, 'content': content}) + return result + + def _to_anthropic_media(self, media_part: MediaPart) -> dict[str, Any]: + """Convert media part to Anthropic format.""" + url = media_part.media.url + if url.startswith('data:'): + _, base64_data = url.split(',', 1) + content_type = url.split(':')[1].split(';')[0] + return { + 'type': 'image', + 'source': { + 'type': 'base64', + 'media_type': content_type, + 'data': base64_data, + }, + } + return {'type': 'image', 'source': {'type': 'url', 'url': url}} + + def _to_genkit_content(self, content_blocks: list) -> list[Part]: + """Convert Anthropic response to Genkit format.""" + parts = [] + for block in content_blocks: + if block.type == 'text': + parts.append(TextPart(text=block.text)) + elif block.type == 'tool_use': + parts.append( + ToolRequestPart( + tool_request={ + 'ref': block.id, + 'name': block.name, + 'input': block.input, + } + ) + ) + return parts diff --git a/py/plugins/anthropic/src/genkit/plugins/anthropic/plugin.py b/py/plugins/anthropic/src/genkit/plugins/anthropic/plugin.py new file mode 100644 index 0000000000..9a71c6dd69 --- /dev/null +++ b/py/plugins/anthropic/src/genkit/plugins/anthropic/plugin.py @@ -0,0 +1,122 @@ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Anthropic plugin for Genkit.""" + +from anthropic import AsyncAnthropic +from genkit.ai import GenkitRegistry, Plugin +from genkit.core.registry import ActionKind +from genkit.plugins.anthropic.model_info import SUPPORTED_ANTHROPIC_MODELS, get_model_info +from genkit.plugins.anthropic.models import AnthropicModel +from genkit.types import GenerationCommonConfig + +ANTHROPIC_PLUGIN_NAME = 'anthropic' + + +def anthropic_name(name: str) -> str: + """Get Anthropic model name. + + Args: + name: The name of Anthropic model. + + Returns: + Fully qualified Anthropic model name. + """ + return f'{ANTHROPIC_PLUGIN_NAME}/{name}' + + +class Anthropic(Plugin): + """Anthropic plugin for Genkit. + + This plugin adds Anthropic models to Genkit for generative AI applications. + """ + + name = ANTHROPIC_PLUGIN_NAME + + def __init__( + self, + models: list[str] | None = None, + **anthropic_params: str, + ) -> None: + """Initializes Anthropic plugin with given configuration. + + Args: + models: List of model names to register. Defaults to all supported models. + **anthropic_params: Additional parameters passed to the AsyncAnthropic client. + This may include api_key, base_url, timeout, and other configuration + settings required by Anthropic's API. + """ + self.models = models or list(SUPPORTED_ANTHROPIC_MODELS.keys()) + self._anthropic_params = anthropic_params + self._anthropic_client = AsyncAnthropic(**anthropic_params) + + def initialize(self, ai: GenkitRegistry) -> None: + """Initialize plugin by registering models. + + Args: + ai: The AI registry to initialize the plugin with. + """ + for model_name in self.models: + self._define_model(ai, model_name) + + def resolve_action( + self, + ai: GenkitRegistry, + kind: ActionKind, + name: str, + ) -> None: + """Resolve an action. + + Args: + ai: Genkit registry. + kind: Action kind. + name: Action name. + """ + if kind == ActionKind.MODEL: + self._resolve_model(ai=ai, name=name) + + def _resolve_model(self, ai: GenkitRegistry, name: str) -> None: + """Resolve and define an Anthropic model. + + Args: + ai: Genkit registry. + name: Model name (may include plugin prefix). + """ + clean_name = name.replace(f'{ANTHROPIC_PLUGIN_NAME}/', '') if name.startswith(ANTHROPIC_PLUGIN_NAME) else name + self._define_model(ai, clean_name) + + def _define_model(self, ai: GenkitRegistry, model_name: str) -> None: + """Define and register a model. + + Args: + ai: Genkit registry. + model_name: Model name. + """ + model = AnthropicModel(model_name=model_name, client=self._anthropic_client) + model_info = get_model_info(model_name) + + metadata = { + 'model': { + 'supports': model_info.supports.model_dump(), + } + } + + ai.define_model( + name=anthropic_name(model_name), + fn=model.generate, + config_schema=GenerationCommonConfig, + metadata=metadata, + ) diff --git a/py/plugins/anthropic/src/genkit/py.typed b/py/plugins/anthropic/src/genkit/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/py/plugins/ollama/tests/__init__.py b/py/plugins/anthropic/tests/__init__.py similarity index 100% rename from py/plugins/ollama/tests/__init__.py rename to py/plugins/anthropic/tests/__init__.py diff --git a/py/plugins/anthropic/tests/test_models.py b/py/plugins/anthropic/tests/test_models.py new file mode 100644 index 0000000000..f346cdb336 --- /dev/null +++ b/py/plugins/anthropic/tests/test_models.py @@ -0,0 +1,250 @@ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Anthropic models.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from genkit.plugins.anthropic.models import AnthropicModel +from genkit.types import ( + GenerateRequest, + GenerateResponseChunk, + GenerationCommonConfig, + Message, + Part, + Role, + TextPart, + ToolDefinition, + ToolRequestPart, +) + + +def _create_sample_request() -> GenerateRequest: + """Create a sample generation request for testing.""" + return GenerateRequest( + messages=[ + Message( + role=Role.USER, + content=[TextPart(text='Hello, how are you?')], + ) + ], + config=GenerationCommonConfig(), + tools=[ + ToolDefinition( + name='get_weather', + description='Get weather for a location', + input_schema={ + 'type': 'object', + 'properties': {'location': {'type': 'string', 'description': 'Location name'}}, + 'required': ['location'], + }, + ) + ], + ) + + +@pytest.mark.asyncio +async def test_generate_basic(): + """Test basic generation.""" + sample_request = _create_sample_request() + + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.content = [MagicMock(type='text', text="Hello! I'm doing well.")] + mock_response.usage = MagicMock(input_tokens=10, output_tokens=15) + mock_response.stop_reason = 'end_turn' + + mock_client.messages.create = AsyncMock(return_value=mock_response) + + model = AnthropicModel(model_name='claude-sonnet-4', client=mock_client) + response = await model.generate(sample_request) + + assert len(response.message.content) == 1 + part = response.message.content[0] + actual_part = part.root if isinstance(part, Part) else part + assert isinstance(actual_part, TextPart) + assert actual_part.text == "Hello! I'm doing well." + assert response.usage.input_tokens == 10 + assert response.usage.output_tokens == 15 + assert response.finish_reason == 'stop' + + +@pytest.mark.asyncio +async def test_generate_with_tools(): + """Test generation with tool calls.""" + sample_request = _create_sample_request() + + mock_client = MagicMock() + mock_response = MagicMock() + mock_block = MagicMock() + mock_block.type = 'tool_use' + mock_block.id = 'tool_123' + mock_block.name = 'get_weather' + mock_block.input = {'location': 'Paris'} + mock_response.content = [mock_block] + mock_response.usage = MagicMock(input_tokens=20, output_tokens=10) + mock_response.stop_reason = 'tool_use' + + mock_client.messages.create = AsyncMock(return_value=mock_response) + + model = AnthropicModel(model_name='claude-sonnet-4', client=mock_client) + response = await model.generate(sample_request) + + assert len(response.message.content) == 1 + part = response.message.content[0] + actual_part = part.root if isinstance(part, Part) else part + assert isinstance(actual_part, ToolRequestPart) + assert actual_part.tool_request.name == 'get_weather' + assert actual_part.tool_request.ref == 'tool_123' + assert actual_part.tool_request.input == {'location': 'Paris'} + + +@pytest.mark.asyncio +async def test_generate_with_config(): + """Test generation with custom config.""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.content = [MagicMock(type='text', text='Response')] + mock_response.usage = MagicMock(input_tokens=5, output_tokens=5) + mock_response.stop_reason = 'end_turn' + + mock_client.messages.create = AsyncMock(return_value=mock_response) + + model = AnthropicModel(model_name='claude-sonnet-4', client=mock_client) + + request = GenerateRequest( + messages=[Message(role=Role.USER, content=[TextPart(text='Test')])], + config=GenerationCommonConfig( + temperature=0.7, + max_output_tokens=100, + top_p=0.9, + ), + ) + + await model.generate(request) + + call_args = mock_client.messages.create.call_args + assert call_args.kwargs['temperature'] == 0.7 + assert call_args.kwargs['max_tokens'] == 100 + assert call_args.kwargs['top_p'] == 0.9 + + +def test_extract_system(): + """Test system prompt extraction.""" + mock_client = MagicMock() + model = AnthropicModel(model_name='claude-sonnet-4', client=mock_client) + + messages = [ + Message(role=Role.SYSTEM, content=[TextPart(text='You are helpful.')]), + Message(role=Role.USER, content=[TextPart(text='Hello')]), + ] + + system = model._extract_system(messages) + assert system == 'You are helpful.' + + +def test_to_anthropic_messages(): + """Test message conversion.""" + mock_client = MagicMock() + model = AnthropicModel(model_name='claude-sonnet-4', client=mock_client) + + messages = [ + Message(role=Role.USER, content=[TextPart(text='Hello')]), + Message(role=Role.MODEL, content=[TextPart(text='Hi there')]), + ] + + anthropic_messages = model._to_anthropic_messages(messages) + + assert len(anthropic_messages) == 2 + assert anthropic_messages[0]['role'] == 'user' + assert anthropic_messages[0]['content'][0]['text'] == 'Hello' + assert anthropic_messages[1]['role'] == 'assistant' + assert anthropic_messages[1]['content'][0]['text'] == 'Hi there' + + +class MockStreamManager: + """Mock stream manager for testing streaming.""" + + def __init__(self, chunks): + self.chunks = chunks + self.final_message = MagicMock() + self.final_message.usage = MagicMock(input_tokens=10, output_tokens=20) + self.final_message.stop_reason = 'end_turn' + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + def __aiter__(self): + return self + + async def __anext__(self): + if not self.chunks: + raise StopAsyncIteration + return self.chunks.pop(0) + + async def get_final_message(self): + return self.final_message + + +@pytest.mark.asyncio +async def test_streaming_generation(): + """Test streaming generation.""" + sample_request = _create_sample_request() + + mock_client = MagicMock() + + chunks = [ + MagicMock(type='content_block_delta', delta=MagicMock(text='Hello')), + MagicMock(type='content_block_delta', delta=MagicMock(text=' world')), + MagicMock(type='content_block_delta', delta=MagicMock(text='!')), + ] + + mock_stream = MockStreamManager(chunks) + mock_client.messages.stream.return_value = mock_stream + + model = AnthropicModel(model_name='claude-sonnet-4', client=mock_client) + + ctx = MagicMock() + ctx.is_streaming = True + collected_chunks = [] + + def send_chunk(chunk: GenerateResponseChunk): + collected_chunks.append(chunk) + + ctx.send_chunk = send_chunk + + response = await model.generate(sample_request, ctx) + + assert len(collected_chunks) == 3 + chunk0_part = collected_chunks[0].content[0] + chunk0_actual = chunk0_part.root if isinstance(chunk0_part, Part) else chunk0_part + assert chunk0_actual.text == 'Hello' + + chunk1_part = collected_chunks[1].content[0] + chunk1_actual = chunk1_part.root if isinstance(chunk1_part, Part) else chunk1_part + assert chunk1_actual.text == ' world' + + chunk2_part = collected_chunks[2].content[0] + chunk2_actual = chunk2_part.root if isinstance(chunk2_part, Part) else chunk2_part + assert chunk2_actual.text == '!' + + assert response.usage.input_tokens == 10 + assert response.usage.output_tokens == 20 diff --git a/py/plugins/anthropic/tests/test_plugin.py b/py/plugins/anthropic/tests/test_plugin.py new file mode 100644 index 0000000000..f55247f4e8 --- /dev/null +++ b/py/plugins/anthropic/tests/test_plugin.py @@ -0,0 +1,147 @@ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Anthropic plugin.""" + +from unittest.mock import ANY, MagicMock, patch + +from genkit.core.registry import ActionKind +from genkit.plugins.anthropic import Anthropic, anthropic_name +from genkit.plugins.anthropic.model_info import ( + SUPPORTED_ANTHROPIC_MODELS as SUPPORTED_MODELS, + get_model_info, +) +from genkit.types import ( + GenerateRequest, + GenerationCommonConfig, + Message, + Role, + TextPart, + ToolDefinition, +) + + +def test_anthropic_name(): + """Test anthropic_name helper function.""" + assert anthropic_name('claude-sonnet-4') == 'anthropic/claude-sonnet-4' + + +def test_init_with_api_key(): + """Test plugin initialization with API key.""" + plugin = Anthropic(api_key='test-key') + assert plugin._anthropic_client.api_key == 'test-key' + assert plugin.models == list(SUPPORTED_MODELS.keys()) + + +def test_init_without_api_key_raises(): + """Test plugin initialization without API key uses default behavior.""" + with patch.dict('os.environ', {}, clear=True): + # AsyncAnthropic allows initialization without API key + # Error only occurs when making actual API calls + plugin = Anthropic() + assert plugin._anthropic_client is not None + + +def test_init_with_env_var(): + """Test plugin initialization with environment variable.""" + with patch.dict('os.environ', {'ANTHROPIC_API_KEY': 'env-key'}): + plugin = Anthropic() + assert plugin._anthropic_client.api_key == 'env-key' + + +def test_custom_models(): + """Test plugin initialization with custom models.""" + plugin = Anthropic(api_key='test-key', models=['claude-sonnet-4']) + assert plugin.models == ['claude-sonnet-4'] + + +def test_plugin_initialize(): + """Test plugin registry initialization.""" + registry = MagicMock() + plugin = Anthropic(api_key='test-key', models=['claude-sonnet-4']) + + plugin.initialize(registry) + + assert registry.define_model.call_count == 1 + registry.define_model.assert_called_once_with( + name='anthropic/claude-sonnet-4', + fn=ANY, + config_schema=ANY, + metadata=ANY, + ) + + +def test_resolve_action_model(): + """Test resolve_action for model.""" + registry = MagicMock() + plugin = Anthropic(api_key='test-key') + + plugin.resolve_action(registry, ActionKind.MODEL, 'claude-sonnet-4') + + registry.define_model.assert_called_once() + + +def test_supported_models(): + """Test that all supported models have proper metadata.""" + assert len(SUPPORTED_MODELS) == 7 + for _name, info in SUPPORTED_MODELS.items(): + assert info.label.startswith('Anthropic - ') + assert info.versions is not None + assert len(info.versions) > 0 + assert info.supports.multiturn is True + assert info.supports.tools is True + assert info.supports.media is True + assert info.supports.system_role is True + + +def test_get_model_info_known(): + """Test get_model_info returns correct info for known model.""" + info = get_model_info('claude-sonnet-4') + assert info.label == 'Anthropic - Claude Sonnet 4' + assert info.supports.multiturn is True + assert info.supports.tools is True + + +def test_get_model_info_unknown(): + """Test get_model_info returns default info for unknown model.""" + info = get_model_info('unknown-model') + assert info.label == 'Anthropic - unknown-model' + assert info.supports.multiturn is True + assert info.supports.tools is True + + +def _create_sample_request() -> GenerateRequest: + """Create a sample generation request for testing.""" + return GenerateRequest( + messages=[ + Message( + role=Role.USER, + content=[TextPart(text='Hello, how are you?')], + ) + ], + config=GenerationCommonConfig(), + tools=[ + ToolDefinition( + name='get_weather', + description='Get weather for a location', + input_schema={ + 'type': 'object', + 'properties': {'location': {'type': 'string', 'description': 'Location name'}}, + 'required': ['location'], + }, + ) + ], + ) diff --git a/py/pyproject.toml b/py/pyproject.toml index 40029f31b5..ce3eba680a 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "dotpromptz==0.1.3", "genkit", + "genkit-plugin-anthropic", "genkit-plugin-dev-local-vectorstore", "genkit-plugin-compat-oai", "genkit-plugin-firebase", @@ -99,6 +100,7 @@ default-groups = ["dev", "lint"] [tool.uv.sources] genkit = { workspace = true } +genkit-plugin-anthropic = { workspace = true } genkit-plugin-compat-oai = { workspace = true } genkit-plugin-dev-local-vectorstore = { workspace = true } genkit-plugin-firebase = { workspace = true } diff --git a/py/samples/anthropic-hello/README.md b/py/samples/anthropic-hello/README.md new file mode 100644 index 0000000000..85c390cb3f --- /dev/null +++ b/py/samples/anthropic-hello/README.md @@ -0,0 +1,19 @@ +## Anthropic Sample + +1. Setup environment and install dependencies: +```bash +uv venv +source .venv/bin/activate + +uv sync +``` + +2. Set Anthropic API key: +```bash +export ANTHROPIC_API_KEY=your-api-key +``` + +3. Run the sample: +```bash +genkit start -- uv run src/anthropic_hello.py +``` diff --git a/py/samples/anthropic-hello/pyproject.toml b/py/samples/anthropic-hello/pyproject.toml new file mode 100644 index 0000000000..0a923fe7e5 --- /dev/null +++ b/py/samples/anthropic-hello/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "anthropic-hello" +version = "0.1.0" +description = "Anthropic Hello Sample" +requires-python = ">=3.10" +dependencies = [ + "genkit", + "genkit-plugin-anthropic", + "pydantic>=2.0.0", + "structlog>=24.0.0", +] + +[tool.uv.sources] +genkit-plugin-anthropic = { workspace = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/py/samples/anthropic-hello/src/anthropic_hello.py b/py/samples/anthropic-hello/src/anthropic_hello.py new file mode 100644 index 0000000000..fc38c2f522 --- /dev/null +++ b/py/samples/anthropic-hello/src/anthropic_hello.py @@ -0,0 +1,213 @@ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Anthropic hello sample. + +Key features demonstrated in this sample: + +| Feature Description | Example Function / Code Snippet | +|-----------------------------------------|-------------------------------------| +| Plugin Initialization | `ai = Genkit(plugins=[Anthropic()])` | +| Default Model Configuration | `ai = Genkit(model=...)` | +| Defining Flows | `@ai.flow()` decorator | +| Defining Tools | `@ai.tool()` decorator | +| Pydantic for Tool Input Schema | `WeatherInput`, `CurrencyInput` | +| Simple Generation (Prompt String) | `say_hi` | +| Streaming Generation | `say_hi_stream` | +| Generation with Tools | `weather_flow`, `currency_exchange` | +| Generation Configuration (temperature) | `say_hi_with_config` | +""" + +import structlog +from pydantic import BaseModel, Field + +from genkit.ai import Genkit +from genkit.plugins.anthropic import Anthropic, anthropic_name +from genkit.types import ActionRunContext, GenerationCommonConfig + +logger = structlog.get_logger(__name__) + +ai = Genkit( + plugins=[Anthropic()], + model=anthropic_name('claude-3-5-haiku'), +) + + +class WeatherInput(BaseModel): + """Weather input schema.""" + + location: str = Field(description='Location to get weather for') + + +class CurrencyInput(BaseModel): + """Currency conversion input schema.""" + + amount: float = Field(description='Amount to convert') + from_currency: str = Field(description='Source currency code (e.g., USD)') + to_currency: str = Field(description='Target currency code (e.g., EUR)') + + +class CurrencyExchangeInput(BaseModel): + """Currency exchange flow input schema.""" + + amount: float = Field(description='Amount to convert') + from_curr: str = Field(description='Source currency code') + to_curr: str = Field(description='Target currency code') + + +@ai.tool() +def get_weather(input: WeatherInput) -> str: + """Get weather for a location. + + Args: + input: Weather input with location. + + Returns: + Weather information. + """ + return f'Weather in {input.location}: Sunny, 23°C' + + +@ai.tool() +def convert_currency(input: CurrencyInput) -> str: + """Convert currency amount. + + Args: + input: Currency conversion parameters. + + Returns: + Converted amount. + """ + # Mock conversion rates + rates = { + ('USD', 'EUR'): 0.85, + ('EUR', 'USD'): 1.18, + ('USD', 'GBP'): 0.73, + ('GBP', 'USD'): 1.37, + } + + rate = rates.get((input.from_currency, input.to_currency), 1.0) + converted = input.amount * rate + + return f'{input.amount} {input.from_currency} = {converted:.2f} {input.to_currency}' + + +@ai.flow() +async def say_hi(name: str) -> str: + """Generate a simple greeting. + + Args: + name: Name to greet. + + Returns: + Greeting message. + """ + response = await ai.generate( + prompt=f'Say hello to {name} in a friendly way', + ) + return response.text + + +@ai.flow() +async def say_hi_stream(topic: str, ctx: ActionRunContext) -> str: + """Generate streaming response. + + Args: + topic: Topic to write about. + ctx: Action run context for streaming. + + Returns: + Complete generated text. + """ + response = await ai.generate( + prompt=f'Write a short story about {topic}', + on_chunk=ctx.send_chunk, + ) + return response.text + + +@ai.flow() +async def weather_flow(location: str) -> str: + """Get weather using tools. + + Args: + location: Location to get weather for. + + Returns: + Weather information. + """ + response = await ai.generate( + prompt=f'What is the weather in {location}?', + tools=['get_weather'], + ) + return response.text + + +@ai.flow() +async def currency_exchange(input: CurrencyExchangeInput) -> str: + """Convert currency using tools. + + Args: + input: Currency exchange parameters. + + Returns: + Conversion result. + """ + response = await ai.generate( + prompt=f'Convert {input.amount} {input.from_curr} to {input.to_curr}', + tools=['convert_currency'], + ) + return response.text + + +@ai.flow() +async def say_hi_with_config(name: str) -> str: + """Generate greeting with custom configuration. + + Args: + name: Name to greet. + + Returns: + Greeting message. + """ + response = await ai.generate( + prompt=f'Say hello to {name}', + config=GenerationCommonConfig( + temperature=0.7, + max_output_tokens=100, + ), + ) + return response.text + + +async def main() -> None: + """Main entry point for the Anthropic sample.""" + + result = await say_hi('John Doe') + await logger.ainfo('Simple greeting', result=result) + + result = await say_hi_with_config('John Doe') + await logger.ainfo('Custom config', result=result) + + result = await weather_flow('Paris') + await logger.ainfo('Weather', result=result) + + result = await currency_exchange(CurrencyExchangeInput(amount=100.0, from_curr='USD', to_curr='EUR')) + await logger.ainfo('Currency', result=result) + + +if __name__ == '__main__': + ai.run_main(main()) diff --git a/py/uv.lock b/py/uv.lock index 3f3031cf0a..2cacda34ef 100644 --- a/py/uv.lock +++ b/py/uv.lock @@ -9,11 +9,13 @@ resolution-markers = [ [manifest] members = [ + "anthropic-hello", "compat-oai-hello", "dev-local-vectorstore-hello", "firestore-retreiver", "flask-hello", "genkit", + "genkit-plugin-anthropic", "genkit-plugin-compat-oai", "genkit-plugin-dev-local-vectorstore", "genkit-plugin-evaluators", @@ -68,6 +70,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675, upload-time = "2019-04-29T20:23:53.83Z" }, ] +[[package]] +name = "anthropic" +version = "0.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, +] + +[[package]] +name = "anthropic-hello" +version = "0.1.0" +source = { editable = "samples/anthropic-hello" } +dependencies = [ + { name = "genkit" }, + { name = "genkit-plugin-anthropic" }, + { name = "pydantic" }, + { name = "structlog" }, +] + +[package.metadata] +requires-dist = [ + { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-plugin-anthropic", editable = "plugins/anthropic" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "structlog", specifier = ">=24.0.0" }, +] + [[package]] name = "anyio" version = "4.9.0" @@ -1204,7 +1244,22 @@ requires-dist = [ { name = "structlog", specifier = ">=25.2.0" }, { name = "uvicorn", specifier = ">=0.34.0" }, ] -provides-extras = ["dev-local-vectorstore", "flask", "google-genai", "ollama", "openai", "google-cloud", "vertex-ai"] +provides-extras = ["dev-local-vectorstore", "flask", "google-cloud", "google-genai", "ollama", "openai", "vertex-ai"] + +[[package]] +name = "genkit-plugin-anthropic" +version = "0.4.0" +source = { editable = "plugins/anthropic" } +dependencies = [ + { name = "anthropic" }, + { name = "genkit" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", specifier = ">=0.40.0" }, + { name = "genkit", editable = "packages/genkit" }, +] [[package]] name = "genkit-plugin-compat-oai" @@ -1383,6 +1438,7 @@ source = { virtual = "." } dependencies = [ { name = "dotpromptz" }, { name = "genkit" }, + { name = "genkit-plugin-anthropic" }, { name = "genkit-plugin-compat-oai" }, { name = "genkit-plugin-dev-local-vectorstore" }, { name = "genkit-plugin-firebase" }, @@ -1424,6 +1480,7 @@ lint = [ requires-dist = [ { name = "dotpromptz", specifier = "==0.1.3" }, { name = "genkit", editable = "packages/genkit" }, + { name = "genkit-plugin-anthropic", editable = "plugins/anthropic" }, { name = "genkit-plugin-compat-oai", editable = "plugins/compat-oai" }, { name = "genkit-plugin-dev-local-vectorstore", editable = "plugins/dev-local-vectorstore" }, { name = "genkit-plugin-firebase", editable = "plugins/firebase" },