Compare commits

...

2 Commits

Author SHA1 Message Date
Luke Street 7239de5a19 fix: Address code review feedback for document upload support
- Use startswith() for MIME type check to handle parameters (e.g.
  "application/pdf; name=doc.pdf")
- Move import base64 to top of test file per PEP 8
- Parameterize expected warning messages in tests instead of just
  checking that any warning was logged
- Rename test function to test_content_to_message_param for clarity
2026-03-05 15:10:44 -07:00
Luke Street 8176f30c73 feat: Add PDF document upload support to Anthropic LLM adapter
Adds support for PDF document parts in the Anthropic adapter by converting
inline PDF data to Anthropic's `DocumentBlockParam` format using base64
encoding, matching the existing pattern for image part handling.
2026-03-05 15:04:52 -07:00
2 changed files with 83 additions and 11 deletions
+23
View File
@@ -90,6 +90,14 @@ def _is_image_part(part: types.Part) -> bool:
)
def _is_document_part(part: types.Part) -> bool:
return (
part.inline_data
and part.inline_data.mime_type
and part.inline_data.mime_type.startswith("application/pdf")
)
def part_to_message_block(
part: types.Part,
) -> Union[
@@ -151,6 +159,14 @@ def part_to_message_block(
type="base64", media_type=part.inline_data.mime_type, data=data
),
)
elif _is_document_part(part):
data = base64.b64encode(part.inline_data.data).decode()
return anthropic_types.DocumentBlockParam(
type="document",
source=dict(
type="base64", media_type=part.inline_data.mime_type, data=data
),
)
elif part.executable_code:
return anthropic_types.TextBlockParam(
type="text",
@@ -179,6 +195,13 @@ def content_to_message_param(
)
continue
# Document data is not supported in Claude for assistant turns.
if content.role != "user" and _is_document_part(part):
logger.warning(
"Document data is not supported in Claude for assistant turns."
)
continue
message_block.append(part_to_message_block(part))
return {
+60 -11
View File
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import json
import os
import sys
@@ -526,6 +527,22 @@ def test_part_to_message_block_with_multiple_content_items():
assert result["content"] == "First part\nSecond part"
def test_part_to_message_block_with_pdf_document():
"""Test that part_to_message_block handles PDF document parts."""
pdf_data = b"%PDF-1.4 fake pdf content"
part = Part(
inline_data=types.Blob(mime_type="application/pdf", data=pdf_data)
)
result = part_to_message_block(part)
assert isinstance(result, dict)
assert result["type"] == "document"
assert result["source"]["type"] == "base64"
assert result["source"]["media_type"] == "application/pdf"
assert result["source"]["data"] == base64.b64encode(pdf_data).decode()
content_to_message_param_test_cases = [
(
"user_role_with_text_and_image",
@@ -542,7 +559,7 @@ content_to_message_param_test_cases = [
),
"user",
2, # Expected content length
False, # Should not log warning
None, # No warning expected
),
(
"model_role_with_text_and_image",
@@ -559,7 +576,7 @@ content_to_message_param_test_cases = [
),
"assistant",
1, # Image filtered out, only text remains
True, # Should log warning
"Image data is not supported in Claude for assistant turns.",
),
(
"assistant_role_with_text_and_image",
@@ -576,30 +593,62 @@ content_to_message_param_test_cases = [
),
"assistant",
1, # Image filtered out, only text remains
True, # Should log warning
"Image data is not supported in Claude for assistant turns.",
),
(
"user_role_with_text_and_document",
Content(
role="user",
parts=[
Part.from_text(text="Summarize this document."),
Part(
inline_data=types.Blob(
mime_type="application/pdf", data=b"fake_pdf_data"
)
),
],
),
"user",
2, # Both text and document included
None, # No warning expected
),
(
"model_role_with_text_and_document",
Content(
role="model",
parts=[
Part.from_text(text="Here is the summary."),
Part(
inline_data=types.Blob(
mime_type="application/pdf", data=b"fake_pdf_data"
)
),
],
),
"assistant",
1, # Document filtered out, only text remains
"Document data is not supported in Claude for assistant turns.",
),
]
@pytest.mark.parametrize(
"_, content, expected_role, expected_content_length, should_log_warning",
"_, content, expected_role, expected_content_length, expected_warning",
content_to_message_param_test_cases,
ids=[case[0] for case in content_to_message_param_test_cases],
)
def test_content_to_message_param_with_images(
_, content, expected_role, expected_content_length, should_log_warning
def test_content_to_message_param(
_, content, expected_role, expected_content_length, expected_warning
):
"""Test content_to_message_param handles images correctly based on role."""
"""Test content_to_message_param handles images and documents based on role."""
with mock.patch("google.adk.models.anthropic_llm.logger") as mock_logger:
result = content_to_message_param(content)
assert result["role"] == expected_role
assert len(result["content"]) == expected_content_length
if should_log_warning:
mock_logger.warning.assert_called_once_with(
"Image data is not supported in Claude for assistant turns."
)
if expected_warning:
mock_logger.warning.assert_called_once_with(expected_warning)
else:
mock_logger.warning.assert_not_called()