|
| 1 | +# SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai> |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +import os |
| 6 | +from collections.abc import Callable |
| 7 | +from typing import Any, ClassVar |
| 8 | + |
| 9 | +from haystack import component, default_from_dict, default_to_dict, logging |
| 10 | +from haystack.dataclasses import ChatMessage, StreamingChunk |
| 11 | +from haystack.dataclasses.streaming_chunk import StreamingCallbackT |
| 12 | +from haystack.tools import ( |
| 13 | + ToolsType, |
| 14 | + _check_duplicate_tool_names, |
| 15 | + deserialize_tools_or_toolset_inplace, |
| 16 | + flatten_tools_or_toolsets, |
| 17 | + serialize_tools_or_toolset, |
| 18 | +) |
| 19 | +from haystack.utils import ( |
| 20 | + Secret, |
| 21 | + deserialize_callable, |
| 22 | + deserialize_secrets_inplace, |
| 23 | + serialize_callable, |
| 24 | +) |
| 25 | + |
| 26 | +from anthropic import AnthropicFoundry, AsyncAnthropicFoundry |
| 27 | + |
| 28 | +from .chat_generator import AnthropicChatGenerator |
| 29 | + |
| 30 | +logger = logging.getLogger(__name__) |
| 31 | + |
| 32 | + |
| 33 | +@component |
| 34 | +class AnthropicFoundryChatGenerator(AnthropicChatGenerator): |
| 35 | + """ |
| 36 | + Enables text generation using Anthropic's Claude models via Azure Foundry. |
| 37 | +
|
| 38 | + A variety of Claude models (Opus, Sonnet, Haiku, and others) are available through Azure Foundry. |
| 39 | +
|
| 40 | + To use AnthropicFoundryChatGenerator, you must have an Azure subscription with Foundry enabled |
| 41 | + and the desired Anthropic model deployed in your Foundry resource. |
| 42 | +
|
| 43 | + For more details, refer to the [Anthropic Foundry documentation](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/lib/foundry.md). |
| 44 | +
|
| 45 | + Any valid text generation parameters for the Anthropic messaging API can be passed to |
| 46 | + the AnthropicFoundry API. Users can provide these parameters directly to the component via |
| 47 | + the `generation_kwargs` parameter in `__init__` or the `run` method. |
| 48 | +
|
| 49 | + For more details on the parameters supported by the Anthropic API, refer to the |
| 50 | + Anthropic Message API [documentation](https://docs.anthropic.com/en/api/messages). |
| 51 | +
|
| 52 | + ```python |
| 53 | + from haystack_integrations.components.generators.anthropic import AnthropicFoundryChatGenerator |
| 54 | + from haystack.dataclasses import ChatMessage |
| 55 | + from haystack.utils import Secret |
| 56 | +
|
| 57 | + messages = [ChatMessage.from_user("What's Natural Language Processing?")] |
| 58 | +
|
| 59 | + client = AnthropicFoundryChatGenerator( |
| 60 | + model="claude-sonnet-4-5", |
| 61 | + api_key=Secret.from_env_var("ANTHROPIC_FOUNDRY_API_KEY"), |
| 62 | + resource="my-resource", |
| 63 | + ) |
| 64 | +
|
| 65 | + response = client.run(messages) |
| 66 | + print(response) |
| 67 | + >> {'replies': [ChatMessage(_role=<ChatRole.ASSISTANT: 'assistant'>, _content=[TextContent(text= |
| 68 | + >> "Natural Language Processing (NLP) is a field of artificial intelligence that |
| 69 | + >> focuses on enabling computers to understand, interpret, and generate human language. It involves developing |
| 70 | + >> techniques and algorithms to analyze and process text or speech data, allowing machines to comprehend and |
| 71 | + >> communicate in natural languages like English, Spanish, or Chinese.")], |
| 72 | + >> _name=None, _meta={'model': 'claude-sonnet-4-5', 'index': 0, 'finish_reason': 'end_turn', |
| 73 | + >> 'usage': {'input_tokens': 15, 'output_tokens': 64}})]} |
| 74 | + ``` |
| 75 | +
|
| 76 | + For more details on supported models and their capabilities, refer to the Anthropic |
| 77 | + [documentation](https://docs.anthropic.com/claude/docs/intro-to-claude). |
| 78 | + """ |
| 79 | + |
| 80 | + SUPPORTED_MODELS: ClassVar[list[str]] = [ |
| 81 | + "claude-opus-4-6", |
| 82 | + "claude-sonnet-4-6", |
| 83 | + "claude-sonnet-4-5", |
| 84 | + "claude-opus-4-5", |
| 85 | + "claude-opus-4-1", |
| 86 | + "claude-haiku-4-5", |
| 87 | + ] |
| 88 | + """A non-exhaustive list of chat models supported by this component. |
| 89 | + The actual availability depends on your Azure Foundry resource configuration.""" |
| 90 | + |
| 91 | + def __init__( |
| 92 | + self, |
| 93 | + *, |
| 94 | + api_key: Secret | None = Secret.from_env_var("ANTHROPIC_FOUNDRY_API_KEY", strict=True), # noqa: B008 |
| 95 | + resource: str | None = None, |
| 96 | + endpoint: str | None = None, |
| 97 | + model: str = "claude-sonnet-4-5", |
| 98 | + streaming_callback: Callable[[StreamingChunk], None] | None = None, |
| 99 | + generation_kwargs: dict[str, Any] | None = None, |
| 100 | + ignore_tools_thinking_messages: bool = True, |
| 101 | + tools: ToolsType | None = None, |
| 102 | + timeout: float | None = None, |
| 103 | + max_retries: int | None = None, |
| 104 | + azure_ad_token_provider: Callable[[], str] | None = None, |
| 105 | + ) -> None: |
| 106 | + """ |
| 107 | + Creates an instance of AnthropicFoundryChatGenerator. |
| 108 | +
|
| 109 | + :param api_key: The API key to use for authentication. |
| 110 | + Defaults to the `ANTHROPIC_FOUNDRY_API_KEY` environment variable. |
| 111 | + Can be `None` when using `azure_ad_token_provider` instead. |
| 112 | + :param resource: The Foundry resource name. Can also be set via the `ANTHROPIC_FOUNDRY_RESOURCE` |
| 113 | + environment variable. Either `resource` or `endpoint` must be provided. |
| 114 | + :param endpoint: The full Foundry endpoint URL (e.g., |
| 115 | + "https://your-resource.openai.azure.com/anthropic"). |
| 116 | + Either `resource` or `endpoint` must be provided. |
| 117 | + :param model: The name of the model to use (deployment name in Foundry). |
| 118 | + :param streaming_callback: A callback function that is called when a new token is received from the stream. |
| 119 | + The callback function accepts StreamingChunk as an argument. |
| 120 | + :param generation_kwargs: Other parameters to use for the model. These parameters are all sent directly to |
| 121 | + the AnthropicFoundry endpoint. See Anthropic [documentation](https://docs.anthropic.com/claude/reference/messages_post) |
| 122 | + for more details. |
| 123 | + Supported generation_kwargs parameters are: |
| 124 | + - `system`: The system message to be passed to the model. |
| 125 | + - `max_tokens`: The maximum number of tokens to generate. |
| 126 | + - `metadata`: A dictionary of metadata to be passed to the model. |
| 127 | + - `stop_sequences`: A list of strings that the model should stop generating at. |
| 128 | + - `temperature`: The temperature to use for sampling. |
| 129 | + - `top_p`: The top_p value to use for nucleus sampling. |
| 130 | + - `top_k`: The top_k value to use for top-k sampling. |
| 131 | + - `extra_headers`: A dictionary of extra headers to be passed to the model (i.e. for beta features). |
| 132 | + :param ignore_tools_thinking_messages: Anthropic's approach to tools (function calling) resolution involves a |
| 133 | + "chain of thought" messages before returning the actual function names and parameters in a message. If |
| 134 | + `ignore_tools_thinking_messages` is `True`, the generator will drop so-called thinking messages when tool |
| 135 | + use is detected. See the Anthropic [tools](https://docs.anthropic.com/en/docs/tool-use#chain-of-thought-tool-use) |
| 136 | + for more details. |
| 137 | + :param tools: A list of Tool and/or Toolset objects, or a single Toolset, that the model can use. |
| 138 | + Each tool should have a unique name. |
| 139 | + :param timeout: |
| 140 | + Timeout for Anthropic client calls. If not set, it defaults to the default set by the Anthropic client. |
| 141 | + :param max_retries: |
| 142 | + Maximum number of retries to attempt for failed requests. If not set, it defaults to the default set by |
| 143 | + the Anthropic client. |
| 144 | + :param azure_ad_token_provider: A function that returns an Azure AD token for authentication. |
| 145 | + Can be used instead of `api_key` for enhanced security. |
| 146 | + See [Azure Identity documentation](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication/overview) |
| 147 | + for more details. |
| 148 | + """ |
| 149 | + if api_key is None and azure_ad_token_provider is None: |
| 150 | + msg = "Please provide an API key or an azure_ad_token_provider." |
| 151 | + raise ValueError(msg) |
| 152 | + |
| 153 | + _check_duplicate_tool_names(flatten_tools_or_toolsets(tools)) |
| 154 | + |
| 155 | + self.api_key: Secret | None = api_key # type: ignore[assignment] |
| 156 | + self.resource = resource or os.environ.get("ANTHROPIC_FOUNDRY_RESOURCE") |
| 157 | + self.endpoint = endpoint |
| 158 | + self.model = model |
| 159 | + self.generation_kwargs = generation_kwargs or {} |
| 160 | + self.streaming_callback = streaming_callback |
| 161 | + self.ignore_tools_thinking_messages = ignore_tools_thinking_messages |
| 162 | + self.tools = tools |
| 163 | + self.timeout = timeout |
| 164 | + self.max_retries = max_retries |
| 165 | + self.azure_ad_token_provider = azure_ad_token_provider |
| 166 | + |
| 167 | + if not self.resource and not self.endpoint: |
| 168 | + msg = ( |
| 169 | + "Either 'resource' or 'endpoint' must be provided. " |
| 170 | + "Set ANTHROPIC_FOUNDRY_RESOURCE environment variable or pass resource/endpoint parameter." |
| 171 | + ) |
| 172 | + raise ValueError(msg) |
| 173 | + |
| 174 | + # Clients are created lazily in warm_up() |
| 175 | + self.client = None # type: ignore[assignment] |
| 176 | + self.async_client = None # type: ignore[assignment] |
| 177 | + self._is_warmed_up = False |
| 178 | + |
| 179 | + def warm_up(self) -> None: |
| 180 | + """ |
| 181 | + Create the AnthropicFoundry clients. |
| 182 | +
|
| 183 | + This method is idempotent — it only creates clients once. |
| 184 | + """ |
| 185 | + if self._is_warmed_up: |
| 186 | + return |
| 187 | + |
| 188 | + client_kwargs: dict[str, Any] = {} |
| 189 | + |
| 190 | + if self.azure_ad_token_provider: |
| 191 | + client_kwargs["azure_ad_token_provider"] = self.azure_ad_token_provider |
| 192 | + else: |
| 193 | + client_kwargs["api_key"] = self.api_key.resolve_value() # type: ignore[union-attr] |
| 194 | + |
| 195 | + if self.endpoint: |
| 196 | + client_kwargs["base_url"] = self.endpoint |
| 197 | + else: |
| 198 | + client_kwargs["resource"] = self.resource |
| 199 | + |
| 200 | + if self.timeout is not None: |
| 201 | + client_kwargs["timeout"] = self.timeout |
| 202 | + |
| 203 | + if self.max_retries is not None: |
| 204 | + client_kwargs["max_retries"] = self.max_retries |
| 205 | + |
| 206 | + self.client = AnthropicFoundry(**client_kwargs) # type: ignore[assignment] |
| 207 | + self.async_client = AsyncAnthropicFoundry(**client_kwargs) # type: ignore[assignment] |
| 208 | + self._is_warmed_up = True |
| 209 | + |
| 210 | + @component.output_types(replies=list[ChatMessage]) |
| 211 | + def run( |
| 212 | + self, |
| 213 | + messages: list[ChatMessage], |
| 214 | + streaming_callback: StreamingCallbackT | None = None, |
| 215 | + generation_kwargs: dict[str, Any] | None = None, |
| 216 | + tools: ToolsType | None = None, |
| 217 | + ) -> dict[str, list[ChatMessage]]: |
| 218 | + """ |
| 219 | + Invokes the AnthropicFoundry API with the given messages and generation kwargs. |
| 220 | +
|
| 221 | + :param messages: A list of ChatMessage instances representing the input messages. |
| 222 | + :param streaming_callback: A callback function that is called when a new token is received from the stream. |
| 223 | + :param generation_kwargs: Optional arguments to pass to the Anthropic generation endpoint. |
| 224 | + :param tools: A list of Tool and/or Toolset objects, or a single Toolset, that the model can use. |
| 225 | + Each tool should have a unique name. If set, it will override the `tools` parameter set during component |
| 226 | + initialization. |
| 227 | + :returns: A dictionary with the following keys: |
| 228 | + - `replies`: The responses from the model |
| 229 | + """ |
| 230 | + if not self._is_warmed_up: |
| 231 | + self.warm_up() |
| 232 | + return super(AnthropicFoundryChatGenerator, self).run( # noqa: UP008 |
| 233 | + messages=messages, streaming_callback=streaming_callback, generation_kwargs=generation_kwargs, tools=tools |
| 234 | + ) |
| 235 | + |
| 236 | + @component.output_types(replies=list[ChatMessage]) |
| 237 | + async def run_async( |
| 238 | + self, |
| 239 | + messages: list[ChatMessage], |
| 240 | + streaming_callback: StreamingCallbackT | None = None, |
| 241 | + generation_kwargs: dict[str, Any] | None = None, |
| 242 | + tools: ToolsType | None = None, |
| 243 | + ) -> dict[str, list[ChatMessage]]: |
| 244 | + """ |
| 245 | + Async version of the run method. Invokes the AnthropicFoundry API with the given messages and generation kwargs. |
| 246 | +
|
| 247 | + :param messages: A list of ChatMessage instances representing the input messages. |
| 248 | + :param streaming_callback: A callback function that is called when a new token is received from the stream. |
| 249 | + :param generation_kwargs: Optional arguments to pass to the Anthropic generation endpoint. |
| 250 | + :param tools: A list of Tool and/or Toolset objects, or a single Toolset, that the model can use. |
| 251 | + Each tool should have a unique name. If set, it will override the `tools` parameter set during component |
| 252 | + initialization. |
| 253 | + :returns: A dictionary with the following keys: |
| 254 | + - `replies`: The responses from the model |
| 255 | + """ |
| 256 | + if not self._is_warmed_up: |
| 257 | + self.warm_up() |
| 258 | + return await super(AnthropicFoundryChatGenerator, self).run_async( # noqa: UP008 |
| 259 | + messages=messages, streaming_callback=streaming_callback, generation_kwargs=generation_kwargs, tools=tools |
| 260 | + ) |
| 261 | + |
| 262 | + def to_dict(self) -> dict[str, Any]: |
| 263 | + """ |
| 264 | + Serialize this component to a dictionary. |
| 265 | +
|
| 266 | + :returns: |
| 267 | + The serialized component as a dictionary. |
| 268 | + """ |
| 269 | + callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None |
| 270 | + azure_ad_token_provider_name = ( |
| 271 | + serialize_callable(self.azure_ad_token_provider) if self.azure_ad_token_provider else None |
| 272 | + ) |
| 273 | + return default_to_dict( |
| 274 | + self, |
| 275 | + api_key=self.api_key.to_dict() if self.api_key else None, |
| 276 | + resource=self.resource, |
| 277 | + endpoint=self.endpoint, |
| 278 | + model=self.model, |
| 279 | + streaming_callback=callback_name, |
| 280 | + generation_kwargs=self.generation_kwargs, |
| 281 | + ignore_tools_thinking_messages=self.ignore_tools_thinking_messages, |
| 282 | + tools=serialize_tools_or_toolset(self.tools), |
| 283 | + timeout=self.timeout, |
| 284 | + max_retries=self.max_retries, |
| 285 | + azure_ad_token_provider=azure_ad_token_provider_name, |
| 286 | + ) |
| 287 | + |
| 288 | + @classmethod |
| 289 | + def from_dict(cls, data: dict[str, Any]) -> "AnthropicFoundryChatGenerator": |
| 290 | + """ |
| 291 | + Deserialize this component from a dictionary. |
| 292 | +
|
| 293 | + :param data: The dictionary representation of this component. |
| 294 | + :returns: |
| 295 | + The deserialized component instance. |
| 296 | + """ |
| 297 | + deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools") |
| 298 | + deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"]) |
| 299 | + |
| 300 | + init_params = data.get("init_parameters", {}) |
| 301 | + if serialized_callback := init_params.get("streaming_callback"): |
| 302 | + data["init_parameters"]["streaming_callback"] = deserialize_callable(serialized_callback) |
| 303 | + if serialized_token_provider := init_params.get("azure_ad_token_provider"): |
| 304 | + data["init_parameters"]["azure_ad_token_provider"] = deserialize_callable(serialized_token_provider) |
| 305 | + |
| 306 | + return default_from_dict(cls, data) |
0 commit comments