From f0342b5a4211f049fac42fa695292f8b66ff6b2a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 22:49:10 +0000 Subject: [PATCH 1/5] CAMEL-24566: Auto-discover GenAI dependencies in Camel CLI Add GenAiDependencyDiscovery to scan route URIs and LangChain4j provider classes for camel-jbang run, export, dependency list and dependency update. - Add camel:ai-observability when GenAI routes are detected (unless disabled) - Add LangChain4j provider JAR mappings to known-dependencies.properties - Integrate discovery into Run and ExportBaseCommand.resolveDependencies Co-authored-by: Cursor Agent --- .../core/commands/ExportBaseCommand.java | 4 + .../camel/dsl/jbang/core/commands/Run.java | 3 + .../core/common/GenAiDependencyDiscovery.java | 245 ++++++++++++++++++ .../dsl/jbang/core/commands/ExportTest.java | 19 ++ .../common/GenAiDependencyDiscoveryTest.java | 219 ++++++++++++++++ .../src/test/resources/genai-route.yaml | 21 ++ .../resources/genai/langchain4j-route.yaml | 21 ++ .../camel-main-known-dependencies.properties | 13 + 8 files changed, 545 insertions(+) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/resources/genai/langchain4j-route.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java index 82fa09b3ed5ad..6758845ae9965 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java @@ -51,6 +51,7 @@ import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.GenAiDependencyDiscovery; import org.apache.camel.dsl.jbang.core.common.HawtioVersion; import org.apache.camel.dsl.jbang.core.common.JavaVersionCompletionCandidates; import org.apache.camel.dsl.jbang.core.common.LoggingLevelCompletionCandidates; @@ -811,6 +812,9 @@ protected Set resolveDependencies(Path settings, Path profile) throws Ex answer.add("mvn:org.hibernate.orm:hibernate-core"); } + // auto-discover GenAI component, provider and observability dependencies + answer.addAll(GenAiDependencyDiscovery.discoverFromSettings(settings, profile, observe, files)); + // remove duplicate versions (keep first) but an explicit --dep version always wins over // an auto-detected dependency for the same groupId:artifactId (e.g. a JDBC driver whose // version is inferred from the camel-dependencies BOM) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index 0cf49cf11e9fa..77b90179de324 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -50,6 +50,7 @@ import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.ExampleHelper; +import org.apache.camel.dsl.jbang.core.common.GenAiDependencyDiscovery; import org.apache.camel.dsl.jbang.core.common.JavaVersionCompletionCandidates; import org.apache.camel.dsl.jbang.core.common.LauncherHelper; import org.apache.camel.dsl.jbang.core.common.LoggingLevelCompletionCandidates; @@ -1261,6 +1262,8 @@ private int run() throws Exception { dependencies.add("camel:observability-services"); main.addOverrideProperty("camel.metrics.logMetricsOnShutdown", "false"); } + GenAiDependencyDiscovery.discover(files, profileProperties, serverOptions.observe) + .forEach(dependencies::add); if (serverOptions.openapiUi) { dependencies.add("camel:platform-http-main"); dependencies.add("camel:openapi-java"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java new file mode 100644 index 0000000000000..d9c679dbc379c --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java @@ -0,0 +1,245 @@ +/* + * 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.dsl.jbang.core.common; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.model.ComponentModel; +import org.apache.camel.util.FileUtil; +import org.apache.camel.util.StringHelper; + +/** + * Discovers GenAI-related dependencies for Camel JBang run/export from route URIs, LangChain4j provider classes and + * observability settings. + */ +public final class GenAiDependencyDiscovery { + + public static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled"; + + private static final Set GEN_AI_SCHEMES = Set.of( + "aws-bedrock", "aws-bedrock-agent", "aws-bedrock-agent-runtime", + "aws2-textract", "docling", + "langchain4j-chat", "langchain4j-embeddings", "langchain4j-embeddingstore", + "langchain4j-tools", "langchain4j-agent", "langchain4j-web-search", + "openai", "kserve", "tensorflow-serving", "djl", + "huggingface", "ai-tool", "google-vertexai"); + + private static final Map LANGCHAIN4J_PROVIDER_DEPENDENCIES = Map.ofEntries( + Map.entry("dev.langchain4j.model.ollama.OllamaChatModel", + "mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.ollama.OllamaEmbeddingModel", + "mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.ollama.OllamaLanguageModel", + "mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.openai.OpenAiChatModel", + "mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.openai.OpenAiEmbeddingModel", + "mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.openai.OpenAiLanguageModel", + "mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.huggingface.HuggingFaceChatModel", + "mvn:dev.langchain4j:langchain4j-hugging-face:${langchain4j-beta-version}"), + Map.entry("dev.langchain4j.model.anthropic.AnthropicChatModel", + "mvn:dev.langchain4j:langchain4j-anthropic:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.azure.AzureOpenAiChatModel", + "mvn:dev.langchain4j:langchain4j-azure-open-ai:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.mistralai.MistralAiChatModel", + "mvn:dev.langchain4j:langchain4j-mistral-ai:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.vertexai.VertexAiChatModel", + "mvn:dev.langchain4j:langchain4j-vertex-ai:${langchain4j-version}"), + Map.entry("dev.langchain4j.model.googleai.GoogleAiGeminiChatModel", + "mvn:dev.langchain4j:langchain4j-google-ai-gemini:${langchain4j-version}")); + + private static final Pattern YAML_SCHEME_PATTERN = Pattern.compile( + "(?:uri:\\s*[\"']?|from:[ \\t]+[\"']?|to:[ \\t]+[\"']?|toD:[ \\t]+[\"']?)" + + "([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?", + Pattern.MULTILINE); + + private static final Pattern XML_SCHEME_PATTERN = Pattern.compile( + "(?: discover(Collection sourceFiles, Properties properties, boolean observe) { + return discover(sourceFiles, properties, observe, new DefaultCamelCatalog()); + } + + static Collection discover( + Collection sourceFiles, Properties properties, boolean observe, + CamelCatalog catalog) { + Set deps = new LinkedHashSet<>(); + boolean hasGenAiRoutes = false; + + for (String file : sourceFiles) { + String content = readContent(file); + if (content == null) { + continue; + } + for (String scheme : extractSchemes(content)) { + if (GEN_AI_SCHEMES.contains(scheme)) { + ComponentModel model = catalog.componentModel(scheme); + if (model != null) { + hasGenAiRoutes = true; + deps.add("camel:" + scheme); + } + } + } + deps.addAll(discoverProviderDependencies(content)); + } + + if (hasGenAiRoutes && includeAiObservability(properties, observe) + && catalog.otherModel("ai-observability") != null) { + deps.add("camel:ai-observability"); + } + + return deps; + } + + public static Collection discoverFromSettings( + Path settings, Path profile, boolean observe, + Collection sourceFiles) + throws IOException { + List files = new ArrayList<>(sourceFiles); + if (settings != null && Files.exists(settings)) { + for (String line : RuntimeUtil.loadPropertiesLines(settings)) { + collectSourceFile(line, "camel.main.routesIncludePattern=", files); + collectSourceFile(line, "java=", files); + collectSourceFile(line, "xml=", files); + collectSourceFile(line, "yaml=", files); + } + } + Properties properties = new Properties(); + if (profile != null && Files.exists(profile)) { + RuntimeUtil.loadProperties(properties, profile); + } + return discover(files, properties, observe); + } + + static boolean includeAiObservability(Properties properties, boolean observe) { + String enabled = properties != null ? properties.getProperty(AI_OBSERVABILITY_ENABLED) : null; + if ("false".equalsIgnoreCase(enabled)) { + return false; + } + if ("true".equalsIgnoreCase(enabled)) { + return true; + } + // include by default for GenAI routes; --observe explicitly enables observability stack + return observe || enabled == null; + } + + static List extractSchemes(String content) { + List schemes = new ArrayList<>(); + addSchemeMatches(schemes, YAML_SCHEME_PATTERN, content); + addSchemeMatches(schemes, XML_SCHEME_PATTERN, content); + addSchemeMatches(schemes, JAVA_URI_PATTERN, content); + schemes.removeIf(scheme -> "http".equals(scheme) || "https".equals(scheme)); + return schemes; + } + + static Collection discoverProviderDependencies(String content) { + Set deps = new LinkedHashSet<>(); + for (Map.Entry entry : LANGCHAIN4J_PROVIDER_DEPENDENCIES.entrySet()) { + if (content.contains(entry.getKey())) { + deps.add(entry.getValue()); + } + } + return deps; + } + + private static void addSchemeMatches(List schemes, Pattern pattern, String content) { + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + String scheme = matcher.group(1); + if (!schemes.contains(scheme)) { + schemes.add(scheme); + } + } + } + + private static void collectSourceFile(String line, String prefix, List files) { + if (!line.startsWith(prefix)) { + return; + } + String value = StringHelper.after(line, prefix); + if (value == null || value.isBlank()) { + return; + } + for (String file : value.split(",")) { + file = file.trim(); + if (file.startsWith("file:")) { + file = file.substring(5); + } + if (!file.isBlank() && !files.contains(file)) { + files.add(file); + } + } + } + + private static String readContent(String file) { + if (file == null || file.isBlank()) { + return null; + } + String path = file; + if (path.startsWith("classpath:")) { + String resource = path.substring("classpath:".length()); + try (var is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(resource)) { + if (is == null) { + return null; + } + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + return null; + } + } + if (path.startsWith("file:")) { + path = path.substring(5); + } + Path source = Paths.get(path); + if (!Files.exists(source) || Files.isDirectory(source)) { + return null; + } + String ext = FileUtil.onlyExt(path, true); + if (ext == null || !Set.of("java", "xml", "yaml", "yml", "properties").contains(ext)) { + return null; + } + try { + return Files.readString(source, StandardCharsets.UTF_8); + } catch (IOException e) { + return null; + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java index 45b4eca08aac5..e633a3e0b3b93 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java @@ -830,6 +830,25 @@ public void shouldExportGroovy(RuntimeType rt) throws Exception { Assertions.assertTrue(f.exists()); } + @Test + public void shouldExportGenAiRouteWithObservability() throws Exception { + Export command = new Export(new CamelJBangMain()); + CommandLine.populateCommand(command, + "--gav=examples:genai:1.0.0", + "--dir=" + workingDir, + "--quiet", + "--runtime=main", + "src/test/resources/genai-route.yaml"); + int exit = command.doCall(); + + Assertions.assertEquals(0, exit); + Model model = readMavenModel(); + Assertions.assertTrue( + containsDependency(model.getDependencies(), "org.apache.camel", "camel-langchain4j-chat", null)); + Assertions.assertTrue( + containsDependency(model.getDependencies(), "org.apache.camel", "camel-ai-observability", null)); + } + @ParameterizedTest @MethodSource("runtimeProvider") public void shouldExportObserve(RuntimeType rt) throws Exception { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java new file mode 100644 index 0000000000000..a5f156e795eee --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java @@ -0,0 +1,219 @@ +/* + * 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.dsl.jbang.core.common; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Properties; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +class GenAiDependencyDiscoveryTest { + + private static final String LANGCHAIN4J_ROUTE = """ + - beans: + - name: chatModel + type: dev.langchain4j.model.ollama.OllamaChatModel + properties: + baseUrl: http://localhost:11434 + modelName: llama3.2 + - from: + uri: timer:tick + steps: + - to: langchain4j-chat:myModel + """; + + private static final String OPENAI_ROUTE = """ + - from: + uri: timer:tick + steps: + - to: openai:completion + """; + + private static final String XML_ROUTE = """ + + + + + + + """; + + private static final String JAVA_ROUTE = """ + public class AiRoute { + public void configure() { + from("timer:tick").to("langchain4j-chat:model"); + } + } + """; + + @TempDir + Path tempDir; + + @Test + void shouldDiscoverLangChain4jComponentAndProvider() throws Exception { + Path route = tempDir.resolve("ai-route.yaml"); + Files.writeString(route, LANGCHAIN4J_ROUTE); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + + assertThat(deps).contains("camel:langchain4j-chat"); + assertThat(deps).contains("mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"); + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void shouldDiscoverOpenAiComponent() throws Exception { + Path route = tempDir.resolve("openai-route.yaml"); + Files.writeString(route, OPENAI_ROUTE); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + + assertThat(deps).contains("camel:openai"); + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void shouldDiscoverFromXmlRoute() throws Exception { + Path route = tempDir.resolve("ai-route.xml"); + Files.writeString(route, XML_ROUTE); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + + assertThat(deps).contains("camel:langchain4j-chat"); + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void shouldDiscoverFromJavaRoute() throws Exception { + Path route = tempDir.resolve("AiRoute.java"); + Files.writeString(route, JAVA_ROUTE); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + + assertThat(deps).contains("camel:langchain4j-chat"); + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void shouldSkipAiObservabilityWhenExplicitlyDisabled() throws Exception { + Path route = tempDir.resolve("ai-route.yaml"); + Files.writeString(route, OPENAI_ROUTE); + Properties properties = new Properties(); + properties.setProperty(GenAiDependencyDiscovery.AI_OBSERVABILITY_ENABLED, "false"); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), properties, true); + + assertThat(deps).contains("camel:openai"); + assertThat(deps).doesNotContain("camel:ai-observability"); + } + + @Test + void shouldIncludeAiObservabilityWhenExplicitlyEnabled() { + Properties properties = new Properties(); + properties.setProperty(GenAiDependencyDiscovery.AI_OBSERVABILITY_ENABLED, "true"); + + assertThat(GenAiDependencyDiscovery.includeAiObservability(properties, false)).isTrue(); + } + + @Test + void shouldIncludeAiObservabilityWithObserveFlag() { + assertThat(GenAiDependencyDiscovery.includeAiObservability(new Properties(), true)).isTrue(); + } + + @Test + void shouldNotDiscoverForNonGenAiRoute() throws Exception { + Path route = tempDir.resolve("route.yaml"); + Files.writeString(route, """ + - from: + uri: timer:tick + steps: + - to: log:info + """); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + + assertThat(deps).isEmpty(); + } + + @Test + void shouldDiscoverFromClasspathResource() { + Collection deps = GenAiDependencyDiscovery.discover( + List.of("classpath:genai/langchain4j-route.yaml"), new Properties(), false); + + assertThat(deps).contains("camel:langchain4j-chat"); + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void shouldDiscoverFromSettingsFile() throws Exception { + Path route = tempDir.resolve("genai-route.yaml"); + Files.writeString(route, OPENAI_ROUTE); + + Path settings = tempDir.resolve("camel-runner.properties"); + Files.writeString(settings, "yaml=" + route + "\n"); + + Path profile = tempDir.resolve("application.properties"); + Files.writeString(profile, "camel.aiObservability.enabled=true\n"); + + Collection deps = GenAiDependencyDiscovery.discoverFromSettings(settings, profile, false, List.of()); + + assertThat(deps).contains("camel:openai"); + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void shouldExtractSchemesFromYamlAndXml() { + assertThat(GenAiDependencyDiscovery.extractSchemes(OPENAI_ROUTE)).containsExactly("timer", "openai"); + assertThat(GenAiDependencyDiscovery.extractSchemes(XML_ROUTE)).containsExactly("timer", "langchain4j-chat"); + } + + @Test + void shouldDiscoverOpenAiProviderFromJavaContent() { + String content = "dev.langchain4j.model.openai.OpenAiChatModel model = OpenAiChatModel.builder().apiKey(key).build();"; + assertThat(GenAiDependencyDiscovery.discoverProviderDependencies(content)) + .contains("mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"); + } + + @Test + void shouldIgnoreUnknownGenAiSchemeWhenNotInCatalog() throws Exception { + Path route = tempDir.resolve("future-route.yaml"); + Files.writeString(route, """ + - from: + uri: openai:completion + """); + + CamelCatalog catalog = new DefaultCamelCatalog() { + @Override + public org.apache.camel.tooling.model.ComponentModel componentModel(String name) { + return null; + } + }; + + Collection deps = GenAiDependencyDiscovery.discover( + List.of(route.toString()), new Properties(), false, catalog); + + assertThat(deps).isEmpty(); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml b/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml new file mode 100644 index 0000000000000..675e94300dbaf --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml @@ -0,0 +1,21 @@ +# +# 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. +# + +- from: + uri: timer:tick + steps: + - to: langchain4j-chat:myModel diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai/langchain4j-route.yaml b/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai/langchain4j-route.yaml new file mode 100644 index 0000000000000..675e94300dbaf --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai/langchain4j-route.yaml @@ -0,0 +1,21 @@ +# +# 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. +# + +- from: + uri: timer:tick + steps: + - to: langchain4j-chat:myModel diff --git a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties index 4f98022c4f35f..8d668356ef6e9 100644 --- a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties +++ b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties @@ -73,3 +73,16 @@ org.apache.qpid.jms.JmsConnectionFactory = org.apache.qpid:qpid-jms-client:${qpi org.messaginghub.pooled.jms.JmsPoolConnectionFactory = org.messaginghub:pooled-jms:${pooled-jms-version} org.postgresql.Driver = org.postgresql:postgresql:${pgjdbc-driver-version} org.postgresql.ds.PGSimpleDataSource = org.postgresql:postgresql:${pgjdbc-driver-version} + +dev.langchain4j.model.ollama.OllamaChatModel = dev.langchain4j:langchain4j-ollama:${langchain4j-version} +dev.langchain4j.model.ollama.OllamaEmbeddingModel = dev.langchain4j:langchain4j-ollama:${langchain4j-version} +dev.langchain4j.model.ollama.OllamaLanguageModel = dev.langchain4j:langchain4j-ollama:${langchain4j-version} +dev.langchain4j.model.openai.OpenAiChatModel = dev.langchain4j:langchain4j-open-ai:${langchain4j-version} +dev.langchain4j.model.openai.OpenAiEmbeddingModel = dev.langchain4j:langchain4j-open-ai:${langchain4j-version} +dev.langchain4j.model.openai.OpenAiLanguageModel = dev.langchain4j:langchain4j-open-ai:${langchain4j-version} +dev.langchain4j.model.huggingface.HuggingFaceChatModel = dev.langchain4j:langchain4j-hugging-face:${langchain4j-beta-version} +dev.langchain4j.model.anthropic.AnthropicChatModel = dev.langchain4j:langchain4j-anthropic:${langchain4j-version} +dev.langchain4j.model.azure.AzureOpenAiChatModel = dev.langchain4j:langchain4j-azure-open-ai:${langchain4j-version} +dev.langchain4j.model.mistralai.MistralAiChatModel = dev.langchain4j:langchain4j-mistral-ai:${langchain4j-version} +dev.langchain4j.model.vertexai.VertexAiChatModel = dev.langchain4j:langchain4j-vertex-ai:${langchain4j-version} +dev.langchain4j.model.googleai.GoogleAiGeminiChatModel = dev.langchain4j:langchain4j-google-ai-gemini:${langchain4j-version} From 023e9c60894650d2c99a03263062a4be93dd84ca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 22:58:42 +0000 Subject: [PATCH 2/5] CAMEL-24566: Address review feedback on GenAI dependency discovery - Make ai-observability opt-in via --observe or camel.aiObservability.enabled=true - Load LangChain4j provider mappings from known-dependencies (package keys) - Scope URI extraction by file type and ignore YAML comments/false positives - Fix settings file scanning for export dependency resolution Co-authored-by: Cursor Agent --- .../core/common/GenAiDependencyDiscovery.java | 166 +++++++++++++----- .../dsl/jbang/core/commands/ExportTest.java | 3 +- .../common/GenAiDependencyDiscoveryTest.java | 70 ++++++-- .../src/test/resources/genai-route.yaml | 21 --- .../camel-main-known-dependencies.properties | 22 ++- 5 files changed, 182 insertions(+), 100 deletions(-) delete mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java index d9c679dbc379c..8621c7ad1627c 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java @@ -17,12 +17,14 @@ package org.apache.camel.dsl.jbang.core.common; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -37,6 +39,9 @@ import org.apache.camel.util.FileUtil; import org.apache.camel.util.StringHelper; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.CLASSPATH_FILES; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.GROOVY_FILES; + /** * Discovers GenAI-related dependencies for Camel JBang run/export from route URIs, LangChain4j provider classes and * observability settings. @@ -45,39 +50,15 @@ public final class GenAiDependencyDiscovery { public static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled"; + private static final String KNOWN_DEPENDENCIES = "camel-main-known-dependencies.properties"; + private static final Set GEN_AI_SCHEMES = Set.of( "aws-bedrock", "aws-bedrock-agent", "aws-bedrock-agent-runtime", "aws2-textract", "docling", "langchain4j-chat", "langchain4j-embeddings", "langchain4j-embeddingstore", "langchain4j-tools", "langchain4j-agent", "langchain4j-web-search", "openai", "kserve", "tensorflow-serving", "djl", - "huggingface", "ai-tool", "google-vertexai"); - - private static final Map LANGCHAIN4J_PROVIDER_DEPENDENCIES = Map.ofEntries( - Map.entry("dev.langchain4j.model.ollama.OllamaChatModel", - "mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.ollama.OllamaEmbeddingModel", - "mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.ollama.OllamaLanguageModel", - "mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.openai.OpenAiChatModel", - "mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.openai.OpenAiEmbeddingModel", - "mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.openai.OpenAiLanguageModel", - "mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.huggingface.HuggingFaceChatModel", - "mvn:dev.langchain4j:langchain4j-hugging-face:${langchain4j-beta-version}"), - Map.entry("dev.langchain4j.model.anthropic.AnthropicChatModel", - "mvn:dev.langchain4j:langchain4j-anthropic:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.azure.AzureOpenAiChatModel", - "mvn:dev.langchain4j:langchain4j-azure-open-ai:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.mistralai.MistralAiChatModel", - "mvn:dev.langchain4j:langchain4j-mistral-ai:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.vertexai.VertexAiChatModel", - "mvn:dev.langchain4j:langchain4j-vertex-ai:${langchain4j-version}"), - Map.entry("dev.langchain4j.model.googleai.GoogleAiGeminiChatModel", - "mvn:dev.langchain4j:langchain4j-google-ai-gemini:${langchain4j-version}")); + "huggingface", "ai-tool", "google-vertexai", "spring-ai-chat"); private static final Pattern YAML_SCHEME_PATTERN = Pattern.compile( "(?:uri:\\s*[\"']?|from:[ \\t]+[\"']?|to:[ \\t]+[\"']?|toD:[ \\t]+[\"']?)" @@ -85,11 +66,13 @@ public final class GenAiDependencyDiscovery { Pattern.MULTILINE); private static final Pattern XML_SCHEME_PATTERN = Pattern.compile( - "(?: langchain4jProviderDependencies; private GenAiDependencyDiscovery() { } @@ -103,13 +86,15 @@ static Collection discover( CamelCatalog catalog) { Set deps = new LinkedHashSet<>(); boolean hasGenAiRoutes = false; + boolean hasProviderReferences = false; for (String file : sourceFiles) { String content = readContent(file); if (content == null) { continue; } - for (String scheme : extractSchemes(content)) { + String ext = extensionOf(file); + for (String scheme : extractSchemes(content, ext)) { if (GEN_AI_SCHEMES.contains(scheme)) { ComponentModel model = catalog.componentModel(scheme); if (model != null) { @@ -118,10 +103,14 @@ static Collection discover( } } } - deps.addAll(discoverProviderDependencies(content)); + Collection providers = discoverProviderDependencies(content); + if (!providers.isEmpty()) { + hasProviderReferences = true; + deps.addAll(providers); + } } - if (hasGenAiRoutes && includeAiObservability(properties, observe) + if ((hasGenAiRoutes || hasProviderReferences) && includeAiObservability(properties, observe) && catalog.otherModel("ai-observability") != null) { deps.add("camel:ai-observability"); } @@ -137,9 +126,8 @@ public static Collection discoverFromSettings( if (settings != null && Files.exists(settings)) { for (String line : RuntimeUtil.loadPropertiesLines(settings)) { collectSourceFile(line, "camel.main.routesIncludePattern=", files); - collectSourceFile(line, "java=", files); - collectSourceFile(line, "xml=", files); - collectSourceFile(line, "yaml=", files); + collectSourceFile(line, CLASSPATH_FILES + "=", files); + collectSourceFile(line, GROOVY_FILES + "=", files); } } Properties properties = new Properties(); @@ -154,42 +142,95 @@ static boolean includeAiObservability(Properties properties, boolean observe) { if ("false".equalsIgnoreCase(enabled)) { return false; } - if ("true".equalsIgnoreCase(enabled)) { - return true; - } - // include by default for GenAI routes; --observe explicitly enables observability stack - return observe || enabled == null; + return observe || "true".equalsIgnoreCase(enabled); } - static List extractSchemes(String content) { + static List extractSchemes(String content, String ext) { + if (content == null || ext == null) { + return List.of(); + } List schemes = new ArrayList<>(); - addSchemeMatches(schemes, YAML_SCHEME_PATTERN, content); - addSchemeMatches(schemes, XML_SCHEME_PATTERN, content); - addSchemeMatches(schemes, JAVA_URI_PATTERN, content); + switch (ext) { + case "yaml", "yml" -> addSchemeMatches(schemes, YAML_SCHEME_PATTERN, stripYamlComments(content)); + case "xml" -> addSchemeMatches(schemes, XML_SCHEME_PATTERN, content, 2); + case "java", "groovy" -> addSchemeMatches(schemes, JAVA_URI_PATTERN, content, 1); + default -> { + } + } schemes.removeIf(scheme -> "http".equals(scheme) || "https".equals(scheme)); return schemes; } static Collection discoverProviderDependencies(String content) { Set deps = new LinkedHashSet<>(); - for (Map.Entry entry : LANGCHAIN4J_PROVIDER_DEPENDENCIES.entrySet()) { + for (Map.Entry entry : providerDependencies().entrySet()) { if (content.contains(entry.getKey())) { - deps.add(entry.getValue()); + deps.add("mvn:" + entry.getValue()); } } return deps; } + private static Map providerDependencies() { + Map answer = langchain4jProviderDependencies; + if (answer != null) { + return answer; + } + synchronized (GenAiDependencyDiscovery.class) { + answer = langchain4jProviderDependencies; + if (answer != null) { + return answer; + } + answer = loadLangChain4jProviderDependencies(); + langchain4jProviderDependencies = answer; + return answer; + } + } + + private static Map loadLangChain4jProviderDependencies() { + Map answer = new LinkedHashMap<>(); + Properties properties = new Properties(); + try (InputStream is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(KNOWN_DEPENDENCIES)) { + if (is != null) { + properties.load(is); + } + } catch (IOException e) { + return answer; + } + for (String key : properties.stringPropertyNames()) { + if (key.startsWith("dev.langchain4j.")) { + answer.put(key, properties.getProperty(key)); + } + } + return answer; + } + private static void addSchemeMatches(List schemes, Pattern pattern, String content) { + addSchemeMatches(schemes, pattern, content, 1); + } + + private static void addSchemeMatches(List schemes, Pattern pattern, String content, int group) { Matcher matcher = pattern.matcher(content); while (matcher.find()) { - String scheme = matcher.group(1); + String scheme = matcher.group(group); if (!schemes.contains(scheme)) { schemes.add(scheme); } } } + private static String stripYamlComments(String content) { + StringBuilder sb = new StringBuilder(content.length()); + for (String line : content.split("\n", -1)) { + int idx = line.indexOf('#'); + if (idx >= 0) { + line = line.substring(0, idx); + } + sb.append(line).append('\n'); + } + return sb.toString(); + } + private static void collectSourceFile(String line, String prefix, List files) { if (!line.startsWith(prefix)) { return; @@ -203,12 +244,33 @@ private static void collectSourceFile(String line, String prefix, List f if (file.startsWith("file:")) { file = file.substring(5); } + int query = file.indexOf('?'); + if (query > 0) { + file = file.substring(0, query); + } if (!file.isBlank() && !files.contains(file)) { files.add(file); } } } + private static String extensionOf(String file) { + if (file == null) { + return null; + } + String path = file; + if (path.startsWith("classpath:")) { + path = path.substring("classpath:".length()); + } else if (path.startsWith("file:")) { + path = path.substring(5); + } + int query = path.indexOf('?'); + if (query > 0) { + path = path.substring(0, query); + } + return FileUtil.onlyExt(path, true); + } + private static String readContent(String file) { if (file == null || file.isBlank()) { return null; @@ -216,6 +278,10 @@ private static String readContent(String file) { String path = file; if (path.startsWith("classpath:")) { String resource = path.substring("classpath:".length()); + int query = resource.indexOf('?'); + if (query > 0) { + resource = resource.substring(0, query); + } try (var is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(resource)) { if (is == null) { return null; @@ -228,12 +294,16 @@ private static String readContent(String file) { if (path.startsWith("file:")) { path = path.substring(5); } + int query = path.indexOf('?'); + if (query > 0) { + path = path.substring(0, query); + } Path source = Paths.get(path); if (!Files.exists(source) || Files.isDirectory(source)) { return null; } String ext = FileUtil.onlyExt(path, true); - if (ext == null || !Set.of("java", "xml", "yaml", "yml", "properties").contains(ext)) { + if (ext == null || !Set.of("java", "xml", "yaml", "yml", "properties", "groovy").contains(ext)) { return null; } try { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java index e633a3e0b3b93..4ada0d4370df8 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java @@ -838,7 +838,8 @@ public void shouldExportGenAiRouteWithObservability() throws Exception { "--dir=" + workingDir, "--quiet", "--runtime=main", - "src/test/resources/genai-route.yaml"); + "--observe=true", + "src/test/resources/genai/langchain4j-route.yaml"); int exit = command.doCall(); Assertions.assertEquals(0, exit); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java index a5f156e795eee..4d8135fcf360f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java @@ -72,11 +72,11 @@ public void configure() { Path tempDir; @Test - void shouldDiscoverLangChain4jComponentAndProvider() throws Exception { + void shouldDiscoverLangChain4jComponentAndProviderWithObserve() throws Exception { Path route = tempDir.resolve("ai-route.yaml"); Files.writeString(route, LANGCHAIN4J_ROUTE); - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); assertThat(deps).contains("camel:langchain4j-chat"); assertThat(deps).contains("mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"); @@ -84,22 +84,35 @@ void shouldDiscoverLangChain4jComponentAndProvider() throws Exception { } @Test - void shouldDiscoverOpenAiComponent() throws Exception { + void shouldDiscoverOpenAiComponentWithExplicitObservabilityProperty() throws Exception { Path route = tempDir.resolve("openai-route.yaml"); Files.writeString(route, OPENAI_ROUTE); + Properties properties = new Properties(); + properties.setProperty(GenAiDependencyDiscovery.AI_OBSERVABILITY_ENABLED, "true"); - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), properties, false); assertThat(deps).contains("camel:openai"); assertThat(deps).contains("camel:ai-observability"); } + @Test + void shouldNotAddAiObservabilityByDefaultWithoutObserve() throws Exception { + Path route = tempDir.resolve("openai-route.yaml"); + Files.writeString(route, OPENAI_ROUTE); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + + assertThat(deps).contains("camel:openai"); + assertThat(deps).doesNotContain("camel:ai-observability"); + } + @Test void shouldDiscoverFromXmlRoute() throws Exception { Path route = tempDir.resolve("ai-route.xml"); Files.writeString(route, XML_ROUTE); - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); assertThat(deps).contains("camel:langchain4j-chat"); assertThat(deps).contains("camel:ai-observability"); @@ -110,7 +123,7 @@ void shouldDiscoverFromJavaRoute() throws Exception { Path route = tempDir.resolve("AiRoute.java"); Files.writeString(route, JAVA_ROUTE); - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); assertThat(deps).contains("camel:langchain4j-chat"); assertThat(deps).contains("camel:ai-observability"); @@ -130,16 +143,20 @@ void shouldSkipAiObservabilityWhenExplicitlyDisabled() throws Exception { } @Test - void shouldIncludeAiObservabilityWhenExplicitlyEnabled() { + void shouldIncludeAiObservabilityForProviderOnlySourceWhenEnabled() throws Exception { + Path beans = tempDir.resolve("beans.yaml"); + Files.writeString(beans, """ + - beans: + - name: chatModel + type: dev.langchain4j.model.ollama.OllamaChatModel + """); Properties properties = new Properties(); properties.setProperty(GenAiDependencyDiscovery.AI_OBSERVABILITY_ENABLED, "true"); - assertThat(GenAiDependencyDiscovery.includeAiObservability(properties, false)).isTrue(); - } + Collection deps = GenAiDependencyDiscovery.discover(List.of(beans.toString()), properties, false); - @Test - void shouldIncludeAiObservabilityWithObserveFlag() { - assertThat(GenAiDependencyDiscovery.includeAiObservability(new Properties(), true)).isTrue(); + assertThat(deps).contains("mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"); + assertThat(deps).contains("camel:ai-observability"); } @Test @@ -152,7 +169,7 @@ void shouldNotDiscoverForNonGenAiRoute() throws Exception { - to: log:info """); - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); assertThat(deps).isEmpty(); } @@ -160,7 +177,7 @@ void shouldNotDiscoverForNonGenAiRoute() throws Exception { @Test void shouldDiscoverFromClasspathResource() { Collection deps = GenAiDependencyDiscovery.discover( - List.of("classpath:genai/langchain4j-route.yaml"), new Properties(), false); + List.of("classpath:genai/langchain4j-route.yaml"), new Properties(), true); assertThat(deps).contains("camel:langchain4j-chat"); assertThat(deps).contains("camel:ai-observability"); @@ -172,7 +189,7 @@ void shouldDiscoverFromSettingsFile() throws Exception { Files.writeString(route, OPENAI_ROUTE); Path settings = tempDir.resolve("camel-runner.properties"); - Files.writeString(settings, "yaml=" + route + "\n"); + Files.writeString(settings, "camel.main.routesIncludePattern=" + route + "\n"); Path profile = tempDir.resolve("application.properties"); Files.writeString(profile, "camel.aiObservability.enabled=true\n"); @@ -185,8 +202,25 @@ void shouldDiscoverFromSettingsFile() throws Exception { @Test void shouldExtractSchemesFromYamlAndXml() { - assertThat(GenAiDependencyDiscovery.extractSchemes(OPENAI_ROUTE)).containsExactly("timer", "openai"); - assertThat(GenAiDependencyDiscovery.extractSchemes(XML_ROUTE)).containsExactly("timer", "langchain4j-chat"); + assertThat(GenAiDependencyDiscovery.extractSchemes(OPENAI_ROUTE, "yaml")).containsExactly("timer", "openai"); + assertThat(GenAiDependencyDiscovery.extractSchemes(XML_ROUTE, "xml")).containsExactly("timer", "langchain4j-chat"); + } + + @Test + void shouldIgnoreYamlCommentsAndPropertyFalsePositives() throws Exception { + Path route = tempDir.resolve("false-positive.yaml"); + Files.writeString(route, """ + model: "openai:gpt-4" + # from: openai:completion + - from: + uri: timer:tick + steps: + - to: log:info + """); + + Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); + + assertThat(deps).isEmpty(); } @Test @@ -212,7 +246,7 @@ public org.apache.camel.tooling.model.ComponentModel componentModel(String name) }; Collection deps = GenAiDependencyDiscovery.discover( - List.of(route.toString()), new Properties(), false, catalog); + List.of(route.toString()), new Properties(), true, catalog); assertThat(deps).isEmpty(); } diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml b/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml deleted file mode 100644 index 675e94300dbaf..0000000000000 --- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/genai-route.yaml +++ /dev/null @@ -1,21 +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. -# - -- from: - uri: timer:tick - steps: - - to: langchain4j-chat:myModel diff --git a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties index 8d668356ef6e9..f88b037486c70 100644 --- a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties +++ b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties @@ -74,15 +74,13 @@ org.messaginghub.pooled.jms.JmsPoolConnectionFactory = org.messaginghub:pooled-j org.postgresql.Driver = org.postgresql:postgresql:${pgjdbc-driver-version} org.postgresql.ds.PGSimpleDataSource = org.postgresql:postgresql:${pgjdbc-driver-version} -dev.langchain4j.model.ollama.OllamaChatModel = dev.langchain4j:langchain4j-ollama:${langchain4j-version} -dev.langchain4j.model.ollama.OllamaEmbeddingModel = dev.langchain4j:langchain4j-ollama:${langchain4j-version} -dev.langchain4j.model.ollama.OllamaLanguageModel = dev.langchain4j:langchain4j-ollama:${langchain4j-version} -dev.langchain4j.model.openai.OpenAiChatModel = dev.langchain4j:langchain4j-open-ai:${langchain4j-version} -dev.langchain4j.model.openai.OpenAiEmbeddingModel = dev.langchain4j:langchain4j-open-ai:${langchain4j-version} -dev.langchain4j.model.openai.OpenAiLanguageModel = dev.langchain4j:langchain4j-open-ai:${langchain4j-version} -dev.langchain4j.model.huggingface.HuggingFaceChatModel = dev.langchain4j:langchain4j-hugging-face:${langchain4j-beta-version} -dev.langchain4j.model.anthropic.AnthropicChatModel = dev.langchain4j:langchain4j-anthropic:${langchain4j-version} -dev.langchain4j.model.azure.AzureOpenAiChatModel = dev.langchain4j:langchain4j-azure-open-ai:${langchain4j-version} -dev.langchain4j.model.mistralai.MistralAiChatModel = dev.langchain4j:langchain4j-mistral-ai:${langchain4j-version} -dev.langchain4j.model.vertexai.VertexAiChatModel = dev.langchain4j:langchain4j-vertex-ai:${langchain4j-version} -dev.langchain4j.model.googleai.GoogleAiGeminiChatModel = dev.langchain4j:langchain4j-google-ai-gemini:${langchain4j-version} +dev.langchain4j.model.ollama = dev.langchain4j:langchain4j-ollama:${langchain4j-version} +dev.langchain4j.model.openai = dev.langchain4j:langchain4j-open-ai:${langchain4j-version} +dev.langchain4j.model.huggingface = dev.langchain4j:langchain4j-hugging-face:${langchain4j-beta-version} +dev.langchain4j.model.anthropic = dev.langchain4j:langchain4j-anthropic:${langchain4j-version} +dev.langchain4j.model.azure = dev.langchain4j:langchain4j-azure-open-ai:${langchain4j-version} +dev.langchain4j.model.mistralai = dev.langchain4j:langchain4j-mistral-ai:${langchain4j-version} +dev.langchain4j.model.vertexai = dev.langchain4j:langchain4j-vertex-ai:${langchain4j-version} +dev.langchain4j.model.googleai = dev.langchain4j:langchain4j-google-ai-gemini:${langchain4j-version} +dev.langchain4j.model.github = dev.langchain4j:langchain4j-github-models:${langchain4j-version} +dev.langchain4j.model.embedding.onnx = dev.langchain4j:langchain4j-embeddings:${langchain4j-beta-version} From 04413ce2747515a85dc856d8d6c72ec5d24c2352 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 15:46:28 +0000 Subject: [PATCH 3/5] CAMEL-24566: Replace route source scanning with catalog-based dependency resolution Remove GenAiDependencyDiscovery regex scanning of YAML/XML/Java routes. GenAI components and LangChain4j providers are resolved through the existing silent-run download pipeline (DependencyDownloaderComponentResolver and KnownDependenciesResolver), matching OpenTelemetry and LRA handling. - Add GenAiDependencyHelper to conditionally add camel:ai-observability from settings/profile when GenAI artifacts are already present - Map GenAiObservabilityImpl in camel-main-known-dependencies.properties - Replace unit tests with GenAiDependencyHelperTest Co-authored-by: Cursor --- .../camel/catalog/schemas/camel-xml-io.xsd | 2 +- .../core/commands/ExportBaseCommand.java | 10 +- .../camel/dsl/jbang/core/commands/Run.java | 5 +- .../core/common/GenAiDependencyDiscovery.java | 315 ------------------ .../core/common/GenAiDependencyHelper.java | 131 ++++++++ .../common/GenAiDependencyDiscoveryTest.java | 253 -------------- .../common/GenAiDependencyHelperTest.java | 98 ++++++ .../camel-main-known-dependencies.properties | 3 + 8 files changed, 242 insertions(+), 575 deletions(-) delete mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java delete mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd index 2b1f4d80bc757..d27bdfbe4cf31 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd @@ -1,4 +1,4 @@ - + diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java index 6758845ae9965..06487a360cd52 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java @@ -51,7 +51,7 @@ import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; -import org.apache.camel.dsl.jbang.core.common.GenAiDependencyDiscovery; +import org.apache.camel.dsl.jbang.core.common.GenAiDependencyHelper; import org.apache.camel.dsl.jbang.core.common.HawtioVersion; import org.apache.camel.dsl.jbang.core.common.JavaVersionCompletionCandidates; import org.apache.camel.dsl.jbang.core.common.LoggingLevelCompletionCandidates; @@ -812,8 +812,12 @@ protected Set resolveDependencies(Path settings, Path profile) throws Ex answer.add("mvn:org.hibernate.orm:hibernate-core"); } - // auto-discover GenAI component, provider and observability dependencies - answer.addAll(GenAiDependencyDiscovery.discoverFromSettings(settings, profile, observe, files)); + // add GenAI observability when silent-run / profile deps already include GenAI artifacts + Properties exportProperties = new Properties(); + if (profile != null && Files.exists(profile)) { + RuntimeUtil.loadProperties(exportProperties, profile); + } + GenAiDependencyHelper.addAiObservabilityIfNeeded(answer, exportProperties, observe); // remove duplicate versions (keep first) but an explicit --dep version always wins over // an auto-detected dependency for the same groupId:artifactId (e.g. a JDBC driver whose diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index 77b90179de324..266ac49253578 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -50,7 +50,7 @@ import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.ExampleHelper; -import org.apache.camel.dsl.jbang.core.common.GenAiDependencyDiscovery; +import org.apache.camel.dsl.jbang.core.common.GenAiDependencyHelper; import org.apache.camel.dsl.jbang.core.common.JavaVersionCompletionCandidates; import org.apache.camel.dsl.jbang.core.common.LauncherHelper; import org.apache.camel.dsl.jbang.core.common.LoggingLevelCompletionCandidates; @@ -1262,8 +1262,7 @@ private int run() throws Exception { dependencies.add("camel:observability-services"); main.addOverrideProperty("camel.metrics.logMetricsOnShutdown", "false"); } - GenAiDependencyDiscovery.discover(files, profileProperties, serverOptions.observe) - .forEach(dependencies::add); + GenAiDependencyHelper.addAiObservabilityIfNeeded(dependencies, profileProperties, serverOptions.observe); if (serverOptions.openapiUi) { dependencies.add("camel:platform-http-main"); dependencies.add("camel:openapi-java"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java deleted file mode 100644 index 8621c7ad1627c..0000000000000 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java +++ /dev/null @@ -1,315 +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.dsl.jbang.core.common; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.camel.catalog.CamelCatalog; -import org.apache.camel.catalog.DefaultCamelCatalog; -import org.apache.camel.tooling.model.ComponentModel; -import org.apache.camel.util.FileUtil; -import org.apache.camel.util.StringHelper; - -import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.CLASSPATH_FILES; -import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.GROOVY_FILES; - -/** - * Discovers GenAI-related dependencies for Camel JBang run/export from route URIs, LangChain4j provider classes and - * observability settings. - */ -public final class GenAiDependencyDiscovery { - - public static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled"; - - private static final String KNOWN_DEPENDENCIES = "camel-main-known-dependencies.properties"; - - private static final Set GEN_AI_SCHEMES = Set.of( - "aws-bedrock", "aws-bedrock-agent", "aws-bedrock-agent-runtime", - "aws2-textract", "docling", - "langchain4j-chat", "langchain4j-embeddings", "langchain4j-embeddingstore", - "langchain4j-tools", "langchain4j-agent", "langchain4j-web-search", - "openai", "kserve", "tensorflow-serving", "djl", - "huggingface", "ai-tool", "google-vertexai", "spring-ai-chat"); - - private static final Pattern YAML_SCHEME_PATTERN = Pattern.compile( - "(?:uri:\\s*[\"']?|from:[ \\t]+[\"']?|to:[ \\t]+[\"']?|toD:[ \\t]+[\"']?)" - + "([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?", - Pattern.MULTILINE); - - private static final Pattern XML_SCHEME_PATTERN = Pattern.compile( - "<(?:[\\w]+:)?(from|to|toD|enrich|wireTap)\\s+uri=[\"']([a-zA-Z][a-zA-Z0-9+.-]*):", - Pattern.CASE_INSENSITIVE); - - private static final Pattern JAVA_URI_PATTERN = Pattern.compile( - "(?:from|to|toD|wireTap|enrich|pollEnrich)\\s*\\(\\s*[\"']([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?[^\"']*[\"']"); - - private static volatile Map langchain4jProviderDependencies; - - private GenAiDependencyDiscovery() { - } - - public static Collection discover(Collection sourceFiles, Properties properties, boolean observe) { - return discover(sourceFiles, properties, observe, new DefaultCamelCatalog()); - } - - static Collection discover( - Collection sourceFiles, Properties properties, boolean observe, - CamelCatalog catalog) { - Set deps = new LinkedHashSet<>(); - boolean hasGenAiRoutes = false; - boolean hasProviderReferences = false; - - for (String file : sourceFiles) { - String content = readContent(file); - if (content == null) { - continue; - } - String ext = extensionOf(file); - for (String scheme : extractSchemes(content, ext)) { - if (GEN_AI_SCHEMES.contains(scheme)) { - ComponentModel model = catalog.componentModel(scheme); - if (model != null) { - hasGenAiRoutes = true; - deps.add("camel:" + scheme); - } - } - } - Collection providers = discoverProviderDependencies(content); - if (!providers.isEmpty()) { - hasProviderReferences = true; - deps.addAll(providers); - } - } - - if ((hasGenAiRoutes || hasProviderReferences) && includeAiObservability(properties, observe) - && catalog.otherModel("ai-observability") != null) { - deps.add("camel:ai-observability"); - } - - return deps; - } - - public static Collection discoverFromSettings( - Path settings, Path profile, boolean observe, - Collection sourceFiles) - throws IOException { - List files = new ArrayList<>(sourceFiles); - if (settings != null && Files.exists(settings)) { - for (String line : RuntimeUtil.loadPropertiesLines(settings)) { - collectSourceFile(line, "camel.main.routesIncludePattern=", files); - collectSourceFile(line, CLASSPATH_FILES + "=", files); - collectSourceFile(line, GROOVY_FILES + "=", files); - } - } - Properties properties = new Properties(); - if (profile != null && Files.exists(profile)) { - RuntimeUtil.loadProperties(properties, profile); - } - return discover(files, properties, observe); - } - - static boolean includeAiObservability(Properties properties, boolean observe) { - String enabled = properties != null ? properties.getProperty(AI_OBSERVABILITY_ENABLED) : null; - if ("false".equalsIgnoreCase(enabled)) { - return false; - } - return observe || "true".equalsIgnoreCase(enabled); - } - - static List extractSchemes(String content, String ext) { - if (content == null || ext == null) { - return List.of(); - } - List schemes = new ArrayList<>(); - switch (ext) { - case "yaml", "yml" -> addSchemeMatches(schemes, YAML_SCHEME_PATTERN, stripYamlComments(content)); - case "xml" -> addSchemeMatches(schemes, XML_SCHEME_PATTERN, content, 2); - case "java", "groovy" -> addSchemeMatches(schemes, JAVA_URI_PATTERN, content, 1); - default -> { - } - } - schemes.removeIf(scheme -> "http".equals(scheme) || "https".equals(scheme)); - return schemes; - } - - static Collection discoverProviderDependencies(String content) { - Set deps = new LinkedHashSet<>(); - for (Map.Entry entry : providerDependencies().entrySet()) { - if (content.contains(entry.getKey())) { - deps.add("mvn:" + entry.getValue()); - } - } - return deps; - } - - private static Map providerDependencies() { - Map answer = langchain4jProviderDependencies; - if (answer != null) { - return answer; - } - synchronized (GenAiDependencyDiscovery.class) { - answer = langchain4jProviderDependencies; - if (answer != null) { - return answer; - } - answer = loadLangChain4jProviderDependencies(); - langchain4jProviderDependencies = answer; - return answer; - } - } - - private static Map loadLangChain4jProviderDependencies() { - Map answer = new LinkedHashMap<>(); - Properties properties = new Properties(); - try (InputStream is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(KNOWN_DEPENDENCIES)) { - if (is != null) { - properties.load(is); - } - } catch (IOException e) { - return answer; - } - for (String key : properties.stringPropertyNames()) { - if (key.startsWith("dev.langchain4j.")) { - answer.put(key, properties.getProperty(key)); - } - } - return answer; - } - - private static void addSchemeMatches(List schemes, Pattern pattern, String content) { - addSchemeMatches(schemes, pattern, content, 1); - } - - private static void addSchemeMatches(List schemes, Pattern pattern, String content, int group) { - Matcher matcher = pattern.matcher(content); - while (matcher.find()) { - String scheme = matcher.group(group); - if (!schemes.contains(scheme)) { - schemes.add(scheme); - } - } - } - - private static String stripYamlComments(String content) { - StringBuilder sb = new StringBuilder(content.length()); - for (String line : content.split("\n", -1)) { - int idx = line.indexOf('#'); - if (idx >= 0) { - line = line.substring(0, idx); - } - sb.append(line).append('\n'); - } - return sb.toString(); - } - - private static void collectSourceFile(String line, String prefix, List files) { - if (!line.startsWith(prefix)) { - return; - } - String value = StringHelper.after(line, prefix); - if (value == null || value.isBlank()) { - return; - } - for (String file : value.split(",")) { - file = file.trim(); - if (file.startsWith("file:")) { - file = file.substring(5); - } - int query = file.indexOf('?'); - if (query > 0) { - file = file.substring(0, query); - } - if (!file.isBlank() && !files.contains(file)) { - files.add(file); - } - } - } - - private static String extensionOf(String file) { - if (file == null) { - return null; - } - String path = file; - if (path.startsWith("classpath:")) { - path = path.substring("classpath:".length()); - } else if (path.startsWith("file:")) { - path = path.substring(5); - } - int query = path.indexOf('?'); - if (query > 0) { - path = path.substring(0, query); - } - return FileUtil.onlyExt(path, true); - } - - private static String readContent(String file) { - if (file == null || file.isBlank()) { - return null; - } - String path = file; - if (path.startsWith("classpath:")) { - String resource = path.substring("classpath:".length()); - int query = resource.indexOf('?'); - if (query > 0) { - resource = resource.substring(0, query); - } - try (var is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(resource)) { - if (is == null) { - return null; - } - return new String(is.readAllBytes(), StandardCharsets.UTF_8); - } catch (IOException e) { - return null; - } - } - if (path.startsWith("file:")) { - path = path.substring(5); - } - int query = path.indexOf('?'); - if (query > 0) { - path = path.substring(0, query); - } - Path source = Paths.get(path); - if (!Files.exists(source) || Files.isDirectory(source)) { - return null; - } - String ext = FileUtil.onlyExt(path, true); - if (ext == null || !Set.of("java", "xml", "yaml", "yml", "properties", "groovy").contains(ext)) { - return null; - } - try { - return Files.readString(source, StandardCharsets.UTF_8); - } catch (IOException e) { - return null; - } - } -} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java new file mode 100644 index 0000000000000..63504c338e699 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java @@ -0,0 +1,131 @@ +/* + * 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.dsl.jbang.core.common; + +import java.util.Collection; +import java.util.Properties; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.model.ComponentModel; + +/** + * Adds optional GenAI observability dependencies using the same settings-driven approach as OpenTelemetry and LRA. + *

+ * GenAI component and LangChain4j provider JARs are resolved by the existing silent-run download pipeline + * ({@code DependencyDownloaderComponentResolver}, {@code KnownDependenciesResolver}) — not by scanning route source. + *

+ */ +public final class GenAiDependencyHelper { + + public static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled"; + + private GenAiDependencyHelper() { + } + + /** + * Adds {@code camel:ai-observability} when GenAI artifacts are already in the dependency set and observability is + * requested via {@code --observe} or {@code camel.aiObservability.enabled=true}. + */ + public static void addAiObservabilityIfNeeded(Collection deps, Properties properties, boolean observe) { + addAiObservabilityIfNeeded(deps, properties, observe, new DefaultCamelCatalog()); + } + + static void addAiObservabilityIfNeeded( + Collection deps, Properties properties, boolean observe, CamelCatalog catalog) { + if (!includeAiObservability(properties, observe)) { + return; + } + if (!hasGenAiDependency(deps, catalog)) { + return; + } + if (catalog.otherModel("ai-observability") != null) { + deps.add("camel:ai-observability"); + } + } + + static boolean includeAiObservability(Properties properties, boolean observe) { + String enabled = properties != null ? properties.getProperty(AI_OBSERVABILITY_ENABLED) : null; + if ("false".equalsIgnoreCase(enabled)) { + return false; + } + return observe || "true".equalsIgnoreCase(enabled); + } + + static boolean hasGenAiDependency(Collection deps, CamelCatalog catalog) { + for (String dep : deps) { + if (dep == null || dep.isBlank()) { + continue; + } + if (isGenAiCamelComponent(dep, catalog)) { + return true; + } + if (isLangChain4jProviderJar(dep)) { + return true; + } + } + return false; + } + + private static boolean isGenAiCamelComponent(String dep, CamelCatalog catalog) { + if (dep.startsWith("camel:")) { + String scheme = dep.substring("camel:".length()); + int query = scheme.indexOf('?'); + if (query > 0) { + scheme = scheme.substring(0, query); + } + ComponentModel model = catalog.componentModel(scheme); + return model != null && isAiLabel(model.getLabel()); + } + return dep.contains(":camel-") && isAiArtifactId(dep); + } + + private static boolean isAiArtifactId(String dep) { + int idx = dep.indexOf(":camel-"); + if (idx < 0) { + return false; + } + String artifact = dep.substring(idx + 1); + int colon = artifact.indexOf(':'); + if (colon > 0) { + artifact = artifact.substring(0, colon); + } + return artifact.startsWith("camel-langchain4j") + || artifact.startsWith("camel-openai") + || artifact.startsWith("camel-spring-ai") + || artifact.startsWith("camel-aws-bedrock") + || artifact.startsWith("camel-google-vertexai") + || artifact.startsWith("camel-ai-") + || artifact.contains("-ai-"); + } + + private static boolean isLangChain4jProviderJar(String dep) { + return dep.contains("dev.langchain4j:langchain4j-"); + } + + private static boolean isAiLabel(String label) { + if (label == null || label.isBlank()) { + return false; + } + for (String token : label.split(",")) { + if ("ai".equals(token.trim())) { + return true; + } + } + return false; + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java deleted file mode 100644 index 4d8135fcf360f..0000000000000 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscoveryTest.java +++ /dev/null @@ -1,253 +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.dsl.jbang.core.common; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.Properties; - -import org.apache.camel.catalog.CamelCatalog; -import org.apache.camel.catalog.DefaultCamelCatalog; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import static org.assertj.core.api.Assertions.assertThat; - -class GenAiDependencyDiscoveryTest { - - private static final String LANGCHAIN4J_ROUTE = """ - - beans: - - name: chatModel - type: dev.langchain4j.model.ollama.OllamaChatModel - properties: - baseUrl: http://localhost:11434 - modelName: llama3.2 - - from: - uri: timer:tick - steps: - - to: langchain4j-chat:myModel - """; - - private static final String OPENAI_ROUTE = """ - - from: - uri: timer:tick - steps: - - to: openai:completion - """; - - private static final String XML_ROUTE = """ - - - - - - - """; - - private static final String JAVA_ROUTE = """ - public class AiRoute { - public void configure() { - from("timer:tick").to("langchain4j-chat:model"); - } - } - """; - - @TempDir - Path tempDir; - - @Test - void shouldDiscoverLangChain4jComponentAndProviderWithObserve() throws Exception { - Path route = tempDir.resolve("ai-route.yaml"); - Files.writeString(route, LANGCHAIN4J_ROUTE); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); - - assertThat(deps).contains("camel:langchain4j-chat"); - assertThat(deps).contains("mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"); - assertThat(deps).contains("camel:ai-observability"); - } - - @Test - void shouldDiscoverOpenAiComponentWithExplicitObservabilityProperty() throws Exception { - Path route = tempDir.resolve("openai-route.yaml"); - Files.writeString(route, OPENAI_ROUTE); - Properties properties = new Properties(); - properties.setProperty(GenAiDependencyDiscovery.AI_OBSERVABILITY_ENABLED, "true"); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), properties, false); - - assertThat(deps).contains("camel:openai"); - assertThat(deps).contains("camel:ai-observability"); - } - - @Test - void shouldNotAddAiObservabilityByDefaultWithoutObserve() throws Exception { - Path route = tempDir.resolve("openai-route.yaml"); - Files.writeString(route, OPENAI_ROUTE); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), false); - - assertThat(deps).contains("camel:openai"); - assertThat(deps).doesNotContain("camel:ai-observability"); - } - - @Test - void shouldDiscoverFromXmlRoute() throws Exception { - Path route = tempDir.resolve("ai-route.xml"); - Files.writeString(route, XML_ROUTE); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); - - assertThat(deps).contains("camel:langchain4j-chat"); - assertThat(deps).contains("camel:ai-observability"); - } - - @Test - void shouldDiscoverFromJavaRoute() throws Exception { - Path route = tempDir.resolve("AiRoute.java"); - Files.writeString(route, JAVA_ROUTE); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); - - assertThat(deps).contains("camel:langchain4j-chat"); - assertThat(deps).contains("camel:ai-observability"); - } - - @Test - void shouldSkipAiObservabilityWhenExplicitlyDisabled() throws Exception { - Path route = tempDir.resolve("ai-route.yaml"); - Files.writeString(route, OPENAI_ROUTE); - Properties properties = new Properties(); - properties.setProperty(GenAiDependencyDiscovery.AI_OBSERVABILITY_ENABLED, "false"); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), properties, true); - - assertThat(deps).contains("camel:openai"); - assertThat(deps).doesNotContain("camel:ai-observability"); - } - - @Test - void shouldIncludeAiObservabilityForProviderOnlySourceWhenEnabled() throws Exception { - Path beans = tempDir.resolve("beans.yaml"); - Files.writeString(beans, """ - - beans: - - name: chatModel - type: dev.langchain4j.model.ollama.OllamaChatModel - """); - Properties properties = new Properties(); - properties.setProperty(GenAiDependencyDiscovery.AI_OBSERVABILITY_ENABLED, "true"); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(beans.toString()), properties, false); - - assertThat(deps).contains("mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"); - assertThat(deps).contains("camel:ai-observability"); - } - - @Test - void shouldNotDiscoverForNonGenAiRoute() throws Exception { - Path route = tempDir.resolve("route.yaml"); - Files.writeString(route, """ - - from: - uri: timer:tick - steps: - - to: log:info - """); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); - - assertThat(deps).isEmpty(); - } - - @Test - void shouldDiscoverFromClasspathResource() { - Collection deps = GenAiDependencyDiscovery.discover( - List.of("classpath:genai/langchain4j-route.yaml"), new Properties(), true); - - assertThat(deps).contains("camel:langchain4j-chat"); - assertThat(deps).contains("camel:ai-observability"); - } - - @Test - void shouldDiscoverFromSettingsFile() throws Exception { - Path route = tempDir.resolve("genai-route.yaml"); - Files.writeString(route, OPENAI_ROUTE); - - Path settings = tempDir.resolve("camel-runner.properties"); - Files.writeString(settings, "camel.main.routesIncludePattern=" + route + "\n"); - - Path profile = tempDir.resolve("application.properties"); - Files.writeString(profile, "camel.aiObservability.enabled=true\n"); - - Collection deps = GenAiDependencyDiscovery.discoverFromSettings(settings, profile, false, List.of()); - - assertThat(deps).contains("camel:openai"); - assertThat(deps).contains("camel:ai-observability"); - } - - @Test - void shouldExtractSchemesFromYamlAndXml() { - assertThat(GenAiDependencyDiscovery.extractSchemes(OPENAI_ROUTE, "yaml")).containsExactly("timer", "openai"); - assertThat(GenAiDependencyDiscovery.extractSchemes(XML_ROUTE, "xml")).containsExactly("timer", "langchain4j-chat"); - } - - @Test - void shouldIgnoreYamlCommentsAndPropertyFalsePositives() throws Exception { - Path route = tempDir.resolve("false-positive.yaml"); - Files.writeString(route, """ - model: "openai:gpt-4" - # from: openai:completion - - from: - uri: timer:tick - steps: - - to: log:info - """); - - Collection deps = GenAiDependencyDiscovery.discover(List.of(route.toString()), new Properties(), true); - - assertThat(deps).isEmpty(); - } - - @Test - void shouldDiscoverOpenAiProviderFromJavaContent() { - String content = "dev.langchain4j.model.openai.OpenAiChatModel model = OpenAiChatModel.builder().apiKey(key).build();"; - assertThat(GenAiDependencyDiscovery.discoverProviderDependencies(content)) - .contains("mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"); - } - - @Test - void shouldIgnoreUnknownGenAiSchemeWhenNotInCatalog() throws Exception { - Path route = tempDir.resolve("future-route.yaml"); - Files.writeString(route, """ - - from: - uri: openai:completion - """); - - CamelCatalog catalog = new DefaultCamelCatalog() { - @Override - public org.apache.camel.tooling.model.ComponentModel componentModel(String name) { - return null; - } - }; - - Collection deps = GenAiDependencyDiscovery.discover( - List.of(route.toString()), new Properties(), true, catalog); - - assertThat(deps).isEmpty(); - } -} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java new file mode 100644 index 0000000000000..ad37d3361ecf1 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java @@ -0,0 +1,98 @@ +/* + * 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.dsl.jbang.core.common; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Properties; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.model.ComponentModel; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class GenAiDependencyHelperTest { + + private final CamelCatalog catalog = new DefaultCamelCatalog(); + + @Test + void addsAiObservabilityWhenGenAiComponentPresentAndObserveEnabled() { + List deps = new ArrayList<>(List.of("camel:langchain4j-chat")); + + GenAiDependencyHelper.addAiObservabilityIfNeeded(deps, new Properties(), true, catalog); + + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void addsAiObservabilityWhenGenAiPropertyEnabled() { + List deps = new ArrayList<>(List.of("mvn:org.apache.camel:camel-openai")); + Properties properties = new Properties(); + properties.setProperty(GenAiDependencyHelper.AI_OBSERVABILITY_ENABLED, "true"); + + GenAiDependencyHelper.addAiObservabilityIfNeeded(deps, properties, false, catalog); + + assertThat(deps).contains("camel:ai-observability"); + } + + @Test + void skipsAiObservabilityWithoutGenAiArtifacts() { + List deps = new ArrayList<>(List.of("camel:timer")); + + GenAiDependencyHelper.addAiObservabilityIfNeeded(deps, new Properties(), true, catalog); + + assertThat(deps).doesNotContain("camel:ai-observability"); + } + + @Test + void skipsAiObservabilityWhenExplicitlyDisabled() { + List deps = new ArrayList<>(List.of("camel:langchain4j-chat")); + Properties properties = new Properties(); + properties.setProperty(GenAiDependencyHelper.AI_OBSERVABILITY_ENABLED, "false"); + + GenAiDependencyHelper.addAiObservabilityIfNeeded(deps, properties, true, catalog); + + assertThat(deps).doesNotContain("camel:ai-observability"); + } + + @Test + void detectsLangChain4jProviderJar() { + Collection deps = List.of("mvn:dev.langchain4j:langchain4j-ollama:1.0.0"); + + assertThat(GenAiDependencyHelper.hasGenAiDependency(deps, catalog)).isTrue(); + } + + @Test + void detectsGenAiComponentFromCatalogLabel() { + CamelCatalog mockCatalog = mock(CamelCatalog.class); + ComponentModel model = new ComponentModel(); + model.setLabel("ai"); + when(mockCatalog.componentModel("openai")).thenReturn(model); + + assertThat(GenAiDependencyHelper.hasGenAiDependency(List.of("camel:openai"), mockCatalog)).isTrue(); + } + + @Test + void timerComponentIsNotGenAi() { + assertThat(GenAiDependencyHelper.hasGenAiDependency(List.of("camel:timer"), catalog)).isFalse(); + } +} diff --git a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties index f88b037486c70..aba66b6524a2e 100644 --- a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties +++ b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties @@ -84,3 +84,6 @@ dev.langchain4j.model.vertexai = dev.langchain4j:langchain4j-vertex-ai:${langcha dev.langchain4j.model.googleai = dev.langchain4j:langchain4j-google-ai-gemini:${langchain4j-version} dev.langchain4j.model.github = dev.langchain4j:langchain4j-github-models:${langchain4j-version} dev.langchain4j.model.embedding.onnx = dev.langchain4j:langchain4j-embeddings:${langchain4j-beta-version} + +org.apache.camel.component.ai.observability.GenAiObservabilityImpl = camel:ai-observability +camel.aiObservability = camel:ai-observability From ea1ba9d259c154486b84196a824a2093585c409d Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Fri, 4 Sep 2026 08:15:10 +0200 Subject: [PATCH 4/5] Regen --- .../resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd index d27bdfbe4cf31..2b1f4d80bc757 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd @@ -1,4 +1,4 @@ - + From 054c4fda859691a15e1b126eebb4aee5fbe8e310 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 21:29:28 +0000 Subject: [PATCH 5/5] CAMEL-24566: Address gnodet review on GenAiDependencyHelper Use catalog modelFromMavenGAV for mvn: GenAI detection instead of artifact-id heuristics, exclude camel-ai-observability from triggering duplicate observability deps, narrow AI_OBSERVABILITY_ENABLED visibility, and document camel.aiObservability known-deps entry. Co-authored-by: Cursor --- .../core/common/GenAiDependencyHelper.java | 87 +++++++++++++------ .../common/GenAiDependencyHelperTest.java | 29 +++++-- .../camel-main-known-dependencies.properties | 1 + 3 files changed, 81 insertions(+), 36 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java index 63504c338e699..40a3426fddb51 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java @@ -21,6 +21,8 @@ import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.maven.MavenGav; +import org.apache.camel.tooling.model.ArtifactModel; import org.apache.camel.tooling.model.ComponentModel; /** @@ -32,7 +34,10 @@ */ public final class GenAiDependencyHelper { - public static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled"; + static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled"; + + private static final String AI_OBSERVABILITY_ARTIFACT = "camel-ai-observability"; + private static final String AI_OBSERVABILITY_SCHEME = "ai-observability"; private GenAiDependencyHelper() { } @@ -53,7 +58,10 @@ static void addAiObservabilityIfNeeded( if (!hasGenAiDependency(deps, catalog)) { return; } - if (catalog.otherModel("ai-observability") != null) { + if (alreadyHasAiObservability(deps)) { + return; + } + if (catalog.otherModel(AI_OBSERVABILITY_SCHEME) != null) { deps.add("camel:ai-observability"); } } @@ -71,7 +79,10 @@ static boolean hasGenAiDependency(Collection deps, CamelCatalog catalog) if (dep == null || dep.isBlank()) { continue; } - if (isGenAiCamelComponent(dep, catalog)) { + if (isGenAiCamelScheme(dep, catalog)) { + return true; + } + if (isGenAiMavenArtifact(dep, catalog)) { return true; } if (isLangChain4jProviderJar(dep)) { @@ -81,36 +92,58 @@ static boolean hasGenAiDependency(Collection deps, CamelCatalog catalog) return false; } - private static boolean isGenAiCamelComponent(String dep, CamelCatalog catalog) { - if (dep.startsWith("camel:")) { - String scheme = dep.substring("camel:".length()); - int query = scheme.indexOf('?'); - if (query > 0) { - scheme = scheme.substring(0, query); - } - ComponentModel model = catalog.componentModel(scheme); - return model != null && isAiLabel(model.getLabel()); + private static boolean isGenAiCamelScheme(String dep, CamelCatalog catalog) { + if (!dep.startsWith("camel:")) { + return false; + } + String scheme = dep.substring("camel:".length()); + int query = scheme.indexOf('?'); + if (query > 0) { + scheme = scheme.substring(0, query); } - return dep.contains(":camel-") && isAiArtifactId(dep); + if (AI_OBSERVABILITY_SCHEME.equals(scheme)) { + return false; + } + ComponentModel model = catalog.componentModel(scheme); + return model != null && isAiLabel(model.getLabel()); } - private static boolean isAiArtifactId(String dep) { - int idx = dep.indexOf(":camel-"); - if (idx < 0) { + private static boolean isGenAiMavenArtifact(String dep, CamelCatalog catalog) { + if (!dep.startsWith("mvn:")) { + return false; + } + try { + MavenGav gav = MavenGav.parseGav(dep.substring(4)); + String artifactId = gav.getArtifactId(); + if (artifactId == null || AI_OBSERVABILITY_ARTIFACT.equals(artifactId)) { + return false; + } + ArtifactModel model = catalog.modelFromMavenGAV(gav.getGroupId(), artifactId, gav.getVersion()); + return model != null && isAiLabel(model.getLabel()); + } catch (Exception e) { return false; } - String artifact = dep.substring(idx + 1); - int colon = artifact.indexOf(':'); - if (colon > 0) { - artifact = artifact.substring(0, colon); + } + + private static boolean alreadyHasAiObservability(Collection deps) { + for (String dep : deps) { + if (dep == null || dep.isBlank()) { + continue; + } + if (dep.startsWith("camel:")) { + String scheme = dep.substring("camel:".length()); + int query = scheme.indexOf('?'); + if (query > 0) { + scheme = scheme.substring(0, query); + } + if (AI_OBSERVABILITY_SCHEME.equals(scheme)) { + return true; + } + } else if (dep.startsWith("mvn:") && dep.contains(":" + AI_OBSERVABILITY_ARTIFACT)) { + return true; + } } - return artifact.startsWith("camel-langchain4j") - || artifact.startsWith("camel-openai") - || artifact.startsWith("camel-spring-ai") - || artifact.startsWith("camel-aws-bedrock") - || artifact.startsWith("camel-google-vertexai") - || artifact.startsWith("camel-ai-") - || artifact.contains("-ai-"); + return false; } private static boolean isLangChain4jProviderJar(String dep) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java index ad37d3361ecf1..323fdf9ca7b8a 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java @@ -23,12 +23,9 @@ import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; -import org.apache.camel.tooling.model.ComponentModel; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; class GenAiDependencyHelperTest { @@ -83,16 +80,30 @@ void detectsLangChain4jProviderJar() { @Test void detectsGenAiComponentFromCatalogLabel() { - CamelCatalog mockCatalog = mock(CamelCatalog.class); - ComponentModel model = new ComponentModel(); - model.setLabel("ai"); - when(mockCatalog.componentModel("openai")).thenReturn(model); - - assertThat(GenAiDependencyHelper.hasGenAiDependency(List.of("camel:openai"), mockCatalog)).isTrue(); + assertThat(GenAiDependencyHelper.hasGenAiDependency(List.of("camel:openai"), catalog)).isTrue(); } @Test void timerComponentIsNotGenAi() { assertThat(GenAiDependencyHelper.hasGenAiDependency(List.of("camel:timer"), catalog)).isFalse(); } + + @Test + void explicitAiObservabilityMavenDepDoesNotCountAsGenAiRouteDependency() { + assertThat(GenAiDependencyHelper.hasGenAiDependency( + List.of("mvn:org.apache.camel:camel-ai-observability"), catalog)).isFalse(); + } + + @Test + void doesNotDuplicateAiObservabilityWhenExplicitlyProvided() { + List deps = new ArrayList<>( + List.of( + "camel:openai", + "mvn:org.apache.camel:camel-ai-observability")); + + GenAiDependencyHelper.addAiObservabilityIfNeeded(deps, new Properties(), true, catalog); + + assertThat(deps).contains("mvn:org.apache.camel:camel-ai-observability"); + assertThat(deps.stream().filter("camel:ai-observability"::equals)).hasSize(0); + } } diff --git a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties index aba66b6524a2e..7a43b2dc61bf6 100644 --- a/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties +++ b/dsl/camel-kamelet-main/src/main/resources/camel-main-known-dependencies.properties @@ -86,4 +86,5 @@ dev.langchain4j.model.github = dev.langchain4j:langchain4j-github-models:${langc dev.langchain4j.model.embedding.onnx = dev.langchain4j:langchain4j-embeddings:${langchain4j-beta-version} org.apache.camel.component.ai.observability.GenAiObservabilityImpl = camel:ai-observability +# camel-main property prefix (same pattern as camel.opentelemetry) — resolves ai-observability when GenAI observability config is accessed camel.aiObservability = camel:ai-observability