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: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]