Skip to content

Commit 9ce78d7

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Update converters for task and artifact events; add long running tools ids
PiperOrigin-RevId: 882591822
1 parent be3b3f8 commit 9ce78d7

4 files changed

Lines changed: 249 additions & 141 deletions

File tree

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

Lines changed: 44 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ public static Optional<TextPart> toTextPart(io.a2a.spec.Part<?> part) {
7878
}
7979

8080
/** Convert an A2A JSON part into a Google GenAI part representation. */
81-
public static Optional<com.google.genai.types.Part> toGenaiPart(io.a2a.spec.Part<?> a2aPart) {
81+
public static com.google.genai.types.Part toGenaiPart(io.a2a.spec.Part<?> a2aPart) {
8282
if (a2aPart == null) {
83-
return Optional.empty();
83+
throw new IllegalArgumentException("A2A part cannot be null");
8484
}
8585

8686
if (a2aPart instanceof TextPart textPart) {
87-
return Optional.of(com.google.genai.types.Part.builder().text(textPart.getText()).build());
87+
return com.google.genai.types.Part.builder().text(textPart.getText()).build();
8888
}
8989

9090
if (a2aPart instanceof FilePart filePart) {
@@ -95,56 +95,41 @@ public static Optional<com.google.genai.types.Part> toGenaiPart(io.a2a.spec.Part
9595
return convertDataPartToGenAiPart(dataPart);
9696
}
9797

98-
logger.warn("Unsupported A2A part type: {}", a2aPart.getClass());
99-
return Optional.empty();
98+
throw new IllegalArgumentException("Unsupported A2A part type: " + a2aPart.getClass());
10099
}
101100

102101
public static ImmutableList<com.google.genai.types.Part> toGenaiParts(
103102
List<io.a2a.spec.Part<?>> a2aParts) {
104-
return a2aParts.stream()
105-
.map(PartConverter::toGenaiPart)
106-
.flatMap(Optional::stream)
107-
.collect(toImmutableList());
103+
return a2aParts.stream().map(PartConverter::toGenaiPart).collect(toImmutableList());
108104
}
109105

110-
private static Optional<com.google.genai.types.Part> convertFilePartToGenAiPart(
111-
FilePart filePart) {
106+
private static com.google.genai.types.Part convertFilePartToGenAiPart(FilePart filePart) {
112107
FileContent fileContent = filePart.getFile();
113108
if (fileContent instanceof FileWithUri fileWithUri) {
114-
return Optional.of(
115-
com.google.genai.types.Part.builder()
116-
.fileData(
117-
FileData.builder()
118-
.fileUri(fileWithUri.uri())
119-
.mimeType(fileWithUri.mimeType())
120-
.build())
121-
.build());
109+
return com.google.genai.types.Part.builder()
110+
.fileData(
111+
FileData.builder()
112+
.fileUri(fileWithUri.uri())
113+
.mimeType(fileWithUri.mimeType())
114+
.build())
115+
.build();
122116
}
123117

124118
if (fileContent instanceof FileWithBytes fileWithBytes) {
125119
String bytesString = fileWithBytes.bytes();
126120
if (bytesString == null) {
127-
logger.warn("FileWithBytes missing byte content");
128-
return Optional.empty();
129-
}
130-
try {
131-
byte[] decoded = Base64.getDecoder().decode(bytesString);
132-
return Optional.of(
133-
com.google.genai.types.Part.builder()
134-
.inlineData(Blob.builder().data(decoded).mimeType(fileWithBytes.mimeType()).build())
135-
.build());
136-
} catch (IllegalArgumentException e) {
137-
logger.warn("Failed to decode base64 file content", e);
138-
return Optional.empty();
121+
throw new GenAiFieldMissingException("FileWithBytes missing byte content");
139122
}
123+
byte[] decoded = Base64.getDecoder().decode(bytesString);
124+
return com.google.genai.types.Part.builder()
125+
.inlineData(Blob.builder().data(decoded).mimeType(fileWithBytes.mimeType()).build())
126+
.build();
140127
}
141128

142-
logger.warn("Unsupported FilePart content: {}", fileContent.getClass());
143-
return Optional.empty();
129+
throw new IllegalArgumentException("Unsupported FilePart content: " + fileContent.getClass());
144130
}
145131

146-
private static Optional<com.google.genai.types.Part> convertDataPartToGenAiPart(
147-
DataPart dataPart) {
132+
private static com.google.genai.types.Part convertDataPartToGenAiPart(DataPart dataPart) {
148133
Map<String, Object> data =
149134
Optional.ofNullable(dataPart.getData()).map(HashMap::new).orElseGet(HashMap::new);
150135
Map<String, Object> metadata =
@@ -154,67 +139,57 @@ private static Optional<com.google.genai.types.Part> convertDataPartToGenAiPart(
154139

155140
if ((data.containsKey(NAME_KEY) && data.containsKey(ARGS_KEY))
156141
|| metadataType.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) {
157-
String functionName = String.valueOf(data.getOrDefault(NAME_KEY, null));
158-
String functionId = String.valueOf(data.getOrDefault(ID_KEY, null));
142+
String functionName = String.valueOf(data.getOrDefault(NAME_KEY, ""));
143+
String functionId = String.valueOf(data.getOrDefault(ID_KEY, ""));
159144
Map<String, Object> args = coerceToMap(data.get(ARGS_KEY));
160-
return Optional.of(
161-
com.google.genai.types.Part.builder()
162-
.functionCall(
163-
FunctionCall.builder().name(functionName).id(functionId).args(args).build())
164-
.build());
145+
return com.google.genai.types.Part.builder()
146+
.functionCall(FunctionCall.builder().name(functionName).id(functionId).args(args).build())
147+
.build();
165148
}
166149

167150
if ((data.containsKey(NAME_KEY) && data.containsKey(RESPONSE_KEY))
168151
|| metadataType.equals(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())) {
169152
String functionName = String.valueOf(data.getOrDefault(NAME_KEY, ""));
170153
String functionId = String.valueOf(data.getOrDefault(ID_KEY, ""));
171154
Map<String, Object> response = coerceToMap(data.get(RESPONSE_KEY));
172-
return Optional.of(
173-
com.google.genai.types.Part.builder()
174-
.functionResponse(
175-
FunctionResponse.builder()
176-
.name(functionName)
177-
.id(functionId)
178-
.response(response)
179-
.build())
180-
.build());
155+
return com.google.genai.types.Part.builder()
156+
.functionResponse(
157+
FunctionResponse.builder()
158+
.name(functionName)
159+
.id(functionId)
160+
.response(response)
161+
.build())
162+
.build();
181163
}
182164

183165
if ((data.containsKey(CODE_KEY) && data.containsKey(LANGUAGE_KEY))
184166
|| metadataType.equals(A2ADataPartMetadataType.EXECUTABLE_CODE.getType())) {
185167
String code = String.valueOf(data.getOrDefault(CODE_KEY, ""));
186168
String language =
187169
String.valueOf(
188-
data.getOrDefault(LANGUAGE_KEY, Language.Known.LANGUAGE_UNSPECIFIED.toString())
189-
.toString());
190-
return Optional.of(
191-
com.google.genai.types.Part.builder()
192-
.executableCode(
193-
ExecutableCode.builder().code(code).language(new Language(language)).build())
194-
.build());
170+
data.getOrDefault(LANGUAGE_KEY, Language.Known.LANGUAGE_UNSPECIFIED.toString()));
171+
return com.google.genai.types.Part.builder()
172+
.executableCode(
173+
ExecutableCode.builder().code(code).language(new Language(language)).build())
174+
.build();
195175
}
196176

197177
if ((data.containsKey(OUTCOME_KEY) && data.containsKey(OUTPUT_KEY))
198178
|| metadataType.equals(A2ADataPartMetadataType.CODE_EXECUTION_RESULT.getType())) {
199179
String outcome =
200180
String.valueOf(data.getOrDefault(OUTCOME_KEY, Outcome.Known.OUTCOME_OK).toString());
201181
String output = String.valueOf(data.getOrDefault(OUTPUT_KEY, ""));
202-
return Optional.of(
203-
com.google.genai.types.Part.builder()
204-
.codeExecutionResult(
205-
CodeExecutionResult.builder()
206-
.outcome(new Outcome(outcome))
207-
.output(output)
208-
.build())
209-
.build());
182+
return com.google.genai.types.Part.builder()
183+
.codeExecutionResult(
184+
CodeExecutionResult.builder().outcome(new Outcome(outcome)).output(output).build())
185+
.build();
210186
}
211187

212188
try {
213189
String json = objectMapper.writeValueAsString(data);
214-
return Optional.of(com.google.genai.types.Part.builder().text(json).build());
190+
return com.google.genai.types.Part.builder().text(json).build();
215191
} catch (JsonProcessingException e) {
216-
logger.warn("Failed to serialize DataPart payload", e);
217-
return Optional.empty();
192+
throw new IllegalArgumentException("Failed to serialize DataPart payload", e);
218193
}
219194
}
220195

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

Lines changed: 95 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package com.google.adk.a2a.converters;
1717

1818
import static com.google.common.collect.ImmutableList.toImmutableList;
19+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
20+
import static com.google.common.collect.Streams.zip;
1921

2022
import com.google.adk.agents.InvocationContext;
2123
import com.google.adk.events.Event;
@@ -29,13 +31,15 @@
2931
import io.a2a.client.TaskEvent;
3032
import io.a2a.client.TaskUpdateEvent;
3133
import io.a2a.spec.Artifact;
34+
import io.a2a.spec.DataPart;
3235
import io.a2a.spec.Message;
3336
import io.a2a.spec.Task;
3437
import io.a2a.spec.TaskArtifactUpdateEvent;
3538
import io.a2a.spec.TaskState;
3639
import io.a2a.spec.TaskStatusUpdateEvent;
3740
import java.time.Instant;
3841
import java.util.List;
42+
import java.util.Map;
3943
import java.util.Objects;
4044
import java.util.Optional;
4145
import java.util.UUID;
@@ -70,6 +74,14 @@ public static Optional<Event> clientEventToEvent(
7074
throw new IllegalArgumentException("Unsupported ClientEvent type: " + event.getClass());
7175
}
7276

77+
private static boolean isPartial(Map<String, Object> metadata) {
78+
if (metadata == null) {
79+
return false;
80+
}
81+
return Objects.equals(
82+
metadata.getOrDefault(PartConverter.A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, false), true);
83+
}
84+
7385
/**
7486
* Converts a A2A {@link TaskUpdateEvent} to an ADK {@link Event}, if applicable. Returns null if
7587
* the event is not a final update for TaskArtifactUpdateEvent or if the message is empty for
@@ -85,7 +97,14 @@ private static Optional<Event> handleTaskUpdate(
8597
boolean isAppend = Objects.equals(artifactEvent.isAppend(), true);
8698
boolean isLastChunk = Objects.equals(artifactEvent.isLastChunk(), true);
8799

100+
if (isLastChunk && isPartial(artifactEvent.getMetadata())) {
101+
return Optional.empty();
102+
}
103+
88104
Event eventPart = artifactToEvent(artifactEvent.getArtifact(), context);
105+
if (eventPart.content().flatMap(Content::parts).orElse(ImmutableList.of()).isEmpty()) {
106+
return Optional.empty();
107+
}
89108
eventPart.setPartial(isAppend || !isLastChunk);
90109
// append=true, lastChunk=false: emit as partial, update aggregation
91110
// append=false, lastChunk=false: emit as partial, reset aggregation
@@ -115,26 +134,21 @@ private static Optional<Event> handleTaskUpdate(
115134
.map(builder -> builder.turnComplete(true))
116135
.map(builder -> builder.partial(false))
117136
.map(Event.Builder::build);
118-
} else {
119-
return messageEvent;
120137
}
138+
return messageEvent;
121139
}
122140
throw new IllegalArgumentException(
123141
"Unsupported TaskUpdateEvent type: " + updateEvent.getClass());
124142
}
125143

126144
/** Converts an artifact to an ADK event. */
127145
public static Event artifactToEvent(Artifact artifact, InvocationContext invocationContext) {
128-
Message message =
129-
new Message.Builder().role(Message.Role.AGENT).parts(artifact.parts()).build();
130-
return messageToEvent(message, invocationContext);
131-
}
132-
133-
/** Converts an A2A message back to ADK events. */
134-
public static Event messageToEvent(Message message, InvocationContext invocationContext) {
135-
return remoteAgentEventBuilder(invocationContext)
136-
.content(fromModelParts(PartConverter.toGenaiParts(message.getParts())))
137-
.build();
146+
Event.Builder eventBuilder = remoteAgentEventBuilder(invocationContext);
147+
ImmutableList<Part> genaiParts = PartConverter.toGenaiParts(artifact.parts());
148+
eventBuilder
149+
.content(fromModelParts(genaiParts))
150+
.longRunningToolIds(getLongRunningToolIds(artifact.parts(), genaiParts));
151+
return eventBuilder.build();
138152
}
139153

140154
/** Converts an A2A message for a failed task to ADK event filling in the error message. */
@@ -147,6 +161,13 @@ public static Event messageToFailedEvent(Message message, InvocationContext invo
147161
return builder.build();
148162
}
149163

164+
/** Converts an A2A message back to ADK events. */
165+
public static Event messageToEvent(Message message, InvocationContext invocationContext) {
166+
return remoteAgentEventBuilder(invocationContext)
167+
.content(fromModelParts(PartConverter.toGenaiParts(message.getParts())))
168+
.build();
169+
}
170+
150171
/**
151172
* Converts an A2A message back to ADK events. For streaming task in pending state it sets the
152173
* thought field to true, to mark them as thought updates.
@@ -168,25 +189,71 @@ public static Event messageToEvent(
168189
* If none of these are present, an empty event is returned.
169190
*/
170191
public static Event taskToEvent(Task task, InvocationContext invocationContext) {
171-
Message taskMessage = null;
172-
173-
if (!task.getArtifacts().isEmpty()) {
174-
taskMessage =
175-
new Message.Builder()
176-
.messageId("")
177-
.role(Message.Role.AGENT)
178-
.parts(Iterables.getLast(task.getArtifacts()).parts())
179-
.build();
180-
} else if (task.getStatus().message() != null) {
181-
taskMessage = task.getStatus().message();
182-
} else if (!task.getHistory().isEmpty()) {
183-
taskMessage = Iterables.getLast(task.getHistory());
192+
ImmutableList.Builder<Part> genaiParts = ImmutableList.builder();
193+
ImmutableSet.Builder<String> longRunningToolIds = ImmutableSet.builder();
194+
195+
for (Artifact artifact : task.getArtifacts()) {
196+
ImmutableList<Part> converted = PartConverter.toGenaiParts(artifact.parts());
197+
longRunningToolIds.addAll(getLongRunningToolIds(artifact.parts(), converted));
198+
genaiParts.addAll(converted);
199+
}
200+
201+
Event.Builder eventBuilder = remoteAgentEventBuilder(invocationContext);
202+
203+
if (task.getStatus().message() != null) {
204+
ImmutableList<Part> msgParts =
205+
PartConverter.toGenaiParts(task.getStatus().message().getParts());
206+
longRunningToolIds.addAll(
207+
getLongRunningToolIds(task.getStatus().message().getParts(), msgParts));
208+
if (task.getStatus().state() == TaskState.FAILED
209+
&& msgParts.size() == 1
210+
&& msgParts.get(0).text().isPresent()) {
211+
eventBuilder.errorMessage(msgParts.get(0).text().get());
212+
} else {
213+
genaiParts.addAll(msgParts);
214+
}
184215
}
185216

186-
if (taskMessage != null) {
187-
return messageToEvent(taskMessage, invocationContext);
217+
ImmutableList<Part> finalParts = genaiParts.build();
218+
boolean isFinal =
219+
task.getStatus().state().isFinal() || task.getStatus().state() == TaskState.INPUT_REQUIRED;
220+
221+
if (finalParts.isEmpty() && !isFinal) {
222+
return emptyEvent(invocationContext);
188223
}
189-
return emptyEvent(invocationContext);
224+
if (!finalParts.isEmpty()) {
225+
eventBuilder.content(fromModelParts(finalParts));
226+
}
227+
if (task.getStatus().state() == TaskState.INPUT_REQUIRED) {
228+
eventBuilder.longRunningToolIds(longRunningToolIds.build());
229+
}
230+
eventBuilder.turnComplete(isFinal);
231+
return eventBuilder.build();
232+
}
233+
234+
private static ImmutableSet<String> getLongRunningToolIds(
235+
List<io.a2a.spec.Part<?>> parts, List<Part> convertedParts) {
236+
return zip(
237+
parts.stream(),
238+
convertedParts.stream(),
239+
(part, convertedPart) -> {
240+
if (!(part instanceof DataPart dataPart)) {
241+
return Optional.<String>empty();
242+
}
243+
Object isLongRunning =
244+
dataPart
245+
.getMetadata()
246+
.get(PartConverter.A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY);
247+
if (!Objects.equals(isLongRunning, true)) {
248+
return Optional.<String>empty();
249+
}
250+
if (convertedPart.functionCall().isEmpty()) {
251+
return Optional.<String>empty();
252+
}
253+
return convertedPart.functionCall().get().id();
254+
})
255+
.flatMap(Optional::stream)
256+
.collect(toImmutableSet());
190257
}
191258

192259
private static Event emptyEvent(InvocationContext invocationContext) {

0 commit comments

Comments
 (0)