Compare commits
2 Commits
943c38f181
...
455e2aa222
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
455e2aa222 | ||
|
|
a17f18cffe |
@@ -59,6 +59,10 @@ export BACKOFFICE_SELECT_AI_DB_PASSWORD=""
|
|||||||
export BACKOFFICE_SELECT_AI_PROFILE=""
|
export BACKOFFICE_SELECT_AI_PROFILE=""
|
||||||
export BACKOFFICE_SELECT_AI_FEW_SHOT_ENABLED="true"
|
export BACKOFFICE_SELECT_AI_FEW_SHOT_ENABLED="true"
|
||||||
export BACKOFFICE_SELECT_AI_FEW_SHOT_TOP_K="3"
|
export BACKOFFICE_SELECT_AI_FEW_SHOT_TOP_K="3"
|
||||||
|
# Customer-owned DB view: game aliases, approved profile objects, and valid DB objects.
|
||||||
|
export BACKOFFICE_GAME_SCOPE_ENABLED="false"
|
||||||
|
export BACKOFFICE_GAME_SCOPE_VIEW=""
|
||||||
|
export BACKOFFICE_GAME_SCOPE_MAX_SCOPES="8"
|
||||||
|
|
||||||
# 공통 데이터 카탈로그. objects는 key/tableName/objectType/businessName/description JSON 배열입니다.
|
# 공통 데이터 카탈로그. objects는 key/tableName/objectType/businessName/description JSON 배열입니다.
|
||||||
# 배포 환경마다 반드시 실제 소유자와 허용 객체를 지정합니다.
|
# 배포 환경마다 반드시 실제 소유자와 허용 객체를 지정합니다.
|
||||||
@@ -81,6 +85,9 @@ export BACKOFFICE_MCP_QA_VECTOR_SEARCH_TOOL_DESCRIPTION="현재 질문에 사용
|
|||||||
export BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_NAME="oracle.select_ai.qa_vector_store"
|
export BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_NAME="oracle.select_ai.qa_vector_store"
|
||||||
export BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_LABEL="Select AI 예제 SQL 저장"
|
export BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_LABEL="Select AI 예제 SQL 저장"
|
||||||
export BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_DESCRIPTION="검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다."
|
export BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_DESCRIPTION="검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다."
|
||||||
|
export BACKOFFICE_MCP_GAME_SCOPE_TOOL_NAME="oracle.select_ai.game_scope_resolve"
|
||||||
|
export BACKOFFICE_MCP_GAME_SCOPE_TOOL_LABEL="게임 조회 범위 확인"
|
||||||
|
export BACKOFFICE_MCP_GAME_SCOPE_TOOL_DESCRIPTION="질문의 게임 별칭을 DB 범위 view로 확인하고, SUPPORTED 결과에만 Few-shot NL2SQL을 호출하도록 안내합니다."
|
||||||
# 마스킹 관리 대상. objectName/policyName JSON 배열이며, 비우면 어떤 DB 정책도 관리하지 않습니다.
|
# 마스킹 관리 대상. objectName/policyName JSON 배열이며, 비우면 어떤 DB 정책도 관리하지 않습니다.
|
||||||
export BACKOFFICE_MASKING_POLICIES=''
|
export BACKOFFICE_MASKING_POLICIES=''
|
||||||
# 보안 SQL 화면에 노출할 번들 SQL. fileName은 패키지의 sql/adb/ 아래 파일명만 허용됩니다.
|
# 보안 SQL 화면에 노출할 번들 SQL. fileName은 패키지의 sql/adb/ 아래 파일명만 허용됩니다.
|
||||||
|
|||||||
69
docs/design/731-sgmp-game-scope-resolver/README.md
Normal file
69
docs/design/731-sgmp-game-scope-resolver/README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# SGMP DB 기반 게임 범위 Resolver (#731)
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
복수 게임이 포함된 데이터 질문에서 애플리케이션 코드나 에이전트 지시문에 게임명, prefix,
|
||||||
|
테이블명을 넣지 않는다. DB가 제공하는 게임 범위 뷰를 먼저 조회하고, 조회 가능으로 판정된
|
||||||
|
게임에만 기존 Few-shot NL2SQL MCP를 호출한다.
|
||||||
|
|
||||||
|
## 범위와 원칙
|
||||||
|
|
||||||
|
- 기존 `oracle.select_ai.smilegate_fewshot_nl2sql`은 예제 검색, SQL 생성, 읽기 전용 실행을
|
||||||
|
담당하는 worker로 유지한다.
|
||||||
|
- 새 `game_scope_resolve` MCP는 SQL을 생성하거나 실행하지 않는다.
|
||||||
|
- 공통 백오피스는 환경변수로 지정된 DB view 이름과 MCP tool 이름만 안다.
|
||||||
|
- 게임명, alias, GAME_ID, GAME_PREFIX, 대상 object는 DB view의 데이터로만 결정한다.
|
||||||
|
- 지원 여부는 대상 날짜의 행 수가 아니라, 현재 승인된 조회 object가 존재하는지로 판정한다.
|
||||||
|
데이터가 0건인 날도 정상 조회 범위다.
|
||||||
|
|
||||||
|
## DB 공통 계약
|
||||||
|
|
||||||
|
고객 DB는 환경변수 `BACKOFFICE_GAME_SCOPE_VIEW`로 지정된 view를 제공한다. view는 아래
|
||||||
|
별칭(column alias)을 반환한다.
|
||||||
|
|
||||||
|
| Column | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| `GAME_KEY` | 내부 게임 식별자 |
|
||||||
|
| `PROFILE_NAME` | 승인 object list를 판정한 Select AI profile |
|
||||||
|
| `DISPLAY_NAME` | 화면 표시용 정식 게임명 |
|
||||||
|
| `GAME_ALIAS` | 질문에서 찾을 게임명 또는 별칭 |
|
||||||
|
| `QUERY_ALLOWED_YN` | 승인된 조회 object 존재 여부 (`Y`/`N`) |
|
||||||
|
| `REASON_CODE` | 미지원 또는 보류 사유 코드 |
|
||||||
|
| `ALIAS_PRIORITY` | 동일/중첩 alias 정렬 우선순위 |
|
||||||
|
| `SCOPE_VERSION` | object list 변경 시 함께 갱신되는 버전 |
|
||||||
|
|
||||||
|
Smilegate view는 전체 게임 마스터와 alias를 기준으로 하고, 현재 Select AI profile별 승인 object list와
|
||||||
|
실제 object 존재 여부를 조합해 `QUERY_ALLOWED_YN`을 계산한다. 따라서 등록 게임이지만 현재
|
||||||
|
조회 object가 없는 게임도 `N`으로 반환된다.
|
||||||
|
|
||||||
|
## MCP와 ReAct 계약
|
||||||
|
|
||||||
|
1. 포털 ReAct는 게임 데이터 질의 전에 `game_scope_resolve(question)`를 호출한다.
|
||||||
|
2. resolver는 질문 문자열과 `GAME_ALIAS`를 정규화해 포함 관계를 찾고, 우선순위와 alias 길이로
|
||||||
|
중복을 제거한다. 동일 우선순위의 복수 게임은 `AMBIGUOUS`로 반환한다.
|
||||||
|
3. `QUERY_ALLOWED_YN=Y`인 scope에는 서명·만료된 opaque `scopeToken`과 worker tool 이름을 반환한다.
|
||||||
|
4. ReAct는 `nextAction=CALL_WORKER`인 항목만 Few-shot NL2SQL에 전달한다. `UNSUPPORTED`와
|
||||||
|
`AMBIGUOUS`는 SQL 실행 없이 결과에 표시한다.
|
||||||
|
5. Few-shot worker는 scope token을 검증하고, token에 담긴 DB scope로만 prompt를 보강한다.
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
- view가 지원 게임과 object list 미연결 게임을 각각 반환하는지 확인한다.
|
||||||
|
- resolver MCP의 결과에 구체 게임/테이블 하드코딩이 없는지 확인한다.
|
||||||
|
- STD-06에서 미지원 게임은 worker가 호출되지 않고, 지원 게임 결과에는 few-shot 예제, 생성 SQL,
|
||||||
|
실행 결과가 포함되는지 확인한다.
|
||||||
|
- 기존 단일 게임 Few-shot NL2SQL 및 미게임명 거절 guardrail 회귀를 확인한다.
|
||||||
|
|
||||||
|
## 논리 조인 메타데이터
|
||||||
|
|
||||||
|
`COMN_GAME_ALIAS_BAS`는 하나의 게임에 여러 alias 행을 갖기 때문에 `GAME_ID`가 유일키가
|
||||||
|
아니다. 따라서 공통 transaction table의 `GAME_ID`에 물리 FK를 추가하지 않는다. 대신
|
||||||
|
`78_sgmp_game_alias_logical_joins.sql`이 fact table에 `GAME_ALIAS_JOIN` annotation을 추가한다.
|
||||||
|
이 annotation은 게임명 필터에서 `EXISTS` 또는 `DISTINCT GAME_ID` alias subquery를 사용하고,
|
||||||
|
alias 원본을 직접 조인해 집계 행을 늘리지 않도록 설명한다. Prefix 전용 테이블은 가짜 FK 없이
|
||||||
|
기존 alias/prefix 소유 범위 annotation을 유지한다.
|
||||||
|
|
||||||
|
## 보류 항목
|
||||||
|
|
||||||
|
전체 게임 마스터에 미지원 게임이 없다면 DB만으로 그 이름을 게임으로 식별할 수 없다. 이 경우
|
||||||
|
고객 원천 게임 마스터를 view에 연결하는 작업이 선행되어야 하며, 모델 추측으로 보완하지 않는다.
|
||||||
175
sql/adb/77_sgmp_game_scope_resolver.sql
Normal file
175
sql/adb/77_sgmp_game_scope_resolver.sql
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
-- DB-backed game query scope contract for MCP orchestration.
|
||||||
|
--
|
||||||
|
-- This script deliberately keeps game facts in database rows, not in application
|
||||||
|
-- code or Select AI instructions. The view combines the active game alias source
|
||||||
|
-- with an operator-maintained registry for known games that currently have no
|
||||||
|
-- approved query object. A zero-row business result is still queryable; only the
|
||||||
|
-- absence of an approved object makes a game scope unavailable.
|
||||||
|
|
||||||
|
DECLARE
|
||||||
|
v_count PLS_INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO v_count
|
||||||
|
FROM user_tables
|
||||||
|
WHERE table_name = 'SG_GAME_SCOPE_REGISTRY';
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
EXECUTE IMMEDIATE q'[
|
||||||
|
CREATE TABLE sg_game_scope_registry (
|
||||||
|
game_key VARCHAR2(128) NOT NULL,
|
||||||
|
display_name VARCHAR2(200) NOT NULL,
|
||||||
|
game_alias VARCHAR2(200) NOT NULL,
|
||||||
|
game_prefix VARCHAR2(30),
|
||||||
|
active_yn VARCHAR2(1) DEFAULT 'Y' NOT NULL,
|
||||||
|
alias_priority NUMBER(10) DEFAULT 100 NOT NULL,
|
||||||
|
source_type VARCHAR2(30) DEFAULT 'OPERATOR' NOT NULL,
|
||||||
|
work_dtm TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT sg_game_scope_registry_pk PRIMARY KEY (game_key, game_alias),
|
||||||
|
CONSTRAINT sg_game_scope_registry_active_ck CHECK (active_yn IN ('Y', 'N'))
|
||||||
|
)]';
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
|
||||||
|
-- Seed only database facts needed to recognise currently unavailable games in
|
||||||
|
-- the customer QA catalogue. Customer game-master synchronization can replace
|
||||||
|
-- these rows without an application deployment.
|
||||||
|
MERGE INTO sg_game_scope_registry t
|
||||||
|
USING (
|
||||||
|
SELECT 'LORDNINE' AS game_key,
|
||||||
|
utl_i18n.raw_to_char(
|
||||||
|
utl_encode.base64_decode(utl_raw.cast_to_raw('66Gc65Oc64KY7J24')),
|
||||||
|
'AL32UTF8'
|
||||||
|
) AS display_name,
|
||||||
|
utl_i18n.raw_to_char(
|
||||||
|
utl_encode.base64_decode(utl_raw.cast_to_raw('66Gc65Oc64KY7J24')),
|
||||||
|
'AL32UTF8'
|
||||||
|
) AS game_alias,
|
||||||
|
CAST(NULL AS VARCHAR2(30)) AS game_prefix,
|
||||||
|
100 AS alias_priority
|
||||||
|
FROM dual
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'BUBBLYZ', 'Bubblyz', 'Bubblyz', CAST(NULL AS VARCHAR2(30)), 100 FROM dual
|
||||||
|
) s
|
||||||
|
ON (t.game_key = s.game_key AND t.game_alias = s.game_alias)
|
||||||
|
WHEN MATCHED THEN UPDATE SET
|
||||||
|
t.display_name = s.display_name,
|
||||||
|
t.game_prefix = s.game_prefix,
|
||||||
|
t.active_yn = 'Y',
|
||||||
|
t.alias_priority = s.alias_priority,
|
||||||
|
t.source_type = 'OPERATOR',
|
||||||
|
t.work_dtm = SYSTIMESTAMP
|
||||||
|
WHEN NOT MATCHED THEN INSERT (
|
||||||
|
game_key, display_name, game_alias, game_prefix, active_yn, alias_priority, source_type
|
||||||
|
) VALUES (
|
||||||
|
s.game_key, s.display_name, s.game_alias, s.game_prefix, 'Y', s.alias_priority, 'OPERATOR'
|
||||||
|
);
|
||||||
|
/
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW sg_game_query_scope_v AS
|
||||||
|
WITH existing_query_objects AS (
|
||||||
|
-- USER_OBJECTS is the authoritative current-schema inventory. The profile
|
||||||
|
-- object list alone is not enough because a stale entry must not make a game
|
||||||
|
-- executable after its table or view has been removed or invalidated.
|
||||||
|
SELECT object_name
|
||||||
|
FROM user_objects
|
||||||
|
WHERE object_type IN ('TABLE', 'VIEW')
|
||||||
|
AND status = 'VALID'
|
||||||
|
),
|
||||||
|
profile_names AS (
|
||||||
|
SELECT DISTINCT profile_name
|
||||||
|
FROM user_cloud_ai_profile_attributes
|
||||||
|
WHERE attribute_name = 'object_list'
|
||||||
|
),
|
||||||
|
profile_objects AS (
|
||||||
|
SELECT DISTINCT p.profile_name, o.object_name
|
||||||
|
FROM user_cloud_ai_profile_attributes p,
|
||||||
|
JSON_TABLE(
|
||||||
|
p.attribute_value,
|
||||||
|
'$[*]' COLUMNS (object_name VARCHAR2(128) PATH '$.name')
|
||||||
|
) o
|
||||||
|
INNER JOIN existing_query_objects e
|
||||||
|
ON e.object_name = o.object_name
|
||||||
|
WHERE p.attribute_name = 'object_list'
|
||||||
|
),
|
||||||
|
source_alias AS (
|
||||||
|
SELECT game_id AS game_key,
|
||||||
|
game_nm AS display_name,
|
||||||
|
game_alias_nm AS game_alias,
|
||||||
|
game_prefix,
|
||||||
|
use_yn AS active_yn,
|
||||||
|
NVL(sort_order, 100) AS alias_priority,
|
||||||
|
'GAME_ALIAS' AS source_type
|
||||||
|
FROM comn_game_alias_bas
|
||||||
|
),
|
||||||
|
all_alias AS (
|
||||||
|
SELECT game_key, display_name, game_alias, game_prefix, active_yn, alias_priority, source_type
|
||||||
|
FROM source_alias
|
||||||
|
UNION ALL
|
||||||
|
SELECT r.game_key, r.display_name, r.game_alias, r.game_prefix,
|
||||||
|
r.active_yn, r.alias_priority, r.source_type
|
||||||
|
FROM sg_game_scope_registry r
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM source_alias a
|
||||||
|
WHERE a.game_key = r.game_key
|
||||||
|
AND a.game_alias = r.game_alias
|
||||||
|
)
|
||||||
|
),
|
||||||
|
scope_object AS (
|
||||||
|
SELECT n.profile_name,
|
||||||
|
a.game_key,
|
||||||
|
a.game_alias,
|
||||||
|
COUNT(p.object_name) AS approved_object_count
|
||||||
|
FROM profile_names n
|
||||||
|
CROSS JOIN all_alias a
|
||||||
|
LEFT JOIN profile_objects p
|
||||||
|
ON p.profile_name = n.profile_name
|
||||||
|
AND a.game_prefix IS NOT NULL
|
||||||
|
AND SUBSTR(p.object_name, 1, LENGTH(a.game_prefix) + 1) = a.game_prefix || '_'
|
||||||
|
GROUP BY n.profile_name, a.game_key, a.game_alias
|
||||||
|
)
|
||||||
|
SELECT o.profile_name,
|
||||||
|
a.game_key,
|
||||||
|
a.display_name,
|
||||||
|
a.game_alias,
|
||||||
|
a.game_prefix,
|
||||||
|
a.active_yn,
|
||||||
|
NVL(o.approved_object_count, 0) AS approved_object_count,
|
||||||
|
CASE
|
||||||
|
WHEN a.active_yn <> 'Y' THEN 'N'
|
||||||
|
WHEN NVL(o.approved_object_count, 0) > 0 THEN 'Y'
|
||||||
|
ELSE 'N'
|
||||||
|
END AS query_allowed_yn,
|
||||||
|
CASE
|
||||||
|
WHEN a.active_yn <> 'Y' THEN 'GAME_INACTIVE'
|
||||||
|
WHEN NVL(o.approved_object_count, 0) > 0 THEN 'APPROVED_OBJECT_AVAILABLE'
|
||||||
|
ELSE 'OBJECT_LIST_NOT_AVAILABLE'
|
||||||
|
END AS reason_code,
|
||||||
|
a.alias_priority,
|
||||||
|
a.source_type,
|
||||||
|
TO_CHAR(MAX(a.alias_priority) OVER (PARTITION BY a.game_key), 'FM999999990') AS scope_version
|
||||||
|
FROM all_alias a
|
||||||
|
LEFT JOIN scope_object o
|
||||||
|
ON o.game_key = a.game_key
|
||||||
|
AND o.game_alias = a.game_alias;
|
||||||
|
/
|
||||||
|
|
||||||
|
COMMENT ON TABLE sg_game_scope_registry IS
|
||||||
|
'Operator-managed game aliases retained for scope resolution when a game has no approved query object.';
|
||||||
|
COMMENT ON COLUMN sg_game_scope_registry.game_key IS
|
||||||
|
'Stable game identifier used only by the DB-backed scope contract.';
|
||||||
|
COMMENT ON COLUMN sg_game_scope_registry.game_alias IS
|
||||||
|
'Question text alias matched by the resolver before any SQL worker is called.';
|
||||||
|
COMMENT ON COLUMN sg_game_scope_registry.game_prefix IS
|
||||||
|
'Optional data-object prefix. The scope view derives approved object availability from it.';
|
||||||
|
COMMENT ON COLUMN sg_game_scope_registry.active_yn IS
|
||||||
|
'Whether the game is eligible for scope resolution; inactive games are never executable.';
|
||||||
|
COMMENT ON COLUMN sg_game_scope_registry.alias_priority IS
|
||||||
|
'Database-defined ordering used to resolve overlapping aliases without application constants.';
|
||||||
|
COMMENT ON COLUMN sg_game_query_scope_v.profile_name IS
|
||||||
|
'Select AI profile whose current approved object list was used for this scope decision.';
|
||||||
|
COMMENT ON COLUMN sg_game_query_scope_v.query_allowed_yn IS
|
||||||
|
'Y only when the active game has at least one current Select AI approved and valid prefix-specific table or view.';
|
||||||
|
COMMENT ON COLUMN sg_game_query_scope_v.reason_code IS
|
||||||
|
'Database-derived explanation for scope availability returned to the MCP agent.';
|
||||||
36
sql/adb/78_sgmp_game_alias_logical_joins.sql
Normal file
36
sql/adb/78_sgmp_game_alias_logical_joins.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- Logical game-alias join guidance for Select AI.
|
||||||
|
--
|
||||||
|
-- COMN_GAME_ALIAS_BAS intentionally has multiple alias rows per GAME_ID, so
|
||||||
|
-- GAME_ID cannot be modelled as a physical foreign key to that table. These
|
||||||
|
-- annotations describe the safe semantic relationship without asserting a
|
||||||
|
-- false database constraint or creating fan-out aggregation errors.
|
||||||
|
|
||||||
|
DECLARE v_result VARCHAR2(4000); BEGIN
|
||||||
|
v_result := sgmp_set_annotation(
|
||||||
|
'SGMP_POC', 'TABLE', 'COMN_SALES_TXN', NULL,
|
||||||
|
'Logical game filter: COMN_SALES_TXN.GAME_ID is resolved through COMN_GAME_ALIAS_BAS. GAME_ID is not unique in the alias table because one game can have multiple aliases. For a game-name filter, use EXISTS against active aliases or join a DISTINCT GAME_ID alias subquery. Do not directly join all alias rows before SUM or COUNT because that can multiply fact rows.',
|
||||||
|
'GAME_ALIAS_JOIN'
|
||||||
|
);
|
||||||
|
dbms_output.put_line(v_result);
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
|
||||||
|
DECLARE v_result VARCHAR2(4000); BEGIN
|
||||||
|
v_result := sgmp_set_annotation(
|
||||||
|
'SGMP_POC', 'TABLE', 'COMN_REFUND_TXN', NULL,
|
||||||
|
'Logical game filter: COMN_REFUND_TXN.GAME_ID is resolved through COMN_GAME_ALIAS_BAS. GAME_ID is not unique in the alias table because one game can have multiple aliases. For a game-name filter, use EXISTS against active aliases or join a DISTINCT GAME_ID alias subquery. Do not directly join all alias rows before SUM or COUNT because that can multiply fact rows.',
|
||||||
|
'GAME_ALIAS_JOIN'
|
||||||
|
);
|
||||||
|
dbms_output.put_line(v_result);
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
|
||||||
|
DECLARE v_result VARCHAR2(4000); BEGIN
|
||||||
|
v_result := sgmp_set_annotation(
|
||||||
|
'SGMP_POC', 'TABLE', 'COMN_SALES_PRODUCT_DISP_BAS', NULL,
|
||||||
|
'Logical product scope: COMN_SALES_PRODUCT_DISP_BAS is keyed by GAME_ID and PRODUCT_ID. Resolve a natural-language game through COMN_GAME_ALIAS_BAS using EXISTS or a DISTINCT GAME_ID alias subquery before joining product data to transaction facts. The alias table has multiple aliases per GAME_ID and is not a physical foreign-key parent.',
|
||||||
|
'GAME_ALIAS_JOIN'
|
||||||
|
);
|
||||||
|
dbms_output.put_line(v_result);
|
||||||
|
END;
|
||||||
|
/
|
||||||
@@ -20,7 +20,10 @@ public record McpProperties(
|
|||||||
String qaVectorStoreToolDescription,
|
String qaVectorStoreToolDescription,
|
||||||
String fewShotNl2SqlToolName,
|
String fewShotNl2SqlToolName,
|
||||||
String fewShotNl2SqlToolLabel,
|
String fewShotNl2SqlToolLabel,
|
||||||
String fewShotNl2SqlToolDescription
|
String fewShotNl2SqlToolDescription,
|
||||||
|
String gameScopeToolName,
|
||||||
|
String gameScopeToolLabel,
|
||||||
|
String gameScopeToolDescription
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private static final String DEFAULT_TOOL_NAME = "oracle.select_ai.data_text2sql";
|
private static final String DEFAULT_TOOL_NAME = "oracle.select_ai.data_text2sql";
|
||||||
@@ -56,6 +59,12 @@ public record McpProperties(
|
|||||||
private static final String DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION =
|
private static final String DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION =
|
||||||
"벡터 Few-shot 예제를 찾아 prompt에 반영하고, SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. "
|
"벡터 Few-shot 예제를 찾아 prompt에 반영하고, SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. "
|
||||||
+ "Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다.";
|
+ "Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다.";
|
||||||
|
private static final String DEFAULT_GAME_SCOPE_TOOL_NAME =
|
||||||
|
"oracle.select_ai.game_scope_resolve";
|
||||||
|
private static final String DEFAULT_GAME_SCOPE_TOOL_LABEL = "게임 조회 범위 확인";
|
||||||
|
private static final String DEFAULT_GAME_SCOPE_TOOL_DESCRIPTION =
|
||||||
|
"질문에서 언급된 게임 별칭을 DB 게임 범위 view로 확인합니다. 데이터 SQL은 실행하지 않으며, "
|
||||||
|
+ "반환된 SUPPORTED scope에만 Few-shot NL2SQL을 호출하세요.";
|
||||||
|
|
||||||
public String resolvedToolName() {
|
public String resolvedToolName() {
|
||||||
return requiredOrDefault(toolName, DEFAULT_TOOL_NAME);
|
return requiredOrDefault(toolName, DEFAULT_TOOL_NAME);
|
||||||
@@ -121,6 +130,18 @@ public record McpProperties(
|
|||||||
return requiredOrDefault(fewShotNl2SqlToolDescription, DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION);
|
return requiredOrDefault(fewShotNl2SqlToolDescription, DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String resolvedGameScopeToolName() {
|
||||||
|
return requiredOrDefault(gameScopeToolName, DEFAULT_GAME_SCOPE_TOOL_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolvedGameScopeToolLabel() {
|
||||||
|
return requiredOrDefault(gameScopeToolLabel, DEFAULT_GAME_SCOPE_TOOL_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolvedGameScopeToolDescription() {
|
||||||
|
return requiredOrDefault(gameScopeToolDescription, DEFAULT_GAME_SCOPE_TOOL_DESCRIPTION);
|
||||||
|
}
|
||||||
|
|
||||||
private String requiredOrDefault(String value, String fallback) {
|
private String requiredOrDefault(String value, String fallback) {
|
||||||
return value == null || value.isBlank() ? fallback : value.trim();
|
return value == null || value.isBlank() ? fallback : value.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,19 +21,22 @@ public class McpSseService {
|
|||||||
private final BackofficeProperties properties;
|
private final BackofficeProperties properties;
|
||||||
private final McpProperties mcpProperties;
|
private final McpProperties mcpProperties;
|
||||||
private final QaVectorService qaVectorService;
|
private final QaVectorService qaVectorService;
|
||||||
|
private final GameScopeService gameScopeService;
|
||||||
|
|
||||||
public McpSseService(
|
public McpSseService(
|
||||||
SelectAiService selectAiService,
|
SelectAiService selectAiService,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
BackofficeProperties properties,
|
BackofficeProperties properties,
|
||||||
McpProperties mcpProperties,
|
McpProperties mcpProperties,
|
||||||
QaVectorService qaVectorService
|
QaVectorService qaVectorService,
|
||||||
|
GameScopeService gameScopeService
|
||||||
) {
|
) {
|
||||||
this.selectAiService = selectAiService;
|
this.selectAiService = selectAiService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.mcpProperties = mcpProperties;
|
this.mcpProperties = mcpProperties;
|
||||||
this.qaVectorService = qaVectorService;
|
this.qaVectorService = qaVectorService;
|
||||||
|
this.gameScopeService = gameScopeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectNode handle(String contextPath, JsonNode request) {
|
public ObjectNode handle(String contextPath, JsonNode request) {
|
||||||
@@ -74,7 +77,7 @@ public class McpSseService {
|
|||||||
public List<McpToolView> registeredTools() {
|
public List<McpToolView> registeredTools() {
|
||||||
return List.of(
|
return List.of(
|
||||||
selectAiQueryView(), selectAiShowpromptView(), qaVectorSearchView(),
|
selectAiQueryView(), selectAiShowpromptView(), qaVectorSearchView(),
|
||||||
qaVectorStoreView(), fewShotNl2SqlView());
|
qaVectorStoreView(), fewShotNl2SqlView(), gameScopeView());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ObjectNode initializeResult(String contextPath) {
|
private ObjectNode initializeResult(String contextPath) {
|
||||||
@@ -98,6 +101,7 @@ public class McpSseService {
|
|||||||
tools.add(toolDefinition(qaVectorSearchView()));
|
tools.add(toolDefinition(qaVectorSearchView()));
|
||||||
tools.add(toolDefinition(qaVectorStoreView()));
|
tools.add(toolDefinition(qaVectorStoreView()));
|
||||||
tools.add(toolDefinition(fewShotNl2SqlView()));
|
tools.add(toolDefinition(fewShotNl2SqlView()));
|
||||||
|
tools.add(toolDefinition(gameScopeView()));
|
||||||
result.set("tools", tools);
|
result.set("tools", tools);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -112,7 +116,7 @@ public class McpSseService {
|
|||||||
ObjectNode properties = objectMapper.createObjectNode();
|
ObjectNode properties = objectMapper.createObjectNode();
|
||||||
|
|
||||||
ArrayNode required = objectMapper.createArrayNode();
|
ArrayNode required = objectMapper.createArrayNode();
|
||||||
if (qaVectorSearchToolName().equals(toolView.name())) {
|
if (qaVectorSearchToolName().equals(toolView.name()) || gameScopeToolName().equals(toolView.name())) {
|
||||||
addStringProperty(properties, "question", "few-shot 예제 SQL을 찾을 현재 질문입니다.", 4000);
|
addStringProperty(properties, "question", "few-shot 예제 SQL을 찾을 현재 질문입니다.", 4000);
|
||||||
ObjectNode topK = properties.putObject("topK");
|
ObjectNode topK = properties.putObject("topK");
|
||||||
topK.put("type", "integer");
|
topK.put("type", "integer");
|
||||||
@@ -129,6 +133,10 @@ public class McpSseService {
|
|||||||
required.add("answerSql");
|
required.add("answerSql");
|
||||||
} else {
|
} else {
|
||||||
addStringProperty(properties, "prompt", promptDescription(), 4000);
|
addStringProperty(properties, "prompt", promptDescription(), 4000);
|
||||||
|
if (fewShotNl2SqlToolName().equals(toolView.name())) {
|
||||||
|
addStringProperty(properties, "scopeGameKey",
|
||||||
|
"선택 사항입니다. game_scope_resolve가 반환한 SUPPORTED gameKey만 전달하세요.", 128);
|
||||||
|
}
|
||||||
required.add("prompt");
|
required.add("prompt");
|
||||||
}
|
}
|
||||||
schema.set("properties", properties);
|
schema.set("properties", properties);
|
||||||
@@ -152,7 +160,9 @@ public class McpSseService {
|
|||||||
boolean qaVectorSearchTool = qaVectorSearchToolName().equals(calledToolName);
|
boolean qaVectorSearchTool = qaVectorSearchToolName().equals(calledToolName);
|
||||||
boolean qaVectorStoreTool = qaVectorStoreToolName().equals(calledToolName);
|
boolean qaVectorStoreTool = qaVectorStoreToolName().equals(calledToolName);
|
||||||
boolean fewShotNl2SqlTool = fewShotNl2SqlToolName().equals(calledToolName);
|
boolean fewShotNl2SqlTool = fewShotNl2SqlToolName().equals(calledToolName);
|
||||||
if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool && !fewShotNl2SqlTool) {
|
boolean gameScopeTool = gameScopeToolName().equals(calledToolName);
|
||||||
|
if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool && !fewShotNl2SqlTool
|
||||||
|
&& !gameScopeTool) {
|
||||||
throw new AppException("등록되지 않은 MCP tool입니다: " + calledToolName);
|
throw new AppException("등록되지 않은 MCP tool입니다: " + calledToolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,10 +183,16 @@ public class McpSseService {
|
|||||||
arguments.path("answerSql").asText(""),
|
arguments.path("answerSql").asText(""),
|
||||||
arguments.path("answer").isMissingNode() ? null : arguments.path("answer").asText(null)
|
arguments.path("answer").isMissingNode() ? null : arguments.path("answer").asText(null)
|
||||||
));
|
));
|
||||||
|
} else if (gameScopeTool) {
|
||||||
|
response = gameScopeResponse(gameScopeService.resolve(
|
||||||
|
token, arguments.path("question").asText("")));
|
||||||
} else {
|
} else {
|
||||||
String prompt = arguments.path("prompt").asText("");
|
String prompt = arguments.path("prompt").asText("");
|
||||||
response = queryTool || fewShotNl2SqlTool
|
response = queryTool
|
||||||
? selectAiService.generateAndExecute(token, prompt)
|
? selectAiService.generateAndExecute(token, prompt)
|
||||||
|
: fewShotNl2SqlTool
|
||||||
|
? selectAiService.generateAndExecute(token, prompt,
|
||||||
|
arguments.path("scopeGameKey").asText(""))
|
||||||
: selectAiService.generatePrompt(token, prompt);
|
: selectAiService.generatePrompt(token, prompt);
|
||||||
}
|
}
|
||||||
} catch (VpdTokenAccessDeniedException ignored) {
|
} catch (VpdTokenAccessDeniedException ignored) {
|
||||||
@@ -231,6 +247,28 @@ public class McpSseService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Converts DB-derived scope facts to a tool response; this tool never executes query SQL. */
|
||||||
|
private ObjectNode gameScopeResponse(GameScopeService.GameScopeResult result) {
|
||||||
|
ObjectNode response = objectMapper.createObjectNode();
|
||||||
|
response.put("status", result.status());
|
||||||
|
response.put("instruction", "반환된 SUPPORTED scope에만 Few-shot NL2SQL을 호출하세요.");
|
||||||
|
response.put("question", result.question());
|
||||||
|
ArrayNode scopes = response.putArray("scopes");
|
||||||
|
for (GameScopeService.GameScope scope : result.scopes()) {
|
||||||
|
ObjectNode item = scopes.addObject();
|
||||||
|
item.put("gameKey", scope.gameKey());
|
||||||
|
item.put("displayName", scope.displayName());
|
||||||
|
item.put("matchedAlias", scope.matchedAlias());
|
||||||
|
item.put("status", scope.status());
|
||||||
|
item.put("reasonCode", scope.reasonCode());
|
||||||
|
item.put("aliasPriority", scope.aliasPriority());
|
||||||
|
item.put("scopeVersion", scope.scopeVersion());
|
||||||
|
item.put("nextAction", "SUPPORTED".equals(scope.status())
|
||||||
|
? "CALL_FEW_SHOT_NL2SQL" : "REPORT_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
private ObjectNode tokenAccessDeniedResult() {
|
private ObjectNode tokenAccessDeniedResult() {
|
||||||
ObjectNode payload = objectMapper.createObjectNode();
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
payload.put("status", "VPD_TOKEN_DENIED");
|
payload.put("status", "VPD_TOKEN_DENIED");
|
||||||
@@ -287,6 +325,12 @@ public class McpSseService {
|
|||||||
fewShotNl2SqlToolLabel(), SELECT_AI_TOOL_PATH);
|
fewShotNl2SqlToolLabel(), SELECT_AI_TOOL_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private McpToolView gameScopeView() {
|
||||||
|
return new McpToolView(
|
||||||
|
gameScopeToolName(), gameScopeToolDescription(), -1L,
|
||||||
|
gameScopeToolLabel(), SELECT_AI_TOOL_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
private String selectAiProfile() {
|
private String selectAiProfile() {
|
||||||
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
|
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
|
||||||
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
|
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
|
||||||
@@ -379,6 +423,21 @@ public class McpSseService {
|
|||||||
: mcpProperties.resolvedFewShotNl2SqlToolDescription();
|
: mcpProperties.resolvedFewShotNl2SqlToolDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String gameScopeToolName() {
|
||||||
|
return mcpProperties == null ? "oracle.select_ai.game_scope_resolve"
|
||||||
|
: mcpProperties.resolvedGameScopeToolName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String gameScopeToolLabel() {
|
||||||
|
return mcpProperties == null ? "게임 조회 범위 확인" : mcpProperties.resolvedGameScopeToolLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String gameScopeToolDescription() {
|
||||||
|
return mcpProperties == null
|
||||||
|
? "질문에서 언급된 게임 별칭을 DB 게임 범위 view로 확인합니다."
|
||||||
|
: mcpProperties.resolvedGameScopeToolDescription();
|
||||||
|
}
|
||||||
|
|
||||||
private String pretty(Object value) {
|
private String pretty(Object value) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ backoffice:
|
|||||||
profile: ${BACKOFFICE_SELECT_AI_PROFILE:}
|
profile: ${BACKOFFICE_SELECT_AI_PROFILE:}
|
||||||
few-shot-enabled: ${BACKOFFICE_SELECT_AI_FEW_SHOT_ENABLED:true}
|
few-shot-enabled: ${BACKOFFICE_SELECT_AI_FEW_SHOT_ENABLED:true}
|
||||||
few-shot-top-k: ${BACKOFFICE_SELECT_AI_FEW_SHOT_TOP_K:3}
|
few-shot-top-k: ${BACKOFFICE_SELECT_AI_FEW_SHOT_TOP_K:3}
|
||||||
|
game-scope:
|
||||||
|
# Customer DB view contract: aliases + profile object list + Oracle objects.
|
||||||
|
# No customer game or table identifier is embedded in application code.
|
||||||
|
enabled: ${BACKOFFICE_GAME_SCOPE_ENABLED:false}
|
||||||
|
view-name: ${BACKOFFICE_GAME_SCOPE_VIEW:}
|
||||||
|
max-scopes: ${BACKOFFICE_GAME_SCOPE_MAX_SCOPES:8}
|
||||||
catalog:
|
catalog:
|
||||||
owner: ${BACKOFFICE_CATALOG_OWNER:}
|
owner: ${BACKOFFICE_CATALOG_OWNER:}
|
||||||
objects: ${BACKOFFICE_CATALOG_OBJECTS:}
|
objects: ${BACKOFFICE_CATALOG_OBJECTS:}
|
||||||
@@ -101,6 +107,9 @@ backoffice:
|
|||||||
few-shot-nl2sql-tool-name: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_NAME:oracle.select_ai.smilegate_fewshot_nl2sql}
|
few-shot-nl2sql-tool-name: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_NAME:oracle.select_ai.smilegate_fewshot_nl2sql}
|
||||||
few-shot-nl2sql-tool-label: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_LABEL:Few-shot NL2SQL 실행}
|
few-shot-nl2sql-tool-label: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_LABEL:Few-shot NL2SQL 실행}
|
||||||
few-shot-nl2sql-tool-description: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION:벡터 Few-shot 예제를 찾아 prompt에 반영하고 SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다.}
|
few-shot-nl2sql-tool-description: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION:벡터 Few-shot 예제를 찾아 prompt에 반영하고 SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다.}
|
||||||
|
game-scope-tool-name: ${BACKOFFICE_MCP_GAME_SCOPE_TOOL_NAME:oracle.select_ai.game_scope_resolve}
|
||||||
|
game-scope-tool-label: ${BACKOFFICE_MCP_GAME_SCOPE_TOOL_LABEL:게임 조회 범위 확인}
|
||||||
|
game-scope-tool-description: ${BACKOFFICE_MCP_GAME_SCOPE_TOOL_DESCRIPTION:질문에서 언급된 게임 별칭을 DB 게임 범위 view로 확인합니다. 데이터 SQL은 실행하지 않으며, 반환된 SUPPORTED scope에만 Few-shot NL2SQL을 호출하세요.}
|
||||||
masking:
|
masking:
|
||||||
policies: ${BACKOFFICE_MASKING_POLICIES:}
|
policies: ${BACKOFFICE_MASKING_POLICIES:}
|
||||||
security-sql-scripts:
|
security-sql-scripts:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.service;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||||
|
import com.cloudhandson.vpdbackoffice.config.GameScopeProperties;
|
||||||
import com.cloudhandson.vpdbackoffice.config.McpProperties;
|
import com.cloudhandson.vpdbackoffice.config.McpProperties;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
@@ -13,6 +14,7 @@ class McpSseServiceTest {
|
|||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private final SelectAiService selectAiService = new CapturingSelectAiService();
|
private final SelectAiService selectAiService = new CapturingSelectAiService();
|
||||||
private final QaVectorService qaVectorService = new CapturingQaVectorService();
|
private final QaVectorService qaVectorService = new CapturingQaVectorService();
|
||||||
|
private final GameScopeService gameScopeService = new CapturingGameScopeService();
|
||||||
private final McpSseService service = new McpSseService(
|
private final McpSseService service = new McpSseService(
|
||||||
selectAiService,
|
selectAiService,
|
||||||
objectMapper,
|
objectMapper,
|
||||||
@@ -40,9 +42,13 @@ class McpSseServiceTest {
|
|||||||
"테스트 예제 SQL을 저장합니다.",
|
"테스트 예제 SQL을 저장합니다.",
|
||||||
"oracle.select_ai.test_fewshot_nl2sql",
|
"oracle.select_ai.test_fewshot_nl2sql",
|
||||||
"테스트 Few-shot NL2SQL",
|
"테스트 Few-shot NL2SQL",
|
||||||
"테스트 Few-shot 검색 및 실행 도구입니다."
|
"테스트 Few-shot 검색 및 실행 도구입니다.",
|
||||||
|
"oracle.select_ai.test_game_scope_resolve",
|
||||||
|
"테스트 게임 범위 확인",
|
||||||
|
"테스트 DB 게임 범위 확인 도구입니다."
|
||||||
),
|
),
|
||||||
qaVectorService
|
qaVectorService,
|
||||||
|
gameScopeService
|
||||||
);
|
);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -50,7 +56,7 @@ class McpSseServiceTest {
|
|||||||
ObjectNode response = service.handle("default", request(1, "tools/list"));
|
ObjectNode response = service.handle("default", request(1, "tools/list"));
|
||||||
|
|
||||||
var tools = response.path("result").path("tools");
|
var tools = response.path("result").path("tools");
|
||||||
assertThat(tools).hasSize(5);
|
assertThat(tools).hasSize(6);
|
||||||
var selectAi = tools.get(0);
|
var selectAi = tools.get(0);
|
||||||
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.test_data_text2sql");
|
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.test_data_text2sql");
|
||||||
assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI");
|
assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI");
|
||||||
@@ -90,6 +96,12 @@ class McpSseServiceTest {
|
|||||||
assertThat(fewShot.path("name").asText())
|
assertThat(fewShot.path("name").asText())
|
||||||
.isEqualTo("oracle.select_ai.test_fewshot_nl2sql");
|
.isEqualTo("oracle.select_ai.test_fewshot_nl2sql");
|
||||||
assertThat(fewShot.path("description").asText()).contains("Few-shot");
|
assertThat(fewShot.path("description").asText()).contains("Few-shot");
|
||||||
|
|
||||||
|
var gameScope = tools.get(5);
|
||||||
|
assertThat(gameScope.path("name").asText())
|
||||||
|
.isEqualTo("oracle.select_ai.test_game_scope_resolve");
|
||||||
|
assertThat(gameScope.path("inputSchema").path("required"))
|
||||||
|
.extracting(node -> node.asText()).contains("question");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -208,6 +220,23 @@ class McpSseServiceTest {
|
|||||||
.contains("exampleId");
|
.contains("exampleId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesGameScopeFromTheDatabaseBackedTool() {
|
||||||
|
ObjectNode request = request(8, "tools/call");
|
||||||
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
||||||
|
params.put("name", "oracle.select_ai.test_game_scope_resolve");
|
||||||
|
params.putObject("arguments").put("question", "games requested");
|
||||||
|
|
||||||
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
||||||
|
|
||||||
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
||||||
|
String payload = response.path("result").path("content").get(0).path("text").asText();
|
||||||
|
assertThat(payload)
|
||||||
|
.contains("RESOLVED")
|
||||||
|
.contains("CALL_FEW_SHOT_NL2SQL")
|
||||||
|
.contains("REPORT_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
|
||||||
private ObjectNode request(int id, String method) {
|
private ObjectNode request(int id, String method) {
|
||||||
ObjectNode request = objectMapper.createObjectNode();
|
ObjectNode request = objectMapper.createObjectNode();
|
||||||
request.put("jsonrpc", "2.0");
|
request.put("jsonrpc", "2.0");
|
||||||
@@ -223,7 +252,7 @@ class McpSseServiceTest {
|
|||||||
private boolean showpromptCalled;
|
private boolean showpromptCalled;
|
||||||
|
|
||||||
private CapturingSelectAiService() {
|
private CapturingSelectAiService() {
|
||||||
super(null, null, null, new ObjectMapper(), null);
|
super(null, null, null, new ObjectMapper(), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -240,6 +269,11 @@ class McpSseServiceTest {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonNode generateAndExecute(String bearerToken, String prompt, String scopeGameKey) {
|
||||||
|
return generateAndExecute(bearerToken, prompt);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JsonNode generatePrompt(String bearerToken, String prompt) {
|
public JsonNode generatePrompt(String bearerToken, String prompt) {
|
||||||
this.bearerToken = bearerToken;
|
this.bearerToken = bearerToken;
|
||||||
@@ -282,4 +316,21 @@ class McpSseServiceTest {
|
|||||||
return new VectorStoreResult(77L, question, "cohere.embed-v4.0");
|
return new VectorStoreResult(77L, question, "cohere.embed-v4.0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class CapturingGameScopeService extends GameScopeService {
|
||||||
|
|
||||||
|
private CapturingGameScopeService() {
|
||||||
|
super(null, new GameScopeProperties(false, "", 8), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GameScopeResult resolve(String bearerToken, String question) {
|
||||||
|
return new GameScopeResult(question, "RESOLVED", java.util.List.of(
|
||||||
|
new GameScope("SUPPORTED_GAME", "Supported game", "Supported", "SUPPORTED",
|
||||||
|
"APPROVED_OBJECT_AVAILABLE", 100, "100"),
|
||||||
|
new GameScope("UNSUPPORTED_GAME", "Unsupported game", "Unsupported", "UNSUPPORTED",
|
||||||
|
"OBJECT_LIST_NOT_AVAILABLE", 100, "100")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user