gnodet-bot commented on code in PR #26613:
URL: https://github.com/apache/camel/pull/26613#discussion_r4052758815


##########
components/camel-ai/camel-langchain4j-core/src/main/java/org/apache/camel/component/langchain4j/core/LangChain4jModelFactory.java:
##########
@@ -0,0 +1,311 @@
+/*
+ * 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.core;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import org.apache.camel.CamelContext;
+import org.apache.camel.PropertyBindingException;
+import org.apache.camel.support.PropertyBindingSupport;
+import org.apache.camel.util.PropertiesHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Creates a LangChain4j {@link ChatModel} or {@link EmbeddingModel} from a 
provider name and the options every provider
+ * has (model name, base URL, API key, temperature, timeout), so a model is 
configured in properties or on the endpoint
+ * without a bean of the provider's class (CAMEL-24820):
+ *
+ * <pre>
+ * camel.component.langchain4j-chat.provider = ollama
+ * camel.component.langchain4j-chat.model-name = qwen2.5
+ * camel.component.langchain4j-chat.base-url = http://localhost:11434
+ * </pre>
+ *
+ * The provider names the model class of a LangChain4j module ({@code ollama} 
is
+ * {@code dev.langchain4j.model.ollama.OllamaChatModel}), which is resolved 
through the Camel class resolver, so Camel
+ * JBang downloads the module ({@code dev.langchain4j:langchain4j-ollama}) as 
it does for any known class. The class is
+ * created through its {@code builder()}: the common options are set with the 
name the builder uses for them
+ * ({@code apiKey} is {@code accessToken} on Hugging Face and {@code 
gitHubToken} on GitHub Models, {@code modelName} is
+ * {@code deploymentName} on Azure OpenAI), and the provider-specific options 
of {@link ModelSpec#modelProperties()} are
+ * set as they are. An option the builder does not have is an error naming 
what the builder accepts. The provider can
+ * also be the fully qualified class name of any model class with a {@code 
builder()}.
+ */
+public final class LangChain4jModelFactory {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(LangChain4jModelFactory.class);
+
+    /**
+     * The options every provider has, and the provider-specific extras.
+     *
+     * @param provider        the provider name (see {@link Provider}) or the 
fully qualified class name of the model
+     * @param modelName       the model, such as qwen2.5 or gpt-4o-mini
+     * @param baseUrl         the URL of the provider's API, for a local or 
self-hosted provider
+     * @param apiKey          the API key or access token of the provider
+     * @param temperature     the sampling temperature
+     * @param timeout         the request timeout
+     * @param modelProperties provider-specific properties of the model's 
builder, set as they are (numPredict on
+     *                        Ollama, maxTokens on OpenAI); may be null
+     */
+    public record ModelSpec(String provider, String modelName, String baseUrl, 
String apiKey, Double temperature,
+            Duration timeout, Map<String, Object> modelProperties) {
+    }
+
+    /** The LangChain4j providers with a short name, and the module each is 
in. */
+    public enum Provider {
+        OLLAMA("ollama", "dev.langchain4j.model.ollama", "OllamaChatModel", 
"OllamaEmbeddingModel", "langchain4j-ollama"),
+        OPENAI("openai", "dev.langchain4j.model.openai", "OpenAiChatModel", 
"OpenAiEmbeddingModel", "langchain4j-open-ai",
+               "open-ai"),
+        ANTHROPIC("anthropic", "dev.langchain4j.model.anthropic", 
"AnthropicChatModel", null, "langchain4j-anthropic"),
+        AZURE_OPENAI("azure-openai", "dev.langchain4j.model.azure", 
"AzureOpenAiChatModel", "AzureOpenAiEmbeddingModel",
+                     "langchain4j-azure-open-ai", "azure-open-ai", "azure"),
+        MISTRAL("mistral", "dev.langchain4j.model.mistralai", 
"MistralAiChatModel", "MistralAiEmbeddingModel",
+                "langchain4j-mistral-ai", "mistral-ai", "mistralai"),
+        GEMINI("gemini", "dev.langchain4j.model.googleai", 
"GoogleAiGeminiChatModel", "GoogleAiEmbeddingModel",
+               "langchain4j-google-ai-gemini", "google-ai-gemini", 
"google-ai", "googleai"),
+        VERTEX_AI("vertex-ai", "dev.langchain4j.model.vertexai", 
"VertexAiGeminiChatModel", "VertexAiEmbeddingModel",
+                  "langchain4j-vertex-ai", "vertexai", "vertex-ai-gemini"),
+        GITHUB("github", "dev.langchain4j.model.github", 
"GitHubModelsChatModel", "GitHubModelsEmbeddingModel",
+               "langchain4j-github-models", "github-models"),
+        HUGGING_FACE("hugging-face", "dev.langchain4j.model.huggingface", 
"HuggingFaceChatModel",
+                     "HuggingFaceEmbeddingModel", "langchain4j-hugging-face", 
"huggingface");
+
+        private final String id;
+        private final String pkg;
+        private final String chatModel;
+        private final String embeddingModel;
+        private final String artifactId;
+        private final String[] aliases;
+
+        Provider(String id, String pkg, String chatModel, String 
embeddingModel, String artifactId, String... aliases) {
+            this.id = id;
+            this.pkg = pkg;
+            this.chatModel = chatModel;
+            this.embeddingModel = embeddingModel;
+            this.artifactId = artifactId;
+            this.aliases = aliases;
+        }
+
+        public String id() {
+            return id;
+        }
+
+        /** The Maven artifact of the LangChain4j module, in the 
dev.langchain4j group. */
+        public String artifactId() {
+            return artifactId;
+        }
+
+        public String chatModelClass() {
+            return pkg + "." + chatModel;
+        }
+
+        /** The embedding model class, or null for a provider without 
embedding models. */
+        public String embeddingModelClass() {
+            return embeddingModel != null ? pkg + "." + embeddingModel : null;
+        }
+
+        /** The provider with the given name or alias, case-insensitively, or 
null. */
+        public static Provider of(String name) {
+            if (name == null) {
+                return null;
+            }
+            String n = name.trim().toLowerCase(Locale.ROOT);
+            for (Provider p : values()) {
+                if (p.id.equals(n)) {
+                    return p;
+                }
+                for (String alias : p.aliases) {
+                    if (alias.equals(n)) {
+                        return p;
+                    }
+                }
+            }
+            return null;
+        }
+
+        static String ids() {
+            StringBuilder sb = new StringBuilder();
+            for (Provider p : values()) {
+                if (!sb.isEmpty()) {
+                    sb.append(", ");
+                }
+                sb.append(p.id);
+            }
+            return sb.toString();
+        }
+    }
+
+    /** The names the builders of the providers use for the common options, 
tried in order. */
+    private static final Map<String, String[]> ALIASES = Map.of(
+            "modelName", new String[] { "modelName", "modelId", 
"deploymentName" },
+            "baseUrl", new String[] { "baseUrl", "endpoint" },
+            "apiKey", new String[] { "apiKey", "accessToken", "gitHubToken" },
+            "temperature", new String[] { "temperature" },
+            "timeout", new String[] { "timeout" });
+
+    /** The prefix of the provider-specific model properties in an endpoint 
URI: {@code model.numPredict=512}. */
+    public static final String MODEL_PROPERTY_PREFIX = "model.";
+
+    private LangChain4jModelFactory() {
+    }
+
+    /**
+     * Takes the {@code model.*} parameters out of the endpoint parameters and 
adds them, without the prefix, to the
+     * model properties of the configuration, as a component does before 
setting the other parameters.
+     *
+     * @param  parameters      the endpoint parameters; the {@code model.*} 
ones are removed
+     * @param  modelProperties the model properties configured so far (on the 
component), or null
+     * @return                 the model properties to set on the 
configuration, or null when there are none
+     */
+    public static Map<String, Object> extractModelProperties(
+            Map<String, Object> parameters, Map<String, Object> 
modelProperties) {
+        Map<String, Object> extra = 
PropertiesHelper.extractProperties(parameters, MODEL_PROPERTY_PREFIX);
+        if (extra.isEmpty()) {
+            return modelProperties;
+        }
+        Map<String, Object> answer = new LinkedHashMap<>();
+        if (modelProperties != null) {
+            answer.putAll(modelProperties);
+        }
+        answer.putAll(extra);
+        return answer;
+    }
+
+    /**
+     * Creates the chat model of the provider.
+     */
+    public static ChatModel createChatModel(CamelContext camelContext, 
ModelSpec spec) {
+        return create(camelContext, spec, ChatModel.class);
+    }
+
+    /**
+     * Creates the embedding model of the provider.
+     */
+    public static EmbeddingModel createEmbeddingModel(CamelContext 
camelContext, ModelSpec spec) {
+        return create(camelContext, spec, EmbeddingModel.class);
+    }
+
+    /**
+     * The class of the model of the provider: the provider's chat or 
embedding model class, or the provider itself when
+     * it is a class name.
+     */
+    public static String modelClassName(String provider, Class<?> kind) {
+        Provider p = Provider.of(provider);
+        if (p == null) {
+            if (provider != null && provider.contains(".")) {
+                return provider;
+            }
+            throw new IllegalArgumentException(
+                    "Unknown LangChain4j provider: " + provider + ". Use one 
of: " + Provider.ids()
+                                               + ", or the fully qualified 
class name of a model class with a builder()");
+        }
+        String name = kind == EmbeddingModel.class ? p.embeddingModelClass() : 
p.chatModelClass();
+        if (name == null) {
+            throw new IllegalArgumentException(
+                    "LangChain4j provider " + p.id() + " has no embedding 
model; use another provider for embeddings");
+        }
+        return name;
+    }
+
+    private static <T> T create(CamelContext camelContext, ModelSpec spec, 
Class<T> kind) {
+        String className = modelClassName(spec.provider(), kind);
+        Provider provider = Provider.of(spec.provider());
+        Class<?> clazz;
+        try {
+            clazz = 
camelContext.getClassResolver().resolveMandatoryClass(className);
+        } catch (ClassNotFoundException e) {
+            String dep = provider != null
+                    ? "dev.langchain4j:" + provider.artifactId() : "the 
LangChain4j module of " + className;
+            throw new IllegalArgumentException(
+                    "LangChain4j provider " + spec.provider() + " needs " + 
dep + " on the classpath (with Camel JBang:"
+                                               + " camel.jbang.dependencies=" 
+ dep + " in application.properties, or --dep)",
+                    e);
+        }
+        if (!kind.isAssignableFrom(clazz)) {
+            throw new IllegalArgumentException(
+                    "LangChain4j provider " + spec.provider() + ": " + 
className + " is not a " + kind.getName());
+        }
+        Object builder = newBuilder(clazz);
+        List<String> accepts = 
PropertyBindingSupport.builderPropertyNames(builder.getClass());
+
+        Map<String, Object> properties = new LinkedHashMap<>();
+        common(properties, accepts, clazz, spec.provider(), "modelName", 
spec.modelName());
+        common(properties, accepts, clazz, spec.provider(), "baseUrl", 
spec.baseUrl());
+        common(properties, accepts, clazz, spec.provider(), "apiKey", 
spec.apiKey());
+        common(properties, accepts, clazz, spec.provider(), "temperature", 
spec.temperature());
+        common(properties, accepts, clazz, spec.provider(), "timeout", 
spec.timeout());
+        if (spec.modelProperties() != null) {
+            properties.putAll(spec.modelProperties());
+        }
+        LOG.debug("Creating {} of provider {} with: {}", className, 
spec.provider(), properties.keySet());
+        try {
+            Object model = PropertyBindingSupport.build()
+                    .withCamelContext(camelContext)
+                    .withTarget(builder)
+                    .withMandatory(true)
+                    .withProperties(properties)
+                    .build(Object.class, 
PropertyBindingSupport.findBuilderMethod(builder, clazz, null));
+            return kind.cast(model);
+        } catch (PropertyBindingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot configure " + className + " of LangChain4j 
provider " + spec.provider() + ": "
+                                               + e.getMessage() + ". The 
builder " + builder.getClass().getName()
+                                               + " accepts: " + String.join(", 
", accepts),
+                    e);
+        }
+    }
+
+    /** Sets a common option under the name the builder has for it, failing 
when the builder has none. */
+    private static void common(
+            Map<String, Object> properties, List<String> accepts, Class<?> 
clazz, String provider, String option,
+            Object value) {
+        if (value == null) {
+            return;
+        }
+        for (String alias : ALIASES.get(option)) {
+            if (accepts.contains(alias)) {
+                properties.put(alias, value);
+                return;
+            }
+        }
+        throw new IllegalArgumentException(
+                "LangChain4j provider " + provider + " (" + clazz.getName() + 
") has no " + option
+                                           + " option; its builder accepts: " 
+ String.join(", ", accepts));
+    }
+
+    /** The builder of the model class from its public static builder(), which 
every LangChain4j model has. */
+    private static Object newBuilder(Class<?> clazz) {
+        try {
+            Method m = clazz.getMethod("builder");
+            if (!Modifier.isStatic(m.getModifiers())) {
+                throw new NoSuchMethodException("builder");
+            }
+            return m.invoke(null);
+        } catch (ReflectiveOperationException e) {
+            throw new IllegalArgumentException(
+                    "Cannot create " + clazz.getName() + " as it has no public 
static builder() method", e);
+        }

Review Comment:
   ⚠️ **Incomplete exception handling — `ExceptionInInitializerError` escapes**
   
   `Method.invoke(null)` can throw `ExceptionInInitializerError` (a 
`LinkageError`, not a `ReflectiveOperationException`) when the model class's 
static initializer fails — for example when a required native library is not on 
the path. That error is not caught here, so it propagates raw with a stack 
trace that points into reflection internals, giving the user no indication of 
what went wrong at the Camel level.
   
   The same applies to `NoClassDefFoundError`, which surfaces when a transitive 
dependency of the model class is missing at link time (different from the 
`ClassNotFoundException` caught above for the model class itself).
   
   ```suggestion
           } catch (ReflectiveOperationException e) {
               throw new IllegalArgumentException(
                       "Cannot create " + clazz.getName() + " as it has no 
public static builder() method", e);
           } catch (ExceptionInInitializerError | NoClassDefFoundError e) {
               throw new IllegalArgumentException(
                       "Cannot initialize " + clazz.getName() + ": a required 
dependency may be missing from the classpath",
                       e);
           }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to