This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26205-langchain4j-embeddings-body in repository https://gitbox.apache.org/repos/asf/camel.git
commit 32b5c4b30581f2c7dea08f43670a15ec7a4afded Author: Claus Ibsen <[email protected]> AuthorDate: Tue Nov 3 00:24:13 2026 +0100 CAMEL-26205: camel-langchain4j-embeddings - embed bytes, stream and file bodies as text, and remove the headers of an earlier call The producer read the body as a TextSegment, and only a String converted to one, so a byte[], InputStream or file consumer body failed with no type conversion. A body, and each element of a List body, is now used as is when it is a TextSegment, converted by a TextSegment type converter when there is one, and otherwise read as text. The single path left an EMBEDDINGS header from an earlier batch call on the same exchange, so a later langchain4j-embeddingstore ADD took the batch path and stored the old batch. Each call now removes the result headers of an earlier call (the batch ones, the single ones, the token counts, the finish reason and the request model) before it sets its own. The TEXT_SEGMENT header javaType had a leading space, which went into the catalog. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../catalog/components/langchain4j-embeddings.json | 4 +- .../docs/langchain4j-embeddings-component.adoc | 11 ++ .../embeddings/langchain4j-embeddings.json | 4 +- .../docs/langchain4j-embeddings-component.adoc | 11 ++ .../embeddings/LangChain4jEmbeddings.java | 2 +- .../embeddings/LangChain4jEmbeddingsEndpoint.java | 10 +- .../embeddings/LangChain4jEmbeddingsHeaders.java | 2 +- .../embeddings/LangChain4jEmbeddingsProducer.java | 44 ++++- .../embeddings/LangChain4jEmbeddingsBodyTest.java | 189 +++++++++++++++++++++ ...angChain4jEmbeddingsEndpointBuilderFactory.java | 2 +- 10 files changed, 264 insertions(+), 15 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json index 892805beec83..663b96a6157d 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json @@ -47,11 +47,11 @@ "CamelLangChain4jEmbeddingsEmbedding": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "dev.langchain4j.data.embedding.Embedding", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Embedding representation of a text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDING" }, "CamelLangChain4jEmbeddingsEmbeddings": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<dev.langchain4j.data.embedding.Embedding>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "List of embeddings from a batch embedAll operation", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDINGS" }, "CamelLangChain4jEmbeddingsVector": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "float[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A dense vector embedding of a text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#VECTOR" }, - "CamelLangChain4jEmbeddingsTextSegment": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": " dev.langchain4j.data.segment.TextSegment", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A TextSegment representation of the vector embedding input text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT" }, + "CamelLangChain4jEmbeddingsTextSegment": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "dev.langchain4j.data.segment.TextSegment", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A TextSegment representation of the vector embedding input text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT" }, "CamelLangChain4jEmbeddingsTextSegments": { "index": 10, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "List of text segments from a batch embedAll operation", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENTS" } }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "pojo", "list" ], "description": "The text to embed as a String or a TextSegment, or a non-empty List of them to embed in one batch. Bytes, a stream, a file, a Map and no body fail: convert the body to a String first" }, + "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "pojo", "list" ], "description": "The text to embed as a String or a TextSegment, or bytes, a stream or a file read as text; or a non-empty List of them to embed in one batch. Another body is embedded as its toString(), and no body fails" }, "producerOut": { "carriers": [ "unchanged" ], "description": "The body is kept. The vector goes in the CamelLangChain4jEmbeddingsVector header as a float array, with the Embedding and the TextSegment in the CamelLangChain4jEmbeddingsEmbedding and CamelLangChain4jEmbeddingsTextSegment headers; a List body gives Lists of them in the CamelLangChain4jEmbeddingsEmbeddings and CamelLangChain4jEmbeddingsTextSegments headers instead" } }, "properties": { diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc index 4fd7aa0d8cf2..932839d7b77b 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc @@ -225,6 +225,17 @@ YAML:: ---- ==== +=== The text to embed + +The body is the text to embed: a `String` or a LangChain4j `TextSegment`. Bytes, a stream and a file (such as the +body of a `file` consumer) are read as text, so a `from("file:...")` route needs no `convertBodyTo(String.class)`. +A non-empty `List` of them is embedded in one batch. + +The body is kept. A single text sets the `CamelLangChain4jEmbeddingsEmbedding`, `CamelLangChain4jEmbeddingsVector` +and `CamelLangChain4jEmbeddingsTextSegment` headers, and a `List` the `CamelLangChain4jEmbeddingsEmbeddings` and +`CamelLangChain4jEmbeddingsTextSegments` headers. Each call removes the headers of an earlier call on the same +exchange, so a single text after a batch is not stored as the old batch by a later embedding store `ADD`. + === Integration with Vector Stores The LangChain4j Embeddings component works seamlessly with vector databases for RAG (Retrieval-Augmented Generation) workflows. diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json b/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json index 892805beec83..663b96a6157d 100644 --- a/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json +++ b/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json @@ -47,11 +47,11 @@ "CamelLangChain4jEmbeddingsEmbedding": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "dev.langchain4j.data.embedding.Embedding", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Embedding representation of a text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDING" }, "CamelLangChain4jEmbeddingsEmbeddings": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<dev.langchain4j.data.embedding.Embedding>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "List of embeddings from a batch embedAll operation", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDINGS" }, "CamelLangChain4jEmbeddingsVector": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "float[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A dense vector embedding of a text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#VECTOR" }, - "CamelLangChain4jEmbeddingsTextSegment": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": " dev.langchain4j.data.segment.TextSegment", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A TextSegment representation of the vector embedding input text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT" }, + "CamelLangChain4jEmbeddingsTextSegment": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "dev.langchain4j.data.segment.TextSegment", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A TextSegment representation of the vector embedding input text", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT" }, "CamelLangChain4jEmbeddingsTextSegments": { "index": 10, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<dev.langchain4j.data.segment.TextSegment>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "List of text segments from a batch embedAll operation", "constantName": "org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENTS" } }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "pojo", "list" ], "description": "The text to embed as a String or a TextSegment, or a non-empty List of them to embed in one batch. Bytes, a stream, a file, a Map and no body fail: convert the body to a String first" }, + "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "pojo", "list" ], "description": "The text to embed as a String or a TextSegment, or bytes, a stream or a file read as text; or a non-empty List of them to embed in one batch. Another body is embedded as its toString(), and no body fails" }, "producerOut": { "carriers": [ "unchanged" ], "description": "The body is kept. The vector goes in the CamelLangChain4jEmbeddingsVector header as a float array, with the Embedding and the TextSegment in the CamelLangChain4jEmbeddingsEmbedding and CamelLangChain4jEmbeddingsTextSegment headers; a List body gives Lists of them in the CamelLangChain4jEmbeddingsEmbeddings and CamelLangChain4jEmbeddingsTextSegments headers instead" } }, "properties": { diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/main/docs/langchain4j-embeddings-component.adoc b/components/camel-ai/camel-langchain4j-embeddings/src/main/docs/langchain4j-embeddings-component.adoc index 4fd7aa0d8cf2..932839d7b77b 100644 --- a/components/camel-ai/camel-langchain4j-embeddings/src/main/docs/langchain4j-embeddings-component.adoc +++ b/components/camel-ai/camel-langchain4j-embeddings/src/main/docs/langchain4j-embeddings-component.adoc @@ -225,6 +225,17 @@ YAML:: ---- ==== +=== The text to embed + +The body is the text to embed: a `String` or a LangChain4j `TextSegment`. Bytes, a stream and a file (such as the +body of a `file` consumer) are read as text, so a `from("file:...")` route needs no `convertBodyTo(String.class)`. +A non-empty `List` of them is embedded in one batch. + +The body is kept. A single text sets the `CamelLangChain4jEmbeddingsEmbedding`, `CamelLangChain4jEmbeddingsVector` +and `CamelLangChain4jEmbeddingsTextSegment` headers, and a `List` the `CamelLangChain4jEmbeddingsEmbeddings` and +`CamelLangChain4jEmbeddingsTextSegments` headers. Each call removes the headers of an earlier call on the same +exchange, so a single text after a batch is not stored as the old batch by a later embedding store `ADD`. + === Integration with Vector Stores The LangChain4j Embeddings component works seamlessly with vector databases for RAG (Retrieval-Augmented Generation) workflows. diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddings.java b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddings.java index a4efbb28fcc3..903cb2453803 100644 --- a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddings.java +++ b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddings.java @@ -50,7 +50,7 @@ public final class LangChain4jEmbeddings { public static final String VECTOR = CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR; @Metadata(description = "A TextSegment representation of the vector embedding input text", - javaType = " dev.langchain4j.data.segment.TextSegment") + javaType = "dev.langchain4j.data.segment.TextSegment") public static final String TEXT_SEGMENT = CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_TEXT_SEGMENT; } } diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsEndpoint.java b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsEndpoint.java index 3671438bbd53..c4db2c4997c8 100644 --- a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsEndpoint.java +++ b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsEndpoint.java @@ -37,10 +37,12 @@ import org.apache.camel.support.DefaultEndpoint; syntax = "langchain4j-embeddings:embeddingId", producerOnly = true, category = { Category.AI }, headersClass = LangChain4jEmbeddingsHeaders.class) -@BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.POJO, Carrier.LIST }, - description = "The text to embed as a String or a TextSegment, or a non-empty List of" - + " them to embed in one batch. Bytes, a stream, a file, a Map and no body" - + " fail: convert the body to a String first"), +@BodyTypes(producerIn = @BodyType(value = { + Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE, Carrier.POJO, + Carrier.LIST }, + description = "The text to embed as a String or a TextSegment, or bytes, a stream or a" + + " file read as text; or a non-empty List of them to embed in one batch." + + " Another body is embedded as its toString(), and no body fails"), producerOut = @BodyType(value = Carrier.UNCHANGED, description = "The body is kept. The vector goes in the CamelLangChain4jEmbeddingsVector" + " header as a float array, with the Embedding and the TextSegment in the" diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java index c18162c78aa5..a7de5a1907ac 100644 --- a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java +++ b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java @@ -49,7 +49,7 @@ public class LangChain4jEmbeddingsHeaders { public static final String VECTOR = CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR; @Metadata(description = "A TextSegment representation of the vector embedding input text", - javaType = " dev.langchain4j.data.segment.TextSegment") + javaType = "dev.langchain4j.data.segment.TextSegment") public static final String TEXT_SEGMENT = CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_TEXT_SEGMENT; @Metadata(description = "List of text segments from a batch embedAll operation", diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java index 308ad54740ed..1f76d6705915 100644 --- a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java +++ b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java @@ -25,6 +25,8 @@ import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.output.Response; import org.apache.camel.Exchange; import org.apache.camel.Message; +import org.apache.camel.NoTypeConversionAvailableException; +import org.apache.camel.TypeConverter; import org.apache.camel.component.ai.observability.GenAiErrorSupport; import org.apache.camel.component.ai.observability.GenAiModelResolver; import org.apache.camel.component.ai.observability.GenAiObservability; @@ -64,7 +66,7 @@ public class LangChain4jEmbeddingsProducer extends DefaultProducer { } private void processSingle(Exchange exchange, EmbeddingModel model, Message message) throws Exception { - final TextSegment in = exchange.getMessage().getMandatoryBody(TextSegment.class); + final TextSegment in = toTextSegment(exchange, message.getMandatoryBody()); GenAiObservationContext observationContext = GenAiObservationContext.builder() .operationName(GenAiOperationName.EMBEDDINGS) .system(GenAiModelResolver.resolveSystem(exchange.getContext().getClassResolver(), model)) @@ -91,11 +93,9 @@ public class LangChain4jEmbeddingsProducer extends DefaultProducer { private void processBatch(Exchange exchange, EmbeddingModel model, Message message, List<Object> bodyList) throws Exception { - // Convert each element to TextSegment using the type converter List<TextSegment> segments = new ArrayList<>(bodyList.size()); for (Object item : bodyList) { - TextSegment segment = exchange.getContext().getTypeConverter().mandatoryConvertTo(TextSegment.class, item); - segments.add(segment); + segments.add(toTextSegment(exchange, item)); } GenAiObservationContext observationContext = GenAiObservationContext.builder() @@ -123,8 +123,43 @@ public class LangChain4jEmbeddingsProducer extends DefaultProducer { } } + /** + * Converts a body, or an element of a List body, to the TextSegment to embed. A TextSegment is used as is, a type + * converter to TextSegment is used when there is one, and other bodies, such as bytes, a stream or a file, are read + * as text. + */ + private static TextSegment toTextSegment(Exchange exchange, Object value) throws NoTypeConversionAvailableException { + if (value instanceof TextSegment segment) { + return segment; + } + TypeConverter tc = exchange.getContext().getTypeConverter(); + TextSegment segment = tc.tryConvertTo(TextSegment.class, exchange, value); + if (segment != null) { + return segment; + } + return TextSegment.from(tc.mandatoryConvertTo(String.class, exchange, value)); + } + + /** + * Removes the headers of an earlier call, so a header that this call does not set does not keep a stale value: an + * EMBEDDINGS header from an earlier batch would make a later embedding store ADD store the old batch. + */ + private static void removeResultHeaders(Message message) { + message.removeHeader(LangChain4jEmbeddingsHeaders.REQUEST_MODEL); + message.removeHeader(LangChain4jEmbeddingsHeaders.FINISH_REASON); + message.removeHeader(LangChain4jEmbeddingsHeaders.INPUT_TOKEN_COUNT); + message.removeHeader(LangChain4jEmbeddingsHeaders.OUTPUT_TOKEN_COUNT); + message.removeHeader(LangChain4jEmbeddingsHeaders.TOTAL_TOKEN_COUNT); + message.removeHeader(LangChain4jEmbeddingsHeaders.VECTOR); + message.removeHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT); + message.removeHeader(LangChain4jEmbeddingsHeaders.EMBEDDING); + message.removeHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS); + message.removeHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS); + } + private static void populateHeaders( Message message, Response<Embedding> result, TextSegment textSegment, String requestModel) { + removeResultHeaders(message); if (requestModel != null) { message.setHeader(LangChain4jEmbeddingsHeaders.REQUEST_MODEL, requestModel); } @@ -146,6 +181,7 @@ public class LangChain4jEmbeddingsProducer extends DefaultProducer { private static void populateBatchHeaders( Message message, Response<List<Embedding>> result, List<TextSegment> segments, String requestModel) { + removeResultHeaders(message); if (requestModel != null) { message.setHeader(LangChain4jEmbeddingsHeaders.REQUEST_MODEL, requestModel); } diff --git a/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsBodyTest.java b/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsBodyTest.java new file mode 100644 index 000000000000..fb02722232b0 --- /dev/null +++ b/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsBodyTest.java @@ -0,0 +1,189 @@ +/* + * 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.component.langchain4j.embeddings; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; +import org.apache.camel.CamelContext; +import org.apache.camel.CamelExecutionException; +import org.apache.camel.Exchange; +import org.apache.camel.InvalidPayloadException; +import org.apache.camel.Message; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.camel.util.IOHelper; +import org.junit.jupiter.api.Test; + +import static org.apache.camel.test.junit6.TestSupport.deleteDirectory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LangChain4jEmbeddingsBodyTest extends CamelTestSupport { + + private static final String FILE_DIR = "target/data/langchain4j-embeddings-body"; + + @Override + protected void doPreSetup() throws Exception { + deleteDirectory(FILE_DIR); + } + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + + LangChain4jEmbeddingsComponent component + = context.getComponent(LangChain4jEmbeddings.SCHEME, LangChain4jEmbeddingsComponent.class); + component.getConfiguration().setEmbeddingModel(new AllMiniLmL6V2EmbeddingModel()); + + return context; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("file:" + FILE_DIR + "?initialDelay=0&delete=true") + .to("langchain4j-embeddings:file") + .to("mock:file"); + + from("direct:batchThenSingle") + .to("langchain4j-embeddings:batch") + .setBody(constant("hello")) + .to("langchain4j-embeddings:single"); + + from("direct:singleThenBatch") + .to("langchain4j-embeddings:single") + .setBody(constant(List.of("hello", "world"))) + .to("langchain4j-embeddings:batch"); + } + }; + } + + @Test + void bytesBodyIsEmbeddedAsText() { + Message result = embed("hello world".getBytes(StandardCharsets.UTF_8)); + + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, TextSegment.class).text()) + .isEqualTo("hello world"); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.VECTOR, float[].class)) + .isEqualTo(embed("hello world").getHeader(LangChain4jEmbeddingsHeaders.VECTOR, float[].class)); + } + + @Test + void streamBodyIsEmbeddedAsText() { + Message result = embed(new ByteArrayInputStream("hello stream".getBytes(StandardCharsets.UTF_8))); + + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, TextSegment.class).text()) + .isEqualTo("hello stream"); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, Embedding.class)).isNotNull(); + } + + @Test + void textSegmentBodyIsKept() { + TextSegment segment = TextSegment.from("hello segment"); + + Message result = embed(segment); + + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT)).isSameAs(segment); + } + + @Test + void fileConsumerBodyIsEmbeddedAsText() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:file"); + mock.expectedMessageCount(1); + + template.sendBodyAndHeader("file:" + FILE_DIR, "hello file", Exchange.FILE_NAME, "hello.txt"); + + mock.assertIsSatisfied(); + Message result = mock.getReceivedExchanges().get(0).getMessage(); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, TextSegment.class).text()) + .isEqualTo("hello file"); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, Embedding.class)).isNotNull(); + } + + @Test + @SuppressWarnings("unchecked") + void batchOfBytesAndStreamsIsEmbeddedAsText() { + InputStream stream = new ByteArrayInputStream("from a stream".getBytes(StandardCharsets.UTF_8)); + + Message result = embed(List.of("from a string", "from bytes".getBytes(StandardCharsets.UTF_8), stream)); + + List<TextSegment> segments = result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS, List.class); + assertThat(segments).extracting(TextSegment::text) + .containsExactly("from a string", "from bytes", "from a stream"); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS, List.class)).hasSize(3); + } + + @Test + void noBodyFails() { + assertThatThrownBy(() -> template.requestBody("langchain4j-embeddings:test", (Object) null)) + .isInstanceOf(CamelExecutionException.class) + .hasCauseInstanceOf(InvalidPayloadException.class); + } + + @Test + void singleAfterBatchRemovesTheBatchHeaders() { + Message result = fluentTemplate.to("direct:batchThenSingle") + .withBody(List.of("one", "two")) + .request(Message.class); + + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS)).isNull(); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS)).isNull(); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, Embedding.class)).isNotNull(); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, TextSegment.class).text()) + .isEqualTo("hello"); + } + + @Test + void batchAfterSingleRemovesTheSingleHeaders() { + Message result = fluentTemplate.to("direct:singleThenBatch") + .withBody("one") + .request(Message.class); + + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING)).isNull(); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.VECTOR)).isNull(); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT)).isNull(); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS, List.class)).hasSize(2); + assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS, List.class)).hasSize(2); + } + + @Test + void headerJavaTypesHaveNoLeadingSpace() throws Exception { + String json; + try (InputStream is = getClass().getResourceAsStream( + "/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json")) { + json = IOHelper.loadText(is); + } + + assertThat(json).contains("\"javaType\": \"dev.langchain4j.data.segment.TextSegment\"") + .doesNotContain("\"javaType\": \" "); + } + + private Message embed(Object body) { + return fluentTemplate.to("langchain4j-embeddings:test") + .withBody(body) + .request(Message.class); + } +} diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java index 9553d5c0f2f4..dcd39b288c6d 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java @@ -512,7 +512,7 @@ public interface LangChain4jEmbeddingsEndpointBuilderFactory { /** * A TextSegment representation of the vector embedding input text. * - * The option is a: {@code dev.langchain4j.data.segment.TextSegment} + * The option is a: {@code dev.langchain4j.data.segment.TextSegment} * type. * * Group: producer
