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.ArrayList;
015import java.util.Collections;
016import java.util.HashMap;
017import java.util.List;
018import java.util.Locale;
019import java.util.Map;
020
021/**
022 * Attribute payload for {@code DBMS_CLOUD_AI.CREATE_PROFILE},
023 * {@code SET_ATTRIBUTE}, and {@code SET_ATTRIBUTES}.
024 * <p>
025 * Profile attributes describe the provider, AI model, credential, object
026 * selection, prompt behavior, conversation behavior, and RAG/vector-index
027 * settings that Select AI uses when answering prompts. Most attributes map to
028 * database-side Select AI profile settings; callers can build the payload once
029 * for profile creation or reuse it for bulk updates.
030 * <p>
031 * The public builder validates SDK-known provider, OCI API format, and
032 * object-list mode values when constructing a creation or update payload. Use
033 * {@link #fromDatabaseAttributeMap(Map)} when hydrating metadata from an
034 * existing database profile; that path preserves values introduced by newer
035 * database releases.
036 * <p>
037 * For complete runnable sample sources that build {@code ProfileAttributes},
038 * see
039 * <a href="{@docRoot}/src-html/com/oracle/database/selectai/samples/profile/CreateProfileSample.html">
040 * CreateProfileSample source</a> and
041 * <a href="{@docRoot}/src-html/com/oracle/database/selectai/samples/profile/SetProfileAttributesSample.html">
042 * SetProfileAttributesSample source</a>.
043 *
044 * <p>Commonly used profile attributes include:</p>
045 *
046 * <table>
047 *   <caption>Common Select AI profile attributes</caption>
048 *   <tr>
049 *     <th>Attribute</th>
050 *     <th>Description</th>
051 *   </tr>
052 *   <tr>
053 *     <td>{@code provider}</td>
054 *     <td>AI provider used by the profile.</td>
055 *   </tr>
056 *   <tr>
057 *     <td>{@code credential_name}</td>
058 *     <td>Credential used to access the AI provider, where required.</td>
059 *   </tr>
060 *   <tr>
061 *     <td>{@code model}</td>
062 *     <td>AI model used by the profile.</td>
063 *   </tr>
064 *   <tr>
065 *     <td>{@code object_list}</td>
066 *     <td>Database objects available for NL2SQL processing.</td>
067 *   </tr>
068 *   <tr>
069 *     <td>{@code object_list_mode}</td>
070 *     <td>Controls how database objects are selected for NL2SQL processing.</td>
071 *   </tr>
072 *   <tr>
073 *     <td>{@code temperature}</td>
074 *     <td>Controls randomness in model-generated responses.</td>
075 *   </tr>
076 *   <tr>
077 *     <td>{@code max_tokens}</td>
078 *     <td>Specifies the maximum number of tokens to generate.</td>
079 *   </tr>
080 *   <tr>
081 *     <td>{@code vector_index_name}</td>
082 *     <td>Specifies the vector index associated with the profile.</td>
083 *   </tr>
084 * </table>
085 *
086 * <p>
087 * The SDK performs basic, deterministic validation of profile attributes where
088 * the constraint can be validated independently of the configured provider or
089 * database version. This includes validation of supported SDK-defined values,
090 * numeric constraints, and basic input structure. Invalid values detected by
091 * the SDK result in an {@link IllegalArgumentException}.
092 *
093 * <p>
094 * Provider-specific, model-specific, database-version-specific, and other
095 * database-side semantic validation is delegated to {@code DBMS_CLOUD_AI} and
096 * Oracle Database. Therefore, an attribute value that passes SDK validation
097 * may still be rejected by the database based on the selected provider, model,
098 * database version, or other database-side requirements.
099 *
100 * <p>
101 * Supported profile attributes, valid values, defaults, requirements, and
102 * provider-specific behavior are defined by Oracle Database and may vary by
103 * database version and provider. See the {@code DBMS_CLOUD_AI} Profile
104 * Attributes documentation for the complete and current list.
105 *
106 * @see <a href="https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/dbms-cloud-ai-package.html#GUID-12D91681-B51C-48E0-93FD-9ABC67B0F375">
107 *      DBMS_CLOUD_AI Profile Attributes</a>
108 * @see <a href="https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/select-ai-manage-profiles.html">
109 *      Manage AI profiles</a>
110 */
111public final class ProfileAttributes {
112    /** Include table/column annotations (26ai) in augmented metadata. Default: false. */
113    private final Boolean annotations;
114    /** Additional prompt instructions supplied to Select AI. */
115    private final String additionalInstructions;
116    /** Azure OpenAI deployment name for chat/completion model. */
117    private final String azureDeploymentName;
118    /** Azure OpenAI deployment name for embedding model. */
119    private final String azureEmbeddingDeploymentName;
120    /** Azure OpenAI resource name. */
121    private final String azureResourceName;
122    /** Whether SQL value matching should be case-sensitive. Default: false. */
123    private final Boolean caseSensitiveValues;
124    /** Include table and column comments in metadata sent to LLM. Default: false. */
125    private final Boolean comments;
126    /** Include referential constraints in metadata. Default: false. */
127    private final Boolean constraints;
128    /** Enable conversation history for profile. Default: false. */
129    private final Boolean conversation;
130    /** Number of past turns retained when conversation is enabled. */
131    private final Integer conversationLength;
132    /** Credential name used to call provider APIs, where required. */
133    private final String credentialName;
134    /** Embedding model name. */
135    private final String embeddingModel;
136    /** Enables custom source URL metadata for RAG citations. Default: false. */
137    private final Boolean enableCustomSourceUri;
138    /** Restrict SQL generation to objects listed in objectList. Default: false. */
139    private final Boolean enforceObjectList;
140    /** Maximum tokens generated per response. */
141    private final Integer maxTokens;
142    /** Provider model identifier. */
143    private final String model;
144    /** JSON array of objects eligible for NL2SQL (owner mandatory, name optional). */
145    private final String objectList;
146    /** Metadata selection mode: all or automated. */
147    private final String objectListMode;
148    /** OCI API format for dedicated endpoint/model OCID (COHERE|GENERIC). */
149    private final String ociApiformat;
150    /** OCI compartment OCID. */
151    private final String ociCompartmentId;
152    /** OCI dedicated endpoint OCID. */
153    private final String ociEndpointId;
154    /** OCI runtime type (deprecated in favor of oci_apiformat). */
155    private final String ociRuntimetype;
156    /** AI provider name (mandatory). */
157    private final String provider;
158    /** Provider endpoint host/path for OpenAI-compatible providers. */
159    private final String providerEndpoint;
160    /** Provider region. */
161    private final String region;
162    /** Role prompt supplied to Select AI. */
163    private final String role;
164    /** Optional deterministic sampling seed (signed 64-bit integer). */
165    private final Long seed;
166    /** Stop sequences for generation. */
167    private final List<String> stopTokens;
168    /** Non-negative sampling temperature. */
169    private final Double temperature;
170    /** Vector index name (Oracle SQL identifier, up to 125 chars). */
171    private final String vectorIndexName;
172    /** Source language for translation. */
173    private final String sourceLanguage;
174    /** Target language for translation. */
175    private final String targetLanguage;
176    /** Unmodeled attributes preserved for metadata round-tripping. */
177    private final Map<String, String> customAttributes;
178
179    private ProfileAttributes(Builder builder) {
180        this.annotations = builder.annotations;
181        this.additionalInstructions = builder.additionalInstructions;
182        this.azureDeploymentName = builder.azureDeploymentName;
183        this.azureEmbeddingDeploymentName = builder.azureEmbeddingDeploymentName;
184        this.azureResourceName = builder.azureResourceName;
185        this.caseSensitiveValues = builder.caseSensitiveValues;
186        this.comments = builder.comments;
187        this.constraints = builder.constraints;
188        this.conversation = builder.conversation;
189        this.conversationLength = builder.conversationLength;
190        this.credentialName = builder.credentialName;
191        this.embeddingModel = builder.embeddingModel;
192        this.enableCustomSourceUri = builder.enableCustomSourceUri;
193        this.enforceObjectList = builder.enforceObjectList;
194        this.maxTokens = builder.maxTokens;
195        this.model = builder.model;
196        this.objectList = builder.objectList;
197        this.objectListMode = builder.objectListMode;
198        this.ociApiformat = builder.ociApiformat;
199        this.ociCompartmentId = builder.ociCompartmentId;
200        this.ociEndpointId = builder.ociEndpointId;
201        this.ociRuntimetype = builder.ociRuntimetype;
202        this.provider = builder.provider;
203        this.providerEndpoint = builder.providerEndpoint;
204        this.region = builder.region;
205        this.role = builder.role;
206        this.seed = builder.seed;
207        this.stopTokens = builder.stopTokens == null ? null : List.copyOf(builder.stopTokens);
208        this.temperature = builder.temperature;
209        this.vectorIndexName = builder.vectorIndexName;
210        this.sourceLanguage = builder.sourceLanguage;
211        this.targetLanguage = builder.targetLanguage;
212        this.customAttributes = Collections.unmodifiableMap(new HashMap<>(builder.customAttributes));
213    }
214
215    /**
216     * Creates a builder for profile attributes.
217     *
218     * @return new builder for constructing ProfileAttributes
219     */
220    public static Builder builder() {
221        return new Builder();
222    }
223
224    /**
225     * Returns whether table/column annotations are included in metadata.
226     *
227     * @return annotations flag value, or {@code null} when unset
228     */
229    public Boolean getAnnotations() {
230        return annotations;
231    }
232
233    /**
234     * Returns additional instructions included in Select AI prompts.
235     *
236     * @return additional instructions, or {@code null} when unset
237     */
238    public String getAdditionalInstructions() {
239        return additionalInstructions;
240    }
241
242    /**
243     * Returns the Azure OpenAI deployment name.
244     *
245     * @return Azure deployment name, or {@code null} when unset
246     */
247    public String getAzureDeploymentName() {
248        return azureDeploymentName;
249    }
250
251    /**
252     * Returns the Azure OpenAI embedding deployment name.
253     *
254     * @return Azure embedding deployment name, or {@code null} when unset
255     */
256    public String getAzureEmbeddingDeploymentName() {
257        return azureEmbeddingDeploymentName;
258    }
259
260    /**
261     * Returns the Azure OpenAI resource name.
262     *
263     * @return Azure resource name, or {@code null} when unset
264     */
265    public String getAzureResourceName() {
266        return azureResourceName;
267    }
268
269    /**
270     * Returns whether generated SQL value matching is case-sensitive.
271     *
272     * @return case-sensitive-values flag, or {@code null} when unset
273     */
274    public Boolean getCaseSensitiveValues() {
275        return caseSensitiveValues;
276    }
277
278    /**
279     * Returns whether table and column comments are included in prompt metadata.
280     *
281     * @return comments flag, or {@code null} when unset
282     */
283    public Boolean getComments() {
284        return comments;
285    }
286
287    /**
288     * Returns whether referential constraints are included in prompt metadata.
289     *
290     * @return constraints flag, or {@code null} when unset
291     */
292    public Boolean getConstraints() {
293        return constraints;
294    }
295
296    /**
297     * Returns whether conversation history is enabled for the profile.
298     *
299     * @return conversation flag, or {@code null} when unset
300     */
301    public Boolean getConversation() {
302        return conversation;
303    }
304
305    /**
306     * Returns how many conversation turns are retained for context.
307     *
308     * @return configured conversation length, or {@code null} when unset
309     */
310    public Integer getConversationLength() {
311        return conversationLength;
312    }
313
314    /**
315     * Returns the credential name used for provider API calls.
316     *
317     * @return credential name, or {@code null} when unset
318     */
319    public String getCredentialName() {
320        return credentialName;
321    }
322
323    /**
324     * Returns the embedding model name.
325     *
326     * @return embedding model name, or {@code null} when unset
327     */
328    public String getEmbeddingModel() {
329        return embeddingModel;
330    }
331
332    /**
333     * Returns whether custom source URI metadata is enabled.
334     *
335     * @return custom-source-URI flag, or {@code null} when unset
336     */
337    public Boolean getEnableCustomSourceUri() {
338        return enableCustomSourceUri;
339    }
340
341    /**
342     * Returns whether SQL generation is restricted to {@code object_list}.
343     *
344     * @return enforce-object-list flag, or {@code null} when unset
345     */
346    public Boolean getEnforceObjectList() {
347        return enforceObjectList;
348    }
349
350    /**
351     * Returns the maximum generated output tokens.
352     *
353     * @return maximum tokens value, or {@code null} when unset
354     */
355    public Integer getMaxTokens() {
356        return maxTokens;
357    }
358
359    /**
360     * Returns the provider model identifier.
361     *
362     * @return provider model identifier, or {@code null} when unset
363     */
364    public String getModel() {
365        return model;
366    }
367
368    /**
369     * Returns the JSON object list supplied to Select AI.
370     *
371     * @return object-list JSON string, or {@code null} when unset
372     */
373    public String getObjectList() {
374        return objectList;
375    }
376
377    /**
378     * Returns how Select AI chooses objects for metadata.
379     *
380     * @return object-list mode value, or {@code null} when unset
381     */
382    public String getObjectListMode() {
383        return objectListMode;
384    }
385
386    /**
387     * Returns the OCI Generative AI API format.
388     *
389     * @return OCI API format value, or {@code null} when unset
390     */
391    public String getOciApiformat() {
392        return ociApiformat;
393    }
394
395    /**
396     * Returns the OCI compartment OCID.
397     *
398     * @return OCI compartment ID, or {@code null} when unset
399     */
400    public String getOciCompartmentId() {
401        return ociCompartmentId;
402    }
403
404    /**
405     * Returns the OCI dedicated endpoint OCID.
406     *
407     * @return OCI endpoint ID, or {@code null} when unset
408     */
409    public String getOciEndpointId() {
410        return ociEndpointId;
411    }
412
413    /**
414     * Returns the OCI runtime type attribute.
415     *
416     * @return OCI runtime type value, or {@code null} when unset
417     */
418    public String getOciRuntimetype() {
419        return ociRuntimetype;
420    }
421
422    /**
423     * Returns the AI provider name.
424     *
425     * @return provider name, or {@code null} when unset
426     */
427    public String getProvider() {
428        return provider;
429    }
430
431    /**
432     * Returns the provider endpoint host/path.
433     *
434     * @return provider endpoint value, or {@code null} when unset
435     */
436    public String getProviderEndpoint() {
437        return providerEndpoint;
438    }
439
440    /**
441     * Returns the provider region.
442     *
443     * @return configured region value, or {@code null} when unset
444     */
445    public String getRegion() {
446        return region;
447    }
448
449    /**
450     * Returns the role prompt used by Select AI.
451     *
452     * @return role prompt, or {@code null} when unset
453     */
454    public String getRole() {
455        return role;
456    }
457
458    /**
459     * Returns the deterministic sampling seed.
460     *
461     * @return deterministic sampling seed, or {@code null} when unset
462     */
463    public Long getSeed() {
464        return seed;
465    }
466
467    /**
468     * Returns stop tokens used to terminate generation.
469     *
470     * @return configured stop-token list, or {@code null} when unset
471     */
472    public List<String> getStopTokens() {
473        return stopTokens;
474    }
475
476    /**
477     * Returns the provider sampling temperature.
478     *
479     * @return sampling temperature, or {@code null} when unset
480     */
481    public Double getTemperature() {
482        return temperature;
483    }
484
485    /**
486     * Returns the vector index name associated with this profile.
487     *
488     * @return vector index name, or {@code null} when unset
489     */
490    public String getVectorIndexName() {
491        return vectorIndexName;
492    }
493
494    /**
495     * Returns the source language for translation requests.
496     *
497     * @return source language for translation, or {@code null} when unset
498     */
499    public String getSourceLanguage() {
500        return sourceLanguage;
501    }
502
503    /**
504     * Returns the target language for translation requests.
505     *
506     * @return target language for translation, or {@code null} when unset
507     */
508    public String getTargetLanguage() {
509        return targetLanguage;
510    }
511
512    /**
513     * Returns unmodeled database attributes preserved for forward-compatible
514     * metadata round-tripping.
515     * <p>
516     * These values are emitted by {@link #toAttributeMap()} but excluded from
517     * {@link #toJson()} to avoid sending a nested {@code custom_attributes}
518     * object to {@code DBMS_CLOUD_AI}.
519     *
520     * @return immutable map of custom attribute names and values
521     */
522    @JsonIgnore
523    public Map<String, String> getCustomAttributes() {
524        return customAttributes;
525    }
526
527    /**
528     * Converts non-null attributes to DBMS_CLOUD_AI attribute names and string values.
529     *
530     * @return map containing non-null profile attributes
531     */
532    public Map<String, String> toAttributeMap() {
533        Map<String, String> map = new HashMap<>();
534        putIfNotNull(map, "annotations", annotations);
535        putIfNotNull(map, "additional_instructions", additionalInstructions);
536        putIfNotNull(map, "azure_deployment_name", azureDeploymentName);
537        putIfNotNull(map, "azure_embedding_deployment_name", azureEmbeddingDeploymentName);
538        putIfNotNull(map, "azure_resource_name", azureResourceName);
539        putIfNotNull(map, "case_sensitive_values", caseSensitiveValues);
540        putIfNotNull(map, "comments", comments);
541        putIfNotNull(map, "constraints", constraints);
542        putIfNotNull(map, "conversation", conversation);
543        putIfNotNull(map, "conversation_length", conversationLength);
544        putIfNotNull(map, "credential_name", credentialName);
545        putIfNotNull(map, "embedding_model", embeddingModel);
546        putIfNotNull(map, "enable_custom_source_uri", enableCustomSourceUri);
547        putIfNotNull(map, "enforce_object_list", enforceObjectList);
548        putIfNotNull(map, "max_tokens", maxTokens);
549        putIfNotNull(map, "model", model);
550        putIfNotNull(map, "object_list", objectList);
551        putIfNotNull(map, "object_list_mode", objectListMode);
552        putIfNotNull(map, "oci_apiformat", ociApiformat);
553        putIfNotNull(map, "oci_compartment_id", ociCompartmentId);
554        putIfNotNull(map, "oci_endpoint_id", ociEndpointId);
555        putIfNotNull(map, "oci_runtimetype", ociRuntimetype);
556        putIfNotNull(map, "provider", provider);
557        putIfNotNull(map, "provider_endpoint", providerEndpoint);
558        putIfNotNull(map, "region", region);
559        putIfNotNull(map, "role", role);
560        putIfNotNull(map, "seed", seed);
561        putStopTokensIfNotNull(map, stopTokens);
562        putIfNotNull(map, "temperature", temperature);
563        putIfNotNull(map, "vector_index_name", vectorIndexName);
564        putIfNotNull(map, "source_language", sourceLanguage);
565        putIfNotNull(map, "target_language", targetLanguage);
566        map.putAll(customAttributes);
567        return map;
568    }
569
570    /**
571     * Builds a {@link ProfileAttributes} instance from an attribute name/value map.
572     * <p>
573     * Recognized keys are converted to strongly-typed builder fields; unknown
574     * keys are accepted so callers can retain custom attributes. Values for
575     * SDK-known provider, OCI API format, and object-list mode fields are
576     * validated as creation/update values. Use
577     * {@link #fromDatabaseAttributeMap(Map)} for database metadata when those
578     * values may have been introduced by a newer database release.
579     *
580     * @param attributes attribute map to convert using SDK validation
581     * @return populated {@link ProfileAttributes} instance
582     */
583    public static ProfileAttributes fromAttributeMap(Map<String, String> attributes) {
584        return fromAttributeMap(attributes, false);
585    }
586
587    /**
588     * Builds profile attributes from values read from the database.
589     * <p>
590     * Database metadata may contain values introduced by a newer database
591     * release. Unlike the public builder methods used to create or update a
592     * profile, this hydration path preserves database-defined values for
593     * {@code provider}, {@code oci_apiformat}, and {@code object_list_mode}
594     * without applying the SDK's current-value allow-list.
595     *
596     * @param attributes attribute map read from database metadata
597     * @return profile attributes containing the database values
598     */
599    public static ProfileAttributes fromDatabaseAttributeMap(Map<String, String> attributes) {
600        return fromAttributeMap(attributes, true);
601    }
602
603    private static ProfileAttributes fromAttributeMap(Map<String, String> attributes,
604                                                      boolean databaseHydration) {
605        Builder builder = ProfileAttributes.builder();
606        if (attributes == null || attributes.isEmpty()) {
607            return builder.build();
608        }
609
610        attributes.forEach((k, v) -> {
611            if (k == null || v == null) {
612                return;
613            }
614            String key = k.toLowerCase(Locale.ROOT);
615            switch (key) {
616                case "annotations" -> builder.annotations(Boolean.parseBoolean(v));
617                case "additional_instructions" -> builder.additionalInstructions(v);
618                case "azure_deployment_name" -> builder.azureDeploymentName(v);
619                case "azure_embedding_deployment_name" -> builder.azureEmbeddingDeploymentName(v);
620                case "azure_resource_name" -> builder.azureResourceName(v);
621                case "case_sensitive_values" -> builder.caseSensitiveValues(Boolean.parseBoolean(v));
622                case "comments" -> builder.comments(Boolean.parseBoolean(v));
623                case "constraints" -> builder.constraints(Boolean.parseBoolean(v));
624                case "conversation" -> builder.conversation(Boolean.parseBoolean(v));
625                case "conversation_length" -> builder.conversationLength(
626                        parseInteger(v, databaseHydration, "conversation_length"));
627                case "credential_name" -> builder.credentialName(v);
628                case "embedding_model" -> builder.embeddingModel(v);
629                case "enable_custom_source_uri" -> builder.enableCustomSourceUri(Boolean.parseBoolean(v));
630                case "enforce_object_list" -> builder.enforceObjectList(Boolean.parseBoolean(v));
631                case "max_tokens" -> builder.maxTokens(
632                        parseInteger(v, databaseHydration, "max_tokens"));
633                case "model" -> builder.model(v);
634                case "object_list" -> builder.objectList(v);
635                case "object_list_mode" -> {
636                    if (databaseHydration) {
637                        builder.objectListModeFromDatabase(v);
638                    } else {
639                        builder.objectListMode(v);
640                    }
641                }
642                case "oci_apiformat" -> {
643                    if (databaseHydration) {
644                        builder.ociApiformatFromDatabase(v);
645                    } else {
646                        builder.ociApiformat(v);
647                    }
648                }
649                case "oci_compartment_id" -> builder.ociCompartmentId(v);
650                case "oci_endpoint_id" -> builder.ociEndpointId(v);
651                case "oci_runtimetype" -> builder.ociRuntimetype(v);
652                case "provider" -> {
653                    if (databaseHydration) {
654                        builder.providerFromDatabase(v);
655                    } else {
656                        builder.provider(v);
657                    }
658                }
659                case "provider_endpoint" -> builder.providerEndpoint(v);
660                case "region" -> builder.region(v);
661                case "role" -> builder.role(v);
662                case "seed" -> builder.seed(parseLong(v, databaseHydration, "seed"));
663                case "stop_tokens" -> builder.stopTokens(parseStopTokens(v, databaseHydration));
664                case "temperature" -> builder.temperature(
665                        parseDouble(v, databaseHydration, "temperature"));
666                case "vector_index_name" -> builder.vectorIndexName(v);
667                case "source_language" -> builder.sourceLanguage(v);
668                case "target_language" -> builder.targetLanguage(v);
669                default -> builder.customAttribute(k, v);
670            }
671        });
672        return builder.build();
673    }
674
675    private static Integer parseInteger(String value, boolean databaseHydration, String attributeName) {
676        try {
677            return Integer.valueOf(value);
678        } catch (NumberFormatException e) {
679            if (databaseHydration) {
680                return null;
681            }
682            throw new IllegalArgumentException(attributeName + " must be an integer", e);
683        }
684    }
685
686    private static Long parseLong(String value, boolean databaseHydration, String attributeName) {
687        try {
688            return Long.valueOf(value);
689        } catch (NumberFormatException e) {
690            if (databaseHydration) {
691                return null;
692            }
693            throw new IllegalArgumentException(attributeName + " must be a long", e);
694        }
695    }
696
697    private static Double parseDouble(String value, boolean databaseHydration, String attributeName) {
698        try {
699            return Double.valueOf(value);
700        } catch (NumberFormatException e) {
701            if (databaseHydration) {
702                return null;
703            }
704            throw new IllegalArgumentException(attributeName + " must be a double", e);
705        }
706    }
707
708    /**
709     * Adds a non-null profile attribute value to the output map.
710     *
711     * @param map destination attribute map
712     * @param key DBMS_CLOUD_AI attribute name
713     * @param value value to stringify and add
714     */
715    private static void putIfNotNull(Map<String, String> map, String key, Object value) {
716        if (value != null) {
717            map.put(key, String.valueOf(value));
718        }
719    }
720
721    /**
722     * Adds stop tokens as JSON array text so string-valued attribute maps can
723     * preserve token boundaries.
724     *
725     * @param map destination attribute map
726     * @param stopTokens stop-token list
727     */
728    private static void putStopTokensIfNotNull(Map<String, String> map, List<String> stopTokens) {
729        if (stopTokens == null) {
730            return;
731        }
732        try {
733            map.put("stop_tokens", new ObjectMapper().writeValueAsString(stopTokens));
734        } catch (Exception e) {
735            throw new IllegalStateException("Failed to serialize stopTokens", e);
736        }
737    }
738
739    /**
740     * Parses a stop_tokens attribute value serialized as a JSON array.
741     *
742     * @param value stop_tokens attribute value
743     * @return parsed stop-token list
744     */
745    private static List<String> parseStopTokens(String value) {
746        return parseStopTokens(value, false);
747    }
748
749    private static List<String> parseStopTokens(String value, boolean databaseHydration) {
750        if (value == null || value.isBlank()) {
751            return null;
752        }
753        try {
754            ObjectMapper mapper = new ObjectMapper();
755            var root = mapper.readTree(value);
756            if (!root.isArray()) {
757                throw new IllegalArgumentException("stop_tokens must be a JSON array");
758            }
759            List<String> parsedStopTokens = new ArrayList<>();
760            for (var node : root) {
761                if (!node.isTextual()) {
762                    throw new IllegalArgumentException("stop_tokens must contain only string values");
763                }
764                parsedStopTokens.add(node.asText());
765            }
766            return parsedStopTokens;
767        } catch (IllegalArgumentException e) {
768            if (databaseHydration) {
769                return null;
770            }
771            throw e;
772        } catch (Exception e) {
773            if (databaseHydration) {
774                return null;
775            }
776            throw new IllegalArgumentException("stop_tokens must be valid JSON array text", e);
777        }
778    }
779
780    /**
781     * Serializes the profile attributes using snake_case JSON names.
782     *
783     * @return JSON representation of this ProfileAttributes instance
784     */
785    public String toJson() {
786        ObjectMapper mapper = new ObjectMapper();
787        try {
788            mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
789            mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
790            return mapper.writeValueAsString(this);
791        } catch (Exception e) {
792            throw new IllegalStateException("Failed to serialize ProfileAttributes to JSON", e);
793        }
794    }
795    
796    /**
797     * Builder for {@link ProfileAttributes}.
798     */
799    public static final class Builder {
800        /** Annotation metadata flag being assembled. */
801        private Boolean annotations;
802        /** Additional prompt instructions being assembled. */
803        private String additionalInstructions;
804        /** Azure OpenAI deployment name being assembled. */
805        private String azureDeploymentName;
806        /** Azure embedding deployment name being assembled. */
807        private String azureEmbeddingDeploymentName;
808        /** Azure resource name being assembled. */
809        private String azureResourceName;
810        /** Case-sensitive value matching flag being assembled. */
811        private Boolean caseSensitiveValues;
812        /** Table/column comments flag being assembled. */
813        private Boolean comments;
814        /** Referential constraints flag being assembled. */
815        private Boolean constraints;
816        /** Conversation support flag being assembled. */
817        private Boolean conversation;
818        /** Conversation context length being assembled. */
819        private Integer conversationLength;
820        /** Provider credential name being assembled. */
821        private String credentialName;
822        /** Embedding model name being assembled. */
823        private String embeddingModel;
824        /** Custom source URI flag being assembled. */
825        private Boolean enableCustomSourceUri;
826        /** Object list enforcement flag being assembled. */
827        private Boolean enforceObjectList;
828        /** Maximum output token count being assembled. */
829        private Integer maxTokens;
830        /** Provider model name being assembled. */
831        private String model;
832        /** Object list JSON being assembled. */
833        private String objectList;
834        /** Object list mode being assembled. */
835        private String objectListMode;
836        /** OCI API format being assembled. */
837        private String ociApiformat;
838        /** OCI compartment OCID being assembled. */
839        private String ociCompartmentId;
840        /** OCI endpoint OCID being assembled. */
841        private String ociEndpointId;
842        /** OCI runtime type being assembled. */
843        private String ociRuntimetype;
844        /** Provider name being assembled. */
845        private String provider;
846        /** Provider endpoint being assembled. */
847        private String providerEndpoint;
848        /** Provider region being assembled. */
849        private String region;
850        /** Role prompt being assembled. */
851        private String role;
852        /** Deterministic sampling seed being assembled. */
853        private Long seed;
854        /** Stop token list being assembled. */
855        private List<String> stopTokens;
856        /** Provider temperature being assembled. */
857        private Double temperature;
858        /** Unmodeled attributes accepted for forward compatibility. */
859        private Map<String, String> customAttributes;
860        /** Vector index name being assembled. */
861        private String vectorIndexName;
862        /** Translation source language being assembled. */
863        private String sourceLanguage;
864        /** Translation target language being assembled. */
865        private String targetLanguage;
866        /** Provider profile shortcut that populated this builder. */
867        private ProviderProfile providerProfile;
868
869        private Builder() {
870            this.customAttributes = new HashMap<>();
871        }
872
873        /**
874         * Sets whether table/column annotations are included in metadata.
875         *
876         * @param annotations annotation metadata flag
877         * @return this builder instance
878         */
879        public Builder annotations(Boolean annotations) {
880            this.annotations = annotations;
881            return this;
882        }
883
884        /**
885         * Sets additional instructions included in Select AI prompts.
886         *
887         * @param additionalInstructions additional prompt instructions
888         * @return this builder instance
889         */
890        public Builder additionalInstructions(String additionalInstructions) {
891            this.additionalInstructions = additionalInstructions;
892            return this;
893        }
894
895        /**
896         * Sets the Azure OpenAI deployment name for the generation model.
897         *
898         * @param azureDeploymentName Azure deployment name
899         * @return this builder instance
900         */
901        public Builder azureDeploymentName(String azureDeploymentName) {
902            this.azureDeploymentName = azureDeploymentName;
903            return this;
904        }
905
906        /**
907         * Sets the Azure OpenAI deployment name for the embedding model.
908         *
909         * @param azureEmbeddingDeploymentName Azure embedding deployment name
910         * @return this builder instance
911         */
912        public Builder azureEmbeddingDeploymentName(String azureEmbeddingDeploymentName) {
913            this.azureEmbeddingDeploymentName = azureEmbeddingDeploymentName;
914            return this;
915        }
916
917        /**
918         * Sets the Azure OpenAI resource name.
919         *
920         * @param azureResourceName Azure resource name
921         * @return this builder instance
922         */
923        public Builder azureResourceName(String azureResourceName) {
924            this.azureResourceName = azureResourceName;
925            return this;
926        }
927
928        /**
929         * Sets whether generated SQL value matching is case-sensitive.
930         *
931         * @param caseSensitiveValues case-sensitive values flag
932         * @return this builder instance
933         */
934        public Builder caseSensitiveValues(Boolean caseSensitiveValues) {
935            this.caseSensitiveValues = caseSensitiveValues;
936            return this;
937        }
938
939        /**
940         * Sets whether table and column comments are included in metadata.
941         *
942         * @param comments comments metadata flag
943         * @return this builder instance
944         */
945        public Builder comments(Boolean comments) {
946            this.comments = comments;
947            return this;
948        }
949
950        /**
951         * Sets whether referential constraints are included in metadata.
952         *
953         * @param constraints constraints metadata flag
954         * @return this builder instance
955         */
956        public Builder constraints(Boolean constraints) {
957            this.constraints = constraints;
958            return this;
959        }
960
961        /**
962         * Sets whether conversation history is enabled for the profile.
963         *
964         * @param conversation conversation flag
965         * @return this builder instance
966         */
967        public Builder conversation(Boolean conversation) {
968            this.conversation = conversation;
969            return this;
970        }
971
972        /**
973         * Sets how many conversation turns are retained for context.
974         *
975         * @param conversationLength positive conversation context length
976         * @return this builder instance
977         */
978        public Builder conversationLength(Integer conversationLength) {
979            if (conversationLength != null && conversationLength <= 0) {
980                throw new IllegalArgumentException("conversationLength must be greater than 0");
981            }
982            this.conversationLength = conversationLength;
983            return this;
984        }
985
986        /**
987         * Sets the database credential used for provider API calls.
988         *
989         * @param credentialName credential name
990         * @return this builder instance
991         */
992        public Builder credentialName(String credentialName) {
993            this.credentialName = credentialName;
994            return this;
995        }
996
997        /**
998         * Sets the embedding model name.
999         *
1000         * @param embeddingModel embedding model name
1001         * @return this builder instance
1002         */
1003        public Builder embeddingModel(String embeddingModel) {
1004            this.embeddingModel = embeddingModel;
1005            return this;
1006        }
1007
1008        /**
1009         * Sets whether custom source URI metadata is enabled.
1010         *
1011         * @param enableCustomSourceUri custom source URI flag
1012         * @return this builder instance
1013         */
1014        public Builder enableCustomSourceUri(Boolean enableCustomSourceUri) {
1015            this.enableCustomSourceUri = enableCustomSourceUri;
1016            return this;
1017        }
1018
1019        /**
1020         * Sets whether generated SQL is restricted to the object list.
1021         *
1022         * @param enforceObjectList object list enforcement flag
1023         * @return this builder instance
1024         */
1025        public Builder enforceObjectList(Boolean enforceObjectList) {
1026            this.enforceObjectList = enforceObjectList;
1027            return this;
1028        }
1029
1030        /**
1031         * Sets the maximum generated output tokens.
1032         *
1033         * @param maxTokens positive token limit
1034         * @return this builder instance
1035         */
1036        public Builder maxTokens(Integer maxTokens) {
1037            if (maxTokens != null && maxTokens <= 0) {
1038                throw new IllegalArgumentException("maxTokens must be greater than 0");
1039            }
1040            this.maxTokens = maxTokens;
1041            return this;
1042        }
1043
1044        /**
1045         * Sets the provider model identifier.
1046         *
1047         * @param model provider model identifier
1048         * @return this builder instance
1049         */
1050        public Builder model(String model) {
1051            this.model = model;
1052            return this;
1053        }
1054
1055        /**
1056         * Sets the object list JSON used to constrain metadata.
1057         *
1058         * @param objectList JSON array containing object descriptors
1059         * @return this builder instance
1060         */
1061        public Builder objectList(String objectList) {
1062            this.objectList = objectList;
1063            return this;
1064        }
1065
1066        /**
1067         * Sets the object-list selection mode used for NL2SQL processing.
1068         *
1069         * <p>A {@code null} value leaves the object-list mode unset. A non-null value
1070         * must correspond to an object-list mode supported by the SDK; otherwise,
1071         * this method throws an {@link IllegalArgumentException}.</p>
1072         *
1073         * @param objectListMode object-list mode, or {@code null} to leave the mode unset
1074         * @return this builder instance
1075         * @throws IllegalArgumentException if {@code objectListMode} is non-null and
1076         *         does not correspond to a supported object-list mode
1077         */
1078        public Builder objectListMode(String objectListMode) {
1079            ObjectListMode parsedObjectListMode =
1080                    ObjectListMode.fromValue(objectListMode);
1081            this.objectListMode = parsedObjectListMode == null ? null : parsedObjectListMode.getValue();
1082            return this;
1083        }
1084
1085        /**
1086         * Sets the object-list selection mode used for NL2SQL processing.
1087         *
1088         * @param objectListMode object-list mode
1089         * @return this builder instance
1090         */
1091        public Builder objectListMode(ObjectListMode objectListMode) {
1092            this.objectListMode = objectListMode == null ? null : objectListMode.getValue();
1093            return this;
1094        }
1095
1096        /** Stores a database-defined object-list mode without SDK allow-list validation. */
1097        private Builder objectListModeFromDatabase(String objectListMode) {
1098            try {
1099                ObjectListMode parsed = ObjectListMode.fromValue(objectListMode);
1100                this.objectListMode = parsed == null ? null : parsed.getValue();
1101            } catch (IllegalArgumentException e) {
1102                this.objectListMode = objectListMode;
1103            }
1104            return this;
1105        }
1106
1107        /**
1108         * Sets the OCI Generative AI API format.
1109         *
1110         * @param ociApiformat OCI API format, such as COHERE or GENERIC
1111         * @return this builder instance
1112         */
1113        public Builder ociApiformat(String ociApiformat) {
1114            OciApiFormat parsedOciApiFormat = OciApiFormat.fromValue(ociApiformat);
1115            this.ociApiformat = parsedOciApiFormat == null ? null : parsedOciApiFormat.getValue();
1116            return this;
1117        }
1118
1119        /**
1120         * Sets the OCI Generative AI API format.
1121         *
1122         * @param ociApiFormat OCI API format
1123         * @return this builder instance
1124         */
1125        public Builder ociApiformat(OciApiFormat ociApiFormat) {
1126            this.ociApiformat = ociApiFormat == null ? null : ociApiFormat.getValue();
1127            return this;
1128        }
1129
1130        /** Stores a database-defined OCI API format without SDK allow-list validation. */
1131        private Builder ociApiformatFromDatabase(String ociApiformat) {
1132            try {
1133                OciApiFormat parsed = OciApiFormat.fromValue(ociApiformat);
1134                this.ociApiformat = parsed == null ? null : parsed.getValue();
1135            } catch (IllegalArgumentException e) {
1136                this.ociApiformat = ociApiformat;
1137            }
1138            return this;
1139        }
1140
1141        /**
1142         * Sets the OCI compartment OCID.
1143         *
1144         * @param ociCompartmentId OCI compartment OCID
1145         * @return this builder instance
1146         */
1147        public Builder ociCompartmentId(String ociCompartmentId) {
1148            this.ociCompartmentId = ociCompartmentId;
1149            return this;
1150        }
1151
1152        /**
1153         * Sets the OCI dedicated endpoint OCID.
1154         *
1155         * @param ociEndpointId OCI endpoint OCID
1156         * @return this builder instance
1157         */
1158        public Builder ociEndpointId(String ociEndpointId) {
1159            this.ociEndpointId = ociEndpointId;
1160            return this;
1161        }
1162
1163        /**
1164         * Sets the OCI runtime type attribute.
1165         *
1166         * @param ociRuntimetype OCI runtime type
1167         * @return this builder instance
1168         */
1169        public Builder ociRuntimetype(String ociRuntimetype) {
1170            this.ociRuntimetype = ociRuntimetype;
1171            return this;
1172        }
1173
1174        /**
1175         * Sets the AI provider for the profile.
1176         *
1177         * <p>A {@code null} value leaves the provider unset. A non-null value must
1178         * correspond to a provider supported by the SDK; otherwise, this method
1179         * throws an {@link IllegalArgumentException}.</p>
1180         *
1181         * @param provider provider name, or {@code null} to leave the provider unset
1182         * @return this builder instance
1183         * @throws IllegalArgumentException if {@code provider} is non-null and does
1184         *         not correspond to a supported provider
1185         */
1186        public Builder provider(String provider) {
1187            if (provider == null) {
1188                this.provider = null;
1189                return this;
1190            }
1191
1192            Provider parsedProvider = Provider.fromValue(provider);
1193
1194            if (parsedProvider == null) {
1195                throw new IllegalArgumentException(
1196                        "Unsupported provider: " + provider);
1197            }
1198
1199            this.provider = parsedProvider.name();
1200            return this;
1201        }
1202
1203        /** Stores a database-defined provider without SDK allow-list validation. */
1204        private Builder providerFromDatabase(String provider) {
1205            try {
1206                Provider parsed = Provider.fromValue(provider);
1207                this.provider = parsed == null ? null : parsed.name();
1208            } catch (IllegalArgumentException e) {
1209                this.provider = provider;
1210            }
1211            return this;
1212        }
1213
1214        /**
1215         * Sets the provider endpoint host/path.
1216         *
1217         * @param providerEndpoint provider endpoint
1218         * @return this builder instance
1219         */
1220        public Builder providerEndpoint(String providerEndpoint) {
1221            this.providerEndpoint = providerEndpoint;
1222            return this;
1223        }
1224
1225        /**
1226         * Sets the provider region.
1227         *
1228         * @param region provider region
1229         * @return this builder instance
1230         */
1231        public Builder region(String region) {
1232            this.region = region;
1233            return this;
1234        }
1235
1236        /**
1237         * Sets the role prompt used by Select AI.
1238         *
1239         * @param role role prompt
1240         * @return this builder instance
1241         */
1242        public Builder role(String role) {
1243            this.role = role;
1244            return this;
1245        }
1246
1247        /**
1248         * Sets the deterministic sampling seed.
1249         *
1250         * @param seed sampling seed
1251         * @return this builder instance
1252         */
1253        public Builder seed(Long seed) {
1254            this.seed = seed;
1255            return this;
1256        }
1257
1258        /**
1259         * Sets stop tokens that terminate generation.
1260         *
1261         * @param stopTokens stop token list
1262         * @return this builder instance
1263         */
1264        public Builder stopTokens(List<String> stopTokens) {
1265            this.stopTokens = stopTokens;
1266            return this;
1267        }
1268
1269        /**
1270         * Sets the provider sampling temperature.
1271         *
1272         * @param temperature non-negative temperature
1273         * @return this builder instance
1274         */
1275        public Builder temperature(Double temperature) {
1276            if (temperature != null && temperature < 0) {
1277                throw new IllegalArgumentException("temperature must be a non-negative float");
1278            }
1279            this.temperature = temperature;
1280            return this;
1281        }
1282
1283        /**
1284         * Sets the vector index name associated with the profile.
1285         *
1286         * @param vectorIndexName Oracle SQL identifier for the vector index
1287         * @return this builder instance
1288         */
1289        public Builder vectorIndexName(String vectorIndexName) {
1290            this.vectorIndexName = vectorIndexName;
1291            return this;
1292        }
1293
1294        /**
1295         * Sets the source language for translation.
1296         *
1297         * @param sourceLanguage source language name or code
1298         * @return this builder instance
1299         */
1300        public Builder sourceLanguage(String sourceLanguage) {
1301            this.sourceLanguage = sourceLanguage;
1302            return this;
1303        }
1304
1305        /**
1306         * Sets the target language for translation.
1307         *
1308         * @param targetLanguage target language name or code
1309         * @return this builder instance
1310         */
1311        public Builder targetLanguage(String targetLanguage) {
1312            this.targetLanguage = targetLanguage;
1313            return this;
1314        }
1315
1316        /**
1317         * Applies a provider profile shortcut to this builder.
1318         *
1319         * @param providerProfile provider profile to apply
1320         * @return this builder instance
1321         */
1322        public Builder providerProfile(ProviderProfile providerProfile) {
1323            if (providerProfile == null) {
1324                return this;
1325            }
1326            this.providerProfile = providerProfile;
1327            providerProfile.applyTo(this);
1328            return this;
1329        }
1330
1331        /**
1332         * Accepts an unmodeled profile attribute for forward compatibility.
1333         * <p>
1334         * Current typed JSON serialization only emits attributes represented by
1335         * this SDK. This method prevents unknown database attributes from failing a
1336         * read/round-trip through {@link #fromAttributeMap(Map)}.
1337         *
1338         * @param key DBMS_CLOUD_AI attribute name
1339         * @param value attribute value
1340         * @return this builder instance
1341         */
1342        public Builder customAttribute(String key, String value) {
1343            if (key != null && value != null) {
1344                this.customAttributes.put(key, value);
1345            }
1346            return this;
1347        }
1348
1349        /**
1350         * Builds immutable profile attributes.
1351         *
1352         * @return immutable ProfileAttributes instance built from current builder state
1353         */
1354        public ProfileAttributes build() {
1355            return new ProfileAttributes(this);
1356        }
1357    }
1358}