Skip to content

Commit aedcb7f

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

7 files changed

Lines changed: 188 additions & 100 deletions

File tree

core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ beanFactory
1313
beanio
1414
bearerToken
1515
bindy
16+
cache
1617
cbor
1718
choice
1819
circuitBreaker
@@ -135,6 +136,8 @@ propertyExpression
135136
protobuf
136137
put
137138
python
139+
python3
140+
quickjs
138141
randomLoadBalancer
139142
recipientList
140143
redeliveryPolicy
@@ -210,11 +213,13 @@ to
210213
toD
211214
tokenize
212215
tokenizer
216+
toon
213217
topicLoadBalancer
214218
transacted
215219
transform
216220
transformDataType
217221
transformers
222+
ubl
218223
univocityCsv
219224
univocityFixed
220225
univocityHeader

dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GenAiDependencyDiscovery.java

Lines changed: 118 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
package org.apache.camel.dsl.jbang.core.common;
1818

1919
import java.io.IOException;
20+
import java.io.InputStream;
2021
import java.nio.charset.StandardCharsets;
2122
import java.nio.file.Files;
2223
import java.nio.file.Path;
2324
import java.nio.file.Paths;
2425
import java.util.ArrayList;
2526
import java.util.Collection;
27+
import java.util.LinkedHashMap;
2628
import java.util.LinkedHashSet;
2729
import java.util.List;
2830
import java.util.Map;
@@ -37,6 +39,9 @@
3739
import org.apache.camel.util.FileUtil;
3840
import org.apache.camel.util.StringHelper;
3941

42+
import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.CLASSPATH_FILES;
43+
import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.GROOVY_FILES;
44+
4045
/**
4146
* Discovers GenAI-related dependencies for Camel JBang run/export from route URIs, LangChain4j provider classes and
4247
* observability settings.
@@ -45,51 +50,29 @@ public final class GenAiDependencyDiscovery {
4550

4651
public static final String AI_OBSERVABILITY_ENABLED = "camel.aiObservability.enabled";
4752

53+
private static final String KNOWN_DEPENDENCIES = "camel-main-known-dependencies.properties";
54+
4855
private static final Set<String> GEN_AI_SCHEMES = Set.of(
4956
"aws-bedrock", "aws-bedrock-agent", "aws-bedrock-agent-runtime",
5057
"aws2-textract", "docling",
5158
"langchain4j-chat", "langchain4j-embeddings", "langchain4j-embeddingstore",
5259
"langchain4j-tools", "langchain4j-agent", "langchain4j-web-search",
5360
"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}"));
61+
"huggingface", "ai-tool", "google-vertexai", "spring-ai-chat");
8162

8263
private static final Pattern YAML_SCHEME_PATTERN = Pattern.compile(
8364
"(?:uri:\\s*[\"']?|from:[ \\t]+[\"']?|to:[ \\t]+[\"']?|toD:[ \\t]+[\"']?)"
8465
+ "([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?",
8566
Pattern.MULTILINE);
8667

8768
private static final Pattern XML_SCHEME_PATTERN = Pattern.compile(
88-
"(?:<from|<to|<toD)\\s+uri=[\"']([a-zA-Z][a-zA-Z0-9+.-]*):",
69+
"<(?:[\\w]+:)?(from|to|toD|enrich|wireTap)\\s+uri=[\"']([a-zA-Z][a-zA-Z0-9+.-]*):",
8970
Pattern.CASE_INSENSITIVE);
9071

9172
private static final Pattern JAVA_URI_PATTERN = Pattern.compile(
92-
"[\"']([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?[^\"']*[\"']");
73+
"(?:from|to|toD|wireTap|enrich|pollEnrich)\\s*\\(\\s*[\"']([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?[^\"']*[\"']");
74+
75+
private static volatile Map<String, String> langchain4jProviderDependencies;
9376

9477
private GenAiDependencyDiscovery() {
9578
}
@@ -103,13 +86,15 @@ static Collection<String> discover(
10386
CamelCatalog catalog) {
10487
Set<String> deps = new LinkedHashSet<>();
10588
boolean hasGenAiRoutes = false;
89+
boolean hasProviderReferences = false;
10690

10791
for (String file : sourceFiles) {
10892
String content = readContent(file);
10993
if (content == null) {
11094
continue;
11195
}
112-
for (String scheme : extractSchemes(content)) {
96+
String ext = extensionOf(file);
97+
for (String scheme : extractSchemes(content, ext)) {
11398
if (GEN_AI_SCHEMES.contains(scheme)) {
11499
ComponentModel model = catalog.componentModel(scheme);
115100
if (model != null) {
@@ -118,10 +103,14 @@ static Collection<String> discover(
118103
}
119104
}
120105
}
121-
deps.addAll(discoverProviderDependencies(content));
106+
Collection<String> providers = discoverProviderDependencies(content);
107+
if (!providers.isEmpty()) {
108+
hasProviderReferences = true;
109+
deps.addAll(providers);
110+
}
122111
}
123112

124-
if (hasGenAiRoutes && includeAiObservability(properties, observe)
113+
if ((hasGenAiRoutes || hasProviderReferences) && includeAiObservability(properties, observe)
125114
&& catalog.otherModel("ai-observability") != null) {
126115
deps.add("camel:ai-observability");
127116
}
@@ -137,9 +126,8 @@ public static Collection<String> discoverFromSettings(
137126
if (settings != null && Files.exists(settings)) {
138127
for (String line : RuntimeUtil.loadPropertiesLines(settings)) {
139128
collectSourceFile(line, "camel.main.routesIncludePattern=", files);
140-
collectSourceFile(line, "java=", files);
141-
collectSourceFile(line, "xml=", files);
142-
collectSourceFile(line, "yaml=", files);
129+
collectSourceFile(line, CLASSPATH_FILES + "=", files);
130+
collectSourceFile(line, GROOVY_FILES + "=", files);
143131
}
144132
}
145133
Properties properties = new Properties();
@@ -154,42 +142,95 @@ static boolean includeAiObservability(Properties properties, boolean observe) {
154142
if ("false".equalsIgnoreCase(enabled)) {
155143
return false;
156144
}
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;
145+
return observe || "true".equalsIgnoreCase(enabled);
162146
}
163147

164-
static List<String> extractSchemes(String content) {
148+
static List<String> extractSchemes(String content, String ext) {
149+
if (content == null || ext == null) {
150+
return List.of();
151+
}
165152
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);
153+
switch (ext) {
154+
case "yaml", "yml" -> addSchemeMatches(schemes, YAML_SCHEME_PATTERN, stripYamlComments(content));
155+
case "xml" -> addSchemeMatches(schemes, XML_SCHEME_PATTERN, content, 2);
156+
case "java", "groovy" -> addSchemeMatches(schemes, JAVA_URI_PATTERN, content, 1);
157+
default -> {
158+
}
159+
}
169160
schemes.removeIf(scheme -> "http".equals(scheme) || "https".equals(scheme));
170161
return schemes;
171162
}
172163

173164
static Collection<String> discoverProviderDependencies(String content) {
174165
Set<String> deps = new LinkedHashSet<>();
175-
for (Map.Entry<String, String> entry : LANGCHAIN4J_PROVIDER_DEPENDENCIES.entrySet()) {
166+
for (Map.Entry<String, String> entry : providerDependencies().entrySet()) {
176167
if (content.contains(entry.getKey())) {
177-
deps.add(entry.getValue());
168+
deps.add("mvn:" + entry.getValue());
178169
}
179170
}
180171
return deps;
181172
}
182173

174+
private static Map<String, String> providerDependencies() {
175+
Map<String, String> answer = langchain4jProviderDependencies;
176+
if (answer != null) {
177+
return answer;
178+
}
179+
synchronized (GenAiDependencyDiscovery.class) {
180+
answer = langchain4jProviderDependencies;
181+
if (answer != null) {
182+
return answer;
183+
}
184+
answer = loadLangChain4jProviderDependencies();
185+
langchain4jProviderDependencies = answer;
186+
return answer;
187+
}
188+
}
189+
190+
private static Map<String, String> loadLangChain4jProviderDependencies() {
191+
Map<String, String> answer = new LinkedHashMap<>();
192+
Properties properties = new Properties();
193+
try (InputStream is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(KNOWN_DEPENDENCIES)) {
194+
if (is != null) {
195+
properties.load(is);
196+
}
197+
} catch (IOException e) {
198+
return answer;
199+
}
200+
for (String key : properties.stringPropertyNames()) {
201+
if (key.startsWith("dev.langchain4j.")) {
202+
answer.put(key, properties.getProperty(key));
203+
}
204+
}
205+
return answer;
206+
}
207+
183208
private static void addSchemeMatches(List<String> schemes, Pattern pattern, String content) {
209+
addSchemeMatches(schemes, pattern, content, 1);
210+
}
211+
212+
private static void addSchemeMatches(List<String> schemes, Pattern pattern, String content, int group) {
184213
Matcher matcher = pattern.matcher(content);
185214
while (matcher.find()) {
186-
String scheme = matcher.group(1);
215+
String scheme = matcher.group(group);
187216
if (!schemes.contains(scheme)) {
188217
schemes.add(scheme);
189218
}
190219
}
191220
}
192221

222+
private static String stripYamlComments(String content) {
223+
StringBuilder sb = new StringBuilder(content.length());
224+
for (String line : content.split("\n", -1)) {
225+
int idx = line.indexOf('#');
226+
if (idx >= 0) {
227+
line = line.substring(0, idx);
228+
}
229+
sb.append(line).append('\n');
230+
}
231+
return sb.toString();
232+
}
233+
193234
private static void collectSourceFile(String line, String prefix, List<String> files) {
194235
if (!line.startsWith(prefix)) {
195236
return;
@@ -203,19 +244,44 @@ private static void collectSourceFile(String line, String prefix, List<String> f
203244
if (file.startsWith("file:")) {
204245
file = file.substring(5);
205246
}
247+
int query = file.indexOf('?');
248+
if (query > 0) {
249+
file = file.substring(0, query);
250+
}
206251
if (!file.isBlank() && !files.contains(file)) {
207252
files.add(file);
208253
}
209254
}
210255
}
211256

257+
private static String extensionOf(String file) {
258+
if (file == null) {
259+
return null;
260+
}
261+
String path = file;
262+
if (path.startsWith("classpath:")) {
263+
path = path.substring("classpath:".length());
264+
} else if (path.startsWith("file:")) {
265+
path = path.substring(5);
266+
}
267+
int query = path.indexOf('?');
268+
if (query > 0) {
269+
path = path.substring(0, query);
270+
}
271+
return FileUtil.onlyExt(path, true);
272+
}
273+
212274
private static String readContent(String file) {
213275
if (file == null || file.isBlank()) {
214276
return null;
215277
}
216278
String path = file;
217279
if (path.startsWith("classpath:")) {
218280
String resource = path.substring("classpath:".length());
281+
int query = resource.indexOf('?');
282+
if (query > 0) {
283+
resource = resource.substring(0, query);
284+
}
219285
try (var is = GenAiDependencyDiscovery.class.getClassLoader().getResourceAsStream(resource)) {
220286
if (is == null) {
221287
return null;
@@ -228,12 +294,16 @@ private static String readContent(String file) {
228294
if (path.startsWith("file:")) {
229295
path = path.substring(5);
230296
}
297+
int query = path.indexOf('?');
298+
if (query > 0) {
299+
path = path.substring(0, query);
300+
}
231301
Path source = Paths.get(path);
232302
if (!Files.exists(source) || Files.isDirectory(source)) {
233303
return null;
234304
}
235305
String ext = FileUtil.onlyExt(path, true);
236-
if (ext == null || !Set.of("java", "xml", "yaml", "yml", "properties").contains(ext)) {
306+
if (ext == null || !Set.of("java", "xml", "yaml", "yml", "properties", "groovy").contains(ext)) {
237307
return null;
238308
}
239309
try {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,8 @@ public void shouldExportGenAiRouteWithObservability() throws Exception {
822822
"--dir=" + workingDir,
823823
"--quiet",
824824
"--runtime=main",
825-
"src/test/resources/genai-route.yaml");
825+
"--observe=true",
826+
"src/test/resources/genai/langchain4j-route.yaml");
826827
int exit = command.doCall();
827828

828829
Assertions.assertEquals(0, exit);

0 commit comments

Comments
 (0)