luigidemasi commented on code in PR #26813:
URL: https://github.com/apache/camel/pull/26813#discussion_r4090767592


##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java:
##########
@@ -0,0 +1,301 @@
+/*
+ * 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("#bean:")) {
+            Object bean = 
context.getRegistry().lookupByName(configured.substring(6));
+            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);
+                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();
+            }
+            if (className.startsWith("#class:")) {
+                className = className.substring(7);
+            }
+            Class<? extends SemanticAdapter> type
+                    = 
context.getClassResolver().resolveMandatoryClass(className).asSubclass(SemanticAdapter.class);
+            synchronized (context.getRegistry()) {
+                if (context.getRegistry().lookupByName(ADAPTER_NAME) != null) {
+                    throw new IllegalArgumentException("Semantic adapter 
registry name is already bound: " + ADAPTER_NAME);
+                }
+                SemanticAdapter instance = 
context.getInjector().newInstance(type);
+                CamelContextAware.trySetCamelContext(instance, context);
+                owned = new ManagedAdapter(context, instance);
+                context.getRegistry().bind(ADAPTER_NAME, instance);
+            }
+            context.addService(owned, true, true);
+            selectedAdapter = owned.instance;
+            return selectedAdapter;
+        } catch (Exception failure) {
+            if (owned != null) {
+                try {
+                    context.removeService(owned);
+                    ServiceHelper.stopAndShutdownService(owned);
+                } catch (Exception cleanup) {
+                    failure.addSuppressed(cleanup);
+                }
+            }
+            throw RuntimeCamelException.wrapRuntimeCamelException(failure);
+        }
+    }
+
+    private static final class ManagedAdapter extends ServiceSupport {
+        private final CamelContext context;
+        private final SemanticAdapter instance;
+
+        private ManagedAdapter(CamelContext context, SemanticAdapter instance) 
{
+            this.context = context;
+            this.instance = instance;
+        }
+
+        @Override
+        protected void doInit() throws Exception {
+            ServiceHelper.initService(instance);
+        }
+
+        @Override
+        protected void doStart() throws Exception {
+            ServiceHelper.startService(instance);
+        }
+
+        @Override
+        protected void doStop() throws Exception {
+            ServiceHelper.stopService(instance);
+        }
+
+        @Override
+        protected void doShutdown() throws Exception {
+            try {
+                ServiceHelper.stopAndShutdownService(instance);
+            } finally {
+                if (context.getRegistry().lookupByName(ADAPTER_NAME) == 
instance) {
+                    context.getRegistry().unbind(ADAPTER_NAME);
+                }
+            }
+        }
+    }
+
+    private final class Evaluation extends ExpressionAdapter {
+        private final String name;
+        private final boolean predicate;
+        private volatile Compiled compiled;
+        private SemanticQuestions questions;
+        private SemanticAdapter provider;

Review Comment:
   Addressed in 
[0c5ba894fe80](https://github.com/apache/camel/commit/0c5ba894fe801efd3c184fa6f574394fc2d003ce).
 Both `questions` and `provider` are now `volatile`, making their visibility 
explicit. Initialization still needs to complete before evaluation; the change 
does not make those two operations atomic. The semantic module tests pass.
   
   _AI-generated by Codex on behalf of 
[luigidemasi](https://github.com/luigidemasi)._



##########
components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java:
##########
@@ -0,0 +1,86 @@
+/*
+ * 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.component.typesafeai;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.semantic.SemanticAdapter;
+import org.apache.camel.semantic.SemanticQuestion;
+import org.apache.camel.semantic.SemanticResult;
+import org.apache.camel.util.json.JsonObject;
+
+/** Maps common questions to TypeSafe AI using the component's configured, 
managed transport. */
+public class TypeSafeAiSemanticAdapter implements SemanticAdapter, 
CamelContextAware {
+    private CamelContext camelContext;
+    private volatile TypeSafeAiEndpoint endpoint;
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    @Override
+    public void validate(SemanticQuestion question) {
+        if (question.getType() == SemanticQuestion.Type.CHOICE && 
question.getCriteria().size() > 255
+                || question.getType() == SemanticQuestion.Type.SCORE && 
question.getLevels().size() > 10) {
+            throw new IllegalArgumentException("TypeSafe AI supports at most 
255 choice criteria or 10 score levels");
+        }
+        if (endpoint == null) {
+            synchronized (this) {
+                if (endpoint == null) {
+                    endpoint = 
camelContext.getEndpoint("typesafe-ai:semantic", TypeSafeAiEndpoint.class);
+                }
+            }
+        }
+    }
+
+    @Override
+    public SemanticResult evaluate(SemanticQuestion question, Object state) 
throws Exception {
+        Map<String, Object> definition = new HashMap<>();
+        definition.put("instructions", question.getInstructions());
+        String type = switch (question.getType()) {
+            case BOOLEAN -> "noul";
+            case CHOICE -> "choice";
+            case SCORE -> "score";
+        };
+        definition.put("type", type);
+        if (question.getType() == SemanticQuestion.Type.SCORE) {
+            definition.put("criteria", question.getLevels());
+        } else if (!question.getCriteria().isEmpty()) {
+            definition.put("criteria", question.getCriteria());
+        }
+        JsonObject response = endpoint.evaluate(Map.of("state", state, 
"questions", Map.of("question", definition)));

Review Comment:
   Addressed in 
[0c5ba894fe80](https://github.com/apache/camel/commit/0c5ba894fe801efd3c184fa6f574394fc2d003ce).
 `evaluate()` now obtains the endpoint through a synchronized lazy initializer 
and checks the question capabilities itself. `validate()` only checks 
capabilities, so direct evaluation no longer depends on validation initializing 
the transport. A missing Camel context produces an explicit 
`IllegalStateException`. Added a regression test that evaluates without prior 
validation and verifies the configured credentials reach the HTTP fixture. All 
134 TypeSafe AI tests pass.
   
   _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]

Reply via email to