Unleash the Power of AI Agents with Java: Building Intelligent Applications with Quarkus, LangChain4J, and Oracle AI Database
Introduction
In this blog, we’ll build a Quarkus-based AI support agent that handles customer support for Oracle database error codes. Using LangChain4J, it processes natural language prompts, searches a knowledge base stored in Oracle AI Database 26ai via Retrieval-Augmented Generation (RAG), and automatically sends emails with error descriptions, root causes, and proposed solutions.
The agent intelligently understands prompts such as “A customer reported ORA-00900” and responds by calling different tools to achieve its goals.
By reporting different error messages in natural language, you’ll see that our support agent may change its behavior more probabilistically - an agentic approach to business solutions instead of deterministic ones based on strict business logic.
I assume you understand what an AI Agent is about. Otherwise, check this blog post — What Are AI Agents?
So without further ado, let’s get started!
Prerequisites
- JDK — Java Development Kit 17+
- Oracle AI Database 26ai Free Container Image
- Oracle JDBC Driver
- Your preferred Java IDE — Eclipse, IntelliJ, VS Code
- Apache Maven or Gradle
Step 1: Configure your Java application
Configure your Maven pom.xml file with the following dependencies: Oracle JDBC driver, Oracle LangChain4J integration, Quarkus AI Services, LangChain4J Agentic Framework, Quarkus LangChain4J integration, UCP, and other dependencies.
Using Maven
<properties>
<compiler-plugin.version>3.14.1</compiler-plugin.version>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.29.4</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
<jdbc.version>23.26.0.0.0</jdbc.version>
</properties>
<dependencies>
<!-- Oracle AI Database 26ai JDBC / UCP -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc17</artifactId>
<version>${jdbc.version}</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ucp17</artifactId>
<version>${jdbc.version}</version>
</dependency>
<!-- Oracle - LangChain4j Integration -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-oracle</artifactId>
<version>1.9.1-beta17</version>
</dependency>
<!-- Quarkus -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-oracle</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkiverse.mailpit</groupId>
<artifactId>quarkus-mailpit</artifactId>
<version>1.7.0</version>
</dependency>
<!-- Quarkus LangChain4j OpenAI -->
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-openai</artifactId>
</dependency>
<!-- Quarkus Qute -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-qute</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.qute.web</groupId>
<artifactId>quarkus-qute-web</artifactId>
</dependency>
<!-- LangChain4j -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.3.0</version>
</dependency>
<!-- LangChain4j Agentic -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId>
<version>1.3.0-beta9</version>
</dependency>
</dependencies>Next, configure the application.properties file, as shown below. It includes properties for OpenAI and its API key, as well as the chat and embedding models.
Besides, note the JDBC data source properties to support using Oracle AI Database 26ai as a vector store.
Also, the Quarkus mock mailer component is enabled for development, so emails will be logged only, instead of being effectively sent.
quarkus.langchain4j.timeout=60s
quarkus.mailer.mock=true
# OpenAI
quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY}
quarkus.langchain4j.openai.chat-model.model-name=gpt-4o
quarkus.langchain4j.openai.embedding-model.model-name=text-embedding-3-small
# Oracle Database JDBC
quarkus.datasource.db-kind=oracle
quarkus.datasource.jdbc.url=jdbc:oracle:thin:@localhost:1521/FREEPDB1
quarkus.datasource.username=${DB_USERNAME}
quarkus.datasource.password=${DB_PASSWORD}
quarkus.datasource.jdbc.driver=oracle.jdbc.OracleDriver
quarkus.datasource.jdbc.initial-size=10
quarkus.datasource.jdbc.min-size=10
quarkus.datasource.jdbc.max-size=50
# Optional vector settings
quarkus.datasource.jdbc.additional-jdbc-properties.vectorDefaultGetObjectType=StringStep 2: Understand the agent architecture components
The agent is implemented with the following core components:
- A REST endpoint (TechSupportAgentResource.java) to receive user prompts.
- A RAG initialization service (RagInitializationService.java) that loads Oracle error codes from a file into the Oracle Database, our vector store.
- An AI Agent interface (TechSupportAgent.java) powered by LangChain4J / Quarkus AI Services.
- Three tools: EmailTool.java, OracleDatabaseTool.java (RAG with Oracle DB), and LoggingTool.java.
- A simple web GUI in HTML served from index.html.
Let’s have a look at each of them.
No tricks here, this is just a trivial REST endpoint, exposing an integration point that accepts HTTP POST requests.
package com.oracle.dev.quarkus.langchain4j.agent;
import java.util.List;
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 TechSupportAgentResource {
@Inject
TechSupportAgent agent;
@Inject
OracleErrorCodeLookup exactCodeLookup;
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public String agent(String prompt) {
return agent.chat(withAuthoritativeErrorDescriptions(prompt));
}
private String withAuthoritativeErrorDescriptions(String prompt) {
List<String> exactMatches = exactCodeLookup.findExactMatches(prompt);
if (exactMatches.isEmpty()) {
return prompt;
}
return prompt + "\n\nAuthoritative Oracle Database knowledge-base entries retrieved for this request "
+ "(use these descriptions; do not state that they are unavailable):\n" + String.join("\n", exactMatches);
}
}On startup, RagInitializationService.java automatically loads the Oracle error code descriptions from our RAG raw data source, a simple markdown file (src/main/resources/rag/oracle-error-codes-descriptions.md). This happens once when the application starts. You will see a log message: Loaded 35 Oracle AI Database 26ai error code entries into the vector store.
It splits the file into individual documents, generates embeddings using OpenAI’s text-embedding-3-small model, and stores them in an Oracle AI Database 26ai table (rag_agent_embeddings), as shown below.
@PostConstruct
public void initialize() throws SQLException {
embeddingStore = OracleEmbeddingStore.builder().dataSource(dataSource)
.embeddingTable("rag_agent_embeddings", CreateOption.CREATE_IF_NOT_EXISTS).build();
ingestOracleErrorCodes();
}Below is the full class.
package com.oracle.dev.quarkus.langchain4j.agent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.oracle.CreateOption;
import dev.langchain4j.store.embedding.oracle.OracleEmbeddingStore;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.runtime.Startup;
@ApplicationScoped
@Startup
public class RagInitializationService {
@Inject
EmbeddingModel embeddingModel;
@Inject
DataSource dataSource;
private EmbeddingStore<TextSegment> embeddingStore;
@PostConstruct
public void initialize() throws SQLException {
embeddingStore = OracleEmbeddingStore.builder().dataSource(dataSource)
.embeddingTable("rag_agent_embeddings", CreateOption.CREATE_IF_NOT_EXISTS).build();
ingestOracleErrorCodes();
}
public EmbeddingStore<TextSegment> getEmbeddingStore() {
return embeddingStore;
}
public EmbeddingModel getEmbeddingModel() {
return embeddingModel;
}
private void ingestOracleErrorCodes() {
if (vectorTableHasRows()) {
System.out.println("--- Vector store already populated; skipping Oracle error code ingestion. ---");
return;
}
List<Document> documents = new ArrayList<>();
try (var inputStream = RagInitializationService.class.getClassLoader()
.getResourceAsStream("rag/oracle-error-codes-descriptions.md")) {
if (inputStream == null) {
throw new IllegalStateException(
"Resource rag/oracle-error-codes-descriptions.md not found in src/main/resources/rag/");
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
documents = reader.lines().filter(line -> !line.trim().isEmpty()).map(Document::from)
.collect(Collectors.toList());
}
} catch (IOException e) {
throw new RuntimeException("Failed to load oracle-error-codes-descriptions.md", e);
}
if (embeddingModel != null) {
for (Document doc : documents) {
Embedding embedding = embeddingModel.embed(doc.text()).content();
embeddingStore.add(embedding, TextSegment.from(doc.text()));
}
}
System.out.println(
"--- Loaded " + documents.size() + " Oracle AI Database 26ai error code entries into the vector store. ---");
}
private boolean vectorTableHasRows() {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement();
var resultSet = statement.executeQuery("SELECT COUNT(*) FROM rag_agent_embeddings")) {
return resultSet.next() && resultSet.getLong(1) > 0;
} catch (SQLException e) {
return false;
}
}
}
TechSupportAgent.java is a Java interface that defines our agent’s contract and processes user prompts, and then invokes the available tools based on the needs inferred from the message, as it’s annotated with @RegisterAiService. The tools run autonomously per the agent’s ReAct loop.
In a nutshell, a ReAct agent applies the “reasoning and acting” framework, blending chain-of-thought reasoning with tool use. This approach empowers large language models to tackle complex tasks and make decisions in agentic workflows.
It also tells Quarkus to generate an AI-backed implementation where the chat(String message) method, marked with the @UserMessage annotation, serves as the agent’s entry point.
package com.oracle.dev.quarkus.langchain4j.agent;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.SystemMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
@RegisterAiService(tools = { EmailTool.class, OracleDatabaseTool.class, LoggingTool.class })
public interface TechSupportAgent {
@SystemMessage("""
You are an Oracle Database customer-support agent. When the user message includes
authoritative Oracle Database knowledge-base entries, treat them as the source of truth:
include the matching error description in your answer and in any email or log you create.
Do not claim that an error code is unavailable when an authoritative entry is present.
""")
String chat(@UserMessage String message);
}Next, we have three simple tools to illustrate how they can be autonomously selected and dynamically invoked.
This tool mocks sending an email via Quarkus Mailer.
package com.oracle.dev.quarkus.langchain4j.agent;
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 EmailTool {
@Inject
Mailer mailer;
@Tool("Sends an email to a specified recipient using the error code as subject and the respective error description as the email text body")
public String sendEmail(String recipient, String subject, String body) {
mailer.send(Mail.withText(recipient, subject, body));
return jakarta.ws.rs.core.Response.Status.OK.name();
}
}This tool uses the Oracle LangChain4J extension and the underlying Oracle AI Vector Search mechanisms to perform semantic searches in the Oracle AI Database 26ai (vector store).
package com.oracle.dev.quarkus.langchain4j.agent;
import java.util.List;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.Content;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.rag.query.Query;
import dev.langchain4j.store.embedding.EmbeddingStore;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class OracleDatabaseTool {
private static final String NO_RESULTS = "--- No relevant information found in the Oracle Database knowledge base. ---";
private ContentRetriever retriever;
@Inject
RagInitializationService ragInitializationService;
@Inject
OracleErrorCodeLookup exactCodeLookup;
@jakarta.annotation.PostConstruct
public void init() {
EmbeddingStore<TextSegment> embeddingStore = ragInitializationService.getEmbeddingStore();
EmbeddingModel embeddingModel = ragInitializationService.getEmbeddingModel();
if (embeddingStore == null || embeddingModel == null) {
throw new IllegalStateException("RagInitializationService not properly initialized");
}
this.retriever = EmbeddingStoreContentRetriever.builder().embeddingStore(embeddingStore)
.embeddingModel(embeddingModel).maxResults(40).minScore(0.3).build();
}
@Tool("Searches the Oracle Database error codes knowledge base to answer a customer's question about error code descriptions")
public String searchKnowledgeBase(String query) {
List<String> exactMatches = exactCodeLookup.findExactMatches(query);
if (!exactMatches.isEmpty()) {
return String.join("\n", exactMatches);
}
List<Content> contents = retriever.retrieve(Query.from(query));
if (contents == null || contents.isEmpty()) {
return NO_RESULTS;
}
var ordered = new java.util.LinkedHashSet<String>();
for (Content content : contents) {
TextSegment segment = content.textSegment();
if (segment == null) {
continue;
}
String text = segment.text();
ordered.add(text);
}
if (ordered.isEmpty()) {
return NO_RESULTS;
}
return String.join("\n", ordered);
}
}This tool only writes messages to the console, so it doesn’t do anything special. However, it will support us in demonstrating different behaviours, depending on the natural language message received from the user.
You can try it by sending messages that do not mention email messages, and you will see that the email tool won’t be included in the ReAct loop.
package com.oracle.dev.quarkus.langchain4j.agent;
import dev.langchain4j.agent.tool.Tool;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class LoggingTool {
@Tool("Given and error code, only if an email message has not been sent to customer@buildstuff.events, logs the corresponding error description as a log message")
public void logErrorDescription(String logMessage) {
System.out.println("--- Logging message: " + logMessage + "---");
}
}Step 3: Build and run the agent application
Build the application.
mvn clean packageThen run it in production mode (or use mvnw quarkus:dev for dev mode)
java -jar target/quarkus-app/quarkus-run.jarAs soon as it starts up, you will see logs indicating Quarkus startup and the HTTP server running on port 8080. You will see messages related to the RAG initialization and vector embeddings.
Open a web browser and navigate to http://localhost:8080.
You will see the web interface below. Note that now you must enter your prompt in natural language, or select one of the Quick Starts options below the Ask the agent button to reuse an existing prompt before submitting.
If you have a database client, connect to your database instance using your credentials and JDBC URL, and inspect the rag_agent_embeddings in your target DB schema. It should have 35 records in total, as expected.
Now I advise you to interact with the agent using natural language and submit as many messages as you want. I will leave that as an exercise for you to analyse the different behaviour. Nevertheless, below is a sample output message.
If you need help configuring a running instance of Oracle AI Database 26ai, my previous blog posts listed below provide all the details.
- Getting started with Oracle’s contribution to LangChain4J
- Retrieval-Augmented Generation (RAG) with LangChain4J, OpenAI, and the Oracle AI Database 26ai
You can also get a Maven project with a complete code sample on GitHub.
Wrapping it up
That’s it! You now have a fully functional AI agent that understands natural language, retrieves information from Oracle AI Database 26ai using vector search, and takes action by using different tools depending on your prompt message. We explored Quarkus and LangChain4J, and concluded that they provide a rich set of out-of-the-box Java components that allow you to build great agentic AI applications.
Combine that with the Oracle AI Database 26ai, the OracleEmbeddingStore, and Oracle AI Vector Search to easily implement RAG solutions, enabling your agents to use a mission-critical, enterprise vector store to build AI applications.
As you build on this foundation, you can extend your agents with richer knowledge bases, more sophisticated tools, and additional capabilities like multi-agent collaboration or Model Context Protocol (MCP) integration.
Last, I invite you to check out the new Database Navigator tool available at the JetBrains Marketplace. In a nutshell, it features a robust SQL and PL/SQL editor with advanced capabilities for managing database connections, executing scripts, editing database objects, and modifying data and code with IntelliJ!
I will soon post more examples of scenarios involving LangChain4J, Spring AI, GraalVM, Micronaut, Quarkus, and other GenAI / Agentic AI-related topics!
That’s it! I hope you liked this blog post. Thanks for reading!
References
LangChain4J — Oracle Embedding Store
Maven — Oracle Embedding Store (langchain4j-oracle)
Oracle AI Database 26ai Free Container Image
Oracle® AI Database JDBC Java API Reference, Release 26ai
Getting started with Oracle’s contribution to LangChain4J
Retrieval-Augmented Generation (RAG) with LangChain4J, OpenAI, and the Oracle Database 23ai
Oracle AI Vector Search User’s Guide
Oracle AI Vector Search Technical Architecture
Database Navigator — JetBrains Marketplace
Developer’s Guide For Oracle JDBC on Maven Central
Oracle JDBC Development Center
Oracle Developers and Oracle OCI Free Tier
Join our Oracle Developers channel on Slack to discuss Java, GenAI, Agentic AI, JDK, JDBC, GraalVM, Micronaut, Spring Boot, Helidon, Quarkus, Reactive Streams, Cloud, DevOps, SRE, IaC, and other topics!
Build, test, and deploy your applications on Oracle Cloud — for free! Get access to the OCI Cloud Free Tier!

