Skip to content
Merged
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
83 changes: 83 additions & 0 deletions .github/workflows/analyze-releases-for-adk-docs-updates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Analyze New Release for ADK Docs Updates

on:
# Runs on every new release.
release:
types: [published]
# Manual trigger for testing and retrying.
workflow_dispatch:
inputs:
start_tag:
description: 'Older release tag (base), e.g. v0.1.0'
required: false
type: string
end_tag:
description: 'Newer release tag (head), e.g. v0.2.0'
required: false
type: string
dry_run:
description: 'Dry run: preview only. Set to false to actually create the issue and PRs.'
required: false
default: true
type: boolean

jobs:
analyze-new-release-for-adk-docs-updates:
runs-on: ubuntu-latest
# These permissions apply to this repo's GITHUB_TOKEN (used only for checkout).
# The agent writes issues, branches and pull requests to the docs repo via the
# ADK_TRIAGE_AGENT PAT, which must have issues + pull-requests + contents write there.
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'

- name: Cache Maven packages
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-

- name: Run the ADK Docs Release Analyzer
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY_FOR_DOCS_AGENTS }}
GOOGLE_GENAI_USE_VERTEXAI: '0'
DOC_OWNER: 'google'
CODE_OWNER: 'google'
DOC_REPO: 'adk-docs'
CODE_REPO: 'adk-java'
ANALYZER_START_TAG: ${{ github.event.inputs.start_tag }}
ANALYZER_END_TAG: ${{ github.event.inputs.end_tag }}
# Defaults to dry-run (preview only). Flip to false via manual dispatch
# to actually create the issue and PRs.
ANALYZER_DRY_RUN: ${{ github.event.inputs.dry_run || 'true' }}
shell: bash
run: |
set -euo pipefail
if [[ "${ANALYZER_DRY_RUN}" == "false" ]]; then
args="--no-dry-run"
else
args="--dry-run"
fi
if [[ -n "${ANALYZER_START_TAG:-}" ]]; then
args="${args} --start-tag ${ANALYZER_START_TAG}"
fi
if [[ -n "${ANALYZER_END_TAG:-}" ]]; then
args="${args} --end-tag ${ANALYZER_END_TAG}"
fi
# Install ADK libs + sample, then run exec:java scoped to this module
# (exec:java with -am would also run on the parent, which has no mainClass).
./mvnw -q -pl contrib/samples/github/adkreleasedocs -am install -DskipTests
./mvnw -q -pl contrib/samples/github/adkreleasedocs exec:java \
-Dexec.args="${args}"
68 changes: 63 additions & 5 deletions a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.adk.a2a.converters;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;

import com.fasterxml.jackson.core.JsonProcessingException;
Expand All @@ -39,6 +40,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 +68,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 All @@ -76,9 +81,7 @@ public static Optional<TextPart> toTextPart(io.a2a.spec.Part<?> part) {

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

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

try {
String json = objectMapper.writeValueAsString(data);
String json = objectMapper.writeValueAsString(dataPart);
String wrappedJson = A2A_DATA_PART_START_TAG + json + A2A_DATA_PART_END_TAG;
byte[] bytes = wrappedJson.getBytes(StandardCharsets.UTF_8);
com.google.genai.types.Part.Builder builder =
com.google.genai.types.Part.builder().text(json);
com.google.genai.types.Part.builder()
.inlineData(
Blob.builder().data(bytes).mimeType(A2A_DATA_PART_TEXT_MIME_TYPE).build());
if (!metadata.isEmpty()) {
builder.partMetadata(metadata);
}
Expand Down Expand Up @@ -334,6 +341,53 @@ private static DataPart createDataPartFromExecutableCode(
return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
}

/**
* {@return true if the given Blob contains inlineData that represents a serialized A2A DataPart,
* false otherwise}
*
* <p>A DataPart in inlineData is expected to have a "text/plain" MIME type and the content
* wrapped in {@link #A2A_DATA_PART_START_TAG} and {@link #A2A_DATA_PART_END_TAG}.
*
* @param blob The Blob to check.
*/
private static boolean isDataPartInlineData(Blob blob) {
String mimeType = blob.mimeType().orElse("");
if (!mimeType.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);
}

private static DataPart inlineDataToA2ADataPart(
Blob blob, ImmutableMap<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 {
DataPart deserialized = objectMapper.readValue(jsonContent, DataPart.class);

ImmutableMap.Builder<String, Object> mergedMetadata = ImmutableMap.builder();
if (deserialized.getMetadata() != null) {
mergedMetadata.putAll(deserialized.getMetadata());
}
mergedMetadata.putAll(metadata);

return new DataPart(deserialized.getData(), mergedMetadata.buildKeepingLast());
} 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 @@ -352,6 +406,10 @@ public static io.a2a.spec.Part<?> fromGenaiPart(Part part, boolean isPartial) {
return new TextPart(part.text().get(), metadata.buildKeepingLast());
}

if (part.inlineData().isPresent() && isDataPartInlineData(part.inlineData().get())) {
return inlineDataToA2ADataPart(part.inlineData().get(), metadata.buildOrThrow());
}

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 @@ -31,7 +31,7 @@ public class PartConverterTest {

@Test
public void toGenaiPart_withNullPart_throwsException() {
assertThrows(IllegalArgumentException.class, () -> PartConverter.toGenaiPart(null));
assertThrows(NullPointerException.class, () -> PartConverter.toGenaiPart(null));
}

@Test
Expand Down Expand Up @@ -194,13 +194,18 @@ 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>{\"data\":{\"key\":\"value\"},\"kind\":\"data\"}</a2a_datapart_json>";
assertThat(new String(blob.data().get(), UTF_8)).isEqualTo(expectedContent);
}

@Test
Expand Down Expand Up @@ -405,4 +410,41 @@ public void fromGenaiPart_withPartMetadata_propagatesMetadata() {

assertThat(result.getMetadata()).containsExactly("key", "value");
}

@Test
public void fromGenaiPart_withDataPartInlineData_returnsDataPart() {
String wrappedJson =
"<a2a_datapart_json>{\"data\":{\"key\":\"value\"},\"kind\":\"data\"}</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");
}

@Test
public void fromGenaiPart_withDataPartInlineDataAndMetadata_returnsDataPartWithMergedMetadata() {
String wrappedJson =
"<a2a_datapart_json>{\"data\":{\"key\":\"value\"},\"metadata\":{\"metaKey\":\"metaValue\"},\"kind\":\"data\"}</a2a_datapart_json>";
Part part =
Part.builder()
.inlineData(
Blob.builder().mimeType("text/plain").data(wrappedJson.getBytes(UTF_8)).build())
.partMetadata(ImmutableMap.of("partMetaKey", "partMetaValue"))
.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");
assertThat(dataPart.getMetadata())
.containsExactly("metaKey", "metaValue", "partMetaKey", "partMetaValue");
}
}
Loading
Loading