You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "davsclaus (via GitHub)" <gi...@apache.org> on 2026/10/03 22:27:49 UTC
[PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
davsclaus opened a new pull request, #27318:
URL: https://github.com/apache/camel/pull/27318
[CAMEL-24834](https://issues.apache.org/jira/browse/CAMEL-24834). The plan is in the Jira comment of 2026-10-03.
**Draft, for review of the design.** Two commits: the first version had read-only SQL for local models; the second removes it ("a developer tool, not for production databases").
A local model gets the small core tool set to keep its prompt short. This adds tool **groups** chosen from what the selected integration has, never by the model. In September the model never asked for a tool on its own.
### What changes
| Group | Loads when the status shows | MCP tools | AI panel |
|---|---|---|---|
| sql | a datasource, sql/sql-stored/jdbc/spring-jdbc/jpa endpoints, or traced SQL | `camel_runtime_sql`, `camel_runtime_datasources`, `camel_runtime_sql_trace` | `tui_execute_sql`, `tui_update_row` |
| tracing | OpenTelemetry on, or message tracing on | `camel_runtime_trace` (+ `camel_runtime_spans` with OpenTelemetry, `camel_runtime_metrics` with Micrometer) | guidance only (`tui_get_spans` is core) |
| resilience | a `circuitBreaker` in the routes, or breaker state reported | `camel_runtime_circuit_breakers` | guidance only (`tui_get_table` tab) |
- **Guidance:** each group adds one line, e.g. "SQL: datasource(s) orders (HikariCP), used by sql endpoints. Table names come from the SQL trace; don't guess a schema."
- **camel-jbang-core** (`AppFeatures.fromStatus`, `ToolGroup`, `ToolGroups.select`):
- The rules are read from `~/.camel/<pid>-status.json`, one local file read.
- A pure function, tolerant of older status files.
- The fingerprint changes only when the groups or datasources change.
- **MCP:** new `camel_runtime_tool_groups(nameOrPid)` returns the core tools, the groups with their tools and guidance, the fingerprint and the signals. Any client can then apply the same selection. `tools/list_changed` was considered and left out: the server has no selected app, and many clients ignore the notification.
- **AI panel, core mode only** (full mode unchanged):
- The groups are recomputed only when another integration is selected or the selected one reloads, and they only grow for the same integration. The tools and prompt stay byte-identical between questions, so the prompt cache survives.
- The guidance lines go at the end of the system prompt.
- `/tools` shows the loaded groups.
- **Prompt budget:** core about 4,663 tokens, core + all three groups about 5,180. A new `CORE_WITH_GROUPS_BUDGET_TOKENS = 5_500` covers the second case.
- **Docs:** TUI local models, TUI AI, camel-jbang-mcp.
### Tests
- New: `AppFeaturesTest` (status fixtures), `ToolGroupsTest`, `AiPanelToolGroupsTest`.
- Extended: `ToolRegistryTest`, `RuntimeToolsTest`, `AiPanelPromptBudgetTest` (core + groups budget; the guidance only names tools in the active set).
- Module suites: camel-jbang-core 1460, camel-jbang-mcp 427, camel-jbang-plugin-tui 1724, all pass.
- Live through the MCP server: an integration with a circuit breaker returns the resilience group, with signals `routes.processors: circuitBreaker in stock-check` and `resilience4j: 1 circuit breaker(s)`.
### Local-model benchmark
qwen3.6 35b-a3b, 64k context, 5 runs per example. The harness asks `camel_runtime_tool_groups` before each step and adds the group's tools and guidance when the fingerprint changes.
| Example | Without groups | With groups |
|---|---|---|
| connect-service-sql | 9/15 (steps 2/2/5) | 9/15 (steps 0/4/5) |
| fail-well-circuit-breaker | 13/15 | 13/15, peak prompt 43.9k vs 65.5k |
- **sql:** the model now uses the tools (sql 11 calls, sql_trace 6, datasources 2), mostly to read the table before reporting on it (step 2 up from 2/5 to 4/5).
- **sql step 1** still fails on the model's own route SQL (named parameters, upsert syntax), not on tool access.
- **circuit-breaker:** the breaker tool was called 4 times once its group loaded.
Phase 2, not here: groups from the project sources before the integration runs, a `/sql` command, aggregator state.
Note: `main` is currently red after CAMEL-24649 (#26203). That change left `eip-samples.json` stale and broke `CatalogSamplesTest.restOpenApiShowsTheConsumerEndpointAndTheContractFirstRest`, so CI here will fail on those too. See #27317.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27318:
URL: https://github.com/apache/camel/pull/27318#discussion_r4175181955
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -301,12 +303,31 @@ interface AcpClientFactory {
AcpAgentClient create(AiProviderSelector.AcpPreset preset, Path cwd) throws IOException;
}
+ /** The selected integration as the tool groups see it; replaced in tests. */
+ interface AppStatusSource {
+ /** The selected pid, null when none. */
+ String selectedPid();
+
+ /** How often it reloaded its routes. */
+ int reloadCount();
+
+ /** What it has, read from its status. */
+ AppFeatures features();
+ }
+
// MCP facade for TUI tool access from the AI panel
private McpFacade mcpFacade;
// /write: confirm (dialog per write), auto (the model may skip it with confirm=false) or live (the edit is replayed
// in the source editor and the user saves or discards it)
private McpFacade.WriteMode writeMode = McpFacade.WriteMode.CONFIRM;
private TuiToolRegistry toolRegistry;
+ // CAMEL-24834: the tool groups (SQL, tracing, resilience) of the selected integration that the core set gets, see
+ // refreshToolGroups()
Review Comment:
💡 **Thread-safety note:** `toolGroupsPid`, `toolGroupsReloads`, and `toolGroupsFeatures` are non-volatile while `toolGroups` is volatile. This is safe as long as `refreshToolGroups()` is only called from the TUI event thread inside `submitQuestion()` — which it is today. But unlike the other panel fields (which are all volatile), the mix is surprising and fragile: if someone later calls `refreshToolGroups()` from the agent thread, the non-volatile fields would be stale.
Worth a comment noting the single-thread assumption, or making them all volatile for consistency with the rest of AiPanel's state fields.
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java:
##########
@@ -344,6 +344,27 @@ String getSelectedIntegrationName() {
return info != null ? info.name : null;
}
+ /** How often the selected integration has reloaded its routes, 0 when nothing is selected. */
+ int getSelectedReloadCount() {
+ if (ctx == null) {
+ return 0;
+ }
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.reloaded : 0;
+ }
+
+ /** The status document of the selected integration, null when nothing is selected or it has none yet. */
+ JsonObject readSelectedStatus() {
+ if (ctx == null || ctx.selectedPid == null) {
Review Comment:
💡 **Minor:** `readSelectedStatus()` catches `NumberFormatException` from `Long.parseLong(ctx.selectedPid)` but not the checked exceptions that `RuntimeHelper.readStatus()` could propagate (file I/O). Looking at the implementation, `readStatusFromFile` already swallows all exceptions and returns null, so this is safe today — but the contract of `readStatus(long)` doesn't guarantee that. A broader catch or a null check on the return value would be more defensive.
Given that `AppFeatures.fromStatus(null)` returns `none()`, the worst case is just a missing group load, which is fine.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What a running integration has that a tool group is for (CAMEL-24834): its datasources and SQL endpoints, circuit
+ * breakers, OpenTelemetry, message tracing and Micrometer. Read from the status file the integration writes
+ * ({@code ~/.camel/<pid>-status.json}); a key that is missing (an older Camel, a console that is not on the classpath)
+ * just means fewer features, never an error.
+ *
+ * @param dataSources the datasources in the registry, by name
+ * @param sqlComponents the SQL components the endpoints and routes use (sql, sql-stored, jdbc, spring-jdbc, jpa)
+ * @param sqlTraced whether SQL statements have been traced
+ * @param circuitBreaker whether the routes have a circuit breaker
+ * @param circuitBreakerRoutes the routes with a circuit breaker, when known
+ * @param openTelemetry whether OpenTelemetry tracing is on
+ * @param messageTracing whether message tracing is enabled
+ * @param micrometer whether Micrometer metrics are on
+ * @param signals the status keys that gave each feature away, with what they said
+ */
+public record AppFeatures(
+ List<DataSource> dataSources,
+ List<String> sqlComponents,
+ boolean sqlTraced,
+ boolean circuitBreaker,
+ List<String> circuitBreakerRoutes,
+ boolean openTelemetry,
+ boolean messageTracing,
+ boolean micrometer,
+ Map<String, String> signals) {
+
+ /** The components whose endpoints talk SQL to a datasource. */
+ static final Set<String> SQL_COMPONENTS = Set.of("sql", "sql-stored", "jdbc", "spring-jdbc", "jpa");
+
+ private static final String CIRCUIT_BREAKER_PROCESSOR = "circuitBreaker";
+ private static final List<String> CIRCUIT_BREAKER_SECTIONS
+ = List.of("resilience4j", "fault-tolerance", "circuit-breaker");
+
+ /**
+ * A datasource of the registry.
+ *
+ * @param name the bean name
+ * @param poolType HikariCP, Agroal or Unknown, null when not known
+ */
+ public record DataSource(String name, String poolType) {
+ }
+
+ public AppFeatures {
+ dataSources = List.copyOf(dataSources);
+ sqlComponents = List.copyOf(sqlComponents);
+ circuitBreakerRoutes = List.copyOf(circuitBreakerRoutes);
+ signals = Collections.unmodifiableMap(new LinkedHashMap<>(signals));
+ }
+
+ /** No integration, or nothing to tell: no tool group loads. */
+ public static AppFeatures none() {
+ return new AppFeatures(List.of(), List.of(), false, false, List.of(), false, false, false, Map.of());
+ }
+
+ /** The names of the datasources. */
+ public List<String> dataSourceNames() {
+ return dataSources.stream().map(DataSource::name).toList();
+ }
+
+ /** Whether the integration works with a database: a datasource, a SQL endpoint or a traced statement. */
+ public boolean sql() {
+ return !dataSources.isEmpty() || !sqlComponents.isEmpty() || sqlTraced;
+ }
+
+ /** Whether there is anything to trace or measure: OpenTelemetry, message tracing or Micrometer. */
+ public boolean tracing() {
+ return openTelemetry || messageTracing || micrometer;
+ }
+
+ /**
+ * Both feature sets together: what an integration had before a reload still counts after it, so the tools a model
+ * was given do not come and go while it works.
+ */
+ public AppFeatures merge(AppFeatures other) {
+ if (other == null) {
+ return this;
+ }
+ Map<String, DataSource> ds = new TreeMap<>();
+ for (DataSource d : dataSources) {
+ ds.put(d.name(), d);
+ }
+ for (DataSource d : other.dataSources) {
+ ds.putIfAbsent(d.name(), d);
+ }
+ Set<String> components = new TreeSet<>(sqlComponents);
+ components.addAll(other.sqlComponents);
+ Set<String> routes = new TreeSet<>(circuitBreakerRoutes);
+ routes.addAll(other.circuitBreakerRoutes);
+ Map<String, String> sig = new LinkedHashMap<>(signals);
+ other.signals.forEach(sig::putIfAbsent);
+ return new AppFeatures(
+ new ArrayList<>(ds.values()), new ArrayList<>(components), sqlTraced || other.sqlTraced,
+ circuitBreaker || other.circuitBreaker, new ArrayList<>(routes),
+ openTelemetry || other.openTelemetry, messageTracing || other.messageTracing,
+ micrometer || other.micrometer, sig);
+ }
+
+ /**
+ * Reads the features from an integration's status document. Pure and defensive: a null document, a missing key or a
+ * value of an unexpected type yields fewer features.
+ */
+ public static AppFeatures fromStatus(JsonObject status) {
+ if (status == null) {
+ return none();
+ }
+ Map<String, String> signals = new LinkedHashMap<>();
+
+ // datasources: dataSources.dataSources[] with name and poolType
+ Map<String, DataSource> dataSources = new TreeMap<>();
+ for (Map<?, ?> entry : objects(section(status, "dataSources").get("dataSources"))) {
+ String name = text(entry.get("name"));
+ if (name != null) {
+ dataSources.put(name, new DataSource(name, text(entry.get("poolType"))));
+ }
+ }
+ if (!dataSources.isEmpty()) {
+ signals.put("dataSources", String.join(",", dataSources.keySet()));
+ }
+
+ // SQL endpoints: the endpoint registry, the route inputs and the processors that send somewhere
+ Set<String> components = new TreeSet<>();
+ for (Map<?, ?> ep : objects(section(status, "endpoints").get("endpoints"))) {
+ addSqlComponent(components, text(ep.get("uri")));
+ }
+ List<Map<?, ?>> routes = objects(status.get("routes"));
+ Set<String> breakerRoutes = new TreeSet<>();
+ boolean breakerProcessor = false;
+ for (Map<?, ?> route : routes) {
+ addSqlComponent(components, text(route.get("from")));
+ for (Map<?, ?> p : objects(route.get("processors"))) {
+ addSqlComponent(components, text(p.get("uri")));
+ if (CIRCUIT_BREAKER_PROCESSOR.equals(text(p.get("processor")))) {
+ breakerProcessor = true;
+ String routeId = text(p.get("routeId"));
+ if (routeId == null) {
+ routeId = text(route.get("routeId"));
+ }
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+ List<Map<?, ?>> statements = objects(section(status, "sqlTrace").get("statements"));
+ for (Map<?, ?> st : statements) {
+ addSqlComponent(components, text(st.get("endpoint")));
+ }
+ if (!components.isEmpty()) {
+ signals.put("endpoints", String.join(",", components));
+ }
+ boolean sqlTraced = false;
+ for (Map<?, ?> st : statements) {
+ if (text(st.get("query")) != null) {
+ sqlTraced = true;
+ break;
+ }
+ }
+ if (sqlTraced) {
+ signals.put("sqlTrace", statements.size() + " statement(s)");
+ }
+
+ // circuit breakers: the circuitBreaker EIP in a route, or the breakers a console reports
+ if (breakerProcessor) {
+ String where = breakerRoutes.isEmpty() ? "" : " in " + String.join(",", breakerRoutes);
+ signals.put("routes.processors", CIRCUIT_BREAKER_PROCESSOR + where);
+ }
+ boolean breakers = false;
+ for (String key : CIRCUIT_BREAKER_SECTIONS) {
+ List<Map<?, ?>> list = objects(section(status, key).get("circuitBreakers"));
+ if (!list.isEmpty()) {
+ breakers = true;
Review Comment:
💡 **Observation (tracing group trigger):** `micrometer` alone triggers the TRACING group, but the MCP side's tracing guidance always includes a line about `camel_runtime_trace` ("enables message tracing") even when the user only has Micrometer and has no interest in message tracing. This is intentional per the design (offering the tool is cheap, and the model might find tracing useful), but the test coverage in `ToolGroupsTest` only tests `messageTracing` isolation — there's no test for a Micrometer-only status producing the tracing group.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Picks the runtime tool groups for an integration from its {@link AppFeatures} (CAMEL-24834). A small local model pays
+ * for every tool schema on every request, so the SQL, tracing and resilience tools only load when the integration has a
+ * database, tracing or circuit breakers; each loaded group adds one line of guidance that names what the integration
+ * has and which tool reads it. The tool names and the wording here are those of the MCP server; the TUI maps the same
+ * groups to its own tools.
+ */
+public final class ToolGroups {
+
+ public static final String SQL_TOOL = "camel_runtime_sql";
+ public static final String DATASOURCES_TOOL = "camel_runtime_datasources";
+ public static final String SQL_TRACE_TOOL = "camel_runtime_sql_trace";
+ public static final String SPANS_TOOL = "camel_runtime_spans";
+ public static final String TRACE_TOOL = "camel_runtime_trace";
+ public static final String METRICS_TOOL = "camel_runtime_metrics";
+ public static final String CIRCUIT_BREAKERS_TOOL = "camel_runtime_circuit_breakers";
+
+ /**
+ * A loaded group.
+ *
+ * @param group the group
+ * @param tools the tools it adds, in the order to offer them
+ * @param guidance one line for the model: what the integration has and which tool reads it
+ */
+ public record Group(ToolGroup group, List<String> tools, String guidance) {
+
+ public Group {
+ tools = List.copyOf(tools);
+ }
+ }
+
+ /**
+ * The groups for an integration.
+ *
+ * @param groups the loaded groups, in {@link ToolGroup} order
+ * @param fingerprint stable for the same groups and datasources: a client rebuilds its tool list only when it
+ * changes
+ */
+ public record Selection(List<Group> groups, String fingerprint) {
+
+ public Selection {
+ groups = List.copyOf(groups);
+ }
+
+ public List<ToolGroup> toolGroups() {
+ return groups.stream().map(Group::group).toList();
+ }
+
+ public boolean has(ToolGroup group) {
+ return groups.stream().anyMatch(g -> g.group() == group);
+ }
+
+ /** The tools of all loaded groups. */
+ public List<String> mcpTools() {
+ return groups.stream().flatMap(g -> g.tools().stream()).distinct().toList();
+ }
+
+ /** The guidance lines of all loaded groups. */
+ public List<String> guidance() {
+ return groups.stream().map(Group::guidance).toList();
+ }
+ }
+
+ private ToolGroups() {
+ }
+
+ /** The groups an integration needs. */
+ public static Selection select(AppFeatures features) {
+ AppFeatures f = features != null ? features : AppFeatures.none();
+ List<Group> groups = new ArrayList<>();
+ for (ToolGroup group : groups(f)) {
+ switch (group) {
+ case SQL -> groups.add(new Group(
+ group,
+ List.of(SQL_TOOL, DATASOURCES_TOOL, SQL_TRACE_TOOL),
Review Comment:
💡 **Fingerprint stability:** The fingerprint sorts group IDs and datasource names, which is correct for order-independence. But it uses `|` as delimiter between groups and datasources — if a datasource were named e.g. `sql,tracing` the fingerprint would be ambiguous. In practice datasource names are simple identifiers, so this is a theoretical concern.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What a running integration has that a tool group is for (CAMEL-24834): its datasources and SQL endpoints, circuit
+ * breakers, OpenTelemetry, message tracing and Micrometer. Read from the status file the integration writes
+ * ({@code ~/.camel/<pid>-status.json}); a key that is missing (an older Camel, a console that is not on the classpath)
+ * just means fewer features, never an error.
+ *
+ * @param dataSources the datasources in the registry, by name
+ * @param sqlComponents the SQL components the endpoints and routes use (sql, sql-stored, jdbc, spring-jdbc, jpa)
+ * @param sqlTraced whether SQL statements have been traced
+ * @param circuitBreaker whether the routes have a circuit breaker
+ * @param circuitBreakerRoutes the routes with a circuit breaker, when known
+ * @param openTelemetry whether OpenTelemetry tracing is on
+ * @param messageTracing whether message tracing is enabled
+ * @param micrometer whether Micrometer metrics are on
+ * @param signals the status keys that gave each feature away, with what they said
+ */
+public record AppFeatures(
+ List<DataSource> dataSources,
+ List<String> sqlComponents,
+ boolean sqlTraced,
+ boolean circuitBreaker,
+ List<String> circuitBreakerRoutes,
+ boolean openTelemetry,
+ boolean messageTracing,
+ boolean micrometer,
+ Map<String, String> signals) {
+
+ /** The components whose endpoints talk SQL to a datasource. */
+ static final Set<String> SQL_COMPONENTS = Set.of("sql", "sql-stored", "jdbc", "spring-jdbc", "jpa");
+
+ private static final String CIRCUIT_BREAKER_PROCESSOR = "circuitBreaker";
+ private static final List<String> CIRCUIT_BREAKER_SECTIONS
+ = List.of("resilience4j", "fault-tolerance", "circuit-breaker");
+
+ /**
+ * A datasource of the registry.
+ *
+ * @param name the bean name
+ * @param poolType HikariCP, Agroal or Unknown, null when not known
+ */
+ public record DataSource(String name, String poolType) {
+ }
+
+ public AppFeatures {
+ dataSources = List.copyOf(dataSources);
+ sqlComponents = List.copyOf(sqlComponents);
+ circuitBreakerRoutes = List.copyOf(circuitBreakerRoutes);
+ signals = Collections.unmodifiableMap(new LinkedHashMap<>(signals));
+ }
+
+ /** No integration, or nothing to tell: no tool group loads. */
+ public static AppFeatures none() {
+ return new AppFeatures(List.of(), List.of(), false, false, List.of(), false, false, false, Map.of());
+ }
+
+ /** The names of the datasources. */
+ public List<String> dataSourceNames() {
+ return dataSources.stream().map(DataSource::name).toList();
+ }
+
+ /** Whether the integration works with a database: a datasource, a SQL endpoint or a traced statement. */
+ public boolean sql() {
+ return !dataSources.isEmpty() || !sqlComponents.isEmpty() || sqlTraced;
+ }
+
+ /** Whether there is anything to trace or measure: OpenTelemetry, message tracing or Micrometer. */
+ public boolean tracing() {
+ return openTelemetry || messageTracing || micrometer;
+ }
+
+ /**
+ * Both feature sets together: what an integration had before a reload still counts after it, so the tools a model
+ * was given do not come and go while it works.
+ */
+ public AppFeatures merge(AppFeatures other) {
+ if (other == null) {
+ return this;
+ }
+ Map<String, DataSource> ds = new TreeMap<>();
+ for (DataSource d : dataSources) {
+ ds.put(d.name(), d);
+ }
+ for (DataSource d : other.dataSources) {
+ ds.putIfAbsent(d.name(), d);
+ }
+ Set<String> components = new TreeSet<>(sqlComponents);
+ components.addAll(other.sqlComponents);
+ Set<String> routes = new TreeSet<>(circuitBreakerRoutes);
+ routes.addAll(other.circuitBreakerRoutes);
+ Map<String, String> sig = new LinkedHashMap<>(signals);
+ other.signals.forEach(sig::putIfAbsent);
+ return new AppFeatures(
+ new ArrayList<>(ds.values()), new ArrayList<>(components), sqlTraced || other.sqlTraced,
+ circuitBreaker || other.circuitBreaker, new ArrayList<>(routes),
+ openTelemetry || other.openTelemetry, messageTracing || other.messageTracing,
+ micrometer || other.micrometer, sig);
+ }
+
+ /**
+ * Reads the features from an integration's status document. Pure and defensive: a null document, a missing key or a
+ * value of an unexpected type yields fewer features.
+ */
+ public static AppFeatures fromStatus(JsonObject status) {
+ if (status == null) {
+ return none();
+ }
+ Map<String, String> signals = new LinkedHashMap<>();
+
+ // datasources: dataSources.dataSources[] with name and poolType
+ Map<String, DataSource> dataSources = new TreeMap<>();
+ for (Map<?, ?> entry : objects(section(status, "dataSources").get("dataSources"))) {
+ String name = text(entry.get("name"));
+ if (name != null) {
+ dataSources.put(name, new DataSource(name, text(entry.get("poolType"))));
+ }
+ }
+ if (!dataSources.isEmpty()) {
+ signals.put("dataSources", String.join(",", dataSources.keySet()));
+ }
+
+ // SQL endpoints: the endpoint registry, the route inputs and the processors that send somewhere
+ Set<String> components = new TreeSet<>();
+ for (Map<?, ?> ep : objects(section(status, "endpoints").get("endpoints"))) {
+ addSqlComponent(components, text(ep.get("uri")));
+ }
+ List<Map<?, ?>> routes = objects(status.get("routes"));
+ Set<String> breakerRoutes = new TreeSet<>();
+ boolean breakerProcessor = false;
+ for (Map<?, ?> route : routes) {
+ addSqlComponent(components, text(route.get("from")));
+ for (Map<?, ?> p : objects(route.get("processors"))) {
+ addSqlComponent(components, text(p.get("uri")));
+ if (CIRCUIT_BREAKER_PROCESSOR.equals(text(p.get("processor")))) {
+ breakerProcessor = true;
+ String routeId = text(p.get("routeId"));
+ if (routeId == null) {
+ routeId = text(route.get("routeId"));
+ }
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+ List<Map<?, ?>> statements = objects(section(status, "sqlTrace").get("statements"));
+ for (Map<?, ?> st : statements) {
+ addSqlComponent(components, text(st.get("endpoint")));
+ }
+ if (!components.isEmpty()) {
+ signals.put("endpoints", String.join(",", components));
+ }
+ boolean sqlTraced = false;
+ for (Map<?, ?> st : statements) {
+ if (text(st.get("query")) != null) {
+ sqlTraced = true;
+ break;
+ }
+ }
+ if (sqlTraced) {
+ signals.put("sqlTrace", statements.size() + " statement(s)");
+ }
+
+ // circuit breakers: the circuitBreaker EIP in a route, or the breakers a console reports
+ if (breakerProcessor) {
+ String where = breakerRoutes.isEmpty() ? "" : " in " + String.join(",", breakerRoutes);
+ signals.put("routes.processors", CIRCUIT_BREAKER_PROCESSOR + where);
+ }
+ boolean breakers = false;
+ for (String key : CIRCUIT_BREAKER_SECTIONS) {
+ List<Map<?, ?>> list = objects(section(status, key).get("circuitBreakers"));
+ if (!list.isEmpty()) {
+ breakers = true;
+ signals.put(key, list.size() + " circuit breaker(s)");
+ for (Map<?, ?> b : list) {
+ String routeId = text(b.get("routeId"));
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+
+ // tracing and metrics
+ Set<String> consoles = new TreeSet<>();
+ for (Object id : list(status.get("devConsoles"))) {
+ if (id != null) {
+ consoles.add(id.toString());
+ }
+ }
+ boolean otel = consoles.contains("opentelemetry");
+ boolean micrometer = consoles.contains("micrometer") || status.get("micrometer") instanceof Map;
+ if (otel || consoles.contains("micrometer")) {
+ List<String> fired = new ArrayList<>();
+ if (otel) {
+ fired.add("opentelemetry");
+ }
+ if (consoles.contains("micrometer")) {
+ fired.add("micrometer");
+ }
+ signals.put("devConsoles", String.join(",", fired));
+ }
+ if (status.get("micrometer") instanceof Map) {
+ signals.put("micrometer", "present");
Review Comment:
💡 **Design question (re: merge semantics):** `merge()` uses `putIfAbsent` for datasources, which means if a datasource's `poolType` changes between reloads (e.g. from `null` to `HikariCP` after the pool initializes), the stale value wins. For *groups* this is fine (the group still loads), but the *guidance text* would say the wrong pool type.
Probably not a real-world issue since pool types don't change, but `put` instead of `putIfAbsent` would keep the freshest info. Same for `sig::putIfAbsent` on line 240 — the latest signal is more accurate than the earliest.
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27318:
URL: https://github.com/apache/camel/pull/27318#issuecomment-5974224911
<!-- regen-patch -->
### :warning: Generated files are out of date
The [CI build](https://github.com/apache/camel/actions/runs/37158528368) regenerated files that are not committed on this PR.
If *Allow edits from maintainers* is enabled, a maintainer bot will automatically
push a "Regen" commit to this branch shortly.
Otherwise, you can apply the patch manually — the missing changes are available
as the `regen-patch` artifact of that run:
```bash
gh pr checkout 27318 --repo apache/camel
gh run download 37158528368 --repo apache/camel -n regen-patch
git apply --index regen.patch && rm regen.patch pr-number
git commit -m "Regen" && git push
```
Alternatively, run `./etc/scripts/regen.sh` locally and commit the resulting changes.
If the patch does not apply cleanly, update the branch from the base branch first —
the patch was generated against the merge with the base branch.
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27318:
URL: https://github.com/apache/camel/pull/27318#issuecomment-5974142288
:star2: Thank you for your contribution to the Apache Camel project! :star2:
:robot: CI automation will test this PR automatically.
:camel: Apache Camel Committers, please review the following items:
* First-time contributors **require MANUAL approval** for the GitHub Actions to run
* You can use the command `/component-test (camel-)component-name1 (camel-)component-name2..` to request a test from the test bot although they are normally detected and executed by CI.
* You can label PRs using `skip-tests` and `test-dependents` to fine-tune the checks executed by this PR.
* Build and test logs are available in the summary page. **Only** [Apache Camel committers](https://camel.apache.org/community/team/#committers) have access to the summary.
:warning: Be careful when sharing logs. Review their contents before sharing them publicly.
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27318:
URL: https://github.com/apache/camel/pull/27318#discussion_r4176408490
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -301,12 +303,31 @@ interface AcpClientFactory {
AcpAgentClient create(AiProviderSelector.AcpPreset preset, Path cwd) throws IOException;
}
+ /** The selected integration as the tool groups see it; replaced in tests. */
+ interface AppStatusSource {
+ /** The selected pid, null when none. */
+ String selectedPid();
+
+ /** How often it reloaded its routes. */
+ int reloadCount();
+
+ /** What it has, read from its status. */
+ AppFeatures features();
+ }
+
// MCP facade for TUI tool access from the AI panel
private McpFacade mcpFacade;
// /write: confirm (dialog per write), auto (the model may skip it with confirm=false) or live (the edit is replayed
// in the source editor and the user saves or discards it)
private McpFacade.WriteMode writeMode = McpFacade.WriteMode.CONFIRM;
private TuiToolRegistry toolRegistry;
+ // CAMEL-24834: the tool groups (SQL, tracing, resilience) of the selected integration that the core set gets, see
+ // refreshToolGroups()
Review Comment:
Made the four tool-group fields volatile like the rest of the panel's state (ff7e9f8f86de). They are only written from the event thread in submitQuestion today, but the mix was surprising.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What a running integration has that a tool group is for (CAMEL-24834): its datasources and SQL endpoints, circuit
+ * breakers, OpenTelemetry, message tracing and Micrometer. Read from the status file the integration writes
+ * ({@code ~/.camel/<pid>-status.json}); a key that is missing (an older Camel, a console that is not on the classpath)
+ * just means fewer features, never an error.
+ *
+ * @param dataSources the datasources in the registry, by name
+ * @param sqlComponents the SQL components the endpoints and routes use (sql, sql-stored, jdbc, spring-jdbc, jpa)
+ * @param sqlTraced whether SQL statements have been traced
+ * @param circuitBreaker whether the routes have a circuit breaker
+ * @param circuitBreakerRoutes the routes with a circuit breaker, when known
+ * @param openTelemetry whether OpenTelemetry tracing is on
+ * @param messageTracing whether message tracing is enabled
+ * @param micrometer whether Micrometer metrics are on
+ * @param signals the status keys that gave each feature away, with what they said
+ */
+public record AppFeatures(
+ List<DataSource> dataSources,
+ List<String> sqlComponents,
+ boolean sqlTraced,
+ boolean circuitBreaker,
+ List<String> circuitBreakerRoutes,
+ boolean openTelemetry,
+ boolean messageTracing,
+ boolean micrometer,
+ Map<String, String> signals) {
+
+ /** The components whose endpoints talk SQL to a datasource. */
+ static final Set<String> SQL_COMPONENTS = Set.of("sql", "sql-stored", "jdbc", "spring-jdbc", "jpa");
+
+ private static final String CIRCUIT_BREAKER_PROCESSOR = "circuitBreaker";
+ private static final List<String> CIRCUIT_BREAKER_SECTIONS
+ = List.of("resilience4j", "fault-tolerance", "circuit-breaker");
+
+ /**
+ * A datasource of the registry.
+ *
+ * @param name the bean name
+ * @param poolType HikariCP, Agroal or Unknown, null when not known
+ */
+ public record DataSource(String name, String poolType) {
+ }
+
+ public AppFeatures {
+ dataSources = List.copyOf(dataSources);
+ sqlComponents = List.copyOf(sqlComponents);
+ circuitBreakerRoutes = List.copyOf(circuitBreakerRoutes);
+ signals = Collections.unmodifiableMap(new LinkedHashMap<>(signals));
+ }
+
+ /** No integration, or nothing to tell: no tool group loads. */
+ public static AppFeatures none() {
+ return new AppFeatures(List.of(), List.of(), false, false, List.of(), false, false, false, Map.of());
+ }
+
+ /** The names of the datasources. */
+ public List<String> dataSourceNames() {
+ return dataSources.stream().map(DataSource::name).toList();
+ }
+
+ /** Whether the integration works with a database: a datasource, a SQL endpoint or a traced statement. */
+ public boolean sql() {
+ return !dataSources.isEmpty() || !sqlComponents.isEmpty() || sqlTraced;
+ }
+
+ /** Whether there is anything to trace or measure: OpenTelemetry, message tracing or Micrometer. */
+ public boolean tracing() {
+ return openTelemetry || messageTracing || micrometer;
+ }
+
+ /**
+ * Both feature sets together: what an integration had before a reload still counts after it, so the tools a model
+ * was given do not come and go while it works.
+ */
+ public AppFeatures merge(AppFeatures other) {
+ if (other == null) {
+ return this;
+ }
+ Map<String, DataSource> ds = new TreeMap<>();
+ for (DataSource d : dataSources) {
+ ds.put(d.name(), d);
+ }
+ for (DataSource d : other.dataSources) {
+ ds.putIfAbsent(d.name(), d);
+ }
+ Set<String> components = new TreeSet<>(sqlComponents);
+ components.addAll(other.sqlComponents);
+ Set<String> routes = new TreeSet<>(circuitBreakerRoutes);
+ routes.addAll(other.circuitBreakerRoutes);
+ Map<String, String> sig = new LinkedHashMap<>(signals);
+ other.signals.forEach(sig::putIfAbsent);
+ return new AppFeatures(
+ new ArrayList<>(ds.values()), new ArrayList<>(components), sqlTraced || other.sqlTraced,
+ circuitBreaker || other.circuitBreaker, new ArrayList<>(routes),
+ openTelemetry || other.openTelemetry, messageTracing || other.messageTracing,
+ micrometer || other.micrometer, sig);
+ }
+
+ /**
+ * Reads the features from an integration's status document. Pure and defensive: a null document, a missing key or a
+ * value of an unexpected type yields fewer features.
+ */
+ public static AppFeatures fromStatus(JsonObject status) {
+ if (status == null) {
+ return none();
+ }
+ Map<String, String> signals = new LinkedHashMap<>();
+
+ // datasources: dataSources.dataSources[] with name and poolType
+ Map<String, DataSource> dataSources = new TreeMap<>();
+ for (Map<?, ?> entry : objects(section(status, "dataSources").get("dataSources"))) {
+ String name = text(entry.get("name"));
+ if (name != null) {
+ dataSources.put(name, new DataSource(name, text(entry.get("poolType"))));
+ }
+ }
+ if (!dataSources.isEmpty()) {
+ signals.put("dataSources", String.join(",", dataSources.keySet()));
+ }
+
+ // SQL endpoints: the endpoint registry, the route inputs and the processors that send somewhere
+ Set<String> components = new TreeSet<>();
+ for (Map<?, ?> ep : objects(section(status, "endpoints").get("endpoints"))) {
+ addSqlComponent(components, text(ep.get("uri")));
+ }
+ List<Map<?, ?>> routes = objects(status.get("routes"));
+ Set<String> breakerRoutes = new TreeSet<>();
+ boolean breakerProcessor = false;
+ for (Map<?, ?> route : routes) {
+ addSqlComponent(components, text(route.get("from")));
+ for (Map<?, ?> p : objects(route.get("processors"))) {
+ addSqlComponent(components, text(p.get("uri")));
+ if (CIRCUIT_BREAKER_PROCESSOR.equals(text(p.get("processor")))) {
+ breakerProcessor = true;
+ String routeId = text(p.get("routeId"));
+ if (routeId == null) {
+ routeId = text(route.get("routeId"));
+ }
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+ List<Map<?, ?>> statements = objects(section(status, "sqlTrace").get("statements"));
+ for (Map<?, ?> st : statements) {
+ addSqlComponent(components, text(st.get("endpoint")));
+ }
+ if (!components.isEmpty()) {
+ signals.put("endpoints", String.join(",", components));
+ }
+ boolean sqlTraced = false;
+ for (Map<?, ?> st : statements) {
+ if (text(st.get("query")) != null) {
+ sqlTraced = true;
+ break;
+ }
+ }
+ if (sqlTraced) {
+ signals.put("sqlTrace", statements.size() + " statement(s)");
+ }
+
+ // circuit breakers: the circuitBreaker EIP in a route, or the breakers a console reports
+ if (breakerProcessor) {
+ String where = breakerRoutes.isEmpty() ? "" : " in " + String.join(",", breakerRoutes);
+ signals.put("routes.processors", CIRCUIT_BREAKER_PROCESSOR + where);
+ }
+ boolean breakers = false;
+ for (String key : CIRCUIT_BREAKER_SECTIONS) {
+ List<Map<?, ?>> list = objects(section(status, key).get("circuitBreakers"));
+ if (!list.isEmpty()) {
+ breakers = true;
+ signals.put(key, list.size() + " circuit breaker(s)");
+ for (Map<?, ?> b : list) {
+ String routeId = text(b.get("routeId"));
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+
+ // tracing and metrics
+ Set<String> consoles = new TreeSet<>();
+ for (Object id : list(status.get("devConsoles"))) {
+ if (id != null) {
+ consoles.add(id.toString());
+ }
+ }
+ boolean otel = consoles.contains("opentelemetry");
+ boolean micrometer = consoles.contains("micrometer") || status.get("micrometer") instanceof Map;
+ if (otel || consoles.contains("micrometer")) {
+ List<String> fired = new ArrayList<>();
+ if (otel) {
+ fired.add("opentelemetry");
+ }
+ if (consoles.contains("micrometer")) {
+ fired.add("micrometer");
+ }
+ signals.put("devConsoles", String.join(",", fired));
+ }
+ if (status.get("micrometer") instanceof Map) {
+ signals.put("micrometer", "present");
Review Comment:
Agreed, the newer value should win: merge now uses put for both the datasources and the signals (ff7e9f8f86de), with a test where the pool type is only known after the pool started.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What a running integration has that a tool group is for (CAMEL-24834): its datasources and SQL endpoints, circuit
+ * breakers, OpenTelemetry, message tracing and Micrometer. Read from the status file the integration writes
+ * ({@code ~/.camel/<pid>-status.json}); a key that is missing (an older Camel, a console that is not on the classpath)
+ * just means fewer features, never an error.
+ *
+ * @param dataSources the datasources in the registry, by name
+ * @param sqlComponents the SQL components the endpoints and routes use (sql, sql-stored, jdbc, spring-jdbc, jpa)
+ * @param sqlTraced whether SQL statements have been traced
+ * @param circuitBreaker whether the routes have a circuit breaker
+ * @param circuitBreakerRoutes the routes with a circuit breaker, when known
+ * @param openTelemetry whether OpenTelemetry tracing is on
+ * @param messageTracing whether message tracing is enabled
+ * @param micrometer whether Micrometer metrics are on
+ * @param signals the status keys that gave each feature away, with what they said
+ */
+public record AppFeatures(
+ List<DataSource> dataSources,
+ List<String> sqlComponents,
+ boolean sqlTraced,
+ boolean circuitBreaker,
+ List<String> circuitBreakerRoutes,
+ boolean openTelemetry,
+ boolean messageTracing,
+ boolean micrometer,
+ Map<String, String> signals) {
+
+ /** The components whose endpoints talk SQL to a datasource. */
+ static final Set<String> SQL_COMPONENTS = Set.of("sql", "sql-stored", "jdbc", "spring-jdbc", "jpa");
+
+ private static final String CIRCUIT_BREAKER_PROCESSOR = "circuitBreaker";
+ private static final List<String> CIRCUIT_BREAKER_SECTIONS
+ = List.of("resilience4j", "fault-tolerance", "circuit-breaker");
+
+ /**
+ * A datasource of the registry.
+ *
+ * @param name the bean name
+ * @param poolType HikariCP, Agroal or Unknown, null when not known
+ */
+ public record DataSource(String name, String poolType) {
+ }
+
+ public AppFeatures {
+ dataSources = List.copyOf(dataSources);
+ sqlComponents = List.copyOf(sqlComponents);
+ circuitBreakerRoutes = List.copyOf(circuitBreakerRoutes);
+ signals = Collections.unmodifiableMap(new LinkedHashMap<>(signals));
+ }
+
+ /** No integration, or nothing to tell: no tool group loads. */
+ public static AppFeatures none() {
+ return new AppFeatures(List.of(), List.of(), false, false, List.of(), false, false, false, Map.of());
+ }
+
+ /** The names of the datasources. */
+ public List<String> dataSourceNames() {
+ return dataSources.stream().map(DataSource::name).toList();
+ }
+
+ /** Whether the integration works with a database: a datasource, a SQL endpoint or a traced statement. */
+ public boolean sql() {
+ return !dataSources.isEmpty() || !sqlComponents.isEmpty() || sqlTraced;
+ }
+
+ /** Whether there is anything to trace or measure: OpenTelemetry, message tracing or Micrometer. */
+ public boolean tracing() {
+ return openTelemetry || messageTracing || micrometer;
+ }
+
+ /**
+ * Both feature sets together: what an integration had before a reload still counts after it, so the tools a model
+ * was given do not come and go while it works.
+ */
+ public AppFeatures merge(AppFeatures other) {
+ if (other == null) {
+ return this;
+ }
+ Map<String, DataSource> ds = new TreeMap<>();
+ for (DataSource d : dataSources) {
+ ds.put(d.name(), d);
+ }
+ for (DataSource d : other.dataSources) {
+ ds.putIfAbsent(d.name(), d);
+ }
+ Set<String> components = new TreeSet<>(sqlComponents);
+ components.addAll(other.sqlComponents);
+ Set<String> routes = new TreeSet<>(circuitBreakerRoutes);
+ routes.addAll(other.circuitBreakerRoutes);
+ Map<String, String> sig = new LinkedHashMap<>(signals);
+ other.signals.forEach(sig::putIfAbsent);
+ return new AppFeatures(
+ new ArrayList<>(ds.values()), new ArrayList<>(components), sqlTraced || other.sqlTraced,
+ circuitBreaker || other.circuitBreaker, new ArrayList<>(routes),
+ openTelemetry || other.openTelemetry, messageTracing || other.messageTracing,
+ micrometer || other.micrometer, sig);
+ }
+
+ /**
+ * Reads the features from an integration's status document. Pure and defensive: a null document, a missing key or a
+ * value of an unexpected type yields fewer features.
+ */
+ public static AppFeatures fromStatus(JsonObject status) {
+ if (status == null) {
+ return none();
+ }
+ Map<String, String> signals = new LinkedHashMap<>();
+
+ // datasources: dataSources.dataSources[] with name and poolType
+ Map<String, DataSource> dataSources = new TreeMap<>();
+ for (Map<?, ?> entry : objects(section(status, "dataSources").get("dataSources"))) {
+ String name = text(entry.get("name"));
+ if (name != null) {
+ dataSources.put(name, new DataSource(name, text(entry.get("poolType"))));
+ }
+ }
+ if (!dataSources.isEmpty()) {
+ signals.put("dataSources", String.join(",", dataSources.keySet()));
+ }
+
+ // SQL endpoints: the endpoint registry, the route inputs and the processors that send somewhere
+ Set<String> components = new TreeSet<>();
+ for (Map<?, ?> ep : objects(section(status, "endpoints").get("endpoints"))) {
+ addSqlComponent(components, text(ep.get("uri")));
+ }
+ List<Map<?, ?>> routes = objects(status.get("routes"));
+ Set<String> breakerRoutes = new TreeSet<>();
+ boolean breakerProcessor = false;
+ for (Map<?, ?> route : routes) {
+ addSqlComponent(components, text(route.get("from")));
+ for (Map<?, ?> p : objects(route.get("processors"))) {
+ addSqlComponent(components, text(p.get("uri")));
+ if (CIRCUIT_BREAKER_PROCESSOR.equals(text(p.get("processor")))) {
+ breakerProcessor = true;
+ String routeId = text(p.get("routeId"));
+ if (routeId == null) {
+ routeId = text(route.get("routeId"));
+ }
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+ List<Map<?, ?>> statements = objects(section(status, "sqlTrace").get("statements"));
+ for (Map<?, ?> st : statements) {
+ addSqlComponent(components, text(st.get("endpoint")));
+ }
+ if (!components.isEmpty()) {
+ signals.put("endpoints", String.join(",", components));
+ }
+ boolean sqlTraced = false;
+ for (Map<?, ?> st : statements) {
+ if (text(st.get("query")) != null) {
+ sqlTraced = true;
+ break;
+ }
+ }
+ if (sqlTraced) {
+ signals.put("sqlTrace", statements.size() + " statement(s)");
+ }
+
+ // circuit breakers: the circuitBreaker EIP in a route, or the breakers a console reports
+ if (breakerProcessor) {
+ String where = breakerRoutes.isEmpty() ? "" : " in " + String.join(",", breakerRoutes);
+ signals.put("routes.processors", CIRCUIT_BREAKER_PROCESSOR + where);
+ }
+ boolean breakers = false;
+ for (String key : CIRCUIT_BREAKER_SECTIONS) {
+ List<Map<?, ?>> list = objects(section(status, key).get("circuitBreakers"));
+ if (!list.isEmpty()) {
+ breakers = true;
Review Comment:
Added a Micrometer-only test (ff7e9f8f86de): it loads the tracing group with camel_runtime_trace and camel_runtime_metrics, and the guidance names the metrics tool. Offering the trace tool there is on purpose, as you say.
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java:
##########
@@ -344,6 +344,27 @@ String getSelectedIntegrationName() {
return info != null ? info.name : null;
}
+ /** How often the selected integration has reloaded its routes, 0 when nothing is selected. */
+ int getSelectedReloadCount() {
+ if (ctx == null) {
+ return 0;
+ }
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.reloaded : 0;
+ }
+
+ /** The status document of the selected integration, null when nothing is selected or it has none yet. */
+ JsonObject readSelectedStatus() {
+ if (ctx == null || ctx.selectedPid == null) {
Review Comment:
readSelectedStatus now catches any RuntimeException (ff7e9f8f86de): an unreadable status means no groups, and the core set is still offered.
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27318:
URL: https://github.com/apache/camel/pull/27318#discussion_r4176408490
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -301,12 +303,31 @@ interface AcpClientFactory {
AcpAgentClient create(AiProviderSelector.AcpPreset preset, Path cwd) throws IOException;
}
+ /** The selected integration as the tool groups see it; replaced in tests. */
+ interface AppStatusSource {
+ /** The selected pid, null when none. */
+ String selectedPid();
+
+ /** How often it reloaded its routes. */
+ int reloadCount();
+
+ /** What it has, read from its status. */
+ AppFeatures features();
+ }
+
// MCP facade for TUI tool access from the AI panel
private McpFacade mcpFacade;
// /write: confirm (dialog per write), auto (the model may skip it with confirm=false) or live (the edit is replayed
// in the source editor and the user saves or discards it)
private McpFacade.WriteMode writeMode = McpFacade.WriteMode.CONFIRM;
private TuiToolRegistry toolRegistry;
+ // CAMEL-24834: the tool groups (SQL, tracing, resilience) of the selected integration that the core set gets, see
+ // refreshToolGroups()
Review Comment:
Made the four tool-group fields volatile like the rest of the panel's state (a6dcc09fc1d4). They are only written from the event thread in submitQuestion today, but the mix was surprising.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What a running integration has that a tool group is for (CAMEL-24834): its datasources and SQL endpoints, circuit
+ * breakers, OpenTelemetry, message tracing and Micrometer. Read from the status file the integration writes
+ * ({@code ~/.camel/<pid>-status.json}); a key that is missing (an older Camel, a console that is not on the classpath)
+ * just means fewer features, never an error.
+ *
+ * @param dataSources the datasources in the registry, by name
+ * @param sqlComponents the SQL components the endpoints and routes use (sql, sql-stored, jdbc, spring-jdbc, jpa)
+ * @param sqlTraced whether SQL statements have been traced
+ * @param circuitBreaker whether the routes have a circuit breaker
+ * @param circuitBreakerRoutes the routes with a circuit breaker, when known
+ * @param openTelemetry whether OpenTelemetry tracing is on
+ * @param messageTracing whether message tracing is enabled
+ * @param micrometer whether Micrometer metrics are on
+ * @param signals the status keys that gave each feature away, with what they said
+ */
+public record AppFeatures(
+ List<DataSource> dataSources,
+ List<String> sqlComponents,
+ boolean sqlTraced,
+ boolean circuitBreaker,
+ List<String> circuitBreakerRoutes,
+ boolean openTelemetry,
+ boolean messageTracing,
+ boolean micrometer,
+ Map<String, String> signals) {
+
+ /** The components whose endpoints talk SQL to a datasource. */
+ static final Set<String> SQL_COMPONENTS = Set.of("sql", "sql-stored", "jdbc", "spring-jdbc", "jpa");
+
+ private static final String CIRCUIT_BREAKER_PROCESSOR = "circuitBreaker";
+ private static final List<String> CIRCUIT_BREAKER_SECTIONS
+ = List.of("resilience4j", "fault-tolerance", "circuit-breaker");
+
+ /**
+ * A datasource of the registry.
+ *
+ * @param name the bean name
+ * @param poolType HikariCP, Agroal or Unknown, null when not known
+ */
+ public record DataSource(String name, String poolType) {
+ }
+
+ public AppFeatures {
+ dataSources = List.copyOf(dataSources);
+ sqlComponents = List.copyOf(sqlComponents);
+ circuitBreakerRoutes = List.copyOf(circuitBreakerRoutes);
+ signals = Collections.unmodifiableMap(new LinkedHashMap<>(signals));
+ }
+
+ /** No integration, or nothing to tell: no tool group loads. */
+ public static AppFeatures none() {
+ return new AppFeatures(List.of(), List.of(), false, false, List.of(), false, false, false, Map.of());
+ }
+
+ /** The names of the datasources. */
+ public List<String> dataSourceNames() {
+ return dataSources.stream().map(DataSource::name).toList();
+ }
+
+ /** Whether the integration works with a database: a datasource, a SQL endpoint or a traced statement. */
+ public boolean sql() {
+ return !dataSources.isEmpty() || !sqlComponents.isEmpty() || sqlTraced;
+ }
+
+ /** Whether there is anything to trace or measure: OpenTelemetry, message tracing or Micrometer. */
+ public boolean tracing() {
+ return openTelemetry || messageTracing || micrometer;
+ }
+
+ /**
+ * Both feature sets together: what an integration had before a reload still counts after it, so the tools a model
+ * was given do not come and go while it works.
+ */
+ public AppFeatures merge(AppFeatures other) {
+ if (other == null) {
+ return this;
+ }
+ Map<String, DataSource> ds = new TreeMap<>();
+ for (DataSource d : dataSources) {
+ ds.put(d.name(), d);
+ }
+ for (DataSource d : other.dataSources) {
+ ds.putIfAbsent(d.name(), d);
+ }
+ Set<String> components = new TreeSet<>(sqlComponents);
+ components.addAll(other.sqlComponents);
+ Set<String> routes = new TreeSet<>(circuitBreakerRoutes);
+ routes.addAll(other.circuitBreakerRoutes);
+ Map<String, String> sig = new LinkedHashMap<>(signals);
+ other.signals.forEach(sig::putIfAbsent);
+ return new AppFeatures(
+ new ArrayList<>(ds.values()), new ArrayList<>(components), sqlTraced || other.sqlTraced,
+ circuitBreaker || other.circuitBreaker, new ArrayList<>(routes),
+ openTelemetry || other.openTelemetry, messageTracing || other.messageTracing,
+ micrometer || other.micrometer, sig);
+ }
+
+ /**
+ * Reads the features from an integration's status document. Pure and defensive: a null document, a missing key or a
+ * value of an unexpected type yields fewer features.
+ */
+ public static AppFeatures fromStatus(JsonObject status) {
+ if (status == null) {
+ return none();
+ }
+ Map<String, String> signals = new LinkedHashMap<>();
+
+ // datasources: dataSources.dataSources[] with name and poolType
+ Map<String, DataSource> dataSources = new TreeMap<>();
+ for (Map<?, ?> entry : objects(section(status, "dataSources").get("dataSources"))) {
+ String name = text(entry.get("name"));
+ if (name != null) {
+ dataSources.put(name, new DataSource(name, text(entry.get("poolType"))));
+ }
+ }
+ if (!dataSources.isEmpty()) {
+ signals.put("dataSources", String.join(",", dataSources.keySet()));
+ }
+
+ // SQL endpoints: the endpoint registry, the route inputs and the processors that send somewhere
+ Set<String> components = new TreeSet<>();
+ for (Map<?, ?> ep : objects(section(status, "endpoints").get("endpoints"))) {
+ addSqlComponent(components, text(ep.get("uri")));
+ }
+ List<Map<?, ?>> routes = objects(status.get("routes"));
+ Set<String> breakerRoutes = new TreeSet<>();
+ boolean breakerProcessor = false;
+ for (Map<?, ?> route : routes) {
+ addSqlComponent(components, text(route.get("from")));
+ for (Map<?, ?> p : objects(route.get("processors"))) {
+ addSqlComponent(components, text(p.get("uri")));
+ if (CIRCUIT_BREAKER_PROCESSOR.equals(text(p.get("processor")))) {
+ breakerProcessor = true;
+ String routeId = text(p.get("routeId"));
+ if (routeId == null) {
+ routeId = text(route.get("routeId"));
+ }
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+ List<Map<?, ?>> statements = objects(section(status, "sqlTrace").get("statements"));
+ for (Map<?, ?> st : statements) {
+ addSqlComponent(components, text(st.get("endpoint")));
+ }
+ if (!components.isEmpty()) {
+ signals.put("endpoints", String.join(",", components));
+ }
+ boolean sqlTraced = false;
+ for (Map<?, ?> st : statements) {
+ if (text(st.get("query")) != null) {
+ sqlTraced = true;
+ break;
+ }
+ }
+ if (sqlTraced) {
+ signals.put("sqlTrace", statements.size() + " statement(s)");
+ }
+
+ // circuit breakers: the circuitBreaker EIP in a route, or the breakers a console reports
+ if (breakerProcessor) {
+ String where = breakerRoutes.isEmpty() ? "" : " in " + String.join(",", breakerRoutes);
+ signals.put("routes.processors", CIRCUIT_BREAKER_PROCESSOR + where);
+ }
+ boolean breakers = false;
+ for (String key : CIRCUIT_BREAKER_SECTIONS) {
+ List<Map<?, ?>> list = objects(section(status, key).get("circuitBreakers"));
+ if (!list.isEmpty()) {
+ breakers = true;
+ signals.put(key, list.size() + " circuit breaker(s)");
+ for (Map<?, ?> b : list) {
+ String routeId = text(b.get("routeId"));
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+
+ // tracing and metrics
+ Set<String> consoles = new TreeSet<>();
+ for (Object id : list(status.get("devConsoles"))) {
+ if (id != null) {
+ consoles.add(id.toString());
+ }
+ }
+ boolean otel = consoles.contains("opentelemetry");
+ boolean micrometer = consoles.contains("micrometer") || status.get("micrometer") instanceof Map;
+ if (otel || consoles.contains("micrometer")) {
+ List<String> fired = new ArrayList<>();
+ if (otel) {
+ fired.add("opentelemetry");
+ }
+ if (consoles.contains("micrometer")) {
+ fired.add("micrometer");
+ }
+ signals.put("devConsoles", String.join(",", fired));
+ }
+ if (status.get("micrometer") instanceof Map) {
+ signals.put("micrometer", "present");
Review Comment:
Agreed, the newer value should win: merge now uses put for both the datasources and the signals (a6dcc09fc1d4), with a test where the pool type is only known after the pool started.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What a running integration has that a tool group is for (CAMEL-24834): its datasources and SQL endpoints, circuit
+ * breakers, OpenTelemetry, message tracing and Micrometer. Read from the status file the integration writes
+ * ({@code ~/.camel/<pid>-status.json}); a key that is missing (an older Camel, a console that is not on the classpath)
+ * just means fewer features, never an error.
+ *
+ * @param dataSources the datasources in the registry, by name
+ * @param sqlComponents the SQL components the endpoints and routes use (sql, sql-stored, jdbc, spring-jdbc, jpa)
+ * @param sqlTraced whether SQL statements have been traced
+ * @param circuitBreaker whether the routes have a circuit breaker
+ * @param circuitBreakerRoutes the routes with a circuit breaker, when known
+ * @param openTelemetry whether OpenTelemetry tracing is on
+ * @param messageTracing whether message tracing is enabled
+ * @param micrometer whether Micrometer metrics are on
+ * @param signals the status keys that gave each feature away, with what they said
+ */
+public record AppFeatures(
+ List<DataSource> dataSources,
+ List<String> sqlComponents,
+ boolean sqlTraced,
+ boolean circuitBreaker,
+ List<String> circuitBreakerRoutes,
+ boolean openTelemetry,
+ boolean messageTracing,
+ boolean micrometer,
+ Map<String, String> signals) {
+
+ /** The components whose endpoints talk SQL to a datasource. */
+ static final Set<String> SQL_COMPONENTS = Set.of("sql", "sql-stored", "jdbc", "spring-jdbc", "jpa");
+
+ private static final String CIRCUIT_BREAKER_PROCESSOR = "circuitBreaker";
+ private static final List<String> CIRCUIT_BREAKER_SECTIONS
+ = List.of("resilience4j", "fault-tolerance", "circuit-breaker");
+
+ /**
+ * A datasource of the registry.
+ *
+ * @param name the bean name
+ * @param poolType HikariCP, Agroal or Unknown, null when not known
+ */
+ public record DataSource(String name, String poolType) {
+ }
+
+ public AppFeatures {
+ dataSources = List.copyOf(dataSources);
+ sqlComponents = List.copyOf(sqlComponents);
+ circuitBreakerRoutes = List.copyOf(circuitBreakerRoutes);
+ signals = Collections.unmodifiableMap(new LinkedHashMap<>(signals));
+ }
+
+ /** No integration, or nothing to tell: no tool group loads. */
+ public static AppFeatures none() {
+ return new AppFeatures(List.of(), List.of(), false, false, List.of(), false, false, false, Map.of());
+ }
+
+ /** The names of the datasources. */
+ public List<String> dataSourceNames() {
+ return dataSources.stream().map(DataSource::name).toList();
+ }
+
+ /** Whether the integration works with a database: a datasource, a SQL endpoint or a traced statement. */
+ public boolean sql() {
+ return !dataSources.isEmpty() || !sqlComponents.isEmpty() || sqlTraced;
+ }
+
+ /** Whether there is anything to trace or measure: OpenTelemetry, message tracing or Micrometer. */
+ public boolean tracing() {
+ return openTelemetry || messageTracing || micrometer;
+ }
+
+ /**
+ * Both feature sets together: what an integration had before a reload still counts after it, so the tools a model
+ * was given do not come and go while it works.
+ */
+ public AppFeatures merge(AppFeatures other) {
+ if (other == null) {
+ return this;
+ }
+ Map<String, DataSource> ds = new TreeMap<>();
+ for (DataSource d : dataSources) {
+ ds.put(d.name(), d);
+ }
+ for (DataSource d : other.dataSources) {
+ ds.putIfAbsent(d.name(), d);
+ }
+ Set<String> components = new TreeSet<>(sqlComponents);
+ components.addAll(other.sqlComponents);
+ Set<String> routes = new TreeSet<>(circuitBreakerRoutes);
+ routes.addAll(other.circuitBreakerRoutes);
+ Map<String, String> sig = new LinkedHashMap<>(signals);
+ other.signals.forEach(sig::putIfAbsent);
+ return new AppFeatures(
+ new ArrayList<>(ds.values()), new ArrayList<>(components), sqlTraced || other.sqlTraced,
+ circuitBreaker || other.circuitBreaker, new ArrayList<>(routes),
+ openTelemetry || other.openTelemetry, messageTracing || other.messageTracing,
+ micrometer || other.micrometer, sig);
+ }
+
+ /**
+ * Reads the features from an integration's status document. Pure and defensive: a null document, a missing key or a
+ * value of an unexpected type yields fewer features.
+ */
+ public static AppFeatures fromStatus(JsonObject status) {
+ if (status == null) {
+ return none();
+ }
+ Map<String, String> signals = new LinkedHashMap<>();
+
+ // datasources: dataSources.dataSources[] with name and poolType
+ Map<String, DataSource> dataSources = new TreeMap<>();
+ for (Map<?, ?> entry : objects(section(status, "dataSources").get("dataSources"))) {
+ String name = text(entry.get("name"));
+ if (name != null) {
+ dataSources.put(name, new DataSource(name, text(entry.get("poolType"))));
+ }
+ }
+ if (!dataSources.isEmpty()) {
+ signals.put("dataSources", String.join(",", dataSources.keySet()));
+ }
+
+ // SQL endpoints: the endpoint registry, the route inputs and the processors that send somewhere
+ Set<String> components = new TreeSet<>();
+ for (Map<?, ?> ep : objects(section(status, "endpoints").get("endpoints"))) {
+ addSqlComponent(components, text(ep.get("uri")));
+ }
+ List<Map<?, ?>> routes = objects(status.get("routes"));
+ Set<String> breakerRoutes = new TreeSet<>();
+ boolean breakerProcessor = false;
+ for (Map<?, ?> route : routes) {
+ addSqlComponent(components, text(route.get("from")));
+ for (Map<?, ?> p : objects(route.get("processors"))) {
+ addSqlComponent(components, text(p.get("uri")));
+ if (CIRCUIT_BREAKER_PROCESSOR.equals(text(p.get("processor")))) {
+ breakerProcessor = true;
+ String routeId = text(p.get("routeId"));
+ if (routeId == null) {
+ routeId = text(route.get("routeId"));
+ }
+ if (routeId != null) {
+ breakerRoutes.add(routeId);
+ }
+ }
+ }
+ }
+ List<Map<?, ?>> statements = objects(section(status, "sqlTrace").get("statements"));
+ for (Map<?, ?> st : statements) {
+ addSqlComponent(components, text(st.get("endpoint")));
+ }
+ if (!components.isEmpty()) {
+ signals.put("endpoints", String.join(",", components));
+ }
+ boolean sqlTraced = false;
+ for (Map<?, ?> st : statements) {
+ if (text(st.get("query")) != null) {
+ sqlTraced = true;
+ break;
+ }
+ }
+ if (sqlTraced) {
+ signals.put("sqlTrace", statements.size() + " statement(s)");
+ }
+
+ // circuit breakers: the circuitBreaker EIP in a route, or the breakers a console reports
+ if (breakerProcessor) {
+ String where = breakerRoutes.isEmpty() ? "" : " in " + String.join(",", breakerRoutes);
+ signals.put("routes.processors", CIRCUIT_BREAKER_PROCESSOR + where);
+ }
+ boolean breakers = false;
+ for (String key : CIRCUIT_BREAKER_SECTIONS) {
+ List<Map<?, ?>> list = objects(section(status, key).get("circuitBreakers"));
+ if (!list.isEmpty()) {
+ breakers = true;
Review Comment:
Added a Micrometer-only test (a6dcc09fc1d4): it loads the tracing group with camel_runtime_trace and camel_runtime_metrics, and the guidance names the metrics tool. Offering the trace tool there is on purpose, as you say.
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java:
##########
@@ -344,6 +344,27 @@ String getSelectedIntegrationName() {
return info != null ? info.name : null;
}
+ /** How often the selected integration has reloaded its routes, 0 when nothing is selected. */
+ int getSelectedReloadCount() {
+ if (ctx == null) {
+ return 0;
+ }
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.reloaded : 0;
+ }
+
+ /** The status document of the selected integration, null when nothing is selected or it has none yet. */
+ JsonObject readSelectedStatus() {
+ if (ctx == null || ctx.selectedPid == null) {
Review Comment:
readSelectedStatus now catches any RuntimeException (a6dcc09fc1d4): an unreadable status means no groups, and the core set is still offered.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.commands.ai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Picks the runtime tool groups for an integration from its {@link AppFeatures} (CAMEL-24834). A small local model pays
+ * for every tool schema on every request, so the SQL, tracing and resilience tools only load when the integration has a
+ * database, tracing or circuit breakers; each loaded group adds one line of guidance that names what the integration
+ * has and which tool reads it. The tool names and the wording here are those of the MCP server; the TUI maps the same
+ * groups to its own tools.
+ */
+public final class ToolGroups {
+
+ public static final String SQL_TOOL = "camel_runtime_sql";
+ public static final String DATASOURCES_TOOL = "camel_runtime_datasources";
+ public static final String SQL_TRACE_TOOL = "camel_runtime_sql_trace";
+ public static final String SPANS_TOOL = "camel_runtime_spans";
+ public static final String TRACE_TOOL = "camel_runtime_trace";
+ public static final String METRICS_TOOL = "camel_runtime_metrics";
+ public static final String CIRCUIT_BREAKERS_TOOL = "camel_runtime_circuit_breakers";
+
+ /**
+ * A loaded group.
+ *
+ * @param group the group
+ * @param tools the tools it adds, in the order to offer them
+ * @param guidance one line for the model: what the integration has and which tool reads it
+ */
+ public record Group(ToolGroup group, List<String> tools, String guidance) {
+
+ public Group {
+ tools = List.copyOf(tools);
+ }
+ }
+
+ /**
+ * The groups for an integration.
+ *
+ * @param groups the loaded groups, in {@link ToolGroup} order
+ * @param fingerprint stable for the same groups and datasources: a client rebuilds its tool list only when it
+ * changes
+ */
+ public record Selection(List<Group> groups, String fingerprint) {
+
+ public Selection {
+ groups = List.copyOf(groups);
+ }
+
+ public List<ToolGroup> toolGroups() {
+ return groups.stream().map(Group::group).toList();
+ }
+
+ public boolean has(ToolGroup group) {
+ return groups.stream().anyMatch(g -> g.group() == group);
+ }
+
+ /** The tools of all loaded groups. */
+ public List<String> mcpTools() {
+ return groups.stream().flatMap(g -> g.tools().stream()).distinct().toList();
+ }
+
+ /** The guidance lines of all loaded groups. */
+ public List<String> guidance() {
+ return groups.stream().map(Group::guidance).toList();
+ }
+ }
+
+ private ToolGroups() {
+ }
+
+ /** The groups an integration needs. */
+ public static Selection select(AppFeatures features) {
+ AppFeatures f = features != null ? features : AppFeatures.none();
+ List<Group> groups = new ArrayList<>();
+ for (ToolGroup group : groups(f)) {
+ switch (group) {
+ case SQL -> groups.add(new Group(
+ group,
+ List.of(SQL_TOOL, DATASOURCES_TOOL, SQL_TRACE_TOOL),
Review Comment:
Left as is: datasource names are bean names (identifiers), and the fingerprint is only compared for change, never parsed.
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus merged PR #27318:
URL: https://github.com/apache/camel/pull/27318
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27318:
URL: https://github.com/apache/camel/pull/27318#issuecomment-5974437900
<!-- ci-tested-modules -->
:test_tube: **CI tested the following changed modules:**
- `docs`
- `dsl/camel-jbang/camel-jbang-core`
- `dsl/camel-jbang/camel-jbang-mcp`
- `dsl/camel-jbang/camel-jbang-plugin-tui`
---
<details><summary>:microscope: Scalpel shadow comparison — Scalpel: 6 of 698 tested, 8 compile-only — current: 6 all tested</summary>
[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **6 affected modules** (current approach: 6).
**Skip-tests mode would test 6 modules** (4 direct + 4 downstream), **skip tests for 8** (generated code, meta-modules)
<details><summary>Modules Scalpel would test (6)</summary>
- `camel-jbang-mcp` ← dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java, dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
- `camel-jbang-plugin-mcp` ← downstream of org.apache.camel:camel-jbang-core
- `camel-jbang-plugin-route-parser` ← downstream of org.apache.camel:camel-jbang-core
- `camel-jbang-plugin-tui` ← dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java, dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java, dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java, dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java, dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java, dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
- `camel-jbang-plugin-validate` ← downstream of org.apache.camel:camel-jbang-core
- `camel-launcher-container` ← downstream of org.apache.camel:camel-launcher
</details>
<details><summary>Modules with tests skipped (8)</summary>
- `camel-jbang-it`
- `camel-jbang-main`
- `camel-jbang-plugin-edit`
- `camel-jbang-plugin-generate`
- `camel-jbang-plugin-kubernetes`
- `camel-jbang-plugin-test`
- `camel-launcher`
- `coverage`
</details>
> :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)
</details>
:warning: **Some tests are disabled on GitHub Actions** (`@DisabledIfSystemProperty(named = "ci.env.name")`) and require manual verification:
- `dsl/camel-jbang/camel-jbang-core`: 2 test(s) disabled on GitHub Actions
- `dsl/camel-jbang/camel-jbang-mcp`: 1 test(s) disabled on GitHub Actions
:bulb: **Manual integration tests recommended:**
> You modified `dsl/camel-jbang/camel-jbang-core`. The related integration tests in `dsl/camel-jbang/camel-jbang-it` are excluded from CI. Consider running them manually:
> ```
> mvn verify -f dsl/camel-jbang/camel-jbang-it -Djbang-it-test
> ```
<details><summary>All tested modules (16 modules, 22.0s total)</summary>
**Total reactor time:** 22.0s
| Module | Duration | Status |
| --- | --- | --- |
| Camel :: Docs | 22.0s | SUCCESS |
| Camel :: Coverage | n/a | |
| Camel :: JBang :: Core | n/a | |
| Camel :: JBang :: Integration tests | n/a | |
| Camel :: JBang :: MCP | n/a | |
| Camel :: JBang :: Main | n/a | |
| Camel :: JBang :: Plugin :: Edit | n/a | |
| Camel :: JBang :: Plugin :: Generate | n/a | |
| Camel :: JBang :: Plugin :: Kubernetes | n/a | |
| Camel :: JBang :: Plugin :: MCP | n/a | |
| Camel :: JBang :: Plugin :: Route Parser | n/a | |
| Camel :: JBang :: Plugin :: TUI | n/a | |
| Camel :: JBang :: Plugin :: Testing | n/a | |
| Camel :: JBang :: Plugin :: Validate | n/a | |
| Camel :: Launcher | n/a | |
| Camel :: Launcher :: Container | n/a | |
**Top 20 slowest modules:**
- `Camel :: Docs` (22.0s)
</details>
---
:gear: [View full build and test results](https://github.com/apache/camel/actions/runs/37159252294)
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-24834: camel-jbang - load AI tool groups (SQL, tracing, resilience) from what the selected integration has [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on PR #27318:
URL: https://github.com/apache/camel/pull/27318#issuecomment-5977281189
Rebased onto main and force-pushed: the eip-samples.json regen (6b7d5063c767) is now on main through #27328, so the branch no longer carries it. The review fixes are in ff7e9f8f86de.
--
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: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org