Skip to content

Commit 9f768f9

Browse files
committed
feat: add sample agent providing currency conversion to demonstrate multi-turn dialogue and streaming responses.
1 parent b1ad6a5 commit 9f768f9

9 files changed

Lines changed: 406 additions & 368 deletions

File tree

samples/java/agents/currency_exchange_rates/client/src/main/java/com/samples/a2a/client/TestClient.java

Lines changed: 185 additions & 161 deletions
Large diffs are not rendered by default.
Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package com.samples.a2a.client;
2-
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
/// usr/bin/env jbang "$0" "$@" ; exit $?
33
//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.2.Final
44
//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.2.Final
55
//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.2.Final
@@ -14,27 +14,29 @@
1414
* <p>
1515
* Prerequisites: - JBang installed (see
1616
* https://www.jbang.dev/documentation/guide/latest/installation.html) - A
17-
* running Currency Agent server (see README.md for instructions on setting up the
17+
* running Currency Agent server (see README.md for instructions on setting
18+
* up the
1819
* agent)
1920
*
2021
* <p>
2122
* Usage: $ jbang TestClientRunner.java
2223
*
2324
* <p>
24-
* The script will communicate with the Currency Agent server and send the message
25+
* The script will communicate with the Currency Agent server and send the
26+
* message
2527
* "how much is 10 USD in INR?" to demonstrate the A2A protocol interaction.
2628
*/
2729
public final class TestClientRunner {
2830

29-
private TestClientRunner() {
30-
// this avoids a lint issue
31-
}
31+
private TestClientRunner() {
32+
// this avoids a lint issue
33+
}
3234

33-
/**
34-
* Client entry point.
35-
* @param args this methode doesn't take into account these args
36-
*/
37-
public static void main(final String[] args) {
38-
TestClient.main(args);
39-
}
35+
/**
36+
* Client entry point.
37+
* @param args this methode doesn't take into account these args
38+
*/
39+
public static void main(final String[] args) {
40+
TestClient.main(args);
41+
}
4042
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Currency Agent package.
3+
*/
4+
package com.samples.a2a.client;

samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgent.java

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,38 @@
1111
@RegisterAiService(tools = CurrencyService.class)
1212
@ApplicationScoped
1313
@SystemMessage("""
14-
You are a specialized assistant for currency conversions.
15-
Your sole purpose is to use the 'getExchangeRate' tool to answer questions about currency exchange rates.
16-
If the user asks about anything other than currency conversion or exchange rates,
17-
politely state that you cannot help with that topic and can only assist with currency-related queries.
18-
Do not attempt to answer unrelated questions or use tools for other purposes.
19-
""")
14+
You are a specialized assistant for currency conversions.
15+
Your sole purpose is to use the 'getExchangeRate' tool to answer questions about currency exchange rates.
16+
If the user asks about anything other than currency conversion or exchange rates,
17+
politely state that you cannot help with that topic and can only assist with currency-related queries.
18+
Do not attempt to answer unrelated questions or use tools for other purposes.
19+
""")
2020
public interface CurrencyAgent {
2121

2222

23-
@SystemMessage("""
24-
Set response status to input_required if the user needs to provide more information to complete the request.
25-
Set response status to error if there is an error while processing the request.
26-
Set response status to completed you have an answer for the currency exchange rate.
27-
You must respond ONLY in valid JSON.
28-
Do not include explanations, comments, or text outside the JSON object.
29-
Your response MUST follow this JSON schema:
30-
{
31-
"status": "input_required | completed | error",
32-
"message": "<string>"
33-
}
34-
""")
35-
ResponseFormat handleRequest(@UserMessage String question);
23+
@SystemMessage("""
24+
Set response status to input_required if the user needs to provide more information to complete the request.
25+
Set response status to error if there is an error while processing the request.
26+
Set response status to completed you have an answer for the currency exchange rate.
27+
You must respond ONLY in valid JSON.
28+
Do not include explanations, comments, or text outside the JSON object.
29+
Your response MUST follow this JSON schema:
30+
{
31+
"status": "input_required | completed | error",
32+
"message": "<string>"
33+
}
34+
35+
You must follow these rules when answering currency‑conversion questions:
36+
1. If the user provides BOTH the source currency and the target currency
37+
(example: "How much is 10 USD in INR"), you must answer the question directly.
38+
39+
2. If the user provides ONLY the source currency without specifying the target
40+
(example: "How much is the exchange rate for 1 USD"), do NOT answer the conversion.
41+
Instead, ask the user to provide the missing information.
42+
Your question must be natural and specific to what is missing.
43+
44+
3. Never assume or guess the target currency.
45+
4. Never provide an answer until all required currencies are explicitly stated.
46+
""")
47+
ResponseFormat handleRequest(@UserMessage String question);
3648
}

samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentCardProducer.java

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,50 @@
1313
@ApplicationScoped
1414
public final class CurrencyAgentCardProducer {
1515

16-
/**
17-
* The HTTP port for the agent service.
18-
*/
19-
@ConfigProperty(name = "quarkus.http.port")
20-
private int httpPort;
16+
/**
17+
* The HTTP port for the agent service.
18+
*/
19+
@ConfigProperty(name = "quarkus.http.port")
20+
private int httpPort;
2121

22-
/**
23-
* Gets the HTTP port.
24-
*
25-
* @return the HTTP port
26-
*/
27-
public int getHttpPort() {
28-
return httpPort;
29-
}
22+
/**
23+
* Gets the HTTP port.
24+
*
25+
* @return the HTTP port
26+
*/
27+
public int getHttpPort() {
28+
return httpPort;
29+
}
3030

31-
/**
32-
* Produces the agent card for the currency agent.
33-
*
34-
* @return the configured agent card
35-
*/
36-
@Produces
37-
@PublicAgentCard
38-
public AgentCard agentCard() {
39-
return new AgentCard.Builder()
40-
.name("Currency Agent")
41-
.description("Assistant for currency conversions")
42-
.url("http://localhost:" + getHttpPort())
43-
.version("1.0.0")
44-
.capabilities(
45-
new AgentCapabilities.Builder()
46-
.streaming(true)
47-
.pushNotifications(true)
48-
.build())
49-
.defaultInputModes(List.of("text"))
50-
.defaultOutputModes(List.of("text"))
51-
.skills(List.of(
52-
new AgentSkill.Builder()
53-
.id("convert_currency")
54-
.name("Currency Exchange Rates Tool")
55-
.description("Helps with exchange values between various currencies")
56-
.tags(List.of("currency conversion", "currency exchange"))
57-
.examples(List.of("What is exchange rate between USD and GBP?"))
58-
.build())
59-
)
60-
.build();
61-
}
31+
/**
32+
* Produces the agent card for the currency agent.
33+
*
34+
* @return the configured agent card
35+
*/
36+
@Produces
37+
@PublicAgentCard
38+
public AgentCard agentCard() {
39+
return new AgentCard.Builder()
40+
.name("Currency Agent")
41+
.description("Assistant for currency conversions")
42+
.url("http://localhost:" + getHttpPort())
43+
.version("1.0.0")
44+
.capabilities(
45+
new AgentCapabilities.Builder()
46+
.streaming(true)
47+
.pushNotifications(true)
48+
.build())
49+
.defaultInputModes(List.of("text"))
50+
.defaultOutputModes(List.of("text"))
51+
.skills(List.of(
52+
new AgentSkill.Builder()
53+
.id("convert_currency")
54+
.name("Currency Exchange Rates Tool")
55+
.description("Helps with exchange values between various currencies")
56+
.tags(List.of("currency conversion", "currency exchange"))
57+
.examples(List.of("What is exchange rate between USD and GBP?"))
58+
.build())
59+
)
60+
.build();
61+
}
6262
}

samples/java/agents/currency_exchange_rates/server/src/main/java/com/samples/a2a/server/CurrencyAgentExecutorProducer.java

Lines changed: 73 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -21,117 +21,91 @@
2121
@ApplicationScoped
2222
public final class CurrencyAgentExecutorProducer {
2323

24-
private final CurrencyAgent currencyAgent;
24+
private final CurrencyAgent currencyAgent;
2525

26-
public CurrencyAgentExecutorProducer(CurrencyAgent currencyAgent) {
27-
this.currencyAgent = currencyAgent;
28-
}
29-
30-
public CurrencyAgent getCurrencyAgent() {
31-
return currencyAgent;
32-
}
33-
34-
@Produces
35-
public AgentExecutor agentExecutor() {
36-
return new CurrencyAgentExecutor(getCurrencyAgent());
37-
}
38-
39-
private class CurrencyAgentExecutor implements AgentExecutor {
40-
private final CurrencyAgent agent;
41-
42-
public CurrencyAgentExecutor(CurrencyAgent currencyAgent) {
43-
this.agent = currencyAgent;
44-
}
45-
46-
@Override
47-
public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
48-
// executeSimple(context, eventQueue);
49-
executeLoop(context, eventQueue);
50-
}
26+
public CurrencyAgentExecutorProducer(CurrencyAgent currencyAgent) {
27+
this.currencyAgent = currencyAgent;
28+
}
5129

52-
void executeLoop(RequestContext context, EventQueue eventQueue) {
53-
var updater = new TaskUpdater(context, eventQueue);
54-
if (context.getTask() == null) {
55-
// Initial message - create task in SUBMITTED → WORKING state
56-
updater.submit();
57-
updater.startWork();
58-
59-
getResponse(context, updater);
60-
} else {
61-
// Subsequent messages - add artifacts
62-
getResponse(context, updater);
63-
}
64-
}
65-
66-
private void getResponse(RequestContext context, TaskUpdater updater) {
67-
// extract the text from the message
68-
var message = extractTextFromMessage(context.getMessage());
69-
70-
// call the currency agent with the message
71-
ResponseFormat response = agent.handleRequest(message);
72-
System.out.printf("Response: %s %n", response);
73-
74-
// create the response part
75-
TextPart responsePart = new TextPart(response.message(), null);
76-
List<Part<?>> parts = List.of(responsePart);
77-
78-
// add the response as an artifact
79-
updater.addArtifact(parts);
80-
switch (response.status()) {
81-
case INPUT_REQUIRED -> updater.requiresInput(true);
82-
case COMPLETED -> updater.complete();
83-
case ERROR -> updater.fail();
84-
}
85-
}
30+
public CurrencyAgent getCurrencyAgent() {
31+
return currencyAgent;
32+
}
8633

87-
void executeSimple(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
88-
var updater = new TaskUpdater(context, eventQueue);
34+
@Produces
35+
public AgentExecutor agentExecutor() {
36+
return new CurrencyAgentExecutor(getCurrencyAgent());
37+
}
8938

90-
// mark the task as submitted and start working on it
91-
if (context.getTask() == null) {
92-
updater.submit();
93-
}
94-
updater.startWork();
39+
private class CurrencyAgentExecutor implements AgentExecutor {
40+
private final CurrencyAgent agent;
9541

96-
// extract the text from the message
97-
var message = extractTextFromMessage(context.getMessage());
42+
public CurrencyAgentExecutor(CurrencyAgent currencyAgent) {
43+
this.agent = currencyAgent;
44+
}
9845

99-
// call the currency agent with the message
100-
ResponseFormat response = agent.handleRequest(message);
101-
System.out.printf("Response: %s %n", response);
46+
@Override
47+
public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
48+
executeLoop(context, eventQueue);
49+
}
10250

103-
// create the response part
104-
TextPart responsePart = new TextPart(response.message(), null);
105-
List<Part<?>> parts = List.of(responsePart);
51+
void executeLoop(RequestContext context, EventQueue eventQueue) {
52+
var updater = new TaskUpdater(context, eventQueue);
53+
if (context.getTask() == null) {
54+
// Initial message - create task in SUBMITTED → WORKING state
55+
updater.submit();
56+
updater.startWork();
57+
58+
getResponse(context, updater);
59+
} else {
60+
// Subsequent messages - add artifacts
61+
getResponse(context, updater);
62+
}
63+
}
10664

107-
// add the response as an artifact and complete the task
108-
updater.addArtifact(parts, null, null, null);
109-
updater.complete();
110-
}
65+
private void getResponse(RequestContext context, TaskUpdater updater) {
66+
// extract the text from the message
67+
var message = extractTextFromMessage(context.getMessage());
68+
69+
// call the currency agent with the message
70+
ResponseFormat response = agent.handleRequest(message);
71+
System.out.printf("Response: %s %n", response);
72+
73+
// create the response part
74+
TextPart responsePart = new TextPart(response.message(), null);
75+
List<Part<?>> parts = List.of(responsePart);
76+
77+
// add the response as an artifact
78+
updater.addArtifact(parts);
79+
switch (response.status()) {
80+
case INPUT_REQUIRED -> updater.requiresInput(true);
81+
case COMPLETED -> updater.complete();
82+
case ERROR -> updater.fail();
83+
}
84+
}
11185

112-
private String extractTextFromMessage(final Message message) {
113-
final StringBuilder builder = new StringBuilder();
114-
if (message.getParts() != null) {
115-
for (final Part<?> part : message.getParts()) {
116-
if (part instanceof TextPart textPart) {
117-
builder.append(textPart.getText());
118-
}
119-
}
120-
}
121-
return builder.toString();
86+
private String extractTextFromMessage(final Message message) {
87+
final StringBuilder builder = new StringBuilder();
88+
if (message.getParts() != null) {
89+
for (final Part<?> part : message.getParts()) {
90+
if (part instanceof TextPart textPart) {
91+
builder.append(textPart.getText());
92+
}
12293
}
94+
}
95+
return builder.toString();
96+
}
12397

124-
@Override
125-
public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
126-
var task = context.getTask();
98+
@Override
99+
public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
100+
var task = context.getTask();
127101

128-
if (task.getStatus().state() == TaskState.CANCELED || task.getStatus().state() == TaskState.COMPLETED) {
129-
// task already canceled or completed
130-
throw new TaskNotCancelableError();
131-
}
102+
if (task.getStatus().state() == TaskState.CANCELED || task.getStatus().state() == TaskState.COMPLETED) {
103+
// task already canceled or completed
104+
throw new TaskNotCancelableError();
105+
}
132106

133-
var updater = new TaskUpdater(context, eventQueue);
134-
updater.cancel();
135-
}
107+
var updater = new TaskUpdater(context, eventQueue);
108+
updater.cancel();
136109
}
110+
}
137111
}

0 commit comments

Comments
 (0)