222 lines
11 KiB
MySQL
222 lines
11 KiB
MySQL
-- ============================================================
|
|
-- 65_kb_select_ai_vpd_query_api.sql
|
|
--
|
|
-- Natural-language KB query API backing package.
|
|
-- Run as POC_2. The caller (CB_ORDS) establishes CB_AGENT_CTX before
|
|
-- invoking this package, so generated SQL is still subject to POC_2 VPD.
|
|
-- ============================================================
|
|
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
|
SET DEFINE OFF
|
|
SET FEEDBACK ON
|
|
|
|
PROMPT === Annotating KB schema for Select AI ===
|
|
COMMENT ON TABLE poc_2.kb_customers IS '고객원장. CUST_ID는 C로 시작하는 고객 식별자 예: C1001006.';
|
|
COMMENT ON COLUMN poc_2.kb_customers.cust_id IS '고객 식별자. C로 시작하는 값 예: C1001006.';
|
|
|
|
COMMENT ON TABLE poc_2.kb_products IS '상품원장 및 약관상품 마스터. PRODUCT_CD는 숫자형 상품/약관 코드 예: 41048.';
|
|
COMMENT ON COLUMN poc_2.kb_products.product_cd IS '상품 또는 약관상품 코드. 숫자형 코드 예: 41048. KB_CONTRACTS.PRODUCT_CD와 조인한다.';
|
|
COMMENT ON COLUMN poc_2.kb_products.clause_product_nm IS '약관상품명 또는 상품명. 고객 설명용 상품 비교에 사용한다.';
|
|
COMMENT ON COLUMN poc_2.kb_products.insurance_type IS '보험 대분류. 예: 자동차, 장기, 일반.';
|
|
COMMENT ON COLUMN poc_2.kb_products.product_type IS '상품 유형 또는 세부 상품군.';
|
|
COMMENT ON COLUMN poc_2.kb_products.clause_version IS '약관 버전.';
|
|
COMMENT ON COLUMN poc_2.kb_products.sale_status IS '판매 상태.';
|
|
COMMENT ON COLUMN poc_2.kb_products.active_yn IS '상품 활성 여부.';
|
|
|
|
COMMENT ON TABLE poc_2.kb_contracts IS '계약원장. CONTRACT_NO는 CT로 시작하는 계약번호이고, PRODUCT_CD는 숫자형 상품/약관 코드다.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.contract_no IS '계약번호. CT로 시작하는 값 예: CT2699001. 41048 같은 순수 숫자 코드는 계약번호가 아니라 PRODUCT_CD일 가능성이 높다.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.cust_id IS '계약 고객 식별자. KB_CUSTOMERS.CUST_ID와 조인한다.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.product_cd IS '계약의 상품/약관 코드. 숫자형 코드 예: 41048. KB_PRODUCTS.PRODUCT_CD와 조인한다.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.fc_id IS '담당 설계사 또는 FC 사용자 ID. 예: FC00789.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.fc_channel IS '담당 채널. 예: 설계사, GA, 다이렉트.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.contract_status IS '계약 상태. 예: 정상, 실효.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.premium IS '계약 보험료.';
|
|
COMMENT ON COLUMN poc_2.kb_contracts.pay_cycle IS '보험료 납입 주기.';
|
|
|
|
COMMENT ON TABLE poc_2.kb_coverages IS '담보원장. 계약번호 CONTRACT_NO 기준으로 KB_CONTRACTS와 조인해 보장/특약을 설명한다.';
|
|
COMMENT ON COLUMN poc_2.kb_coverages.contract_no IS '담보가 속한 계약번호. KB_CONTRACTS.CONTRACT_NO와 조인한다.';
|
|
COMMENT ON COLUMN poc_2.kb_coverages.cust_id IS '담보 고객 식별자.';
|
|
COMMENT ON COLUMN poc_2.kb_coverages.coverage_nm IS '담보명 또는 특약명.';
|
|
COMMENT ON COLUMN poc_2.kb_coverages.coverage_type IS '담보 유형.';
|
|
COMMENT ON COLUMN poc_2.kb_coverages.coverage_div IS '담보 구분.';
|
|
COMMENT ON COLUMN poc_2.kb_coverages.insured_amt IS '가입금액 또는 보장금액.';
|
|
COMMENT ON COLUMN poc_2.kb_coverages.renew_due_dt IS '갱신 예정일.';
|
|
|
|
COMMENT ON TABLE poc_2.kb_external_holdings IS '외부보유정보 원장. 타 보험사 보유계약과 약관상품을 고객 CUST_ID 기준으로 비교한다.';
|
|
COMMENT ON COLUMN poc_2.kb_external_holdings.cust_id IS '외부보유정보 고객 식별자. KB_CUSTOMERS.CUST_ID와 조인한다.';
|
|
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_insurer IS '외부 보험사명. 예: 삼성화재.';
|
|
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_product_grp IS '외부 상품군. 예: 자동차.';
|
|
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_product_type IS '외부 상품 유형. 예: 개인용.';
|
|
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_clause_nm IS '외부 약관상품명.';
|
|
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_sale_status IS '외부 상품 판매 상태.';
|
|
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_renew_month IS '외부 보유계약 갱신월.';
|
|
|
|
PROMPT === Creating VPD-aware Select AI query API ===
|
|
CREATE OR REPLACE PACKAGE poc_2.kb_select_ai_vpd_query_api AUTHID DEFINER AS
|
|
PROCEDURE open_query(
|
|
p_prompt IN CLOB,
|
|
p_limit IN PLS_INTEGER,
|
|
p_rows OUT SYS_REFCURSOR,
|
|
p_generated_sql OUT CLOB
|
|
);
|
|
END kb_select_ai_vpd_query_api;
|
|
/
|
|
|
|
CREATE OR REPLACE PACKAGE BODY poc_2.kb_select_ai_vpd_query_api AS
|
|
c_profile_name CONSTANT VARCHAR2(128) := 'KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1';
|
|
c_max_prompt_length CONSTANT PLS_INTEGER := 4000;
|
|
c_max_rows CONSTANT PLS_INTEGER := 100;
|
|
|
|
FUNCTION normalized_sql(p_generated_sql IN CLOB) RETURN VARCHAR2 IS
|
|
v_sql VARCHAR2(32767);
|
|
v_comment_end PLS_INTEGER;
|
|
BEGIN
|
|
IF p_generated_sql IS NULL OR DBMS_LOB.GETLENGTH(p_generated_sql) = 0 THEN
|
|
RAISE_APPLICATION_ERROR(-20811, 'Select AI did not generate SQL.');
|
|
END IF;
|
|
IF DBMS_LOB.GETLENGTH(p_generated_sql) > 32767 THEN
|
|
RAISE_APPLICATION_ERROR(-20812, 'Generated SQL is too long.');
|
|
END IF;
|
|
|
|
v_sql := TRIM(DBMS_LOB.SUBSTR(p_generated_sql, 32767, 1));
|
|
|
|
-- Select AI sometimes wraps an otherwise valid SQL statement in markdown
|
|
-- fences or adds a short leading explanation comment. Strip only those
|
|
-- wrapper artifacts before the strict safety validator runs. Comments that
|
|
-- remain inside the executable statement are still rejected below.
|
|
LOOP
|
|
v_sql := TRIM(v_sql);
|
|
v_sql := REGEXP_REPLACE(v_sql, '^[[:space:]]*```[[:alpha:]]*[[:space:]]*', '');
|
|
v_sql := REGEXP_REPLACE(v_sql, '[[:space:]]*```[[:space:]]*$', '');
|
|
v_sql := TRIM(v_sql);
|
|
|
|
IF SUBSTR(v_sql, 1, 2) = '--' THEN
|
|
v_comment_end := INSTR(v_sql, CHR(10));
|
|
IF v_comment_end = 0 THEN
|
|
RAISE_APPLICATION_ERROR(-20813, 'Select AI returned only a comment, not SQL.');
|
|
END IF;
|
|
v_sql := SUBSTR(v_sql, v_comment_end + 1);
|
|
ELSIF SUBSTR(v_sql, 1, 2) = '/*' THEN
|
|
v_comment_end := INSTR(v_sql, '*/');
|
|
IF v_comment_end = 0 THEN
|
|
RAISE_APPLICATION_ERROR(-20814, 'Unclosed generated SQL comment is not allowed.');
|
|
END IF;
|
|
v_sql := SUBSTR(v_sql, v_comment_end + 2);
|
|
ELSE
|
|
EXIT;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
v_sql := RTRIM(TRIM(v_sql), ';');
|
|
RETURN TRIM(v_sql);
|
|
END normalized_sql;
|
|
|
|
PROCEDURE assert_safe_kb_select(p_sql IN VARCHAR2) IS
|
|
-- Select AI normally double-quotes Oracle identifiers. Validate a quote-free
|
|
-- copy so POC_2.KB_CLAIMS and "POC_2"."KB_CLAIMS" follow the same policy.
|
|
v_upper VARCHAR2(32767) := REPLACE(UPPER(p_sql), '"', '');
|
|
-- Mask string literals before checking semicolons, keywords, packages, and
|
|
-- data dictionary names. A harmless CASE output string should not be treated
|
|
-- as executable SQL syntax.
|
|
v_lexical VARCHAR2(32767) :=
|
|
REGEXP_REPLACE(REPLACE(UPPER(p_sql), '"', ''), '''(''''|[^''])*''', '''X''');
|
|
BEGIN
|
|
IF NOT REGEXP_LIKE(v_lexical, '^(SELECT|WITH)[[:space:]]') THEN
|
|
RAISE_APPLICATION_ERROR(
|
|
-20813,
|
|
'Only a single SELECT or WITH query is allowed. Generated prefix: '
|
|
|| SUBSTR(v_upper, 1, 160)
|
|
);
|
|
END IF;
|
|
IF INSTR(v_lexical, ';') > 0
|
|
OR INSTR(v_lexical, '--') > 0
|
|
OR INSTR(v_lexical, '/*') > 0
|
|
OR INSTR(v_lexical, '*/') > 0 THEN
|
|
RAISE_APPLICATION_ERROR(
|
|
-20814,
|
|
'Comments or multiple SQL statements remain after normalization. Generated prefix: '
|
|
|| SUBSTR(v_upper, 1, 160)
|
|
);
|
|
END IF;
|
|
IF REGEXP_LIKE(
|
|
v_lexical,
|
|
'(^|[^A-Z_])(ALTER|BEGIN|COMMIT|CREATE|DECLARE|DELETE|DROP|EXECUTE|GRANT|INSERT|MERGE|REVOKE|ROLLBACK|TRUNCATE|UPDATE)([^A-Z_]|$)'
|
|
) THEN
|
|
RAISE_APPLICATION_ERROR(-20815, 'Only read-only SQL is allowed.');
|
|
END IF;
|
|
IF INSTR(v_lexical, 'DBMS_') > 0
|
|
OR INSTR(v_lexical, 'UTL_') > 0
|
|
OR INSTR(v_lexical, 'SYS.') > 0
|
|
OR INSTR(v_lexical, 'ADMIN.') > 0
|
|
OR INSTR(v_lexical, 'CB_') > 0
|
|
OR INSTR(v_lexical, 'ALL_') > 0
|
|
OR INSTR(v_lexical, 'DBA_') > 0
|
|
OR INSTR(v_lexical, 'USER_') > 0
|
|
OR INSTR(v_lexical, 'FOR UPDATE') > 0 THEN
|
|
RAISE_APPLICATION_ERROR(-20816, 'System, backoffice, and locking objects are not allowed.');
|
|
END IF;
|
|
IF NOT REGEXP_LIKE(
|
|
v_lexical,
|
|
'(FROM|JOIN)[[:space:]]+(POC_2[.])?KB_(CUSTOMERS|PRODUCTS|CONTRACTS|COVERAGES|CLAIMS|EXTERNAL_HOLDINGS|STAKEHOLDERS)([[:space:],)]|$)'
|
|
) THEN
|
|
RAISE_APPLICATION_ERROR(-20817, 'The generated SQL must query a permitted KB business table.');
|
|
END IF;
|
|
END assert_safe_kb_select;
|
|
|
|
PROCEDURE open_query(
|
|
p_prompt IN CLOB,
|
|
p_limit IN PLS_INTEGER,
|
|
p_rows OUT SYS_REFCURSOR,
|
|
p_generated_sql OUT CLOB
|
|
) IS
|
|
v_prompt VARCHAR2(4000);
|
|
v_generation_prompt CLOB;
|
|
v_generated_sql CLOB;
|
|
v_sql VARCHAR2(32767);
|
|
v_limit PLS_INTEGER;
|
|
BEGIN
|
|
IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(TRIM(p_prompt)) = 0 THEN
|
|
RAISE_APPLICATION_ERROR(-20801, 'prompt is required.');
|
|
END IF;
|
|
IF DBMS_LOB.GETLENGTH(p_prompt) > c_max_prompt_length THEN
|
|
RAISE_APPLICATION_ERROR(-20802, 'prompt must be 4,000 characters or fewer.');
|
|
END IF;
|
|
|
|
v_prompt := TRIM(DBMS_LOB.SUBSTR(p_prompt, c_max_prompt_length, 1));
|
|
IF REGEXP_LIKE(UPPER(v_prompt), 'SELECT[[:space:]]+AI') THEN
|
|
RAISE_APPLICATION_ERROR(-20803, 'SELECT AI action prefixes are not allowed in prompt.');
|
|
END IF;
|
|
v_limit := LEAST(GREATEST(NVL(p_limit, 50), 1), c_max_rows);
|
|
|
|
v_generation_prompt :=
|
|
'Return exactly one Oracle SQL SELECT or WITH statement and no markdown. '
|
|
|| 'Use only these POC_2 business tables: KB_CUSTOMERS, KB_PRODUCTS, KB_CONTRACTS, '
|
|
|| 'KB_COVERAGES, KB_CLAIMS, KB_EXTERNAL_HOLDINGS, KB_STAKEHOLDERS. '
|
|
|| 'Never use DDL, DML, PL/SQL, system views, backoffice tables, comments, or FOR UPDATE. '
|
|
|| 'The caller request is: ' || v_prompt;
|
|
|
|
v_generated_sql := DBMS_CLOUD_AI.GENERATE(
|
|
prompt => v_generation_prompt,
|
|
profile_name => c_profile_name,
|
|
action => 'showsql'
|
|
);
|
|
v_sql := normalized_sql(v_generated_sql);
|
|
assert_safe_kb_select(v_sql);
|
|
|
|
-- CB_AGENT_CTX was set by CB_ORDS before this call. Parsing this statement
|
|
-- in the same database session makes Oracle evaluate POC_2 VPD policies.
|
|
OPEN p_rows FOR
|
|
'SELECT * FROM (' || v_sql || ') WHERE ROWNUM <= :row_limit'
|
|
USING v_limit;
|
|
p_generated_sql := v_sql;
|
|
END open_query;
|
|
END kb_select_ai_vpd_query_api;
|
|
/
|
|
|
|
SHOW ERRORS
|
|
|
|
PROMPT === Granting fixed query API to CB_ORDS ===
|
|
GRANT EXECUTE ON poc_2.kb_select_ai_vpd_query_api TO cb_ords;
|
|
|
|
PROMPT === POC_2 Select AI VPD query API ready ===
|
|
EXIT
|