Micronaut LangChain4j

Integration between Micronaut and Langchain4j

Version: 2.4.0-SNAPSHOT

1 Introduction

This module provides integration between Micronaut and Langchain4j.

This module is regarded as experimental and subject to change since the underlying technology (AI) is volatile and subject to change.

Various modules are provided that allow automatically configuring common Langchain4j types like ChatModel, ImageModel etc. Refer to the sections below for the supported Langchain4j extensions.

2 Quick Start

Add the following annotation processor dependency:

annotationProcessor("io.micronaut.langchain4j:micronaut-langchain4j-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.langchain4j</groupId>
        <artifactId>micronaut-langchain4j-processor</artifactId>
    </path>
</annotationProcessorPaths>
[tool.pyronaut.dependencies]
build = [
    "io.micronaut.langchain4j:micronaut-langchain4j-processor",
]

Then the core module:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-core")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-core</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-core",
]

You are now ready to configure one of the Chat Language Models, for the quick start we will use Ollama:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-ollama")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-ollama</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-ollama",
]

To test the integration add the test resources integration to your Maven build or Gradle build.

testResourcesService("io.micronaut.langchain4j:micronaut-langchain4j-ollama-testresource")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-ollama-testresource</artifactId>
    <scope>testResourcesService</scope>
</dependency>
[tool.pyronaut.dependencies]
test = [
    "io.micronaut.langchain4j:micronaut-langchain4j-ollama-testresource",
]

Add the necessary configuration to configure the model name you want to use:

Configuring the Model Name
langchain4j.ollama.model-name=orca-mini
langchain4j.ollama.model-name: orca-mini
"langchain4j.ollama.model-name" = "orca-mini"
langchain4j.ollama.modelName = "orca-mini"
{
  "langchain4j.ollama.model-name" = "orca-mini"
}
{
  "langchain4j.ollama.model-name": "orca-mini"
}

3 Response Streaming

It is possible to use response streaming. First, you need to configure a streaming chat model with langchain4j.*.streaming-chat-model.*. For example, with OpenAI:

Example Configuration
langchain4j.open-ai.api-key=${OPENAI_API_KEY}
langchain4j.open-ai.streaming-chat-model.model-name=gpt-4o
langchain4j.open-ai.streaming-chat-model.log-requests=true
langchain4j.open-ai.streaming-chat-model.log-responses=true
langchain4j:
  open-ai:
    api-key: ${OPENAI_API_KEY}
    streaming-chat-model:
      model-name: gpt-4o
      log-requests: true
      log-responses: true
[langchain4j.open-ai]
api-key = "${OPENAI_API_KEY}"

[langchain4j.open-ai.streaming-chat-model]
model-name = "gpt-4o"
log-requests = true
log-responses = true
langchain4j {
  openAi {
    apiKey = "${OPENAI_API_KEY}"
    streamingChatModel {
      modelName = "gpt-4o"
      logRequests = true
      logResponses = true
    }
  }
}
{
  langchain4j {
    open-ai {
      api-key = "${OPENAI_API_KEY}"
      streaming-chat-model {
        model-name = "gpt-4o"
        log-requests = true
        log-responses = true
      }
    }
  }
}
{
  "langchain4j": {
    "open-ai": {
      "api-key": "${OPENAI_API_KEY}",
      "streaming-chat-model": {
        "model-name": "gpt-4o",
        "log-requests": true,
        "log-responses": true
      }
    }
  }
}

Then, you will be able to inject a bean of type dev.langchain4j.model.chat.StreamingChatModel.

Additionally, you can use an AI Service with a method whose return type uses Project Reactor. For example, an @AIService interface with a method whose return type is Flux<String>. In order to do this, you will need to add the following dependency:

implementation("dev.langchain4j:langchain4j-reactor")
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "dev.langchain4j:langchain4j-reactor",
]

4 AI Service

You can also define new AI services:

Defining @AiService interfaces
package example.micronaut.aiservice;

import dev.langchain4j.service.SystemMessage;
import io.micronaut.langchain4j.annotation.AiService;

@AiService // (1)
public interface Friend {

    @SystemMessage("You are a good friend of mine. Answer using slang.") // (2)
    String chat(String userMessage);
}
Defining @AiService interfaces
from abc import ABC, abstractmethod

from dev.langchain4j.service import SystemMessage
from micronaut.langchain4j.annotation import AiService


@AiService  # (1)
class Friend(ABC):

    @SystemMessage("You are a good friend of mine. Answer using slang.")  # (2)
    @abstractmethod
    def chat(self, user_message: str) -> str:
        ...
Defining @AiService interfaces
package example.micronaut.aiservice

import dev.langchain4j.service.SystemMessage
import io.micronaut.langchain4j.annotation.AiService

@AiService // (1)
interface Friend {
    @SystemMessage("You are a good friend of mine. Answer using slang.") // (2)
    fun chat(userMessage: String): String
}
Defining @AiService interfaces
package example.micronaut.aiservice

import dev.langchain4j.service.SystemMessage
import io.micronaut.langchain4j.annotation.AiService

@AiService // (1)
interface Friend {
    @SystemMessage("You are a good friend of mine. Answer using slang.") // (2)
    String chat(String userMessage)
}
1 Define an interface annotated with @AiService
2 Use Langchain4j annotations like @SystemMessage
In Python an AI service is an abstract class (abc.ABC) whose methods are declared with …​ bodies; the decorators mirror the LangChain4j annotations.
LangChain4j builds the service implementation reflectively from the Java interface generated for the Python class and reads the @SystemMessage/@UserMessage annotations from it, so the annotations must be copied onto the generated interface: declare the @AllowsReflection hint (micronaut.core.annotation) on the AI service, or name its package in the micronaut.introspection.allow-reflection compiler option (micronautBuild.python.compilerArgs.add("-Amicronaut.introspection.allowReflection=example.micronaut.*") in the Gradle build, as the examples of this guide do).

You can now inject the @AiService definition into any Micronaut component including tests:

Calling @AiService definitions
package example.micronaut.aiservice;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import dev.langchain4j.model.chat.ChatModel;
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AiServiceTest implements OllamaTestPropertyProvider {
    @Test
    void testAiService(Friend friend, ChatModel languageModel) {
        String result = friend.chat("Hello");

        assertNotNull(result);
        assertNotNull(languageModel);
    }
}
Calling @AiService definitions
from typing import Annotated

from dev.langchain4j.model.chat import ChatModel
from jakarta.inject import Inject
from micronaut.test.extensions.junit5.annotation import MicronautTest
from org.junit.jupiter.api import Test

from example.micronaut.aiservice.Friend import Friend


@MicronautTest(startApplication=False, environments=["ollama"])
class AiServiceTest:
    friend: Annotated[Friend, Inject]
    language_model: Annotated[ChatModel, Inject]

    @Test
    def test_ai_service(self):
        result = self.friend.chat("Hello")

        assert result is not None
        assert self.language_model is not None
Calling @AiService definitions
package example.micronaut.aiservice

import dev.langchain4j.model.chat.ChatModel
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class AiServiceTest : OllamaTestPropertyProvider {
    @Test
    fun testAiService(friend: Friend, languageModel: ChatModel) {
        val result: String = friend.chat("Hello")
        Assertions.assertNotNull(result)
        Assertions.assertNotNull(languageModel)
    }
}
Calling @AiService definitions
package example.micronaut.aiservice

import dev.langchain4j.model.chat.ChatModel
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.testcontainers.junit.jupiter.Testcontainers

import static org.junit.jupiter.api.Assertions.assertNotNull

@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AiServiceTest implements OllamaTestPropertyProvider {
    @Test
    void testAiService(Friend friend, ChatModel languageModel) {
        String result = friend.chat("Hello")
        assertNotNull(result)
        assertNotNull(languageModel)
    }
}

Retrieval Augmented Generation

Retrieval is opt-in. An @AiService performs retrieval augmented generation (RAG) only when the application declares one of:

  • a dev.langchain4j.rag.RetrievalAugmentor bean, or

  • a dev.langchain4j.rag.content.retriever.ContentRetriever bean, which LangChain4j wraps in a DefaultRetrievalAugmentor.

A RetrievalAugmentor bean takes precedence over a ContentRetriever bean. For either type, a bean @Named after the service (for example @Named("expert") for @AiService("expert")) is preferred over the default bean. If several candidates exist and none is named after the service or marked @Primary, no retrieval is configured.

Defining an @AiService that returns Result<String>
package example.micronaut.aiservice.rag;

import dev.langchain4j.service.Result;
import io.micronaut.context.annotation.Requires;
import io.micronaut.langchain4j.annotation.AiService;

@Requires(property = "spec.name", value = "RagTest")
@AiService // (1)
public interface Expert {
    Result<String> ask(String question); // (2)
}
Defining an @AiService that returns Result<String>
package example.micronaut.aiservice.rag

import dev.langchain4j.service.Result
import io.micronaut.context.annotation.Requires
import io.micronaut.langchain4j.annotation.AiService

@Requires(property = "spec.name", value = "RagTest")
@AiService // (1)
interface Expert {
    fun ask(question: String): Result<String> // (2)
}
Defining an @AiService that returns Result<String>
package example.micronaut.aiservice.rag

import dev.langchain4j.service.Result
import io.micronaut.context.annotation.Requires
import io.micronaut.langchain4j.annotation.AiService

@Requires(property = "spec.name", value = "RagTest")
@AiService // (1)
interface Expert {
    Result<String> ask(String question) // (2)
}
1 A plain @AiService; nothing on the interface refers to retrieval.
2 Returning dev.langchain4j.service.Result exposes the retrieved sources alongside the answer.
Declaring a ContentRetriever bean
package example.micronaut.aiservice.rag;

import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Singleton;

@Requires(property = "spec.name", value = "RagTest")
@Factory
class RetrievalFactory {

    @Singleton // (1)
    ContentRetriever contentRetriever(EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) { // (2)
        return EmbeddingStoreContentRetriever.builder()
            .embeddingStore(embeddingStore)
            .embeddingModel(embeddingModel)
            .maxResults(3)
            .build();
    }
}
Declaring a ContentRetriever bean
package example.micronaut.aiservice.rag

import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.model.embedding.EmbeddingModel
import dev.langchain4j.rag.content.retriever.ContentRetriever
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
import dev.langchain4j.store.embedding.EmbeddingStore
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import jakarta.inject.Singleton

@Requires(property = "spec.name", value = "RagTest")
@Factory
internal class RetrievalFactory {

    @Singleton // (1)
    fun contentRetriever(embeddingStore: EmbeddingStore<TextSegment>, embeddingModel: EmbeddingModel): ContentRetriever = // (2)
        EmbeddingStoreContentRetriever.builder()
            .embeddingStore(embeddingStore)
            .embeddingModel(embeddingModel)
            .maxResults(3)
            .build()
}
Declaring a ContentRetriever bean
package example.micronaut.aiservice.rag

import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.model.embedding.EmbeddingModel
import dev.langchain4j.rag.content.retriever.ContentRetriever
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
import dev.langchain4j.store.embedding.EmbeddingStore
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import jakarta.inject.Singleton

@Requires(property = "spec.name", value = "RagTest")
@Factory
@CompileStatic
class RetrievalFactory {

    @Singleton // (1)
    ContentRetriever contentRetriever(EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) { // (2)
        EmbeddingStoreContentRetriever.builder()
            .embeddingStore(embeddingStore)
            .embeddingModel(embeddingModel)
            .maxResults(3)
            .build()
    }
}
1 The bean is picked up by every @AiService that has no bean named after it. Add @Named to target one service.
2 Build the retriever from the configured EmbeddingStore and EmbeddingModel.
Retrieved content reaches the model and the result
package example.micronaut.aiservice.rag;

import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.service.Result;
import dev.langchain4j.store.embedding.EmbeddingStore;
import io.micronaut.context.annotation.Property;
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Property(name = "spec.name", value = "RagTest")
@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RagTest implements OllamaTestPropertyProvider {

    @Test
    void retrievedContentIsPassedToTheModel(Expert expert, EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) {
        TextSegment fact = TextSegment.from("Micronaut LangChain4j integrates LangChain4j with the Micronaut framework.");
        embeddingStore.add(embeddingModel.embed(fact).content(), fact); // (1)

        Result<String> result = expert.ask("What does Micronaut LangChain4j do?"); // (2)

        assertNotNull(result.content());
        assertTrue(result.sources().stream().anyMatch(content -> content.textSegment().text().equals(fact.text()))); // (3)
    }
}
Retrieved content reaches the model and the result
package example.micronaut.aiservice.rag

import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.model.embedding.EmbeddingModel
import dev.langchain4j.store.embedding.EmbeddingStore
import io.micronaut.context.annotation.Property
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.testcontainers.junit.jupiter.Testcontainers

@Property(name = "spec.name", value = "RagTest")
@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class RagTest : OllamaTestPropertyProvider {

    @Test
    fun retrievedContentIsPassedToTheModel(expert: Expert, embeddingStore: EmbeddingStore<TextSegment>, embeddingModel: EmbeddingModel) {
        val fact = TextSegment.from("Micronaut LangChain4j integrates LangChain4j with the Micronaut framework.")
        embeddingStore.add(embeddingModel.embed(fact).content(), fact) // (1)

        val result = expert.ask("What does Micronaut LangChain4j do?") // (2)

        assertNotNull(result.content())
        assertTrue(result.sources().any { it.textSegment().text() == fact.text() }) // (3)
    }
}
Retrieved content reaches the model and the result
package example.micronaut.aiservice.rag

import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.model.embedding.EmbeddingModel
import dev.langchain4j.service.Result
import dev.langchain4j.store.embedding.EmbeddingStore
import io.micronaut.context.annotation.Property
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.testcontainers.junit.jupiter.Testcontainers

import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertTrue

@Property(name = "spec.name", value = "RagTest")
@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RagTest implements OllamaTestPropertyProvider {

    @Test
    void retrievedContentIsPassedToTheModel(Expert expert, EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) {
        TextSegment fact = TextSegment.from("Micronaut LangChain4j integrates LangChain4j with the Micronaut framework.")
        embeddingStore.add(embeddingModel.embed(fact).content(), fact) // (1)

        Result<String> result = expert.ask("What does Micronaut LangChain4j do?") // (2)

        assertNotNull(result.content())
        assertTrue(result.sources().any { it.textSegment().text() == fact.text() }) // (3)
    }
}
1 Ingest a segment into the embedding store.
2 Call the service as usual.
3 The retrieved segment is available as a source of the result.

To configure a single service, declare an AiServiceCustomizer bean for it and call contentRetriever(…​) or retrievalAugmentor(…​) on AiServiceCreationContext.aiServices().

Before 2.3.0 an EmbeddingStoreContentRetriever was attached to every @AiService automatically whenever an EmbeddingModel bean and an EmbeddingStore bean existed. Because provider modules expose an embedding model and the in-memory embedding store is enabled by default, that was the case in most applications, causing an embedding round trip on every call and failures against models without embedding support. Declare a ContentRetriever bean as shown above to restore the previous behaviour.

5 Agentic Service

Agentic Service

Micronaut lets you declare LangChain4j Agentic services and have concrete agents generated at runtime.

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-agentic")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-agentic</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-agentic",
]

Annotate an interface with AgenticService and declare methods using LangChain4j Agentic annotations.

In Python an agentic service is an abstract class (abc.ABC) whose methods are declared with …​ bodies; the decorators mirror the LangChain4j annotations.
LangChain4j builds the agents reflectively from the Java interface generated for the Python class and reads the @Agent, @UserMessage, @SequenceAgent, …​ annotations from it, so the annotations must be copied onto the generated interface: declare the @AllowsReflection hint (micronaut.core.annotation) on the agentic service, or name its package in the micronaut.introspection.allow-reflection compiler option (micronautBuild.python.compilerArgs.add("-Amicronaut.introspection.allowReflection=example.micronaut.*") in the Gradle build, as the examples of this guide do).

The integration interprets your annotations and uses Micronaut DI to wire the underlying LangChain4j builders. It supports:

  • Typed agents built via AgenticServices.agentBuilder(Class) with @Agent methods.

  • Declarative workflow agents via AgenticServices.createAgenticSystem(…​).

  • Micronaut model, memory, RAG, tool, and lifecycle integration for each generated agent builder.

Quick start

package example.micronaut.agentic;

import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import io.micronaut.langchain4j.agentic.annotation.AgenticService;

/**
 * Minimal typed Agentic service used by the test-suite to validate Micronaut integration.
 */
@AgenticService
public interface GreeterAgent {

    @UserMessage("Say hello to {{name}}")
    @Agent(description = "Greets a person by name")
    String greet(@V("name") String name);
}
from abc import ABC, abstractmethod
from typing import Annotated

from dev.langchain4j.agentic import Agent
from dev.langchain4j.service import UserMessage, V
from micronaut.langchain4j.agentic.annotation import AgenticService


# Minimal typed Agentic service used by the test-suite to validate Micronaut integration.
@AgenticService
class GreeterAgent(ABC):

    @UserMessage("Say hello to {{name}}")
    @Agent(description="Greets a person by name")
    @abstractmethod
    def greet(self, name: Annotated[str, V("name")]) -> str:
        ...
package example.micronaut.agentic

import dev.langchain4j.agentic.Agent
import dev.langchain4j.service.UserMessage
import dev.langchain4j.service.V
import io.micronaut.langchain4j.agentic.annotation.AgenticService

/**
 * Minimal typed Agentic service used by the test-suite to validate Micronaut integration.
 */
@AgenticService
interface GreeterAgent {

    @UserMessage("Say hello to {{name}}")
    @Agent(description = "Greets a person by name")
    fun greet(@V("name") name: String): String
}
package example.micronaut.agentic

import dev.langchain4j.agentic.Agent
import dev.langchain4j.service.UserMessage
import dev.langchain4j.service.V
import io.micronaut.langchain4j.agentic.annotation.AgenticService

/**
 * Minimal typed Agentic service used by the test-suite to validate Micronaut integration.
 */
@AgenticService
interface GreeterAgent {

    @UserMessage("Say hello to {{name}}")
    @Agent(description = "Greets a person by name")
    String greet(@V("name") String name)
}

Usage in tests

package example.micronaut.agentic;

import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@MicronautTest(startApplication = false, environments = "agentic-test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AgenticServiceTest {

    @Test
    void testAgenticGreeter(GreeterAgent agent) {
        String result = agent.greet("John");
        assertNotNull(result);
    }
}
from typing import Annotated

from jakarta.inject import Inject
from micronaut.test.extensions.junit5.annotation import MicronautTest
from org.junit.jupiter.api import Test

from example.micronaut.agentic.GreeterAgent import GreeterAgent


@MicronautTest(startApplication=False, environments=["agentic-test"])
class AgenticServiceTest:
    agent: Annotated[GreeterAgent, Inject]

    @Test
    def test_agentic_greeter(self):
        result = self.agent.greet("John")
        assert result is not None
package example.micronaut.agentic

import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

@MicronautTest(startApplication = false, environments = ["agentic-test"])
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class AgenticServiceTest {

    @Test
    fun testAgenticGreeter(agent: GreeterAgent) {
        val result = agent.greet("John")
        assertNotNull(result)
    }
}
package example.micronaut.agentic

import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

import static org.junit.jupiter.api.Assertions.assertNotNull

@MicronautTest(startApplication = false, environments = "agentic-test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AgenticServiceTest {

    @Test
    void testAgenticGreeter(GreeterAgent agent) {
        String result = agent.greet("John")
        assertNotNull(result)
    }
}

Declarative workflows

You can build workflows using LangChain4j’s declarative annotations. The integration routes construction through Micronaut DI so that supported workflow builders are Micronaut-managed beans (allowing listeners and customization).

Supported patterns:

Supervisor-style and planner-style agents can still be modeled using LangChain4j typed agents or composed workflows, but the Micronaut-managed workflow builder lifecycle currently applies to the workflow types listed above.

Example: an evening planner (Sequence)

package example.micronaut.agentic;

import dev.langchain4j.agentic.declarative.SequenceAgent;
import dev.langchain4j.service.V;
import io.micronaut.langchain4j.agentic.annotation.AgenticService;

/**
 * Declarative sequence workflow inspired by the "EveningPlannerAgent" example.
 * This agent coordinates sub-agents and produces a final "plan" output.
 */
@AgenticService(outputKey = "plan")
public interface EveningPlannerAgent {

    // Declarative sequence workflow definition (no method body needed)
    @SequenceAgent(
        subAgents = {
            TravelRecommenderAgent.class,
            RecipeAdvisorAgent.class,
            PlanSynthesizerAgent.class
        },
        outputKey = "plan",
        name = "planEvening"
    )
    String plan(@V("topic") String topic);
}
from abc import ABC, abstractmethod
from typing import Annotated

from dev.langchain4j.agentic.declarative import SequenceAgent
from dev.langchain4j.service import V
from micronaut.langchain4j.agentic.annotation import AgenticService

from .PlanSynthesizerAgent import PlanSynthesizerAgent
from .RecipeAdvisorAgent import RecipeAdvisorAgent
from .TravelRecommenderAgent import TravelRecommenderAgent


# Declarative sequence workflow inspired by the "EveningPlannerAgent" example.
# This agent coordinates sub-agents and produces a final "plan" output.
@AgenticService(outputKey="plan")
class EveningPlannerAgent(ABC):

    # Declarative sequence workflow definition (no method body needed)
    @SequenceAgent(
        subAgents=[
            TravelRecommenderAgent,
            RecipeAdvisorAgent,
            PlanSynthesizerAgent
        ],
        outputKey="plan",
        name="planEvening"
    )
    @abstractmethod
    def plan(self, topic: Annotated[str, V("topic")]) -> str:
        ...
package example.micronaut.agentic

import dev.langchain4j.agentic.declarative.SequenceAgent
import dev.langchain4j.service.V
import io.micronaut.langchain4j.agentic.annotation.AgenticService

/**
 * Declarative sequence workflow inspired by the "EveningPlannerAgent" example.
 * This agent coordinates sub-agents and produces a final "plan" output.
 */
@AgenticService(outputKey = "plan")
interface EveningPlannerAgent {

    // Declarative sequence workflow definition (no method body needed)
    @SequenceAgent(
        subAgents = [
            TravelRecommenderAgent::class,
            RecipeAdvisorAgent::class,
            PlanSynthesizerAgent::class
        ],
        outputKey = "plan",
        name = "planEvening"
    )
    fun plan(@V("topic") topic: String): String
}
package example.micronaut.agentic

import dev.langchain4j.agentic.declarative.SequenceAgent
import dev.langchain4j.service.V
import io.micronaut.langchain4j.agentic.annotation.AgenticService

/**
 * Declarative sequence workflow inspired by the "EveningPlannerAgent" example.
 * This agent coordinates sub-agents and produces a final "plan" output.
 */
@AgenticService(outputKey = "plan")
interface EveningPlannerAgent {

    // Declarative sequence workflow definition (no method body needed)
    @SequenceAgent(
        subAgents = [
            TravelRecommenderAgent,
            RecipeAdvisorAgent,
            PlanSynthesizerAgent
        ],
        outputKey = "plan",
        name = "planEvening"
    )
    String plan(@V("topic") String topic)
}

Parallel workflow example

package example.micronaut.agentic;

import dev.langchain4j.agentic.Agent;
import dev.langchain4j.agentic.declarative.Output;
import dev.langchain4j.agentic.declarative.ParallelAgent;
import dev.langchain4j.agentic.declarative.ParallelExecutor;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import io.micronaut.langchain4j.agentic.annotation.AgenticService;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;

import static org.junit.jupiter.api.Assertions.assertFalse;

/**
 * Validates declarative ParallelAgent workflow wiring through @AgenticService.
 * Ensures Micronaut DI correctly builds the agentic system and executes the parallel plan.
 */
@MicronautTest(startApplication = false, environments = "agentic-test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ParallelPlanningAgentTest {

    @Test
    void testDeclarativeParallel(EveningPlanner agent) {
        String plan = agent.plan("jazz", "romantic");
        System.out.println("parallel plan = " + plan);
        assertFalse(plan.isEmpty());
    }

    @AgenticService
    public interface MusicPlanner {
        @UserMessage("""
            Choose a band which plays music in the {{style}} style.
            Answer with the name of the band only: no details, no explanation.
            """)
        @Agent(outputKey = "band")
        String suggestBand(@V("style") String style);
    }

    @AgenticService
    public interface DinnerPlanner {
        @UserMessage("""
            Choose a menu for dinner for the following mood: {{mood}}
            Answer with the menu only: no details, no explanations.
            """)
        @Agent(outputKey = "menu")
        String suggestMenu(@V("mood") String mood);
    }

    /**
     * Demonstrates declarative ParallelAgent orchestration using Micronaut DI.
     */
    @AgenticService(outputKey = "plan")
    public interface EveningPlanner {

        @ParallelAgent(
            subAgents = {
                MusicPlanner.class,
                DinnerPlanner.class
            },
            outputKey = "plan",
            name = "planEvening"
        )
        String plan(@V("style") String style, @V("mood") String mood);

        // Use a shared executor for parallel execution
        @ParallelExecutor
        static Executor executor() {
            return ForkJoinPool.commonPool();
        }

        // Aggregate the parallel outputs into a single "plan" string
        @Output
        static String aggregate(@V("band") String band, @V("menu") String menu) {
            String c = band == null ? "" : band.trim();
            String r = menu == null ? "" : menu.trim();
            if (c.isEmpty() && r.isEmpty()) {
                return "";
            }
            if (c.isEmpty()) {
                return "Play: " + r;
            }
            if (r.isEmpty()) {
                return "Menu: " + c;
            }
            return "Play: " + c + " | Menu: " + r;
        }
    }
}
from abc import ABC, abstractmethod
from typing import Annotated

from dev.langchain4j.agentic import Agent
from dev.langchain4j.agentic.declarative import Output, ParallelAgent, ParallelExecutor
from dev.langchain4j.service import UserMessage, V
from jakarta.inject import Inject
from java.util.concurrent import Executor, ForkJoinPool
from micronaut.langchain4j.agentic.annotation import AgenticService
from micronaut.test.extensions.junit5.annotation import MicronautTest
from org.junit.jupiter.api import Test


@AgenticService
class MusicPlanner(ABC):
    @UserMessage("""
        Choose a band which plays music in the {{style}} style.
        Answer with the name of the band only: no details, no explanation.
        """)
    @Agent(outputKey="band")
    @abstractmethod
    def suggest_band(self, style: Annotated[str, V("style")]) -> str:
        ...


@AgenticService
class DinnerPlanner(ABC):
    @UserMessage("""
        Choose a menu for dinner for the following mood: {{mood}}
        Answer with the menu only: no details, no explanations.
        """)
    @Agent(outputKey="menu")
    @abstractmethod
    def suggest_menu(self, mood: Annotated[str, V("mood")]) -> str:
        ...


# Demonstrates declarative ParallelAgent orchestration using Micronaut DI.
@AgenticService(outputKey="plan")
class EveningPlanner(ABC):

    @ParallelAgent(
        subAgents=[
            MusicPlanner,
            DinnerPlanner
        ],
        outputKey="plan",
        name="planEvening"
    )
    @abstractmethod
    def plan(self, style: Annotated[str, V("style")], mood: Annotated[str, V("mood")]) -> str:
        ...

    # Use a shared executor for parallel execution
    @ParallelExecutor
    @staticmethod
    def executor() -> Executor:
        return ForkJoinPool.commonPool()

    # Aggregate the parallel outputs into a single "plan" string
    @Output
    @staticmethod
    def aggregate(band: Annotated[str, V("band")], menu: Annotated[str, V("menu")]) -> str:
        c = "" if band is None else band.strip()
        r = "" if menu is None else menu.strip()
        if c == "" and r == "":
            return ""
        if c == "":
            return f"Play: {r}"
        if r == "":
            return f"Menu: {c}"
        return f"Play: {c} | Menu: {r}"


# Validates declarative ParallelAgent workflow wiring through @AgenticService.
# Ensures Micronaut DI correctly builds the agentic system and executes the parallel plan.
@MicronautTest(startApplication=False, environments=["agentic-test"])
class ParallelPlanningAgentTest:
    agent: Annotated[EveningPlanner, Inject]

    @Test
    def test_declarative_parallel(self):
        plan = self.agent.plan("jazz", "romantic")
        print(f"parallel plan = {plan}")
        assert plan != ""
package example.micronaut.agentic

import dev.langchain4j.agentic.Agent
import dev.langchain4j.agentic.declarative.Output
import dev.langchain4j.agentic.declarative.ParallelAgent
import dev.langchain4j.agentic.declarative.ParallelExecutor
import dev.langchain4j.service.UserMessage
import dev.langchain4j.service.V
import io.micronaut.langchain4j.agentic.annotation.AgenticService
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.util.concurrent.Executor
import java.util.concurrent.ForkJoinPool

/**
 * Validates declarative ParallelAgent workflow wiring through @AgenticService.
 * Ensures Micronaut DI correctly builds the agentic system and executes the parallel plan.
 */
@MicronautTest(startApplication = false, environments = ["agentic-test"])
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ParallelPlanningAgentTest {

    @Test
    fun testDeclarativeParallel(agent: EveningPlanner) {
        val plan = agent.plan("jazz", "romantic")
        println("parallel plan = $plan")
        assertFalse(plan.isEmpty())
    }

    @AgenticService
    interface MusicPlanner {
        @UserMessage("""
            Choose a band which plays music in the {{style}} style.
            Answer with the name of the band only: no details, no explanation.
            """)
        @Agent(outputKey = "band")
        fun suggestBand(@V("style") style: String): String
    }

    @AgenticService
    interface DinnerPlanner {
        @UserMessage("""
            Choose a menu for dinner for the following mood: {{mood}}
            Answer with the menu only: no details, no explanations.
            """)
        @Agent(outputKey = "menu")
        fun suggestMenu(@V("mood") mood: String): String
    }

    /**
     * Demonstrates declarative ParallelAgent orchestration using Micronaut DI.
     */
    @AgenticService(outputKey = "plan")
    interface EveningPlanner {

        @ParallelAgent(
            subAgents = [
                MusicPlanner::class,
                DinnerPlanner::class
            ],
            outputKey = "plan",
            name = "planEvening"
        )
        fun plan(@V("style") style: String, @V("mood") mood: String): String

        companion object {
            // Use a shared executor for parallel execution
            @JvmStatic
            @ParallelExecutor
            fun executor(): Executor = ForkJoinPool.commonPool()

            // Aggregate the parallel outputs into a single "plan" string
            @JvmStatic
            @Output
            fun aggregate(@V("band") band: String?, @V("menu") menu: String?): String {
                val c = band?.trim() ?: ""
                val r = menu?.trim() ?: ""
                if (c.isEmpty() && r.isEmpty()) {
                    return ""
                }
                if (c.isEmpty()) {
                    return "Play: $r"
                }
                if (r.isEmpty()) {
                    return "Menu: $c"
                }
                return "Play: $c | Menu: $r"
            }
        }
    }
}
package example.micronaut.agentic

import dev.langchain4j.agentic.Agent
import dev.langchain4j.agentic.declarative.Output
import dev.langchain4j.agentic.declarative.ParallelAgent
import dev.langchain4j.agentic.declarative.ParallelExecutor
import dev.langchain4j.service.UserMessage
import dev.langchain4j.service.V
import io.micronaut.langchain4j.agentic.annotation.AgenticService
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

import java.util.concurrent.Executor
import java.util.concurrent.ForkJoinPool

import static org.junit.jupiter.api.Assertions.assertFalse

/**
 * Validates declarative ParallelAgent workflow wiring through @AgenticService.
 * Ensures Micronaut DI correctly builds the agentic system and executes the parallel plan.
 */
@MicronautTest(startApplication = false, environments = "agentic-test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ParallelPlanningAgentTest {

    @Test
    void testDeclarativeParallel(EveningPlanner agent) {
        String plan = agent.plan("jazz", "romantic")
        println("parallel plan = " + plan)
        assertFalse(plan.isEmpty())
    }

    @AgenticService
    static interface MusicPlanner {
        @UserMessage("""
            Choose a band which plays music in the {{style}} style.
            Answer with the name of the band only: no details, no explanation.
            """)
        @Agent(outputKey = "band")
        String suggestBand(@V("style") String style)
    }

    @AgenticService
    static interface DinnerPlanner {
        @UserMessage("""
            Choose a menu for dinner for the following mood: {{mood}}
            Answer with the menu only: no details, no explanations.
            """)
        @Agent(outputKey = "menu")
        String suggestMenu(@V("mood") String mood)
    }

    /**
     * Demonstrates declarative ParallelAgent orchestration using Micronaut DI.
     */
    @AgenticService(outputKey = "plan")
    static interface EveningPlanner {

        @ParallelAgent(
            subAgents = [
                MusicPlanner,
                DinnerPlanner
            ],
            outputKey = "plan",
            name = "planEvening"
        )
        String plan(@V("style") String style, @V("mood") String mood)

        // Use a shared executor for parallel execution
        @ParallelExecutor
        static Executor executor() {
            ForkJoinPool.commonPool()
        }

        // Aggregate the parallel outputs into a single "plan" string
        @Output
        static String aggregate(@V("band") String band, @V("menu") String menu) {
            String c = band == null ? "" : band.trim()
            String r = menu == null ? "" : menu.trim()
            if (c.isEmpty() && r.isEmpty()) {
                return ""
            }
            if (c.isEmpty()) {
                return "Play: " + r
            }
            if (r.isEmpty()) {
                return "Menu: " + c
            }
            return "Play: " + c + " | Menu: " + r
        }
    }
}

Loop example with customization via a listener

package example.micronaut.agentic;

import dev.langchain4j.agentic.Agent;
import dev.langchain4j.agentic.declarative.LoopAgent;
import dev.langchain4j.agentic.workflow.LoopAgentService;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.langchain4j.agentic.annotation.AgenticService;
import jakarta.inject.Singleton;

/**
 * Demonstrates declarative LoopAgent orchestration using Micronaut DI.
 */
@AgenticService(outputKey = "translation")
public interface LoopingPlannerAgent {

    @LoopAgent(
        subAgents = {
            TranslatorAgent.class
        },
        outputKey = "text",
        maxIterations = 3
    )
    String translatesInLoop(@V("text") String text);

    interface TranslatorAgent {
        @UserMessage("""
            You are a translator.
            If the text is in English, translate to French.
            If the text is in French, translate to German.
            If the text is in German, translate to Spanish.
            Translate this: "{{text}}". Answer with the translation only, no explanations, no details.
            """)
        @Agent(outputKey = "text")
        String translate(@V("text") String text);
    }

    @Singleton
    @Requires(property = "spec.name", value = "LoopingPlannerAgentTest")
    class LoopBuilderListener implements BeanCreatedEventListener<LoopAgentService<?>> {

        @Override
        public LoopAgentService<?> onCreated(@NonNull BeanCreatedEvent<LoopAgentService<?>> event) {
            LoopAgentService<?> builder = event.getBean();
            builder.exitCondition((scope, idx) -> idx == 3);
            return builder;
        }
    }
}
from abc import ABC, abstractmethod
from typing import Annotated

from dev.langchain4j.agentic import Agent
from dev.langchain4j.agentic.declarative import LoopAgent
from dev.langchain4j.agentic.workflow import LoopAgentService
from dev.langchain4j.service import UserMessage, V
from jakarta.inject import Singleton
from micronaut.context.annotation import Requires
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener
from micronaut.langchain4j.agentic.annotation import AgenticService


class TranslatorAgent(ABC):
    @UserMessage("""
        You are a translator.
        If the text is in English, translate to French.
        If the text is in French, translate to German.
        If the text is in German, translate to Spanish.
        Translate this: "{{text}}". Answer with the translation only, no explanations, no details.
        """)
    @Agent(outputKey="text")
    @abstractmethod
    def translate(self, text: Annotated[str, V("text")]) -> str:
        ...


# Demonstrates declarative LoopAgent orchestration using Micronaut DI.
@AgenticService(outputKey="translation")
class LoopingPlannerAgent(ABC):

    @LoopAgent(
        subAgents=[
            TranslatorAgent
        ],
        outputKey="text",
        maxIterations=3
    )
    @abstractmethod
    def translates_in_loop(self, text: Annotated[str, V("text")]) -> str:
        ...


@Singleton
@Requires(property="spec.name", value="LoopingPlannerAgentTest")
class LoopBuilderListener(BeanCreatedEventListener[LoopAgentService]):

    def onCreated(self, event: BeanCreatedEvent[LoopAgentService]) -> LoopAgentService:
        builder = event.getBean()
        builder.exitCondition(lambda scope, idx: idx == 3)
        return builder
package example.micronaut.agentic

import dev.langchain4j.agentic.Agent
import dev.langchain4j.agentic.declarative.LoopAgent
import dev.langchain4j.agentic.workflow.LoopAgentService
import dev.langchain4j.service.UserMessage
import dev.langchain4j.service.V
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import io.micronaut.langchain4j.agentic.annotation.AgenticService
import jakarta.inject.Singleton

/**
 * Demonstrates declarative LoopAgent orchestration using Micronaut DI.
 */
@AgenticService(outputKey = "translation")
interface LoopingPlannerAgent {

    @LoopAgent(
        subAgents = [
            TranslatorAgent::class
        ],
        outputKey = "text",
        maxIterations = 3
    )
    fun translatesInLoop(@V("text") text: String): String

    interface TranslatorAgent {
        @UserMessage("""
            You are a translator.
            If the text is in English, translate to French.
            If the text is in French, translate to German.
            If the text is in German, translate to Spanish.
            Translate this: "{{text}}". Answer with the translation only, no explanations, no details.
            """)
        @Agent(outputKey = "text")
        fun translate(@V("text") text: String): String
    }

    @Singleton
    @Requires(property = "spec.name", value = "LoopingPlannerAgentTest")
    class LoopBuilderListener : BeanCreatedEventListener<LoopAgentService<*>> {

        override fun onCreated(event: BeanCreatedEvent<LoopAgentService<*>>): LoopAgentService<*> {
            val builder = event.bean
            builder.exitCondition { _, idx -> idx == 3 }
            return builder
        }
    }
}
package example.micronaut.agentic

import dev.langchain4j.agentic.Agent
import dev.langchain4j.agentic.declarative.LoopAgent
import dev.langchain4j.agentic.workflow.LoopAgentService
import dev.langchain4j.service.UserMessage
import dev.langchain4j.service.V
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import io.micronaut.langchain4j.agentic.annotation.AgenticService
import jakarta.inject.Singleton

/**
 * Demonstrates declarative LoopAgent orchestration using Micronaut DI.
 */
@AgenticService(outputKey = "translation")
interface LoopingPlannerAgent {

    @LoopAgent(
        subAgents = [
            TranslatorAgent
        ],
        outputKey = "text",
        maxIterations = 3
    )
    String translatesInLoop(@V("text") String text)

    interface TranslatorAgent {
        @UserMessage("""
            You are a translator.
            If the text is in English, translate to French.
            If the text is in French, translate to German.
            If the text is in German, translate to Spanish.
            Translate this: "{{text}}". Answer with the translation only, no explanations, no details.
            """)
        @Agent(outputKey = "text")
        String translate(@V("text") String text)
    }

    @Singleton
    @Requires(property = "spec.name", value = "LoopingPlannerAgentTest")
    class LoopBuilderListener implements BeanCreatedEventListener<LoopAgentService<?>> {

        @Override
        LoopAgentService<?> onCreated(BeanCreatedEvent<LoopAgentService<?>> event) {
            LoopAgentService<?> builder = event.bean
            builder.exitCondition { scope, idx -> idx == 3 }
            builder
        }
    }
}

Configuration

You can select which ChatModel bean to use per agent via configuration. The agent id is derived from the interface name by:

  • stripping a trailing "Agent" suffix

  • converting UpperCamel to lower-kebab (e.g. CreativeWriterAgent → creative-writer)

langchain4j.agentic.agents.<agent-id>.chat-model

Examples

langchain4j.agentic.agents.greeter.chat-model=friendly-chat-model
langchain4j.agentic.agents.creative-writer.chat-model=creative-chat-model
langchain4j:
  agentic:
    agents:
      greeter:
        chat-model: friendly-chat-model
      creative-writer:
        chat-model: creative-chat-model
[langchain4j.agentic.agents.greeter]
chat-model = "friendly-chat-model"

[langchain4j.agentic.agents.creative-writer]
chat-model = "creative-chat-model"
langchain4j {
  agentic {
    agents {
      greeter {
        chatModel = "friendly-chat-model"
      }
      creativeWriter {
        chatModel = "creative-chat-model"
      }
    }
  }
}
{
  langchain4j {
    agentic {
      agents {
        greeter {
          chat-model = "friendly-chat-model"
        }
        creative-writer {
          chat-model = "creative-chat-model"
        }
      }
    }
  }
}
{
  "langchain4j": {
    "agentic": {
      "agents": {
        "greeter": {
          "chat-model": "friendly-chat-model"
        },
        "creative-writer": {
          "chat-model": "creative-chat-model"
        }
      }
    }
  }
}

If the property is not set and there is exactly one ChatModel bean in the context, it will be used automatically.

Memory

By default, agentic services reuse the core MessageWindowChatMemory built from the configured ChatMemoryStore. The core module publishes a MessageWindowChatMemory.Builder per available ChatMemoryStore (via @EachBean(ChatMemoryStore)), so agents automatically use the default builder when a single memory store is configured.

You can optionally override memory per agent:

langchain4j.agentic.agents.<agent-id>.memory.store

langchain4j.agentic.agents.<agent-id>.memory.max-messages

Notes: - If memory.store is not set, the default MessageWindowChatMemory.Builder is used (as resolved by Micronaut DI). - If multiple memory stores are available, set memory.store to select the store for an agent. - If memory.max-messages is not set, the global core setting langchain4j.chat-memory-store.message-window.max-messages applies.

Examples

langchain4j.agentic.agents.greeter.memory.store=redis
langchain4j.agentic.agents.creative-writer.memory.max-messages=50
langchain4j:
  agentic:
    agents:
      greeter:
        memory:
          store: redis
      creative-writer:
        memory:
          max-messages: 50
[langchain4j.agentic.agents.greeter.memory]
store = "redis"

[langchain4j.agentic.agents.creative-writer.memory]
max-messages = 50
langchain4j {
  agentic {
    agents {
      greeter {
        memory {
          store = "redis"
        }
      }
      creativeWriter {
        memory {
          maxMessages = 50
        }
      }
    }
  }
}
{
  langchain4j {
    agentic {
      agents {
        greeter {
          memory {
            store = "redis"
          }
        }
        creative-writer {
          memory {
            max-messages = 50
          }
        }
      }
    }
  }
}
{
  "langchain4j": {
    "agentic": {
      "agents": {
        "greeter": {
          "memory": {
            "store": "redis"
          }
        },
        "creative-writer": {
          "memory": {
            "max-messages": 50
          }
        }
      }
    }
  }
}

Retrieval

Retrieval is opt-in. An agent performs retrieval augmented generation only when the application declares a dev.langchain4j.rag.RetrievalAugmentor bean or a dev.langchain4j.rag.content.retriever.ContentRetriever bean. A RetrievalAugmentor takes precedence over a ContentRetriever. For either type, a bean @Named after the agent id (for example @Named("greeter") for GreeterAgent) is preferred over the default bean. Embedding model and embedding store beans are never paired automatically.

Tools

You can request specific tool beans to be registered with the agent builder using the tools attribute. Tool classes should be Micronaut beans containing methods annotated with dev.langchain4j.agent.tool.Tool.

Customization

You can customize builders using Micronaut lifecycle listeners. Because workflow builders are created as Micronaut beans during declarative system construction, your listeners can adjust names, exit conditions, parallelism, etc., before the system executes.

Typed agents can also be customized via BeanCreatedEventListener<AgentBuilder<?, ?>>; the LoopingPlannerAgent snippet above shows the same Micronaut lifecycle technique applied to LoopAgentService.

For example, this listener customizes every generated declarative agent builder before LangChain4j builds the agentic system:

package example.micronaut.agentic;

import dev.langchain4j.agentic.agent.AgentBuilder;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.micronaut.core.annotation.NonNull;
import jakarta.inject.Singleton;

@Singleton
@Requires(env = "agentic-docs")
final class SupportAgentBuilderListener implements BeanCreatedEventListener<AgentBuilder<?, ?>> {

    @Override
    public AgentBuilder<?, ?> onCreated(@NonNull BeanCreatedEvent<AgentBuilder<?, ?>> event) {
        AgentBuilder<?, ?> builder = event.getBean();
        builder.name("customer-support-agent");
        builder.outputKey("supportResponse");
        return builder;
    }
}
from dev.langchain4j.agentic.agent import AgentBuilder
from jakarta.inject import Singleton
from micronaut.context.annotation import Requires
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener


@Singleton
@Requires(env="agentic-docs")
class SupportAgentBuilderListener(BeanCreatedEventListener[AgentBuilder]):

    def onCreated(self, event: BeanCreatedEvent[AgentBuilder]) -> AgentBuilder:
        builder = event.getBean()
        builder.name("customer-support-agent")
        builder.outputKey("supportResponse")
        return builder
package example.micronaut.agentic

import dev.langchain4j.agentic.agent.AgentBuilder
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@Singleton
@Requires(env = ["agentic-docs"])
internal class SupportAgentBuilderListener : BeanCreatedEventListener<AgentBuilder<*, *>> {

    override fun onCreated(event: BeanCreatedEvent<AgentBuilder<*, *>>): AgentBuilder<*, *> {
        val builder = event.bean
        builder.name("customer-support-agent")
        builder.outputKey("supportResponse")
        return builder
    }
}
package example.micronaut.agentic

import dev.langchain4j.agentic.agent.AgentBuilder
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@Singleton
@Requires(env = "agentic-docs")
class SupportAgentBuilderListener implements BeanCreatedEventListener<AgentBuilder<?, ?>> {

    @Override
    AgentBuilder<?, ?> onCreated(BeanCreatedEvent<AgentBuilder<?, ?>> event) {
        AgentBuilder<?, ?> builder = event.bean
        builder.name("customer-support-agent")
        builder.outputKey("supportResponse")
        builder
    }
}

Workflow builders can be customized the same way. This example changes the loop exit condition for all LoopAgentService builders created by the agentic integration:

package example.micronaut.agentic;

import dev.langchain4j.agentic.workflow.LoopAgentService;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.micronaut.core.annotation.NonNull;
import jakarta.inject.Singleton;

@Singleton
@Requires(env = "agentic-docs")
final class LoopAgentServiceListener implements BeanCreatedEventListener<LoopAgentService<?>> {

    @Override
    public LoopAgentService<?> onCreated(@NonNull BeanCreatedEvent<LoopAgentService<?>> event) {
        LoopAgentService<?> builder = event.getBean();
        builder.exitCondition((scope, iteration) -> iteration >= 3);
        return builder;
    }
}
from dev.langchain4j.agentic.workflow import LoopAgentService
from jakarta.inject import Singleton
from micronaut.context.annotation import Requires
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener


@Singleton
@Requires(env="agentic-docs")
class LoopAgentServiceListener(BeanCreatedEventListener[LoopAgentService]):

    def onCreated(self, event: BeanCreatedEvent[LoopAgentService]) -> LoopAgentService:
        builder = event.getBean()
        builder.exitCondition(lambda scope, iteration: iteration >= 3)
        return builder
package example.micronaut.agentic

import dev.langchain4j.agentic.workflow.LoopAgentService
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@Singleton
@Requires(env = ["agentic-docs"])
internal class LoopAgentServiceListener : BeanCreatedEventListener<LoopAgentService<*>> {

    override fun onCreated(event: BeanCreatedEvent<LoopAgentService<*>>): LoopAgentService<*> {
        val builder = event.bean
        builder.exitCondition { _, iteration -> iteration >= 3 }
        return builder
    }
}
package example.micronaut.agentic

import dev.langchain4j.agentic.workflow.LoopAgentService
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@Singleton
@Requires(env = "agentic-docs")
class LoopAgentServiceListener implements BeanCreatedEventListener<LoopAgentService<?>> {

    @Override
    LoopAgentService<?> onCreated(BeanCreatedEvent<LoopAgentService<?>> event) {
        LoopAgentService<?> builder = event.bean
        builder.exitCondition { scope, iteration -> iteration >= 3 }
        builder
    }
}

The same approach applies to Micronaut-managed workflow services: SequentialAgentService, ParallelAgentService, ParallelMapperService, ConditionalAgentService, and LoopAgentService.

6 Structured Outputs

When an @AiService or @AgenticService method returns a POJO (or a List or Set of POJOs), LangChain4j asks the model for a JSON answer: it derives a JSON schema from the return type reflectively and sends it as the response format, or appends format instructions to the user message when the model does not support JSON schemas.

With Micronaut JSON Schema, the schema is generated at compile time instead, and the generated schema is the one the model receives. Compared with the schema LangChain4j derives:

  • the descriptions come from the Javadoc (or KDoc, or Python docstrings) of the type and its properties, and the validation constraints (format, pattern, minimum, …​) are passed on to the model in the descriptions;

  • no reflection is needed to build the schema, which suits GraalVM native images;

  • the schema only contains the properties of the type, whereas the reflective schema of a class generated by a compiler (for example, the Java class of a Python class) can contain synthetic or internal fields.

Add the JSON Schema annotation processor and the runtime support:

annotationProcessor("io.micronaut.jsonschema:micronaut-json-schema-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.jsonschema</groupId>
        <artifactId>micronaut-json-schema-processor</artifactId>
    </path>
</annotationProcessorPaths>
[tool.pyronaut.dependencies]
build = [
    "io.micronaut.jsonschema:micronaut-json-schema-processor",
]

implementation("io.micronaut.jsonschema:micronaut-json-schema-utils")
<dependency>
    <groupId>io.micronaut.jsonschema</groupId>
    <artifactId>micronaut-json-schema-utils</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.jsonschema:micronaut-json-schema-utils",
]

Then annotate the types returned by your services with io.micronaut.jsonschema.JsonSchema:

A structured output type
package example.structured;

import io.micronaut.jsonschema.JsonSchema;

/**
 * A conference talk recommended to an attendee.
 *
 * @param title The title of the talk
 * @param reason Why the talk matches the interests of the attendee
 * @param score How well the talk matches the interests, from 1 to 5
 */
@JsonSchema // (1)
public record TalkRecommendation(String title, String reason, int score) {
}
A structured output type
from dataclasses import dataclass

from micronaut.jsonschema import JsonSchema
from micronaut.core.annotation import Introspected


@JsonSchema  # (1)
@Introspected
@dataclass
class TalkRecommendation:
    """A conference talk recommended to an attendee."""

    title: str
    """The title of the talk"""

    reason: str
    """Why the talk matches the interests of the attendee"""

    score: int
    """How well the talk matches the interests, from 1 to 5"""
A structured output type
package example.structured

import io.micronaut.jsonschema.JsonSchema

/**
 * A conference talk recommended to an attendee.
 */
@JsonSchema // (1)
data class TalkRecommendation(
    /** The title of the talk */
    var title: String = "",
    /** Why the talk matches the interests of the attendee */
    var reason: String = "",
    /** How well the talk matches the interests, from 1 to 5 */
    var score: Int = 0,
)
A structured output type
package example.structured

import groovy.transform.EqualsAndHashCode
import io.micronaut.jsonschema.JsonSchema

@JsonSchema(description = "A conference talk recommended to an attendee.") // (1)
@EqualsAndHashCode
class TalkRecommendation {
    String title
    String reason
    int score
}
1 Micronaut JSON Schema generates the schema of the type, with its documentation as descriptions.
The Groovy compiler does not keep the Groovydoc of the classes, so set the description with the description member of @JsonSchema.
The class is declared @Introspected so that LangChain4j, which parses the answer of the model with Jackson, can construct it. In Python, do not name the @JsonSchema types in the micronaut.introspection.allow-reflection compiler option (the schema would be generated twice, failing the compilation): the integration does not need the annotation at runtime.
Micronaut JSON Schema describes the properties of a type: the components of a record, the accessors of a bean, or the fields of a class annotated with @Introspected(accessKind = Introspected.AccessKind.FIELD). LangChain4j also reads and writes plain fields, which are not properties for Micronaut. For a class whose state is only held in such fields, the generated schema has no properties: a warning is logged, and the schema LangChain4j derives is sent instead.

No other change is needed: the AI services and agents returning these types send the generated schemas.

An @AiService returning structured outputs
package example.micronaut.aiservice.structured;

import dev.langchain4j.service.SystemMessage;
import example.structured.TalkRecommendation;
import io.micronaut.langchain4j.annotation.AiService;

import java.util.List;

@AiService
public interface TalkAdvisor {

    @SystemMessage("You recommend conference talks to attendees.")
    TalkRecommendation recommend(String interests); // (1)

    @SystemMessage("You recommend conference talks to attendees.")
    List<TalkRecommendation> shortlist(String interests); // (2)
}
An @AiService returning structured outputs
from abc import ABC, abstractmethod

from dev.langchain4j.service import SystemMessage
from micronaut.langchain4j.annotation import AiService

from example.structured.TalkRecommendation import TalkRecommendation


@AiService
class TalkAdvisor(ABC):

    @SystemMessage("You recommend conference talks to attendees.")
    @abstractmethod
    def recommend(self, interests: str) -> TalkRecommendation:  # (1)
        ...

    @SystemMessage("You recommend conference talks to attendees.")
    @abstractmethod
    def shortlist(self, interests: str) -> list[TalkRecommendation]:  # (2)
        ...
An @AiService returning structured outputs
package example.micronaut.aiservice.structured

import example.structured.TalkRecommendation
import dev.langchain4j.service.SystemMessage
import io.micronaut.langchain4j.annotation.AiService

@AiService
interface TalkAdvisor {

    @SystemMessage("You recommend conference talks to attendees.")
    fun recommend(interests: String): TalkRecommendation // (1)

    @SystemMessage("You recommend conference talks to attendees.")
    fun shortlist(interests: String): List<TalkRecommendation> // (2)
}
An @AiService returning structured outputs
package example.micronaut.aiservice.structured

import example.structured.TalkRecommendation
import dev.langchain4j.service.SystemMessage
import io.micronaut.langchain4j.annotation.AiService

@AiService
interface TalkAdvisor {

    @SystemMessage("You recommend conference talks to attendees.")
    TalkRecommendation recommend(String interests) // (1)

    @SystemMessage("You recommend conference talks to attendees.")
    List<TalkRecommendation> shortlist(String interests) // (2)
}
1 The schema generated for TalkRecommendation is sent as the response format.
2 For a list, LangChain4j asks for an object whose values property holds the elements: the generated schema describes the elements.
Checking the schema received by the model
package example.micronaut.aiservice.structured;

import dev.langchain4j.model.chat.request.json.JsonArraySchema;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.model.chat.request.json.JsonSchema;
import example.structured.TalkRecommendation;
import io.micronaut.langchain4j.jsonschema.StructuredOutputSchemaProvider;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest(startApplication = false, environments = "structured-output")
class StructuredOutputTest {

    @Inject
    TalkAdvisor talkAdvisor;

    @Inject
    StructuredOutputChatModel chatModel;

    @Inject
    StructuredOutputSchemaProvider schemaProvider;

    @Test
    void sendsTheGeneratedSchema() {
        TalkRecommendation recommendation = talkAdvisor.recommend("LLMs and Java"); // (1)

        assertEquals(new TalkRecommendation("Structured outputs", "You like LLMs", 5), recommendation);
        JsonSchema schema = chatModel.lastRequest().responseFormat().jsonSchema(); // (2)
        assertEquals("TalkRecommendation", schema.name());
        assertEquals(schemaProvider.findSchema(TalkRecommendation.class).orElseThrow(), schema.rootElement()); // (3)
        JsonObjectSchema root = (JsonObjectSchema) schema.rootElement();
        assertEquals("A conference talk recommended to an attendee.", root.description());
        assertEquals(Set.of("title", "reason", "score"), root.properties().keySet());
        assertEquals("Why the talk matches the interests of the attendee", root.properties().get("reason").description());
    }

    @Test
    void sendsTheGeneratedSchemaOfListElements() {
        List<TalkRecommendation> shortlist = talkAdvisor.shortlist("LLMs and Java");

        assertEquals(1, shortlist.size());
        JsonSchema schema = chatModel.lastRequest().responseFormat().jsonSchema();
        assertEquals("List_of_TalkRecommendation", schema.name());
        JsonArraySchema values = (JsonArraySchema) ((JsonObjectSchema) schema.rootElement()).properties().get("values");
        assertEquals(schemaProvider.findSchema(TalkRecommendation.class).orElseThrow(), values.items());
    }
}
Checking the schema received by the model
from typing import Annotated

from micronaut.langchain4j.jsonschema import StructuredOutputSchemaProvider
from jakarta.inject import Inject
from micronaut.test.extensions.junit5.annotation import MicronautTest
from org.junit.jupiter.api import Test

from example.micronaut.aiservice.structured.StructuredOutputChatModel import StructuredOutputChatModel
from example.micronaut.aiservice.structured.TalkAdvisor import TalkAdvisor
from example.structured.TalkRecommendation import TalkRecommendation


@MicronautTest(startApplication=False, environments=["structured-output"])
class StructuredOutputTest:
    talk_advisor: Annotated[TalkAdvisor, Inject]
    chat_model: Annotated[StructuredOutputChatModel, Inject]
    schema_provider: Annotated[StructuredOutputSchemaProvider, Inject]

    @Test
    def test_sends_the_generated_schema(self):
        recommendation = self.talk_advisor.recommend("LLMs and Java")  # (1)

        assert recommendation.title == "Structured outputs"
        assert recommendation.score == 5
        schema = self.chat_model.last_request().responseFormat().jsonSchema()  # (2)
        assert schema.name() == "TalkRecommendation"
        assert schema.rootElement().equals(self.schema_provider.findSchema(TalkRecommendation).orElseThrow())  # (3)
        root = schema.rootElement()
        assert root.description() == "A conference talk recommended to an attendee."
        assert set(str(name) for name in root.properties().keySet()) == {"title", "reason", "score"}

    @Test
    def test_sends_the_generated_schema_of_list_elements(self):
        shortlist = self.talk_advisor.shortlist("LLMs and Java")

        assert len(shortlist) == 1
        schema = self.chat_model.last_request().responseFormat().jsonSchema()
        assert schema.name() == "List_of_TalkRecommendation"
        values = schema.rootElement().properties().get("values")
        assert values.items().equals(self.schema_provider.findSchema(TalkRecommendation).orElseThrow())
Checking the schema received by the model
package example.micronaut.aiservice.structured

import example.structured.TalkRecommendation
import dev.langchain4j.model.chat.request.json.JsonArraySchema
import dev.langchain4j.model.chat.request.json.JsonObjectSchema
import io.micronaut.langchain4j.jsonschema.StructuredOutputSchemaProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

@MicronautTest(startApplication = false, environments = ["structured-output"])
class StructuredOutputTest {

    @Inject
    lateinit var talkAdvisor: TalkAdvisor

    @Inject
    lateinit var chatModel: StructuredOutputChatModel

    @Inject
    lateinit var schemaProvider: StructuredOutputSchemaProvider

    @Test
    fun sendsTheGeneratedSchema() {
        val recommendation = talkAdvisor.recommend("LLMs and Java") // (1)

        assertEquals(TalkRecommendation("Structured outputs", "You like LLMs", 5), recommendation)
        val schema = chatModel.lastRequest!!.responseFormat().jsonSchema() // (2)
        assertEquals("TalkRecommendation", schema.name())
        assertEquals(schemaProvider.findSchema(TalkRecommendation::class.java).orElseThrow(), schema.rootElement()) // (3)
        val root = schema.rootElement() as JsonObjectSchema
        assertEquals("A conference talk recommended to an attendee.", root.description())
        assertEquals(setOf("title", "reason", "score"), root.properties().keys)
        assertEquals("Why the talk matches the interests of the attendee", root.properties()["reason"]!!.description())
    }

    @Test
    fun sendsTheGeneratedSchemaOfListElements() {
        val shortlist = talkAdvisor.shortlist("LLMs and Java")

        assertEquals(1, shortlist.size)
        val schema = chatModel.lastRequest!!.responseFormat().jsonSchema()
        assertEquals("List_of_TalkRecommendation", schema.name())
        val values = (schema.rootElement() as JsonObjectSchema).properties()["values"] as JsonArraySchema
        assertEquals(schemaProvider.findSchema(TalkRecommendation::class.java).orElseThrow(), values.items())
    }
}
Checking the schema received by the model
package example.micronaut.aiservice.structured

import example.structured.TalkRecommendation
import dev.langchain4j.model.chat.request.json.JsonArraySchema
import dev.langchain4j.model.chat.request.json.JsonObjectSchema
import dev.langchain4j.model.chat.request.json.JsonSchema
import io.micronaut.langchain4j.jsonschema.StructuredOutputSchemaProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals

@MicronautTest(startApplication = false, environments = "structured-output")
class StructuredOutputTest {

    @Inject
    TalkAdvisor talkAdvisor

    @Inject
    StructuredOutputChatModel chatModel

    @Inject
    StructuredOutputSchemaProvider schemaProvider

    @Test
    void sendsTheGeneratedSchema() {
        TalkRecommendation recommendation = talkAdvisor.recommend("LLMs and Java") // (1)

        assertEquals(new TalkRecommendation(title: "Structured outputs", reason: "You like LLMs", score: 5), recommendation)
        JsonSchema schema = chatModel.lastRequest.responseFormat().jsonSchema() // (2)
        assertEquals("TalkRecommendation", schema.name())
        assertEquals(schemaProvider.findSchema(TalkRecommendation).orElseThrow(), schema.rootElement()) // (3)
        JsonObjectSchema root = (JsonObjectSchema) schema.rootElement()
        assertEquals("A conference talk recommended to an attendee.", root.description())
        assertEquals(["title", "reason", "score"] as Set, root.properties().keySet())
    }

    @Test
    void sendsTheGeneratedSchemaOfListElements() {
        List<TalkRecommendation> shortlist = talkAdvisor.shortlist("LLMs and Java")

        assertEquals(1, shortlist.size())
        JsonSchema schema = chatModel.lastRequest.responseFormat().jsonSchema()
        assertEquals("List_of_TalkRecommendation", schema.name())
        JsonArraySchema values = (JsonArraySchema) ((JsonObjectSchema) schema.rootElement()).properties().get("values")
        assertEquals(schemaProvider.findSchema(TalkRecommendation).orElseThrow(), values.items())
    }
}
1 Call the service as usual.
2 The test chat model records the request it receives.
3 The response format carries the schema generated at compile time.

The schema of a type returned directly, in a List or a Set, or wrapped in Result, CompletableFuture or CompletionStage is replaced. References to the schemas of other @JsonSchema types are resolved and inlined, except recursive ones, which are sent as definitions ($defs). LangChain4j still parses the answer and decides whether the response format is sent; streaming methods are unaffected.

To supply the schemas from another source, register a bean of type StructuredOutputSchemaProvider.

Models without JSON schema support

LangChain4j only sends the schema to the chat models that declare the RESPONSE_FORMAT_JSON_SCHEMA capability. For the other models, it appends format instructions derived from the type to the user message, and the generated schema is not used.

Some models accept a JSON schema on each request but do not always declare the capability. Google AI Gemini declares it only when a JSON response format is configured on the model, yet it honors the response format of each request: with the micronaut-langchain4j-googleai-gemini module, the services returning a type with a generated schema send it to Gemini without further configuration. Providers declare such models with a bean of type JsonSchemaResponseFormatSupport.

For other models, set langchain4j.structured-output.force-json-schema-response-format to true to send the schema anyway, or to false to never send it to the models that do not declare the capability:

langchain4j.structured-output.force-json-schema-response-format=true
langchain4j.structured-output.force-json-schema-response-format: true
"langchain4j.structured-output.force-json-schema-response-format" = true
langchain4j.structuredOutput.forceJsonSchemaResponseFormat = true
{
  "langchain4j.structured-output.force-json-schema-response-format" = true
}
{
  "langchain4j.structured-output.force-json-schema-response-format": true
}

Only use it with models that support a JSON schema response format: a model that does not may ignore it or reject the request.

To use the schemas LangChain4j derives instead of the generated ones, set langchain4j.structured-output.enabled to false.

7 Testing

Micronaut LangChain4j includes a small evaluation API for asserting AI responses in tests.

The EvaluationRequest record captures the original user text, optional grounding context, and generated response. An Evaluator consumes that request and returns an EvaluationResult.

Built-in evaluators include:

  • RelevancyEvaluator for checking whether the response answers the user request.

  • FactCheckingEvaluator for checking whether the response is grounded in the supplied context.

When an AI service returns dev.langchain4j.service.Result<T>, you can reuse retrieved sources as evaluation context:

Defining an @AiService that returns Result<String>
package example.micronaut.aiservice.evaluation;

import dev.langchain4j.service.Result;
import dev.langchain4j.service.SystemMessage;
import io.micronaut.context.annotation.Requires;
import io.micronaut.langchain4j.annotation.AiService;

@Requires(property = "spec.name", value = "AiServiceEvaluationExample")
@AiService
public interface EvaluatingFriend {
    @SystemMessage("You are a good friend of mine. Answer using slang.")
    Result<String> chat(String userMessage);
}
Defining an @AiService that returns Result<String>
from abc import ABC, abstractmethod

from dev.langchain4j.service import Result, SystemMessage
from micronaut.context.annotation import Requires
from micronaut.langchain4j.annotation import AiService


@Requires(property="spec.name", value="AiServiceEvaluationExample")
@AiService
class EvaluatingFriend(ABC):

    @SystemMessage("You are a good friend of mine. Answer using slang.")
    @abstractmethod
    def chat(self, user_message: str) -> Result[str]:
        ...
Defining an @AiService that returns Result<String>
package example.micronaut.aiservice.evaluation

import dev.langchain4j.service.Result
import dev.langchain4j.service.SystemMessage
import io.micronaut.context.annotation.Requires
import io.micronaut.langchain4j.annotation.AiService

@Requires(property = "spec.name", value = "AiServiceEvaluationExample")
@AiService
interface EvaluatingFriend {
    @SystemMessage("You are a good friend of mine. Answer using slang.")
    fun chat(userMessage: String): Result<String>
}
Defining an @AiService that returns Result<String>
package example.micronaut.aiservice.evaluation

import dev.langchain4j.service.Result
import dev.langchain4j.service.SystemMessage
import io.micronaut.context.annotation.Requires
import io.micronaut.langchain4j.annotation.AiService

@Requires(property = "spec.name", value = "AiServiceEvaluationExample")
@AiService
interface EvaluatingFriend {
    @SystemMessage("You are a good friend of mine. Answer using slang.")
    Result<String> chat(String userMessage)
}
Evaluating an AI service response
package example.micronaut.aiservice.evaluation;

import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.Result;
import io.micronaut.context.annotation.Property;
import io.micronaut.langchain4j.evaluation.EvaluationRequest;
import io.micronaut.langchain4j.evaluation.EvaluationResult;
import io.micronaut.langchain4j.evaluation.RelevancyEvaluator;
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@Property(name = "spec.name", value = "AiServiceEvaluationExample")
@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AiServiceEvaluationExample implements OllamaTestPropertyProvider {

    @Test
    void evaluatesAiServiceResponse(EvaluatingFriend friend, ChatModel chatModel) {
        String userText = "Reply with exactly: Micronaut is a JVM framework.";
        Result<String> response = friend.chat(userText);

        RelevancyEvaluator evaluator = new RelevancyEvaluator(chatModel);
        EvaluationResult evaluation = evaluator.evaluate(EvaluationRequest.from(userText, response));

        assertNotNull(evaluation);
        assertFalse(evaluation.feedback().isBlank());
    }
}
Evaluating an AI service response
from typing import Annotated

from dev.langchain4j.model.chat import ChatModel
from jakarta.inject import Inject
from micronaut.context.annotation import Property
from micronaut.langchain4j.evaluation import EvaluationRequest, RelevancyEvaluator
from micronaut.test.extensions.junit5.annotation import MicronautTest
from org.junit.jupiter.api import Test

from example.micronaut.aiservice.evaluation.EvaluatingFriend import EvaluatingFriend


@Property(name="spec.name", value="AiServiceEvaluationExample")
@MicronautTest(startApplication=False, environments=["ollama"])
class AiServiceEvaluationExample:
    friend: Annotated[EvaluatingFriend, Inject]
    chat_model: Annotated[ChatModel, Inject]

    @Test
    def evaluates_ai_service_response(self):
        user_text = "Reply with exactly: Micronaut is a JVM framework."
        response = self.friend.chat(user_text)

        evaluator = RelevancyEvaluator(self.chat_model)
        evaluation = evaluator.evaluate(EvaluationRequest.from_(user_text, response))

        assert evaluation is not None
        assert evaluation.feedback().strip() != ""
Evaluating an AI service response
package example.micronaut.aiservice.evaluation

import dev.langchain4j.model.chat.ChatModel
import io.micronaut.context.annotation.Property
import io.micronaut.langchain4j.evaluation.EvaluationRequest
import io.micronaut.langchain4j.evaluation.RelevancyEvaluator
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.testcontainers.junit.jupiter.Testcontainers

@Property(name = "spec.name", value = "AiServiceEvaluationExample")
@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class AiServiceEvaluationExample : OllamaTestPropertyProvider {

    @Test
    fun evaluatesAiServiceResponse(friend: EvaluatingFriend, chatModel: ChatModel) {
        val userText = "Reply with exactly: Micronaut is a JVM framework."
        val response = friend.chat(userText)

        val evaluator = RelevancyEvaluator(chatModel)
        val evaluation = evaluator.evaluate(EvaluationRequest.from(userText, response))

        assertNotNull(evaluation)
        assertFalse(evaluation.feedback().isBlank())
    }
}
Evaluating an AI service response
package example.micronaut.aiservice.evaluation

import dev.langchain4j.model.chat.ChatModel
import dev.langchain4j.service.Result
import io.micronaut.context.annotation.Property
import io.micronaut.langchain4j.evaluation.EvaluationRequest
import io.micronaut.langchain4j.evaluation.EvaluationResult
import io.micronaut.langchain4j.evaluation.RelevancyEvaluator
import io.micronaut.langchain4j.testutils.OllamaTestPropertyProvider
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.testcontainers.junit.jupiter.Testcontainers

import static org.junit.jupiter.api.Assertions.assertFalse
import static org.junit.jupiter.api.Assertions.assertNotNull

@Property(name = "spec.name", value = "AiServiceEvaluationExample")
@Testcontainers(disabledWithoutDocker = true)
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AiServiceEvaluationExample implements OllamaTestPropertyProvider {

    @Test
    void evaluatesAiServiceResponse(EvaluatingFriend friend, ChatModel chatModel) {
        String userText = "Reply with exactly: Micronaut is a JVM framework."
        Result<String> response = friend.chat(userText)

        RelevancyEvaluator evaluator = new RelevancyEvaluator(chatModel)
        EvaluationResult evaluation = evaluator.evaluate(EvaluationRequest.from(userText, response))

        assertNotNull(evaluation)
        assertFalse(evaluation.feedback().isBlank())
    }
}

FactCheckingEvaluator requires non-empty context, which makes it a good fit for RAG-style responses backed by retrieved sources.

8 Tools

Tools allow AI models to request specific actions that extends beyond their built-in capabilities.

package example.micronaut.aiservice.tools;

import dev.langchain4j.agent.tool.Tool;
import jakarta.inject.Singleton;

import java.time.LocalDate;

@Singleton // (1)
public class LegalDocumentTools {
    @Tool("Returns the last time the PRIVACY document was updated") // (2)
    public LocalDate lastUpdatePrivacy() {
        return LocalDate.of(2013, 3, 9); // (3)
    }
}
from dev.langchain4j.agent.tool import Tool
from jakarta.inject import Singleton
from java.time import LocalDate


@Singleton  # (1)
class LegalDocumentTools:
    @Tool("Returns the last time the PRIVACY document was updated")  # (2)
    def lastUpdatePrivacy(self) -> LocalDate:
        return LocalDate.of(2013, 3, 9)  # (3)
package example.micronaut.aiservice.tools

import dev.langchain4j.agent.tool.Tool
import jakarta.inject.Singleton
import java.time.LocalDate

@Singleton // (1)
class LegalDocumentTools {
    @Tool("Returns the last time the PRIVACY document was updated") // (2)
    fun lastUpdatePrivacy(): LocalDate = LocalDate.of(2013, 3, 9) // (3)
}
package example.micronaut.aiservice.tools

import dev.langchain4j.agent.tool.Tool
import groovy.transform.CompileStatic
import jakarta.inject.Singleton

import java.time.LocalDate

@Singleton // (1)
@CompileStatic
class LegalDocumentTools {
    @Tool("Returns the last time the PRIVACY document was updated") // (2)
    LocalDate lastUpdatePrivacy() {
        return LocalDate.of(2013, 3, 9) // (3)
    }
}
1 You should annotate the classes with @Tool methods with @Singleton.
2 The @Tool value specifies the description of the tool. The annotated method returns the date when a company PRIVACY document was last updated. A model would have no way to know the last date when the PRIVACY document was updated. Hence, it is a good candidate for a tool.
3 The dates are harcoded for the purpose of this example, but they could have been retrieved from a database or external API.

You can supply the tools to use to an @AiService:

package example.micronaut.aiservice.tools;

import io.micronaut.langchain4j.annotation.AiService;

@AiService(tools = LegalDocumentTools.class)
public interface CompanyBot {
    String ask(String question);
}
from abc import ABC, abstractmethod

from micronaut.langchain4j.annotation import AiService

from .LegalDocumentTools import LegalDocumentTools


@AiService(tools=[LegalDocumentTools])
class CompanyBot(ABC):
    @abstractmethod
    def ask(self, question: str) -> str:
        ...
package example.micronaut.aiservice.tools

import io.micronaut.langchain4j.annotation.AiService

@AiService(tools = [LegalDocumentTools::class])
interface CompanyBot {
    fun ask(question: String): String
}
package example.micronaut.aiservice.tools

import io.micronaut.langchain4j.annotation.AiService

@AiService(tools = LegalDocumentTools.class)
interface CompanyBot {
    String ask(String question)
}
LangChain4j discovers @Tool methods reflectively on the Java class of the tool bean, so the annotations of Python tool classes and AI services must be copied onto the generated Java classes: declare the @AllowsReflection hint (micronaut.core.annotation) on them, or name their packages in the micronaut.introspection.allow-reflection compiler option (micronautBuild.python.compilerArgs.add("-Amicronaut.introspection.allowReflection=example.micronaut.*") in the Gradle build, as the examples of this guide do). The tool is registered under the method name, which the model sees, so the Python example keeps the Java name lastUpdatePrivacy.

9 Chat Language Models

The following modules provide integration with Langchain4j Language Models.

Each module configures one or more ChatLanguageModel beans, making them available for dependency injection based on configuration.

9.1 ChatModel Example

This example, asks a chat model to generate the list of the top 3 albums of a Jazz musician.

package example.micronaut;

import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import jakarta.inject.Singleton;

import java.util.List;

@Singleton
public class MusicianAssistant {
    private static final SystemMessage SYSTEM_MSG = SystemMessage.from("""
      You are an expert in Jazz music.
      Reply with only the names of the artists, albums, etc.
      Be very concise.
      If a list is given, separate the items with commas.""");

    private final ChatModel model;

    public MusicianAssistant(ChatModel model) { // (1)
        this.model = model;
    }

    public Musician generateTopThreeAlbums(String name) {
        List<ChatMessage> messages = generateTopThreeAlbumsMessages(name);
        ChatResponse albums = model.chat(messages);
        String topThreeAlbums = albums.aiMessage().text();
        return new Musician(name, topThreeAlbums);
    }

    private static List<ChatMessage> generateTopThreeAlbumsMessages(String name) {
        return List.of(SYSTEM_MSG, UserMessage.from(
            String.format("Only list the top 3 albums of %s", name)
        ));
    }
}
from dev.langchain4j.data.message import SystemMessage, UserMessage
from dev.langchain4j.model.chat import ChatModel
from jakarta.inject import Singleton
from java.util import List

from .Musician import Musician

SYSTEM_MSG = SystemMessage.from_("""\
      You are an expert in Jazz music.
      Reply with only the names of the artists, albums, etc.
      Be very concise.
      If a list is given, separate the items with commas.""")


@Singleton
class MusicianAssistant:

    def __init__(self, model: ChatModel):  # (1)
        self.model = model

    def generate_top_three_albums(self, name: str) -> Musician:
        messages = generate_top_three_albums_messages(name)
        albums = self.model.chat(messages)
        top_three_albums = albums.aiMessage().text()
        return Musician(name, top_three_albums)


def generate_top_three_albums_messages(name: str):
    return List.of(SYSTEM_MSG, UserMessage.from_(
        f"Only list the top 3 albums of {name}"
    ))
package example.micronaut

import dev.langchain4j.data.message.ChatMessage
import dev.langchain4j.data.message.SystemMessage
import dev.langchain4j.data.message.UserMessage
import dev.langchain4j.model.chat.ChatModel
import jakarta.inject.Singleton

@Singleton
class MusicianAssistant(private val model: ChatModel) { // (1)
    fun generateTopThreeAlbums(name: String): Musician {
        val messages = generateTopThreeAlbumsMessages(name)
        val albums = model.chat(messages)
        val topThreeAlbums = albums.aiMessage().text()
        return Musician(name, topThreeAlbums)
    }

    private fun generateTopThreeAlbumsMessages(name: String): List<ChatMessage> {
        return listOf(
            SYSTEM_MSG, UserMessage.from(
                String.format("Only list the top 3 albums of %s", name)
            )
        )
    }

    companion object {
        private val SYSTEM_MSG: SystemMessage = SystemMessage.from(
            """
          You are an expert in Jazz music.
          Reply with only the names of the artists, albums, etc.
          Be very concise.
          If a list is given, separate the items with commas.
          """.trimIndent()
        )
    }
}
package example.micronaut

import dev.langchain4j.data.message.ChatMessage
import dev.langchain4j.data.message.SystemMessage
import dev.langchain4j.data.message.UserMessage
import dev.langchain4j.model.chat.ChatModel
import dev.langchain4j.model.chat.response.ChatResponse
import jakarta.inject.Singleton
import groovy.transform.CompileStatic

@CompileStatic
@Singleton
class MusicianAssistant {
    private static final SystemMessage SYSTEM_MSG = SystemMessage.from("""
You are an expert in Jazz music.
Reply with only the names of the artists, albums, etc.
Be very concise.
If a list is given, separate the items with commas.""")
    private final ChatModel model

    MusicianAssistant(ChatModel model) { // (1)
        this.model = model
    }

    Musician generateTopThreeAlbums(String name) {
        List<ChatMessage> messages = generateTopThreeAlbumsMessages(name)
        ChatResponse albums = model.chat(messages);
        String topThreeAlbums = albums.aiMessage().text();
        new Musician(name: name, albums: topThreeAlbums);
    }

    private static List<ChatMessage> generateTopThreeAlbumsMessages(String name) {
        [
                SYSTEM_MSG,
                UserMessage.from(String.format("Only list the top 3 albums of %s", name))
        ]
    }
}
1 Inject via constructor injection a bean of type ChatModel.
package example.micronaut;

public record Musician(String name, String albums) {
}
from dataclasses import dataclass

from micronaut.core.annotation import Introspected


@Introspected
@dataclass(frozen=True)
class Musician:
    name: str
    albums: str
package example.micronaut

data class Musician(val name: String, val albums: String)
package example.micronaut

import groovy.transform.CompileStatic

@CompileStatic
class Musician {
    String name
    String albums
}

You can configure the chat model via configuration.

For example, you may want to configure OpenAI in the main classpath:

src/main/resources/application.properties
micronaut.application.name=micronaut-guide
langchain4j.open-ai.chat-model.log-requests=true
langchain4j.open-ai.chat-model.log-responses=true
langchain4j.open-ai.chat-model.timeout=60s
langchain4j.open-ai.chat-model.temperature=0.3
langchain4j.open-ai.chat-model.model-name=gpt-4.1

And a local SLM (Small Language Model) such as Ollama in the test classpath:

src/test/resources/application-test.properties
langchain4j.open-ai.enabled=false
langchain4j.ollama.model-name=tinyllama
langchain4j.ollama.chat-model.timeout=5m
# Ollama 0.32.10 changed the default from 1.1 to 1.0; without the penalty tinyllama ignores the chat history
langchain4j.ollama.chat-model.repeat-penalty=1.1
langchain4j.ollama.chat-model.log-requests=true
langchain4j.ollama.chat-model.log-responses=true

Moreover, you can also register a bean of type BeanCreatedEventListener to configure the Chat Model builder programmatically if configuration is not enough.

package example.micronaut;

import dev.langchain4j.model.ollama.OllamaChatModel;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import org.jspecify.annotations.NonNull;
import jakarta.inject.Singleton;

@Singleton
class OllamaChatModelBuilderListener
    implements BeanCreatedEventListener<OllamaChatModel.OllamaChatModelBuilder> {
    @Override
    public OllamaChatModel.OllamaChatModelBuilder onCreated(
        @NonNull BeanCreatedEvent<OllamaChatModel.OllamaChatModelBuilder> event) {
        OllamaChatModel.OllamaChatModelBuilder builder = event.getBean();
        builder.temperature(0.0);
        return builder;
    }
}
from dev.langchain4j.model.ollama import OllamaChatModel
from jakarta.inject import Singleton
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener


@Singleton
class OllamaChatModelBuilderListener(
    BeanCreatedEventListener[OllamaChatModel.OllamaChatModelBuilder]
):
    def onCreated(
        self, event: BeanCreatedEvent[OllamaChatModel.OllamaChatModelBuilder]
    ) -> OllamaChatModel.OllamaChatModelBuilder:
        builder = event.getBean()
        builder.temperature(0.0)
        return builder
package example.micronaut

import dev.langchain4j.model.ollama.OllamaChatModel.OllamaChatModelBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import org.jspecify.annotations.NonNull
import jakarta.inject.Singleton

@Singleton
class OllamaChatModelBuilderListener : BeanCreatedEventListener<OllamaChatModelBuilder> {
    override fun onCreated(event: @NonNull BeanCreatedEvent<OllamaChatModelBuilder>): OllamaChatModelBuilder {
        val builder = event.bean
        builder.temperature(0.0)
        return builder
    }
}
package example.micronaut

import dev.langchain4j.model.ollama.OllamaChatModel
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import org.jspecify.annotations.NonNull
import jakarta.inject.Singleton

@Singleton
class OllamaChatModelBuilderListener
    implements BeanCreatedEventListener<OllamaChatModel.OllamaChatModelBuilder> {
    @Override
    OllamaChatModel.OllamaChatModelBuilder onCreated(
        @NonNull BeanCreatedEvent<OllamaChatModel.OllamaChatModelBuilder> event) {
        OllamaChatModel.OllamaChatModelBuilder builder = event.getBean()
        builder.temperature(0.0)
        builder
    }
}

9.2 Chat Memory

Models are stateless by design. Chat memory serves as container for previous messages, helping you maintain context in a conversation, but the model itself is not aware of this memory; it relies on you to include the relevant messages in each request for coherent and contextually relevant responses.

Langchain4J provides an API ChatMemory to help you manage chat memory. You can provide your own implementation or use one of the provided implementations.

The default implementation of ChatMemory, dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore, stores ChatMessage instances in memory.

To use the Redis implementation dev.langchain4j.community.store.memory.chat.redis.RedisChatMemoryStore, add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-redis")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-redis</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-redis",
]

To use the Neo4J implementation dev.langchain4j.community.store.memory.chat.neo4j.Neo4jChatMemoryStore, add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-neo4j")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-neo4j</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-neo4j",
]

To use the Cassandra implementation dev.langchain4j.store.memory.chat.cassandra.CassandraChatMemoryStore, add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-cassandra")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-cassandra</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-cassandra",
]

To use the Oracle implementation dev.langchain4j.store.memory.chat.oracle.OracleChatMemoryStore, add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-oracle")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-oracle</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-oracle",
]

Then configure a JDBC datasource and the chat memory store properties, for example:

datasources.default.dialect=oracle
langchain4j.chat-memory-store.oracle.default.enabled=true
langchain4j.chat-memory-store.oracle.default.table-name=CHAT_MEMORY
langchain4j.chat-memory-store.oracle.default.memory-id-column-name=MEMORY_ID
langchain4j.chat-memory-store.oracle.default.content-column-name=CONTENT
datasources.default.dialect: oracle
langchain4j.chat-memory-store.oracle.default.enabled: true
langchain4j.chat-memory-store.oracle.default.table-name: CHAT_MEMORY
langchain4j.chat-memory-store.oracle.default.memory-id-column-name: MEMORY_ID
langchain4j.chat-memory-store.oracle.default.content-column-name: CONTENT
"datasources.default.dialect" = "oracle"
"langchain4j.chat-memory-store.oracle.default.enabled" = true
"langchain4j.chat-memory-store.oracle.default.table-name" = "CHAT_MEMORY"
"langchain4j.chat-memory-store.oracle.default.memory-id-column-name" = "MEMORY_ID"
"langchain4j.chat-memory-store.oracle.default.content-column-name" = "CONTENT"
datasources.default.dialect = "oracle"
langchain4j.chatMemoryStore.oracle.default.enabled = true
langchain4j.chatMemoryStore.oracle.default.tableName = "CHAT_MEMORY"
langchain4j.chatMemoryStore.oracle.default.memoryIdColumnName = "MEMORY_ID"
langchain4j.chatMemoryStore.oracle.default.contentColumnName = "CONTENT"
{
  "datasources.default.dialect" = "oracle"
  "langchain4j.chat-memory-store.oracle.default.enabled" = true
  "langchain4j.chat-memory-store.oracle.default.table-name" = "CHAT_MEMORY"
  "langchain4j.chat-memory-store.oracle.default.memory-id-column-name" = "MEMORY_ID"
  "langchain4j.chat-memory-store.oracle.default.content-column-name" = "CONTENT"
}
{
  "datasources.default.dialect": "oracle",
  "langchain4j.chat-memory-store.oracle.default.enabled": true,
  "langchain4j.chat-memory-store.oracle.default.table-name": "CHAT_MEMORY",
  "langchain4j.chat-memory-store.oracle.default.memory-id-column-name": "MEMORY_ID",
  "langchain4j.chat-memory-store.oracle.default.content-column-name": "CONTENT"
}

The table must already exist. By default, the expected schema is CHAT_MEMORY(MEMORY_ID, CONTENT).

The segment under oracle (for example default) maps to the datasource name. For multiple datasources, configure multiple entries such as langchain4j.chat-memory-store.oracle.reporting.*.

The following example shows how to use the ChatMemory:

package example.micronaut;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import jakarta.inject.Singleton;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Singleton
public class AssistantWithMemory {
    private final Map<String, ChatMemory> conversations = new ConcurrentHashMap<>();
    private final ChatModel model;
    private final MessageWindowChatMemory.Builder messageWindowChatMemoryBuilder;

    public AssistantWithMemory(MessageWindowChatMemory.Builder messageWindowChatMemoryBuilder,
                               ChatModel model) {
        this.messageWindowChatMemoryBuilder = messageWindowChatMemoryBuilder;
        this.model = model;
    }

    public MemoryIdAndResponse chat(String conversationId, String message) {
        ChatMemory chatMemory = conversations.get(conversationId);
        if (chatMemory == null) {
            throw new IllegalArgumentException("Unknown conversation: " + conversationId);
        }
        chatMemory.add(UserMessage.from(message));
        ChatResponse chatResponse = model.chat(chatMemory.messages());
        AiMessage answer = chatResponse.aiMessage();
        chatMemory.add(answer);
        return new MemoryIdAndResponse(conversationId, answer.text());
    }

    public MemoryIdAndResponse chat(String message) {
        String conversationId = startConversation();
        return chat(conversationId, message);
    }

    private String startConversation() {
        String memoryId = generateChatMemoryId();
        ChatMemory chatMemory = generateChatMemory(memoryId);
        conversations.putIfAbsent(memoryId, chatMemory);
        return memoryId;
    }

    private String generateChatMemoryId() {
        return UUID.randomUUID().toString();
    }

    private ChatMemory generateChatMemory(String memoryId) {
        return messageWindowChatMemoryBuilder
            .id(memoryId)
            .build();
    }
}
from dev.langchain4j.data.message import UserMessage
from dev.langchain4j.memory.chat import MessageWindowChatMemory
from dev.langchain4j.model.chat import ChatModel
from jakarta.inject import Singleton
from java.util import UUID
from java.util.concurrent import ConcurrentHashMap

from .MemoryIdAndResponse import MemoryIdAndResponse


@Singleton
class AssistantWithMemory:

    def __init__(self, message_window_chat_memory_builder: MessageWindowChatMemory.Builder,
                 model: ChatModel):
        self.conversations = ConcurrentHashMap()
        self.message_window_chat_memory_builder = message_window_chat_memory_builder
        self.model = model

    def chat(self, message: str, conversation_id: str | None = None) -> MemoryIdAndResponse:
        if conversation_id is None:
            conversation_id = self.start_conversation()
        chat_memory = self.conversations.get(conversation_id)
        if chat_memory is None:
            raise ValueError(f"Unknown conversation: {conversation_id}")
        chat_memory.add(UserMessage.from_(message))
        chat_response = self.model.chat(chat_memory.messages())
        answer = chat_response.aiMessage()
        chat_memory.add(answer)
        return MemoryIdAndResponse(conversation_id, answer.text())

    def start_conversation(self) -> str:
        memory_id = self.generate_chat_memory_id()
        chat_memory = self.generate_chat_memory(memory_id)
        self.conversations.putIfAbsent(memory_id, chat_memory)
        return memory_id

    def generate_chat_memory_id(self) -> str:
        return UUID.randomUUID().toString()

    def generate_chat_memory(self, memory_id: str):
        return (self.message_window_chat_memory_builder
                .id(memory_id)
                .build())
package example.micronaut

import dev.langchain4j.data.message.UserMessage
import dev.langchain4j.memory.ChatMemory
import dev.langchain4j.memory.chat.MessageWindowChatMemory
import dev.langchain4j.model.chat.ChatModel
import jakarta.inject.Singleton
import java.util.*
import java.util.concurrent.ConcurrentHashMap

@Singleton
class AssistantWithMemory(
    val messageWindowChatMemoryBuilder: MessageWindowChatMemory.Builder,
    val model: ChatModel) {
    private val conversations: MutableMap<String, ChatMemory> = ConcurrentHashMap<String, ChatMemory>()

    fun chat(conversationId: String, message: String): MemoryIdAndResponse {
        val chatMemory = requireNotNull(this.conversations[conversationId]) {
            "Unknown conversation: $conversationId"
        }
        chatMemory.add(UserMessage.from(message))
        val chatResponse = model.chat(chatMemory.messages())
        val aiMessage = chatResponse.aiMessage()
        chatMemory.add(aiMessage)
        return MemoryIdAndResponse(conversationId, aiMessage.text())
    }

    fun chat(message: String): MemoryIdAndResponse {
        val conversationId = startConversation()
        return chat(conversationId, message)
    }

    private fun startConversation(): String {
        val memoryId = generateChatMemoryId()
        val chatMemory = generateChatMemory(memoryId)
        conversations.putIfAbsent(memoryId, chatMemory)
        return memoryId
    }

    private fun generateChatMemoryId(): String {
        return UUID.randomUUID().toString()
    }

    private fun generateChatMemory(memoryId: String): ChatMemory {
        return messageWindowChatMemoryBuilder
            .id(memoryId)
            .build()
    }
}
package example.micronaut

import dev.langchain4j.data.message.AiMessage
import dev.langchain4j.data.message.UserMessage
import dev.langchain4j.memory.ChatMemory
import dev.langchain4j.memory.chat.MessageWindowChatMemory
import dev.langchain4j.model.chat.ChatModel
import dev.langchain4j.model.chat.response.ChatResponse
import jakarta.inject.Singleton

import java.util.concurrent.ConcurrentHashMap

@Singleton
class AssistantWithMemory {
    private final Map<String, ChatMemory> conversations = new ConcurrentHashMap<>()
    private final MessageWindowChatMemory.Builder messageWindowChatMemoryBuilder
    private final ChatModel model

    AssistantWithMemory(MessageWindowChatMemory.Builder messageWindowChatMemoryBuilder,
                        ChatModel model) {
        this.model = model
        this.messageWindowChatMemoryBuilder = messageWindowChatMemoryBuilder
    }

    private String startConversation() {
        String memoryId = generateChatMemoryId()
        ChatMemory chatMemory = generateChatMemory(memoryId)
        conversations.putIfAbsent(memoryId, chatMemory)
        memoryId
    }

    private String generateChatMemoryId() {
        UUID.randomUUID().toString()
    }

    private ChatMemory generateChatMemory(String memoryId) {
        messageWindowChatMemoryBuilder
                .id(memoryId)
                .build()
    }

    MemoryIdAndResponse chat(String memoryId, String message) {
        ChatMemory chatMemory = conversations.get(memoryId)
        if (chatMemory == null) {
            throw new IllegalArgumentException("Unknown conversation: " + memoryId)
        }
        chatMemory.add(UserMessage.from(message))
        ChatResponse chatResponse = model.chat(chatMemory.messages())
        AiMessage aiMessage = chatResponse.aiMessage()
        chatMemory.add(aiMessage)
        new MemoryIdAndResponse(memoryId: memoryId, response: aiMessage.text())
    }

    MemoryIdAndResponse chat(String message) {
        String conversationId = startConversation()
        chat(conversationId, message)
    }
}

You could invoke the previous class as illustrated in the following test:

    @Test
    void chatWithMemory(AssistantWithMemory assistant) {
        MemoryIdAndResponse johnConversation = assistant.chat("Let me introduce myself. My name is John");
        String johnConversationId = johnConversation.memoryId();
        assertNotNull(johnConversationId);
        MemoryIdAndResponse aegonConversation = assistant.chat("Let me introduce myself. My name is Dan");
        String aegonConversationId = aegonConversation.memoryId();
        assertNotNull(aegonConversationId);
        MemoryIdAndResponse answer = assistant.chat(johnConversationId, "What's my name?");
        assertTrue(answer.response().toLowerCase().contains("john"), answer.response());
        answer = assistant.chat(aegonConversationId, "What's my name?");
        assertTrue(answer.response().toLowerCase().contains("dan"), answer.response());
    }
    @Test
    def chat_with_memory(self):
        john_conversation = self.assistant.chat("Let me introduce myself. My name is John")
        john_conversation_id = john_conversation.memory_id
        assert john_conversation_id is not None
        aegon_conversation = self.assistant.chat("Let me introduce myself. My name is Dan")
        aegon_conversation_id = aegon_conversation.memory_id
        assert aegon_conversation_id is not None
        answer = self.assistant.chat("What's my name?", john_conversation_id)
        assert "john" in answer.response.lower(), answer.response
        answer = self.assistant.chat("What's my name?", aegon_conversation_id)
        assert "dan" in answer.response.lower(), answer.response
    @Test
    fun chatWithMemory(assistant: AssistantWithMemory) {
        val johnConversation = assistant.chat("Let me introduce myself. My name is John")
        val johnConversationId = johnConversation.memoryId
        assertNotNull(johnConversationId)
        val aegonConversation = assistant.chat("Let me introduce myself. My name is Dan")
        val aegonConversationId = aegonConversation.memoryId
        assertNotNull(aegonConversationId)
        var answer = assistant.chat(johnConversationId, "What's my name?")
        assertTrue(answer.response.lowercase().contains("john"), answer.response)
        answer = assistant.chat(aegonConversationId, "What's my name?")
        assertTrue(answer.response.lowercase().contains("dan"), answer.response)
    }
    @Test
    void chatWithMemory(AssistantWithMemory assistant) {
        MemoryIdAndResponse johnConversation = assistant.chat("Let me introduce myself. My name is John")
        String johnConversationId = johnConversation.memoryId
        assertNotNull(johnConversationId)
        MemoryIdAndResponse aegonConversation = assistant.chat("Let me introduce myself. My name is Dan")
        String aegonConversationId = aegonConversation.memoryId
        assertNotNull(aegonConversationId)
        MemoryIdAndResponse answer = assistant.chat(johnConversationId, "What's my name?")
        assertTrue(answer.response.toLowerCase().contains("john"), answer.response)
        answer = assistant.chat(aegonConversationId, "What's my name?")
        assertTrue(answer.response.toLowerCase().contains("dan"), answer.response)
    }

9.3 Anthropic

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-anthropic")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-anthropic</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-anthropic",
]

Then add the necessary configuration.

Example Configuration
langchain4j.anthropic.api-key=YOUR_KEY
langchain4j.anthropic.api-key: YOUR_KEY
"langchain4j.anthropic.api-key" = "YOUR_KEY"
langchain4j.anthropic.apiKey = "YOUR_KEY"
{
  "langchain4j.anthropic.api-key" = "YOUR_KEY"
}
{
  "langchain4j.anthropic.api-key": "YOUR_KEY"
}

9.4 Azure

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-azure")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-azure</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-azure",
]

Then add the necessary configuration.

Example Configuration
langchain4j.azure-open-ai.api-key=YOUR_KEY
langchain4j.azure-open-ai.endpoint=YOUR_ENDPOINT
langchain4j.azure-open-ai.api-key: YOUR_KEY
langchain4j.azure-open-ai.endpoint: YOUR_ENDPOINT
"langchain4j.azure-open-ai.api-key" = "YOUR_KEY"
"langchain4j.azure-open-ai.endpoint" = "YOUR_ENDPOINT"
langchain4j.azureOpenAi.apiKey = "YOUR_KEY"
langchain4j.azureOpenAi.endpoint = "YOUR_ENDPOINT"
{
  "langchain4j.azure-open-ai.api-key" = "YOUR_KEY"
  "langchain4j.azure-open-ai.endpoint" = "YOUR_ENDPOINT"
}
{
  "langchain4j.azure-open-ai.api-key": "YOUR_KEY",
  "langchain4j.azure-open-ai.endpoint": "YOUR_ENDPOINT"
}

You will additionally need to define a bean of type TokenCredentials.

One way to do this is to include the Azure SDK module.

9.5 Bedrock

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-bedrock")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-bedrock</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-bedrock",
]

Then add the necessary configuration.

Example Configuration
langchain4j.bedrock-llama.api-key=YOUR_KEY
langchain4j.bedrock-llama.api-key: YOUR_KEY
"langchain4j.bedrock-llama.api-key" = "YOUR_KEY"
langchain4j.bedrockLlama.apiKey = "YOUR_KEY"
{
  "langchain4j.bedrock-llama.api-key" = "YOUR_KEY"
}
{
  "langchain4j.bedrock-llama.api-key": "YOUR_KEY"
}

You will additionally need to define a bean of type AwsCredentialsProvider.

One way to do this is to include the AWS SDK module.

9.6 HuggingFace

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-hugging-face")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-hugging-face</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-hugging-face",
]

Then add the necessary configuration.

Example Configuration
langchain4j.hugging-face.access-token=YOUR_ACCESS_TOKEN
langchain4j.hugging-face.access-token: YOUR_ACCESS_TOKEN
"langchain4j.hugging-face.access-token" = "YOUR_ACCESS_TOKEN"
langchain4j.huggingFace.accessToken = "YOUR_ACCESS_TOKEN"
{
  "langchain4j.hugging-face.access-token" = "YOUR_ACCESS_TOKEN"
}
{
  "langchain4j.hugging-face.access-token": "YOUR_ACCESS_TOKEN"
}

9.7 MistralAi

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-mistralai")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-mistralai</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-mistralai",
]

Then add the necessary configuration.

Example Configuration
langchain4j.mistral-ai.api-key=YOUR_KEY
langchain4j.mistral-ai.api-key: YOUR_KEY
"langchain4j.mistral-ai.api-key" = "YOUR_KEY"
langchain4j.mistralAi.apiKey = "YOUR_KEY"
{
  "langchain4j.mistral-ai.api-key" = "YOUR_KEY"
}
{
  "langchain4j.mistral-ai.api-key": "YOUR_KEY"
}

9.8 Ollama

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-ollama")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-ollama</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-ollama",
]

Then add the necessary configuration.

Example Configuration
langchain4j.ollama.base-url=YOUR_URL
langchain4j.ollama.base-url: YOUR_URL
"langchain4j.ollama.base-url" = "YOUR_URL"
langchain4j.ollama.baseUrl = "YOUR_URL"
{
  "langchain4j.ollama.base-url" = "YOUR_URL"
}
{
  "langchain4j.ollama.base-url": "YOUR_URL"
}

9.9 Oracle Cloud GenAI

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-oci-genai")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-oci-genai</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-oci-genai",
]

Setup a supported OCI authentication method.

Then add the necessary configuration to configure a chat model.

Example Configuration
langchain4j.oci-gen-ai.chat-model.model-name=orca-mini
langchain4j.oci-gen-ai.compartment-id=your-compartment
langchain4j.oci-gen-ai.chat-model.model-name: orca-mini
langchain4j.oci-gen-ai.compartment-id: your-compartment
"langchain4j.oci-gen-ai.chat-model.model-name" = "orca-mini"
"langchain4j.oci-gen-ai.compartment-id" = "your-compartment"
langchain4j.ociGenAi.chatModel.modelName = "orca-mini"
langchain4j.ociGenAi.compartmentId = "your-compartment"
{
  "langchain4j.oci-gen-ai.chat-model.model-name" = "orca-mini"
  "langchain4j.oci-gen-ai.compartment-id" = "your-compartment"
}
{
  "langchain4j.oci-gen-ai.chat-model.model-name": "orca-mini",
  "langchain4j.oci-gen-ai.compartment-id": "your-compartment"
}

9.10 OpenAi

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-openai")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-openai</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-openai",
]

Provider modules use Micronaut’s HTTP client API and exclude LangChain4j’s JDK HTTP client. Add a concrete Micronaut HTTP client implementation to your application, for example the default Netty implementation:

runtimeOnly("io.micronaut:micronaut-http-client")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-http-client</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut:micronaut-http-client",
]

For tests that instantiate OpenAI models, add the same implementation to the test runtime classpath:

testRuntimeOnly("io.micronaut:micronaut-http-client")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-http-client</artifactId>
    <scope>test</scope>
</dependency>
[tool.pyronaut.dependencies]
test = [
    "io.micronaut:micronaut-http-client",
]

Then add the necessary configuration.

Example Configuration
langchain4j.open-ai.api-key=YOUR_KEY
langchain4j.open-ai.api-key: YOUR_KEY
"langchain4j.open-ai.api-key" = "YOUR_KEY"
langchain4j.openAi.apiKey = "YOUR_KEY"
{
  "langchain4j.open-ai.api-key" = "YOUR_KEY"
}
{
  "langchain4j.open-ai.api-key": "YOUR_KEY"
}

9.11 Google AI Gemini

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-googleai-gemini")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-googleai-gemini</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-googleai-gemini",
]

Then add the necessary configuration.

Example Configuration
langchain4j.google-ai-gemini.api-key=YOUR_API_KEY
langchain4j.google-ai-gemini.api-key: YOUR_API_KEY
"langchain4j.google-ai-gemini.api-key" = "YOUR_API_KEY"
langchain4j.googleAiGemini.apiKey = "YOUR_API_KEY"
{
  "langchain4j.google-ai-gemini.api-key" = "YOUR_API_KEY"
}
{
  "langchain4j.google-ai-gemini.api-key": "YOUR_API_KEY"
}

9.12 VertexAi

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-vertexai")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-vertexai</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-vertexai",
]

Then add the necessary configuration.

To provide explicit Google Cloud credentials, register a GoogleCredentials bean. When no such bean is present, the Vertex AI client uses Application Default Credentials.

Example Configuration
langchain4j.vertex-ai.endpoint=YOUR_ENDPOINT
langchain4j.vertex-ai.model-name=YOUR_MODEL
langchain4j.vertex-ai.project=YOUR_PROJECT
langchain4j.vertex-ai.location=YOUR_LOCATION
langchain4j.vertex-ai.publisher=YOUR_PUBLISHER
langchain4j.vertex-ai.endpoint: YOUR_ENDPOINT
langchain4j.vertex-ai.model-name: YOUR_MODEL
langchain4j.vertex-ai.project: YOUR_PROJECT
langchain4j.vertex-ai.location: YOUR_LOCATION
langchain4j.vertex-ai.publisher: YOUR_PUBLISHER
"langchain4j.vertex-ai.endpoint" = "YOUR_ENDPOINT"
"langchain4j.vertex-ai.model-name" = "YOUR_MODEL"
"langchain4j.vertex-ai.project" = "YOUR_PROJECT"
"langchain4j.vertex-ai.location" = "YOUR_LOCATION"
"langchain4j.vertex-ai.publisher" = "YOUR_PUBLISHER"
langchain4j.vertexAi.endpoint = "YOUR_ENDPOINT"
langchain4j.vertexAi.modelName = "YOUR_MODEL"
langchain4j.vertexAi.project = "YOUR_PROJECT"
langchain4j.vertexAi.location = "YOUR_LOCATION"
langchain4j.vertexAi.publisher = "YOUR_PUBLISHER"
{
  "langchain4j.vertex-ai.endpoint" = "YOUR_ENDPOINT"
  "langchain4j.vertex-ai.model-name" = "YOUR_MODEL"
  "langchain4j.vertex-ai.project" = "YOUR_PROJECT"
  "langchain4j.vertex-ai.location" = "YOUR_LOCATION"
  "langchain4j.vertex-ai.publisher" = "YOUR_PUBLISHER"
}
{
  "langchain4j.vertex-ai.endpoint": "YOUR_ENDPOINT",
  "langchain4j.vertex-ai.model-name": "YOUR_MODEL",
  "langchain4j.vertex-ai.project": "YOUR_PROJECT",
  "langchain4j.vertex-ai.location": "YOUR_LOCATION",
  "langchain4j.vertex-ai.publisher": "YOUR_PUBLISHER"
}

9.13 VertexAi Gemini

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-vertexai-gemini")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-vertexai-gemini</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-vertexai-gemini",
]

Then add the necessary configuration.

To provide explicit Google Cloud credentials, register a GoogleCredentials bean. When no such bean is present, the Vertex AI Gemini client uses Application Default Credentials.

Example Configuration
langchain4j.vertex-ai-gemini.model-name=YOUR_MODEL
langchain4j.vertex-ai-gemini.project=YOUR_PROJECT
langchain4j.vertex-ai-gemini.location=YOUR_LOCATION
langchain4j.vertex-ai-gemini.model-name: YOUR_MODEL
langchain4j.vertex-ai-gemini.project: YOUR_PROJECT
langchain4j.vertex-ai-gemini.location: YOUR_LOCATION
"langchain4j.vertex-ai-gemini.model-name" = "YOUR_MODEL"
"langchain4j.vertex-ai-gemini.project" = "YOUR_PROJECT"
"langchain4j.vertex-ai-gemini.location" = "YOUR_LOCATION"
langchain4j.vertexAiGemini.modelName = "YOUR_MODEL"
langchain4j.vertexAiGemini.project = "YOUR_PROJECT"
langchain4j.vertexAiGemini.location = "YOUR_LOCATION"
{
  "langchain4j.vertex-ai-gemini.model-name" = "YOUR_MODEL"
  "langchain4j.vertex-ai-gemini.project" = "YOUR_PROJECT"
  "langchain4j.vertex-ai-gemini.location" = "YOUR_LOCATION"
}
{
  "langchain4j.vertex-ai-gemini.model-name": "YOUR_MODEL",
  "langchain4j.vertex-ai-gemini.project": "YOUR_PROJECT",
  "langchain4j.vertex-ai-gemini.location": "YOUR_LOCATION"
}

10 Embedding Stores

10.1 In-Memory

An in-memory embedding store is enabled by default, set the following property langchain4j.in-memory.embedding-store.enabled with value false to disable it.

The store is not attached to AI services automatically. See the Retrieval Augmented Generation section of the AI Service guide to declare a ContentRetriever bean.

10.2 Chroma

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-chroma")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-chroma</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-chroma",
]

Example Configuration
langchain4j.chroma.embedding-store.base-url=http://localhost:8000
langchain4j.chroma.embedding-store.collection-name=documents
langchain4j.chroma.embedding-store.api-version=V2
langchain4j.chroma.embedding-store.base-url: http://localhost:8000
langchain4j.chroma.embedding-store.collection-name: documents
langchain4j.chroma.embedding-store.api-version: V2
"langchain4j.chroma.embedding-store.base-url" = "http://localhost:8000"
"langchain4j.chroma.embedding-store.collection-name" = "documents"
"langchain4j.chroma.embedding-store.api-version" = "V2"
langchain4j.chroma.embeddingStore.baseUrl = "http://localhost:8000"
langchain4j.chroma.embeddingStore.collectionName = "documents"
langchain4j.chroma.embeddingStore.apiVersion = "V2"
{
  "langchain4j.chroma.embedding-store.base-url" = "http://localhost:8000"
  "langchain4j.chroma.embedding-store.collection-name" = "documents"
  "langchain4j.chroma.embedding-store.api-version" = "V2"
}
{
  "langchain4j.chroma.embedding-store.base-url": "http://localhost:8000",
  "langchain4j.chroma.embedding-store.collection-name": "documents",
  "langchain4j.chroma.embedding-store.api-version": "V2"
}

10.3 Elastic Search

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-elasticsearch")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-elasticsearch</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-elasticsearch",
]

Example Configuration
elasticsearch.httpHosts=http://localhost:9200,http://127.0.0.2:9200
langchain4j.elasticsearch.embedding-stores.default.dimension=384
elasticsearch.httpHosts: "http://localhost:9200,http://127.0.0.2:9200"
langchain4j.elasticsearch.embedding-stores.default.dimension: 384
"elasticsearch.httpHosts" = "http://localhost:9200,http://127.0.0.2:9200"
"langchain4j.elasticsearch.embedding-stores.default.dimension" = 384
elasticsearch.httpHosts = "http://localhost:9200,http://127.0.0.2:9200"
langchain4j.elasticsearch.embeddingStores.default.dimension = 384
{
  "elasticsearch.httpHosts" = "http://localhost:9200,http://127.0.0.2:9200"
  "langchain4j.elasticsearch.embedding-stores.default.dimension" = 384
}
{
  "elasticsearch.httpHosts": "http://localhost:9200,http://127.0.0.2:9200",
  "langchain4j.elasticsearch.embedding-stores.default.dimension": 384
}

10.4 MongoDB

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-mongodb-atlas")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-mongodb-atlas</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-mongodb-atlas",
]

Configuring a MongoDB server
mongodb.servers.default.uri: mongodb://username:password@localhost:27017/databaseName
Example Configuration
langchain4j.mongodb-atlas.embedding-stores.default.database-name=testdb
langchain4j.mongodb-atlas.embedding-stores.default.collection-name=testcol
langchain4j.mongodb-atlas.embedding-stores.default.index-name=testindex
langchain4j.mongodb-atlas.embedding-stores.default.database-name: testdb
langchain4j.mongodb-atlas.embedding-stores.default.collection-name: testcol
langchain4j.mongodb-atlas.embedding-stores.default.index-name: testindex
"langchain4j.mongodb-atlas.embedding-stores.default.database-name" = "testdb"
"langchain4j.mongodb-atlas.embedding-stores.default.collection-name" = "testcol"
"langchain4j.mongodb-atlas.embedding-stores.default.index-name" = "testindex"
langchain4j.mongodbAtlas.embeddingStores.default.databaseName = "testdb"
langchain4j.mongodbAtlas.embeddingStores.default.collectionName = "testcol"
langchain4j.mongodbAtlas.embeddingStores.default.indexName = "testindex"
{
  "langchain4j.mongodb-atlas.embedding-stores.default.database-name" = "testdb"
  "langchain4j.mongodb-atlas.embedding-stores.default.collection-name" = "testcol"
  "langchain4j.mongodb-atlas.embedding-stores.default.index-name" = "testindex"
}
{
  "langchain4j.mongodb-atlas.embedding-stores.default.database-name": "testdb",
  "langchain4j.mongodb-atlas.embedding-stores.default.collection-name": "testcol",
  "langchain4j.mongodb-atlas.embedding-stores.default.index-name": "testindex"
}

10.5 Neo4j

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-neo4j")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-neo4j</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-neo4j",
]

Example Configuration
neo4j.uri=bolt://localhost
langchain4j.neo4j.embedding-stores.default.dimension=384
neo4j.uri: bolt://localhost
langchain4j.neo4j.embedding-stores.default.dimension: 384
"neo4j.uri" = "bolt://localhost"
"langchain4j.neo4j.embedding-stores.default.dimension" = 384
neo4j.uri = "bolt://localhost"
langchain4j.neo4j.embeddingStores.default.dimension = 384
{
  "neo4j.uri" = "bolt://localhost"
  "langchain4j.neo4j.embedding-stores.default.dimension" = 384
}
{
  "neo4j.uri": "bolt://localhost",
  "langchain4j.neo4j.embedding-stores.default.dimension": 384
}

10.6 Oracle

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-oracle")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-oracle</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-oracle",
]

Then add one of the supported JDBC connection pools, for example Hikari:

runtimeOnly("io.micronaut.sql:micronaut-jdbc-hikari")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-hikari</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-hikari",
]

Example Configuration
datasources.default.dialect=oracle
langchain4j.oracle.embedding-stores.default.table=test
langchain4j.oracle.embedding-stores.default.table.create-option=create_if_not_exists
datasources.default.dialect: oracle
langchain4j.oracle.embedding-stores.default.table: test
langchain4j.oracle.embedding-stores.default.table.create-option: create_if_not_exists
"datasources.default.dialect" = "oracle"
"langchain4j.oracle.embedding-stores.default.table" = "test"
"langchain4j.oracle.embedding-stores.default.table.create-option" = "create_if_not_exists"
datasources.default.dialect = "oracle"
langchain4j.oracle.embeddingStores.default.table = "test"
langchain4j.oracle.embeddingStores.default.table.createOption = "create_if_not_exists"
{
  "datasources.default.dialect" = "oracle"
  "langchain4j.oracle.embedding-stores.default.table" = "test"
  "langchain4j.oracle.embedding-stores.default.table.create-option" = "create_if_not_exists"
}
{
  "datasources.default.dialect": "oracle",
  "langchain4j.oracle.embedding-stores.default.table": "test",
  "langchain4j.oracle.embedding-stores.default.table.create-option": "create_if_not_exists"
}

10.7 Open Search

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-opensearch")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-opensearch</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-opensearch",
]

Example Configuration
micronaut.opensearch.rest-client.http-hosts=http://localhost:9200,http://127.0.0.2:9200
langchain4j.opensearch.embedding-stores.default.dimension=384
micronaut.opensearch.rest-client.http-hosts: "http://localhost:9200,http://127.0.0.2:9200"
langchain4j.opensearch.embedding-stores.default.dimension: 384
"micronaut.opensearch.rest-client.http-hosts" = "http://localhost:9200,http://127.0.0.2:9200"
"langchain4j.opensearch.embedding-stores.default.dimension" = 384
micronaut.opensearch.restClient.httpHosts = "http://localhost:9200,http://127.0.0.2:9200"
langchain4j.opensearch.embeddingStores.default.dimension = 384
{
  "micronaut.opensearch.rest-client.http-hosts" = "http://localhost:9200,http://127.0.0.2:9200"
  "langchain4j.opensearch.embedding-stores.default.dimension" = 384
}
{
  "micronaut.opensearch.rest-client.http-hosts": "http://localhost:9200,http://127.0.0.2:9200",
  "langchain4j.opensearch.embedding-stores.default.dimension": 384
}

10.8 PGVector

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-pgvector")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-pgvector</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-pgvector",
]

Then add one of the supported JDBC connection pools, for example Hikari:

runtimeOnly("io.micronaut.sql:micronaut-jdbc-hikari")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-hikari</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-hikari",
]

Example Configuration
datasources.default.dialect=postgres
langchain4j.pgvector.embedding-stores.default.table=mytable
langchain4j.pgvector.embedding-stores.default.dimension=384
test-resources.containers.postgres.image-name=pgvector/pgvector:pg16
datasources.default.dialect: postgres
langchain4j.pgvector.embedding-stores.default.table: "mytable"
langchain4j.pgvector.embedding-stores.default.dimension: 384

# Add this if you plan to use testresources
test-resources.containers.postgres.image-name: pgvector/pgvector:pg16
"datasources.default.dialect" = "postgres"
"langchain4j.pgvector.embedding-stores.default.table" = "mytable"
"langchain4j.pgvector.embedding-stores.default.dimension" = 384
"test-resources.containers.postgres.image-name" = "pgvector/pgvector:pg16"
datasources.default.dialect = "postgres"
langchain4j.pgvector.embeddingStores.default.table = "mytable"
langchain4j.pgvector.embeddingStores.default.dimension = 384
testResources.containers.postgres.imageName = "pgvector/pgvector:pg16"
{
  "datasources.default.dialect" = "postgres"
  "langchain4j.pgvector.embedding-stores.default.table" = "mytable"
  "langchain4j.pgvector.embedding-stores.default.dimension" = 384
  "test-resources.containers.postgres.image-name" = "pgvector/pgvector:pg16"
}
{
  "datasources.default.dialect": "postgres",
  "langchain4j.pgvector.embedding-stores.default.table": "mytable",
  "langchain4j.pgvector.embedding-stores.default.dimension": 384,
  "test-resources.containers.postgres.image-name": "pgvector/pgvector:pg16"
}

10.9 Redis

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-redis")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-redis</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-redis",
]

Example Configuration
langchain4j.redis.embedding-store.host=localhost
langchain4j.redis.embedding-store.port=6379
langchain4j.redis.embedding-stores.default.dimension=384
langchain4j.redis.embedding-store.host: localhost
langchain4j.redis.embedding-store.port: 6379
langchain4j.redis.embedding-stores.default.dimension: 384
"langchain4j.redis.embedding-store.host" = "localhost"
"langchain4j.redis.embedding-store.port" = 6379
"langchain4j.redis.embedding-stores.default.dimension" = 384
langchain4j.redis.embeddingStore.host = "localhost"
langchain4j.redis.embeddingStore.port = 6379
langchain4j.redis.embeddingStores.default.dimension = 384
{
  "langchain4j.redis.embedding-store.host" = "localhost"
  "langchain4j.redis.embedding-store.port" = 6379
  "langchain4j.redis.embedding-stores.default.dimension" = 384
}
{
  "langchain4j.redis.embedding-store.host": "localhost",
  "langchain4j.redis.embedding-store.port": 6379,
  "langchain4j.redis.embedding-stores.default.dimension": 384
}

10.10 Qdrant

Add the following dependency:

implementation("io.micronaut.langchain4j:micronaut-langchain4j-store-qdrant")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-store-qdrant</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.langchain4j:micronaut-langchain4j-store-qdrant",
]

To use Testcontainers & Test Resources add the following dependency:

testResourcesService("io.micronaut.langchain4j:micronaut-langchain4j-qdrant-testresource")
<dependency>
    <groupId>io.micronaut.langchain4j</groupId>
    <artifactId>micronaut-langchain4j-qdrant-testresource</artifactId>
    <scope>testResourcesService</scope>
</dependency>
[tool.pyronaut.dependencies]
test = [
    "io.micronaut.langchain4j:micronaut-langchain4j-qdrant-testresource",
]

Example Configuration
langchain4j.qdrant.embedding-store.host=localhost
langchain4j.qdrant.embedding-store.port=6334
langchain4j.qdrant.embedding-store.collection-name=mycollection
# Omitt the following 2 properties if you use Test resources
langchain4j.qdrant.embedding-store.host: localhost
langchain4j.qdrant.embedding-store.port: 6334

# Minimal configuration required for Test resources
langchain4j.qdrant.embedding-store.collection-name: mycollection
"langchain4j.qdrant.embedding-store.host" = "localhost"
"langchain4j.qdrant.embedding-store.port" = 6334
"langchain4j.qdrant.embedding-store.collection-name" = "mycollection"
langchain4j.qdrant.embeddingStore.host = "localhost"
langchain4j.qdrant.embeddingStore.port = 6334
langchain4j.qdrant.embeddingStore.collectionName = "mycollection"
{
  "langchain4j.qdrant.embedding-store.host" = "localhost"
  "langchain4j.qdrant.embedding-store.port" = 6334
  "langchain4j.qdrant.embedding-store.collection-name" = "mycollection"
}
{
  "langchain4j.qdrant.embedding-store.host": "localhost",
  "langchain4j.qdrant.embedding-store.port": 6334,
  "langchain4j.qdrant.embedding-store.collection-name": "mycollection"
}

11 Repository

You can find the source code of this project in this repository:

12 Release History

For this project, you can find a list of releases (with release notes) here: