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
The following commit(s) were added to refs/heads/main by this push:
new 2d4a5f5c27 Fixes #9225. Let the RAG bridge's bean census count Quarkus
LangChain4j's synthetic beans
2d4a5f5c27 is described below
commit 2d4a5f5c2766ba4ce29cb6a549baed1293a41de9
Author: Jiří Ondrušek <[email protected]>
AuthorDate: Thu Sep 24 07:11:57 2026 +0200
Fixes #9225. Let the RAG bridge's bean census count Quarkus LangChain4j's
synthetic beans
* Fixes #9225. Let the RAG bridge's bean census count Quarkus LangChain4j's
synthetic beans
The bridge decided whether to produce the default RetrievalAugmentor through
a bean census over BeanDiscoveryFinishedBuildItem, which lists class-based
beans only. A store or model registered by a Quarkus LangChain4j extension
is
a synthetic bean, so with a pgvector store no augmentor was produced and
@RegisterAiService ran without RAG.
The conditional support-langchain4j-ql4j module now announces those beans
through the new core AnnouncedSyntheticBeanBuildItem, derived from the build
items Quarkus LangChain4j emits for its stores and models, and the census
counts the default ones. A model selected for a named configuration is
announced with its name and not counted, as it cannot back the default
augmentor. A store item carries no name, so the default augmentor's beans
are
verified at startup once the synthetic beans exist, failing fast with the
configuration that selects named beans instead of failing on the first AI
service call.
A deployment test registers a synthetic store the way a store extension does
and checks that the default augmentor retrieves from it. The migration guide
notes that applications with a Quarkus LangChain4j store now get retrieval
without a configuration change.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Count only @Default beans for the default augmentor and warn instead of
failing at startup
Review of #9229: the discovered-bean census counted qualified stores and
models too, so a class-based @EmbeddingStoreName-only store registered the
default augmentor and the new startup verification failed applications that
never use it. The census now counts only beans carrying @Default, on both
the
discovered and the announced side, which is what the default augmentor can
resolve.
The startup verification resolves through ArcContainer.select, which does
not
create the bean and so neither instantiates @Singleton stores and models at
startup nor leaks @Dependent instances, and it logs a warning with the
configuration that selects named beans instead of throwing: a Quarkus
LangChain4j named-only store cannot be told apart at build time, and an
application that never uses the augmentor must keep starting.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---------
Co-authored-by: Claude Fable 5.1 <[email protected]>
---
.../modules/ROOT/pages/migration-guide/3.40.0.adoc | 27 +++++++
.../spi/AnnouncedSyntheticBeanBuildItem.java | 65 +++++++++++++++
.../SupportLangchain4jQl4jProcessor.java | 34 ++++++++
.../langchain4j-embeddingstore/deployment/pom.xml | 5 ++
.../DefaultRetrievalAugmentorBuildItem.java | 26 ++++++
.../Langchain4jEmbeddingstoreRagProcessor.java | 75 ++++++++++++++---
.../deployment/FakeEmbeddingModel.java | 45 +++++++++++
.../deployment/RagAugmentorSyntheticStoreTest.java | 93 ++++++++++++++++++++++
.../embeddingstore/deployment/SyntheticStore.java | 27 +++++++
.../deployment/SyntheticStoreCreator.java | 29 +++++++
.../Langchain4jEmbeddingstoreRecorder.java | 23 ++++++
11 files changed, 439 insertions(+), 10 deletions(-)
diff --git a/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
b/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
index 27e367f7ba..abf156a37b 100644
--- a/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
+++ b/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
@@ -90,6 +90,33 @@ The `RagRetrievalFilterSupplier` interface moved with it.
Update the import.
import
org.apache.camel.quarkus.component.langchain4j.embeddingstore.RagRetrievalFilterSupplier;
----
+=== The default RAG augmentor now sees Quarkus LangChain4j stores and models
+
+The default `RetrievalAugmentor` is produced when an `EmbeddingStore` and an
`EmbeddingModel` bean
+exist. In 3.39.0 the detection missed the stores and models Quarkus
LangChain4j registers, such as
+a `quarkus-langchain4j-pgvector` store, so an application with such a store, a
model and an
+`@RegisterAiService` interface ran without retrieval. It now gets retrieval
without any
+configuration change: content from the store flows into the prompts of every
AI service.
+
+An AI service that must not answer from the store opts out:
+
+[source,java]
+----
+@RegisterAiService(retrievalAugmentor =
RegisterAiService.NoRetrievalAugmentorSupplier.class)
+----
+
+A store shared by several tenants needs a `RagRetrievalFilterSupplier` before
the upgrade,
+otherwise retrieval serves every tenant's documents.
+
+The default augmentor is backed by the `@Default` store and model. A store
that exists only under
+a name does not produce it: a class-based bean qualified with
`@EmbeddingStoreName` no longer
+counts, and an application that wants retrieval over such a store selects it
with
+`quarkus.camel.langchain4j.rag.augmentors.<name>.embedding-store-name`. A
Quarkus LangChain4j
+store that exists only under a name, for example with
+`quarkus.langchain4j.pgvector.default-store-enabled=false`, cannot be told
apart at build time, so
+the default augmentor is produced; startup logs a warning naming that
configuration, and an AI
+service using the augmentor fails on its first call.
+
== JTA extension changes
=== Rollback and resume failures are no longer swallowed
diff --git
a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/AnnouncedSyntheticBeanBuildItem.java
b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/AnnouncedSyntheticBeanBuildItem.java
new file mode 100644
index 0000000000..c531c04f57
--- /dev/null
+++
b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/AnnouncedSyntheticBeanBuildItem.java
@@ -0,0 +1,65 @@
+/*
+ * 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.core.deployment.spi;
+
+import io.quarkus.builder.item.MultiBuildItem;
+import org.jboss.jandex.DotName;
+
+/**
+ * Announces a bean that another extension registers synthetically. {@code
BeanDiscoveryFinishedBuildItem} lists
+ * class-based beans only, so a build step that counts beans of a type through
it misses the synthetic ones; the
+ * extension knowing about such a bean produces one item per bean, naming the
type it counts as and, for a bean
+ * qualified by a name, that name. A consumer looking for the default bean of
a type must skip the named
+ * announcements: a named bean does not carry the {@code @Default} qualifier.
+ */
+public final class AnnouncedSyntheticBeanBuildItem extends MultiBuildItem {
+
+ private final DotName beanType;
+ private final String name;
+
+ /** Announces the default, unqualified bean of the given type. */
+ public AnnouncedSyntheticBeanBuildItem(Class<?> beanType) {
+ this(DotName.createSimple(beanType), null);
+ }
+
+ /**
+ * @param beanType the type the bean is counted as
+ * @param name the value of the name qualifier of a named bean, {@code
null} for the default bean
+ */
+ public AnnouncedSyntheticBeanBuildItem(Class<?> beanType, String name) {
+ this(DotName.createSimple(beanType), name);
+ }
+
+ public AnnouncedSyntheticBeanBuildItem(DotName beanType, String name) {
+ this.beanType = beanType;
+ this.name = name;
+ }
+
+ public DotName getBeanType() {
+ return beanType;
+ }
+
+ /** The name qualifier value of a named bean, {@code null} for the default
bean. */
+ public String getName() {
+ return name;
+ }
+
+ /** Whether the announced bean is the default, unqualified bean of its
type. */
+ public boolean isDefault() {
+ return name == null;
+ }
+}
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 1cd25c8ebe..b34ec5ee25 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,9 +24,14 @@ import dev.langchain4j.guardrail.Guardrail;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrail;
import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.store.embedding.EmbeddingStore;
import io.quarkiverse.langchain4j.RegisterAiService;
+import io.quarkiverse.langchain4j.deployment.EmbeddingStoreBuildItem;
import
io.quarkiverse.langchain4j.deployment.ExcludeFromImpliedAiServiceBuildItem;
+import io.quarkiverse.langchain4j.deployment.items.InProcessEmbeddingBuildItem;
import
io.quarkiverse.langchain4j.deployment.items.SelectedChatModelProviderBuildItem;
+import
io.quarkiverse.langchain4j.deployment.items.SelectedEmbeddingModelCandidateBuildItem;
import io.quarkiverse.langchain4j.runtime.NamedConfigUtil;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
@@ -40,6 +45,7 @@ import
io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import jakarta.inject.Singleton;
import
org.apache.camel.quarkus.component.support.langchain4j.ql4j.QuarkusLangchain4jRecorder;
+import
org.apache.camel.quarkus.core.deployment.spi.AnnouncedSyntheticBeanBuildItem;
import
org.apache.camel.quarkus.core.deployment.spi.CamelBeanQualifierResolverBuildItem;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
@@ -65,6 +71,34 @@ class SupportLangchain4jQl4jProcessor {
"io.quarkiverse.langchain4j.jaxrsclient.JaxRsHttpClientBuilderFactory");
}
+ /**
+ * Quarkus LangChain4j registers its embedding stores and models as
synthetic beans, which bean
+ * discovery does not list. They are announced through {@link
AnnouncedSyntheticBeanBuildItem} for
+ * the build steps counting such beans, the RAG bridge of
camel-quarkus-langchain4j-embeddingstore
+ * among them.
+ *
+ * <p>
+ * A store extension produces one {@link EmbeddingStoreBuildItem} per
store bean, without its name:
+ * a named-only store, for example pgvector with {@code
default-store-enabled=false}, is announced
+ * as a default one and can only be told apart at runtime. A selected
provider registers the model
+ * it was selected for, qualified with {@code @ModelName} unless it serves
the default configuration,
+ * and an in-process model becomes a default bean only when no provider
was selected at all.
+ */
+ @BuildStep
+ void announceQuarkusLangchain4jBeans(
+ List<EmbeddingStoreBuildItem> embeddingStores,
+ List<SelectedEmbeddingModelCandidateBuildItem>
selectedEmbeddingModels,
+ List<InProcessEmbeddingBuildItem> inProcessEmbeddingModels,
+ BuildProducer<AnnouncedSyntheticBeanBuildItem> syntheticBeans) {
+ embeddingStores.forEach(store -> syntheticBeans.produce(new
AnnouncedSyntheticBeanBuildItem(EmbeddingStore.class)));
+ selectedEmbeddingModels.forEach(model -> syntheticBeans.produce(new
AnnouncedSyntheticBeanBuildItem(
+ EmbeddingModel.class,
NamedConfigUtil.isDefault(model.getConfigName()) ? null :
model.getConfigName())));
+ if (selectedEmbeddingModels.isEmpty()) {
+ inProcessEmbeddingModels
+ .forEach(model -> syntheticBeans.produce(new
AnnouncedSyntheticBeanBuildItem(EmbeddingModel.class)));
+ }
+ }
+
@BuildStep
@SuppressWarnings("unchecked")
@Record(ExecutionTime.STATIC_INIT)
diff --git a/extensions/langchain4j-embeddingstore/deployment/pom.xml
b/extensions/langchain4j-embeddingstore/deployment/pom.xml
index ea9b0fa447..58ba4e3510 100644
--- a/extensions/langchain4j-embeddingstore/deployment/pom.xml
+++ b/extensions/langchain4j-embeddingstore/deployment/pom.xml
@@ -42,6 +42,11 @@
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-langchain4j-embeddingstore</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+
<artifactId>camel-quarkus-support-langchain4j-ql4j-deployment</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit-internal</artifactId>
diff --git
a/extensions/langchain4j-embeddingstore/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/DefaultRetrievalAugmentorBuildItem.java
b/extensions/langchain4j-embeddingstore/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/DefaultRetrievalAugmentorBuildItem.java
new file mode 100644
index 0000000000..d7000b0204
--- /dev/null
+++
b/extensions/langchain4j-embeddingstore/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/DefaultRetrievalAugmentorBuildItem.java
@@ -0,0 +1,26 @@
+/*
+ * 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.embeddingstore.deployment;
+
+import io.quarkus.builder.item.SimpleBuildItem;
+
+/**
+ * Produced when the auto-detected default {@code RetrievalAugmentor} is
registered, so that its
+ * {@code @Default} store and model can be verified once the synthetic beans
exist.
+ */
+public final class DefaultRetrievalAugmentorBuildItem extends SimpleBuildItem {
+}
diff --git
a/extensions/langchain4j-embeddingstore/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/Langchain4jEmbeddingstoreRagProcessor.java
b/extensions/langchain4j-embeddingstore/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/Langchain4jEmbeddingstoreRagProcessor.java
index db96d87c9f..e019f330e7 100644
---
a/extensions/langchain4j-embeddingstore/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/Langchain4jEmbeddingstoreRagProcessor.java
+++
b/extensions/langchain4j-embeddingstore/deployment/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/Langchain4jEmbeddingstoreRagProcessor.java
@@ -19,6 +19,7 @@ package
org.apache.camel.quarkus.component.langchain4j.embeddingstore.deployment
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
@@ -33,6 +34,7 @@ import
io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import
io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.BeanInfo;
+import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
@@ -48,6 +50,7 @@ import
org.apache.camel.quarkus.component.langchain4j.embeddingstore.RagAugmento
import
org.apache.camel.quarkus.component.langchain4j.embeddingstore.RagBridgeConfig;
import
org.apache.camel.quarkus.component.langchain4j.embeddingstore.RagBridgeConfig.AugmentorConfig;
import
org.apache.camel.quarkus.component.langchain4j.embeddingstore.RagRetrievalFilterSupplier;
+import
org.apache.camel.quarkus.core.deployment.spi.AnnouncedSyntheticBeanBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelRegistryBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelRuntimeTaskBuildItem;
import org.jboss.jandex.DotName;
@@ -84,9 +87,10 @@ class Langchain4jEmbeddingstoreRagProcessor {
*/
@BuildStep
void validateRagRetrievalFilterSupplier(BeanDiscoveryFinishedBuildItem
beanDiscovery,
+ List<AnnouncedSyntheticBeanBuildItem> announcedSyntheticBeans,
RagBridgeConfig ragBridgeConfig,
BuildProducer<ValidationErrorBuildItem> validationErrors) {
- BeanCensus census = BeanCensus.of(beanDiscovery);
+ BeanCensus census = BeanCensus.of(beanDiscovery,
announcedSyntheticBeans);
if (census.filterSuppliers().isEmpty()) {
return;
}
@@ -162,20 +166,26 @@ class Langchain4jEmbeddingstoreRagProcessor {
* produces a {@code @Named("<name>")} RetrievalAugmentor backed by the
configured store. The entry
* marked {@code default=true} also serves the unqualified lookup; with a
single entry that marking
* is optional, with several it is required.</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>
+ * <li><b>Auto-detection</b> — when no config entries exist, at least one
{@code @Default}
+ * EmbeddingStore and one {@code @Default} EmbeddingModel are present, and
no RetrievalAugmentor
+ * exists yet, a default one is produced backed by those beans. A bean
qualified by a name, such
+ * as {@code @EmbeddingStoreName}, cannot back the default augmentor and
does not count. Beans
+ * other extensions register synthetically, the stores and models of
Quarkus LangChain4j among
+ * them, are invisible to bean discovery and are counted through
+ * {@link AnnouncedSyntheticBeanBuildItem}.</li>
* </ul>
*/
@BuildStep(onlyIfNot = EasyRagPresent.class)
@Record(ExecutionTime.RUNTIME_INIT)
void registerDefaultRetrievalAugmentor(
BeanDiscoveryFinishedBuildItem beanDiscovery,
+ List<AnnouncedSyntheticBeanBuildItem> announcedSyntheticBeans,
RagBridgeConfig ragBridgeConfig,
Langchain4jEmbeddingstoreRecorder recorder,
- BuildProducer<SyntheticBeanBuildItem> syntheticBeans) {
+ BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
+ BuildProducer<DefaultRetrievalAugmentorBuildItem>
defaultAugmentor) {
- BeanCensus census = BeanCensus.of(beanDiscovery);
+ BeanCensus census = BeanCensus.of(beanDiscovery,
announcedSyntheticBeans);
// Effective augmentors: one per explicit config entry. Sorted, so
that a message naming
// them reads the same on every build - SmallRye's map is not
declaration-ordered.
@@ -233,9 +243,32 @@ class Langchain4jEmbeddingstoreRagProcessor {
.setRuntimeInit()
.supplier(recorder.createRetrievalAugmentorSupplier(null,
null, null))
.done());
+ defaultAugmentor.produce(new DefaultRetrievalAugmentorBuildItem());
}
}
+ /**
+ * The auto-detected augmentor resolves its {@code @Default} store and
model on first use. An
+ * announced store carries no name, so a named-only store, such as
pgvector with
+ * {@code default-store-enabled=false}, is counted like a default one. The
beans are therefore
+ * checked at startup, once the synthetic ones are initialised: a missing
default bean is
+ * reported with the configuration that selects the named beans, as a
warning, since an
+ * application that never uses the augmentor must keep starting.
+ */
+ @BuildStep
+ @Record(ExecutionTime.RUNTIME_INIT)
+ @Consume(SyntheticBeansRuntimeInitBuildItem.class)
+ void verifyDefaultRetrievalAugmentorBeans(
+ Optional<DefaultRetrievalAugmentorBuildItem> defaultAugmentor,
+ Langchain4jEmbeddingstoreRecorder recorder,
+ BuildProducer<CamelRuntimeTaskBuildItem> runtimeTasks) {
+ if (defaultAugmentor.isEmpty()) {
+ return;
+ }
+ recorder.verifyDefaultRetrievalAugmentorBeans();
+ runtimeTasks.produce(new
CamelRuntimeTaskBuildItem("rag-default-augmentor-beans"));
+ }
+
/**
* Decides which augmentor is the unqualified default, or fails the build:
silence here would
* mean an ambiguous CDI lookup and RAG silently switched off for every AI
service.
@@ -282,11 +315,15 @@ class Langchain4jEmbeddingstoreRagProcessor {
record AugmentorDefinition(String embeddingStoreName, String
embeddingModelName, boolean markedDefault) {
}
- /** One pass over the discovered beans, answering everything the RAG
bridge decides on. */
+ /**
+ * One pass over the discovered beans and the announced synthetic ones,
answering everything
+ * the RAG bridge decides on.
+ */
record BeanCensus(int embeddingStores, int embeddingModels, boolean
retrievalAugmentor,
List<BeanInfo> filterSuppliers) {
- static BeanCensus of(BeanDiscoveryFinishedBuildItem beanDiscovery) {
+ static BeanCensus of(BeanDiscoveryFinishedBuildItem beanDiscovery,
+ List<AnnouncedSyntheticBeanBuildItem> announcedSyntheticBeans)
{
DotName embeddingStoreDN =
DotName.createSimple(EmbeddingStore.class.getName());
DotName embeddingModelDN =
DotName.createSimple(EmbeddingModel.class.getName());
DotName retrievalAugmentorDN =
DotName.createSimple(RetrievalAugmentor.class.getName());
@@ -297,12 +334,16 @@ class Langchain4jEmbeddingstoreRagProcessor {
List<BeanInfo> filterSuppliers = new ArrayList<>();
for (BeanInfo bean :
beanDiscovery.beanStream().collect(Collectors.toList())) {
+ // a store or model qualified by a name does not carry
@Default and cannot back
+ // the default augmentor; it is reachable through the explicit
configuration
+ boolean defaultBean = bean.getQualifiers().stream()
+ .anyMatch(qualifier ->
qualifier.name().equals(DotNames.DEFAULT));
for (Type type : bean.getTypes()) {
DotName typeName = type.name();
if (typeName.equals(embeddingStoreDN)) {
- embeddingStores++;
+ embeddingStores += defaultBean ? 1 : 0;
} else if (typeName.equals(embeddingModelDN)) {
- embeddingModels++;
+ embeddingModels += defaultBean ? 1 : 0;
} else if (typeName.equals(retrievalAugmentorDN)) {
retrievalAugmentor = true;
} else if
(typeName.equals(RAG_RETRIEVAL_FILTER_SUPPLIER_DOTNAME)) {
@@ -310,6 +351,20 @@ class Langchain4jEmbeddingstoreRagProcessor {
}
}
}
+ // synthetic beans are not part of bean discovery; the extensions
registering them
+ // announce them instead. A named bean does not carry @Default, so
it cannot back the
+ // default augmentor and is not counted
+ for (AnnouncedSyntheticBeanBuildItem bean :
announcedSyntheticBeans) {
+ if (!bean.isDefault()) {
+ continue;
+ }
+ DotName typeName = bean.getBeanType();
+ if (typeName.equals(embeddingStoreDN)) {
+ embeddingStores++;
+ } else if (typeName.equals(embeddingModelDN)) {
+ embeddingModels++;
+ }
+ }
return new BeanCensus(embeddingStores, embeddingModels,
retrievalAugmentor, filterSuppliers);
}
diff --git
a/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/FakeEmbeddingModel.java
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/FakeEmbeddingModel.java
new file mode 100644
index 0000000000..219cc67da5
--- /dev/null
+++
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/FakeEmbeddingModel.java
@@ -0,0 +1,45 @@
+/*
+ * 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.embeddingstore.deployment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.model.output.Response;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/** A deterministic stand-in for an embedding model: equal texts get equal
vectors. */
+@ApplicationScoped
+public class FakeEmbeddingModel implements EmbeddingModel {
+
+ @Override
+ public Response<List<Embedding>> embedAll(List<TextSegment> segments) {
+ List<Embedding> embeddings = new ArrayList<>(segments.size());
+ for (TextSegment segment : segments) {
+ int hash = segment.text().hashCode();
+ float[] vector = new float[8];
+ for (int i = 0; i < vector.length; i++) {
+ vector[i] = ((hash >>> (i * 4)) & 0xF) + 1f;
+ }
+ embeddings.add(Embedding.from(vector));
+ }
+ return Response.from(embeddings);
+ }
+}
diff --git
a/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/RagAugmentorSyntheticStoreTest.java
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/RagAugmentorSyntheticStoreTest.java
new file mode 100644
index 0000000000..077a164b77
--- /dev/null
+++
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/RagAugmentorSyntheticStoreTest.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.embeddingstore.deployment;
+
+import java.util.List;
+
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.rag.AugmentationRequest;
+import dev.langchain4j.rag.AugmentationResult;
+import dev.langchain4j.rag.RetrievalAugmentor;
+import dev.langchain4j.rag.query.Metadata;
+import dev.langchain4j.store.embedding.EmbeddingStore;
+import io.quarkiverse.langchain4j.deployment.EmbeddingStoreBuildItem;
+import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
+import io.quarkus.test.QuarkusExtensionTest;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Instance;
+import jakarta.inject.Inject;
+import org.jboss.jandex.ClassType;
+import org.jboss.jandex.ParameterizedType;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The auto-detected default augmentor must see a store that Quarkus
LangChain4j registers: such a store is a
+ * synthetic bean, invisible to bean discovery, announced through the store
extension's build item.
+ */
+class RagAugmentorSyntheticStoreTest {
+
+ @RegisterExtension
+ static final QuarkusExtensionTest CONFIG = new QuarkusExtensionTest()
+ .addBuildChainCustomizer(builder -> builder.addBuildStep(context
-> {
+ // what a Quarkus LangChain4j store extension such as pgvector
does
+
context.produce(SyntheticBeanBuildItem.configure(SyntheticStore.class)
+ .types(ClassType.create(EmbeddingStore.class),
+ ParameterizedType.create(EmbeddingStore.class,
ClassType.create(TextSegment.class)))
+ .scope(ApplicationScoped.class)
+ .defaultBean()
+ .unremovable()
+ .creator(SyntheticStoreCreator.class)
+ .done());
+ context.produce(new EmbeddingStoreBuildItem());
+ })
+ .produces(SyntheticBeanBuildItem.class)
+ .produces(EmbeddingStoreBuildItem.class)
+ .build())
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(FakeEmbeddingModel.class,
SyntheticStore.class, SyntheticStoreCreator.class));
+
+ @Inject
+ Instance<RetrievalAugmentor> augmentor;
+
+ @Inject
+ EmbeddingStore<TextSegment> store;
+
+ @Inject
+ EmbeddingModel model;
+
+ @Test
+ void defaultAugmentorRetrievesFromTheSyntheticStore() {
+ assertTrue(augmentor.isResolvable(), "a default RetrievalAugmentor
must be produced for the synthetic store");
+
+ String text = "The warranty period is twenty four months.";
+ store.add(model.embed(text).content(), TextSegment.from(text));
+
+ UserMessage question = UserMessage.from(text);
+ AugmentationResult result = augmentor.get()
+ .augment(new AugmentationRequest(question,
Metadata.from(question, "test", List.of())));
+ assertEquals(1, result.contents().size(), "the augmentor must retrieve
from the synthetic store");
+ assertEquals(text, result.contents().get(0).textSegment().text());
+ }
+}
diff --git
a/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/SyntheticStore.java
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/SyntheticStore.java
new file mode 100644
index 0000000000..52bd882893
--- /dev/null
+++
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/SyntheticStore.java
@@ -0,0 +1,27 @@
+/*
+ * 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.embeddingstore.deployment;
+
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+
+/**
+ * The store {@link RagAugmentorSyntheticStoreTest} registers synthetically.
An application class stands in for the
+ * store class a Quarkus LangChain4j store extension ships, so the generated
bean lives with the application classes.
+ */
+public class SyntheticStore extends InMemoryEmbeddingStore<TextSegment> {
+}
diff --git
a/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/SyntheticStoreCreator.java
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/SyntheticStoreCreator.java
new file mode 100644
index 0000000000..3e504d6b54
--- /dev/null
+++
b/extensions/langchain4j-embeddingstore/deployment/src/test/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/deployment/SyntheticStoreCreator.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.embeddingstore.deployment;
+
+import io.quarkus.arc.BeanCreator;
+import io.quarkus.arc.SyntheticCreationalContext;
+
+/** Creates the synthetic store of {@link RagAugmentorSyntheticStoreTest}. */
+public class SyntheticStoreCreator implements BeanCreator<SyntheticStore> {
+
+ @Override
+ public SyntheticStore create(SyntheticCreationalContext<SyntheticStore>
context) {
+ return new SyntheticStore();
+ }
+}
diff --git
a/extensions/langchain4j-embeddingstore/runtime/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/Langchain4jEmbeddingstoreRecorder.java
b/extensions/langchain4j-embeddingstore/runtime/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/Langchain4jEmbeddingstoreRecorder.java
index 41fc229a61..8ede4ac14d 100644
---
a/extensions/langchain4j-embeddingstore/runtime/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/Langchain4jEmbeddingstoreRecorder.java
+++
b/extensions/langchain4j-embeddingstore/runtime/src/main/java/org/apache/camel/quarkus/component/langchain4j/embeddingstore/Langchain4jEmbeddingstoreRecorder.java
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.function.Supplier;
import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.RetrievalAugmentor;
import dev.langchain4j.store.embedding.EmbeddingStore;
import io.quarkus.arc.Arc;
@@ -83,4 +84,26 @@ public class Langchain4jEmbeddingstoreRecorder {
String embeddingStoreName, String embeddingModelName, String
augmentorName) {
return new DefaultRetrievalAugmentorSupplier(embeddingStoreName,
embeddingModelName, augmentorName);
}
+
+ /**
+ * The auto-produced default augmentor resolves the {@code @Default} store
and model on first use. The
+ * build detected at least one bean of each type, but a store it detected
may be named rather than
+ * default, which would fail that late; so the beans are checked at
startup, once the synthetic ones
+ * exist. The check resolves without creating an instance, and it warns
rather than fails: an
+ * application that never uses the augmentor must keep starting.
+ */
+ public void verifyDefaultRetrievalAugmentorBeans() {
+ boolean store =
Arc.container().select(EMBEDDING_STORE_TYPE).isResolvable();
+ boolean model =
Arc.container().select(EmbeddingModel.class).isResolvable();
+ if (store && model) {
+ return;
+ }
+ LOG.warnf("The default RetrievalAugmentor was produced because an
EmbeddingStore and an EmbeddingModel bean"
+ + " were detected, but no unambiguous @Default %s bean
resolves, so an AI service using the augmentor"
+ + " will fail. If the beans are named, select them with"
+ + "
quarkus.camel.langchain4j.rag.augmentors.<name>.embedding-store-name and
.embedding-model-name;"
+ + " otherwise provide default beans, or opt the AI service out
with"
+ + " @RegisterAiService(retrievalAugmentor =
RegisterAiService.NoRetrievalAugmentorSupplier.class)",
+ store ? "EmbeddingModel" : "EmbeddingStore");
+ }
}