diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d6a5f76bd..b0f3ba770 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,4 @@ { - ".": "0.8.0" + ".": "0.9.0" } + diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5b9e5eb..ab111e90c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,61 @@ # Changelog +## [0.9.0](https://github.com/google/adk-java/compare/v0.8.0...v0.9.0) (2026-03-13) + + +### ⚠ BREAKING CHANGES + +* refactor ApiClient constructors hierarchy to remove Optional parameters +* remove deprecated LlmAgent.canonicalTools method +* remove deprecated LoadArtifactsTool.loadArtifacts method +* update LoopAgent's maxIteration field and methods to be @Nullable instead of Optional +* Remove Optional parameters in EventActions +* remove deprecated url method in ComputerState.Builder +* Remove deprecated create method in ResponseProcessor +* remove McpAsyncToolset constructors +* use @Nullable fields in Event class +* remove methods with Optional params from VertexCredential.Builder + +### Features + +* add formatting to the RemoteA2A agent so it filters out the previous agent responses and updates the context of the function calls and responses ([0d6dd55](https://github.com/google/adk-java/commit/0d6dd55f4870007e79db23e21bd261879dbfba79)) +* add multiple LLM responses to LLM recordings for conformance tests ([bdfb7a7](https://github.com/google/adk-java/commit/bdfb7a72188ce6e72c12c16c0abedb824b846160)) +* add support for gemini models in VertexAiRagRetrieval ([924fb71](https://github.com/google/adk-java/commit/924fb7174855b46a58be43373c1a29284c47dfa8)) +* Fixing the spans produced by agent calls to have the right parent spans ([3c8f488](https://github.com/google/adk-java/commit/3c8f4886f0e4c76abdbeb64a348bfccd5c16120e)) +* Fixing the spans produced by agent calls to have the right parent spans ([973f887](https://github.com/google/adk-java/commit/973f88743cabebcd2e6e7a8d5f141142b596dbbb)) +* refactor ApiClient constructors hierarchy to remove Optional parameters ([910d727](https://github.com/google/adk-java/commit/910d727f1981498151dea4cb91b9e5836f91e3ba)) +* Remove deprecated create method in ResponseProcessor ([5e1e1d4](https://github.com/google/adk-java/commit/5e1e1d434fa1f3931af30194422800757de96cb6)) +* remove deprecated LlmAgent.canonicalTools method ([aabf15a](https://github.com/google/adk-java/commit/aabf15a526ba525cdb47c74c246c178eff1851d5)) +* remove deprecated LoadArtifactsTool.loadArtifacts method ([bc38558](https://github.com/google/adk-java/commit/bc385589057a6daf0209a335280bf19d20b2126b)) +* remove deprecated url method in ComputerState.Builder ([a86ede0](https://github.com/google/adk-java/commit/a86ede007c3442ed73ee08a5c6ad0e2efa12998a)) +* remove executionId method that takes Optional param from CodeExecutionUtils ([be3b3f8](https://github.com/google/adk-java/commit/be3b3f8360888ea1f13796969bb19893c32727e0)) +* remove McpAsyncToolset constructors ([82ef5ac](https://github.com/google/adk-java/commit/82ef5ac2689e01676aa95d2616e3b4d8463e573e)) +* remove methods with Optional params from VertexCredential.Builder ([0b9057c](https://github.com/google/adk-java/commit/0b9057c9ccab98ea58597ec55b8168e32ac7c9a6)) +* Remove Optional parameters in EventActions ([b8316b1](https://github.com/google/adk-java/commit/b8316b1944ce17cc9208963cc09d900c379444c6)) +* replace Optional type of version in BaseArtifactService.loadArtifact with Nullable ([5fd4c53](https://github.com/google/adk-java/commit/5fd4c53c88e977d004b9eee8fa3697625ec85f47)) +* Trigger traceCallLlm to set call_llm attributes before span ends ([d9d84ee](https://github.com/google/adk-java/commit/d9d84ee67406cce8eeb66abcf1be24fad9c58e29)) +* Update converters for task and artifact events; add long running tools ids ([9ce78d7](https://github.com/google/adk-java/commit/9ce78d7c3e1b0fb6d8d4fdce9052a572ffb9e515)) +* update LoopAgent's maxIteration field and methods to be @Nullable instead of Optional ([e0d833b](https://github.com/google/adk-java/commit/e0d833b337e958e299d0d11a03f6bfa1468731bc)) +* update return type for artifactDelta getter and setter to Map from ConcurrentMap ([d1d5539](https://github.com/google/adk-java/commit/d1d5539ef763b6bfd5057c6ea0f2591225a98535)) +* update return type for requestedToolConfirmations getter and setter to Map from ConcurrentMap ([143b656](https://github.com/google/adk-java/commit/143b656949d61363d135e0b74ef5696e78eb270a)) +* update return type for stateDelta() to Map from ConcurrentMap ([3f6504e](https://github.com/google/adk-java/commit/3f6504e9416f9f644ef431e612ec983b9a2edd9d)) +* update State constructors to accept general Map types ([c6fdb63](https://github.com/google/adk-java/commit/c6fdb63c92e2f3481a01cfeafa946b6dce728c51)) +* use @Nullable fields in Event class ([67b602f](https://github.com/google/adk-java/commit/67b602f245f564238ea22298a37bf70049e56a12)) + + +### Bug Fixes + +* Explicitly setting the otel parent spans in agents, llm flow and function calls ([20f863f](https://github.com/google/adk-java/commit/20f863f716f653979551c481d85d4e7fa56a35da)) +* Make sure that `InvocationContext.callbackContextData` remains the same instance ([14ee28b](https://github.com/google/adk-java/commit/14ee28ba593a9f6f5f7b9bb6003441539fe33a18)) +* Removing deprecated InvocationContext methods ([41f5af0](https://github.com/google/adk-java/commit/41f5af0dceb78501ca8b94e434e4d751f608a699)) +* Removing deprecated methods in Runner ([0d8e22d](https://github.com/google/adk-java/commit/0d8e22d6e9fe4e8d29c87d485915ba51a22eb350)) +* Removing deprecated methods in Runner ([b857f01](https://github.com/google/adk-java/commit/b857f010a0f51df0eb25ecdc364465ffdd9fef65)) + + +### Miscellaneous Chores + +* override new version to 0.9.0 ([a47b651](https://github.com/google/adk-java/commit/a47b651b5c4868a603fd79df164b70bc712c3a80)) + ## [0.8.0](https://github.com/google/adk-java/compare/v0.7.0...v0.8.0) (2026-03-06) diff --git a/README.md b/README.md index 4a5dab81f..de1cfbef7 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ If you're using Maven, add the following to your dependencies: com.google.adk google-adk - 0.8.0 + 0.9.0 com.google.adk google-adk-dev - 0.8.0 + 0.9.0 ``` diff --git a/a2a/pom.xml b/a2a/pom.xml index 5857720fd..a2f9d9456 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT google-adk-a2a 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 36af6cc8b..61f24fa21 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 @@ -78,13 +78,13 @@ public static Optional toTextPart(io.a2a.spec.Part part) { } /** Convert an A2A JSON part into a Google GenAI part representation. */ - public static Optional toGenaiPart(io.a2a.spec.Part a2aPart) { + public static com.google.genai.types.Part toGenaiPart(io.a2a.spec.Part a2aPart) { if (a2aPart == null) { - return Optional.empty(); + throw new IllegalArgumentException("A2A part cannot be null"); } if (a2aPart instanceof TextPart textPart) { - return Optional.of(com.google.genai.types.Part.builder().text(textPart.getText()).build()); + return com.google.genai.types.Part.builder().text(textPart.getText()).build(); } if (a2aPart instanceof FilePart filePart) { @@ -95,56 +95,41 @@ public static Optional toGenaiPart(io.a2a.spec.Part return convertDataPartToGenAiPart(dataPart); } - logger.warn("Unsupported A2A part type: {}", a2aPart.getClass()); - return Optional.empty(); + throw new IllegalArgumentException("Unsupported A2A part type: " + a2aPart.getClass()); } public static ImmutableList toGenaiParts( List> a2aParts) { - return a2aParts.stream() - .map(PartConverter::toGenaiPart) - .flatMap(Optional::stream) - .collect(toImmutableList()); + return a2aParts.stream().map(PartConverter::toGenaiPart).collect(toImmutableList()); } - private static Optional convertFilePartToGenAiPart( - FilePart filePart) { + private static com.google.genai.types.Part convertFilePartToGenAiPart(FilePart filePart) { FileContent fileContent = filePart.getFile(); if (fileContent instanceof FileWithUri fileWithUri) { - return Optional.of( - com.google.genai.types.Part.builder() - .fileData( - FileData.builder() - .fileUri(fileWithUri.uri()) - .mimeType(fileWithUri.mimeType()) - .build()) - .build()); + return com.google.genai.types.Part.builder() + .fileData( + FileData.builder() + .fileUri(fileWithUri.uri()) + .mimeType(fileWithUri.mimeType()) + .build()) + .build(); } if (fileContent instanceof FileWithBytes fileWithBytes) { String bytesString = fileWithBytes.bytes(); if (bytesString == null) { - logger.warn("FileWithBytes missing byte content"); - return Optional.empty(); - } - try { - byte[] decoded = Base64.getDecoder().decode(bytesString); - return Optional.of( - com.google.genai.types.Part.builder() - .inlineData(Blob.builder().data(decoded).mimeType(fileWithBytes.mimeType()).build()) - .build()); - } catch (IllegalArgumentException e) { - logger.warn("Failed to decode base64 file content", e); - return Optional.empty(); + throw new GenAiFieldMissingException("FileWithBytes missing byte content"); } + byte[] decoded = Base64.getDecoder().decode(bytesString); + return com.google.genai.types.Part.builder() + .inlineData(Blob.builder().data(decoded).mimeType(fileWithBytes.mimeType()).build()) + .build(); } - logger.warn("Unsupported FilePart content: {}", fileContent.getClass()); - return Optional.empty(); + throw new IllegalArgumentException("Unsupported FilePart content: " + fileContent.getClass()); } - private static Optional convertDataPartToGenAiPart( - DataPart dataPart) { + private static com.google.genai.types.Part convertDataPartToGenAiPart(DataPart dataPart) { Map data = Optional.ofNullable(dataPart.getData()).map(HashMap::new).orElseGet(HashMap::new); Map metadata = @@ -154,14 +139,12 @@ private static Optional convertDataPartToGenAiPart( if ((data.containsKey(NAME_KEY) && data.containsKey(ARGS_KEY)) || metadataType.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) { - String functionName = String.valueOf(data.getOrDefault(NAME_KEY, null)); - String functionId = String.valueOf(data.getOrDefault(ID_KEY, null)); + String functionName = String.valueOf(data.getOrDefault(NAME_KEY, "")); + String functionId = String.valueOf(data.getOrDefault(ID_KEY, "")); Map args = coerceToMap(data.get(ARGS_KEY)); - return Optional.of( - com.google.genai.types.Part.builder() - .functionCall( - FunctionCall.builder().name(functionName).id(functionId).args(args).build()) - .build()); + return com.google.genai.types.Part.builder() + .functionCall(FunctionCall.builder().name(functionName).id(functionId).args(args).build()) + .build(); } if ((data.containsKey(NAME_KEY) && data.containsKey(RESPONSE_KEY)) @@ -169,15 +152,14 @@ private static Optional convertDataPartToGenAiPart( String functionName = String.valueOf(data.getOrDefault(NAME_KEY, "")); String functionId = String.valueOf(data.getOrDefault(ID_KEY, "")); Map response = coerceToMap(data.get(RESPONSE_KEY)); - return Optional.of( - com.google.genai.types.Part.builder() - .functionResponse( - FunctionResponse.builder() - .name(functionName) - .id(functionId) - .response(response) - .build()) - .build()); + return com.google.genai.types.Part.builder() + .functionResponse( + FunctionResponse.builder() + .name(functionName) + .id(functionId) + .response(response) + .build()) + .build(); } if ((data.containsKey(CODE_KEY) && data.containsKey(LANGUAGE_KEY)) @@ -185,13 +167,11 @@ private static Optional convertDataPartToGenAiPart( String code = String.valueOf(data.getOrDefault(CODE_KEY, "")); String language = String.valueOf( - data.getOrDefault(LANGUAGE_KEY, Language.Known.LANGUAGE_UNSPECIFIED.toString()) - .toString()); - return Optional.of( - com.google.genai.types.Part.builder() - .executableCode( - ExecutableCode.builder().code(code).language(new Language(language)).build()) - .build()); + data.getOrDefault(LANGUAGE_KEY, Language.Known.LANGUAGE_UNSPECIFIED.toString())); + return com.google.genai.types.Part.builder() + .executableCode( + ExecutableCode.builder().code(code).language(new Language(language)).build()) + .build(); } if ((data.containsKey(OUTCOME_KEY) && data.containsKey(OUTPUT_KEY)) @@ -199,22 +179,17 @@ private static Optional convertDataPartToGenAiPart( String outcome = String.valueOf(data.getOrDefault(OUTCOME_KEY, Outcome.Known.OUTCOME_OK).toString()); String output = String.valueOf(data.getOrDefault(OUTPUT_KEY, "")); - return Optional.of( - com.google.genai.types.Part.builder() - .codeExecutionResult( - CodeExecutionResult.builder() - .outcome(new Outcome(outcome)) - .output(output) - .build()) - .build()); + return com.google.genai.types.Part.builder() + .codeExecutionResult( + CodeExecutionResult.builder().outcome(new Outcome(outcome)).output(output).build()) + .build(); } try { String json = objectMapper.writeValueAsString(data); - return Optional.of(com.google.genai.types.Part.builder().text(json).build()); + return com.google.genai.types.Part.builder().text(json).build(); } catch (JsonProcessingException e) { - logger.warn("Failed to serialize DataPart payload", e); - return Optional.empty(); + throw new IllegalArgumentException("Failed to serialize DataPart payload", e); } } diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java index f3be48c1b..503432a30 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java @@ -16,6 +16,8 @@ package com.google.adk.a2a.converters; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Streams.zip; import com.google.adk.agents.InvocationContext; import com.google.adk.events.Event; @@ -29,6 +31,7 @@ import io.a2a.client.TaskEvent; import io.a2a.client.TaskUpdateEvent; import io.a2a.spec.Artifact; +import io.a2a.spec.DataPart; import io.a2a.spec.Message; import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; @@ -36,6 +39,7 @@ import io.a2a.spec.TaskStatusUpdateEvent; import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -70,6 +74,14 @@ public static Optional clientEventToEvent( throw new IllegalArgumentException("Unsupported ClientEvent type: " + event.getClass()); } + private static boolean isPartial(Map metadata) { + if (metadata == null) { + return false; + } + return Objects.equals( + metadata.getOrDefault(PartConverter.A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, false), true); + } + /** * Converts a A2A {@link TaskUpdateEvent} to an ADK {@link Event}, if applicable. Returns null if * the event is not a final update for TaskArtifactUpdateEvent or if the message is empty for @@ -85,7 +97,14 @@ private static Optional handleTaskUpdate( boolean isAppend = Objects.equals(artifactEvent.isAppend(), true); boolean isLastChunk = Objects.equals(artifactEvent.isLastChunk(), true); + if (isLastChunk && isPartial(artifactEvent.getMetadata())) { + return Optional.empty(); + } + Event eventPart = artifactToEvent(artifactEvent.getArtifact(), context); + if (eventPart.content().flatMap(Content::parts).orElse(ImmutableList.of()).isEmpty()) { + return Optional.empty(); + } eventPart.setPartial(isAppend || !isLastChunk); // append=true, lastChunk=false: emit as partial, update aggregation // append=false, lastChunk=false: emit as partial, reset aggregation @@ -115,9 +134,8 @@ private static Optional handleTaskUpdate( .map(builder -> builder.turnComplete(true)) .map(builder -> builder.partial(false)) .map(Event.Builder::build); - } else { - return messageEvent; } + return messageEvent; } throw new IllegalArgumentException( "Unsupported TaskUpdateEvent type: " + updateEvent.getClass()); @@ -125,16 +143,12 @@ private static Optional handleTaskUpdate( /** Converts an artifact to an ADK event. */ public static Event artifactToEvent(Artifact artifact, InvocationContext invocationContext) { - Message message = - new Message.Builder().role(Message.Role.AGENT).parts(artifact.parts()).build(); - return messageToEvent(message, invocationContext); - } - - /** Converts an A2A message back to ADK events. */ - public static Event messageToEvent(Message message, InvocationContext invocationContext) { - return remoteAgentEventBuilder(invocationContext) - .content(fromModelParts(PartConverter.toGenaiParts(message.getParts()))) - .build(); + Event.Builder eventBuilder = remoteAgentEventBuilder(invocationContext); + ImmutableList genaiParts = PartConverter.toGenaiParts(artifact.parts()); + eventBuilder + .content(fromModelParts(genaiParts)) + .longRunningToolIds(getLongRunningToolIds(artifact.parts(), genaiParts)); + return eventBuilder.build(); } /** 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 return builder.build(); } + /** Converts an A2A message back to ADK events. */ + public static Event messageToEvent(Message message, InvocationContext invocationContext) { + return remoteAgentEventBuilder(invocationContext) + .content(fromModelParts(PartConverter.toGenaiParts(message.getParts()))) + .build(); + } + /** * Converts an A2A message back to ADK events. For streaming task in pending state it sets the * thought field to true, to mark them as thought updates. @@ -168,25 +189,71 @@ public static Event messageToEvent( * If none of these are present, an empty event is returned. */ public static Event taskToEvent(Task task, InvocationContext invocationContext) { - Message taskMessage = null; - - if (!task.getArtifacts().isEmpty()) { - taskMessage = - new Message.Builder() - .messageId("") - .role(Message.Role.AGENT) - .parts(Iterables.getLast(task.getArtifacts()).parts()) - .build(); - } else if (task.getStatus().message() != null) { - taskMessage = task.getStatus().message(); - } else if (!task.getHistory().isEmpty()) { - taskMessage = Iterables.getLast(task.getHistory()); + ImmutableList.Builder genaiParts = ImmutableList.builder(); + ImmutableSet.Builder longRunningToolIds = ImmutableSet.builder(); + + for (Artifact artifact : task.getArtifacts()) { + ImmutableList converted = PartConverter.toGenaiParts(artifact.parts()); + longRunningToolIds.addAll(getLongRunningToolIds(artifact.parts(), converted)); + genaiParts.addAll(converted); + } + + Event.Builder eventBuilder = remoteAgentEventBuilder(invocationContext); + + if (task.getStatus().message() != null) { + ImmutableList msgParts = + PartConverter.toGenaiParts(task.getStatus().message().getParts()); + longRunningToolIds.addAll( + getLongRunningToolIds(task.getStatus().message().getParts(), msgParts)); + if (task.getStatus().state() == TaskState.FAILED + && msgParts.size() == 1 + && msgParts.get(0).text().isPresent()) { + eventBuilder.errorMessage(msgParts.get(0).text().get()); + } else { + genaiParts.addAll(msgParts); + } } - if (taskMessage != null) { - return messageToEvent(taskMessage, invocationContext); + ImmutableList finalParts = genaiParts.build(); + boolean isFinal = + task.getStatus().state().isFinal() || task.getStatus().state() == TaskState.INPUT_REQUIRED; + + if (finalParts.isEmpty() && !isFinal) { + return emptyEvent(invocationContext); } - return emptyEvent(invocationContext); + if (!finalParts.isEmpty()) { + eventBuilder.content(fromModelParts(finalParts)); + } + if (task.getStatus().state() == TaskState.INPUT_REQUIRED) { + eventBuilder.longRunningToolIds(longRunningToolIds.build()); + } + eventBuilder.turnComplete(isFinal); + return eventBuilder.build(); + } + + private static ImmutableSet getLongRunningToolIds( + List> parts, List convertedParts) { + return zip( + parts.stream(), + convertedParts.stream(), + (part, convertedPart) -> { + if (!(part instanceof DataPart dataPart)) { + return Optional.empty(); + } + Object isLongRunning = + dataPart + .getMetadata() + .get(PartConverter.A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY); + if (!Objects.equals(isLongRunning, true)) { + return Optional.empty(); + } + if (convertedPart.functionCall().isEmpty()) { + return Optional.empty(); + } + return convertedPart.functionCall().get().id(); + }) + .flatMap(Optional::stream) + .collect(toImmutableSet()); } private static Event emptyEvent(InvocationContext invocationContext) { 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 8e8982ffa..d93466dd2 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 @@ -18,7 +18,6 @@ import io.a2a.spec.FileWithUri; import io.a2a.spec.TextPart; import java.util.Base64; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -27,29 +26,27 @@ public class PartConverterTest { @Test - public void toGenaiPart_withNullPart_returnsEmpty() { - assertThat(PartConverter.toGenaiPart(null)).isEmpty(); + public void toGenaiPart_withNullPart_throwsException() { + assertThrows(IllegalArgumentException.class, () -> PartConverter.toGenaiPart(null)); } @Test public void toGenaiPart_withTextPart_returnsGenaiTextPart() { TextPart textPart = new TextPart("Hello"); - Optional result = PartConverter.toGenaiPart(textPart); + Part result = PartConverter.toGenaiPart(textPart); - assertThat(result).isPresent(); - assertThat(result.get().text()).hasValue("Hello"); + assertThat(result.text()).hasValue("Hello"); } @Test public void toGenaiPart_withFilePartUri_returnsGenaiFilePart() { FilePart filePart = new FilePart(new FileWithUri("text/plain", "file.txt", "http://file.txt")); - Optional result = PartConverter.toGenaiPart(filePart); + Part result = PartConverter.toGenaiPart(filePart); - assertThat(result).isPresent(); - assertThat(result.get().fileData()).isPresent(); - FileData fileData = result.get().fileData().get(); + assertThat(result.fileData()).isPresent(); + FileData fileData = result.fileData().get(); assertThat(fileData.mimeType()).hasValue("text/plain"); assertThat(fileData.fileUri()).hasValue("http://file.txt"); } @@ -60,26 +57,25 @@ public void toGenaiPart_withFilePartBytes_returnsGenaiBlobPart() { String encoded = Base64.getEncoder().encodeToString(bytes); FilePart filePart = new FilePart(new FileWithBytes("text/plain", "file.txt", encoded)); - Optional result = PartConverter.toGenaiPart(filePart); + Part result = PartConverter.toGenaiPart(filePart); - assertThat(result).isPresent(); - assertThat(result.get().inlineData()).isPresent(); - Blob blob = result.get().inlineData().get(); + assertThat(result.inlineData()).isPresent(); + Blob blob = result.inlineData().get(); assertThat(blob.mimeType()).hasValue("text/plain"); assertThat(blob.data().get()).isEqualTo(bytes); } @Test - public void toGenaiPart_withFilePartBytes_handlesNullBytes() { + public void toGenaiPart_withFilePartBytes_handlesNullBytes_throwsException() { FilePart filePart = new FilePart(new FileWithBytes("text/plain", "file.txt", null)); - assertThat(PartConverter.toGenaiPart(filePart)).isEmpty(); + assertThrows(GenAiFieldMissingException.class, () -> PartConverter.toGenaiPart(filePart)); } @Test public void toGenaiPart_withFilePartBytes_handlesInvalidBase64() { FilePart filePart = new FilePart(new FileWithBytes("text/plain", "file.txt", "invalid-base64!")); - assertThat(PartConverter.toGenaiPart(filePart)).isEmpty(); + assertThrows(IllegalArgumentException.class, () -> PartConverter.toGenaiPart(filePart)); } @Test @@ -93,11 +89,10 @@ public void toGenaiPart_withDataPartFunctionCall_returnsGenaiFunctionCallPart() PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_CALL.getType())); - Optional result = PartConverter.toGenaiPart(dataPart); + Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result).isPresent(); - assertThat(result.get().functionCall()).isPresent(); - FunctionCall functionCall = result.get().functionCall().get(); + assertThat(result.functionCall()).isPresent(); + FunctionCall functionCall = result.functionCall().get(); assertThat(functionCall.name()).hasValue("func"); assertThat(functionCall.id()).hasValue("1"); assertThat(functionCall.args()).hasValue(ImmutableMap.of()); @@ -109,11 +104,10 @@ public void toGenaiPart_withDataPartFunctionCallByNameAndArgs_returnsGenaiFuncti ImmutableMap.of("name", "func", "id", "1", "args", ImmutableMap.of("param", "value")); DataPart dataPart = new DataPart(data, null); - Optional result = PartConverter.toGenaiPart(dataPart); + Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result).isPresent(); - assertThat(result.get().functionCall()).isPresent(); - FunctionCall functionCall = result.get().functionCall().get(); + assertThat(result.functionCall()).isPresent(); + FunctionCall functionCall = result.functionCall().get(); assertThat(functionCall.name()).hasValue("func"); assertThat(functionCall.id()).hasValue("1"); assertThat(functionCall.args()).hasValue(ImmutableMap.of("param", "value")); @@ -130,11 +124,10 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())); - Optional result = PartConverter.toGenaiPart(dataPart); + Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result).isPresent(); - assertThat(result.get().functionResponse()).isPresent(); - FunctionResponse functionResponse = result.get().functionResponse().get(); + assertThat(result.functionResponse()).isPresent(); + FunctionResponse functionResponse = result.functionResponse().get(); assertThat(functionResponse.name()).hasValue("func"); assertThat(functionResponse.id()).hasValue("1"); assertThat(functionResponse.response()).hasValue(ImmutableMap.of()); @@ -147,11 +140,10 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons ImmutableMap.of("name", "func", "id", "1", "response", ImmutableMap.of("result", "value")); DataPart dataPart = new DataPart(data, null); - Optional result = PartConverter.toGenaiPart(dataPart); + Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result).isPresent(); - assertThat(result.get().functionResponse()).isPresent(); - FunctionResponse functionResponse = result.get().functionResponse().get(); + assertThat(result.functionResponse()).isPresent(); + FunctionResponse functionResponse = result.functionResponse().get(); assertThat(functionResponse.name()).hasValue("func"); assertThat(functionResponse.id()).hasValue("1"); assertThat(functionResponse.response()).hasValue(ImmutableMap.of("result", "value")); @@ -162,10 +154,9 @@ public void toGenaiPart_withOtherDataPart_returnsGenaiTextPartWithJson() { ImmutableMap data = ImmutableMap.of("key", "value"); DataPart dataPart = new DataPart(data, null); - Optional result = PartConverter.toGenaiPart(dataPart); + Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result).isPresent(); - assertThat(result.get().text()).hasValue("{\"key\":\"value\"}"); + assertThat(result.text()).hasValue("{\"key\":\"value\"}"); } @Test @@ -293,11 +284,10 @@ public void toGenaiPart_dataPartWithEmptyStringCoercedToEmptyMap() { ImmutableMap data = ImmutableMap.of("name", "func", "id", "1", "args", ""); DataPart dataPart = new DataPart(data, null); - Optional result = PartConverter.toGenaiPart(dataPart); + Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result).isPresent(); - assertThat(result.get().functionCall()).isPresent(); - assertThat(result.get().functionCall().get().args()).hasValue(ImmutableMap.of()); + assertThat(result.functionCall()).isPresent(); + assertThat(result.functionCall().get().args()).hasValue(ImmutableMap.of()); } @Test @@ -305,10 +295,9 @@ public void toGenaiPart_dataPartWithNonMapCoercedToMap() { ImmutableMap data = ImmutableMap.of("name", "func", "id", "1", "args", 123); DataPart dataPart = new DataPart(data, null); - Optional result = PartConverter.toGenaiPart(dataPart); + Part result = PartConverter.toGenaiPart(dataPart); - assertThat(result).isPresent(); - assertThat(result.get().functionCall()).isPresent(); - assertThat(result.get().functionCall().get().args()).hasValue(ImmutableMap.of("value", 123)); + assertThat(result.functionCall()).isPresent(); + assertThat(result.functionCall().get().args()).hasValue(ImmutableMap.of("value", 123)); } } diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java index 5378bdd7b..d84dc42cd 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java @@ -11,10 +11,12 @@ import com.google.adk.sessions.InMemorySessionService; import com.google.adk.sessions.Session; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.genai.types.Content; import io.a2a.client.MessageEvent; import io.a2a.client.TaskUpdateEvent; import io.a2a.spec.Artifact; +import io.a2a.spec.DataPart; import io.a2a.spec.Message; import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; @@ -144,6 +146,81 @@ public void taskToEvent_withNoMessage_returnsEmptyEvent() { assertThat(event.invocationId()).isEqualTo(invocationContext.invocationId()); } + @Test + public void taskToEvent_withInputRequired_parsesLongRunningToolIds() { + ImmutableMap data = + ImmutableMap.of("name", "myTool", "id", "call_123", "args", ImmutableMap.of()); + ImmutableMap metadata = + ImmutableMap.of( + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, + "function_call", + PartConverter.A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY, + true); + DataPart dataPart = new DataPart(data, metadata); + ImmutableMap statusData = + ImmutableMap.of("name", "messageTools", "id", "msg_123", "args", ImmutableMap.of()); + ImmutableMap statusMetadata = + ImmutableMap.of( + PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, + "function_call", + PartConverter.A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY, + true); + DataPart statusDataPart = new DataPart(statusData, statusMetadata); + Message statusMessage = + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(statusDataPart)) + .build(); + TaskStatus status = new TaskStatus(TaskState.INPUT_REQUIRED, statusMessage, null); + + Artifact artifact = + new Artifact.Builder().artifactId("artifact-1").parts(ImmutableList.of(dataPart)).build(); + Task task = testTask().status(status).artifacts(ImmutableList.of(artifact)).build(); + + Event event = ResponseConverter.taskToEvent(task, invocationContext); + assertThat(event).isNotNull(); + assertThat(event.longRunningToolIds().get()).containsExactly("call_123", "msg_123"); + } + + @Test + public void taskToEvent_withFailedState_setsErrorCode() { + Message statusMessage = + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("Task failed"))) + .build(); + TaskStatus status = new TaskStatus(TaskState.FAILED, statusMessage, null); + Task task = testTask().status(status).artifacts(ImmutableList.of()).build(); + + Event event = ResponseConverter.taskToEvent(task, invocationContext); + assertThat(event).isNotNull(); + assertThat(event.errorMessage()).hasValue("Task failed"); + } + + @Test + public void taskToEvent_withFinalEvent_returnsEmptyEvent() { + TaskStatus status = new TaskStatus(TaskState.COMPLETED); + Task task = testTask().status(status).artifacts(ImmutableList.of()).build(); + + Event event = ResponseConverter.taskToEvent(task, invocationContext); + assertThat(event).isNotNull(); + assertThat(event.invocationId()).isEqualTo(invocationContext.invocationId()); + assertThat(event.turnComplete()).hasValue(true); + assertThat(event.content().flatMap(Content::parts).orElse(ImmutableList.of())).isEmpty(); + } + + @Test + public void taskToEvent_withEmptyParts_returnsEmptyEvent() { + TaskStatus status = new TaskStatus(TaskState.SUBMITTED); + Task task = testTask().status(status).artifacts(ImmutableList.of()).build(); + + Event event = ResponseConverter.taskToEvent(task, invocationContext); + assertThat(event).isNotNull(); + assertThat(event.invocationId()).isEqualTo(invocationContext.invocationId()); + assertThat(event.content()).isPresent(); + assertThat(event.content().get().parts().orElse(ImmutableList.of())).isEmpty(); + } + @Test public void clientEventToEvent_withTaskUpdateEventAndThought_returnsThoughtEvent() { Message statusMessage = diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index a62bff5b6..0079dce24 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index e2ba4a7fb..c2326fa0a 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../../pom.xml diff --git a/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java b/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java index 04a2aa585..2dca5c49c 100644 --- a/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java +++ b/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java @@ -53,7 +53,7 @@ public static List runLoop(BaseAgent agent, boolean streaming, Object... allEvents.addAll( runner .runAsync( - session, + session.sessionKey(), messageContent, RunConfig.builder() .setStreamingMode( diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index 82b11b96f..0eccb733b 100644 --- a/contrib/samples/a2a_basic/pom.xml +++ b/contrib/samples/a2a_basic/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT .. diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml index 84023e260..0677ad718 100644 --- a/contrib/samples/a2a_server/pom.xml +++ b/contrib/samples/a2a_server/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT .. diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index 6f7bfff83..059bd8a38 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index 36d12eaf0..df5d5e709 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index 935aa6531..16b139d35 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 905f8e711..4a415113f 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index f49c3faae..b24fa4b63 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../../pom.xml diff --git a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java index 6843c8eaa..328df0415 100644 --- a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java +++ b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; import com.google.adk.agents.LlmAgent; +import com.google.adk.agents.RunConfig; import com.google.adk.events.Event; import com.google.adk.models.springai.integrations.tools.WeatherTool; import com.google.adk.runner.InMemoryRunner; @@ -73,14 +74,15 @@ public ChatResponse call(Prompt prompt) { // when Runner runner = new InMemoryRunner(agent); - Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet(); + Session session = + runner.sessionService().createSession(agent.name(), "test-user").blockingGet(); Content userMessage = Content.builder().role("user").parts(List.of(Part.fromText("What is a qubit?"))).build(); List events = runner - .runAsync(session, userMessage, com.google.adk.agents.RunConfig.builder().build()) + .runAsync(session.sessionKey(), userMessage, RunConfig.builder().build()) .toList() .blockingGet(); @@ -149,7 +151,8 @@ public ChatResponse call(Prompt prompt) { // when Runner runner = new InMemoryRunner(agent); - Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet(); + Session session = + runner.sessionService().createSession(agent.name(), "test-user").blockingGet(); Content userMessage = Content.builder() @@ -159,7 +162,7 @@ public ChatResponse call(Prompt prompt) { List events = runner - .runAsync(session, userMessage, com.google.adk.agents.RunConfig.builder().build()) + .runAsync(session.userId(), session.id(), userMessage, RunConfig.builder().build()) .toList() .blockingGet(); @@ -217,7 +220,8 @@ public Flux stream(Prompt prompt) { // when Runner runner = new InMemoryRunner(agent); - Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet(); + Session session = + runner.sessionService().createSession(agent.name(), "test-user").blockingGet(); Content userMessage = Content.builder() @@ -228,11 +232,10 @@ public Flux stream(Prompt prompt) { List events = runner .runAsync( - session, + session.userId(), + session.id(), userMessage, - com.google.adk.agents.RunConfig.builder() - .setStreamingMode(com.google.adk.agents.RunConfig.StreamingMode.SSE) - .build()) + RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.SSE).build()) .toList() .blockingGet(); diff --git a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java index f18ded055..c23e68eae 100644 --- a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java +++ b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java @@ -46,7 +46,7 @@ public static List askAgent(BaseAgent agent, boolean streaming, Object... allEvents.addAll( runner .runAsync( - session, + session.sessionKey(), messageContent, RunConfig.builder() .setStreamingMode( @@ -67,13 +67,17 @@ public static List askBlockingAgent(BaseAgent agent, Object... messages) } Runner runner = new InMemoryRunner(agent); - Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet(); + Session session = + runner.sessionService().createSession(agent.name(), "test-user").blockingGet(); List events = new ArrayList<>(); for (Content content : contents) { List batchEvents = - runner.runAsync(session, content, RunConfig.builder().build()).toList().blockingGet(); + runner + .runAsync(session.userId(), session.id(), content, RunConfig.builder().build()) + .toList() + .blockingGet(); events.addAll(batchEvents); } @@ -88,7 +92,8 @@ public static List askAgentStreaming(BaseAgent agent, Object... messages) } Runner runner = new InMemoryRunner(agent); - Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet(); + Session session = + runner.sessionService().createSession(agent.name(), "test-user").blockingGet(); List events = new ArrayList<>(); @@ -96,7 +101,8 @@ public static List askAgentStreaming(BaseAgent agent, Object... messages) List batchEvents = runner .runAsync( - session, + session.userId(), + session.id(), content, RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.SSE).build()) .toList() diff --git a/core/pom.xml b/core/pom.xml index a0f843f56..8c3c2069c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT google-adk @@ -92,6 +92,10 @@ com.google.errorprone error_prone_annotations + + com.fasterxml.jackson.core + jackson-annotations + com.fasterxml.jackson.core jackson-databind diff --git a/core/src/main/java/com/google/adk/Version.java b/core/src/main/java/com/google/adk/Version.java index 1dc0282c3..a7aeb8b1f 100644 --- a/core/src/main/java/com/google/adk/Version.java +++ b/core/src/main/java/com/google/adk/Version.java @@ -22,7 +22,7 @@ */ public final class Version { // Don't touch this, release-please should keep it up to date. - public static final String JAVA_ADK_VERSION = "0.8.0"; // x-release-please-released-version + public static final String JAVA_ADK_VERSION = "0.9.0"; // x-release-please-released-version private Version() {} } diff --git a/core/src/main/java/com/google/adk/agents/InvocationContext.java b/core/src/main/java/com/google/adk/agents/InvocationContext.java index 7f0e49d0c..91ce13a87 100644 --- a/core/src/main/java/com/google/adk/agents/InvocationContext.java +++ b/core/src/main/java/com/google/adk/agents/InvocationContext.java @@ -27,7 +27,6 @@ import com.google.adk.sessions.Session; import com.google.adk.summarizer.EventsCompactionConfig; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.InlineMe; import com.google.genai.types.Content; import java.util.Map; import java.util.Objects; @@ -81,62 +80,6 @@ protected InvocationContext(Builder builder) { this.callbackContextData = builder.callbackContextData; } - /** - * @deprecated Use {@link #builder()} instead. - */ - @InlineMe( - replacement = - "InvocationContext.builder()" - + ".sessionService(sessionService)" - + ".artifactService(artifactService)" - + ".invocationId(invocationId)" - + ".agent(agent)" - + ".session(session)" - + ".userContent(userContent)" - + ".runConfig(runConfig)" - + ".build()", - imports = {"com.google.adk.agents.InvocationContext"}) - @Deprecated(forRemoval = true) - public static InvocationContext create( - BaseSessionService sessionService, - BaseArtifactService artifactService, - String invocationId, - BaseAgent agent, - Session session, - Content userContent, - RunConfig runConfig) { - return builder() - .sessionService(sessionService) - .artifactService(artifactService) - .invocationId(invocationId) - .agent(agent) - .session(session) - .userContent(userContent) - .runConfig(runConfig) - .build(); - } - - /** - * @deprecated Use {@link #builder()} instead. - */ - @Deprecated(forRemoval = true) - public static InvocationContext create( - BaseSessionService sessionService, - BaseArtifactService artifactService, - BaseAgent agent, - Session session, - LiveRequestQueue liveRequestQueue, - RunConfig runConfig) { - return builder() - .sessionService(sessionService) - .artifactService(artifactService) - .agent(agent) - .session(session) - .liveRequestQueue(liveRequestQueue) - .runConfig(runConfig) - .build(); - } - /** Returns a new {@link Builder} for creating {@link InvocationContext} instances. */ public static Builder builder() { return new Builder(); diff --git a/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java b/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java index a9bb6ba4d..acf5979c2 100644 --- a/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java +++ b/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java @@ -22,7 +22,7 @@ import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; -import java.util.Optional; +import org.jspecify.annotations.Nullable; /** Base interface for artifact services. */ public interface BaseArtifactService { @@ -75,7 +75,7 @@ default Single saveAndReloadArtifact( /** Loads the latest version of an artifact from the service. */ default Maybe loadArtifact( String appName, String userId, String sessionId, String filename) { - return loadArtifact(appName, userId, sessionId, filename, Optional.empty()); + return loadArtifact(appName, userId, sessionId, filename, /* version= */ (Integer) null); } /** Loads the latest version of an artifact from the service. */ @@ -86,7 +86,7 @@ default Maybe loadArtifact(SessionKey sessionKey, String filename) { /** Loads a specific version of an artifact from the service. */ default Maybe loadArtifact( String appName, String userId, String sessionId, String filename, int version) { - return loadArtifact(appName, userId, sessionId, filename, Optional.of(version)); + return loadArtifact(appName, userId, sessionId, filename, Integer.valueOf(version)); } default Maybe loadArtifact(SessionKey sessionKey, String filename, int version) { @@ -94,13 +94,8 @@ default Maybe loadArtifact(SessionKey sessionKey, String filename, int ver sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename, version); } - /** - * @deprecated Use {@link #loadArtifact(String, String, String, String)} or {@link - * #loadArtifact(String, String, String, String, int)} instead. - */ - @Deprecated Maybe loadArtifact( - String appName, String userId, String sessionId, String filename, Optional version); + String appName, String userId, String sessionId, String filename, @Nullable Integer version); /** * Lists all the artifact filenames within a session. diff --git a/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java b/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java index e31d50327..977153828 100644 --- a/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java +++ b/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java @@ -38,6 +38,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import org.jspecify.annotations.Nullable; /** An artifact service implementation using Google Cloud Storage (GCS). */ public final class GcsArtifactService implements BaseArtifactService { @@ -126,8 +127,8 @@ public Single saveArtifact( */ @Override public Maybe loadArtifact( - String appName, String userId, String sessionId, String filename, Optional version) { - return version + String appName, String userId, String sessionId, String filename, @Nullable Integer version) { + return Optional.ofNullable(version) .map(Maybe::just) .orElseGet( () -> diff --git a/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java b/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java index 8c8ec2af8..510c96c2e 100644 --- a/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java +++ b/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java @@ -28,8 +28,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.IntStream; +import org.jspecify.annotations.Nullable; /** An in-memory implementation of the {@link BaseArtifactService}. */ public final class InMemoryArtifactService implements BaseArtifactService { @@ -61,7 +61,7 @@ public Single saveArtifact( */ @Override public Maybe loadArtifact( - String appName, String userId, String sessionId, String filename, Optional version) { + String appName, String userId, String sessionId, String filename, @Nullable Integer version) { List versions = getArtifactsMap(appName, userId, sessionId) .computeIfAbsent(filename, unused -> new ArrayList<>()); @@ -69,10 +69,9 @@ public Maybe loadArtifact( if (versions.isEmpty()) { return Maybe.empty(); } - if (version.isPresent()) { - int v = version.get(); - if (v >= 0 && v < versions.size()) { - return Maybe.just(versions.get(v)); + if (version != null) { + if (version >= 0 && version < versions.size()) { + return Maybe.just(versions.get(version)); } else { return Maybe.empty(); } diff --git a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java index b9afdcaff..a4d3771c3 100644 --- a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java +++ b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java @@ -34,6 +34,7 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; /** Utility functions for code execution. */ public final class CodeExecutionUtils { @@ -237,8 +238,7 @@ public abstract static class CodeExecutionInput extends JsonBaseModel { public static Builder builder() { return new AutoValue_CodeExecutionUtils_CodeExecutionInput.Builder() - .inputFiles(ImmutableList.of()) - .executionId(Optional.empty()); + .inputFiles(ImmutableList.of()); } /** Builder for {@link CodeExecutionInput}. */ @@ -248,7 +248,7 @@ public abstract static class Builder { public abstract Builder inputFiles(List inputFiles); - public abstract Builder executionId(Optional executionId); + public abstract Builder executionId(@Nullable String executionId); public abstract CodeExecutionInput build(); } diff --git a/core/src/main/java/com/google/adk/events/EventActions.java b/core/src/main/java/com/google/adk/events/EventActions.java index 0b167de93..1ca856b45 100644 --- a/core/src/main/java/com/google/adk/events/EventActions.java +++ b/core/src/main/java/com/google/adk/events/EventActions.java @@ -83,11 +83,11 @@ public void setSkipSummarization(boolean skipSummarization) { } @JsonProperty("stateDelta") - public ConcurrentMap stateDelta() { + public Map stateDelta() { return stateDelta; } - @Deprecated // Use stateDelta(), addState() and removeStateByKey() instead. + @Deprecated // Use stateDelta() and removeStateByKey() instead. public void setStateDelta(ConcurrentMap stateDelta) { this.stateDelta = stateDelta; } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java index f2cbe967e..d76cd1a04 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java @@ -240,7 +240,8 @@ private static Flowable runPreProcessor( .code(codeStr) .inputFiles(ImmutableList.of(file)) .executionId( - getOrSetExecutionId(invocationContext, codeExecutorContext)) + getOrSetExecutionId(invocationContext, codeExecutorContext) + .orElse(null)) .build()); codeExecutorContext.updateCodeExecutionResult( @@ -320,7 +321,9 @@ private static Flowable runPostProcessor( CodeExecutionInput.builder() .code(codeStr) .inputFiles(codeExecutorContext.getInputFiles()) - .executionId(getOrSetExecutionId(invocationContext, codeExecutorContext)) + .executionId( + getOrSetExecutionId(invocationContext, codeExecutorContext) + .orElse(null)) .build()); codeExecutorContext.updateCodeExecutionResult( invocationContext.invocationId(), diff --git a/core/src/main/java/com/google/adk/plugins/LoggingPlugin.java b/core/src/main/java/com/google/adk/plugins/LoggingPlugin.java index 573f5048d..7daf13b11 100644 --- a/core/src/main/java/com/google/adk/plugins/LoggingPlugin.java +++ b/core/src/main/java/com/google/adk/plugins/LoggingPlugin.java @@ -29,7 +29,7 @@ import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Maybe; import java.util.Map; -import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +57,7 @@ private void log(String message) { @Override public Maybe onUserMessageCallback( - InvocationContext invocationContext, Content userMessage) { + InvocationContext invocationContext, @Nullable Content userMessage) { return Maybe.fromAction( () -> { log("🚀 USER MESSAGE RECEIVED"); @@ -66,7 +66,7 @@ public Maybe onUserMessageCallback( log(" User ID: " + invocationContext.userId()); log(" App Name: " + invocationContext.appName()); log(" Root Agent: " + invocationContext.agent().name()); - log(" User Content: " + formatContent(Optional.ofNullable(userMessage))); + log(" User Content: " + formatContent(userMessage)); invocationContext.branch().ifPresent(branch -> log(" Branch: " + branch)); }); } @@ -88,7 +88,7 @@ public Maybe onEventCallback(InvocationContext invocationContext, Event e log("📢 EVENT YIELDED"); log(" Event ID: " + event.id()); log(" Author: " + event.author()); - log(" Content: " + formatContent(event.content())); + log(" Content: " + formatContent(event.content().orElse(null))); log(" Final Response: " + event.finalResponse()); if (!event.functionCalls().isEmpty()) { @@ -190,7 +190,7 @@ public Maybe afterModelCallback( log(" ❌ ERROR - Code: " + llmResponse.errorCode().get()); log(" Error Message: " + llmResponse.errorMessage().orElse("None")); } else { - log(" Content: " + formatContent(llmResponse.content())); + log(" Content: " + formatContent(llmResponse.content().orElse(null))); llmResponse.partial().ifPresent(partial -> log(" Partial: " + partial)); llmResponse .turnComplete() @@ -265,12 +265,8 @@ public Maybe> onToolErrorCallback( }); } - private String formatContent(Optional contentOptional) { - if (contentOptional.isEmpty()) { - return "None"; - } - Content content = contentOptional.get(); - if (content.parts().isEmpty() || content.parts().get().isEmpty()) { + private String formatContent(@Nullable Content content) { + if (content == null || content.parts().isEmpty() || content.parts().get().isEmpty()) { return "None"; } return content.parts().get().stream() diff --git a/core/src/main/java/com/google/adk/sessions/ApiClient.java b/core/src/main/java/com/google/adk/sessions/ApiClient.java index e850199e9..1b0485dd2 100644 --- a/core/src/main/java/com/google/adk/sessions/ApiClient.java +++ b/core/src/main/java/com/google/adk/sessions/ApiClient.java @@ -16,11 +16,11 @@ package com.google.adk.sessions; -import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.StandardSystemProperty.JAVA_VERSION; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Ascii; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.genai.errors.GenAiIOException; import com.google.genai.types.HttpOptions; @@ -35,83 +35,69 @@ abstract class ApiClient { OkHttpClient httpClient; // For Google AI APIs - final Optional apiKey; + final @Nullable String apiKey; // For Vertex AI APIs - final Optional project; - final Optional location; - final Optional credentials; + final @Nullable String project; + final @Nullable String location; + final @Nullable GoogleCredentials credentials; HttpOptions httpOptions; final boolean vertexAI; /** Constructs an ApiClient for Google AI APIs. */ - ApiClient(Optional apiKey, Optional customHttpOptions) { - checkNotNull(apiKey, "API Key cannot be null"); - checkNotNull(customHttpOptions, "customHttpOptions cannot be null"); + ApiClient(@Nullable String apiKey, @Nullable HttpOptions customHttpOptions) { - try { - this.apiKey = Optional.of(apiKey.orElseGet(() -> System.getenv("GOOGLE_API_KEY"))); - } catch (NullPointerException e) { + this.apiKey = apiKey != null ? apiKey : System.getenv("GOOGLE_API_KEY"); + + if (Strings.isNullOrEmpty(this.apiKey)) { throw new IllegalArgumentException( - "API key must either be provided or set in the environment variable" + " GOOGLE_API_KEY.", - e); + "API key must either be provided or set in the environment variable" + + " GOOGLE_API_KEY."); } - this.project = Optional.empty(); - this.location = Optional.empty(); - this.credentials = Optional.empty(); + this.project = null; + this.location = null; + this.credentials = null; this.vertexAI = false; this.httpOptions = defaultHttpOptions(/* vertexAI= */ false, this.location); - if (customHttpOptions.isPresent()) { - applyHttpOptions(customHttpOptions.get()); + if (customHttpOptions != null) { + applyHttpOptions(customHttpOptions); } this.httpClient = createHttpClient(httpOptions.timeout().orElse(null)); } ApiClient( - Optional project, - Optional location, - Optional credentials, - Optional customHttpOptions) { - checkNotNull(project, "project cannot be null"); - checkNotNull(location, "location cannot be null"); - checkNotNull(credentials, "credentials cannot be null"); - checkNotNull(customHttpOptions, "customHttpOptions cannot be null"); + @Nullable String project, + @Nullable String location, + @Nullable GoogleCredentials credentials, + @Nullable HttpOptions customHttpOptions) { - try { - this.project = Optional.of(project.orElseGet(() -> System.getenv("GOOGLE_CLOUD_PROJECT"))); - } catch (NullPointerException e) { + this.project = project != null ? project : System.getenv("GOOGLE_CLOUD_PROJECT"); + + if (Strings.isNullOrEmpty(this.project)) { throw new IllegalArgumentException( "Project must either be provided or set in the environment variable" - + " GOOGLE_CLOUD_PROJECT.", - e); - } - if (this.project.get().isEmpty()) { - throw new IllegalArgumentException("Project must not be empty."); + + " GOOGLE_CLOUD_PROJECT."); } - try { - this.location = Optional.of(location.orElse(System.getenv("GOOGLE_CLOUD_LOCATION"))); - } catch (NullPointerException e) { + this.location = location != null ? location : System.getenv("GOOGLE_CLOUD_LOCATION"); + + if (Strings.isNullOrEmpty(this.location)) { throw new IllegalArgumentException( "Location must either be provided or set in the environment variable" - + " GOOGLE_CLOUD_LOCATION.", - e); - } - if (this.location.get().isEmpty()) { - throw new IllegalArgumentException("Location must not be empty."); + + " GOOGLE_CLOUD_LOCATION."); } - this.credentials = Optional.of(credentials.orElseGet(this::defaultCredentials)); + this.credentials = credentials != null ? credentials : defaultCredentials(); this.httpOptions = defaultHttpOptions(/* vertexAI= */ true, this.location); - if (customHttpOptions.isPresent()) { - applyHttpOptions(customHttpOptions.get()); + if (customHttpOptions != null) { + applyHttpOptions(customHttpOptions); } - this.apiKey = Optional.empty(); + this.apiKey = null; this.vertexAI = true; this.httpClient = createHttpClient(httpOptions.timeout().orElse(null)); } @@ -142,17 +128,17 @@ public boolean vertexAI() { /** Returns the project ID for Vertex AI APIs. */ public @Nullable String project() { - return project.orElse(null); + return project; } /** Returns the location for Vertex AI APIs. */ public @Nullable String location() { - return location.orElse(null); + return location; } /** Returns the API key for Google AI APIs. */ public @Nullable String apiKey() { - return apiKey.orElse(null); + return apiKey; } /** Returns the HttpClient for API calls. */ @@ -192,7 +178,7 @@ private void applyHttpOptions(HttpOptions httpOptionsToApply) { this.httpOptions = mergedHttpOptionsBuilder.build(); } - static HttpOptions defaultHttpOptions(boolean vertexAI, Optional location) { + static HttpOptions defaultHttpOptions(boolean vertexAI, @Nullable String location) { ImmutableMap.Builder defaultHeaders = ImmutableMap.builder(); defaultHeaders .put("Content-Type", "application/json") @@ -202,14 +188,14 @@ static HttpOptions defaultHttpOptions(boolean vertexAI, Optional locatio HttpOptions.Builder defaultHttpOptionsBuilder = HttpOptions.builder().headers(defaultHeaders.buildOrThrow()); - if (vertexAI && location.isPresent()) { + if (vertexAI && location != null) { defaultHttpOptionsBuilder .baseUrl( - Ascii.equalsIgnoreCase(location.get(), "global") + Ascii.equalsIgnoreCase(location, "global") ? "https://aiplatform.googleapis.com" - : String.format("https://%s-aiplatform.googleapis.com", location.get())) + : String.format("https://%s-aiplatform.googleapis.com", location)) .apiVersion("v1beta1"); - } else if (vertexAI && location.isEmpty()) { + } else if (vertexAI && Strings.isNullOrEmpty(location)) { throw new IllegalArgumentException("Location must be provided for Vertex AI APIs."); } else { defaultHttpOptionsBuilder diff --git a/core/src/main/java/com/google/adk/sessions/HttpApiClient.java b/core/src/main/java/com/google/adk/sessions/HttpApiClient.java index bba39da89..3ddb97bda 100644 --- a/core/src/main/java/com/google/adk/sessions/HttpApiClient.java +++ b/core/src/main/java/com/google/adk/sessions/HttpApiClient.java @@ -18,16 +18,17 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Ascii; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.genai.errors.GenAiIOException; import com.google.genai.types.HttpOptions; import java.io.IOException; import java.util.Map; -import java.util.Optional; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import org.jspecify.annotations.Nullable; /** Base client for the HTTP APIs. */ public class HttpApiClient extends ApiClient { @@ -35,16 +36,16 @@ public class HttpApiClient extends ApiClient { MediaType.parse("application/json; charset=utf-8"); /** Constructs an ApiClient for Google AI APIs. */ - HttpApiClient(Optional apiKey, Optional httpOptions) { + HttpApiClient(@Nullable String apiKey, @Nullable HttpOptions httpOptions) { super(apiKey, httpOptions); } /** Constructs an ApiClient for Vertex AI APIs. */ HttpApiClient( - Optional project, - Optional location, - Optional credentials, - Optional httpOptions) { + @Nullable String project, + @Nullable String location, + @Nullable GoogleCredentials credentials, + @Nullable HttpOptions httpOptions) { super(project, location, credentials, httpOptions); } @@ -54,9 +55,7 @@ public ApiResponse request(String httpMethod, String path, String requestJson) { boolean queryBaseModel = Ascii.equalsIgnoreCase(httpMethod, "GET") && path.startsWith("publishers/google/models/"); if (this.vertexAI() && !path.startsWith("projects/") && !queryBaseModel) { - path = - String.format("projects/%s/locations/%s/", this.project.get(), this.location.get()) - + path; + path = String.format("projects/%s/locations/%s/", this.project, this.location) + path; } String requestUrl = String.format( @@ -85,11 +84,11 @@ private void setHeaders(Request.Builder requestBuilder) { requestBuilder.header(header.getKey(), header.getValue()); } - if (apiKey.isPresent()) { - requestBuilder.header("x-goog-api-key", apiKey.get()); + if (apiKey != null) { + requestBuilder.header("x-goog-api-key", apiKey); } else { - GoogleCredentials cred = - credentials.orElseThrow(() -> new IllegalStateException("credentials is required")); + Preconditions.checkState(credentials != null, "credentials is required"); + GoogleCredentials cred = credentials; try { cred.refreshIfExpired(); } catch (IOException e) { diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java index 718738b92..1168d1166 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java @@ -17,7 +17,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import okhttp3.ResponseBody; @@ -37,17 +36,15 @@ final class VertexAiClient { } VertexAiClient() { - this.apiClient = - new HttpApiClient(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + this.apiClient = new HttpApiClient((String) null, null, null, null); } VertexAiClient( String project, String location, - Optional credentials, - Optional httpOptions) { - this.apiClient = - new HttpApiClient(Optional.of(project), Optional.of(location), credentials, httpOptions); + @Nullable GoogleCredentials credentials, + @Nullable HttpOptions httpOptions) { + this.apiClient = new HttpApiClient(project, location, credentials, httpOptions); } Maybe createSession( diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java index 2fff7a752..4336f96c9 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java @@ -40,7 +40,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Connects to the managed Vertex AI Session Service. */ // TODO: Use the genai HttpApiClient and ApiResponse methods once they are public. @@ -65,8 +65,8 @@ public VertexAiSessionService() { public VertexAiSessionService( String project, String location, - Optional credentials, - Optional httpOptions) { + @Nullable GoogleCredentials credentials, + @Nullable HttpOptions httpOptions) { this.client = new VertexAiClient(project, location, credentials, httpOptions); } diff --git a/core/src/main/java/com/google/adk/telemetry/Tracing.java b/core/src/main/java/com/google/adk/telemetry/Tracing.java index 35bf3cc96..215e317e1 100644 --- a/core/src/main/java/com/google/adk/telemetry/Tracing.java +++ b/core/src/main/java/com/google/adk/telemetry/Tracing.java @@ -37,16 +37,20 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.CompletableObserver; import io.reactivex.rxjava3.core.CompletableSource; import io.reactivex.rxjava3.core.CompletableTransformer; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.FlowableTransformer; import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.MaybeObserver; import io.reactivex.rxjava3.core.MaybeSource; import io.reactivex.rxjava3.core.MaybeTransformer; import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleObserver; import io.reactivex.rxjava3.core.SingleSource; import io.reactivex.rxjava3.core.SingleTransformer; +import io.reactivex.rxjava3.disposables.Disposable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -58,6 +62,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -550,4 +556,185 @@ public CompletableSource apply(Completable upstream) { }); } } + + /** + * Returns a transformer that re-activates a given context for the duration of the stream's + * subscription. + * + * @param context The context to re-activate. + * @param The type of the stream. + * @return A transformer that re-activates the context. + */ + public static ContextTransformer withContext(Context context) { + return new ContextTransformer<>(context); + } + + /** + * A transformer that re-activates a given context for the duration of the stream's subscription. + * + * @param The type of the stream. + */ + public static final class ContextTransformer + implements FlowableTransformer, + SingleTransformer, + MaybeTransformer, + CompletableTransformer { + private final Context context; + + private ContextTransformer(Context context) { + this.context = context; + } + + @Override + public Publisher apply(Flowable upstream) { + return upstream.lift(subscriber -> TracingObserver.wrap(context, subscriber)); + } + + @Override + public SingleSource apply(Single upstream) { + return upstream.lift(observer -> TracingObserver.wrap(context, observer)); + } + + @Override + public MaybeSource apply(Maybe upstream) { + return upstream.lift(observer -> TracingObserver.wrap(context, observer)); + } + + @Override + public CompletableSource apply(Completable upstream) { + return upstream.lift(observer -> TracingObserver.wrap(context, observer)); + } + } + + /** + * An observer that wraps another observer and ensures that the OpenTelemetry context is active + * during all callback methods. + * + *

This implementation only wraps the data-flow callbacks (`onNext`, `onSuccess`, etc.). The + * `Subscription.request/cancel` and `Disposable.dispose` calls are not wrapped in the context. If + * the upstream logic depends on the context during these signals, they might lose trace + * information. Given this is a manual `withContext` utility, this might be an acceptable + * trade-off for simplicity/performance, but worth keeping in mind. + * + * @param The type of the items emitted by the stream. + */ + private static final class TracingObserver + implements Subscriber, SingleObserver, MaybeObserver, CompletableObserver { + private final Context context; + private final Subscriber subscriber; + private final SingleObserver singleObserver; + private final MaybeObserver maybeObserver; + private final CompletableObserver completableObserver; + + private TracingObserver( + Context context, + Subscriber subscriber, + SingleObserver singleObserver, + MaybeObserver maybeObserver, + CompletableObserver completableObserver) { + this.context = context; + this.subscriber = subscriber; + this.singleObserver = singleObserver; + this.maybeObserver = maybeObserver; + this.completableObserver = completableObserver; + } + + static TracingObserver wrap(Context context, Subscriber subscriber) { + return new TracingObserver<>(context, subscriber, null, null, null); + } + + static TracingObserver wrap(Context context, SingleObserver observer) { + return new TracingObserver<>(context, null, observer, null, null); + } + + static TracingObserver wrap(Context context, MaybeObserver observer) { + return new TracingObserver<>(context, null, null, observer, null); + } + + static TracingObserver wrap(Context context, CompletableObserver observer) { + return new TracingObserver<>(context, null, null, null, observer); + } + + private void runInContext(Runnable action) { + try (Scope scope = context.makeCurrent()) { + action.run(); + } + } + + @Override + public void onSubscribe(Subscription s) { + runInContext( + () -> { + if (subscriber != null) { + subscriber.onSubscribe(s); + } + }); + } + + @Override + public void onSubscribe(Disposable d) { + runInContext( + () -> { + if (singleObserver != null) { + singleObserver.onSubscribe(d); + } else if (maybeObserver != null) { + maybeObserver.onSubscribe(d); + } else if (completableObserver != null) { + completableObserver.onSubscribe(d); + } + }); + } + + @Override + public void onNext(T t) { + runInContext( + () -> { + if (subscriber != null) { + subscriber.onNext(t); + } + }); + } + + @Override + public void onSuccess(T t) { + runInContext( + () -> { + if (singleObserver != null) { + singleObserver.onSuccess(t); + } else if (maybeObserver != null) { + maybeObserver.onSuccess(t); + } + }); + } + + @Override + public void onError(Throwable t) { + runInContext( + () -> { + if (subscriber != null) { + subscriber.onError(t); + } else if (singleObserver != null) { + singleObserver.onError(t); + } else if (maybeObserver != null) { + maybeObserver.onError(t); + } else if (completableObserver != null) { + completableObserver.onError(t); + } + }); + } + + @Override + public void onComplete() { + runInContext( + () -> { + if (subscriber != null) { + subscriber.onComplete(); + } else if (maybeObserver != null) { + maybeObserver.onComplete(); + } else if (completableObserver != null) { + completableObserver.onComplete(); + } + }); + } + } } diff --git a/core/src/main/java/com/google/adk/utils/InstructionUtils.java b/core/src/main/java/com/google/adk/utils/InstructionUtils.java index ea118b362..ff2a7b8bd 100644 --- a/core/src/main/java/com/google/adk/utils/InstructionUtils.java +++ b/core/src/main/java/com/google/adk/utils/InstructionUtils.java @@ -25,7 +25,6 @@ import io.reactivex.rxjava3.core.Single; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -167,12 +166,7 @@ private static Single resolveMatchAsync(InvocationContext context, Match Maybe artifactMaybe = context .artifactService() - .loadArtifact( - session.appName(), - session.userId(), - session.id(), - artifactName, - Optional.empty()); + .loadArtifact(session.appName(), session.userId(), session.id(), artifactName); return artifactMaybe .map(Part::toJson) diff --git a/core/src/test/java/com/google/adk/artifacts/GcsArtifactServiceTest.java b/core/src/test/java/com/google/adk/artifacts/GcsArtifactServiceTest.java index 88abd60c4..3b3c8c402 100644 --- a/core/src/test/java/com/google/adk/artifacts/GcsArtifactServiceTest.java +++ b/core/src/test/java/com/google/adk/artifacts/GcsArtifactServiceTest.java @@ -158,7 +158,7 @@ public void load_latestVersion_loadsCorrectly() { when(mockStorage.get(blobIdV1)).thenReturn(blobV1); Optional loadedArtifact = - asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, Optional.empty())); + asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME)); assertThat(loadedArtifact).isPresent(); Optional actualDataOptional = loadedArtifact.get().inlineData().get().data(); @@ -177,7 +177,7 @@ public void load_specificVersion_loadsCorrectly() { when(mockStorage.get(blobIdV0)).thenReturn(blobV0); Optional loadedArtifact = - asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, Optional.of(0))); + asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, 0)); assertThat(loadedArtifact).isPresent(); Optional actualDataOptional = loadedArtifact.get().inlineData().get().data(); @@ -197,8 +197,7 @@ public void load_userNamespace_loadsCorrectly() { when(mockStorage.get(blobIdV0)).thenReturn(blobV0); Optional loadedArtifact = - asOptional( - service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, USER_FILENAME, Optional.empty())); + asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, USER_FILENAME)); assertThat(loadedArtifact).isPresent(); Optional actualDataOptional = loadedArtifact.get().inlineData().get().data(); @@ -216,7 +215,7 @@ public void load_versionNotFound_returnsEmpty() { when(mockStorage.get(blobIdV0)).thenReturn(null); Optional loadedArtifact = - asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, Optional.of(0))); + asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, 0)); assertThat(loadedArtifact).isEmpty(); verify(mockStorage).get(blobIdV0); @@ -227,7 +226,7 @@ public void load_noVersionsExist_returnsEmpty() { when(mockBlobPage.iterateAll()).thenReturn(ImmutableList.of()); Optional loadedArtifact = - asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, Optional.empty())); + asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME)); assertThat(loadedArtifact).isEmpty(); } @@ -400,7 +399,7 @@ public void load_storageException_returnsEmpty() { when(mockStorage.get(blobIdV0)).thenThrow(new StorageException(500, "Induced error")); Optional loadedArtifact = - asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, Optional.of(0))); + asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, 0)); assertThat(loadedArtifact).isEmpty(); } diff --git a/core/src/test/java/com/google/adk/artifacts/InMemoryArtifactServiceTest.java b/core/src/test/java/com/google/adk/artifacts/InMemoryArtifactServiceTest.java index 4cb493277..124a5e9d8 100644 --- a/core/src/test/java/com/google/adk/artifacts/InMemoryArtifactServiceTest.java +++ b/core/src/test/java/com/google/adk/artifacts/InMemoryArtifactServiceTest.java @@ -59,7 +59,7 @@ public void loadArtifact_loadsLatest() { var unused2 = service.saveArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, artifact2).blockingGet(); Optional result = - asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME, Optional.empty())); + asOptional(service.loadArtifact(APP_NAME, USER_ID, SESSION_ID, FILENAME)); assertThat(result).hasValue(artifact2); } diff --git a/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java b/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java index 353504dac..1485ca2c4 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java @@ -20,6 +20,7 @@ import static com.google.adk.testing.TestUtils.createTestAgentBuilder; import static com.google.adk.testing.TestUtils.createTestLlm; import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; @@ -32,14 +33,19 @@ import com.google.adk.codeexecutors.CodeExecutionUtils.CodeExecutionInput; import com.google.adk.codeexecutors.CodeExecutionUtils.CodeExecutionResult; import com.google.adk.events.Event; +import com.google.adk.flows.llmflows.RequestProcessor.RequestProcessingResult; +import com.google.adk.models.LlmRequest; import com.google.adk.models.LlmResponse; import com.google.adk.sessions.InMemorySessionService; import com.google.adk.sessions.Session; import com.google.adk.testing.TestLlm; import com.google.common.collect.ImmutableList; +import com.google.genai.types.Blob; import com.google.genai.types.Content; import com.google.genai.types.Part; import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import java.util.ArrayList; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -115,4 +121,38 @@ public void testResponseProcessor_withCode_executesCode() { assertThat(executionResultPart.codeExecutionResult().get().output()) .hasValue("Code execution result:\nhello\n\n"); } + + @Test + public void testRequestProcessor_withCode_hasNoErrors() throws Exception { + // arrange + LlmRequest.Builder llmReqBuilder = LlmRequest.builder(); + when(mockCodeExecutor.codeBlockDelimiters()) + .thenReturn(ImmutableList.of(ImmutableList.of("```tool_code", "\n```"))); + when(mockCodeExecutor.optimizeDataFile()).thenReturn(true); + when(mockCodeExecutor.errorRetryAttempts()).thenReturn(2); + CodeExecutionResult executionResult = CodeExecutionResult.builder().stdout("hello\n").build(); + when(mockCodeExecutor.executeCode(any(), any())).thenReturn(executionResult); + llmReqBuilder.contents( + new ArrayList<>( + ImmutableList.of( + Content.builder() + .role("user") + .parts( + ImmutableList.of( + Part.builder() + .inlineData( + Blob.builder() + .mimeType("text/csv") + .data("1,2,3\n".getBytes(UTF_8))) + .build())) + .build()))); + + // act + Single result = + CodeExecution.requestProcessor.processRequest(invocationContext, llmReqBuilder.build()); + TestObserver testObserver = result.test(); + + // assert + testObserver.assertNoErrors(); + } } diff --git a/core/src/test/java/com/google/adk/flows/llmflows/InstructionsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/InstructionsTest.java index 2ac9e454d..90f710856 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/InstructionsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/InstructionsTest.java @@ -32,7 +32,6 @@ import com.google.genai.types.Part; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import org.junit.Before; import org.junit.Rule; @@ -122,11 +121,7 @@ public void processRequest_agentInstructionString_noPlaceholders_appendsInstruct Session session = createSession(); Part artifactPart = Part.fromText("Artifact content"); when(mockArtifactService.loadArtifact( - eq(session.appName()), - eq(session.userId()), - eq(session.id()), - eq("file.txt"), - eq(Optional.empty()))) + eq(session.appName()), eq(session.userId()), eq(session.id()), eq("file.txt"))) .thenReturn(Maybe.just(artifactPart)); LlmAgent agent = LlmAgent.builder().name("agent").instruction("File content: {artifact.file.txt}").build(); diff --git a/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java b/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java index f809193cf..e5795d61f 100644 --- a/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java +++ b/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java @@ -31,6 +31,7 @@ import com.google.adk.runner.Runner; import com.google.adk.sessions.InMemorySessionService; import com.google.adk.sessions.Session; +import com.google.adk.sessions.SessionKey; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.genai.types.Content; @@ -44,12 +45,17 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; import io.opentelemetry.sdk.trace.data.SpanData; +import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.After; import org.junit.Before; @@ -380,6 +386,70 @@ public void testTraceFlowable() throws InterruptedException { assertTrue(flowableSpanData.hasEnded()); } + @Test + public void testWithContextFlowable() throws InterruptedException { + ContextKey testKey = ContextKey.named("test-key"); + Context testContext = Context.root().with(testKey, "test-value"); + + Flowable flowable = + Flowable.just(1, 2, 3) + .compose(Tracing.withContext(testContext)) + .subscribeOn(Schedulers.computation()) + .doOnNext( + i -> { + assertEquals("test-value", Context.current().get(testKey)); + }); + flowable.test().await().assertComplete(); + } + + @Test + public void testWithContextSingle() throws InterruptedException { + ContextKey testKey = ContextKey.named("test-key"); + Context testContext = Context.root().with(testKey, "test-value"); + + Single single = + Single.just(1) + .compose(Tracing.withContext(testContext)) + .subscribeOn(Schedulers.computation()) + .doOnSuccess( + i -> { + assertEquals("test-value", Context.current().get(testKey)); + }); + single.test().await().assertComplete(); + } + + @Test + public void testWithContextMaybe() throws InterruptedException { + ContextKey testKey = ContextKey.named("test-key"); + Context testContext = Context.root().with(testKey, "test-value"); + + Maybe maybe = + Maybe.just(1) + .compose(Tracing.withContext(testContext)) + .subscribeOn(Schedulers.computation()) + .doOnSuccess( + i -> { + assertEquals("test-value", Context.current().get(testKey)); + }); + maybe.test().await().assertComplete(); + } + + @Test + public void testWithContextCompletable() throws InterruptedException { + ContextKey testKey = ContextKey.named("test-key"); + Context testContext = Context.root().with(testKey, "test-value"); + + Completable completable = + Completable.complete() + .compose(Tracing.withContext(testContext)) + .subscribeOn(Schedulers.computation()) + .doOnComplete( + () -> { + assertEquals("test-value", Context.current().get(testKey)); + }); + completable.test().await().assertComplete(); + } + @Test public void testTraceTransformer() throws InterruptedException { Span parentSpan = tracer.spanBuilder("parent").startSpan(); @@ -595,7 +665,7 @@ public void runnerRunAsync_propagatesContext() throws InterruptedException { Session session = runner .sessionService() - .createSession("test-app", "test-user", null, "test-session") + .createSession(new SessionKey("test-app", "test-user", "test-session")) .blockingGet(); Content newMessage = Content.fromParts(Part.fromText("hi")); RunConfig runConfig = RunConfig.builder().build(); @@ -623,13 +693,20 @@ public void runnerRunLive_propagatesContext() throws InterruptedException { Span parentSpan = tracer.spanBuilder("parent").startSpan(); try (Scope s = parentSpan.makeCurrent()) { Session session = - Session.builder("test-session").userId("test-user").appName("test-app").build(); + runner + .sessionService() + .createSession("test-app", "test-user", (Map) null, "test-session") + .blockingGet(); Content newMessage = Content.fromParts(Part.fromText("hi")); RunConfig runConfig = RunConfig.builder().build(); LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); liveRequestQueue.content(newMessage); liveRequestQueue.close(); - runner.runLive(session, liveRequestQueue, runConfig).test().await().assertComplete(); + runner + .runLive(session.userId(), session.id(), liveRequestQueue, runConfig) + .test() + .await() + .assertComplete(); } finally { parentSpan.end(); } diff --git a/core/src/test/java/com/google/adk/tools/LoadArtifactsToolTest.java b/core/src/test/java/com/google/adk/tools/LoadArtifactsToolTest.java index 5ed7a1f40..8405cfc42 100644 --- a/core/src/test/java/com/google/adk/tools/LoadArtifactsToolTest.java +++ b/core/src/test/java/com/google/adk/tools/LoadArtifactsToolTest.java @@ -1,7 +1,7 @@ package com.google.adk.tools; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; @@ -105,7 +105,7 @@ public void processLlmRequest_noArtifactsInContext_completesWithoutLoading() { assertThat(finalRequest.config()).isPresent(); assertThat(finalRequest.config().get().systemInstruction()).isEmpty(); verify(mockArtifactService, never()) - .loadArtifact(anyString(), anyString(), anyString(), anyString(), any()); + .loadArtifact(anyString(), anyString(), anyString(), anyString(), anyInt()); } @Test @@ -130,7 +130,7 @@ public void processLlmRequest_artifactsInContext_noFunctionCall_appendsInstructi assertThat(appendedInstruction).contains("call the `load_artifacts` function"); verify(mockArtifactService, never()) - .loadArtifact(anyString(), anyString(), anyString(), anyString(), any()); + .loadArtifact(anyString(), anyString(), anyString(), anyString(), anyInt()); } @Test @@ -215,7 +215,7 @@ public void processLlmRequest_artifactsInContext_withOtherFunctionCall_doesNotLo .contains("You have a list of artifacts:"); verify(mockArtifactService, never()) - .loadArtifact(anyString(), anyString(), anyString(), anyString(), any()); + .loadArtifact(anyString(), anyString(), anyString(), anyString(), anyInt()); assertThat(finalRequest.contents()).containsExactly(functionCallContent); } } diff --git a/dev/pom.xml b/dev/pom.xml index 57aa808c2..6cabcba7c 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT google-adk-dev diff --git a/dev/src/main/java/com/google/adk/plugins/ReplayPlugin.java b/dev/src/main/java/com/google/adk/plugins/ReplayPlugin.java index 5571b8d57..89032082c 100644 --- a/dev/src/main/java/com/google/adk/plugins/ReplayPlugin.java +++ b/dev/src/main/java/com/google/adk/plugins/ReplayPlugin.java @@ -90,7 +90,11 @@ public Maybe beforeModelCallback( logger.debug("Verified and replaying LLM response for agent {}", agentName); // Return the recorded response - return recording.llmResponse().map(Maybe::just).orElse(Maybe.empty()); + return recording + .llmResponses() + .filter(responses -> !responses.isEmpty()) + .map(responses -> Maybe.just(responses.get(0))) + .orElse(Maybe.empty()); } @Override diff --git a/dev/src/main/java/com/google/adk/plugins/recordings/LlmRecording.java b/dev/src/main/java/com/google/adk/plugins/recordings/LlmRecording.java index fe17aac0d..701b1e7ae 100644 --- a/dev/src/main/java/com/google/adk/plugins/recordings/LlmRecording.java +++ b/dev/src/main/java/com/google/adk/plugins/recordings/LlmRecording.java @@ -20,6 +20,7 @@ import com.google.adk.models.LlmRequest; import com.google.adk.models.LlmResponse; import com.google.auto.value.AutoValue; +import java.util.List; import java.util.Optional; import javax.annotation.Nullable; @@ -31,8 +32,8 @@ public abstract class LlmRecording { /** The LLM request. */ public abstract Optional llmRequest(); - /** The LLM response. */ - public abstract Optional llmResponse(); + /** The LLM responses. */ + public abstract Optional> llmResponses(); public static Builder builder() { return new AutoValue_LlmRecording.Builder(); @@ -44,7 +45,7 @@ public static Builder builder() { public abstract static class Builder { public abstract Builder llmRequest(@Nullable LlmRequest llmRequest); - public abstract Builder llmResponse(@Nullable LlmResponse llmResponse); + public abstract Builder llmResponses(@Nullable List llmResponses); public abstract LlmRecording build(); } diff --git a/dev/src/main/java/com/google/adk/web/controller/ArtifactController.java b/dev/src/main/java/com/google/adk/web/controller/ArtifactController.java index 27164f216..c181ab558 100644 --- a/dev/src/main/java/com/google/adk/web/controller/ArtifactController.java +++ b/dev/src/main/java/com/google/adk/web/controller/ArtifactController.java @@ -24,7 +24,6 @@ import io.reactivex.rxjava3.core.Single; import java.util.Collections; import java.util.List; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -78,8 +77,7 @@ public Part loadArtifact( versionStr); Maybe artifactMaybe = - artifactService.loadArtifact( - appName, userId, sessionId, artifactName, Optional.ofNullable(version)); + artifactService.loadArtifact(appName, userId, sessionId, artifactName, version); Part artifact = artifactMaybe.blockingGet(); @@ -126,8 +124,7 @@ public Part loadArtifactVersion( versionId); Maybe artifactMaybe = - artifactService.loadArtifact( - appName, userId, sessionId, artifactName, Optional.of(versionId)); + artifactService.loadArtifact(appName, userId, sessionId, artifactName, versionId); Part artifact = artifactMaybe.blockingGet(); diff --git a/dev/src/main/java/com/google/adk/web/service/RunnerService.java b/dev/src/main/java/com/google/adk/web/service/RunnerService.java index 7297af833..480c68472 100644 --- a/dev/src/main/java/com/google/adk/web/service/RunnerService.java +++ b/dev/src/main/java/com/google/adk/web/service/RunnerService.java @@ -78,13 +78,14 @@ public Runner getRunner(String appName) { "RunnerService: Creating Runner for appName: {}, using agent definition: {}", appName, agent.name()); - return new Runner( - agent, - appName, - this.artifactService, - this.sessionService, - this.memoryService, - this.extraPlugins); + return Runner.builder() + .agent(agent) + .appName(appName) + .artifactService(this.artifactService) + .sessionService(this.sessionService) + .memoryService(this.memoryService) + .plugins(this.extraPlugins) + .build(); } catch (java.util.NoSuchElementException e) { log.error( "Agent/App named '{}' not found in registry. Available apps: {}", diff --git a/dev/src/test/java/com/google/adk/plugins/ReplayPluginTest.java b/dev/src/test/java/com/google/adk/plugins/ReplayPluginTest.java index f29298bce..8e89c2567 100644 --- a/dev/src/test/java/com/google/adk/plugins/ReplayPluginTest.java +++ b/dev/src/test/java/com/google/adk/plugins/ReplayPluginTest.java @@ -83,11 +83,11 @@ void beforeModelCallback_withMatchingRecording_returnsRecordedResponse() throws - role: "user" parts: - text: "Hello" - llm_response: - content: - role: "model" - parts: - - text: "Recorded response" + llm_responses: + - content: + role: "model" + parts: + - text: "Recorded response" """); // Step 1: Setup replay config diff --git a/dev/src/test/java/com/google/adk/plugins/recordings/RecordingsLoaderTest.java b/dev/src/test/java/com/google/adk/plugins/recordings/RecordingsLoaderTest.java index 92d12bc6d..ee115644c 100644 --- a/dev/src/test/java/com/google/adk/plugins/recordings/RecordingsLoaderTest.java +++ b/dev/src/test/java/com/google/adk/plugins/recordings/RecordingsLoaderTest.java @@ -58,16 +58,16 @@ void testLoadCRecording() throws Exception { - function_declarations: - name: validate_email description: Validates email format - llm_response: - content: - parts: - - thought_signature: Cq0EAR_MhbYyfIgI1M5KlVyG9HzjQ_CvZiHb_RQ2KR0H_UkDj-LDdxdVayqSpG8F6wPq4aGB6lZlqjZIGvA5H2zX2RQ_Iu8Wb8t_wKoEpW4XcwzzU9Org_ZvTNx4TZHll5cH5ebo1LPRWfTqVn7cC1N5KwDZtS2XLwCmitucAAKGzGH4c-tM0dgj57NoMFa63iaHizzi2zupKoGPBB-ZmakNHAHRspkl85hKaq8m4fELHNNMnyi596jcGRHxTDBiqHmNG8PyRiOXRM9VOkNnPU8l2DN7b6CvaBPmH84t0MaHxFMmrMjTQaNTBw92lXT7LZfwYJrDxf1ZpVHjztpbIhfZyYyZmxhIDNcVlb5i4Xoe8Rcva51NgBJN-UAm9cXWBSvr2_EdQbWs7Tz57niquyLpD6fhnTPOWBN6PU2Nz5nMgq-SUyM7srg2Ta6OV9uwOYFAFl0klSBouZ44YTM-T-voCin7EobkTzzXcllDPJ5TPretD_mpkeATlJ3Gi3nPfFLuU2DqFb8fLZjovY5oseSkEvf6NYnGt26r290QzG0cFsZbpJdtysBL-lH-yOwKEl-26IjiWztk0wAxnIdrmILlD9hgXRuyudXI0hx4gH1KTIH7njNNyLMNevUYVGC4cGxa1IpCh4EevhfCT9PQYM-QPyRT4dRBNzoG_y_lZERctUNHAfp80ObBClHEvDjElC2H6kWlO_jBeDiyJpezO7OeYjmDipvKFk3rQgNP87A= - function_call: - name: validate_email - args: - email: test@example.com - role: model - finish_reason: STOP + llm_responses: + - content: + parts: + - thought_signature: Cq0EAR_MhbYyfIgI1M5KlVyG9HzjQ_CvZiHb_RQ2KR0H_UkDj-LDdxdVayqSpG8F6wPq4aGB6lZlqjZIGvA5H2zX2RQ_Iu8Wb8t_wKoEpW4XcwzzU9Org_ZvTNx4TZHll5cH5ebo1LPRWfTqVn7cC1N5KwDZtS2XLwCmitucAAKGzGH4c-tM0dgj57NoMFa63iaHizzi2zupKoGPBB-ZmakNHAHRspkl85hKaq8m4fELHNNMnyi596jcGRHxTDBiqHmNG8PyRiOXRM9VOkNnPU8l2DN7b6CvaBPmH84t0MaHxFMmrMjTQaNTBw92lXT7LZfwYJrDxf1ZpVHjztpbIhfZyYyZmxhIDNcVlb5i4Xoe8Rcva51NgBJN-UAm9cXWBSvr2_EdQbWs7Tz57niquyLpD6fhnTPOWBN6PU2Nz5nMgq-SUyM7srg2Ta6OV9uwOYFAFl0klSBouZ44YTM-T-voCin7EobkTzzXcllDPJ5TPretD_mpkeATlJ3Gi3nPfFLuU2DqFb8fLZjovY5oseSkEvf6NYnGt26r290QzG0cFsZbpJdtysBL-lH-yOwKEl-26IjiWztk0wAxnIdrmILlD9hgXRuyudXI0hx4gH1KTIH7njNNyLMNevUYVGC4cGxa1IpCh4EevhfCT9PQYM-QPyRT4dRBNzoG_y_lZERctUNHAfp80ObBClHEvDjElC2H6kWlO_jBeDiyJpezO7OeYjmDipvKFk3rQgNP87A= + function_call: + name: validate_email + args: + email: test@example.com + role: model + finish_reason: STOP - user_message_index: 0 agent_name: booking_assistant tool_recording: @@ -108,7 +108,8 @@ void testLoadCRecording() throws Exception { assertThat(systemInstructionText.get()).contains("booking assistant"); // Verify URL-safe Base64 deserialization (thought_signature with '_' and '-' characters) - var responseContent = firstRecording.llmRecording().get().llmResponse().get().content().get(); + var responseContent = + firstRecording.llmRecording().get().llmResponses().get().get(0).content().get(); var thoughtSignature = getOnlyPart(responseContent).thoughtSignature(); assertThat(thoughtSignature).isPresent(); assertThat(thoughtSignature.get()).isNotEmpty(); diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index abd3c60f2..f2118f9cc 100644 --- a/maven_plugin/examples/custom_tools/pom.xml +++ b/maven_plugin/examples/custom_tools/pom.xml @@ -4,7 +4,7 @@ com.example custom-tools-example - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index 309fe9364..5c0f4462d 100644 --- a/maven_plugin/examples/simple-agent/pom.xml +++ b/maven_plugin/examples/simple-agent/pom.xml @@ -4,7 +4,7 @@ com.example simple-adk-agent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index 6ff3404f3..c48331f72 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index ffe904d74..11696db73 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT pom Google Agent Development Kit Maven Parent POM diff --git a/tutorials/city-time-weather/pom.xml b/tutorials/city-time-weather/pom.xml index f4e8bdb52..76b7331f3 100644 --- a/tutorials/city-time-weather/pom.xml +++ b/tutorials/city-time-weather/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index b6e649222..a330cf4bd 100644 --- a/tutorials/live-audio-single-agent/pom.xml +++ b/tutorials/live-audio-single-agent/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.1-SNAPSHOT + 0.9.1-SNAPSHOT ../../pom.xml