refs #736: route Smilegate portal to few-shot MCP
This commit is contained in:
@@ -10,6 +10,7 @@ with the Bearer token entered on the screen.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import html
|
||||
@@ -67,7 +68,7 @@ from src.poc4.qa_history_store import QaHistoryStore, QaHistoryStoreError
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
MCP_PROTOCOL_VERSION = "2025-11-25"
|
||||
PREFERRED_TOOL = "oracle.select_ai.smilegate_game_text2sql"
|
||||
PREFERRED_TOOL = "oracle.select_ai.smilegate_fewshot_nl2sql"
|
||||
DEFAULT_QUESTION = ""
|
||||
MAX_RESPONSE_BYTES = 1_000_000
|
||||
MAX_CONVERSATION_MESSAGES = 8
|
||||
@@ -93,6 +94,7 @@ 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_AUTH_COOKIE_NAME = "poc4_portal_auth"
|
||||
AUDIT_SCHEMA = "SGMP_POC"
|
||||
REFERENCE_EVIDENCE_ENABLED = (
|
||||
os.environ.get("POC4_REFERENCE_EVIDENCE_ENABLED", "").strip().lower()
|
||||
@@ -568,17 +570,9 @@ def _qa_history_store() -> QaHistoryStore:
|
||||
|
||||
def _load_qa_questions() -> tuple[list[QaQuestion], QaHistoryStore | None, str]:
|
||||
try:
|
||||
store = _qa_history_store()
|
||||
questions = store.list_questions(limit=200)
|
||||
if questions:
|
||||
return questions, store, ""
|
||||
return [], store, "질답 이력 DB에 아직 적재된 기준 질문이 없습니다."
|
||||
except QaHistoryStoreError as exc:
|
||||
try:
|
||||
fallback = list(load_benchmark_questions(QA_BENCHMARK_FILE))
|
||||
except Exception:
|
||||
fallback = []
|
||||
return fallback, None, str(exc)
|
||||
return list(load_benchmark_questions(QA_BENCHMARK_FILE)), _qa_history_store(), ""
|
||||
except Exception as exc:
|
||||
return [], None, f"질답 기준 파일을 읽지 못했습니다: {exc}"
|
||||
|
||||
|
||||
def _judgment_label(status: str) -> str:
|
||||
@@ -637,8 +631,10 @@ def _render_qa_benchmark_panel(
|
||||
"제목": question.title,
|
||||
"질문": question.question_text,
|
||||
"기대 기준": _qa_answer_preview(question.expected_focus, 150),
|
||||
"최근 판정": _judgment_label(question.last_judgment_status),
|
||||
"최근 실행": question.last_evaluated_at or "과거 기준",
|
||||
"기준 상태": _judgment_label(question.last_judgment_status)
|
||||
if question.last_judgment_status
|
||||
else "과거 기준",
|
||||
"최근 실행": question.last_evaluated_at or "후보 선택 시 조회",
|
||||
}
|
||||
for question in visible_questions
|
||||
],
|
||||
@@ -663,6 +659,16 @@ def _render_qa_benchmark_panel(
|
||||
st.info("자유 텍스트 질문도 실행하고 이력으로 남길 수 있습니다. 자유 질의는 정답 기준이 없어 수동 검토로 표시됩니다.")
|
||||
return None
|
||||
|
||||
if store is not None:
|
||||
try:
|
||||
stored_question = store.get_question_by_code(selected.question_code)
|
||||
if stored_question is not None:
|
||||
selected = stored_question
|
||||
else:
|
||||
st.warning("선택한 후보가 ADB 이력 테이블에 아직 적재되지 않았습니다.")
|
||||
except QaHistoryStoreError as exc:
|
||||
st.warning("선택한 후보의 ADB 이력을 불러오지 못했습니다. " + str(exc))
|
||||
|
||||
with st.expander("선택한 질문의 정답 기준·원본·과거 답변", expanded=True):
|
||||
left, right = st.columns(2)
|
||||
with left:
|
||||
@@ -771,34 +777,78 @@ def _portal_credentials_are_valid(username: str, password: str) -> bool:
|
||||
return username_matches and password_matches
|
||||
|
||||
|
||||
def _portal_cookie_user() -> str:
|
||||
"""Return the verified portal user stored in the HttpOnly auth cookie."""
|
||||
try:
|
||||
token = str(st.context.cookies.get(PORTAL_AUTH_COOKIE_NAME, "")).strip()
|
||||
encoded_claims, supplied_signature = token.rsplit(".", 1)
|
||||
secret = _portal_auth_value("POC4_LOGIN_REMEMBER_SECRET")
|
||||
expected_signature = hmac.new(
|
||||
secret.encode("utf-8"), encoded_claims.encode("ascii"), hashlib.sha256
|
||||
).hexdigest()
|
||||
if not secret or not hmac.compare_digest(supplied_signature, expected_signature):
|
||||
return ""
|
||||
padding = "=" * (-len(encoded_claims) % 4)
|
||||
claims = json.loads(
|
||||
base64.urlsafe_b64decode((encoded_claims + padding).encode("ascii")).decode("utf-8")
|
||||
)
|
||||
username = str(claims.get("u", "")).strip()
|
||||
expires_at = int(claims.get("e", 0))
|
||||
expected_username = _portal_auth_value("POC4_LOGIN_USER")
|
||||
if claims.get("v") != 1 or expires_at <= int(datetime.now(timezone.utc).timestamp()):
|
||||
return ""
|
||||
if not username or not hmac.compare_digest(username, expected_username):
|
||||
return ""
|
||||
return username
|
||||
except (AttributeError, TypeError, ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
||||
return ""
|
||||
|
||||
|
||||
def _restore_portal_session_from_cookie() -> None:
|
||||
if st.session_state.get(PORTAL_AUTHENTICATED_KEY, False):
|
||||
return
|
||||
username = _portal_cookie_user()
|
||||
if username:
|
||||
st.session_state[PORTAL_AUTHENTICATED_KEY] = True
|
||||
st.session_state[PORTAL_AUTH_USER_KEY] = username
|
||||
st.session_state[PORTAL_LOGIN_FAILURE_KEY] = False
|
||||
|
||||
|
||||
def _render_portal_login(profile: AppProfile) -> None:
|
||||
with st.container(key="console_login_container"):
|
||||
render_login_brand(st, profile)
|
||||
if not _portal_auth_configured():
|
||||
st.info("데모 계정 설정 중입니다. 운영 담당자에게 계정 발급을 요청해 주세요.")
|
||||
return
|
||||
with st.form("poc4_portal_login_form", clear_on_submit=True):
|
||||
username = st.text_input(
|
||||
"사용자 ID",
|
||||
max_chars=80,
|
||||
placeholder="사용자 ID를 입력하세요.",
|
||||
)
|
||||
password = st.text_input(
|
||||
"비밀번호",
|
||||
type="password",
|
||||
max_chars=200,
|
||||
placeholder="비밀번호를 입력하세요.",
|
||||
)
|
||||
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
|
||||
st.rerun()
|
||||
st.session_state[PORTAL_LOGIN_FAILURE_KEY] = True
|
||||
if st.session_state.get(PORTAL_LOGIN_FAILURE_KEY, False):
|
||||
if st.query_params.get("login") == "failed":
|
||||
st.error("사용자 ID 또는 비밀번호를 확인해 주세요.")
|
||||
components.html(
|
||||
"""
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: sans-serif; color: #15293a; }
|
||||
form { display: grid; gap: 10px; }
|
||||
label { font-size: 13px; font-weight: 700; color: #365064; }
|
||||
input { width: 100%; border: 1px solid #afc4d3; border-radius: 7px;
|
||||
padding: 11px 12px; font-size: 15px; color: #15293a; }
|
||||
button { margin-top: 6px; width: 100%; border: 0; border-radius: 7px;
|
||||
padding: 12px; background: #005c97; color: white; cursor: pointer;
|
||||
font-size: 15px; font-weight: 700; }
|
||||
button:hover { background: #004b7c; }
|
||||
</style>
|
||||
<form action="/_poc4/auth/login" method="post" target="_parent">
|
||||
<label>사용자 ID
|
||||
<input name="username" autocomplete="username" maxlength="80" required>
|
||||
</label>
|
||||
<label>비밀번호
|
||||
<input name="password" type="password" autocomplete="current-password"
|
||||
maxlength="200" required>
|
||||
</label>
|
||||
<button type="submit">로그인</button>
|
||||
</form>
|
||||
""",
|
||||
height=220,
|
||||
)
|
||||
st.markdown(
|
||||
f'<p class="console-muted">{html.escape(profile.login_footer)}</p>',
|
||||
unsafe_allow_html=True,
|
||||
@@ -809,7 +859,10 @@ def _logout_portal() -> None:
|
||||
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)
|
||||
st.rerun()
|
||||
components.html(
|
||||
'<script>window.parent.location.assign("/_poc4/auth/logout");</script>',
|
||||
height=0,
|
||||
)
|
||||
|
||||
|
||||
def _render_app_header(profile: AppProfile) -> None:
|
||||
@@ -3907,15 +3960,17 @@ def _render_mcp_result_sections(
|
||||
st.json(step.get("result_summary", {}))
|
||||
|
||||
if generated_sql:
|
||||
with st.expander("생성 SQL"):
|
||||
with st.expander("실행 SQL", expanded=True):
|
||||
st.code(generated_sql, language="sql")
|
||||
|
||||
if items:
|
||||
with st.expander(f"조회 결과 테이블 · {len(items)}건"):
|
||||
with st.expander(f"조회 결과 · {len(items)}건", expanded=True):
|
||||
display_items = items[:100]
|
||||
st.dataframe(display_items, use_container_width=True)
|
||||
if len(items) > len(display_items):
|
||||
st.caption(f"화면에는 최초 {len(display_items)}건만 표시합니다.")
|
||||
elif generated_sql:
|
||||
st.info("실행 SQL은 생성되었지만 반환된 조회 결과 행이 없습니다.")
|
||||
|
||||
with st.expander("MCP 호출 상세"):
|
||||
st.write(f"route: {details.get('server_id')} / {details.get('tool_name')}")
|
||||
@@ -5746,6 +5801,7 @@ def main() -> None:
|
||||
page_title=profile.page_title, page_icon=profile.page_icon, layout="wide"
|
||||
)
|
||||
_apply_console_theme(profile)
|
||||
_restore_portal_session_from_cookie()
|
||||
if not st.session_state.get(PORTAL_AUTHENTICATED_KEY, False):
|
||||
_render_portal_login(profile)
|
||||
return
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
"endpoint_url": "https://smilegate-backoffice.cloud-handson.com/mcp",
|
||||
"auth_token_env": "SMILEGATE_MCP_BEARER_TOKEN",
|
||||
"timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS",
|
||||
"default_tool": "oracle.select_ai.smilegate_game_text2sql",
|
||||
"default_tool": "oracle.select_ai.smilegate_fewshot_nl2sql",
|
||||
"router_model_profile": "gpt54_mini_oci",
|
||||
"tool_allowlist": [
|
||||
"oracle.select_ai.smilegate_fewshot_nl2sql",
|
||||
"oracle.select_ai.smilegate_game_text2sql",
|
||||
"oracle.select_ai.qa_vector_search",
|
||||
"oracle.select_ai.qa_vector_store"
|
||||
|
||||
Reference in New Issue
Block a user