Build Your First AI Agent in Java: Quarkus, Langchain4j, and the A2A SDK
Markus

Markus @myfear

About: Markus is a Java Champion, former Java EE Expert Group member, founder of JavaLand, reputed speaker at Java conferences around the world, and a very well known figure in the Enterprise Java world.

Location:
Munich, Germany
Joined:
Mar 26, 2024

Build Your First AI Agent in Java: Quarkus, Langchain4j, and the A2A SDK

Publish Date: Jul 30
0 0

Hero image

AI Agents aren't just hype. They're the foundation for the next generation of autonomous and semi-autonomous applications. In this tutorial, you'll build your first agent using modern Java tooling: Quarkus for blazing-fast cloud-native development, Langchain4j for easy LLM interaction, and the A2A Agent SDK, a promising new protocol for peer-to-peer software agent communication.

We’ll build a real, functioning agent that accepts a block of text and returns a concise summary using an LLM. And by the end, you'll understand what it means to expose AI capabilities in a way that's interoperable, reactive, and easy to test.

Before I forget it: Thank you for kicking me off into the right direction with this tutorial when I was stuck!

What You'll Build

You’ll create a "Summarization Agent" that does four things:

  1. Exposes itself as a capable A2A-compliant agent

  2. Accepts a text payload via the /a2a endpoint

  3. Uses Langchain4j to summarize the input using an LLM

  4. Returns the summary in a structured A2A response

Prerequisites

Before diving in, make sure you have:

  • JDK 17+

  • Apache Maven 3.8+

  • A Java IDE (e.g., IntelliJ, VS Code)

  • A local model via Ollama (or just the Quarkus Dev Service)

Bootstrap the Quarkus Project

Open your terminal and scaffold a new project:

mvn io.quarkus.platform:quarkus-maven-plugin:create \
    -DprojectGroupId=com.example \
    -DprojectArtifactId=summarization-agent \
    -DclassName="com.example.SummarizationResource" \
    -Dpath="/summarize"
    -Dextensions="quarkus-rest-jackson, quarkus-langchain4j-ollama,quarkus-smallrye-openapi"
cd summarization-agent
Enter fullscreen mode Exit fullscreen mode

You need to manually add the following dependency to your pom.xml after you created the project.

<dependency>
    <groupId>io.github.a2asdk</groupId>
    <artifactId>a2a-java-sdk-server-quarkus</artifactId>
    <version>0.2.3.Beta1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Configure Your LLM

Configure your local Olama based LLM in src/main/resources/application.properties:

quarkus.langchain4j.ollama.chat-model.model-id=llama3.2:latest
quarkus.langchain4j.ollama.timeout=60s
quarkus.swagger-ui.always-include=true
quarkus.log.category."io.a2a".level=DEBUG
quarkus.rest-client.logging.scope=ALL

# Binding Quarkus explicitly
quarkus.http.host=0.0.0.0
Enter fullscreen mode Exit fullscreen mode

Create the Summarization Agent

Langchain4j with Quarkus makes AI integration look like a normal Java interface. Create SummarizationAgent.java:

package com.example;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import jakarta.enterprise.context.ApplicationScoped;

@RegisterAiService
@ApplicationScoped
public interface SummarizationAgent {

    @SystemMessage("You are a professional text summarizer. Your task is to provide a concise summary of the given text.")
    @UserMessage("Summarize the following text: {text}")
    String summarize(String text);
}
Enter fullscreen mode Exit fullscreen mode

This is a simple interface that defines a summarize method. The @RegisterAiService annotation tells Quarkus to create a proxy for this service that will interact with the configured LLM. The @SystemMessage and @UserMessage annotations provide instructions to the LLM. The @ApplicationScoped annotation makes it a CDI bean.

Define the Agent

Now it's time to create our agent. In the A2A world, an agent has two main components:

  • Agent Card: This is like a business card for the agent. It contains information about the agent, such as its name, description, and capabilities.

  • Agent Executor: This is the brain of the agent. It contains the logic for how the agent should handle requests.

Let's create a new class called SummarizationAgentCardProducer in the src/main/java/com/example directory:

package com.example;

import java.util.Collections;

import io.a2a.server.PublicAgentCard;
import io.a2a.spec.AgentCapabilities;
import io.a2a.spec.AgentCard;
import io.a2a.spec.AgentSkill;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;

@ApplicationScoped
public class SummarizationAgentCardProducer {

        @Produces
        @PublicAgentCard
        public AgentCard agentCard() {
                Log.info("agentCard() called");
                return new AgentCard.Builder()
                                .name("Summarization Agent")
                                .description("An agent that summarizes text.")
                                .defaultInputModes(Collections.singletonList("text"))
                                .defaultOutputModes(Collections.singletonList("text"))
                                .url("http://host.containers.internal:8080/")
                                .version("1.0.0")
                                .capabilities(new AgentCapabilities.Builder()
                                                .streaming(false)
                                                .pushNotifications(false)
                                                .stateTransitionHistory(false)
                                                .build())
                                .skills(Collections.singletonList(new AgentSkill.Builder()
                                                .id("summarize_text")
                                                .name("Summarize Text")
                                                .description("Summarizes the provided text.")
                                                .tags(Collections.singletonList("text_summarization"))
                                                .build()))
                                .build();
        }
}
Enter fullscreen mode Exit fullscreen mode

This class registers your agent with the A2A runtime and defines how it handles requests. One thing is important here. I am binding the agent to

http://host.containers.internal:8080/. This is necessary for testing with the Google A2A-inspector later on. Usually, you would set this to the host url of your agent.

And now we need the Agent executor. Create SummarizationAgentExecutorProducer in the src/main/java/com/example directory:

package com.example;

import java.util.List;

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.Task;
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 jakarta.inject.Inject;

@ApplicationScoped
public class SummarizationAgentExecutorProducer {

    @Inject
    SummarizationAgent summarizationAgent;

    @Produces
    public AgentExecutor agentExecutor() {
        Log.info("agentExecutor() called");
        return new SummarizationAgentExecutor(summarizationAgent);
    }

    private static class SummarizationAgentExecutor implements AgentExecutor {

        private final SummarizationAgent summarizationAgent;

        public SummarizationAgentExecutor(SummarizationAgent summarizationAgent) {
            Log.info("SummarizationAgentExecutor() called");
            this.summarizationAgent = summarizationAgent;
        }

        @Override
        public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
            Log.infof("execute() called %s", context.getTaskId());
            TaskUpdater updater = new TaskUpdater(context, eventQueue);

            // mark the task as submitted and start working on it
            if (context.getTask() == null) {
                updater.submit();
            }
            updater.startWork();

            // extract the text from the message
            String userMessage = extractTextFromMessage(context.getMessage());

            // call the summarization agent
            String response = summarizationAgent.summarize(userMessage);

            // create the response part
            TextPart responsePart = new TextPart(response, null);
            List<Part<?>> parts = List.of(responsePart);

            // add the response as an artifact and complete the task
            updater.addArtifact(parts, null, null, null);
            updater.complete();
        }

        @Override
        public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
            Log.infof("cancel() called %s", context.getTaskId());
            Task task = context.getTask();

            if (task.getStatus().state() == TaskState.CANCELED) {
                // task already cancelled
                throw new TaskNotCancelableError();
            }

            if (task.getStatus().state() == TaskState.COMPLETED) {
                // task already completed
                throw new TaskNotCancelableError();
            }

            // cancel the task
            TaskUpdater updater = new TaskUpdater(context, eventQueue);
            updater.cancel();
        }

        private String extractTextFromMessage(Message message) {
            Log.infof("extractTextFromMessage() called %s", message.getTaskId());
            StringBuilder textBuilder = new StringBuilder();
            if (message.getParts() != null) {
                for (Part<?> part : message.getParts()) {
                    if (part instanceof TextPart textPart) {
                        textBuilder.append(textPart.getText());
                    }
                }
            }
            return textBuilder.toString();
        }

    }

}
Enter fullscreen mode Exit fullscreen mode

A2A Endpoint Activation

The a2a-java-sdk-server-quarkus dependency automatically exposes your agent at:

POST /a2a
Enter fullscreen mode Exit fullscreen mode

No need to write a controller. This is handled via JAX-RS behind the scenes.

Test Your Agent

This is getting a little more tricky, but you can manage. We are going to use Google’s A2A-inspector tool. Clone it locally and build a container that we can run. Because, we don’t really want to install Python or Node on our systems, don’t we?

git clone https://github.com/a2aproject/a2a-inspector.git
cd a2a-inspector
Enter fullscreen mode Exit fullscreen mode

Now let Podman do the container build:

podman build -t a2a-inspector .  
Enter fullscreen mode Exit fullscreen mode

And when that is done, all you have to do is to launch it locally.

podman run -d -p 8081:8080 a2a-inspector
Enter fullscreen mode Exit fullscreen mode

Note that I have changed the external port hier because we are going to run our Agent on port 8080 already. Check if the inspector is running by going to: http://localhost:8081/. You should see something like this:

A2A Agent Inspector

Now it’s time to start our Agent:

./mvnw quarkus:dev
Enter fullscreen mode Exit fullscreen mode

Go to the A2A inspector and enter: host.containers.internal:8080 as the URL. This is the “standard” url that Podman exposes the host (at least on machines where it uses a VM internally).

Now you can input a message. For example:

Shorten below text: Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of \"de Finibus Bonorum et Malorum\" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, \"Lorem ipsum dolor sit amet..\", comes from a line in section 1.10.32. The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from \"de Finibus Bonorum et Malorum\" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
Enter fullscreen mode Exit fullscreen mode

You can send it and inspect the output in the debug console:

{
  "artifacts": [
    {
      "artifactId": "2a0cd14f-74b6-4ef3-afee-3a27cf2abfa9",
      "parts": [
        {
          "kind": "text",
          "text": "Here's a shortened version:\n\nLorem Ipsum is not random text, but rather an ancient Latin phrase dating back to 45 BC. A professor discovered its connection to Cicero's book \"The Extremes of Good and Evil\". The first line of Lorem Ipsum comes from section 1.10.32 of this work."
        }
      ]
    }
  ]
Enter fullscreen mode Exit fullscreen mode

Just like that, your AI agent is live and answering your calls.

What's Next?

Now that you’ve built and exposed your first Java agent, here’s where you can go:

  • Add more capabilities to your agent by defining new @UserMessage methods

  • Allow it to call other tools or agents

  • Chain multiple agents into workflows using A2A routing

Agents are a powerful abstraction that unlock real-time, goal-driven, distributed AI programming. And you’ve just built one in Java.

Comments 0 total

    Add comment