luigidemasi commented on code in PR #26684: URL: https://github.com/apache/camel/pull/26684#discussion_r4069918818
########## components/camel-ai/camel-jev/src/main/java/org/apache/camel/component/jev/JevClient.java: ########## @@ -0,0 +1,137 @@ +/* + * 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.jev; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; + +/** HTTP transport shared by the endpoint's producer and predicates. */ +final class JevClient implements AutoCloseable { + private final HttpClient http; + private final URI uri; + private final String apiKey; + private final String model; + private final Duration timeout; + private final int maxConcurrentRequests; + private final Set<CompletableFuture<HttpResponse<String>>> pending = new HashSet<>(); + private int activeRequests; + private boolean closed; + + JevClient(JevConfiguration configuration) { + timeout = Duration.ofMillis(configuration.getRequestTimeout()); + maxConcurrentRequests = configuration.getMaxConcurrentRequests(); + apiKey = configuration.getApiKey(); + model = configuration.getModel(); + String baseUrl = configuration.getBaseUrl(); + uri = URI.create((baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl) + "/v1/systemone"); + http = HttpClient.newBuilder().connectTimeout(timeout).followRedirects(HttpClient.Redirect.NEVER).build(); + } + + JsonObject evaluate(Map<String, Object> input) throws Exception { + synchronized (this) { + if (closed) { + throw new IllegalStateException("Jev client is stopped"); + } + if (activeRequests >= maxConcurrentRequests) { + throw new RejectedExecutionException("Jev maxConcurrentRequests limit reached: " + maxConcurrentRequests); + } + activeRequests++; + } + try { + return send(input); + } finally { + synchronized (this) { + activeRequests--; + } + } + } + + private JsonObject send(Map<String, Object> input) throws Exception { + JsonObject request = JevJson.request(input, model); + HttpRequest httpRequest = HttpRequest.newBuilder(uri).timeout(timeout) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json").header("Accept", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(Jsoner.serialize(request), StandardCharsets.UTF_8)).build(); + CompletableFuture<HttpResponse<String>> future; + synchronized (this) { + if (closed) { + throw new IllegalStateException("Jev client is stopped"); + } + future = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + pending.add(future); + } + try { + // Keep the transport future itself: cancelling a derived future need not cancel the HTTP transfer. + HttpResponse<String> response = future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new JevHttpException( + response.statusCode(), + response.headers().firstValue("x-typesafe-request-id").orElse(null), + response.headers().firstValue("Retry-After").orElse(null)); + } + return JevJson.response(response.body(), request); + } catch (InterruptedException e) { + future.cancel(true); + Thread.currentThread().interrupt(); + throw e; + } catch (TimeoutException e) { + future.cancel(true); + throw e; + } catch (ExecutionException e) { + if (e.getCause() instanceof Exception cause) { + throw cause; + } Review Comment: Addressed in [742fcb02c9b6](https://github.com/apache/camel/commit/742fcb02c9b6330b81849baa5c5fad6a45522759). Normalized both JDK HTTP timeout exceptions (including connect timeouts) to java.util.concurrent.TimeoutException, preserving the original cause. The producer now asserts the single exception type. Added deterministic coverage of both HTTP timeout types and a route-level onException(TimeoutException.class) check; the existing stalled-body cancellation tests remain in place. _Generated by Codex on behalf of @luigidemasi via /oss-address-review._ ########## components/camel-ai/camel-jev/src/main/docs/jev-component.adoc: ########## @@ -0,0 +1,468 @@ += Jev Component +:doctitle: Jev +:shortname: jev +:artifactid: camel-jev +:description: Evaluate text and structured state with the TypeSafe AI Jev decision API. +:since: 4.23 +:supportlevel: Preview +:tabs-sync-option: +:component-header: Only producer is supported +//Manually maintained attributes +:group: AI + +*Since Camel {since}* + +*{component-header}* + +The Jev component evaluates explicit state against Noul (yes/no), Choice (one category), +and Score (ordered rubric) questions using https://docs.typesafe.ai/api[TypeSafe AI's HTTP API]. +It calls the service directly using the JDK HTTP client and Camel JSON utilities. +It also provides a standard Camel `Predicate` for semantic conditions. + +[source,xml] +---- +<dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-jev</artifactId> + <version>x.x.x</version> +</dependency> +---- + +== URI format + +[source,text] +---- +jev:name[?options] +---- + +`name` identifies an endpoint; it is not sent to the model. +Producers and predicates using the same URI share its client and configuration. +Each endpoint owns one HTTP client, connection pool, and default executor. Reuse the same +endpoint URI when its configuration is shared. Camel manages its lifecycle and cancels +in-flight requests on stop. Java 21 and later also close the HTTP client explicitly; +on Java 17, the JDK reclaims its remaining resources after it becomes unreachable. + +// component options: START +include::partial$component-configure-options.adoc[] +include::partial$component-endpoint-options.adoc[] +include::partial$component-endpoint-headers.adoc[] +// component options: END + +== Configuration + +Configure credentials through property placeholders or a supported vault: + +[source,properties] +---- +camel.component.jev.api-key={{env:TYPESAFE_API_KEY}} +camel.component.jev.model=jev-1.13.0 +camel.component.jev.request-timeout=30000 +camel.component.jev.max-concurrent-requests=64 +---- + +`apiKey` is a secret option. `baseUrl` defaults to `https://api.typesafe.ai`; the client +appends `/v1/systemone`. A base path can address a proxy. HTTP redirects are not followed. +Use HTTPS for the service; HTTP is useful for local testing. + +`model` defaults to `jev-latest`. Pin a version when tuning thresholds; aliases can change +behavior without a route change. The request map may explicitly override the endpoint's +model for that request. The response's `model` identifies the model that answered. +See https://docs.typesafe.ai/models[available models]. + +=== Concurrent requests + +`maxConcurrentRequests` limits concurrent evaluations to 64 per endpoint by default and must +be positive. Producers and predicates using the same endpoint share this limit. A component +property sets the default for each endpoint; an endpoint URI can override it, for example +`jev:refund?maxConcurrentRequests=8`. Different endpoints have independent limits. + +When the limit is reached, the evaluation fails immediately with +`java.util.concurrent.RejectedExecutionException` before request validation, serialization, +or HTTP submission. No request is queued. The slot is released when the evaluation finishes, +including on failure, timeout, interruption, or cancellation. Camel's error handler can handle +the rejection through `onException(RejectedExecutionException.class)`. + +== Configure questions and routing with properties + +All connection, question, and state-selection settings can be supplied through properties. +Camel discovers the component; no application bean needs to be constructed or registered. +The route calls the producer explicitly, then uses an ordinary Simple condition on its result. + +[source,properties] +---- +camel.component.jev.api-key={{env:TYPESAFE_API_KEY}} +camel.component.jev.model=jev-1.13.0 +camel.component.jev.request-timeout=30000 +camel.component.jev.questions={"refund":{"type":"noul","instructions":"Is a refund requested?","criteria":{"true":"Explicit request for money back","false":"Anything else"}}} +camel.component.jev.state=${header.customerText} +camel.component.jev.result-property=evaluation +refund.threshold=0.8 +---- + +When `questions` is configured, the producer evaluates the state selected by the `state` +Simple expression (the body by default). The input body does not need to be a request map. +`resultProperty` keeps the original body and places the structured response in the named +exchange property. `refund.threshold` is an application property used by the local condition: + +[source,java] +---- +from("direct:evaluate").to("jev:decisions"); + +from("direct:route").to("jev:refund").choice() + .when().simple("${exchangeProperty.evaluation[answers][refund][noul]} >= '{{refund.threshold}}'") + .to("direct:refund-handler") + .otherwise().to("direct:general-handler"); + +from("direct:admit").to("jev:refund") + .filter().simple("${exchangeProperty.evaluation[answers][refund][noul]} >= '{{refund.threshold}}'") + .to("direct:refund-handler"); + +from("direct:require").to("jev:refund") + .validate().simple("${exchangeProperty.evaluation[answers][refund][noul]} >= '{{refund.threshold}}'") + .to("direct:refund-handler"); +---- + +The same pattern works in XML and YAML without a registry reference: + +[source,xml] +---- +<route> + <from uri="direct:admit"/> + <to uri="jev:refund"/> + <filter> + <simple>${exchangeProperty.evaluation[answers][refund][noul]} >= '{{refund.threshold}}'</simple> + <to uri="direct:refund-handler"/> + </filter> +</route> +---- + +[source,yaml] +---- +- from: + uri: direct:admit + steps: + - to: jev:refund + - filter: + simple: "${exchangeProperty.evaluation[answers][refund][noul]} >= '{{refund.threshold}}'" + steps: + - to: direct:refund-handler +---- + +The HTTP call occurs at the `to` step. Subsequent conditions read its stored result locally. +Keep the threshold quoted in Simple so it is converted to the answer's numeric type. +Configure credentials through component properties or secret placeholders; the conditions +contain only answer lookups and routing policy. URI options can override component defaults. +Service failures propagate as errors before the condition is evaluated. + +For stream bodies, such as an HTTP consumer's `InputStream`, select text explicitly with +`camel.component.jev.state=${bodyAs(String)}` or convert the body to a String before the Jev step. +Reading a stream consumes it; use Camel stream caching or convert the body first when later steps need it. + +Configured producer questions may mix Noul, Choice, and Score. When `questions` is absent, +the producer accepts a complete request map as described below. `state` applies only to configured questions. + +== Producer request and response + +Without configured `questions`, the input body must be a `Map<String, Object>` with `state` and a nonempty `questions` map. +An optional `model` overrides the endpoint default. Each named question is a map with `type` +(`noul`, `choice`, or `score`), optional `instructions`, and the applicable `criteria`. +Instructions may be omitted or null. Noul criteria and either yes/no description may also +be omitted or null, matching the official Python SDK and OpenAPI schema. +State, instructions, and descriptions can be text or structured maps/lists. Nested content +supports strings, finite numbers, booleans, nulls, lists, and maps with string keys. +Convert other objects explicitly before submitting them. The component does not collect +exchange headers or properties automatically. Do not send an entire exchange as state. + +The output is an `org.apache.camel.util.json.JsonObject`, which implements `Map<String, Object>`. +It preserves the API's JSON structure, including additional fields. Nested objects are maps, +arrays are lists, and numeric values implement `Number`; use `doubleValue()` or `longValue()` +rather than assuming a specific numeric class. + +Missing or mismatched answers, invalid numeric ranges, unknown choices, and incomplete +probability maps fail the exchange. The selected Choice must have maximal probability; +ties are allowed. Probabilities and scores are preserved as returned. Like the official +Python SDK, the component does not enforce a probability-sum tolerance or recompute a Score +from its returned probabilities, which may have been rounded independently. + +Token counts may be missing or null. Missing values remain absent and null values remain +null; neither is replaced with zero. Present counts must be nonnegative integers. + +[source,java] +---- +Map<String, Object> request = Map.of( + "state", "Please refund the duplicate payment", + "questions", Map.of( + "refund", Map.of("type", "noul", "instructions", "Is a refund requested?", + "criteria", Map.of("true", "An explicit request for money back", "false", "No refund requested")), + "team", Map.of("type", "choice", "instructions", "Which team should handle this?", + "criteria", Map.of("billing", "Payments and refunds", "technical", "Product failures", + "other", "Neither team applies")), + "urgency", Map.of("type", "score", "instructions", "How urgent is this?", + "criteria", List.of("Routine", "Urgent", "Critical")))); + +JsonObject response = template.requestBody("jev:decisions", request, JsonObject.class); +JsonObject answers = response.getJsonObject("answers"); +boolean requested = answers.getJsonObject("refund").getDouble("noul") >= 0.8; +String category = answers.getJsonObject("team").getString("choice"); +double score = answers.getJsonObject("urgency").getDouble("score"); +---- + +This mixed batch makes one HTTP request, with the same state for every question. +There is no implicit batching across producer calls or predicates. + +[cols="1,3"] +|=== +| Answer | Mapping +| Noul | `noul` is the probability of yes, in [0,1]. There is no separate confidence value. +| Choice | `choice`, `probabilities` and `confidence`. Exactly one supplied category is selected. Criteria may contain up to 255 options; an option's description may be null. +| Score | `score` is a possibly fractional position in 1 to 10 ordered levels. `legend` and `probabilities` are maps keyed by string indices starting at `"0"`; legend descriptions may be strings, objects, or arrays. `confidence` describes uncertainty. +| Response | `model`, `answers`, and `usage`. The `input_tokens` and `output_tokens` counts may be absent or null. +|=== + +With `resultProperty=evaluation`, the producer stores the response on that exchange property +and preserves the input request map. The component does not mutate that map when applying +the default model. To retain a business message while constructing a request, save the +original explicitly and restore it after evaluation: + +[source,java] +---- +from("direct:evaluate") + .setProperty("original", body()) + .process(exchange -> exchange.getMessage().setBody(Map.of( + "state", exchange.getMessage().getBody(String.class), + "questions", Map.of("refund", Map.of("type", "noul", "instructions", "Is a refund requested?"))))) + .to("jev:decisions?resultProperty=evaluation") + .setBody(exchangeProperty("original")); +---- + +== Reusable Noul predicates + +`JevPredicate` implements Camel's standard `Predicate` contract. Supply an endpoint URI, +a thread-safe state `Expression`, a question string, and an explicit threshold. +Alternatively, supply a Noul question map to include structured instructions or yes/no criteria. +The predicate snapshots the question at construction; later changes to the supplied map do not alter it. +It preserves the body and stores the full response `JsonObject` in exchange property +`JevPredicate.RESULT` (`CamelJevResult`). Each invocation replaces that property; +it is cleared before evaluation, including when the new evaluation fails. + +[source,java] +---- +JevPredicate refund = new JevPredicate("jev:refund", body(), + "Does this message request a refund?", 0.8); + +from("direct:route").choice() + .when(refund).to("direct:refund-handler") + .otherwise().to("direct:general-handler"); + +from("direct:admit").filter(refund).to("direct:refund-handler"); + +from("direct:require").validate(refund).to("direct:refund-handler"); +---- + +Without an uncertainty band, a probability greater than or equal to the threshold matches. +The optional band is inclusive on both ends, centered on the threshold. A half-width of +zero disables it. Both boundaries must lie within [0,1]. + +[source,java] +---- +JevPredicate refund = new JevPredicate("jev:refund", header("customerText"), + Map.of("type", "noul", "instructions", "Refund requested?", + "criteria", Map.of("true", "An explicit request", "false", "Anything else")), + 0.5, 0.125, JevPredicate.UncertaintyPolicy.NonMatch); +---- + +Here [0.375, 0.625] is uncertain and does not match. `UncertaintyPolicy.Fail` instead raises +`JevUncertainResultException`, available through Camel's exception cause chain. The result +remains available for uncertainty handling. Outside the band, the threshold comparison applies. +A non-match retains each EIP's behavior: Filter discards it, Validate raises validation failure, +and Choice tries the next branch. Uncertainty is distinct from service failure. + +Predicates compose normally with `PredicateBuilder.and`, `or`, and `not`. Short-circuiting +controls whether a remote evaluation occurs. A predicate registered as a bean can be used +through the existing `ref` language in Java, XML, or YAML: + +[source,xml] +---- +<filter> + <ref>refundPredicate</ref> + <to uri="direct:refund-handler"/> +</filter> +---- + +[source,yaml] +---- +- beans: + - name: messageBody + type: org.apache.camel.Expression + factoryBean: org.apache.camel.support.builder.ExpressionBuilder + factoryMethod: bodyExpression + - name: refundPredicate + type: >- + org.apache.camel.component.jev.JevPredicate('jev:refund', '#bean:messageBody', 'Does this message request a refund?', 0.8) + +- from: + uri: direct:admit + steps: + - filter: + ref: refundPredicate + steps: + - to: direct:refund-handler + +- from: + uri: direct:route + steps: + - choice: + when: + - ref: refundPredicate + steps: + - to: direct:refund-handler + otherwise: + steps: + - to: direct:general-handler +---- + +The YAML example constructs both the body expression and the predicate declaratively; +no Java code or manual registry registration is needed. Configure credentials with the +component properties shown above. The explicit constructor signature selects the +String-question overload with a numeric threshold. Each evaluation through `ref` makes +an HTTP request and stores the response in `CamelJevResult`. +Use the producer and local conditions shown above when no predicate bean is desired. +`JevPredicate` uses Camel's existing Predicate contract. + +== Choice classification followed by Camel Choice + +Independent Noul predicates preserve Camel Choice's *first matching branch* semantics. +They do not compare categories. For best-category classification, submit one Choice question +first, then branch on its answer. This is an explicit opt-in pattern. + +For example, prepare the `team` question from the batch example and store the response in +`evaluation`. Restore the original body as shown above, then use ordinary predicates: + +[source,java] +---- +from("direct:classified").choice() + .when(exchange -> selects(exchange, "billing")).to("direct:billing") + .when(exchange -> selects(exchange, "technical")).to("direct:technical") + .otherwise().to("direct:manual-review"); + +// A helper in the route class: +static boolean selects(Exchange exchange, String label) { + JsonObject result = exchange.getProperty("evaluation", JsonObject.class); + JsonObject answer = result.getJsonObject("answers").getJsonObject("team"); + return label.equals(answer.getString("choice")) + && answer.getJsonObject("probabilities").getDouble(label) >= 0.8 + && answer.getDouble("confidence") >= 0.7; +} +---- + +Only route-author-defined labels map to route-author-defined destinations. Never interpret +a model-selected label as an arbitrary endpoint URI. Include an explicit `other` category +when the supplied categories are incomplete. Overlapping descriptions can spread probability +across categories; a forced winner is not evidence that the distinction was clear. + +A valid `other` answer or a result below either threshold reaches `otherwise`. +Timeouts, HTTP errors, missing answers, unknown categories, and malformed responses fail the +producer before routing; they are not ordinary business fallbacks. + +`JevChoiceTest` exercises a complete producer-to-Choice route with these supplied responses: + +[cols="3,1,1,1,1"] +|=== +| State | Winner | Probability | Confidence | Branch +| Refund my duplicate payment | billing | 0.9 | 0.85 | billing +| The export button fails | technical | 0.9 | 0.85 | technical +| Thanks for the newsletter | other | 0.9 | 0.85 | manual +| A refund or help fixing the export | billing | 0.55 | 0.2 | manual +| A payment question | billing | 0.75 | 0.75 | manual +| It fails sometimes | technical | 0.9 | 0.4 | manual +|=== + +Each fixture causes one request; four of six reach manual review. These are deterministic +HTTP fixtures that establish routing and fallback behavior, *not measured model accuracy*. +The two high-confidence classifications take their configured destinations and preserve the +original body. A separate fixture proves a service failure does not select `otherwise`. +This composition needs no Choice-specific adapter. + +== Other EIP integration points Review Comment: Addressed in [742fcb02c9b6](https://github.com/apache/camel/commit/742fcb02c9b6330b81849baa5c5fad6a45522759). Removed the fixture table and speculative EIP/cost analysis, and condensed the component page to configuration, requests/responses and operational behavior. The generic language examples now have their own short language page. The blocking remote-call and token-cost behavior remains documented. _Generated by Codex on behalf of @luigidemasi via /oss-address-review._ ########## components/camel-ai/camel-jev/src/main/java/org/apache/camel/component/jev/JevPredicate.java: ########## @@ -0,0 +1,114 @@ +/* + * 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.jev; + +import java.util.Map; +import java.util.Objects; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.Expression; +import org.apache.camel.Predicate; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.util.json.JsonObject; + +/** + * A synchronous Noul predicate. Each invocation submits freshly selected state, preserves the message body and stores + * the complete response in {@link #RESULT}. The supplied state expression must be thread-safe. + */ +public final class JevPredicate implements Predicate { Review Comment: Addressed in [742fcb02c9b6](https://github.com/apache/camel/commit/742fcb02c9b6330b81849baa5c5fad6a45522759). The replacement language-produced predicate has a readable description containing the logical endpoint URI and threshold. It omits endpoint query options and question text; a regression test checks that an API key cannot appear in that description. _Generated by Codex on behalf of @luigidemasi via /oss-address-review._ ########## components/camel-ai/camel-jev/src/main/java/org/apache/camel/component/jev/JevPredicate.java: ########## @@ -0,0 +1,114 @@ +/* + * 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.jev; + +import java.util.Map; +import java.util.Objects; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.Expression; +import org.apache.camel.Predicate; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.util.json.JsonObject; + +/** + * A synchronous Noul predicate. Each invocation submits freshly selected state, preserves the message body and stores + * the complete response in {@link #RESULT}. The supplied state expression must be thread-safe. + */ +public final class JevPredicate implements Predicate { + public static final String RESULT = "CamelJevResult"; + + public enum UncertaintyPolicy { + NonMatch, + Fail + } + + private final String endpointUri; + private final Expression state; + // Private snapshot, read-only after construction and safe to share between evaluations. + private final JsonObject question; + private final double threshold; + private final double uncertainty; + private final UncertaintyPolicy uncertaintyPolicy; + private JevEndpoint endpoint; + + public JevPredicate(String endpointUri, Expression state, String instructions, double threshold) { + this(endpointUri, state, Map.of("type", "noul", "instructions", instructions), threshold); Review Comment: Addressed in [742fcb02c9b6](https://github.com/apache/camel/commit/742fcb02c9b6330b81849baa5c5fad6a45522759). The String constructor is gone with the public JevPredicate API. JevLanguage now rejects null or blank question text with IllegalArgumentException("Jev question must not be null or blank"), including tooling validation. Producer request maps retain the SDK-compatible behavior allowing omitted/null instructions. _Generated by Codex on behalf of @luigidemasi via /oss-address-review._ -- 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]
