This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 9e2b7ec3ba11 CAMEL-26512: camel-spring-ai-embeddings - fail a Map or
POJO body, or List element, instead of embedding its toString() (#29108)
9e2b7ec3ba11 is described below
commit 9e2b7ec3ba11c35b31d224cb7ee5339cd58721b4
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 4 19:59:46 2026 +0100
CAMEL-26512: camel-spring-ai-embeddings - fail a Map or POJO body, or List
element, instead of embedding its toString() (#29108)
- CAMEL-26512: camel-spring-ai-embeddings - fail a Map or POJO body, or
List element, instead of embedding its toString()
- CAMEL-26512: camel-spring-ai-embeddings - the batch doc example sends a
List in XML and YAML
- CAMEL-26512: camel-spring-ai-embeddings - name the char[], ByteBuf and
converter bodies in the upgrade guide, and say only a java.util.List is batched
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../catalog/components/spring-ai-embeddings.json | 2 +-
.../docs/spring-ai-embeddings-component.adoc | 11 +-
.../springai/embeddings/spring-ai-embeddings.json | 2 +-
.../main/docs/spring-ai-embeddings-component.adoc | 11 +-
.../embeddings/SpringAiEmbeddingsEndpoint.java | 10 +-
.../embeddings/SpringAiEmbeddingsProducer.java | 82 ++++++++--
.../SpringAiEmbeddingsComponentTest.java | 180 ++++++++++++++++++++-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 19 ++-
8 files changed, 281 insertions(+), 36 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
index e182c253f1ef..d918a7f2730e 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
@@ -38,7 +38,7 @@
"CamelSpringAiEmbeddingInputTexts": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "description": "List of input texts
that were embedded", "constantName":
"org.apache.camel.component.springai.embeddings.SpringAiEmbeddingsHeaders#INPUT_TEXTS"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "description": "The text to embed,
or a List of texts for a batch. Another body, and each element of a List, is
converted to text (a Map or a POJO as its toString()); no body, or a null
element in a List, fails" },
+ "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "list" ],
"description": "The text to embed: a String, bytes, a stream or a file read as
text, or a Spring AI Document as its text; or a java.util.List of them for a
batch (a Set or an array of them fails). Another body or List element, such as
a Map or a POJO, fails, as do no body and a null element: convert it to text
first" },
"producerOut": { "carriers": [ "list" ], "description": "The vector as a
float[] for one text, and a List of float[] for a List of texts, also for a
List with one text or none. The Embedding and the input text are in headers,
which spring-ai-vector-store reads" }
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
index 99ab8537b98c..a2036cd24c7f 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
@@ -50,7 +50,10 @@ The component accepts two types of input:
* **Single text**: A String containing the text to embed
* **Batch processing**: A List<String> containing multiple texts to embed at
once
-Another body, and each element of a List, is converted to a String. A `null`
element in a List fails the exchange
+Bytes, a stream, a `Reader` or a file is read as text, and a Spring AI
`Document` is embedded as its text, as a body
+and as an element of a List. Any other body or List element, such as a `Map`,
a POJO or a number, fails the exchange
+with an `InvalidPayloadException` instead of being embedded as its
`toString()`: turn it into text first, with
+`convertBodyTo(String.class)` or a marshal to JSON, say. No body, or a `null`
element in a List, fails the exchange
with an `IllegalArgumentException`.
The embedding vectors are returned in the message body, with additional
metadata available in headers.
@@ -124,7 +127,7 @@ XML::
<route>
<from uri="direct:batch"/>
<setBody>
- <constant>["text 1", "text 2", "text 3"]</constant>
+ <simple>${list(text 1,text 2,text 3)}</simple>
</setBody>
<to uri="spring-ai-embeddings:myEmbedding"/>
<log message="Number of embeddings: ${body.size()}"/>
@@ -140,7 +143,9 @@ YAML::
uri: direct:batch
steps:
- setBody:
- constant: '["text 1", "text 2", "text 3"]'
+ expression:
+ simple:
+ expression: "${list(text 1,text 2,text 3)}"
- to:
uri: spring-ai-embeddings:myEmbedding
- log:
diff --git
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
index e182c253f1ef..d918a7f2730e 100644
---
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
+++
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
@@ -38,7 +38,7 @@
"CamelSpringAiEmbeddingInputTexts": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "description": "List of input texts
that were embedded", "constantName":
"org.apache.camel.component.springai.embeddings.SpringAiEmbeddingsHeaders#INPUT_TEXTS"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "description": "The text to embed,
or a List of texts for a batch. Another body, and each element of a List, is
converted to text (a Map or a POJO as its toString()); no body, or a null
element in a List, fails" },
+ "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "list" ],
"description": "The text to embed: a String, bytes, a stream or a file read as
text, or a Spring AI Document as its text; or a java.util.List of them for a
batch (a Set or an array of them fails). Another body or List element, such as
a Map or a POJO, fails, as do no body and a null element: convert it to text
first" },
"producerOut": { "carriers": [ "list" ], "description": "The vector as a
float[] for one text, and a List of float[] for a List of texts, also for a
List with one text or none. The Embedding and the input text are in headers,
which spring-ai-vector-store reads" }
},
"properties": {
diff --git
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
index 99ab8537b98c..a2036cd24c7f 100644
---
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
+++
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
@@ -50,7 +50,10 @@ The component accepts two types of input:
* **Single text**: A String containing the text to embed
* **Batch processing**: A List<String> containing multiple texts to embed at
once
-Another body, and each element of a List, is converted to a String. A `null`
element in a List fails the exchange
+Bytes, a stream, a `Reader` or a file is read as text, and a Spring AI
`Document` is embedded as its text, as a body
+and as an element of a List. Any other body or List element, such as a `Map`,
a POJO or a number, fails the exchange
+with an `InvalidPayloadException` instead of being embedded as its
`toString()`: turn it into text first, with
+`convertBodyTo(String.class)` or a marshal to JSON, say. No body, or a `null`
element in a List, fails the exchange
with an `IllegalArgumentException`.
The embedding vectors are returned in the message body, with additional
metadata available in headers.
@@ -124,7 +127,7 @@ XML::
<route>
<from uri="direct:batch"/>
<setBody>
- <constant>["text 1", "text 2", "text 3"]</constant>
+ <simple>${list(text 1,text 2,text 3)}</simple>
</setBody>
<to uri="spring-ai-embeddings:myEmbedding"/>
<log message="Number of embeddings: ${body.size()}"/>
@@ -140,7 +143,9 @@ YAML::
uri: direct:batch
steps:
- setBody:
- constant: '["text 1", "text 2", "text 3"]'
+ expression:
+ simple:
+ expression: "${list(text 1,text 2,text 3)}"
- to:
uri: spring-ai-embeddings:myEmbedding
- log:
diff --git
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
index e82a65c38365..3276d8b93391 100644
---
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
+++
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
@@ -37,10 +37,12 @@ import org.apache.camel.support.DefaultEndpoint;
syntax = "spring-ai-embeddings:embeddingId", producerOnly = true,
category = {
Category.AI
}, headersClass = SpringAiEmbeddingsHeaders.class)
-@BodyTypes(producerIn = @BodyType(value = Carrier.ANY,
- description = "The text to embed, or a List
of texts for a batch. Another body, and"
- + " each element of a List, is
converted to text (a Map or a POJO as its"
- + " toString()); no body, or a
null element in a List, fails"),
+@BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES,
Carrier.STREAM, Carrier.FILE, Carrier.LIST },
+ description = "The text to embed: a String,
bytes, a stream or a file read as text, or a"
+ + " Spring AI Document as its
text; or a java.util.List of them for a batch (a"
+ + " Set or an array of them
fails). Another body or List element, such as a"
+ + " Map or a POJO, fails, as
do no body and a null element: convert it to"
+ + " text first"),
producerOut = @BodyType(value = Carrier.LIST,
description = "The vector as a float[] for
one text, and a List of float[] for a List"
+ " of texts, also for a List
with one text or none. The Embedding and"
diff --git
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
index a3a64b8710ea..4f9d1bbda7d6 100644
---
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
+++
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
@@ -16,12 +16,23 @@
*/
package org.apache.camel.component.springai.embeddings;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
import org.apache.camel.Message;
+import org.apache.camel.NoTypeConversionAvailableException;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
import org.apache.camel.support.DefaultProducer;
+import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingOptions;
@@ -43,7 +54,7 @@ public class SpringAiEmbeddingsProducer extends
DefaultProducer {
final Message message = exchange.getMessage();
final EmbeddingModel model =
getEndpoint().getConfiguration().getEmbeddingModel();
- // Support both single String and List<String> as input
+ // Support a single text and a List of texts as input
List<String> inputTexts;
Object body = message.getBody();
boolean batch = body instanceof List;
@@ -52,14 +63,10 @@ public class SpringAiEmbeddingsProducer extends
DefaultProducer {
inputTexts = new ArrayList<>(list.size());
for (int i = 0; i < list.size(); i++) {
Object element = list.get(i);
- String text = element != null
- ?
exchange.getContext().getTypeConverter().convertTo(String.class, exchange,
element) : null;
- if (text == null) {
- throw new IllegalArgumentException(
- "Element " + i + " of the List body must be text,
but was: "
- + (element != null ?
element.getClass().getName() : "null"));
+ if (element == null) {
+ throw new IllegalArgumentException("Element " + i + " of
the List body must be text, but was: null");
}
- inputTexts.add(text);
+ inputTexts.add(toText(exchange, element, i));
}
if (inputTexts.isEmpty()) {
// nothing to embed, so do not call the model
@@ -69,14 +76,10 @@ public class SpringAiEmbeddingsProducer extends
DefaultProducer {
return;
}
} else {
- String str = message.getBody(String.class);
- if (str != null) {
- inputTexts = List.of(str);
- } else {
- throw new IllegalArgumentException(
- "Message body must be a String or List<String>, but
was: "
- + (body != null ?
body.getClass().getName() : "null"));
+ if (body == null) {
+ throw new IllegalArgumentException("Message body must be a
String or List<String>, but was: null");
}
+ inputTexts = List.of(toText(exchange, body, -1));
}
final EmbeddingResponse response = model.call(new EmbeddingRequest(
@@ -109,4 +112,53 @@ public class SpringAiEmbeddingsProducer extends
DefaultProducer {
message.setHeader(SpringAiEmbeddingsHeaders.EMBEDDING_METADATA,
response.getMetadata());
}
}
+
+ /**
+ * The text to embed of a body, or of an element of a List body: a Spring
AI Document as its text, and text, bytes,
+ * a stream, a Reader or a file read as text. Any other value, such as a
Map or a POJO, fails as an
+ * InvalidPayloadException instead of being embedded as its toString().
+ *
+ * @param index the index of the element in a List body, or -1 for the
body itself
+ */
+ private static String toText(Exchange exchange, Object value, int index)
+ throws InvalidPayloadException, NoTypeConversionAvailableException
{
+ if (value instanceof Document document) {
+ if (document.getText() == null) {
+ throw new InvalidPayloadException(
+ exchange, String.class, exchange.getMessage(),
+ listElement(value,
+ index) + "spring-ai-embeddings embeds the text
of a Document, but the Document has no text");
+ }
+ return document.getText();
+ }
+ if (!isReadableAsText(value)) {
+ throw new InvalidPayloadException(
+ exchange, String.class, exchange.getMessage(),
+ listElement(value,
+ index) + "spring-ai-embeddings embeds text, bytes,
a stream, a Reader, a file or a Spring AI Document;"
+ + " convert
the body to text before the endpoint, such"
+ + " as with
convertBodyTo(String) or a marshal to JSON");
+ }
+ return
exchange.getContext().getTypeConverter().mandatoryConvertTo(String.class,
exchange, value);
+ }
+
+ /** A value Camel converts to its text, rather than to the toString() of
an object. */
+ private static boolean isReadableAsText(Object value) {
+ return value instanceof CharSequence || value instanceof byte[] ||
value instanceof ByteBuffer
+ || value instanceof InputStream || value instanceof Reader ||
value instanceof StreamCache
+ || value instanceof ByteArrayOutputStream
+ || value instanceof File || value instanceof Path || value
instanceof WrappedFile;
+ }
+
+ /**
+ * Where a value that fails is in the body, for the message of the
exception: the index and the type of an element
+ * of a List body, or nothing for the body itself. Only built when the
value fails.
+ */
+ private static String listElement(Object value, int index) {
+ if (index < 0) {
+ return "";
+ }
+ return "the element at index " + index + " of the List body is "
+ + (value == null ? "null" : value.getClass().getName()) + "; ";
+ }
}
diff --git
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
index 61472572c4a4..aa4222f0117f 100644
---
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
+++
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
@@ -16,22 +16,31 @@
*/
package org.apache.camel.component.springai.embeddings;
+import java.io.ByteArrayInputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.camel.CamelContext;
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.test.junit6.CamelTestSupport;
import org.junit.jupiter.api.Test;
+import org.springframework.ai.content.Media;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
+import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -140,6 +149,24 @@ public class SpringAiEmbeddingsComponentTest extends
CamelTestSupport {
assertThat(vectors.get(0)).hasSameSizeAs(vectors.get(2));
}
+ @Test
+ public void testSimpleListFunctionGivesBatch() throws Exception {
+ // the batch example of the component doc (XML and YAML)
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:batch")
+ .setBody(simple("${list(text 1,text 2,text 3)}"))
+ .to("spring-ai-embeddings:myEmbedding");
+ }
+ });
+
+ Message result =
fluentTemplate.to("direct:batch").request(Message.class);
+
+ assertThat(requests).containsExactly(List.of("text 1", "text 2", "text
3"));
+ assertThat(result.getBody(List.class)).hasSize(3);
+ }
+
@Test
public void testListOfOneTextGivesList() {
Message result = fluentTemplate.to("spring-ai-embeddings:one")
@@ -157,15 +184,145 @@ public class SpringAiEmbeddingsComponentTest extends
CamelTestSupport {
}
@Test
- public void testListOfNonStringsIsConvertedToText() {
+ public void testListOfTextCarriersIsConvertedToText() {
Message result = fluentTemplate.to("spring-ai-embeddings:convert")
- .withBody(List.of(42, new StringBuilder("hello")))
+ .withBody(List.of(new StringBuilder("hello"),
"bytes".getBytes(StandardCharsets.UTF_8),
+ new Document("document text")))
.request(Message.class);
- assertThat(requests).containsExactly(List.of("42", "hello"));
+ assertThat(requests).containsExactly(List.of("hello", "bytes",
"document text"));
List<float[]> vectors = result.getBody(List.class);
- assertThat(vectors).hasSize(2);
- assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS,
List.class)).containsExactly("42", "hello");
+ assertThat(vectors).hasSize(3);
+ assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS,
List.class))
+ .containsExactly("hello", "bytes", "document text");
+ }
+
+ @Test
+ public void testDocumentBodyEmbedsItsText() {
+ Message result = fluentTemplate.to("spring-ai-embeddings:document")
+ .withBody(new Document("document text", Map.of("source",
"test")))
+ .request(Message.class);
+
+ assertThat(requests).containsExactly(List.of("document text"));
+ assertThat(result.getBody(float[].class)).containsExactly(0.1f, 0.2f,
0.3f, 0.4f, 0.5f);
+
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXT)).isEqualTo("document
text");
+ }
+
+ @Test
+ public void testDocumentWithoutTextFails() {
+ Document media = new Document(
+ new Media(MimeTypeUtils.IMAGE_PNG,
URI.create("https://example.com/image.png")),
+ Map.of());
+ Exchange exchange = template.send("spring-ai-embeddings:media", e ->
e.getMessage().setBody(media));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("the Document has no text");
+ assertThat(requests).isEmpty();
+ }
+
+ @Test
+ public void testStreamBodyIsReadAsText() {
+ Message result = fluentTemplate.to("spring-ai-embeddings:stream")
+ .withBody(new ByteArrayInputStream("streamed
text".getBytes(StandardCharsets.UTF_8)))
+ .request(Message.class);
+
+ assertThat(requests).containsExactly(List.of("streamed text"));
+
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXT)).isEqualTo("streamed
text");
+ }
+
+ @Test
+ public void testMapBodyFails() {
+ Exchange exchange = template.send("spring-ai-embeddings:map",
+ e -> e.getMessage().setBody(Map.of("a", 1)));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("convert the body to text before the
endpoint");
+ assertThat(requests).isEmpty();
+ }
+
+ @Test
+ public void testPojoBodyFails() {
+ Exchange exchange = template.send("spring-ai-embeddings:pojo",
+ e -> e.getMessage().setBody(new Pojo("hello")));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("convert the body to text before the
endpoint");
+ assertThat(requests).isEmpty();
+ }
+
+ @Test
+ public void testListWithMapElementFails() {
+ Exchange exchange = template.send("spring-ai-embeddings:listmap",
+ e -> e.getMessage().setBody(List.of("hello", Map.of("a", 1))));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("the element at index 1 of the List body
is java.util.")
+ .hasMessageContaining("convert the body to text before the
endpoint");
+ assertThat(requests).isEmpty();
+ }
+
+ @Test
+ public void testListWithNumberElementFails() {
+ Exchange exchange = template.send("spring-ai-embeddings:listnumber",
+ e -> e.getMessage().setBody(List.of("hello", 42)));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("the element at index 1 of the List body
is java.lang.Integer");
+ assertThat(requests).isEmpty();
+ }
+
+ @Test
+ public void testCharArrayBodyFailsAndConvertBodyToStringEmbedsIt() throws
Exception {
+ // the upgrade guide: a body Camel converts to text but that is not
text, bytes, a stream or a file, such as a
+ // char[], fails; convertBodyTo(String.class) before the endpoint
embeds its text
+ Exchange exchange = template.send("spring-ai-embeddings:chars",
+ e -> e.getMessage().setBody("hello".toCharArray()));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("convert the body to text before the
endpoint");
+ assertThat(requests).isEmpty();
+
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:chars")
+ .convertBodyTo(String.class)
+ .to("spring-ai-embeddings:chars");
+ }
+ });
+
+ Message result =
fluentTemplate.to("direct:chars").withBody("hello".toCharArray()).request(Message.class);
+
+ assertThat(requests).containsExactly(List.of("hello"));
+
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXT)).isEqualTo("hello");
+ }
+
+ @Test
+ public void testSetBodyIsNotBatchedAndFails() {
+ Exchange exchange = template.send("spring-ai-embeddings:set",
+ e -> e.getMessage().setBody(new LinkedHashSet<>(List.of("a",
"b"))));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("convert the body to text before the
endpoint");
+ assertThat(requests).isEmpty();
+ }
+
+ @Test
+ public void testStringArrayBodyIsNotBatchedAndFails() {
+ Exchange exchange = template.send("spring-ai-embeddings:array",
+ e -> e.getMessage().setBody(new String[] { "a", "b" }));
+
+ assertThat(exchange.getException())
+ .isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("convert the body to text before the
endpoint");
+ assertThat(requests).isEmpty();
}
@Test
@@ -191,4 +348,17 @@ public class SpringAiEmbeddingsComponentTest extends
CamelTestSupport {
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS,
List.class)).isEmpty();
assertThat(requests).isEmpty();
}
+
+ private static final class Pojo {
+ private final String text;
+
+ Pojo(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public String toString() {
+ return "Pojo[text=" + text + "]";
+ }
+ }
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d6da752a051d..22b29bdfcb8c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1120,16 +1120,27 @@ stream failed after part of the request had already
been sent.
The JSON of the event is now written before the request is sent, with a
`Content-Length` instead of a chunked request
body. A body or a header that cannot be written as JSON fails the exchange
without sending anything to Splunk.
-=== camel-spring-ai-embeddings - a List body always gives a List of vectors
+=== camel-spring-ai-embeddings - a List body always gives a List of vectors,
and a Map or POJO body fails
A `List` body with one text now gives a `List<float[]>` with one vector, as a
`List` of more texts does, and sets the
`CamelSpringAiEmbeddings` and `CamelSpringAiEmbeddingInputTexts` headers.
Before, it gave a single `float[]` and set the
`CamelSpringAiEmbedding`, `CamelSpringAiEmbeddingIndex` and
`CamelSpringAiEmbeddingInputText` headers, as for a `String`
body. A `String` body still gives a single `float[]`.
-Each element of a `List` body is now converted to a `String`, and a `null`
element fails with an
-`IllegalArgumentException`. Before, a `List` of other objects failed with a
`ClassCastException` in the embedding
-model. An empty `List` now gives an empty `List` without calling the embedding
model.
+Each element of a `List` body is now read as text, and a `null` element fails
with an `IllegalArgumentException`.
+Before, a `List` of other objects failed with a `ClassCastException` in the
embedding model. An empty `List` now gives
+an empty `List` without calling the embedding model.
+
+The body, and each element of a `List` body, must now be text, bytes, a
stream, a `Reader`, a file or a Spring AI
+`Document` (its text is embedded). Any other value, such as a `Map`, a POJO or
a number, fails the exchange with an
+`InvalidPayloadException`. Before, such a body was embedded as its
`toString()`, for example `{a=1, b=2}`, and a
+`Document` too. Convert the body to text before the endpoint, with
`convertBodyTo(String.class)` or a marshal to JSON.
+
+This also applies to bodies that Camel can convert to text but that are not in
the list above, and that were embedded
+as their text before: a `char[]`, a Netty `ByteBuf` (for example from
netty-http with `disableStreamCache=true`), and a
+custom type with its own type converter to `String`. A `List` with such an
element fails too, as do a `Set` and an
+array of texts (only a `java.util.List` is embedded as a batch). Add
`convertBodyTo(String.class)` before the
+endpoint for such a body, or convert each element of the `List` to a `String`.
=== camel-spring-ai-vector-store - DELETE takes the ids from the body first,
and the embeddings headers go with a vector body