How to Send Emails with an AI Agent: A Quarkus and Langchain4j Tutorial

This tutorial provides a step-by-step guide on how to build a Java application that can send emails based on natural language commands. We will create an AI agent using Quarkus and Langchain4j, equipping it with a “tool” to send emails. This agent will run entirely on your local machine, leveraging Ollama to serve a local Large Language Model (LLM).

Instead of just processing information, our agent will be an actor, capable of interacting with external systems. The goal is to move beyond simple information retrieval and construct a system that can understand a request, reason about the necessary steps, and execute a real-world action: sending an email. By the end of this guide, you will have a working example of an agentic AI system built on a modern, efficient Java stack.

Preparing Your Local AI Environment

Before writing any Java code, you need to set up a local development environment. A local setup gives you full control, ensures data privacy, and creates a rapid development loop.

The Foundation: Ollama and Local Models

Ollama is the cornerstone of our local AI setup. It is a lightweight server that runs large language models on your machine, exposing them through a simple API that your applications can use.

First, install Ollama by following the instructions on its official website. Once installed, use the command line to pull the two models required for this project. This ensures both the reasoning and data-embedding capabilities are available locally.

Pull the chat model, which will act as the agent’s “brain” for understanding prompts and using tools:

ollama pull gpt-oss:20b

Next, pull the embedding model. This model will be used by the Retrieval-Augmented Generation (RAG) system to convert text into numerical representations for searching.

ollama pull nomic-embed-text:v1.5

The Scaffolding: A New Quarkus Project

With the AI models ready, create the application structure using the Quarkus command line interface. This tool scaffolds the project and configures the necessary extensions from the start.

Execute the following command in your terminal:

quarkus create app com.eldermoraes:email-agent \
  --extension='rest,quarkus-langchain4j-ollama,quarkus-langchain4j-easy-rag,quarkus-mailer'

This command creates a new project with four critical Quarkus extensions. Each plays a specific role in the agent’s architecture.

Table 1: Project Dependencies (Quarkus Extensions)

Extension Purpose
quarkus-rest Provides the JAX-RS implementation to create a REST endpoint for interacting with the AI agent.
quarkus-langchain4j-ollama Connects the Langchain4j framework to the local Ollama inference server.
quarkus-langchain4j-easy-rag Offers a high-level abstraction that simplifies implementing a Retrieval-Augmented Generation pipeline.
quarkus-mailer Delivers the core functionality for sending emails, which will become the agent’s primary “tool”.

These extensions are designed to work together, making complex integrations like AI feel native to the Quarkus ecosystem. This allows you to focus on application logic rather than on wiring components together.

The Agent’s Mind: Configuring the AI Service

With the project structure in place, it is time to connect the Java application to the language model. This enables the agent to process language and reason. The integration between Quarkus and Langchain4j provides a declarative, low-boilerplate approach.

Declarative AI in Practice

A core abstraction in Langchain4j is the AiService. This feature allows you to interact with LLMs through a standard Java interface, abstracting away the complexities of HTTP clients and JSON parsing.

Create a new Java interface named EmailAgent. By annotating it with @RegisterAiService, you instruct Quarkus to automatically generate and configure an implementation that communicates with the LLM.

package com.eldermoraes;

import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService
public interface EmailAgent {

    String chat(@UserMessage String message);

}

Connecting to Ollama

The @RegisterAiService annotation tells Quarkus to create a bean, but it needs to know which LLM to use. This connection is defined in the src/main/resources/application.properties file. These properties configure the bridge to the local gpt-oss:20b model running in Ollama.

# Configure the chat model to use the gpt-oss:20b model ID from Ollama
quarkus.langchain4j.ollama.chat-model.model-id=gpt-oss:20b

# Set the base URL for the local Ollama server
quarkus.langchain4j.ollama.base-url=http://localhost:11434

# Increase the timeout, as local models can take longer to respond than cloud APIs
quarkus.langchain4j.timeout=60s

Verifying the Connection

To confirm the setup is working, create a simple test endpoint. This provides an immediate feedback loop and verifies communication with the LLM before adding more complex features.

Create a new JAX-RS resource class named AgentResource and inject the EmailAgent interface.

package com.eldermoraes;

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/agent")
public class AgentResource {

    @Inject
    EmailAgent agent;

    @POST
    @Consumes(MediaType.TEXT_PLAIN)
    @Produces(MediaType.TEXT_PLAIN)
    public String agent(String prompt) {
        return agent.chat(prompt);
    }
}

This integration leverages Quarkus’s Contexts and Dependency Injection (CDI) model. The @RegisterAiService annotation makes the AI interaction layer behave like any other injectable CDI bean. For developers familiar with CDI, adopting AI services requires no new mental model, making the developer experience smooth and intuitive.

The Agent’s Hands: Forging an Email Tool

An agent that can reason is impressive, but an agent that can act is transformative. In this section, we will give the agent a tool that allows it to perform a real-world action: sending an email.

Introducing Function Calling

This is enabled by a mechanism known as function calling, or tool use. The LLM does not execute Java code directly. Instead, when given a user prompt and a list of available tools, it can generate a structured JSON object requesting to invoke a specific function with parameters it infers from the prompt. The application receives this request, executes the corresponding Java method, and sends the result back to the LLM. The LLM then uses this result to formulate its final response.

Building the EmailService

To encapsulate the email-sending logic, create a new CDI bean named EmailService. This class will contain the methods exposed to the LLM as tools.

Implement a public method, sendEmail, that accepts a recipient, subject, and body. The @Tool annotation from Langchain4j makes this method available to the LLM. The annotation’s description is critical; it’s a natural language explanation that the LLM uses to understand the tool’s purpose and decide when to use it.

package com.eldermoraes;

import dev.langchain4j.agent.tool.Tool;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class EmailService {

    @Inject
    Mailer mailer;

    @Tool("Sends an email to a specified recipient with a given subject and body")
    public String sendEmail(String recipient, String subject, String body) {
        mailer.send(Mail.withText(recipient, subject, body));
        return "OK";
    }
}

Implementing the Action with Quarkus Mailer

Inside the sendEmail method, the Quarkus Mailer is used to send the email. For development, it’s best to configure the mailer in mock mode. This prevents sending actual emails and instead logs them to the console, providing a safe way to verify the tool’s execution.

Add the following line to application.properties:

# Enable mock mode to prevent sending real emails during development
quarkus.mailer.mock=true

Enabling the Tool

Finally, make the agent aware of the tool by updating the EmailAgent interface. The EmailService class, which contains our @Tool annotated method, is registered directly within the @RegisterAiService annotation. This tells Langchain4j that all methods annotated with @Tool inside EmailService are available for the LLM to use across all methods in this interface.

package com.eldermoraes;

import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService(tools = EmailService.class)
public interface EmailAgent {

    String chat(@UserMessage String message);

}

Testing the Tool

Now, the agent is equipped to act. Start the application in development mode (./mvnw quarkus:dev) and trigger the tool using a natural language command via curl.

curl -X POST -H "Content-Type: text/plain" \
  -d "Send an email to test@example.com with the subject 'Hello from your AI Agent' and the body 'This is a test email.'" \
  http://localhost:8080/agent

Observe the application console logs. You should see output from the mock mailer confirming the email was “sent.” This proves the LLM correctly interpreted the prompt, identified the sendEmail tool, extracted the parameters, and initiated the function call.

This is a different way of building applications. Instead of writing step-by-step orchestration logic, you define capabilities using the @Tool annotation and give the LLM a high-level goal. The LLM then acts as the orchestrator, figuring out which tools to call and with what parameters. This declarative, goal-oriented approach is more flexible and powerful than traditional imperative code.

The Agent’s Memory: Providing Context with Easy RAG

An agent that can act is powerful. An agent that can find and use information to inform its actions is intelligent. To elevate our system, we will provide it with a knowledge base it can consult using Retrieval-Augmented Generation (RAG).

The Need for External Knowledge

Consider a new requirement: “A customer’s new Quantum Widget is broken. Send them an email explaining how to handle a defective product return.” The agent has the tool to send an email, but it lacks a critical piece of information: the correct procedure and contact email for defective returns. This is a classic use case for RAG. The agent must first retrieve relevant information from an external source to complete its task.

Configuring Easy RAG

The quarkus-langchain4j-easy-rag extension simplifies setting up a RAG pipeline. By adding a few properties, a complete ingestion and retrieval mechanism can be enabled.

Add the following configuration to application.properties:

# Tell Easy RAG where to find the knowledge documents
quarkus.langchain4j.easy-rag.path=src/main/resources/rag

# Specify that the embedding model is provided by Ollama
quarkus.langchain4j.embedding-model.provider=ollama

# Configure the specific embedding model ID from Ollama
quarkus.langchain4j.ollama.embedding-model.model-id=nomic-embed-text:v1.5

Creating the Knowledge Source

With the configuration in place, create the knowledge source. In the src/main/resources directory, create a new subdirectory named rag. Inside this rag directory, create a Markdown file named product-policy.md with the following content.

product-policy.md (Knowledge Source)

# Product Return Policy for "Quantum Widget"

## Standard Returns
- Customers can return the Quantum Widget within 30 days of purchase for a full refund.
- The product must be in its original packaging and in unused condition.
- The original receipt is required for all returns.

## Defective Products
- If the Quantum Widget is defective, customers can return it within 90 days for a replacement or a full refund.
- Proof of defect (e.g., photo, description of the issue) may be required.

## Contact for Returns
- To initiate a return, please contact our support team at support@quantumcorp.com.
- For defective product inquiries, please email defects@quantumcorp.com.

Upon startup, Quarkus Easy RAG will automatically find this directory, read the file, split it into chunks, use the nomic-embed-text:v1.5 model to convert those chunks into vector embeddings, and store them in an in-memory vector database.

A Smarter Interaction: The RAG + Tool Synergy

Now, test the complete workflow that combines information retrieval with action execution. Use a prompt that requires the agent to use its new knowledge.

curl -X POST -H "Content-Type: text/plain" \
  -d "A customer's Quantum Widget is broken. Send an email to them at customer@example.com explaining how to handle a defective product return." \
  http://localhost:8080/agent

The application now follows a more sophisticated execution path:

  1. The user’s prompt is sent to the EmailAgent.
  2. A RetrievalAugmentor intercepts the prompt before it goes to the LLM.
  3. The RetrievalAugmentor analyzes the prompt, identifies key terms like “Quantum Widget” and “broken,” and uses the nomic-embed-text model to create a vector embedding of this query.
  4. It performs a similarity search against the vectorized content of product-policy.md and finds a strong match with the chunk containing information about defective products, including the email defects@quantumcorp.com.
  5. The augmentor enriches the original prompt with this retrieved context.
  6. This augmented prompt is sent to the gpt-oss:20b model.
  7. The LLM now has all the information it needs: the customer’s request, the correct procedure, and the contact email. It reasons that it should use the sendEmail tool.
  8. The LLM generates the JSON request to call sendEmail(recipient="customer@example.com", subject="Return Policy for Defective Quantum Widget", body="..."), creating a suitable subject and body based on the retrieved policy.
  9. The application executes the tool call, and the mock email is logged to the console.

Combining RAG and Tools creates a complete perception-action loop. RAG provides the “perception” by allowing the agent to query its knowledge base. Tools provide the “action” by allowing it to interact with external systems. This combination is the foundation for building intelligent, autonomous agents. You can extend this pattern by adding more knowledge sources and more tools.

The Complete Blueprint: Code and Configuration

This section consolidates the complete source code and configuration for the project.

Final Code Review

The following are the final versions of the three Java classes created.

AgentResource.java (The JAX-RS Endpoint)

package com.eldermoraes;

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/agent")
public class AgentResource {

    @Inject
    EmailAgent agent;

    @POST
    @Consumes(MediaType.TEXT_PLAIN)
    @Produces(MediaType.TEXT_PLAIN)
    public String agent(String prompt) {
        return agent.chat(prompt);
    }
}

EmailAgent.java (The AiService Interface)

package com.eldermoraes;

import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService(tools = EmailService.class)
public interface EmailAgent {

    String chat(@UserMessage String message);

}

EmailService.java (The Tool Provider)

package com.eldermoraes;

import dev.langchain4j.agent.tool.Tool;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class EmailService {

    @Inject
    Mailer mailer;

    @Tool("Sends an email to a specified recipient with a given subject and body")
    public String sendEmail(String recipient, String subject, String body) {
        mailer.send(Mail.withText(recipient, subject, body));
        return "Email sent successfully.";
    }
}

Unified Configuration

The application.properties file contains all the necessary configuration.

Table 2: Key Configuration Properties (application.properties)

Property Group Purpose
quarkus.langchain4j.ollama.* Configures the connection to the local Ollama server, specifying the model IDs for both the chat and embedding models, the server URL, and a request timeout.
quarkus.langchain4j.embedding-model.* Explicitly sets the provider for embedding models to Ollama, ensuring Easy RAG uses the correct service.
quarkus.langchain4j.easy-rag.* Enables and configures the Easy RAG pipeline, pointing it to the local directory containing knowledge documents.
quarkus.mailer.* Configures the Quarkus Mailer, specifically enabling mock mode for safe local development and testing.

application.properties (Complete File)

# ---------------------------------
# Ollama Language Model Configuration
# ---------------------------------
quarkus.langchain4j.ollama.base-url=http://localhost:11434
quarkus.langchain4j.ollama.chat-model.model-id=gpt-oss:20b
quarkus.langchain4j.ollama.embedding-model.model-id=nomic-embed-text:v1.5
quarkus.langchain4j.timeout=60s

# ---------------------------------
# Langchain4j Embedding Model Configuration
# ---------------------------------
quarkus.langchain4j.embedding-model.provider=ollama

# ---------------------------------
# Easy RAG Configuration
# ---------------------------------
quarkus.langchain4j.easy-rag.path=src/main/resources/rag

# ---------------------------------
# Quarkus Mailer Configuration
# ---------------------------------
quarkus.mailer.mock=true

Running the Application

With all code and configuration in place, run the application:

./mvnw quarkus:dev

Once started, execute the final curl command to trigger the full RAG-and-Tool workflow.

curl -X POST -H "Content-Type: text/plain" \
  -d "A customer's Quantum Widget is broken. Send an email to them at customer@example.com explaining how to handle a defective product return." \
  http://localhost:8080/agent

Check the console output. You will see logs indicating that the mock mailer has sent an email to customer@example.com with the correct return information, confirming that the agent successfully completed its perception-action loop.

The Age of the Java Agents

We have constructed a sophisticated AI agent capable of reasoning, retrieving information from a knowledge base, and executing actions in an external system. This was all accomplished using the Java ecosystem and a completely local, private, and open-source AI stack.

This example is just a starting point. The architectural pattern is extensible. Imagine giving the agent a tool to query a customer database or call external REST APIs for real-time data. The agent’s capabilities are limited only by the tools you provide.

The combination of Quarkus’s developer experience and Langchain4j’s powerful abstractions positions Java as a premier platform for building the next generation of agentic AI systems. The future of software is about empowering applications to understand our goals and act on our behalf.

 

 

If you run Java at scale, grab the free whitepaper “The Enterprise Guide to AI in Java (POC to Production)”. Download it here.

2 thoughts on “How to Send Emails with an AI Agent: A Quarkus and Langchain4j Tutorial”

  1. Excellent, Elder. Would the same scheme work for RAG with a vector database instead of a file.md?

    Reply
    • Thanks, Anderson! Definitely yes, but the approach it’s a little bit different. I’ll publish another article to talk about it!

      Reply

Leave a Comment