luigidemasi commented on code in PR #26813: URL: https://github.com/apache/camel/pull/26813#discussion_r4091903158
########## components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguageConfigurer.java: ########## @@ -0,0 +1,40 @@ +/* + * 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.semantic; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.PropertyConfigurer; + +/** Preserves adapter references so the language can validate their type and manage only instances it creates. */ +public class SemanticLanguageConfigurer implements PropertyConfigurer { + @Override + public boolean configureRaw(CamelContext context, Object target, String name, Object value, boolean ignoreCase) { + if (ignoreCase ? "adapter".equalsIgnoreCase(name) : "adapter".equals(name)) { + if (!(value instanceof String)) { + throw new IllegalArgumentException("Semantic adapter selection must be a bean reference or class name"); + } + ((SemanticLanguage) target).setAdapter((String) value); + return true; + } + return false; + } + + @Override + public boolean configure(CamelContext context, Object target, String name, Object value, boolean ignoreCase) { Review Comment: Removed in 7c10c75e1bf5 together with both service registrations. The language uses standard property binding for `adapter` and `defaultState`; Camel Main tests cover bean names, class names and property placeholders. _AI-generated by Codex on behalf of [luigidemasi](https://github.com/luigidemasi)._ ########## components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.dsl.yaml.deserializers; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.camel.CamelContext; +import org.apache.camel.dsl.yaml.common.YamlDeserializationContext; +import org.apache.camel.dsl.yaml.common.YamlDeserializerSupport; +import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException; +import org.apache.camel.semantic.SemanticQuestion; +import org.apache.camel.semantic.SemanticQuestions; +import org.apache.camel.spi.CamelContextCustomizer; +import org.apache.camel.spi.annotations.YamlIn; +import org.apache.camel.spi.annotations.YamlProperty; +import org.apache.camel.spi.annotations.YamlType; +import org.snakeyaml.engine.v2.api.ConstructNode; +import org.snakeyaml.engine.v2.nodes.Node; +import org.snakeyaml.engine.v2.nodes.NodeTuple; +import org.snakeyaml.engine.v2.nodes.SequenceNode; + +/** Named semantic declarations are installed in a resource-wide pass before route references are resolved. */ +@YamlIn +@YamlType(nodes = "semantic", properties = { + @YamlProperty(name = "question", + type = "map:org.apache.camel.dsl.yaml.deserializers.SemanticDefinitionDeserializer$QuestionSchema", + required = true) +}) +public class SemanticDefinitionDeserializer extends YamlDeserializerSupport implements ConstructNode { + private static final Set<String> FIELDS + = Set.of("type", "instructions", "state", "criteria", "threshold", "uncertainty", "uncertaintyPolicy"); + + @Override + public Object construct(Node node) { + read(node); + // Registration happens once for the entire resource, including declarations after routes. + return (CamelContextCustomizer) context -> { + }; + } + + public static void configure(CamelContext context, YamlDeserializationContext dc, Node root) { + if (!(root instanceof SequenceNode sequence)) { + return; + } + Map<String, SemanticQuestion> definitions = new LinkedHashMap<>(); + for (Node node : sequence.getValue()) { + for (NodeTuple tuple : asMappingNode(node).getValue()) { + if ("semantic".equals(asText(tuple.getKeyNode()))) { + read(tuple.getValueNode()).forEach((name, question) -> { + if (definitions.putIfAbsent(name, question) != null) { + throw new IllegalArgumentException("Duplicate semantic question: " + name); + } + }); + } + } + } + SemanticQuestions.get(context).replace(dc.getResource(), definitions); + } + + private static Map<String, SemanticQuestion> read(Node node) { + Map<String, Node> semantic = fields(node); + if (!semantic.keySet().equals(Set.of("question"))) { + throw new IllegalArgumentException("Semantic declaration requires only question"); + } + Map<String, SemanticQuestion> result = new LinkedHashMap<>(); + fields(semantic.get("question")).forEach((name, definition) -> { + Map<String, Node> values = fields(definition); + if (!FIELDS.containsAll(values.keySet())) { + throw new IllegalArgumentException("Unknown property in semantic question: " + name); + } + String typeName = asText(values.get("type")); + if (typeName == null) { + throw new IllegalArgumentException("Semantic question type is required: " + name); + } + SemanticQuestion.Type type = SemanticQuestion.Type.valueOf(typeName.toUpperCase(Locale.ROOT)); Review Comment: The diagnostic gap is valid and remains open in this update; moving the parser into `camel-semantic` does not fix it. I reproduced unlocated enum failures. One detail differs from the report: an empty policy scalar produces an `IllegalArgumentException` from enum parsing, rather than an NPE, because `asText` returns an empty string. Located diagnostics for enum and structural errors still need follow-up. _AI-generated by Codex on behalf of [luigidemasi](https://github.com/luigidemasi)._ ########## components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java: ########## @@ -0,0 +1,88 @@ +/* + * 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.HashMap; +import java.util.Map; + +import org.apache.camel.CamelContext; +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 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(); + + public static SemanticQuestions get(CamelContext context) { + synchronized (context) { Review Comment: Agreed that private coordination is preferable. This remains open in this update. A replacement must still coordinate separate language instances sharing one context/registry; simply changing both monitors to per-instance locks would lose that property. No actual deadlock was established during this assessment. _AI-generated by Codex on behalf of [luigidemasi](https://github.com/luigidemasi)._ ########## components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java: ########## @@ -0,0 +1,307 @@ +/* + * 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.semantic; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.Exchange; +import org.apache.camel.Expression; +import org.apache.camel.Predicate; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.semantic.SemanticAdapter; +import org.apache.camel.semantic.SemanticQuestion; +import org.apache.camel.semantic.SemanticQuestions; +import org.apache.camel.semantic.SemanticResult; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.annotations.Language; +import org.apache.camel.support.ExpressionAdapter; +import org.apache.camel.support.LanguageSupport; +import org.apache.camel.support.service.ServiceHelper; +import org.apache.camel.support.service.ServiceSupport; + +/** Evaluates a named, provider-independent question against selected message state. */ +@Language(value = "semantic", modelName = "language") +@Metadata(title = "Semantic", description = "Evaluate named semantic questions through a provider adapter", + label = "language,ai", firstVersion = "4.23.0") +public class SemanticLanguage extends LanguageSupport { + public static final String RESULT = "CamelSemanticResult"; + public static final String ADAPTER_NAME = "camelSemanticAdapter"; + public static final String ADAPTER_RESOURCE = "META-INF/services/org.apache.camel.semantic.SemanticAdapter"; + + private String adapter; + private String defaultState = "${body}"; + private SemanticAdapter selectedAdapter; + + public String getAdapter() { + return adapter; + } + + /** + * Adapter registry reference (#bean:name) or implementation class (plain FQCN or #class:FQCN). Absent selects the + * sole advertised adapter. + */ + public void setAdapter(String adapter) { + this.adapter = adapter; + } + + public String getDefaultState() { + return defaultState; + } + + /** Default Simple state selector for questions without their own selector. Defaults to the body. */ + public void setDefaultState(String defaultState) { + this.defaultState = defaultState; + } + + @Override + public Expression createExpression(String expression) { + return createEvaluation(expression, false); + } + + @Override + public Predicate createPredicate(String expression) { + return createEvaluation(expression, true); + } + + public boolean validateExpression(String expression) { + if (expression == null || !expression.startsWith("ref:") || expression.substring(4).isBlank()) { + throw new IllegalArgumentException("Semantic expression must reference a named question using ref:name"); + } + return true; + } + + public boolean validatePredicate(String expression) { + return validateExpression(expression); + } + + private Evaluation createEvaluation(String expression, boolean predicate) { + validateExpression(expression); + Evaluation evaluation = new Evaluation(expression.substring(4), predicate); + if (getCamelContext() != null) { + evaluation.init(getCamelContext()); + } + return evaluation; + } + + private synchronized SemanticAdapter adapter() { + if (selectedAdapter != null) { + return selectedAdapter; + } + CamelContext context = getCamelContext(); + String configured = adapter == null ? null : context.resolvePropertyPlaceholders(adapter); + if (configured != null && configured.startsWith("#") && !configured.startsWith("#class:")) { + String name = configured.startsWith("#bean:") ? configured.substring(6) : configured.substring(1); + if (name.contains(":")) { + throw new IllegalArgumentException("Semantic adapter reference must use #bean:name, #name or #class:FQCN"); + } + Object bean = context.getRegistry().lookupByName(name); + if (!(bean instanceof SemanticAdapter found)) { + throw new IllegalArgumentException("Semantic adapter bean is missing or does not implement SemanticAdapter"); + } + selectedAdapter = found; + return found; + } + ManagedAdapter owned = null; + try { + String className = configured; + if (className == null) { + Set<String> candidates = new TreeSet<>(); + Enumeration<URL> resources = context.getClassResolver().loadAllResourcesAsURL(ADAPTER_RESOURCE); Review Comment: The discovery implementation is unchanged in this update. `FactoryFinder.findClass` reads a single Camel `class=...` resource; it does not directly preserve enumeration, deduplication and ambiguity rejection across the current provider descriptors. A replacement needs to retain those semantics. Native-image discovery has not been validated, so that compatibility concern remains open. _AI-generated by Codex on behalf of [luigidemasi](https://github.com/luigidemasi)._ ########## core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ChoiceReifier.java: ########## @@ -49,6 +52,25 @@ public ChoiceReifier(Route route, ProcessorDefinition<?> definition) { @Override public Processor createProcessor() throws Exception { final boolean isPrecondition = Boolean.TRUE == parseBoolean(definition.getPrecondition()); + boolean selecting = definition.getSelector() != null; + if (selecting && isPrecondition) { + throw new IllegalArgumentException("Choice selector cannot be combined with precondition mode"); + } + Set<String> uniqueValues = new HashSet<>(); + for (WhenDefinition when : definition.getWhenClauses()) { + if (selecting) { + if (when.getValue() == null || when.getExpression() != null) { + throw new IllegalArgumentException("Choice selector requires literal when values without predicates"); + } + if (!uniqueValues.add(parseString(when.getValue()))) { + throw new IllegalArgumentException("Choice selector has duplicate when values"); + } + } else if (when.getValue() != null || when.getExpression() == null) { + throw new IllegalArgumentException("Choice without a selector requires when predicates without values"); + } + } + Expression selector = selecting ? createExpression(definition.getSelector().getExpressionType()) : null; Review Comment: Removed in 7c10c75e1bf5. `ChoiceReifier` matches the original base and no longer accepts or dereferences a selector. Semantic category routing uses existing Set Property and Choice expressions. _AI-generated by Codex on behalf of [luigidemasi](https://github.com/luigidemasi)._ ########## core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedChoice.java: ########## @@ -76,8 +76,9 @@ public TabularData extendedInformation() { FilterProcessor filter = filters.get(i); CompositeType ct = CamelOpenMBeanTypes.choiceCompositeType(); - String predicate = when.getExpression().getExpression(); - String language = when.getExpression().getLanguage(); + String predicate = when.getValue() != null ? when.getValue() : when.getExpression().getExpression(); + String language = getDefinition().getSelector() != null + ? getDefinition().getSelector().getExpressionType().getLanguage() : when.getExpression().getLanguage(); Review Comment: Removed in 7c10c75e1bf5. `ManagedChoice` matches the original base; its JMX path no longer contains selector handling. _AI-generated by Codex on behalf of [luigidemasi](https://github.com/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]
