From f6a526300a2979b40fb3b1dcee7944687109d618 Mon Sep 17 00:00:00 2001 From: Maanik Garg Date: Sat, 4 Apr 2026 12:23:59 +0530 Subject: [PATCH] fix: restore .text attribute when loading text artifacts from GCS GcsArtifactService._load_artifact() always used Part.from_bytes() which populates inline_data even for text/plain content. This caused Part.from_text() artifacts to lose their .text attribute after a save/load cycle. Now detects text/plain content type and returns Part(text=...) instead, matching the behavior of FileArtifactService which already handles this correctly. Fixes #3157 --- .../adk/artifacts/gcs_artifact_service.py | 4 ++ .../artifacts/test_artifact_service.py | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/google/adk/artifacts/gcs_artifact_service.py b/src/google/adk/artifacts/gcs_artifact_service.py index f8706dedbd..fa1214f8a5 100644 --- a/src/google/adk/artifacts/gcs_artifact_service.py +++ b/src/google/adk/artifacts/gcs_artifact_service.py @@ -268,6 +268,10 @@ def _load_artifact( artifact_bytes = blob.download_as_bytes() if not artifact_bytes: return None + # Restore text artifacts with the .text attribute rather than + # inline_data so that round-tripping Part.from_text() works correctly. + if blob.content_type and blob.content_type.startswith("text/plain"): + return types.Part(text=artifact_bytes.decode("utf-8")) artifact = types.Part.from_bytes( data=artifact_bytes, mime_type=blob.content_type ) diff --git a/tests/unittests/artifacts/test_artifact_service.py b/tests/unittests/artifacts/test_artifact_service.py index 25294d4909..b1f1edcf3c 100644 --- a/tests/unittests/artifacts/test_artifact_service.py +++ b/tests/unittests/artifacts/test_artifact_service.py @@ -896,3 +896,49 @@ async def test_save_artifact_with_snake_case_dict( assert loaded is not None assert loaded.inline_data is not None assert loaded.inline_data.mime_type == "text/plain" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "service_type", + [ + ArtifactServiceType.IN_MEMORY, + ArtifactServiceType.GCS, + ArtifactServiceType.FILE, + ], +) +async def test_text_artifact_roundtrip(service_type, artifact_service_factory): + """Text artifacts saved via Part.from_text() should preserve .text on load. + + Regression test for https://github.com/google/adk-python/issues/3157 + """ + artifact_service = artifact_service_factory(service_type) + app_name = "app0" + user_id = "user0" + session_id = "sess0" + filename = "report.txt" + text_content = '{"status": "success", "report": "Sunny, 25C"}' + + artifact = types.Part.from_text(text=text_content) + assert artifact.text == text_content # sanity check + + version = await artifact_service.save_artifact( + app_name=app_name, + user_id=user_id, + session_id=session_id, + filename=filename, + artifact=artifact, + ) + assert version == 0 + + loaded = await artifact_service.load_artifact( + app_name=app_name, + user_id=user_id, + session_id=session_id, + filename=filename, + ) + assert loaded is not None + assert loaded.text == text_content, ( + f"Expected .text='{text_content}', got .text={loaded.text!r} " + f"(inline_data={loaded.inline_data!r})" + )