davsclaus commented on code in PR #26813:
URL: https://github.com/apache/camel/pull/26813#discussion_r4092123479
##########
dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlCompletionMojo.java:
##########
@@ -418,7 +426,8 @@ private void enrichNodeMetadata(ObjectNode node, String
nodeName, String fqName,
}
// check for language model
- LanguageModel langModel = catalog.languageModel(nodeName);
+ LanguageModel langModel =
fqName.startsWith("org.apache.camel.model.language.")
+ ? catalog.languageModel(nodeName) : null;
Review Comment:
🟠**This is a fix for an unrelated bug, and it changes shipped metadata for
the `bean` EIP.**
Gating the language lookup on the model package stops the `bean` **EIP**
node (`org.apache.camel.model.BeanDefinition`) from picking up the `bean`
**language**'s catalog entry. I regenerated the schemas to confirm it is
deterministic output, and it is the only non-semantic change across the three
generated schema files:
```
- "title" : "Bean Method", "description" : "Calls a Java bean method",
"label" : "language,core,java",
+ "title" : "Bean", "description" : "Invokes a method on a Java
bean, ...", "label" : "eip,endpoint",
```
I think the new behaviour is right — completion was showing the language's
description on the EIP node because the two share a name. But it is a separate
bug fix to shared tooling riding along in an AI feature PR, unmentioned in the
description and untested.
Please either lift it into its own commit or PR, or describe it in the PR
body and add a test pinning `bean` to the EIP metadata so the collision cannot
quietly come back.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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.yaml;
+
+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.YamlDeserializerResolver;
+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.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema",
+ required = true)
+})
+public class SemanticDefinitionDeserializer extends YamlDeserializerSupport
implements ConstructNode, YamlDeserializerResolver {
+ private static final Set<String> FIELDS
+ = Set.of("type", "instructions", "state", "criteria", "threshold",
"uncertainty", "uncertaintyPolicy");
+
+ @Override
+ public ConstructNode resolve(String id) {
+ return "semantic".equals(id) ? this : null;
+ }
+
+ @Override
+ public Object construct(Node node) {
+ read(node);
+ // Registration happens once for the entire resource, including
declarations after routes.
+ return (CamelContextCustomizer) context -> {
+ };
+ }
+
+ @Override
+ public void preParse(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);
+ }
+ });
+ }
+ }
+ }
+ CamelContext context = dc.getCamelContext();
+ SemanticQuestions questions = definitions.isEmpty()
+ ?
context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
+ : SemanticQuestions.get(context);
+ if (questions != null) {
+ questions.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:
Item 5 from the previous round. `Enum.valueOf` throws a bare
`IllegalArgumentException` (`No enum constant SemanticQuestion.Type.BOOL`) with
no YAML location and no question name — while the numeric fields a few lines
below get a located `YamlDeserializationException`.
`YamlDeserializerSupport.asEnum` gives you the located error and is
case-insensitive:
```suggestion
SemanticQuestion.Type type = asEnum(values.get("type"),
SemanticQuestion.Type.class);
```
The `typeName == null` check above stays useful, since `asEnum` returns
`null` for a null node rather than complaining.
*Correction to my round-3 review:* I wrote that this path "can NPE". That
was wrong — `asText` returns `null` only for a null node and throws a located
`InvalidNodeTypeException` for a non-scalar. It is a bare
`IllegalArgumentException`, not an NPE. The remedy is unchanged.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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.yaml;
+
+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.YamlDeserializerResolver;
+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.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema",
+ required = true)
+})
+public class SemanticDefinitionDeserializer extends YamlDeserializerSupport
implements ConstructNode, YamlDeserializerResolver {
+ private static final Set<String> FIELDS
+ = Set.of("type", "instructions", "state", "criteria", "threshold",
"uncertainty", "uncertaintyPolicy");
+
+ @Override
+ public ConstructNode resolve(String id) {
+ return "semantic".equals(id) ? this : null;
+ }
+
+ @Override
+ public Object construct(Node node) {
+ read(node);
+ // Registration happens once for the entire resource, including
declarations after routes.
+ return (CamelContextCustomizer) context -> {
+ };
+ }
+
+ @Override
+ public void preParse(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);
+ }
+ });
+ }
+ }
+ }
+ CamelContext context = dc.getCamelContext();
+ SemanticQuestions questions = definitions.isEmpty()
+ ?
context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
+ : SemanticQuestions.get(context);
+ if (questions != null) {
+ questions.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));
+ 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")).forEach((key, value) ->
criteria.put(key, asText(value)));
+ }
+ }
+ if (type != SemanticQuestion.Type.BOOLEAN &&
(values.containsKey("threshold") || values.containsKey("uncertainty")
+ || values.containsKey("uncertaintyPolicy"))) {
+ throw new IllegalArgumentException("Threshold and uncertainty
policy require a boolean question: " + name);
+ }
+ SemanticQuestion.UncertaintyPolicy policy =
values.containsKey("uncertaintyPolicy")
+ ? SemanticQuestion.UncertaintyPolicy
+
.valueOf(asText(values.get("uncertaintyPolicy")).replace('-',
'_').toUpperCase(Locale.ROOT))
+ : SemanticQuestion.UncertaintyPolicy.FAIL;
Review Comment:
Same as the `type` enum above. `asEnum` also handles the dash form via
`StringHelper.asEnumConstantValue`, so `non-match` still resolves to
`NON_MATCH` and the manual `replace('-', '_').toUpperCase(...)` goes away:
```suggestion
SemanticQuestion.UncertaintyPolicy policy =
values.containsKey("uncertaintyPolicy")
? asEnum(values.get("uncertaintyPolicy"),
SemanticQuestion.UncertaintyPolicy.class)
: SemanticQuestion.UncertaintyPolicy.FAIL;
```
With both `valueOf` calls gone the `java.util.Locale` import on line 21
becomes unused — OpenRewrite and impsort will flag it, so drop it in the same
commit.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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.yaml;
+
+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.YamlDeserializerResolver;
+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.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema",
+ required = true)
+})
+public class SemanticDefinitionDeserializer extends YamlDeserializerSupport
implements ConstructNode, YamlDeserializerResolver {
+ private static final Set<String> FIELDS
+ = Set.of("type", "instructions", "state", "criteria", "threshold",
"uncertainty", "uncertaintyPolicy");
+
+ @Override
+ public ConstructNode resolve(String id) {
+ return "semantic".equals(id) ? this : null;
+ }
+
+ @Override
+ public Object construct(Node node) {
+ read(node);
+ // Registration happens once for the entire resource, including
declarations after routes.
+ return (CamelContextCustomizer) context -> {
+ };
+ }
+
+ @Override
+ public void preParse(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()) {
Review Comment:
🔵 `preParse` runs for **every** YAML resource now, including those with no
`semantic:` node, so this walks every top-level node of every route file.
`asMappingNode` throws `InvalidNodeTypeException` for anything that is not a
mapping, which means a malformed resource fails inside the *semantic* resolver
rather than in the normal deserialization path where the message makes sense to
the user.
An `instanceof MappingNode mapping` guard with a `continue` would leave the
reporting where it belongs. The 420 `camel-yaml-dsl` tests pass as-is, so this
is about the error path rather than a live break.
##########
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:
Item 7, confirmed still present. This reimplements service-file discovery —
reading the URLs, stripping `#` comments, trimming, deduplicating — where
`FactoryFinder` already does it, handles class loading, and is what the rest of
Camel uses for exactly this lookup.
Not a defect: the code is correct as written. It is hand-maintained parsing
of a format Camel already owns a reader for.
##########
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:
Item 6, the other half. `synchronized (context)` takes the monitor of the
`CamelContext`, an object reachable by every piece of framework and user code
in the JVM. A private lock object, or plugin-registration support on the
context extension if one exists, keeps the critical section without exporting
the monitor.
##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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.yaml;
+
+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.YamlDeserializerResolver;
+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.semantic.yaml.SemanticDefinitionDeserializer$QuestionSchema",
+ required = true)
+})
+public class SemanticDefinitionDeserializer extends YamlDeserializerSupport
implements ConstructNode, YamlDeserializerResolver {
+ private static final Set<String> FIELDS
+ = Set.of("type", "instructions", "state", "criteria", "threshold",
"uncertainty", "uncertaintyPolicy");
+
+ @Override
+ public ConstructNode resolve(String id) {
+ return "semantic".equals(id) ? this : null;
+ }
+
+ @Override
+ public Object construct(Node node) {
+ read(node);
+ // Registration happens once for the entire resource, including
declarations after routes.
+ return (CamelContextCustomizer) context -> {
+ };
+ }
+
+ @Override
+ public void preParse(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);
+ }
+ });
+ }
+ }
+ }
+ CamelContext context = dc.getCamelContext();
+ SemanticQuestions questions = definitions.isEmpty()
+ ?
context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)
+ : SemanticQuestions.get(context);
+ if (questions != null) {
+ questions.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));
+ 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")).forEach((key, value) ->
criteria.put(key, asText(value)));
+ }
+ }
+ if (type != SemanticQuestion.Type.BOOLEAN &&
(values.containsKey("threshold") || values.containsKey("uncertainty")
+ || values.containsKey("uncertaintyPolicy"))) {
+ throw new IllegalArgumentException("Threshold and uncertainty
policy require a boolean question: " + name);
+ }
+ SemanticQuestion.UncertaintyPolicy policy =
values.containsKey("uncertaintyPolicy")
+ ? SemanticQuestion.UncertaintyPolicy
+
.valueOf(asText(values.get("uncertaintyPolicy")).replace('-',
'_').toUpperCase(Locale.ROOT))
+ : SemanticQuestion.UncertaintyPolicy.FAIL;
+ result.put(name, new SemanticQuestion(
Review Comment:
The remainder of item 5: `SemanticQuestion`'s constructor validates blank
instructions, criteria shape, score levels, the threshold/uncertainty band and
the boolean criteria keys — every one with a bare `IllegalArgumentException`
carrying neither the YAML location nor the question name. A file missing
`instructions` reports `Question instructions must not be blank` with nothing
pointing at which question, or which line.
Wrapping just this construction closes the whole set at once:
```java
try {
result.put(name, new SemanticQuestion(...));
} catch (IllegalArgumentException | NullPointerException e) {
throw new YamlDeserializationException(definition, "Invalid semantic
question '" + name + "': " + e.getMessage(), e);
}
```
##########
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:
Item 6, confirmed still present at head. Synchronizing on the registry means
any other code that happens to lock that object participates in this class's
lock ordering, and neither side can see the other. A private lock owned by this
class gives the same mutual exclusion over the check-then-bind without
publishing the monitor.
Same point applies to `SemanticQuestions:32`, which locks the `CamelContext`
itself.
--
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]