001/*
002 * Copyright (c) 2026, Oracle and/or its affiliates.
003 *
004 * Licensed under the Universal Permissive License Version 1.0 as shown at
005 * https://oss.oracle.com/licenses/upl/
006 */
007package com.oracle.database.selectai.model;
008
009import java.util.Locale;
010
011/**
012 * Immutable request payload for {@code DBMS_CLOUD_AI.FEEDBACK}.
013 * <p>
014 * Use this request object when an application wants to tell Select AI whether
015 * generated SQL was useful, provide a correction for a poor NL2SQL answer, or
016 * remove previously submitted guidance. Feedback is profile-specific prompt
017 * guidance for future SQL generation, not model fine-tuning and not a general
018 * rating mechanism for chat or RAG responses. The target profile is supplied by
019 * the {@code Profile} object used to submit the request. A feedback request
020 * identifies one generated SQL statement either by its database SQL identifier
021 * or by the SQL text itself.
022 *
023 * @see <a href="https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/dbms-cloud-ai-package.html">
024 *      DBMS_CLOUD_AI feedback reference</a>
025 */
026public final class Feedback {
027
028    /**
029     * Action to perform on the feedback store.
030     */
031    public enum Operation {
032        /**
033         * Stores feedback for a generated SQL statement.
034         */
035        ADD("add"),
036
037        /**
038         * Removes feedback that was previously stored for a generated SQL statement.
039         */
040        DELETE("delete");
041
042        /** Database value expected by DBMS_CLOUD_AI.FEEDBACK. */
043        private final String databaseValue;
044
045        Operation(String databaseValue) {
046            this.databaseValue = databaseValue;
047        }
048
049        /**
050         * Returns the lowercase database value.
051         *
052         * @return DBMS_CLOUD_AI.FEEDBACK operation value
053         */
054        public String getDatabaseValue() {
055            return databaseValue;
056        }
057
058        /**
059         * Parses user-supplied operation text.
060         *
061         * @param value action text such as {@code add} or {@code delete}
062         * @return parsed operation, or {@code null} when input is null/blank so
063         *         DBMS_CLOUD_AI can apply its default
064         */
065        public static Operation from(String value) {
066            if (value == null || value.isBlank()) {
067                return null;
068            }
069            String normalized = value.trim().toUpperCase(Locale.ROOT);
070            try {
071                return Operation.valueOf(normalized);
072            } catch (IllegalArgumentException e) {
073                throw new IllegalArgumentException("operation must be add or delete", e);
074            }
075        }
076    }
077
078    /**
079     * User assessment of generated SQL accepted by DBMS_CLOUD_AI.FEEDBACK.
080     */
081    public enum FeedbackType {
082        /**
083         * Marks generated SQL as useful or correct.
084         */
085        POSITIVE("positive"),
086
087        /**
088         * Marks generated SQL as incorrect or requiring guidance.
089         */
090        NEGATIVE("negative");
091
092        /** Database value expected by DBMS_CLOUD_AI.FEEDBACK. */
093        private final String databaseValue;
094
095        FeedbackType(String databaseValue) {
096            this.databaseValue = databaseValue;
097        }
098
099        /**
100         * Returns the lowercase database value.
101         *
102         * @return DBMS_CLOUD_AI.FEEDBACK feedback_type value
103         */
104        public String getDatabaseValue() {
105            return databaseValue;
106        }
107
108        /**
109         * Parses user-supplied feedback type text.
110         *
111         * @param value feedback type text such as {@code positive} or {@code negative}
112         * @return parsed feedback type, or {@code null} when input is null/blank
113         */
114        public static FeedbackType from(String value) {
115            if (value == null || value.isBlank()) {
116                return null;
117            }
118            String normalized = value.trim().toUpperCase(Locale.ROOT);
119            try {
120                return FeedbackType.valueOf(normalized);
121            } catch (IllegalArgumentException e) {
122                throw new IllegalArgumentException("feedbackType must be positive or negative", e);
123            }
124        }
125    }
126
127    /**
128     * Database SQL identifier for the generated statement.
129     * <p>
130     * Use this when the generated SQL has an identifier available from database
131     * execution or query history. Provide either this value or the SQL text, not
132     * both.
133     */
134    private final String sqlId;
135
136    /**
137     * Generated SQL statement text.
138     * <p>
139     * Use this when the SQL statement should be reviewed but a database SQL
140     * identifier is not available. Provide either this value or the SQL
141     * identifier, not both. This value may contain customer data; SDK code
142     * must not log it.
143     */
144    private final String sqlText;
145
146    /**
147     * User's assessment of the generated SQL.
148     * <p>
149     * Use positive feedback to accept a good generated statement. Use negative
150     * feedback when the statement needs a correction or better guidance.
151     * Required when adding feedback.
152     */
153    private final FeedbackType feedbackType;
154
155    /**
156     * Expected or corrected response for the reviewed SQL.
157     * <p>
158     * This is especially important for negative feedback because it gives Select
159     * AI a concrete target for what the generated SQL should have produced.
160     * This value may contain customer data; SDK code must not log it.
161     */
162    private final String response;
163
164    /**
165     * Natural-language feedback from the user.
166     * <p>
167     * Use this field for comments such as why the SQL was correct, what was
168     * wrong, or how the query should be improved. This value may contain
169     * customer data; SDK code must not log it.
170     */
171    private final String feedbackContent;
172
173    /**
174     * Requested action for the feedback request.
175     */
176    private final Operation operation;
177
178    private Feedback(Builder builder) {
179        this.sqlId = builder.sqlId;
180        this.sqlText = builder.sqlText;
181        this.feedbackType = builder.feedbackType;
182        this.response = builder.response;
183        this.feedbackContent = builder.feedbackContent;
184        this.operation = Operation.from(builder.operation);
185
186        if (!hasValue(sqlId) && !hasValue(sqlText)) {
187            throw new IllegalArgumentException("Either sqlId or sqlText must be provided");
188        }
189
190        if (hasValue(sqlId) && hasValue(sqlText)) {
191            throw new IllegalArgumentException("Provide only one of sqlId or sqlText");
192        }
193
194        if (operation == Operation.ADD) {
195            if (feedbackType == null) {
196                throw new IllegalArgumentException("feedbackType is required when operation is ADD");
197            }
198            if (feedbackType == FeedbackType.NEGATIVE && !hasValue(response)) {
199                throw new IllegalArgumentException(
200                        "response is required when feedbackType is negative and operation is ADD");
201            }
202        } else if (operation == Operation.DELETE
203                && (feedbackType != null || hasValue(response) || hasValue(feedbackContent))) {
204            throw new IllegalArgumentException(
205                    "DELETE feedback must not include feedbackType, response, or feedbackContent");
206        }
207    }
208
209    /**
210     * Returns whether a string contains non-blank content.
211     *
212     * @param value value to inspect
213     * @return {@code true} when the value is non-null and non-blank
214     */
215    private static boolean hasValue(String value) {
216        return value != null && !value.isBlank();
217    }
218
219    /**
220     * Returns the database SQL identifier used to locate the reviewed SQL.
221     *
222     * @return SQL identifier associated with the feedback, or {@code null} when SQL text is used instead
223     */
224    public String getSqlId() {
225        return sqlId;
226    }
227
228    /**
229     * Returns the generated SQL text being reviewed.
230     *
231     * @return SQL text associated with the feedback, or {@code null} when SQL identifier is used instead
232     */
233    public String getSqlText() {
234        return sqlText;
235    }
236
237    /**
238     * Returns whether the generated SQL was marked useful or needing correction.
239     *
240     * @return normalized feedback type
241     */
242    public FeedbackType getFeedbackType() {
243        return feedbackType;
244    }
245
246    /**
247     * Returns the lowercase database value for {@code feedback_type}.
248     *
249     * @return {@code positive}, {@code negative}, or {@code null}
250     */
251    public String getFeedbackTypeValue() {
252        return feedbackType == null ? null : feedbackType.getDatabaseValue();
253    }
254
255    /**
256     * Returns the expected or corrected response for the reviewed SQL.
257     *
258     * @return expected SQL response; required for negative {@code ADD} feedback
259     */
260    public String getResponse() {
261        return response;
262    }
263
264    /**
265     * Returns the user's natural-language explanation for the feedback.
266     *
267     * @return optional natural-language feedback content
268     */
269    public String getFeedbackContent() {
270        return feedbackContent;
271    }
272
273    /**
274     * Returns the normalized action to perform on the feedback store.
275     *
276     * @return normalized feedback operation enum, or {@code null} when DBMS_CLOUD_AI
277     *         should apply its default operation
278     */
279    public Operation getOperation() {
280        return operation;
281    }
282
283    /**
284     * Returns the database value for the requested operation.
285     *
286     * @return lowercase operation value, or {@code null} when DBMS_CLOUD_AI should apply
287     *         its default operation
288     */
289    public String getOperationValue() {
290        return operation == null ? null : operation.getDatabaseValue();
291    }
292
293    /**
294     * Creates a builder for a feedback request.
295     *
296     * @return new builder for constructing validated Feedback instances
297     */
298    public static Builder builder() {
299        return new Builder();
300    }
301
302    /**
303     * Builder for {@link Feedback} request payloads.
304     */
305    public static final class Builder {
306        /** Optional database SQL identifier for the generated statement. */
307        private String sqlId;
308        /** Optional generated SQL text when an identifier is not available. */
309        private String sqlText;
310        /** User assessment of the generated SQL. */
311        private FeedbackType feedbackType;
312        /** Expected or corrected response for negative feedback. */
313        private String response;
314        /** Natural-language feedback notes. */
315        private String feedbackContent;
316        /** Action text before it is normalized into {@link Operation}. */
317        private String operation;
318
319        private Builder() {
320        }
321
322        /**
323         * Identifies the reviewed SQL by its database SQL identifier.
324         * <p>
325         * Use this when the generated SQL can be identified by SQL ID. Do not
326         * also set {@link #sqlText(String)}.
327         *
328         * @param sqlId SQL_ID value
329         * @return this builder instance
330         */
331        public Builder sqlId(String sqlId) {
332            this.sqlId = sqlId;
333            return this;
334        }
335
336        /**
337         * Identifies the reviewed SQL by its full statement text.
338         * <p>
339         * Use this when the generated SQL should be identified by its full SQL
340         * text. Do not also set {@link #sqlId(String)}.
341         *
342         * @param sqlText generated SQL text
343         * @return this builder instance
344         */
345        public Builder sqlText(String sqlText) {
346            this.sqlText = sqlText;
347            return this;
348        }
349
350        /**
351         * Sets whether the generated SQL was useful or needs correction.
352         *
353         * @param feedbackType feedback type, typically {@code positive} or {@code negative}
354         * @return this builder instance
355         */
356        public Builder feedbackType(String feedbackType) {
357            this.feedbackType = FeedbackType.from(feedbackType);
358            return this;
359        }
360
361        /**
362         * Sets whether the generated SQL was useful or needs correction.
363         *
364         * @param feedbackType feedback type enum
365         * @return this builder instance
366         */
367        public Builder feedbackType(FeedbackType feedbackType) {
368            this.feedbackType = feedbackType;
369            return this;
370        }
371
372        /**
373         * Sets the expected or corrected response for the reviewed SQL.
374         *
375         * @param response expected SQL result or corrected SQL response
376         * @return this builder instance
377         */
378        public Builder response(String response) {
379            this.response = response;
380            return this;
381        }
382
383        /**
384         * Sets the natural-language explanation for the feedback.
385         *
386         * @param feedbackContent natural-language feedback notes or revised SQL guidance
387         * @return this builder instance
388         */
389        public Builder feedbackContent(String feedbackContent) {
390            this.feedbackContent = feedbackContent;
391            return this;
392        }
393
394        /**
395         * Sets the action to perform on the feedback store.
396         * <p>
397         * Null or blank input is preserved so {@code DBMS_CLOUD_AI.FEEDBACK}
398         * can apply its database default. Non-blank values are parsed into
399         * {@link Operation#ADD} or {@link Operation#DELETE}.
400         *
401         * @param operation operation text, for example {@code add} or {@code delete}
402         * @return this builder instance
403         */
404        public Builder operation(String operation) {
405            this.operation = operation;
406            return this;
407        }
408
409        /**
410         * Sets the action to perform on the feedback store.
411         *
412         * @param operation operation enum, or {@code null} to let
413         *        {@code DBMS_CLOUD_AI.FEEDBACK} apply its database default
414         * @return this builder instance
415         */
416        public Builder operation(Operation operation) {
417            this.operation = operation == null ? null : operation.name();
418            return this;
419        }
420
421        /**
422         * Builds and validates the feedback request.
423         * <p>
424         * A valid request must identify the generated SQL exactly one way, must
425         * include an assessment when storing feedback, must include an
426         * expected/corrected response when storing negative feedback, and must
427         * not include add-only fields when deleting feedback.
428         *
429         * @return validated immutable Feedback request
430         * @throws IllegalArgumentException when the request is invalid
431         */
432        public Feedback build() {
433            return new Feedback(this);
434        }
435    }
436}