-- 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; /