diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java index 2e957695b..8464dbe50 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java @@ -39,6 +39,7 @@ import io.a2a.spec.FileWithUri; import io.a2a.spec.Message; import io.a2a.spec.TextPart; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -66,6 +67,9 @@ public final class PartConverter { public static final String PARTIAL_ARGS_KEY = "partialArgs"; public static final String SCHEDULING_KEY = "scheduling"; public static final String PARTS_KEY = "parts"; + public static final String A2A_DATA_PART_START_TAG = ""; + public static final String A2A_DATA_PART_END_TAG = ""; + public static final String A2A_DATA_PART_TEXT_MIME_TYPE = "text/plain"; public static Optional toTextPart(io.a2a.spec.Part part) { if (part instanceof TextPart textPart) { @@ -190,7 +194,11 @@ private static com.google.genai.types.Part convertDataPartToGenAiPart(DataPart d try { String json = objectMapper.writeValueAsString(data); - return com.google.genai.types.Part.builder().text(json).build(); + String wrappedJson = A2A_DATA_PART_START_TAG + json + A2A_DATA_PART_END_TAG; + byte[] bytes = wrappedJson.getBytes(StandardCharsets.UTF_8); + return com.google.genai.types.Part.builder() + .inlineData(Blob.builder().data(bytes).mimeType(A2A_DATA_PART_TEXT_MIME_TYPE).build()) + .build(); } catch (JsonProcessingException e) { throw new IllegalArgumentException("Failed to serialize DataPart payload", e); } @@ -298,6 +306,37 @@ private static DataPart createDataPartFromExecutableCode( return new DataPart(data.buildOrThrow(), metadata.buildOrThrow()); } + private static boolean isDataPartInlineData(Blob blob) { + if (!blob.mimeType().orElse("").equals(A2A_DATA_PART_TEXT_MIME_TYPE)) { + return false; + } + byte[] data = blob.data().orElse(null); + if (data == null) { + return false; + } + String str = new String(data, StandardCharsets.UTF_8); + return str.startsWith(A2A_DATA_PART_START_TAG) && str.endsWith(A2A_DATA_PART_END_TAG); + } + + @SuppressWarnings("unchecked") + private static DataPart inlineDataToDataPart( + Blob blob, ImmutableMap.Builder metadata) { + byte[] data = blob.data().orElse(null); + if (data == null) { + throw new IllegalArgumentException("Blob data cannot be null"); + } + String str = new String(data, StandardCharsets.UTF_8); + String jsonContent = + str.substring( + A2A_DATA_PART_START_TAG.length(), str.length() - A2A_DATA_PART_END_TAG.length()); + try { + Map dataMap = objectMapper.readValue(jsonContent, Map.class); + return new DataPart(dataMap, metadata.buildOrThrow()); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse DataPart payload from inlineData", e); + } + } + private PartConverter() {} /** Convert a GenAI part into the A2A JSON representation. */ @@ -315,6 +354,10 @@ public static io.a2a.spec.Part fromGenaiPart(Part part, boolean isPartial) { return new TextPart(part.text().get(), metadata.buildOrThrow()); } + if (part.inlineData().isPresent() && isDataPartInlineData(part.inlineData().get())) { + return inlineDataToDataPart(part.inlineData().get(), metadata); + } + if (part.fileData().isPresent() || part.inlineData().isPresent()) { return filePartToA2A(part, metadata); } diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java index 563a9afdc..bbbb47467 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java @@ -193,13 +193,17 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons } @Test - public void toGenaiPart_withOtherDataPart_returnsGenaiTextPartWithJson() { + public void toGenaiPart_withOtherDataPart_returnsGenaiInlineDataPartWithWrappedJson() { ImmutableMap data = ImmutableMap.of("key", "value"); DataPart dataPart = new DataPart(data, null); Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result.text()).hasValue("{\"key\":\"value\"}"); + assertThat(result.inlineData()).isPresent(); + Blob blob = result.inlineData().get(); + assertThat(blob.mimeType()).hasValue("text/plain"); + String expectedContent = "{\"key\":\"value\"}"; + assertThat(new String(blob.data().get(), UTF_8)).isEqualTo(expectedContent); } @Test @@ -374,4 +378,20 @@ public void toGenaiPart_dataPartWithNonMapCoercedToMap() { assertThat(result.functionCall()).isPresent(); assertThat(result.functionCall().get().args()).hasValue(ImmutableMap.of("value", 123)); } + + @Test + public void fromGenaiPart_withDataPartInlineData_returnsDataPart() { + String wrappedJson = "{\"key\":\"value\"}"; + Part part = + Part.builder() + .inlineData( + Blob.builder().mimeType("text/plain").data(wrappedJson.getBytes(UTF_8)).build()) + .build(); + + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); + + assertThat(result).isInstanceOf(DataPart.class); + DataPart dataPart = (DataPart) result; + assertThat(dataPart.getData()).containsExactly("key", "value"); + } }