This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit c9d4f78d98b6fce3f51f7efd88cf0216f192af01 Author: James Netherton <[email protected]> AuthorDate: Mon Sep 14 13:45:46 2026 +0100 Exclude AiAgentWithoutMemoryService from Quarkus LangChain4j implied AI service registration When Quarkus LangChain4j is present with a default chat model configured, it registers AiAgentWithoutMemoryService as an implied AI service backed by the CDI ChatMemoryProvider, silently making AgentWithoutMemory stateful. Fixes #8836 Co-Authored-By: Jiri Ondrusek <[email protected]> Co-Authored-By: Claude Opus 5 <[email protected]> --- .../SupportLangchain4jQl4jProcessor.java | 12 ++++++ .../agent/it/Langchain4jAgentQl4jResource.java} | 41 +++++++++--------- .../agent/it/Langchain4jAgentQl4jRoutes.java} | 28 +++--------- .../src/main/resources/application.properties | 4 ++ .../agent/it/Langchain4jAgentQl4jTest.java | 50 +++++++++++++++++++++- 5 files changed, 93 insertions(+), 42 deletions(-) diff --git a/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java b/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java index d21f55f276..805868b04d 100644 --- a/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java +++ b/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java @@ -24,6 +24,7 @@ import dev.langchain4j.guardrail.Guardrail; import dev.langchain4j.guardrail.InputGuardrail; import dev.langchain4j.guardrail.OutputGuardrail; import io.quarkiverse.langchain4j.RegisterAiService; +import io.quarkiverse.langchain4j.deployment.ExcludeFromImpliedAiServiceBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -49,6 +50,8 @@ class SupportLangchain4jQl4jProcessor { private static final DotName REGISTER_AI_SERVICES_DOTNAME = DotName.createSimple(RegisterAiService.class); private static final DotName JSON_EXTRACTOR_OUTPUT_GUARDRAIL = DotName .createSimple("dev.langchain4j.guardrails.JsonExtractorOutputGuardrail"); + private static final DotName AI_AGENT_WITHOUT_MEMORY_SERVICE = DotName + .createSimple("org.apache.camel.component.langchain4j.agent.api.AiAgentWithoutMemoryService"); private static final Logger LOG = Logger.getLogger(SupportLangchain4jQl4jProcessor.class); @BuildStep @@ -127,6 +130,15 @@ class SupportLangchain4jQl4jProcessor { .build()); } + @BuildStep + void excludeCamelAgentInterfacesFromImpliedRegistration( + CombinedIndexBuildItem combinedIndex, + BuildProducer<ExcludeFromImpliedAiServiceBuildItem> excludedFromImplied) { + if (combinedIndex.getIndex().getClassByName(AI_AGENT_WITHOUT_MEMORY_SERVICE) != null) { + excludedFromImplied.produce(new ExcludeFromImpliedAiServiceBuildItem(AI_AGENT_WITHOUT_MEMORY_SERVICE.toString())); + } + } + @BuildStep void validateAndRegisterAiServices( CombinedIndexBuildItem indexBuildItem, diff --git a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jResource.java similarity index 52% copy from integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java copy to integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jResource.java index 6dfceb6f57..45e27302e7 100644 --- a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java +++ b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jResource.java @@ -16,27 +16,28 @@ */ package org.apache.camel.quarkus.component.langchain4j.agent.it; -import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.apache.camel.FluentProducerTemplate; -import static org.hamcrest.Matchers.*; +@Path("/langchain4j-agent-ql4j") +@ApplicationScoped +public class Langchain4jAgentQl4jResource { + @Inject + FluentProducerTemplate producerTemplate; -@ExtendWith(Langchain4jTestWatcher.class) -@QuarkusTestResource(Langchain4jAgentTestResource.class) -@QuarkusTest -class Langchain4jAgentQl4jTest { - @Test - void simpleUserMessage() { - RestAssured.given() - .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE) - .post("/langchain4j-agent/simple") - .then() - .statusCode(200) - .body( - not(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE), - containsString("Apache Camel")); + @Path("/simple") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String simple(String userMessage) { + return producerTemplate.to("seda:simple-agent-without-request-context") + .withBody(userMessage) + .request(String.class); } } diff --git a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jRoutes.java similarity index 52% copy from integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java copy to integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jRoutes.java index 6dfceb6f57..ed2fe0b523 100644 --- a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java +++ b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jRoutes.java @@ -16,27 +16,13 @@ */ package org.apache.camel.quarkus.component.langchain4j.agent.it; -import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.apache.camel.builder.RouteBuilder; -import static org.hamcrest.Matchers.*; - -@ExtendWith(Langchain4jTestWatcher.class) -@QuarkusTestResource(Langchain4jAgentTestResource.class) -@QuarkusTest -class Langchain4jAgentQl4jTest { - @Test - void simpleUserMessage() { - RestAssured.given() - .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE) - .post("/langchain4j-agent/simple") - .then() - .statusCode(200) - .body( - not(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE), - containsString("Apache Camel")); +public class Langchain4jAgentQl4jRoutes extends RouteBuilder { + @Override + public void configure() throws Exception { + // Invoke the agent on a seda consumer thread where there is no active CDI request context + from("seda:simple-agent-without-request-context") + .to("direct:simple-agent"); } } diff --git a/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties b/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties index 58f1089f9b..033697c56c 100644 --- a/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties +++ b/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties @@ -23,3 +23,7 @@ quarkus.langchain4j.devservices.preload=false # The custom AiService receives registered ai-tools (all-tools default for user-built # services), so it needs a tool-capable model; orca-mini rejects requests carrying tools agent.tests.custom-service-model-id=granite4:3b + +# Configure a default QL4J model to trigger implied AI service registration +quarkus.langchain4j.ollama.base-url=${langchain4j.ollama.base-url} +quarkus.langchain4j.ollama.chat-model.model-id=orca-mini diff --git a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java b/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java index 6dfceb6f57..12c94db143 100644 --- a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java +++ b/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java @@ -20,23 +20,71 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.extension.ExtendWith; import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertFalse; @ExtendWith(Langchain4jTestWatcher.class) @QuarkusTestResource(Langchain4jAgentTestResource.class) @QuarkusTest class Langchain4jAgentQl4jTest { + private static final String SIMPLE_ENDPOINT = "/langchain4j-agent/simple"; + private static final String SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT = "/langchain4j-agent-ql4j/simple"; + private static final String STATELESS_SECRET = "PARROT42"; + @Test void simpleUserMessage() { RestAssured.given() .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE) - .post("/langchain4j-agent/simple") + .post(SIMPLE_ENDPOINT) .then() .statusCode(200) .body( not(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE), containsString("Apache Camel")); } + + @Test + @DisabledIfEnvironmentVariable(named = "LANGCHAIN4J_OLLAMA_BASE_URL", matches = ".+", disabledReason = "This test uses WireMock strict matching") + void agentWithoutMemoryIsStatelessWiremock() { + String first = RestAssured.given() + .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE) + .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT) + .then() + .statusCode(200) + .extract().asString(); + + String second = RestAssured.given() + .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE) + .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT) + .then() + .statusCode(200) + .extract().asString(); + + assertFalse(first.isEmpty(), "First response should not be empty"); + assertFalse(second.isEmpty(), "Second response should not be empty"); + } + + @Test + @EnabledIfEnvironmentVariable(named = "LANGCHAIN4J_OLLAMA_BASE_URL", matches = ".+", disabledReason = "Requires a real LLM — set LANGCHAIN4J_OLLAMA_BASE_URL") + void agentWithoutMemoryIsStatelessRealApi() { + RestAssured.given() + .body("The secret code is " + STATELESS_SECRET + ". Acknowledge it.") + .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT) + .then() + .statusCode(200); + + String second = RestAssured.given() + .body("What secret code did I tell you? Reply ONLY the code or say UNKNOWN.") + .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT) + .then() + .statusCode(200) + .extract().asString(); + + assertFalse(second.contains(STATELESS_SECRET), + "Agent should NOT remember the secret across calls — memory leaked"); + } }
