From e3ea378051e5c4e5e5031657467145779e42db55 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 27 Feb 2026 08:52:36 -0800 Subject: [PATCH] feat: add example on how to expose agent via A2A protocol PiperOrigin-RevId: 876276519 --- a2a/pom.xml | 5 + contrib/samples/a2a_server/README.md | 70 +++++++++ contrib/samples/a2a_server/pom.xml | 142 ++++++++++++++++++ .../samples/a2aagent/AgentCardProducer.java | 33 ++++ .../a2aagent/AgentExecutorProducer.java | 28 ++++ .../adk/samples/a2aagent/StartupConfig.java | 17 +++ .../adk/samples/a2aagent/agent/Agent.java | 107 +++++++++++++ .../src/main/resources/agent/agent.json | 18 +++ .../src/main/resources/application.properties | 10 ++ contrib/samples/pom.xml | 1 + 10 files changed, 431 insertions(+) create mode 100644 contrib/samples/a2a_server/README.md create mode 100644 contrib/samples/a2a_server/pom.xml create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java create mode 100644 contrib/samples/a2a_server/src/main/resources/agent/agent.json create mode 100644 contrib/samples/a2a_server/src/main/resources/application.properties diff --git a/a2a/pom.xml b/a2a/pom.xml index d98064ec8..d7034f36c 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -104,6 +104,11 @@ ${truth.version} test + + org.mockito + mockito-core + test + diff --git a/contrib/samples/a2a_server/README.md b/contrib/samples/a2a_server/README.md new file mode 100644 index 000000000..5351e32c0 --- /dev/null +++ b/contrib/samples/a2a_server/README.md @@ -0,0 +1,70 @@ +# Google ADK A2A Agent Server Sample + +This sample demonstrates how to expose a Google ADK (Agent Development Kit) +agent via the A2A (Agent-to-Agent) protocol using A2A SDK and Quarkus service. + +## Overview + +The application implements a simple conversational agent that checks whether +given numbers are prime numbers. It uses the `LlmAgent` from the Google ADK and +exposes it via an A2A server. + +### Key Components + +* **`Agent.java`**: Defines the `LlmAgent` instance (`check_prime_agent`) and + the `checkPrime` tool function it uses to verify numbers. +* **`AgentCardProducer.java`**: Loads and provides the `AgentCard` metadata + (from `agent.json`) which defines the agent's identity and capabilities in + the A2A network. +* **`AgentExecutorProducer.java`**: Configures and provides the A2A + `AgentExecutor`, implemented by the ADK library to wire ADK-owned agents + automatically. +* **`StartupConfig.java`**: Contains initialization logic, such as registering + JSON modules for the Vert.x/Quarkus runtime. +* **`application.properties`**: Contains a configuration for the Quarkus + service and A2A, such as port where application will be exposed, application + name and event processing timeouts. + +## Building the Project + +You can build the project using Maven: + +```shell +mvn clean install +``` + +The Java server can be started using `mvn` as follow (don't forget to set your +GOOGLE_API_KEY before running the service): + +```bash +export GOOGLE_API_KEY= + +cd contrib/samples/a2a_server +mvn quarkus:dev +``` + +## Sample request + +```bash +curl -X POST http://localhost:9090 \ + -H 'Content-Type: application/json' \ + -d '{ + "jsonrpc": "2.0", + "id": "cli-check-2", + "method": "message/stream", + "params": { + "message": { + "kind": "message", + "contextId": "cli-demo-context", + "messageId": "cli-check-2", + "role": "user", + "parts": [ + { + "kind": "text", + "text": "Is 2 prime?" + } + ] + } + } + }' +``` diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml new file mode 100644 index 000000000..d47a0c984 --- /dev/null +++ b/contrib/samples/a2a_server/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + + com.google.adk + google-adk-samples + 0.7.1-SNAPSHOT + .. + + + google-adk-sample-a2a-agent + jar + + Google ADK - Sample - A2A Agent Server + Demonstrates exposing ADK agent via A2A. + + + UTF-8 + 17 + ${project.version} + ${project.version} + 0.3.0.Beta1 + 3.30.6 + 0.8 + + + + + + io.quarkus + quarkus-bom + ${quarkus.platform.version} + pom + import + + + + + + + io.github.a2asdk + a2a-java-sdk-reference-jsonrpc + ${a2a.sdk.version} + + + io.quarkus + quarkus-resteasy-jackson + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.google.adk + google-adk + ${google-adk.version} + + + com.google.adk + google-adk-a2a + ${google-adk-a2a.version} + + + + io.github.a2asdk + a2a-java-sdk-spec + ${a2a.sdk.version} + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-reactive-routes + + + io.quarkus + quarkus-jackson + + + + io.github.a2asdk + a2a-java-sdk-client + ${a2a.sdk.version} + + + + com.google.flogger + flogger + ${flogger.version} + + + + com.google.flogger + google-extensions + ${flogger.version} + + + + com.google.flogger + flogger-system-backend + ${flogger.version} + + + + + + + + src/main/java + + **/*.json + + + + src/main/resources + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + + diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java new file mode 100644 index 000000000..0937e2512 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java @@ -0,0 +1,33 @@ +package com.google.adk.samples.a2aagent; + +import io.a2a.server.PublicAgentCard; +import io.a2a.spec.AgentCard; +import io.a2a.util.Utils; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** Produces the {@link AgentCard} from the bundled JSON resources. */ +@ApplicationScoped +public class AgentCardProducer { + + @Produces + @PublicAgentCard + public AgentCard agentCard() { + try (InputStream is = getClass().getResourceAsStream("/agent/agent.json")) { + if (is == null) { + throw new RuntimeException("agent.json not found in resources"); + } + + // Read the JSON file content + String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); + + // Use the SDK's built-in mapper to convert JSON string to AgentCard record + return Utils.OBJECT_MAPPER.readValue(json, AgentCard.class); + + } catch (Exception e) { + throw new RuntimeException("Failed to load AgentCard from JSON", e); + } + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java new file mode 100644 index 000000000..4ecd2517d --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java @@ -0,0 +1,28 @@ +package com.google.adk.samples.a2aagent; + +import com.google.adk.a2a.executor.AgentExecutorConfig; +import com.google.adk.samples.a2aagent.agent.Agent; +import com.google.adk.sessions.InMemorySessionService; +import io.a2a.server.agentexecution.AgentExecutor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** Produces the {@link AgentExecutor} instance that handles agent interactions. */ +@ApplicationScoped +public class AgentExecutorProducer { + + @ConfigProperty(name = "my.adk.app.name", defaultValue = "default-app") + String appName; + + @Produces + public AgentExecutor agentExecutor() { + InMemorySessionService sessionService = new InMemorySessionService(); + return new com.google.adk.a2a.executor.AgentExecutor.Builder() + .agent(Agent.ROOT_AGENT) + .appName(appName) + .sessionService(sessionService) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .build(); + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java new file mode 100644 index 000000000..0da70b086 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java @@ -0,0 +1,17 @@ +package com.google.adk.samples.a2aagent; + +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.quarkus.runtime.StartupEvent; +import io.vertx.core.json.jackson.DatabindCodec; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +/** Configuration applied on startup, such as Jackson module registrations. */ +@ApplicationScoped +public class StartupConfig { + + void onStart(@Observes StartupEvent ev) { + // Register globally for Vert.x's internal JSON handling + DatabindCodec.mapper().registerModule(new JavaTimeModule()); + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java new file mode 100644 index 000000000..a415f618f --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java @@ -0,0 +1,107 @@ +package com.google.adk.samples.a2aagent.agent; + +import static java.util.stream.Collectors.joining; + +import com.google.adk.agents.LlmAgent; +import com.google.adk.tools.FunctionTool; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.flogger.GoogleLogger; +import io.reactivex.rxjava3.core.Maybe; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Agent that can check whether numbers are prime. */ +public final class Agent { + + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + /** + * Checks if a list of numbers are prime. + * + * @param nums The list of numbers to check + * @return A map containing the result message + */ + public static ImmutableMap checkPrime(List nums) { + logger.atInfo().log("checkPrime called with nums=%s", nums); + Set primes = new HashSet<>(); + for (int num : nums) { + if (num <= 1) { + continue; + } + boolean isPrime = true; + for (int i = 2; i <= Math.sqrt(num); i++) { + if (num % i == 0) { + isPrime = false; + break; + } + } + if (isPrime) { + primes.add(num); + } + } + String result; + if (primes.isEmpty()) { + result = "No prime numbers found."; + } else if (primes.size() == 1) { + int only = primes.iterator().next(); + // Per request: singular phrasing without article + result = only + " is prime number."; + } else { + result = primes.stream().map(String::valueOf).collect(joining(", ")) + " are prime numbers."; + } + logger.atInfo().log("checkPrime result=%s", result); + return ImmutableMap.of("result", result); + } + + public static final LlmAgent ROOT_AGENT = + LlmAgent.builder() + .model("gemini-2.5-pro") + .name("check_prime_agent") + .description("check prime agent that can check whether numbers are prime.") + .instruction( + """ + You check whether numbers are prime. + + If the last user message contains numbers, call checkPrime exactly once with exactly + those integers as a list (e.g., [2]). Never add other numbers. Do not ask for + clarification. Return only the tool's result. + + Always pass a list of integers to the tool (use a single-element list for one + number). Never pass strings. + """) + // Log the exact contents passed to the LLM request for verification + .beforeModelCallback( + (callbackContext, llmRequest) -> { + try { + logger.atInfo().log( + "Invocation events (count=%d): %s", + callbackContext.events().size(), callbackContext.events()); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("BeforeModel logging error"); + } + return Maybe.empty(); + }) + .afterModelCallback( + (callbackContext, llmResponse) -> { + try { + String content = + llmResponse.content().map(Object::toString).orElse(""); + logger.atInfo().log("AfterModel content=%s", content); + llmResponse + .errorMessage() + .ifPresent( + error -> + logger.atInfo().log( + "AfterModel errorMessage=%s", error.replace("\n", "\\n"))); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("AfterModel logging error"); + } + return Maybe.empty(); + }) + .tools(ImmutableList.of(FunctionTool.create(Agent.class, "checkPrime"))) + .build(); + + private Agent() {} +} diff --git a/contrib/samples/a2a_server/src/main/resources/agent/agent.json b/contrib/samples/a2a_server/src/main/resources/agent/agent.json new file mode 100644 index 000000000..4a0848282 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/resources/agent/agent.json @@ -0,0 +1,18 @@ +{ + "capabilities": {"streaming": true}, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["application/json"], + "description": "An agent specialized in checking whether numbers are prime. It can efficiently determine the primality of individual numbers or lists of numbers.", + "name": "check_prime_agent", + "skills": [ + { + "id": "prime_checking", + "name": "Prime Number Checking", + "description": "Check if numbers in a list are prime using efficient mathematical algorithms", + "tags": ["mathematical", "computation", "prime", "numbers"] + } + ], + "preferredTransport": "JSONRPC", + "url": "http://localhost:9090", + "version": "1.0.0" +} diff --git a/contrib/samples/a2a_server/src/main/resources/application.properties b/contrib/samples/a2a_server/src/main/resources/application.properties new file mode 100644 index 000000000..ba7a5f2b0 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/resources/application.properties @@ -0,0 +1,10 @@ +# Timeout for the agent to complete execution (default 30s) +a2a.blocking.agent.timeout.seconds=30 + +# Timeout for final event processing (default 5s) +a2a.blocking.consumption.timeout.seconds=5 + +# Custom application name for the ADK Runner +my.adk.app.name=My-JSONRPC-Agent + +quarkus.http.port=9090 \ No newline at end of file diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 84a4898d6..f3c8359b8 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -17,6 +17,7 @@ a2a_basic + a2a_server configagent helloworld mcpfilesystem