This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 3c32d125ebd0 CAMEL-24526/24528/24529/24530/24534: camel-ai fixes
backport to camel-4.22.x (#25895)
3c32d125ebd0 is described below
commit 3c32d125ebd047482b07a1f65bc4e11c90ba2f72
Author: Andrea Cosentino <[email protected]>
AuthorDate: Sun Aug 30 13:13:43 2026 +0200
CAMEL-24526/24528/24529/24530/24534: camel-ai fixes backport to
camel-4.22.x (#25895)
Straight cherry-pick backport to camel-4.22.x of five camel-ai fixes
already reviewed and merged on main:
- CAMEL-24526: camel-djl - reject the zoo tabular applications instead of
returning no-op predictors (silent wrong result)
- CAMEL-24528: camel-huggingface - surface Python inference errors in four
task predictors (errors were delivered as success)
- CAMEL-24529: camel-djl - close the loaded ZooModel when the producer
stops (native-memory leak)
- CAMEL-24530: camel-djl - close the InputStream opened from File/Path
bodies in DJLConverter (FD leak)
- CAMEL-24534: camel-qdrant - honour metadata value types instead of
casting them to String (runtime ClassCastException on numeric metadata)
Each commit is a clean cherry-pick of the merged main commit with no manual
conflict resolution.
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../apache/camel/component/djl/DJLConverter.java | 16 +++-
.../apache/camel/component/djl/DJLProducer.java | 6 ++
.../component/djl/model/AbstractPredictor.java | 9 ++
.../djl/model/ModelPredictorProducer.java | 15 ++--
.../djl/model/audio/ZooAudioPredictor.java | 7 ++
.../djl/model/cv/AbstractCvZooPredictor.java | 7 ++
.../djl/model/cv/ZooImageGenerationPredictor.java | 7 ++
.../djl/model/nlp/AbstractNlpZooPredictor.java | 7 ++
.../djl/model/nlp/ZooQuestionAnswerPredictor.java | 7 ++
.../tabular/ZooLinearRegressionPredictor.java | 32 -------
.../tabular/ZooSoftmaxRegressionPredictor.java | 32 -------
.../model/timeseries/ZooForecastingPredictor.java | 7 ++
.../camel/component/djl/DJLConverterTest.java | 49 +++++++++++
.../camel/component/djl/DJLProducerTest.java | 43 ++++++++++
.../djl/model/ModelPredictorProducerTest.java | 20 ++++-
.../tasks/QuestionAnsweringPredictor.java | 4 +
.../huggingface/tasks/SummarizationPredictor.java | 4 +
.../huggingface/tasks/TextGenerationPredictor.java | 4 +
.../huggingface/tasks/TextToImagePredictor.java | 7 ++
.../tasks/PredictorErrorHandlingTest.java | 97 ++++++++++++++++++++++
.../QdrantEmbeddingsDataTypeTransformer.java | 24 +++++-
.../QdrantEmbeddingsDataTypeTransformerTest.java | 66 +++++++++++++++
22 files changed, 391 insertions(+), 79 deletions(-)
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLConverter.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLConverter.java
index 1a841cde1745..8bcd6c895916 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLConverter.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLConverter.java
@@ -48,12 +48,16 @@ public class DJLConverter {
@Converter
public static Image toImage(File file) throws IOException {
- return toImage(new FileInputStream(file));
+ try (InputStream inputStream = new FileInputStream(file)) {
+ return toImage(inputStream);
+ }
}
@Converter
public static Image toImage(Path path) throws IOException {
- return toImage(Files.newInputStream(path));
+ try (InputStream inputStream = Files.newInputStream(path)) {
+ return toImage(inputStream);
+ }
}
@Converter
@@ -105,12 +109,16 @@ public class DJLConverter {
@Converter
public static Audio toAudio(File file) throws IOException {
- return toAudio(new FileInputStream(file));
+ try (InputStream inputStream = new FileInputStream(file)) {
+ return toAudio(inputStream);
+ }
}
@Converter
public static Audio toAudio(Path path) throws IOException {
- return toAudio(Files.newInputStream(path));
+ try (InputStream inputStream = Files.newInputStream(path)) {
+ return toAudio(inputStream);
+ }
}
@Converter
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLProducer.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLProducer.java
index d82c57fac13f..e0552a868850 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLProducer.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/DJLProducer.java
@@ -36,4 +36,10 @@ public class DJLProducer extends DefaultProducer {
public void process(Exchange exchange) throws Exception {
this.predictor.process(exchange);
}
+
+ @Override
+ protected void doStop() throws Exception {
+ super.doStop();
+ this.predictor.close();
+ }
}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/AbstractPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/AbstractPredictor.java
index 3d0d45a3faef..ea7b93cf2b73 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/AbstractPredictor.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/AbstractPredictor.java
@@ -29,6 +29,15 @@ public abstract class AbstractPredictor {
public abstract void process(Exchange exchange) throws Exception;
+ /**
+ * Releases any resources held by this predictor, such as a model loaded
from the DJL model zoo. Called when the
+ * owning producer is stopped. The default implementation does nothing;
predictors that keep a long-lived model
+ * override this method to close it.
+ */
+ public void close() {
+ // no-op by default
+ }
+
protected DJLEndpoint getEndpoint() {
return endpoint;
}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/ModelPredictorProducer.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/ModelPredictorProducer.java
index 0e61056b8d83..29b0c289b146 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/ModelPredictorProducer.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/ModelPredictorProducer.java
@@ -55,8 +55,6 @@ import
org.apache.camel.component.djl.model.nlp.ZooTextEmbeddingPredictor;
import org.apache.camel.component.djl.model.nlp.ZooTextGenerationPredictor;
import
org.apache.camel.component.djl.model.nlp.ZooTokenClassificationPredictor;
import org.apache.camel.component.djl.model.tabular.CustomTabularPredictor;
-import
org.apache.camel.component.djl.model.tabular.ZooLinearRegressionPredictor;
-import
org.apache.camel.component.djl.model.tabular.ZooSoftmaxRegressionPredictor;
import
org.apache.camel.component.djl.model.timeseries.CustomForecastingPredictor;
import org.apache.camel.component.djl.model.timeseries.ZooForecastingPredictor;
@@ -135,11 +133,14 @@ public final class ModelPredictorProducer {
return new ZooTextEmbeddingPredictor(endpoint);
}
- // Tabular
- if (LINEAR_REGRESSION.getPath().equals(applicationPath)) {
- return new ZooLinearRegressionPredictor(endpoint);
- } else if (SOFTMAX_REGRESSION.getPath().equals(applicationPath)) {
- return new ZooSoftmaxRegressionPredictor(endpoint);
+ // Tabular: the DJL model zoo does not publish tabular regression
models, and the input and
+ // output types of a tabular model are specific to the user's data, so
there is no generic zoo
+ // predictor for these applications. Users must supply their own model
and translator and use
+ // the custom variant (see getCustomPredictor /
CustomTabularPredictor) instead.
+ if (LINEAR_REGRESSION.getPath().equals(applicationPath) ||
SOFTMAX_REGRESSION.getPath().equals(applicationPath)) {
+ throw new RuntimeCamelException(
+ "Zoo models are not available for tabular application: " +
applicationPath
+ + ". Provide your own model and
translator and use the custom predictor instead.");
}
// Audio
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/audio/ZooAudioPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/audio/ZooAudioPredictor.java
index 9d369c922087..e35048a22a99 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/audio/ZooAudioPredictor.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/audio/ZooAudioPredictor.java
@@ -74,4 +74,11 @@ public class ZooAudioPredictor extends AbstractPredictor {
throw new RuntimeCamelException("Could not process input or
output", e);
}
}
+
+ @Override
+ public void close() {
+ if (model != null) {
+ model.close();
+ }
+ }
}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/AbstractCvZooPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/AbstractCvZooPredictor.java
index 914ef40c76b7..f2bf3e617c9c 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/AbstractCvZooPredictor.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/AbstractCvZooPredictor.java
@@ -55,4 +55,11 @@ public abstract class AbstractCvZooPredictor<T> extends
AbstractPredictor {
throw new RuntimeCamelException("Could not process input or
output", e);
}
}
+
+ @Override
+ public void close() {
+ if (model != null) {
+ model.close();
+ }
+ }
}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/ZooImageGenerationPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/ZooImageGenerationPredictor.java
index 9101e7fd22b4..bafc76db3a5e 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/ZooImageGenerationPredictor.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/cv/ZooImageGenerationPredictor.java
@@ -73,4 +73,11 @@ public class ZooImageGenerationPredictor extends
AbstractPredictor {
throw new RuntimeCamelException("Could not process input or
output", e);
}
}
+
+ @Override
+ public void close() {
+ if (model != null) {
+ model.close();
+ }
+ }
}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/AbstractNlpZooPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/AbstractNlpZooPredictor.java
index 64d9cfdf3509..557750e889db 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/AbstractNlpZooPredictor.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/AbstractNlpZooPredictor.java
@@ -52,4 +52,11 @@ public abstract class AbstractNlpZooPredictor<T> extends
AbstractPredictor {
throw new RuntimeCamelException("Could not process input or
output", e);
}
}
+
+ @Override
+ public void close() {
+ if (model != null) {
+ model.close();
+ }
+ }
}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/ZooQuestionAnswerPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/ZooQuestionAnswerPredictor.java
index 259d2916496e..f4ad767291bb 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/ZooQuestionAnswerPredictor.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/nlp/ZooQuestionAnswerPredictor.java
@@ -82,4 +82,11 @@ public class ZooQuestionAnswerPredictor extends
AbstractPredictor {
throw new RuntimeCamelException("Could not process input or
output", e);
}
}
+
+ @Override
+ public void close() {
+ if (model != null) {
+ model.close();
+ }
+ }
}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/tabular/ZooLinearRegressionPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/tabular/ZooLinearRegressionPredictor.java
deleted file mode 100644
index 43961f8a28bf..000000000000
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/tabular/ZooLinearRegressionPredictor.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.djl.model.tabular;
-
-import org.apache.camel.Exchange;
-import org.apache.camel.component.djl.DJLEndpoint;
-import org.apache.camel.component.djl.model.AbstractPredictor;
-
-public class ZooLinearRegressionPredictor extends AbstractPredictor {
- public ZooLinearRegressionPredictor(DJLEndpoint endpoint) {
- super(endpoint);
- }
-
- @Override
- public void process(Exchange exchange) throws Exception {
- // TODO: impl
- }
-}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/tabular/ZooSoftmaxRegressionPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/tabular/ZooSoftmaxRegressionPredictor.java
deleted file mode 100644
index ee2f684e5c58..000000000000
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/tabular/ZooSoftmaxRegressionPredictor.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.djl.model.tabular;
-
-import org.apache.camel.Exchange;
-import org.apache.camel.component.djl.DJLEndpoint;
-import org.apache.camel.component.djl.model.AbstractPredictor;
-
-public class ZooSoftmaxRegressionPredictor extends AbstractPredictor {
- public ZooSoftmaxRegressionPredictor(DJLEndpoint endpoint) {
- super(endpoint);
- }
-
- @Override
- public void process(Exchange exchange) throws Exception {
- // TODO: impl
- }
-}
diff --git
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/timeseries/ZooForecastingPredictor.java
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/timeseries/ZooForecastingPredictor.java
index c79fa3f0a9d7..6c5d04ce09ed 100644
---
a/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/timeseries/ZooForecastingPredictor.java
+++
b/components/camel-ai/camel-djl/src/main/java/org/apache/camel/component/djl/model/timeseries/ZooForecastingPredictor.java
@@ -74,4 +74,11 @@ public class ZooForecastingPredictor extends
AbstractPredictor {
throw new RuntimeCamelException("Could not process input or
output", e);
}
}
+
+ @Override
+ public void close() {
+ if (model != null) {
+ model.close();
+ }
+ }
}
diff --git
a/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/DJLConverterTest.java
b/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/DJLConverterTest.java
new file mode 100644
index 000000000000..19e74069b0ef
--- /dev/null
+++
b/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/DJLConverterTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.djl;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import ai.djl.modality.cv.Image;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DJLConverterTest {
+
+ private static final String IMAGE_RESOURCE = "/data/detect/kitten.jpg";
+
+ // Converting from a File opens a stream internally; it must be closed
(try-with-resources) so the
+ // file descriptor is not leaked per conversion. The conversion must still
return a valid Image.
+ @Test
+ void toImageFromFileReturnsImageWithoutLeakingStream() throws Exception {
+ File file = new
File(DJLConverterTest.class.getResource(IMAGE_RESOURCE).toURI());
+ Image image = DJLConverter.toImage(file);
+ assertNotNull(image);
+ assertTrue(image.getWidth() > 0 && image.getHeight() > 0);
+ }
+
+ @Test
+ void toImageFromPathReturnsImageWithoutLeakingStream() throws Exception {
+ Path path =
Path.of(DJLConverterTest.class.getResource(IMAGE_RESOURCE).toURI());
+ Image image = DJLConverter.toImage(path);
+ assertNotNull(image);
+ assertTrue(image.getWidth() > 0 && image.getHeight() > 0);
+ }
+}
diff --git
a/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/DJLProducerTest.java
b/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/DJLProducerTest.java
new file mode 100644
index 000000000000..a3e5bf58861e
--- /dev/null
+++
b/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/DJLProducerTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.djl;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+class DJLProducerTest {
+
+ // Stopping the producer must release the predictor so a zoo model does
not leak native memory across
+ // restart/redeploy (DJLProducer.doStop -> AbstractPredictor.close). For
the custom (model-less) predictor
+ // path close() is the inherited no-op, so stopping must complete without
error. The zoo predictors close
+ // their loaded model in their own close() overrides.
+ @Test
+ void stoppingProducerReleasesPredictor() {
+ DJLEndpoint endpoint = new
DJLEndpoint("djl:tabular/linear_regression", null, "tabular/linear_regression");
+ endpoint.setCamelContext(new DefaultCamelContext());
+ endpoint.setModel("MyModel");
+ endpoint.setTranslator("MyTranslator");
+
+ assertDoesNotThrow(() -> {
+ DJLProducer producer = new DJLProducer(endpoint);
+ producer.start();
+ producer.stop();
+ });
+ }
+}
diff --git
a/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/model/ModelPredictorProducerTest.java
b/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/model/ModelPredictorProducerTest.java
index a4ab4000ceb9..ac36192ccf5a 100644
---
a/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/model/ModelPredictorProducerTest.java
+++
b/components/camel-ai/camel-djl/src/test/java/org/apache/camel/component/djl/model/ModelPredictorProducerTest.java
@@ -20,6 +20,7 @@ import java.io.IOException;
import ai.djl.MalformedModelException;
import ai.djl.repository.zoo.ModelNotFoundException;
+import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.djl.DJLEndpoint;
import org.apache.camel.component.djl.model.audio.CustomAudioPredictor;
import org.apache.camel.component.djl.model.cv.CustomCvPredictor;
@@ -45,6 +46,8 @@ import org.junit.jupiter.api.Test;
import static
org.apache.camel.component.djl.model.ModelPredictorProducer.getCustomPredictor;
import static
org.apache.camel.component.djl.model.ModelPredictorProducer.getZooPredictor;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class ModelPredictorProducerTest {
@@ -88,9 +91,7 @@ class ModelPredictorProducerTest {
// No builtin zoo model available for "nlp/multiple_choice"
// No builtin zoo model available for "nlp/text_embedding"
- // Tabular
- // No builtin zoo model available for "tabular/linear_regression"
- // No builtin zoo model available for "tabular/softmax_regression"
+ // Tabular: no zoo predictor exists (see
testGetZooPredictorRejectsTabularApplications)
// Audio
// No builtin zoo model available for "audio"
@@ -100,6 +101,19 @@ class ModelPredictorProducerTest {
getZooPredictor(zooEndpoint("timeseries/forecasting",
"ai.djl.pytorch:deepar:0.0.1")));
}
+ @Test
+ void testGetZooPredictorRejectsTabularApplications() {
+ // The DJL model zoo publishes no tabular regression models and
tabular I/O types are
+ // model-specific, so the zoo predictor factory must reject these
applications with a clear
+ // error rather than returning a no-op predictor that echoes the input
as the prediction.
+ RuntimeCamelException linear =
assertThrows(RuntimeCamelException.class,
+ () -> getZooPredictor(zooEndpoint("tabular/linear_regression",
"any:artifact:0.0.1")));
+ assertTrue(linear.getMessage().contains("tabular/linear_regression"));
+ RuntimeCamelException softmax =
assertThrows(RuntimeCamelException.class,
+ () ->
getZooPredictor(zooEndpoint("tabular/softmax_regression",
"any:artifact:0.0.1")));
+
assertTrue(softmax.getMessage().contains("tabular/softmax_regression"));
+ }
+
@Test
void testGetCustomPredictor() {
var modelName = "MyModel";
diff --git
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/QuestionAnsweringPredictor.java
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/QuestionAnsweringPredictor.java
index ec07bfea7202..ec66b59a5f3d 100644
---
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/QuestionAnsweringPredictor.java
+++
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/QuestionAnsweringPredictor.java
@@ -25,6 +25,7 @@ import ai.djl.modality.Output;
import ai.djl.modality.nlp.qa.QAInput;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.camel.Exchange;
+import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.huggingface.HuggingFaceConstants;
import org.apache.camel.component.huggingface.HuggingFaceEndpoint;
@@ -116,6 +117,9 @@ public class QuestionAnsweringPredictor extends
AbstractTaskPredictor {
@Override
protected void processOutput(Exchange exchange, Output output) {
String result = output.getAsString("data");
+ if (result.contains("\"error\"")) {
+ throw new RuntimeCamelException("Python inference failed: " +
result);
+ }
exchange.getMessage().setBody(result);
exchange.getMessage().setHeader(HuggingFaceConstants.OUTPUT, result);
}
diff --git
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/SummarizationPredictor.java
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/SummarizationPredictor.java
index 09673a143721..1826a48740ce 100644
---
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/SummarizationPredictor.java
+++
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/SummarizationPredictor.java
@@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
import ai.djl.modality.Input;
import ai.djl.modality.Output;
import org.apache.camel.Exchange;
+import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.huggingface.HuggingFaceConstants;
import org.apache.camel.component.huggingface.HuggingFaceEndpoint;
@@ -105,6 +106,9 @@ public class SummarizationPredictor extends
AbstractTaskPredictor {
@Override
protected void processOutput(Exchange exchange, Output output) {
String result = output.getAsString("data");
+ if (result.contains("\"error\"")) {
+ throw new RuntimeCamelException("Python inference failed: " +
result);
+ }
exchange.getMessage().setBody(result);
exchange.getMessage().setHeader(HuggingFaceConstants.OUTPUT, result);
}
diff --git
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextGenerationPredictor.java
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextGenerationPredictor.java
index a33f3ccda1d9..582b70594203 100644
---
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextGenerationPredictor.java
+++
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextGenerationPredictor.java
@@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
import ai.djl.modality.Input;
import ai.djl.modality.Output;
import org.apache.camel.Exchange;
+import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.huggingface.HuggingFaceConstants;
import org.apache.camel.component.huggingface.HuggingFaceEndpoint;
@@ -103,6 +104,9 @@ public class TextGenerationPredictor extends
AbstractTaskPredictor {
@Override
protected void processOutput(Exchange exchange, Output output) {
String result = output.getAsString("data");
+ if (result.contains("\"error\"")) {
+ throw new RuntimeCamelException("Python inference failed: " +
result);
+ }
exchange.getMessage().setBody(result);
exchange.getMessage().setHeader(HuggingFaceConstants.OUTPUT, result);
}
diff --git
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextToImagePredictor.java
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextToImagePredictor.java
index 227c5b2e400b..a928854acd1a 100644
---
a/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextToImagePredictor.java
+++
b/components/camel-ai/camel-huggingface/src/main/java/org/apache/camel/component/huggingface/tasks/TextToImagePredictor.java
@@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
import ai.djl.modality.Input;
import ai.djl.modality.Output;
import org.apache.camel.Exchange;
+import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.huggingface.HuggingFaceEndpoint;
/**
@@ -108,6 +109,12 @@ public class TextToImagePredictor extends
AbstractTaskPredictor {
@Override
protected void processOutput(Exchange exchange, Output output) throws
Exception {
byte[] imageBytes = output.getAsBytes("data");
+ // A failed Python inference returns a JSON error payload instead of
image bytes; surface it as
+ // an error rather than serving the error text as an image.
+ String resultJson = new String(imageBytes, StandardCharsets.UTF_8);
+ if (resultJson.contains("\"error\"")) {
+ throw new RuntimeCamelException("Python inference failed: " +
resultJson);
+ }
exchange.getMessage().setBody(imageBytes);
exchange.getMessage().setHeader("Content-Type", "image/png");
}
diff --git
a/components/camel-ai/camel-huggingface/src/test/java/org/apache/camel/component/huggingface/tasks/PredictorErrorHandlingTest.java
b/components/camel-ai/camel-huggingface/src/test/java/org/apache/camel/component/huggingface/tasks/PredictorErrorHandlingTest.java
new file mode 100644
index 000000000000..eacbce07f13f
--- /dev/null
+++
b/components/camel-ai/camel-huggingface/src/test/java/org/apache/camel/component/huggingface/tasks/PredictorErrorHandlingTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.huggingface.tasks;
+
+import ai.djl.modality.Output;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.component.huggingface.HuggingFaceConfiguration;
+import org.apache.camel.component.huggingface.HuggingFaceEndpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Verifies that the task predictors whose Python script can return a JSON
error payload surface it as an error instead
+ * of treating it as a successful result.
+ */
+class PredictorErrorHandlingTest {
+
+ private static final String ERROR_PAYLOAD = "{\"error\": \"model failed to
load\"}";
+
+ private CamelContext context;
+ private HuggingFaceEndpoint endpoint;
+
+ @BeforeEach
+ void setUp() {
+ context = new DefaultCamelContext();
+ endpoint = new HuggingFaceEndpoint(null, null, new
HuggingFaceConfiguration());
+ }
+
+ @AfterEach
+ void tearDown() {
+ context.stop();
+ }
+
+ private Output outputWith(String data) {
+ Output output = new Output();
+ output.add("data", data);
+ return output;
+ }
+
+ @Test
+ void textGenerationSurfacesInferenceError() {
+ TextGenerationPredictor predictor = new
TextGenerationPredictor(endpoint);
+ Exchange exchange = new DefaultExchange(context);
+ assertThrows(RuntimeCamelException.class, () ->
predictor.processOutput(exchange, outputWith(ERROR_PAYLOAD)));
+ }
+
+ @Test
+ void textGenerationPassesThroughSuccessfulResult() throws Exception {
+ TextGenerationPredictor predictor = new
TextGenerationPredictor(endpoint);
+ Exchange exchange = new DefaultExchange(context);
+ predictor.processOutput(exchange, outputWith("a generated sentence"));
+ assertEquals("a generated sentence",
exchange.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void summarizationSurfacesInferenceError() {
+ SummarizationPredictor predictor = new
SummarizationPredictor(endpoint);
+ Exchange exchange = new DefaultExchange(context);
+ assertThrows(RuntimeCamelException.class, () ->
predictor.processOutput(exchange, outputWith(ERROR_PAYLOAD)));
+ }
+
+ @Test
+ void questionAnsweringSurfacesInferenceError() {
+ QuestionAnsweringPredictor predictor = new
QuestionAnsweringPredictor(endpoint);
+ Exchange exchange = new DefaultExchange(context);
+ assertThrows(RuntimeCamelException.class, () ->
predictor.processOutput(exchange, outputWith(ERROR_PAYLOAD)));
+ }
+
+ @Test
+ void textToImageSurfacesInferenceError() {
+ TextToImagePredictor predictor = new TextToImagePredictor(endpoint);
+ Exchange exchange = new DefaultExchange(context);
+ assertThrows(RuntimeCamelException.class, () ->
predictor.processOutput(exchange, outputWith(ERROR_PAYLOAD)));
+ }
+}
diff --git
a/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/transform/QdrantEmbeddingsDataTypeTransformer.java
b/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/transform/QdrantEmbeddingsDataTypeTransformer.java
index 3046fe09238e..6207c3b43087 100644
---
a/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/transform/QdrantEmbeddingsDataTypeTransformer.java
+++
b/components/camel-ai/camel-qdrant/src/main/java/org/apache/camel/component/qdrant/transform/QdrantEmbeddingsDataTypeTransformer.java
@@ -25,6 +25,7 @@ import io.qdrant.client.PointIdFactory;
import io.qdrant.client.ValueFactory;
import io.qdrant.client.VectorsFactory;
import io.qdrant.client.grpc.Common;
+import io.qdrant.client.grpc.JsonWithInt.Value;
import io.qdrant.client.grpc.Points;
import org.apache.camel.Message;
import org.apache.camel.ai.CamelLangchain4jAttributes;
@@ -55,10 +56,31 @@ public class QdrantEmbeddingsDataTypeTransformer extends
Transformer {
builder.putPayload("text_segment",
ValueFactory.value(text.text()));
Metadata metadata = text.metadata();
metadata.toMap()
- .forEach((key, value) -> builder.putPayload(key,
ValueFactory.value((String) value)));
+ .forEach((key, value) -> builder.putPayload(key,
toValue(value)));
}
message.setBody(builder.build());
}
+
+ /**
+ * Converts a LangChain4j metadata value to a Qdrant payload value.
Metadata is not always a String - document
+ * splitters routinely add numeric entries such as the chunk index or page
number - so the value type must be
+ * honoured instead of being blindly cast to String.
+ */
+ private static Value toValue(Object value) {
+ if (value == null) {
+ return ValueFactory.nullValue();
+ }
+ if (value instanceof Boolean booleanValue) {
+ return ValueFactory.value(booleanValue);
+ }
+ if (value instanceof Integer || value instanceof Long) {
+ return ValueFactory.value(((Number) value).longValue());
+ }
+ if (value instanceof Number number) {
+ return ValueFactory.value(number.doubleValue());
+ }
+ return ValueFactory.value(String.valueOf(value));
+ }
}
diff --git
a/components/camel-ai/camel-qdrant/src/test/java/org/apache/camel/component/qdrant/transform/QdrantEmbeddingsDataTypeTransformerTest.java
b/components/camel-ai/camel-qdrant/src/test/java/org/apache/camel/component/qdrant/transform/QdrantEmbeddingsDataTypeTransformerTest.java
new file mode 100644
index 000000000000..f6b1f34da15b
--- /dev/null
+++
b/components/camel-ai/camel-qdrant/src/test/java/org/apache/camel/component/qdrant/transform/QdrantEmbeddingsDataTypeTransformerTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.qdrant.transform;
+
+import java.util.Map;
+
+import dev.langchain4j.data.document.Metadata;
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.segment.TextSegment;
+import io.qdrant.client.grpc.JsonWithInt.Value;
+import io.qdrant.client.grpc.Points;
+import org.apache.camel.Exchange;
+import org.apache.camel.ai.CamelLangchain4jAttributes;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.DataType;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class QdrantEmbeddingsDataTypeTransformerTest {
+
+ @Test
+ void mapsMixedMetadataTypesToTypedPayload() throws Exception {
+ // A TextSegment carrying String and numeric metadata, as document
splitters routinely produce
+ // (chunk index, page number, ...). The transformer must not blindly
cast every value to String.
+ Metadata metadata = new Metadata()
+ .put("source", "doc.txt")
+ .put("index", 3)
+ .put("score", 0.75);
+ TextSegment segment = TextSegment.from("hello world", metadata);
+ Embedding embedding = new Embedding(new float[] { 0.1f, 0.2f, 0.3f });
+
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ context.start();
+ Exchange exchange = new DefaultExchange(context);
+
exchange.getMessage().setHeader(CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR,
embedding);
+ exchange.getMessage().setBody(segment);
+
+ new
QdrantEmbeddingsDataTypeTransformer().transform(exchange.getMessage(),
DataType.ANY, DataType.ANY);
+
+ Points.PointStruct point =
exchange.getMessage().getBody(Points.PointStruct.class);
+ assertThat(point).isNotNull();
+
+ Map<String, Value> payload = point.getPayloadMap();
+
assertThat(payload.get("text_segment").getStringValue()).isEqualTo("hello
world");
+
assertThat(payload.get("source").getStringValue()).isEqualTo("doc.txt");
+ assertThat(payload.get("index").getIntegerValue()).isEqualTo(3L);
+ assertThat(payload.get("score").getDoubleValue()).isEqualTo(0.75);
+ }
+ }
+}