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 com.fasterxml.jackson.annotation.JsonInclude; 010import com.fasterxml.jackson.annotation.JsonIgnore; 011import com.fasterxml.jackson.databind.ObjectMapper; 012import com.fasterxml.jackson.databind.PropertyNamingStrategies; 013 014import java.util.HashMap; 015import java.util.Locale; 016import java.util.Map; 017 018/** 019 * Attributes used to create or update a Select AI conversation. 020 * <p> 021 * Conversation attributes control the user-visible title and description, 022 * conversation retention, and the number of conversation turns retained for 023 * contextual follow-up prompts. 024 * <p> 025 * For complete runnable sample sources that build 026 * {@code ConversationAttributes}, see 027 * <a href="{@docRoot}/src-html/com/oracle/database/selectai/samples/conversation/CreateConversationSample.html"> 028 * CreateConversationSample source</a> and 029 * <a href="{@docRoot}/src-html/com/oracle/database/selectai/samples/conversation/SetConversationAttributesSample.html"> 030 * SetConversationAttributesSample source</a>. 031 * 032 * <p> 033 * The SDK performs basic, deterministic validation and normalization of 034 * conversation attribute values where the constraint can be evaluated 035 * independently of Oracle Database. Blank title and description values are 036 * normalized to {@code null}. When specified, {@code retentionDays} must be 037 * non-negative and {@code conversationLength} must be greater than zero. 038 * Invalid values detected by the SDK result in an 039 * {@link IllegalArgumentException}. 040 * 041 * <p> 042 * Database-specific and database-version-specific semantic validation is 043 * delegated to {@code DBMS_CLOUD_AI} and Oracle Database. Therefore, an 044 * attribute value that passes SDK validation may still be rejected by the 045 * database when the conversation is created or updated. 046 * 047 * @see <a href="https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/dbms-cloud-ai-package.html"> 048 * DBMS_CLOUD_AI conversation reference</a> 049 */ 050public final class ConversationAttributes { 051 /** Human-readable conversation title. */ 052 private final String title; 053 /** Optional conversation description. */ 054 private final String description; 055 /** Number of days Select AI should retain the conversation. */ 056 private final Integer retentionDays; 057 /** Number of prompt/response pairs retained for conversation context. */ 058 private final Integer conversationLength; 059 060 private ConversationAttributes(Builder builder) { 061 this.title = builder.title; 062 this.description = builder.description; 063 this.retentionDays = builder.retentionDays; 064 this.conversationLength = builder.conversationLength; 065 } 066 067 /** 068 * Creates a builder for conversation attributes. 069 * 070 * @return new builder 071 */ 072 public static Builder builder() { 073 return new Builder(); 074 } 075 076 /** 077 * Returns the configured conversation title. 078 * 079 * @return conversation title, or {@code null} when unset 080 */ 081 public String getTitle() { 082 return title; 083 } 084 085 /** 086 * Returns the optional conversation description. 087 * 088 * @return optional conversation description, or {@code null} when unset 089 */ 090 public String getDescription() { 091 return description; 092 } 093 094 /** 095 * Returns the conversation retention period in days. 096 * 097 * @return retention days value, or {@code null} when unset 098 */ 099 public Integer getRetentionDays() { 100 return retentionDays; 101 } 102 103 /** 104 * Returns the configured conversation history length. 105 * 106 * @return optional conversation history length, or {@code null} when not configured 107 */ 108 public Integer getConversationLength() { 109 return conversationLength; 110 } 111 112 /** 113 * Returns whether this instance contains no configured conversation attributes. 114 * 115 * @return {@code true} when no conversation attributes are configured 116 */ 117 @JsonIgnore 118 public boolean isEmpty() { 119 return title == null 120 && description == null 121 && retentionDays == null 122 && conversationLength == null; 123 } 124 125 /** 126 * Converts attributes to DBMS_CLOUD_AI attribute names and string values. 127 * 128 * @return map representation containing only non-null attributes 129 */ 130 public Map<String, String> toAttributeMap() { 131 Map<String, String> map = new HashMap<>(); 132 putIfNotNull(map, "title", title); 133 putIfNotNull(map, "description", description); 134 putIfNotNull(map, "retention_days", retentionDays); 135 putIfNotNull(map, "conversation_length", conversationLength); 136 return map; 137 } 138 139 /** 140 * Builds conversation attributes from database attribute rows. 141 * 142 * @param attributes map using DBMS_CLOUD_AI attribute names 143 * @return ConversationAttributes instance built from recognized map keys 144 */ 145 public static ConversationAttributes fromAttributeMap(Map<String, String> attributes) { 146 Builder builder = ConversationAttributes.builder(); 147 if (attributes == null || attributes.isEmpty()) { 148 return builder.build(); 149 } 150 151 attributes.forEach((k, v) -> { 152 if (k == null || v == null) { 153 return; 154 } 155 String key = k.toLowerCase(Locale.ROOT); 156 switch (key) { 157 case "title" -> builder.title(v); 158 case "description" -> builder.description(v); 159 case "retention_days" -> builder.retentionDays(parseIntegerAttribute("retention_days", v)); 160 case "conversation_length" -> builder.conversationLength(parseIntegerAttribute("conversation_length", v)); 161 default -> { 162 // Ignore unknown attributes. 163 } 164 } 165 }); 166 return builder.build(); 167 } 168 169 /** 170 * Adds a non-null attribute value to the output map. 171 * 172 * @param map destination attribute map 173 * @param key DBMS_CLOUD_AI attribute name 174 * @param value value to stringify and add 175 */ 176 private static void putIfNotNull(Map<String, String> map, String key, Object value) { 177 if (value != null) { 178 map.put(key, String.valueOf(value)); 179 } 180 } 181 182 /** 183 * Parses a database numeric conversation attribute value. 184 * 185 * @param attributeName DBMS_CLOUD_AI attribute name 186 * @param value database value 187 * @return parsed integer value 188 * @throws IllegalArgumentException when value is not an integer 189 */ 190 private static Integer parseIntegerAttribute(String attributeName, String value) { 191 try { 192 return Integer.parseInt(value); 193 } catch (NumberFormatException e) { 194 throw new IllegalArgumentException(attributeName + " must be an integer", e); 195 } 196 } 197 198 /** 199 * Serializes attributes using snake_case JSON keys expected by DBMS_CLOUD_AI. 200 * 201 * @return JSON string representation of this object with snake_case keys and non-null fields 202 */ 203 public String toJson() { 204 ObjectMapper mapper = new ObjectMapper(); 205 try { 206 mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); 207 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 208 return mapper.writeValueAsString(this); 209 } catch (Exception e) { 210 throw new IllegalStateException("Failed to serialize ConversationAttributes to JSON", e); 211 } 212 } 213 214 /** 215 * Returns a non-sensitive summary of configured conversation attributes. 216 * 217 * @return summary string that omits title and description values 218 */ 219 @Override 220 public String toString() { 221 return "ConversationAttributes{" 222 + "hasTitle=" + (title != null) 223 + ", hasDescription=" + (description != null) 224 + ", retentionDays=" + retentionDays 225 + ", conversationLength=" + conversationLength 226 + '}'; 227 } 228 229 /** 230 * Builder for {@link ConversationAttributes}. 231 */ 232 public static final class Builder { 233 /** Conversation title being assembled. */ 234 private String title; 235 /** Conversation description being assembled. */ 236 private String description; 237 /** Retention period in days being assembled. */ 238 private Integer retentionDays; 239 /** Conversation history length being assembled. */ 240 private Integer conversationLength; 241 242 private Builder() { 243 } 244 245 /** 246 * Sets the conversation title. 247 * 248 * @param title conversation title 249 * @return this builder instance 250 */ 251 public Builder title(String title) { 252 this.title = normalize(title); 253 return this; 254 } 255 256 /** 257 * Sets the conversation description. 258 * 259 * @param description optional conversation description 260 * @return this builder instance 261 */ 262 public Builder description(String description) { 263 this.description = normalize(description); 264 return this; 265 } 266 267 /** 268 * Sets how many days Select AI should retain the conversation. 269 * 270 * @param retentionDays retention period in days, or {@code null} when unset 271 * @return this builder instance 272 * @throws IllegalArgumentException if {@code retentionDays} is negative. 273 */ 274 public Builder retentionDays(Integer retentionDays) { 275 if (retentionDays != null && retentionDays < 0) { 276 throw new IllegalArgumentException( 277 "retentionDays must be non-negative"); 278 } 279 this.retentionDays = retentionDays; 280 return this; 281 } 282 283 /** 284 * Sets how many turns Select AI should keep in conversation context. 285 * 286 * @param conversationLength context length, or {@code null} when unset 287 * @return this builder instance 288 * @throws IllegalArgumentException if {@code conversationLength} is less than 289 * or equal to zero 290 */ 291 public Builder conversationLength(Integer conversationLength) { 292 if (conversationLength != null && conversationLength <= 0) { 293 throw new IllegalArgumentException( 294 "conversationLength must be greater than 0"); 295 } 296 this.conversationLength = conversationLength; 297 return this; 298 } 299 300 /** 301 * Normalizes blank input strings to {@code null}. 302 * 303 * @param value value to trim 304 * @return trimmed value, or {@code null} when input is null/blank 305 */ 306 private static String normalize(String value) { 307 if (value == null) { 308 return null; 309 } 310 String trimmed = value.trim(); 311 return trimmed.isEmpty() ? null : trimmed; 312 } 313 314 /** 315 * Builds immutable conversation attributes. 316 * 317 * @return immutable ConversationAttributes instance from current builder state 318 */ 319 public ConversationAttributes build() { 320 return new ConversationAttributes(this); 321 } 322 } 323}