alnzng commented on code in PR #139: URL: https://github.com/apache/flink-agents/pull/139#discussion_r2322957166
########## python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py: ########## @@ -0,0 +1,245 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################# +import uuid +from typing import Any, Dict, List, Optional, Sequence + +from anthropic import Anthropic +from anthropic._types import NOT_GIVEN +from anthropic.types import MessageParam, TextBlockParam, ToolParam +from pydantic import Field, PrivateAttr + +from flink_agents.api.chat_message import ChatMessage, MessageRole +from flink_agents.api.chat_models.chat_model import ( + BaseChatModelConnection, + BaseChatModelSetup, +) +from flink_agents.api.tools.tool import BaseTool, ToolMetadata + + +def to_anthropic_tool(*, metadata: ToolMetadata, skip_length_check: bool = False) -> ToolParam: + """Convert to Anthropic tool: https://docs.anthropic.com/en/api/messages#body-tools.""" + if not skip_length_check and len(metadata.description) > 1024: + msg = ( + "Tool description exceeds maximum length of 1024 characters. " + "Please shorten your description or move it to the prompt." + ) + raise ValueError(msg) + return { + "name": metadata.name, + "description": metadata.description, + "input_schema": metadata.get_parameters_dict() + } + + +def convert_to_anthropic_message(message: ChatMessage) -> MessageParam: + """Convert ChatMessage to Anthropic MessageParam format.""" + if message.role == MessageRole.TOOL: + return { + "role": MessageRole.USER.value, + "content": [ + { + "type": "tool_result", + "tool_use_id": message.extra_args.get("external_id"), + "content": message.content, + } + ], + } + else: + return { + "role": message.role.value, + "content": message.content, + } + + +def convert_to_anthropic_messages(messages: Sequence[ChatMessage]) -> List[MessageParam]: + """Convert user/assistant messages to Anthropic input messages. + + See: https://docs.anthropic.com/en/api/messages#body-messages + """ + return [convert_to_anthropic_message(message) for message in messages if + message.role in [MessageRole.USER, MessageRole.ASSISTANT, MessageRole.TOOL]] + + +def convert_to_anthropic_system_prompts(messages: Sequence[ChatMessage]) -> List[TextBlockParam]: + """Convert system messages to Anthropic system prompts. + + See: https://docs.anthropic.com/en/api/messages#body-system + """ + system_messages = [message for message in messages if message.role == MessageRole.SYSTEM] + return [ + TextBlockParam( + type="text", + text=message.content + ) + for message in system_messages + ] + + +class AnthropicChatModelConnection(BaseChatModelConnection): + """Manages the connection to the Anthropic AI models for chat interactions. + + Attributes: + ---------- + api_key : str + The Anthropic API key. + max_retries : int + The number of times to retry the API call upon failure. + timeout : float + The number of seconds to wait for an API call before it times out. + reuse_client : bool + Whether to reuse the Anthropic client between requests. + """ + + api_key: str = Field(default=None, description="The Anthropic API key.") + + max_retries: int = Field( + default=3, + description="The number of times to retry the API call upon failure.", + ge=0, + ) + timeout: float = Field( + default=60.0, + description="The number of seconds to wait for an API call before it times out.", + ge=0, + ) + + def __init__( + self, + api_key: Optional[str] = None, + max_retries: int = 3, + timeout: float = 60.0, + **kwargs: Any, + ) -> None: + """Initialize the Anthropic chat model connection.""" + super().__init__( + api_key=api_key, + max_retries=max_retries, + timeout=timeout, + **kwargs, + ) + + _client: Optional[Anthropic] = PrivateAttr(default=None) + + @property + def client(self) -> Anthropic: + """Get or create the Anthropic client instance.""" + if self._client is None: + self._client = Anthropic(api_key=self.api_key, max_retries=self.max_retries, timeout=self.timeout) + return self._client + + def chat(self, messages: Sequence[ChatMessage], tools: Optional[List[BaseTool]] = None, + **kwargs: Any) -> ChatMessage: + """Direct communication with Anthropic model service for chat conversation.""" + anthropic_tools = None + if tools is not None: + anthropic_tools = [to_anthropic_tool(metadata=tool.metadata) for tool in tools] + + anthropic_system = convert_to_anthropic_system_prompts(messages) + anthropic_messages = convert_to_anthropic_messages(messages) + + message = self.client.messages.create( + messages=anthropic_messages, + tools=anthropic_tools or NOT_GIVEN, + system=anthropic_system or NOT_GIVEN, + **kwargs, + ) + + if message.stop_reason == "tool_use": + tool_calls = [ + { + "id": uuid.uuid4(), + "type": "function", + "function": { + "name": content_block.name, + "arguments": content_block.input, + }, + "original_id": content_block.id, + } + for content_block in message.content + if content_block.type == 'tool_use' + ] + + return ChatMessage( + role=MessageRole(message.role), + content=message.content, Review Comment: Yes, we need the previous content blocks as context. This implementation follows Anthropic official SDK example: https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/tools.py#L39 IIUC, This is one of few major differences between Anthropic SDK and other SDKs(like OpenAI SDK). It requires the client pass the previous full context, otherwise it throws runtime errors like below example: ``` anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.2.content.0: unexpected `tool_use_id` found in `tool_result` blocks: toolu_01RPcDHy7vKQpAYgzL8Cd5Dr. Each `tool_result` block must have a corresponding `tool_use` block in the previous message.'}, 'request_id': 'req_011CSomfwQM8agx2hSbivysG'} ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
