> parts) {
+ final StringBuilder textBuilder = new StringBuilder();
+ if (parts != null) {
+ for (final Part> part : parts) {
+ if (part instanceof TextPart textPart) {
+ textBuilder.append(textPart.getText());
+ }
+ }
+ }
+ return textBuilder.toString();
+ }
+}
diff --git a/samples/java/agents/currency_exchange_rates/client/src/main/java/com/samples/a2a/client/TestClientRunner.java b/samples/java/agents/currency_exchange_rates/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
new file mode 100644
index 000000000..ba2df7e66
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
@@ -0,0 +1,42 @@
+package com.samples.a2a.client;
+/// usr/bin/env jbang "$0" "$@" ; exit $?
+//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.2.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.2.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.2.Final
+//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
+//DEPS io.grpc:grpc-netty-shaded:1.69.1
+//SOURCES TestClient.java
+
+/**
+ * JBang script to run the A2A TestClient example for the Currency Agent. This
+ * script automatically handles the dependencies and runs the client.
+ *
+ *
+ * Prerequisites: - JBang installed (see
+ * https://www.jbang.dev/documentation/guide/latest/installation.html) - A
+ * running Currency Agent server (see README.md for instructions on setting
+ * up the
+ * agent)
+ *
+ *
+ * Usage: $ jbang TestClientRunner.java
+ *
+ *
+ * The script will communicate with the Currency Agent server and send the
+ * message
+ * "how much is 10 USD in INR?" to demonstrate the A2A protocol interaction.
+ */
+public final class TestClientRunner {
+
+ private TestClientRunner() {
+ // this avoids a lint issue
+ }
+
+ /**
+ * Client entry point.
+ * @param args this methode doesn't take into account these args
+ */
+ public static void main(final String[] args) {
+ TestClient.main(args);
+ }
+}
diff --git a/samples/java/agents/currency_exchange_rates/client/src/main/java/com/samples/a2a/client/package-info.java b/samples/java/agents/currency_exchange_rates/client/src/main/java/com/samples/a2a/client/package-info.java
new file mode 100644
index 000000000..00e4fd512
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/client/src/main/java/com/samples/a2a/client/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Currency Agent package.
+ */
+package com.samples.a2a.client;
diff --git a/samples/java/agents/currency_exchange_rates/pom.xml b/samples/java/agents/currency_exchange_rates/pom.xml
new file mode 100644
index 000000000..170089fd4
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/pom.xml
@@ -0,0 +1,20 @@
+
+
+ 4.0.0
+
+
+ com.samples.a2a
+ agents-parent
+ 0.1.0
+
+
+ currency_exchange_rates
+ pom
+
+
+ server
+ client
+
+
diff --git a/samples/java/agents/currency_exchange_rates/server/pom.xml b/samples/java/agents/currency_exchange_rates/server/pom.xml
new file mode 100644
index 000000000..c1166fb94
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+
+ com.samples.a2a
+ currency_exchange_rates
+ 0.1.0
+
+
+ currency-agent-server
+ Currency Agent Server
+ A2A Currency Agent Server Implementation
+
+
+
+ io.github.a2asdk
+ a2a-java-sdk-reference-jsonrpc
+ ${io.a2a.sdk.version}
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ jakarta.enterprise
+ jakarta.enterprise.cdi-api
+ ${jakarta.enterprise.cdi-api.version}
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-ollama
+ ${quarkus.langchain4j.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+
diff --git a/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgent.java b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgent.java
new file mode 100644
index 000000000..aa5c2e787
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgent.java
@@ -0,0 +1,58 @@
+package com.samples.a2a.server;
+
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Currency agent interface that provides currency conversions assistance.
+ */
+@RegisterAiService(tools = CurrencyService.class)
+@ApplicationScoped
+@SystemMessage("""
+ You are a specialized assistant for currency conversions.
+ Your sole purpose is to use the 'getExchangeRate' tool to answer questions
+ about currency exchange rates.
+ If the user asks about anything other than currency conversion or exchange
+ rates, politely state that you cannot help with that topic and can only assist
+ with currency-related queries.
+ Do not attempt to answer unrelated questions or use tools for other purposes.
+ """)
+public interface CurrencyAgent {
+
+ /**
+ * Handle message and provide currency conversion.
+ * @param question the users' question
+ * @return the answer
+ */
+ @SystemMessage("""
+ Set response status to input_required if the user needs to provide more
+ information to complete the request.
+ Set response status to error if there is an error while processing
+ the request.
+ Set response status to completed you have an answer for the currency
+ exchange rate.
+ You must respond ONLY in valid JSON.
+ Do not include explanations, comments, or text outside the JSON object.
+ Your response MUST follow this JSON schema:
+ {
+ "status": "input_required | completed | error",
+ "message": ""
+ }
+ You must follow these rules when answering currency‑conversion questions:
+ 1. If the user provides BOTH the source currency and the target currency
+ (example: "How much is 10 USD in INR"), you must answer the question
+ directly.
+ 2. If the user provides ONLY the source currency without specifying the
+ target (example: "How much is the exchange rate for 1 USD"), do NOT
+ answer the conversion.
+ Instead, ask the user to provide the missing information.
+ Your question must be natural and specific to what is missing.
+ 3. Never assume or guess the target currency.
+ 4. Never provide an answer until all required currencies are explicitly
+ stated.
+ 5. You must replace all placeholders with real values.
+ """)
+ ResponseFormat handleRequest(@UserMessage String question);
+}
diff --git a/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentCardProducer.java b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentCardProducer.java
new file mode 100644
index 000000000..b0fc5f45b
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentCardProducer.java
@@ -0,0 +1,62 @@
+package com.samples.a2a.server;
+
+import io.a2a.server.PublicAgentCard;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentSkill;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import java.util.List;
+
+@ApplicationScoped
+public final class CurrencyAgentCardProducer {
+
+ /**
+ * The HTTP port for the agent service.
+ */
+ @ConfigProperty(name = "quarkus.http.port")
+ private int httpPort;
+
+ /**
+ * Gets the HTTP port.
+ *
+ * @return the HTTP port
+ */
+ public int getHttpPort() {
+ return httpPort;
+ }
+
+ /**
+ * Produces the agent card for the currency agent.
+ *
+ * @return the configured agent card
+ */
+ @Produces
+ @PublicAgentCard
+ public AgentCard agentCard() {
+ return new AgentCard.Builder()
+ .name("Currency Agent")
+ .description("Assistant for currency conversions")
+ .url("http://localhost:" + getHttpPort())
+ .version("1.0.0")
+ .capabilities(
+ new AgentCapabilities.Builder()
+ .streaming(true)
+ .pushNotifications(true)
+ .build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(List.of(
+ new AgentSkill.Builder()
+ .id("convert_currency")
+ .name("Currency Exchange Rates Tool")
+ .description("Helps with exchange values between various currencies")
+ .tags(List.of("currency conversion", "currency exchange"))
+ .examples(List.of("What is exchange rate between USD and GBP?"))
+ .build())
+ )
+ .build();
+ }
+}
diff --git a/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentExecutorProducer.java b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentExecutorProducer.java
new file mode 100644
index 000000000..97b7c57a5
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentExecutorProducer.java
@@ -0,0 +1,147 @@
+package com.samples.a2a.server;
+
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.server.agentexecution.RequestContext;
+import io.a2a.server.events.EventQueue;
+import io.a2a.server.tasks.TaskUpdater;
+import io.a2a.spec.JSONRPCError;
+import io.a2a.spec.Message;
+import io.a2a.spec.Part;
+import io.a2a.spec.TaskNotCancelableError;
+import io.a2a.spec.TaskState;
+import io.a2a.spec.TextPart;
+import io.quarkus.logging.Log;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+
+import java.util.List;
+
+/**
+ * Producer for currency agent executor.
+ */
+@ApplicationScoped
+public final class CurrencyAgentExecutorProducer {
+
+ /**
+ * The currency agent instance.
+ */
+ private final CurrencyAgent agent;
+
+ /**
+ * Constructor for CurrencyAgentExecutorProducer.
+ *
+ * @param currencyAgent the currency agent
+ */
+ public CurrencyAgentExecutorProducer(final CurrencyAgent currencyAgent) {
+ this.agent = currencyAgent;
+ }
+
+ /**
+ * The currency agent instance.
+ *
+ * @return the currency agent instance
+ */
+ public CurrencyAgent getCurrencyAgent() {
+ return agent;
+ }
+
+ /**
+ * Produces the agent executor for the currency agent.
+ *
+ * @return the configured agent executor
+ */
+ @Produces
+ public AgentExecutor agentExecutor() {
+ return new CurrencyAgentExecutor(getCurrencyAgent());
+ }
+
+ /**
+ * Currency agent executor implementation.
+ */
+ private class CurrencyAgentExecutor implements AgentExecutor {
+ /**
+ * The currency agent instance.
+ */
+ private final CurrencyAgent agent;
+
+ /**
+ * Constructor for CurrencyAgentExecutor.
+ *
+ * @param currencyAgent the currency agent instance
+ */
+ CurrencyAgentExecutor(final CurrencyAgent currencyAgent) {
+ this.agent = currencyAgent;
+ }
+
+ @Override
+ public void execute(final RequestContext context,
+ final EventQueue eventQueue) throws JSONRPCError {
+ executeLoop(context, eventQueue);
+ }
+
+ void executeLoop(final RequestContext context,
+ final EventQueue eventQueue) {
+ var updater = new TaskUpdater(context, eventQueue);
+ if (context.getTask() == null) {
+ // Initial message - create task in SUBMITTED → WORKING state
+ updater.submit();
+ updater.startWork();
+
+ getResponse(context, updater);
+ } else {
+ // Subsequent messages - add artifacts
+ getResponse(context, updater);
+ }
+ }
+
+ private void getResponse(final RequestContext context,
+ final TaskUpdater updater) {
+ // extract the text from the message
+ var message = extractTextFromMessage(context.getMessage());
+
+ // call the currency agent with the message
+ ResponseFormat response = agent.handleRequest(message);
+ Log.infof("Response: %s", response);
+
+ // create the response part
+ TextPart responsePart = new TextPart(response.message(), null);
+ List> parts = List.of(responsePart);
+
+ // add the response as an artifact
+ updater.addArtifact(parts);
+ switch (response.status()) {
+ case INPUT_REQUIRED -> updater.requiresInput(true);
+ case COMPLETED -> updater.complete();
+ case ERROR -> updater.fail();
+ default -> throw new RuntimeException("Unknown status.");
+ }
+ }
+
+ private String extractTextFromMessage(final Message message) {
+ final StringBuilder builder = new StringBuilder();
+ if (message.getParts() != null) {
+ for (final Part> part : message.getParts()) {
+ if (part instanceof TextPart textPart) {
+ builder.append(textPart.getText());
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public void cancel(final RequestContext context,
+ final EventQueue eventQueue) throws JSONRPCError {
+ var task = context.getTask();
+
+ if (task.getStatus().state() == TaskState.CANCELED
+ || task.getStatus().state() == TaskState.COMPLETED) {
+ // task already canceled or completed
+ throw new TaskNotCancelableError();
+ }
+
+ var updater = new TaskUpdater(context, eventQueue);
+ updater.cancel();
+ }
+ }
+}
diff --git a/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyService.java b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyService.java
new file mode 100644
index 000000000..910b3af6d
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyService.java
@@ -0,0 +1,67 @@
+package com.samples.a2a.server;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.langchain4j.agent.tool.Tool;
+import jakarta.enterprise.context.ApplicationScoped;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Map;
+
+@ApplicationScoped
+public final class CurrencyService {
+
+ /**
+ * Object mapper to use.
+ */
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ /**
+ * ERROR_CODE_400 to use.
+ */
+ public static final int ERROR_CODE_400 = 400;
+ /**
+ * HttpClient to use.
+ */
+ private final HttpClient client = HttpClient.newBuilder().build();
+
+ /**
+ * Provides currency conversions from one to another currency.
+ *
+ * @param currencyFrom source currency
+ * @param currencyTo target currency
+ * @return currency‑conversion rate
+ */
+ @Tool("Provides currency conversions from one to another currency")
+ public Map getExchangeRate(final String currencyFrom,
+ final String currencyTo) {
+ String url = "https://api.frankfurter.app/latest";
+ URI uri = URI.create(url + "?from=%s&to=%s".formatted(currencyFrom,
+ currencyTo));
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(uri)
+ .GET()
+ .build();
+ HttpResponse response = client.send(request,
+ HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() >= ERROR_CODE_400) {
+ throw new RuntimeException("Request exchange rate failed with status "
+ + response.statusCode());
+ }
+
+ Map data = OBJECT_MAPPER.readValue(response.body(),
+ Map.class);
+ if (data == null || !data.containsKey("rates")) {
+ throw new RuntimeException(
+ "Invalid API response format from Frankfurter API");
+ }
+ return data;
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("Request exchange rate failed.", e);
+ }
+ }
+}
diff --git a/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/ResponseFormat.java b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/ResponseFormat.java
new file mode 100644
index 000000000..97242104f
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/ResponseFormat.java
@@ -0,0 +1,18 @@
+package com.samples.a2a.server;
+
+public record ResponseFormat(Status status, String message) {
+ public enum Status {
+ /**
+ * INPUT_REQUIRED status.
+ */
+ INPUT_REQUIRED,
+ /**
+ * COMPLETED status.
+ */
+ COMPLETED,
+ /**
+ * ERROR status.
+ */
+ ERROR
+ }
+}
diff --git a/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/package-info.java b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/package-info.java
new file mode 100644
index 000000000..4860241fb
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Currency Agent package.
+ */
+package com.samples.a2a.server;
diff --git a/samples/java/agents/currency_exchange_rates/server/src/main/resources/application.properties b/samples/java/agents/currency_exchange_rates/server/src/main/resources/application.properties
new file mode 100644
index 000000000..02ca86242
--- /dev/null
+++ b/samples/java/agents/currency_exchange_rates/server/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+quarkus.http.port=10000
+quarkus.langchain4j.ollama.chat-model.model-id=qwen2.5:7b
+quarkus.langchain4j.ollama.chat-model.temperature=0
+quarkus.langchain4j.ollama.timeout=180s
diff --git a/samples/java/agents/pom.xml b/samples/java/agents/pom.xml
index 8e6db5163..193a76053 100644
--- a/samples/java/agents/pom.xml
+++ b/samples/java/agents/pom.xml
@@ -15,6 +15,7 @@
dice_agent_multi_transport
magic_8_ball_security
weather_mcp
+ currency_exchange_rates