You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ld...@apache.org on 2026/10/10 13:26:45 UTC
(camel) 06/08: CAMEL-25499: Classify semantic default-selection errors
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 410257124006812d831c839f38e818416e9d6215
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Sat Oct 10 14:37:03 2026 +0200
CAMEL-25499: Classify semantic default-selection errors
Classify missing or ambiguous automatic selection through a typed internal
exception and expose a stable defaultErrorCode from semantic metadata.
Let the CLI use the code with a compatibility fallback for older runtimes,
while preserving real errors and the existing response constructor.
Document the intentionally shared live/persisted audit-record validation
and bounded defensive input copy. Add producer and CLI regressions and
regenerate console metadata and catalog documentation.
Co-authored-by: OpenAI Codex <no...@openai.com>
---
.../apache/camel/catalog/dev-consoles-openapi.json | 4 ++
.../catalog/dev-consoles/semantic-metadata.json | 4 ++
.../camel/catalog/docs/semantic-language.adoc | 5 ++
.../camel/dev-console/semantic-metadata.json | 4 ++
.../src/main/docs/semantic-language.adoc | 5 ++
.../camel/language/semantic/SemanticLanguage.java | 5 +-
.../apache/camel/semantic/SemanticAuditRecord.java | 2 +-
.../camel/semantic/SemanticMetadataConsole.java | 13 ++++-
.../internal/NoUniqueSemanticExpertException.java | 29 +++++++++++
.../semantic/internal/SemanticAuditService.java | 1 +
.../semantic/SemanticMetadataConsoleTest.java | 56 ++++++++++++++++++++++
.../modules/ROOT/pages/camel-jbang-managing.adoc | 4 +-
.../jbang/core/commands/action/CamelSemantic.java | 7 ++-
.../core/commands/action/SemanticCommandTest.java | 22 +++++++++
14 files changed, 153 insertions(+), 8 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index 1bfbf908ff0c..66dfb8d9507b 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -7435,6 +7435,10 @@
"defaultError": {
"type": "string",
"description": "Default selection error, if any"
+ },
+ "defaultErrorCode": {
+ "type": "string",
+ "description": "Default selection error code: no_unique_expert or default_expert_error"
}
}
}
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/semantic-metadata.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/semantic-metadata.json
index 9f01551d9f8f..7ea4452ed6a2 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/semantic-metadata.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/semantic-metadata.json
@@ -176,6 +176,10 @@
"defaultError": {
"type": "string",
"description": "Default selection error, if any"
+ },
+ "defaultErrorCode": {
+ "type": "string",
+ "description": "Default selection error code: no_unique_expert or default_expert_error"
}
}
}
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index d2a930bf897f..e82cea82d6b7 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -914,6 +914,11 @@ Use `camel semantic` to inspect definitions and `camel semantic eval` to evaluat
xref:manual::camel-jbang-managing.adoc#_semantic_definitions_and_evaluation[Camel CLI].
`semantic-metadata` exposes definitions and expert contracts without performing inference.
+With `overview=true`, a failed default selection includes `defaultError` and a stable
+`defaultErrorCode`: `no_unique_expert` means automatic discovery found zero or multiple
+eligible experts; `default_expert_error` identifies a configuration, contract or descriptor
+failure. Consumers can distinguish these cases without parsing the diagnostic message.
+
`semantic-evaluate` evaluates a named definition against a sample exchange or calls an expert
operation directly. It uses the configured provider and its normal validation, without sending
the sample through an application route.
diff --git a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/dev-console/semantic-metadata.json b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/dev-console/semantic-metadata.json
index 9f01551d9f8f..7ea4452ed6a2 100644
--- a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/dev-console/semantic-metadata.json
+++ b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/dev-console/semantic-metadata.json
@@ -176,6 +176,10 @@
"defaultError": {
"type": "string",
"description": "Default selection error, if any"
+ },
+ "defaultErrorCode": {
+ "type": "string",
+ "description": "Default selection error code: no_unique_expert or default_expert_error"
}
}
}
diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index d2a930bf897f..e82cea82d6b7 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -914,6 +914,11 @@ Use `camel semantic` to inspect definitions and `camel semantic eval` to evaluat
xref:manual::camel-jbang-managing.adoc#_semantic_definitions_and_evaluation[Camel CLI].
`semantic-metadata` exposes definitions and expert contracts without performing inference.
+With `overview=true`, a failed default selection includes `defaultError` and a stable
+`defaultErrorCode`: `no_unique_expert` means automatic discovery found zero or multiple
+eligible experts; `default_expert_error` identifies a configuration, contract or descriptor
+failure. Consumers can distinguish these cases without parsing the diagnostic message.
+
`semantic-evaluate` evaluates a named definition against a sample exchange or calls an expert
operation directly. It uses the configured provider and its normal validation, without sending
the sample through an application route.
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index ea64debe3cf4..ac7bbf6599f6 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -48,6 +48,7 @@ import org.apache.camel.semantic.SemanticEvaluation;
import org.apache.camel.semantic.SemanticEvaluations;
import org.apache.camel.semantic.SemanticExpert.ResultType;
import org.apache.camel.semantic.SemanticResult;
+import org.apache.camel.semantic.internal.NoUniqueSemanticExpertException;
import org.apache.camel.semantic.internal.SemanticAuditService;
import org.apache.camel.spi.FactoryFinder;
import org.apache.camel.spi.Metadata;
@@ -374,9 +375,7 @@ public class SemanticLanguage extends LanguageSupport {
if (candidates.size() + instances.size() != 1) {
Set<String> names = new TreeSet<>(registered.keySet());
names.addAll(candidates);
- throw new IllegalArgumentException(
- "Semantic language requires exactly one eligible expert; available experts: " + names
- + ". Specify expert or configure camel.language.semantic.default-expert explicitly");
+ throw new NoUniqueSemanticExpertException(names);
}
if (instances.size() == 1) {
SemanticAdapter instance = instances.iterator().next();
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java
index cd996b2beaa5..13dfd58aab45 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java
@@ -38,7 +38,7 @@ public final class SemanticAuditRecord {
this.timestamp = Instant.parse((String) fields.get("timestamp"));
}
- /** Reconstruct a persisted version-1 record; rejects unknown fields and bounds nested data. */
+ /** Create or restore a version-1 record; rejects unknown fields and defensively copies bounded nested data. */
public static SemanticAuditRecord fromMap(Map<String, ?> data) {
Set<String> keys = Set.of("schemaVersion", "eventId", "invocationId", "timestamp", "category", "origin", "requestId",
"batchId", "definition", "target", "expert", "operation", "contextId", "exchangeId", "routeId", "semantics",
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticMetadataConsole.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticMetadataConsole.java
index 255c279fd30c..19776950316a 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticMetadataConsole.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticMetadataConsole.java
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.TreeMap;
import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.semantic.internal.NoUniqueSemanticExpertException;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.annotations.DevConsole;
import org.apache.camel.support.console.AbstractDevConsole;
@@ -73,7 +74,13 @@ public class SemanticMetadataConsole extends AbstractDevConsole {
@Metadata(description = "Known experts, when overview is requested") List<Expert> experts,
@Metadata(description = "Published definitions, when overview is requested") List<Definition> evaluations,
@Metadata(description = "Resolved default expert") String defaultExpert,
- @Metadata(description = "Default selection error, if any") String defaultError) {
+ @Metadata(description = "Default selection error, if any") String defaultError,
+ @Metadata(description = "Default selection error code: no_unique_expert or default_expert_error") String defaultErrorCode) {
+
+ public Response(List<Operation> operations, List<Expert> experts, List<Definition> evaluations,
+ String defaultExpert, String defaultError) {
+ this(operations, experts, evaluations, defaultExpert, defaultError, null);
+ }
}
public SemanticMetadataConsole() {
@@ -112,10 +119,12 @@ public class SemanticMetadataConsole extends AbstractDevConsole {
});
String defaultExpert = null;
String defaultError = null;
+ String defaultErrorCode = null;
try {
defaultExpert = addExpert(experts, language.describeExpert(null));
} catch (IllegalArgumentException | IOException e) {
defaultError = e.getMessage();
+ defaultErrorCode = e instanceof NoUniqueSemanticExpertException ? "no_unique_expert" : "default_expert_error";
}
List<Definition> definitions = new ArrayList<>();
SemanticEvaluations registry = getCamelContext().getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
@@ -138,7 +147,7 @@ public class SemanticMetadataConsole extends AbstractDevConsole {
evaluation.getParameters(), resultType, error));
});
}
- return new Response(null, List.copyOf(experts.values()), definitions, defaultExpert, defaultError);
+ return new Response(null, List.copyOf(experts.values()), definitions, defaultExpert, defaultError, defaultErrorCode);
}
private static String addExpert(Map<String, Expert> experts, SemanticLanguage.ExpertMetadata metadata) {
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/NoUniqueSemanticExpertException.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/NoUniqueSemanticExpertException.java
new file mode 100644
index 000000000000..ace6686d7a4c
--- /dev/null
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/NoUniqueSemanticExpertException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.semantic.internal;
+
+import java.util.Set;
+
+/** Automatic discovery found zero or multiple eligible experts. */
+public final class NoUniqueSemanticExpertException extends IllegalArgumentException {
+ private static final long serialVersionUID = 1L;
+
+ public NoUniqueSemanticExpertException(Set<String> experts) {
+ super("Semantic language requires exactly one eligible expert; available experts: " + experts
+ + ". Specify expert or configure camel.language.semantic.default-expert explicitly");
+ }
+}
diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java
index bd8c000b4001..de2a56b1f102 100644
--- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java
+++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java
@@ -472,6 +472,7 @@ public final class SemanticAuditService extends ServiceSupport {
}
if (enabled) {
fields.putAll(inputFields);
+ // Keep live and restored records on the same validation path, including the bounded defensive input copy.
publish(inputFields.isEmpty() ? record : SemanticAuditRecord.fromMap(fields));
}
}
diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticMetadataConsoleTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticMetadataConsoleTest.java
index 24119edc841c..93079a7aceeb 100644
--- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticMetadataConsoleTest.java
+++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticMetadataConsoleTest.java
@@ -28,11 +28,62 @@ import org.apache.camel.semantic.SemanticExpert.ResultType;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class SemanticMetadataConsoleTest {
+ @ParameterizedTest
+ @ValueSource(ints = { 0, 2 })
+ void overviewClassifiesNonUniqueAutomaticSelection(int expertCount) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ for (int i = 0; i < expertCount; i++) {
+ context.getRegistry().bind("security" + i, new Detector());
+ }
+ context.start();
+ DevConsole console = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("semantic-metadata");
+ JsonObject response = (JsonObject) console.call(DevConsole.MediaType.JSON, Map.of("overview", true));
+ assertThat(response).containsEntry("defaultErrorCode", "no_unique_expert");
+ assertThat(response.getString("defaultError")).contains("requires exactly one eligible expert");
+ assertThat(response.get("defaultExpert")).isNull();
+ List<JsonObject> experts = response.getCollection("experts");
+ assertThat(experts).hasSize(expertCount);
+ }
+ }
+
+ @Test
+ void overviewResolvesTheOnlyExpertWithoutAnErrorCode() throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("security", new Detector());
+ context.start();
+ DevConsole console = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("semantic-metadata");
+ JsonObject response = (JsonObject) console.call(DevConsole.MediaType.JSON, Map.of("overview", true));
+ assertThat(response).containsEntry("defaultExpert", "security");
+ assertThat(response.get("defaultError")).isNull();
+ assertThat(response.get("defaultErrorCode")).isNull();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "missing", "wrong-type", "invalid-contract" })
+ void overviewDistinguishesBrokenDefaultsFromAutomaticSelection(String expert) throws Exception {
+ try (var context = new DefaultCamelContext()) {
+ context.getRegistry().bind("security", new Detector());
+ context.getRegistry().bind("wrong-type", new Object());
+ context.getRegistry().bind("invalid-contract", new InvalidDetector());
+ var language = (SemanticLanguage) context.resolveLanguage("semantic");
+ language.setDefaultExpert(expert);
+ context.start();
+ DevConsole console = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("semantic-metadata");
+ JsonObject response = (JsonObject) console.call(DevConsole.MediaType.JSON, Map.of("overview", true));
+ assertThat(response).containsEntry("defaultErrorCode", "default_expert_error");
+ assertThat(response.getString("defaultError")).isNotBlank();
+ assertThat(response.get("defaultExpert")).isNull();
+ }
+ }
+
@Test
void inspectionDoesNotSelectAnAdapterAndErrorsUseTheResolvedName() throws Exception {
try (var context = new DefaultCamelContext()) {
@@ -133,4 +184,9 @@ class SemanticMetadataConsoleTest {
throw new AssertionError("Metadata must not construct a class-discovered expert");
}
}
+
+ @SemanticExpert(name = "invalid", description = "Invalid contract", provider = "test", artifactId = "test",
+ operations = {})
+ public static class InvalidDetector extends Detector {
+ }
}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index 3d43c9edada4..7be473ac9496 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -301,7 +301,9 @@ definition names, experts, operations, state selectors, result types and resolut
errors, followed by the known experts and default expert. Listing does not require a
default expert: the text output shows `Default expert: none` when automatic selection has
no eligible expert or is ambiguous. Configuration and contract-resolution errors still appear
-as `Default expert error`. `--json` retains all default-selection diagnostics. `--expert`
+as `Default expert error`. `--json` retains all default-selection diagnostics, including
+`defaultErrorCode` when provided by the runtime. The CLI uses that code to identify an
+absent or ambiguous automatic default and supports older runtimes that return only a message. `--expert`
shows one expert's operations, input and result semantics, and parameter contract.
Inspection does not perform inference. Use `--json` for machine-readable output.
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java
index b29b478d8973..9f18a04f5788 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java
@@ -98,7 +98,12 @@ public class CamelSemantic extends SemanticActionCommand {
String defaultExpert = text(response, "defaultExpert");
printer().println("Default expert: " + (defaultExpert.isEmpty() ? "none" : defaultExpert));
String defaultError = text(response, "defaultError");
- if (!defaultError.isEmpty() && !defaultError.startsWith("Semantic language requires exactly one eligible expert")) {
+ String defaultErrorCode = text(response, "defaultErrorCode");
+ // Older runtimes expose only the message; structured codes take precedence when available.
+ boolean noUniqueExpert = defaultErrorCode.isEmpty()
+ ? defaultError.startsWith("Semantic language requires exactly one eligible expert")
+ : "no_unique_expert".equals(defaultErrorCode);
+ if (!defaultError.isEmpty() && !noUniqueExpert) {
printer().println("Default expert error: " + defaultError);
}
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
index ed42f0128194..0945e52f599c 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
@@ -113,6 +113,27 @@ class SemanticCommandTest {
assertEquals("", errors.toString());
}
+ @Test
+ void listsExpertsUsingTheStructuredDefaultSelectionCode() throws Exception {
+ JsonObject response = json("{\"evaluations\":[],\"experts\":[]}");
+ response.put("defaultErrorCode", "no_unique_expert");
+ response.put("defaultError", "Automatic expert selection is unavailable");
+ assertEquals(0, respond(new CamelSemantic(main()), r -> response));
+ assertTrue(printer.getOutput().contains("Default expert: none"));
+ assertFalse(printer.getOutput().contains("Default expert error:"), printer.getOutput());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "default_expert_error", "future_error_code" })
+ void structuredDefaultErrorsTakePrecedenceOverLegacyMessageMatching(String code) throws Exception {
+ JsonObject response = json("{\"evaluations\":[],\"experts\":[]}");
+ String message = "Semantic language requires exactly one eligible expert";
+ response.put("defaultErrorCode", code);
+ response.put("defaultError", message);
+ assertEquals(0, respond(new CamelSemantic(main()), r -> response));
+ assertTrue(printer.getOutput().contains("Default expert error: " + message), printer.getOutput());
+ }
+
@ParameterizedTest
@ValueSource(strings = {
"Unknown expert or bean does not implement SemanticAdapter: missing",
@@ -130,6 +151,7 @@ class SemanticCommandTest {
@Test
void emitsMetadataAsOneJsonDocumentIncludingEmptyLists() throws Exception {
JsonObject response = json("{\"evaluations\":[],\"experts\":[]}");
+ response.put("defaultErrorCode", "default_expert_error");
response.put("defaultError", "Unknown expert or bean does not implement SemanticAdapter: missing");
assertEquals(0, respond(new CamelSemantic(main()), r -> response, "--json"));
assertEquals(response, json(printer.getOutput()));