Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = "<a2a_datapart_json>";
public static final String A2A_DATA_PART_END_TAG = "</a2a_datapart_json>";
public static final String A2A_DATA_PART_TEXT_MIME_TYPE = "text/plain";

public static Optional<TextPart> toTextPart(io.a2a.spec.Part<?> part) {
if (part instanceof TextPart textPart) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<String, Object> 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<String, Object> 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. */
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,17 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons
}

@Test
public void toGenaiPart_withOtherDataPart_returnsGenaiTextPartWithJson() {
public void toGenaiPart_withOtherDataPart_returnsGenaiInlineDataPartWithWrappedJson() {
ImmutableMap<String, Object> 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 = "<a2a_datapart_json>{\"key\":\"value\"}</a2a_datapart_json>";
assertThat(new String(blob.data().get(), UTF_8)).isEqualTo(expectedContent);
}

@Test
Expand Down Expand Up @@ -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 = "<a2a_datapart_json>{\"key\":\"value\"}</a2a_datapart_json>";
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");
}
}