refs #737: move game query planning into ADB MCP tool
This commit is contained in:
194
sql/adb/81_sg_game_query_plan.sql
Normal file
194
sql/adb/81_sg_game_query_plan.sql
Normal file
@@ -0,0 +1,194 @@
|
||||
-- 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;
|
||||
/
|
||||
|
||||
-- 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_supported 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_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_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_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');
|
||||
|
||||
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_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_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('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);
|
||||
IF NOT v_seen_game_keys.EXISTS(v_selected_key) THEN
|
||||
v_supported.append(v_matched_candidate);
|
||||
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_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);
|
||||
END IF;
|
||||
v_mention_results.append(v_mention_result);
|
||||
END LOOP;
|
||||
|
||||
v_result.put('scopeType', v_scope_type);
|
||||
IF v_supported.get_size = 0 THEN
|
||||
v_result.put('status', 'NO_MATCH');
|
||||
ELSIF v_unmatched.get_size > 0 THEN
|
||||
v_result.put('status', 'PARTIAL');
|
||||
ELSE
|
||||
v_result.put('status', 'SUPPORTED');
|
||||
END IF;
|
||||
v_result.put('matchedGames', v_supported);
|
||||
v_result.put('mentionResults', v_mention_results);
|
||||
v_result.put('supportedGames', v_supported);
|
||||
v_result.put('unmatchedGames', v_unmatched);
|
||||
|
||||
IF v_supported.get_size = 0 THEN
|
||||
v_result.put('nextAction', 'STOP_NO_MATCH');
|
||||
ELSIF v_scope_type = 'MULTI_GAME' THEN
|
||||
v_result.put('nextAction', 'CALL_FEWSHOT_PER_GAME');
|
||||
ELSE
|
||||
v_result.put('nextAction', 'CALL_FEWSHOT');
|
||||
END IF;
|
||||
|
||||
CASE v_scope_type
|
||||
WHEN 'SINGLE_GAME' THEN v_result.put('executionMode', 'SINGLE');
|
||||
WHEN 'MULTI_GAME' THEN v_result.put('executionMode', 'FAN_OUT');
|
||||
WHEN 'ALL_GAMES' THEN v_result.put('executionMode', 'GROUPED');
|
||||
ELSE v_result.put('executionMode', 'BLOCKED');
|
||||
END CASE;
|
||||
|
||||
RETURN v_result.to_clob;
|
||||
END;
|
||||
/
|
||||
Reference in New Issue
Block a user