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 1161b46de4ed2862670db963f54de779cd0c45a4 Author: Jiri Ondrusek <[email protected]> AuthorDate: Thu Aug 6 16:15:43 2026 +0200 Fixes #8966. Auto-produce RetrievalAugmentor bridging Camel ingestion with @RegisterAiService RAG Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../langchain4j/deployment/EasyRagPresent.java | 33 ++++ .../SupportQuarkusLangchain4jProcessor.java | 100 +++++++++++++ extensions-support/langchain4j/runtime/pom.xml | 5 + .../DefaultRetrievalAugmentorSupplier.java | 105 +++++++++++++ .../langchain4j/QuarkusLangchain4jRecorder.java | 7 + .../support/langchain4j/RagBridgeConfig.java | 58 +++++++ .../langchain4j-rag-bridge-ql4j/pom.xml | 166 +++++++++++++++++++++ .../langchain4j/ragbridge/it/RagAiService.java | 54 +++++++ .../ragbridge/it/RagBridgeProducers.java | 59 ++++++++ .../ragbridge/it/RagBridgeResource.java | 86 +++++++++++ .../langchain4j/ragbridge/it/RagBridgeRoutes.java | 33 ++++ .../src/main/resources/application.properties | 18 +++ .../ragbridge/it/ExistingAugmentorProducer.java | 39 +++++ .../ragbridge/it/ExistingAugmentorProfile.java | 28 ++++ .../ragbridge/it/ExistingAugmentorTest.java | 57 +++++++ .../ragbridge/it/MultiAugmentorProfile.java | 30 ++++ .../ragbridge/it/MultiAugmentorTest.java | 60 ++++++++ .../langchain4j/ragbridge/it/RagBridgeIT.java | 24 +++ .../langchain4j/ragbridge/it/RagBridgeTest.java | 67 +++++++++ .../ragbridge/it/SingleAugmentorProfile.java | 29 ++++ .../ragbridge/it/SingleAugmentorTest.java | 93 ++++++++++++ integration-tests/pom.xml | 1 + poms/bom/pom.xml | 5 + poms/bom/src/main/generated/flattened-full-pom.xml | 5 + .../src/main/generated/flattened-reduced-pom.xml | 5 + .../generated/flattened-reduced-verbose-pom.xml | 5 + tooling/scripts/test-categories.yaml | 1 + 27 files changed, 1173 insertions(+) diff --git a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/EasyRagPresent.java b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/EasyRagPresent.java new file mode 100644 index 0000000000..e462ed5785 --- /dev/null +++ b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/EasyRagPresent.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.support.langchain4j.deployment; + +import java.util.function.BooleanSupplier; + +public class EasyRagPresent implements BooleanSupplier { + private static final String EASY_RAG_CONFIG_CLASS = "io.quarkiverse.langchain4j.easyrag.runtime.EasyRagConfig"; + + @Override + public boolean getAsBoolean() { + try { + Thread.currentThread().getContextClassLoader().loadClass(EASY_RAG_CONFIG_CLASS); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java index 92133cb549..84dddef6b8 100644 --- a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java +++ b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java @@ -26,11 +26,16 @@ import java.util.stream.Collectors; import dev.langchain4j.guardrail.Guardrail; import dev.langchain4j.guardrail.InputGuardrail; import dev.langchain4j.guardrail.OutputGuardrail; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.store.embedding.EmbeddingStore; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.processor.BeanInfo; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -44,16 +49,21 @@ import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Named; import jakarta.inject.Singleton; import org.apache.camel.quarkus.component.support.langchain4j.AiToolSpecConverter; import org.apache.camel.quarkus.component.support.langchain4j.CamelAiToolProvider; import org.apache.camel.quarkus.component.support.langchain4j.CamelAiToolsInterceptor; import org.apache.camel.quarkus.component.support.langchain4j.QuarkusLangchain4jRecorder; +import org.apache.camel.quarkus.component.support.langchain4j.RagBridgeConfig; +import org.apache.camel.quarkus.component.support.langchain4j.RagBridgeConfig.AugmentorConfig; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.Type; import org.jboss.logging.Logger; import static io.quarkus.arc.deployment.UnremovableBeanBuildItem.beanClassNames; @@ -256,4 +266,94 @@ class SupportQuarkusLangchain4jProcessor { } } } + + /** + * Produces {@link RetrievalAugmentor} CDI beans that bridge Camel ingestion routes with + * {@code @RegisterAiService} RAG. + * + * <p> + * Two modes: + * <ul> + * <li><b>Explicit config</b> — each entry under {@code quarkus.camel.langchain4j.rag.augmentors.<name>} + * produces a {@code @Named("<name>")} RetrievalAugmentor backed by the configured store. + * If only one entry exists, it is also marked as {@code defaultBean()} for auto-discovery.</li> + * <li><b>Auto-detection</b> — when no config entries exist, at least one EmbeddingStore and one + * EmbeddingModel are present, and no RetrievalAugmentor exists yet, a default one is produced + * backed by the {@code @Default} CDI bean.</li> + * </ul> + */ + @BuildStep(onlyIfNot = EasyRagPresent.class) + @Record(ExecutionTime.RUNTIME_INIT) + void registerDefaultRetrievalAugmentor( + BeanDiscoveryFinishedBuildItem beanDiscovery, + RagBridgeConfig ragBridgeConfig, + QuarkusLangchain4jRecorder recorder, + BuildProducer<SyntheticBeanBuildItem> syntheticBeans) { + + DotName embeddingStoreDN = DotName.createSimple(EmbeddingStore.class.getName()); + DotName embeddingModelDN = DotName.createSimple(EmbeddingModel.class.getName()); + DotName retrievalAugmentorDN = DotName.createSimple(RetrievalAugmentor.class.getName()); + + int embeddingStoreCount = 0; + int embeddingModelCount = 0; + boolean hasRetrievalAugmentor = false; + + for (BeanInfo bean : beanDiscovery.beanStream().collect(Collectors.toList())) { + for (Type type : bean.getTypes()) { + DotName typeName = type.name(); + if (typeName.equals(embeddingStoreDN)) { + embeddingStoreCount++; + } else if (typeName.equals(embeddingModelDN)) { + embeddingModelCount++; + } else if (typeName.equals(retrievalAugmentorDN)) { + hasRetrievalAugmentor = true; + } + } + } + + Map<String, AugmentorConfig> augmentors = ragBridgeConfig.augmentors(); + + if (!augmentors.isEmpty()) { + for (Map.Entry<String, AugmentorConfig> entry : augmentors.entrySet()) { + String name = entry.getKey(); + AugmentorConfig cfg = entry.getValue(); + + LOG.debugf("Registering named RetrievalAugmentor '%s' backed by EmbeddingStore '%s'", + name, cfg.embeddingStoreName()); + + SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + .configure(RetrievalAugmentor.class) + .scope(ApplicationScoped.class) + .addQualifier().annotation(Named.class).addValue("value", name).done() + .setRuntimeInit() + .supplier(recorder.createDefaultRetrievalAugmentorSupplier( + cfg.embeddingStoreName(), cfg.embeddingModelName().orElse(null))); + + // Single augmentor configured and no user/Easy-RAG augmentor present: + // mark as defaultBean() so @RegisterAiService discovers it without a qualifier + if (augmentors.size() == 1 && !hasRetrievalAugmentor) { + configurator.defaultBean(); + } + + syntheticBeans.produce(configurator.done()); + } + return; + } + + if (hasRetrievalAugmentor) { + return; + } + + if (embeddingStoreCount >= 1 && embeddingModelCount >= 1) { + LOG.debug("EmbeddingStore and EmbeddingModel CDI beans detected" + + " - registering default RetrievalAugmentor backed by @Default store"); + syntheticBeans.produce(SyntheticBeanBuildItem + .configure(RetrievalAugmentor.class) + .scope(ApplicationScoped.class) + .defaultBean() + .setRuntimeInit() + .supplier(recorder.createDefaultRetrievalAugmentorSupplier(null, null)) + .done()); + } + } } diff --git a/extensions-support/langchain4j/runtime/pom.xml b/extensions-support/langchain4j/runtime/pom.xml index 9214158d85..f0f798cfae 100644 --- a/extensions-support/langchain4j/runtime/pom.xml +++ b/extensions-support/langchain4j/runtime/pom.xml @@ -50,6 +50,11 @@ <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings</artifactId> </dependency> + <dependency> + <groupId>io.quarkiverse.langchain4j</groupId> + <artifactId>quarkus-langchain4j-core</artifactId> + <optional>true</optional> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-ai-tool</artifactId> diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/DefaultRetrievalAugmentorSupplier.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/DefaultRetrievalAugmentorSupplier.java new file mode 100644 index 0000000000..b60cd2d82b --- /dev/null +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/DefaultRetrievalAugmentorSupplier.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.support.langchain4j; + +import java.util.function.Supplier; + +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.rag.DefaultRetrievalAugmentor; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; +import dev.langchain4j.store.embedding.EmbeddingStore; +import io.quarkiverse.langchain4j.EmbeddingStoreName; +import io.quarkus.arc.Arc; +import jakarta.enterprise.inject.literal.NamedLiteral; +import jakarta.enterprise.util.TypeLiteral; +import org.jboss.logging.Logger; + +/** + * Lazily creates a {@link RetrievalAugmentor} backed by the CDI {@link EmbeddingStore} + * and {@link EmbeddingModel} beans. Used as a supplier for the synthetic bean produced + * by the deployment processor. + */ +public class DefaultRetrievalAugmentorSupplier implements Supplier<RetrievalAugmentor> { + + private static final Logger LOG = Logger.getLogger(DefaultRetrievalAugmentorSupplier.class); + + private static final TypeLiteral<EmbeddingStore<TextSegment>> EMBEDDING_STORE_TYPE = new TypeLiteral<>() { + }; + + private final String embeddingStoreName; + private final String embeddingModelName; + + public DefaultRetrievalAugmentorSupplier() { + this(null, null); + } + + public DefaultRetrievalAugmentorSupplier(String embeddingStoreName, String embeddingModelName) { + this.embeddingStoreName = embeddingStoreName; + this.embeddingModelName = embeddingModelName; + } + + @Override + public RetrievalAugmentor get() { + EmbeddingStore<TextSegment> store; + if (embeddingStoreName != null) { + store = Arc.container() + .instance(EMBEDDING_STORE_TYPE, EmbeddingStoreName.Literal.of(embeddingStoreName)).get(); + if (store == null) { + store = Arc.container() + .instance(EMBEDDING_STORE_TYPE, NamedLiteral.of(embeddingStoreName)).get(); + } + if (store == null) { + throw new IllegalStateException( + "No EmbeddingStore CDI bean found with @EmbeddingStoreName(\"" + embeddingStoreName + + "\") or @Named(\"" + embeddingStoreName + "\")"); + } + } else { + store = Arc.container().instance(EMBEDDING_STORE_TYPE).get(); + if (store == null) { + throw new IllegalStateException("No default EmbeddingStore CDI bean found"); + } + } + + EmbeddingModel model; + if (embeddingModelName != null) { + model = Arc.container() + .instance(EmbeddingModel.class, NamedLiteral.of(embeddingModelName)).get(); + if (model == null) { + throw new IllegalStateException( + "No EmbeddingModel CDI bean found with @Named(\"" + embeddingModelName + "\")"); + } + } else { + model = Arc.container().instance(EmbeddingModel.class).get(); + if (model == null) { + throw new IllegalStateException("No default EmbeddingModel CDI bean found"); + } + } + + LOG.debugf("Creating default RetrievalAugmentor bridging Camel ingestion with @RegisterAiService RAG" + + " (store=%s, model=%s)", embeddingStoreName != null ? embeddingStoreName : "@Default", + embeddingModelName != null ? embeddingModelName : "@Default"); + + return DefaultRetrievalAugmentor.builder() + .contentRetriever(EmbeddingStoreContentRetriever.builder() + .embeddingStore(store) + .embeddingModel(model) + .build()) + .build(); + } +} diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java index ac23912b1b..b7d8fa23f2 100644 --- a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java @@ -18,8 +18,10 @@ package org.apache.camel.quarkus.component.support.langchain4j; import java.lang.reflect.InvocationTargetException; import java.util.Map; +import java.util.function.Supplier; import dev.langchain4j.guardrail.Guardrail; +import dev.langchain4j.rag.RetrievalAugmentor; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import org.jboss.logging.Logger; @@ -41,4 +43,9 @@ public class QuarkusLangchain4jRecorder { return null; } } + + public Supplier<RetrievalAugmentor> createDefaultRetrievalAugmentorSupplier( + String embeddingStoreName, String embeddingModelName) { + return new DefaultRetrievalAugmentorSupplier(embeddingStoreName, embeddingModelName); + } } diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/RagBridgeConfig.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/RagBridgeConfig.java new file mode 100644 index 0000000000..4744841a31 --- /dev/null +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/RagBridgeConfig.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.support.langchain4j; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; + +// BUILD_AND_RUN_TIME_FIXED: augmentor bean definitions are baked in at build time and cannot change at runtime +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +@ConfigMapping(prefix = "quarkus.camel.langchain4j.rag") +public interface RagBridgeConfig { + + /** + * Named RetrievalAugmentor beans, each backed by a specific EmbeddingStore. + * The map key becomes the CDI bean name ({@code @Named("key")}). + * + * Example: + * + * <pre> + * quarkus.camel.langchain4j.rag.augmentors.products.embedding-store-name=products + * quarkus.camel.langchain4j.rag.augmentors.support.embedding-store-name=support-docs + * </pre> + */ + Map<String, AugmentorConfig> augmentors(); + + interface AugmentorConfig { + + /** + * CDI bean name of the {@code EmbeddingStore} to use. + * Matches beans annotated with {@code @Named("name")} or {@code @EmbeddingStoreName("name")}. + */ + String embeddingStoreName(); + + /** + * CDI bean name of the {@code EmbeddingModel} to use. + * When not set, the default (unnamed) EmbeddingModel is used. + */ + Optional<String> embeddingModelName(); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/pom.xml b/integration-tests/langchain4j-rag-bridge-ql4j/pom.xml new file mode 100644 index 0000000000..12a045478e --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/pom.xml @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-build-parent-it</artifactId> + <version>3.39.0-SNAPSHOT</version> + <relativePath>../../poms/build-parent-it/pom.xml</relativePath> + </parent> + + <artifactId>camel-quarkus-integration-test-langchain4j-rag-bridge-ql4j</artifactId> + <name>Camel Quarkus :: Integration Tests :: LangChain4j RAG Bridge QL4J</name> + <description>Integration tests for the automatic RetrievalAugmentor bridge between Camel ingestion and Quarkus LangChain4j @RegisterAiService</description> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>io.quarkiverse.langchain4j</groupId> + <artifactId>quarkus-langchain4j-bom</artifactId> + <version>${quarkiverse-langchain4j.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-direct</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-langchain4j-embeddingstore</artifactId> + </dependency> + <!-- Quarkus-langchain4j dependency for @RegisterAiService --> + <dependency> + <groupId>io.quarkiverse.langchain4j</groupId> + <artifactId>quarkus-langchain4j-ollama</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-rest</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-rest-jackson</artifactId> + </dependency> + <dependency> + <groupId>dev.langchain4j</groupId> + <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId> + </dependency> + + <!-- test dependencies --> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- AllMiniLmL6V2EmbeddingModel loads a native .so via JNI; + @TestProfile re-augmentation creates a new classloader + which cannot reload the same native library --> + <reuseForks>false</reuseForks> + </configuration> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>native</id> + <activation> + <property> + <name>native</name> + </property> + </activation> + <properties> + <quarkus.native.enabled>true</quarkus.native.enabled> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>virtualDependencies</id> + <activation> + <property> + <name>!noVirtualDependencies</name> + </property> + </activation> + <dependencies> + <!-- The following dependencies guarantee that this module is built after them. You can update them by running `mvn process-resources -Pformat -N` from the source tree root directory --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-direct-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-langchain4j-embeddingstore-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> + </profile> + </profiles> +</project> diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagAiService.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagAiService.java new file mode 100644 index 0000000000..b9a887e862 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagAiService.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +@RegisterAiService(chatLanguageModelSupplier = RagAiService.EchoChatModelSupplier.class) +public interface RagAiService { + + String chat(@dev.langchain4j.service.UserMessage String question); + + class EchoChatModelSupplier implements Supplier<ChatModel> { + @Override + public ChatModel get() { + return new ChatModel() { + @Override + public ChatResponse doChat(ChatRequest chatRequest) { + String allContent = chatRequest.messages().stream() + .filter(m -> m instanceof UserMessage) + .map(ChatMessage::toString) + .collect(Collectors.joining("\n")); + return ChatResponse.builder() + .aiMessage(new AiMessage(allContent)) + .build(); + } + }; + } + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeProducers.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeProducers.java new file mode 100644 index 0000000000..d5f22ecc38 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeProducers.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; +import io.quarkiverse.langchain4j.EmbeddingStoreName; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@ApplicationScoped +public class RagBridgeProducers { + + // @Named registers the bean in the Camel registry so the langchain4j-embeddingstore + // endpoint can reference it via ?embeddingStore=#defaultStore (autowiring alone is + // ambiguous when multiple EmbeddingStore beans exist) + @Produces + @Singleton + @Named("defaultStore") + EmbeddingStore<TextSegment> embeddingStore() { + return new InMemoryEmbeddingStore<>(); + } + + // @Named makes this store addressable in Camel URIs as #products; + // @EmbeddingStoreName is the Quarkus LangChain4j qualifier used by the + // RAG bridge's DefaultRetrievalAugmentorSupplier to resolve the store at runtime + @Produces + @Singleton + @Named("products") + @EmbeddingStoreName("products") + EmbeddingStore<TextSegment> productsEmbeddingStore() { + return new InMemoryEmbeddingStore<>(); + } + + @Produces + @Singleton + EmbeddingModel embeddingModel() { + return new AllMiniLmL6V2EmbeddingModel(); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeResource.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeResource.java new file mode 100644 index 0000000000..8cd27ec5a2 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeResource.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import dev.langchain4j.rag.RetrievalAugmentor; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +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.ProducerTemplate; + +@Path("/rag-bridge") +public class RagBridgeResource { + + @Inject + Instance<RetrievalAugmentor> retrievalAugmentorInstance; + + @Inject + @Named("products") + Instance<RetrievalAugmentor> namedAugmentorInstance; + + @Inject + ProducerTemplate producerTemplate; + + @Inject + RagAiService aiService; + + @GET + @Path("/augmentor-present") + @Produces(MediaType.TEXT_PLAIN) + public boolean isAugmentorPresent() { + return retrievalAugmentorInstance.isResolvable(); + } + + @GET + @Path("/named-augmentor-present") + @Produces(MediaType.TEXT_PLAIN) + public boolean isNamedAugmentorPresent() { + return namedAugmentorInstance.isResolvable(); + } + + @POST + @Path("/ingest") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String ingest(String text) { + producerTemplate.sendBody("direct:ingest", text); + return "ingested"; + } + + @POST + @Path("/ingest-products") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String ingestProducts(String text) { + producerTemplate.sendBody("direct:ingest-products", text); + return "ingested"; + } + + @POST + @Path("/ask") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String ask(String question) { + return aiService.chat(question); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeRoutes.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeRoutes.java new file mode 100644 index 0000000000..517de183c2 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeRoutes.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.builder.RouteBuilder; + +@ApplicationScoped +public class RagBridgeRoutes extends RouteBuilder { + + @Override + public void configure() throws Exception { + from("direct:ingest") + .to("langchain4j-embeddingstore:default?embeddingStore=#defaultStore&action=ADD"); + + from("direct:ingest-products") + .to("langchain4j-embeddingstore:products?embeddingStore=#products&action=ADD"); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/main/resources/application.properties b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/resources/application.properties new file mode 100644 index 0000000000..d61317715e --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/main/resources/application.properties @@ -0,0 +1,18 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## --------------------------------------------------------------------------- + +quarkus.devservices.enabled=false diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorProducer.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorProducer.java new file mode 100644 index 0000000000..214ac192fe --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorProducer.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import java.util.List; + +import dev.langchain4j.rag.DefaultRetrievalAugmentor; +import dev.langchain4j.rag.RetrievalAugmentor; +import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; + +@ApplicationScoped +@IfBuildProperty(name = "test.existing-augmentor", stringValue = "true", enableIfMissing = false) +public class ExistingAugmentorProducer { + + @Produces + @Singleton + RetrievalAugmentor existingAugmentor() { + return DefaultRetrievalAugmentor.builder() + .contentRetriever(query -> List.of()) + .build(); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorProfile.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorProfile.java new file mode 100644 index 0000000000..7396475f3c --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorProfile.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class ExistingAugmentorProfile implements QuarkusTestProfile { + @Override + public Map<String, String> getConfigOverrides() { + return Map.of("test.existing-augmentor", "true"); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorTest.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorTest.java new file mode 100644 index 0000000000..8838e23cae --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/ExistingAugmentorTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.is; + +/** + * Verifies that the bridge yields when a user-provided {@code RetrievalAugmentor} + * already exists in CDI. The bridge should not produce a conflicting bean. + * + * <p> + * No {@code @QuarkusIntegrationTest} counterpart exists because this test uses + * {@code @TestProfile} to change {@code BUILD_AND_RUN_TIME_FIXED} config. + * Native binaries are pre-built with the default config baked in, so profile + * overrides have no effect in {@code @QuarkusIntegrationTest}. + */ +@QuarkusTest +@TestProfile(ExistingAugmentorProfile.class) +class ExistingAugmentorTest { + + @Test + void existingAugmentorIsResolvable() { + RestAssured.given() + .get("/rag-bridge/augmentor-present") + .then() + .statusCode(200) + .body(is("true")); + } + + @Test + void noNamedAugmentorWhenExistingPresent() { + RestAssured.given() + .get("/rag-bridge/named-augmentor-present") + .then() + .statusCode(200) + .body(is("false")); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/MultiAugmentorProfile.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/MultiAugmentorProfile.java new file mode 100644 index 0000000000..112155722d --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/MultiAugmentorProfile.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class MultiAugmentorProfile implements QuarkusTestProfile { + @Override + public Map<String, String> getConfigOverrides() { + return Map.of( + "quarkus.camel.langchain4j.rag.augmentors.products.embedding-store-name", "products", + "quarkus.camel.langchain4j.rag.augmentors.support.embedding-store-name", "support"); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/MultiAugmentorTest.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/MultiAugmentorTest.java new file mode 100644 index 0000000000..60845f0cbe --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/MultiAugmentorTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.is; + +/** + * Verifies that when multiple augmentors are configured, no default (unqualified) + * RetrievalAugmentor bean is produced. Each augmentor should only be resolvable + * via its {@code @Named} qualifier to prevent silent auto-selection. + * + * <p> + * No {@code @QuarkusIntegrationTest} counterpart exists because this test uses + * {@code @TestProfile} to change {@code BUILD_AND_RUN_TIME_FIXED} config. + * Native binaries are pre-built with the default config baked in, so profile + * overrides have no effect in {@code @QuarkusIntegrationTest}. A native-mode + * test would require a separate Maven module with its own + * {@code application.properties}. + */ +@QuarkusTest +@TestProfile(MultiAugmentorProfile.class) +class MultiAugmentorTest { + + @Test + void noDefaultAugmentorWithMultipleConfigured() { + RestAssured.given() + .get("/rag-bridge/augmentor-present") + .then() + .statusCode(200) + .body(is("false")); + } + + @Test + void namedAugmentorStillResolvable() { + RestAssured.given() + .get("/rag-bridge/named-augmentor-present") + .then() + .statusCode(200) + .body(is("true")); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeIT.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeIT.java new file mode 100644 index 0000000000..c43139d5a4 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeIT.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RagBridgeIT extends RagBridgeTest { + +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeTest.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeTest.java new file mode 100644 index 0000000000..d65e08dc10 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/RagBridgeTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Tests the auto-detection path: no augmentor config is set, but EmbeddingStore + * and EmbeddingModel beans are present, so the bridge auto-produces a default + * RetrievalAugmentor backed by the {@code @Default} store. + */ +@QuarkusTest +class RagBridgeTest { + + @Test + void defaultAugmentorIsResolvable() { + RestAssured.given() + .get("/rag-bridge/augmentor-present") + .then() + .statusCode(200) + .body(is("true")); + } + + @Test + void noNamedAugmentorInAutoDetection() { + RestAssured.given() + .get("/rag-bridge/named-augmentor-present") + .then() + .statusCode(200) + .body(is("false")); + } + + @Test + void autoDetectedAugmentorRetrievesFromDefaultStore() { + RestAssured.given() + .body("Apache Camel is a powerful open source integration framework") + .post("/rag-bridge/ingest") + .then() + .statusCode(200); + + RestAssured.given() + .body("Tell me about integration frameworks") + .post("/rag-bridge/ask") + .then() + .statusCode(200) + .body(containsString("Apache Camel")); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/SingleAugmentorProfile.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/SingleAugmentorProfile.java new file mode 100644 index 0000000000..1136d451f3 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/SingleAugmentorProfile.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class SingleAugmentorProfile implements QuarkusTestProfile { + @Override + public Map<String, String> getConfigOverrides() { + return Map.of( + "quarkus.camel.langchain4j.rag.augmentors.products.embedding-store-name", "products"); + } +} diff --git a/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/SingleAugmentorTest.java b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/SingleAugmentorTest.java new file mode 100644 index 0000000000..54000a4dd9 --- /dev/null +++ b/integration-tests/langchain4j-rag-bridge-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/ragbridge/it/SingleAugmentorTest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.langchain4j.ragbridge.it; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +/** + * Verifies explicit single-augmentor configuration: a single named augmentor is + * configured via {@code quarkus.camel.langchain4j.rag.augmentors.products}, so + * it is also marked as {@code defaultBean()} for auto-discovery by + * {@code @RegisterAiService}. + * + * <p> + * No {@code @QuarkusIntegrationTest} counterpart exists because this test uses + * {@code @TestProfile} to change {@code BUILD_AND_RUN_TIME_FIXED} config. + * Native binaries are pre-built with the default config baked in, so profile + * overrides have no effect in {@code @QuarkusIntegrationTest}. + */ +@QuarkusTest +@TestProfile(SingleAugmentorProfile.class) +class SingleAugmentorTest { + + @Test + void defaultAugmentorIsResolvable() { + RestAssured.given() + .get("/rag-bridge/augmentor-present") + .then() + .statusCode(200) + .body(is("true")); + } + + @Test + void namedAugmentorIsResolvable() { + RestAssured.given() + .get("/rag-bridge/named-augmentor-present") + .then() + .statusCode(200) + .body(is("true")); + } + + @Test + void namedAugmentorRetrievesFromCorrectStore() { + RestAssured.given() + .body("Apache Camel is a powerful open source integration framework") + .post("/rag-bridge/ingest-products") + .then() + .statusCode(200); + + RestAssured.given() + .body("Tell me about integration frameworks") + .post("/rag-bridge/ask") + .then() + .statusCode(200) + .body(containsString("Apache Camel")); + } + + @Test + void namedAugmentorDoesNotSeeDataInOtherStore() { + RestAssured.given() + .body("Quarkus is a supersonic subatomic Java framework") + .post("/rag-bridge/ingest") + .then() + .statusCode(200); + + RestAssured.given() + .body("Tell me about Java frameworks") + .post("/rag-bridge/ask") + .then() + .statusCode(200) + .body(not(containsString("Quarkus"))); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 3eac678682..9528083d19 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -165,6 +165,7 @@ <module>langchain4j-chat</module> <module>langchain4j-embeddings</module> <module>langchain4j-embeddingstore</module> + <module>langchain4j-rag-bridge-ql4j</module> <module>langchain4j-tokenizer</module> <module>langchain4j-tools</module> <module>langchain4j-web-search</module> diff --git a/poms/bom/pom.xml b/poms/bom/pom.xml index a3c91e8553..3c43b7ca15 100644 --- a/poms/bom/pom.xml +++ b/poms/bom/pom.xml @@ -7948,6 +7948,11 @@ <artifactId>quarkus-jsch-deployment</artifactId> <version>${quarkiverse-jsch.version}</version> </dependency> + <dependency> + <groupId>io.quarkiverse.langchain4j</groupId> + <artifactId>quarkus-langchain4j-core</artifactId> + <version>${quarkiverse-langchain4j.version}</version> + </dependency> <dependency> <groupId>io.quarkiverse.mcp</groupId> <artifactId>quarkus-mcp-server-http</artifactId> diff --git a/poms/bom/src/main/generated/flattened-full-pom.xml b/poms/bom/src/main/generated/flattened-full-pom.xml index c1feff44b3..2c91642cbb 100644 --- a/poms/bom/src/main/generated/flattened-full-pom.xml +++ b/poms/bom/src/main/generated/flattened-full-pom.xml @@ -7820,6 +7820,11 @@ <artifactId>quarkus-jackson-jq-deployment</artifactId> <version>2.5.2</version> </dependency> + <dependency> + <groupId>io.quarkiverse.langchain4j</groupId> + <artifactId>quarkus-langchain4j-core</artifactId> + <version>1.12.2</version> + </dependency> <dependency> <groupId>io.quarkiverse.jsch</groupId> <artifactId>quarkus-jsch</artifactId> diff --git a/poms/bom/src/main/generated/flattened-reduced-pom.xml b/poms/bom/src/main/generated/flattened-reduced-pom.xml index 9a86650e2c..0d38069882 100644 --- a/poms/bom/src/main/generated/flattened-reduced-pom.xml +++ b/poms/bom/src/main/generated/flattened-reduced-pom.xml @@ -1187,6 +1187,11 @@ <artifactId>quarkus-jsch-deployment</artifactId> <version>3.2.1</version> </dependency> + <dependency> + <groupId>io.quarkiverse.langchain4j</groupId> + <artifactId>quarkus-langchain4j-core</artifactId> + <version>1.12.2</version> + </dependency> <dependency> <groupId>io.quarkiverse.mcp</groupId> <artifactId>quarkus-mcp-server-http</artifactId> diff --git a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml index c2e86a9267..81c4a57000 100644 --- a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml +++ b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml @@ -1187,6 +1187,11 @@ <artifactId>quarkus-jsch-deployment</artifactId> <version>3.2.1</version> </dependency> + <dependency> + <groupId>io.quarkiverse.langchain4j</groupId> + <artifactId>quarkus-langchain4j-core</artifactId> + <version>1.12.2</version> + </dependency> <dependency> <groupId>io.quarkiverse.mcp</groupId> <artifactId>quarkus-mcp-server-http</artifactId> diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml index 98d19bc52c..94803799be 100644 --- a/tooling/scripts/test-categories.yaml +++ b/tooling/scripts/test-categories.yaml @@ -142,6 +142,7 @@ group-07: - jslt - ldap - langchain4j-embeddings + - langchain4j-rag-bridge-ql4j - lumberjack - ognl - olingo4
