This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26208-spring-ai-embeddings-list
in repository https://gitbox.apache.org/repos/asf/camel.git

commit caea45dbc14b83decc3379cfc75a9c51060a8ea1
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 00:23:30 2026 +0100

    CAMEL-26208: camel-spring-ai-embeddings - a List body always gives a List 
of vectors, and its elements are converted to text
    
    A List body with one text gave a single float[] instead of a List<float[]>, 
so the
    output shape depended on the list size. A List body now always gives a 
List<float[]>
    with the CamelSpringAiEmbeddings and CamelSpringAiEmbeddingInputTexts 
headers.
    
    A List body was cast to List<String> unchecked, so a List of other objects 
failed
    with a ClassCastException inside Spring AI. Each element is now converted 
to a
    String, and a null element fails with an IllegalArgumentException. An empty 
List
    gives an empty List without calling the model (Ollama rejects an empty 
request).
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../catalog/components/spring-ai-embeddings.json   |  4 +-
 .../docs/spring-ai-embeddings-component.adoc       |  6 ++-
 .../springai/embeddings/spring-ai-embeddings.json  |  4 +-
 .../main/docs/spring-ai-embeddings-component.adoc  |  6 ++-
 .../embeddings/SpringAiEmbeddingsEndpoint.java     | 10 ++--
 .../embeddings/SpringAiEmbeddingsProducer.java     | 25 ++++++++--
 .../SpringAiEmbeddingsComponentTest.java           | 57 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    | 11 +++++
 8 files changed, 109 insertions(+), 14 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 6b33fa8447b1..e182c253f1ef 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,8 +38,8 @@
     "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 is converted to text (a Map or a 
POJO as its toString()), and no body fails" },
-    "producerOut": { "carriers": [ "list" ], "description": "The vector as a 
float[] for one text, also for a List with one text, and a List of float[] for 
a List of more texts. The Embedding and the input text are in headers, which 
spring-ai-vector-store reads" }
+    "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" },
+    "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": {
     "embeddingId": { "index": 0, "kind": "path", "displayName": "Embedding 
Id", "group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The id" },
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 ecd88fd3fca8..99ab8537b98c 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,6 +50,9 @@ 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
+with an `IllegalArgumentException`.
+
 The embedding vectors are returned in the message body, with additional 
metadata available in headers.
 
 === Single Text Example
@@ -145,7 +148,8 @@ YAML::
 ----
 ====
 
-When processing a List<String>, the component returns:
+When processing a List<String>, the component returns the following, also for 
a List with a single text. An empty
+List gives an empty List, without calling the embedding model.
 
 * **Message Body**: List of embedding vectors as `List<float[]>`
 * **Headers**:
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 6b33fa8447b1..e182c253f1ef 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,8 +38,8 @@
     "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 is converted to text (a Map or a 
POJO as its toString()), and no body fails" },
-    "producerOut": { "carriers": [ "list" ], "description": "The vector as a 
float[] for one text, also for a List with one text, and a List of float[] for 
a List of more texts. The Embedding and the input text are in headers, which 
spring-ai-vector-store reads" }
+    "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" },
+    "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": {
     "embeddingId": { "index": 0, "kind": "path", "displayName": "Embedding 
Id", "group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The id" },
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 ecd88fd3fca8..99ab8537b98c 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,6 +50,9 @@ 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
+with an `IllegalArgumentException`.
+
 The embedding vectors are returned in the message body, with additional 
metadata available in headers.
 
 === Single Text Example
@@ -145,7 +148,8 @@ YAML::
 ----
 ====
 
-When processing a List<String>, the component returns:
+When processing a List<String>, the component returns the following, also for 
a List with a single text. An empty
+List gives an empty List, without calling the embedding model.
 
 * **Message Body**: List of embedding vectors as `List<float[]>`
 * **Headers**:
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 a0e3683f2a24..e82a65c38365 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
@@ -38,12 +38,12 @@ import org.apache.camel.support.DefaultEndpoint;
                      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 is"
-                                                + " converted to text (a Map 
or a POJO as its toString()), and no body"
-                                                + " fails"),
+                                  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"),
            producerOut = @BodyType(value = Carrier.LIST,
-                                   description = "The vector as a float[] for 
one text, also for a List with one text,"
-                                                 + " and a List of float[] for 
a List of more texts. The Embedding and"
+                                   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"))
 public class SpringAiEmbeddingsEndpoint extends DefaultEndpoint {
     @Metadata(required = true)
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 f844951a8dcc..a3a64b8710ea 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
@@ -46,9 +46,28 @@ public class SpringAiEmbeddingsProducer extends 
DefaultProducer {
         // Support both single String and List<String> as input
         List<String> inputTexts;
         Object body = message.getBody();
+        boolean batch = body instanceof List;
 
-        if (body instanceof List) {
-            inputTexts = (List<String>) body;
+        if (body instanceof List<?> list) {
+            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"));
+                }
+                inputTexts.add(text);
+            }
+            if (inputTexts.isEmpty()) {
+                // nothing to embed, so do not call the model
+                message.setBody(new ArrayList<float[]>());
+                message.setHeader(SpringAiEmbeddingsHeaders.EMBEDDINGS, new 
ArrayList<Embedding>());
+                message.setHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS, 
inputTexts);
+                return;
+            }
         } else {
             String str = message.getBody(String.class);
             if (str != null) {
@@ -65,7 +84,7 @@ public class SpringAiEmbeddingsProducer extends 
DefaultProducer {
                 EmbeddingOptions.builder().build()));
 
         if (response.getResults() != null && !response.getResults().isEmpty()) 
{
-            if (inputTexts.size() == 1) {
+            if (!batch) {
                 Embedding embedding = response.getResults().get(0);
                 message.setBody(embedding.getOutput());
                 message.setHeader(SpringAiEmbeddingsHeaders.EMBEDDING_INDEX, 
embedding.getIndex());
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 3fe09376371d..61472572c4a4 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
@@ -19,8 +19,10 @@ package org.apache.camel.component.springai.embeddings;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
@@ -35,6 +37,8 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 public class SpringAiEmbeddingsComponentTest extends CamelTestSupport {
 
+    private final List<List<String>> requests = new CopyOnWriteArrayList<>();
+
     @Override
     protected CamelContext createCamelContext() throws Exception {
         CamelContext context = super.createCamelContext();
@@ -46,6 +50,7 @@ public class SpringAiEmbeddingsComponentTest extends 
CamelTestSupport {
         EmbeddingModel embeddingModel = new EmbeddingModel() {
             @Override
             public EmbeddingResponse call(EmbeddingRequest request) {
+                requests.add(request.getInstructions());
                 List<Embedding> embeddings = new ArrayList<>();
                 int index = 0;
                 for (String text : request.getInstructions()) {
@@ -134,4 +139,56 @@ public class SpringAiEmbeddingsComponentTest extends 
CamelTestSupport {
         assertThat(vectors.get(0)).hasSameSizeAs(vectors.get(1));
         assertThat(vectors.get(0)).hasSameSizeAs(vectors.get(2));
     }
+
+    @Test
+    public void testListOfOneTextGivesList() {
+        Message result = fluentTemplate.to("spring-ai-embeddings:one")
+                .withBody(List.of("hello"))
+                .request(Message.class);
+
+        assertThat(result.getBody()).isInstanceOf(List.class);
+        List<float[]> vectors = result.getBody(List.class);
+        assertThat(vectors).hasSize(1);
+        assertThat(vectors.get(0)).containsExactly(0.1f, 0.2f, 0.3f, 0.4f, 
0.5f);
+
+        assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS, 
List.class)).containsExactly("hello");
+        assertThat(result.getHeader(SpringAiEmbeddingsHeaders.EMBEDDINGS, 
List.class)).hasSize(1);
+        
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXT)).isNull();
+    }
+
+    @Test
+    public void testListOfNonStringsIsConvertedToText() {
+        Message result = fluentTemplate.to("spring-ai-embeddings:convert")
+                .withBody(List.of(42, new StringBuilder("hello")))
+                .request(Message.class);
+
+        assertThat(requests).containsExactly(List.of("42", "hello"));
+        List<float[]> vectors = result.getBody(List.class);
+        assertThat(vectors).hasSize(2);
+        assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS, 
List.class)).containsExactly("42", "hello");
+    }
+
+    @Test
+    public void testListWithNullElementFails() {
+        Exchange exchange
+                = template.send("spring-ai-embeddings:null", e -> 
e.getMessage().setBody(Arrays.asList("hello", null)));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Element 1 of the List body");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    public void testEmptyListGivesEmptyList() {
+        Message result = fluentTemplate.to("spring-ai-embeddings:empty")
+                .withBody(new ArrayList<String>())
+                .request(Message.class);
+
+        assertThat(result.getBody()).isInstanceOf(List.class);
+        assertThat(result.getBody(List.class)).isEmpty();
+        assertThat(result.getHeader(SpringAiEmbeddingsHeaders.EMBEDDINGS, 
List.class)).isEmpty();
+        assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS, 
List.class)).isEmpty();
+        assertThat(requests).isEmpty();
+    }
 }
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 10f5a47c1b89..72594f2371be 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
@@ -804,6 +804,17 @@ CamelContext of that provider. It now closes each of them 
only when no other `JC
 it returned the closed cache, whose operations throw `IllegalStateException`. 
This matters for code that calls a
 `JCacheManager` (for example one created by a custom `JCacheManagerFactory`) 
after it was closed.
 
+=== camel-spring-ai-embeddings - a List body always gives a List of vectors
+
+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.
+
 === camel-spring-batch - the CamelSpringBatchJobName header needs 
jobFromHeader=true
 
 The producer now takes the job name from the `CamelSpringBatchJobName` header 
only when the endpoint has

Reply via email to