luigidemasi commented on code in PR #26813: URL: https://github.com/apache/camel/pull/26813#discussion_r4093269911
########## components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java: ########## @@ -0,0 +1,309 @@ +/* + * 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 bean name or fully qualified implementation class name, without a reference prefix. Registry + * lookup takes precedence over class resolution. 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) { + if (configured.isBlank() || configured.startsWith("#")) { + throw new IllegalArgumentException("Semantic adapter must be a bean name or class name without a # prefix"); + } + Object bean = context.getRegistry().lookupByName(configured); + if (bean != null) { + if (!(bean instanceof SemanticAdapter found)) { + throw new IllegalArgumentException( + "Semantic adapter bean does not implement SemanticAdapter: " + configured); + } + 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); + while (resources.hasMoreElements()) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader( + resources.nextElement().openStream(), StandardCharsets.UTF_8))) { + reader.lines().map(line -> line.split("#", 2)[0].trim()).filter(line -> !line.isEmpty()) + .forEach(candidates::add); + } + } + if (candidates.size() != 1) { + throw new IllegalArgumentException( + "Semantic language requires exactly one advertised adapter; found " + + candidates + ". Configure camel.language.semantic.adapter explicitly"); + } + className = candidates.iterator().next(); + } + Class<?> resolved = context.getClassResolver().resolveClass(className); + if (resolved == null) { + throw new IllegalArgumentException("No semantic adapter bean or class found: " + className); + } + Class<? extends SemanticAdapter> type = resolved.asSubclass(SemanticAdapter.class); + synchronized (context.getRegistry()) { Review Comment: Addressed in `7aa7aed20d23`. Adapter registration and identity-checked unbinding use a private context-local monitor. Concurrency tests cover competing language instances, public registry-monitor avoidance and a blocked constructor in one context while another context continues independently. _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: Addressed in `7aa7aed20d23`. Question-registry creation now uses a private static creation lock instead of the CamelContext monitor. Tests verify that concurrent callers receive the same instance and that creation completes while another thread holds the public context monitor. _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,309 @@ +/* + * 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 bean name or fully qualified implementation class name, without a reference prefix. Registry + * lookup takes precedence over class resolution. 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) { + if (configured.isBlank() || configured.startsWith("#")) { + throw new IllegalArgumentException("Semantic adapter must be a bean name or class name without a # prefix"); + } + Object bean = context.getRegistry().lookupByName(configured); + if (bean != null) { + if (!(bean instanceof SemanticAdapter found)) { + throw new IllegalArgumentException( + "Semantic adapter bean does not implement SemanticAdapter: " + configured); + } + 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: Addressed in `f39755996d0c`. Replaced the manual JDK service-file parser with Camel FactoryFinder discovery. Providers now advertise a standard semantic-adapter descriptor with class=..., generated for TypeSafe AI by @JdkService. A small preflight reads the standard properties to preserve the requirement that different advertised adapters fail before construction; repeated declarations of the same class remain valid. FactoryFinder resolves the selected class, and Camel's Injector performs construction after type checking. Tests cover ambiguity, malformed descriptors, duplicate declarations, a mismatched custom finder, constructor injection, explicit selection and the generated TypeSafe descriptor. _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: Addressed in `f39755996d0c`. Replaced the manual JDK service-file parser with Camel FactoryFinder discovery. Providers now advertise a standard semantic-adapter descriptor with class=..., generated for TypeSafe AI by @JdkService. A small preflight reads the standard properties to preserve the requirement that different advertised adapters fail before construction; repeated declarations of the same class remain valid. FactoryFinder resolves the selected class, and Camel's Injector performs construction after type checking. Tests cover ambiguity, malformed descriptors, duplicate declarations, a mismatched custom finder, constructor injection, explicit selection and the generated TypeSafe descriptor. A native-image build was not run, so this does not claim native-image verification. _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: Addressed in `7aa7aed20d23` and `9ac532851f76`. Enum parsing now uses the common YAML `asEnum` helper, with the question name, field and original cause retained in a located exception. Constructor validation and structural errors also carry YAML source marks. Tests exercise invalid/empty enums, constructor validation and malformed declarations with schema validation disabled. _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]
