Files
adk-python/tests/unittests/tools/test_load_artifacts_tool.py
T
George WealeandCopybara-Service fdc98d5c92 fix: Convert unsupported inline artifact MIME types to text in LoadArtifactsTool
The LoadArtifactsTool now checks if an artifact's inline data MIME type is supported by Gemini. If not, it attempts to convert the artifact content into a text Part

Close #4028

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 856404510
2026-01-14 16:01:06 -08:00

163 lines
5.2 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed 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 base64
from google.adk.models.llm_request import LlmRequest
from google.adk.tools.load_artifacts_tool import _maybe_base64_to_bytes
from google.adk.tools.load_artifacts_tool import load_artifacts_tool
from google.genai import types
from pytest import mark
class _StubToolContext:
"""Minimal ToolContext stub for LoadArtifactsTool tests."""
def __init__(self, artifacts_by_name: dict[str, types.Part]):
self._artifacts_by_name = artifacts_by_name
async def list_artifacts(self) -> list[str]:
return list(self._artifacts_by_name.keys())
async def load_artifact(self, name: str) -> types.Part | None:
return self._artifacts_by_name.get(name)
@mark.asyncio
async def test_load_artifacts_converts_unsupported_mime_to_text():
"""Unsupported inline MIME types are converted to text parts."""
artifact_name = 'test.csv'
csv_bytes = b'col1,col2\n1,2\n'
artifact = types.Part(
inline_data=types.Blob(data=csv_bytes, mime_type='application/csv')
)
tool_context = _StubToolContext({artifact_name: artifact})
llm_request = LlmRequest(
contents=[
types.Content(
role='user',
parts=[
types.Part(
function_response=types.FunctionResponse(
name='load_artifacts',
response={'artifact_names': [artifact_name]},
)
)
],
)
]
)
await load_artifacts_tool.process_llm_request(
tool_context=tool_context, llm_request=llm_request
)
assert llm_request.contents[-1].parts[0].text == (
f'Artifact {artifact_name} is:'
)
artifact_part = llm_request.contents[-1].parts[1]
assert artifact_part.inline_data is None
assert artifact_part.text == csv_bytes.decode('utf-8')
@mark.asyncio
async def test_load_artifacts_converts_base64_unsupported_mime_to_text():
"""Unsupported base64 string data is converted to text parts."""
artifact_name = 'test.csv'
csv_bytes = b'col1,col2\n1,2\n'
csv_base64 = base64.b64encode(csv_bytes).decode('ascii')
artifact = types.Part(
inline_data=types.Blob(data=csv_base64, mime_type='application/csv')
)
tool_context = _StubToolContext({artifact_name: artifact})
llm_request = LlmRequest(
contents=[
types.Content(
role='user',
parts=[
types.Part(
function_response=types.FunctionResponse(
name='load_artifacts',
response={'artifact_names': [artifact_name]},
)
)
],
)
]
)
await load_artifacts_tool.process_llm_request(
tool_context=tool_context, llm_request=llm_request
)
artifact_part = llm_request.contents[-1].parts[1]
assert artifact_part.inline_data is None
assert artifact_part.text == csv_bytes.decode('utf-8')
@mark.asyncio
async def test_load_artifacts_keeps_supported_mime_types():
"""Supported inline MIME types are passed through unchanged."""
artifact_name = 'test.pdf'
artifact = types.Part(
inline_data=types.Blob(data=b'%PDF-1.4', mime_type='application/pdf')
)
tool_context = _StubToolContext({artifact_name: artifact})
llm_request = LlmRequest(
contents=[
types.Content(
role='user',
parts=[
types.Part(
function_response=types.FunctionResponse(
name='load_artifacts',
response={'artifact_names': [artifact_name]},
)
)
],
)
]
)
await load_artifacts_tool.process_llm_request(
tool_context=tool_context, llm_request=llm_request
)
artifact_part = llm_request.contents[-1].parts[1]
assert artifact_part.inline_data is not None
assert artifact_part.inline_data.mime_type == 'application/pdf'
def test_maybe_base64_to_bytes_decodes_standard_base64():
"""Standard base64 encoded strings are decoded correctly."""
original = b'hello world'
encoded = base64.b64encode(original).decode('ascii')
assert _maybe_base64_to_bytes(encoded) == original
def test_maybe_base64_to_bytes_decodes_urlsafe_base64():
"""URL-safe base64 encoded strings are decoded correctly."""
original = b'\xfb\xff\xfe' # bytes that produce +/ in std but -_ in urlsafe
encoded = base64.urlsafe_b64encode(original).decode('ascii')
assert _maybe_base64_to_bytes(encoded) == original
def test_maybe_base64_to_bytes_returns_none_for_invalid():
"""Invalid base64 strings return None."""
# Single character is invalid (base64 requires length % 4 == 0 after padding)
assert _maybe_base64_to_bytes('x') is None