Files
vpd-permission-poc/database/adb/81_sg_game_query_plan.sql
2026-08-03 12:31:50 +09:00

365 lines
13 KiB
MySQL

-- OCI GenAI candidate selection. Vector distance only orders candidates; the
-- model may select one supplied game_key or explicitly reject all candidates.
CREATE OR REPLACE FUNCTION sg_game_match_candidate(
p_mention IN CLOB,
p_candidates_json IN CLOB
) RETURN CLOB AUTHID DEFINER
IS
v_prompt CLOB;
v_result CLOB;
BEGIN
v_prompt := 'Decide whether the extracted game-name mention refers to exactly '
|| 'one game in the supplied candidate array. Candidate vector distance is '
|| 'retrieval evidence only and must not establish identity. Compare the '
|| 'mention with candidate gameName, aliases, gameId, and gamePrefix. '
|| 'If one candidate clearly identifies the same game, return exactly '
|| '{"status":"MATCHED","game_key":"candidate key","reason":"short reason"}. '
|| 'If none clearly identifies the same game, return exactly '
|| '{"status":"UNMATCHED","game_key":null,"reason":"short reason"}. '
|| 'Never invent a game_key and never return a key absent from the supplied '
|| 'candidate array. Mention: '
|| DBMS_LOB.SUBSTR(p_mention, 1000, 1)
|| ' Candidates: '
|| DBMS_LOB.SUBSTR(p_candidates_json, 12000, 1);
v_result := DBMS_CLOUD_AI.GENERATE(
prompt => v_prompt,
profile_name => 'SGMP_POC_OCI_GPT54MINI',
action => 'chat'
);
RETURN v_result;
END;
/
-- A game can be known to the scope registry while not being an active alias
-- source for a common fact query. Keep that availability separate from a
-- role-specific physical-object availability such as a user-master table.
CREATE OR REPLACE FUNCTION sg_game_fact_scope_status(
p_game_id IN VARCHAR2
) RETURN VARCHAR2 AUTHID DEFINER IS
v_exists NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_exists
FROM SGMP_POC.comn_game_alias_bas
WHERE use_yn = 'Y'
AND game_id = p_game_id;
RETURN CASE WHEN v_exists > 0 THEN 'ACTIVE_ALIAS' ELSE 'REGISTRY_ONLY' END;
END;
/
-- Complete game query planning inside ADB. The MCP server only invokes this
-- function and returns its structured result.
CREATE OR REPLACE FUNCTION sg_game_query_plan(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 5
) RETURN CLOB AUTHID DEFINER
IS
TYPE t_seen_game_keys IS TABLE OF BOOLEAN INDEX BY VARCHAR2(128);
v_extract_raw CLOB;
v_extract JSON_OBJECT_T;
v_mentions JSON_ARRAY_T;
v_scope_type VARCHAR2(30);
v_target_type VARCHAR2(20);
v_targets JSON_ARRAY_T := JSON_ARRAY_T();
v_supported JSON_ARRAY_T := JSON_ARRAY_T();
v_data_eligible JSON_ARRAY_T := JSON_ARRAY_T();
v_data_ineligible JSON_ARRAY_T := JSON_ARRAY_T();
v_unresolved JSON_ARRAY_T := JSON_ARRAY_T();
v_unmatched JSON_ARRAY_T := JSON_ARRAY_T();
v_mention_results JSON_ARRAY_T := JSON_ARRAY_T();
v_seen_game_keys t_seen_game_keys;
v_result JSON_OBJECT_T := JSON_OBJECT_T();
v_matched_count PLS_INTEGER := 0;
v_data_eligible_count PLS_INTEGER := 0;
v_unmatched_count PLS_INTEGER := 0;
v_mention VARCHAR2(1000);
v_candidates JSON_ARRAY_T;
v_candidate JSON_OBJECT_T;
v_decision_raw CLOB;
v_decision JSON_OBJECT_T;
v_decision_status VARCHAR2(30);
v_selected_key VARCHAR2(128);
v_reason VARCHAR2(4000);
v_matched_candidate JSON_OBJECT_T;
v_mention_result JSON_OBJECT_T;
v_target JSON_OBJECT_T;
v_unmatched_item JSON_OBJECT_T;
v_cursor SYS_REFCURSOR;
v_game_key VARCHAR2(128);
v_game_id VARCHAR2(128);
v_game_prefix VARCHAR2(128);
v_game_nm VARCHAR2(512);
v_game_alias_nm VARCHAR2(512);
v_user_master_object_name VARCHAR2(128);
v_fact_scope_status VARCHAR2(30);
v_cosine_distance NUMBER;
BEGIN
v_extract_raw := sg_game_extract_mentions(p_question);
v_extract := JSON_OBJECT_T.parse(v_extract_raw);
v_mentions := v_extract.get_array('game_mentions');
v_scope_type := NVL(v_extract.get_string('scope_hint'), 'UNKNOWN');
IF v_scope_type = 'ALL_GAMES' THEN
v_target_type := 'ALL';
ELSIF v_mentions.get_size = 0 THEN
v_target_type := 'NONE';
ELSIF v_mentions.get_size = 1 THEN
v_target_type := 'SINGLE';
ELSE
v_target_type := 'MULTI';
END IF;
IF v_target_type = 'ALL' THEN
FOR game_row IN (
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
user_master_object_name
FROM SGMP_POC.sg_game_catalog
WHERE active_yn = 'Y'
ORDER BY priority, game_key
) LOOP
v_target := JSON_OBJECT_T();
v_target.put_null('mention');
v_target.put('status', 'CATALOG');
v_target.put('matchStatus', 'MATCHED');
v_target.put('gameKey', game_row.game_key);
v_target.put('gameId', game_row.game_id);
v_target.put('gamePrefix', game_row.game_prefix);
v_target.put('gameName', game_row.game_nm);
v_target.put('aliases', game_row.game_alias_nm);
v_target.put(
'userMasterObjectName',
game_row.user_master_object_name
);
v_target.put(
'objectStatus',
CASE WHEN game_row.user_master_object_name IS NULL
THEN 'UNAVAILABLE' ELSE 'AVAILABLE' END
);
v_fact_scope_status := sg_game_fact_scope_status(game_row.game_id);
v_target.put('factScopeStatus', v_fact_scope_status);
v_target.put(
'dataStatus',
CASE WHEN v_fact_scope_status = 'ACTIVE_ALIAS'
THEN 'AVAILABLE' ELSE 'UNAVAILABLE' END
);
v_target.put_null('cosineDistance');
v_target.put('reasonCode', 'ALL_GAMES_CATALOG');
v_target.put('reason', 'Active game returned from the database catalog.');
v_targets.append(v_target);
v_matched_count := v_matched_count + 1;
IF game_row.user_master_object_name IS NOT NULL THEN
v_supported.append(v_target);
END IF;
IF v_fact_scope_status = 'ACTIVE_ALIAS' THEN
v_data_eligible.append(v_target);
v_data_eligible_count := v_data_eligible_count + 1;
ELSE
v_data_ineligible.append(v_target);
v_unresolved.append(v_target);
END IF;
END LOOP;
ELSIF v_target_type = 'NONE' THEN
v_target := JSON_OBJECT_T();
v_target.put_null('mention');
v_target.put('status', 'NONE');
v_target.put('matchStatus', 'NOT_APPLICABLE');
v_target.put_null('gameKey');
v_target.put_null('gameId');
v_target.put_null('gamePrefix');
v_target.put_null('gameName');
v_target.put_null('aliases');
v_target.put_null('userMasterObjectName');
v_target.put('objectStatus', 'NOT_APPLICABLE');
v_target.put('factScopeStatus', 'NOT_APPLICABLE');
v_target.put('dataStatus', 'NOT_APPLICABLE');
v_target.put_null('cosineDistance');
v_target.put('reasonCode', 'NO_GAME_TARGET');
v_target.put(
'reason',
'The question does not select a particular game.'
);
v_targets.append(v_target);
ELSE
FOR i IN 0 .. v_mentions.get_size - 1 LOOP
v_mention := v_mentions.get_string(i);
v_candidates := JSON_ARRAY_T();
v_cursor := SGMP_POC.sg_game_catalog_search(
v_mention,
LEAST(GREATEST(NVL(p_top_k, 5), 1), 20)
);
LOOP
FETCH v_cursor INTO
v_game_key,
v_game_id,
v_game_prefix,
v_game_nm,
v_game_alias_nm,
v_user_master_object_name,
v_cosine_distance;
EXIT WHEN v_cursor%NOTFOUND;
v_candidate := JSON_OBJECT_T();
v_candidate.put('gameKey', v_game_key);
v_candidate.put('gameId', v_game_id);
v_candidate.put('gamePrefix', v_game_prefix);
v_candidate.put('gameName', v_game_nm);
v_candidate.put('aliases', v_game_alias_nm);
v_candidate.put(
'userMasterObjectName',
v_user_master_object_name
);
v_candidate.put(
'objectStatus',
CASE WHEN v_user_master_object_name IS NULL
THEN 'UNAVAILABLE' ELSE 'AVAILABLE' END
);
v_candidate.put(
'factScopeStatus',
sg_game_fact_scope_status(v_game_id)
);
v_candidate.put('cosineDistance', v_cosine_distance);
v_candidates.append(v_candidate);
END LOOP;
CLOSE v_cursor;
v_decision_raw := sg_game_match_candidate(
v_mention,
v_candidates.to_clob
);
v_decision := JSON_OBJECT_T.parse(v_decision_raw);
v_decision_status := UPPER(
NVL(v_decision.get_string('status'), 'UNMATCHED')
);
v_selected_key := v_decision.get_string('game_key');
v_reason := v_decision.get_string('reason');
v_matched_candidate := NULL;
IF v_decision_status = 'MATCHED' AND v_selected_key IS NOT NULL THEN
FOR j IN 0 .. v_candidates.get_size - 1 LOOP
v_candidate := TREAT(v_candidates.get(j) AS JSON_OBJECT_T);
IF v_candidate.get_string('gameKey') = v_selected_key THEN
v_matched_candidate := v_candidate;
EXIT;
END IF;
END LOOP;
END IF;
v_mention_result := JSON_OBJECT_T();
v_mention_result.put('mention', v_mention);
v_mention_result.put('candidateGames', v_candidates);
v_mention_result.put('reason', v_reason);
IF v_matched_candidate IS NOT NULL THEN
v_mention_result.put('status', 'MATCHED');
v_mention_result.put('reasonCode', 'LLM_CANDIDATE_MATCH');
v_mention_result.put('matchedGameKey', v_selected_key);
v_target := JSON_OBJECT_T.parse(v_matched_candidate.to_clob);
v_target.put('mention', v_mention);
v_target.put('status', 'MATCHED');
v_target.put('matchStatus', 'MATCHED');
v_target.put('reasonCode', 'LLM_CANDIDATE_MATCH');
v_target.put('reason', v_reason);
v_matched_count := v_matched_count + 1;
IF v_matched_candidate.get_string('factScopeStatus') = 'ACTIVE_ALIAS' THEN
v_target.put('dataStatus', 'AVAILABLE');
v_data_eligible.append(v_target);
v_data_eligible_count := v_data_eligible_count + 1;
ELSE
v_target.put('dataStatus', 'UNAVAILABLE');
v_data_ineligible.append(v_target);
v_unresolved.append(v_target);
END IF;
v_targets.append(v_target);
IF NOT v_seen_game_keys.EXISTS(v_selected_key) THEN
v_supported.append(v_target);
v_seen_game_keys(v_selected_key) := TRUE;
END IF;
ELSE
v_mention_result.put('status', 'UNMATCHED');
IF v_decision_status = 'MATCHED' THEN
v_mention_result.put(
'reasonCode',
'LLM_SELECTED_UNKNOWN_CANDIDATE'
);
ELSE
v_mention_result.put('reasonCode', 'LLM_NO_CANDIDATE_MATCH');
END IF;
v_target := JSON_OBJECT_T();
v_target.put('mention', v_mention);
v_target.put('status', 'UNMATCHED');
v_target.put('matchStatus', 'UNMATCHED');
v_target.put_null('gameKey');
v_target.put_null('gameId');
v_target.put_null('gamePrefix');
v_target.put_null('gameName');
v_target.put_null('aliases');
v_target.put_null('userMasterObjectName');
v_target.put('objectStatus', 'UNAVAILABLE');
v_target.put('factScopeStatus', 'UNAVAILABLE');
v_target.put('dataStatus', 'UNAVAILABLE');
v_target.put_null('cosineDistance');
v_target.put(
'reasonCode',
v_mention_result.get_string('reasonCode')
);
v_target.put('reason', v_reason);
v_targets.append(v_target);
v_unmatched_item := JSON_OBJECT_T();
v_unmatched_item.put('mention', v_mention);
v_unmatched_item.put(
'reasonCode',
v_mention_result.get_string('reasonCode')
);
v_unmatched_item.put('reason', v_reason);
v_unmatched.append(v_unmatched_item);
v_unresolved.append(v_target);
v_unmatched_count := v_unmatched_count + 1;
END IF;
v_mention_results.append(v_mention_result);
END LOOP;
END IF;
v_result.put('contractVersion', '1.0');
v_result.put('targetType', v_target_type);
v_result.put('scopeType', v_target_type);
v_result.put('extractScopeHint', v_scope_type);
IF v_target_type = 'NONE' THEN
v_result.put('status', 'NO_TARGET');
ELSIF v_matched_count = 0 THEN
v_result.put('status', 'UNMATCHED');
ELSIF v_data_eligible_count = 0 THEN
v_result.put('status', 'UNAVAILABLE');
ELSIF v_unmatched_count > 0 THEN
v_result.put('status', 'PARTIAL');
ELSE
v_result.put('status', 'SUPPORTED');
END IF;
v_result.put('targets', v_targets);
v_result.put('matchedGames', v_supported);
v_result.put('mentionResults', v_mention_results);
v_result.put('supportedGames', v_supported);
v_result.put('dataEligibleTargets', v_data_eligible);
v_result.put('unresolvedTargets', v_unresolved);
v_result.put('dataIneligibleTargets', v_data_ineligible);
v_result.put('unmatchedGames', v_unmatched);
v_result.put('nextAction', 'CALL_FEWSHOT');
CASE v_target_type
WHEN 'NONE' THEN v_result.put('executionMode', 'UNSCOPED');
WHEN 'SINGLE' THEN v_result.put('executionMode', 'SINGLE');
WHEN 'MULTI' THEN v_result.put('executionMode', 'COMBINED');
WHEN 'ALL' THEN v_result.put('executionMode', 'ALL');
END CASE;
RETURN v_result.to_clob;
END;
/