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 a6ee358474 Fixes #8979. Bridge @EmbeddingStoreName-qualified CDI beans
into the Camel registry
a6ee358474 is described below
commit a6ee3584747dcba97abad4db2a81f6c361deeb6e
Author: Jiří Ondrušek <[email protected]>
AuthorDate: Thu Aug 13 14:54:42 2026 +0200
Fixes #8979. Bridge @EmbeddingStoreName-qualified CDI beans into the Camel
registry
At RUNTIME_INIT, scans Arc for EmbeddingStore beans with ql4j's
@EmbeddingStoreName qualifier and registers each in the Camel registry
under its qualifier name. Routes can then reference named stores as
embeddingStore=#products without manual registry binding.
Such a store is typically declared only for route use and never injected
anywhere in Java, so ArC removed it as unused and the scan found nothing.
An UnremovableBeanBuildItem now retains any bean carrying the qualifier.
The test store is qualified with @EmbeddingStoreName only, with no @Named:
CDI @Named beans reach the Camel registry on their own, so a store with
both qualifiers cannot show whether the bridge did anything.
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../extensions/langchain4j-embeddingstore.adoc | 19 ++++++++
.../SupportQuarkusLangchain4jProcessor.java | 29 ++++++++++++
.../langchain4j/QuarkusLangchain4jRecorder.java | 52 ++++++++++++++++++++++
.../runtime/src/main/doc/usage.adoc | 14 ++++++
.../ragbridge/it/RagBridgeProducers.java | 11 +++++
.../ragbridge/it/RagBridgeResource.java | 14 ++++++
.../ragbridge/it/ExistingAugmentorProducer.java | 1 +
.../langchain4j/ragbridge/it/RagBridgeTest.java | 52 ++++++++++++++++++++++
8 files changed, 192 insertions(+)
diff --git
a/docs/modules/ROOT/pages/reference/extensions/langchain4j-embeddingstore.adoc
b/docs/modules/ROOT/pages/reference/extensions/langchain4j-embeddingstore.adoc
index 107fc4387e..70ebefe845 100644
---
a/docs/modules/ROOT/pages/reference/extensions/langchain4j-embeddingstore.adoc
+++
b/docs/modules/ROOT/pages/reference/extensions/langchain4j-embeddingstore.adoc
@@ -44,6 +44,25 @@ ifeval::[{doc-show-user-guide-link} == true]
Check the xref:user-guide/index.adoc[User guide] for more information about
writing Camel Quarkus applications.
endif::[]
+[id="extensions-langchain4j-embeddingstore-usage"]
+== Usage
+[id="extensions-langchain4j-embeddingstore-usage-named-embedding-stores-from-quarkus-langchain4j"]
+=== Named embedding stores from Quarkus LangChain4j
+
+When the
https://docs.quarkiverse.io/quarkus-langchain4j/dev/index.html[Quarkus
LangChain4j] extensions are present, any `EmbeddingStore` CDI bean qualified
with `@EmbeddingStoreName` is automatically bound into the Camel registry under
its qualifier value.
+This covers both stores declared through Quarkus LangChain4j named
configuration (for example `quarkus.langchain4j.pgvector.products.\*`) and
stores produced by your own `@Produces` methods.
+
+Such a store can be referenced from a route by name, with no manual registry
binding:
+
+[source,java]
+----
+from("direct:ingest-products")
+
.to("langchain4j-embeddingstore:products?embeddingStore=#products&embeddingModel=#embeddingModel");
+----
+
+A store only referenced from routes does not need to be injected anywhere in
Java code; it is retained and instantiated lazily on first use. If the Camel
registry already resolves a different `EmbeddingStore` under the same name (for
example a `@Named` bean), that existing bean keeps winning lookups and a
warning is logged at startup.
+
+
[id="extensions-langchain4j-embeddingstore-quarkus-langchain4j-bom"]
== LangChain4j usage
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 84dddef6b8..9cfe643323 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
@@ -34,11 +34,13 @@ 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.SyntheticBeansRuntimeInitBuildItem;
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;
+import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
@@ -58,6 +60,8 @@ import
org.apache.camel.quarkus.component.support.langchain4j.CamelAiToolsInterc
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.apache.camel.quarkus.core.deployment.spi.CamelRegistryBuildItem;
+import org.apache.camel.quarkus.core.deployment.spi.CamelRuntimeTaskBuildItem;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
@@ -78,6 +82,8 @@ class SupportQuarkusLangchain4jProcessor {
.createSimple("io.quarkiverse.langchain4j.RegisterAiService");
private static final DotName CAMEL_AI_TOOLS_DOTNAME = DotName
.createSimple("org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools");
+ private static final DotName EMBEDDING_STORE_NAME_DOTNAME = DotName
+ .createSimple("io.quarkiverse.langchain4j.EmbeddingStoreName");
private static final Logger LOG =
Logger.getLogger(SupportQuarkusLangchain4jProcessor.class);
@@ -236,6 +242,29 @@ class SupportQuarkusLangchain4jProcessor {
.build());
}
+ // A store declared only for use from a Camel route is never injected
anywhere in Java,
+ // so ArC would remove it as unused and registerNamedEmbeddingStores would
find nothing.
+ @BuildStep
+ UnremovableBeanBuildItem retainNamedEmbeddingStores() {
+ return new UnremovableBeanBuildItem(bean -> bean.getQualifiers()
+ .stream()
+ .anyMatch(qualifier ->
qualifier.name().equals(EMBEDDING_STORE_NAME_DOTNAME)));
+ }
+
+ // Bridges @EmbeddingStoreName-qualified CDI beans into the Camel registry
so routes
+ // can reference them by name (e.g. embeddingStore=#products) without
manual binding.
+ // Stores declared via Quarkus LangChain4j configuration (pgvector, redis,
...) are
+ // runtime-init synthetic beans, so the scan must not run before ArC
initializes them.
+ @BuildStep
+ @Record(ExecutionTime.RUNTIME_INIT)
+ @Consume(SyntheticBeansRuntimeInitBuildItem.class)
+ CamelRuntimeTaskBuildItem registerNamedEmbeddingStores(
+ QuarkusLangchain4jRecorder recorder,
+ CamelRegistryBuildItem registry) {
+ recorder.registerNamedEmbeddingStores(registry.getRegistry());
+ return new CamelRuntimeTaskBuildItem("named-embedding-stores");
+ }
+
@BuildStep
void validateAndRegisterAiServices(
CombinedIndexBuildItem indexBuildItem,
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 b7d8fa23f2..9c72c20d1a 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
@@ -16,23 +16,75 @@
*/
package org.apache.camel.quarkus.component.support.langchain4j;
+import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
+import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
+import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.guardrail.Guardrail;
import dev.langchain4j.rag.RetrievalAugmentor;
+import dev.langchain4j.store.embedding.EmbeddingStore;
+import io.quarkus.arc.Arc;
+import io.quarkus.arc.InstanceHandle;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.spi.Bean;
+import jakarta.enterprise.util.TypeLiteral;
+import org.apache.camel.spi.Registry;
import org.jboss.logging.Logger;
@Recorder
public class QuarkusLangchain4jRecorder {
+ private static final Logger LOG =
Logger.getLogger(QuarkusLangchain4jRecorder.class);
+ private static final String EMBEDDING_STORE_NAME_CLASS =
"io.quarkiverse.langchain4j.EmbeddingStoreName";
+ @SuppressWarnings("serial")
+ private static final TypeLiteral<EmbeddingStore<TextSegment>>
EMBEDDING_STORE_TYPE = new TypeLiteral<>() {
+ };
+
public void setCamelAiToolTagMap(Map<String, String> tagMap) {
CamelAiToolProvider.TAG_MAP.putAll(tagMap);
}
+ public void registerNamedEmbeddingStores(RuntimeValue<Registry>
camelRegistry) {
+ List<InstanceHandle<EmbeddingStore<TextSegment>>> handles =
Arc.container()
+ .listAll(EMBEDDING_STORE_TYPE, Any.Literal.INSTANCE);
+
+ Registry registry = camelRegistry.getValue();
+ for (InstanceHandle<EmbeddingStore<TextSegment>> handle : handles) {
+ Bean<?> bean = handle.getBean();
+ if (bean == null) {
+ continue;
+ }
+ for (Annotation qualifier : bean.getQualifiers()) {
+ if
(EMBEDDING_STORE_NAME_CLASS.equals(qualifier.annotationType().getName())) {
+ try {
+ String name = (String) qualifier.annotationType()
+ .getMethod("value").invoke(qualifier);
+ if (name != null && !name.isEmpty()) {
+ // The identity check keeps a bean carrying both
@Named("x") and
+ // @EmbeddingStoreName("x") from triggering a
spurious warning
+ EmbeddingStore<?> existing =
registry.lookupByNameAndType(name, EmbeddingStore.class);
+ if (existing != null && existing != handle.get()) {
+ LOG.warnf(
+ "The Camel registry already resolves a
different EmbeddingStore under the name \"%s\"; lookups by that name may not
return the @EmbeddingStoreName(\"%s\") bean",
+ name, name);
+ }
+ // Bind via supplier so a store never referenced
by a route is not instantiated
+ registry.bind(name, EmbeddingStore.class,
handle::get);
+ LOG.debugf("Registered @EmbeddingStoreName(\"%s\")
in Camel registry", name);
+ }
+ } catch (ReflectiveOperationException e) {
+ LOG.warnf(e, "Failed to extract name from
@EmbeddingStoreName qualifier");
+ }
+ }
+ }
+ }
+ }
+
public RuntimeValue<Guardrail<?, ?>>
instantiateGuardrails(Class<Guardrail<?, ?>> guardrailClass) {
try {
return new
RuntimeValue<>(guardrailClass.getConstructor().newInstance());
diff --git
a/extensions/langchain4j-embeddingstore/runtime/src/main/doc/usage.adoc
b/extensions/langchain4j-embeddingstore/runtime/src/main/doc/usage.adoc
new file mode 100644
index 0000000000..46609a802e
--- /dev/null
+++ b/extensions/langchain4j-embeddingstore/runtime/src/main/doc/usage.adoc
@@ -0,0 +1,14 @@
+=== Named embedding stores from Quarkus LangChain4j
+
+When the
https://docs.quarkiverse.io/quarkus-langchain4j/dev/index.html[Quarkus
LangChain4j] extensions are present, any `EmbeddingStore` CDI bean qualified
with `@EmbeddingStoreName` is automatically bound into the Camel registry under
its qualifier value.
+This covers both stores declared through Quarkus LangChain4j named
configuration (for example `quarkus.langchain4j.pgvector.products.\*`) and
stores produced by your own `@Produces` methods.
+
+Such a store can be referenced from a route by name, with no manual registry
binding:
+
+[source,java]
+----
+from("direct:ingest-products")
+
.to("langchain4j-embeddingstore:products?embeddingStore=#products&embeddingModel=#embeddingModel");
+----
+
+A store only referenced from routes does not need to be injected anywhere in
Java code; it is retained and instantiated lazily on first use. If the Camel
registry already resolves a different `EmbeddingStore` under the same name (for
example a `@Named` bean), that existing bean keeps winning lookups and a
warning is logged at startup.
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
index d5f22ecc38..29ac7c7dac 100644
---
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
@@ -51,6 +51,17 @@ public class RagBridgeProducers {
return new InMemoryEmbeddingStore<>();
}
+ // Deliberately carries no @Named qualifier. CDI @Named beans end up in
the Camel
+ // registry on their own, so a store that has both qualifiers cannot tell
us whether
+ // the @EmbeddingStoreName registry bridge did anything. This one is only
reachable
+ // through the bridge.
+ @Produces
+ @Singleton
+ @EmbeddingStoreName("qualifier-only")
+ EmbeddingStore<TextSegment> qualifierOnlyEmbeddingStore() {
+ return new InMemoryEmbeddingStore<>();
+ }
+
@Produces
@Singleton
EmbeddingModel embeddingModel() {
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
index 8cd27ec5a2..b0ef892923 100644
---
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
@@ -17,6 +17,7 @@
package org.apache.camel.quarkus.component.langchain4j.ragbridge.it;
import dev.langchain4j.rag.RetrievalAugmentor;
+import dev.langchain4j.store.embedding.EmbeddingStore;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -24,8 +25,10 @@ import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
+import org.apache.camel.CamelContext;
import org.apache.camel.ProducerTemplate;
@Path("/rag-bridge")
@@ -41,6 +44,9 @@ public class RagBridgeResource {
@Inject
ProducerTemplate producerTemplate;
+ @Inject
+ CamelContext camelContext;
+
@Inject
RagAiService aiService;
@@ -76,6 +82,14 @@ public class RagBridgeResource {
return "ingested";
}
+ @GET
+ @Path("/registry/embedding-store/{name}")
+ @Produces(MediaType.TEXT_PLAIN)
+ public boolean isEmbeddingStoreInRegistry(@PathParam("name") String name) {
+ EmbeddingStore<?> store =
camelContext.getRegistry().lookupByNameAndType(name, EmbeddingStore.class);
+ return store != null;
+ }
+
@POST
@Path("/ask")
@Consumes(MediaType.TEXT_PLAIN)
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
index 214ac192fe..7ba70c8b28 100644
---
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
@@ -25,6 +25,7 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
+//located in *test* packages, because the test does not have (deliberately)
the native *IT
@ApplicationScoped
@IfBuildProperty(name = "test.existing-augmentor", stringValue = "true",
enableIfMissing = false)
public class ExistingAugmentorProducer {
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
index d65e08dc10..92ea5ec6d1 100644
---
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
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
/**
* Tests the auto-detection path: no augmentor config is set, but
EmbeddingStore
@@ -64,4 +65,55 @@ class RagBridgeTest {
.statusCode(200)
.body(containsString("Apache Camel"));
}
+
+ @Test
+ void namedEmbeddingStoreIsInCamelRegistry() {
+ RestAssured.given()
+ .get("/rag-bridge/registry/embedding-store/products")
+ .then()
+ .statusCode(200)
+ .body(is("true"));
+ }
+
+ /**
+ * Exercises the {@code @EmbeddingStoreName} registry bridge in isolation.
The
+ * {@code qualifier-only} store carries no {@code @Named} qualifier, so
CDI does not
+ * put it in the Camel registry by itself - the only thing that can bind
it under
+ * that name is {@code
QuarkusLangchain4jRecorder.registerNamedEmbeddingStores()}.
+ * Unlike {@link #namedEmbeddingStoreIsInCamelRegistry()}, this fails if
the bridge
+ * is removed.
+ */
+ @Test
+ void qualifierOnlyEmbeddingStoreIsBridgedIntoCamelRegistry() {
+ RestAssured.given()
+ .get("/rag-bridge/registry/embedding-store/qualifier-only")
+ .then()
+ .statusCode(200)
+ .body(is("true"));
+ }
+
+ @Test
+ void unknownEmbeddingStoreIsNotInCamelRegistry() {
+ RestAssured.given()
+ .get("/rag-bridge/registry/embedding-store/nonexistent")
+ .then()
+ .statusCode(200)
+ .body(is("false"));
+ }
+
+ @Test
+ void defaultAugmentorDoesNotSeeDataInOtherStore() {
+ RestAssured.given()
+ .body("Quarkus is a supersonic subatomic Java framework")
+ .post("/rag-bridge/ingest-products")
+ .then()
+ .statusCode(200);
+
+ RestAssured.given()
+ .body("Tell me about Java frameworks")
+ .post("/rag-bridge/ask")
+ .then()
+ .statusCode(200)
+ .body(not(containsString("Quarkus")));
+ }
}