refs #739: formalize game query plan contract
This commit is contained in:
@@ -20,22 +20,64 @@
|
|||||||
|
|
||||||
## 3. 계약
|
## 3. 계약
|
||||||
|
|
||||||
`game_query_plan`은 다음 필드를 반환한다.
|
`game_query_plan`은 ReAct가 판단에 사용할 고정 JSON 계약을 반환한다. 자연어
|
||||||
|
설명, 후보 검색 원문, 기존 호환 필드는 진단 영역으로 분리하며 실행 판단은 아래
|
||||||
|
권위 필드만 사용한다.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contractVersion": "1.0",
|
||||||
|
"scopeType": "MULTI",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"mention": "사용자가 언급한 게임명",
|
||||||
|
"matchStatus": "MATCHED",
|
||||||
|
"dataStatus": "AVAILABLE",
|
||||||
|
"gameKey": "카탈로그 키 또는 null",
|
||||||
|
"gameId": "카탈로그 식별자 또는 null",
|
||||||
|
"userMasterObjectName": "승인 객체 또는 null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dataEligibleTargets": [],
|
||||||
|
"unresolvedTargets": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
고정 enum은 다음과 같다.
|
||||||
|
|
||||||
|
- `scopeType`: `NONE`, `SINGLE`, `MULTI`, `ALL`
|
||||||
|
- `matchStatus`: `NOT_APPLICABLE`, `MATCHED`, `UNMATCHED`
|
||||||
|
- `dataStatus`: `NOT_APPLICABLE`, `AVAILABLE`, `UNAVAILABLE`
|
||||||
|
|
||||||
|
`scopeType`은 질문에 언급된 범위만 나타낸다. 실행 가능성이나 SQL 조립 방식을
|
||||||
|
암시하지 않는다. `targets`는 언급 순서를 유지하고, `dataEligibleTargets`는 현재
|
||||||
|
질의에 사용할 수 있는 카탈로그 대상만, `unresolvedTargets`는 미매칭 또는 데이터
|
||||||
|
객체가 없는 대상을 보존한다. ReAct는 이 계약 전체를 관찰한 뒤 공통 팩트 단일
|
||||||
|
호출, 대상별 호출, 또는 빈 결과 응답을 선택한다.
|
||||||
|
|
||||||
|
기존 API 호환 기간에는 아래 필드도 반환할 수 있지만 ReAct의 실행 판단에 사용하지
|
||||||
|
않는다: `targetType`, `status`, `matchedGames`, `supportedGames`,
|
||||||
|
`mentionResults`, `dataIneligibleTargets`, `unmatchedGames`, `nextAction`,
|
||||||
|
`executionMode`.
|
||||||
|
|
||||||
|
기존 필드의 의미는 다음과 같다.
|
||||||
|
|
||||||
| 필드 | 내용 |
|
| 필드 | 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `targetType` | `NONE`, `SINGLE`, `MULTI`, `ALL` |
|
| `targetType` | 호환용 `scopeType` 별칭 |
|
||||||
| `status` | `NO_TARGET`, `SUPPORTED`, `PARTIAL`, `UNMATCHED` |
|
| `status` | 호환용 요약 상태. ReAct 실행 분기에 사용하지 않음 |
|
||||||
| `targets` | 언급 순서를 보존한 게임 대상 배열 |
|
| `targets` | 언급 순서를 보존한 게임 대상 배열 |
|
||||||
| `nextAction` | 항상 `CALL_FEWSHOT` |
|
| `nextAction` | 호환용 안내. 실행을 강제하지 않음 |
|
||||||
| `executionMode` | `UNSCOPED`, `SINGLE`, `COMBINED`, `ALL` |
|
| `executionMode` | 호환용 안내. 실행을 강제하지 않음 |
|
||||||
|
|
||||||
각 `targets` 항목은 다음 값을 가진다.
|
각 `targets` 항목은 다음 값을 가진다.
|
||||||
|
|
||||||
| 필드 | 내용 |
|
| 필드 | 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `mention` | 질문에서 추출한 게임명. `NONE`은 `null` |
|
| `mention` | 질문에서 추출한 게임명. `NONE`은 `null` |
|
||||||
| `status` | `NONE`, `MATCHED`, `UNMATCHED`, `CATALOG` |
|
| `status` | 호환용 대상 상태 |
|
||||||
|
| `matchStatus` | `NOT_APPLICABLE`, `MATCHED`, `UNMATCHED` |
|
||||||
|
| `dataStatus` | `NOT_APPLICABLE`, `AVAILABLE`, `UNAVAILABLE` |
|
||||||
| `gameKey`, `gameId` | DB 카탈로그가 반환한 게임 식별자 |
|
| `gameKey`, `gameId` | DB 카탈로그가 반환한 게임 식별자 |
|
||||||
| `gamePrefix` | DB 카탈로그가 반환한 prefix |
|
| `gamePrefix` | DB 카탈로그가 반환한 prefix |
|
||||||
| `gameName`, `aliases` | 표시명과 별칭 |
|
| `gameName`, `aliases` | 표시명과 별칭 |
|
||||||
@@ -193,3 +235,8 @@ read-only·객체 범위 검증만 담당한다.
|
|||||||
답변 이력 `422`는 `PASS`로 기록됐다. 직접 정답 실행 분기는 두지 않는다.
|
답변 이력 `422`는 `PASS`로 기록됐다. 직접 정답 실행 분기는 두지 않는다.
|
||||||
- 2026-07-28: `NONE`을 질의 차단으로 해석하지 않도록 경계 예제에 논리 객체 역할을
|
- 2026-07-28: `NONE`을 질의 차단으로 해석하지 않도록 경계 예제에 논리 객체 역할을
|
||||||
부여하고, 공통 객체 질의에는 경계 SQL을 일반화하지 않는 프롬프트 규칙을 추가했다.
|
부여하고, 공통 객체 질의에는 경계 SQL을 일반화하지 않는 프롬프트 규칙을 추가했다.
|
||||||
|
- 2026-07-29: ReAct가 자유 설명문이나 중복 요약 필드를 해석하지 않도록, 게임 범위
|
||||||
|
계획의 권위 JSON 계약을 `contractVersion`, `scopeType`, `targets[*].matchStatus`,
|
||||||
|
`targets[*].dataStatus`, `dataEligibleTargets`, `unresolvedTargets`로 정리했다.
|
||||||
|
이 계약은 게임명·prefix·물리 객체를 애플리케이션에 고정하지 않으며, 다음 도구 호출
|
||||||
|
횟수와 방식은 ReAct가 현재 질문과 계획 결과로 판단한다.
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ BEGIN
|
|||||||
END;
|
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
|
-- Complete game query planning inside ADB. The MCP server only invokes this
|
||||||
-- function and returns its structured result.
|
-- function and returns its structured result.
|
||||||
CREATE OR REPLACE FUNCTION sg_game_query_plan(
|
CREATE OR REPLACE FUNCTION sg_game_query_plan(
|
||||||
@@ -46,10 +63,16 @@ IS
|
|||||||
v_target_type VARCHAR2(20);
|
v_target_type VARCHAR2(20);
|
||||||
v_targets JSON_ARRAY_T := JSON_ARRAY_T();
|
v_targets JSON_ARRAY_T := JSON_ARRAY_T();
|
||||||
v_supported 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_unmatched JSON_ARRAY_T := JSON_ARRAY_T();
|
||||||
v_mention_results JSON_ARRAY_T := JSON_ARRAY_T();
|
v_mention_results JSON_ARRAY_T := JSON_ARRAY_T();
|
||||||
v_seen_game_keys t_seen_game_keys;
|
v_seen_game_keys t_seen_game_keys;
|
||||||
v_result JSON_OBJECT_T := JSON_OBJECT_T();
|
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_mention VARCHAR2(1000);
|
||||||
v_candidates JSON_ARRAY_T;
|
v_candidates JSON_ARRAY_T;
|
||||||
@@ -71,6 +94,7 @@ IS
|
|||||||
v_game_nm VARCHAR2(512);
|
v_game_nm VARCHAR2(512);
|
||||||
v_game_alias_nm VARCHAR2(512);
|
v_game_alias_nm VARCHAR2(512);
|
||||||
v_user_master_object_name VARCHAR2(128);
|
v_user_master_object_name VARCHAR2(128);
|
||||||
|
v_fact_scope_status VARCHAR2(30);
|
||||||
v_cosine_distance NUMBER;
|
v_cosine_distance NUMBER;
|
||||||
BEGIN
|
BEGIN
|
||||||
v_extract_raw := sg_game_extract_mentions(p_question);
|
v_extract_raw := sg_game_extract_mentions(p_question);
|
||||||
@@ -92,13 +116,14 @@ BEGIN
|
|||||||
FOR game_row IN (
|
FOR game_row IN (
|
||||||
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
|
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
|
||||||
user_master_object_name
|
user_master_object_name
|
||||||
FROM sg_game_catalog
|
FROM SGMP_POC.sg_game_catalog
|
||||||
WHERE active_yn = 'Y'
|
WHERE active_yn = 'Y'
|
||||||
ORDER BY priority, game_key
|
ORDER BY priority, game_key
|
||||||
) LOOP
|
) LOOP
|
||||||
v_target := JSON_OBJECT_T();
|
v_target := JSON_OBJECT_T();
|
||||||
v_target.put_null('mention');
|
v_target.put_null('mention');
|
||||||
v_target.put('status', 'CATALOG');
|
v_target.put('status', 'CATALOG');
|
||||||
|
v_target.put('matchStatus', 'MATCHED');
|
||||||
v_target.put('gameKey', game_row.game_key);
|
v_target.put('gameKey', game_row.game_key);
|
||||||
v_target.put('gameId', game_row.game_id);
|
v_target.put('gameId', game_row.game_id);
|
||||||
v_target.put('gamePrefix', game_row.game_prefix);
|
v_target.put('gamePrefix', game_row.game_prefix);
|
||||||
@@ -113,18 +138,34 @@ BEGIN
|
|||||||
CASE WHEN game_row.user_master_object_name IS NULL
|
CASE WHEN game_row.user_master_object_name IS NULL
|
||||||
THEN 'UNAVAILABLE' ELSE 'AVAILABLE' END
|
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_null('cosineDistance');
|
||||||
v_target.put('reasonCode', 'ALL_GAMES_CATALOG');
|
v_target.put('reasonCode', 'ALL_GAMES_CATALOG');
|
||||||
v_target.put('reason', 'Active game returned from the database catalog.');
|
v_target.put('reason', 'Active game returned from the database catalog.');
|
||||||
v_targets.append(v_target);
|
v_targets.append(v_target);
|
||||||
|
v_matched_count := v_matched_count + 1;
|
||||||
IF game_row.user_master_object_name IS NOT NULL THEN
|
IF game_row.user_master_object_name IS NOT NULL THEN
|
||||||
v_supported.append(v_target);
|
v_supported.append(v_target);
|
||||||
END IF;
|
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;
|
END LOOP;
|
||||||
ELSIF v_target_type = 'NONE' THEN
|
ELSIF v_target_type = 'NONE' THEN
|
||||||
v_target := JSON_OBJECT_T();
|
v_target := JSON_OBJECT_T();
|
||||||
v_target.put_null('mention');
|
v_target.put_null('mention');
|
||||||
v_target.put('status', 'NONE');
|
v_target.put('status', 'NONE');
|
||||||
|
v_target.put('matchStatus', 'NOT_APPLICABLE');
|
||||||
v_target.put_null('gameKey');
|
v_target.put_null('gameKey');
|
||||||
v_target.put_null('gameId');
|
v_target.put_null('gameId');
|
||||||
v_target.put_null('gamePrefix');
|
v_target.put_null('gamePrefix');
|
||||||
@@ -132,6 +173,8 @@ BEGIN
|
|||||||
v_target.put_null('aliases');
|
v_target.put_null('aliases');
|
||||||
v_target.put_null('userMasterObjectName');
|
v_target.put_null('userMasterObjectName');
|
||||||
v_target.put('objectStatus', 'NOT_APPLICABLE');
|
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_null('cosineDistance');
|
||||||
v_target.put('reasonCode', 'NO_GAME_TARGET');
|
v_target.put('reasonCode', 'NO_GAME_TARGET');
|
||||||
v_target.put(
|
v_target.put(
|
||||||
@@ -143,7 +186,7 @@ BEGIN
|
|||||||
FOR i IN 0 .. v_mentions.get_size - 1 LOOP
|
FOR i IN 0 .. v_mentions.get_size - 1 LOOP
|
||||||
v_mention := v_mentions.get_string(i);
|
v_mention := v_mentions.get_string(i);
|
||||||
v_candidates := JSON_ARRAY_T();
|
v_candidates := JSON_ARRAY_T();
|
||||||
v_cursor := sg_game_catalog_search(
|
v_cursor := SGMP_POC.sg_game_catalog_search(
|
||||||
v_mention,
|
v_mention,
|
||||||
LEAST(GREATEST(NVL(p_top_k, 5), 1), 20)
|
LEAST(GREATEST(NVL(p_top_k, 5), 1), 20)
|
||||||
);
|
);
|
||||||
@@ -174,6 +217,10 @@ BEGIN
|
|||||||
CASE WHEN v_user_master_object_name IS NULL
|
CASE WHEN v_user_master_object_name IS NULL
|
||||||
THEN 'UNAVAILABLE' ELSE 'AVAILABLE' END
|
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_candidate.put('cosineDistance', v_cosine_distance);
|
||||||
v_candidates.append(v_candidate);
|
v_candidates.append(v_candidate);
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -214,12 +261,23 @@ BEGIN
|
|||||||
v_target := JSON_OBJECT_T.parse(v_matched_candidate.to_clob);
|
v_target := JSON_OBJECT_T.parse(v_matched_candidate.to_clob);
|
||||||
v_target.put('mention', v_mention);
|
v_target.put('mention', v_mention);
|
||||||
v_target.put('status', 'MATCHED');
|
v_target.put('status', 'MATCHED');
|
||||||
|
v_target.put('matchStatus', 'MATCHED');
|
||||||
v_target.put('reasonCode', 'LLM_CANDIDATE_MATCH');
|
v_target.put('reasonCode', 'LLM_CANDIDATE_MATCH');
|
||||||
v_target.put('reason', v_reason);
|
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
|
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;
|
v_seen_game_keys(v_selected_key) := TRUE;
|
||||||
END IF;
|
END IF;
|
||||||
ELSE
|
ELSE
|
||||||
@@ -236,6 +294,7 @@ BEGIN
|
|||||||
v_target := JSON_OBJECT_T();
|
v_target := JSON_OBJECT_T();
|
||||||
v_target.put('mention', v_mention);
|
v_target.put('mention', v_mention);
|
||||||
v_target.put('status', 'UNMATCHED');
|
v_target.put('status', 'UNMATCHED');
|
||||||
|
v_target.put('matchStatus', 'UNMATCHED');
|
||||||
v_target.put_null('gameKey');
|
v_target.put_null('gameKey');
|
||||||
v_target.put_null('gameId');
|
v_target.put_null('gameId');
|
||||||
v_target.put_null('gamePrefix');
|
v_target.put_null('gamePrefix');
|
||||||
@@ -243,6 +302,8 @@ BEGIN
|
|||||||
v_target.put_null('aliases');
|
v_target.put_null('aliases');
|
||||||
v_target.put_null('userMasterObjectName');
|
v_target.put_null('userMasterObjectName');
|
||||||
v_target.put('objectStatus', 'UNAVAILABLE');
|
v_target.put('objectStatus', 'UNAVAILABLE');
|
||||||
|
v_target.put('factScopeStatus', 'UNAVAILABLE');
|
||||||
|
v_target.put('dataStatus', 'UNAVAILABLE');
|
||||||
v_target.put_null('cosineDistance');
|
v_target.put_null('cosineDistance');
|
||||||
v_target.put(
|
v_target.put(
|
||||||
'reasonCode',
|
'reasonCode',
|
||||||
@@ -259,21 +320,24 @@ BEGIN
|
|||||||
);
|
);
|
||||||
v_unmatched_item.put('reason', v_reason);
|
v_unmatched_item.put('reason', v_reason);
|
||||||
v_unmatched.append(v_unmatched_item);
|
v_unmatched.append(v_unmatched_item);
|
||||||
|
v_unresolved.append(v_target);
|
||||||
|
v_unmatched_count := v_unmatched_count + 1;
|
||||||
END IF;
|
END IF;
|
||||||
v_mention_results.append(v_mention_result);
|
v_mention_results.append(v_mention_result);
|
||||||
END LOOP;
|
END LOOP;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
v_result.put('contractVersion', '1.0');
|
||||||
v_result.put('targetType', v_target_type);
|
v_result.put('targetType', v_target_type);
|
||||||
v_result.put('scopeType', v_target_type);
|
v_result.put('scopeType', v_target_type);
|
||||||
v_result.put('extractScopeHint', v_scope_type);
|
v_result.put('extractScopeHint', v_scope_type);
|
||||||
IF v_target_type = 'NONE' THEN
|
IF v_target_type = 'NONE' THEN
|
||||||
v_result.put('status', 'NO_TARGET');
|
v_result.put('status', 'NO_TARGET');
|
||||||
ELSIF v_target_type = 'ALL' THEN
|
ELSIF v_matched_count = 0 THEN
|
||||||
v_result.put('status', 'SUPPORTED');
|
|
||||||
ELSIF v_supported.get_size = 0 THEN
|
|
||||||
v_result.put('status', 'UNMATCHED');
|
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');
|
v_result.put('status', 'PARTIAL');
|
||||||
ELSE
|
ELSE
|
||||||
v_result.put('status', 'SUPPORTED');
|
v_result.put('status', 'SUPPORTED');
|
||||||
@@ -282,6 +346,9 @@ BEGIN
|
|||||||
v_result.put('matchedGames', v_supported);
|
v_result.put('matchedGames', v_supported);
|
||||||
v_result.put('mentionResults', v_mention_results);
|
v_result.put('mentionResults', v_mention_results);
|
||||||
v_result.put('supportedGames', v_supported);
|
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('unmatchedGames', v_unmatched);
|
||||||
v_result.put('nextAction', 'CALL_FEWSHOT');
|
v_result.put('nextAction', 'CALL_FEWSHOT');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user