From f7af931a8da97a4c2357ec6d14d66fec5f739f2f Mon Sep 17 00:00:00 2001 From: Chengmian Liu Date: Sun, 4 Oct 2026 17:20:31 +0800 Subject: [PATCH] fix(sessions): keep inlineData mimeType when encoding or decoding content SessionUtils.encodeContent and SessionUtils.decodeContent rebuilt each inline blob with Blob.builder().data(...), which produces a Blob carrying only `data` and silently drops `mimeType` and `displayName`. `mimeType` is documented as required on com.google.genai.types.Blob, and this is the persistence path used by VertexAiSessionService (appendEvent -> SessionJsonConverter.convertEventToJson -> encodeContent, and back through fromApiEvent -> decodeContent), so every multimodal event stored to Vertex AI lost its MIME type permanently. The round trip decodeContent(encodeContent(content)) did not return the input either. Reuse the existing blob's builder so the remaining fields are preserved, and add tests covering encode, decode, the round trip and the JSON that is persisted. --- .../com/google/adk/sessions/SessionUtils.java | 8 +- .../google/adk/sessions/SessionUtilsTest.java | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 core/src/test/java/com/google/adk/sessions/SessionUtilsTest.java diff --git a/core/src/main/java/com/google/adk/sessions/SessionUtils.java b/core/src/main/java/com/google/adk/sessions/SessionUtils.java index 1aeca98c9..8fac6161a 100644 --- a/core/src/main/java/com/google/adk/sessions/SessionUtils.java +++ b/core/src/main/java/com/google/adk/sessions/SessionUtils.java @@ -45,7 +45,9 @@ public static Content encodeContent(Content content) { byte[] dataBytes = dataOptional.get(); byte[] encodedData = Base64.getEncoder().encode(dataBytes); encodedParts.add( - part.toBuilder().inlineData(Blob.builder().data(encodedData).build()).build()); + part.toBuilder() + .inlineData(inlineDataBlob.toBuilder().data(encodedData).build()) + .build()); isInlineDataPresent = true; } } @@ -71,7 +73,9 @@ public static Content decodeContent(Content content) { byte[] dataBytes = dataOptional.get(); byte[] decodedData = Base64.getDecoder().decode(dataBytes); decodedParts.add( - part.toBuilder().inlineData(Blob.builder().data(decodedData).build()).build()); + part.toBuilder() + .inlineData(inlineDataBlob.toBuilder().data(decodedData).build()) + .build()); isInlineDataPresent = true; } } diff --git a/core/src/test/java/com/google/adk/sessions/SessionUtilsTest.java b/core/src/test/java/com/google/adk/sessions/SessionUtilsTest.java new file mode 100644 index 000000000..0bb8d4caa --- /dev/null +++ b/core/src/test/java/com/google/adk/sessions/SessionUtilsTest.java @@ -0,0 +1,93 @@ +/* + * 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. + */ + +package com.google.adk.sessions; + +import static com.google.common.truth.Truth.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.adk.JsonBaseModel; +import com.google.adk.events.Event; +import com.google.genai.types.Blob; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SessionUtilsTest { + + private static final ObjectMapper objectMapper = JsonBaseModel.getMapper(); + private static final byte[] PAYLOAD = "payload".getBytes(StandardCharsets.UTF_8); + + private static Content contentWithInlineData() { + return Content.builder() + .role("user") + .parts( + Part.builder() + .inlineData(Blob.builder().mimeType("image/png").data(PAYLOAD).build()) + .build()) + .build(); + } + + @Test + public void encodeContent_inlineData_preservesMimeType() { + Content encoded = SessionUtils.encodeContent(contentWithInlineData()); + + Blob encodedBlob = encoded.parts().get().get(0).inlineData().get(); + assertThat(encodedBlob.mimeType()).hasValue("image/png"); + } + + @Test + public void decodeContent_inlineData_preservesMimeType() { + Content decoded = SessionUtils.decodeContent(contentWithInlineData()); + + Blob decodedBlob = decoded.parts().get().get(0).inlineData().get(); + assertThat(decodedBlob.mimeType()).hasValue("image/png"); + } + + @Test + public void decodeContent_encodeContent_roundTripsInlineData() { + Content roundTripped = + SessionUtils.decodeContent(SessionUtils.encodeContent(contentWithInlineData())); + + Blob blob = roundTripped.parts().get().get(0).inlineData().get(); + assertThat(blob.mimeType()).hasValue("image/png"); + assertThat(blob.data().get()).isEqualTo(PAYLOAD); + } + + /** The MIME type has to survive the JSON the Vertex AI Sessions API stores the event in. */ + @Test + public void convertEventToJson_inlineData_persistsMimeType() throws Exception { + Event event = + Event.builder() + .author("user") + .invocationId("inv-123") + .timestamp(Instant.parse("2023-01-01T00:00:00Z").toEpochMilli()) + .content(contentWithInlineData()) + .build(); + + JsonNode jsonNode = objectMapper.readTree(SessionJsonConverter.convertEventToJson(event)); + + JsonNode inlineData = jsonNode.get("content").get("parts").get(0).get("inlineData"); + assertThat(inlineData.has("mimeType")).isTrue(); + assertThat(inlineData.get("mimeType").asText()).isEqualTo("image/png"); + } +}