You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "luigidemasi (via GitHub)" <gi...@apache.org> on 2026/10/07 12:04:56 UTC
[PR] CAMEL-25382: Introduce expert-owned semantic evaluation contracts [camel]
luigidemasi opened a new pull request, #27494:
URL: https://github.com/apache/camel/pull/27494
# Description
The semantic language currently assumes instruction-driven boolean, choice and score evaluations. This change lets each expert declare a static contract for its operations, accepted input shapes, parameters and result meaning. Camel validates that contract; the expert owns omitted defaults and applies decision policy exactly once.
- Add operation and parameter annotations, structured input, and classification results alongside boolean, choice and score results.
- Support named evaluations and nested typed parameters consistently in Java, YAML and XML, with validation before startup or publication of replacements.
- Add `${semantic('name')}` to Simple, preserving typed values and requiring a boolean operation when used as a bare predicate.
- Migrate TypeSafe AI to the contract, including its threshold and uncertainty policy, and update documentation, generated schemas and catalog metadata.
Validation on the rebased commit:
- Full root `mvnd -B -ntp -Dmaven.build.cache.enabled=false clean install -DskipTests`: 699 modules passed; no generated-file drift.
- `camel-semantic`: 237 tests; `camel-typesafe-ai`: 198 tests.
- YAML DSL: 463 tests; YAML validator: 222 tests.
- Focused Simple regression suite: 61 tests. All 1,181 tests passed with no failures, errors or skips.
- `-Psourcecheck` validation passed for the eight affected runtime, DSL and catalog modules.
Regression coverage includes boolean-only Simple predicates, TypeSafe uncertainty-band boundaries and policies, criteria collisions, structured inputs, declaration replacement and YAML reload, parameter placeholders, and schema constraints.
# Target
- [x] The change targets `main` (Camel 4).
# Tracking
- [x] Tracked by [CAMEL-25382](https://issues.apache.org/jira/browse/CAMEL-25382).
# Apache Camel coding standards and style
- [x] The single signed commit has a meaningful subject and body.
- [x] Ran the full root `clean install -DskipTests` build using `mvnd`, and all generated changes are committed.
# AI-assisted contributions
- [x] The commit includes AI co-authorship attribution and a Signed-off-by trailer. This contribution was prepared with Codex on behalf of @luigidemasi.
Generated by Codex via /oss-fix-issue
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4217010355
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -69,127 +79,150 @@ public void preParse(YamlDeserializationContext dc, Node root) {
if (!(root instanceof SequenceNode sequence)) {
return;
}
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (Node node : sequence.getValue()) {
if (!(node instanceof MappingNode mapping)) {
- // Leave malformed entries to the route loader without replacing the resource's questions.
+ // Leave malformed entries to the route loader without replacing the resource's evaluations.
return;
}
for (NodeTuple tuple : mapping.getValue()) {
if ("semantic".equals(asText(tuple.getKeyNode()))) {
- read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> {
- if (definitions.putIfAbsent(name, question) != null) {
+ read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, evaluation) -> {
+ if (definitions.putIfAbsent(name, evaluation) != null) {
throw new YamlDeserializationException(
- tuple.getValueNode(), "Duplicate semantic question: " + name);
+ tuple.getValueNode(), "Duplicate semantic evaluation: " + name);
}
});
}
}
}
CamelContext context = dc.getCamelContext();
- SemanticQuestions questions = definitions.isEmpty()
- ? context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
- : SemanticQuestions.get(context);
- if (questions != null) {
+ SemanticEvaluations evaluations = definitions.isEmpty()
+ ? context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)
+ : SemanticEvaluations.get(context);
+ if (evaluations != null) {
try {
- questions.replace(dc.getResource(), definitions);
+ evaluations.replace(dc.getResource(), definitions);
} catch (IllegalArgumentException e) {
throw new YamlDeserializationException(root, e.getMessage(), e);
}
}
}
- private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
+ private static Map<String, SemanticEvaluation> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
- if (!semantic.keySet().equals(Set.of("question"))) {
- throw new YamlDeserializationException(node, "Semantic declaration requires only question");
+ for (String field : semantic.keySet()) {
+ if (!Set.of("evaluation", "expert", "state").contains(field)) {
+ throw new YamlDeserializationException(
+ semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
+ }
}
- Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get("question"), "semantic questions").forEach((name, definition) -> {
+ if (!semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires evaluation");
+ }
+ Map<String, SemanticEvaluation> result = new LinkedHashMap<>();
+ fields(semantic.get("evaluation"), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
- throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
+ throw new YamlDeserializationException(definition, "Semantic evaluation requires a nonblank name");
+ }
+ Map<String, Node> values = fields(definition, "semantic evaluation '" + name + "'");
+ for (String common : List.of("expert", "state")) {
+ if (!values.containsKey(common) && semantic.containsKey(common)) {
+ values.put(common, semantic.get(common));
+ }
}
- Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
- String expert = values.containsKey("expert") ? asText(values.get("expert")) : "default/automatic";
+ String expert = "default/automatic";
try {
- result.put(name, readQuestion(context, name, expert, definition, values));
+ if (values.containsKey("expert")) {
+ expert = "invalid expert reference";
+ expert = asText(values.get("expert"));
+ }
+ result.put(name, readEvaluation(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
- definition, "Invalid semantic question '" + name + "': " + e.getMessage()
+ definition, "Invalid semantic evaluation '" + name + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
});
return result;
}
- private static SemanticQuestion readQuestion(
+ private static SemanticEvaluation readEvaluation(
CamelContext context, String name, String expert, Node definition, Map<String, Node> values) {
- String description = "semantic question '" + name + "'";
+ String description = "semantic evaluation '" + name + "'";
String expertContext = " (expert '" + expert + "')";
values.forEach((field, value) -> {
if (!FIELDS.contains(field)) {
throw new YamlDeserializationException(
value, "Unknown property '" + field + "' in " + description + expertContext);
}
});
- if (!values.containsKey("type")) {
- throw new YamlDeserializationException(definition, "Semantic question type is required: " + name + expertContext);
+ if (values.containsKey("type") == values.containsKey("operation")) {
+ throw new YamlDeserializationException(
+ definition, "Specify exactly one operation or type: " + name + expertContext);
}
- SemanticQuestion.Type type = enumeration(values.get("type"), name, expert, "type", SemanticQuestion.Type.class);
- Map<String, String> criteria = new LinkedHashMap<>();
- List<String> levels = List.of();
- if (values.containsKey("criteria")) {
- if (type == SemanticQuestion.Type.SCORE) {
- levels = asSequenceNode(values.get("criteria")).getValue().stream().map(YamlDeserializerSupport::asText)
- .toList();
- } else {
- fields(values.get("criteria"), "criteria for " + description + expertContext)
- .forEach((key, value) -> criteria.put(key, asText(value)));
- }
+ String operation = values.containsKey("operation")
+ ? asText(values.get("operation"))
+ : asText(values.get("type")).toLowerCase(Locale.ROOT);
+ SemanticEvaluationBuilder builder = new SemanticEvaluationBuilder().operation(operation)
+ .expert(asText(values.get("expert"))).state(asText(values.get("state")));
+ if (values.containsKey("parameters")) {
+ fields(values.get("parameters"), "evaluation parameters")
+ .forEach((key, value) -> builder.parameter(key, value(context, value)));
}
- if (type != SemanticQuestion.Type.BOOLEAN && (values.containsKey("threshold") || values.containsKey("uncertainty")
- || values.containsKey("uncertaintyPolicy"))) {
- throw new YamlDeserializationException(
- definition, "Threshold and uncertainty policy require a boolean question: " + name + expertContext);
+ for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
+ Node node = values.get(field);
+ if (node == null) {
+ continue;
+ }
+ switch (field) {
+ case "instructions" -> builder.instructions(asText(node));
+ case "threshold", "uncertainty" -> {
+ try {
+ builder.parameter(field, Double.valueOf(context.resolvePropertyPlaceholders(asText(node))));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(
+ node,
+ "Invalid numeric value for '" + field + "' in " + description + expertContext);
+ }
+ }
+ case "uncertaintyPolicy" -> builder.uncertaintyPolicy(asText(node));
+ case "criteria" -> {
+ if (node instanceof MappingNode) {
+ Map<String, String> criteria = new LinkedHashMap<>();
+ fields(node, "criteria").forEach((key, child) -> criteria.put(key, asText(child)));
+ builder.parameter(field, criteria);
+ } else if (node instanceof SequenceNode sequence) {
+ builder.parameter(field,
+ sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
+ } else {
+ builder.parameter(field, value(context, node));
+ }
+ }
+ default -> throw new IllegalStateException(field);
+ }
}
- SemanticQuestion.UncertaintyPolicy policy = values.containsKey("uncertaintyPolicy")
- ? enumeration(values.get("uncertaintyPolicy"), name, expert, "uncertaintyPolicy",
- SemanticQuestion.UncertaintyPolicy.class)
- : SemanticQuestion.UncertaintyPolicy.FAIL;
- return new SemanticQuestion(
- type, asText(values.get("instructions")), asText(values.get("state")),
- criteria, levels, number(context, values, name, expert, "threshold", 0.5),
- number(context, values, name, expert, "uncertainty", 0), policy, asText(values.get("expert")));
+ return builder.build(context);
}
- private static <T extends Enum<T>> T enumeration(Node node, String question, String expert, String field, Class<T> type) {
- try {
- return asEnum(node, type);
- } catch (InvalidEnumException e) {
- throw new YamlDeserializationException(
- node, "Invalid value for '" + field + "' in semantic question '" + question + "': " + asText(node)
- + " (expert '" + expert + "')",
- e);
+ private static Object value(CamelContext context, Node node) {
+ if (node instanceof MappingNode) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(context, child)));
+ return result;
}
- }
-
- private static double number(
- CamelContext context, Map<String, Node> values, String question, String expert, String name, double fallback) {
- if (!values.containsKey(name)) {
- return fallback;
+ if (node instanceof SequenceNode sequence) {
+ return sequence.getValue().stream().map(child -> value(context, child)).toList();
}
- Node node = values.get(name);
- String raw = asText(node);
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(raw));
- } catch (NumberFormatException e) {
- throw new YamlDeserializationException(
- node,
- "Invalid numeric value for '" + name + "' in semantic question '" + question + "': " + raw
- + " (expert '" + expert + "')",
- e);
+ if (Tag.FLOAT.equals(node.getTag()) || NUMBER.equals(node.getTag())) {
+ try {
+ return new BigDecimal(context.resolvePropertyPlaceholders(asText(node)));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(node, "Invalid numeric parameter");
+ }
}
+ return new StandardConstructor(LoadSettings.builder().build()).constructSingleDocument(Optional.of(node));
Review Comment:
Resolving with the explanation above: the pinned SnakeYAML Engine 3.1.1 constructor owns mutable construction maps, sets and deferred-fill lists, so a shared static instance is unsafe across concurrent loaders. Construction remains local to declaration parsing; it is not on the message-evaluation path. The suggested singleton is intentionally not applied.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
+public final class SemanticEvaluation {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
+ private final String operation;
+ private final String expert;
+ private final String state;
+ private final Map<String, Object> parameters;
+
+ public SemanticEvaluation(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+
+ static Map<String, Object> immutableMap(Map<String, ?> values) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ values.forEach((name, value) -> {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Parameter names must not be blank");
+ }
+ copy.put(name, immutableValue(value));
+ });
+ return Collections.unmodifiableMap(copy);
+ }
+
+ private static Object immutableValue(Object value) {
+ if (value instanceof Map<?, ?> map) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ map.forEach((key, entry) -> {
+ if (!(key instanceof String name)) {
+ throw new IllegalArgumentException("Parameter maps require string keys");
+ }
+ copy.put(name, entry);
+ });
+ return immutableMap(copy);
Review Comment:
Confirmed fixed in [d338ec439c24](https://github.com/apache/camel/commit/d338ec439c247a869f0f54b88e8dab71e06f9389). Nested maps validate keys and recursively copy values in one traversal, then return an unmodifiable map. The intermediate map and second traversal are gone, while immutability and invalid-key regression coverage remain.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java:
##########
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.util.StringHelper;
+
+/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
+public final class SemanticEvaluationBuilder {
+ private final SemanticEvaluationsBuilder parent;
+ private final Map<String, Object> parameters = new LinkedHashMap<>();
+ private final Map<String, String> criteria = new LinkedHashMap<>();
+ private final List<String> levels = new ArrayList<>();
+ private String threshold;
+ private String uncertainty;
+ private boolean normalizeUncertaintyPolicy;
+ private String operation;
+ private String expert;
+ private String state;
+
+ /** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
+ public SemanticEvaluationBuilder() {
+ this(null);
+ }
+
+ SemanticEvaluationBuilder(SemanticEvaluationsBuilder parent) {
+ this.parent = parent;
+ }
+
+ public SemanticEvaluationBuilder operation(String operation) {
+ this.operation = operation;
+ return this;
+ }
+
+ /** Select an instruction-driven operation by type, such as boolean, choice or score. */
+ public SemanticEvaluationBuilder type(String type) {
+ return operation(type.toLowerCase(Locale.ROOT));
+ }
+
+ public SemanticEvaluationBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ String getExpert() {
+ return expert != null ? expert : parent != null ? parent.getExpert() : null;
+ }
+
+ public SemanticEvaluationBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameter(String name, Object value) {
+ if (parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
+ }
+ parameters.put(name, value);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameters(Map<String, ?> values) {
+ values.forEach(this::parameter);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder instructions(String instructions) {
+ return parameter("instructions", instructions);
+ }
+
+ public SemanticEvaluationBuilder criterion(String name, String description) {
+ if (criteria.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
+ }
+ criteria.put(name, description);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder level(String level) {
+ levels.add(level);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder threshold(double threshold) {
+ return parameter("threshold", threshold);
+ }
+
+ public SemanticEvaluationBuilder threshold(String threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertainty(double uncertainty) {
+ return parameter("uncertainty", uncertainty);
+ }
+
+ public SemanticEvaluationBuilder uncertainty(String uncertainty) {
+ this.uncertainty = uncertainty;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
+ return this;
+ }
+
+ public SemanticEvaluationsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
+ return parent;
+ }
+
+ public void register() {
+ end().register();
+ }
+
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticEvaluation build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
+ }
+ values.replaceAll((name, value) -> resolve(context, value));
+ if (normalizeUncertaintyPolicy && values.get("uncertaintyPolicy") instanceof String policy) {
+ values.put("uncertaintyPolicy",
+ StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
+ }
+ return new SemanticEvaluation(
+ operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
+ }
+
+ private static Object resolve(CamelContext context, Object value) {
+ if (value instanceof String text) {
+ return context.resolvePropertyPlaceholders(text);
+ }
+ if (value instanceof Map<?, ?> map) {
+ Map<Object, Object> resolved = new LinkedHashMap<>();
Review Comment:
Resolving with the existing checked-validation path. `resolve()` also accepts arbitrary Java builder maps; casting their keys to `String` here can throw `ClassCastException`. The intermediate `Map<Object, Object>` lets the common immutable declaration reject invalid keys with its intended `IllegalArgumentException` and evaluation context. `nonStringNestedKeysFailWithValidationError()` covers this case.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,38 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return null;
Review Comment:
The test deliberately covers cached lookup independence, while simultaneous first creation still uses the private lock; it does not claim that first creation is lock-free. Concurrent creation for one context has its own test. The null-supplier intent is documented in [384c0e7f7de1](https://github.com/apache/camel/commit/384c0e7f7de14a473b201a4ef05f6d8ee0a0423d), and [96e6d465f016](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d) adds a descriptive assertion: after the gated lookup leaves the plugin absent, `get()` creates the registry. Resolving with this clarified scope.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4209808223
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java:
##########
@@ -223,6 +223,7 @@ static boolean isOfTheApplication(String error) {
static boolean isMissingDependency(String error) {
return error.startsWith("No language could be found for:")
+ || error.equals("The semantic function requires camel-semantic on the classpath")
Review Comment:
⚠️ **Fragile string coupling:** `equals()` ties this check to the exact upstream wording of the error message. Any phrasing change in the error producer silently breaks the dependency detection.
Consider `contains("requires camel-semantic")` (the key phrase is already used in tests on line 9451 of the diff) or extract the literal as a shared constant near its production site in `ExpressionSupport`.
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,38 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return null;
Review Comment:
⚠️ **Lazy supplier returns `null` but test asserts `isNotNull()`:** The supplier registered at line 86 intentionally returns `null` (simulating a slow creator), yet line 103 asserts `creation.get(...).isNotNull()`. If `SemanticEvaluations.get()` propagates the supplier's return value as-is, this test will fail. If it guards against `null` and substitutes a default, the assertion is correct but the supplier's `null` return is misleading.
Either the supplier should return a real stub value, or the assertion should expect `null`. As written, the test exposes an implicit assumption about how `lazyAddContextPlugin` handles a `null` return from its initializer.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java:
##########
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.util.StringHelper;
+
+/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
+public final class SemanticEvaluationBuilder {
+ private final SemanticEvaluationsBuilder parent;
+ private final Map<String, Object> parameters = new LinkedHashMap<>();
+ private final Map<String, String> criteria = new LinkedHashMap<>();
+ private final List<String> levels = new ArrayList<>();
+ private String threshold;
+ private String uncertainty;
+ private boolean normalizeUncertaintyPolicy;
+ private String operation;
+ private String expert;
+ private String state;
+
+ /** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
+ public SemanticEvaluationBuilder() {
+ this(null);
+ }
+
+ SemanticEvaluationBuilder(SemanticEvaluationsBuilder parent) {
+ this.parent = parent;
+ }
+
+ public SemanticEvaluationBuilder operation(String operation) {
+ this.operation = operation;
+ return this;
+ }
+
+ /** Select an instruction-driven operation by type, such as boolean, choice or score. */
+ public SemanticEvaluationBuilder type(String type) {
+ return operation(type.toLowerCase(Locale.ROOT));
+ }
+
+ public SemanticEvaluationBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ String getExpert() {
+ return expert != null ? expert : parent != null ? parent.getExpert() : null;
+ }
+
+ public SemanticEvaluationBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameter(String name, Object value) {
+ if (parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
+ }
+ parameters.put(name, value);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameters(Map<String, ?> values) {
+ values.forEach(this::parameter);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder instructions(String instructions) {
+ return parameter("instructions", instructions);
+ }
+
+ public SemanticEvaluationBuilder criterion(String name, String description) {
+ if (criteria.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
+ }
+ criteria.put(name, description);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder level(String level) {
+ levels.add(level);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder threshold(double threshold) {
+ return parameter("threshold", threshold);
+ }
+
+ public SemanticEvaluationBuilder threshold(String threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertainty(double uncertainty) {
+ return parameter("uncertainty", uncertainty);
+ }
+
+ public SemanticEvaluationBuilder uncertainty(String uncertainty) {
+ this.uncertainty = uncertainty;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
+ return this;
+ }
+
+ public SemanticEvaluationsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
+ return parent;
+ }
+
+ public void register() {
+ end().register();
+ }
+
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticEvaluation build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
+ }
+ values.replaceAll((name, value) -> resolve(context, value));
+ if (normalizeUncertaintyPolicy && values.get("uncertaintyPolicy") instanceof String policy) {
+ values.put("uncertaintyPolicy",
+ StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
+ }
+ return new SemanticEvaluation(
+ operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
+ }
+
+ private static Object resolve(CamelContext context, Object value) {
+ if (value instanceof String text) {
+ return context.resolvePropertyPlaceholders(text);
+ }
+ if (value instanceof Map<?, ?> map) {
+ Map<Object, Object> resolved = new LinkedHashMap<>();
Review Comment:
🔍 **`Map<Object, Object>` should be `Map<String, Object>`:** The parameter map fed into `SemanticEvaluation` is keyed by parameter names, which are always `String`. Using `Map<Object, Object>` here loses that type safety and forces callers and internal code to cast unnecessarily.
Since `map` is already typed `Map<?, ?>` from the wildcard capture, just narrow the loop variable and the result type:
```suggestion
Map<String, Object> resolved = new LinkedHashMap<>();
map.forEach((key, item) -> resolved.put((String) key, resolve(context, item)));
```
The explicit cast will surface non-String keys at runtime rather than silently accepting them.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java:
##########
@@ -138,221 +86,309 @@ public String getArtifactId() {
return artifactId;
}
- public Set<InputType> getInputTypes() {
- return inputTypes;
- }
-
- public Set<ResultType> getResultTypes() {
- return resultTypes;
+ public Map<String, Operation> getOperations() {
+ return operations;
}
- public Instructions getInstructions() {
- return instructions;
+ public Operation operation(String name) {
+ Operation result = operations.get(name);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unknown operation '" + name + "'; supported operations: " + operations.keySet());
+ }
+ return result;
}
- public boolean isCallerDefinedCriteria() {
- return callerDefinedCriteria;
+ public void validate(SemanticEvaluation evaluation) {
+ operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
- public boolean isBooleanProbability() {
- return booleanProbability;
+ private static String required(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return value;
}
- public boolean isChoiceProbabilities() {
- return choiceProbabilities;
+ private static boolean withinBounds(Number number, double minimum, double maximum) {
+ if (minimum == Double.POSITIVE_INFINITY || maximum == Double.NEGATIVE_INFINITY) {
+ return false;
+ }
+ try {
+ // Preserve decimal precision, including values just outside a declared boundary.
+ BigDecimal value = new BigDecimal(number.toString());
+ return (minimum == Double.NEGATIVE_INFINITY || value.compareTo(BigDecimal.valueOf(minimum)) >= 0)
+ && (maximum == Double.POSITIVE_INFINITY || value.compareTo(BigDecimal.valueOf(maximum)) <= 0);
+ } catch (NumberFormatException invalid) {
+ return false;
+ }
}
- public Set<ResultType> getConfidenceTypes() {
- return confidenceTypes;
- }
+ /** Operation metadata and common validation, independent of any expert instance. */
+ public static final class Operation {
+ private final SemanticOperation declaration;
+ private final Set<InputType> inputTypes;
+ private final Set<String> labels;
+ private final Map<String, Parameter> parameters;
+
+ private Operation(SemanticOperation declaration) {
+ this.declaration = declaration;
+ required(declaration.name(), "Operation name");
+ required(declaration.description(), "Operation description");
+ required(declaration.inputRequirements(), "Operation input requirements");
+ required(declaration.resultMeaning(), "Operation result meaning");
+ inputTypes = Set.copyOf(List.of(declaration.inputTypes()));
+ labels = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(declaration.labels())));
+ if (inputTypes.isEmpty() || Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum() || labels.stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Invalid operation contract: " + declaration.name());
+ }
+ if (declaration.probability() || declaration.probabilities()) {
+ required(declaration.probabilityMeaning(), "Probability meaning");
+ }
+ if (declaration.confidence()) {
+ required(declaration.confidenceMeaning(), "Confidence meaning");
+ }
+ Map<String, Parameter> values = new LinkedHashMap<>();
+ for (SemanticParameter parameter : declaration.parameters()) {
+ Parameter contract = new Parameter(parameter);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate parameter: " + contract.getName());
+ }
+ }
+ parameters = Collections.unmodifiableMap(values);
+ }
- public String getProbabilityMeaning() {
- return probabilityMeaning;
- }
+ public String getName() {
+ return declaration.name();
+ }
- public String getConfidenceMeaning() {
- return confidenceMeaning;
- }
+ public String getDescription() {
+ return declaration.description();
+ }
- public String getTrueMeaning() {
- return trueMeaning;
- }
+ public Set<InputType> getInputTypes() {
+ return inputTypes;
+ }
- public int getMaxChoices() {
- return maxChoices;
- }
+ public String getInputRequirements() {
+ return declaration.inputRequirements();
+ }
- public int getMaxScoreLevels() {
- return maxScoreLevels;
- }
+ public Map<String, Parameter> getParameters() {
+ return parameters;
+ }
- /** Validate the common contract before the provider checks any model-specific restrictions. */
- public void validate(SemanticQuestion question) {
- if ((!known || instructions == Instructions.REQUIRED) && question.getInstructions() == null) {
- throw new IllegalArgumentException("Question instructions are required");
+ public ResultType getResultType() {
+ return declaration.resultType();
}
- if (!known) {
- return;
+
+ public String getResultMeaning() {
+ return declaration.resultMeaning();
}
- if (!resultTypes.contains(ResultType.valueOf(question.getType().name()))) {
- throw new IllegalArgumentException(
- "Requires " + question.getType() + "; supports " + resultTypes + ": " + description);
+
+ public Set<String> getLabels() {
+ return labels;
}
- if (instructions == Instructions.UNSUPPORTED && question.getInstructions() != null) {
- throw new IllegalArgumentException("Instructions are unsupported by this fixed expert");
+
+ public double getMinimum() {
+ return declaration.minimum();
}
- if (!callerDefinedCriteria && (!question.getCriteria().isEmpty() || !question.getLevels().isEmpty())) {
- throw new IllegalArgumentException("Caller-defined criteria and score levels are unsupported by this fixed expert");
+
+ public double getMaximum() {
+ return declaration.maximum();
}
- if (question.getType() == SemanticQuestion.Type.CHOICE && maxChoices > 0
- && question.getCriteria().size() > maxChoices) {
- throw new IllegalArgumentException("Supports at most " + maxChoices + " choice criteria");
+
+ public boolean isProbability() {
+ return declaration.probability();
}
- if (question.getType() == SemanticQuestion.Type.SCORE && maxScoreLevels > 0
- && question.getLevels().size() > maxScoreLevels) {
- throw new IllegalArgumentException("Supports at most " + maxScoreLevels + " score levels");
+
+ public boolean isProbabilities() {
+ return declaration.probabilities();
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN && !booleanProbability
- && (question.getThreshold() != 0.5 || question.getUncertainty() != 0
- || question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL)) {
- throw new IllegalArgumentException("Boolean decision policy requires positive-class probability support");
+
+ public String getProbabilityMeaning() {
+ return declaration.probabilityMeaning();
}
- }
- /** Input shape can only be checked once the state selector has been evaluated. */
- public void validateInput(Object state) {
- if (known && !(state instanceof String && inputTypes.contains(InputType.TEXT)
- || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
- throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ public boolean isConfidence() {
+ return declaration.confidence();
}
- }
- /** Named construction of an immutable, explicitly supported contract. */
- public static final class Builder {
- private String name;
- private String description;
- private String provider;
- private String artifactId;
- private Set<InputType> inputTypes = Set.of();
- private Set<ResultType> resultTypes = Set.of();
- private Instructions instructions = Instructions.REQUIRED;
- private boolean callerDefinedCriteria;
- private boolean booleanProbability;
- private boolean choiceProbabilities;
- private Set<ResultType> confidenceTypes = Set.of();
- private String probabilityMeaning;
- private String confidenceMeaning;
- private String trueMeaning;
- private int maxChoices;
- private int maxScoreLevels;
+ public String getConfidenceMeaning() {
+ return declaration.confidenceMeaning();
+ }
- private Builder() {
+ public void validate(Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ if (!parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
+ }
+ }
+ parameters.forEach((name, parameter) -> parameter.validate(values.get(name), values.containsKey(name)));
Review Comment:
🔍 **Map key blankness not validated:** `validate()` checks that all provided keys are recognised parameters (line 223-226), but it never checks that the keys themselves are non-blank. A caller passing `{"": value}` would bypass the unknown-parameter guard because `parameters.containsKey("")` is false only if `""` isn't a registered name — the error message would then be misleading.
Consider adding a blank-key guard before the containsKey check:
```suggestion
for (String name : values.keySet()) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Parameter name must not be blank for operation '" + getName() + "'");
}
if (!parameters.containsKey(name)) {
throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
}
}
```
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -69,127 +79,150 @@ public void preParse(YamlDeserializationContext dc, Node root) {
if (!(root instanceof SequenceNode sequence)) {
return;
}
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (Node node : sequence.getValue()) {
if (!(node instanceof MappingNode mapping)) {
- // Leave malformed entries to the route loader without replacing the resource's questions.
+ // Leave malformed entries to the route loader without replacing the resource's evaluations.
return;
}
for (NodeTuple tuple : mapping.getValue()) {
if ("semantic".equals(asText(tuple.getKeyNode()))) {
- read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> {
- if (definitions.putIfAbsent(name, question) != null) {
+ read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, evaluation) -> {
+ if (definitions.putIfAbsent(name, evaluation) != null) {
throw new YamlDeserializationException(
- tuple.getValueNode(), "Duplicate semantic question: " + name);
+ tuple.getValueNode(), "Duplicate semantic evaluation: " + name);
}
});
}
}
}
CamelContext context = dc.getCamelContext();
- SemanticQuestions questions = definitions.isEmpty()
- ? context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
- : SemanticQuestions.get(context);
- if (questions != null) {
+ SemanticEvaluations evaluations = definitions.isEmpty()
+ ? context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)
+ : SemanticEvaluations.get(context);
+ if (evaluations != null) {
try {
- questions.replace(dc.getResource(), definitions);
+ evaluations.replace(dc.getResource(), definitions);
} catch (IllegalArgumentException e) {
throw new YamlDeserializationException(root, e.getMessage(), e);
}
}
}
- private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
+ private static Map<String, SemanticEvaluation> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
- if (!semantic.keySet().equals(Set.of("question"))) {
- throw new YamlDeserializationException(node, "Semantic declaration requires only question");
+ for (String field : semantic.keySet()) {
+ if (!Set.of("evaluation", "expert", "state").contains(field)) {
+ throw new YamlDeserializationException(
+ semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
+ }
}
- Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get("question"), "semantic questions").forEach((name, definition) -> {
+ if (!semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires evaluation");
+ }
+ Map<String, SemanticEvaluation> result = new LinkedHashMap<>();
+ fields(semantic.get("evaluation"), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
- throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
+ throw new YamlDeserializationException(definition, "Semantic evaluation requires a nonblank name");
+ }
+ Map<String, Node> values = fields(definition, "semantic evaluation '" + name + "'");
+ for (String common : List.of("expert", "state")) {
+ if (!values.containsKey(common) && semantic.containsKey(common)) {
+ values.put(common, semantic.get(common));
+ }
}
- Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
- String expert = values.containsKey("expert") ? asText(values.get("expert")) : "default/automatic";
+ String expert = "default/automatic";
try {
- result.put(name, readQuestion(context, name, expert, definition, values));
+ if (values.containsKey("expert")) {
+ expert = "invalid expert reference";
+ expert = asText(values.get("expert"));
+ }
+ result.put(name, readEvaluation(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
- definition, "Invalid semantic question '" + name + "': " + e.getMessage()
+ definition, "Invalid semantic evaluation '" + name + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
});
return result;
}
- private static SemanticQuestion readQuestion(
+ private static SemanticEvaluation readEvaluation(
CamelContext context, String name, String expert, Node definition, Map<String, Node> values) {
- String description = "semantic question '" + name + "'";
+ String description = "semantic evaluation '" + name + "'";
String expertContext = " (expert '" + expert + "')";
values.forEach((field, value) -> {
if (!FIELDS.contains(field)) {
throw new YamlDeserializationException(
value, "Unknown property '" + field + "' in " + description + expertContext);
}
});
- if (!values.containsKey("type")) {
- throw new YamlDeserializationException(definition, "Semantic question type is required: " + name + expertContext);
+ if (values.containsKey("type") == values.containsKey("operation")) {
+ throw new YamlDeserializationException(
+ definition, "Specify exactly one operation or type: " + name + expertContext);
}
- SemanticQuestion.Type type = enumeration(values.get("type"), name, expert, "type", SemanticQuestion.Type.class);
- Map<String, String> criteria = new LinkedHashMap<>();
- List<String> levels = List.of();
- if (values.containsKey("criteria")) {
- if (type == SemanticQuestion.Type.SCORE) {
- levels = asSequenceNode(values.get("criteria")).getValue().stream().map(YamlDeserializerSupport::asText)
- .toList();
- } else {
- fields(values.get("criteria"), "criteria for " + description + expertContext)
- .forEach((key, value) -> criteria.put(key, asText(value)));
- }
+ String operation = values.containsKey("operation")
+ ? asText(values.get("operation"))
+ : asText(values.get("type")).toLowerCase(Locale.ROOT);
+ SemanticEvaluationBuilder builder = new SemanticEvaluationBuilder().operation(operation)
+ .expert(asText(values.get("expert"))).state(asText(values.get("state")));
+ if (values.containsKey("parameters")) {
+ fields(values.get("parameters"), "evaluation parameters")
+ .forEach((key, value) -> builder.parameter(key, value(context, value)));
}
- if (type != SemanticQuestion.Type.BOOLEAN && (values.containsKey("threshold") || values.containsKey("uncertainty")
- || values.containsKey("uncertaintyPolicy"))) {
- throw new YamlDeserializationException(
- definition, "Threshold and uncertainty policy require a boolean question: " + name + expertContext);
+ for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
+ Node node = values.get(field);
+ if (node == null) {
+ continue;
+ }
+ switch (field) {
+ case "instructions" -> builder.instructions(asText(node));
+ case "threshold", "uncertainty" -> {
+ try {
+ builder.parameter(field, Double.valueOf(context.resolvePropertyPlaceholders(asText(node))));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(
+ node,
+ "Invalid numeric value for '" + field + "' in " + description + expertContext);
+ }
+ }
+ case "uncertaintyPolicy" -> builder.uncertaintyPolicy(asText(node));
+ case "criteria" -> {
+ if (node instanceof MappingNode) {
+ Map<String, String> criteria = new LinkedHashMap<>();
+ fields(node, "criteria").forEach((key, child) -> criteria.put(key, asText(child)));
+ builder.parameter(field, criteria);
+ } else if (node instanceof SequenceNode sequence) {
+ builder.parameter(field,
+ sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
+ } else {
+ builder.parameter(field, value(context, node));
+ }
+ }
+ default -> throw new IllegalStateException(field);
+ }
}
- SemanticQuestion.UncertaintyPolicy policy = values.containsKey("uncertaintyPolicy")
- ? enumeration(values.get("uncertaintyPolicy"), name, expert, "uncertaintyPolicy",
- SemanticQuestion.UncertaintyPolicy.class)
- : SemanticQuestion.UncertaintyPolicy.FAIL;
- return new SemanticQuestion(
- type, asText(values.get("instructions")), asText(values.get("state")),
- criteria, levels, number(context, values, name, expert, "threshold", 0.5),
- number(context, values, name, expert, "uncertainty", 0), policy, asText(values.get("expert")));
+ return builder.build(context);
}
- private static <T extends Enum<T>> T enumeration(Node node, String question, String expert, String field, Class<T> type) {
- try {
- return asEnum(node, type);
- } catch (InvalidEnumException e) {
- throw new YamlDeserializationException(
- node, "Invalid value for '" + field + "' in semantic question '" + question + "': " + asText(node)
- + " (expert '" + expert + "')",
- e);
+ private static Object value(CamelContext context, Node node) {
+ if (node instanceof MappingNode) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(context, child)));
+ return result;
}
- }
-
- private static double number(
- CamelContext context, Map<String, Node> values, String question, String expert, String name, double fallback) {
- if (!values.containsKey(name)) {
- return fallback;
+ if (node instanceof SequenceNode sequence) {
+ return sequence.getValue().stream().map(child -> value(context, child)).toList();
}
- Node node = values.get(name);
- String raw = asText(node);
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(raw));
- } catch (NumberFormatException e) {
- throw new YamlDeserializationException(
- node,
- "Invalid numeric value for '" + name + "' in semantic question '" + question + "': " + raw
- + " (expert '" + expert + "')",
- e);
+ if (Tag.FLOAT.equals(node.getTag()) || NUMBER.equals(node.getTag())) {
+ try {
+ return new BigDecimal(context.resolvePropertyPlaceholders(asText(node)));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(node, "Invalid numeric parameter");
+ }
}
+ return new StandardConstructor(LoadSettings.builder().build()).constructSingleDocument(Optional.of(node));
Review Comment:
⚡ **`StandardConstructor` allocated per scalar parse:** `new StandardConstructor(LoadSettings.builder().build())` is constructed each time `constructSingleDocument` is called. `LoadSettings` is immutable and `StandardConstructor` is stateless once built — this is a per-call allocation that could easily be a private static constant.
```suggestion
private static final StandardConstructor STANDARD_CONSTRUCTOR =
new StandardConstructor(LoadSettings.builder().build());
```
Then replace the call site with `STANDARD_CONSTRUCTOR.constructSingleDocument(...)`.
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4210337790
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,38 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return null;
Review Comment:
Added an explanatory comment in [384c0e7f7de1](https://github.com/apache/camel/commit/384c0e7f7de14a473b201a4ef05f6d8ee0a0423d). The supplier intentionally leaves the plugin absent after releasing the lookup gate. `DefaultContextPluginManager` removes the null-producing lazy entry; `SemanticEvaluations.get()` then creates and registers a real registry, which is what the non-null assertion verifies.
Returning a stub would bypass that creation path, while asserting null would contradict `get()`'s behavior. The initialization tests pass with the existing assertion retained.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27494:
URL: https://github.com/apache/camel/pull/27494#issuecomment-6037534382
: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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on PR #27494:
URL: https://github.com/apache/camel/pull/27494#issuecomment-6054127830
Regarding the remaining points in [gnodet-bot's latest review](https://github.com/apache/camel/pull/27494#pullrequestreview-5446519672), each already has an individual reply:
- **Blank keys:** [verified validation paths](https://github.com/apache/camel/pull/27494#discussion_r4210336807). `SemanticEvaluation` rejects blank/null parameter names before contract validation; annotation metadata rejects blank names, and direct operation validation rejects unknown keys. The reported bypass is not reachable.
- **Shared YAML constructor:** [constructor state](https://github.com/apache/camel/pull/27494#discussion_r4210337165). The pinned SnakeYAML Engine constructor has mutable maps, sets and deferred-construction lists, so it is not stateless and is unsuitable as a shared static singleton.
- **Intermediate map key type:** [validation rationale](https://github.com/apache/camel/pull/27494#discussion_r4210337422). The builder temporarily accepts unvalidated key types so the declaration can reject them with the evaluation-specific `IllegalArgumentException`. An early `String` cast changes that into `ClassCastException`; it does not strengthen validation.
The new assertion-description suggestion is fixed and answered in its own thread. The earlier diagnostic matcher fix remains covered by its regression test.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4210388472
##########
components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java:
##########
@@ -94,36 +160,86 @@ public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> q
return results;
}
- private Map<String, Object> definition(SemanticQuestion question) {
+ private Map<String, Object> definition(SemanticEvaluation question) {
validate(question);
Map<String, Object> definition = new HashMap<>();
- definition.put("instructions", question.getInstructions());
+ definition.put("instructions", question.getParameters().get("instructions"));
definition.put("type", type(question));
- if (question.getType() == SemanticQuestion.Type.SCORE) {
- definition.put("criteria", question.getLevels());
- } else if (!question.getCriteria().isEmpty()) {
- definition.put("criteria", question.getCriteria());
+ if ("score".equals(question.getOperation())) {
+ definition.put("criteria", levels(question));
+ } else if (!criteria(question).isEmpty()) {
+ definition.put("criteria", criteria(question));
}
return definition;
}
- private String type(SemanticQuestion question) {
- return switch (question.getType()) {
- case BOOLEAN -> "noul";
- case CHOICE -> "choice";
- case SCORE -> "score";
+ private String type(SemanticEvaluation question) {
+ return switch (question.getOperation()) {
+ case "boolean" -> "noul";
+ case "choice" -> "choice";
+ case "score" -> "score";
+ default -> throw new IllegalArgumentException("Unsupported TypeSafe AI operation");
};
}
- private SemanticResult result(SemanticQuestion question, JsonObject answer, JsonObject response) {
+ private SemanticResult result(SemanticEvaluation question, JsonObject answer, JsonObject response) {
Map<String, Double> probabilities = new HashMap<>();
if (answer.get("probabilities") instanceof Map<?, ?> values) {
values.forEach((key, value) -> probabilities.put((String) key, ((Number) value).doubleValue()));
}
- return new SemanticResult(
- question.getType() == SemanticQuestion.Type.BOOLEAN ? null : answer.get(type(question)),
- question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null,
+ Double probability = "boolean".equals(question.getOperation()) ? answer.getDouble("noul") : null;
+ Object value = switch (question.getOperation()) {
+ case "boolean" -> booleanDecision(question, probability);
Review Comment:
Still open from my earlier review: when the response has no `noul`, `probability` is null and unboxing it into `booleanDecision(..., double)` throws an NPE with no message. Something like this would give a clear error:
```java
Double probability = null;
if ("boolean".equals(question.getOperation())) {
probability = answer.getDouble("noul");
if (probability == null) {
throw new IllegalArgumentException("TypeSafe AI boolean result is missing its probability");
}
}
```
##########
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java:
##########
@@ -0,0 +1,32 @@
+/*
+ * 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.language.simple;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+
+/**
+ * Opts a Simple function into using its predicate contract for negation, including any validation in
+ * {@link #matches(org.apache.camel.Exchange)}. Existing expressions retain value-based truthiness, even when they also
+ * implement {@link Predicate}.
+ */
+public interface SimplePredicateExpression extends Expression, Predicate {
Review Comment:
Nice, this opt-in keeps existing negation semantics intact. Nit: since this is a new public interface in core, could the javadoc get a `@since 4.23` tag?
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "apupier (via GitHub)" <gi...@apache.org>.
apupier commented on PR #27494:
URL: https://github.com/apache/camel/pull/27494#issuecomment-6059302391
I suppose that i tis this PR which introduced this test failure https://issues.apache.org/jira/browse/CAMEL-25464
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4215913878
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java:
##########
@@ -0,0 +1,177 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.spi.Resource;
+
+/** Context-local named evaluations, replaced atomically per source when a route resource is reloaded. */
+public final class SemanticEvaluations {
+ private static final Object CREATION_LOCK = new Object();
+ private final CamelContext context;
+ private final Map<String, Map<String, SemanticEvaluation>> sources = new HashMap<>();
+ private final Map<String, Resource> resources = new HashMap<>();
+ private volatile Map<String, SemanticEvaluation> evaluations = Map.of();
+ private final Map<Consumer<Map<String, SemanticEvaluation>>, List<String>> validators = new WeakHashMap<>();
+
+ private SemanticEvaluations(CamelContext context) {
+ this.context = context;
+ }
+
+ /** Validate every declaration, including evaluations not referenced by a route. */
+ public void validate() {
+ if (!evaluations.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(evaluations);
+ }
+ }
+
+ /**
+ * Validate current declarations and weakly track a callback owned by its initialized expression. The callback
+ * receives the complete immutable snapshot so nested references can be validated against the same replacement.
+ */
+ public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticEvaluation>> validator) {
+ // A replacement may have occurred between the expression's initial compilation and registration.
+ get(names);
+ validator.accept(evaluations);
Review Comment:
💡 **Suggestion:** The comment above the method explains *what* the snapshot does, but doesn't document *why* it's safe for `validator.accept(evaluations)` to see the pre-replacement snapshot here even though `setValidator` is `synchronized`. Consider a short inline note:
```suggestion
validator.accept(evaluations); // evaluations is volatile; reads the published snapshot atomically
```
The invariant being relied on (volatile read of `evaluations` is atomic and sees the last-published snapshot) is non-obvious when the method is read in isolation. A one-liner saves the next reader a reasoning trip through the field declaration.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -290,8 +296,57 @@ private Object discoverAdapter(CamelContext context) throws IOException {
return resolved;
}
- private ResolvedExpert expert(String name, SemanticQuestion question) {
- String reference = question.getExpert() != null ? question.getExpert() : defaultExpert;
+ /** Validate declarations, including unused evaluations, without starting managed expert resources. */
+ public void validateDeclarations(Map<String, SemanticEvaluation> declarations) {
+ validateDeclarations(declarations, null);
+ }
+
+ /** Validate changed declarations against the complete candidate snapshot without publishing it. */
+ public void validateDeclarations(
+ Map<String, SemanticEvaluation> declarations, Map<String, SemanticEvaluation> snapshot) {
+ validationOnly(snapshot, () -> {
+ declarations.forEach((name, evaluation) -> {
+ expert(name, evaluation);
+ String selector = evaluation.getState() != null ? evaluation.getState() : defaultState;
+ if (selector == null || selector.isBlank()) {
+ throw new IllegalArgumentException("Semantic evaluation '" + name + "': state must not be blank");
+ }
+ Expression state = getCamelContext().resolveLanguage("simple")
+ .createExpression(getCamelContext().resolvePropertyPlaceholders(selector));
+ state.init(getCamelContext());
+ });
+ return null;
+ });
+ }
+
+ private <T> T validationOnly(Map<String, SemanticEvaluation> snapshot, Supplier<T> action) {
+ Validation previous = validation.get();
+ validation.set(snapshot != null || previous == null ? new Validation(snapshot) : previous);
+ try {
Review Comment:
💡 **Suggestion:** The ternary `snapshot != null || previous == null ? new Validation(snapshot) : previous` encodes a non-trivial truth table. A brief comment would help readers follow the intent without re-deriving all four cases:
```suggestion
// Explicit snapshot overrides; re-entrant calls inherit the outer validation scope unless a new snapshot is provided.
validation.set(snapshot != null || previous == null ? new Validation(snapshot) : previous);
```
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4216164382
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java:
##########
@@ -0,0 +1,177 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.spi.Resource;
+
+/** Context-local named evaluations, replaced atomically per source when a route resource is reloaded. */
+public final class SemanticEvaluations {
+ private static final Object CREATION_LOCK = new Object();
+ private final CamelContext context;
+ private final Map<String, Map<String, SemanticEvaluation>> sources = new HashMap<>();
+ private final Map<String, Resource> resources = new HashMap<>();
+ private volatile Map<String, SemanticEvaluation> evaluations = Map.of();
+ private final Map<Consumer<Map<String, SemanticEvaluation>>, List<String>> validators = new WeakHashMap<>();
+
+ private SemanticEvaluations(CamelContext context) {
+ this.context = context;
+ }
+
+ /** Validate every declaration, including evaluations not referenced by a route. */
+ public void validate() {
+ if (!evaluations.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(evaluations);
+ }
+ }
+
+ /**
+ * Validate current declarations and weakly track a callback owned by its initialized expression. The callback
+ * receives the complete immutable snapshot so nested references can be validated against the same replacement.
+ */
+ public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticEvaluation>> validator) {
+ // A replacement may have occurred between the expression's initial compilation and registration.
+ get(names);
+ validator.accept(evaluations);
Review Comment:
Clarified in [this commit](https://github.com/apache/camel/commit/a1c6db3386a06c540ae89f41c3adcc286000de42). The comment explains that reentrant registration first checks published definitions because the replacement is not visible until it succeeds. `volatile` provides visibility, but does not by itself make replacement validation complete.
Checking this path also exposed a late-registration gap: a predicate created from an expert callback after the validator list was copied could miss an incompatible pending result-type change. The registry now checks those registrations against the pending replacement as well, under the existing registry monitor. Public getters continue to return published definitions.
The regression failed before the fix. Both incompatible-replacement rollback and compatible replacement now pass, together with all 251 semantic tests and the full repository build with tests skipped.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -290,8 +296,57 @@ private Object discoverAdapter(CamelContext context) throws IOException {
return resolved;
}
- private ResolvedExpert expert(String name, SemanticQuestion question) {
- String reference = question.getExpert() != null ? question.getExpert() : defaultExpert;
+ /** Validate declarations, including unused evaluations, without starting managed expert resources. */
+ public void validateDeclarations(Map<String, SemanticEvaluation> declarations) {
+ validateDeclarations(declarations, null);
+ }
+
+ /** Validate changed declarations against the complete candidate snapshot without publishing it. */
+ public void validateDeclarations(
+ Map<String, SemanticEvaluation> declarations, Map<String, SemanticEvaluation> snapshot) {
+ validationOnly(snapshot, () -> {
+ declarations.forEach((name, evaluation) -> {
+ expert(name, evaluation);
+ String selector = evaluation.getState() != null ? evaluation.getState() : defaultState;
+ if (selector == null || selector.isBlank()) {
+ throw new IllegalArgumentException("Semantic evaluation '" + name + "': state must not be blank");
+ }
+ Expression state = getCamelContext().resolveLanguage("simple")
+ .createExpression(getCamelContext().resolvePropertyPlaceholders(selector));
+ state.init(getCamelContext());
+ });
+ return null;
+ });
+ }
+
+ private <T> T validationOnly(Map<String, SemanticEvaluation> snapshot, Supplier<T> action) {
+ Validation previous = validation.get();
+ validation.set(snapshot != null || previous == null ? new Validation(snapshot) : previous);
+ try {
Review Comment:
Added the clarification in [this commit](https://github.com/apache/camel/commit/a1c6db3386a06c540ae89f41c3adcc286000de42): an explicit snapshot overrides the current scope, while nested calls without one inherit the outer validation scope. This is a comment-only change to `validationOnly()`.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27494:
URL: https://github.com/apache/camel/pull/27494#issuecomment-6037865343
<!-- ci-tested-modules -->
:test_tube: **CI tested the following changed modules:**
- `catalog/camel-catalog`
- `components/camel-ai/camel-semantic`
- `components/camel-ai/camel-typesafe-ai`
- `core/camel-core-languages`
- `core/camel-core`
- `core/camel-util`
- `dsl/camel-yaml-dsl/camel-yaml-dsl-validator`
- `dsl/camel-yaml-dsl/camel-yaml-dsl`
> :information_source: Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the `test-dependents` label to force testing all dependents.
---
<details><summary>:microscope: Scalpel shadow comparison — Scalpel: 576 of 699 tested, 0 compile-only — current: 576 all tested</summary>
[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **576 affected modules** (current approach: 576).
**Skip-tests mode would test 576 modules** (8 direct + 30 downstream), **skip tests for 0** (generated code, meta-modules)
<details><summary>Modules Scalpel would test (576)</summary>
- `archetypes` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-a2a` ← depends on affected reactor module org.apache.camel:camel-platform-http-vertx
- `camel-activemq` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-activemq6` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-ai-observability` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ai-observability-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ai-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-ai-resource` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ai-tool` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-alibaba-eventbridge` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-fc` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-kms` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-mns` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-oss` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-ots` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-alibaba-sls` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-sms` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-amqp` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-api` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-api-component-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-arangodb` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-archetype-api-component` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-component` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-dataformat` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-java` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-main` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-as2` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-as2-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-as2-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-asn1` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-asterisk` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-atmosphere-websocket` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-atom` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-attachments` ← depends on affected reactor module org.apache.camel:camel-bean
- `camel-avro` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-avro-rpc` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-avro-rpc-jetty` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-avro-rpc-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-avro-rpc-spi` ← downstream of org.apache.camel:camel-core-model
- `camel-aws-bedrock` ← depends on affected reactor module org.apache.camel:camel-base64
- `camel-aws-cloudtrail` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-aws-config` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-aws-parameter-store` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-aws-secrets-manager` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws-security-hub` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-athena` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-comprehend` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-cw` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ddb` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ec2` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ecs` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-eks` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-eventbridge` ← depends on affected reactor module org.apache.camel:camel-aws2-sqs
- `camel-aws2-iam` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-kinesis` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-aws2-kms` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-aws2-lambda` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-mq` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-msk` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-polly` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-redshift` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-rekognition` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-s3` ← depends on affected reactor module org.apache.camel:camel-zip-deflater
- `camel-aws2-s3-vectors` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ses` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-sns` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-sqs` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-step-functions` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-sts` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-aws2-textract` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-timestream` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-transcribe` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-aws2-translate` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-azure-common` ← downstream of org.apache.camel:camel-core-model
- `camel-azure-cosmosdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-eventgrid` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-eventhubs` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-files` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-functions` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-azure-key-vault` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-azure-schema-registry` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-azure-servicebus` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-azure-storage-blob` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-storage-datalake` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-storage-queue` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-barcode` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-base` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-base-engine` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-base64` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-bean` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-bean-validator` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-beanio` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-bindy` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-bonita` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-box` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-box-api` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-box-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-braintree` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-browse` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-caffeine` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-camunda` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cassandraql` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-catalog-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-cbor` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-chatscript` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-chunk` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cli-connector` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cli-debug` ← depends on affected reactor module org.apache.camel:camel-cli-connector
- `camel-clickhouse` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-clickup` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-cloudevents` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cluster` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-cm-sms` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-coap` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cometd` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-console` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-consul` ← depends on affected reactor module org.apache.camel:camel-spring-main
- `camel-controlbus` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-core` ← core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
- `camel-core-all` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-core-catalog` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-engine` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-languages` ← core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json, core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc, core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java, core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java, core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java, core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java, core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/QueryLanguageFunctionFactory.java
- `camel-core-model` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-processor` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-reifier` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-xml` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-couchbase` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-couchdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cron` ← depends on affected reactor module org.apache.camel:camel-timer
- `camel-crypto` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-crypto-pgp` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-csv` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-cxf-common` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cxf-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-cxf-rest` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-soap` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-spring-common` ← depends on affected reactor module org.apache.camel:camel-http-base
- `camel-cxf-spring-rest` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-spring-soap` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-spring-transport` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cxf-transport` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cyberark-vault` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dapr` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dataformat` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-dataset` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-datasonnet` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-dataweave` ← downstream of org.apache.camel:camel-core-model
- `camel-debezium-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-debezium-common-parent` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-debezium-db2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-debezium-mongodb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-mysql` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-oracle` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-debezium-postgres` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-sqlserver` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debug` ← depends on affected reactor module org.apache.camel:camel-xml-io
- `camel-dependencies` ← downstream of org.apache.camel:sync-properties-maven-plugin
- `camel-dfdl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dhis2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dhis2-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dhis2-parent` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-diagram` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-direct` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-disruptor` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-djl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dns` ← depends on affected reactor module org.apache.camel:camel-log
- `camel-docker` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-docling` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-drill` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dropbox` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dsl-modeline` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dsl-support` ← depends on affected reactor module org.apache.camel:camel-core-model
- `camel-duckdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dynamic-router` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-ehcache` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-eip-documentation-enricher-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-elasticsearch` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-elasticsearch-rest-client` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-event` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-exec` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-fastjson` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-fhir` ← depends on affected reactor module org.apache.camel:camel-hl7
- `camel-fhir-api` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-fhir-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-file` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-file-watch` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-flatpack` ← depends on affected reactor module org.apache.camel:camel-direct
- `camel-flink` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-flowable` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-fop` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-fory` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-freemarker` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-ftp` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-ftp-common` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-geocoder` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-git` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-github2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-bigquery` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-calendar` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-common` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-google-drive` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-firestore` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-functions` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-mail` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-google-pubsub` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-secret-manager` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-sheets` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-google-speech-to-text` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-storage` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-text-to-speech` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-vertexai` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-vision` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-graphql` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-grok` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-groovy` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-grpc` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-gson` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-hashicorp-vault` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-hazelcast` ← depends on affected reactor module org.apache.camel:camel-spring-main
- `camel-health` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-hivemq` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-hl7` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-http` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-http-base` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-http-common` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huawei-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-huaweicloud-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-huaweicloud-dms` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-frs` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-huaweicloud-functiongraph` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-iam` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-imagerecognition` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-huaweicloud-obs` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-smn` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-huggingface` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-cos` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-ibm-secrets-manager` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watson-discovery` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watson-language` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watson-speech-to-text` ← depends on affected reactor module org.apache.camel:camel-ibm-watson-text-to-speech
- `camel-ibm-watson-text-to-speech` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watsonx-ai` ← depends on affected reactor module org.apache.camel:camel-vertx-websocket
- `camel-ibm-watsonx-data` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ical` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-iggy` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-ignite` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-infinispan` ← depends on affected reactor module org.apache.camel:camel-stub
- `camel-infinispan-common` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-infinispan-embedded` ← depends on affected reactor module org.apache.camel:camel-log
- `camel-infinispan-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-influxdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-influxdb2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-iso8583` ← depends on affected reactor module org.apache.camel:camel-groovy
- `camel-jackson` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jackson-avro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson-protobuf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson3` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jackson3-avro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson3-protobuf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson3xml` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jacksonxml` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jactl` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jandex` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-jasypt` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-java-io` ← depends on affected reactor module org.apache.camel:camel-xml-io
- `camel-java-joor-dsl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-javascript` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jaxb` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-jbang-console` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-jbang-mcp` ← depends on affected reactor module org.apache.camel:camel-xml-io-dsl
- `camel-jbang-plugin-mcp` ← depends on affected reactor module org.apache.camel:camel-jbang-core
- `camel-jbang-plugin-route-parser` ← depends on affected reactor module org.apache.camel:camel-route-parser
- `camel-jbang-plugin-tui` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jbang-plugin-validate` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jcache` ← depends on affected reactor module org.apache.camel:camel-core-languages
- `camel-jcr` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jdbc` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jetty` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-jetty-common` ← depends on affected reactor module org.apache.camel:camel-jaxb
- `camel-jfr` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-jgroups` ← depends on affected reactor module org.apache.camel:camel-master
- `camel-jgroups-raft` ← depends on affected reactor module org.apache.camel:camel-master
- `camel-jira` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jms` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-jmx` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jolt` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jooq` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-joor` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jpa` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-jq` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jsch` ← depends on affected reactor module org.apache.camel:camel-ssh
- `camel-jslt` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-json-validator` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jsonapi` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jsonata` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jsonb` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jsonpath` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jsoup` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-jt400` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jta` ← depends on affected reactor module org.apache.camel:camel-sql
- `camel-jte` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-kafka` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-kamelet` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-kamelet-main-support` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-keycloak` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-knative` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-knative-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-knative-http` ← depends on affected reactor module org.apache.camel:camel-http
- `camel-knative-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-kserve` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-kubernetes` ← depends on affected reactor module org.apache.camel:camel-log
- `camel-kudu` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-langchain4j-agent` ← depends on affected reactor module org.apache.camel:camel-telemetry
- `camel-langchain4j-agent-api` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-langchain4j-chat` ← depends on affected reactor module org.apache.camel:camel-telemetry
- `camel-langchain4j-core` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-langchain4j-embeddings` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-langchain4j-embeddingstore` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-langchain4j-embeddingstore-api` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-langchain4j-ingest` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-langchain4j-tokenizer` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-langchain4j-web-search` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-language` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-launcher-container` ← depends on affected reactor module org.apache.camel:camel-launcher
- `camel-ldap` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ldif` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-log` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-lra` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-lucene` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-lumberjack` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-lzf` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-mail` ← depends on affected reactor module org.apache.camel:camel-quartz
- `camel-mail-microsoft-oauth` ← depends on affected reactor module org.apache.camel:camel-mail
- `camel-main` ← depends on affected reactor module org.apache.camel:camel-seda
- `camel-management` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-management-api` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-mapstruct` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-master` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-spring-main
- `camel-mcp-server` ← depends on affected reactor module org.apache.camel:camel-openai
- `camel-mcp-server-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mdc` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-metrics` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-micrometer` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-micrometer-observability` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-micrometer-prometheus` ← depends on affected reactor module org.apache.camel:camel-platform-http-main
- `camel-microprofile-config` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-microprofile-fault-tolerance` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-microprofile-health` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-microprofile-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-milo` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-milvus` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mina` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-mina-sftp` ← depends on affected reactor module org.apache.camel:camel-core-languages
- `camel-minio` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-mllp` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-mock` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-mongodb` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-mongodb-gridfs` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mustache` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mvel` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-mybatis` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-nats` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-neo4j` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-netty` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-netty-http` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-oaipmh` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-oauth` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-observability-services` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-observation` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-ocsf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-odata` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ognl` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-olingo2` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-olingo2-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-olingo2-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-olingo4` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-olingo4-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-olingo4-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-once` ← depends on affected reactor module org.apache.camel:camel-groovy
- `camel-opa` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-openai` ← depends on affected reactor module org.apache.camel:camel-jackson
- `camel-openapi-java` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-openapi-rest-dsl-generator` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-openapi-validator` ← depends on affected reactor module org.apache.camel:camel-platform-http-vertx
- `camel-openfga` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-opensearch` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-openstack` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-opentelemetry` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-opentelemetry-metrics` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-opentelemetry2` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-optaplanner` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-paho` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-paho-mqtt5` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-parquet-avro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pdf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pg-replication-slot` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pgevent` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pgvector` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pinecone` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-platform-http` ← depends on affected reactor module org.apache.camel:camel-http
- `camel-platform-http-jolokia` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-platform-http-main` ← depends on affected reactor module org.apache.camel:camel-openapi-java
- `camel-platform-http-vertx` ← depends on affected reactor module org.apache.camel:camel-vertx-http
- `camel-plc4x` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pqc` ← depends on affected reactor module org.apache.camel:camel-crypto
- `camel-printer` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-protobuf` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-pubnub` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pulsar` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-python` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-python3` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-qdrant` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-quartz` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-quickfix` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-quickjs` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-reactive-streams` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-reactor` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-redis` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ref` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-resilience4j` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-resilience4j-micrometer` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-resourceresolver-github` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-rest` ← depends on affected reactor module org.apache.camel:camel-core-languages
- `camel-rest-openapi` ← depends on affected reactor module org.apache.camel:camel-vertx-http
- `camel-rest-postman` ← depends on affected reactor module org.apache.camel:camel-vertx-http
- `camel-restdsl-openapi-plugin` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-robotframework` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-rocketmq` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-rss` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-rxjava` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-saga` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-salesforce` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-salesforce-codegen` ← depends on affected reactor module org.apache.camel:camel-salesforce
- `camel-salesforce-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-sap-netweaver` ← depends on affected reactor module org.apache.camel:camel-velocity
- `camel-saxon` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-scheduler` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-schematron` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-seda` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-semantic` ← components/camel-ai/camel-semantic/src/generated/resources/META-INF/org/apache/camel/language/semantic/semantic.json, components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/language.properties, components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAdapter.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticExpert.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticOperation.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticParameter.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/
semantic/SemanticQuestion.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticResult.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java, components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/FixedSemanticExpert.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticBatchTest.java, components/camel-ai/camel-se
mantic/src/test/java/org/apache/camel/semantic/SemanticCapabilitiesTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticContractTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticEipTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticExpertYamlTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticQuestionBuilderTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResult
Test.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java, components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/TestSemanticAdapter.java, own pom components/camel-ai/camel-semantic/pom.xml changed
- `camel-servicenow` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-servicenow-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-servicenow-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-servlet` ← depends on affected reactor module org.apache.camel:camel-gson
- `camel-shell` ← depends on affected reactor module org.apache.camel:camel-test-junit5
- `camel-shiro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-sjms` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-sjms2` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-slack` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-smb` ← depends on affected reactor module org.apache.camel:camel-test-infra-smb
- `camel-smooks` ← depends on affected reactor module org.apache.camel:camel-attachments
- `camel-smpp` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-snakeyaml` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-snmp` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-soap` ← depends on affected reactor module org.apache.camel:camel-cxf-soap
- `camel-solr` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-spiffe` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-splunk-hec` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-spring` ← depends on affected reactor module org.apache.camel:camel-cron
- `camel-spring-ai-chat` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-ai-embeddings` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-spring-ai-image` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-spring-ai-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-spring-ai-vector-store` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-spring-batch` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-cloud-config` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-jdbc` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-ldap` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-main` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-spring-rabbitmq` ← depends on affected reactor module org.apache.camel:camel-http
- `camel-spring-redis` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-security` ← depends on affected reactor module org.apache.camel:camel-core-xml
- `camel-spring-ws` ← depends on affected reactor module org.apache.camel:camel-jaxb
- `camel-spring-xml` ← depends on affected reactor module org.apache.camel:camel-cron
- `camel-sql` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-ssh` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-state-store` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-state-store-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-stax` ← depends on affected reactor module org.apache.camel:camel-jaxb
- `camel-stitch` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-stream` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-streamcaching-test` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-stringtemplate` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-stripe` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-stub` ← depends on affected reactor module org.apache.camel:camel-seda
- `camel-support` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-swift` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-syslog` ← depends on affected reactor module org.apache.camel:camel-mina
- `camel-tahu` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-tarfile` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-telegram` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-telemetry` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-telemetry-dev` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-tensorflow-serving` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-test-infra-all` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-test-infra-artemis` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-test-infra-cli` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-test-infra-core` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-test-infra-jetty` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-test-infra-qdrant` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-test-infra-smb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-test-junit5` ← depends on affected reactor module org.apache.camel:camel-xml-jaxb
- `camel-test-junit6` ← depends on affected reactor module org.apache.camel:camel-xml-jaxb
- `camel-test-main-junit5` ← depends on affected reactor module org.apache.camel:camel-yaml-io
- `camel-test-main-junit6` ← depends on affected reactor module org.apache.camel:camel-yaml-io
- `camel-test-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-test-spring-junit5` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-test-spring-junit6` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-thrift` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-thymeleaf` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-tika` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-timer` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-tooling-maven` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-toon` ← depends on affected reactor module org.apache.camel:camel-test-junit5
- `camel-tracing` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-twilio` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-twitter` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-typesafe-ai` ← components/camel-ai/camel-typesafe-ai/src/main/docs/typesafe-ai-component.adoc, components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java, components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
- `camel-ubl` ← depends on affected reactor module org.apache.camel:camel-test-junit5
- `camel-undertow` ← depends on affected reactor module org.apache.camel:camel-openapi-java
- `camel-undertow-spring-security` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-univocity-parsers` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-util` ← core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
- `camel-validator` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-velocity` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-vertx` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-vertx-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-vertx-http` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-vertx-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-vertx-websocket` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-wal` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-wasm` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-weather` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-weaviate` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-web3j` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-webhook` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-whatsapp` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-wordpress` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-workday` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-xchange` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xj` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-io` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-xml-io-dsl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxb` ← depends on affected reactor module org.apache.camel:camel-core-model
- `camel-xml-jaxb-dsl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxb-dsl-test-definition` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxb-dsl-test-spring` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxp` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-xmlsecurity` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xmpp` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xpath` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-xslt` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-xslt-saxon` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-yaml-dsl-common` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-yaml-dsl-validator` ← dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
- `camel-yaml-dsl-validator-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
- `camel-yaml-io` ← depends on affected reactor module org.apache.camel:camel-xml-io
- `camel-zendesk` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-zip-deflater` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-zipfile` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-zookeeper` ← depends on affected reactor module org.apache.camel:camel-master
- `camel-zookeeper-master` ← depends on affected reactor module org.apache.camel:camel-ftp
- `components` ← downstream of org.apache.camel:camel-core-model
- `sync-properties-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-util
</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:
- `core/camel-core`: 4 test(s) disabled on GitHub Actions
<details><summary>Build reactor — dependencies compiled but only changed modules were tested (8 modules, 1m 21s total)</summary>
**Total reactor time:** 1m 21s
| Module | Duration | Status |
| --- | --- | --- |
| Camel :: Catalog :: Camel Catalog | 16.6s | SUCCESS |
| Camel :: AI :: TypeSafe AI | 14.3s | SUCCESS |
| Camel :: YAML DSL :: Validator | 13.7s | SUCCESS |
| Camel :: Util | 13.2s | SUCCESS |
| Camel :: YAML DSL | 12.3s | SUCCESS |
| Camel :: AI :: Semantic Evaluation | 7.6s | SUCCESS |
| Camel :: Core Languages | 3.4s | SUCCESS |
| Camel :: Core | n/a | |
**Top 20 slowest modules:**
- `Camel :: Catalog :: Camel Catalog` (16.6s)
- `Camel :: AI :: TypeSafe AI` (14.3s)
- `Camel :: YAML DSL :: Validator` (13.7s)
- `Camel :: Util` (13.2s)
- `Camel :: YAML DSL` (12.3s)
- `Camel :: AI :: Semantic Evaluation` (7.6s)
- `Camel :: Core Languages` (3.4s)
</details>
---
:gear: [View full build and test results](https://github.com/apache/camel/actions/runs/37618486911)
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4208613068
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java:
##########
@@ -138,221 +86,309 @@ public String getArtifactId() {
return artifactId;
}
- public Set<InputType> getInputTypes() {
- return inputTypes;
- }
-
- public Set<ResultType> getResultTypes() {
- return resultTypes;
+ public Map<String, Operation> getOperations() {
+ return operations;
}
- public Instructions getInstructions() {
- return instructions;
+ public Operation operation(String name) {
+ Operation result = operations.get(name);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unknown operation '" + name + "'; supported operations: " + operations.keySet());
+ }
+ return result;
}
- public boolean isCallerDefinedCriteria() {
- return callerDefinedCriteria;
+ public void validate(SemanticEvaluation evaluation) {
+ operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
- public boolean isBooleanProbability() {
- return booleanProbability;
+ private static String required(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return value;
}
- public boolean isChoiceProbabilities() {
- return choiceProbabilities;
+ private static boolean withinBounds(Number number, double minimum, double maximum) {
+ if (minimum == Double.POSITIVE_INFINITY || maximum == Double.NEGATIVE_INFINITY) {
+ return false;
+ }
+ try {
+ // Preserve decimal precision, including values just outside a declared boundary.
+ BigDecimal value = new BigDecimal(number.toString());
+ return (minimum == Double.NEGATIVE_INFINITY || value.compareTo(BigDecimal.valueOf(minimum)) >= 0)
+ && (maximum == Double.POSITIVE_INFINITY || value.compareTo(BigDecimal.valueOf(maximum)) <= 0);
+ } catch (NumberFormatException invalid) {
+ return false;
+ }
}
- public Set<ResultType> getConfidenceTypes() {
- return confidenceTypes;
- }
+ /** Operation metadata and common validation, independent of any expert instance. */
+ public static final class Operation {
+ private final SemanticOperation declaration;
+ private final Set<InputType> inputTypes;
+ private final Set<String> labels;
+ private final Map<String, Parameter> parameters;
+
+ private Operation(SemanticOperation declaration) {
+ this.declaration = declaration;
+ required(declaration.name(), "Operation name");
+ required(declaration.description(), "Operation description");
+ required(declaration.inputRequirements(), "Operation input requirements");
+ required(declaration.resultMeaning(), "Operation result meaning");
+ inputTypes = Set.copyOf(List.of(declaration.inputTypes()));
+ labels = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(declaration.labels())));
+ if (inputTypes.isEmpty() || Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum() || labels.stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Invalid operation contract: " + declaration.name());
+ }
+ if (declaration.probability() || declaration.probabilities()) {
+ required(declaration.probabilityMeaning(), "Probability meaning");
+ }
+ if (declaration.confidence()) {
+ required(declaration.confidenceMeaning(), "Confidence meaning");
+ }
+ Map<String, Parameter> values = new LinkedHashMap<>();
+ for (SemanticParameter parameter : declaration.parameters()) {
+ Parameter contract = new Parameter(parameter);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate parameter: " + contract.getName());
+ }
+ }
+ parameters = Collections.unmodifiableMap(values);
+ }
- public String getProbabilityMeaning() {
- return probabilityMeaning;
- }
+ public String getName() {
+ return declaration.name();
+ }
- public String getConfidenceMeaning() {
- return confidenceMeaning;
- }
+ public String getDescription() {
+ return declaration.description();
+ }
- public String getTrueMeaning() {
- return trueMeaning;
- }
+ public Set<InputType> getInputTypes() {
+ return inputTypes;
+ }
- public int getMaxChoices() {
- return maxChoices;
- }
+ public String getInputRequirements() {
+ return declaration.inputRequirements();
+ }
- public int getMaxScoreLevels() {
- return maxScoreLevels;
- }
+ public Map<String, Parameter> getParameters() {
+ return parameters;
+ }
- /** Validate the common contract before the provider checks any model-specific restrictions. */
- public void validate(SemanticQuestion question) {
- if ((!known || instructions == Instructions.REQUIRED) && question.getInstructions() == null) {
- throw new IllegalArgumentException("Question instructions are required");
+ public ResultType getResultType() {
+ return declaration.resultType();
}
- if (!known) {
- return;
+
+ public String getResultMeaning() {
+ return declaration.resultMeaning();
}
- if (!resultTypes.contains(ResultType.valueOf(question.getType().name()))) {
- throw new IllegalArgumentException(
- "Requires " + question.getType() + "; supports " + resultTypes + ": " + description);
+
+ public Set<String> getLabels() {
+ return labels;
}
- if (instructions == Instructions.UNSUPPORTED && question.getInstructions() != null) {
- throw new IllegalArgumentException("Instructions are unsupported by this fixed expert");
+
+ public double getMinimum() {
+ return declaration.minimum();
}
- if (!callerDefinedCriteria && (!question.getCriteria().isEmpty() || !question.getLevels().isEmpty())) {
- throw new IllegalArgumentException("Caller-defined criteria and score levels are unsupported by this fixed expert");
+
+ public double getMaximum() {
+ return declaration.maximum();
}
- if (question.getType() == SemanticQuestion.Type.CHOICE && maxChoices > 0
- && question.getCriteria().size() > maxChoices) {
- throw new IllegalArgumentException("Supports at most " + maxChoices + " choice criteria");
+
+ public boolean isProbability() {
+ return declaration.probability();
}
- if (question.getType() == SemanticQuestion.Type.SCORE && maxScoreLevels > 0
- && question.getLevels().size() > maxScoreLevels) {
- throw new IllegalArgumentException("Supports at most " + maxScoreLevels + " score levels");
+
+ public boolean isProbabilities() {
+ return declaration.probabilities();
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN && !booleanProbability
- && (question.getThreshold() != 0.5 || question.getUncertainty() != 0
- || question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL)) {
- throw new IllegalArgumentException("Boolean decision policy requires positive-class probability support");
+
+ public String getProbabilityMeaning() {
+ return declaration.probabilityMeaning();
}
- }
- /** Input shape can only be checked once the state selector has been evaluated. */
- public void validateInput(Object state) {
- if (known && !(state instanceof String && inputTypes.contains(InputType.TEXT)
- || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
- throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ public boolean isConfidence() {
+ return declaration.confidence();
}
- }
- /** Named construction of an immutable, explicitly supported contract. */
- public static final class Builder {
- private String name;
- private String description;
- private String provider;
- private String artifactId;
- private Set<InputType> inputTypes = Set.of();
- private Set<ResultType> resultTypes = Set.of();
- private Instructions instructions = Instructions.REQUIRED;
- private boolean callerDefinedCriteria;
- private boolean booleanProbability;
- private boolean choiceProbabilities;
- private Set<ResultType> confidenceTypes = Set.of();
- private String probabilityMeaning;
- private String confidenceMeaning;
- private String trueMeaning;
- private int maxChoices;
- private int maxScoreLevels;
+ public String getConfidenceMeaning() {
+ return declaration.confidenceMeaning();
+ }
- private Builder() {
+ public void validate(Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ if (!parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
+ }
+ }
+ parameters.forEach((name, parameter) -> parameter.validate(values.get(name), values.containsKey(name)));
Review Comment:
This is already rejected during declaration construction: the previous `immutableValue()` delegated nested maps to `immutableMap()`, which checks `name.isBlank()`. I verified the supplied YAML example and nested maps inside lists; they fail before adapter validation.
The single-pass cleanup in [d338ec439c24](https://github.com/apache/camel/commit/d338ec439c247a869f0f54b88e8dab71e06f9389) preserves this check explicitly. Added regression coverage for empty, whitespace, null and non-string keys, both in nested maps and maps inside lists. No additional SPI check is needed.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java:
##########
@@ -0,0 +1,183 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.spi.Resource;
+
+/** Context-local named evaluations, replaced atomically per source when a route resource is reloaded. */
+public final class SemanticEvaluations {
+ private static final Object CREATION_LOCK = new Object();
Review Comment:
Addressed the repeated locking in [d338ec439c24](https://github.com/apache/camel/commit/d338ec439c247a869f0f54b88e8dab71e06f9389) with an unlocked lookup and a second check inside the existing private creation lock. Already-created registries no longer acquire the global lock; first creation still briefly serializes.
I retained the private monitor because [fcf09afc4800](https://github.com/apache/camel/commit/fcf09afc4800187e598f5b4078f9fa31d16a66a2) deliberately removed locking on the public context. The existing test requires initialization to proceed while another thread holds that monitor. A new regression also verifies that a blocked registry lookup in another context cannot delay an already-created registry; it failed before this change and now passes.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4208624009
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java:
##########
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.util.StringHelper;
+
+/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
+public final class SemanticEvaluationBuilder {
+ private final SemanticEvaluationsBuilder parent;
+ private final Map<String, Object> parameters = new LinkedHashMap<>();
+ private final Map<String, String> criteria = new LinkedHashMap<>();
+ private final List<String> levels = new ArrayList<>();
+ private String threshold;
+ private String uncertainty;
+ private boolean normalizeUncertaintyPolicy;
+ private String operation;
+ private String expert;
+ private String state;
+
+ /** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
+ public SemanticEvaluationBuilder() {
+ this(null);
+ }
+
+ SemanticEvaluationBuilder(SemanticEvaluationsBuilder parent) {
+ this.parent = parent;
+ }
+
+ public SemanticEvaluationBuilder operation(String operation) {
+ this.operation = operation;
+ return this;
+ }
+
+ /** Select an instruction-driven operation by type, such as boolean, choice or score. */
+ public SemanticEvaluationBuilder type(String type) {
+ return operation(type.toLowerCase(Locale.ROOT));
+ }
+
+ public SemanticEvaluationBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ String getExpert() {
+ return expert != null ? expert : parent != null ? parent.getExpert() : null;
+ }
+
+ public SemanticEvaluationBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameter(String name, Object value) {
+ if (parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
+ }
+ parameters.put(name, value);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameters(Map<String, ?> values) {
+ values.forEach(this::parameter);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder instructions(String instructions) {
+ return parameter("instructions", instructions);
+ }
+
+ public SemanticEvaluationBuilder criterion(String name, String description) {
+ if (criteria.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
+ }
+ criteria.put(name, description);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder level(String level) {
+ levels.add(level);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder threshold(double threshold) {
+ return parameter("threshold", threshold);
+ }
+
+ public SemanticEvaluationBuilder threshold(String threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertainty(double uncertainty) {
+ return parameter("uncertainty", uncertainty);
+ }
+
+ public SemanticEvaluationBuilder uncertainty(String uncertainty) {
+ this.uncertainty = uncertainty;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
+ return this;
+ }
+
+ public SemanticEvaluationsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
+ return parent;
+ }
+
+ public void register() {
+ end().register();
+ }
+
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticEvaluation build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
+ }
+ values.replaceAll((name, value) -> resolve(context, value));
+ if (normalizeUncertaintyPolicy && values.get("uncertaintyPolicy") instanceof String policy) {
+ values.put("uncertaintyPolicy",
+ StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
+ }
+ return new SemanticEvaluation(
+ operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
+ }
+
+ private static Object resolve(CamelContext context, Object value) {
+ if (value instanceof String text) {
+ return context.resolvePropertyPlaceholders(text);
+ }
+ if (value instanceof Map<?, ?> map) {
+ Map<Object, Object> resolved = new LinkedHashMap<>();
Review Comment:
I kept the intermediate `Map<Object, Object>` so key validation remains in the common immutable declaration. This method also handles Java builder input: `parameter(String, Object)` accepts arbitrary nested maps, so the proposed `(String) key` cast is not safe for every caller. It would replace the deliberate `IllegalArgumentException` with a `ClassCastException` before the shared validation/error wrapping.
Added `nonStringNestedKeysFailWithValidationError()` in [d338ec439c24](https://github.com/apache/camel/commit/d338ec439c247a869f0f54b88e8dab71e06f9389) to preserve that behavior. A checked conversion could retain the diagnostic, but would duplicate the existing key validation without changing the accepted input.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4208105281
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java:
##########
@@ -0,0 +1,183 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.spi.Resource;
+
+/** Context-local named evaluations, replaced atomically per source when a route resource is reloaded. */
+public final class SemanticEvaluations {
+ private static final Object CREATION_LOCK = new Object();
Review Comment:
💡 **Static `CREATION_LOCK` serializes all CamelContexts in the JVM**
`CREATION_LOCK` is a `static final Object` shared across all `CamelContext` instances. In a test suite or multi-tenant deployment that starts several contexts concurrently, they all serialize on this one lock during `SemanticEvaluations.get()`. The critical section is trivially short, so the window is small — but the contention is unnecessary.
Consider locking on the context itself (double-checked) or documenting the trade-off:
```suggestion
public static SemanticEvaluations get(CamelContext context) {
SemanticEvaluations answer = context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
if (answer == null) {
synchronized (context) {
answer = context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class);
if (answer == null) {
answer = new SemanticEvaluations(context);
context.getCamelContextExtension().addContextPlugin(SemanticEvaluations.class, answer);
}
}
}
return answer;
}
```
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java:
##########
@@ -138,221 +86,309 @@ public String getArtifactId() {
return artifactId;
}
- public Set<InputType> getInputTypes() {
- return inputTypes;
- }
-
- public Set<ResultType> getResultTypes() {
- return resultTypes;
+ public Map<String, Operation> getOperations() {
+ return operations;
}
- public Instructions getInstructions() {
- return instructions;
+ public Operation operation(String name) {
+ Operation result = operations.get(name);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unknown operation '" + name + "'; supported operations: " + operations.keySet());
+ }
+ return result;
}
- public boolean isCallerDefinedCriteria() {
- return callerDefinedCriteria;
+ public void validate(SemanticEvaluation evaluation) {
+ operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
- public boolean isBooleanProbability() {
- return booleanProbability;
+ private static String required(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return value;
}
- public boolean isChoiceProbabilities() {
- return choiceProbabilities;
+ private static boolean withinBounds(Number number, double minimum, double maximum) {
+ if (minimum == Double.POSITIVE_INFINITY || maximum == Double.NEGATIVE_INFINITY) {
+ return false;
+ }
+ try {
+ // Preserve decimal precision, including values just outside a declared boundary.
+ BigDecimal value = new BigDecimal(number.toString());
+ return (minimum == Double.NEGATIVE_INFINITY || value.compareTo(BigDecimal.valueOf(minimum)) >= 0)
+ && (maximum == Double.POSITIVE_INFINITY || value.compareTo(BigDecimal.valueOf(maximum)) <= 0);
+ } catch (NumberFormatException invalid) {
+ return false;
+ }
}
- public Set<ResultType> getConfidenceTypes() {
- return confidenceTypes;
- }
+ /** Operation metadata and common validation, independent of any expert instance. */
+ public static final class Operation {
+ private final SemanticOperation declaration;
+ private final Set<InputType> inputTypes;
+ private final Set<String> labels;
+ private final Map<String, Parameter> parameters;
+
+ private Operation(SemanticOperation declaration) {
+ this.declaration = declaration;
+ required(declaration.name(), "Operation name");
+ required(declaration.description(), "Operation description");
+ required(declaration.inputRequirements(), "Operation input requirements");
+ required(declaration.resultMeaning(), "Operation result meaning");
+ inputTypes = Set.copyOf(List.of(declaration.inputTypes()));
+ labels = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(declaration.labels())));
+ if (inputTypes.isEmpty() || Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum() || labels.stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Invalid operation contract: " + declaration.name());
+ }
+ if (declaration.probability() || declaration.probabilities()) {
+ required(declaration.probabilityMeaning(), "Probability meaning");
+ }
+ if (declaration.confidence()) {
+ required(declaration.confidenceMeaning(), "Confidence meaning");
+ }
+ Map<String, Parameter> values = new LinkedHashMap<>();
+ for (SemanticParameter parameter : declaration.parameters()) {
+ Parameter contract = new Parameter(parameter);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate parameter: " + contract.getName());
+ }
+ }
+ parameters = Collections.unmodifiableMap(values);
+ }
- public String getProbabilityMeaning() {
- return probabilityMeaning;
- }
+ public String getName() {
+ return declaration.name();
+ }
- public String getConfidenceMeaning() {
- return confidenceMeaning;
- }
+ public String getDescription() {
+ return declaration.description();
+ }
- public String getTrueMeaning() {
- return trueMeaning;
- }
+ public Set<InputType> getInputTypes() {
+ return inputTypes;
+ }
- public int getMaxChoices() {
- return maxChoices;
- }
+ public String getInputRequirements() {
+ return declaration.inputRequirements();
+ }
- public int getMaxScoreLevels() {
- return maxScoreLevels;
- }
+ public Map<String, Parameter> getParameters() {
+ return parameters;
+ }
- /** Validate the common contract before the provider checks any model-specific restrictions. */
- public void validate(SemanticQuestion question) {
- if ((!known || instructions == Instructions.REQUIRED) && question.getInstructions() == null) {
- throw new IllegalArgumentException("Question instructions are required");
+ public ResultType getResultType() {
+ return declaration.resultType();
}
- if (!known) {
- return;
+
+ public String getResultMeaning() {
+ return declaration.resultMeaning();
}
- if (!resultTypes.contains(ResultType.valueOf(question.getType().name()))) {
- throw new IllegalArgumentException(
- "Requires " + question.getType() + "; supports " + resultTypes + ": " + description);
+
+ public Set<String> getLabels() {
+ return labels;
}
- if (instructions == Instructions.UNSUPPORTED && question.getInstructions() != null) {
- throw new IllegalArgumentException("Instructions are unsupported by this fixed expert");
+
+ public double getMinimum() {
+ return declaration.minimum();
}
- if (!callerDefinedCriteria && (!question.getCriteria().isEmpty() || !question.getLevels().isEmpty())) {
- throw new IllegalArgumentException("Caller-defined criteria and score levels are unsupported by this fixed expert");
+
+ public double getMaximum() {
+ return declaration.maximum();
}
- if (question.getType() == SemanticQuestion.Type.CHOICE && maxChoices > 0
- && question.getCriteria().size() > maxChoices) {
- throw new IllegalArgumentException("Supports at most " + maxChoices + " choice criteria");
+
+ public boolean isProbability() {
+ return declaration.probability();
}
- if (question.getType() == SemanticQuestion.Type.SCORE && maxScoreLevels > 0
- && question.getLevels().size() > maxScoreLevels) {
- throw new IllegalArgumentException("Supports at most " + maxScoreLevels + " score levels");
+
+ public boolean isProbabilities() {
+ return declaration.probabilities();
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN && !booleanProbability
- && (question.getThreshold() != 0.5 || question.getUncertainty() != 0
- || question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL)) {
- throw new IllegalArgumentException("Boolean decision policy requires positive-class probability support");
+
+ public String getProbabilityMeaning() {
+ return declaration.probabilityMeaning();
}
- }
- /** Input shape can only be checked once the state selector has been evaluated. */
- public void validateInput(Object state) {
- if (known && !(state instanceof String && inputTypes.contains(InputType.TEXT)
- || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
- throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ public boolean isConfidence() {
+ return declaration.confidence();
}
- }
- /** Named construction of an immutable, explicitly supported contract. */
- public static final class Builder {
- private String name;
- private String description;
- private String provider;
- private String artifactId;
- private Set<InputType> inputTypes = Set.of();
- private Set<ResultType> resultTypes = Set.of();
- private Instructions instructions = Instructions.REQUIRED;
- private boolean callerDefinedCriteria;
- private boolean booleanProbability;
- private boolean choiceProbabilities;
- private Set<ResultType> confidenceTypes = Set.of();
- private String probabilityMeaning;
- private String confidenceMeaning;
- private String trueMeaning;
- private int maxChoices;
- private int maxScoreLevels;
+ public String getConfidenceMeaning() {
+ return declaration.confidenceMeaning();
+ }
- private Builder() {
+ public void validate(Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ if (!parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
+ }
+ }
+ parameters.forEach((name, parameter) -> parameter.validate(values.get(name), values.containsKey(name)));
Review Comment:
⚠️ **Missing blank-key validation for nested Map parameters**
`Operation.validate()` calls `parameter.validate(values.get(name), ...)` which in turn calls `map.values().forEach(this::validateItem)` — but **map keys are never checked for blankness**. A declaration like:
```yaml
parameters:
criteria:
"": "empty key"
```
passes SPI validation and reaches the adapter. `SemanticEvaluation.immutableValue` checks that nested map keys are `instanceof String` but not that they're non-blank. TypeSafe AI catches this in its own `validate()`, but custom experts that rely on the generic SPI contract will silently accept blank keys and produce confusing behaviour at inference time.
Consider adding a blank-key check in `validateItem` (or in `immutableValue` — see the `SemanticEvaluation` comment below for a combined fix).
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
+public final class SemanticEvaluation {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
+ private final String operation;
+ private final String expert;
+ private final String state;
+ private final Map<String, Object> parameters;
+
+ public SemanticEvaluation(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+
+ static Map<String, Object> immutableMap(Map<String, ?> values) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ values.forEach((name, value) -> {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Parameter names must not be blank");
+ }
+ copy.put(name, immutableValue(value));
+ });
+ return Collections.unmodifiableMap(copy);
+ }
+
+ private static Object immutableValue(Object value) {
+ if (value instanceof Map<?, ?> map) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ map.forEach((key, entry) -> {
+ if (!(key instanceof String name)) {
+ throw new IllegalArgumentException("Parameter maps require string keys");
+ }
+ copy.put(name, entry);
+ });
+ return immutableMap(copy);
Review Comment:
💡 **`immutableValue` makes two passes over nested Map entries**
For a `Map` parameter, `immutableValue` builds `copy` with raw `entry` values (line 72), then calls `immutableMap(copy)` (line 74), which calls `immutableValue` on each entry again (line 60). The intermediate map is immediately discarded. Calling `immutableValue(entry)` inline during the first pass avoids the redundant traversal and also catches the missing blank-key check for nested map keys:
```suggestion
private static Object immutableValue(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> copy = new LinkedHashMap<>();
map.forEach((key, entry) -> {
if (!(key instanceof String name)) {
throw new IllegalArgumentException("Parameter maps require string keys");
}
if (name.isBlank()) {
throw new IllegalArgumentException("Parameter map keys must not be blank");
}
copy.put(name, immutableValue(entry));
});
return Collections.unmodifiableMap(copy);
}
```
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -69,127 +79,150 @@ public void preParse(YamlDeserializationContext dc, Node root) {
if (!(root instanceof SequenceNode sequence)) {
return;
}
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (Node node : sequence.getValue()) {
if (!(node instanceof MappingNode mapping)) {
- // Leave malformed entries to the route loader without replacing the resource's questions.
+ // Leave malformed entries to the route loader without replacing the resource's evaluations.
return;
}
for (NodeTuple tuple : mapping.getValue()) {
if ("semantic".equals(asText(tuple.getKeyNode()))) {
- read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> {
- if (definitions.putIfAbsent(name, question) != null) {
+ read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, evaluation) -> {
+ if (definitions.putIfAbsent(name, evaluation) != null) {
throw new YamlDeserializationException(
- tuple.getValueNode(), "Duplicate semantic question: " + name);
+ tuple.getValueNode(), "Duplicate semantic evaluation: " + name);
}
});
}
}
}
CamelContext context = dc.getCamelContext();
- SemanticQuestions questions = definitions.isEmpty()
- ? context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
- : SemanticQuestions.get(context);
- if (questions != null) {
+ SemanticEvaluations evaluations = definitions.isEmpty()
+ ? context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)
+ : SemanticEvaluations.get(context);
+ if (evaluations != null) {
try {
- questions.replace(dc.getResource(), definitions);
+ evaluations.replace(dc.getResource(), definitions);
} catch (IllegalArgumentException e) {
throw new YamlDeserializationException(root, e.getMessage(), e);
}
}
}
- private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
+ private static Map<String, SemanticEvaluation> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
- if (!semantic.keySet().equals(Set.of("question"))) {
- throw new YamlDeserializationException(node, "Semantic declaration requires only question");
+ for (String field : semantic.keySet()) {
+ if (!Set.of("evaluation", "expert", "state").contains(field)) {
+ throw new YamlDeserializationException(
+ semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
+ }
}
- Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get("question"), "semantic questions").forEach((name, definition) -> {
+ if (!semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires evaluation");
+ }
+ Map<String, SemanticEvaluation> result = new LinkedHashMap<>();
+ fields(semantic.get("evaluation"), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
- throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
+ throw new YamlDeserializationException(definition, "Semantic evaluation requires a nonblank name");
+ }
+ Map<String, Node> values = fields(definition, "semantic evaluation '" + name + "'");
+ for (String common : List.of("expert", "state")) {
+ if (!values.containsKey(common) && semantic.containsKey(common)) {
+ values.put(common, semantic.get(common));
+ }
}
- Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
- String expert = values.containsKey("expert") ? asText(values.get("expert")) : "default/automatic";
+ String expert = "default/automatic";
try {
- result.put(name, readQuestion(context, name, expert, definition, values));
+ if (values.containsKey("expert")) {
+ expert = "invalid expert reference";
+ expert = asText(values.get("expert"));
+ }
+ result.put(name, readEvaluation(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
- definition, "Invalid semantic question '" + name + "': " + e.getMessage()
+ definition, "Invalid semantic evaluation '" + name + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
});
return result;
}
- private static SemanticQuestion readQuestion(
+ private static SemanticEvaluation readEvaluation(
CamelContext context, String name, String expert, Node definition, Map<String, Node> values) {
- String description = "semantic question '" + name + "'";
+ String description = "semantic evaluation '" + name + "'";
String expertContext = " (expert '" + expert + "')";
values.forEach((field, value) -> {
if (!FIELDS.contains(field)) {
throw new YamlDeserializationException(
value, "Unknown property '" + field + "' in " + description + expertContext);
}
});
- if (!values.containsKey("type")) {
- throw new YamlDeserializationException(definition, "Semantic question type is required: " + name + expertContext);
+ if (values.containsKey("type") == values.containsKey("operation")) {
+ throw new YamlDeserializationException(
+ definition, "Specify exactly one operation or type: " + name + expertContext);
}
- SemanticQuestion.Type type = enumeration(values.get("type"), name, expert, "type", SemanticQuestion.Type.class);
- Map<String, String> criteria = new LinkedHashMap<>();
- List<String> levels = List.of();
- if (values.containsKey("criteria")) {
- if (type == SemanticQuestion.Type.SCORE) {
- levels = asSequenceNode(values.get("criteria")).getValue().stream().map(YamlDeserializerSupport::asText)
- .toList();
- } else {
- fields(values.get("criteria"), "criteria for " + description + expertContext)
- .forEach((key, value) -> criteria.put(key, asText(value)));
- }
+ String operation = values.containsKey("operation")
+ ? asText(values.get("operation"))
+ : asText(values.get("type")).toLowerCase(Locale.ROOT);
+ SemanticEvaluationBuilder builder = new SemanticEvaluationBuilder().operation(operation)
+ .expert(asText(values.get("expert"))).state(asText(values.get("state")));
+ if (values.containsKey("parameters")) {
+ fields(values.get("parameters"), "evaluation parameters")
+ .forEach((key, value) -> builder.parameter(key, value(context, value)));
}
- if (type != SemanticQuestion.Type.BOOLEAN && (values.containsKey("threshold") || values.containsKey("uncertainty")
- || values.containsKey("uncertaintyPolicy"))) {
- throw new YamlDeserializationException(
- definition, "Threshold and uncertainty policy require a boolean question: " + name + expertContext);
+ for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
+ Node node = values.get(field);
+ if (node == null) {
+ continue;
+ }
+ switch (field) {
+ case "instructions" -> builder.instructions(asText(node));
+ case "threshold", "uncertainty" -> {
+ try {
+ builder.parameter(field, Double.valueOf(context.resolvePropertyPlaceholders(asText(node))));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(
+ node,
+ "Invalid numeric value for '" + field + "' in " + description + expertContext);
+ }
+ }
+ case "uncertaintyPolicy" -> builder.uncertaintyPolicy(asText(node));
+ case "criteria" -> {
+ if (node instanceof MappingNode) {
+ Map<String, String> criteria = new LinkedHashMap<>();
+ fields(node, "criteria").forEach((key, child) -> criteria.put(key, asText(child)));
+ builder.parameter(field, criteria);
+ } else if (node instanceof SequenceNode sequence) {
+ builder.parameter(field,
+ sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
+ } else {
+ builder.parameter(field, value(context, node));
+ }
+ }
+ default -> throw new IllegalStateException(field);
+ }
}
- SemanticQuestion.UncertaintyPolicy policy = values.containsKey("uncertaintyPolicy")
- ? enumeration(values.get("uncertaintyPolicy"), name, expert, "uncertaintyPolicy",
- SemanticQuestion.UncertaintyPolicy.class)
- : SemanticQuestion.UncertaintyPolicy.FAIL;
- return new SemanticQuestion(
- type, asText(values.get("instructions")), asText(values.get("state")),
- criteria, levels, number(context, values, name, expert, "threshold", 0.5),
- number(context, values, name, expert, "uncertainty", 0), policy, asText(values.get("expert")));
+ return builder.build(context);
}
- private static <T extends Enum<T>> T enumeration(Node node, String question, String expert, String field, Class<T> type) {
- try {
- return asEnum(node, type);
- } catch (InvalidEnumException e) {
- throw new YamlDeserializationException(
- node, "Invalid value for '" + field + "' in semantic question '" + question + "': " + asText(node)
- + " (expert '" + expert + "')",
- e);
+ private static Object value(CamelContext context, Node node) {
+ if (node instanceof MappingNode) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(context, child)));
+ return result;
}
- }
-
- private static double number(
- CamelContext context, Map<String, Node> values, String question, String expert, String name, double fallback) {
- if (!values.containsKey(name)) {
- return fallback;
+ if (node instanceof SequenceNode sequence) {
+ return sequence.getValue().stream().map(child -> value(context, child)).toList();
}
- Node node = values.get(name);
- String raw = asText(node);
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(raw));
- } catch (NumberFormatException e) {
- throw new YamlDeserializationException(
- node,
- "Invalid numeric value for '" + name + "' in semantic question '" + question + "': " + raw
- + " (expert '" + expert + "')",
- e);
+ if (Tag.FLOAT.equals(node.getTag()) || NUMBER.equals(node.getTag())) {
+ try {
+ return new BigDecimal(context.resolvePropertyPlaceholders(asText(node)));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(node, "Invalid numeric parameter");
+ }
}
+ return new StandardConstructor(LoadSettings.builder().build()).constructSingleDocument(Optional.of(node));
Review Comment:
💡 **`StandardConstructor` allocated per scalar value**
A `new StandardConstructor(LoadSettings.builder().build())` is created for every scalar parameter value parsed in a YAML evaluation declaration. For declarations with many parameters this creates unnecessary garbage per parse. `StandardConstructor` with default `LoadSettings` is stateless across `constructSingleDocument` calls in SnakeYAML Engine v2.
Consider caching it as a static constant:
```suggestion
return YAML_CONSTRUCTOR.constructSingleDocument(Optional.of(node));
```
And add at class level:
```java
private static final StandardConstructor YAML_CONSTRUCTOR
= new StandardConstructor(LoadSettings.builder().build());
```
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java:
##########
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.util.StringHelper;
+
+/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
+public final class SemanticEvaluationBuilder {
+ private final SemanticEvaluationsBuilder parent;
+ private final Map<String, Object> parameters = new LinkedHashMap<>();
+ private final Map<String, String> criteria = new LinkedHashMap<>();
+ private final List<String> levels = new ArrayList<>();
+ private String threshold;
+ private String uncertainty;
+ private boolean normalizeUncertaintyPolicy;
+ private String operation;
+ private String expert;
+ private String state;
+
+ /** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
+ public SemanticEvaluationBuilder() {
+ this(null);
+ }
+
+ SemanticEvaluationBuilder(SemanticEvaluationsBuilder parent) {
+ this.parent = parent;
+ }
+
+ public SemanticEvaluationBuilder operation(String operation) {
+ this.operation = operation;
+ return this;
+ }
+
+ /** Select an instruction-driven operation by type, such as boolean, choice or score. */
+ public SemanticEvaluationBuilder type(String type) {
+ return operation(type.toLowerCase(Locale.ROOT));
+ }
+
+ public SemanticEvaluationBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ String getExpert() {
+ return expert != null ? expert : parent != null ? parent.getExpert() : null;
+ }
+
+ public SemanticEvaluationBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameter(String name, Object value) {
+ if (parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
+ }
+ parameters.put(name, value);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameters(Map<String, ?> values) {
+ values.forEach(this::parameter);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder instructions(String instructions) {
+ return parameter("instructions", instructions);
+ }
+
+ public SemanticEvaluationBuilder criterion(String name, String description) {
+ if (criteria.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
+ }
+ criteria.put(name, description);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder level(String level) {
+ levels.add(level);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder threshold(double threshold) {
+ return parameter("threshold", threshold);
+ }
+
+ public SemanticEvaluationBuilder threshold(String threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertainty(double uncertainty) {
+ return parameter("uncertainty", uncertainty);
+ }
+
+ public SemanticEvaluationBuilder uncertainty(String uncertainty) {
+ this.uncertainty = uncertainty;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
+ return this;
+ }
+
+ public SemanticEvaluationsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
+ return parent;
+ }
+
+ public void register() {
+ end().register();
+ }
+
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticEvaluation build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
+ }
+ values.replaceAll((name, value) -> resolve(context, value));
+ if (normalizeUncertaintyPolicy && values.get("uncertaintyPolicy") instanceof String policy) {
+ values.put("uncertaintyPolicy",
+ StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
+ }
+ return new SemanticEvaluation(
+ operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
+ }
+
+ private static Object resolve(CamelContext context, Object value) {
+ if (value instanceof String text) {
+ return context.resolvePropertyPlaceholders(text);
+ }
+ if (value instanceof Map<?, ?> map) {
+ Map<Object, Object> resolved = new LinkedHashMap<>();
Review Comment:
💡 **`resolve()` loses the `String` key type through `Map<Object, Object>`**
The resolved nested map is declared `Map<Object, Object>` even though map parameter keys must be `String` (enforced downstream by `SemanticEvaluation.immutableValue`). Declaring it `Map<String, Object>` makes the constraint explicit at the builder level:
```suggestion
Map<String, Object> resolved = new LinkedHashMap<>();
map.forEach((key, item) -> resolved.put((String) key, resolve(context, item)));
```
The cast is safe for YAML-originated maps (YAML keys are always strings) and the same constraint is enforced at runtime by `immutableValue`.
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4217012567
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java:
##########
@@ -138,221 +86,309 @@ public String getArtifactId() {
return artifactId;
}
- public Set<InputType> getInputTypes() {
- return inputTypes;
- }
-
- public Set<ResultType> getResultTypes() {
- return resultTypes;
+ public Map<String, Operation> getOperations() {
+ return operations;
}
- public Instructions getInstructions() {
- return instructions;
+ public Operation operation(String name) {
+ Operation result = operations.get(name);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unknown operation '" + name + "'; supported operations: " + operations.keySet());
+ }
+ return result;
}
- public boolean isCallerDefinedCriteria() {
- return callerDefinedCriteria;
+ public void validate(SemanticEvaluation evaluation) {
+ operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
- public boolean isBooleanProbability() {
- return booleanProbability;
+ private static String required(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return value;
}
- public boolean isChoiceProbabilities() {
- return choiceProbabilities;
+ private static boolean withinBounds(Number number, double minimum, double maximum) {
+ if (minimum == Double.POSITIVE_INFINITY || maximum == Double.NEGATIVE_INFINITY) {
+ return false;
+ }
+ try {
+ // Preserve decimal precision, including values just outside a declared boundary.
+ BigDecimal value = new BigDecimal(number.toString());
+ return (minimum == Double.NEGATIVE_INFINITY || value.compareTo(BigDecimal.valueOf(minimum)) >= 0)
+ && (maximum == Double.POSITIVE_INFINITY || value.compareTo(BigDecimal.valueOf(maximum)) <= 0);
+ } catch (NumberFormatException invalid) {
+ return false;
+ }
}
- public Set<ResultType> getConfidenceTypes() {
- return confidenceTypes;
- }
+ /** Operation metadata and common validation, independent of any expert instance. */
+ public static final class Operation {
+ private final SemanticOperation declaration;
+ private final Set<InputType> inputTypes;
+ private final Set<String> labels;
+ private final Map<String, Parameter> parameters;
+
+ private Operation(SemanticOperation declaration) {
+ this.declaration = declaration;
+ required(declaration.name(), "Operation name");
+ required(declaration.description(), "Operation description");
+ required(declaration.inputRequirements(), "Operation input requirements");
+ required(declaration.resultMeaning(), "Operation result meaning");
+ inputTypes = Set.copyOf(List.of(declaration.inputTypes()));
+ labels = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(declaration.labels())));
+ if (inputTypes.isEmpty() || Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum() || labels.stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Invalid operation contract: " + declaration.name());
+ }
+ if (declaration.probability() || declaration.probabilities()) {
+ required(declaration.probabilityMeaning(), "Probability meaning");
+ }
+ if (declaration.confidence()) {
+ required(declaration.confidenceMeaning(), "Confidence meaning");
+ }
+ Map<String, Parameter> values = new LinkedHashMap<>();
+ for (SemanticParameter parameter : declaration.parameters()) {
+ Parameter contract = new Parameter(parameter);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate parameter: " + contract.getName());
+ }
+ }
+ parameters = Collections.unmodifiableMap(values);
+ }
- public String getProbabilityMeaning() {
- return probabilityMeaning;
- }
+ public String getName() {
+ return declaration.name();
+ }
- public String getConfidenceMeaning() {
- return confidenceMeaning;
- }
+ public String getDescription() {
+ return declaration.description();
+ }
- public String getTrueMeaning() {
- return trueMeaning;
- }
+ public Set<InputType> getInputTypes() {
+ return inputTypes;
+ }
- public int getMaxChoices() {
- return maxChoices;
- }
+ public String getInputRequirements() {
+ return declaration.inputRequirements();
+ }
- public int getMaxScoreLevels() {
- return maxScoreLevels;
- }
+ public Map<String, Parameter> getParameters() {
+ return parameters;
+ }
- /** Validate the common contract before the provider checks any model-specific restrictions. */
- public void validate(SemanticQuestion question) {
- if ((!known || instructions == Instructions.REQUIRED) && question.getInstructions() == null) {
- throw new IllegalArgumentException("Question instructions are required");
+ public ResultType getResultType() {
+ return declaration.resultType();
}
- if (!known) {
- return;
+
+ public String getResultMeaning() {
+ return declaration.resultMeaning();
}
- if (!resultTypes.contains(ResultType.valueOf(question.getType().name()))) {
- throw new IllegalArgumentException(
- "Requires " + question.getType() + "; supports " + resultTypes + ": " + description);
+
+ public Set<String> getLabels() {
+ return labels;
}
- if (instructions == Instructions.UNSUPPORTED && question.getInstructions() != null) {
- throw new IllegalArgumentException("Instructions are unsupported by this fixed expert");
+
+ public double getMinimum() {
+ return declaration.minimum();
}
- if (!callerDefinedCriteria && (!question.getCriteria().isEmpty() || !question.getLevels().isEmpty())) {
- throw new IllegalArgumentException("Caller-defined criteria and score levels are unsupported by this fixed expert");
+
+ public double getMaximum() {
+ return declaration.maximum();
}
- if (question.getType() == SemanticQuestion.Type.CHOICE && maxChoices > 0
- && question.getCriteria().size() > maxChoices) {
- throw new IllegalArgumentException("Supports at most " + maxChoices + " choice criteria");
+
+ public boolean isProbability() {
+ return declaration.probability();
}
- if (question.getType() == SemanticQuestion.Type.SCORE && maxScoreLevels > 0
- && question.getLevels().size() > maxScoreLevels) {
- throw new IllegalArgumentException("Supports at most " + maxScoreLevels + " score levels");
+
+ public boolean isProbabilities() {
+ return declaration.probabilities();
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN && !booleanProbability
- && (question.getThreshold() != 0.5 || question.getUncertainty() != 0
- || question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL)) {
- throw new IllegalArgumentException("Boolean decision policy requires positive-class probability support");
+
+ public String getProbabilityMeaning() {
+ return declaration.probabilityMeaning();
}
- }
- /** Input shape can only be checked once the state selector has been evaluated. */
- public void validateInput(Object state) {
- if (known && !(state instanceof String && inputTypes.contains(InputType.TEXT)
- || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
- throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ public boolean isConfidence() {
+ return declaration.confidence();
}
- }
- /** Named construction of an immutable, explicitly supported contract. */
- public static final class Builder {
- private String name;
- private String description;
- private String provider;
- private String artifactId;
- private Set<InputType> inputTypes = Set.of();
- private Set<ResultType> resultTypes = Set.of();
- private Instructions instructions = Instructions.REQUIRED;
- private boolean callerDefinedCriteria;
- private boolean booleanProbability;
- private boolean choiceProbabilities;
- private Set<ResultType> confidenceTypes = Set.of();
- private String probabilityMeaning;
- private String confidenceMeaning;
- private String trueMeaning;
- private int maxChoices;
- private int maxScoreLevels;
+ public String getConfidenceMeaning() {
+ return declaration.confidenceMeaning();
+ }
- private Builder() {
+ public void validate(Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ if (!parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
+ }
+ }
+ parameters.forEach((name, parameter) -> parameter.validate(values.get(name), values.containsKey(name)));
Review Comment:
Confirmed at the current head: declaration construction rejects null or blank parameter names, expert annotation construction rejects blank declared names, and direct `Operation.validate()` rejects unrecognized keys. There is no validation bypass. Resolving with the explanation above; an additional guard would duplicate validation and only alter the direct-call diagnostic.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -69,127 +79,150 @@ public void preParse(YamlDeserializationContext dc, Node root) {
if (!(root instanceof SequenceNode sequence)) {
return;
}
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (Node node : sequence.getValue()) {
if (!(node instanceof MappingNode mapping)) {
- // Leave malformed entries to the route loader without replacing the resource's questions.
+ // Leave malformed entries to the route loader without replacing the resource's evaluations.
return;
}
for (NodeTuple tuple : mapping.getValue()) {
if ("semantic".equals(asText(tuple.getKeyNode()))) {
- read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> {
- if (definitions.putIfAbsent(name, question) != null) {
+ read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, evaluation) -> {
+ if (definitions.putIfAbsent(name, evaluation) != null) {
throw new YamlDeserializationException(
- tuple.getValueNode(), "Duplicate semantic question: " + name);
+ tuple.getValueNode(), "Duplicate semantic evaluation: " + name);
}
});
}
}
}
CamelContext context = dc.getCamelContext();
- SemanticQuestions questions = definitions.isEmpty()
- ? context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
- : SemanticQuestions.get(context);
- if (questions != null) {
+ SemanticEvaluations evaluations = definitions.isEmpty()
+ ? context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)
+ : SemanticEvaluations.get(context);
+ if (evaluations != null) {
try {
- questions.replace(dc.getResource(), definitions);
+ evaluations.replace(dc.getResource(), definitions);
} catch (IllegalArgumentException e) {
throw new YamlDeserializationException(root, e.getMessage(), e);
}
}
}
- private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
+ private static Map<String, SemanticEvaluation> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
- if (!semantic.keySet().equals(Set.of("question"))) {
- throw new YamlDeserializationException(node, "Semantic declaration requires only question");
+ for (String field : semantic.keySet()) {
+ if (!Set.of("evaluation", "expert", "state").contains(field)) {
+ throw new YamlDeserializationException(
+ semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
+ }
}
- Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get("question"), "semantic questions").forEach((name, definition) -> {
+ if (!semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires evaluation");
+ }
+ Map<String, SemanticEvaluation> result = new LinkedHashMap<>();
+ fields(semantic.get("evaluation"), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
- throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
+ throw new YamlDeserializationException(definition, "Semantic evaluation requires a nonblank name");
+ }
+ Map<String, Node> values = fields(definition, "semantic evaluation '" + name + "'");
+ for (String common : List.of("expert", "state")) {
+ if (!values.containsKey(common) && semantic.containsKey(common)) {
+ values.put(common, semantic.get(common));
+ }
}
- Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
- String expert = values.containsKey("expert") ? asText(values.get("expert")) : "default/automatic";
+ String expert = "default/automatic";
try {
- result.put(name, readQuestion(context, name, expert, definition, values));
+ if (values.containsKey("expert")) {
+ expert = "invalid expert reference";
+ expert = asText(values.get("expert"));
+ }
+ result.put(name, readEvaluation(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
- definition, "Invalid semantic question '" + name + "': " + e.getMessage()
+ definition, "Invalid semantic evaluation '" + name + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
});
return result;
}
- private static SemanticQuestion readQuestion(
+ private static SemanticEvaluation readEvaluation(
CamelContext context, String name, String expert, Node definition, Map<String, Node> values) {
- String description = "semantic question '" + name + "'";
+ String description = "semantic evaluation '" + name + "'";
String expertContext = " (expert '" + expert + "')";
values.forEach((field, value) -> {
if (!FIELDS.contains(field)) {
throw new YamlDeserializationException(
value, "Unknown property '" + field + "' in " + description + expertContext);
}
});
- if (!values.containsKey("type")) {
- throw new YamlDeserializationException(definition, "Semantic question type is required: " + name + expertContext);
+ if (values.containsKey("type") == values.containsKey("operation")) {
+ throw new YamlDeserializationException(
+ definition, "Specify exactly one operation or type: " + name + expertContext);
}
- SemanticQuestion.Type type = enumeration(values.get("type"), name, expert, "type", SemanticQuestion.Type.class);
- Map<String, String> criteria = new LinkedHashMap<>();
- List<String> levels = List.of();
- if (values.containsKey("criteria")) {
- if (type == SemanticQuestion.Type.SCORE) {
- levels = asSequenceNode(values.get("criteria")).getValue().stream().map(YamlDeserializerSupport::asText)
- .toList();
- } else {
- fields(values.get("criteria"), "criteria for " + description + expertContext)
- .forEach((key, value) -> criteria.put(key, asText(value)));
- }
+ String operation = values.containsKey("operation")
+ ? asText(values.get("operation"))
+ : asText(values.get("type")).toLowerCase(Locale.ROOT);
+ SemanticEvaluationBuilder builder = new SemanticEvaluationBuilder().operation(operation)
+ .expert(asText(values.get("expert"))).state(asText(values.get("state")));
+ if (values.containsKey("parameters")) {
+ fields(values.get("parameters"), "evaluation parameters")
+ .forEach((key, value) -> builder.parameter(key, value(context, value)));
}
- if (type != SemanticQuestion.Type.BOOLEAN && (values.containsKey("threshold") || values.containsKey("uncertainty")
- || values.containsKey("uncertaintyPolicy"))) {
- throw new YamlDeserializationException(
- definition, "Threshold and uncertainty policy require a boolean question: " + name + expertContext);
+ for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
+ Node node = values.get(field);
+ if (node == null) {
+ continue;
+ }
+ switch (field) {
+ case "instructions" -> builder.instructions(asText(node));
+ case "threshold", "uncertainty" -> {
+ try {
+ builder.parameter(field, Double.valueOf(context.resolvePropertyPlaceholders(asText(node))));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(
+ node,
+ "Invalid numeric value for '" + field + "' in " + description + expertContext);
+ }
+ }
+ case "uncertaintyPolicy" -> builder.uncertaintyPolicy(asText(node));
+ case "criteria" -> {
+ if (node instanceof MappingNode) {
+ Map<String, String> criteria = new LinkedHashMap<>();
+ fields(node, "criteria").forEach((key, child) -> criteria.put(key, asText(child)));
+ builder.parameter(field, criteria);
+ } else if (node instanceof SequenceNode sequence) {
+ builder.parameter(field,
+ sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
+ } else {
+ builder.parameter(field, value(context, node));
+ }
+ }
+ default -> throw new IllegalStateException(field);
+ }
}
- SemanticQuestion.UncertaintyPolicy policy = values.containsKey("uncertaintyPolicy")
- ? enumeration(values.get("uncertaintyPolicy"), name, expert, "uncertaintyPolicy",
- SemanticQuestion.UncertaintyPolicy.class)
- : SemanticQuestion.UncertaintyPolicy.FAIL;
- return new SemanticQuestion(
- type, asText(values.get("instructions")), asText(values.get("state")),
- criteria, levels, number(context, values, name, expert, "threshold", 0.5),
- number(context, values, name, expert, "uncertainty", 0), policy, asText(values.get("expert")));
+ return builder.build(context);
}
- private static <T extends Enum<T>> T enumeration(Node node, String question, String expert, String field, Class<T> type) {
- try {
- return asEnum(node, type);
- } catch (InvalidEnumException e) {
- throw new YamlDeserializationException(
- node, "Invalid value for '" + field + "' in semantic question '" + question + "': " + asText(node)
- + " (expert '" + expert + "')",
- e);
+ private static Object value(CamelContext context, Node node) {
+ if (node instanceof MappingNode) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(context, child)));
+ return result;
}
- }
-
- private static double number(
- CamelContext context, Map<String, Node> values, String question, String expert, String name, double fallback) {
- if (!values.containsKey(name)) {
- return fallback;
+ if (node instanceof SequenceNode sequence) {
+ return sequence.getValue().stream().map(child -> value(context, child)).toList();
}
- Node node = values.get(name);
- String raw = asText(node);
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(raw));
- } catch (NumberFormatException e) {
- throw new YamlDeserializationException(
- node,
- "Invalid numeric value for '" + name + "' in semantic question '" + question + "': " + raw
- + " (expert '" + expert + "')",
- e);
+ if (Tag.FLOAT.equals(node.getTag()) || NUMBER.equals(node.getTag())) {
+ try {
+ return new BigDecimal(context.resolvePropertyPlaceholders(asText(node)));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(node, "Invalid numeric parameter");
+ }
}
+ return new StandardConstructor(LoadSettings.builder().build()).constructSingleDocument(Optional.of(node));
Review Comment:
Resolving with the previously verified constructor-state explanation. SnakeYAML Engine 3.1.1 uses the `v2` namespace, but `BaseConstructor` still has mutable per-document maps, sets and deferred-fill lists. Clearing them between calls does not make concurrent use safe. The constructor therefore remains local rather than becoming a static singleton.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4217016059
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java:
##########
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.util.StringHelper;
+
+/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
+public final class SemanticEvaluationBuilder {
+ private final SemanticEvaluationsBuilder parent;
+ private final Map<String, Object> parameters = new LinkedHashMap<>();
+ private final Map<String, String> criteria = new LinkedHashMap<>();
+ private final List<String> levels = new ArrayList<>();
+ private String threshold;
+ private String uncertainty;
+ private boolean normalizeUncertaintyPolicy;
+ private String operation;
+ private String expert;
+ private String state;
+
+ /** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
+ public SemanticEvaluationBuilder() {
+ this(null);
+ }
+
+ SemanticEvaluationBuilder(SemanticEvaluationsBuilder parent) {
+ this.parent = parent;
+ }
+
+ public SemanticEvaluationBuilder operation(String operation) {
+ this.operation = operation;
+ return this;
+ }
+
+ /** Select an instruction-driven operation by type, such as boolean, choice or score. */
+ public SemanticEvaluationBuilder type(String type) {
+ return operation(type.toLowerCase(Locale.ROOT));
+ }
+
+ public SemanticEvaluationBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ String getExpert() {
+ return expert != null ? expert : parent != null ? parent.getExpert() : null;
+ }
+
+ public SemanticEvaluationBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameter(String name, Object value) {
+ if (parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
+ }
+ parameters.put(name, value);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameters(Map<String, ?> values) {
+ values.forEach(this::parameter);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder instructions(String instructions) {
+ return parameter("instructions", instructions);
+ }
+
+ public SemanticEvaluationBuilder criterion(String name, String description) {
+ if (criteria.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
+ }
+ criteria.put(name, description);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder level(String level) {
+ levels.add(level);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder threshold(double threshold) {
+ return parameter("threshold", threshold);
+ }
+
+ public SemanticEvaluationBuilder threshold(String threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertainty(double uncertainty) {
+ return parameter("uncertainty", uncertainty);
+ }
+
+ public SemanticEvaluationBuilder uncertainty(String uncertainty) {
+ this.uncertainty = uncertainty;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
+ return this;
+ }
+
+ public SemanticEvaluationsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
+ return parent;
+ }
+
+ public void register() {
+ end().register();
+ }
+
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticEvaluation build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
+ }
+ values.replaceAll((name, value) -> resolve(context, value));
+ if (normalizeUncertaintyPolicy && values.get("uncertaintyPolicy") instanceof String policy) {
+ values.put("uncertaintyPolicy",
+ StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
+ }
+ return new SemanticEvaluation(
+ operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
+ }
+
+ private static Object resolve(CamelContext context, Object value) {
+ if (value instanceof String text) {
+ return context.resolvePropertyPlaceholders(text);
+ }
+ if (value instanceof Map<?, ?> map) {
+ Map<Object, Object> resolved = new LinkedHashMap<>();
Review Comment:
Resolving with the existing validation behavior. Non-string keys are already rejected by `SemanticEvaluation.immutableValue()`; they are not silently accepted. Keeping the intermediate map avoids an early `ClassCastException` and preserves the common `IllegalArgumentException` diagnostic, as covered by `nonStringNestedKeysFailWithValidationError()`.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,38 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return null;
Review Comment:
Clarified in [384c0e7f7de1](https://github.com/apache/camel/commit/384c0e7f7de14a473b201a4ef05f6d8ee0a0423d) and [96e6d465f016](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d). The lazy supplier intentionally leaves the plugin absent after releasing the lookup gate, so `SemanticEvaluations.get()` creates and registers a real instance. The explanatory comment and assertion description now state that behavior. The non-null assertion is therefore correct and remains in place.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java:
##########
@@ -94,36 +160,86 @@ public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> q
return results;
}
- private Map<String, Object> definition(SemanticQuestion question) {
+ private Map<String, Object> definition(SemanticEvaluation question) {
validate(question);
Map<String, Object> definition = new HashMap<>();
- definition.put("instructions", question.getInstructions());
+ definition.put("instructions", question.getParameters().get("instructions"));
definition.put("type", type(question));
- if (question.getType() == SemanticQuestion.Type.SCORE) {
- definition.put("criteria", question.getLevels());
- } else if (!question.getCriteria().isEmpty()) {
- definition.put("criteria", question.getCriteria());
+ if ("score".equals(question.getOperation())) {
+ definition.put("criteria", levels(question));
+ } else if (!criteria(question).isEmpty()) {
+ definition.put("criteria", criteria(question));
}
return definition;
}
- private String type(SemanticQuestion question) {
- return switch (question.getType()) {
- case BOOLEAN -> "noul";
- case CHOICE -> "choice";
- case SCORE -> "score";
+ private String type(SemanticEvaluation question) {
+ return switch (question.getOperation()) {
+ case "boolean" -> "noul";
+ case "choice" -> "choice";
+ case "score" -> "score";
+ default -> throw new IllegalArgumentException("Unsupported TypeSafe AI operation");
};
}
- private SemanticResult result(SemanticQuestion question, JsonObject answer, JsonObject response) {
+ private SemanticResult result(SemanticEvaluation question, JsonObject answer, JsonObject response) {
Map<String, Double> probabilities = new HashMap<>();
if (answer.get("probabilities") instanceof Map<?, ?> values) {
values.forEach((key, value) -> probabilities.put((String) key, ((Number) value).doubleValue()));
}
- return new SemanticResult(
- question.getType() == SemanticQuestion.Type.BOOLEAN ? null : answer.get(type(question)),
- question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null,
+ Double probability = "boolean".equals(question.getOperation()) ? answer.getDouble("noul") : null;
+ Object value = switch (question.getOperation()) {
+ case "boolean" -> booleanDecision(question, probability);
Review Comment:
Thanks for confirming this in [your latest review](https://github.com/apache/camel/pull/27494#pullrequestreview-5453768092). The three HTTP regressions in [96e6d465f016](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d) establish that missing `noul`, the named answer or `answers` fail in the client validation layer before adapter mapping. Resolving this thread with the existing diagnostics preserved.
_AI-generated by Codex on behalf of @luigidemasi._
##########
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java:
##########
@@ -0,0 +1,32 @@
+/*
+ * 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.language.simple;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+
+/**
+ * Opts a Simple function into using its predicate contract for negation, including any validation in
+ * {@link #matches(org.apache.camel.Exchange)}. Existing expressions retain value-based truthiness, even when they also
+ * implement {@link Predicate}.
+ */
+public interface SimplePredicateExpression extends Expression, Predicate {
Review Comment:
Confirmed present at the current head: [96e6d465f016](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d) adds `@since 4.23` to `SimplePredicateExpression`. Resolving following your acknowledgment in [your latest review](https://github.com/apache/camel/pull/27494#pullrequestreview-5453768092).
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4216997672
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -495,7 +545,14 @@ private Object evaluate(Exchange exchange, boolean asPredicate) {
current = compile(Map.of(name, single));
}
}
+ if (asPredicate) {
+ requireBoolean(current.operations.get(names.get(0)));
+ }
+ startAdapter(current.groups);
Review Comment:
Fixed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). Managed-adapter activation now happens when a declaration is compiled or recompiled. Evaluation reuses the compiled result while the selected declarations are unchanged, so ordinary messages no longer enter `startAdapter()` or scan managed adapters. Resolving this earlier thread as requested in [your latest review](https://github.com/apache/camel/pull/27494#pullrequestreview-5453768092).
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -65,6 +68,7 @@ public class SemanticLanguage extends LanguageSupport {
public static final String ADAPTER_FACTORY = "semantic-adapter";
public static final String ADAPTER_RESOURCE = FactoryFinder.DEFAULT_PATH + ADAPTER_FACTORY;
+ private final ThreadLocal<Boolean> validating = new ThreadLocal<>();
Review Comment:
Addressed in [96e6d465f016](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d) and [a1c6db3386a0](https://github.com/apache/camel/commit/a1c6db3386a06c540ae89f41c3adcc286000de42). The registry passes candidate snapshots explicitly and has no ThreadLocal. The private scope remaining in `SemanticLanguage` is limited to nested Simple compilation and is suspended around expert resolution and validation callbacks, then restored in `finally`. Reentrant callback and late-predicate tests cover successful replacement, rejection and cleanup. This preserves existing Simple behavior. Resolving following your acceptance in [your latest review](https://github.com/apache/camel/pull/27494#pullrequestreview-5453768092).
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java:
##########
@@ -25,16 +25,30 @@
import java.util.function.Consumer;
import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.spi.Resource;
/** Context-local named questions, replaced atomically per source when a route resource is reloaded. */
public final class SemanticQuestions {
private static final Object CREATION_LOCK = new Object();
+ private final CamelContext context;
private final Map<String, Map<String, SemanticQuestion>> sources = new HashMap<>();
private final Map<String, Resource> resources = new HashMap<>();
private volatile Map<String, SemanticQuestion> questions = Map.of();
+ private final ThreadLocal<Map<String, SemanticQuestion>> candidate = new ThreadLocal<>();
Review Comment:
Fixed in [96e6d465f016](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d): public `get(name)` and `get(names)` always read published definitions. Candidate snapshots are passed explicitly to validation; `PendingReplacement` additionally checks validators registered reentrantly under the registry monitor. Public reads during callbacks and concurrent reads are covered by regression tests. Resolving following [your latest review](https://github.com/apache/camel/pull/27494#pullrequestreview-5453768092).
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4217018693
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java:
##########
@@ -0,0 +1,177 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.spi.Resource;
+
+/** Context-local named evaluations, replaced atomically per source when a route resource is reloaded. */
+public final class SemanticEvaluations {
+ private static final Object CREATION_LOCK = new Object();
+ private final CamelContext context;
+ private final Map<String, Map<String, SemanticEvaluation>> sources = new HashMap<>();
+ private final Map<String, Resource> resources = new HashMap<>();
+ private volatile Map<String, SemanticEvaluation> evaluations = Map.of();
+ private final Map<Consumer<Map<String, SemanticEvaluation>>, List<String>> validators = new WeakHashMap<>();
+
+ private SemanticEvaluations(CamelContext context) {
+ this.context = context;
+ }
+
+ /** Validate every declaration, including evaluations not referenced by a route. */
+ public void validate() {
+ if (!evaluations.isEmpty()) {
+ ((SemanticLanguage) context.resolveLanguage("semantic")).validateDeclarations(evaluations);
+ }
+ }
+
+ /**
+ * Validate current declarations and weakly track a callback owned by its initialized expression. The callback
+ * receives the complete immutable snapshot so nested references can be validated against the same replacement.
+ */
+ public synchronized void setValidator(List<String> names, Consumer<Map<String, SemanticEvaluation>> validator) {
+ // A replacement may have occurred between the expression's initial compilation and registration.
+ get(names);
+ validator.accept(evaluations);
Review Comment:
Confirmed addressed in [a1c6db3386a0](https://github.com/apache/camel/commit/a1c6db3386a06c540ae89f41c3adcc286000de42). The inline comment explains why registration first checks published definitions; reentrant registration also checks any relevant pending replacement before succeeding. `latePredicatesValidatePendingReplacement` covers both compatible publication and incompatible rejection/rollback, plus cleanup before later registration. Resolving with the fix and explanation above.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -290,8 +296,57 @@ private Object discoverAdapter(CamelContext context) throws IOException {
return resolved;
}
- private ResolvedExpert expert(String name, SemanticQuestion question) {
- String reference = question.getExpert() != null ? question.getExpert() : defaultExpert;
+ /** Validate declarations, including unused evaluations, without starting managed expert resources. */
+ public void validateDeclarations(Map<String, SemanticEvaluation> declarations) {
+ validateDeclarations(declarations, null);
+ }
+
+ /** Validate changed declarations against the complete candidate snapshot without publishing it. */
+ public void validateDeclarations(
+ Map<String, SemanticEvaluation> declarations, Map<String, SemanticEvaluation> snapshot) {
+ validationOnly(snapshot, () -> {
+ declarations.forEach((name, evaluation) -> {
+ expert(name, evaluation);
+ String selector = evaluation.getState() != null ? evaluation.getState() : defaultState;
+ if (selector == null || selector.isBlank()) {
+ throw new IllegalArgumentException("Semantic evaluation '" + name + "': state must not be blank");
+ }
+ Expression state = getCamelContext().resolveLanguage("simple")
+ .createExpression(getCamelContext().resolvePropertyPlaceholders(selector));
+ state.init(getCamelContext());
+ });
+ return null;
+ });
+ }
+
+ private <T> T validationOnly(Map<String, SemanticEvaluation> snapshot, Supplier<T> action) {
+ Validation previous = validation.get();
+ validation.set(snapshot != null || previous == null ? new Validation(snapshot) : previous);
+ try {
Review Comment:
Confirmed added in [a1c6db3386a0](https://github.com/apache/camel/commit/a1c6db3386a06c540ae89f41c3adcc286000de42): an explicit snapshot overrides the scope, while nested calls without one inherit the outer validation scope. This change only documents the existing `validationOnly()` behavior. Resolving this suggestion.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4215765521
##########
components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java:
##########
@@ -94,36 +160,86 @@ public Map<String, SemanticResult> evaluateBatch(Map<String, SemanticQuestion> q
return results;
}
- private Map<String, Object> definition(SemanticQuestion question) {
+ private Map<String, Object> definition(SemanticEvaluation question) {
validate(question);
Map<String, Object> definition = new HashMap<>();
- definition.put("instructions", question.getInstructions());
+ definition.put("instructions", question.getParameters().get("instructions"));
definition.put("type", type(question));
- if (question.getType() == SemanticQuestion.Type.SCORE) {
- definition.put("criteria", question.getLevels());
- } else if (!question.getCriteria().isEmpty()) {
- definition.put("criteria", question.getCriteria());
+ if ("score".equals(question.getOperation())) {
+ definition.put("criteria", levels(question));
+ } else if (!criteria(question).isEmpty()) {
+ definition.put("criteria", criteria(question));
}
return definition;
}
- private String type(SemanticQuestion question) {
- return switch (question.getType()) {
- case BOOLEAN -> "noul";
- case CHOICE -> "choice";
- case SCORE -> "score";
+ private String type(SemanticEvaluation question) {
+ return switch (question.getOperation()) {
+ case "boolean" -> "noul";
+ case "choice" -> "choice";
+ case "score" -> "score";
+ default -> throw new IllegalArgumentException("Unsupported TypeSafe AI operation");
};
}
- private SemanticResult result(SemanticQuestion question, JsonObject answer, JsonObject response) {
+ private SemanticResult result(SemanticEvaluation question, JsonObject answer, JsonObject response) {
Map<String, Double> probabilities = new HashMap<>();
if (answer.get("probabilities") instanceof Map<?, ?> values) {
values.forEach((key, value) -> probabilities.put((String) key, ((Number) value).doubleValue()));
}
- return new SemanticResult(
- question.getType() == SemanticQuestion.Type.BOOLEAN ? null : answer.get(type(question)),
- question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null,
+ Double probability = "boolean".equals(question.getOperation()) ? answer.getDouble("noul") : null;
+ Object value = switch (question.getOperation()) {
+ case "boolean" -> booleanDecision(question, probability);
Review Comment:
I traced this through the HTTP client and added three adapter-level HTTP regressions in [this commit](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d). All three malformed responses fail before reaching this mapping code:
- Missing `noul`: `IOException: Invalid TypeSafe AI response: Expected a number`.
- Missing named answer: `IOException: Invalid TypeSafe AI response: Answer names must match question names`.
- Missing `answers`: `IOException: Invalid TypeSafe AI response: Expected an object`.
`TypeSafeAiClient.send()` always passes the response through `TypeSafeAiJson.response(..., request)`, which validates the answer names and required numeric probability. The tests exercise that real path through the adapter and a local HTTP server. I kept the existing diagnostic and exception contract instead of adding an unreachable null guard in the adapter.
_AI-generated by Codex on behalf of @luigidemasi._
##########
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateExpression.java:
##########
@@ -0,0 +1,32 @@
+/*
+ * 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.language.simple;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+
+/**
+ * Opts a Simple function into using its predicate contract for negation, including any validation in
+ * {@link #matches(org.apache.camel.Exchange)}. Existing expressions retain value-based truthiness, even when they also
+ * implement {@link Predicate}.
+ */
+public interface SimplePredicateExpression extends Expression, Predicate {
Review Comment:
Added `@since 4.23` to `SimplePredicateExpression` in [this commit](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d).
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,39 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ // Leave the plugin absent so get() creates and registers it after this lookup is released.
+ return null;
+ });
+ var creation = callers.submit(() -> SemanticEvaluations.get(creating));
+ try {
+ assertThat(entered.await(10, TimeUnit.SECONDS)).isTrue();
+ var lookup = callers.submit(() -> SemanticEvaluations.get(cached));
+ assertThat(lookup.get(10, TimeUnit.SECONDS)).isSameAs(expected);
+ } finally {
+ release.countDown();
+ assertThat(creation.get(10, TimeUnit.SECONDS)).isNotNull();
Review Comment:
Added `.as("registry created after the lazy lookup returns no plugin")` to the final creation-future assertion in [this commit](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d).
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on PR #27494:
URL: https://github.com/apache/camel/pull/27494#issuecomment-6054127426
Regarding point 2 of [Claus's review](https://github.com/apache/camel/pull/27494#pullrequestreview-5446482078), the public getter issue is now fixed in [this commit](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d).
`SemanticEvaluations` no longer has a `ThreadLocal`: both public getter overloads always read the published snapshot, including on the validation thread. Replacement validation receives the complete candidate snapshot explicitly. One private compilation scope remains in `SemanticLanguage` for nested Simple expressions; it is suspended around expert resolution/validation callbacks and restored in `finally`, so callbacks use published definitions and normal runtime compilation.
This addresses the observable behavior in this PR while preserving existing Simple APIs and behavior. Removing that remaining private scope entirely would require a separate Simple compilation-context design; this fix does not claim to do that. No follow-up JIRA was created.
The getter Javadoc and component documentation now describe published-snapshot behavior. Regression coverage includes concurrent readers, same-thread callback reads, reentrant validation, failed-replacement rollback, candidate-only nested references, and existing lazy `not(...)` / `iif(...)` behavior. All 249 semantic tests pass.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4210421311
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,39 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ // Leave the plugin absent so get() creates and registers it after this lookup is released.
+ return null;
+ });
+ var creation = callers.submit(() -> SemanticEvaluations.get(creating));
+ try {
+ assertThat(entered.await(10, TimeUnit.SECONDS)).isTrue();
+ var lookup = callers.submit(() -> SemanticEvaluations.get(cached));
+ assertThat(lookup.get(10, TimeUnit.SECONDS)).isSameAs(expected);
+ } finally {
+ release.countDown();
+ assertThat(creation.get(10, TimeUnit.SECONDS)).isNotNull();
Review Comment:
💡 **Suggestion:** This assertion verifies a non-null result but gives no diagnostic context when it fails. Consider adding `.as()` to name the operation under test:
```suggestion
assertThat(creation.get(10, TimeUnit.SECONDS)).as("creation task must produce the SemanticEvaluations instance").isNotNull();
```
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4206868191
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -495,7 +545,14 @@ private Object evaluate(Exchange exchange, boolean asPredicate) {
current = compile(Map.of(name, single));
}
}
+ if (asPredicate) {
+ requireBoolean(current.operations.get(names.get(0)));
+ }
+ startAdapter(current.groups);
Review Comment:
`startAdapter` is `synchronized` on the language, and `activate()` is synchronized too. Calling it on every evaluation serializes all concurrent exchanges on one monitor and does a stream scan each time, even once the adapter is active. Could activation happen in `compile()` only, or behind a volatile `activated` fast path checked before taking the lock?
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -65,6 +68,7 @@ public class SemanticLanguage extends LanguageSupport {
public static final String ADAPTER_FACTORY = "semantic-adapter";
public static final String ADAPTER_RESOURCE = FactoryFinder.DEFAULT_PATH + ADAPTER_FACTORY;
+ private final ThreadLocal<Boolean> validating = new ThreadLocal<>();
Review Comment:
Question: a `ThreadLocal` flag changes the behaviour of `compile`, `init` and `startAdapter`, and the same pattern appears with `SemanticQuestions.candidate`. Anything an adapter's `validate()` does on this thread also runs in validation mode. Would an explicit package-private `prepare(selected, validationOnly)` / `validate(snapshot)` path be simpler and safer?
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java:
##########
@@ -25,16 +25,30 @@
import java.util.function.Consumer;
import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
import org.apache.camel.spi.Resource;
/** Context-local named questions, replaced atomically per source when a route resource is reloaded. */
public final class SemanticQuestions {
private static final Object CREATION_LOCK = new Object();
+ private final CamelContext context;
private final Map<String, Map<String, SemanticQuestion>> sources = new HashMap<>();
private final Map<String, Resource> resources = new HashMap<>();
private volatile Map<String, SemanticQuestion> questions = Map.of();
+ private final ThreadLocal<Map<String, SemanticQuestion>> candidate = new ThreadLocal<>();
Review Comment:
With this, `get(name)` / `get(names)` silently return a candidate snapshot depending on thread state, which is surprising for a public API. See the comment on `SemanticLanguage.validating`.
##########
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java:
##########
@@ -102,10 +103,7 @@ public void init(CamelContext context) {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- Object value = exp.evaluate(exchange, Object.class);
- // the same rule the language uses for a predicate on its own, where ${body} is true and a missing
- // header is false, so !${body} and !${header.foo} answer the opposite of those (CAMEL-24984)
- boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
Review Comment:
This creates a new `ExpressionToPredicateAdapter` on every evaluation of `!${...}`. Can the predicate be built once in `createNotExpression` (`final Predicate predicate = ExpressionToPredicateAdapter.toPredicate(exp);`)? Please also keep the CAMEL-24984 comment explaining the `${body}` / missing-header semantics. The same applies to `SimpleFunctionExpression.createNegatedExpression`.
##########
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java:
##########
@@ -148,9 +149,8 @@ public void init(CamelContext context) {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- Object value = exp.evaluate(exchange, Object.class);
- // the same rule the language uses for a predicate on its own (CAMEL-24984)
- boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ // Preserve a function's predicate contract, including semantic's boolean-only guard.
+ boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
Review Comment:
Same as in `UnaryExpression`: please hoist the predicate out of `evaluate()`.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java:
##########
@@ -16,118 +16,178 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
-/**
- * Immutable question definition. Probabilities and thresholds are provider-specific evidence, not accuracy guarantees.
- */
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
public final class SemanticQuestion {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
public enum Type {
BOOLEAN,
CHOICE,
- SCORE
+ SCORE,
+ CLASSIFICATION
}
public enum UncertaintyPolicy {
FAIL,
NON_MATCH
}
- private final Type type;
+ private final String operation;
private final String expert;
- private final String instructions;
private final String state;
- private final Map<String, String> criteria;
- private final List<String> levels;
- private final double threshold;
- private final double uncertainty;
- private final UncertaintyPolicy uncertaintyPolicy;
+ private final Map<String, Object> parameters;
+
+ public SemanticQuestion(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+ /** Convenience declaration for instruction-driven operations named boolean, choice or score. */
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy) {
- this(type, instructions, state, criteria, levels, threshold, uncertainty, uncertaintyPolicy, null);
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy) {
+ this(type, instructions, state, criteria, levels, threshold, uncertainty, policy, null);
}
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy,
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy,
String expert) {
- this.type = Objects.requireNonNull(type, "Question type is required");
- if (expert != null && expert.isBlank()) {
- throw new IllegalArgumentException("Question expert must not be blank");
+ this(type.name().toLowerCase(Locale.ROOT), expert, state,
+ instructionParameters(type, instructions, criteria, levels, threshold, uncertainty, policy));
+ }
+
+ private static Map<String, Object> instructionParameters(
+ Type type, String instructions, Map<String, String> criteria,
+ List<String> levels, double threshold, double uncertainty,
+ UncertaintyPolicy policy) {
+ Map<String, Object> values = new LinkedHashMap<>();
+ if (instructions != null) {
+ values.put("instructions", instructions);
}
- this.expert = expert;
- if (instructions != null && instructions.isBlank()) {
- throw new IllegalArgumentException("Question instructions must not be blank");
+ if (criteria != null && !criteria.isEmpty()) {
+ values.put("criteria", criteria);
}
- if (state != null && state.isBlank()) {
- throw new IllegalArgumentException("Question state selector must not be blank");
+ if (levels != null && !levels.isEmpty()) {
+ values.put("criteria", levels);
Review Comment:
If both `criteria` and `levels` are passed, `levels` silently overwrites `criteria`. `SemanticQuestionBuilder.build` rejects that combination, so the convenience constructor should probably do the same.
##########
components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java:
##########
@@ -120,10 +187,47 @@ private SemanticResult result(SemanticQuestion question, JsonObject answer, Json
if (answer.get("probabilities") instanceof Map<?, ?> values) {
values.forEach((key, value) -> probabilities.put((String) key, ((Number) value).doubleValue()));
}
- return new SemanticResult(
- question.getType() == SemanticQuestion.Type.BOOLEAN ? null : answer.get(type(question)),
- question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null,
+ Double probability = question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null;
+ Object value = switch (question.getType()) {
+ case BOOLEAN -> booleanDecision(question, probability);
Review Comment:
If the response has no `noul`, `probability` is null here and unboxing it into `booleanDecision(..., double)` throws an NPE with no message. Could we throw an `IllegalArgumentException` such as "TypeSafe AI boolean result is missing its probability" so a malformed response produces a clear error?
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java:
##########
@@ -55,6 +57,28 @@ public static SemanticQuestionsBuilder semanticQuestions(RouteBuilder builder) {
return new SemanticQuestionsBuilder(builder);
}
+ public SemanticQuestionsBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ public SemanticQuestionsBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ String getExpert() {
+ return expert;
+ }
+
+ String getState() {
+ return state;
+ }
+
+ public SemanticQuestionBuilder evaluation(String name) {
Review Comment:
Naming: there is now `evaluation(name)` and `question(name)` here, `evaluation:` and `question:` in YAML, and `<evaluation>` and `<question>` in XML, while the classes are still `SemanticQuestion*`. Before 4.23.0 ships, could we pick one term (the JIRA and docs use "evaluation") rather than shipping both?
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java:
##########
@@ -78,84 +96,89 @@ public SemanticQuestionBuilder criterion(String name, String description) {
return this;
}
- /** Add an ordered score level. */
public SemanticQuestionBuilder level(String level) {
levels.add(level);
return this;
}
- /** Boolean decision threshold; defaults to 0.5. */
public SemanticQuestionBuilder threshold(double threshold) {
- return threshold(Double.toString(threshold));
+ return parameter("threshold", threshold);
}
- /** Boolean decision threshold, optionally using property placeholders. */
public SemanticQuestionBuilder threshold(String threshold) {
this.threshold = threshold;
return this;
}
- /** Boolean uncertainty band; defaults to zero. */
public SemanticQuestionBuilder uncertainty(double uncertainty) {
- return uncertainty(Double.toString(uncertainty));
+ return parameter("uncertainty", uncertainty);
}
- /** Boolean uncertainty band, optionally using property placeholders. */
public SemanticQuestionBuilder uncertainty(String uncertainty) {
this.uncertainty = uncertainty;
return this;
}
- /** Boolean uncertainty policy: fail (default) or non-match. */
- public SemanticQuestionBuilder uncertaintyPolicy(String uncertaintyPolicy) {
- this.uncertaintyPolicy = uncertaintyPolicy;
+ public SemanticQuestionBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
return this;
}
- /** Return to the group to add another question. */
public SemanticQuestionsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
return parent;
}
- /** Validate and register the entire group. */
public void register() {
- parent.register();
+ end().register();
}
- SemanticQuestion build(CamelContext context) {
- if (type == null) {
- throw new IllegalArgumentException("Question type is required");
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticQuestion build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
}
- SemanticQuestion.Type questionType = enumeration(type, SemanticQuestion.Type.class);
- if (questionType != SemanticQuestion.Type.BOOLEAN
- && (threshold != null || uncertainty != null || uncertaintyPolicy != null)) {
- throw new IllegalArgumentException("Threshold and uncertainty policy require a boolean question");
+ values.replaceAll((name, value) -> resolve(context, value));
Review Comment:
This resolves property placeholders in every string parameter, including free-text `instructions` (a prompt containing `{{` will fail). A numeric value such as `threshold: "{{x}}"` nested in `parameters` stays a String and then fails Number validation, so experts other than TypeSafe cannot externalise numeric policy through properties. Is that intended? If so, maybe give an example in the docs of how to externalise a nested numeric parameter.
##########
dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java:
##########
@@ -126,6 +126,9 @@ public boolean isSupportedExtension(String extension) {
}
protected RouteBuilder builder(final YamlDeserializationContext ctx, final Node root) {
+ // All resources have prepared their beans before declarations are validated and routes constructed.
+ setDeserializationContext(root, ctx);
+ ctx.preParse(root);
Review Comment:
This moves the resolver `preParse` hook from the pre-parse phase into `builder()`. Fine today, since only camel-semantic implements the hook, but exceptions thrown here are no longer wrapped with `Error pre-parsing resource: <location>`. It is also a generic YAML-loader change hidden in a camel-semantic PR. Could you mention it in the PR description?
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java:
##########
@@ -16,118 +16,178 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
-/**
- * Immutable question definition. Probabilities and thresholds are provider-specific evidence, not accuracy guarantees.
- */
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
public final class SemanticQuestion {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
public enum Type {
BOOLEAN,
CHOICE,
- SCORE
+ SCORE,
+ CLASSIFICATION
}
public enum UncertaintyPolicy {
FAIL,
NON_MATCH
}
- private final Type type;
+ private final String operation;
private final String expert;
- private final String instructions;
private final String state;
- private final Map<String, String> criteria;
- private final List<String> levels;
- private final double threshold;
- private final double uncertainty;
- private final UncertaintyPolicy uncertaintyPolicy;
+ private final Map<String, Object> parameters;
+
+ public SemanticQuestion(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+ /** Convenience declaration for instruction-driven operations named boolean, choice or score. */
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy) {
- this(type, instructions, state, criteria, levels, threshold, uncertainty, uncertaintyPolicy, null);
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy) {
+ this(type, instructions, state, criteria, levels, threshold, uncertainty, policy, null);
}
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy,
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy,
String expert) {
- this.type = Objects.requireNonNull(type, "Question type is required");
- if (expert != null && expert.isBlank()) {
- throw new IllegalArgumentException("Question expert must not be blank");
+ this(type.name().toLowerCase(Locale.ROOT), expert, state,
+ instructionParameters(type, instructions, criteria, levels, threshold, uncertainty, policy));
+ }
+
+ private static Map<String, Object> instructionParameters(
+ Type type, String instructions, Map<String, String> criteria,
+ List<String> levels, double threshold, double uncertainty,
+ UncertaintyPolicy policy) {
+ Map<String, Object> values = new LinkedHashMap<>();
+ if (instructions != null) {
+ values.put("instructions", instructions);
}
- this.expert = expert;
- if (instructions != null && instructions.isBlank()) {
- throw new IllegalArgumentException("Question instructions must not be blank");
+ if (criteria != null && !criteria.isEmpty()) {
+ values.put("criteria", criteria);
}
- if (state != null && state.isBlank()) {
- throw new IllegalArgumentException("Question state selector must not be blank");
+ if (levels != null && !levels.isEmpty()) {
+ values.put("criteria", levels);
}
- this.instructions = instructions;
- this.state = state;
- this.criteria = criteria == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(criteria));
- this.levels = levels == null ? List.of() : List.copyOf(levels);
- this.criteria.forEach((key, value) -> {
- if (key == null || key.isBlank() || value == null || value.isBlank()) {
- throw new IllegalArgumentException("Question criteria require nonblank keys and descriptions");
+ if (type == Type.BOOLEAN) {
+ values.put("threshold", threshold);
+ values.put("uncertainty", uncertainty);
+ values.put("uncertaintyPolicy", policy == UncertaintyPolicy.NON_MATCH ? "non-match" : "fail");
+ }
+ return values;
+ }
+
+ static Map<String, Object> immutableMap(Map<String, ?> values) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ values.forEach((name, value) -> {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Parameter names must not be blank");
}
+ copy.put(name, immutableValue(value));
});
- if (this.levels.stream().anyMatch(String::isBlank)) {
- throw new IllegalArgumentException("Question score levels must not be blank");
- }
- if (type == Type.CHOICE && this.criteria.isEmpty() || type == Type.SCORE && this.levels.isEmpty()
- || type != Type.SCORE && !this.levels.isEmpty() || type == Type.SCORE && !this.criteria.isEmpty()) {
- throw new IllegalArgumentException(
- "Choice needs criteria, score needs ordered levels, boolean accepts true/false criteria");
+ return Collections.unmodifiableMap(copy);
+ }
+
+ private static Object immutableValue(Object value) {
+ if (value instanceof Map<?, ?> map) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ map.forEach((key, entry) -> {
+ if (!(key instanceof String name)) {
+ throw new IllegalArgumentException("Parameter maps require string keys");
+ }
+ copy.put(name, entry);
+ });
+ return immutableMap(copy);
}
- if (type == Type.BOOLEAN && this.criteria.keySet().stream().anyMatch(k -> !k.equals("true") && !k.equals("false"))) {
- throw new IllegalArgumentException("Boolean criteria keys must be true or false");
+ if (value instanceof List<?> list) {
+ List<Object> copy = new ArrayList<>();
+ list.forEach(entry -> copy.add(immutableValue(entry)));
+ return Collections.unmodifiableList(copy);
}
- if (!Double.isFinite(threshold) || !Double.isFinite(uncertainty) || uncertainty < 0
- || threshold - uncertainty < 0 || threshold + uncertainty > 1) {
- throw new IllegalArgumentException("Boolean threshold and uncertainty band must be within [0,1]");
+ if (value == null || value instanceof String || value instanceof Boolean
+ || NUMBER_TYPES.contains(value.getClass())) {
+ return value;
}
- this.threshold = threshold;
- this.uncertainty = uncertainty;
- this.uncertaintyPolicy = Objects.requireNonNull(uncertaintyPolicy, "Uncertainty policy is required");
+ throw new IllegalArgumentException("Parameters require immutable scalar, map or list values");
+ }
+
+ public String getOperation() {
+ return operation;
}
public String getExpert() {
return expert;
}
- public Type getType() {
- return type;
+ public String getState() {
+ return state;
}
- public String getInstructions() {
- return instructions;
+ public Map<String, Object> getParameters() {
+ return parameters;
}
- public String getState() {
- return state;
+ /**
+ * Instruction-driven operation kind; custom operations have their result type in the expert contract.
+ *
+ * @throws IllegalArgumentException if the operation is not a built-in instruction-driven kind
+ */
+ public Type getType() {
+ return Type.valueOf(operation.toUpperCase(Locale.ROOT));
}
+ public String getInstructions() {
+ return (String) parameters.get("instructions");
+ }
+
+ @SuppressWarnings("unchecked")
public Map<String, String> getCriteria() {
- return criteria;
+ return parameters.get("criteria") instanceof Map<?, ?> map ? (Map<String, String>) map : Map.of();
}
+ @SuppressWarnings("unchecked")
public List<String> getLevels() {
- return levels;
+ return parameters.get("criteria") instanceof List<?> list ? (List<String>) list : List.of();
}
- public double getThreshold() {
- return threshold;
+ /** Requested threshold, or null when omitted. Defaults belong to the expert. */
+ public Double getThreshold() {
+ return parameters.get("threshold") instanceof Number number ? number.doubleValue() : null;
}
- public double getUncertainty() {
- return uncertainty;
+ /** Requested uncertainty band, or null when omitted. Defaults belong to the expert. */
+ public Double getUncertainty() {
+ return parameters.get("uncertainty") instanceof Number number ? number.doubleValue() : null;
}
+ /** Requested instruction-driven policy, or null when omitted. Custom policies remain available in parameters. */
public UncertaintyPolicy getUncertaintyPolicy() {
Review Comment:
The javadoc says custom policies remain available in parameters, but this getter throws `IllegalArgumentException` for any value other than fail/non-match. Maybe return null for unknown values, or keep this getter in the TypeSafe adapter only.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java:
##########
@@ -16,118 +16,178 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
-/**
- * Immutable question definition. Probabilities and thresholds are provider-specific evidence, not accuracy guarantees.
- */
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
public final class SemanticQuestion {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
public enum Type {
Review Comment:
`SemanticQuestion.Type` has the same four values as `SemanticExpert.ResultType`, but means "instruction-driven operation name" and throws for custom operations. Likewise `UncertaintyPolicy` and `getThreshold`/`getUncertainty`/`getUncertaintyPolicy` are TypeSafe concepts on the generic declaration. Since this is the first release, could these move to the TypeSafe adapter (or a small helper) so the generic `SemanticQuestion` is just operation/expert/state/parameters?
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4217003844
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java:
##########
@@ -16,118 +16,178 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
-/**
- * Immutable question definition. Probabilities and thresholds are provider-specific evidence, not accuracy guarantees.
- */
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
public final class SemanticQuestion {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
public enum Type {
BOOLEAN,
CHOICE,
- SCORE
+ SCORE,
+ CLASSIFICATION
}
public enum UncertaintyPolicy {
FAIL,
NON_MATCH
}
- private final Type type;
+ private final String operation;
private final String expert;
- private final String instructions;
private final String state;
- private final Map<String, String> criteria;
- private final List<String> levels;
- private final double threshold;
- private final double uncertainty;
- private final UncertaintyPolicy uncertaintyPolicy;
+ private final Map<String, Object> parameters;
+
+ public SemanticQuestion(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+ /** Convenience declaration for instruction-driven operations named boolean, choice or score. */
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy) {
- this(type, instructions, state, criteria, levels, threshold, uncertainty, uncertaintyPolicy, null);
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy) {
+ this(type, instructions, state, criteria, levels, threshold, uncertainty, policy, null);
}
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy,
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy,
String expert) {
- this.type = Objects.requireNonNull(type, "Question type is required");
- if (expert != null && expert.isBlank()) {
- throw new IllegalArgumentException("Question expert must not be blank");
+ this(type.name().toLowerCase(Locale.ROOT), expert, state,
+ instructionParameters(type, instructions, criteria, levels, threshold, uncertainty, policy));
+ }
+
+ private static Map<String, Object> instructionParameters(
+ Type type, String instructions, Map<String, String> criteria,
+ List<String> levels, double threshold, double uncertainty,
+ UncertaintyPolicy policy) {
+ Map<String, Object> values = new LinkedHashMap<>();
+ if (instructions != null) {
+ values.put("instructions", instructions);
}
- this.expert = expert;
- if (instructions != null && instructions.isBlank()) {
- throw new IllegalArgumentException("Question instructions must not be blank");
+ if (criteria != null && !criteria.isEmpty()) {
+ values.put("criteria", criteria);
}
- if (state != null && state.isBlank()) {
- throw new IllegalArgumentException("Question state selector must not be blank");
+ if (levels != null && !levels.isEmpty()) {
+ values.put("criteria", levels);
Review Comment:
Addressed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d) by removing the TypeSafe-specific convenience constructor. `SemanticEvaluation` now has one generic constructor taking the parameter map. The builder keeps criteria and levels separately and rejects mixing them, or combining either with an explicit `criteria` parameter. Java and XML regression cases cover these conflicts.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java:
##########
@@ -16,118 +16,178 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
-/**
- * Immutable question definition. Probabilities and thresholds are provider-specific evidence, not accuracy guarantees.
- */
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
public final class SemanticQuestion {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
public enum Type {
BOOLEAN,
CHOICE,
- SCORE
+ SCORE,
+ CLASSIFICATION
}
public enum UncertaintyPolicy {
FAIL,
NON_MATCH
}
- private final Type type;
+ private final String operation;
private final String expert;
- private final String instructions;
private final String state;
- private final Map<String, String> criteria;
- private final List<String> levels;
- private final double threshold;
- private final double uncertainty;
- private final UncertaintyPolicy uncertaintyPolicy;
+ private final Map<String, Object> parameters;
+
+ public SemanticQuestion(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+ /** Convenience declaration for instruction-driven operations named boolean, choice or score. */
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy) {
- this(type, instructions, state, criteria, levels, threshold, uncertainty, uncertaintyPolicy, null);
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy) {
+ this(type, instructions, state, criteria, levels, threshold, uncertainty, policy, null);
}
public SemanticQuestion(Type type, String instructions, String state, Map<String, String> criteria,
- List<String> levels, double threshold, double uncertainty, UncertaintyPolicy uncertaintyPolicy,
+ List<String> levels, double threshold, double uncertainty, UncertaintyPolicy policy,
String expert) {
- this.type = Objects.requireNonNull(type, "Question type is required");
- if (expert != null && expert.isBlank()) {
- throw new IllegalArgumentException("Question expert must not be blank");
+ this(type.name().toLowerCase(Locale.ROOT), expert, state,
+ instructionParameters(type, instructions, criteria, levels, threshold, uncertainty, policy));
+ }
+
+ private static Map<String, Object> instructionParameters(
+ Type type, String instructions, Map<String, String> criteria,
+ List<String> levels, double threshold, double uncertainty,
+ UncertaintyPolicy policy) {
+ Map<String, Object> values = new LinkedHashMap<>();
+ if (instructions != null) {
+ values.put("instructions", instructions);
}
- this.expert = expert;
- if (instructions != null && instructions.isBlank()) {
- throw new IllegalArgumentException("Question instructions must not be blank");
+ if (criteria != null && !criteria.isEmpty()) {
+ values.put("criteria", criteria);
}
- if (state != null && state.isBlank()) {
- throw new IllegalArgumentException("Question state selector must not be blank");
+ if (levels != null && !levels.isEmpty()) {
+ values.put("criteria", levels);
}
- this.instructions = instructions;
- this.state = state;
- this.criteria = criteria == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(criteria));
- this.levels = levels == null ? List.of() : List.copyOf(levels);
- this.criteria.forEach((key, value) -> {
- if (key == null || key.isBlank() || value == null || value.isBlank()) {
- throw new IllegalArgumentException("Question criteria require nonblank keys and descriptions");
+ if (type == Type.BOOLEAN) {
+ values.put("threshold", threshold);
+ values.put("uncertainty", uncertainty);
+ values.put("uncertaintyPolicy", policy == UncertaintyPolicy.NON_MATCH ? "non-match" : "fail");
+ }
+ return values;
+ }
+
+ static Map<String, Object> immutableMap(Map<String, ?> values) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ values.forEach((name, value) -> {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Parameter names must not be blank");
}
+ copy.put(name, immutableValue(value));
});
- if (this.levels.stream().anyMatch(String::isBlank)) {
- throw new IllegalArgumentException("Question score levels must not be blank");
- }
- if (type == Type.CHOICE && this.criteria.isEmpty() || type == Type.SCORE && this.levels.isEmpty()
- || type != Type.SCORE && !this.levels.isEmpty() || type == Type.SCORE && !this.criteria.isEmpty()) {
- throw new IllegalArgumentException(
- "Choice needs criteria, score needs ordered levels, boolean accepts true/false criteria");
+ return Collections.unmodifiableMap(copy);
+ }
+
+ private static Object immutableValue(Object value) {
+ if (value instanceof Map<?, ?> map) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ map.forEach((key, entry) -> {
+ if (!(key instanceof String name)) {
+ throw new IllegalArgumentException("Parameter maps require string keys");
+ }
+ copy.put(name, entry);
+ });
+ return immutableMap(copy);
}
- if (type == Type.BOOLEAN && this.criteria.keySet().stream().anyMatch(k -> !k.equals("true") && !k.equals("false"))) {
- throw new IllegalArgumentException("Boolean criteria keys must be true or false");
+ if (value instanceof List<?> list) {
+ List<Object> copy = new ArrayList<>();
+ list.forEach(entry -> copy.add(immutableValue(entry)));
+ return Collections.unmodifiableList(copy);
}
- if (!Double.isFinite(threshold) || !Double.isFinite(uncertainty) || uncertainty < 0
- || threshold - uncertainty < 0 || threshold + uncertainty > 1) {
- throw new IllegalArgumentException("Boolean threshold and uncertainty band must be within [0,1]");
+ if (value == null || value instanceof String || value instanceof Boolean
+ || NUMBER_TYPES.contains(value.getClass())) {
+ return value;
}
- this.threshold = threshold;
- this.uncertainty = uncertainty;
- this.uncertaintyPolicy = Objects.requireNonNull(uncertaintyPolicy, "Uncertainty policy is required");
+ throw new IllegalArgumentException("Parameters require immutable scalar, map or list values");
+ }
+
+ public String getOperation() {
+ return operation;
}
public String getExpert() {
return expert;
}
- public Type getType() {
- return type;
+ public String getState() {
+ return state;
}
- public String getInstructions() {
- return instructions;
+ public Map<String, Object> getParameters() {
+ return parameters;
}
- public String getState() {
- return state;
+ /**
+ * Instruction-driven operation kind; custom operations have their result type in the expert contract.
+ *
+ * @throws IllegalArgumentException if the operation is not a built-in instruction-driven kind
+ */
+ public Type getType() {
+ return Type.valueOf(operation.toUpperCase(Locale.ROOT));
}
+ public String getInstructions() {
+ return (String) parameters.get("instructions");
+ }
+
+ @SuppressWarnings("unchecked")
public Map<String, String> getCriteria() {
- return criteria;
+ return parameters.get("criteria") instanceof Map<?, ?> map ? (Map<String, String>) map : Map.of();
}
+ @SuppressWarnings("unchecked")
public List<String> getLevels() {
- return levels;
+ return parameters.get("criteria") instanceof List<?> list ? (List<String>) list : List.of();
}
- public double getThreshold() {
- return threshold;
+ /** Requested threshold, or null when omitted. Defaults belong to the expert. */
+ public Double getThreshold() {
+ return parameters.get("threshold") instanceof Number number ? number.doubleValue() : null;
}
- public double getUncertainty() {
- return uncertainty;
+ /** Requested uncertainty band, or null when omitted. Defaults belong to the expert. */
+ public Double getUncertainty() {
+ return parameters.get("uncertainty") instanceof Number number ? number.doubleValue() : null;
}
+ /** Requested instruction-driven policy, or null when omitted. Custom policies remain available in parameters. */
public UncertaintyPolicy getUncertaintyPolicy() {
Review Comment:
Addressed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). The generic policy getter was removed along with the TypeSafe-specific declaration API. Custom experts receive their policy values unchanged through `getParameters()` and validate their own contract; TypeSafe policy interpretation stays in its adapter.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java:
##########
@@ -55,6 +57,28 @@ public static SemanticQuestionsBuilder semanticQuestions(RouteBuilder builder) {
return new SemanticQuestionsBuilder(builder);
}
+ public SemanticQuestionsBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ public SemanticQuestionsBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ String getExpert() {
+ return expert;
+ }
+
+ String getState() {
+ return state;
+ }
+
+ public SemanticQuestionBuilder evaluation(String name) {
Review Comment:
Addressed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). The public declaration classes, Java builder, YAML and XML consistently use `SemanticEvaluation` / `evaluation`; the `question` declaration aliases were removed. Schemas, tooling examples, documentation and generated catalog copies were updated. The TypeSafe wire protocol retains its existing `questions` field.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4208620217
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -69,127 +79,150 @@ public void preParse(YamlDeserializationContext dc, Node root) {
if (!(root instanceof SequenceNode sequence)) {
return;
}
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (Node node : sequence.getValue()) {
if (!(node instanceof MappingNode mapping)) {
- // Leave malformed entries to the route loader without replacing the resource's questions.
+ // Leave malformed entries to the route loader without replacing the resource's evaluations.
return;
}
for (NodeTuple tuple : mapping.getValue()) {
if ("semantic".equals(asText(tuple.getKeyNode()))) {
- read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> {
- if (definitions.putIfAbsent(name, question) != null) {
+ read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, evaluation) -> {
+ if (definitions.putIfAbsent(name, evaluation) != null) {
throw new YamlDeserializationException(
- tuple.getValueNode(), "Duplicate semantic question: " + name);
+ tuple.getValueNode(), "Duplicate semantic evaluation: " + name);
}
});
}
}
}
CamelContext context = dc.getCamelContext();
- SemanticQuestions questions = definitions.isEmpty()
- ? context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
- : SemanticQuestions.get(context);
- if (questions != null) {
+ SemanticEvaluations evaluations = definitions.isEmpty()
+ ? context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)
+ : SemanticEvaluations.get(context);
+ if (evaluations != null) {
try {
- questions.replace(dc.getResource(), definitions);
+ evaluations.replace(dc.getResource(), definitions);
} catch (IllegalArgumentException e) {
throw new YamlDeserializationException(root, e.getMessage(), e);
}
}
}
- private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
+ private static Map<String, SemanticEvaluation> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
- if (!semantic.keySet().equals(Set.of("question"))) {
- throw new YamlDeserializationException(node, "Semantic declaration requires only question");
+ for (String field : semantic.keySet()) {
+ if (!Set.of("evaluation", "expert", "state").contains(field)) {
+ throw new YamlDeserializationException(
+ semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
+ }
}
- Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get("question"), "semantic questions").forEach((name, definition) -> {
+ if (!semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires evaluation");
+ }
+ Map<String, SemanticEvaluation> result = new LinkedHashMap<>();
+ fields(semantic.get("evaluation"), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
- throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
+ throw new YamlDeserializationException(definition, "Semantic evaluation requires a nonblank name");
+ }
+ Map<String, Node> values = fields(definition, "semantic evaluation '" + name + "'");
+ for (String common : List.of("expert", "state")) {
+ if (!values.containsKey(common) && semantic.containsKey(common)) {
+ values.put(common, semantic.get(common));
+ }
}
- Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
- String expert = values.containsKey("expert") ? asText(values.get("expert")) : "default/automatic";
+ String expert = "default/automatic";
try {
- result.put(name, readQuestion(context, name, expert, definition, values));
+ if (values.containsKey("expert")) {
+ expert = "invalid expert reference";
+ expert = asText(values.get("expert"));
+ }
+ result.put(name, readEvaluation(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
- definition, "Invalid semantic question '" + name + "': " + e.getMessage()
+ definition, "Invalid semantic evaluation '" + name + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
});
return result;
}
- private static SemanticQuestion readQuestion(
+ private static SemanticEvaluation readEvaluation(
CamelContext context, String name, String expert, Node definition, Map<String, Node> values) {
- String description = "semantic question '" + name + "'";
+ String description = "semantic evaluation '" + name + "'";
String expertContext = " (expert '" + expert + "')";
values.forEach((field, value) -> {
if (!FIELDS.contains(field)) {
throw new YamlDeserializationException(
value, "Unknown property '" + field + "' in " + description + expertContext);
}
});
- if (!values.containsKey("type")) {
- throw new YamlDeserializationException(definition, "Semantic question type is required: " + name + expertContext);
+ if (values.containsKey("type") == values.containsKey("operation")) {
+ throw new YamlDeserializationException(
+ definition, "Specify exactly one operation or type: " + name + expertContext);
}
- SemanticQuestion.Type type = enumeration(values.get("type"), name, expert, "type", SemanticQuestion.Type.class);
- Map<String, String> criteria = new LinkedHashMap<>();
- List<String> levels = List.of();
- if (values.containsKey("criteria")) {
- if (type == SemanticQuestion.Type.SCORE) {
- levels = asSequenceNode(values.get("criteria")).getValue().stream().map(YamlDeserializerSupport::asText)
- .toList();
- } else {
- fields(values.get("criteria"), "criteria for " + description + expertContext)
- .forEach((key, value) -> criteria.put(key, asText(value)));
- }
+ String operation = values.containsKey("operation")
+ ? asText(values.get("operation"))
+ : asText(values.get("type")).toLowerCase(Locale.ROOT);
+ SemanticEvaluationBuilder builder = new SemanticEvaluationBuilder().operation(operation)
+ .expert(asText(values.get("expert"))).state(asText(values.get("state")));
+ if (values.containsKey("parameters")) {
+ fields(values.get("parameters"), "evaluation parameters")
+ .forEach((key, value) -> builder.parameter(key, value(context, value)));
}
- if (type != SemanticQuestion.Type.BOOLEAN && (values.containsKey("threshold") || values.containsKey("uncertainty")
- || values.containsKey("uncertaintyPolicy"))) {
- throw new YamlDeserializationException(
- definition, "Threshold and uncertainty policy require a boolean question: " + name + expertContext);
+ for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
+ Node node = values.get(field);
+ if (node == null) {
+ continue;
+ }
+ switch (field) {
+ case "instructions" -> builder.instructions(asText(node));
+ case "threshold", "uncertainty" -> {
+ try {
+ builder.parameter(field, Double.valueOf(context.resolvePropertyPlaceholders(asText(node))));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(
+ node,
+ "Invalid numeric value for '" + field + "' in " + description + expertContext);
+ }
+ }
+ case "uncertaintyPolicy" -> builder.uncertaintyPolicy(asText(node));
+ case "criteria" -> {
+ if (node instanceof MappingNode) {
+ Map<String, String> criteria = new LinkedHashMap<>();
+ fields(node, "criteria").forEach((key, child) -> criteria.put(key, asText(child)));
+ builder.parameter(field, criteria);
+ } else if (node instanceof SequenceNode sequence) {
+ builder.parameter(field,
+ sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
+ } else {
+ builder.parameter(field, value(context, node));
+ }
+ }
+ default -> throw new IllegalStateException(field);
+ }
}
- SemanticQuestion.UncertaintyPolicy policy = values.containsKey("uncertaintyPolicy")
- ? enumeration(values.get("uncertaintyPolicy"), name, expert, "uncertaintyPolicy",
- SemanticQuestion.UncertaintyPolicy.class)
- : SemanticQuestion.UncertaintyPolicy.FAIL;
- return new SemanticQuestion(
- type, asText(values.get("instructions")), asText(values.get("state")),
- criteria, levels, number(context, values, name, expert, "threshold", 0.5),
- number(context, values, name, expert, "uncertainty", 0), policy, asText(values.get("expert")));
+ return builder.build(context);
}
- private static <T extends Enum<T>> T enumeration(Node node, String question, String expert, String field, Class<T> type) {
- try {
- return asEnum(node, type);
- } catch (InvalidEnumException e) {
- throw new YamlDeserializationException(
- node, "Invalid value for '" + field + "' in semantic question '" + question + "': " + asText(node)
- + " (expert '" + expert + "')",
- e);
+ private static Object value(CamelContext context, Node node) {
+ if (node instanceof MappingNode) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(context, child)));
+ return result;
}
- }
-
- private static double number(
- CamelContext context, Map<String, Node> values, String question, String expert, String name, double fallback) {
- if (!values.containsKey(name)) {
- return fallback;
+ if (node instanceof SequenceNode sequence) {
+ return sequence.getValue().stream().map(child -> value(context, child)).toList();
}
- Node node = values.get(name);
- String raw = asText(node);
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(raw));
- } catch (NumberFormatException e) {
- throw new YamlDeserializationException(
- node,
- "Invalid numeric value for '" + name + "' in semantic question '" + question + "': " + raw
- + " (expert '" + expert + "')",
- e);
+ if (Tag.FLOAT.equals(node.getTag()) || NUMBER.equals(node.getTag())) {
+ try {
+ return new BigDecimal(context.resolvePropertyPlaceholders(asText(node)));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(node, "Invalid numeric parameter");
+ }
}
+ return new StandardConstructor(LoadSettings.builder().build()).constructSingleDocument(Optional.of(node));
Review Comment:
The allocation is real, but a static `StandardConstructor` would introduce shared mutable state. The pinned SnakeYAML Engine version is 3.1.1, using the `v2` package namespace. Its `BaseConstructor` maintains `constructedObjects`, `recursiveObjects`, `maps2fill` and `sets2fill`; clearing state between calls permits sequential reuse, not concurrent use from different context/reload threads.
I have kept construction local. This runs during declaration loading rather than message evaluation. Reuse confined to a single parse would be a possible optimization if parsing allocations become a measured concern, but the proposed static singleton is not safe.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluation.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
+public final class SemanticEvaluation {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
+ private final String operation;
+ private final String expert;
+ private final String state;
+ private final Map<String, Object> parameters;
+
+ public SemanticEvaluation(String operation, String expert, String state, Map<String, ?> parameters) {
+ if (operation == null || operation.isBlank()) {
+ throw new IllegalArgumentException("Evaluation operation is required");
+ }
+ if (expert != null && expert.isBlank()) {
+ throw new IllegalArgumentException("Evaluation expert must not be blank");
+ }
+ if (state != null && state.isBlank()) {
+ throw new IllegalArgumentException("Evaluation state selector must not be blank");
+ }
+ this.operation = operation;
+ this.expert = expert;
+ this.state = state;
+ this.parameters = immutableMap(parameters == null ? Map.of() : parameters);
+ }
+
+ static Map<String, Object> immutableMap(Map<String, ?> values) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ values.forEach((name, value) -> {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Parameter names must not be blank");
+ }
+ copy.put(name, immutableValue(value));
+ });
+ return Collections.unmodifiableMap(copy);
+ }
+
+ private static Object immutableValue(Object value) {
+ if (value instanceof Map<?, ?> map) {
+ Map<String, Object> copy = new LinkedHashMap<>();
+ map.forEach((key, entry) -> {
+ if (!(key instanceof String name)) {
+ throw new IllegalArgumentException("Parameter maps require string keys");
+ }
+ copy.put(name, entry);
+ });
+ return immutableMap(copy);
Review Comment:
Applied in [d338ec439c24](https://github.com/apache/camel/commit/d338ec439c247a869f0f54b88e8dab71e06f9389). Nested maps now validate their keys and recursively copy values in one pass, then return an unmodifiable map. This removes the intermediate map and second traversal while preserving the existing blank-key and string-key validation. The tests also verify that nested maps and lists remain immutable and detached from caller-owned containers.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4208743082
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,38 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return null;
Review Comment:
💡 **Test does not cover simultaneous CREATION_LOCK contention; supplier returns `null`**
The `cachedRegistryLookupDoesNotWaitForAnotherContext` test proves that a _cached_ lookup on one context is not blocked by a concurrent _creation_ on another (DCL fast-path works for the cached side). What is not covered: two contexts both finding `null` on the fast-path and racing into `synchronized (CREATION_LOCK)` simultaneously. Because `CREATION_LOCK` is `static final`, whichever context wins the monitor serialises the other's creation globally — a test without a pre-populated `cached` instance would confirm this path is still a contention point.
Additionally, the lazy supplier returns `null` on line 94. Depending on `lazyAddContextPlugin`'s contract, a `null` return may silently discard the entry or leave it uninitialised; subsequent calls to `getContextPlugin` on the same extension could then bypass the intended lazy initialisation path entirely. The `assertThat(creation.get(10, TimeUnit.SECONDS)).isNotNull()` assertion passes because `SemanticEvaluations.get()` recovers from a null plugin by creating a fresh instance — but this recovery path is not explicitly exercised as the test's intent.
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4210336142
##########
components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticInitializationTest.java:
##########
@@ -77,6 +76,38 @@ void concurrentQuestionRegistryCreationReturnsOneInstance() throws Exception {
}
}
+ @Test
+ void cachedRegistryLookupDoesNotWaitForAnotherContext() throws Exception {
+ ExecutorService callers = Executors.newFixedThreadPool(2);
+ CountDownLatch entered = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ try (var creating = new DefaultCamelContext(); var cached = new DefaultCamelContext()) {
+ var expected = SemanticEvaluations.get(cached);
+ creating.getCamelContextExtension().lazyAddContextPlugin(SemanticEvaluations.class, () -> {
+ entered.countDown();
+ try {
+ assertThat(release.await(30, TimeUnit.SECONDS)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return null;
Review Comment:
The remaining first-creation serialization is intentional: the private creation lock still protects publication when no registry exists. The optimization targets already-created registries, which now avoid that lock. The existing `concurrentEvaluationRegistryCreationReturnsOneInstance` test separately covers concurrent callers creating the same context's registry. We are not claiming that simultaneous first creation across different contexts is lock-free.
The null supplier is a gate for the lookup: `DefaultContextPluginManager` removes a lazy entry whose supplier returns null, and `SemanticEvaluations.get()` then creates and registers the missing registry. Added a comment explaining that intent in [384c0e7f7de1](https://github.com/apache/camel/commit/384c0e7f7de14a473b201a4ef05f6d8ee0a0423d); the final non-null assertion is retained. The initialization suite passes.
_AI-generated by Codex on behalf of @luigidemasi._
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java:
##########
@@ -223,6 +223,7 @@ static boolean isOfTheApplication(String error) {
static boolean isMissingDependency(String error) {
return error.startsWith("No language could be found for:")
+ || error.equals("The semantic function requires camel-semantic on the classpath")
Review Comment:
Changed to `contains("requires camel-semantic")` in [384c0e7f7de1](https://github.com/apache/camel/commit/384c0e7f7de14a473b201a4ef05f6d8ee0a0423d), so the check no longer requires identical surrounding wording. Added a regression through `SourceValidator.validateYamlSimple()` with a differently worded catalog diagnostic; it fails with the old exact match and passes with this change. Existing tests still reject malformed semantic arguments. The real-diagnostic assertion now checks the same stable phrase. No Simple API changes are needed.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java:
##########
@@ -138,221 +86,309 @@ public String getArtifactId() {
return artifactId;
}
- public Set<InputType> getInputTypes() {
- return inputTypes;
- }
-
- public Set<ResultType> getResultTypes() {
- return resultTypes;
+ public Map<String, Operation> getOperations() {
+ return operations;
}
- public Instructions getInstructions() {
- return instructions;
+ public Operation operation(String name) {
+ Operation result = operations.get(name);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unknown operation '" + name + "'; supported operations: " + operations.keySet());
+ }
+ return result;
}
- public boolean isCallerDefinedCriteria() {
- return callerDefinedCriteria;
+ public void validate(SemanticEvaluation evaluation) {
+ operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
- public boolean isBooleanProbability() {
- return booleanProbability;
+ private static String required(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return value;
}
- public boolean isChoiceProbabilities() {
- return choiceProbabilities;
+ private static boolean withinBounds(Number number, double minimum, double maximum) {
+ if (minimum == Double.POSITIVE_INFINITY || maximum == Double.NEGATIVE_INFINITY) {
+ return false;
+ }
+ try {
+ // Preserve decimal precision, including values just outside a declared boundary.
+ BigDecimal value = new BigDecimal(number.toString());
+ return (minimum == Double.NEGATIVE_INFINITY || value.compareTo(BigDecimal.valueOf(minimum)) >= 0)
+ && (maximum == Double.POSITIVE_INFINITY || value.compareTo(BigDecimal.valueOf(maximum)) <= 0);
+ } catch (NumberFormatException invalid) {
+ return false;
+ }
}
- public Set<ResultType> getConfidenceTypes() {
- return confidenceTypes;
- }
+ /** Operation metadata and common validation, independent of any expert instance. */
+ public static final class Operation {
+ private final SemanticOperation declaration;
+ private final Set<InputType> inputTypes;
+ private final Set<String> labels;
+ private final Map<String, Parameter> parameters;
+
+ private Operation(SemanticOperation declaration) {
+ this.declaration = declaration;
+ required(declaration.name(), "Operation name");
+ required(declaration.description(), "Operation description");
+ required(declaration.inputRequirements(), "Operation input requirements");
+ required(declaration.resultMeaning(), "Operation result meaning");
+ inputTypes = Set.copyOf(List.of(declaration.inputTypes()));
+ labels = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(declaration.labels())));
+ if (inputTypes.isEmpty() || Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum() || labels.stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Invalid operation contract: " + declaration.name());
+ }
+ if (declaration.probability() || declaration.probabilities()) {
+ required(declaration.probabilityMeaning(), "Probability meaning");
+ }
+ if (declaration.confidence()) {
+ required(declaration.confidenceMeaning(), "Confidence meaning");
+ }
+ Map<String, Parameter> values = new LinkedHashMap<>();
+ for (SemanticParameter parameter : declaration.parameters()) {
+ Parameter contract = new Parameter(parameter);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate parameter: " + contract.getName());
+ }
+ }
+ parameters = Collections.unmodifiableMap(values);
+ }
- public String getProbabilityMeaning() {
- return probabilityMeaning;
- }
+ public String getName() {
+ return declaration.name();
+ }
- public String getConfidenceMeaning() {
- return confidenceMeaning;
- }
+ public String getDescription() {
+ return declaration.description();
+ }
- public String getTrueMeaning() {
- return trueMeaning;
- }
+ public Set<InputType> getInputTypes() {
+ return inputTypes;
+ }
- public int getMaxChoices() {
- return maxChoices;
- }
+ public String getInputRequirements() {
+ return declaration.inputRequirements();
+ }
- public int getMaxScoreLevels() {
- return maxScoreLevels;
- }
+ public Map<String, Parameter> getParameters() {
+ return parameters;
+ }
- /** Validate the common contract before the provider checks any model-specific restrictions. */
- public void validate(SemanticQuestion question) {
- if ((!known || instructions == Instructions.REQUIRED) && question.getInstructions() == null) {
- throw new IllegalArgumentException("Question instructions are required");
+ public ResultType getResultType() {
+ return declaration.resultType();
}
- if (!known) {
- return;
+
+ public String getResultMeaning() {
+ return declaration.resultMeaning();
}
- if (!resultTypes.contains(ResultType.valueOf(question.getType().name()))) {
- throw new IllegalArgumentException(
- "Requires " + question.getType() + "; supports " + resultTypes + ": " + description);
+
+ public Set<String> getLabels() {
+ return labels;
}
- if (instructions == Instructions.UNSUPPORTED && question.getInstructions() != null) {
- throw new IllegalArgumentException("Instructions are unsupported by this fixed expert");
+
+ public double getMinimum() {
+ return declaration.minimum();
}
- if (!callerDefinedCriteria && (!question.getCriteria().isEmpty() || !question.getLevels().isEmpty())) {
- throw new IllegalArgumentException("Caller-defined criteria and score levels are unsupported by this fixed expert");
+
+ public double getMaximum() {
+ return declaration.maximum();
}
- if (question.getType() == SemanticQuestion.Type.CHOICE && maxChoices > 0
- && question.getCriteria().size() > maxChoices) {
- throw new IllegalArgumentException("Supports at most " + maxChoices + " choice criteria");
+
+ public boolean isProbability() {
+ return declaration.probability();
}
- if (question.getType() == SemanticQuestion.Type.SCORE && maxScoreLevels > 0
- && question.getLevels().size() > maxScoreLevels) {
- throw new IllegalArgumentException("Supports at most " + maxScoreLevels + " score levels");
+
+ public boolean isProbabilities() {
+ return declaration.probabilities();
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN && !booleanProbability
- && (question.getThreshold() != 0.5 || question.getUncertainty() != 0
- || question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL)) {
- throw new IllegalArgumentException("Boolean decision policy requires positive-class probability support");
+
+ public String getProbabilityMeaning() {
+ return declaration.probabilityMeaning();
}
- }
- /** Input shape can only be checked once the state selector has been evaluated. */
- public void validateInput(Object state) {
- if (known && !(state instanceof String && inputTypes.contains(InputType.TEXT)
- || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
- throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ public boolean isConfidence() {
+ return declaration.confidence();
}
- }
- /** Named construction of an immutable, explicitly supported contract. */
- public static final class Builder {
- private String name;
- private String description;
- private String provider;
- private String artifactId;
- private Set<InputType> inputTypes = Set.of();
- private Set<ResultType> resultTypes = Set.of();
- private Instructions instructions = Instructions.REQUIRED;
- private boolean callerDefinedCriteria;
- private boolean booleanProbability;
- private boolean choiceProbabilities;
- private Set<ResultType> confidenceTypes = Set.of();
- private String probabilityMeaning;
- private String confidenceMeaning;
- private String trueMeaning;
- private int maxChoices;
- private int maxScoreLevels;
+ public String getConfidenceMeaning() {
+ return declaration.confidenceMeaning();
+ }
- private Builder() {
+ public void validate(Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ if (!parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
+ }
+ }
+ parameters.forEach((name, parameter) -> parameter.validate(values.get(name), values.containsKey(name)));
Review Comment:
I checked both entry points. `SemanticEvaluation.immutableMap()` rejects null, empty and whitespace-only parameter names before common contract validation, and `SemanticCapabilities.Parameter` rejects blank names in the expert's annotation. A blank name therefore cannot be a registered parameter.
Direct calls to `Operation.validate()` also reject these keys as unknown parameters. I verified all three cases with executable probes. There is no bypass; an additional guard here would only change the direct-call diagnostic and duplicate the declaration check, so I have left this code unchanged.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -69,127 +79,150 @@ public void preParse(YamlDeserializationContext dc, Node root) {
if (!(root instanceof SequenceNode sequence)) {
return;
}
- Map<String, SemanticQuestion> definitions = new LinkedHashMap<>();
+ Map<String, SemanticEvaluation> definitions = new LinkedHashMap<>();
for (Node node : sequence.getValue()) {
if (!(node instanceof MappingNode mapping)) {
- // Leave malformed entries to the route loader without replacing the resource's questions.
+ // Leave malformed entries to the route loader without replacing the resource's evaluations.
return;
}
for (NodeTuple tuple : mapping.getValue()) {
if ("semantic".equals(asText(tuple.getKeyNode()))) {
- read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> {
- if (definitions.putIfAbsent(name, question) != null) {
+ read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, evaluation) -> {
+ if (definitions.putIfAbsent(name, evaluation) != null) {
throw new YamlDeserializationException(
- tuple.getValueNode(), "Duplicate semantic question: " + name);
+ tuple.getValueNode(), "Duplicate semantic evaluation: " + name);
}
});
}
}
}
CamelContext context = dc.getCamelContext();
- SemanticQuestions questions = definitions.isEmpty()
- ? context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
- : SemanticQuestions.get(context);
- if (questions != null) {
+ SemanticEvaluations evaluations = definitions.isEmpty()
+ ? context.getCamelContextExtension().getContextPlugin(SemanticEvaluations.class)
+ : SemanticEvaluations.get(context);
+ if (evaluations != null) {
try {
- questions.replace(dc.getResource(), definitions);
+ evaluations.replace(dc.getResource(), definitions);
} catch (IllegalArgumentException e) {
throw new YamlDeserializationException(root, e.getMessage(), e);
}
}
}
- private static Map<String, SemanticQuestion> read(CamelContext context, Node node) {
+ private static Map<String, SemanticEvaluation> read(CamelContext context, Node node) {
Map<String, Node> semantic = fields(node, "semantic declaration");
- if (!semantic.keySet().equals(Set.of("question"))) {
- throw new YamlDeserializationException(node, "Semantic declaration requires only question");
+ for (String field : semantic.keySet()) {
+ if (!Set.of("evaluation", "expert", "state").contains(field)) {
+ throw new YamlDeserializationException(
+ semantic.get(field), "Unknown property '" + field + "' in semantic declaration");
+ }
}
- Map<String, SemanticQuestion> result = new LinkedHashMap<>();
- fields(semantic.get("question"), "semantic questions").forEach((name, definition) -> {
+ if (!semantic.containsKey("evaluation")) {
+ throw new YamlDeserializationException(node, "Semantic declaration requires evaluation");
+ }
+ Map<String, SemanticEvaluation> result = new LinkedHashMap<>();
+ fields(semantic.get("evaluation"), "semantic evaluations").forEach((name, definition) -> {
if (name.isBlank()) {
- throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name");
+ throw new YamlDeserializationException(definition, "Semantic evaluation requires a nonblank name");
+ }
+ Map<String, Node> values = fields(definition, "semantic evaluation '" + name + "'");
+ for (String common : List.of("expert", "state")) {
+ if (!values.containsKey(common) && semantic.containsKey(common)) {
+ values.put(common, semantic.get(common));
+ }
}
- Map<String, Node> values = fields(definition, "semantic question '" + name + "'");
- String expert = values.containsKey("expert") ? asText(values.get("expert")) : "default/automatic";
+ String expert = "default/automatic";
try {
- result.put(name, readQuestion(context, name, expert, definition, values));
+ if (values.containsKey("expert")) {
+ expert = "invalid expert reference";
+ expert = asText(values.get("expert"));
+ }
+ result.put(name, readEvaluation(context, name, expert, definition, values));
} catch (IllegalArgumentException | InvalidNodeTypeException e) {
throw new YamlDeserializationException(
- definition, "Invalid semantic question '" + name + "': " + e.getMessage()
+ definition, "Invalid semantic evaluation '" + name + "': " + e.getMessage()
+ " (expert '" + expert + "')",
e);
}
});
return result;
}
- private static SemanticQuestion readQuestion(
+ private static SemanticEvaluation readEvaluation(
CamelContext context, String name, String expert, Node definition, Map<String, Node> values) {
- String description = "semantic question '" + name + "'";
+ String description = "semantic evaluation '" + name + "'";
String expertContext = " (expert '" + expert + "')";
values.forEach((field, value) -> {
if (!FIELDS.contains(field)) {
throw new YamlDeserializationException(
value, "Unknown property '" + field + "' in " + description + expertContext);
}
});
- if (!values.containsKey("type")) {
- throw new YamlDeserializationException(definition, "Semantic question type is required: " + name + expertContext);
+ if (values.containsKey("type") == values.containsKey("operation")) {
+ throw new YamlDeserializationException(
+ definition, "Specify exactly one operation or type: " + name + expertContext);
}
- SemanticQuestion.Type type = enumeration(values.get("type"), name, expert, "type", SemanticQuestion.Type.class);
- Map<String, String> criteria = new LinkedHashMap<>();
- List<String> levels = List.of();
- if (values.containsKey("criteria")) {
- if (type == SemanticQuestion.Type.SCORE) {
- levels = asSequenceNode(values.get("criteria")).getValue().stream().map(YamlDeserializerSupport::asText)
- .toList();
- } else {
- fields(values.get("criteria"), "criteria for " + description + expertContext)
- .forEach((key, value) -> criteria.put(key, asText(value)));
- }
+ String operation = values.containsKey("operation")
+ ? asText(values.get("operation"))
+ : asText(values.get("type")).toLowerCase(Locale.ROOT);
+ SemanticEvaluationBuilder builder = new SemanticEvaluationBuilder().operation(operation)
+ .expert(asText(values.get("expert"))).state(asText(values.get("state")));
+ if (values.containsKey("parameters")) {
+ fields(values.get("parameters"), "evaluation parameters")
+ .forEach((key, value) -> builder.parameter(key, value(context, value)));
}
- if (type != SemanticQuestion.Type.BOOLEAN && (values.containsKey("threshold") || values.containsKey("uncertainty")
- || values.containsKey("uncertaintyPolicy"))) {
- throw new YamlDeserializationException(
- definition, "Threshold and uncertainty policy require a boolean question: " + name + expertContext);
+ for (String field : List.of("instructions", "criteria", "threshold", "uncertainty", "uncertaintyPolicy")) {
+ Node node = values.get(field);
+ if (node == null) {
+ continue;
+ }
+ switch (field) {
+ case "instructions" -> builder.instructions(asText(node));
+ case "threshold", "uncertainty" -> {
+ try {
+ builder.parameter(field, Double.valueOf(context.resolvePropertyPlaceholders(asText(node))));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(
+ node,
+ "Invalid numeric value for '" + field + "' in " + description + expertContext);
+ }
+ }
+ case "uncertaintyPolicy" -> builder.uncertaintyPolicy(asText(node));
+ case "criteria" -> {
+ if (node instanceof MappingNode) {
+ Map<String, String> criteria = new LinkedHashMap<>();
+ fields(node, "criteria").forEach((key, child) -> criteria.put(key, asText(child)));
+ builder.parameter(field, criteria);
+ } else if (node instanceof SequenceNode sequence) {
+ builder.parameter(field,
+ sequence.getValue().stream().map(SemanticDefinitionDeserializer::asText).toList());
+ } else {
+ builder.parameter(field, value(context, node));
+ }
+ }
+ default -> throw new IllegalStateException(field);
+ }
}
- SemanticQuestion.UncertaintyPolicy policy = values.containsKey("uncertaintyPolicy")
- ? enumeration(values.get("uncertaintyPolicy"), name, expert, "uncertaintyPolicy",
- SemanticQuestion.UncertaintyPolicy.class)
- : SemanticQuestion.UncertaintyPolicy.FAIL;
- return new SemanticQuestion(
- type, asText(values.get("instructions")), asText(values.get("state")),
- criteria, levels, number(context, values, name, expert, "threshold", 0.5),
- number(context, values, name, expert, "uncertainty", 0), policy, asText(values.get("expert")));
+ return builder.build(context);
}
- private static <T extends Enum<T>> T enumeration(Node node, String question, String expert, String field, Class<T> type) {
- try {
- return asEnum(node, type);
- } catch (InvalidEnumException e) {
- throw new YamlDeserializationException(
- node, "Invalid value for '" + field + "' in semantic question '" + question + "': " + asText(node)
- + " (expert '" + expert + "')",
- e);
+ private static Object value(CamelContext context, Node node) {
+ if (node instanceof MappingNode) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ fields(node, "parameter map").forEach((key, child) -> result.put(key, value(context, child)));
+ return result;
}
- }
-
- private static double number(
- CamelContext context, Map<String, Node> values, String question, String expert, String name, double fallback) {
- if (!values.containsKey(name)) {
- return fallback;
+ if (node instanceof SequenceNode sequence) {
+ return sequence.getValue().stream().map(child -> value(context, child)).toList();
}
- Node node = values.get(name);
- String raw = asText(node);
- try {
- return Double.parseDouble(context.resolvePropertyPlaceholders(raw));
- } catch (NumberFormatException e) {
- throw new YamlDeserializationException(
- node,
- "Invalid numeric value for '" + name + "' in semantic question '" + question + "': " + raw
- + " (expert '" + expert + "')",
- e);
+ if (Tag.FLOAT.equals(node.getTag()) || NUMBER.equals(node.getTag())) {
+ try {
+ return new BigDecimal(context.resolvePropertyPlaceholders(asText(node)));
+ } catch (NumberFormatException invalid) {
+ throw new YamlDeserializationException(node, "Invalid numeric parameter");
+ }
}
+ return new StandardConstructor(LoadSettings.builder().build()).constructSingleDocument(Optional.of(node));
Review Comment:
The allocation is real, but `StandardConstructor` is not stateless. In the pinned SnakeYAML Engine 3.1.1 sources, its `BaseConstructor` maintains mutable `constructedObjects`, `recursiveObjects`, `maps2fill` and `sets2fill`. Clearing them between documents permits sequential reuse, not concurrent calls from different loaders or contexts.
I have retained local construction to avoid introducing shared mutable state. Sharing only immutable settings or reusing a constructor within one parse could be considered if profiling establishes a loading bottleneck; the proposed static constructor is unsafe.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java:
##########
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.util.StringHelper;
+
+/** Fluent definition of an expert-owned evaluation. All DSLs build the same immutable declaration. */
+public final class SemanticEvaluationBuilder {
+ private final SemanticEvaluationsBuilder parent;
+ private final Map<String, Object> parameters = new LinkedHashMap<>();
+ private final Map<String, String> criteria = new LinkedHashMap<>();
+ private final List<String> levels = new ArrayList<>();
+ private String threshold;
+ private String uncertainty;
+ private boolean normalizeUncertaintyPolicy;
+ private String operation;
+ private String expert;
+ private String state;
+
+ /** Create a standalone declaration, completed with {@link #build(CamelContext)}. */
+ public SemanticEvaluationBuilder() {
+ this(null);
+ }
+
+ SemanticEvaluationBuilder(SemanticEvaluationsBuilder parent) {
+ this.parent = parent;
+ }
+
+ public SemanticEvaluationBuilder operation(String operation) {
+ this.operation = operation;
+ return this;
+ }
+
+ /** Select an instruction-driven operation by type, such as boolean, choice or score. */
+ public SemanticEvaluationBuilder type(String type) {
+ return operation(type.toLowerCase(Locale.ROOT));
+ }
+
+ public SemanticEvaluationBuilder expert(String expert) {
+ this.expert = expert;
+ return this;
+ }
+
+ String getExpert() {
+ return expert != null ? expert : parent != null ? parent.getExpert() : null;
+ }
+
+ public SemanticEvaluationBuilder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameter(String name, Object value) {
+ if (parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic parameter: " + name);
+ }
+ parameters.put(name, value);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder parameters(Map<String, ?> values) {
+ values.forEach(this::parameter);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder instructions(String instructions) {
+ return parameter("instructions", instructions);
+ }
+
+ public SemanticEvaluationBuilder criterion(String name, String description) {
+ if (criteria.containsKey(name)) {
+ throw new IllegalArgumentException("Duplicate semantic criterion: " + name);
+ }
+ criteria.put(name, description);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder level(String level) {
+ levels.add(level);
+ return this;
+ }
+
+ public SemanticEvaluationBuilder threshold(double threshold) {
+ return parameter("threshold", threshold);
+ }
+
+ public SemanticEvaluationBuilder threshold(String threshold) {
+ this.threshold = threshold;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertainty(double uncertainty) {
+ return parameter("uncertainty", uncertainty);
+ }
+
+ public SemanticEvaluationBuilder uncertainty(String uncertainty) {
+ this.uncertainty = uncertainty;
+ return this;
+ }
+
+ public SemanticEvaluationBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
+ return this;
+ }
+
+ public SemanticEvaluationsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
+ return parent;
+ }
+
+ public void register() {
+ end().register();
+ }
+
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticEvaluation build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
+ }
+ values.replaceAll((name, value) -> resolve(context, value));
+ if (normalizeUncertaintyPolicy && values.get("uncertaintyPolicy") instanceof String policy) {
+ values.put("uncertaintyPolicy",
+ StringHelper.asEnumConstantValue(policy).toLowerCase(Locale.ROOT).replace('_', '-'));
+ }
+ return new SemanticEvaluation(
+ operation, getExpert(), state != null ? state : parent != null ? parent.getState() : null, values);
+ }
+
+ private static Object resolve(CamelContext context, Object value) {
+ if (value instanceof String text) {
+ return context.resolvePropertyPlaceholders(text);
+ }
+ if (value instanceof Map<?, ?> map) {
+ Map<Object, Object> resolved = new LinkedHashMap<>();
Review Comment:
Non-string keys are already rejected by `SemanticEvaluation.immutableValue()` with `IllegalArgumentException("Parameter maps require string keys")`; they are not silently accepted. `resolve()` also processes Java builder input, where arbitrary nested maps can reach this intermediate step.
The proposed cast would instead throw `ClassCastException` before the common declaration validation and its error wrapping. I retained the intermediate map and reran the existing `nonStringNestedKeysFailWithValidationError()` regression during assessment. A checked conversion would duplicate the validation without changing accepted input.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4217005674
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluationBuilder.java:
##########
@@ -78,84 +96,89 @@ public SemanticQuestionBuilder criterion(String name, String description) {
return this;
}
- /** Add an ordered score level. */
public SemanticQuestionBuilder level(String level) {
levels.add(level);
return this;
}
- /** Boolean decision threshold; defaults to 0.5. */
public SemanticQuestionBuilder threshold(double threshold) {
- return threshold(Double.toString(threshold));
+ return parameter("threshold", threshold);
}
- /** Boolean decision threshold, optionally using property placeholders. */
public SemanticQuestionBuilder threshold(String threshold) {
this.threshold = threshold;
return this;
}
- /** Boolean uncertainty band; defaults to zero. */
public SemanticQuestionBuilder uncertainty(double uncertainty) {
- return uncertainty(Double.toString(uncertainty));
+ return parameter("uncertainty", uncertainty);
}
- /** Boolean uncertainty band, optionally using property placeholders. */
public SemanticQuestionBuilder uncertainty(String uncertainty) {
this.uncertainty = uncertainty;
return this;
}
- /** Boolean uncertainty policy: fail (default) or non-match. */
- public SemanticQuestionBuilder uncertaintyPolicy(String uncertaintyPolicy) {
- this.uncertaintyPolicy = uncertaintyPolicy;
+ public SemanticQuestionBuilder uncertaintyPolicy(String policy) {
+ parameter("uncertaintyPolicy", policy);
+ normalizeUncertaintyPolicy = true;
return this;
}
- /** Return to the group to add another question. */
public SemanticQuestionsBuilder end() {
+ if (parent == null) {
+ throw new IllegalStateException("Complete a standalone declaration with build(context)");
+ }
return parent;
}
- /** Validate and register the entire group. */
public void register() {
- parent.register();
+ end().register();
}
- SemanticQuestion build(CamelContext context) {
- if (type == null) {
- throw new IllegalArgumentException("Question type is required");
+ /** Build an immutable declaration, resolving placeholders in parameter values while retaining their types. */
+ public SemanticQuestion build(CamelContext context) {
+ Map<String, Object> values = new LinkedHashMap<>(parameters);
+ if (!criteria.isEmpty() || !levels.isEmpty()) {
+ if (values.containsKey("criteria") || !criteria.isEmpty() && !levels.isEmpty()) {
+ throw new IllegalArgumentException("Duplicate parameter 'criteria'");
+ }
+ values.put("criteria", !criteria.isEmpty() ? criteria : levels);
+ }
+ for (String numeric : List.of("threshold", "uncertainty")) {
+ String text = numeric.equals("threshold") ? threshold : uncertainty;
+ if (text != null) {
+ if (values.containsKey(numeric)) {
+ throw new IllegalArgumentException("Duplicate parameter '" + numeric + "'");
+ }
+ try {
+ values.put(numeric, Double.valueOf(context.resolvePropertyPlaceholders(text)));
+ } catch (NumberFormatException invalid) {
+ throw new IllegalArgumentException("Parameter '" + numeric + "' must be a valid number");
+ }
+ }
}
- SemanticQuestion.Type questionType = enumeration(type, SemanticQuestion.Type.class);
- if (questionType != SemanticQuestion.Type.BOOLEAN
- && (threshold != null || uncertainty != null || uncertaintyPolicy != null)) {
- throw new IllegalArgumentException("Threshold and uncertainty policy require a boolean question");
+ values.replaceAll((name, value) -> resolve(context, value));
Review Comment:
Addressed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). The docs now explain that resolved string parameters remain strings and show a nested expert policy using YAML `!number "{content.cutoff:0.8}"`. XML uses `<number>` and Java passes an explicitly converted number after resolving the placeholder; these work for any expert parameter. Literal prompt braces use Camel placeholder escaping, which is documented and covered by `stringParametersRemainStringsAndLiteralPromptPlaceholdersCanBeEscaped()`.
_AI-generated by Codex on behalf of @luigidemasi._
##########
dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java:
##########
@@ -126,6 +126,9 @@ public boolean isSupportedExtension(String extension) {
}
protected RouteBuilder builder(final YamlDeserializationContext ctx, final Node root) {
+ // All resources have prepared their beans before declarations are validated and routes constructed.
+ setDeserializationContext(root, ctx);
+ ctx.preParse(root);
Review Comment:
Addressed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). The deferred hook now preserves `YamlDeserializationException` node locations and wraps other failures with `Error pre-parsing resource: <location>`. The PR description explicitly explains why declaration pre-parsing runs after beans have been prepared: a reload can introduce its expert bean and declarations together.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticCapabilities.java:
##########
@@ -138,221 +86,309 @@ public String getArtifactId() {
return artifactId;
}
- public Set<InputType> getInputTypes() {
- return inputTypes;
- }
-
- public Set<ResultType> getResultTypes() {
- return resultTypes;
+ public Map<String, Operation> getOperations() {
+ return operations;
}
- public Instructions getInstructions() {
- return instructions;
+ public Operation operation(String name) {
+ Operation result = operations.get(name);
+ if (result == null) {
+ throw new IllegalArgumentException(
+ "Unknown operation '" + name + "'; supported operations: " + operations.keySet());
+ }
+ return result;
}
- public boolean isCallerDefinedCriteria() {
- return callerDefinedCriteria;
+ public void validate(SemanticEvaluation evaluation) {
+ operation(evaluation.getOperation()).validate(evaluation.getParameters());
}
- public boolean isBooleanProbability() {
- return booleanProbability;
+ private static String required(String value, String field) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return value;
}
- public boolean isChoiceProbabilities() {
- return choiceProbabilities;
+ private static boolean withinBounds(Number number, double minimum, double maximum) {
+ if (minimum == Double.POSITIVE_INFINITY || maximum == Double.NEGATIVE_INFINITY) {
+ return false;
+ }
+ try {
+ // Preserve decimal precision, including values just outside a declared boundary.
+ BigDecimal value = new BigDecimal(number.toString());
+ return (minimum == Double.NEGATIVE_INFINITY || value.compareTo(BigDecimal.valueOf(minimum)) >= 0)
+ && (maximum == Double.POSITIVE_INFINITY || value.compareTo(BigDecimal.valueOf(maximum)) <= 0);
+ } catch (NumberFormatException invalid) {
+ return false;
+ }
}
- public Set<ResultType> getConfidenceTypes() {
- return confidenceTypes;
- }
+ /** Operation metadata and common validation, independent of any expert instance. */
+ public static final class Operation {
+ private final SemanticOperation declaration;
+ private final Set<InputType> inputTypes;
+ private final Set<String> labels;
+ private final Map<String, Parameter> parameters;
+
+ private Operation(SemanticOperation declaration) {
+ this.declaration = declaration;
+ required(declaration.name(), "Operation name");
+ required(declaration.description(), "Operation description");
+ required(declaration.inputRequirements(), "Operation input requirements");
+ required(declaration.resultMeaning(), "Operation result meaning");
+ inputTypes = Set.copyOf(List.of(declaration.inputTypes()));
+ labels = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(declaration.labels())));
+ if (inputTypes.isEmpty() || Double.isNaN(declaration.minimum()) || Double.isNaN(declaration.maximum())
+ || declaration.minimum() > declaration.maximum() || labels.stream().anyMatch(String::isBlank)) {
+ throw new IllegalArgumentException("Invalid operation contract: " + declaration.name());
+ }
+ if (declaration.probability() || declaration.probabilities()) {
+ required(declaration.probabilityMeaning(), "Probability meaning");
+ }
+ if (declaration.confidence()) {
+ required(declaration.confidenceMeaning(), "Confidence meaning");
+ }
+ Map<String, Parameter> values = new LinkedHashMap<>();
+ for (SemanticParameter parameter : declaration.parameters()) {
+ Parameter contract = new Parameter(parameter);
+ if (values.putIfAbsent(contract.getName(), contract) != null) {
+ throw new IllegalArgumentException("Duplicate parameter: " + contract.getName());
+ }
+ }
+ parameters = Collections.unmodifiableMap(values);
+ }
- public String getProbabilityMeaning() {
- return probabilityMeaning;
- }
+ public String getName() {
+ return declaration.name();
+ }
- public String getConfidenceMeaning() {
- return confidenceMeaning;
- }
+ public String getDescription() {
+ return declaration.description();
+ }
- public String getTrueMeaning() {
- return trueMeaning;
- }
+ public Set<InputType> getInputTypes() {
+ return inputTypes;
+ }
- public int getMaxChoices() {
- return maxChoices;
- }
+ public String getInputRequirements() {
+ return declaration.inputRequirements();
+ }
- public int getMaxScoreLevels() {
- return maxScoreLevels;
- }
+ public Map<String, Parameter> getParameters() {
+ return parameters;
+ }
- /** Validate the common contract before the provider checks any model-specific restrictions. */
- public void validate(SemanticQuestion question) {
- if ((!known || instructions == Instructions.REQUIRED) && question.getInstructions() == null) {
- throw new IllegalArgumentException("Question instructions are required");
+ public ResultType getResultType() {
+ return declaration.resultType();
}
- if (!known) {
- return;
+
+ public String getResultMeaning() {
+ return declaration.resultMeaning();
}
- if (!resultTypes.contains(ResultType.valueOf(question.getType().name()))) {
- throw new IllegalArgumentException(
- "Requires " + question.getType() + "; supports " + resultTypes + ": " + description);
+
+ public Set<String> getLabels() {
+ return labels;
}
- if (instructions == Instructions.UNSUPPORTED && question.getInstructions() != null) {
- throw new IllegalArgumentException("Instructions are unsupported by this fixed expert");
+
+ public double getMinimum() {
+ return declaration.minimum();
}
- if (!callerDefinedCriteria && (!question.getCriteria().isEmpty() || !question.getLevels().isEmpty())) {
- throw new IllegalArgumentException("Caller-defined criteria and score levels are unsupported by this fixed expert");
+
+ public double getMaximum() {
+ return declaration.maximum();
}
- if (question.getType() == SemanticQuestion.Type.CHOICE && maxChoices > 0
- && question.getCriteria().size() > maxChoices) {
- throw new IllegalArgumentException("Supports at most " + maxChoices + " choice criteria");
+
+ public boolean isProbability() {
+ return declaration.probability();
}
- if (question.getType() == SemanticQuestion.Type.SCORE && maxScoreLevels > 0
- && question.getLevels().size() > maxScoreLevels) {
- throw new IllegalArgumentException("Supports at most " + maxScoreLevels + " score levels");
+
+ public boolean isProbabilities() {
+ return declaration.probabilities();
}
- if (question.getType() == SemanticQuestion.Type.BOOLEAN && !booleanProbability
- && (question.getThreshold() != 0.5 || question.getUncertainty() != 0
- || question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL)) {
- throw new IllegalArgumentException("Boolean decision policy requires positive-class probability support");
+
+ public String getProbabilityMeaning() {
+ return declaration.probabilityMeaning();
}
- }
- /** Input shape can only be checked once the state selector has been evaluated. */
- public void validateInput(Object state) {
- if (known && !(state instanceof String && inputTypes.contains(InputType.TEXT)
- || (state instanceof Map<?, ?> || state instanceof List<?>) && inputTypes.contains(InputType.STRUCTURED))) {
- throw new IllegalArgumentException("Unsupported selected state; accepts " + inputTypes + " input");
+ public boolean isConfidence() {
+ return declaration.confidence();
}
- }
- /** Named construction of an immutable, explicitly supported contract. */
- public static final class Builder {
- private String name;
- private String description;
- private String provider;
- private String artifactId;
- private Set<InputType> inputTypes = Set.of();
- private Set<ResultType> resultTypes = Set.of();
- private Instructions instructions = Instructions.REQUIRED;
- private boolean callerDefinedCriteria;
- private boolean booleanProbability;
- private boolean choiceProbabilities;
- private Set<ResultType> confidenceTypes = Set.of();
- private String probabilityMeaning;
- private String confidenceMeaning;
- private String trueMeaning;
- private int maxChoices;
- private int maxScoreLevels;
+ public String getConfidenceMeaning() {
+ return declaration.confidenceMeaning();
+ }
- private Builder() {
+ public void validate(Map<String, Object> values) {
+ for (String name : values.keySet()) {
+ if (!parameters.containsKey(name)) {
+ throw new IllegalArgumentException("Unknown parameter '" + name + "' for operation '" + getName() + "'");
+ }
+ }
+ parameters.forEach((name, parameter) -> parameter.validate(values.get(name), values.containsKey(name)));
Review Comment:
Confirmed at the current head: `SemanticEvaluation.immutableValue()` rejects blank nested map keys, including maps inside lists, before an adapter sees the declaration. [d338ec439c24](https://github.com/apache/camel/commit/d338ec439c247a869f0f54b88e8dab71e06f9389) made the recursive copy single-pass while retaining that validation; `nestedParameterMapsRejectInvalidKeys()` covers the reported inputs. Resolving with the existing validation and regression coverage.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticEvaluations.java:
##########
@@ -0,0 +1,183 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.spi.Resource;
+
+/** Context-local named evaluations, replaced atomically per source when a route resource is reloaded. */
+public final class SemanticEvaluations {
+ private static final Object CREATION_LOCK = new Object();
Review Comment:
The unlocked lookup and double-check in [d338ec439c24](https://github.com/apache/camel/commit/d338ec439c247a869f0f54b88e8dab71e06f9389) remove the shared lock from already-created registry lookups. The private monitor is retained for first creation: locking on the public context would violate the existing monitor-isolation regression. `cachedRegistryLookupDoesNotWaitForAnotherContext()` covers the optimized path. Resolving with that limited, intentional first-creation trade-off.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi commented on code in PR #27494:
URL: https://github.com/apache/camel/pull/27494#discussion_r4216999298
##########
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java:
##########
@@ -102,10 +103,7 @@ public void init(CamelContext context) {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- Object value = exp.evaluate(exchange, Object.class);
- // the same rule the language uses for a predicate on its own, where ${body} is true and a missing
- // header is false, so !${body} and !${header.foo} answer the opposite of those (CAMEL-24984)
- boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
Review Comment:
Fixed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). `createNotExpression()` creates the predicate once, outside `evaluate()`, and retains the CAMEL-24984 explanation. Existing expressions keep value-based negation, including body and missing-header behavior; only the new `SimplePredicateExpression` opt-in uses the predicate contract. `SimpleNotOperatorTest` covers compatibility.
_AI-generated by Codex on behalf of @luigidemasi._
##########
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java:
##########
@@ -148,9 +149,8 @@ public void init(CamelContext context) {
@Override
public <T> T evaluate(Exchange exchange, Class<T> type) {
- Object value = exp.evaluate(exchange, Object.class);
- // the same rule the language uses for a predicate on its own (CAMEL-24984)
- boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ // Preserve a function's predicate contract, including semantic's boolean-only guard.
+ boolean matches = ExpressionToPredicateAdapter.toPredicate(exp).matches(exchange);
Review Comment:
Fixed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). `createNegatedExpression()` builds its predicate once and reuses it for evaluation. The CAMEL-24984 comment and existing value-based semantics are retained; semantic expressions opt into predicate semantics through `SimplePredicateExpression`.
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java:
##########
@@ -120,10 +187,47 @@ private SemanticResult result(SemanticQuestion question, JsonObject answer, Json
if (answer.get("probabilities") instanceof Map<?, ?> values) {
values.forEach((key, value) -> probabilities.put((String) key, ((Number) value).doubleValue()));
}
- return new SemanticResult(
- question.getType() == SemanticQuestion.Type.BOOLEAN ? null : answer.get(type(question)),
- question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null,
+ Double probability = question.getType() == SemanticQuestion.Type.BOOLEAN ? answer.getDouble("noul") : null;
+ Object value = switch (question.getType()) {
+ case BOOLEAN -> booleanDecision(question, probability);
Review Comment:
The HTTP client rejects a missing `noul` with `IOException: Invalid TypeSafe AI response: Expected a number` before this mapping code runs. [96e6d465f016](https://github.com/apache/camel/commit/96e6d465f016856fb43c7e95b8e67818cda8112d) adds adapter-level HTTP regressions for missing `noul`, the named answer and `answers`. No duplicate null guard is needed here. Resolving following your confirmation in [your latest review](https://github.com/apache/camel/pull/27494#pullrequestreview-5453768092).
_AI-generated by Codex on behalf of @luigidemasi._
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestion.java:
##########
@@ -16,118 +16,178 @@
*/
package org.apache.camel.semantic;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
-/**
- * Immutable question definition. Probabilities and thresholds are provider-specific evidence, not accuracy guarantees.
- */
+/** Immutable evaluation declaration. The expert defines the operation, parameter vocabulary and result semantics. */
public final class SemanticQuestion {
+ private static final Set<Class<?>> NUMBER_TYPES = Set.of(Byte.class, Short.class, Integer.class, Long.class,
+ Float.class, Double.class, BigInteger.class, BigDecimal.class);
+
public enum Type {
Review Comment:
Addressed in [2ad0b3a75251](https://github.com/apache/camel/commit/2ad0b3a75251700121823e954c49f9f975ef170d). The generic declaration is now `SemanticEvaluation`, containing only operation, expert, state and immutable parameters. The generic TypeSafe-specific enum, convenience constructors and policy getters were removed; operation mapping, policy defaults and interpretation live in `TypeSafeAiSemanticAdapter`.
_AI-generated by Codex on behalf of @luigidemasi._
--
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-25382: Introduce expert-owned semantic evaluation contracts [camel]
Posted by "luigidemasi (via GitHub)" <gi...@apache.org>.
luigidemasi merged PR #27494:
URL: https://github.com/apache/camel/pull/27494
--
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