This is an automated email from the ASF dual-hosted git repository.
xtsong pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 25ea18f [integration][python] Anthropic Chat Model APIs Integration
(#139)
25ea18f is described below
commit 25ea18f6d684c8f65ef991cd67ad290ccd531961
Author: Alan Z. <[email protected]>
AuthorDate: Fri Sep 5 00:03:06 2025 -0700
[integration][python] Anthropic Chat Model APIs Integration (#139)
---
python/flink_agents/api/events/tool_event.py | 6 +-
.../chat_models/anthropic/__init__.py} | 33 ---
.../chat_models/anthropic/anthropic_chat_model.py | 257 +++++++++++++++++++++
.../chat_models/anthropic/tests/__init__.py} | 33 ---
.../anthropic/tests/test_anthropic_chat_model.py | 98 ++++++++
.../flink_agents/plan/actions/chat_model_action.py | 7 +-
python/pyproject.toml | 3 +-
7 files changed, 368 insertions(+), 69 deletions(-)
diff --git a/python/flink_agents/api/events/tool_event.py
b/python/flink_agents/api/events/tool_event.py
index a5e5c47..6bda919 100644
--- a/python/flink_agents/api/events/tool_event.py
+++ b/python/flink_agents/api/events/tool_event.py
@@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#################################################################################
-from typing import Any
+from typing import Any, Optional
from flink_agents.api.events.event import Event
@@ -29,10 +29,14 @@ class ToolRequestEvent(Event):
The name of the tool to be called.
kwargs : dict
The arguments passed to the tool.
+ external_id : Optional[str]
+ Optional identifier for storing original tool call IDs from external
systems
+ (e.g., Anthropic tool_use_id).
"""
tool: str
kwargs: dict
+ external_id: Optional[str] = None
class ToolResponseEvent(Event):
diff --git a/python/flink_agents/api/events/tool_event.py
b/python/flink_agents/integrations/chat_models/anthropic/__init__.py
similarity index 59%
copy from python/flink_agents/api/events/tool_event.py
copy to python/flink_agents/integrations/chat_models/anthropic/__init__.py
index a5e5c47..e154fad 100644
--- a/python/flink_agents/api/events/tool_event.py
+++ b/python/flink_agents/integrations/chat_models/anthropic/__init__.py
@@ -15,36 +15,3 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#################################################################################
-from typing import Any
-
-from flink_agents.api.events.event import Event
-
-
-class ToolRequestEvent(Event):
- """Event representing a tool call request.
-
- Attributes:
- ----------
- tool : str
- The name of the tool to be called.
- kwargs : dict
- The arguments passed to the tool.
- """
-
- tool: str
- kwargs: dict
-
-
-class ToolResponseEvent(Event):
- """Event representing a result from tool call.
-
- Attributes:
- ----------
- request : ToolRequestEvent
- The correspond request of the response.
- response : Any
- The response from the tool.
- """
-
- request: ToolRequestEvent
- response: Any
diff --git
a/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
b/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
new file mode 100644
index 0000000..061d213
--- /dev/null
+++
b/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
@@ -0,0 +1,257 @@
+################################################################################
+# 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,
+ }
+ ],
+ }
+ elif message.role == MessageRole.ASSISTANT:
+ # Use original Anthropic content blocks if available for context
+ anthropic_content_blocks =
message.extra_args.get("anthropic_content_blocks")
+ content = anthropic_content_blocks if anthropic_content_blocks is not
None else message.content
+ return {
+ "role": message.role.value,
+ "content": content, # type: ignore
+ }
+ 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[0].text,
+ tool_calls=tool_calls,
+ extra_args={
+ "anthropic_content_blocks": message.content
+ }
+
+ )
+ else:
+ # TODO: handle other stop_reason values according to Anthropic API:
+ # https://docs.anthropic.com/en/api/messages#response-stop-reason
+ return ChatMessage(
+ role=MessageRole(message.role),
+ content=message.content[0].text,
+ )
+
+
+DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514"
+DEFAULT_MAX_TOKENS = 1024
+DEFAULT_TEMPERATURE = 0.1
+
+
+class AnthropicChatModelSetup(BaseChatModelSetup):
+ """The settings for Anthropic Chat Model.
+
+ Attributes:
+ ----------
+ model : str
+ Specifies the Anthropic model to use. Defaults to
claude-sonnet-4-20250514.
+ max_tokens: int
+ The maximum number of tokens to generate before stopping. Defaults to
1024.
+ temperature : float
+ Amount of randomness injected into the response.
+ """
+
+ model: str = Field(
+ default=DEFAULT_ANTHROPIC_MODEL, description="Specifies the Anthropic
model to use. Defaults to "
+
"claude-sonnet-4-20250514."
+ )
+ max_tokens: int = Field(
+ default=DEFAULT_MAX_TOKENS,
+ description="The maximum number of tokens to generate before stopping.
Defaults to 1024.",
+ ge=1,
+ )
+ temperature: float = Field(
+ default=DEFAULT_TEMPERATURE,
+ description="Amount of randomness injected into the response. Defaults
to 0.1",
+ ge=0.0,
+ le=1.0,
+ )
+
+ def __init__(
+ self,
+ connection: str,
+ model: str = DEFAULT_ANTHROPIC_MODEL,
+ max_tokens: int = DEFAULT_MAX_TOKENS,
+ temperature: float = DEFAULT_TEMPERATURE,
+ **kwargs: Any,
+ ) -> None:
+ """Init method."""
+ super().__init__(
+ connection=connection,
+ model=model,
+ max_tokens=max_tokens,
+ temperature=temperature,
+ **kwargs,
+ )
+
+ @property
+ def model_kwargs(self) -> Dict[str, Any]:
+ """Get model-specific keyword arguments."""
+ return {"model": self.model, "max_tokens": self.max_tokens,
"temperature": self.temperature}
diff --git a/python/flink_agents/api/events/tool_event.py
b/python/flink_agents/integrations/chat_models/anthropic/tests/__init__.py
similarity index 59%
copy from python/flink_agents/api/events/tool_event.py
copy to python/flink_agents/integrations/chat_models/anthropic/tests/__init__.py
index a5e5c47..e154fad 100644
--- a/python/flink_agents/api/events/tool_event.py
+++ b/python/flink_agents/integrations/chat_models/anthropic/tests/__init__.py
@@ -15,36 +15,3 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#################################################################################
-from typing import Any
-
-from flink_agents.api.events.event import Event
-
-
-class ToolRequestEvent(Event):
- """Event representing a tool call request.
-
- Attributes:
- ----------
- tool : str
- The name of the tool to be called.
- kwargs : dict
- The arguments passed to the tool.
- """
-
- tool: str
- kwargs: dict
-
-
-class ToolResponseEvent(Event):
- """Event representing a result from tool call.
-
- Attributes:
- ----------
- request : ToolRequestEvent
- The correspond request of the response.
- response : Any
- The response from the tool.
- """
-
- request: ToolRequestEvent
- response: Any
diff --git
a/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_chat_model.py
b/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_chat_model.py
new file mode 100644
index 0000000..45116ac
--- /dev/null
+++
b/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_chat_model.py
@@ -0,0 +1,98 @@
+################################################################################
+# 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 os
+
+import pytest
+
+from flink_agents.api.chat_message import ChatMessage, MessageRole
+from flink_agents.api.resource import Resource, ResourceType
+from flink_agents.integrations.chat_models.anthropic.anthropic_chat_model
import (
+ AnthropicChatModelConnection,
+ AnthropicChatModelSetup,
+)
+from flink_agents.plan.tools.function_tool import from_callable
+
+test_model = os.environ.get("TEST_MODEL")
+api_key = os.environ.get("TEST_API_KEY")
+
+
[email protected](api_key is None, reason="TEST_API_KEY is not set")
+def test_anthropic_chat_model() -> None: # noqa: D103
+ connection = AnthropicChatModelConnection(
+ name="anthropic_server", api_key=api_key
+ )
+
+ def get_resource(name: str, type: ResourceType) -> Resource:
+ if type == ResourceType.CHAT_MODEL_CONNECTION:
+ return connection
+ else:
+ return get_resource(name, ResourceType.TOOL)
+
+ chat_model = AnthropicChatModelSetup(
+ name="anthropic", model=test_model, connection="anthropic_server",
get_resource=get_resource
+ )
+ response = chat_model.chat([ChatMessage(role=MessageRole.USER,
content="Hello!")])
+ assert response is not None
+ assert str(response).strip() != ""
+
+
+def add(a: int, b: int) -> int:
+ """Calculate the sum of a and b.
+
+ Parameters
+ ----------
+ a : int
+ The first operand
+ b : int
+ The second operand
+
+ Returns:
+ -------
+ int:
+ The sum of a and b
+ """
+ return a + b
+
+
[email protected](api_key is None, reason="TEST_API_KEY is not set")
+def test_anthropic_chat_with_tools() -> None: # noqa : D103
+ connection = AnthropicChatModelConnection(
+ name="anthropic_server", api_key=api_key
+ )
+
+ def get_resource(name: str, type: ResourceType) -> Resource:
+ if type == ResourceType.CHAT_MODEL_CONNECTION:
+ return connection
+ else:
+ return from_callable(name=name, func=add)
+
+ chat_model = AnthropicChatModelSetup(
+ name="anthropic",
+ model=test_model,
+ connection="anthropic_server",
+ tools=["add"],
+ get_resource=get_resource,
+ )
+ response = chat_model.chat(
+ [ChatMessage(role=MessageRole.USER, content="What is 1 + 1?")]
+ )
+ tool_calls = response.tool_calls
+ assert len(tool_calls) == 1
+ tool_call = tool_calls[0]
+ assert add(**tool_call["function"]["arguments"]) == 2
+ assert tool_call.get("original_id") is not None
diff --git a/python/flink_agents/plan/actions/chat_model_action.py
b/python/flink_agents/plan/actions/chat_model_action.py
index 03ef48e..f51a0fb 100644
--- a/python/flink_agents/plan/actions/chat_model_action.py
+++ b/python/flink_agents/plan/actions/chat_model_action.py
@@ -57,6 +57,7 @@ def process_chat_request_or_tool_response(event: Event, ctx:
RunnerContext) -> N
id=tool_call_id,
tool=tool_call["function"]["name"],
kwargs=tool_call["function"]["arguments"],
+ external_id=tool_call.get("original_id"),
)
)
@@ -73,7 +74,11 @@ def process_chat_request_or_tool_response(event: Event, ctx:
RunnerContext) -> N
# get the specific tool call context from short term memory
specific_tool_ctx = tool_context.pop(tool_call_id)
specific_tool_ctx.messages.append(
- ChatMessage(role=MessageRole.TOOL,
content=str(event.response))
+ ChatMessage(
+ role=MessageRole.TOOL,
+ content=str(event.response),
+ extra_args={"external_id": event.request.external_id}
if event.request.external_id else {}
+ )
)
ctx.send_event(specific_tool_ctx)
# update short term memory to remove the specific tool call
context
diff --git a/python/pyproject.toml b/python/pyproject.toml
index a586a00..528bb8d 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -50,7 +50,8 @@ dependencies = [
#TODO: Seperate integration dependencies from project
"ollama==0.4.8",
"dashscope~=1.24.2",
- "openai>=1.66.3"
+ "openai>=1.66.3",
+ "anthropic>=0.64.0",
]
# Optional dependencies (dependency groups)