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}