Enterprise CDI extension for LangChain4j - inject AI services directly into your Jakarta EE and MicroProfile applications.
- Overview
- Quick Start
- Dependencies
- Configuration Reference
- AI Service Registration
- Agent Registration — full details in langchain4j-cdi-a2a/README.md
- Tools and Function Calling
- RAG (Retrieval Augmented Generation)
- Chat Memory
- MicroProfile Integration
- Expression Resolvers — full details in langchain4j-cdi-el/README.md
- MCP Server — Expose your CDI beans as an MCP server
- Examples
- Troubleshooting
This project provides seamless integration between LangChain4j and CDI (Contexts and Dependency Injection), enabling you to:
- Inject AI services as CDI beans using
@RegisterAIService - Build agentic systems with per-topology annotations (
@RegisterSimpleAgent,@RegisterSequenceAgent, etc.) for multi-agent workflows (sequences, loops, supervisors, A2A) - Configure LLM components via properties (can use microprofile configuration adapter or provide your own)
- Add resilience with MicroProfile Fault Tolerance (
@Retry,@Timeout,@CircuitBreaker) - Monitor AI operations with MicroProfile Telemetry/OpenTelemetry
| Runtime | Extension Type |
|---|---|
| Quarkus | Build-compatible |
| Helidon | Both |
| WildFly | Portable |
| Payara | Portable |
| GlassFish | Portable |
| Liberty | Portable |
For portable extension (WildFly, Payara, GlassFish, Liberty):
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-portable-ext</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>For build-compatible extension (Quarkus, Helidon):
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-build-compatible-ext</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>For configuration using properties:
Provide your own LLMConfig SPI implementation (see Configuration Architecture),
or, if you use MicroProfile:
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-config</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>Add your LLM provider dependency:
<!-- For Ollama (local models) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- For OpenAI -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- For Anthropic Claude -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
<version>${langchain4j.version}</version>
</dependency>For langchain4j-cdi-config create src/main/resources/META-INF/microprofile-config.properties,
for Quarkus add properties in application.properties.
# Chat model configuration (Ollama example)
dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.ollama.OllamaChatModel
dev.langchain4j.cdi.plugin.chat-model.config.base-url=http://localhost:11434
dev.langchain4j.cdi.plugin.chat-model.config.model-name=llama3.1
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.anthropic.AnthropicChatModel
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${ANTHROPIC_API_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.model-name=claude-sonnet-4-20250514
dev.langchain4j.cdi.plugin.chat-model.config.max-tokens=4096You can skip property-based configuration entirely and create your ChatModel using a CDI producer method:
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class ChatModelProducer {
@Produces
@ApplicationScoped
public ChatModel chatModel() {
return AnthropicChatModel.builder()
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
.modelName("claude-sonnet-4-20250514")
.maxTokens(4096)
.build();
}
}This approach gives you full programmatic control over the model configuration.
import dev.langchain4j.cdi.spi.RegisterAIService;
import dev.langchain4j.service.SystemMessage;
@RegisterAIService
public interface AssistantService {
@SystemMessage("You are a helpful assistant.")
String chat(String userMessage);
}import jakarta.inject.Inject;
import jakarta.ws.rs.*;
@Path("/assistant")
public class AssistantResource {
@Inject
AssistantService assistant;
@GET
@Path("/chat")
public String chat(@QueryParam("message") String message) {
return assistant.chat(message);
}
}| Module | Purpose |
|---|---|
langchain4j-cdi-core |
Core CDI integration classes |
langchain4j-cdi-portable-ext |
Runtime CDI extension |
langchain4j-cdi-build-compatible-ext |
Build-time CDI extension |
langchain4j-cdi-config |
MicroProfile Config integration (also provides ${...} expression resolver) |
langchain4j-cdi-el |
Jakarta EL expression resolver for #{...} expressions in annotation attributes |
langchain4j-cdi-a2a |
CDI wiring for the A2A (Agent-to-Agent) topology |
langchain4j-cdi-fault-tolerance |
MicroProfile Fault Tolerance support |
langchain4j-cdi-telemetry |
OpenTelemetry metrics for AI operations |
langchain4j-agentic |
LangChain4j agentic framework (required for agentic topologies) |
langchain4j-agentic-a2a |
A2A protocol support (required for A2A topology) |
<!-- For fault tolerance (@Retry, @Timeout, @CircuitBreaker) -->
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-fault-tolerance</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>
<!-- For OpenTelemetry metrics -->
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-telemetry</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>
<!-- To use microprofile configurations -->
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-config</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>
<!-- To resolve Jakarta EL expressions #{...} in annotation attributes -->
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-el</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>LangChain4j CDI uses a Service Provider Interface (SPI) pattern for configuration, making it portable across different configuration systems.
At the core is the abstract LLMConfig class (dev.langchain4j.cdi.core.config.spi.LLMConfig). This class defines three abstract methods that any configuration provider must implement:
public abstract class LLMConfig {
public static final String PREFIX = "dev.langchain4j.cdi.plugin";
/** Initialize the configuration source (called once at startup) */
public abstract void init();
/** Return all property keys available in the configuration */
public abstract Set<String> getPropertyKeys();
/** Return the value for a given property key, or null if not found */
public abstract String getValue(String key);
}The LLMConfig implementation is discovered via Java ServiceLoader. You register your implementation in:
META-INF/services/dev.langchain4j.cdi.core.config.spi.LLMConfig
The langchain4j-cdi-config module provides LLMConfigMPConfig, an implementation that uses MicroProfile Config:
public class LLMConfigMPConfig extends LLMConfig {
private Config config;
@Override
public void init() {
config = ConfigProvider.getConfig();
}
@Override
public Set<String> getPropertyKeys() {
return StreamSupport.stream(config.getPropertyNames().spliterator(), false)
.filter(prop -> prop.startsWith(PREFIX))
.collect(Collectors.toSet());
}
@Override
public String getValue(String key) {
return config.getOptionalValue(key, String.class).orElse(null);
}
}This is registered in the service file:
# META-INF/services/dev.langchain4j.cdi.core.config.spi.LLMConfig
dev.langchain4j.cdi.core.mpconfig.LLMConfigMPConfig
When you add langchain4j-cdi-config to your dependencies, this implementation is automatically used.
You can create your own LLMConfig implementation for other configuration sources (YAML, database, etc.):
public class YamlLLMConfig extends LLMConfig {
private Map<String, String> properties;
@Override
public void init() {
// Load from YAML file
properties = loadYamlConfig("llm-config.yaml");
}
@Override
public Set<String> getPropertyKeys() {
return properties.keySet();
}
@Override
public String getValue(String key) {
return properties.get(key);
}
}Register it in META-INF/services/dev.langchain4j.cdi.core.config.spi.LLMConfig:
com.example.YamlLLMConfig
All LangChain4j components are configured using the following pattern:
dev.langchain4j.cdi.plugin.<bean-name>.class=<fully.qualified.ClassName>
dev.langchain4j.cdi.plugin.<bean-name>.scope=<scope-annotation> # Optional, defaults to @ApplicationScoped
dev.langchain4j.cdi.plugin.<bean-name>.config.<property>=<value>The <bean-name> becomes the CDI bean name (used with @Named qualifier).
The <property> is the property name camel cased converted to dashed text : logResponses -> log-responses.
dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.ollama.OllamaChatModel
dev.langchain4j.cdi.plugin.chat-model.config.base-url=http://localhost:11434
dev.langchain4j.cdi.plugin.chat-model.config.model-name=llama3.1
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7
dev.langchain4j.cdi.plugin.chat-model.config.timeout=PT60S
dev.langchain4j.cdi.plugin.chat-model.config.log-requests=true
dev.langchain4j.cdi.plugin.chat-model.config.log-responses=truedev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.openai.OpenAiChatModel
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${OPENAI_API_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.model-name=gpt-4
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7
dev.langchain4j.cdi.plugin.chat-model.config.max-tokens=1000dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.azure.AzureOpenAiChatModel
dev.langchain4j.cdi.plugin.chat-model.config.endpoint=${AZURE_OPENAI_ENDPOINT}
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${AZURE_OPENAI_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.deployment-name=gpt-4dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.anthropic.AnthropicChatModel
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${ANTHROPIC_API_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.model-name=claude-sonnet-4-20250514
dev.langchain4j.cdi.plugin.chat-model.config.max-tokens=4096
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7
dev.langchain4j.cdi.plugin.chat-model.config.log-requests=true
dev.langchain4j.cdi.plugin.chat-model.config.log-responses=true# MessageWindowChatMemory - keeps last N messages
dev.langchain4j.cdi.plugin.my-memory.class=dev.langchain4j.memory.chat.MessageWindowChatMemory
dev.langchain4j.cdi.plugin.my-memory.scope=jakarta.enterprise.context.ApplicationScoped
dev.langchain4j.cdi.plugin.my-memory.config.maxMessages=20dev.langchain4j.cdi.plugin.my-retriever.class=dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
dev.langchain4j.cdi.plugin.my-retriever.config.embeddingStore=lookup:@default
dev.langchain4j.cdi.plugin.my-retriever.config.embeddingModel=lookup:@default
dev.langchain4j.cdi.plugin.my-retriever.config.maxResults=5
dev.langchain4j.cdi.plugin.my-retriever.config.minScore=0.7When a configuration property expects another CDI bean, use the lookup: prefix:
| Value | Description |
|---|---|
lookup:@default |
Use the default (unqualified) CDI bean |
lookup:@all |
Inject all beans as a List |
lookup:<bean-name> |
Use the bean with @Named("<bean-name>") |
@RegisterAIService(
scope = RequestScoped.class, // CDI scope (default: RequestScoped)
tools = {BookingTools.class}, // Tool classes for function calling
chatModelName = "chat-model", // Name of ChatModel bean (default: "#default")
chatMemoryName = "my-memory", // Name of ChatMemory bean
contentRetrieverName = "my-retriever", // Name of ContentRetriever bean (for RAG)
retrievalAugmentorName = "", // Alternative to contentRetriever
moderationModelName = "", // Name of ModerationModel bean
streamingChatModelName = "", // Name of StreamingChatModel bean
chatMemoryProviderName = "", // Name of ChatMemoryProvider bean
toolProviderName = "" // Name of ToolProvider bean
)
public interface MyAiService {
// ...
}| Attribute | Default | Description |
|---|---|---|
scope |
RequestScoped.class |
CDI scope for the AI service bean |
tools |
{} |
Array of CDI bean classes containing @Tool methods |
chatModelName |
"#default" |
Name of the ChatModel bean. "#default" uses the default bean. Supports expressions |
chatMemoryName |
"" |
Name of ChatMemory bean (empty = no memory). Supports expressions |
contentRetrieverName |
"" |
Name of ContentRetriever bean for RAG. Supports expressions |
retrievalAugmentorName |
"" |
Name of RetrievalAugmentor bean (alternative to contentRetriever). Supports expressions |
moderationModelName |
"" |
Name of ModerationModel bean for content moderation. Supports expressions |
streamingChatModelName |
"" |
Name of StreamingChatModel bean for streaming responses. Supports expressions |
chatMemoryProviderName |
"" |
Name of ChatMemoryProvider bean (for per-user memory). Supports expressions |
toolProviderName |
"" |
Name of ToolProvider bean (dynamic tool discovery). Supports expressions |
Use standard LangChain4j annotations on your AI service methods:
@RegisterAIService
public interface MyAiService {
@SystemMessage("You are a helpful assistant specialized in {{topic}}.")
@UserMessage("Answer this question: {{question}}")
String ask(@V("topic") String topic, @V("question") String question);
@SystemMessage("Summarize the following text.")
String summarize(@UserMessage String text);
}Guardrails provide input and output validation for AI service interactions, allowing you to enforce rules, validate content, and ensure safe AI operations. They can be configured at the class or method level.
Configure guardrails using the @RegisterAIService annotation:
@RegisterAIService(
chatModelName = "chat-model",
inputGuardrails = {NoEmptyMessageGuardrail.class},
outputGuardrails = {ContentFilterGuardrail.class}
)
public interface SafeAssistant {
String chat(String message);
}Guardrails are resolved as CDI beans, enabling full dependency injection support:
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.InputGuardrailResult;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.logging.Logger;
@ApplicationScoped
public class NoEmptyMessageGuardrail implements InputGuardrail {
@Inject
Logger logger;
@Override
public InputGuardrailResult validate(UserMessage userMessage) {
String text = userMessage.singleText();
if (text == null || text.isBlank()) {
logger.warning("Empty message rejected by guardrail");
return fatal("Message must not be empty");
}
return success();
}
}Output guardrails validate AI responses before returning them to the user:
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.guardrail.OutputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrailResult;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
@ApplicationScoped
public class ContentFilterGuardrail implements OutputGuardrail {
private static final List<String> FORBIDDEN_WORDS = List.of("forbidden", "blocked");
@Override
public OutputGuardrailResult validate(AiMessage responseFromLLM) {
String text = responseFromLLM.text();
for (String word : FORBIDDEN_WORDS) {
if (text.toLowerCase().contains(word)) {
return reprompt("Response contains forbidden content. Please rephrase.");
}
}
return success();
}
}Override class-level guardrails for specific methods using @InputGuardrails and @OutputGuardrails annotations:
import dev.langchain4j.service.guardrail.InputGuardrails;
import dev.langchain4j.service.guardrail.OutputGuardrails;
@RegisterAIService(
chatModelName = "chat-model",
inputGuardrails = {BasicGuardrail.class} // Applied to all methods by default
)
public interface FlexibleAssistant {
String chat(String message); // Uses BasicGuardrail
@InputGuardrails(StrictGuardrail.class) // Overrides class-level for this method
@OutputGuardrails(ContentFilterGuardrail.class)
String sensitiveChat(String message);
}If a guardrail class is not available as a CDI bean, the framework will attempt to instantiate it using the no-arg constructor:
// This guardrail doesn't need to be a CDI bean
public class SimpleGuardrail implements InputGuardrail {
public SimpleGuardrail() {
// No-arg constructor required for fallback
}
@Override
public InputGuardrailResult validate(UserMessage userMessage) {
return success();
}
}You can also reference guardrails by CDI bean name using @Named:
@ApplicationScoped
@Named("customInputGuardrail")
public class CustomInputGuardrail implements InputGuardrail {
@Override
public InputGuardrailResult validate(UserMessage userMessage) {
// your validation logic
return success();
}
}@RegisterAIService(
chatModelName = "chat-model",
inputGuardrailNames = {"customInputGuardrail"},
outputGuardrailNames = {"customOutputGuardrail"}
)
public interface NamedGuardrailService {
String chat(String message);
}Note: If both inputGuardrails (classes) and inputGuardrailNames (bean names) are specified, the classes take precedence and names are ignored. A warning will be logged.
Guardrails can return different result types:
| Result Type | Method | Description |
|---|---|---|
| Success | success() |
Validation passed, continue processing |
| Fatal | fatal(String message) |
Validation failed, abort with error |
| Reprompt | reprompt(String message) |
For output guardrails: retry with modified prompt |
Configure maximum retry attempts for output guardrails:
import dev.langchain4j.service.guardrail.OutputGuardrails;
@RegisterAIService
public interface RetryableAssistant {
@OutputGuardrails(value = ContentFilterGuardrail.class, maxRetries = 3)
String chat(String message);
}Input and output guardrails have separate configuration objects:
| Config Class | Properties | Description |
|---|---|---|
InputGuardrailsConfig |
(none) | Input guardrails have no configurable properties. Failures are passed directly to the caller as a GuardrailException. |
OutputGuardrailsConfig |
maxRetries (default: 2) |
Maximum retry attempts when an output guardrail triggers a retry or reprompt. Set to 0 to disable retries. |
Guardrails can be applied at both the class level (via @RegisterAIService) and the method level (via @InputGuardrails / @OutputGuardrails annotations from LangChain4j). When both are present, method-level annotations override class-level settings for that specific method:
@RegisterAIService(
chatModelName = "chat-model",
inputGuardrails = {BasicGuardrail.class} // Applied to methods without overrides
)
@OutputGuardrails(ContentFilterGuardrail.class) // Class-level output guardrail
public interface FlexibleAssistant {
String chat(String message); // Uses BasicGuardrail (input) + ContentFilterGuardrail (output)
@InputGuardrails(StrictGuardrail.class) // Overrides class-level input guardrail
String sensitiveChat(String message); // Uses StrictGuardrail (input) + ContentFilterGuardrail (output)
}-
Use appropriate CDI scopes:
@ApplicationScopedfor stateless guardrails (recommended)@RequestScopedfor guardrails that need request-specific data
-
Keep guardrails focused: Each guardrail should validate one specific concern
-
Provide clear error messages: Use descriptive messages in
fatal()andreprompt()results -
Consider performance: Guardrails are executed on every AI service call
-
Test thoroughly: Write unit tests for your guardrail logic
-
Log appropriately: Use injected loggers to track guardrail decisions
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.InputGuardrailResult;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.logging.Logger;
@ApplicationScoped
public class MessageLengthGuardrail implements InputGuardrail {
private static final int MAX_LENGTH = 1000;
private static final int MIN_LENGTH = 5;
@Inject
Logger logger;
@Override
public InputGuardrailResult validate(UserMessage userMessage) {
String text = userMessage.singleText();
if (text == null || text.length() < MIN_LENGTH) {
logger.warning("Message too short: " + (text != null ? text.length() : 0) + " characters");
return fatal("Message must be at least " + MIN_LENGTH + " characters long");
}
if (text.length() > MAX_LENGTH) {
logger.warning("Message too long: " + text.length() + " characters");
return fatal("Message must not exceed " + MAX_LENGTH + " characters");
}
logger.fine("Message length validated: " + text.length() + " characters");
return success();
}
}Each agentic topology has a dedicated CDI stereotype annotation. The 11 supported topologies are: SIMPLE, SEQUENCE, LOOP, PARALLEL, PARALLEL_MAPPER, CONDITIONAL, SUPERVISOR, PLANNER, A2A (Agent-to-Agent over HTTP), MCP_CLIENT, and HUMAN_IN_THE_LOOP.
For full documentation including the attribute reference for each annotation and the A2A SPI, see langchain4j-cdi-a2a/README.md.
@RegisterSequenceAgent(
name = "content-pipeline",
subAgentNames = {"researcher", "writer", "reviewer"},
outputKey = "final-content")
public interface ContentPipeline {
@Agent
String produceContent(@V("topic") String topic);
}Tools enable your AI service to call your business logic.
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class BookingTools {
@Tool("Get all bookings for a customer by their name")
public String getBookings(@P("Customer full name") String customerName) {
// Your business logic here
return "Booking #123: Rental car from 2024-01-15 to 2024-01-20";
}
@Tool("Cancel a booking by its ID")
public String cancelBooking(
@P("The booking ID to cancel") String bookingId,
@P("Customer name for verification") String customerName) {
// Your business logic here
return "Booking " + bookingId + " has been cancelled.";
}
}@RegisterAIService(tools = BookingTools.class)
public interface BookingAssistant {
@SystemMessage("""
You are a booking assistant for a car rental company.
Use the available tools to help customers with their bookings.
Always verify customer identity before making changes.
""")
String chat(String userMessage);
}@RegisterAIService(tools = {BookingTools.class, PaymentTools.class, NotificationTools.class})
public interface FullServiceAssistant {
String chat(String message);
}RAG enables your AI to answer questions based on your documents.
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class EmbeddingProducers {
@Produces
@ApplicationScoped
public EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}
@Produces
@ApplicationScoped
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
}dev.langchain4j.cdi.plugin.doc-retriever.class=dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
dev.langchain4j.cdi.plugin.doc-retriever.config.embeddingStore=lookup:@default
dev.langchain4j.cdi.plugin.doc-retriever.config.embeddingModel=lookup:@default
dev.langchain4j.cdi.plugin.doc-retriever.config.maxResults=5
dev.langchain4j.cdi.plugin.doc-retriever.config.minScore=0.6@RegisterAIService(contentRetrieverName = "doc-retriever")
public interface DocumentAssistant {
@SystemMessage("Answer questions based on the provided context. If unsure, say so.")
String askAboutDocuments(String question);
}import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
@ApplicationScoped
public class DocumentLoader {
@Inject
EmbeddingStore<TextSegment> embeddingStore;
@Inject
EmbeddingModel embeddingModel;
@PostConstruct
void loadDocuments() {
List<Document> documents = FileSystemDocumentLoader.loadDocuments("docs/");
EmbeddingStoreIngestor.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.documentSplitter(DocumentSplitters.recursive(500, 50))
.build()
.ingest(documents);
}
}Chat memory maintains conversation context across multiple interactions.
dev.langchain4j.cdi.plugin.shared-memory.class=dev.langchain4j.memory.chat.MessageWindowChatMemory
dev.langchain4j.cdi.plugin.shared-memory.scope=jakarta.enterprise.context.ApplicationScoped
dev.langchain4j.cdi.plugin.shared-memory.config.maxMessages=50@RegisterAIService(chatMemoryName = "shared-memory")
public interface ChatBot {
String chat(String message);
}For multi-user scenarios, use a ChatMemoryProvider:
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ApplicationScoped
@Named("per-user-memory")
public class UserChatMemoryProvider implements ChatMemoryProvider {
private final Map<Object, ChatMemory> memories = new ConcurrentHashMap<>();
@Override
public ChatMemory get(Object memoryId) {
return memories.computeIfAbsent(memoryId,
id -> MessageWindowChatMemory.withMaxMessages(20));
}
}@RegisterAIService(chatMemoryProviderName = "per-user-memory")
public interface UserChatBot {
@SystemMessage("You are a helpful assistant.")
String chat(@MemoryId String sessionId, @UserMessage String message);
}Annotation string attributes on @RegisterAIService and agent stereotype annotations can contain expressions resolved at runtime:
| Syntax | Module | Description |
|---|---|---|
${property.key} |
langchain4j-cdi-config |
Resolved via MicroProfile Config |
#{el.expression} |
langchain4j-cdi-el |
Evaluated as Jakarta EL (can access CDI @Named beans) |
Both modules are optional and can coexist. Resolution is done via the ExpressionResolver SPI, discovered through ServiceLoader and applied as a pipeline.
For full documentation including the custom resolver SPI, runtime requirements, and ordering guarantees, see langchain4j-cdi-el/README.md.
Add resilience to AI operations:
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-fault-tolerance</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>import org.eclipse.microprofile.faulttolerance.*;
import java.time.temporal.ChronoUnit;
@RegisterAIService
public interface ResilientAssistant {
@Retry(maxRetries = 3, delay = 1000)
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5)
@Fallback(fallbackMethod = "fallbackChat")
String chat(String message);
default String fallbackChat(String message) {
return "I'm temporarily unavailable. Please try again later.";
}
}Monitor AI operations with OpenTelemetry:
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-telemetry</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>Collected metrics:
gen_ai.client.token.usage- Input/output token countsgen_ai.client.operation.duration- Operation duration in seconds
All examples are based on a "Miles of Smiles" car rental company application inspired from the Java meets AI talk from Lize Raes at Devoxx Belgium 2023. Each example demonstrates:
- Chat Service: Customer assistant with RAG (Retrieval Augmented Generation)
- Fraud Detection: AI-powered fraud detection service
- Function Calling: Integration with business logic through tools
Complete example applications are available in the examples/ directory:
| Example | Runtime | Extension | Description |
|---|---|---|---|
payara-car-booking |
Payara Micro | Portable | Payara Micro with web UI |
liberty-car-booking |
Open Liberty | Portable | Liberty with OpenAPI UI |
liberty-car-booking-mcp |
Open Liberty | Portable | Liberty with MCP (Model Context Protocol) |
helidon-car-booking |
Helidon 4 | Build-compatible | Helidon with build-time extension |
helidon-car-booking-portable-ext |
Helidon 4 | Portable | Helidon with runtime extension |
quarkus-car-booking |
Quarkus | Build-compatible | Quarkus dev UI integration |
glassfish-car-booking |
GlassFish | Portable | Jakarta EE full profile |
Each example includes a run.sh script that starts Ollama (if needed) and the application server.
Quick start with any example:
cd examples/<example-name>
./run.shManual setup:
- Start Ollama:
# Using Docker/Podman
docker run -d --name ollama -p 11434:11434 -v ollama:/root/.ollama ollama/ollama
docker exec -it ollama ollama pull llama3.1
# Or install locally: https://ollama.ai/
ollama pull llama3.1- Run the example:
| Example | Command | Port |
|---|---|---|
payara-car-booking |
./run.sh |
8080 |
liberty-car-booking |
mvn liberty:dev |
9080 |
helidon-car-booking |
./run.sh |
8080 |
quarkus-car-booking |
./runexample.sh |
8080 |
glassfish-car-booking |
./run.sh |
8080 |
- Access the application:
- Payara: http://localhost:8080/
- Liberty: http://localhost:9080/openapi/ui
- Helidon: http://localhost:8080/openapi/ui
- Quarkus: http://localhost:8080/
- GlassFish: http://localhost:8080/glassfish-car-booking/api/car-booking/
Once running, you can ask questions like:
- "Hello, how can you help me?"
- "What is your cancellation policy?"
- "What is your list of cars?"
- "My name is James Bond, please list my bookings"
- "Is my booking 123-456 cancelable?"
Bean not found for ChatModel
- Ensure you have configured the chat model in
microprofile-config.properties - Check the bean name matches what you specified in
chatModelName
Configuration not loading
- Verify
microprofile-config.propertiesis insrc/main/resources/META-INF/ - Check property names match the builder method names (use kebab-case:
base-url,model-name)
Tool methods not being called
- Ensure tool class is a CDI bean (
@ApplicationScoped) - Check
@Tooldescription is clear for the LLM to understand when to use it - Verify tool class is listed in
@RegisterAIService(tools = ...)
Enable request/response logging:
dev.langchain4j.cdi.plugin.chat-model.config.log-requests=true
dev.langchain4j.cdi.plugin.chat-model.config.log-responses=trueSee CONTRIBUTING.md for contribution guidelines.
Apache License 2.0 - see LICENSE file.
