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]
