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..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,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.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; @@ -811,6 +812,13 @@ protected Set resolveDependencies(Path settings, Path profile) throws Ex answer.add("mvn:org.hibernate.orm:hibernate-core"); } + // 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 // 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..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,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.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; @@ -1261,6 +1262,7 @@ private int run() throws Exception { dependencies.add("camel:observability-services"); main.addOverrideProperty("camel.metrics.logMetricsOnShutdown", "false"); } + 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/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..40a3426fddb51 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelper.java @@ -0,0 +1,164 @@ +/* + * 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.maven.MavenGav; +import org.apache.camel.tooling.model.ArtifactModel; +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 { + + 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() { + } + + /** + * 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 (alreadyHasAiObservability(deps)) { + return; + } + if (catalog.otherModel(AI_OBSERVABILITY_SCHEME) != 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 (isGenAiCamelScheme(dep, catalog)) { + return true; + } + if (isGenAiMavenArtifact(dep, catalog)) { + return true; + } + if (isLangChain4jProviderJar(dep)) { + return true; + } + } + return false; + } + + 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); + } + if (AI_OBSERVABILITY_SCHEME.equals(scheme)) { + return false; + } + ComponentModel model = catalog.componentModel(scheme); + return model != null && isAiLabel(model.getLabel()); + } + + 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; + } + } + + 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 false; + } + + 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/commands/ExportTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java index 45b4eca08aac5..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 @@ -830,6 +830,26 @@ 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", + "--observe=true", + "src/test/resources/genai/langchain4j-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/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..323fdf9ca7b8a --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyHelperTest.java @@ -0,0 +1,109 @@ +/* + * 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.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +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() { + 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-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..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 @@ -73,3 +73,18 @@ 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 = 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} + +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