1717package org .apache .camel .dsl .jbang .core .common ;
1818
1919import java .io .IOException ;
20+ import java .io .InputStream ;
2021import java .nio .charset .StandardCharsets ;
2122import java .nio .file .Files ;
2223import java .nio .file .Path ;
2324import java .nio .file .Paths ;
2425import java .util .ArrayList ;
2526import java .util .Collection ;
27+ import java .util .LinkedHashMap ;
2628import java .util .LinkedHashSet ;
2729import java .util .List ;
2830import java .util .Map ;
3739import org .apache .camel .util .FileUtil ;
3840import 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 {
0 commit comments