Compare commits

...

3 Commits

Author SHA1 Message Date
devmrko
7ba173240c refs #708: report configured Smilegate Select AI profile 2026-07-23 14:06:16 +09:00
devmrko
ebc8aeee41 refs #710: remove POC4 URL remember tokens 2026-07-23 14:03:20 +09:00
devmrko
75d0a9a998 refs #708: move Smilegate Select AI to OCI GPT 5.4 Mini 2026-07-23 13:45:55 +09:00
9 changed files with 193 additions and 93 deletions

View File

@@ -56,7 +56,7 @@ export BACKOFFICE_ORDS_DB_PASSWORD=""
export BACKOFFICE_SELECT_AI_DB_URL="${BACKOFFICE_DB_URL}" export BACKOFFICE_SELECT_AI_DB_URL="${BACKOFFICE_DB_URL}"
export BACKOFFICE_SELECT_AI_DB_USERNAME="SGMP_POC" export BACKOFFICE_SELECT_AI_DB_USERNAME="SGMP_POC"
export BACKOFFICE_SELECT_AI_DB_PASSWORD="" export BACKOFFICE_SELECT_AI_DB_PASSWORD=""
export BACKOFFICE_SELECT_AI_PROFILE="SGMP_POC_HAIKU45" export BACKOFFICE_SELECT_AI_PROFILE="SGMP_POC_OCI_GPT54MINI"
# --- (2c) OpenAI 호환 AI 호출 (MCP-style Reasoning 탭) --- # --- (2c) OpenAI 호환 AI 호출 (MCP-style Reasoning 탭) ---
export BACKOFFICE_AI_ENABLED="false" export BACKOFFICE_AI_ENABLED="false"

View File

@@ -0,0 +1,48 @@
# #708 Smilegate Select AI OCI GenAI 전환
## 프로젝트 개요
Smilegate DATA & AI PoC는 Autonomous Database의 게임 데이터에서 Select AI Text2SQL을 생성하고, MCP와 백오피스를 통해 결과를 조회한다. 현재 운영 프로파일은 외부 OpenRouter 경유 Claude를 사용한다.
## 목표
`SGMP_POC_HAIKU45`가 가진 게임 데이터 object list와 메타데이터 활용 범위는 유지하면서, LLM 호출 경로만 OCI Generative AI `openai.gpt-5.4-mini`로 전환한다. ADB Resource Principal이 아닌 이 머신의 `~/.oci/config` DEFAULT API signing credential을 ADB credential으로 등록한다.
## 전환 설계
| 구분 | 기존 | 전환 후 |
| --- | --- | --- |
| 프로파일 | `SGMP_POC_HAIKU45` | `SGMP_POC_OCI_GPT54MINI` |
| 제공자 | OpenAI 호환 외부 경로 | OCI Generative AI (`provider: oci`) |
| 모델 | Claude Haiku 4.5 | `openai.gpt-5.4-mini` |
| 인증 | 외부 API credential | DEFAULT API signing key 기반 `SGMP_POC_OCI_DEFAULT_CRED` |
| OCI 리전 | 외부 서비스 | `us-chicago-1` (GPT-5.4 Mini OCI inference route) |
| 게임 데이터 범위 | 기존 object list | 기존 profile attributes에서 복제 |
| MCP 설정 | 기존 프로파일명 | `BACKOFFICE_SELECT_AI_PROFILE=SGMP_POC_OCI_GPT54MINI` |
신규 프로파일은 기존 프로파일의 metadata 관련 attributes를 복사하고, 외부 endpoint·credential·model은 OCI 값으로 새로 설정한다. 따라서 object list, comment, annotation, constraint 기반 Text2SQL 문맥은 유지된다. 기존 외부 프로파일은 삭제하지 않으며, 전환 실패 시 환경변수만 원래 값으로 되돌린다.
## 사전 조건
1. `~/.oci/config` DEFAULT의 user, tenancy, fingerprint, key_file이 유효한 OCI API signing key여야 한다.
2. DEFAULT API signing user가 Chicago 리전 root compartment에서 OCI Generative AI `openai.gpt-5.4-mini` 호출 권한을 가져야 한다. DEFAULT config의 signing region과 GPT inference route는 독립적이므로 profile attribute `region``us-chicago-1`로 명시한다.
3. 스크립트는 `SGMP_POC` 프로파일 소유자로 실행한다.
## 구현 순서
1. `sql/adb/72_sgmp_select_ai_oci_genai_profile.sql`로 신규 OCI 프로파일을 만든다.
2. 새 프로파일로 한글 Text2SQL `SHOWSQL`과 생성 SQL의 읽기 전용 실행을 검증한다.
3. 운영 서버의 `BACKOFFICE_SELECT_AI_PROFILE`만 새 프로파일로 교체하고 백오피스를 재기동한다.
4. 운영 MCP의 `oracle.select_ai.smilegate_game_text2sql` 응답 profile과 집계 결과를 검증한다.
## 롤백
새 프로파일을 삭제하지 않는다. MCP에서 오류가 나거나 SQL 품질이 허용 기준을 충족하지 않으면 `/etc/smilegate/backoffice.env``BACKOFFICE_SELECT_AI_PROFILE``SGMP_POC_HAIKU45`로 되돌린 뒤 서비스를 재기동한다.
## 검증 기준
- 새 프로파일 provider=`oci`, model=`openai.gpt-5.4-mini`, credential=`SGMP_POC_OCI_DEFAULT_CRED`, region=`us-chicago-1`
- 한글 질의는 SQLcl UTF-8 base64 복원식으로 전달한다.
- `DBMS_CLOUD_AI.GENERATE(..., 'showsql')`이 읽기 전용 `SELECT` 또는 `WITH` SQL을 생성한다.
- 해당 SQL을 읽기 전용 트랜잭션에서 실행해 결과를 반환한다.
- 운영 MCP 응답의 `profile``SGMP_POC_OCI_GPT54MINI`이다.

View File

@@ -0,0 +1,27 @@
# #710 POC4 URL 로그인 토큰 제거
## 프로젝트 개요
Smilegate DATA & AI PoC의 POC4 Streamlit 콘솔은 게임 데이터 MCP와 Select AI Text2SQL 데모를 제공한다. 포털 로그인은 콘솔 접근을 보호한다.
## 문제
로그인 유지용 서명 토큰이 `poc4_remember` query parameter로 URL에 포함됐다. URL은 브라우저 기록, 프록시 로그, 공유 링크, Referrer에 남을 수 있으므로 인증 정보를 전달하는 경로로 사용하면 안 된다.
## 조치 설계
1. Streamlit 코드에서 URL 토큰 생성·검증·삭제를 모두 제거한다.
2. 로그인 상태는 현재 Streamlit 브라우저 세션에서만 유지한다. 서버가 `HttpOnly`, `Secure`, `SameSite` cookie를 발급하는 전용 인증 경로가 마련되기 전에는 영구 로그인 기능을 제공하지 않는다.
3. `POC4_LOGIN_REMEMBER_SECRET`을 교체해 기존 서명 링크를 무효화한다.
4. Caddy가 기존 `poc4_remember` query 요청을 애플리케이션으로 전달하지 않고 `https://smilegate.cloud-handson.com/`으로 303 redirect한다.
## 검증 기준
- `mcp_discovery_ui.py``poc4_remember` 또는 `st.query_params` 로그인 토큰 코드가 없다.
- 기존 query URL 요청은 query가 없는 루트 URL로 303 응답한다.
- `smilegate-poc4-console.service`가 정상 기동한다.
- 토큰, password hash, signing key는 Git·Redmine·명령 출력에 기록하지 않는다.
## 후속 개선
영구 로그인 요구가 다시 생기면 POC4 자체가 아닌 서버 인증 endpoint가 `HttpOnly; Secure; SameSite=Lax` cookie를 발급하고, Streamlit은 요청 cookie의 서버 검증 결과만 읽는 구조로 구현한다.

View File

@@ -10,8 +10,6 @@ with the Bearer token entered on the screen.
from __future__ import annotations from __future__ import annotations
import base64
import binascii
import hashlib import hashlib
import hmac import hmac
import html import html
@@ -86,8 +84,6 @@ VPD_OPERATIONS_URL = "https://smilegate-backoffice.cloud-handson.com/"
PORTAL_AUTHENTICATED_KEY = "poc4_portal_authenticated" PORTAL_AUTHENTICATED_KEY = "poc4_portal_authenticated"
PORTAL_AUTH_USER_KEY = "poc4_portal_auth_user" PORTAL_AUTH_USER_KEY = "poc4_portal_auth_user"
PORTAL_LOGIN_FAILURE_KEY = "poc4_portal_login_failed" PORTAL_LOGIN_FAILURE_KEY = "poc4_portal_login_failed"
PORTAL_REMEMBER_TOKEN_PARAM = "poc4_remember"
PORTAL_REMEMBER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
AUDIT_SCHEMA = "SGMP_POC" AUDIT_SCHEMA = "SGMP_POC"
REFERENCE_EVIDENCE_ENABLED = ( REFERENCE_EVIDENCE_ENABLED = (
os.environ.get("POC4_REFERENCE_EVIDENCE_ENABLED", "").strip().lower() os.environ.get("POC4_REFERENCE_EVIDENCE_ENABLED", "").strip().lower()
@@ -2106,63 +2102,6 @@ def _portal_credentials_are_valid(username: str, password: str) -> bool:
return username_matches and password_matches return username_matches and password_matches
def _portal_remember_secret() -> str:
return _portal_auth_value("POC4_LOGIN_REMEMBER_SECRET")
def _portal_remember_token(username: str) -> str:
secret = _portal_remember_secret()
if not secret:
return ""
payload = {
"v": 1,
"u": username.strip(),
"e": int(datetime.now(timezone.utc).timestamp()) + PORTAL_REMEMBER_MAX_AGE_SECONDS,
}
encoded = base64.urlsafe_b64encode(
json.dumps(payload, separators=(",", ":")).encode("utf-8")
).decode("ascii").rstrip("=")
signature = hmac.new(
secret.encode("utf-8"), encoded.encode("ascii"), hashlib.sha256
).hexdigest()
return f"{encoded}.{signature}"
def _restore_portal_remembered_session() -> None:
if st.session_state.get(PORTAL_AUTHENTICATED_KEY, False):
return
token = st.query_params.get(PORTAL_REMEMBER_TOKEN_PARAM, "")
if not isinstance(token, str) or not token or len(token) > 2048:
return
secret = _portal_remember_secret()
expected_username = _portal_auth_value("POC4_LOGIN_USER")
try:
encoded, supplied_signature = token.split(".", 1)
expected_signature = hmac.new(
secret.encode("utf-8"), encoded.encode("ascii"), hashlib.sha256
).hexdigest()
padded = encoded + "=" * (-len(encoded) % 4)
payload = json.loads(base64.urlsafe_b64decode(padded).decode("utf-8"))
expires_at = int(payload["e"])
username = str(payload["u"])
except (binascii.Error, KeyError, TypeError, ValueError, UnicodeDecodeError):
return
if not secret or not hmac.compare_digest(supplied_signature, expected_signature):
return
if expires_at < int(datetime.now(timezone.utc).timestamp()):
return
if not hmac.compare_digest(username, expected_username):
return
st.session_state[PORTAL_AUTHENTICATED_KEY] = True
st.session_state[PORTAL_AUTH_USER_KEY] = username
st.session_state[PORTAL_LOGIN_FAILURE_KEY] = False
def _clear_portal_remembered_session() -> None:
if PORTAL_REMEMBER_TOKEN_PARAM in st.query_params:
del st.query_params[PORTAL_REMEMBER_TOKEN_PARAM]
def _render_portal_login(profile: AppProfile) -> None: def _render_portal_login(profile: AppProfile) -> None:
with st.container(key="console_login_container"): with st.container(key="console_login_container"):
render_login_brand(st, profile) render_login_brand(st, profile)
@@ -2181,23 +2120,12 @@ def _render_portal_login(profile: AppProfile) -> None:
max_chars=200, max_chars=200,
placeholder="비밀번호를 입력하세요.", placeholder="비밀번호를 입력하세요.",
) )
remember_login = st.checkbox(
"로그인 유지 (7일)",
disabled=not bool(_portal_remember_secret()),
help="이 브라우저에서 7일 동안 로그인 상태를 유지합니다.",
)
submitted = st.form_submit_button("로그인", use_container_width=True) submitted = st.form_submit_button("로그인", use_container_width=True)
if submitted: if submitted:
if _portal_credentials_are_valid(username, password): if _portal_credentials_are_valid(username, password):
st.session_state[PORTAL_AUTHENTICATED_KEY] = True st.session_state[PORTAL_AUTHENTICATED_KEY] = True
st.session_state[PORTAL_AUTH_USER_KEY] = username.strip() st.session_state[PORTAL_AUTH_USER_KEY] = username.strip()
st.session_state[PORTAL_LOGIN_FAILURE_KEY] = False st.session_state[PORTAL_LOGIN_FAILURE_KEY] = False
if remember_login:
token = _portal_remember_token(username)
if token:
st.query_params[PORTAL_REMEMBER_TOKEN_PARAM] = token
else:
_clear_portal_remembered_session()
st.rerun() st.rerun()
st.session_state[PORTAL_LOGIN_FAILURE_KEY] = True st.session_state[PORTAL_LOGIN_FAILURE_KEY] = True
if st.session_state.get(PORTAL_LOGIN_FAILURE_KEY, False): if st.session_state.get(PORTAL_LOGIN_FAILURE_KEY, False):
@@ -2209,7 +2137,6 @@ def _render_portal_login(profile: AppProfile) -> None:
def _logout_portal() -> None: def _logout_portal() -> None:
_clear_portal_remembered_session()
st.session_state.pop(PORTAL_AUTHENTICATED_KEY, None) st.session_state.pop(PORTAL_AUTHENTICATED_KEY, None)
st.session_state.pop(PORTAL_AUTH_USER_KEY, None) st.session_state.pop(PORTAL_AUTH_USER_KEY, None)
st.session_state.pop(PORTAL_LOGIN_FAILURE_KEY, None) st.session_state.pop(PORTAL_LOGIN_FAILURE_KEY, None)
@@ -7221,7 +7148,6 @@ def main() -> None:
page_title=profile.page_title, page_icon=profile.page_icon, layout="wide" page_title=profile.page_title, page_icon=profile.page_icon, layout="wide"
) )
_apply_console_theme(profile) _apply_console_theme(profile)
_restore_portal_remembered_session()
if not st.session_state.get(PORTAL_AUTHENTICATED_KEY, False): if not st.session_state.get(PORTAL_AUTHENTICATED_KEY, False):
_render_portal_login(profile) _render_portal_login(profile)
return return

View File

@@ -0,0 +1,74 @@
-- 72_sgmp_select_ai_oci_genai_profile.sql
-- Run as SGMP_POC after creating SGMP_POC_OCI_DEFAULT_CRED from the local
-- ~/.oci/config DEFAULT API signing key. No private-key material belongs in
-- this script or the repository.
--
-- The source external profile is retained. Metadata attributes are copied
-- individually so object_list, comments, annotations and instructions remain
-- intact while external endpoint, credential and model settings are replaced.
DECLARE
v_exists PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_exists
FROM user_cloud_ai_profiles
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI';
IF v_exists > 0 THEN
DBMS_CLOUD_AI.DROP_PROFILE(
profile_name => 'SGMP_POC_OCI_GPT54MINI',
force => TRUE
);
END IF;
DBMS_CLOUD_AI.CREATE_PROFILE(
profile_name => 'SGMP_POC_OCI_GPT54MINI',
attributes => '{
"provider": "oci",
"credential_name": "SGMP_POC_OCI_DEFAULT_CRED",
"model": "openai.gpt-5.4-mini",
"region": "us-chicago-1",
"oci_compartment_id": "<DEFAULT tenancy OCID>"
}',
description => 'Smilegate Text2SQL on OCI GenAI GPT-5.4 Mini'
);
FOR source_attribute IN (
SELECT attribute_name,
attribute_value
FROM user_cloud_ai_profile_attributes
WHERE profile_name = 'SGMP_POC_HAIKU45'
AND attribute_name NOT IN (
'credential_name',
'model',
'provider',
'provider_endpoint',
'region',
'oci_compartment_id',
'oci_endpoint_id',
'oci_apiformat',
'oci_runtimetype'
)
) LOOP
DBMS_CLOUD_AI.SET_ATTRIBUTE(
profile_name => 'SGMP_POC_OCI_GPT54MINI',
attribute_name => source_attribute.attribute_name,
attribute_value => source_attribute.attribute_value
);
END LOOP;
END;
/
SELECT attribute_name,
attribute_value
FROM user_cloud_ai_profile_attributes
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
AND attribute_name IN (
'provider',
'model',
'credential_name',
'region',
'oci_compartment_id'
)
ORDER BY attribute_name;

View File

@@ -1,5 +1,6 @@
package com.cloudhandson.vpdbackoffice.service; package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView; import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -14,24 +15,20 @@ public class McpSseService {
private static final String SELECT_AI_VPD_QUERY_TOOL = "oracle.select_ai.smilegate_game_text2sql"; private static final String SELECT_AI_VPD_QUERY_TOOL = "oracle.select_ai.smilegate_game_text2sql";
private static final String SELECT_AI_VPD_QUERY_PATH = "/mcp (tools/call)"; private static final String SELECT_AI_VPD_QUERY_PATH = "/mcp (tools/call)";
private static final String SELECT_AI_VPD_QUERY_PROFILE = "SGMP_POC_HAIKU45"; private static final String DEFAULT_SELECT_AI_PROFILE = "SGMP_POC_OCI_GPT54MINI";
private static final McpToolView SELECT_AI_VPD_QUERY_VIEW = new McpToolView(
SELECT_AI_VPD_QUERY_TOOL,
"SGMP_POC_HAIKU45 프로파일로 게임 로그·서비스 데이터용 읽기 전용 SELECT/WITH SQL을 생성하고, 검증 후 읽기 전용 트랜잭션에서 실행합니다. 생성 SQL과 최대 100건의 조회 결과를 함께 반환하며 DDL/DML/잠금/패키지 호출은 실행하지 않습니다.",
-1L,
"Smilegate 게임 데이터 Text2SQL",
SELECT_AI_VPD_QUERY_PATH
);
private final SmilegateSelectAiService smilegateSelectAiService; private final SmilegateSelectAiService smilegateSelectAiService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final BackofficeProperties properties;
public McpSseService( public McpSseService(
SmilegateSelectAiService smilegateSelectAiService, SmilegateSelectAiService smilegateSelectAiService,
ObjectMapper objectMapper ObjectMapper objectMapper,
BackofficeProperties properties
) { ) {
this.smilegateSelectAiService = smilegateSelectAiService; this.smilegateSelectAiService = smilegateSelectAiService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.properties = properties;
} }
public ObjectNode handle(String contextPath, JsonNode request) { public ObjectNode handle(String contextPath, JsonNode request) {
@@ -70,7 +67,7 @@ public class McpSseService {
/** The only tool registered by this MCP server. */ /** The only tool registered by this MCP server. */
public List<McpToolView> registeredTools() { public List<McpToolView> registeredTools() {
return List.of(SELECT_AI_VPD_QUERY_VIEW); return List.of(selectAiVpdQueryView());
} }
private ObjectNode initializeResult(String contextPath) { private ObjectNode initializeResult(String contextPath) {
@@ -97,7 +94,7 @@ public class McpSseService {
private ObjectNode selectAiVpdQueryTool() { private ObjectNode selectAiVpdQueryTool() {
ObjectNode item = objectMapper.createObjectNode(); ObjectNode item = objectMapper.createObjectNode();
item.put("name", SELECT_AI_VPD_QUERY_TOOL); item.put("name", SELECT_AI_VPD_QUERY_TOOL);
item.put("description", SELECT_AI_VPD_QUERY_VIEW.description()); item.put("description", selectAiVpdQueryView().description());
ObjectNode schema = objectMapper.createObjectNode(); ObjectNode schema = objectMapper.createObjectNode();
schema.put("type", "object"); schema.put("type", "object");
@@ -138,7 +135,7 @@ public class McpSseService {
ObjectNode payload = objectMapper.createObjectNode(); ObjectNode payload = objectMapper.createObjectNode();
payload.put("toolName", SELECT_AI_VPD_QUERY_TOOL); payload.put("toolName", SELECT_AI_VPD_QUERY_TOOL);
payload.put("profile", SELECT_AI_VPD_QUERY_PROFILE); payload.put("profile", selectAiProfile());
payload.put("ordsPath", SELECT_AI_VPD_QUERY_PATH); payload.put("ordsPath", SELECT_AI_VPD_QUERY_PATH);
payload.set("response", response); payload.set("response", response);
@@ -169,6 +166,25 @@ public class McpSseService {
return result; return result;
} }
private McpToolView selectAiVpdQueryView() {
String profile = selectAiProfile();
return new McpToolView(
SELECT_AI_VPD_QUERY_TOOL,
profile + " 프로파일로 게임 로그·서비스 데이터용 읽기 전용 SELECT/WITH SQL을 생성하고, 검증 후 읽기 전용 트랜잭션에서 실행합니다. 생성 SQL과 최대 100건의 조회 결과를 함께 반환하며 DDL/DML/잠금/패키지 호출은 실행하지 않습니다.",
-1L,
"Smilegate 게임 데이터 Text2SQL",
SELECT_AI_VPD_QUERY_PATH
);
}
private String selectAiProfile() {
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
return DEFAULT_SELECT_AI_PROFILE;
}
return selectAi.profile().trim();
}
private String pretty(Object value) { private String pretty(Object value) {
try { try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value); return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);

View File

@@ -74,4 +74,4 @@ backoffice:
db-url: ${BACKOFFICE_SELECT_AI_DB_URL:} db-url: ${BACKOFFICE_SELECT_AI_DB_URL:}
db-username: ${BACKOFFICE_SELECT_AI_DB_USERNAME:} db-username: ${BACKOFFICE_SELECT_AI_DB_USERNAME:}
db-password: ${BACKOFFICE_SELECT_AI_DB_PASSWORD:} db-password: ${BACKOFFICE_SELECT_AI_DB_PASSWORD:}
profile: ${BACKOFFICE_SELECT_AI_PROFILE:SGMP_POC_HAIKU45} profile: ${BACKOFFICE_SELECT_AI_PROFILE:SGMP_POC_OCI_GPT54MINI}

View File

@@ -8,7 +8,7 @@
<h1>MCP 연동</h1> <h1>MCP 연동</h1>
<details class="explanation-details"> <details class="explanation-details">
<summary>도움말</summary> <summary>도움말</summary>
<p>사용자 Bearer Token을 검증한 뒤 <code>SGMP_POC_HAIKU45</code> Select AI 프로파일로 게임 데이터 Text2SQL을 생성하는 단일 MCP tool을 제공합니다. Select AI는 comment, annotation, constraint 메타데이터를 함께 사용하며, 생성 SQL은 자동 실행하지 않습니다.</p> <p>사용자 Bearer Token을 검증한 뒤 <code>SGMP_POC_OCI_GPT54MINI</code> Select AI 프로파일로 게임 데이터 Text2SQL을 생성하는 단일 MCP tool을 제공합니다. Select AI는 comment, annotation, constraint 메타데이터를 함께 사용하며, 생성 SQL은 자동 실행하지 않습니다.</p>
</details> </details>
</section> </section>
@@ -118,7 +118,7 @@
<pre class="code-block">{ <pre class="code-block">{
"prompt": "카제나의 최신 BASE_DT 기준 AU(활성 사용자 수)를 조회하는 SQL을 만들어줘." "prompt": "카제나의 최신 BASE_DT 기준 AU(활성 사용자 수)를 조회하는 SQL을 만들어줘."
}</pre> }</pre>
<p class="form-hint">등록 tool은 <code>oracle.select_ai.smilegate_game_text2sql</code> 하나입니다. 활성 사용자 Bearer Token을 확인한 뒤 <code>SGMP_POC_HAIKU45</code>가 comment, annotation, constraint를 참고해 읽기 전용 게임 데이터 SQL을 생성합니다. 실행은 자동으로 수행하지 않습니다.</p> <p class="form-hint">등록 tool은 <code>oracle.select_ai.smilegate_game_text2sql</code> 하나입니다. 활성 사용자 Bearer Token을 확인한 뒤 <code>SGMP_POC_OCI_GPT54MINI</code>가 comment, annotation, constraint를 참고해 읽기 전용 게임 데이터 SQL을 생성합니다. 실행은 자동으로 수행하지 않습니다.</p>
</details> </details>
</section> </section>

View File

@@ -1,6 +1,7 @@
package com.cloudhandson.vpdbackoffice.service; 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.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -12,7 +13,14 @@ class McpSseServiceTest {
private final SmilegateSelectAiService selectAiService = new CapturingSmilegateSelectAiService(); private final SmilegateSelectAiService selectAiService = new CapturingSmilegateSelectAiService();
private final McpSseService service = new McpSseService( private final McpSseService service = new McpSseService(
selectAiService, selectAiService,
objectMapper objectMapper,
new BackofficeProperties(
null,
null,
null,
null,
new BackofficeProperties.SelectAi("", "", "", "SGMP_POC_OCI_GPT54MINI")
)
); );
@Test @Test
@@ -23,6 +31,7 @@ class McpSseServiceTest {
assertThat(tools).hasSize(1); assertThat(tools).hasSize(1);
var selectAi = tools.get(0); var selectAi = tools.get(0);
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.smilegate_game_text2sql"); assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.smilegate_game_text2sql");
assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI");
assertThat(selectAi.path("inputSchema").path("required")) assertThat(selectAi.path("inputSchema").path("required"))
.extracting(node -> node.asText()) .extracting(node -> node.asText())
.contains("prompt"); .contains("prompt");
@@ -49,7 +58,7 @@ class McpSseServiceTest {
assertThat(response.path("result").path("isError").asBoolean()).isFalse(); assertThat(response.path("result").path("isError").asBoolean()).isFalse();
String payload = response.path("result").path("content").get(0).path("text").asText(); String payload = response.path("result").path("content").get(0).path("text").asText();
assertThat(payload) assertThat(payload)
.contains("SGMP_POC_HAIKU45") .contains("SGMP_POC_OCI_GPT54MINI")
.contains("SELECT 1 FROM DUAL") .contains("SELECT 1 FROM DUAL")
.contains("SHOWSQL_AND_EXECUTED") .contains("SHOWSQL_AND_EXECUTED")
.contains("READ_ONLY_EXECUTED") .contains("READ_ONLY_EXECUTED")
@@ -94,7 +103,7 @@ class McpSseServiceTest {
this.bearerToken = bearerToken; this.bearerToken = bearerToken;
this.prompt = prompt; this.prompt = prompt;
ObjectNode response = new ObjectMapper().createObjectNode() ObjectNode response = new ObjectMapper().createObjectNode()
.put("profile", "SGMP_POC_HAIKU45") .put("profile", "SGMP_POC_OCI_GPT54MINI")
.put("generatedSql", "SELECT 1 FROM DUAL") .put("generatedSql", "SELECT 1 FROM DUAL")
.put("status", "SHOWSQL_AND_EXECUTED") .put("status", "SHOWSQL_AND_EXECUTED")
.put("execution", "READ_ONLY_EXECUTED") .put("execution", "READ_ONLY_EXECUTED")