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 <[email protected]> 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 <[email protected]> --- .../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()));
