Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI

An AI agent refers to a software entity that can perceive, reason, and act autonomously to achieve specific goals using artificial intelligence techniques like natural language processing, machine learning, or reasoning systems.

I developed an AI agen…


This content originally appeared on DEV Community and was authored by Tobi Awanebi

An AI agent refers to a software entity that can perceive, reason, and act autonomously to achieve specific goals using artificial intelligence techniques like natural language processing, machine learning, or reasoning systems.

I developed an AI agent for Telex which takes a regex pattern and provides a human-friendly explanation on the type(s) of string matched by that regex pattern. The inspiration for this agent lie in an API I developed just before this, where I had to use regex for some natural language processing (you can check out the project here). Though I had learnt regex previously, it felt like I was seeing it for the first time. Regex is just like that. So when Telex sought for more AI agents for their platform, I decided to develop this agent.

Here's how I did it using Java, Spring AI and Spring Boot

Initial Setup

1. Spring Boot Project Initialization

I initialize your project using the initializer provided by Spring. Notice that I included Spring Web and Open AI in the dependencies

intialize spring project

2. Set Up API Credentials

In my application.properties files, I set up Spring AI to use my API credentials (my API key). I got a free Google's Gemini API key using Google AI studio. Here's how my application.properties file is set up:

spring.config.import=classpath:AI.properties

spring.application.name=regexplain

spring.ai.openai.api-key = ${GEMINI_API_KEY}
spring.ai.openai.base-url = https://generativelanguage.googleapis.com/v1beta/openai
spring.ai.openai.chat.completions-path = /chat/completions
spring.ai.openai.chat.options.model = gemini-2.5-pro

The first line imports the file that contains my API key. It is important that you don't expose your API key to the public. The file is located in the same folder as application.properties.

3. First Project Run

Using my package manager(maven), I installed the needed dependencies. Then I ran my main class to be sure that everything works as it should. If you everything right up to this point, yours should run without errors. If you encounter any error, look it up on Google to find a fix.

A2A Request and Response Model

Before I go into the implementation, let's talk a bit on the structure of an A2A-compliant request and response. The A2A protocol adheres to the standard JSON-RPC 2.0 structures for requests and responses.

All method calls are encapsulated in a request object, which looks like this:

{
  jsonrpc: "2.0"; 
  method: String; 
  id: String | Integer;
  params: Message;
}

The response is a bit similar:

{
  jsonrpc: "2.0"; 
  id: String | Integer | null;
  result?: Task | Message | null;
  error?: JSONRPCError;
}

The response ID MUST be the same as the request ID.

For more information on the A2A protocol, check out the A2A protocol docs.

That is the general structure of the request and response. I developed this agent for use in the Telex platform, so some of my implementation may be specific to Telex.

Now, to the implementation. I created a folder called model, where I'll store my models. The request model class called A2ARequest looks like this:

public class A2ARequest {
    private String id;
    private RequestParamsProperty params;

    public A2ARequest(String id, RequestParamsProperty params) {
        this.id = id;
        this.params = params;
    }

    // getters and setters
}

The RequestParamsProperties class represent the structure of the information contained in params. It looks like this:

public class RequestParamsProperty {
    private HistoryMessage message;
    private String messageId;

    public RequestParamsProperty(HistoryMessage message, String messageId) {
        this.message = message;
        this.messageId = messageId;
    }

    // getters and setter
}

HistoryMessage class looks like this:

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class HistoryMessage {
    private String kind;
    private String role;
    private List<MessagePart> parts;
    private String messageId;
    private String taskId;

    public HistoryMessage() {}

    public HistoryMessage(String role, List<MessagePart> parts, String messageId, String taskId) {
        this.kind = "message";
        this.role = role;
        this.parts = parts;
        this.messageId = messageId;
        this.taskId = taskId;
    }

    // getters and setters
}

The annotations are so that spring knows what to include in the JSON representation of the request and response. If a property doesn't exist in the request, it should ignore it and set it to null in the class. If a property is set to null, it shouldn't include it in the response.

MessagePart class looks like this:

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MessagePart {
    private String kind;
    private String text;
    private List<MessagePart> data;

    public MessagePart(String kind, String text, List<MessagePart> data) {
        this.kind = kind;
        this.text = text;
        this.data = data;
    }

    // getters and setters
}

That's all the classes needed to represent the request structure received from Telex. Now to create a model for my response, and all supporting classes needed to represent my response

@JsonInclude(JsonInclude.Include.NON_NULL)
public class A2AResponse {
    private final String jsonrpc;
    @JsonInclude(JsonInclude.Include.ALWAYS)
    private String id;
    private Result result;
    private CustomError error;

    public A2AResponse() {
        this.jsonrpc = "2.0";
    }

    public A2AResponse(String id, Result result, CustomError error) {
        this.jsonrpc = "2.0";
        this.id = id;
        this.result = result;
        this.error = error;
    }

    //getters and setters
}

Result class:

public class Result {
    private String id;
    private String contextId;
    private TaskStatus status;
    private List<Artifact> artifacts;
    private List<HistoryMessage> history;
    private String kind;

    public Result() {}

    public Result(String id, String contextId, TaskStatus status, List<Artifact> artifacts, List<HistoryMessage> history, String task) {
        this.id = id;
        this.contextId = contextId;
        this.status = status;
        this.artifacts = artifacts;
        this.history = history;
        this.kind = task;
    }

    // getters and setters
}

CustomError class:

public class CustomError {
    private int code;
    private String message;
    private Map<String, String> data;

    public CustomError(int code, String message, Map<String, String> data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // getters and setters
}

TaskStatus class:

@JsonInclude(JsonInclude.Include.NON_NULL)
public class TaskStatus {
    private String state;
    private Instant timestamp;
    private HistoryMessage message;

    public TaskStatus() {}

    public TaskStatus(String state, Instant timestamp, HistoryMessage message) {
        this.state = state;
        this.timestamp = timestamp;
        this.message = message;
    }

    // getters and setters
}

Artifact class:

public class Artifact {
    private String artifactId;
    private String name;
    private List<MessagePart> parts; // come back to review that type

    public Artifact() {}

    public Artifact(String artifactId, String name, List<MessagePart> parts) {
        this.artifactId = artifactId;
        this.name = name;
        this.parts = parts;
    }

    // getters and setters
}

The A2A protocol also includes something called the agent card. I created a model for it also.

public class AgentCard {
    private String name;
    private String description;
    private String url;
    private Map<String, String> provider;
    private String version;
    private Map<String, Boolean> capabilities;
    private List<String> defaultInputModes;
    private List<String> defaultOutputModes;
    private List<Map<String, Object>> skills;

    public AgentCard() {
        this.provider = new HashMap<>();
        this.capabilities = new HashMap<>();
        this.skills = new ArrayList<>();
    }

    // getters and setters
}

That's all for models. Moving on...

The Service Class

What my agent does is to get a regex string and then send it to OpenAI's API with a predefined prompt. The service class handles communicating with OpenAI, sending the prompt and receiving the response. I created another folder called service which is where my service class resides. This is how I wrote my service class:

@Service
public class RegExPlainService {
    private ChatClient chatClient;

    RegExPlainService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @Tool(name = "regexplain", description = "An agent that explains what type of string a regex pattern matches")
    public String generateResponse(String regex) {
        return chatClient
                .prompt("Give me a simple explanation of the type of string matched by this regex pattern: %s. No validating statements from you. Just straight to the point".formatted(regex))
                .call()
                .content();
    }
}

The service annotation allows Spring Boot to perform a service injection into your controller. The Tool annotation marks the method as an agent tool which can be autonomously called if the agent is to be extended to include that functionality. It's not needed right now, though.

The Controller

The controller exposes the agent using REST API. In this case, I have two endpoints, a GET endpoint and a POST endpoint. I created my controller in a folder called controller. This is the implementation:

@RestController
public class RegExPlainController {
    private final RegExPlainService regexplainService;

    @Autowired
    RegExPlainController (RegExPlainService regexplainService) {
        this.regexplainService = regexplainService;
    }

    @GetMapping("/a2a/agent/regexplain/.well-known/agent.json")
    public ResponseEntity<AgentCard> getAgentCard () {
        AgentCard agentCard = new AgentCard();
        agentCard.setName("regexplain");
        agentCard.setDescription("An agent that provides a simple explanation of the type of string a regex pattern matches");
        agentCard.setUrl("regexplain-production.up.railway.app/api");
        agentCard.setProvider("Bituan", null);
        agentCard.setVersion("1.0");
        agentCard.setCapabilities(false, false, false);
        agentCard.setDefaultInputModes(List.of("text/plain"));
        agentCard.setDefaultOutputModes(List.of("application/json", "text/plain"));
        agentCard.setSkill("skill-001", "Explain Regex", "Provides a simple explanation of the type of string a regex pattern matches",
                List.of("text/plain"), List.of("text/plain"), List.of());

        return ResponseEntity.ok(agentCard);
    }

    @PostMapping("/a2a/agent/regexplain")
    public ResponseEntity<A2AResponse> explainRegex (@RequestBody A2ARequest request) {
        String regexRequest;
        String responseText;

        // return 403 if parameter is invalid
        try {
            regexRequest = request.getParams().getMessage().getParts().get(0).getText();
        } catch (Exception e) {
            CustomError error = new CustomError(-32603, "Invalid Parameter", Map.of("details", e.getMessage()));
            A2AResponse errorResponse = new A2AResponse(null, null,  error);
            return ResponseEntity.status(HttpStatusCode.valueOf(403)).body(errorResponse);
        }

        // return error 500 if call to service fails
        try {
            responseText = regexplainService.generateResponse(regexRequest);
        } catch (Exception e) {
            CustomError error = new CustomError(-32603, "Internal Error", Map.of("details", e.getMessage()));
            A2AResponse errorResponse = new A2AResponse(null, null,  error);
            return ResponseEntity.internalServerError().body(errorResponse);
        }

        // response building
        A2AResponse response = new A2AResponse();
        response.setId(request.getId());

        // response building -> result building
        Result result = new Result();
        result.setId(UUID.randomUUID().toString());
        result.setContextId(UUID.randomUUID().toString());
        result.setKind("task");

        // response building -> result building -> status building
        TaskStatus status = new TaskStatus();
        status.setState("completed");
        status.setTimestamp(Instant.now());

        // response building -> result building -> status building -> message building
        HistoryMessage message = new HistoryMessage();
        message.setRole("agent");
        message.setParts(List.of(new MessagePart("text", responseText, null)));
        message.setKind("message");
        message.setMessageId(UUID.randomUUID().toString());

        // response building -> result building -> status building contd
        status.setMessage(message);

        // response building -> result building -> artifact building
        List<Artifact> artifacts = new ArrayList<>();
        Artifact artifact = new Artifact();
        artifact.setArtifactId(UUID.randomUUID().toString());
        artifact.setName("regexplainerResponse");
        artifact.setParts(List.of(new MessagePart("text", responseText, null)));
        artifacts.add(artifact);


        //response building -> result building -> history building
        List<HistoryMessage> history = new ArrayList<>();

        //response building -> result building contd
        result.setStatus(status);
        result.setArtifacts(artifacts);
        result.setHistory(history);

        // response building contd
        response.setResult(result);

        return ResponseEntity.ok(response);
    }
}

The GET endpoint uses a route part of the A2A protocol standard for getting the agent card. The agent card is a description of the agent and what it can do.
The POST endpoint takes an A2A-compliant request and executes the agent, before returning an appropriate response.

Conclusion

That's it. That's how I wrote Regexplain.
With this, you can build your AI agent from scratch and make it A2A-compliant. Or, at least, I hope this has given you some insight on how to go about developing your A2A-compliant AI agent in Java.

Thanks for reading. Bye!


This content originally appeared on DEV Community and was authored by Tobi Awanebi


Print Share Comment Cite Upload Translate Updates
APA

Tobi Awanebi | Sciencx (2025-11-04T12:23:23+00:00) Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI. Retrieved from https://www.scien.cx/2025/11/04/developing-an-a2a-compliant-ai-agent-with-java-spring-boot-and-spring-ai/

MLA
" » Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI." Tobi Awanebi | Sciencx - Tuesday November 4, 2025, https://www.scien.cx/2025/11/04/developing-an-a2a-compliant-ai-agent-with-java-spring-boot-and-spring-ai/
HARVARD
Tobi Awanebi | Sciencx Tuesday November 4, 2025 » Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI., viewed ,<https://www.scien.cx/2025/11/04/developing-an-a2a-compliant-ai-agent-with-java-spring-boot-and-spring-ai/>
VANCOUVER
Tobi Awanebi | Sciencx - » Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/04/developing-an-a2a-compliant-ai-agent-with-java-spring-boot-and-spring-ai/
CHICAGO
" » Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI." Tobi Awanebi | Sciencx - Accessed . https://www.scien.cx/2025/11/04/developing-an-a2a-compliant-ai-agent-with-java-spring-boot-and-spring-ai/
IEEE
" » Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI." Tobi Awanebi | Sciencx [Online]. Available: https://www.scien.cx/2025/11/04/developing-an-a2a-compliant-ai-agent-with-java-spring-boot-and-spring-ai/. [Accessed: ]
rf:citation
» Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI | Tobi Awanebi | Sciencx | https://www.scien.cx/2025/11/04/developing-an-a2a-compliant-ai-agent-with-java-spring-boot-and-spring-ai/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.