From 88a292d7119cceaf79f5f355c1e729550b226c56 Mon Sep 17 00:00:00 2001 From: devmrko Date: Fri, 24 Jul 2026 14:23:33 +0900 Subject: [PATCH] feat: add dedicated Cohere Embed 4 QA vector retrieval --- database/adb/75_sgmp_qa_vector_retrieval.sql | 193 ++++++++++++++++++ .../723-sgmp-qa-vector-retrieval/README.md | 70 +++++++ scripts/setup-sgmp-qa-vector.sh | 61 ++++++ 3 files changed, 324 insertions(+) create mode 100644 database/adb/75_sgmp_qa_vector_retrieval.sql create mode 100644 docs/design/723-sgmp-qa-vector-retrieval/README.md create mode 100755 scripts/setup-sgmp-qa-vector.sh diff --git a/database/adb/75_sgmp_qa_vector_retrieval.sql b/database/adb/75_sgmp_qa_vector_retrieval.sql new file mode 100644 index 0000000..aa4165d --- /dev/null +++ b/database/adb/75_sgmp_qa_vector_retrieval.sql @@ -0,0 +1,193 @@ +-- SGMP QA example vector store. +-- +-- Run as SGMP_POC after scripts/setup-sgmp-qa-vector.sh has registered the +-- DBMS_VECTOR credential and granted the HTTPS ACL. No API key material is +-- stored in this file. +-- +-- Cohere Embed 4 is intentionally fixed to 1536 dimensions. Stored examples +-- use search_document; incoming questions use search_query. + +DECLARE + v_count PLS_INTEGER; +BEGIN + SELECT COUNT(*) INTO v_count + FROM user_tables + WHERE table_name = 'SG_QA_VECTOR_CONFIG'; + + IF v_count = 0 THEN + EXECUTE IMMEDIATE q'[ + CREATE TABLE sg_qa_vector_config ( + config_key VARCHAR2(64) PRIMARY KEY, + config_value VARCHAR2(4000) NOT NULL, + updated_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL + )]'; + END IF; +END; +/ + +MERGE INTO sg_qa_vector_config c +USING ( + SELECT 'CREDENTIAL_NAME' AS config_key, 'SGMP_POC_QA_VECTOR_CRED' AS config_value FROM dual + UNION ALL SELECT 'ENDPOINT_URL', 'https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText' FROM dual + UNION ALL SELECT 'MODEL_NAME', 'cohere.embed-v4.0' FROM dual + UNION ALL SELECT 'DIMENSION', '1536' FROM dual +) s +ON (c.config_key = s.config_key) +WHEN MATCHED THEN UPDATE SET c.config_value = s.config_value, c.updated_at = SYSTIMESTAMP +WHEN NOT MATCHED THEN INSERT (config_key, config_value) VALUES (s.config_key, s.config_value); +/ + +DECLARE + v_count PLS_INTEGER; +BEGIN + SELECT COUNT(*) INTO v_count + FROM user_tables + WHERE table_name = 'SG_QA_VECTOR_EXAMPLE'; + + IF v_count = 0 THEN + EXECUTE IMMEDIATE q'[ + CREATE TABLE sg_qa_vector_example ( + example_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + question CLOB NOT NULL, + answer_sql CLOB NOT NULL, + answer_text CLOB, + embedding_input CLOB NOT NULL, + embedding VECTOR(1536, FLOAT32) NOT NULL, + embedding_model VARCHAR2(128) DEFAULT 'cohere.embed-v4.0' NOT NULL, + created_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + updated_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL + )]'; + END IF; +END; +/ + +CREATE OR REPLACE FUNCTION sg_qa_vector_params(p_input_type IN VARCHAR2) + RETURN CLOB +AUTHID DEFINER +IS + v_credential VARCHAR2(4000); + v_endpoint VARCHAR2(4000); + v_model VARCHAR2(4000); +BEGIN + SELECT MAX(CASE WHEN config_key = 'CREDENTIAL_NAME' THEN config_value END), + MAX(CASE WHEN config_key = 'ENDPOINT_URL' THEN config_value END), + MAX(CASE WHEN config_key = 'MODEL_NAME' THEN config_value END) + INTO v_credential, v_endpoint, v_model + FROM sg_qa_vector_config; + + IF v_credential IS NULL OR v_endpoint IS NULL OR v_model IS NULL THEN + RAISE_APPLICATION_ERROR(-20001, 'SG QA vector configuration is incomplete.'); + END IF; + + RETURN TO_CLOB('{"provider":"ocigenai","credential_name":"') + || v_credential + || '","url":"' || v_endpoint + || '","model":"' || v_model + || '","truncate":"END"}'; +END; +/ + +CREATE OR REPLACE FUNCTION sg_qa_vector_store( + p_question IN CLOB, + p_answer_sql IN CLOB, + p_answer IN CLOB DEFAULT NULL +) RETURN NUMBER +AUTHID DEFINER +IS + v_input CLOB; + v_embedding VECTOR; + v_example_id NUMBER; +BEGIN + IF p_question IS NULL OR p_answer_sql IS NULL THEN + RAISE_APPLICATION_ERROR(-20002, 'question and answer_sql are required.'); + END IF; + + v_input := TO_CLOB('Question: ') || p_question + || TO_CLOB(CHR(10) || 'Answer SQL: ') || p_answer_sql + || CASE WHEN p_answer IS NULL THEN NULL ELSE TO_CLOB(CHR(10) || 'Answer: ') || p_answer END; + v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING( + v_input, + JSON(sg_qa_vector_params('search_document')) + ); + + INSERT INTO sg_qa_vector_example ( + question, answer_sql, answer_text, embedding_input, embedding, embedding_model + ) VALUES ( + p_question, p_answer_sql, p_answer, v_input, v_embedding, 'cohere.embed-v4.0' + ) RETURNING example_id INTO v_example_id; + + RETURN v_example_id; +END; +/ + +CREATE OR REPLACE FUNCTION sg_qa_vector_search( + p_question IN CLOB, + p_top_k IN PLS_INTEGER DEFAULT 3 +) RETURN SYS_REFCURSOR +AUTHID DEFINER +IS + v_query_vector VECTOR; + v_results SYS_REFCURSOR; +BEGIN + IF p_question IS NULL THEN + RAISE_APPLICATION_ERROR(-20003, 'question is required.'); + END IF; + IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN + RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.'); + END IF; + + v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING( + p_question, + JSON(sg_qa_vector_params('search_query')) + ); + + OPEN v_results FOR + SELECT example_id, + question, + answer_sql, + answer_text, + embedding_model, + vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance + FROM sg_qa_vector_example + ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id + FETCH FIRST p_top_k ROWS ONLY; + RETURN v_results; +END; +/ + +CREATE OR REPLACE FUNCTION sg_qa_vector_context( + p_question IN CLOB, + p_top_k IN PLS_INTEGER DEFAULT 3 +) RETURN CLOB +AUTHID DEFINER +IS + v_results SYS_REFCURSOR; + v_id NUMBER; + v_q CLOB; + v_sql CLOB; + v_answer CLOB; + v_model VARCHAR2(128); + v_dist NUMBER; + v_context CLOB := EMPTY_CLOB(); +BEGIN + v_results := sg_qa_vector_search(p_question, p_top_k); + LOOP + FETCH v_results INTO v_id, v_q, v_sql, v_answer, v_model, v_dist; + EXIT WHEN v_results%NOTFOUND; + v_context := v_context + || CASE WHEN DBMS_LOB.GETLENGTH(v_context) = 0 THEN NULL ELSE CHR(10) || CHR(10) END + || '[Example ' || v_id || ', cosine_distance=' || TO_CHAR(v_dist, 'FM0D000000') || ']' || CHR(10) + || 'Question: ' || v_q || CHR(10) + || 'Answer SQL: ' || v_sql + || CASE WHEN v_answer IS NULL THEN NULL ELSE CHR(10) || 'Answer: ' || v_answer END; + END LOOP; + CLOSE v_results; + RETURN v_context; +END; +/ + +COMMENT ON TABLE sg_qa_vector_example IS + 'Question-to-SQL QA examples embedded with OCI GenAI Cohere Embed 4 for retrieval-augmented prompt context.'; + +COMMENT ON COLUMN sg_qa_vector_example.embedding IS + '1536-dimensional Cohere Embed 4 document embedding; generated through the dedicated SGMP vector API credential.'; diff --git a/docs/design/723-sgmp-qa-vector-retrieval/README.md b/docs/design/723-sgmp-qa-vector-retrieval/README.md new file mode 100644 index 0000000..cc6169f --- /dev/null +++ b/docs/design/723-sgmp-qa-vector-retrieval/README.md @@ -0,0 +1,70 @@ +# 723. SGMP QA Vector Retrieval + +## Goal + +Store curated question, answer SQL, answer text, and their combined retrieval +document in `SGMP_POC`. Retrieve the top-K closest examples for a new question +and pass the returned context to the Text2SQL prompt in a later application +integration. + +## Security boundary + +- IAM identity: `sgmp-qa-vector-api` +- IAM group: `sgmp-vector-embed-group` +- IAM policy: only `use generative-ai-text-embedding in tenancy` +- Database credential: `SGMP_POC_QA_VECTOR_CRED`, created from that dedicated + API signing key only. It does not reuse `SGMP_POC_OCI_DEFAULT_CRED`. +- Network: HTTPS only to OCI GenAI Chicago EmbedText endpoint on port 443. The + `SGMP_POC` ACE is provisioned once by an ADB `ADMIN` connection because an + application schema cannot administer network ACLs. +- The private key is read from `VECTOR_OCI_API_KEY_FILE`; it is never committed, + displayed, or persisted outside the encrypted database credential. + +## Embedding contract + +- Model: `cohere.embed-v4.0` +- Dimension: `1536` FLOAT32 +- The current ADB `DBMS_VECTOR` OCI adapter does not forward Cohere Embed 4's + `input_type` field; both paths therefore use the provider's compatible + default request shape. The model and 1536-dimension vector contract remain + fixed. Once the adapter exposes Embed 4 `input_type`, switch stored examples + to `search_document` and incoming questions to `search_query`. +- `p_top_k` default: `3` (accepted range `1..20`) + +Oracle recommends distinct document/query input types for Cohere Embed 4 RAG +flows and its default output size is 1536. See [Cohere Embed 4](https://docs.oracle.com/en-us/iaas/Content/generative-ai/cohere-embed-4.htm). + +## Database API + +```sql +-- Stores question + answer SQL + optional answer and returns EXAMPLE_ID. +SELECT sg_qa_vector_store(:question, :answer_sql, :answer_text) FROM dual; + +-- Returns EXAMPLE_ID, QUESTION, ANSWER_SQL, ANSWER_TEXT, MODEL and distance. +DECLARE + results SYS_REFCURSOR; +BEGIN + results := sg_qa_vector_search(:question); -- default top 3 +END; +/ + +-- Ready-to-insert textual context for a prompt. +SELECT sg_qa_vector_context(:question, 3) FROM dual; +``` + +## Apply + +```bash +export SGMP_POC_DB_PASSWORD='...' +export SGMP_POC_WALLET_DIR='/path/to/Wallet_SGMPAIPOC' +export VECTOR_OCI_USER_OCID='...' +export VECTOR_OCI_TENANCY_OCID='...' +export VECTOR_OCI_COMPARTMENT_OCID='...' +export VECTOR_OCI_API_KEY_FILE='/secure/path/sgmp_qa_vector_api_key.pem' +export VECTOR_OCI_API_KEY_FINGERPRINT='...' +./scripts/setup-sgmp-qa-vector.sh +``` + +For the initial small QA corpus, exact cosine search is deliberate: it makes +results immediately verifiable. Add a vector index only after the corpus size +and recall/latency target are measured. diff --git a/scripts/setup-sgmp-qa-vector.sh b/scripts/setup-sgmp-qa-vector.sh new file mode 100755 index 0000000..1b78f2f --- /dev/null +++ b/scripts/setup-sgmp-qa-vector.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Registers only the dedicated OCI API signing key for the QA vector store, +# then applies sql/adb/75_*.sql. The narrow outbound HTTPS ACL is an ADMIN +# operation and must be applied before this script runs. +# Secrets stay in environment variables and are never written to this repo. + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +: "${SGMP_POC_DB_PASSWORD:?SGMP_POC_DB_PASSWORD is required}" + +SQLCL_BIN="${SQLCL_BIN:-sql}" +SGMP_POC_DB_USER="${SGMP_POC_DB_USER:-SGMP_POC}" +SGMP_POC_DB_SERVICE="${SGMP_POC_DB_SERVICE:-sgmpaipoc_low}" +SGMP_POC_WALLET_DIR="${SGMP_POC_WALLET_DIR:?SGMP_POC_WALLET_DIR is required}" +VECTOR_OCI_USER_OCID="${VECTOR_OCI_USER_OCID:?VECTOR_OCI_USER_OCID is required}" +VECTOR_OCI_TENANCY_OCID="${VECTOR_OCI_TENANCY_OCID:?VECTOR_OCI_TENANCY_OCID is required}" +VECTOR_OCI_COMPARTMENT_OCID="${VECTOR_OCI_COMPARTMENT_OCID:?VECTOR_OCI_COMPARTMENT_OCID is required}" +VECTOR_OCI_API_KEY_FILE="${VECTOR_OCI_API_KEY_FILE:?VECTOR_OCI_API_KEY_FILE is required}" +VECTOR_OCI_API_KEY_FINGERPRINT="${VECTOR_OCI_API_KEY_FINGERPRINT:?VECTOR_OCI_API_KEY_FINGERPRINT is required}" + +command -v "$SQLCL_BIN" >/dev/null +command -v jq >/dev/null +[[ -f "$SGMP_POC_WALLET_DIR/tnsnames.ora" ]] +[[ -f "$VECTOR_OCI_API_KEY_FILE" ]] + +private_key_json="$(jq -Rs . "$VECTOR_OCI_API_KEY_FILE")" +credential_params="$(jq -cn \ + --arg user_ocid "$VECTOR_OCI_USER_OCID" \ + --arg tenancy_ocid "$VECTOR_OCI_TENANCY_OCID" \ + --arg compartment_ocid "$VECTOR_OCI_COMPARTMENT_OCID" \ + --argjson private_key "$private_key_json" \ + --arg fingerprint "$VECTOR_OCI_API_KEY_FINGERPRINT" \ + '{user_ocid:$user_ocid, tenancy_ocid:$tenancy_ocid, compartment_ocid:$compartment_ocid, private_key:$private_key, fingerprint:$fingerprint}')" +credential_params_b64="$(printf '%s' "$credential_params" | base64 | tr -d '\n')" + +"$SQLCL_BIN" -thin -L -S -tnsadmin "$SGMP_POC_WALLET_DIR" /nolog < 'SGMP_POC_QA_VECTOR_CRED', + params => JSON(v_params) + ); +END; +/ +@${ROOT}/sql/adb/75_sgmp_qa_vector_retrieval.sql +SELECT config_key, config_value FROM sg_qa_vector_config ORDER BY config_key; +exit success +SQL