Skip to content

Commit 2c1b7ab

Browse files
google-genai-botcopybara-github
authored andcommitted
ADK changes
PiperOrigin-RevId: 934614564
1 parent fc480ec commit 2c1b7ab

2 files changed

Lines changed: 108 additions & 8 deletions

File tree

a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.google.adk.a2a.converters;
1717

18+
import static com.google.common.base.Preconditions.checkNotNull;
1819
import static com.google.common.collect.ImmutableList.toImmutableList;
1920

2021
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -39,6 +40,7 @@
3940
import io.a2a.spec.FileWithUri;
4041
import io.a2a.spec.Message;
4142
import io.a2a.spec.TextPart;
43+
import java.nio.charset.StandardCharsets;
4244
import java.util.Base64;
4345
import java.util.HashMap;
4446
import java.util.List;
@@ -66,6 +68,9 @@ public final class PartConverter {
6668
public static final String PARTIAL_ARGS_KEY = "partialArgs";
6769
public static final String SCHEDULING_KEY = "scheduling";
6870
public static final String PARTS_KEY = "parts";
71+
public static final String A2A_DATA_PART_START_TAG = "<a2a_datapart_json>";
72+
public static final String A2A_DATA_PART_END_TAG = "</a2a_datapart_json>";
73+
public static final String A2A_DATA_PART_TEXT_MIME_TYPE = "text/plain";
6974

7075
public static Optional<TextPart> toTextPart(io.a2a.spec.Part<?> part) {
7176
if (part instanceof TextPart textPart) {
@@ -76,9 +81,7 @@ public static Optional<TextPart> toTextPart(io.a2a.spec.Part<?> part) {
7681

7782
/** Convert an A2A JSON part into a Google GenAI part representation. */
7883
public static com.google.genai.types.Part toGenaiPart(io.a2a.spec.Part<?> a2aPart) {
79-
if (a2aPart == null) {
80-
throw new IllegalArgumentException("A2A part cannot be null");
81-
}
84+
checkNotNull(a2aPart, "A2A part cannot be null");
8285

8386
if (a2aPart instanceof TextPart textPart) {
8487
com.google.genai.types.Part.Builder partBuilder =
@@ -220,9 +223,13 @@ private static com.google.genai.types.Part convertDataPartToGenAiPart(DataPart d
220223
}
221224

222225
try {
223-
String json = objectMapper.writeValueAsString(data);
226+
String json = objectMapper.writeValueAsString(dataPart);
227+
String wrappedJson = A2A_DATA_PART_START_TAG + json + A2A_DATA_PART_END_TAG;
228+
byte[] bytes = wrappedJson.getBytes(StandardCharsets.UTF_8);
224229
com.google.genai.types.Part.Builder builder =
225-
com.google.genai.types.Part.builder().text(json);
230+
com.google.genai.types.Part.builder()
231+
.inlineData(
232+
Blob.builder().data(bytes).mimeType(A2A_DATA_PART_TEXT_MIME_TYPE).build());
226233
if (!metadata.isEmpty()) {
227234
builder.partMetadata(metadata);
228235
}
@@ -334,6 +341,53 @@ private static DataPart createDataPartFromExecutableCode(
334341
return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
335342
}
336343

344+
/**
345+
* {@return true if the given Blob contains inlineData that represents a serialized A2A DataPart,
346+
* false otherwise}
347+
*
348+
* <p>A DataPart in inlineData is expected to have a "text/plain" MIME type and the content
349+
* wrapped in {@link #A2A_DATA_PART_START_TAG} and {@link #A2A_DATA_PART_END_TAG}.
350+
*
351+
* @param blob The Blob to check.
352+
*/
353+
private static boolean isDataPartInlineData(Blob blob) {
354+
String mimeType = blob.mimeType().orElse("");
355+
if (!mimeType.equals(A2A_DATA_PART_TEXT_MIME_TYPE)) {
356+
return false;
357+
}
358+
byte[] data = blob.data().orElse(null);
359+
if (data == null) {
360+
return false;
361+
}
362+
String str = new String(data, StandardCharsets.UTF_8);
363+
return str.startsWith(A2A_DATA_PART_START_TAG) && str.endsWith(A2A_DATA_PART_END_TAG);
364+
}
365+
366+
private static DataPart inlineDataToA2ADataPart(
367+
Blob blob, ImmutableMap<String, Object> metadata) {
368+
byte[] data = blob.data().orElse(null);
369+
if (data == null) {
370+
throw new IllegalArgumentException("Blob data cannot be null");
371+
}
372+
String str = new String(data, StandardCharsets.UTF_8);
373+
String jsonContent =
374+
str.substring(
375+
A2A_DATA_PART_START_TAG.length(), str.length() - A2A_DATA_PART_END_TAG.length());
376+
try {
377+
DataPart deserialized = objectMapper.readValue(jsonContent, DataPart.class);
378+
379+
ImmutableMap.Builder<String, Object> mergedMetadata = ImmutableMap.builder();
380+
if (deserialized.getMetadata() != null) {
381+
mergedMetadata.putAll(deserialized.getMetadata());
382+
}
383+
mergedMetadata.putAll(metadata);
384+
385+
return new DataPart(deserialized.getData(), mergedMetadata.buildKeepingLast());
386+
} catch (Exception e) {
387+
throw new IllegalArgumentException("Failed to parse DataPart payload from inlineData", e);
388+
}
389+
}
390+
337391
private PartConverter() {}
338392

339393
/** Convert a GenAI part into the A2A JSON representation. */
@@ -352,6 +406,10 @@ public static io.a2a.spec.Part<?> fromGenaiPart(Part part, boolean isPartial) {
352406
return new TextPart(part.text().get(), metadata.buildKeepingLast());
353407
}
354408

409+
if (part.inlineData().isPresent() && isDataPartInlineData(part.inlineData().get())) {
410+
return inlineDataToA2ADataPart(part.inlineData().get(), metadata.buildOrThrow());
411+
}
412+
355413
if (part.fileData().isPresent() || part.inlineData().isPresent()) {
356414
return filePartToA2A(part, metadata);
357415
}

a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class PartConverterTest {
3131

3232
@Test
3333
public void toGenaiPart_withNullPart_throwsException() {
34-
assertThrows(IllegalArgumentException.class, () -> PartConverter.toGenaiPart(null));
34+
assertThrows(NullPointerException.class, () -> PartConverter.toGenaiPart(null));
3535
}
3636

3737
@Test
@@ -194,13 +194,18 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons
194194
}
195195

196196
@Test
197-
public void toGenaiPart_withOtherDataPart_returnsGenaiTextPartWithJson() {
197+
public void toGenaiPart_withOtherDataPart_returnsGenaiInlineDataPartWithWrappedJson() {
198198
ImmutableMap<String, Object> data = ImmutableMap.of("key", "value");
199199
DataPart dataPart = new DataPart(data, null);
200200

201201
Part result = PartConverter.toGenaiPart(dataPart);
202202

203-
assertThat(result.text()).hasValue("{\"key\":\"value\"}");
203+
assertThat(result.inlineData()).isPresent();
204+
Blob blob = result.inlineData().get();
205+
assertThat(blob.mimeType()).hasValue("text/plain");
206+
String expectedContent =
207+
"<a2a_datapart_json>{\"data\":{\"key\":\"value\"},\"kind\":\"data\"}</a2a_datapart_json>";
208+
assertThat(new String(blob.data().get(), UTF_8)).isEqualTo(expectedContent);
204209
}
205210

206211
@Test
@@ -405,4 +410,41 @@ public void fromGenaiPart_withPartMetadata_propagatesMetadata() {
405410

406411
assertThat(result.getMetadata()).containsExactly("key", "value");
407412
}
413+
414+
@Test
415+
public void fromGenaiPart_withDataPartInlineData_returnsDataPart() {
416+
String wrappedJson =
417+
"<a2a_datapart_json>{\"data\":{\"key\":\"value\"},\"kind\":\"data\"}</a2a_datapart_json>";
418+
Part part =
419+
Part.builder()
420+
.inlineData(
421+
Blob.builder().mimeType("text/plain").data(wrappedJson.getBytes(UTF_8)).build())
422+
.build();
423+
424+
io.a2a.spec.Part<?> result = PartConverter.fromGenaiPart(part, false);
425+
426+
assertThat(result).isInstanceOf(DataPart.class);
427+
DataPart dataPart = (DataPart) result;
428+
assertThat(dataPart.getData()).containsExactly("key", "value");
429+
}
430+
431+
@Test
432+
public void fromGenaiPart_withDataPartInlineDataAndMetadata_returnsDataPartWithMergedMetadata() {
433+
String wrappedJson =
434+
"<a2a_datapart_json>{\"data\":{\"key\":\"value\"},\"metadata\":{\"metaKey\":\"metaValue\"},\"kind\":\"data\"}</a2a_datapart_json>";
435+
Part part =
436+
Part.builder()
437+
.inlineData(
438+
Blob.builder().mimeType("text/plain").data(wrappedJson.getBytes(UTF_8)).build())
439+
.partMetadata(ImmutableMap.of("partMetaKey", "partMetaValue"))
440+
.build();
441+
442+
io.a2a.spec.Part<?> result = PartConverter.fromGenaiPart(part, false);
443+
444+
assertThat(result).isInstanceOf(DataPart.class);
445+
DataPart dataPart = (DataPart) result;
446+
assertThat(dataPart.getData()).containsExactly("key", "value");
447+
assertThat(dataPart.getMetadata())
448+
.containsExactly("metaKey", "metaValue", "partMetaKey", "partMetaValue");
449+
}
408450
}

0 commit comments

Comments
 (0)