This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch release-0.3
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/release-0.3 by this push:
new d2ea208b [integration][anthropic] Don't assume the first content block
is text (#916)
d2ea208b is described below
commit d2ea208b5d1d75a8316e0d3cb7e30520d90e3f71
Author: Edson <[email protected]>
AuthorDate: Sun Jul 19 23:18:44 2026 -0400
[integration][anthropic] Don't assume the first content block is text (#916)
(cherry picked from commit b5dd2715ee0f20b9de0b3684cb189387270f6abd)
---
.../chat_models/anthropic/anthropic_chat_model.py | 11 ++-
.../tests/test_anthropic_response_parsing.py | 96 ++++++++++++++++++++++
2 files changed, 105 insertions(+), 2 deletions(-)
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
index c077c6c8..af5483dc 100644
---
a/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
+++
b/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
@@ -195,6 +195,13 @@ class
AnthropicChatModelConnection(BaseChatModelConnection):
extra_args["promptTokens"] = message.usage.input_tokens
extra_args["completionTokens"] = message.usage.output_tokens
+ # A response may lead with a non-text block (e.g. a tool_use block when
+ # the model calls a tool without any preface), so pick the first text
+ # block instead of assuming content[0] is text.
+ text = next(
+ (block.text for block in message.content if block.type == "text"),
""
+ )
+
if message.stop_reason == "tool_use":
tool_calls = [
{
@@ -213,7 +220,7 @@ class AnthropicChatModelConnection(BaseChatModelConnection):
extra_args["anthropic_content_blocks"] = message.content
return ChatMessage(
role=MessageRole(message.role),
- content=message.content[0].text,
+ content=text,
tool_calls=tool_calls,
extra_args=extra_args,
)
@@ -222,7 +229,7 @@ class AnthropicChatModelConnection(BaseChatModelConnection):
# https://docs.anthropic.com/en/api/messages#response-stop-reason
return ChatMessage(
role=MessageRole(message.role),
- content=message.content[0].text,
+ content=text,
)
@override
diff --git
a/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_response_parsing.py
b/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_response_parsing.py
new file mode 100644
index 00000000..89a1d5bf
--- /dev/null
+++
b/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_response_parsing.py
@@ -0,0 +1,96 @@
+################################################################################
+# 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.
+#################################################################################
+from unittest.mock import MagicMock
+
+from anthropic.types import Message, TextBlock, ToolUseBlock, Usage
+
+from flink_agents.api.chat_message import ChatMessage, MessageRole
+from flink_agents.integrations.chat_models.anthropic.anthropic_chat_model
import (
+ AnthropicChatModelConnection,
+)
+
+
+def _connection_returning(message: Message) -> AnthropicChatModelConnection:
+ connection = AnthropicChatModelConnection(name="test", api_key="dummy")
+ client = MagicMock()
+ client.messages.create.return_value = message
+ connection._client = client
+ return connection
+
+
+def _usage() -> Usage:
+ return Usage(input_tokens=1, output_tokens=1)
+
+
+def test_tool_use_response_without_leading_text() -> None:
+ # When the model calls a tool it commonly returns only a tool_use block, so
+ # content[0] is not a text block. Parsing must not assume content[0].text.
+ message = Message(
+ id="m",
+ model="claude",
+ role="assistant",
+ type="message",
+ stop_reason="tool_use",
+ content=[
+ ToolUseBlock(type="tool_use", id="t1", name="add", input={"a": 1,
"b": 2})
+ ],
+ usage=_usage(),
+ )
+ response = _connection_returning(message).chat(
+ [ChatMessage(role=MessageRole.USER, content="add 1 and 2")]
+ )
+ assert response.content == ""
+ assert len(response.tool_calls) == 1
+ assert response.tool_calls[0]["function"]["name"] == "add"
+
+
+def test_tool_use_response_keeps_leading_text() -> None:
+ # A tool_use response may be preceded by a text block; that text is kept.
+ message = Message(
+ id="m",
+ model="claude",
+ role="assistant",
+ type="message",
+ stop_reason="tool_use",
+ content=[
+ TextBlock(type="text", text="Let me add those."),
+ ToolUseBlock(type="tool_use", id="t1", name="add", input={"a": 1,
"b": 2}),
+ ],
+ usage=_usage(),
+ )
+ response = _connection_returning(message).chat(
+ [ChatMessage(role=MessageRole.USER, content="add 1 and 2")]
+ )
+ assert response.content == "Let me add those."
+ assert len(response.tool_calls) == 1
+
+
+def test_plain_text_response() -> None:
+ message = Message(
+ id="m",
+ model="claude",
+ role="assistant",
+ type="message",
+ stop_reason="end_turn",
+ content=[TextBlock(type="text", text="Hello!")],
+ usage=_usage(),
+ )
+ response = _connection_returning(message).chat(
+ [ChatMessage(role=MessageRole.USER, content="hi")]
+ )
+ assert response.content == "Hello!"