Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -811,6 +812,13 @@ protected Set<String> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* GenAI component and LangChain4j provider JARs are resolved by the existing silent-run download pipeline
* ({@code DependencyDownloaderComponentResolver}, {@code KnownDependenciesResolver}) — not by scanning route source.
* </p>
*/
public final class GenAiDependencyHelper {

Comment thread
davsclaus marked this conversation as resolved.
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<String> deps, Properties properties, boolean observe) {
addAiObservabilityIfNeeded(deps, properties, observe, new DefaultCamelCatalog());
}

static void addAiObservabilityIfNeeded(
Collection<String> 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<String> 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;
Comment thread
davsclaus marked this conversation as resolved.
}
}

private static boolean alreadyHasAiObservability(Collection<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> deps = new ArrayList<>(List.of("camel:timer"));

GenAiDependencyHelper.addAiObservabilityIfNeeded(deps, new Properties(), true, catalog);

assertThat(deps).doesNotContain("camel:ai-observability");
}

@Test
void skipsAiObservabilityWhenExplicitlyDisabled() {
List<String> 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");
}
Comment thread
davsclaus marked this conversation as resolved.

@Test
void detectsLangChain4jProviderJar() {
Collection<String> 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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Comment thread
davsclaus marked this conversation as resolved.
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}
Comment thread
davsclaus marked this conversation as resolved.

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