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}