davsclaus commented on code in PR #27839:
URL: https://github.com/apache/camel/pull/27839#discussion_r4275139428


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java:
##########
@@ -1434,9 +1651,396 @@ private static JsonObject componentDoc(
             result.put("docPages", new JsonArray(docPages));
             result.put("docPagesHint", pagesHint(docPages, "page"));
         }
+        if (!how.sections().isEmpty()) {
+            // what the excerpt leaves out is often what decides correctness 
(the mail useHeader* options, the xslt
+            // header parameters), so the page says what else it has, the 
sections with the filter term first
+            JsonArray titles = sectionTitles(how.sections(), filter);
+            result.put("sections", titles);
+            int matching = filter != null ? sectionsContaining(how.sections(), 
filter) : 0;
+            result.put("sectionsHint", "docPage=<section title> returns that 
section of the page as text"
+                                       + (matching > 0
+                                               ? "; the first " + matching + " 
contain '" + filter + "'"
+                                               : ""));
+        }
+        String mainGroup = MAIN_CONFIG_GROUPS.get(model.getScheme());
+        if (mainGroup != null) {
+            putMainConfig(result, how, mainGroup);
+        }
         return result;
     }
 
+    /**
+     * The options of a component: those the scope accepts and the filter 
matches, the required ones first, then the
+     * endpoint options (what a URI takes) and the component options, with 
their application.properties key. Without a
+     * filter only the first {@link #DEFAULT_OPTIONS_LIMIT} are listed in full 
and the rest by name. A filter that
+     * matches nothing gets a note and the closest option names, and the 
prefix options are always said, as they take
+     * keys no option list names (CAMEL-25541).
+     */
+    private static void putComponentOptions(
+            JsonObject result, ComponentModel model, ComponentAnswer how, 
OptionScope scope) {
+        String filter = how.filter();
+        List<JsonObject> required = new ArrayList<>();
+        List<JsonObject> byName = new ArrayList<>();
+        List<JsonObject> others = new ArrayList<>();
+        int omitted = 0;
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                if (!matchesOptionFilter(opt, filter)) {
+                    continue;
+                }
+                if (!scope.accepts(opt, filter)) {
+                    omitted++;
+                    continue;
+                }
+                JsonObject o = optionToJson(opt, optionScope);
+                if ("component".equals(optionScope)) {
+                    // the only valid application.properties key of an option: 
camel.component.<scheme>.* sets a
+                    // component option, never an endpoint one
+                    o.put("configKey", "camel.component." + model.getScheme() 
+ "." + opt.getName());
+                }
+                if (opt.isRequired()) {
+                    required.add(o);
+                } else if (filter != null && 
opt.getName().toLowerCase(Locale.ROOT).contains(filter)) {
+                    // a name match before a match in a description
+                    byName.add(o);
+                } else {
+                    others.add(o);
+                }
+            }
+        }
+        List<JsonObject> ordered = new ArrayList<>(required);
+        ordered.addAll(byName);
+        ordered.addAll(others);
+        int max = how.limit() >= 0 ? how.limit() : filter == null ? 
DEFAULT_OPTIONS_LIMIT : Integer.MAX_VALUE;
+        // the required options are always listed
+        int listed = Math.min(ordered.size(), Math.max(max, required.size()));
+        putOptions(result, new JsonArray(ordered.subList(0, listed)), omitted, 
scope);
+        result.put("matchedOptions", ordered.size());
+        if (listed < ordered.size()) {
+            Set<String> shown = new HashSet<>();
+            ordered.subList(0, listed).forEach(o -> 
shown.add(o.getString("name")));
+            Set<String> rest = new LinkedHashSet<>();
+            for (JsonObject o : ordered.subList(listed, ordered.size())) {
+                if (!shown.contains(o.getString("name"))) {
+                    rest.add(o.getString("name"));
+                }

Review Comment:
   Endpoint options come first, so with the limit of 20 the component option 
with the same name is past the limit. This check then removes its name from 
`moreOptions` too. For kafka, the default answer then has no `configKey` at 
all: 112 of its 116 common component options share a name with an endpoint 
option. Could an endpoint option that has a component option of the same name 
carry the `configKey` (or be listed once with both scopes)? Then the key is 
there without `optionsFilter`, and `matchedOptions` stops counting those 
options twice.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -103,11 +103,9 @@ static void register(Consumer<ToolDescriptor> registry) {
                         "common (default: no deprecated or advanced), 
required, all or false", false)
                 .param("includeHeaders", "boolean", "Include the message 
headers of a component (default false)", false)
                 .param("includeDoc", "boolean", "Include the full AsciiDoc 
page (default false)", false)
-                .param("docPage", "string", "simple doc sub-page to return as 
text (functions, operators, ognl, advanced)",
-                        false)
-                .param("optionsFilter", "string",
-                        "Keyword to match in option names or descriptions; for 
an EIP whose own options do not match, the options of its elements are searched 
(nestedOptions)",
-                        false)
+                .param("docPage", "string", CatalogDocs.DOC_PAGE_DESC, false)
+                .param("optionsFilter", "string", 
CatalogDocs.OPTIONS_FILTER_DESC, false)
+                .param("limit", "integer", CatalogDocs.OPTIONS_LIMIT_DESC, 
false)

Review Comment:
   The TUI AI panel sends this schema too, because `camel_catalog_doc` is a 
core tool. The new `limit` argument and the longer `docPage` and 
`optionsFilter` descriptions, together with the full-key sentence on line 94, 
add about 400 characters (~100 tokens). That pushes all four 
`AiPanelPromptBudgetTest` budgets over their limits in 
`camel-jbang-plugin-tui`, which is why the Java 17 build fails. Please shorten 
these descriptions, or raise the budgets in that test with a comment that says 
why.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java:
##########
@@ -1434,9 +1651,396 @@ private static JsonObject componentDoc(
             result.put("docPages", new JsonArray(docPages));
             result.put("docPagesHint", pagesHint(docPages, "page"));
         }
+        if (!how.sections().isEmpty()) {
+            // what the excerpt leaves out is often what decides correctness 
(the mail useHeader* options, the xslt
+            // header parameters), so the page says what else it has, the 
sections with the filter term first
+            JsonArray titles = sectionTitles(how.sections(), filter);
+            result.put("sections", titles);
+            int matching = filter != null ? sectionsContaining(how.sections(), 
filter) : 0;
+            result.put("sectionsHint", "docPage=<section title> returns that 
section of the page as text"
+                                       + (matching > 0
+                                               ? "; the first " + matching + " 
contain '" + filter + "'"
+                                               : ""));
+        }
+        String mainGroup = MAIN_CONFIG_GROUPS.get(model.getScheme());
+        if (mainGroup != null) {
+            putMainConfig(result, how, mainGroup);
+        }
         return result;
     }
 
+    /**
+     * The options of a component: those the scope accepts and the filter 
matches, the required ones first, then the
+     * endpoint options (what a URI takes) and the component options, with 
their application.properties key. Without a
+     * filter only the first {@link #DEFAULT_OPTIONS_LIMIT} are listed in full 
and the rest by name. A filter that
+     * matches nothing gets a note and the closest option names, and the 
prefix options are always said, as they take
+     * keys no option list names (CAMEL-25541).
+     */
+    private static void putComponentOptions(
+            JsonObject result, ComponentModel model, ComponentAnswer how, 
OptionScope scope) {
+        String filter = how.filter();
+        List<JsonObject> required = new ArrayList<>();
+        List<JsonObject> byName = new ArrayList<>();
+        List<JsonObject> others = new ArrayList<>();
+        int omitted = 0;
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                if (!matchesOptionFilter(opt, filter)) {
+                    continue;
+                }
+                if (!scope.accepts(opt, filter)) {
+                    omitted++;
+                    continue;
+                }
+                JsonObject o = optionToJson(opt, optionScope);
+                if ("component".equals(optionScope)) {
+                    // the only valid application.properties key of an option: 
camel.component.<scheme>.* sets a
+                    // component option, never an endpoint one
+                    o.put("configKey", "camel.component." + model.getScheme() 
+ "." + opt.getName());
+                }
+                if (opt.isRequired()) {
+                    required.add(o);
+                } else if (filter != null && 
opt.getName().toLowerCase(Locale.ROOT).contains(filter)) {
+                    // a name match before a match in a description
+                    byName.add(o);
+                } else {
+                    others.add(o);
+                }
+            }
+        }
+        List<JsonObject> ordered = new ArrayList<>(required);
+        ordered.addAll(byName);
+        ordered.addAll(others);
+        int max = how.limit() >= 0 ? how.limit() : filter == null ? 
DEFAULT_OPTIONS_LIMIT : Integer.MAX_VALUE;
+        // the required options are always listed
+        int listed = Math.min(ordered.size(), Math.max(max, required.size()));
+        putOptions(result, new JsonArray(ordered.subList(0, listed)), omitted, 
scope);
+        result.put("matchedOptions", ordered.size());
+        if (listed < ordered.size()) {
+            Set<String> shown = new HashSet<>();
+            ordered.subList(0, listed).forEach(o -> 
shown.add(o.getString("name")));
+            Set<String> rest = new LinkedHashSet<>();
+            for (JsonObject o : ordered.subList(listed, ordered.size())) {
+                if (!shown.contains(o.getString("name"))) {
+                    rest.add(o.getString("name"));
+                }
+            }
+            result.put("truncated", true);
+            result.put("listedOptions", listed);
+            result.put("moreOptions", new JsonArray(new ArrayList<>(rest)));
+            String hint = listed + " of " + ordered.size() + " options listed 
in full (required first, then the"
+                          + " endpoint options), the others by name in 
moreOptions; optionsFilter=<word or option name>"
+                          + " returns the matching ones in full, 
includeOptions=all with limit=<n> lists more";
+            String omittedHint = result.getString("optionsHint");
+            result.put("optionsHint", omittedHint != null ? hint + "; " + 
omittedHint : hint);
+        }
+        if (filter != null && ordered.isEmpty() && omitted == 0) {
+            // an empty list read as "not supported" (smtp + tls, xslt + 
param): say that nothing matched, and where
+            // else to look
+            result.put("note", "no option matches '" + filter + "': see 
closestOptions, prefixOptions and the"
+                               + " sections of the page below 
(docPage=<section title> returns one)");
+            JsonArray closest = closestOptions(model, filter);
+            if (!closest.isEmpty()) {
+                result.put("closestOptions", closest);
+            }
+        }
+        JsonArray prefixOptions = prefixOptions(model);
+        if (!prefixOptions.isEmpty()) {
+            result.put("prefixOptions", prefixOptions);
+        }
+    }
+
+    /**
+     * The options whose name, or a word of it, is closest to the filter by 
edit distance, with their group: ssl for a
+     * mistyped tsl.
+     */
+    private static JsonArray closestOptions(ComponentModel model, String 
filter) {
+        Map<String, List<BaseOptionModel>> byWord = new LinkedHashMap<>();
+        Map<BaseOptionModel, String> scopes = new LinkedHashMap<>();
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                scopes.putIfAbsent(opt, optionScope);
+                byWord.computeIfAbsent(opt.getName().toLowerCase(Locale.ROOT), 
k -> new ArrayList<>()).add(opt);
+                for (String word : 
opt.getName().split("(?<=[a-z0-9])(?=[A-Z])")) {
+                    if (word.length() > 1) {
+                        byWord.computeIfAbsent(word.toLowerCase(Locale.ROOT), 
k -> new ArrayList<>()).add(opt);
+                    }
+                }
+            }
+        }
+        JsonArray answer = new JsonArray();
+        Set<String> seen = new HashSet<>();
+        for (String word : 
EditDistanceSuggestionStrategy.suggest(byWord.keySet(), filter, 5)) {
+            for (BaseOptionModel opt : byWord.get(word)) {
+                if (answer.size() < 5 && seen.add(opt.getName())) {
+                    JsonObject o = new JsonObject();
+                    o.put("name", opt.getName());
+                    if (opt.getGroup() != null) {
+                        o.put("group", opt.getGroup());
+                    }
+                    o.put("scope", scopes.get(opt));
+                    answer.add(o);
+                }
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * The multi-value options of a component, which take every URI parameter 
with their prefix: any mail.* parameter of
+     * smtp goes to JavaMail (mail.smtp.starttls.enable=true), which no option 
is named after.
+     */
+    private static JsonArray prefixOptions(ComponentModel model) {
+        JsonArray answer = new JsonArray();
+        Set<String> seen = new HashSet<>();
+        if (model.getEndpointOptions() == null) {
+            return answer;
+        }
+        for (BaseOptionModel opt : model.getEndpointOptions()) {
+            if (opt.isMultiValue() && opt.getPrefix() != null && 
seen.add(opt.getName())) {
+                JsonObject o = new JsonObject();
+                o.put("name", opt.getName());
+                o.put("prefix", opt.getPrefix());
+                o.put("usage", "any " + opt.getPrefix() + "* URI parameter is 
passed on as " + opt.getName() + ": ?"
+                               + opt.getPrefix() + "<key>=<value>");
+                answer.add(o);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Where a component that runs on a server of camel run or Camel Main gets 
its settings: the HTTP port of
+     * platform-http is camel.server.port, which an agent searched the 
component's options for (CAMEL-25541).
+     */
+    private static void putMainConfig(JsonObject result, ComponentAnswer how, 
String group) {
+        result.put("mainConfig", "with camel run or Camel Main the HTTP server 
is configured by the " + group
+                                 + ".* keys of application.properties, not by 
an option of this component:"
+                                 + " camel.server.enabled=true and 
camel.server.port=8080 (camel run --port=8080 sets"
+                                 + " both); camel_catalog_doc name=" + group + 
" lists the keys");

Review Comment:
   Under `camel run` the server does not need `camel.server.enabled=true`. 
`DependencyDownloaderComponentResolver` starts it as soon as platform-http is 
used, and since CAMEL-25533 `MainHttpServerFactory.configureHttpServer` applies 
`camel.server.*` to it and ignores `enabled`. Only plain Camel Main needs 
`enabled=true`. Something like: "with camel run the HTTP server starts when a 
route uses platform-http (port 8080; camel.server.port or --port changes it); 
with Camel Main set camel.server.enabled=true".



##########
components/camel-platform-http/src/main/docs/platform-http-component.adoc:
##########
@@ -40,6 +40,11 @@ include::partial$component-endpoint-headers.adoc[]
 To use Platform HTTP, a provider (engine) is required to be available on the 
classpath.
 The purpose is to have drivers for different runtimes such as Quarkus, or 
Spring Boot.
 
+With Camel Main and `camel run`, the provider is the embedded HTTP server of 
`camel-platform-http-main`,
+which is disabled by default. Enable it and set its port in 
`application.properties` with
+`camel.server.enabled=true` and `camel.server.port=8080` (`camel run 
--port=8080` sets both).
+See xref:others:platform-http-main.adoc[Platform HTTP Main] for the other 
`camel.server.*` options.

Review Comment:
   This is accurate for Camel Main, but not for `camel run`. There the embedded 
server starts by itself when a route uses platform-http, and the 
`camel.server.*` keys (port, host, ...) are applied to it (CAMEL-25533, 
#27667). Suggest: "With `camel run`, the embedded HTTP server of 
`camel-platform-http-main` starts when a route uses platform-http, on port 8080 
(`camel.server.port` or `camel run --port` changes it). With Camel Main it is 
disabled by default: enable it with `camel.server.enabled=true` and set 
`camel.server.port`." Remember to regenerate the catalog mirror.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to