diff --git a/src/google/adk/artifacts/gcs_artifact_service.py b/src/google/adk/artifacts/gcs_artifact_service.py index 4108cfb06b..4a46c9e4c6 100644 --- a/src/google/adk/artifacts/gcs_artifact_service.py +++ b/src/google/adk/artifacts/gcs_artifact_service.py @@ -221,7 +221,8 @@ def _save_artifact( data=artifact.inline_data.data, content_type=artifact.inline_data.mime_type, ) - elif artifact.text: + elif artifact.text is not None: + blob.metadata = {**(blob.metadata or {}), "_adk_is_text": "true"} blob.upload_from_string( data=artifact.text, content_type="text/plain", @@ -260,15 +261,18 @@ def _load_artifact( blob_name = self._get_blob_name( app_name, user_id, filename, version, session_id ) - blob = self.bucket.blob(blob_name) + blob = self.bucket.get_blob(blob_name) + if not blob: + return None artifact_bytes = blob.download_as_bytes() - if not artifact_bytes: - return None - artifact = types.Part.from_bytes( + + if blob.metadata and blob.metadata.get("_adk_is_text") == "true": + return types.Part(text=artifact_bytes.decode("utf-8")) + + return types.Part.from_bytes( data=artifact_bytes, mime_type=blob.content_type ) - return artifact def _list_artifact_keys( self, app_name: str, user_id: str, session_id: Optional[str] diff --git a/tests/unittests/artifacts/test_artifact_service.py b/tests/unittests/artifacts/test_artifact_service.py index ec74f8abe3..59e31d93bc 100644 --- a/tests/unittests/artifacts/test_artifact_service.py +++ b/tests/unittests/artifacts/test_artifact_service.py @@ -766,3 +766,74 @@ async def test_file_save_artifact_rejects_absolute_path_within_scope(tmp_path): filename=str(absolute_in_scope), artifact=part, ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "service_type", + [ + ArtifactServiceType.IN_MEMORY, + ArtifactServiceType.GCS, + ArtifactServiceType.FILE, + ], +) +@pytest.mark.parametrize( + "text_content", + ['{"key": "value"}', "some other text"], +) +async def test_save_load_text_artifact( + service_type, artifact_service_factory, text_content +): + """Tests that text artifacts retain .text after round-trip save/load.""" + artifact_service = artifact_service_factory(service_type) + artifact = types.Part.from_text(text=text_content) + + await artifact_service.save_artifact( + app_name="app0", + user_id="user0", + session_id="123", + filename="data.json", + artifact=artifact, + ) + loaded = await artifact_service.load_artifact( + app_name="app0", + user_id="user0", + session_id="123", + filename="data.json", + ) + assert loaded is not None + assert loaded.text == text_content + assert loaded.inline_data is None + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "service_type", + [ + ArtifactServiceType.GCS, + ArtifactServiceType.FILE, + ], +) +async def test_save_load_empty_text_artifact( + service_type, artifact_service_factory +): + """Tests that empty text artifacts survive round-trip save/load.""" + artifact_service = artifact_service_factory(service_type) + artifact = types.Part.from_text(text="") + + await artifact_service.save_artifact( + app_name="app0", + user_id="user0", + session_id="123", + filename="empty.txt", + artifact=artifact, + ) + loaded = await artifact_service.load_artifact( + app_name="app0", + user_id="user0", + session_id="123", + filename="empty.txt", + ) + assert loaded is not None + assert loaded.text == "" + assert loaded.inline_data is None