Skip to content

Commit 429ebfa

Browse files
committed
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 <cursoragent@cursor.com>
1 parent cd29991 commit 429ebfa

8 files changed

Lines changed: 545 additions & 0 deletions

File tree

dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper;
5353
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
54+
import org.apache.camel.dsl.jbang.core.common.GenAiDependencyDiscovery;
5455
import org.apache.camel.dsl.jbang.core.common.HawtioVersion;
5556
import org.apache.camel.dsl.jbang.core.common.JavaVersionCompletionCandidates;
5657
import org.apache.camel.dsl.jbang.core.common.LoggingLevelCompletionCandidates;
@@ -811,6 +812,9 @@ protected Set<String> resolveDependencies(Path settings, Path profile) throws Ex
811812
answer.add("mvn:org.hibernate.orm:hibernate-core");
812813
}
813814

815+
// auto-discover GenAI component, provider and observability dependencies
816+
answer.addAll(GenAiDependencyDiscovery.discoverFromSettings(settings, profile, observe, files));
817+
814818
// remove duplicate versions (keep first)
815819
Map<String, String> versions = new HashMap<>();
816820
Set<String> toBeRemoved = new HashSet<>();

dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
5151
import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
5252
import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
53+
import org.apache.camel.dsl.jbang.core.common.GenAiDependencyDiscovery;
5354
import org.apache.camel.dsl.jbang.core.common.JavaVersionCompletionCandidates;
5455
import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
5556
import org.apache.camel.dsl.jbang.core.common.LoggingLevelCompletionCandidates;
@@ -1261,6 +1262,8 @@ private int run() throws Exception {
12611262
dependencies.add("camel:observability-services");
12621263
main.addOverrideProperty("camel.metrics.logMetricsOnShutdown", "false");
12631264
}
1265+
GenAiDependencyDiscovery.discover(files, profileProperties, serverOptions.observe)
1266+
.forEach(dependencies::add);
12641267
if (serverOptions.openapiUi) {
12651268
dependencies.add("camel:platform-http-main");
12661269
dependencies.add("camel:openapi-java");
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.dsl.jbang.core.common;
18+
19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.nio.file.Paths;
24+
import java.util.ArrayList;
25+
import java.util.Collection;
26+
import java.util.LinkedHashSet;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Properties;
30+
import java.util.Set;
31+
import java.util.regex.Matcher;
32+
import java.util.regex.Pattern;
33+
34+
import org.apache.camel.catalog.CamelCatalog;
35+
import org.apache.camel.catalog.DefaultCamelCatalog;
36+
import org.apache.camel.tooling.model.ComponentModel;
37+
import org.apache.camel.util.FileUtil;
38+
import org.apache.camel.util.StringHelper;
39+
40+
/**
41+
* Discovers GenAI-related dependencies for Camel JBang run/export from route URIs, LangChain4j provider classes and
42+
* observability settings.
43+
*/
44+
public final class GenAiDependencyDiscovery {
45+
46+
public static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled";
47+
48+
private static final Set<String> GEN_AI_SCHEMES = Set.of(
49+
"aws-bedrock", "aws-bedrock-agent", "aws-bedrock-agent-runtime",
50+
"aws2-textract", "docling",
51+
"langchain4j-chat", "langchain4j-embeddings", "langchain4j-embeddingstore",
52+
"langchain4j-tools", "langchain4j-agent", "langchain4j-web-search",
53+
"openai", "kserve", "tensorflow-serving", "djl",
54+
"huggingface", "ai-tool", "google-vertexai");
55+
56+
private static final Map<String, String> LANGCHAIN4J_PROVIDER_DEPENDENCIES = Map.ofEntries(
57+
Map.entry("dev.langchain4j.model.ollama.OllamaChatModel",
58+
"mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"),
59+
Map.entry("dev.langchain4j.model.ollama.OllamaEmbeddingModel",
60+
"mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"),
61+
Map.entry("dev.langchain4j.model.ollama.OllamaLanguageModel",
62+
"mvn:dev.langchain4j:langchain4j-ollama:${langchain4j-version}"),
63+
Map.entry("dev.langchain4j.model.openai.OpenAiChatModel",
64+
"mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"),
65+
Map.entry("dev.langchain4j.model.openai.OpenAiEmbeddingModel",
66+
"mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"),
67+
Map.entry("dev.langchain4j.model.openai.OpenAiLanguageModel",
68+
"mvn:dev.langchain4j:langchain4j-open-ai:${langchain4j-version}"),
69+
Map.entry("dev.langchain4j.model.huggingface.HuggingFaceChatModel",
70+
"mvn:dev.langchain4j:langchain4j-hugging-face:${langchain4j-beta-version}"),
71+
Map.entry("dev.langchain4j.model.anthropic.AnthropicChatModel",
72+
"mvn:dev.langchain4j:langchain4j-anthropic:${langchain4j-version}"),
73+
Map.entry("dev.langchain4j.model.azure.AzureOpenAiChatModel",
74+
"mvn:dev.langchain4j:langchain4j-azure-open-ai:${langchain4j-version}"),
75+
Map.entry("dev.langchain4j.model.mistralai.MistralAiChatModel",
76+
"mvn:dev.langchain4j:langchain4j-mistral-ai:${langchain4j-version}"),
77+
Map.entry("dev.langchain4j.model.vertexai.VertexAiChatModel",
78+
"mvn:dev.langchain4j:langchain4j-vertex-ai:${langchain4j-version}"),
79+
Map.entry("dev.langchain4j.model.googleai.GoogleAiGeminiChatModel",
80+
"mvn:dev.langchain4j:langchain4j-google-ai-gemini:${langchain4j-version}"));
81+
82+
private static final Pattern YAML_SCHEME_PATTERN = Pattern.compile(
83+
"(?:uri:\\s*[\"']?|from:[ \\t]+[\"']?|to:[ \\t]+[\"']?|toD:[ \\t]+[\"']?)"
84+
+ "([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?",
85+
Pattern.MULTILINE);
86+
87+
private static final Pattern XML_SCHEME_PATTERN = Pattern.compile(
88+
"(?:<from|<to|<toD)\\s+uri=[\"']([a-zA-Z][a-zA-Z0-9+.-]*):",
89+
Pattern.CASE_INSENSITIVE);
90+
91+
private static final Pattern JAVA_URI_PATTERN = Pattern.compile(
92+
"[\"']([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?[^\"']*[\"']");
93+
94+
private GenAiDependencyDiscovery() {
95+
}
96+
97+
public static Collection<String> discover(Collection<String> sourceFiles, Properties properties, boolean observe) {
98+
return discover(sourceFiles, properties, observe, new DefaultCamelCatalog());
99+
}
100+
101+
static Collection<String> discover(
102+
Collection<String> sourceFiles, Properties properties, boolean observe,
103+
CamelCatalog catalog) {
104+
Set<String> deps = new LinkedHashSet<>();
105+
boolean hasGenAiRoutes = false;
106+
107+
for (String file : sourceFiles) {
108+
String content = readContent(file);
109+
if (content == null) {
110+
continue;
111+
}
112+
for (String scheme : extractSchemes(content)) {
113+
if (GEN_AI_SCHEMES.contains(scheme)) {
114+
ComponentModel model = catalog.componentModel(scheme);
115+
if (model != null) {
116+
hasGenAiRoutes = true;
117+
deps.add("camel:" + scheme);
118+
}
119+
}
120+
}
121+
deps.addAll(discoverProviderDependencies(content));
122+
}
123+
124+
if (hasGenAiRoutes && includeAiObservability(properties, observe)
125+
&& catalog.otherModel("ai-observability") != null) {
126+
deps.add("camel:ai-observability");
127+
}
128+
129+
return deps;
130+
}
131+
132+
public static Collection<String> discoverFromSettings(
133+
Path settings, Path profile, boolean observe,
134+
Collection<String> sourceFiles)
135+
throws IOException {
136+
List<String> files = new ArrayList<>(sourceFiles);
137+
if (settings != null && Files.exists(settings)) {
138+
for (String line : RuntimeUtil.loadPropertiesLines(settings)) {
139+
collectSourceFile(line, "camel.main.routesIncludePattern=", files);
140+
collectSourceFile(line, "java=", files);
141+
collectSourceFile(line, "xml=", files);
142+
collectSourceFile(line, "yaml=", files);
143+
}
144+
}
145+
Properties properties = new Properties();
146+
if (profile != null && Files.exists(profile)) {
147+
RuntimeUtil.loadProperties(properties, profile);
148+
}
149+
return discover(files, properties, observe);
150+
}
151+
152+
static boolean includeAiObservability(Properties properties, boolean observe) {
153+
String enabled = properties != null ? properties.getProperty(AI_OBSERVABILITY_ENABLED) : null;
154+
if ("false".equalsIgnoreCase(enabled)) {
155+
return false;
156+
}
157+
if ("true".equalsIgnoreCase(enabled)) {
158+
return true;
159+
}
160+
// include by default for GenAI routes; --observe explicitly enables observability stack
161+
return observe || enabled == null;
162+
}
163+
164+
static List<String> extractSchemes(String content) {
165+
List<String> schemes = new ArrayList<>();
166+
addSchemeMatches(schemes, YAML_SCHEME_PATTERN, content);
167+
addSchemeMatches(schemes, XML_SCHEME_PATTERN, content);
168+
addSchemeMatches(schemes, JAVA_URI_PATTERN, content);
169+
schemes.removeIf(scheme -> "http".equals(scheme) || "https".equals(scheme));
170+
return schemes;
171+
}
172+
173+
static Collection<String> discoverProviderDependencies(String content) {
174+
Set<String> deps = new LinkedHashSet<>();
175+
for (Map.Entry<String, String> entry : LANGCHAIN4J_PROVIDER_DEPENDENCIES.entrySet()) {
176+
if (content.contains(entry.getKey())) {
177+
deps.add(entry.getValue());
178+
}
179+
}
180+
return deps;
181+
}
182+
183+
private static void addSchemeMatches(List<String> schemes, Pattern pattern, String content) {
184+
Matcher matcher = pattern.matcher(content);
185+
while (matcher.find()) {
186+
String scheme = matcher.group(1);
187+
if (!schemes.contains(scheme)) {
188+
schemes.add(scheme);
189+
}
190+
}
191+
}
192+
193+
private static void collectSourceFile(String line, String prefix, List<String> files) {
194+
if (!line.startsWith(prefix)) {
195+
return;
196+
}
197+
String value = StringHelper.after(line, prefix);
198+
if (value == null || value.isBlank()) {
199+
return;
200+
}
201+
for (String file : value.split(",")) {
202+
file = file.trim();
203+
if (file.startsWith("file:")) {
204+
file = file.substring(5);
205+
}
206+
if (!file.isBlank() && !files.contains(file)) {
207+
files.add(file);
208+
}
209+
}
210+
}
211+
212+
private static String readContent(String file) {
213+
if (file == null || file.isBlank()) {
214+
return null;
215+
}
216+
String path = file;
217+
if (path.startsWith("classpath:")) {
218+
String resource = path.substring("classpath:".length());
219+
try (var is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(resource)) {
220+
if (is == null) {
221+
return null;
222+
}
223+
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
224+
} catch (IOException e) {
225+
return null;
226+
}
227+
}
228+
if (path.startsWith("file:")) {
229+
path = path.substring(5);
230+
}
231+
Path source = Paths.get(path);
232+
if (!Files.exists(source) || Files.isDirectory(source)) {
233+
return null;
234+
}
235+
String ext = FileUtil.onlyExt(path, true);
236+
if (ext == null || !Set.of("java", "xml", "yaml", "yml", "properties").contains(ext)) {
237+
return null;
238+
}
239+
try {
240+
return Files.readString(source, StandardCharsets.UTF_8);
241+
} catch (IOException e) {
242+
return null;
243+
}
244+
}
245+
}

dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,25 @@ public void shouldExportGroovy(RuntimeType rt) throws Exception {
814814
Assertions.assertTrue(f.exists());
815815
}
816816

817+
@Test
818+
public void shouldExportGenAiRouteWithObservability() throws Exception {
819+
Export command = new Export(new CamelJBangMain());
820+
CommandLine.populateCommand(command,
821+
"--gav=examples:genai:1.0.0",
822+
"--dir=" + workingDir,
823+
"--quiet",
824+
"--runtime=main",
825+
"src/test/resources/genai-route.yaml");
826+
int exit = command.doCall();
827+
828+
Assertions.assertEquals(0, exit);
829+
Model model = readMavenModel();
830+
Assertions.assertTrue(
831+
containsDependency(model.getDependencies(), "org.apache.camel", "camel-langchain4j-chat", null));
832+
Assertions.assertTrue(
833+
containsDependency(model.getDependencies(), "org.apache.camel", "camel-ai-observability", null));
834+
}
835+
817836
@ParameterizedTest
818837
@MethodSource("runtimeProvider")
819838
public void shouldExportObserve(RuntimeType rt) throws Exception {

0 commit comments

Comments
 (0)