refs #739: formalize game query plan contract

This commit is contained in:
devmrko
2026-07-29 09:52:19 +09:00
parent e1fd486b01
commit e0ec0c9340
2 changed files with 128 additions and 14 deletions

View File

@@ -30,6 +30,23 @@ BEGIN
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(
@@ -46,10 +63,16 @@ IS
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;
@@ -71,6 +94,7 @@ IS
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);
@@ -92,13 +116,14 @@ BEGIN
FOR game_row IN (
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
user_master_object_name
FROM sg_game_catalog
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);
@@ -113,18 +138,34 @@ BEGIN
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');
@@ -132,6 +173,8 @@ BEGIN
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(
@@ -143,7 +186,7 @@ BEGIN
FOR i IN 0 .. v_mentions.get_size - 1 LOOP
v_mention := v_mentions.get_string(i);
v_candidates := JSON_ARRAY_T();
v_cursor := sg_game_catalog_search(
v_cursor := SGMP_POC.sg_game_catalog_search(
v_mention,
LEAST(GREATEST(NVL(p_top_k, 5), 1), 20)
);
@@ -174,6 +217,10 @@ BEGIN
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;
@@ -214,12 +261,23 @@ BEGIN
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_targets.append(v_target);
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_matched_candidate);
v_supported.append(v_target);
v_seen_game_keys(v_selected_key) := TRUE;
END IF;
ELSE
@@ -236,6 +294,7 @@ BEGIN
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');
@@ -243,6 +302,8 @@ BEGIN
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',
@@ -259,21 +320,24 @@ BEGIN
);
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_target_type = 'ALL' THEN
v_result.put('status', 'SUPPORTED');
ELSIF v_supported.get_size = 0 THEN
ELSIF v_matched_count = 0 THEN
v_result.put('status', 'UNMATCHED');
ELSIF v_unmatched.get_size > 0 THEN
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');
@@ -282,6 +346,9 @@ BEGIN
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');