Compare commits
3 Commits
32fccced20
...
7ba173240c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba173240c | ||
|
|
ebc8aeee41 | ||
|
|
75d0a9a998 |
@@ -56,7 +56,7 @@ export BACKOFFICE_ORDS_DB_PASSWORD=""
|
||||
export BACKOFFICE_SELECT_AI_DB_URL="${BACKOFFICE_DB_URL}"
|
||||
export BACKOFFICE_SELECT_AI_DB_USERNAME="SGMP_POC"
|
||||
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 탭) ---
|
||||
export BACKOFFICE_AI_ENABLED="false"
|
||||
|
||||
48
docs/design/708-smilegate-select-ai-oci-genai/README.md
Normal file
48
docs/design/708-smilegate-select-ai-oci-genai/README.md
Normal 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`이다.
|
||||
27
docs/design/710-smilegate-poc4-url-token-security/README.md
Normal file
27
docs/design/710-smilegate-poc4-url-token-security/README.md
Normal 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의 서버 검증 결과만 읽는 구조로 구현한다.
|
||||
@@ -10,8 +10,6 @@ with the Bearer token entered on the screen.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import hmac
|
||||
import html
|
||||
@@ -86,8 +84,6 @@ VPD_OPERATIONS_URL = "https://smilegate-backoffice.cloud-handson.com/"
|
||||
PORTAL_AUTHENTICATED_KEY = "poc4_portal_authenticated"
|
||||
PORTAL_AUTH_USER_KEY = "poc4_portal_auth_user"
|
||||
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"
|
||||
REFERENCE_EVIDENCE_ENABLED = (
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
with st.container(key="console_login_container"):
|
||||
render_login_brand(st, profile)
|
||||
@@ -2181,23 +2120,12 @@ def _render_portal_login(profile: AppProfile) -> None:
|
||||
max_chars=200,
|
||||
placeholder="비밀번호를 입력하세요.",
|
||||
)
|
||||
remember_login = st.checkbox(
|
||||
"로그인 유지 (7일)",
|
||||
disabled=not bool(_portal_remember_secret()),
|
||||
help="이 브라우저에서 7일 동안 로그인 상태를 유지합니다.",
|
||||
)
|
||||
submitted = st.form_submit_button("로그인", use_container_width=True)
|
||||
if submitted:
|
||||
if _portal_credentials_are_valid(username, password):
|
||||
st.session_state[PORTAL_AUTHENTICATED_KEY] = True
|
||||
st.session_state[PORTAL_AUTH_USER_KEY] = username.strip()
|
||||
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.session_state[PORTAL_LOGIN_FAILURE_KEY] = True
|
||||
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:
|
||||
_clear_portal_remembered_session()
|
||||
st.session_state.pop(PORTAL_AUTHENTICATED_KEY, None)
|
||||
st.session_state.pop(PORTAL_AUTH_USER_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"
|
||||
)
|
||||
_apply_console_theme(profile)
|
||||
_restore_portal_remembered_session()
|
||||
if not st.session_state.get(PORTAL_AUTHENTICATED_KEY, False):
|
||||
_render_portal_login(profile)
|
||||
return
|
||||
|
||||
74
sql/adb/72_sgmp_select_ai_oci_genai_profile.sql
Normal file
74
sql/adb/72_sgmp_select_ai_oci_genai_profile.sql
Normal 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;
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||
import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
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_PATH = "/mcp (tools/call)";
|
||||
private static final String SELECT_AI_VPD_QUERY_PROFILE = "SGMP_POC_HAIKU45";
|
||||
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 static final String DEFAULT_SELECT_AI_PROFILE = "SGMP_POC_OCI_GPT54MINI";
|
||||
|
||||
private final SmilegateSelectAiService smilegateSelectAiService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final BackofficeProperties properties;
|
||||
|
||||
public McpSseService(
|
||||
SmilegateSelectAiService smilegateSelectAiService,
|
||||
ObjectMapper objectMapper
|
||||
ObjectMapper objectMapper,
|
||||
BackofficeProperties properties
|
||||
) {
|
||||
this.smilegateSelectAiService = smilegateSelectAiService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public ObjectNode handle(String contextPath, JsonNode request) {
|
||||
@@ -70,7 +67,7 @@ public class McpSseService {
|
||||
|
||||
/** The only tool registered by this MCP server. */
|
||||
public List<McpToolView> registeredTools() {
|
||||
return List.of(SELECT_AI_VPD_QUERY_VIEW);
|
||||
return List.of(selectAiVpdQueryView());
|
||||
}
|
||||
|
||||
private ObjectNode initializeResult(String contextPath) {
|
||||
@@ -97,7 +94,7 @@ public class McpSseService {
|
||||
private ObjectNode selectAiVpdQueryTool() {
|
||||
ObjectNode item = objectMapper.createObjectNode();
|
||||
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();
|
||||
schema.put("type", "object");
|
||||
@@ -138,7 +135,7 @@ public class McpSseService {
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
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.set("response", response);
|
||||
|
||||
@@ -169,6 +166,25 @@ public class McpSseService {
|
||||
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) {
|
||||
try {
|
||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
||||
|
||||
@@ -74,4 +74,4 @@ backoffice:
|
||||
db-url: ${BACKOFFICE_SELECT_AI_DB_URL:}
|
||||
db-username: ${BACKOFFICE_SELECT_AI_DB_USERNAME:}
|
||||
db-password: ${BACKOFFICE_SELECT_AI_DB_PASSWORD:}
|
||||
profile: ${BACKOFFICE_SELECT_AI_PROFILE:SGMP_POC_HAIKU45}
|
||||
profile: ${BACKOFFICE_SELECT_AI_PROFILE:SGMP_POC_OCI_GPT54MINI}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>MCP 연동</h1>
|
||||
<details class="explanation-details">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<pre class="code-block">{
|
||||
"prompt": "카제나의 최신 BASE_DT 기준 AU(활성 사용자 수)를 조회하는 SQL을 만들어줘."
|
||||
}</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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
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.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
@@ -12,7 +13,14 @@ class McpSseServiceTest {
|
||||
private final SmilegateSelectAiService selectAiService = new CapturingSmilegateSelectAiService();
|
||||
private final McpSseService service = new McpSseService(
|
||||
selectAiService,
|
||||
objectMapper
|
||||
objectMapper,
|
||||
new BackofficeProperties(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new BackofficeProperties.SelectAi("", "", "", "SGMP_POC_OCI_GPT54MINI")
|
||||
)
|
||||
);
|
||||
|
||||
@Test
|
||||
@@ -23,6 +31,7 @@ class McpSseServiceTest {
|
||||
assertThat(tools).hasSize(1);
|
||||
var selectAi = tools.get(0);
|
||||
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"))
|
||||
.extracting(node -> node.asText())
|
||||
.contains("prompt");
|
||||
@@ -49,7 +58,7 @@ class McpSseServiceTest {
|
||||
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
||||
String payload = response.path("result").path("content").get(0).path("text").asText();
|
||||
assertThat(payload)
|
||||
.contains("SGMP_POC_HAIKU45")
|
||||
.contains("SGMP_POC_OCI_GPT54MINI")
|
||||
.contains("SELECT 1 FROM DUAL")
|
||||
.contains("SHOWSQL_AND_EXECUTED")
|
||||
.contains("READ_ONLY_EXECUTED")
|
||||
@@ -94,7 +103,7 @@ class McpSseServiceTest {
|
||||
this.bearerToken = bearerToken;
|
||||
this.prompt = prompt;
|
||||
ObjectNode response = new ObjectMapper().createObjectNode()
|
||||
.put("profile", "SGMP_POC_HAIKU45")
|
||||
.put("profile", "SGMP_POC_OCI_GPT54MINI")
|
||||
.put("generatedSql", "SELECT 1 FROM DUAL")
|
||||
.put("status", "SHOWSQL_AND_EXECUTED")
|
||||
.put("execution", "READ_ONLY_EXECUTED")
|
||||
|
||||
Reference in New Issue
Block a user