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