luigidemasi commented on code in PR #27850: URL: https://github.com/apache/camel/pull/27850#discussion_r4276603758
########## components/camel-ai/camel-semantic-openai/src/main/java/org/apache/camel/semantic/openai/OpenAIDecisionsSemanticAdapter.java: ########## @@ -0,0 +1,368 @@ +/* + * 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.openai; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.openai.models.decisions.Decision; +import com.openai.models.decisions.DecisionChoiceOption; +import com.openai.models.decisions.DecisionCreateParams; +import com.openai.models.decisions.DecisionInputImage; +import com.openai.models.decisions.DecisionInputMessage; +import com.openai.models.decisions.DecisionInputPart; +import com.openai.models.decisions.DecisionInputText; +import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.ResolveEndpointFailedException; +import org.apache.camel.component.openai.OpenAIDecisionServiceException; +import org.apache.camel.component.openai.OpenAIDecisionsSupport; +import org.apache.camel.component.openai.OpenAIEndpoint; +import org.apache.camel.component.openai.OpenAIOperations; +import org.apache.camel.semantic.SemanticAdapter; +import org.apache.camel.semantic.SemanticCapabilities; +import org.apache.camel.semantic.SemanticEvaluation; +import org.apache.camel.semantic.SemanticExpert; +import org.apache.camel.semantic.SemanticExpert.InputType; +import org.apache.camel.semantic.SemanticExpert.ResultType; +import org.apache.camel.semantic.SemanticInput; +import org.apache.camel.semantic.SemanticOperation; +import org.apache.camel.semantic.SemanticParameter; +import org.apache.camel.semantic.SemanticResult; + +/** An explicitly registered expert; its OpenAI endpoint owns the client, credentials and transport lifecycle. */ +@SemanticExpert(name = "openai-decisions", provider = "openai", artifactId = "camel-semantic-openai", + description = "Typed text and image assessments using the OpenAI Decisions API", + operations = { + @SemanticOperation(name = "boolean", description = "Assess whether a condition holds", + inputTypes = { InputType.TEXT, InputType.IMAGE }, + inputRequirements = "Text, an image body with Content-Type, an image data URL, or an ordered $semanticInput envelope", + resultType = ResultType.BOOLEAN, + resultMeaning = "Predicate probability at or above threshold, subject to the configured uncertainty policy", + probability = true, + probabilityMeaning = "Provider probability that the condition is true", + parameters = { + @SemanticParameter(name = "instructions", + description = "Question to assess against the input", + required = true, minSize = 1, maxSize = 1048576), + @SemanticParameter(name = "threshold", + description = "Inclusive positive probability threshold", + type = Number.class, minimum = 0, maximum = 1, + omission = "Use 0.5"), + @SemanticParameter(name = "uncertainty", + description = "Half-width of the uncertainty band around the threshold", + type = Number.class, minimum = 0, maximum = 1, + omission = "No uncertainty band"), + @SemanticParameter(name = "uncertaintyPolicy", + description = "Behavior inside the uncertainty band", + values = { "fail", "non-match" }, + omission = "Fail the evaluation") }), + @SemanticOperation(name = "choice", description = "Select one declared category", + inputTypes = { InputType.TEXT, InputType.IMAGE }, + inputRequirements = "Text, an image body with Content-Type, an image data URL, or an ordered $semanticInput envelope", + resultType = ResultType.CHOICE, choiceLabelsParameter = "criteria", + noMatchDeclared = true, completeProbabilities = true, + resultMeaning = "The provider-selected category key", + probabilities = true, + probabilityMeaning = "Provider probability for each declared category", + confidence = true, + confidenceMeaning = "Confidence reported by OpenAI; no derived confidence is added", + parameters = { + @SemanticParameter(name = "instructions", + description = "Question to assess against the input", + required = true, minSize = 1, maxSize = 1048576), + @SemanticParameter(name = "criteria", + description = "Category keys and descriptions", + type = Map.class, itemType = String.class, + required = true, + minSize = 2, maxSize = 255) }), + @SemanticOperation(name = "score", description = "Assess input against ordered rubric levels", + inputTypes = { InputType.TEXT, InputType.IMAGE }, + inputRequirements = "Text, an image body with Content-Type, an image data URL, or an ordered $semanticInput envelope", + resultType = ResultType.SCORE, minimum = 0, scoreLevelsParameter = "criteria", + scoreDescriptionsParameter = "descriptions", completeProbabilities = true, + resultMeaning = "Provider probability-weighted zero-based rubric index; fractional scores are preserved", + probabilities = true, + probabilityMeaning = "Provider probability for each zero-based rubric index", + confidence = true, + confidenceMeaning = "Confidence reported by OpenAI; no derived confidence is added", + parameters = { + @SemanticParameter(name = "instructions", + description = "Question to assess against the input", + required = true, minSize = 1, maxSize = 1048576), + @SemanticParameter(name = "criteria", + description = "Distinct rubric labels ordered from lowest to highest", + type = List.class, itemType = String.class, + required = true, minSize = 1), + @SemanticParameter(name = "descriptions", + description = "Optional descriptions keyed by rubric label", + type = Map.class, itemType = String.class, + omission = "Use the rubric labels") }) }) +public class OpenAIDecisionsSemanticAdapter implements SemanticAdapter, CamelContextAware { + private static final SemanticCapabilities CAPABILITIES = SemanticCapabilities.from(OpenAIDecisionsSemanticAdapter.class); + private CamelContext camelContext; + private String endpointUri = "openai:decisions"; + private int maxImageBytes = 10_000_000; + private volatile OpenAIEndpoint endpoint; + + @Override + public CamelContext getCamelContext() { + return camelContext; + } + + @Override + public void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + } + + public String getEndpointUri() { + return endpointUri; + } + + @Override + public int getMaxImageBytes() { + return maxImageBytes; + } + + /** Maximum bytes per image before base64 encoding. Defaults to 10 MB. Configure before evaluation. */ + public void setMaxImageBytes(int maxImageBytes) { + if (maxImageBytes <= 0) { + throw new IllegalArgumentException("maxImageBytes must be greater than zero"); + } + this.maxImageBytes = maxImageBytes; + } + + /** Configured Decisions endpoint, including model and transport options. Set before the first evaluation. */ + public synchronized void setEndpointUri(String endpointUri) { + if (endpoint != null || endpointUri == null || endpointUri.isBlank()) { + throw new IllegalArgumentException("Set a nonblank Decisions endpoint URI before evaluation"); + } + this.endpointUri = endpointUri; + } + + private OpenAIEndpoint endpoint() { + if (camelContext != null && (camelContext.isStopping() || camelContext.isStopped())) { + throw new OpenAIDecisionServiceException("OpenAI Decisions requires a running CamelContext"); + } + if (endpoint == null) { + synchronized (this) { + if (endpoint == null) { + if (camelContext == null) { + throw new IllegalStateException("OpenAI Decisions expert requires a CamelContext"); + } + OpenAIEndpoint resolved; + try { + resolved = camelContext.getEndpoint(endpointUri, OpenAIEndpoint.class); + } catch (ResolveEndpointFailedException e) { + throw new OpenAIDecisionServiceException("Unable to initialize the OpenAI Decisions endpoint", e); + } + if (resolved.getOperation() != OpenAIOperations.decisions) { + throw new IllegalArgumentException("OpenAI Decisions expert requires a Decisions endpoint"); + } + endpoint = resolved; + } + } + } + return endpoint; + } + + @Override + public void validate(SemanticEvaluation evaluation) { + CAPABILITIES.validate(evaluation); + if ("boolean".equals(evaluation.getOperation())) { + double threshold = number(evaluation, "threshold", 0.5); + double uncertainty = number(evaluation, "uncertainty", 0); + if (threshold - uncertainty < 0 || threshold + uncertainty > 1) { + throw new IllegalArgumentException("Threshold and uncertainty must keep the band within [0,1]"); + } + } else if ("choice".equals(evaluation.getOperation())) { + descriptions(evaluation, "criteria").forEach((label, description) -> { + text(label); + text(description); + }); + } else { + List<String> levels = levels(evaluation); + if (new HashSet<>(levels).size() != levels.size()) { + throw new IllegalArgumentException("Score rubric labels must be distinct"); + } + levels.forEach(OpenAIDecisionsSemanticAdapter::text); + descriptions(evaluation, "descriptions").forEach((label, description) -> { + if (!levels.contains(label)) { + throw new IllegalArgumentException("Score descriptions must refer to declared rubric labels"); + } + text(description); + }); + } + } + + private static void text(String text) { + if (text.isBlank() || text.length() > 1_048_576) { + throw new IllegalArgumentException("Criteria require nonblank text within the provider text limit"); + } + } + + @Override + public void validateInput(SemanticEvaluation evaluation, Object state) { + state = SemanticInput.normalize(state); + CAPABILITIES.operation(evaluation.getOperation()).validateInput(state); + } + + @Override + public void validateState(Object state) { + SemanticInput.validateImageSize(SemanticInput.normalize(state), maxImageBytes); + OpenAIDecisionsSupport.validateInput(input(SemanticInput.normalize(state))); Review Comment: Fixed in [15d09dbef493](https://github.com/apache/camel/commit/15d09dbef493b7b9164a232187b966c52e5cf17d). `validateState()` now normalizes once and reuses the value for both checks. The `camel-semantic-openai` module build, source checks and all 20 tests passed. _AI-generated by OpenAI Codex on behalf of @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]
