Compare commits
81 Commits
hmm-backof
...
03bf0e096c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03bf0e096c | ||
|
|
4d2964b5c6 | ||
|
|
022ae7f9d2 | ||
|
|
7bf8199343 | ||
|
|
ccc7d9e7ec | ||
|
|
e0ec0c9340 | ||
|
|
e1fd486b01 | ||
|
|
5ca02e646a | ||
|
|
0a27ce9cd5 | ||
|
|
5ac9f5d591 | ||
|
|
ae37235258 | ||
|
|
d932782c9f | ||
|
|
7bc13446bc | ||
|
|
9f15ef6be1 | ||
|
|
8298ecb511 | ||
|
|
5a3b03060d | ||
|
|
8ee7fe262b | ||
|
|
471c50a40f | ||
|
|
9816088453 | ||
|
|
5dc9572210 | ||
|
|
af311f60f8 | ||
|
|
698baee583 | ||
|
|
c4643eca8b | ||
|
|
eee87b364d | ||
|
|
f16d3e5798 | ||
|
|
4afa4b219e | ||
|
|
9b2c54d27f | ||
|
|
caa2df3e5d | ||
|
|
5d4af888f3 | ||
|
|
9089897d88 | ||
|
|
a1685f533e | ||
|
|
b69abe0f3a | ||
|
|
e77b3e0543 | ||
|
|
859840e9bd | ||
|
|
593b1750f5 | ||
|
|
0662c4140c | ||
|
|
0cbac8d23b | ||
|
|
caa7d55085 | ||
|
|
b738708528 | ||
|
|
48476961ed | ||
|
|
745c091113 | ||
|
|
7175460314 | ||
|
|
c90c43facf | ||
|
|
2407c1bcc8 | ||
|
|
e7213eabb5 | ||
|
|
88a292d711 | ||
|
|
2efd1559aa | ||
|
|
f452b05209 | ||
|
|
a579501d6e | ||
|
|
14b237a734 | ||
|
|
c3aca16a09 | ||
|
|
ddd487c96a | ||
|
|
983bd5d4cb | ||
|
|
a66fdb2413 | ||
|
|
ec6a0304b2 | ||
|
|
87b26225b6 | ||
|
|
94c7bc2e07 | ||
|
|
a7ea010f5c | ||
|
|
b21d7ad01b | ||
|
|
e843bc9b31 | ||
|
|
aa9ea48b24 | ||
|
|
5d6754df05 | ||
|
|
68ad2f81e9 | ||
|
|
de4a2c4be7 | ||
|
|
820f026e17 | ||
|
|
55b5b7fd9d | ||
|
|
068b8fba2d | ||
|
|
3304b22bc4 | ||
|
|
750bfbab5b | ||
|
|
22c0b571e2 | ||
|
|
20c6a82338 | ||
|
|
b3f2422b63 | ||
|
|
1538aadb14 | ||
|
|
404244534a | ||
|
|
ea4fc1de8b | ||
|
|
0eb68664f1 | ||
|
|
4eec33b2ed | ||
|
|
b4ed8649f1 | ||
|
|
f7295277f1 | ||
|
|
075eb46ef8 | ||
|
|
2b66e27bfb |
50
.env.example
50
.env.example
@@ -52,41 +52,49 @@ export BACKOFFICE_ORDS_DB_URL="${BACKOFFICE_DB_URL}"
|
||||
export BACKOFFICE_ORDS_DB_USERNAME="CB_ORDS"
|
||||
export BACKOFFICE_ORDS_DB_PASSWORD=""
|
||||
|
||||
# Select AI 프로파일 소유 스키마 연결은 SHOWSQL 생성에만 사용합니다.
|
||||
# Select AI는 프로파일 소유 스키마로 별도 접속합니다.
|
||||
# 원문 비밀번호는 .env 또는 배포 환경 secret에만 두며 Git에 올리지 않습니다.
|
||||
export BACKOFFICE_SELECT_AI_DB_URL="${BACKOFFICE_DB_URL}"
|
||||
export BACKOFFICE_SELECT_AI_DB_USERNAME="${BACKOFFICE_DB_USERNAME}"
|
||||
export BACKOFFICE_SELECT_AI_DB_PASSWORD="${BACKOFFICE_DB_PASSWORD}"
|
||||
export BACKOFFICE_SELECT_AI_DB_USERNAME=""
|
||||
export BACKOFFICE_SELECT_AI_DB_PASSWORD=""
|
||||
export BACKOFFICE_SELECT_AI_PROFILE=""
|
||||
# 생성 SQL은 반드시 EXEMPT ACCESS POLICY가 없는 별도 계정으로 실행합니다.
|
||||
# 런타임 비밀번호는 Git에 저장하지 말고 배포 서버 secret 환경 파일에만 넣으세요.
|
||||
export BACKOFFICE_SELECT_AI_RUNTIME_DB_URL="${BACKOFFICE_DB_URL}"
|
||||
export BACKOFFICE_SELECT_AI_RUNTIME_DB_USERNAME="CB_ORDS"
|
||||
export BACKOFFICE_SELECT_AI_RUNTIME_DB_PASSWORD=""
|
||||
# Optional deployment-specific JSON contract. Keep project rules out of Java.
|
||||
export BACKOFFICE_SELECT_AI_QUERY_CONTRACT_FILE=""
|
||||
export BACKOFFICE_SELECT_AI_FEW_SHOT_ENABLED="true"
|
||||
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"
|
||||
|
||||
# --- (2c) 재사용 가능한 백오피스 카탈로그와 표시 설정 ---
|
||||
# 승인 객체는 key/tableName/objectType/businessName/description JSON 배열입니다.
|
||||
# 공통 데이터 카탈로그. objects는 key/tableName/objectType/businessName/description JSON 배열입니다.
|
||||
# 배포 환경마다 반드시 실제 소유자와 허용 객체를 지정합니다.
|
||||
export BACKOFFICE_CATALOG_OWNER="APP_OWNER"
|
||||
export BACKOFFICE_CATALOG_OBJECTS='[{"key":"employees","tableName":"EMPLOYEES","objectType":"TABLE","businessName":"직원","description":"직원 기본 정보"}]'
|
||||
export BACKOFFICE_CATALOG_OBJECTS='[{"key":"customers","tableName":"CUSTOMER","objectType":"TABLE","businessName":"고객","description":"고객 기본 정보"}]'
|
||||
export BACKOFFICE_PRODUCT_NAME="Data & AI Backoffice"
|
||||
export BACKOFFICE_PRODUCT_TITLE="Data & AI Backoffice"
|
||||
export BACKOFFICE_PRODUCT_DATA_LABEL="업무 데이터"
|
||||
|
||||
# 단일 Select AI 도구 호환 설정. 여러 Agent Tool을 쓸 때는 BACKOFFICE_MCP_TOOLS가 우선합니다.
|
||||
export BACKOFFICE_MCP_PUBLIC_URL="https://example.com/mcp"
|
||||
export BACKOFFICE_MCP_SERVER_NAME="data-ai-backoffice"
|
||||
export BACKOFFICE_MCP_TOOL_NAME="oracle.select_ai.data_text2sql"
|
||||
export BACKOFFICE_MCP_TOOL_LABEL="업무 데이터 Text2SQL"
|
||||
export BACKOFFICE_MCP_TOOL_DESCRIPTION="승인된 업무 데이터에 대해 읽기 전용 SQL을 생성하고 실행합니다."
|
||||
export BACKOFFICE_MCP_PROMPT_DESCRIPTION="업무 데이터에서 조회할 내용을 자연어로 입력합니다."
|
||||
export BACKOFFICE_MCP_TOOLS=''
|
||||
|
||||
# Data Redaction 관리 대상과 보안 SQL 화면 allowlist. 빈 값이면 관리/노출하지 않습니다.
|
||||
export BACKOFFICE_MCP_SHOWPROMPT_TOOL_NAME="oracle.select_ai.data_showprompt"
|
||||
export BACKOFFICE_MCP_SHOWPROMPT_TOOL_LABEL="업무 데이터 SHOWPROMPT"
|
||||
export BACKOFFICE_MCP_SHOWPROMPT_TOOL_DESCRIPTION="Select AI가 SQL 생성에 사용한 prompt를 조회하는 읽기 전용 진단 도구입니다."
|
||||
# Select AI few-shot 예제 SQL 조회·저장 MCP. 운영 환경은 고객별 도구명과 안내문만 변경합니다.
|
||||
export BACKOFFICE_MCP_QA_VECTOR_SEARCH_TOOL_NAME="oracle.select_ai.qa_vector_search"
|
||||
export BACKOFFICE_MCP_QA_VECTOR_SEARCH_TOOL_LABEL="Select AI 예제 SQL 조회"
|
||||
export BACKOFFICE_MCP_QA_VECTOR_SEARCH_TOOL_DESCRIPTION="현재 질문에 사용할 유사 예제 SQL을 Select AI 실행 전에 조회합니다."
|
||||
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_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 정책도 관리하지 않습니다.
|
||||
export BACKOFFICE_MASKING_POLICIES=''
|
||||
# 보안 SQL 화면에 노출할 번들 SQL. fileName은 패키지의 sql/adb/ 아래 파일명만 허용됩니다.
|
||||
export BACKOFFICE_SECURITY_SQL_SCRIPTS=''
|
||||
|
||||
# --- (2d) OpenAI 호환 AI 호출 (MCP-style Reasoning 탭) ---
|
||||
# --- (2c) OpenAI 호환 AI 호출 (MCP-style Reasoning 탭) ---
|
||||
export BACKOFFICE_AI_ENABLED="false"
|
||||
export BACKOFFICE_AI_PROVIDER="openai" # openai | oci
|
||||
export BACKOFFICE_AI_BASE_URL="" # 예: https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
# 환경/비밀 — 절대 commit 금지
|
||||
.env
|
||||
.runtime/
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
@@ -18,11 +17,9 @@ logs/
|
||||
# Java / Maven
|
||||
target/
|
||||
|
||||
# Python
|
||||
# Python / Streamlit
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
data/
|
||||
|
||||
# Locally downloaded development tools (for example SQLcl)
|
||||
.tools/
|
||||
|
||||
28
AGENTS.md
28
AGENTS.md
@@ -1,28 +0,0 @@
|
||||
# 문서 작성 공통 규칙
|
||||
|
||||
이 저장소에서 새 문서를 만들거나 기존 문서를 크게 고칠 때는 아래 구조를 기본으로 한다.
|
||||
|
||||
1. 첫 문서(`README.md`)는 독자가 2~3분 안에 목적, 범위, 결정사항, 전체 구성, 현재 상태를 파악하는 **개요 문서**로 작성한다.
|
||||
2. README에는 복잡한 절차와 모든 오류 사례를 누적하지 않는다. 아래와 같이 역할별 상세 문서로 분리하고, 개요에서 명확한 링크를 제공한다.
|
||||
- `architecture.md`: 신뢰 경계, 컴포넌트 책임, 데이터·인증 흐름, 설계 결정
|
||||
- `cookbook.md`: 준비물, 단계별 적용 명령/화면값, 검증, 롤백
|
||||
- `troubleshooting.md`: 증상 → 원인 → 확인 방법 → 해결 → 재발 방지
|
||||
- 필요하면 `operations.md`, `security.md`, `adr/` 등 목적이 드러나는 파일을 추가한다.
|
||||
3. 그림은 한 장에 모든 세부사항을 넣지 않는다.
|
||||
- README에는 시스템 경계와 핵심 흐름만 보이는 **개요도**를 둔다.
|
||||
- 상세 연결·claim·포트·예외 흐름은 architecture 또는 cookbook의 **상세도**로 분리한다.
|
||||
- 각 그림 아래에는 독자가 알아야 할 결론을 한두 문장으로 적는다.
|
||||
4. Cookbook은 실제 적용 순서를 따르며, 각 단계에 입력값의 출처, 성공 판정, 실패 시 연결할 troubleshooting 항목을 포함한다.
|
||||
5. 비밀번호, client secret, access token, 개인 식별 정보는 어떤 문서·그림·명령 출력에도 기록하지 않는다. 위치와 안전한 조회/rotation 방법만 기록한다.
|
||||
6. README의 문서 지도와 각 상세 문서의 상호 링크를 변경 후 반드시 확인한다.
|
||||
|
||||
문서의 독자는 운영자·개발자·검토자다. 제품명과 전문 용어는 필요할 때만 쓰고, 처음 한 번은 평이한 말로 역할을 정의한다.
|
||||
|
||||
## HMM Select AI·HTML 리포트 보호 기준
|
||||
|
||||
1. 사용자 권한이 적용되는 HMM MCP의 기준 endpoint는 `https://hmm-backoffice.cloud-handson.com/mcp`다.
|
||||
2. MCP route `search_carrier_performance`는 기존 DB Agent Tool `HMM_CARRIER_FEDERATION_SEARCH`를 호출한다. 이 Tool은 자연어를 Select AI Federation으로 처리하며, 고정 SQL·고정 데이터·대체 package로 바꾸지 않는다.
|
||||
3. `hmm-mcp.cloud-handson.com`은 별도 호환성·무 VPD 시험 환경이다. 기준 endpoint나 운영 Select AI 경로의 대체재로 사용하지 않는다.
|
||||
4. HTML 기능 추가 범위는 후속 표현 Tool `HMM_CARRIER_REPORT_RENDERER`와 포털의 범용 이전 결과 전달뿐이다. 렌더러는 사용자·권한·업무 데이터를 다시 조회하거나 보충하지 않는다.
|
||||
5. 조회 Tool, target, package 또는 endpoint를 생성·삭제·교체하기 전에는 현재 MCP discovery, `BACKOFFICE_MCP_TOOLS`, DB Agent Tool metadata를 먼저 대조한다. 기존 조회 경로 변경은 사용자가 명시적으로 요청한 경우에만 한다.
|
||||
6. 회귀 검증은 같은 Bearer 문맥에서 `search_carrier_performance`의 원본 행 수와 renderer에 전달된 `rows` 수가 같은지 확인한다. 대표 시나리오의 현재 기준은 E1001 팀장 질문에 대한 8건이지만, 코드는 E1001이나 8을 조건으로 사용하지 않는다.
|
||||
@@ -3,11 +3,6 @@
|
||||
|
||||
# MCP
|
||||
HMM_MCP_BEARER_TOKEN=
|
||||
HMM_MCP_BEARER_TOKEN_E1001=
|
||||
HMM_MCP_BEARER_TOKEN_E1002=
|
||||
HMM_MCP_BEARER_TOKEN_E1003=
|
||||
HMM_MCP_BEARER_TOKEN_E1005=
|
||||
HMM_MCP_BEARER_TOKEN_E1007=
|
||||
AI_WEB_AGENT_CONSOLE_MCP_TIMEOUT_SECONDS=45
|
||||
|
||||
# OCI Generative AI SDK
|
||||
|
||||
@@ -41,9 +41,7 @@ python3 -m venv .venv
|
||||
- `config/hmm_hr_query_contracts.json`: 질의별 필수 근거와 계산·시간 규칙
|
||||
|
||||
실제 토큰, DB 비밀번호, Wallet, OCI private key는 Git에 넣지 않는다. 데모 사용자 JSON은
|
||||
`HMM_MCP_BEARER_TOKEN_E1001`처럼 사용자별 환경변수 이름만 참조한다. token 원문은 설정 JSON이나
|
||||
문서에 기록하지 않는다. 사용자 preset들이 같은 환경변수를 공유하면 화면의 사용자만 바뀌고 DB
|
||||
권한 문맥은 바뀌지 않으므로 허용하지 않는다.
|
||||
`HMM_MCP_BEARER_TOKEN` 같은 환경변수 이름만 참조한다.
|
||||
|
||||
## 검증
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ def build_mcp_tool_arguments(
|
||||
elif "limit" in properties:
|
||||
args["limit"] = limit
|
||||
return args
|
||||
return {"prompt": question, "limit": limit}
|
||||
return {}
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -26,7 +26,10 @@ ALLOWED_OCI_SETTINGS = frozenset(
|
||||
"OCI_PROFILE",
|
||||
}
|
||||
)
|
||||
_COMPARTMENT_ID = re.compile(r"^ocid1\.compartment\.[A-Za-z0-9._-]+$")
|
||||
# OCI permits the tenancy OCID when the root compartment is selected.
|
||||
_COMPARTMENT_OR_ROOT_ID = re.compile(
|
||||
r"^ocid1\.(?:compartment|tenancy)\.[A-Za-z0-9._-]+$"
|
||||
)
|
||||
|
||||
|
||||
class CompletionClient(Protocol):
|
||||
@@ -95,7 +98,7 @@ def load_oci_settings() -> OCISettings:
|
||||
raise ValueError("unsupported OCI authentication mode")
|
||||
|
||||
compartment_id = values.get("OCI_GENAI_COMPARTMENT_ID", "").strip()
|
||||
if not _COMPARTMENT_ID.fullmatch(compartment_id):
|
||||
if not _COMPARTMENT_OR_ROOT_ID.fullmatch(compartment_id):
|
||||
raise ValueError("OCI Generative AI compartment is not configured")
|
||||
return OCISettings(
|
||||
auth_type=auth_type,
|
||||
|
||||
@@ -35,12 +35,64 @@ def apply_console_theme(st: Any, profile: AppProfile) -> None:
|
||||
input, textarea, [data-baseweb="select"] > div, [data-testid="stSidebar"] button {{
|
||||
background:#fff !important; border:1px solid var(--console-border) !important;
|
||||
border-radius:4px !important; box-shadow:none !important; }}
|
||||
/* Streamlit JSON uses an independently styled code surface. Keep the
|
||||
MCP detail payload readable even when the browser/system prefers a
|
||||
dark code theme. */
|
||||
[data-testid="stJson"], [data-testid="stJson"] > div,
|
||||
[data-testid="stJson"] .react-json-view, [data-testid="stJson"] pre {{
|
||||
background:#f8fafc !important; color:var(--console-text) !important;
|
||||
border-color:var(--console-border) !important; color-scheme:light !important; }}
|
||||
[data-testid="stJson"] *, [data-testid="stJson"] pre *,
|
||||
[data-testid="stJson"] code {{
|
||||
background:transparent !important; color:var(--console-text) !important;
|
||||
-webkit-text-fill-color:var(--console-text) !important; }}
|
||||
/* Baseline answers and generated SQL use Streamlit's separate code
|
||||
surface. Keep it readable when the browser prefers dark mode. */
|
||||
[data-testid="stCode"], [data-testid="stCode"] pre,
|
||||
[data-testid="stCode"] code, [data-testid="stCodeBlock"],
|
||||
[data-testid="stCodeBlock"] pre, [data-testid="stCodeBlock"] code {{
|
||||
background:#f8fafc !important; color:var(--console-text) !important;
|
||||
border-color:var(--console-border) !important; color-scheme:light !important;
|
||||
-webkit-text-fill-color:var(--console-text) !important; }}
|
||||
[data-testid="stCode"] *, [data-testid="stCodeBlock"] * {{
|
||||
color:var(--console-text) !important;
|
||||
-webkit-text-fill-color:var(--console-text) !important; }}
|
||||
/* Chat responses are rendered in a separate Streamlit surface. Without
|
||||
these rules a dark browser theme can leave the answer card dark while
|
||||
its Markdown keeps the light-theme text color. */
|
||||
div[data-testid="stChatMessage"], div[data-testid="stChatMessageContent"] {{
|
||||
background:#fff !important; color:var(--console-text) !important;
|
||||
border-color:var(--console-border) !important; color-scheme:light !important; }}
|
||||
div[data-testid="stChatMessage"] [data-testid="stMarkdownContainer"],
|
||||
div[data-testid="stChatMessage"] [data-testid="stMarkdownContainer"] *,
|
||||
div[data-testid="stChatMessage"] [data-testid="stCaptionContainer"],
|
||||
div[data-testid="stChatMessage"] [data-testid="stCaptionContainer"] * {{
|
||||
color:var(--console-text) !important;
|
||||
-webkit-text-fill-color:var(--console-text) !important; }}
|
||||
/* Streamlit expanders use a dark summary bar in dark browser themes. */
|
||||
details, details > summary {{
|
||||
background:#fff !important; color:var(--console-text) !important;
|
||||
border-color:var(--console-border) !important; color-scheme:light !important; }}
|
||||
details > summary *, details > summary::marker {{
|
||||
color:var(--console-text) !important;
|
||||
-webkit-text-fill-color:var(--console-text) !important; }}
|
||||
[data-testid="stExpander"] > details,
|
||||
[data-testid="stExpander"] > details > summary,
|
||||
[data-testid="stExpander"] > details > summary > div,
|
||||
[data-testid="stExpander"] > details > summary > div > div {{
|
||||
background:#fff !important; color:var(--console-text) !important;
|
||||
border-color:var(--console-border) !important; }}
|
||||
[data-testid="stExpander"] > details > summary *,
|
||||
[data-testid="stExpander"] > details > summary svg {{
|
||||
color:var(--console-text) !important; fill:var(--console-text) !important;
|
||||
stroke:var(--console-text) !important;
|
||||
-webkit-text-fill-color:var(--console-text) !important; }}
|
||||
div[data-testid="stButton"] > button, div[data-testid="stFormSubmitButton"] > button {{
|
||||
background:#fff !important; color:var(--console-text) !important;
|
||||
-webkit-text-fill-color:var(--console-text) !important;
|
||||
border:1px solid var(--console-border) !important;
|
||||
border-radius:4px !important; box-shadow:none !important; }}
|
||||
div[data-testid="stButton"] > button *,
|
||||
div[data-testid="stFormSubmitButton"] > button * {{
|
||||
border-radius:4px !important; box-shadow:none !important; color-scheme:light !important; }}
|
||||
div[data-testid="stButton"] > button *, div[data-testid="stFormSubmitButton"] > button * {{
|
||||
color:var(--console-text) !important; -webkit-text-fill-color:var(--console-text) !important; }}
|
||||
div[data-testid="stButton"] > button[kind="primary"],
|
||||
div[data-testid="stFormSubmitButton"] > button[data-testid="stBaseButton-primaryFormSubmit"] {{
|
||||
|
||||
242
ai-web-agent-console/ai_web_agent_console/qa_history.py
Normal file
242
ai-web-agent-console/ai_web_agent_console/qa_history.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""Customer QA benchmark parsing and deterministic SQL evaluation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
class QaBenchmarkError(RuntimeError):
|
||||
"""Raised when the QA benchmark source cannot be used safely."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QaQuestion:
|
||||
question_id: int | None
|
||||
question_code: str
|
||||
category: str
|
||||
title: str
|
||||
question_text: str
|
||||
source_document: str
|
||||
source_sheet: str
|
||||
source_row: int | None
|
||||
source_scenario: str
|
||||
sample_sql: str
|
||||
expected_focus: str
|
||||
baseline_sql: str
|
||||
baseline_answer: str
|
||||
support_level: str
|
||||
evaluation_rule: Mapping[str, Any]
|
||||
last_judgment_status: str = ""
|
||||
last_evaluated_at: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QaJudgment:
|
||||
status: str
|
||||
reason: str
|
||||
|
||||
|
||||
def question_fingerprint(question_text: str) -> str:
|
||||
normalized = " ".join(str(question_text or "").split()).casefold()
|
||||
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _compact_text(value: object) -> str:
|
||||
return str(value or "").strip()
|
||||
|
||||
|
||||
def _string_list(value: object) -> tuple[str, ...]:
|
||||
if not isinstance(value, list):
|
||||
return ()
|
||||
return tuple(_compact_text(item) for item in value if _compact_text(item))
|
||||
|
||||
|
||||
def load_benchmark_questions(path: Path) -> tuple[QaQuestion, ...]:
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, ValueError) as exc:
|
||||
raise QaBenchmarkError(f"질답 기준 파일을 읽지 못했습니다: {path}") from exc
|
||||
rows = payload.get("scenarios") if isinstance(payload, Mapping) else None
|
||||
if not isinstance(rows, list):
|
||||
raise QaBenchmarkError("질답 기준 파일에 scenarios 배열이 필요합니다.")
|
||||
|
||||
questions: list[QaQuestion] = []
|
||||
seen_codes: set[str] = set()
|
||||
for row in rows:
|
||||
if not isinstance(row, Mapping):
|
||||
raise QaBenchmarkError("질답 기준의 각 시나리오는 객체여야 합니다.")
|
||||
source = row.get("source") if isinstance(row.get("source"), Mapping) else {}
|
||||
history = (
|
||||
row.get("historical_answer")
|
||||
if isinstance(row.get("historical_answer"), Mapping)
|
||||
else {}
|
||||
)
|
||||
code = _compact_text(row.get("case_id")).upper()
|
||||
question_text = _compact_text(row.get("question"))
|
||||
if not code or not question_text:
|
||||
raise QaBenchmarkError("각 질답 기준에는 case_id와 question이 필요합니다.")
|
||||
if code in seen_codes:
|
||||
raise QaBenchmarkError(f"중복된 질답 case_id입니다: {code}")
|
||||
evaluation_rule = row.get("evaluation_rule")
|
||||
if not isinstance(evaluation_rule, Mapping):
|
||||
evaluation_rule = {}
|
||||
questions.append(
|
||||
QaQuestion(
|
||||
question_id=None,
|
||||
question_code=code,
|
||||
category=_compact_text(row.get("category")) or "GENERAL",
|
||||
title=_compact_text(row.get("title")) or code,
|
||||
question_text=question_text,
|
||||
source_document=_compact_text(source.get("workbook")),
|
||||
source_sheet=_compact_text(source.get("sheet")),
|
||||
source_row=_number_or_none(source.get("excel_row")),
|
||||
source_scenario=_compact_text(source.get("scenario")),
|
||||
sample_sql=_compact_text(source.get("sample_query")),
|
||||
expected_focus=_compact_text(row.get("expected_focus")),
|
||||
baseline_sql=_compact_text(history.get("generated_sql")),
|
||||
baseline_answer=_compact_text(history.get("answer_text")),
|
||||
support_level=_compact_text(row.get("support_level")).upper() or "UNKNOWN",
|
||||
evaluation_rule={
|
||||
"required_sql_terms": list(
|
||||
_string_list(evaluation_rule.get("required_sql_terms"))
|
||||
),
|
||||
"recommended_sql_terms": list(
|
||||
_string_list(evaluation_rule.get("recommended_sql_terms"))
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
seen_codes.add(code)
|
||||
return tuple(questions)
|
||||
|
||||
|
||||
def question_from_record(record: Mapping[str, Any]) -> QaQuestion:
|
||||
rule = record.get("evaluation_rule")
|
||||
if isinstance(rule, str):
|
||||
try:
|
||||
rule = json.loads(rule)
|
||||
except ValueError:
|
||||
rule = {}
|
||||
if not isinstance(rule, Mapping):
|
||||
rule = {}
|
||||
return QaQuestion(
|
||||
question_id=_number_or_none(record.get("question_id")),
|
||||
question_code=_compact_text(record.get("question_code")),
|
||||
category=_compact_text(record.get("category")) or "GENERAL",
|
||||
title=_compact_text(record.get("title")) or _compact_text(record.get("question_code")),
|
||||
question_text=_compact_text(record.get("question_text")),
|
||||
source_document=_compact_text(record.get("source_document")),
|
||||
source_sheet=_compact_text(record.get("source_sheet")),
|
||||
source_row=_number_or_none(record.get("source_row")),
|
||||
source_scenario=_compact_text(record.get("source_scenario")),
|
||||
sample_sql=_compact_text(record.get("sample_sql")),
|
||||
expected_focus=_compact_text(record.get("expected_focus")),
|
||||
baseline_sql=_compact_text(record.get("baseline_sql")),
|
||||
baseline_answer=_compact_text(record.get("baseline_answer")),
|
||||
support_level=_compact_text(record.get("support_level")).upper() or "UNKNOWN",
|
||||
evaluation_rule={
|
||||
"required_sql_terms": list(
|
||||
_string_list(rule.get("required_sql_terms"))
|
||||
),
|
||||
"recommended_sql_terms": list(
|
||||
_string_list(rule.get("recommended_sql_terms"))
|
||||
),
|
||||
},
|
||||
last_judgment_status=_compact_text(record.get("last_judgment_status")),
|
||||
last_evaluated_at=_compact_text(record.get("last_evaluated_at")),
|
||||
)
|
||||
|
||||
|
||||
def _number_or_none(value: object) -> int | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _append_issue(issues: list[str], condition: bool, message: str) -> None:
|
||||
if condition:
|
||||
issues.append(message)
|
||||
|
||||
|
||||
def evaluate_sql(
|
||||
question: QaQuestion | None,
|
||||
generated_sql: str,
|
||||
*,
|
||||
execution_succeeded: bool,
|
||||
error_text: str = "",
|
||||
game_plan_status: str = "",
|
||||
) -> QaJudgment:
|
||||
"""Evaluate the generated SQL against the customer-approved benchmark rule."""
|
||||
if question is None or not question.question_code:
|
||||
return QaJudgment(
|
||||
status="REVIEW",
|
||||
reason="자유 질의입니다. 고객 기준 정답 시나리오와 연결되지 않아 수동 검토가 필요합니다.",
|
||||
)
|
||||
|
||||
sql = _compact_text(generated_sql)
|
||||
upper_sql = sql.upper()
|
||||
lower_sql = sql.lower()
|
||||
execution_status = "PASS" if execution_succeeded else "FAIL_EXECUTION"
|
||||
issues: list[str] = []
|
||||
failure_markers = ("could not be generated", "exception encountered", "invalid identifier", "ora-")
|
||||
has_failure_text = any(marker in lower_sql for marker in failure_markers)
|
||||
required = _string_list(question.evaluation_rule.get("required_sql_terms"))
|
||||
recommended = _string_list(question.evaluation_rule.get("recommended_sql_terms"))
|
||||
missing_required = [term for term in required if term.upper() not in upper_sql]
|
||||
missing_recommended = [term for term in recommended if term.upper() not in upper_sql]
|
||||
|
||||
if not execution_succeeded:
|
||||
issues.append(f"실행 상태가 {execution_status}입니다.")
|
||||
if not sql:
|
||||
issues.append("생성 SQL이 없습니다.")
|
||||
if has_failure_text:
|
||||
issues.append("생성 SQL에 오류 또는 생성 실패 문구가 포함되어 있습니다.")
|
||||
if missing_required:
|
||||
issues.append("필수 SQL 요소 누락: " + ", ".join(missing_required))
|
||||
if missing_recommended:
|
||||
issues.append("권장 SQL 요소 누락: " + ", ".join(missing_recommended))
|
||||
_append_issue(
|
||||
issues,
|
||||
bool(re.search(r'_[A-Z0-9]*YN"\s*=\s*\'1\'', sql, flags=re.IGNORECASE)),
|
||||
"*_YN 컬럼은 샘플 메타데이터의 Y/N 값으로 비교해야 합니다.",
|
||||
)
|
||||
_append_issue(
|
||||
issues,
|
||||
bool(re.search(r'_[A-Z0-9]*FLAG"\s*=\s*\'Y\'', sql, flags=re.IGNORECASE)),
|
||||
"*_FLAG 컬럼은 샘플 메타데이터의 0/1 값으로 비교해야 합니다.",
|
||||
)
|
||||
|
||||
support = question.support_level
|
||||
if support == "UNSUPPORTED":
|
||||
plan_status = _compact_text(game_plan_status).upper()
|
||||
safe_empty_result = bool(
|
||||
re.search(r"\bFROM\s+DUAL\b", upper_sql)
|
||||
and re.search(r"\bWHERE\s+1\s*=\s*0\b", upper_sql)
|
||||
)
|
||||
if plan_status in {"UNAVAILABLE", "UNMATCHED"} and execution_succeeded and safe_empty_result:
|
||||
return QaJudgment(
|
||||
"PASS",
|
||||
"게임 계획이 데이터 미지원 또는 미매칭으로 판정됐고, 임의 객체 선택 없이 빈 결과를 반환했습니다.",
|
||||
)
|
||||
if not sql and any(marker in error_text.lower() for marker in failure_markers):
|
||||
return QaJudgment("PASS", "미지원 게임 질문이 실행 가능한 SQL로 변환되지 않았습니다. 기대한 안전 차단입니다.")
|
||||
return QaJudgment("FAIL", "미지원 게임이 게임 계획의 안전한 빈 결과로 처리되지 않았거나 실행에 실패했습니다.")
|
||||
|
||||
if not execution_succeeded or not sql or has_failure_text or missing_required:
|
||||
return QaJudgment("FAIL", "\n".join(issues) or "필수 SQL 또는 실행 검증에 실패했습니다.")
|
||||
|
||||
if any(issue.startswith("필수") for issue in issues):
|
||||
return QaJudgment("FAIL", "\n".join(issues))
|
||||
if support == "PARTIAL":
|
||||
issues.append("지원 범위가 일부인 질문이므로 결과 범위를 함께 검토해야 합니다.")
|
||||
if issues:
|
||||
return QaJudgment("WARN", "\n".join(issues))
|
||||
return QaJudgment("PASS", "고객 기준의 필수 SQL 요소와 실행 결과를 확인했습니다.")
|
||||
583
ai-web-agent-console/ai_web_agent_console/qa_history_store.py
Normal file
583
ai-web-agent-console/ai_web_agent_console/qa_history_store.py
Normal file
@@ -0,0 +1,583 @@
|
||||
"""Oracle ADB persistence for the Smilegate customer QA benchmark."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator, Mapping
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import oracledb
|
||||
|
||||
from src.poc4.qa_history import QaQuestion, load_benchmark_questions, question_fingerprint, question_from_record
|
||||
|
||||
|
||||
class QaHistoryStoreError(RuntimeError):
|
||||
"""A safe user-facing persistence error."""
|
||||
|
||||
|
||||
QUESTION_TABLE = "SG_AI_QA_QUESTION"
|
||||
ANSWER_TABLE = "SG_AI_QA_ANSWER"
|
||||
HISTORICAL_RUN_KEY = "HISTORICAL:2026-07-21:term-dict-final-v2"
|
||||
|
||||
|
||||
def _env_value(name: str, env_file: Path | None = None) -> str:
|
||||
value = os.environ.get(name, "").strip()
|
||||
if value or env_file is None or not env_file.is_file():
|
||||
return value
|
||||
try:
|
||||
lines = env_file.read_text(encoding="utf-8").splitlines()
|
||||
except (OSError, UnicodeError):
|
||||
return ""
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
if line.startswith("export "):
|
||||
line = line[7:].lstrip()
|
||||
key, raw = line.split("=", 1)
|
||||
if key.strip() != name:
|
||||
continue
|
||||
raw = raw.strip()
|
||||
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in {"'", '"'}:
|
||||
raw = raw[1:-1]
|
||||
return raw.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _normalize_oracle_dsn(raw_dsn: str) -> tuple[str, str]:
|
||||
value = str(raw_dsn or "").strip()
|
||||
if value.startswith("jdbc:oracle:thin:@"):
|
||||
value = value[len("jdbc:oracle:thin:@"):]
|
||||
if "?" not in value:
|
||||
return value, ""
|
||||
dsn, query = value.split("?", 1)
|
||||
parsed = parse_qs(query, keep_blank_values=False)
|
||||
wallet_dir = (parsed.get("TNS_ADMIN") or parsed.get("tns_admin") or [""])[0]
|
||||
return dsn.strip(), wallet_dir.strip()
|
||||
|
||||
|
||||
def _read_lob(value: Any) -> Any:
|
||||
return value.read() if hasattr(value, "read") else value
|
||||
|
||||
|
||||
def _record_from_cursor(cursor: Any, row: Any) -> dict[str, Any]:
|
||||
names = [column[0].lower() for column in cursor.description]
|
||||
return {name: _read_lob(value) for name, value in zip(names, row)}
|
||||
|
||||
|
||||
def _to_json(value: Mapping[str, Any] | None) -> str:
|
||||
payload = dict(value or {})
|
||||
text = json.dumps(payload, ensure_ascii=False, default=str)
|
||||
if len(text) <= 120_000:
|
||||
return text
|
||||
return json.dumps(
|
||||
{
|
||||
"truncated": True,
|
||||
"preview": text[:119_800],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def _answer_record(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
result_json = str(row.get("result_json") or "").strip()
|
||||
try:
|
||||
result = json.loads(result_json) if result_json else {}
|
||||
except ValueError:
|
||||
result = {"raw": result_json}
|
||||
return {
|
||||
"answer_seq": row.get("answer_seq"),
|
||||
"question_id": row.get("question_id"),
|
||||
"answer_kind": str(row.get("answer_kind") or ""),
|
||||
"run_key": str(row.get("run_key") or ""),
|
||||
"conversation_id": str(row.get("conversation_id") or ""),
|
||||
"requested_by": str(row.get("requested_by") or ""),
|
||||
"requested_at": str(row.get("requested_at") or ""),
|
||||
"model_profile": str(row.get("model_profile") or ""),
|
||||
"generated_sql": str(row.get("generated_sql") or ""),
|
||||
"answer_text": str(row.get("answer_text") or ""),
|
||||
"result": result,
|
||||
"execution_output": str(row.get("execution_output") or ""),
|
||||
"execution_status": str(row.get("execution_status") or ""),
|
||||
"judgment_status": str(row.get("judgment_status") or ""),
|
||||
"judgment_reason": str(row.get("judgment_reason") or ""),
|
||||
"duration_ms": row.get("duration_ms"),
|
||||
"created_at": str(row.get("created_at") or ""),
|
||||
}
|
||||
|
||||
|
||||
class QaHistoryStore:
|
||||
def __init__(self, *, env_file: Path | None = None) -> None:
|
||||
self._env_file = env_file
|
||||
self._pool: Any | None = None
|
||||
|
||||
def _config(self) -> dict[str, str]:
|
||||
username = (
|
||||
_env_value("POC4_QA_DB_USERNAME", self._env_file)
|
||||
or _env_value("BACKOFFICE_SELECT_AI_DB_USERNAME", self._env_file)
|
||||
or _env_value("BACKOFFICE_DB_USERNAME", self._env_file)
|
||||
)
|
||||
password = (
|
||||
_env_value("POC4_QA_DB_PASSWORD", self._env_file)
|
||||
or _env_value("BACKOFFICE_SELECT_AI_DB_PASSWORD", self._env_file)
|
||||
or _env_value("BACKOFFICE_DB_PASSWORD", self._env_file)
|
||||
)
|
||||
raw_dsn = (
|
||||
_env_value("POC4_QA_DB_DSN", self._env_file)
|
||||
or _env_value("BACKOFFICE_SELECT_AI_DB_URL", self._env_file)
|
||||
or _env_value("BACKOFFICE_DB_URL", self._env_file)
|
||||
)
|
||||
dsn, wallet_from_dsn = _normalize_oracle_dsn(raw_dsn)
|
||||
wallet_dir = (
|
||||
_env_value("POC4_QA_DB_WALLET_DIR", self._env_file)
|
||||
or wallet_from_dsn
|
||||
or _env_value("ORACLE_WALLET_DIR", self._env_file)
|
||||
)
|
||||
if not username or not password or not dsn:
|
||||
raise QaHistoryStoreError("질답 이력 DB 접속 설정을 확인해 주세요.")
|
||||
return {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"dsn": dsn,
|
||||
"wallet_dir": wallet_dir,
|
||||
}
|
||||
|
||||
def _get_pool(self) -> Any:
|
||||
if self._pool is not None:
|
||||
return self._pool
|
||||
config = self._config()
|
||||
kwargs: dict[str, Any] = {
|
||||
"user": config["username"],
|
||||
"password": config["password"],
|
||||
"dsn": config["dsn"],
|
||||
"min": 1,
|
||||
"max": 3,
|
||||
"increment": 1,
|
||||
"getmode": oracledb.POOL_GETMODE_WAIT,
|
||||
}
|
||||
wallet_dir = Path(config["wallet_dir"]).expanduser()
|
||||
if config["wallet_dir"]:
|
||||
if not wallet_dir.is_dir():
|
||||
raise QaHistoryStoreError("질답 이력 DB Wallet 경로를 확인해 주세요.")
|
||||
kwargs["config_dir"] = str(wallet_dir)
|
||||
try:
|
||||
self._pool = oracledb.create_pool(**kwargs)
|
||||
return self._pool
|
||||
except (oracledb.Error, OSError, ValueError) as exc:
|
||||
raise QaHistoryStoreError("질답 이력 DB에 연결하지 못했습니다.") from exc
|
||||
|
||||
@contextmanager
|
||||
def _connection(self) -> Iterator[Any]:
|
||||
try:
|
||||
with self._get_pool().acquire() as connection:
|
||||
yield connection
|
||||
except QaHistoryStoreError:
|
||||
raise
|
||||
except (oracledb.Error, OSError, ValueError) as exc:
|
||||
raise QaHistoryStoreError("질답 이력 DB 작업에 실패했습니다.") from exc
|
||||
|
||||
def list_questions(self, *, limit: int = 200) -> list[QaQuestion]:
|
||||
sql = f"""
|
||||
SELECT q.question_id, q.question_code, q.category, q.title,
|
||||
q.question_text, q.source_document, q.source_sheet,
|
||||
q.source_row, q.source_scenario, q.sample_sql,
|
||||
q.expected_focus, q.baseline_sql, q.baseline_answer,
|
||||
q.support_level, q.evaluation_rule_json,
|
||||
latest.judgment_status AS last_judgment_status,
|
||||
TO_CHAR(latest.evaluated_at AT TIME ZONE 'Asia/Seoul',
|
||||
'YYYY-MM-DD HH24:MI:SS TZH:TZM') AS last_evaluated_at
|
||||
FROM {QUESTION_TABLE} q
|
||||
LEFT JOIN (
|
||||
SELECT question_id, judgment_status, evaluated_at
|
||||
FROM (
|
||||
SELECT question_id, judgment_status, evaluated_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY question_id ORDER BY answer_seq DESC
|
||||
) AS row_no
|
||||
FROM {ANSWER_TABLE}
|
||||
)
|
||||
WHERE row_no = 1
|
||||
) latest ON latest.question_id = q.question_id
|
||||
WHERE q.active_yn = 'Y'
|
||||
ORDER BY q.category, q.question_code
|
||||
FETCH FIRST :row_limit ROWS ONLY
|
||||
"""
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, {"row_limit": int(limit)})
|
||||
rows = [_record_from_cursor(cursor, row) for row in cursor]
|
||||
return [question_from_record(row) for row in rows]
|
||||
|
||||
def get_question(self, question_id: int) -> QaQuestion | None:
|
||||
sql = f"""
|
||||
SELECT question_id, question_code, category, title, question_text,
|
||||
source_document, source_sheet, source_row, source_scenario,
|
||||
sample_sql, expected_focus, baseline_sql, baseline_answer,
|
||||
support_level, evaluation_rule_json
|
||||
FROM {QUESTION_TABLE}
|
||||
WHERE question_id = :question_id AND active_yn = 'Y'
|
||||
"""
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, {"question_id": int(question_id)})
|
||||
row = cursor.fetchone()
|
||||
return question_from_record(_record_from_cursor(cursor, row)) if row else None
|
||||
|
||||
def get_question_by_code(self, question_code: str) -> QaQuestion | None:
|
||||
sql = f"""
|
||||
SELECT question_id, question_code, category, title, question_text,
|
||||
source_document, source_sheet, source_row, source_scenario,
|
||||
sample_sql, expected_focus, baseline_sql, baseline_answer,
|
||||
support_level, evaluation_rule_json
|
||||
FROM {QUESTION_TABLE}
|
||||
WHERE question_code = :question_code AND active_yn = 'Y'
|
||||
"""
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, {"question_code": str(question_code).upper()})
|
||||
row = cursor.fetchone()
|
||||
return question_from_record(_record_from_cursor(cursor, row)) if row else None
|
||||
|
||||
def list_answers(self, question_id: int, *, limit: int = 30) -> list[dict[str, Any]]:
|
||||
sql = f"""
|
||||
SELECT answer_seq, question_id, answer_kind, run_key, conversation_id,
|
||||
requested_by,
|
||||
TO_CHAR(requested_at AT TIME ZONE 'Asia/Seoul',
|
||||
'YYYY-MM-DD HH24:MI:SS TZH:TZM') AS requested_at,
|
||||
model_profile, generated_sql, answer_text, result_json,
|
||||
execution_output, execution_status, judgment_status,
|
||||
judgment_reason, duration_ms,
|
||||
TO_CHAR(created_at AT TIME ZONE 'Asia/Seoul',
|
||||
'YYYY-MM-DD HH24:MI:SS TZH:TZM') AS created_at
|
||||
FROM {ANSWER_TABLE}
|
||||
WHERE question_id = :question_id
|
||||
ORDER BY answer_seq DESC
|
||||
FETCH FIRST :row_limit ROWS ONLY
|
||||
"""
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, {"question_id": int(question_id), "row_limit": int(limit)})
|
||||
rows = [_record_from_cursor(cursor, row) for row in cursor]
|
||||
return [_answer_record(row) for row in rows]
|
||||
|
||||
def find_or_create_free_text_question(self, question_text: str) -> QaQuestion:
|
||||
normalized = str(question_text or "").strip()
|
||||
if not normalized:
|
||||
raise QaHistoryStoreError("자유 질의가 비어 있습니다.")
|
||||
fingerprint = question_fingerprint(normalized)
|
||||
code = f"ADHOC-{fingerprint[:12].upper()}"
|
||||
merge_sql = f"""
|
||||
MERGE INTO {QUESTION_TABLE} target
|
||||
USING (SELECT :question_hash AS question_hash FROM dual) source
|
||||
ON (target.question_hash = source.question_hash)
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
question_code, question_source, question_hash, category, title,
|
||||
question_text, support_level, evaluation_rule_json, active_yn
|
||||
) VALUES (
|
||||
:question_code, 'FREE_TEXT', :question_hash, 'FREE_TEXT',
|
||||
:title, :question_text, 'REVIEW', '{{}}', 'Y'
|
||||
)
|
||||
"""
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
merge_sql,
|
||||
{
|
||||
"question_hash": fingerprint,
|
||||
"question_code": code,
|
||||
"title": normalized[:180],
|
||||
"question_text": normalized,
|
||||
},
|
||||
)
|
||||
connection.commit()
|
||||
cursor.execute(
|
||||
f"""SELECT question_id FROM {QUESTION_TABLE}
|
||||
WHERE question_hash = :question_hash""",
|
||||
{"question_hash": fingerprint},
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise QaHistoryStoreError("자유 질의 마스터를 저장하지 못했습니다.")
|
||||
question = self.get_question(int(row[0]))
|
||||
if question is None:
|
||||
raise QaHistoryStoreError("자유 질의 마스터를 다시 읽지 못했습니다.")
|
||||
return question
|
||||
|
||||
def record_answer(
|
||||
self,
|
||||
*,
|
||||
question_id: int,
|
||||
answer_kind: str,
|
||||
conversation_id: str,
|
||||
requested_by: str,
|
||||
model_profile: str,
|
||||
generated_sql: str,
|
||||
answer_text: str,
|
||||
result: Mapping[str, Any] | None,
|
||||
execution_output: str,
|
||||
execution_status: str,
|
||||
judgment_status: str,
|
||||
judgment_reason: str,
|
||||
duration_ms: int | None,
|
||||
run_key: str = "",
|
||||
) -> None:
|
||||
sql = f"""
|
||||
INSERT INTO {ANSWER_TABLE} (
|
||||
question_id, answer_kind, run_key, conversation_id, requested_by,
|
||||
requested_at, model_profile, generated_sql, answer_text, result_json,
|
||||
execution_output, execution_status, judgment_status,
|
||||
judgment_reason, duration_ms
|
||||
) VALUES (
|
||||
:question_id, :answer_kind, :run_key, :conversation_id,
|
||||
:requested_by, SYSTIMESTAMP, :model_profile, :generated_sql,
|
||||
:answer_text, :result_json, :execution_output, :execution_status,
|
||||
:judgment_status, :judgment_reason, :duration_ms
|
||||
)
|
||||
"""
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
sql,
|
||||
{
|
||||
"question_id": int(question_id),
|
||||
"answer_kind": str(answer_kind)[:20],
|
||||
"run_key": str(run_key)[:100] or None,
|
||||
"conversation_id": str(conversation_id)[:100] or None,
|
||||
"requested_by": str(requested_by)[:100] or None,
|
||||
"model_profile": str(model_profile)[:100] or None,
|
||||
"generated_sql": str(generated_sql or ""),
|
||||
"answer_text": str(answer_text or ""),
|
||||
"result_json": _to_json(result),
|
||||
"execution_output": str(execution_output or ""),
|
||||
"execution_status": str(execution_status)[:40] or None,
|
||||
"judgment_status": str(judgment_status)[:20],
|
||||
"judgment_reason": str(judgment_reason or ""),
|
||||
"duration_ms": duration_ms,
|
||||
},
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
def seed_benchmark(self, benchmark_file: Path) -> tuple[int, int]:
|
||||
questions = load_benchmark_questions(benchmark_file)
|
||||
raw = json.loads(benchmark_file.read_text(encoding="utf-8"))
|
||||
raw_by_code = {
|
||||
str(item.get("case_id") or "").upper(): item
|
||||
for item in raw.get("scenarios", [])
|
||||
if isinstance(item, Mapping)
|
||||
}
|
||||
seeded_questions = 0
|
||||
seeded_answers = 0
|
||||
for question in questions:
|
||||
question_id = self._upsert_benchmark_question(question)
|
||||
seeded_questions += 1
|
||||
raw_item = raw_by_code[question.question_code]
|
||||
history = raw_item.get("historical_answer") if isinstance(raw_item.get("historical_answer"), Mapping) else {}
|
||||
inserted = self._seed_historical_answer(question_id, history, raw)
|
||||
seeded_answers += 1 if inserted else 0
|
||||
return seeded_questions, seeded_answers
|
||||
|
||||
def _upsert_benchmark_question(self, question: QaQuestion) -> int:
|
||||
sql = f"""
|
||||
MERGE INTO {QUESTION_TABLE} target
|
||||
USING (SELECT :question_code AS question_code FROM dual) source
|
||||
ON (target.question_code = source.question_code)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
question_source = 'CUSTOMER_EXCEL',
|
||||
question_hash = :question_hash,
|
||||
category = :category,
|
||||
title = :title,
|
||||
question_text = :question_text,
|
||||
source_document = :source_document,
|
||||
source_sheet = :source_sheet,
|
||||
source_row = :source_row,
|
||||
source_scenario = :source_scenario,
|
||||
sample_sql = :sample_sql,
|
||||
expected_focus = :expected_focus,
|
||||
baseline_sql = :baseline_sql,
|
||||
baseline_answer = :baseline_answer,
|
||||
support_level = :support_level,
|
||||
evaluation_rule_json = :evaluation_rule_json,
|
||||
active_yn = 'Y',
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
question_code, question_source, question_hash, category, title,
|
||||
question_text, source_document, source_sheet, source_row,
|
||||
source_scenario, sample_sql, expected_focus, baseline_sql,
|
||||
baseline_answer, support_level, evaluation_rule_json, active_yn
|
||||
) VALUES (
|
||||
:question_code, 'CUSTOMER_EXCEL', :question_hash, :category,
|
||||
:title, :question_text, :source_document, :source_sheet,
|
||||
:source_row, :source_scenario, :sample_sql, :expected_focus,
|
||||
:baseline_sql, :baseline_answer, :support_level,
|
||||
:evaluation_rule_json, 'Y'
|
||||
)
|
||||
"""
|
||||
binds = {
|
||||
"question_code": question.question_code,
|
||||
"question_hash": question_fingerprint(question.question_text),
|
||||
"category": question.category[:30],
|
||||
"title": question.title[:200],
|
||||
"question_text": question.question_text,
|
||||
"source_document": question.source_document[:255] or None,
|
||||
"source_sheet": question.source_sheet[:255] or None,
|
||||
"source_row": question.source_row,
|
||||
"source_scenario": question.source_scenario,
|
||||
"sample_sql": question.sample_sql,
|
||||
"expected_focus": question.expected_focus,
|
||||
"baseline_sql": question.baseline_sql,
|
||||
"baseline_answer": question.baseline_answer,
|
||||
"support_level": question.support_level[:20],
|
||||
"evaluation_rule_json": json.dumps(question.evaluation_rule, ensure_ascii=False),
|
||||
}
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, binds)
|
||||
connection.commit()
|
||||
cursor.execute(
|
||||
f"SELECT question_id FROM {QUESTION_TABLE} WHERE question_code = :question_code",
|
||||
{"question_code": question.question_code},
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise QaHistoryStoreError(f"질문 마스터를 적재하지 못했습니다: {question.question_code}")
|
||||
return int(row[0])
|
||||
|
||||
def _seed_historical_answer(
|
||||
self,
|
||||
question_id: int,
|
||||
history: Mapping[str, Any],
|
||||
benchmark: Mapping[str, Any],
|
||||
) -> bool:
|
||||
exists_sql = f"""
|
||||
SELECT COUNT(*) FROM {ANSWER_TABLE}
|
||||
WHERE question_id = :question_id AND run_key = :run_key
|
||||
"""
|
||||
with self._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(exists_sql, {"question_id": question_id, "run_key": HISTORICAL_RUN_KEY})
|
||||
if int(cursor.fetchone()[0]) > 0:
|
||||
return False
|
||||
result = {
|
||||
"source_report": str(benchmark.get("source_report") or ""),
|
||||
"source_redmine": benchmark.get("source_redmine"),
|
||||
"historical_execution_output": str(history.get("execution_output") or ""),
|
||||
}
|
||||
self.record_answer(
|
||||
question_id=question_id,
|
||||
answer_kind="HISTORICAL",
|
||||
run_key=HISTORICAL_RUN_KEY,
|
||||
conversation_id="",
|
||||
requested_by="customer-excel-baseline",
|
||||
model_profile=str(history.get("profile") or ""),
|
||||
generated_sql=str(history.get("generated_sql") or ""),
|
||||
answer_text=str(history.get("answer_text") or ""),
|
||||
result=result,
|
||||
execution_output=str(history.get("execution_output") or ""),
|
||||
execution_status=str(history.get("execution_status") or ""),
|
||||
judgment_status=str(history.get("judgment_status") or "REVIEW"),
|
||||
judgment_reason=str(history.get("judgment_reason") or ""),
|
||||
duration_ms=int(history.get("duration_ms") or 0),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def schema_statements() -> tuple[str, ...]:
|
||||
return (
|
||||
f"""
|
||||
CREATE TABLE {QUESTION_TABLE} (
|
||||
question_id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
|
||||
question_code VARCHAR2(30) UNIQUE,
|
||||
question_source VARCHAR2(30) NOT NULL,
|
||||
question_hash VARCHAR2(64) NOT NULL UNIQUE,
|
||||
category VARCHAR2(30) NOT NULL,
|
||||
title VARCHAR2(200) NOT NULL,
|
||||
question_text CLOB NOT NULL,
|
||||
source_document VARCHAR2(255),
|
||||
source_sheet VARCHAR2(255),
|
||||
source_row NUMBER,
|
||||
source_scenario CLOB,
|
||||
sample_sql CLOB,
|
||||
expected_focus CLOB,
|
||||
baseline_sql CLOB,
|
||||
baseline_answer CLOB,
|
||||
support_level VARCHAR2(20) NOT NULL,
|
||||
evaluation_rule_json CLOB CHECK (evaluation_rule_json IS JSON),
|
||||
active_yn CHAR(1) DEFAULT 'Y' NOT NULL CHECK (active_yn IN ('Y', 'N')),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT sg_ai_qa_question_source_ck
|
||||
CHECK (question_source IN ('CUSTOMER_EXCEL', 'FREE_TEXT'))
|
||||
)
|
||||
""",
|
||||
f"""
|
||||
CREATE TABLE {ANSWER_TABLE} (
|
||||
answer_seq NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
question_id NUMBER NOT NULL,
|
||||
answer_kind VARCHAR2(20) NOT NULL,
|
||||
run_key VARCHAR2(100),
|
||||
conversation_id VARCHAR2(100),
|
||||
requested_by VARCHAR2(100),
|
||||
requested_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
model_profile VARCHAR2(100),
|
||||
generated_sql CLOB,
|
||||
answer_text CLOB,
|
||||
result_json CLOB CHECK (result_json IS JSON),
|
||||
execution_output CLOB,
|
||||
execution_status VARCHAR2(40),
|
||||
judgment_status VARCHAR2(20) NOT NULL,
|
||||
judgment_reason CLOB,
|
||||
duration_ms NUMBER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT sg_ai_qa_answer_question_fk
|
||||
FOREIGN KEY (question_id)
|
||||
REFERENCES {QUESTION_TABLE} (question_id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT sg_ai_qa_answer_kind_ck
|
||||
CHECK (answer_kind IN ('HISTORICAL', 'LIVE')),
|
||||
CONSTRAINT sg_ai_qa_answer_judgment_ck
|
||||
CHECK (judgment_status IN ('PASS', 'WARN', 'FAIL', 'REVIEW'))
|
||||
)
|
||||
""",
|
||||
f"""
|
||||
CREATE INDEX sg_ai_qa_answer_question_ix
|
||||
ON {ANSWER_TABLE} (question_id, answer_seq DESC)
|
||||
""",
|
||||
f"""
|
||||
CREATE UNIQUE INDEX sg_ai_qa_answer_run_uk
|
||||
ON {ANSWER_TABLE} (question_id, run_key)
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def ensure_schema(store: QaHistoryStore) -> None:
|
||||
objects = (QUESTION_TABLE, ANSWER_TABLE)
|
||||
with store._connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT table_name FROM user_tables WHERE table_name IN (:q, :a)",
|
||||
{"q": objects[0], "a": objects[1]},
|
||||
)
|
||||
existing = {str(row[0]) for row in cursor}
|
||||
statements = schema_statements()
|
||||
if QUESTION_TABLE not in existing:
|
||||
cursor.execute(statements[0])
|
||||
if ANSWER_TABLE not in existing:
|
||||
cursor.execute(statements[1])
|
||||
cursor.execute(
|
||||
"SELECT index_name FROM user_indexes WHERE index_name IN (:ix1, :ix2)",
|
||||
{"ix1": "SG_AI_QA_ANSWER_QUESTION_IX", "ix2": "SG_AI_QA_ANSWER_RUN_UK"},
|
||||
)
|
||||
indexes = {str(row[0]) for row in cursor}
|
||||
if "SG_AI_QA_ANSWER_QUESTION_IX" not in indexes:
|
||||
cursor.execute(statements[2])
|
||||
if "SG_AI_QA_ANSWER_RUN_UK" not in indexes:
|
||||
cursor.execute(statements[3])
|
||||
connection.commit()
|
||||
|
||||
|
||||
def timestamp_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Smilegate demo modules.
|
||||
|
||||
The portal is assembled from small modules so each feature can be reviewed and
|
||||
released independently.
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
"""Presentation modules for the Smilegate demo."""
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Blank presentation shell.
|
||||
|
||||
No authentication, data access, MCP call, persistence, or customer text belongs
|
||||
in this module. It exists only to prove the minimal Streamlit runtime path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def render_blank_shell(st: Any) -> None:
|
||||
"""Render the intentionally empty first review screen."""
|
||||
st.set_page_config(page_title="Smilegate Demo", layout="wide")
|
||||
st.markdown(
|
||||
"""
|
||||
<style>
|
||||
[data-testid="stHeader"],
|
||||
[data-testid="stToolbar"],
|
||||
#MainMenu,
|
||||
footer { display: none; }
|
||||
[data-testid="stAppViewContainer"],
|
||||
.stApp { background: #ffffff; }
|
||||
.block-container { padding: 0; max-width: none; }
|
||||
</style>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,72 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>HMM | 팀 포트폴리오 선사 실적</title>
|
||||
<style>
|
||||
:root { --hmm:#0065ad; --hmm-deep:#004a84; --ink:#102c43; --copy:#3d5363; --muted:#71818c; --line:#0d78c4; --pale:#edf7fc; --canvas:#f4f5f6; --green:#16815d; --amber:#b66b00; --red:#c5413c; font-family:"Noto Sans KR","Malgun Gothic",Arial,sans-serif; color:var(--ink); }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; min-width:320px; background:var(--canvas); }
|
||||
.page { min-height:100vh; max-width:1480px; margin:0 auto; background:#fff; box-shadow:0 0 40px rgba(12,38,60,.08); }
|
||||
.wordmark { display:flex; align-items:flex-end; gap:9px; font-size:18px; font-weight:900; letter-spacing:-.03em; }
|
||||
.wordmark::before { content:""; width:24px; height:21px; display:inline-block; background:linear-gradient(155deg,transparent 37%,#0571bc 38% 47%,transparent 48%),linear-gradient(26deg,transparent 42%,#0571bc 43% 52%,transparent 53%); border-left:2px solid #0571bc; border-bottom:2px solid #0571bc; }
|
||||
.report-name { margin:24px 0 31px; font-weight:800; font-size:13px; line-height:1.3; }
|
||||
.report-name span { display:block; color:var(--hmm); }
|
||||
.side-tools { padding:12px 0 22px; border-bottom:1px solid #8f969a; color:#056bad; font-size:16px; letter-spacing:6px; }
|
||||
nav { margin-top:22px; } .nav-group { padding:15px 0; border-bottom:1px solid #8f969a; }
|
||||
.nav-group-title { margin:0 0 11px; font-size:12px; font-weight:850; text-transform:uppercase; }
|
||||
.nav-item { display:block; padding:5px 9px; color:#69747b; font-size:11px; line-height:1.3; text-decoration:none; }
|
||||
.nav-item.active { color:#fff; background:linear-gradient(90deg,#0066ae,#52a9d9); font-weight:800; }
|
||||
.side-foot { margin-top:28px; color:#748087; font-size:10px; line-height:1.6; }
|
||||
.content { padding:42px min(6vw,92px) 45px; }
|
||||
.brand-banner { display:flex; align-items:center; justify-content:space-between; gap:18px; padding-bottom:18px; }
|
||||
.brand-banner img { display:block; width:128px; height:auto; }
|
||||
.brand-banner span { color:#627887; font-size:10px; font-weight:750; letter-spacing:.12em; text-transform:uppercase; }
|
||||
.top-line { height:4px; background:#045fa5; margin-bottom:32px; }
|
||||
.report-head { display:flex; justify-content:space-between; align-items:flex-start; gap:28px; padding-bottom:35px; border-bottom:1px solid var(--line); }
|
||||
.kicker { margin:0 0 10px; color:#0070ba; font-size:12px; font-weight:850; letter-spacing:.025em; }
|
||||
h1 { margin:0; color:#075b9f; font-size:clamp(30px,3.25vw,50px); line-height:1.16; letter-spacing:-.07em; font-weight:700; }
|
||||
h1 b { font-weight:850; }
|
||||
.head-meta { min-width:195px; padding-left:22px; border-left:1px solid #96c8e7; color:#647682; font-size:11px; line-height:1.65; }
|
||||
.head-meta strong { display:block; color:#075b9f; font-size:12px; }
|
||||
.intro { padding:18px 0 23px; color:var(--copy); font-size:15px; line-height:1.75; border-bottom:1px solid #8ac5e8; }
|
||||
.section-heading { margin:0 0 16px; color:#005fa9; font-size:21px; letter-spacing:-.05em; }
|
||||
.section-heading small { margin-left:8px; color:#71818c; font-size:11px; letter-spacing:0; font-weight:500; }
|
||||
.executive { display:grid; grid-template-columns:1.45fr 1fr; gap:42px; padding:29px 0 30px; border-bottom:1px solid var(--line); }
|
||||
.metrics { display:grid; grid-template-columns:repeat(2,1fr); border-top:2px solid #1478bb; border-left:1px solid #c8dce9; }
|
||||
.metric { min-height:111px; padding:17px 18px; border-right:1px solid #c8dce9; border-bottom:1px solid #c8dce9; }
|
||||
.metric-label { color:#5b6f7e; font-size:11px; font-weight:700; }.metric-value { margin:8px 0 5px; color:#075b9f; font-size:27px; letter-spacing:-.055em; font-weight:850; font-variant-numeric:tabular-nums; }.metric-value.money { font-size:23px; }.metric-note { color:#75848c; font-size:10px; }
|
||||
.risk-panel { padding:2px 0 0 25px; border-left:1px dotted #1478bb; }.risk-panel h2 { margin:0 0 15px; color:#005fa9; font-size:20px; letter-spacing:-.05em; }
|
||||
.risk-message { margin:0 0 17px; color:var(--copy); font-size:12px; line-height:1.65; }.risk-list { display:grid; gap:9px; }.risk-item { display:flex; align-items:center; gap:10px; padding-bottom:8px; border-bottom:1px solid #d7e8f3; font-size:11px; }.risk-item:last-child { border-bottom:0; }.risk-item-name { flex:1; font-weight:750; }.pill { padding:3px 7px; border-radius:2px; font-size:10px; font-weight:850; }.pill.red { color:var(--red); background:#fff1f0; }.pill.amber { color:var(--amber); background:#fff6e7; }.pill.green { color:var(--green); background:#eaf7f0; }
|
||||
.portfolio { padding:31px 0 28px; border-bottom:1px solid var(--line); }.portfolio-grid { display:grid; grid-template-columns:minmax(0,1.65fr) minmax(260px,.8fr); gap:43px; }.chart-note { margin:-8px 0 22px; color:#73838e; font-size:11px; }.bar-chart { display:grid; gap:12px; }.bar-row { display:grid; grid-template-columns:146px minmax(100px,1fr) 112px; align-items:center; gap:12px; }.bar-person { font-size:11px; }.bar-person b { display:block; color:#134f7f; font-size:12px; }.bar-person span { color:#788791; }.bar-lane { height:24px; background:#ebf2f6; overflow:hidden; }.bar { height:100%; min-width:4px; background:linear-gradient(90deg,#0073bb,#1aa2d8); position:relative; }.bar.critical { background:linear-gradient(90deg,#0073bb 0 84%,#d75349 84%); }.bar-value { color:#15486f; text-align:right; font-size:11px; font-weight:800; font-variant-numeric:tabular-nums; }.bar-value span { display:block; color:#72828c; font-size:9px; font-weight:500; }
|
||||
.chart-aside { padding:14px 0 0 22px; border-left:1px dotted #1478bb; }.chart-aside h3 { margin:0 0 14px; color:#005fa9; font-size:16px; }.legend { display:flex; justify-content:space-between; align-items:end; padding:10px 0; border-bottom:1px solid #d5e4ed; }.legend:last-child { border-bottom:0; }.legend-label { display:flex; align-items:center; gap:8px; font-size:11px; }.dot { width:10px; height:10px; border-radius:50%; }.dot.green { background:var(--green); }.dot.amber { background:var(--amber); }.dot.red { background:var(--red); }.legend strong { color:#075b9f; font-size:20px; }
|
||||
.detail { padding:31px 0 28px; }.detail-head { display:flex; justify-content:space-between; align-items:end; gap:16px; }.detail-head p { margin:0 0 16px; color:#75848c; font-size:11px; }.detail-table { width:100%; border-collapse:collapse; border-top:2px solid #1478bb; }.detail-table th { padding:11px 10px; background:#eff8fd; color:#176aa4; text-align:left; font-size:10px; font-weight:800; }.detail-table td { padding:12px 10px; border-bottom:1px solid #d4e3eb; color:#334f62; font-size:11px; }.detail-table .code { display:block; margin-bottom:2px; color:#75848c; font-size:9px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }.detail-table .name { font-weight:750; }.number { text-align:right; font-variant-numeric:tabular-nums; }.negative { color:var(--red)!important; }.empty { padding:35px; color:#77868f; text-align:center; border:1px solid #d5e4ed; }
|
||||
.footnotes { display:grid; grid-template-columns:1.4fr 1fr; gap:25px; padding-top:19px; border-top:1px solid var(--line); color:#697d89; font-size:10px; line-height:1.7; }.footnotes h3 { margin:0 0 5px; color:#075b9f; font-size:11px; }.footnotes p,.footnotes ul { margin:0; padding-left:15px; }.footnotes p { padding-left:0; }
|
||||
@media(max-width:1000px){.content{padding:32px 35px 42px}.executive,.portfolio-grid{gap:25px}.bar-row{grid-template-columns:118px minmax(80px,1fr) 95px}}
|
||||
@media(max-width:760px){.content{padding:24px 18px 35px}.top-line{margin-bottom:20px}.report-head,.executive,.portfolio-grid,.footnotes{grid-template-columns:1fr;display:grid}.report-head{gap:17px;padding-bottom:24px}.head-meta{padding-left:0;border-left:0;border-top:1px solid #96c8e7;padding-top:10px}.intro{font-size:13px}.risk-panel,.chart-aside{padding:24px 0 0;border-left:0;border-top:1px dotted #1478bb}.bar-row{grid-template-columns:105px minmax(30px,1fr) 80px;gap:7px}.detail{overflow-x:auto}.detail-table{min-width:690px}.footnotes{gap:14px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="report-root" class="page" aria-live="polite"></main>
|
||||
<script>
|
||||
// 서버가 이미 권한 검사를 마친 조회 결과 JSON을 이 위치에 주입한다.
|
||||
const reportData = __REPORT_DATA__;
|
||||
const esc=(v)=>String(v??"").replace(/[&<>'"]/g,c=>({"&":"&","<":"<",">":">","'":"'","\"":"""}[c]));
|
||||
const n=(v,d=2)=>new Intl.NumberFormat("en-US",{maximumFractionDigits:d}).format(Number(v||0));
|
||||
const usd=(v)=>`${n(v)} USD`; const pct=(v)=>`${n(v)}%`; const when=(v)=>new Intl.DateTimeFormat("ko-KR",{dateStyle:"medium",timeStyle:"short",timeZone:"Asia/Seoul"}).format(new Date(v));
|
||||
const riskClass=(v)=>String(v||"").toLowerCase(); const riskLabel=(v)=>({GREEN:"정상",AMBER:"주의",RED:"위험"}[v]||v||"미분류");
|
||||
function aggregate(rows){ const risks={GREEN:0,AMBER:0,RED:0}; const people=new Map(); rows.forEach(row=>{const revenue=Number(row.latestRevenueUsd||0); const margin=Number(row.latestGrossMarginUsd||0); risks[row.latestRiskLevel]=(risks[row.latestRiskLevel]||0)+1; if(!people.has(row.employeeCode)) people.set(row.employeeCode,{code:row.employeeCode,name:row.employeeName,revenue:0,carriers:0,hasRisk:false}); const p=people.get(row.employeeCode);p.revenue+=revenue;p.carriers+=1;p.hasRisk||=row.latestRiskLevel==="RED"||row.latestRiskLevel==="AMBER";}); const totalRevenue=rows.reduce((s,r)=>s+Number(r.latestRevenueUsd||0),0); const totalMargin=rows.reduce((s,r)=>s+Number(r.latestGrossMarginUsd||0),0); return {risks,people:[...people.values()].sort((a,b)=>b.revenue-a.revenue),totalRevenue,totalMargin,avgReliability:rows.length?rows.reduce((s,r)=>s+Number(r.latestScheduleReliabilityPct||0),0)/rows.length:0}; }
|
||||
function badge(value){return `<span class="pill ${riskClass(value)}">${esc(riskLabel(value))}</span>`;}
|
||||
function detailRow(row){return `<tr><td><span class="code">${esc(row.employeeCode)}</span><span class="name">${esc(row.employeeName)}</span></td><td><span class="code">${esc(row.carrierCode)}</span><span class="name">${esc(row.carrierName)}</span></td><td class="number">${usd(row.latestRevenueUsd)}</td><td class="number ${Number(row.latestGrossMarginUsd)<0?"negative":""}">${usd(row.latestGrossMarginUsd)}</td><td class="number">${pct(row.latestScheduleReliabilityPct)}</td><td>${badge(row.latestRiskLevel)}</td></tr>`;}
|
||||
function renderHmmCarrierPerformanceReport(payload,target=document.getElementById("report-root")){const report=payload?.report||{};const rows=Array.isArray(payload?.rows)?payload.rows:[];const s=aggregate(rows);const max=Math.max(...s.people.map(p=>p.revenue),1);const watch=rows.filter(r=>r.latestRiskLevel!=="GREEN").sort((a,b)=>a.latestRiskLevel.localeCompare(b.latestRiskLevel));target.innerHTML=`
|
||||
<section class="content"><div class="brand-banner"><img src="https://eu-images.contentstack.com/v3/assets/bltdcfe6aab5515629e/bltaf50776e73f3149f/668ea7b97dc26754645e1830/hmmci.png?width=1400&auto=webp&quality=80&disable=upscale" alt="HMM"><span>HMM Management Report · Internal Demo</span></div><div class="top-line"></div><header class="report-head"><div><p class="kicker">${esc(report.id||"FEDERATION")} · ${esc(report.category||"HMM Business Intelligence")}</p><h1><b>${esc(report.title||"팀 포트폴리오")}</b> — 최신 선사 실적</h1></div><div class="head-meta"><strong>${esc(report.requestedBy||"-")} 팀장 조회</strong>생성 ${esc(when(report.generatedAt))}<br>MCP ${esc(report.execution?.calls||0)}회 호출<br>${esc(report.execution?.tool||"-")}</div></header>
|
||||
<p class="intro">${esc(report.answer||report.question||"조회된 선사 실적입니다.")} 전체 ${n(rows.length,0)}개 담당 선사의 매출·수익성·운항 지표를 담당자별 포트폴리오 관점에서 요약했습니다.</p>
|
||||
<section id="summary" class="executive"><div><h2 class="section-heading">핵심 요약 <small>Latest performance snapshot</small></h2><div class="metrics"><article class="metric"><div class="metric-label">담당 선사</div><div class="metric-value">${n(rows.length,0)}<small>개</small></div><div class="metric-note">팀원 ${n(s.people.length,0)}명 기준</div></article><article class="metric"><div class="metric-label">최신 매출 합계</div><div class="metric-value money">${usd(s.totalRevenue)}</div><div class="metric-note">담당 선사별 최신 기준월 합산</div></article><article class="metric"><div class="metric-label">매출총이익 합계</div><div class="metric-value money ${s.totalMargin<0?"negative":""}">${usd(s.totalMargin)}</div><div class="metric-note">음수 마진 선사 포함</div></article><article class="metric"><div class="metric-label">평균 정시 운항률</div><div class="metric-value">${pct(s.avgReliability)}</div><div class="metric-note">담당 선사 단순 평균</div></article></div></div><aside id="risk" class="risk-panel"><h2>위험 신호</h2><p class="risk-message">주의·위험 등급 선사 ${n(s.risks.AMBER+s.risks.RED,0)}개를 우선 점검 대상으로 표시합니다.</p><div class="risk-list">${watch.length?watch.map(r=>`<div class="risk-item">${badge(r.latestRiskLevel)}<span class="risk-item-name">${esc(r.carrierName)}</span><span>${esc(r.employeeName)}</span></div>`).join(""):'<div class="risk-item">현재 주의·위험 선사가 없습니다.</div>'}</div></aside></section>
|
||||
<section id="portfolio" class="portfolio"><h2 class="section-heading">담당자별 포트폴리오 매출 <small>Latest revenue by employee</small></h2><div class="portfolio-grid"><div><p class="chart-note">각 막대는 담당 선사 최신 매출의 합계입니다. 막대 끝의 색상은 해당 담당자 포트폴리오에 주의·위험 선사가 있는 경우를 나타냅니다.</p><div class="bar-chart">${s.people.map(p=>`<div class="bar-row"><div class="bar-person"><b>${esc(p.name)}</b><span>${esc(p.code)} · ${n(p.carriers,0)}개 선사</span></div><div class="bar-lane"><div class="bar ${p.hasRisk?"critical":""}" style="width:${(p.revenue/max*100).toFixed(2)}%"></div></div><div class="bar-value">${usd(p.revenue)}<span>팀 매출 ${(p.revenue/s.totalRevenue*100).toFixed(1)}%</span></div></div>`).join("")||'<div class="empty">차트 데이터가 없습니다.</div>'}</div></div><aside class="chart-aside"><h3>위험 등급 분포</h3><div class="legend"><span class="legend-label"><i class="dot green"></i>정상</span><strong>${n(s.risks.GREEN,0)}</strong></div><div class="legend"><span class="legend-label"><i class="dot amber"></i>주의</span><strong>${n(s.risks.AMBER,0)}</strong></div><div class="legend"><span class="legend-label"><i class="dot red"></i>위험</span><strong>${n(s.risks.RED,0)}</strong></div></aside></div></section>
|
||||
<section id="detail" class="detail"><div class="detail-head"><h2 class="section-heading">선사별 최신 지표 <small>Carrier detail</small></h2><p>총 ${n(rows.length,0)}건 · 최신순 1~${n(rows.length,0)}건 표시</p></div>${rows.length?`<table class="detail-table"><thead><tr><th>담당자</th><th>선사</th><th class="number">최신 매출</th><th class="number">매출총이익</th><th class="number">정시 운항률</th><th>위험 등급</th></tr></thead><tbody>${rows.map(detailRow).join("")}</tbody></table>`:'<div class="empty">표시할 상세 데이터가 없습니다.</div>'}</section>
|
||||
<footer id="notes" class="footnotes"><section><h3>답변 근거</h3><ul>${(report.evidence||[]).map(e=>`<li>${esc(e)}</li>`).join("")||'<li>제공된 근거가 없습니다.</li>'}</ul></section><section><h3>제약 및 주의</h3><p>${esc(report.limitation||"제약 정보가 제공되지 않았습니다.")}</p></section></footer></section>`;}
|
||||
window.renderHmmCarrierPerformanceReport=renderHmmCarrierPerformanceReport;renderHmmCarrierPerformanceReport(reportData);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"version": 1,
|
||||
"product": {
|
||||
"name": "AI 업무 에이전트",
|
||||
"short_name": "AGENT",
|
||||
"page_title": "AI 업무 에이전트",
|
||||
"name": "SMILEGATE DATA & AI POC",
|
||||
"short_name": "SMILEGATE",
|
||||
"page_title": "SMILEGATE DATA & AI POC",
|
||||
"page_icon": "🤖",
|
||||
"header_title": "AI 업무 에이전트",
|
||||
"header_description": "사용자 권한에 맞는 업무 질의와 보안 관리 기능을 제공합니다.",
|
||||
"login_kicker": "DATA & AI DEMO",
|
||||
"login_title": "AI 업무 에이전트",
|
||||
"login_description": "사용자 인증 후 업무 질의와 보안 관리 기능을 이용할 수 있습니다.",
|
||||
"login_footer": "인증된 DEMO 사용자만 접근할 수 있습니다."
|
||||
"header_title": "스마일게이트 게임 데이터 AI 에이전트",
|
||||
"header_description": "게임 로그·서비스 데이터를 기반으로 AI 업무 효율화와 데이터 플랫폼 활용 방식을 검증합니다.",
|
||||
"login_kicker": "SMILEGATE DATA & AI POC",
|
||||
"login_title": "스마일게이트 게임 데이터 AI 에이전트",
|
||||
"login_description": "사용자 인증 후 게임 데이터 AI 질의와 보안 관리 기능을 이용할 수 있습니다.",
|
||||
"login_footer": "승인된 Data & AI PoC 사용자만 접근할 수 있습니다."
|
||||
},
|
||||
"theme": {
|
||||
"primary_color": "#003b70",
|
||||
"text_color": "#172b3a",
|
||||
"muted_color": "#667785",
|
||||
"border_color": "#dfe7ed"
|
||||
"primary_color": "#113F67",
|
||||
"text_color": "#15283B",
|
||||
"muted_color": "#5D6C7C",
|
||||
"border_color": "#D7E0E8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,37 +40,23 @@
|
||||
{
|
||||
"id": "FED-01",
|
||||
"enabled": true,
|
||||
"category": "선사 실적",
|
||||
"title": "우리 팀 담당 선사 최신 실적",
|
||||
"question": "우리 팀원별 담당 선사와 각 선사의 최신 매출, 매출총이익, 정시 운항률, 위험 등급을 보여줘"
|
||||
"category": "선사 실적 Federation",
|
||||
"title": "팀원별 담당 선사 최신 실적",
|
||||
"question": "E1001 팀장의 팀원별 담당 선사와 해당 선사의 최신 매출, 매출총이익, 정시 운항률, 위험 등급을 보여줘"
|
||||
},
|
||||
{
|
||||
"id": "FED-02",
|
||||
"enabled": true,
|
||||
"category": "선사 실적",
|
||||
"title": "주의가 필요한 선사",
|
||||
"question": "우리 팀이 담당하는 선사 중 최신 위험 등급이 RED인 선사의 담당자, 선사명, 매출, 정시 운항률과 클레임 발생률을 보여줘"
|
||||
"category": "선사 실적 Federation",
|
||||
"title": "위험 선사와 담당자",
|
||||
"question": "E1001 팀에서 최신 위험 등급이 RED인 선사를 담당하는 직원, 선사명, 매출, 정시 운항률과 클레임 발생률을 보여줘"
|
||||
},
|
||||
{
|
||||
"id": "FED-03",
|
||||
"enabled": true,
|
||||
"category": "선사 실적",
|
||||
"title": "내 담당 선사 월별 추이",
|
||||
"question": "내가 담당하는 선사의 월별 매출, 운송 물동량, 매출총이익과 위험 등급을 기준월 순서로 보여줘"
|
||||
},
|
||||
{
|
||||
"id": "FED-04",
|
||||
"enabled": true,
|
||||
"category": "선사 실적 리포트",
|
||||
"title": "우리 팀 선사 실적 리포트",
|
||||
"question": "우리 팀원별 담당 선사의 최신 매출, 매출총이익, 정시 운항률과 위험 등급을 HMM 리포트로 보여줘"
|
||||
},
|
||||
{
|
||||
"id": "FED-05",
|
||||
"enabled": true,
|
||||
"category": "선사 실적 리포트",
|
||||
"title": "내 담당 선사 실적 리포트",
|
||||
"question": "내 담당 선사의 최신 매출, 매출총이익, 정시 운항률과 위험 등급을 HMM 리포트로 보여줘"
|
||||
"category": "선사 실적 Federation",
|
||||
"title": "담당 선사 월별 추이",
|
||||
"question": "E1006 직원이 담당하는 선사의 월별 매출, 운송 물동량, 매출총이익과 위험 등급을 기준월 순서로 보여줘"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,24 +1,58 @@
|
||||
{
|
||||
"default_server_id": "hmm_hr_mcp",
|
||||
"default_server_id": "smilegate_game_data_mcp",
|
||||
"servers": [
|
||||
{
|
||||
"id": "hmm_hr_mcp",
|
||||
"id": "smilegate_game_data_mcp",
|
||||
"enabled": true,
|
||||
"provider": "hmm_compat_mcp",
|
||||
"provider": "smilegate_select_ai_mcp",
|
||||
"transport": "http",
|
||||
"endpoint_url": "https://hmm-backoffice.cloud-handson.com/mcp",
|
||||
"auth_token_env": "HMM_MCP_BEARER_TOKEN",
|
||||
"timeout_seconds_env": "AI_WEB_AGENT_CONSOLE_MCP_TIMEOUT_SECONDS",
|
||||
"default_tool": "search_hr_data",
|
||||
"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_fewshot_nl2sql",
|
||||
"router_model_profile": "gpt54_mini_oci",
|
||||
"tool_allowlist": [
|
||||
"search_hr_data",
|
||||
"resolve_hr_term",
|
||||
"search_hr_policy",
|
||||
"search_carrier_performance",
|
||||
"render_hmm_carrier_report"
|
||||
"oracle.select_ai.fewshot_preflight",
|
||||
"oracle.select_ai.game_query_plan",
|
||||
"oracle.select_ai.game_daily_au_lookup",
|
||||
"oracle.select_ai.smilegate_fewshot_nl2sql",
|
||||
"oracle.select_ai.smilegate_game_text2sql",
|
||||
"oracle.select_ai.qa_vector_search",
|
||||
"oracle.select_ai.qa_vector_store"
|
||||
],
|
||||
"description": "HMM HR knowledge, ADB employee assignment, and RDS carrier performance MCP server"
|
||||
"tool_workflow": [
|
||||
{
|
||||
"tool": "oracle.select_ai.fewshot_preflight",
|
||||
"prelude": true
|
||||
},
|
||||
{
|
||||
"tool": "oracle.select_ai.game_query_plan",
|
||||
"prelude": true
|
||||
},
|
||||
{
|
||||
"tool": "oracle.select_ai.smilegate_fewshot_nl2sql",
|
||||
"arguments_from": [
|
||||
{
|
||||
"argument": "fewShotPreflight",
|
||||
"tool": "oracle.select_ai.fewshot_preflight"
|
||||
},
|
||||
{
|
||||
"argument": "queryPlan",
|
||||
"tool": "oracle.select_ai.game_query_plan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tool": "oracle.select_ai.game_daily_au_lookup",
|
||||
"arguments_from": [
|
||||
{
|
||||
"argument": "queryPlan",
|
||||
"tool": "oracle.select_ai.game_query_plan"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": "Smilegate game-data Text2SQL MCP server"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
41
ai-web-agent-console/config/smilegate_demo_scenarios.json
Normal file
41
ai-web-agent-console/config/smilegate_demo_scenarios.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": 1,
|
||||
"description": "Smilegate Data & AI PoC 화면에 표시할 게임 데이터 질의 샘플입니다.",
|
||||
"scenarios": [
|
||||
{
|
||||
"id": "GAME-01",
|
||||
"enabled": true,
|
||||
"category": "활성 사용자",
|
||||
"title": "카제나 최신 AU",
|
||||
"question": "카제나 최신 기준 활성 사용자 수(AU)를 알려줘"
|
||||
},
|
||||
{
|
||||
"id": "GAME-02",
|
||||
"enabled": true,
|
||||
"category": "매출",
|
||||
"title": "게임별 판매 현황",
|
||||
"question": "최신 기준 게임별 판매 건수와 판매 금액을 보여줘"
|
||||
},
|
||||
{
|
||||
"id": "GAME-03",
|
||||
"enabled": true,
|
||||
"category": "환불",
|
||||
"title": "최근 환불 현황",
|
||||
"question": "최신 기준 게임별 환불 건수와 환불 금액을 보여줘"
|
||||
},
|
||||
{
|
||||
"id": "GAME-04",
|
||||
"enabled": true,
|
||||
"category": "게임·서버",
|
||||
"title": "게임 서버 구성",
|
||||
"question": "등록된 게임과 게임 서버 정보를 보여줘"
|
||||
},
|
||||
{
|
||||
"id": "GAME-05",
|
||||
"enabled": true,
|
||||
"category": "사용자 분석",
|
||||
"title": "신규 사용자 현황",
|
||||
"question": "최신 월 기준 게임별 신규 사용자 수를 보여줘"
|
||||
}
|
||||
]
|
||||
}
|
||||
1832
ai-web-agent-console/config/smilegate_qa_benchmark.json
Normal file
1832
ai-web-agent-console/config/smilegate_qa_benchmark.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,52 +1,24 @@
|
||||
{
|
||||
"version": 2,
|
||||
"description": "HMM HR 데모 사용자 선택 목록입니다. 파일명은 기존 배포 호환성을 위해 유지합니다. token 원문은 저장하지 않고 mcp_token_env의 서버 환경변수만 참조합니다.",
|
||||
"presets": [
|
||||
{
|
||||
"enabled": true,
|
||||
"default": true,
|
||||
"mcp_token_env": "HMM_MCP_BEARER_TOKEN_E1001",
|
||||
"user_id": "E1001",
|
||||
"name": "Kim Minseo",
|
||||
"role": "HR Team Manager",
|
||||
"team": "HMM HR Demo Team",
|
||||
"scope": "팀원 6명의 휴가·근태 현황을 확인하는 관리자 데모"
|
||||
"user_id": "1001",
|
||||
"name": "Data & AI TF 팀장",
|
||||
"role": "DATA_AI_POC_ADMIN",
|
||||
"channel": "DATA_AI_TF",
|
||||
"scope": "SGMP_POC 게임 데이터 전체",
|
||||
"token_env": "SMILEGATE_TEAMLEAD_BEARER_TOKEN"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"mcp_token_env": "HMM_MCP_BEARER_TOKEN_E1002",
|
||||
"user_id": "E1002",
|
||||
"name": "Lee Jiwon",
|
||||
"role": "HR Operations Specialist",
|
||||
"team": "HMM HR Demo Team",
|
||||
"scope": "본인 휴가 잔여·신청·근태를 확인하는 팀원 데모"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"mcp_token_env": "HMM_MCP_BEARER_TOKEN_E1003",
|
||||
"user_id": "E1003",
|
||||
"name": "Park Dohyun",
|
||||
"role": "People Analytics Analyst",
|
||||
"team": "HMM HR Demo Team",
|
||||
"scope": "본인 휴가·근태와 팀 인력 현황을 확인하는 분석 담당 데모"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"mcp_token_env": "HMM_MCP_BEARER_TOKEN_E1005",
|
||||
"user_id": "E1005",
|
||||
"name": "Han Seojun",
|
||||
"role": "Recruiting Specialist",
|
||||
"team": "HMM HR Demo Team",
|
||||
"scope": "대기 중인 2일 연차 신청을 확인하는 팀원 데모"
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"mcp_token_env": "HMM_MCP_BEARER_TOKEN_E1007",
|
||||
"user_id": "E1007",
|
||||
"name": "Kang Minho",
|
||||
"role": "HR Coordinator",
|
||||
"team": "HMM HR Demo Team",
|
||||
"scope": "대기 중인 1일 연차 신청과 휴가 근태를 확인하는 팀원 데모"
|
||||
"default": false,
|
||||
"user_id": "1002",
|
||||
"name": "Data & AI TF 팀원",
|
||||
"role": "DATA_AI_POC_ADMIN",
|
||||
"channel": "DATA_AI_TF",
|
||||
"scope": "SGMP_POC 게임 데이터 전체",
|
||||
"token_env": "SMILEGATE_TEAMMEMBER_BEARER_TOKEN"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
128
ai-web-agent-console/portal_auth_gateway.py
Normal file
128
ai-web-agent-console/portal_auth_gateway.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Small same-origin authentication gateway for the Smilegate Streamlit portal.
|
||||
|
||||
The gateway issues a signed HttpOnly cookie after validating the configured
|
||||
PBKDF2 password. The Streamlit application verifies the signature and expiry
|
||||
from the incoming request, so browser refreshes and WebSocket reconnects do not
|
||||
require a new login.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
|
||||
COOKIE_NAME = "poc4_portal_auth"
|
||||
MAX_BODY_BYTES = 8_192
|
||||
COOKIE_TTL_SECONDS = int(os.environ.get("POC4_LOGIN_COOKIE_TTL_SECONDS", "43200"))
|
||||
|
||||
|
||||
def _password_matches(password: str, encoded_password: str) -> bool:
|
||||
try:
|
||||
scheme, iterations_text, salt_hex, expected_hex = encoded_password.split("$", 3)
|
||||
iterations = int(iterations_text)
|
||||
salt = bytes.fromhex(salt_hex)
|
||||
expected = bytes.fromhex(expected_hex)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if scheme != "pbkdf2_sha256" or not 100_000 <= iterations <= 2_000_000:
|
||||
return False
|
||||
candidate = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
|
||||
return hmac.compare_digest(candidate, expected)
|
||||
|
||||
|
||||
def _cookie_value(username: str) -> str:
|
||||
secret = os.environ["POC4_LOGIN_REMEMBER_SECRET"]
|
||||
claims = {"v": 1, "u": username, "e": int(time.time()) + COOKIE_TTL_SECONDS}
|
||||
encoded = base64.urlsafe_b64encode(
|
||||
json.dumps(claims, 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 _set_cookie(handler: BaseHTTPRequestHandler, value: str, max_age: int) -> None:
|
||||
attributes = [
|
||||
f"{COOKIE_NAME}={value}",
|
||||
"Path=/",
|
||||
f"Max-Age={max_age}",
|
||||
"HttpOnly",
|
||||
"Secure",
|
||||
"SameSite=Lax",
|
||||
]
|
||||
handler.send_header("Set-Cookie", "; ".join(attributes))
|
||||
|
||||
|
||||
class PortalAuthHandler(BaseHTTPRequestHandler):
|
||||
server_version = "SmilegatePortalAuth/1.0"
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
# Do not log form data or authentication details.
|
||||
return
|
||||
|
||||
def _redirect(self, location: str, cookie_value: str | None = None, max_age: int = 0) -> None:
|
||||
self.send_response(HTTPStatus.SEE_OTHER)
|
||||
if cookie_value is not None:
|
||||
_set_cookie(self, cookie_value, max_age)
|
||||
self.send_header("Location", location)
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
if self.path == "/health":
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"ok\n")
|
||||
return
|
||||
if self.path == "/logout":
|
||||
self._redirect("/", "", 0)
|
||||
return
|
||||
self.send_error(HTTPStatus.NOT_FOUND)
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
if self.path != "/login":
|
||||
self.send_error(HTTPStatus.NOT_FOUND)
|
||||
return
|
||||
try:
|
||||
content_length = int(self.headers.get("Content-Length", "0"))
|
||||
except ValueError:
|
||||
content_length = 0
|
||||
if content_length <= 0 or content_length > MAX_BODY_BYTES:
|
||||
self._redirect("/?login=failed")
|
||||
return
|
||||
form = parse_qs(self.rfile.read(content_length).decode("utf-8"), keep_blank_values=True)
|
||||
username = form.get("username", [""])[0].strip()
|
||||
password = form.get("password", [""])[0]
|
||||
expected_username = os.environ.get("POC4_LOGIN_USER", "").strip()
|
||||
encoded_password = os.environ.get("POC4_LOGIN_PASSWORD_PBKDF2", "").strip()
|
||||
if (
|
||||
expected_username
|
||||
and hmac.compare_digest(username, expected_username)
|
||||
and _password_matches(password, encoded_password)
|
||||
):
|
||||
self._redirect("/", _cookie_value(username), COOKIE_TTL_SECONDS)
|
||||
return
|
||||
self._redirect("/?login=failed")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
address = os.environ.get("POC4_AUTH_BIND", "127.0.0.1")
|
||||
port = int(os.environ.get("POC4_AUTH_PORT", "8623"))
|
||||
required = ("POC4_LOGIN_USER", "POC4_LOGIN_PASSWORD_PBKDF2", "POC4_LOGIN_REMEMBER_SECRET")
|
||||
missing = [name for name in required if not os.environ.get(name, "").strip()]
|
||||
if missing:
|
||||
raise RuntimeError("missing required portal auth configuration")
|
||||
ThreadingHTTPServer((address, port), PortalAuthHandler).serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
47
ai-web-agent-console/scripts/sync_smilegate_qa_history.py
Normal file
47
ai-web-agent-console/scripts/sync_smilegate_qa_history.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create and seed the Smilegate customer QA benchmark history tables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.poc4.qa_history_store import QaHistoryStore, ensure_schema
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--benchmark",
|
||||
type=Path,
|
||||
default=ROOT / "config" / "smilegate_qa_benchmark.json",
|
||||
help="Customer Excel benchmark JSON generated from the approved QA report.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env-file",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Optional environment file containing the QA DB connection settings.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if not args.benchmark.is_file():
|
||||
raise SystemExit(f"Benchmark file not found: {args.benchmark}")
|
||||
store = QaHistoryStore(env_file=args.env_file)
|
||||
ensure_schema(store)
|
||||
question_count, historical_insert_count = store.seed_benchmark(args.benchmark)
|
||||
print(
|
||||
"qa_history_sync"
|
||||
f" questions={question_count}"
|
||||
f" historical_answers_inserted={historical_insert_count}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
20
ai-web-agent-console/smilegate_demo.py
Normal file
20
ai-web-agent-console/smilegate_demo.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Minimal Smilegate Streamlit demo entrypoint.
|
||||
|
||||
This entrypoint intentionally wires only the blank presentation shell. Feature
|
||||
modules such as authentication, MCP querying, and history are added separately
|
||||
after each review.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from ai_web_agent_console.smilegate_demo.ui.shell import render_blank_shell
|
||||
|
||||
|
||||
def main() -> None:
|
||||
render_blank_shell(st)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
51
ai-web-agent-console/tests/test_oci_genai_settings.py
Normal file
51
ai-web-agent-console/tests/test_oci_genai_settings.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""OCI GenAI configuration validation tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from src.oci_genai_sdk import ALLOWED_OCI_SETTINGS, load_oci_settings
|
||||
|
||||
|
||||
class OCISettingsTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._previous = {key: os.environ.get(key) for key in ALLOWED_OCI_SETTINGS}
|
||||
os.environ.update(
|
||||
{
|
||||
"OCI_AUTH_TYPE": "config_file",
|
||||
"OCI_CONFIG_FILE": "/home/opc/.oci/config",
|
||||
"OCI_PROFILE": "DEFAULT",
|
||||
}
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
for key, value in self._previous.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
def test_accepts_a_child_compartment_ocid(self) -> None:
|
||||
os.environ["OCI_GENAI_COMPARTMENT_ID"] = "ocid1.compartment.oc1..example"
|
||||
|
||||
settings = load_oci_settings()
|
||||
|
||||
self.assertEqual("ocid1.compartment.oc1..example", settings.compartment_id)
|
||||
|
||||
def test_accepts_a_tenancy_ocid_for_the_root_compartment(self) -> None:
|
||||
os.environ["OCI_GENAI_COMPARTMENT_ID"] = "ocid1.tenancy.oc1..example"
|
||||
|
||||
settings = load_oci_settings()
|
||||
|
||||
self.assertEqual("ocid1.tenancy.oc1..example", settings.compartment_id)
|
||||
|
||||
def test_rejects_an_invalid_compartment_identifier(self) -> None:
|
||||
os.environ["OCI_GENAI_COMPARTMENT_ID"] = "not-an-ocid"
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "compartment is not configured"):
|
||||
load_oci_settings()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
80
ai-web-agent-console/tests/test_qa_history.py
Normal file
80
ai-web-agent-console/tests/test_qa_history.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from src.poc4.qa_history import evaluate_sql, load_benchmark_questions
|
||||
from src.poc4.qa_history_store import _normalize_oracle_dsn, schema_statements
|
||||
|
||||
|
||||
class QaHistoryTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
benchmark = Path(__file__).parents[1] / "config" / "smilegate_qa_benchmark.json"
|
||||
cls.questions = {item.question_code: item for item in load_benchmark_questions(benchmark)}
|
||||
|
||||
def test_customer_excel_benchmark_contains_all_47_cases(self) -> None:
|
||||
self.assertEqual(47, len(self.questions))
|
||||
self.assertIn("STD-01", self.questions)
|
||||
self.assertIn("CZN-19", self.questions)
|
||||
|
||||
def test_supported_query_passes_when_required_terms_are_present(self) -> None:
|
||||
judgment = evaluate_sql(
|
||||
self.questions["STD-13"],
|
||||
"SELECT SUM(PAYMT_AMT) FROM COMN_SALES_TXN",
|
||||
execution_succeeded=True,
|
||||
)
|
||||
self.assertEqual("PASS", judgment.status)
|
||||
|
||||
def test_monthly_au_with_au_flag_fails(self) -> None:
|
||||
judgment = evaluate_sql(
|
||||
self.questions["STD-27"],
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM CZN_COMN_USER_MST
|
||||
WHERE AU_FLAG = 1
|
||||
AND BASE_DT = (SELECT MAX(BASE_DT) FROM CZN_COMN_USER_MST)
|
||||
AND LAST_CONN_DT >= ADD_MONTHS(BASE_DT, -1)
|
||||
AND STD_USER_YN = 'Y'
|
||||
AND EXPT_USER_YN = 'N'
|
||||
""",
|
||||
execution_succeeded=True,
|
||||
)
|
||||
self.assertEqual("FAIL", judgment.status)
|
||||
self.assertIn("AU_FLAG", judgment.reason)
|
||||
|
||||
def test_unsupported_game_requires_safe_alias_lookup(self) -> None:
|
||||
safe = evaluate_sql(
|
||||
self.questions["STD-02"],
|
||||
"SELECT GAME_ID FROM COMN_GAME_ALIAS_BAS WHERE GAME_NM LIKE '%버블리즈%'",
|
||||
execution_succeeded=True,
|
||||
)
|
||||
unsafe = evaluate_sql(
|
||||
self.questions["STD-02"],
|
||||
"SELECT COUNT(*) FROM CZN_COMN_USER_MST WHERE GAME_ID = 'STOVE_CHAOSZERO'",
|
||||
execution_succeeded=True,
|
||||
)
|
||||
self.assertEqual("PASS", safe.status)
|
||||
self.assertEqual("FAIL", unsafe.status)
|
||||
|
||||
def test_free_text_is_review_not_automatic_pass(self) -> None:
|
||||
judgment = evaluate_sql(None, "SELECT 1 FROM DUAL", execution_succeeded=True)
|
||||
self.assertEqual("REVIEW", judgment.status)
|
||||
|
||||
def test_jdbc_url_wallet_is_normalized_for_python_driver(self) -> None:
|
||||
self.assertEqual(
|
||||
("sgmpaipoc_medium", "/home/opc/wallet/sgmpaipoc"),
|
||||
_normalize_oracle_dsn(
|
||||
"jdbc:oracle:thin:@sgmpaipoc_medium?TNS_ADMIN=/home/opc/wallet/sgmpaipoc"
|
||||
),
|
||||
)
|
||||
|
||||
def test_schema_defines_two_history_tables_and_indexes(self) -> None:
|
||||
statements = "\n".join(schema_statements())
|
||||
self.assertIn("CREATE TABLE SG_AI_QA_QUESTION", statements)
|
||||
self.assertIn("CREATE TABLE SG_AI_QA_ANSWER", statements)
|
||||
self.assertIn("answer_seq NUMBER GENERATED ALWAYS AS IDENTITY", statements)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,439 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import html
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Mapping
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from ai_web_agent_console.scenarios import ScenarioConfigError, load_demo_scenarios
|
||||
from ai_web_agent_console.profile import load_app_profile
|
||||
from ai_web_agent_console.mcp_tool_router import McpTool, build_mcp_tool_arguments
|
||||
from ai_web_agent_console.mcp_result import (
|
||||
has_actionable_text_result,
|
||||
status_result_evidence,
|
||||
status_result_summary,
|
||||
)
|
||||
from ai_web_agent_console.model_registry import load_model_registry
|
||||
|
||||
|
||||
def _load_console_query_helpers():
|
||||
"""Load the Streamlit entrypoint only when its optional runtime is installed."""
|
||||
|
||||
try:
|
||||
from app import _prepare_hmm_hr_tool_query
|
||||
except ModuleNotFoundError:
|
||||
return None
|
||||
return _prepare_hmm_hr_tool_query
|
||||
from src.poc4.scenarios import ScenarioConfigError, load_demo_scenarios
|
||||
from src.agent_console.profile import load_app_profile
|
||||
|
||||
|
||||
class DemoScenarioConfigTest(unittest.TestCase):
|
||||
def test_model_registry_uses_console_names(self) -> None:
|
||||
registry = load_model_registry()
|
||||
|
||||
self.assertEqual(
|
||||
registry.registry_name,
|
||||
"AI_WEB_AGENT_CONSOLE_MODEL_PROFILES",
|
||||
)
|
||||
self.assertTrue(registry.default_profile.default_for_console)
|
||||
|
||||
def test_profile_environment_overrides_json_defaults(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "app_profile.json"
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"AGENT_CONSOLE_SHORT_NAME": "HMM",
|
||||
"AGENT_CONSOLE_PAGE_TITLE": "HMM AI 업무 에이전트",
|
||||
"AGENT_CONSOLE_PRIMARY_COLOR": "#003b70",
|
||||
"AGENT_CONSOLE_SHORT_NAME": "SMILEGATE",
|
||||
"AGENT_CONSOLE_PAGE_TITLE": "SMILEGATE DATA & AI POC",
|
||||
"AGENT_CONSOLE_PRIMARY_COLOR": "#113F67",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
profile = load_app_profile(path)
|
||||
|
||||
self.assertEqual(profile.short_name, "HMM")
|
||||
self.assertEqual(profile.page_title, "HMM AI 업무 에이전트")
|
||||
self.assertEqual(profile.primary_color, "#003b70")
|
||||
self.assertEqual(profile.short_name, "SMILEGATE")
|
||||
self.assertEqual(profile.page_title, "SMILEGATE DATA & AI POC")
|
||||
self.assertEqual(profile.primary_color, "#113F67")
|
||||
|
||||
def test_profile_reads_dotenv_values(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "app_profile.json"
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
env_file = Path(temp_dir) / ".env"
|
||||
env_file.write_text("AGENT_CONSOLE_SHORT_NAME=HMM\n", encoding="utf-8")
|
||||
env_file.write_text("AGENT_CONSOLE_SHORT_NAME=SMILEGATE\n", encoding="utf-8")
|
||||
profile = load_app_profile(path, env_file)
|
||||
|
||||
self.assertEqual(profile.short_name, "HMM")
|
||||
self.assertEqual(profile.short_name, "SMILEGATE")
|
||||
|
||||
def test_common_theme_covers_lists_expanders_and_secondary_buttons(self) -> None:
|
||||
path = Path(__file__).parents[1] / "ai_web_agent_console" / "presentation.py"
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn('[data-testid="stAppViewContainer"] li', source)
|
||||
self.assertIn('[data-testid="stExpander"] summary', source)
|
||||
self.assertIn('div[data-testid="stButton"] > button', source)
|
||||
self.assertIn('[data-baseweb="tab-list"] [role="tab"]', source)
|
||||
self.assertIn('[data-testid="stTab"]', source)
|
||||
self.assertIn('[role="tab"][aria-selected="true"]', source)
|
||||
|
||||
def test_audit_tab_uses_hmm_access_audit_loaders(self) -> None:
|
||||
root = Path(__file__).parents[1]
|
||||
entrypoint = (root / "app.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
renderer = (root / "ai_web_agent_console" / "audit.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
self.assertIn("FROM ADMIN.HMM_ACCESS_AUDIT", entrypoint)
|
||||
self.assertIn("_load_hmm_audit_inventory", entrypoint)
|
||||
self.assertIn("(protocol=tcps)(port=1521)", entrypoint)
|
||||
self.assertIn(
|
||||
"AI_WEB_AGENT_CONSOLE_AUDIT_WALLET_PASSWORD",
|
||||
entrypoint,
|
||||
)
|
||||
self.assertIn("HMM 접근 관리", renderer)
|
||||
self.assertNotIn('AUDIT_SCHEMA = "POC_2"', entrypoint)
|
||||
|
||||
def test_hmm_scenarios_are_enabled_and_unique(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "hmm_demo_scenarios.json"
|
||||
def test_smilegate_scenarios_are_enabled_and_unique(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "smilegate_demo_scenarios.json"
|
||||
scenarios = load_demo_scenarios(path)
|
||||
|
||||
self.assertGreaterEqual(len(scenarios), 3)
|
||||
self.assertEqual(len(scenarios), len({item.scenario_id for item in scenarios}))
|
||||
self.assertTrue(all(item.question.strip() for item in scenarios))
|
||||
by_id = {item.scenario_id: item for item in scenarios}
|
||||
self.assertEqual(
|
||||
{"FED-01", "FED-02", "FED-03", "FED-04", "FED-05"},
|
||||
{"FED-01", "FED-02", "FED-03", "FED-04", "FED-05"} & set(by_id),
|
||||
)
|
||||
self.assertTrue(
|
||||
all("선사" in by_id[scenario_id].question for scenario_id in (
|
||||
"FED-01", "FED-02", "FED-03", "FED-04", "FED-05"
|
||||
))
|
||||
)
|
||||
carrier_scenarios = [
|
||||
item for item in scenarios if item.scenario_id.startswith("FED-")
|
||||
]
|
||||
self.assertTrue(
|
||||
all(
|
||||
re.search(r"\bE\d{4,}\b", item.question, flags=re.IGNORECASE) is None
|
||||
for item in carrier_scenarios
|
||||
)
|
||||
)
|
||||
report_scenarios = [
|
||||
item for item in carrier_scenarios if "리포트로 보여줘" in item.question
|
||||
]
|
||||
self.assertGreaterEqual(len(report_scenarios), 2)
|
||||
self.assertTrue(
|
||||
all(item.category == "선사 실적 리포트" for item in report_scenarios)
|
||||
)
|
||||
|
||||
def test_hmm_mcp_allows_carrier_federation_tool(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "mcp_servers.json"
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
server = next(
|
||||
item for item in payload["servers"] if item["id"] == "hmm_hr_mcp"
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"search_carrier_performance",
|
||||
server["tool_allowlist"],
|
||||
)
|
||||
|
||||
def test_hmm_mcp_allows_dynamic_html_renderer(self) -> None:
|
||||
root = Path(__file__).parents[1]
|
||||
payload = json.loads(
|
||||
(root / "config" / "mcp_servers.json").read_text(encoding="utf-8")
|
||||
)
|
||||
server = next(
|
||||
item for item in payload["servers"] if item["id"] == "hmm_hr_mcp"
|
||||
)
|
||||
source = (root / "app.py").read_text(encoding="utf-8")
|
||||
|
||||
self.assertEqual(
|
||||
server["endpoint_url"],
|
||||
"https://hmm-backoffice.cloud-handson.com/mcp",
|
||||
)
|
||||
self.assertIn("render_hmm_carrier_report", server["tool_allowlist"])
|
||||
self.assertNotIn('tool.name == "render_hmm_carrier_report"', source)
|
||||
|
||||
def test_hmm_report_rows_accept_nested_select_ai_json_array(self) -> None:
|
||||
source = (Path(__file__).parents[1] / "app.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
helper = next(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "_mcp_structured_rows"
|
||||
)
|
||||
namespace: dict[str, Any] = {
|
||||
"Any": Any,
|
||||
"Mapping": Mapping,
|
||||
"json": json,
|
||||
}
|
||||
exec(compile(ast.Module(body=[helper], type_ignores=[]), "app.py", "exec"), namespace)
|
||||
|
||||
rows = namespace["_mcp_structured_rows"](
|
||||
{
|
||||
"response": {
|
||||
"result": json.dumps(
|
||||
[
|
||||
{"EMPLOYEE_CODE": "E9001", "CARRIER_CODE": "C901"},
|
||||
{"EMPLOYEE_CODE": "E9002", "CARRIER_CODE": "C902"},
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual(rows[0]["CARRIER_CODE"], "C901")
|
||||
|
||||
def test_hmm_report_normalizes_repeated_select_ai_column_labels(self) -> None:
|
||||
source = (Path(__file__).parents[1] / "app.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
helpers = [
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name in {"_camel_case_key", "_normalize_presentation_value"}
|
||||
]
|
||||
namespace: dict[str, Any] = {
|
||||
"Any": Any,
|
||||
"Mapping": Mapping,
|
||||
"re": re,
|
||||
}
|
||||
exec(compile(ast.Module(body=helpers, type_ignores=[]), "app.py", "exec"), namespace)
|
||||
|
||||
first = namespace["_normalize_presentation_value"](
|
||||
{"CARRIER_CODE": "C001", "LATEST_REVENUE_USD": 100}
|
||||
)
|
||||
repeated = namespace["_normalize_presentation_value"](
|
||||
{"carrier Code": "C002", "LATEST REVENUE USD": 200}
|
||||
)
|
||||
|
||||
self.assertEqual(first, {"carrierCode": "C001", "latestRevenueUsd": 100})
|
||||
self.assertEqual(repeated, {"carrierCode": "C002", "latestRevenueUsd": 200})
|
||||
|
||||
def test_hmm_report_title_and_answer_follow_presentation_contract(self) -> None:
|
||||
source = (Path(__file__).parents[1] / "app.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
helper = next(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "_clean_presentation_title"
|
||||
)
|
||||
namespace: dict[str, Any] = {
|
||||
"Any": Any,
|
||||
"html": html,
|
||||
"re": re,
|
||||
}
|
||||
exec(compile(ast.Module(body=[helper], type_ignores=[]), "app.py", "exec"), namespace)
|
||||
|
||||
title = namespace["_clean_presentation_title"](
|
||||
"<b>E1001 팀 포트폴리오</b>"
|
||||
)
|
||||
|
||||
self.assertEqual(title, "E1001 팀 포트폴리오")
|
||||
self.assertIn('"title": _clean_presentation_title(title)', source)
|
||||
self.assertIn(
|
||||
'assistant_message["content"] = presentation_answer',
|
||||
source,
|
||||
)
|
||||
self.assertIn(
|
||||
"presentation_answer = _presentation_completion_answer(",
|
||||
source,
|
||||
)
|
||||
self.assertIn(
|
||||
"do not reproduce HTML tags, Markdown tables",
|
||||
source,
|
||||
)
|
||||
|
||||
def test_chat_context_is_scoped_to_current_selected_user(self) -> None:
|
||||
source = (Path(__file__).parents[1] / "app.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
helper = next(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "load_chat_context"
|
||||
)
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
class FakeConnection:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def execute(self, sql, params):
|
||||
captured["sql"] = sql
|
||||
captured["params"] = params
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
return [{"question": "내 담당 선사", "answer": "2건"}]
|
||||
|
||||
namespace: dict[str, Any] = {
|
||||
"CHAT_CONTEXT_TURNS": 8,
|
||||
"MAX_CONVERSATION_MESSAGES": 16,
|
||||
"_chat_db_connect": FakeConnection,
|
||||
"_is_failed_synthesis_answer": lambda _value: False,
|
||||
}
|
||||
exec(compile(ast.Module(body=[helper], type_ignores=[]), "app.py", "exec"), namespace)
|
||||
|
||||
messages = namespace["load_chat_context"](
|
||||
"conversation-1",
|
||||
selected_user_id="E1002",
|
||||
)
|
||||
|
||||
self.assertIn("selected_user_id = ?", captured["sql"])
|
||||
self.assertEqual(
|
||||
captured["params"],
|
||||
("conversation-1", "E1002", "E1002", 8),
|
||||
)
|
||||
self.assertEqual(messages[0]["content"], "내 담당 선사")
|
||||
|
||||
def test_standalone_question_uses_current_user_without_prior_context(self) -> None:
|
||||
source = (Path(__file__).parents[1] / "app.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
helpers = [
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name in {"_conversation_context", "resolve_standalone_question"}
|
||||
]
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
class FakeClient:
|
||||
def complete(self, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return json.dumps(
|
||||
{
|
||||
"standalone_question": (
|
||||
"E1002 사용자의 담당 선사 최신 실적을 조회해줘"
|
||||
)
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
namespace: dict[str, Any] = {
|
||||
"Any": Any,
|
||||
"Mapping": Mapping,
|
||||
"MAX_CONVERSATION_MESSAGES": 16,
|
||||
"json": json,
|
||||
"resolve_model_profile": lambda _key: SimpleNamespace(
|
||||
model_id="model",
|
||||
answer_model_region="region",
|
||||
answer_model_endpoint="endpoint",
|
||||
),
|
||||
"build_oci_genai_completion_client": lambda *_args: FakeClient(),
|
||||
"temperature_for_model_profile": lambda _profile: 0.0,
|
||||
}
|
||||
exec(compile(ast.Module(body=helpers, type_ignores=[]), "app.py", "exec"), namespace)
|
||||
|
||||
rewritten = namespace["resolve_standalone_question"](
|
||||
question="내 담당 선사 최신 실적을 리포트로 보여줘",
|
||||
messages=[],
|
||||
model_profile_key="test",
|
||||
selected_user_id="E1002",
|
||||
)
|
||||
prompt_payload = json.loads(captured["user_prompt"])
|
||||
|
||||
self.assertTrue(rewritten.startswith("E1002"))
|
||||
self.assertEqual(prompt_payload["current_selected_user_id"], "E1002")
|
||||
self.assertIn("authoritative", captured["system_prompt"])
|
||||
|
||||
def test_report_payload_requester_prefers_current_selected_user(self) -> None:
|
||||
source = (Path(__file__).parents[1] / "app.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
helper = next(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "_presentation_payload"
|
||||
)
|
||||
namespace: dict[str, Any] = {
|
||||
"Any": Any,
|
||||
"Mapping": Mapping,
|
||||
"datetime": __import__("datetime").datetime,
|
||||
"timezone": __import__("datetime").timezone,
|
||||
"re": re,
|
||||
"_mcp_structured_rows": lambda _value: [],
|
||||
"_normalize_presentation_value": lambda value: value,
|
||||
"_clean_presentation_title": lambda value: str(value),
|
||||
}
|
||||
exec(compile(ast.Module(body=[helper], type_ignores=[]), "app.py", "exec"), namespace)
|
||||
|
||||
payload = namespace["_presentation_payload"](
|
||||
"E1001 팀장 문맥이 남은 질문",
|
||||
[],
|
||||
title="담당 선사 실적",
|
||||
selected_user_id="E1002",
|
||||
)
|
||||
|
||||
self.assertEqual(payload["report"]["requestedBy"], "E1002")
|
||||
|
||||
def test_hmm_report_data_query_drops_html_format_request(self) -> None:
|
||||
source = (Path(__file__).parents[1] / "app.py").read_text(encoding="utf-8")
|
||||
tree = ast.parse(source)
|
||||
helper = next(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "_fallback_presentation_data_query"
|
||||
)
|
||||
namespace: dict[str, Any] = {"re": re}
|
||||
exec(compile(ast.Module(body=[helper], type_ignores=[]), "app.py", "exec"), namespace)
|
||||
|
||||
query = namespace["_fallback_presentation_data_query"](
|
||||
"E1001 팀장의 담당 선사 최신 매출, 매출총이익, 정시 운항률, "
|
||||
"위험 등급을 HTML로 보여줘"
|
||||
)
|
||||
|
||||
self.assertNotIn("HTML", query.upper())
|
||||
self.assertIn("E1001", query)
|
||||
self.assertIn("매출총이익", query)
|
||||
self.assertIn("정시 운항률", query)
|
||||
self.assertIn("위험 등급", query)
|
||||
self.assertIn("_mcp_rows_contain_presentation_markup(mcp_result)", source)
|
||||
|
||||
personal_query = namespace["_fallback_presentation_data_query"](
|
||||
"내 담당 선사와 최신 매출을 리포트로 보여줘"
|
||||
)
|
||||
self.assertEqual(personal_query, "내 담당 선사와 최신 매출을 보여줘")
|
||||
|
||||
def test_hmm_report_template_contains_only_dynamic_payload_slot(self) -> None:
|
||||
template = (
|
||||
Path(__file__).parents[1]
|
||||
/ "assets"
|
||||
/ "hmm-carrier-performance-report.html"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
self.assertEqual(template.count("__REPORT_DATA__"), 1)
|
||||
self.assertNotIn("Bluewave Maritime", template)
|
||||
self.assertNotIn("Southern Cross Marine", template)
|
||||
|
||||
def test_hmm_demo_user_presets_reference_runtime_token_only(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "vpd_token_presets.json"
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
presets = payload["presets"]
|
||||
|
||||
self.assertEqual(payload["version"], 2)
|
||||
self.assertEqual({item["user_id"] for item in presets}, {
|
||||
"E1001", "E1002", "E1003", "E1005", "E1007"
|
||||
})
|
||||
self.assertEqual(
|
||||
{item["mcp_token_env"] for item in presets},
|
||||
{f"HMM_MCP_BEARER_TOKEN_{item['user_id']}" for item in presets},
|
||||
)
|
||||
self.assertTrue(all("token" not in item for item in presets))
|
||||
|
||||
def test_duplicate_id_is_rejected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
@@ -452,87 +62,6 @@ class DemoScenarioConfigTest(unittest.TestCase):
|
||||
with self.assertRaises(ScenarioConfigError):
|
||||
load_demo_scenarios(path)
|
||||
|
||||
def test_default_mcp_tool_arguments_follow_discovered_query_schema(self) -> None:
|
||||
tool = McpTool(
|
||||
name="search_hr_data",
|
||||
description="",
|
||||
schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}},
|
||||
"required": ["query"],
|
||||
},
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
arguments = build_mcp_tool_arguments(
|
||||
tool, "직원 E1005의 휴가 신청 내역", 50, preferred_tool="search_hr_data"
|
||||
)
|
||||
|
||||
self.assertEqual(arguments, {"query": "직원 E1005의 휴가 신청 내역"})
|
||||
|
||||
def test_term_tool_arguments_follow_discovered_term_schema(self) -> None:
|
||||
tool = McpTool(
|
||||
name="resolve_hr_term",
|
||||
description="",
|
||||
schema={
|
||||
"type": "object",
|
||||
"properties": {"term": {"type": "string"}},
|
||||
"required": ["term"],
|
||||
},
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
arguments = build_mcp_tool_arguments(
|
||||
tool, "반차", 50, preferred_tool="search_hr_data"
|
||||
)
|
||||
|
||||
self.assertEqual(arguments, {"term": "반차"})
|
||||
|
||||
def test_status_result_policy_text_is_preserved_as_answer_evidence(self) -> None:
|
||||
result = {
|
||||
"status": "success",
|
||||
"result": (
|
||||
"HR_POLICY_SEARCH_RESULT\n"
|
||||
"EVIDENCE|file=KR_Leave_Policy.pdf|chunk=13|text=이월 기준"
|
||||
),
|
||||
}
|
||||
|
||||
summary = status_result_summary(result, excerpt_chars=40)
|
||||
evidence = status_result_evidence(result)
|
||||
|
||||
self.assertEqual(summary["status"], "success")
|
||||
self.assertGreater(summary["result_chars"], 40)
|
||||
self.assertIn("KR_Leave_Policy.pdf", evidence["result"])
|
||||
self.assertTrue(has_actionable_text_result(result))
|
||||
|
||||
def test_no_data_text_is_not_actionable(self) -> None:
|
||||
self.assertFalse(
|
||||
has_actionable_text_result({"status": "success", "result": "No data found"})
|
||||
)
|
||||
|
||||
@unittest.skipIf(_load_console_query_helpers() is None, "Streamlit runtime is optional")
|
||||
def test_policy_query_does_not_include_demo_user_context(self) -> None:
|
||||
prepare = _load_console_query_helpers()
|
||||
assert prepare is not None
|
||||
tool = McpTool(
|
||||
name="search_hr_policy",
|
||||
description="Search policy documents",
|
||||
schema={"properties": {"query": {"type": "string"}}},
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
query = prepare(
|
||||
question="연차 휴가 이월 기준과 제한을 알려줘",
|
||||
tool=tool,
|
||||
model_profile_key="gpt54_mini_oci",
|
||||
selected_user_id="E1001",
|
||||
selected_user_role="HR Team Manager",
|
||||
selected_user_team="HMM HR Demo Team",
|
||||
selected_user_scope="팀원 6명 관리",
|
||||
)
|
||||
|
||||
self.assertEqual(query, "연차 휴가 이월 기준과 제한을 알려줘")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
22
database/adb/100_sgmp_czn05_country_business_au_fewshot.sql
Normal file
22
database/adb/100_sgmp_czn05_country_business_au_fewshot.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Approve the reviewed customer QA example for a grouped business-AU query.
|
||||
-- Empty result sets remain valid executed query results.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
answer_text = 'Expected focus: aggregate business AU by the user-master country attribute. '
|
||||
|| 'Join CZN_CUSTOM_BIZ_USER_TXN to CZN_COMN_USER_MST by GUID and BASE_DT; filter BIZ_AU_FLAG=1 and EXPT_USER_YN=''N'', then group by LAST_CONN_COUNTRY_CD. '
|
||||
|| 'A successfully executed query with no country rows is a valid result, not a SQL failure. '
|
||||
|| 'Historical answer: no result rows.',
|
||||
inspection_note = 'Customer QA verified: country business-AU is a grouped join; an empty result is a valid query outcome.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-05';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT example_id, reference_status, inspection_status, source_case_id, answer_text
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-05';
|
||||
16
database/adb/101_sgmp_czn06_country_standard_au_fewshot.sql
Normal file
16
database/adb/101_sgmp_czn06_country_standard_au_fewshot.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Approve the reviewed customer QA example for a grouped standard-AU query.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
answer_text = 'Expected focus: aggregate standard AU by user-master country, joining COMN_COUNTRY_BAS only for the country display name. '
|
||||
|| 'Use CZN_COMN_USER_MST with AU_FLAG=1 and EXPT_USER_YN=''N'', grouped by LAST_CONN_COUNTRY_CD and COUNTRY_KR_NM. '
|
||||
|| 'The label standard AU does not imply STD_USER_YN. A successfully executed empty result is valid. '
|
||||
|| 'Historical answer: no result rows.',
|
||||
inspection_note = 'Customer QA verified: country standard-AU is grouped AU_FLAG aggregation; empty output is valid.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-06';
|
||||
|
||||
COMMIT;
|
||||
15
database/adb/102_sgmp_czn07_crystal_holdings_fewshot.sql
Normal file
15
database/adb/102_sgmp_czn07_crystal_holdings_fewshot.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Approve the reviewed customer QA example for daily in-game currency holdings.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
answer_text = 'Expected focus: daily crystal holdings use CZN_CUSTOM_GOODS_HAVE_TXN joined to CZN_COMN_USER_MST and CZN_COMN_SVC_DIM_BAS. '
|
||||
|| 'Filter the goods dimension to crystal, nonzero HAVE_CNT, eligible returning-user population, and the requested date range; group by BASE_DT. '
|
||||
|| 'A successfully executed empty result is valid. Historical answer: no result rows.',
|
||||
inspection_note = 'Customer QA verified: daily crystal holdings are a date-grouped goods/user/dimension join; empty output is valid.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-07';
|
||||
|
||||
COMMIT;
|
||||
15
database/adb/103_sgmp_czn08_crystal_average_fewshot.sql
Normal file
15
database/adb/103_sgmp_czn08_crystal_average_fewshot.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Approve the exact customer QA for standard-AU crystal holdings per user.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
answer_text = 'Use the verified customer SQL template for crystal holdings among standard AU. '
|
||||
|| 'The standard-AU population uses AU_FLAG=1 and EXPT_USER_YN=''N''; do not add STD_USER_YN unless explicitly requested. '
|
||||
|| 'Use the template population denominator for the per-user average. Null aggregate values are valid when the qualifying set is empty.',
|
||||
inspection_note = 'Customer QA verified: retain the approved standard-AU population and average denominator semantics.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-08';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Customer-provided CZN benchmark examples are the approved reference corpus
|
||||
-- for exact-question Few-shot retrieval. Their SQL and expected-answer text
|
||||
-- remain the source of metric semantics; no runtime game/table branching is added.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Customer QA benchmark approved for exact-question Few-shot retrieval.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id LIKE 'CZN-%'
|
||||
AND reference_status <> 'APPROVED';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT source_case_id, reference_status, inspection_status
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id LIKE 'CZN-%'
|
||||
ORDER BY source_case_id;
|
||||
11
database/adb/105_sgmp_czn13_zero_aggregate_fewshot.sql
Normal file
11
database/adb/105_sgmp_czn13_zero_aggregate_fewshot.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Preserve customer QA output semantics for empty numeric aggregates.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_text = NVL(answer_text, '') || ' For this approved metric, normalize an empty numeric aggregate to 0 in the returned result. Preserve the template join from CZN_CUSTOM_GOODS_CHANGE_TXN to CZN_COMN_USER_MST, apply u.EXPT_USER_YN=''N'', and count distinct u.GUID.',
|
||||
inspection_note = 'Customer QA verified: empty total Ether usage is reported as numeric zero with the template user-master join, excluded-user filter, and user population.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-13';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Approve the remaining customer-provided standard QA references for exact-question Few-shot retrieval.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Customer QA benchmark approved for exact-question Few-shot retrieval.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id LIKE 'STD-%'
|
||||
AND reference_status <> 'APPROVED';
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_text = NVL(answer_text, '') || ' This unavailable-object case must not fabricate a DUAL/NULL result row. Return no result rows and explain that no approved physical object is available for the resolved game.',
|
||||
inspection_note = 'Customer QA verified: unavailable game objects return no result rows; no synthetic DUAL result.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-01';
|
||||
|
||||
COMMIT;
|
||||
64
database/adb/107_sgmp_separate_customer_qa_from_fewshot.sql
Normal file
64
database/adb/107_sgmp_separate_customer_qa_from_fewshot.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
-- Customer QA is evaluation data, never production Few-shot context.
|
||||
-- Preserve it for SG_AI_QA_* baseline/history audit while retiring its vector copies.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'RETIRED',
|
||||
inspection_note = 'Evaluation-only customer QA. Excluded from production Few-shot retrieval.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_EVALUATION_SEPARATION'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK';
|
||||
|
||||
COMMIT;
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
|
||||
END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'target_type must be NONE, SINGLE, MULTI, ALL, or ANY.');
|
||||
END IF;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question,
|
||||
JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id,
|
||||
question,
|
||||
answer_sql,
|
||||
answer_text,
|
||||
embedding_model,
|
||||
reference_kind,
|
||||
target_type,
|
||||
object_role,
|
||||
source_case_id,
|
||||
source_type,
|
||||
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND (source_type IS NULL OR source_type <> 'CUSTOMER_QA_BENCHMARK')
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT source_type, reference_status, COUNT(*) AS example_count
|
||||
FROM sg_qa_vector_example
|
||||
GROUP BY source_type, reference_status
|
||||
ORDER BY source_type, reference_status;
|
||||
@@ -0,0 +1,165 @@
|
||||
-- Build one generalized runtime Few-shot pattern for every customer QA case.
|
||||
-- The source benchmark remains evaluation-only; this derived record contains
|
||||
-- no customer game name, date literal, expected result, or physical CZN object.
|
||||
|
||||
DECLARE
|
||||
v_pattern_question CLOB;
|
||||
v_pattern_sql CLOB;
|
||||
v_embedding_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_object_role VARCHAR2(64);
|
||||
v_exists NUMBER;
|
||||
|
||||
FUNCTION generalized_question(p_question CLOB) RETURN CLOB IS
|
||||
v_value CLOB := p_question;
|
||||
BEGIN
|
||||
-- Resolved names/aliases become a semantic game placeholder.
|
||||
FOR token IN (
|
||||
SELECT column_value AS value
|
||||
FROM TABLE(sys.odcivarchar2list(
|
||||
'카오스 제로 나이트메어', '카오스제로나이트메어', 'Chaos Zero Nightmare',
|
||||
'STOVE_CHAOSZERO', '카제나', 'CZN', 'Bubblyz', '버블리즈',
|
||||
'로드나인', '로나', '테스트게임', 'BUBBLYZ', 'LORDNINE'
|
||||
))
|
||||
) LOOP
|
||||
v_value := REPLACE(v_value, token.value, '<게임>');
|
||||
END LOOP;
|
||||
v_value := REGEXP_REPLACE(v_value, '[0-9]{4}년[[:space:]]*[0-9]{1,2}월[[:space:]]*[0-9]{1,2}일', '<기준일>');
|
||||
v_value := REGEXP_REPLACE(v_value, '[0-9]{4}-[0-9]{2}-[0-9]{2}', '<기준일>');
|
||||
RETURN v_value;
|
||||
END;
|
||||
|
||||
FUNCTION generalized_sql(p_sql CLOB) RETURN CLOB IS
|
||||
v_value CLOB := p_sql;
|
||||
BEGIN
|
||||
-- Physical game objects become logical roles. Common dimensions remain
|
||||
-- logical as well so the current metadata/plan selects real objects.
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_COMN_USER_MST"', '<RESOLVED_GAME_USER_MASTER>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_COMN_CHARACTER_MST"', '<RESOLVED_GAME_CHARACTER_MASTER>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_GOODS_HAVE_TXN"', '<RESOLVED_GAME_GOODS_HOLDINGS>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_GOODS_CHANGE_TXN"', '<RESOLVED_GAME_GOODS_CHANGE>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_BIZ_USER_TXN"', '<RESOLVED_GAME_BUSINESS_USER>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_USER_GOODS_TXN"', '<RESOLVED_GAME_USER_GOODS>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_SALES_TXN"', '<APPROVED_SALES_TRANSACTION>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_REFUND_TXN"', '<APPROVED_REFUND_TRANSACTION>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_GAME_ALIAS_BAS"', '<GAME_ALIAS_CATALOG>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_COUNTRY_BAS"', '<COUNTRY_DIMENSION>');
|
||||
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_COMN_SVC_DIM_BAS"', '<RESOLVED_GAME_SERVICE_DIMENSION>');
|
||||
v_value := REPLACE(v_value, 'STOVE_CHAOSZERO', '<RESOLVED_GAME_ID>');
|
||||
v_value := REPLACE(v_value, '''카제나''', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REPLACE(v_value, '''CZN''', '<RESOLVED_GAME_PREFIX>');
|
||||
v_value := REGEXP_REPLACE(v_value, 'CZN_[A-Z0-9_]+', '<RESOLVED_GAME_OBJECT>');
|
||||
v_value := REPLACE(v_value, '카제나', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REPLACE(v_value, '카오스 제로 나이트메어', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REPLACE(v_value, '카오스제로나이트메어', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REPLACE(v_value, 'CZN', '<RESOLVED_GAME_PREFIX>');
|
||||
v_value := REPLACE(v_value, 'BUBBLYZ', '<RESOLVED_GAME_ID>');
|
||||
v_value := REPLACE(v_value, 'Bubblyz', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REPLACE(v_value, '버블리즈', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REPLACE(v_value, 'LORDNINE', '<RESOLVED_GAME_ID>');
|
||||
v_value := REPLACE(v_value, '로드나인', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REPLACE(v_value, '테스트게임', '<RESOLVED_GAME_NAME>');
|
||||
v_value := REGEXP_REPLACE(v_value, 'TO_DATE\(''[0-9]{4}-[0-9]{2}-[0-9]{2}'', ''YYYY-MM-DD''\)', '<BUSINESS_DATE>');
|
||||
v_value := REGEXP_REPLACE(v_value, 'TO_DATE\(''[0-9]{8}'', ''YYYYMMDD''\)', '<BUSINESS_DATE>');
|
||||
v_value := REGEXP_REPLACE(v_value, 'DATE ''[0-9]{4}-[0-9]{2}-[0-9]{2}''', '<BUSINESS_DATE>');
|
||||
RETURN v_value;
|
||||
END;
|
||||
|
||||
FUNCTION role_of(p_sql CLOB) RETURN VARCHAR2 IS
|
||||
BEGIN
|
||||
IF DBMS_LOB.INSTR(p_sql, 'CZN_COMN_CHARACTER_MST') > 0 THEN
|
||||
RETURN 'GAME_CHARACTER_MASTER';
|
||||
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_CUSTOM_GOODS_HAVE_TXN') > 0 THEN
|
||||
RETURN 'GAME_GOODS_HOLDINGS';
|
||||
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_CUSTOM_GOODS_CHANGE_TXN') > 0 THEN
|
||||
RETURN 'GAME_GOODS_CHANGE';
|
||||
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_CUSTOM_BIZ_USER_TXN') > 0 THEN
|
||||
RETURN 'GAME_BUSINESS_USER';
|
||||
ELSIF DBMS_LOB.INSTR(p_sql, 'COMN_SALES_TXN') > 0 THEN
|
||||
RETURN 'SALES_TRANSACTION';
|
||||
ELSIF DBMS_LOB.INSTR(p_sql, 'COMN_REFUND_TXN') > 0 THEN
|
||||
RETURN 'REFUND_TRANSACTION';
|
||||
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_COMN_USER_MST') > 0 THEN
|
||||
RETURN 'GAME_USER_MASTER';
|
||||
END IF;
|
||||
RETURN 'METADATA_OR_OPERATION';
|
||||
END;
|
||||
BEGIN
|
||||
FOR source_row IN (
|
||||
SELECT example_id, source_case_id, question, answer_sql
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
ORDER BY source_case_id
|
||||
) LOOP
|
||||
v_pattern_question := generalized_question(source_row.question);
|
||||
v_pattern_sql := generalized_sql(source_row.answer_sql);
|
||||
v_object_role := role_of(source_row.answer_sql);
|
||||
v_embedding_input := TO_CLOB('Generalized question pattern: ') || v_pattern_question
|
||||
|| CHR(10) || 'Logical object role: ' || v_object_role
|
||||
|| CHR(10) || 'Structural SQL template: ' || v_pattern_sql
|
||||
|| CHR(10) || 'Use only current game scope metadata and replace placeholders from the current request.';
|
||||
|
||||
-- A generalized runtime pattern must not contain known customer answer
|
||||
-- identifiers or fixed business-date literals.
|
||||
IF REGEXP_LIKE(v_pattern_question,
|
||||
'카제나|버블리즈|Bubblyz|로드나인|테스트게임|[0-9]{4}년|[0-9]{4}-[0-9]{2}-[0-9]{2}', 'i')
|
||||
OR REGEXP_LIKE(v_pattern_sql,
|
||||
'CZN_|STOVE_CHAOSZERO|카제나|버블리즈|Bubblyz|[0-9]{4}-[0-9]{2}-[0-9]{2}', 'i') THEN
|
||||
RAISE_APPLICATION_ERROR(-20061, 'Generalization leak in ' || source_row.source_case_id);
|
||||
END IF;
|
||||
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_embedding_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO v_exists
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
|
||||
AND source_case_id = 'PAT-' || source_row.source_case_id;
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, object_role,
|
||||
inspection_status, inspection_note, verified_at, verified_by,
|
||||
source_case_id, source_type
|
||||
) VALUES (
|
||||
v_pattern_question,
|
||||
v_pattern_sql,
|
||||
'Question-specific generalized Few-shot. Structural only: it contains no customer game, date, result, or executable answer. First decide whether this pattern is applicable; then apply the authoritative NONE/SINGLE/MULTI/ALL game plan and replace placeholders from current metadata.',
|
||||
v_embedding_input,
|
||||
v_embedding,
|
||||
'cohere.embed-v4.0',
|
||||
'APPROVED', 'SQL_TEMPLATE', 'ANY', v_object_role,
|
||||
'VERIFIED',
|
||||
'Derived from a customer QA structure after game/date/result/object leakage validation; runtime uses this generalized pattern only.',
|
||||
SYSTIMESTAMP, 'SGMP_POC_PATTERN_REVIEW',
|
||||
'PAT-' || source_row.source_case_id, 'GENERALIZED_QUESTION_PATTERN'
|
||||
);
|
||||
ELSE
|
||||
UPDATE sg_qa_vector_example
|
||||
SET question = v_pattern_question,
|
||||
answer_sql = v_pattern_sql,
|
||||
answer_text = 'Question-specific generalized Few-shot. Structural only: it contains no customer game, date, result, or executable answer. First decide whether this pattern is applicable; then apply the authoritative NONE/SINGLE/MULTI/ALL game plan and replace placeholders from current metadata.',
|
||||
embedding_input = v_embedding_input,
|
||||
embedding = v_embedding,
|
||||
object_role = v_object_role,
|
||||
reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Derived from a customer QA structure after game/date/result/object leakage validation; runtime uses this generalized pattern only.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_PATTERN_REVIEW'
|
||||
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
|
||||
AND source_case_id = 'PAT-' || source_row.source_case_id;
|
||||
END IF;
|
||||
END LOOP;
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT source_type, reference_status, COUNT(*) AS example_count
|
||||
FROM sg_qa_vector_example
|
||||
GROUP BY source_type, reference_status
|
||||
ORDER BY source_type, reference_status;
|
||||
@@ -0,0 +1,358 @@
|
||||
-- Generate one reusable, question-specific Few-shot pattern per customer QA
|
||||
-- benchmark without promoting the benchmark answer itself. Game identity is
|
||||
-- deliberately not inferred here: sg_game_query_plan owns that through OCI
|
||||
-- GenAI chat + the current game catalog.
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_genai_generalize_pattern(
|
||||
p_question IN CLOB,
|
||||
p_answer_sql IN CLOB,
|
||||
p_target_type IN VARCHAR2
|
||||
) RETURN CLOB AUTHID DEFINER
|
||||
IS
|
||||
v_prompt CLOB;
|
||||
v_result CLOB;
|
||||
BEGIN
|
||||
v_prompt :=
|
||||
'Create one reusable, question-specific Few-shot SQL pattern from the source example. '
|
||||
|| 'This is training guidance, never an answer key. Return exactly these tagged sections and nothing else: '
|
||||
|| '[[PATTERN_QUESTION]], [[STRUCTURAL_SQL_PATTERN]], [[OBJECT_ROLE]], [[TARGET_TYPE]], '
|
||||
|| '[[APPLICABILITY]], [[END]]. '
|
||||
|| 'Preserve only the query intent and structural operations such as aggregation, joins, '
|
||||
|| 'grouping, ordering, date semantics, and filters. Replace every game name, alias, game ID, '
|
||||
|| 'schema name, physical object name, column name, literal date, literal number, user ID, '
|
||||
|| 'currency amount, and expected output with semantic placeholders such as <GAME_SCOPE>, '
|
||||
|| '<LOGICAL_FACT>, <LOGICAL_DIMENSION>, <METRIC>, <AS_OF_DATE>, <FILTER>, and <GROUPING>. '
|
||||
|| 'In STRUCTURAL_SQL_PATTERN, every non-SQL identifier must be an angle-bracket placeholder: '
|
||||
|| 'do not retain any source column, alias, table, schema, literal, code, or business value. '
|
||||
|| 'Do not include executable SQL. Do not include a game name or a customer answer. '
|
||||
|| 'The current game scope is supplied separately at runtime by a database OCI GenAI chat '
|
||||
|| 'resolver, therefore never choose or imply a game. The TARGET_TYPE section must be one of NONE, '
|
||||
|| 'SINGLE, MULTI, ALL, ANY and must describe applicability, not a game identity. '
|
||||
|| 'Source target type from the current resolver: ' || NVL(p_target_type, 'ANY') || CHR(10)
|
||||
|| 'Source question:' || CHR(10) || DBMS_LOB.SUBSTR(p_question, 4000, 1) || CHR(10)
|
||||
|| 'Source SQL (structure only; do not copy identifiers or values):' || CHR(10)
|
||||
|| DBMS_LOB.SUBSTR(p_answer_sql, 12000, 1);
|
||||
v_result := DBMS_CLOUD_AI.GENERATE(
|
||||
prompt => v_prompt,
|
||||
profile_name => 'SGMP_POC_OCI_GPT54MINI',
|
||||
action => 'chat'
|
||||
);
|
||||
RETURN v_result;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_genai_validate_pattern(
|
||||
p_pattern_json IN CLOB
|
||||
) RETURN CLOB AUTHID DEFINER
|
||||
IS
|
||||
v_prompt CLOB;
|
||||
v_result CLOB;
|
||||
BEGIN
|
||||
v_prompt :=
|
||||
'Inspect only concrete-answer leakage in this reusable Few-shot pattern. Return exactly '
|
||||
|| '[[CONCRETE_LEAKAGE]] YES or NO, then [[REASON]] and a short reason, then [[END]]. '
|
||||
|| 'Return YES only when a customer answer, concrete game identity, physical schema/table/column '
|
||||
|| 'identifier, literal date, literal business result, or executable SQL against a real object remains. '
|
||||
|| 'Return NO when all such references are semantic angle-bracket placeholders. A pseudo-SQL pattern '
|
||||
|| 'using SELECT/FROM/JOIN/GROUP BY, generic game-scope checks, EXISTS, UNION, or equality with '
|
||||
|| 'angle-bracket placeholders is not concrete leakage and must return NO. Do not judge usefulness or '
|
||||
|| 'completeness; classify leakage only. '
|
||||
|| 'Candidate:' || CHR(10) || DBMS_LOB.SUBSTR(p_pattern_json, 16000, 1);
|
||||
v_result := DBMS_CLOUD_AI.GENERATE(
|
||||
prompt => v_prompt,
|
||||
profile_name => 'SGMP_POC_OCI_GPT54MINI',
|
||||
action => 'chat'
|
||||
);
|
||||
RETURN v_result;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_generate_generalized_patterns
|
||||
RETURN NUMBER AUTHID DEFINER
|
||||
IS
|
||||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||||
v_plan_raw CLOB;
|
||||
v_plan JSON_OBJECT_T;
|
||||
v_target_type VARCHAR2(16);
|
||||
v_pattern_raw CLOB;
|
||||
v_validation_raw CLOB;
|
||||
v_status VARCHAR2(16);
|
||||
v_validation_note CLOB;
|
||||
v_question CLOB;
|
||||
v_sql_pattern CLOB;
|
||||
v_answer_text CLOB;
|
||||
v_object_role VARCHAR2(64);
|
||||
v_embedding_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_count NUMBER := 0;
|
||||
|
||||
FUNCTION parse_json_result(p_value CLOB) RETURN JSON_OBJECT_T IS
|
||||
v_text CLOB := TRIM(p_value);
|
||||
BEGIN
|
||||
IF DBMS_LOB.SUBSTR(v_text, 7, 1) = '```json' THEN
|
||||
v_text := REGEXP_REPLACE(v_text, '^```json[[:space:]]*', '');
|
||||
v_text := REGEXP_REPLACE(v_text, '[[:space:]]*```[[:space:]]*$', '');
|
||||
ELSIF DBMS_LOB.SUBSTR(v_text, 3, 1) = '```' THEN
|
||||
v_text := REGEXP_REPLACE(v_text, '^```[[:space:]]*', '');
|
||||
v_text := REGEXP_REPLACE(v_text, '[[:space:]]*```[[:space:]]*$', '');
|
||||
END IF;
|
||||
RETURN JSON_OBJECT_T.parse(v_text);
|
||||
END;
|
||||
|
||||
FUNCTION section_value(
|
||||
p_raw IN CLOB, p_start_tag IN VARCHAR2, p_end_tag IN VARCHAR2
|
||||
) RETURN CLOB IS
|
||||
v_start PLS_INTEGER;
|
||||
v_end PLS_INTEGER;
|
||||
BEGIN
|
||||
v_start := DBMS_LOB.INSTR(p_raw, p_start_tag, 1, 1);
|
||||
IF v_start = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20071, 'OCI GenAI response is missing ' || p_start_tag);
|
||||
END IF;
|
||||
v_start := v_start + LENGTH(p_start_tag);
|
||||
v_end := DBMS_LOB.INSTR(p_raw, p_end_tag, v_start, 1);
|
||||
IF v_end = 0 OR v_end <= v_start THEN
|
||||
RAISE_APPLICATION_ERROR(-20072, 'OCI GenAI response is missing ' || p_end_tag);
|
||||
END IF;
|
||||
RETURN TRIM(DBMS_LOB.SUBSTR(p_raw, LEAST(v_end - v_start, 32767), v_start));
|
||||
END;
|
||||
|
||||
PROCEDURE upsert_pattern(
|
||||
p_case_id IN VARCHAR2,
|
||||
p_status IN VARCHAR2,
|
||||
p_note IN CLOB
|
||||
) IS
|
||||
BEGIN
|
||||
UPDATE sg_qa_vector_example
|
||||
SET question = v_question,
|
||||
answer_sql = v_sql_pattern,
|
||||
answer_text = v_answer_text,
|
||||
embedding_input = v_embedding_input,
|
||||
embedding = v_embedding,
|
||||
embedding_model = 'cohere.embed-v4.0',
|
||||
reference_status = p_status,
|
||||
reference_kind = 'SQL_PATTERN',
|
||||
target_type = v_target_type,
|
||||
object_role = v_object_role,
|
||||
inspection_status = CASE WHEN p_status = 'APPROVED' THEN 'GENAI_VERIFIED' ELSE 'GENAI_REJECTED' END,
|
||||
inspection_note = p_note,
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_OCI_GENAI_PATTERN'
|
||||
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
|
||||
AND source_case_id = 'PAT-' || p_case_id;
|
||||
|
||||
IF SQL%ROWCOUNT = 0 THEN
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, object_role, inspection_status,
|
||||
inspection_note, verified_at, verified_by, source_case_id, source_type
|
||||
) VALUES (
|
||||
v_question, v_sql_pattern, v_answer_text, v_embedding_input, v_embedding, 'cohere.embed-v4.0',
|
||||
p_status, 'SQL_PATTERN', v_target_type, v_object_role,
|
||||
CASE WHEN p_status = 'APPROVED' THEN 'GENAI_VERIFIED' ELSE 'GENAI_REJECTED' END,
|
||||
p_note, SYSTIMESTAMP, 'SGMP_POC_OCI_GENAI_PATTERN',
|
||||
'PAT-' || p_case_id, 'GENERALIZED_QUESTION_PATTERN'
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
BEGIN
|
||||
FOR source_row IN (
|
||||
SELECT source.source_case_id, source.question, source.answer_sql
|
||||
FROM sg_qa_vector_example source
|
||||
WHERE source.source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sg_qa_vector_example pattern
|
||||
WHERE pattern.source_type = 'GENERALIZED_QUESTION_PATTERN'
|
||||
AND pattern.source_case_id = 'PAT-' || source.source_case_id
|
||||
AND pattern.reference_status = 'APPROVED'
|
||||
)
|
||||
ORDER BY source_case_id
|
||||
) LOOP
|
||||
BEGIN
|
||||
-- The target category comes from the existing OCI GenAI game resolver;
|
||||
-- no alias, prefix, table, or name is transformed in this migration.
|
||||
v_plan_raw := sg_game_query_plan(source_row.question, 5);
|
||||
v_plan := parse_json_result(v_plan_raw);
|
||||
v_target_type := UPPER(NVL(v_plan.get_string('targetType'), 'ANY'));
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL') THEN
|
||||
v_target_type := 'ANY';
|
||||
END IF;
|
||||
|
||||
v_pattern_raw := sg_qa_genai_generalize_pattern(
|
||||
source_row.question, source_row.answer_sql, v_target_type
|
||||
);
|
||||
v_question := section_value(v_pattern_raw, '[[PATTERN_QUESTION]]', '[[STRUCTURAL_SQL_PATTERN]]');
|
||||
v_sql_pattern := section_value(v_pattern_raw, '[[STRUCTURAL_SQL_PATTERN]]', '[[OBJECT_ROLE]]');
|
||||
v_object_role := SUBSTR(section_value(v_pattern_raw, '[[OBJECT_ROLE]]', '[[TARGET_TYPE]]'), 1, 64);
|
||||
IF section_value(v_pattern_raw, '[[TARGET_TYPE]]', '[[APPLICABILITY]]')
|
||||
IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
v_target_type := section_value(v_pattern_raw, '[[TARGET_TYPE]]', '[[APPLICABILITY]]');
|
||||
END IF;
|
||||
v_answer_text := TO_CLOB('Generalized, question-specific structural pattern. '
|
||||
|| 'Current game scope must be supplied only by sg_game_query_plan. Applicability: ')
|
||||
|| section_value(v_pattern_raw, '[[APPLICABILITY]]', '[[END]]');
|
||||
v_embedding_input := TO_CLOB('Question-specific generalized Few-shot pattern:' || CHR(10))
|
||||
|| v_question || CHR(10) || 'Logical role: ' || v_object_role || CHR(10)
|
||||
|| 'Structural SQL pattern:' || CHR(10) || v_sql_pattern;
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_embedding_input, JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
v_validation_raw := sg_qa_genai_validate_pattern(v_pattern_raw);
|
||||
v_status := CASE
|
||||
WHEN REGEXP_SUBSTR(
|
||||
UPPER(section_value(v_validation_raw, '[[CONCRETE_LEAKAGE]]', '[[REASON]]')),
|
||||
'[A-Z]+'
|
||||
) = 'NO'
|
||||
THEN 'APPROVE'
|
||||
ELSE 'REJECT'
|
||||
END;
|
||||
v_validation_note := section_value(v_validation_raw, '[[REASON]]', '[[END]]');
|
||||
|
||||
IF v_status = 'APPROVE' THEN
|
||||
upsert_pattern(source_row.source_case_id, 'APPROVED',
|
||||
'ADB OCI GenAI generated and independently validated a generalized pattern. '
|
||||
|| 'The original customer QA remains evaluation-only. ' || v_validation_note);
|
||||
v_count := v_count + 1;
|
||||
ELSE
|
||||
upsert_pattern(source_row.source_case_id, 'DRAFT',
|
||||
'ADB OCI GenAI rejected the generalized pattern: ' || v_validation_note);
|
||||
END IF;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Persist an auditable non-runtime draft and continue with the other
|
||||
-- customer questions; one malformed LLM response must not block all 47.
|
||||
v_question := source_row.question;
|
||||
v_sql_pattern := TO_CLOB('<PATTERN_GENERATION_FAILED>');
|
||||
v_answer_text := TO_CLOB('No runtime Few-shot pattern: OCI GenAI generalization failed.');
|
||||
v_object_role := 'UNSPECIFIED';
|
||||
v_target_type := 'ANY';
|
||||
v_embedding_input := TO_CLOB('Failed generalized pattern: ') || source_row.question;
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_embedding_input, JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
upsert_pattern(source_row.source_case_id, 'DRAFT',
|
||||
'OCI GenAI pattern generation error: ' || SQLERRM);
|
||||
END;
|
||||
END LOOP;
|
||||
COMMIT;
|
||||
RETURN v_count;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Re-run only the independent OCI Chat safety review after its policy changes.
|
||||
-- It never reads a customer benchmark and never changes the generated pattern.
|
||||
CREATE OR REPLACE FUNCTION sg_qa_revalidate_generalized_patterns
|
||||
RETURN NUMBER AUTHID DEFINER
|
||||
IS
|
||||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||||
v_raw CLOB;
|
||||
v_status VARCHAR2(16);
|
||||
v_reason CLOB;
|
||||
v_start PLS_INTEGER;
|
||||
v_end PLS_INTEGER;
|
||||
v_count NUMBER := 0;
|
||||
|
||||
FUNCTION section_value(
|
||||
p_raw IN CLOB, p_start_tag IN VARCHAR2, p_end_tag IN VARCHAR2
|
||||
) RETURN CLOB IS
|
||||
v_from PLS_INTEGER;
|
||||
v_to PLS_INTEGER;
|
||||
BEGIN
|
||||
v_from := DBMS_LOB.INSTR(p_raw, p_start_tag, 1, 1);
|
||||
IF v_from = 0 THEN RAISE_APPLICATION_ERROR(-20073, 'Missing ' || p_start_tag); END IF;
|
||||
v_from := v_from + LENGTH(p_start_tag);
|
||||
v_to := DBMS_LOB.INSTR(p_raw, p_end_tag, v_from, 1);
|
||||
IF v_to = 0 OR v_to <= v_from THEN RAISE_APPLICATION_ERROR(-20074, 'Missing ' || p_end_tag); END IF;
|
||||
RETURN TRIM(DBMS_LOB.SUBSTR(p_raw, LEAST(v_to - v_from, 32767), v_from));
|
||||
END;
|
||||
BEGIN
|
||||
FOR item IN (
|
||||
SELECT example_id, question, answer_sql, answer_text
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
|
||||
ORDER BY source_case_id
|
||||
) LOOP
|
||||
BEGIN
|
||||
v_raw := sg_qa_genai_validate_pattern(
|
||||
TO_CLOB('[[PATTERN_QUESTION]]') || item.question
|
||||
|| TO_CLOB(CHR(10) || '[[STRUCTURAL_SQL_PATTERN]]') || item.answer_sql
|
||||
|| TO_CLOB(CHR(10) || '[[APPLICABILITY]]') || item.answer_text || CHR(10) || '[[END]]'
|
||||
);
|
||||
v_status := CASE
|
||||
WHEN REGEXP_SUBSTR(
|
||||
UPPER(section_value(v_raw, '[[CONCRETE_LEAKAGE]]', '[[REASON]]')),
|
||||
'[A-Z]+'
|
||||
) = 'NO'
|
||||
THEN 'APPROVE'
|
||||
ELSE 'REJECT'
|
||||
END;
|
||||
v_reason := section_value(v_raw, '[[REASON]]', '[[END]]');
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = CASE WHEN v_status = 'APPROVE' THEN 'APPROVED' ELSE 'DRAFT' END,
|
||||
inspection_status = CASE WHEN v_status = 'APPROVE' THEN 'GENAI_VERIFIED' ELSE 'GENAI_REJECTED' END,
|
||||
inspection_note = 'ADB OCI GenAI independent revalidation: ' || v_reason,
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_OCI_GENAI_PATTERN'
|
||||
WHERE example_id = item.example_id;
|
||||
IF v_status = 'APPROVE' THEN v_count := v_count + 1; END IF;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
v_reason := TO_CLOB('OCI GenAI revalidation error: ' || SQLERRM);
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'DRAFT',
|
||||
inspection_status = 'GENAI_REJECTED',
|
||||
inspection_note = v_reason,
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_OCI_GENAI_PATTERN'
|
||||
WHERE example_id = item.example_id;
|
||||
END;
|
||||
END LOOP;
|
||||
COMMIT;
|
||||
RETURN v_count;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Production retrieval accepts only independently generalized patterns or
|
||||
-- policy templates. Customer QA benchmarks remain evaluation-only forever.
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
|
||||
END IF;
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question, JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
OPEN v_results FOR
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND source_type IN ('GENERALIZED_QUESTION_PATTERN', 'POLICY_TEMPLATE')
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
9
database/adb/110_sg_game_query_plan_admin_grants.sql
Normal file
9
database/adb/110_sg_game_query_plan_admin_grants.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- The MCP DB account owns the OCI GenAI planning functions while the game
|
||||
-- catalog is owned by the data schema. Definer-rights PL/SQL needs direct
|
||||
-- object grants; role grants are not sufficient at compile time.
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'GRANT SELECT ON SGMP_POC.SG_GAME_CATALOG TO ADMIN';
|
||||
EXECUTE IMMEDIATE 'GRANT SELECT ON SGMP_POC.COMN_GAME_ALIAS_BAS TO ADMIN';
|
||||
EXECUTE IMMEDIATE 'GRANT EXECUTE ON SGMP_POC.SG_GAME_CATALOG_SEARCH TO ADMIN';
|
||||
END;
|
||||
/
|
||||
148
database/adb/111_sg_game_daily_au_lookup.sql
Normal file
148
database/adb/111_sg_game_daily_au_lookup.sql
Normal file
@@ -0,0 +1,148 @@
|
||||
-- Deterministic daily-AU lookup for an already resolved game query plan.
|
||||
-- Physical user-master objects are selected only from SG_GAME_CATALOG.
|
||||
-- No game name, alias, prefix, or object name is embedded in this function.
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_game_daily_au_lookup(
|
||||
p_query_plan IN CLOB,
|
||||
p_base_date IN DATE DEFAULT NULL
|
||||
) RETURN CLOB AUTHID DEFINER IS
|
||||
v_plan JSON_OBJECT_T;
|
||||
v_targets JSON_ARRAY_T;
|
||||
v_target JSON_OBJECT_T;
|
||||
v_result JSON_OBJECT_T := JSON_OBJECT_T();
|
||||
v_items JSON_ARRAY_T := JSON_ARRAY_T();
|
||||
v_item JSON_OBJECT_T;
|
||||
v_game_key VARCHAR2(128);
|
||||
v_game_id VARCHAR2(128);
|
||||
v_game_name VARCHAR2(512);
|
||||
v_object_name VARCHAR2(128);
|
||||
v_safe_object_name VARCHAR2(128);
|
||||
v_effective_date DATE;
|
||||
v_au_count NUMBER;
|
||||
v_column_count PLS_INTEGER;
|
||||
v_object_count PLS_INTEGER;
|
||||
v_seen SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
|
||||
v_target_count PLS_INTEGER := 0;
|
||||
|
||||
FUNCTION is_seen(p_game_key IN VARCHAR2) RETURN BOOLEAN IS
|
||||
BEGIN
|
||||
FOR i IN 1 .. v_seen.COUNT LOOP
|
||||
IF v_seen(i) = p_game_key THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
END LOOP;
|
||||
RETURN FALSE;
|
||||
END;
|
||||
|
||||
PROCEDURE add_status(
|
||||
p_game_key IN VARCHAR2,
|
||||
p_status IN VARCHAR2,
|
||||
p_reason IN VARCHAR2
|
||||
) IS
|
||||
BEGIN
|
||||
v_item := JSON_OBJECT_T();
|
||||
v_item.put('gameKey', p_game_key);
|
||||
v_item.put('status', p_status);
|
||||
v_item.put('reason', p_reason);
|
||||
v_items.append(v_item);
|
||||
END;
|
||||
BEGIN
|
||||
IF p_query_plan IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20001, 'queryPlan is required');
|
||||
END IF;
|
||||
|
||||
v_plan := JSON_OBJECT_T.parse(p_query_plan);
|
||||
v_targets := v_plan.get_array('dataEligibleTargets');
|
||||
IF v_targets IS NULL THEN
|
||||
v_targets := v_plan.get_array('targets');
|
||||
END IF;
|
||||
|
||||
IF v_targets IS NOT NULL AND v_targets.get_size > 0 THEN
|
||||
FOR i IN 0 .. v_targets.get_size - 1 LOOP
|
||||
v_target := TREAT(v_targets.get(i) AS JSON_OBJECT_T);
|
||||
IF v_target IS NULL OR NOT v_target.has('gameKey') THEN
|
||||
CONTINUE;
|
||||
END IF;
|
||||
v_game_key := v_target.get_string('gameKey');
|
||||
IF v_game_key IS NULL OR is_seen(v_game_key) THEN
|
||||
CONTINUE;
|
||||
END IF;
|
||||
v_seen.EXTEND;
|
||||
v_seen(v_seen.COUNT) := v_game_key;
|
||||
v_target_count := v_target_count + 1;
|
||||
|
||||
BEGIN
|
||||
SELECT game_id, game_nm, user_master_object_name
|
||||
INTO v_game_id, v_game_name, v_object_name
|
||||
FROM sg_game_catalog
|
||||
WHERE game_key = v_game_key
|
||||
AND active_yn = 'Y';
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
add_status(v_game_key, 'UNAVAILABLE', 'Catalog target is not active.');
|
||||
CONTINUE;
|
||||
END;
|
||||
|
||||
IF v_object_name IS NULL THEN
|
||||
add_status(v_game_key, 'UNAVAILABLE', 'No approved user-master object is registered.');
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
v_safe_object_name := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(v_object_name));
|
||||
SELECT COUNT(*) INTO v_object_count
|
||||
FROM user_objects
|
||||
WHERE object_name = v_safe_object_name
|
||||
AND object_type IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
|
||||
AND status = 'VALID';
|
||||
SELECT COUNT(*) INTO v_column_count
|
||||
FROM user_tab_columns
|
||||
WHERE table_name = v_safe_object_name
|
||||
AND column_name IN ('GUID', 'BASE_DT', 'AU_FLAG', 'EXPT_USER_YN');
|
||||
IF v_object_count = 0 OR v_column_count <> 4 THEN
|
||||
add_status(v_game_key, 'UNAVAILABLE', 'Approved user-master object is not query-ready.');
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
IF p_base_date IS NULL THEN
|
||||
EXECUTE IMMEDIATE 'SELECT MAX(BASE_DT) FROM ' || v_safe_object_name
|
||||
INTO v_effective_date;
|
||||
ELSE
|
||||
v_effective_date := TRUNC(p_base_date);
|
||||
END IF;
|
||||
IF v_effective_date IS NULL THEN
|
||||
add_status(v_game_key, 'NO_DATA', 'No available base date in the selected object.');
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
EXECUTE IMMEDIATE
|
||||
'SELECT COUNT(DISTINCT GUID) FROM ' || v_safe_object_name
|
||||
|| ' WHERE BASE_DT = :1 AND AU_FLAG = 1 AND EXPT_USER_YN = ''N'''
|
||||
INTO v_au_count USING v_effective_date;
|
||||
|
||||
v_item := JSON_OBJECT_T();
|
||||
v_item.put('gameKey', v_game_key);
|
||||
v_item.put('gameId', v_game_id);
|
||||
v_item.put('gameName', v_game_name);
|
||||
v_item.put('objectName', v_safe_object_name);
|
||||
v_item.put('baseDate', TO_CHAR(v_effective_date, 'YYYY-MM-DD'));
|
||||
v_item.put('auCount', v_au_count);
|
||||
v_item.put('status', 'READY');
|
||||
v_item.put('sqlTemplate',
|
||||
'SELECT COUNT(DISTINCT GUID) AS AU_COUNT FROM <catalog_user_master_object> '
|
||||
|| 'WHERE BASE_DT = :baseDate AND AU_FLAG = 1 AND EXPT_USER_YN = ''N''');
|
||||
v_items.append(v_item);
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
v_result.put('status', CASE WHEN v_target_count = 0 THEN 'NO_GAME_TARGET' ELSE 'GAME_AU_LOOKUP' END);
|
||||
v_result.put('targetType', NVL(v_plan.get_string('targetType'), 'NONE'));
|
||||
IF p_base_date IS NULL THEN
|
||||
v_result.put_null('requestedBaseDate');
|
||||
ELSE
|
||||
v_result.put('requestedBaseDate', TO_CHAR(TRUNC(p_base_date), 'YYYY-MM-DD'));
|
||||
END IF;
|
||||
v_result.put('targetCount', v_target_count);
|
||||
v_result.put('items', v_items);
|
||||
RETURN v_result.to_clob;
|
||||
END;
|
||||
/
|
||||
81
database/adb/112_sgmp_select_ai_oci_llama4scout_profile.sql
Normal file
81
database/adb/112_sgmp_select_ai_oci_llama4scout_profile.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- Creates a non-operational comparison profile for Smilegate game-scope chat.
|
||||
-- The active GPT profile remains unchanged. Provider credentials and profile
|
||||
-- metadata are copied from it so the only comparison variable is the model.
|
||||
|
||||
DECLARE
|
||||
v_exists PLS_INTEGER;
|
||||
v_credential_name VARCHAR2(128);
|
||||
v_region VARCHAR2(128);
|
||||
v_compartment_id VARCHAR2(4000);
|
||||
v_attributes CLOB;
|
||||
v_attribute_json JSON_OBJECT_T := JSON_OBJECT_T();
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_exists
|
||||
FROM user_cloud_ai_profiles
|
||||
WHERE profile_name = 'SGMP_POC_OCI_LLAMA4SCOUT';
|
||||
|
||||
IF v_exists > 0 THEN
|
||||
DBMS_CLOUD_AI.DROP_PROFILE(
|
||||
profile_name => 'SGMP_POC_OCI_LLAMA4SCOUT',
|
||||
force => TRUE
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 128, 1)
|
||||
INTO v_credential_name
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'credential_name';
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 128, 1)
|
||||
INTO v_region
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'region';
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 4000, 1)
|
||||
INTO v_compartment_id
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'oci_compartment_id';
|
||||
|
||||
v_attribute_json.put('provider', 'oci');
|
||||
v_attribute_json.put('credential_name', v_credential_name);
|
||||
v_attribute_json.put('model', 'meta.llama-4-scout-17b-16e-instruct');
|
||||
v_attribute_json.put('region', v_region);
|
||||
v_attribute_json.put('oci_compartment_id', v_compartment_id);
|
||||
v_attributes := v_attribute_json.to_clob;
|
||||
|
||||
DBMS_CLOUD_AI.CREATE_PROFILE(
|
||||
profile_name => 'SGMP_POC_OCI_LLAMA4SCOUT',
|
||||
attributes => v_attributes,
|
||||
description => 'Non-operational Smilegate game-scope latency comparison'
|
||||
);
|
||||
|
||||
FOR source_attribute IN (
|
||||
SELECT attribute_name, attribute_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
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_LLAMA4SCOUT',
|
||||
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_LLAMA4SCOUT'
|
||||
AND attribute_name IN (
|
||||
'provider', 'model', 'credential_name', 'region', 'oci_compartment_id'
|
||||
)
|
||||
ORDER BY attribute_name;
|
||||
60
database/adb/113_sgmp_game_scope_chat_profile_benchmark.sql
Normal file
60
database/adb/113_sgmp_game_scope_chat_profile_benchmark.sql
Normal file
@@ -0,0 +1,60 @@
|
||||
-- Read-only latency and JSON-shape comparison for the game-mention extraction
|
||||
-- stage. Korean input is reconstructed from UTF-8 base64 for SQLcl safety.
|
||||
|
||||
set serveroutput on size unlimited
|
||||
|
||||
DECLARE
|
||||
v_question CLOB := utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'66Gc65Oc64KY7J247J20656RIOy5tOygnOuCmOydmCAyMDI264WEIDfsm5QgMTXsnbwgQVXrpbwg6rCB6rCBIOyVjOugpOykmC4='
|
||||
)),
|
||||
'AL32UTF8'
|
||||
);
|
||||
v_prompt CLOB;
|
||||
v_result CLOB;
|
||||
v_json JSON_OBJECT_T;
|
||||
v_started PLS_INTEGER;
|
||||
v_elapsed_seconds NUMBER;
|
||||
|
||||
PROCEDURE run_profile(p_profile_name IN VARCHAR2) IS
|
||||
BEGIN
|
||||
v_started := DBMS_UTILITY.GET_TIME;
|
||||
v_result := DBMS_CLOUD_AI.GENERATE(
|
||||
prompt => v_prompt,
|
||||
profile_name => p_profile_name,
|
||||
action => 'chat'
|
||||
);
|
||||
v_elapsed_seconds := (DBMS_UTILITY.GET_TIME - v_started) / 100;
|
||||
v_json := JSON_OBJECT_T.parse(v_result);
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
p_profile_name
|
||||
|| '|elapsed_seconds=' || TO_CHAR(v_elapsed_seconds, 'FM9990D00')
|
||||
|| '|scope_hint=' || NVL(v_json.get_string('scope_hint'), 'NULL')
|
||||
|| '|mention_count=' || v_json.get_array('game_mentions').get_size
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
p_profile_name || '|elapsed_seconds='
|
||||
|| TO_CHAR(v_elapsed_seconds, 'FM9990D00')
|
||||
|| '|ERROR|' || SQLCODE || '|' || SUBSTR(SQLERRM, 1, 300)
|
||||
);
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
p_profile_name || '|raw_response=' || DBMS_LOB.SUBSTR(v_result, 1000, 1)
|
||||
);
|
||||
END;
|
||||
BEGIN
|
||||
v_prompt := 'Extract only game-name mentions from the user question. '
|
||||
|| 'Metrics, acronyms, dates, filters, and database object or column names are not game names unless they are themselves an explicit game title. '
|
||||
|| 'When a title-like noun directly qualifies a game data request such as user master, character, sales, AU, NRU, server, or game log, preserve that noun as a game-name mention even when it is not in a catalog. '
|
||||
|| 'Do not discard an unknown title merely because it cannot be resolved. General scope words such as common, overall, all, total, or every are not game-name mentions unless they are part of an explicit title. '
|
||||
|| 'Return exactly one JSON object with keys game_mentions (array of strings) '
|
||||
|| 'and scope_hint (GLOBAL, SINGLE_GAME, MULTI_GAME, ALL_GAMES, UNKNOWN). '
|
||||
|| 'Do not resolve names to IDs and do not generate SQL. '
|
||||
|| 'Return raw JSON only: no prose, no Markdown, and no code fence. Question: '
|
||||
|| v_question;
|
||||
|
||||
run_profile('SGMP_POC_OCI_GPT54MINI');
|
||||
run_profile('SGMP_POC_OCI_LLAMA4SCOUT');
|
||||
END;
|
||||
/
|
||||
@@ -0,0 +1,90 @@
|
||||
-- Creates non-operational OCI profiles for game-scope extraction benchmarks.
|
||||
-- Every profile inherits the active GPT profile's OCI credential, region,
|
||||
-- object list, and metadata. Only model is varied.
|
||||
|
||||
DECLARE
|
||||
v_credential_name VARCHAR2(128);
|
||||
v_region VARCHAR2(128);
|
||||
v_compartment_id VARCHAR2(4000);
|
||||
v_attributes CLOB;
|
||||
v_attribute_json JSON_OBJECT_T;
|
||||
v_exists PLS_INTEGER;
|
||||
|
||||
PROCEDURE create_candidate(
|
||||
p_profile_name IN VARCHAR2,
|
||||
p_model IN VARCHAR2
|
||||
) IS
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_exists
|
||||
FROM user_cloud_ai_profiles
|
||||
WHERE profile_name = p_profile_name;
|
||||
|
||||
IF v_exists > 0 THEN
|
||||
DBMS_CLOUD_AI.DROP_PROFILE(profile_name => p_profile_name, force => TRUE);
|
||||
END IF;
|
||||
|
||||
v_attribute_json := JSON_OBJECT_T();
|
||||
v_attribute_json.put('provider', 'oci');
|
||||
v_attribute_json.put('credential_name', v_credential_name);
|
||||
v_attribute_json.put('model', p_model);
|
||||
v_attribute_json.put('region', v_region);
|
||||
v_attribute_json.put('oci_compartment_id', v_compartment_id);
|
||||
v_attributes := v_attribute_json.to_clob;
|
||||
|
||||
DBMS_CLOUD_AI.CREATE_PROFILE(
|
||||
profile_name => p_profile_name,
|
||||
attributes => v_attributes,
|
||||
description => 'Non-operational Smilegate game-scope benchmark profile'
|
||||
);
|
||||
|
||||
FOR source_attribute IN (
|
||||
SELECT attribute_name, attribute_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
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 => p_profile_name,
|
||||
attribute_name => source_attribute.attribute_name,
|
||||
attribute_value => source_attribute.attribute_value
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
BEGIN
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 128, 1)
|
||||
INTO v_credential_name
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'credential_name';
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 128, 1)
|
||||
INTO v_region
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'region';
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 4000, 1)
|
||||
INTO v_compartment_id
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'oci_compartment_id';
|
||||
|
||||
create_candidate('SGMP_SCOPE_COHERE_VISION', 'cohere.command-a-vision');
|
||||
create_candidate('SGMP_SCOPE_COHERE_COMMAND', 'cohere.command-latest');
|
||||
create_candidate('SGMP_SCOPE_COHERE_PLUS', 'cohere.command-plus-latest');
|
||||
create_candidate('SGMP_SCOPE_GEMINI_FLASH', 'google.gemini-2.5-flash-lite');
|
||||
create_candidate('SGMP_SCOPE_LLAMA_MAV', 'meta.llama-4-maverick-17b-128e-instruct-fp8');
|
||||
create_candidate('SGMP_SCOPE_GROK_NONR', 'xai.grok-4.20-non-reasoning');
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT profile_name, attribute_value AS model
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name LIKE 'SGMP_SCOPE_%'
|
||||
AND attribute_name = 'model'
|
||||
ORDER BY profile_name;
|
||||
112
database/adb/115_sgmp_scope_chat_candidate_benchmark.sql
Normal file
112
database/adb/115_sgmp_scope_chat_candidate_benchmark.sql
Normal file
@@ -0,0 +1,112 @@
|
||||
-- Read-only benchmark for candidate game-scope extraction profiles.
|
||||
-- Korean test questions use UTF-8 base64 reconstruction for SQLcl safety.
|
||||
|
||||
set serveroutput on size unlimited
|
||||
prompt SG_SCOPE_PROFILE_BENCHMARK_LOADED
|
||||
|
||||
DECLARE
|
||||
TYPE t_case IS RECORD (
|
||||
case_name VARCHAR2(12),
|
||||
question CLOB,
|
||||
expected_scope VARCHAR2(20),
|
||||
expected_mentions PLS_INTEGER
|
||||
);
|
||||
TYPE t_cases IS TABLE OF t_case INDEX BY PLS_INTEGER;
|
||||
v_cases t_cases;
|
||||
v_prompt_prefix CLOB :=
|
||||
'Extract only game-name mentions from the user question. '
|
||||
|| 'Metrics, acronyms, dates, filters, and database object or column names are not game names unless they are themselves an explicit game title. '
|
||||
|| 'When a title-like noun directly qualifies a game data request such as user master, character, sales, AU, NRU, server, or game log, preserve that noun as a game-name mention even when it is not in a catalog. '
|
||||
|| 'Do not discard an unknown title merely because it cannot be resolved. General scope words such as common, overall, all, total, or every are not game-name mentions unless they are part of an explicit title. '
|
||||
|| 'Return exactly one JSON object with keys game_mentions (array of strings) '
|
||||
|| 'and scope_hint (GLOBAL, SINGLE_GAME, MULTI_GAME, ALL_GAMES, UNKNOWN). '
|
||||
|| 'Do not resolve names to IDs and do not generate SQL. '
|
||||
|| 'Return raw JSON only: no prose, no Markdown, and no code fence. Question: ';
|
||||
v_result CLOB;
|
||||
v_json JSON_OBJECT_T;
|
||||
v_started PLS_INTEGER;
|
||||
v_elapsed NUMBER;
|
||||
v_scope VARCHAR2(20);
|
||||
v_mentions PLS_INTEGER;
|
||||
v_raw_json VARCHAR2(5);
|
||||
|
||||
PROCEDURE run_case(
|
||||
p_profile_name IN VARCHAR2,
|
||||
p_case t_case
|
||||
) IS
|
||||
BEGIN
|
||||
v_started := DBMS_UTILITY.GET_TIME;
|
||||
v_result := DBMS_CLOUD_AI.GENERATE(
|
||||
prompt => v_prompt_prefix || p_case.question,
|
||||
profile_name => p_profile_name,
|
||||
action => 'chat'
|
||||
);
|
||||
v_elapsed := (DBMS_UTILITY.GET_TIME - v_started) / 100;
|
||||
v_json := JSON_OBJECT_T.parse(v_result);
|
||||
v_raw_json := 'TRUE';
|
||||
v_scope := v_json.get_string('scope_hint');
|
||||
v_mentions := v_json.get_array('game_mentions').get_size;
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
p_profile_name || '|' || p_case.case_name
|
||||
|| '|seconds=' || TO_CHAR(v_elapsed, 'FM9990D00')
|
||||
|| '|raw_json=' || v_raw_json
|
||||
|| '|scope=' || NVL(v_scope, 'NULL')
|
||||
|| '|mentions=' || v_mentions
|
||||
|| '|expected=' || p_case.expected_scope || '/' || p_case.expected_mentions
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
p_profile_name || '|' || p_case.case_name
|
||||
|| '|seconds=' || TO_CHAR(v_elapsed, 'FM9990D00')
|
||||
|| '|ERROR=' || SQLCODE || '|' || SUBSTR(SQLERRM, 1, 180)
|
||||
);
|
||||
END;
|
||||
BEGIN
|
||||
DBMS_OUTPUT.PUT_LINE('benchmark_started');
|
||||
v_cases(1).case_name := 'NONE';
|
||||
v_cases(1).question := utl_i18n.raw_to_char(utl_encode.base64_decode(
|
||||
utl_raw.cast_to_raw('7KCE7LK0IOunpOy2nCDslYzroKTspJgu')), 'AL32UTF8');
|
||||
v_cases(1).expected_scope := 'GLOBAL';
|
||||
v_cases(1).expected_mentions := 0;
|
||||
|
||||
v_cases(2).case_name := 'SINGLE';
|
||||
v_cases(2).question := utl_i18n.raw_to_char(utl_encode.base64_decode(
|
||||
utl_raw.cast_to_raw('7Lm07KCc64KYIOy1nOyLoCBBVSDslYzroKTspJgu')), 'AL32UTF8');
|
||||
v_cases(2).expected_scope := 'SINGLE_GAME';
|
||||
v_cases(2).expected_mentions := 1;
|
||||
|
||||
v_cases(3).case_name := 'MULTI';
|
||||
v_cases(3).question := utl_i18n.raw_to_char(utl_encode.base64_decode(
|
||||
utl_raw.cast_to_raw('66Gc65Oc64KY7J247J20656RIOy5tOygnOuCmOydmCAyMDI264WEIDfsm5QgMTXsnbwgQVXrpbwg6rCB6rCBIOyVjOugpOykmC4=')), 'AL32UTF8');
|
||||
v_cases(3).expected_scope := 'MULTI_GAME';
|
||||
v_cases(3).expected_mentions := 2;
|
||||
|
||||
v_cases(4).case_name := 'ALL';
|
||||
v_cases(4).question := utl_i18n.raw_to_char(utl_encode.base64_decode(
|
||||
utl_raw.cast_to_raw('7KCE7LK0IOqyjOyehOydmCDrp6Tstpwg7JWM66Ck7KSYLg==')), 'AL32UTF8');
|
||||
v_cases(4).expected_scope := 'ALL_GAMES';
|
||||
v_cases(4).expected_mentions := 0;
|
||||
|
||||
FOR profile_row IN (
|
||||
SELECT profile_name
|
||||
FROM user_cloud_ai_profiles
|
||||
WHERE profile_name IN (
|
||||
'SGMP_POC_OCI_GPT54MINI',
|
||||
'SGMP_SCOPE_COHERE_VISION',
|
||||
'SGMP_SCOPE_COHERE_COMMAND',
|
||||
'SGMP_SCOPE_COHERE_PLUS',
|
||||
'SGMP_SCOPE_GEMINI_FLASH',
|
||||
'SGMP_SCOPE_LLAMA_MAV',
|
||||
'SGMP_SCOPE_GROK_NONR'
|
||||
)
|
||||
ORDER BY profile_name
|
||||
) LOOP
|
||||
DBMS_OUTPUT.PUT_LINE('profile=' || profile_row.profile_name);
|
||||
FOR i IN 1 .. 4 LOOP
|
||||
run_case(profile_row.profile_name, v_cases(i));
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END;
|
||||
/
|
||||
prompt SG_SCOPE_PROFILE_BENCHMARK_COMPLETED
|
||||
122
database/adb/116_sg_game_catalog_alias_embeddings.sql
Normal file
122
database/adb/116_sg_game_catalog_alias_embeddings.sql
Normal file
@@ -0,0 +1,122 @@
|
||||
-- Store all game-name variants as one JSON array per game and embed that JSON
|
||||
-- as the canonical game-search vector. No customer game name is hardcoded.
|
||||
|
||||
DECLARE
|
||||
v_column_count PLS_INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_column_count
|
||||
FROM user_tab_columns
|
||||
WHERE table_name = 'SG_GAME_CATALOG'
|
||||
AND column_name = 'ALIASES_JSON';
|
||||
|
||||
IF v_column_count = 0 THEN
|
||||
EXECUTE IMMEDIATE 'ALTER TABLE sg_game_catalog ADD (aliases_json CLOB)';
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
UPDATE sg_game_catalog
|
||||
SET aliases_json = '[]'
|
||||
WHERE aliases_json IS NULL;
|
||||
/
|
||||
|
||||
DECLARE
|
||||
v_constraint_count PLS_INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_constraint_count
|
||||
FROM user_constraints
|
||||
WHERE table_name = 'SG_GAME_CATALOG'
|
||||
AND constraint_name = 'SG_GAME_CATALOG_ALIASES_JS_CK';
|
||||
|
||||
IF v_constraint_count = 0 THEN
|
||||
EXECUTE IMMEDIATE
|
||||
'ALTER TABLE sg_game_catalog ADD CONSTRAINT sg_game_catalog_aliases_js_ck '
|
||||
|| 'CHECK (aliases_json IS JSON)';
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
MERGE INTO sg_game_catalog c
|
||||
USING (
|
||||
WITH source_alias AS (
|
||||
SELECT game_id AS game_key, game_nm AS alias_name
|
||||
FROM comn_game_alias_bas
|
||||
WHERE use_yn = 'Y' AND game_nm IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT game_id, game_alias_nm
|
||||
FROM comn_game_alias_bas
|
||||
WHERE use_yn = 'Y' AND game_alias_nm IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT game_id, game_id
|
||||
FROM comn_game_alias_bas
|
||||
WHERE use_yn = 'Y' AND game_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT game_id, game_prefix
|
||||
FROM comn_game_alias_bas
|
||||
WHERE use_yn = 'Y' AND game_prefix IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT game_key, display_name
|
||||
FROM sg_game_scope_registry
|
||||
WHERE active_yn = 'Y' AND display_name IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT game_key, game_alias
|
||||
FROM sg_game_scope_registry
|
||||
WHERE active_yn = 'Y' AND game_alias IS NOT NULL
|
||||
),
|
||||
deduplicated_alias AS (
|
||||
SELECT game_key, alias_name
|
||||
FROM source_alias
|
||||
WHERE TRIM(alias_name) IS NOT NULL
|
||||
GROUP BY game_key, alias_name
|
||||
)
|
||||
SELECT game_key,
|
||||
JSON_ARRAYAGG(alias_name ORDER BY alias_name RETURNING CLOB) AS aliases_json
|
||||
FROM deduplicated_alias
|
||||
GROUP BY game_key
|
||||
) s
|
||||
ON (c.game_key = s.game_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
c.aliases_json = s.aliases_json,
|
||||
c.updated_at = SYSTIMESTAMP;
|
||||
/
|
||||
|
||||
-- A game has one canonical vector made from its complete JSON alias array.
|
||||
UPDATE sg_game_catalog c
|
||||
SET c.embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
c.aliases_json,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
),
|
||||
c.updated_at = SYSTIMESTAMP
|
||||
WHERE c.active_yn = 'Y';
|
||||
/
|
||||
|
||||
COMMENT ON COLUMN sg_game_catalog.aliases_json IS
|
||||
'Canonical JSON string array of every game-name variant used as the embedding input.';
|
||||
COMMENT ON COLUMN sg_game_catalog.embedding IS
|
||||
'One vector per game, generated from the complete aliases_json array.';
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_game_catalog_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 5
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER IS
|
||||
v_query VECTOR;
|
||||
v_result SYS_REFCURSOR;
|
||||
BEGIN
|
||||
v_query := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question,
|
||||
JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
OPEN v_result FOR
|
||||
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
|
||||
user_master_object_name,
|
||||
VECTOR_DISTANCE(embedding, v_query, COSINE) AS cosine_distance
|
||||
FROM sg_game_catalog
|
||||
WHERE active_yn = 'Y' AND embedding IS NOT NULL
|
||||
ORDER BY VECTOR_DISTANCE(embedding, v_query, COSINE), priority, game_key
|
||||
FETCH FIRST LEAST(GREATEST(NVL(p_top_k, 5), 1), 20) ROWS ONLY;
|
||||
RETURN v_result;
|
||||
END;
|
||||
/
|
||||
68
database/adb/117_sg_game_scope_policy.sql
Normal file
68
database/adb/117_sg_game_scope_policy.sql
Normal file
@@ -0,0 +1,68 @@
|
||||
-- Customer-managed score policy for vector-only game identity resolution.
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE q'[
|
||||
CREATE TABLE sg_game_scope_policy (
|
||||
policy_key VARCHAR2(128) PRIMARY KEY,
|
||||
number_value NUMBER,
|
||||
text_value VARCHAR2(4000),
|
||||
description VARCHAR2(1000) NOT NULL,
|
||||
active_yn CHAR(1) DEFAULT 'Y' NOT NULL,
|
||||
updated_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT sg_game_scope_policy_active_ck CHECK (active_yn IN ('Y', 'N'))
|
||||
)]';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -955 THEN RAISE; END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
MERGE INTO sg_game_scope_policy t
|
||||
USING (
|
||||
SELECT 'GAME_ALIAS_MAX_COSINE_DISTANCE' AS policy_key,
|
||||
0.500000 AS number_value,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS text_value,
|
||||
'Maximum cosine distance for accepting the closest independently embedded game alias.' AS description
|
||||
FROM dual
|
||||
UNION ALL
|
||||
SELECT 'SCOPE_GUIDANCE_NONE', NULL,
|
||||
'{"mode":"GAME_UNSPECIFIED","allowGameScopedObjects":false,"targetExecution":"COMMON_OBJECTS_OR_ZERO_ROW","instruction":"No game was selected. Do not use a game-scoped object. Use only a game-neutral common object when it answers the question; otherwise return a zero-row result."}',
|
||||
'Prompt guidance for a question without a selected game.'
|
||||
FROM dual
|
||||
UNION ALL
|
||||
SELECT 'SCOPE_GUIDANCE_SINGLE', NULL,
|
||||
'{"mode":"EXACT_TARGETS","allowGameScopedObjects":true,"targetExecution":"ONLY_RESOLVED_TARGETS","instruction":"Use only the resolved target in targets. Do not select another game-scoped object."}',
|
||||
'Prompt guidance for exactly one resolved game target.'
|
||||
FROM dual
|
||||
UNION ALL
|
||||
SELECT 'SCOPE_GUIDANCE_MULTI', NULL,
|
||||
'{"mode":"MULTIPLE_TARGETS","allowGameScopedObjects":true,"targetExecution":"ALL_RESOLVED_TARGETS","instruction":"Return results for all resolved available targets. Preserve unresolved targets as unavailable; do not replace them with another game."}',
|
||||
'Prompt guidance for multiple game targets.'
|
||||
FROM dual
|
||||
UNION ALL
|
||||
SELECT 'SCOPE_GUIDANCE_ALL', NULL,
|
||||
'{"mode":"ALL_CATALOG_TARGETS","allowGameScopedObjects":true,"targetExecution":"ALL_AVAILABLE_CATALOG_TARGETS","instruction":"Use all available catalog targets. Do not invent games or game-scoped objects outside the catalog."}',
|
||||
'Prompt guidance for every catalog game.'
|
||||
FROM dual
|
||||
) s
|
||||
ON (t.policy_key = s.policy_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.text_value = CASE
|
||||
WHEN s.policy_key LIKE 'SCOPE_GUIDANCE_%' THEN s.text_value
|
||||
ELSE t.text_value
|
||||
END,
|
||||
t.description = s.description,
|
||||
t.active_yn = 'Y',
|
||||
t.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
policy_key, number_value, text_value, description, active_yn
|
||||
) VALUES (
|
||||
s.policy_key, s.number_value, s.text_value, s.description, 'Y'
|
||||
);
|
||||
/
|
||||
|
||||
COMMENT ON TABLE sg_game_scope_policy IS
|
||||
'Customer-managed game scope policy values; changing a value requires no application deployment.';
|
||||
COMMENT ON COLUMN sg_game_scope_policy.number_value IS
|
||||
'Numeric policy value. GAME_ALIAS_MAX_COSINE_DISTANCE applies to the closest alias vector.';
|
||||
/
|
||||
82
database/adb/118_sgmp_qa_vector_quality_threshold.sql
Normal file
82
database/adb/118_sgmp_qa_vector_quality_threshold.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- Customer-managed quality floor for runtime Few-shot retrieval.
|
||||
-- Lower cosine distance is more similar. The value is data, not application code.
|
||||
MERGE INTO sg_game_scope_policy t
|
||||
USING (
|
||||
SELECT 'QA_VECTOR_MAX_COSINE_DISTANCE' AS policy_key,
|
||||
0.650000 AS number_value,
|
||||
'Maximum cosine distance accepted for a runtime approved Few-shot example.' AS description
|
||||
FROM dual
|
||||
) s
|
||||
ON (t.policy_key = s.policy_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.number_value = s.number_value,
|
||||
t.description = s.description,
|
||||
t.active_yn = 'Y',
|
||||
t.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
policy_key, number_value, text_value, description, active_yn
|
||||
) VALUES (
|
||||
s.policy_key, s.number_value, NULL, s.description, 'Y'
|
||||
);
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
v_max_cosine_distance NUMBER;
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
|
||||
END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
|
||||
END IF;
|
||||
|
||||
SELECT number_value
|
||||
INTO v_max_cosine_distance
|
||||
FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
|
||||
AND active_yn = 'Y'
|
||||
AND number_value IS NOT NULL;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question, JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
cosine_distance
|
||||
FROM (
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
-- A generated structural pattern is review material, not a runtime
|
||||
-- Few-shot. Runtime examples must have human verification and an
|
||||
-- executable SQL body rather than unresolved logical placeholders.
|
||||
AND inspection_status = 'VERIFIED'
|
||||
AND answer_sql IS NOT NULL
|
||||
AND NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i')
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
)
|
||||
WHERE cosine_distance <= v_max_cosine_distance
|
||||
ORDER BY cosine_distance, example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMENT ON TABLE sg_game_scope_policy IS
|
||||
'Customer-managed game scope and runtime retrieval policy values; changing a value requires no application deployment.';
|
||||
/
|
||||
78
database/adb/118_sgmp_scope_command_profile.sql
Normal file
78
database/adb/118_sgmp_scope_command_profile.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- Operational OCI Cohere profile for short game-name and scope extraction.
|
||||
-- It inherits the active GPT profile's OCI credential, region, and metadata.
|
||||
|
||||
DECLARE
|
||||
v_credential_name VARCHAR2(128);
|
||||
v_region VARCHAR2(128);
|
||||
v_compartment_id VARCHAR2(4000);
|
||||
v_attributes CLOB;
|
||||
v_attribute_json JSON_OBJECT_T;
|
||||
v_exists PLS_INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_exists
|
||||
FROM user_cloud_ai_profiles
|
||||
WHERE profile_name = 'SGMP_POC_OCI_COHERE_COMMAND';
|
||||
|
||||
IF v_exists > 0 THEN
|
||||
DBMS_CLOUD_AI.DROP_PROFILE(
|
||||
profile_name => 'SGMP_POC_OCI_COHERE_COMMAND',
|
||||
force => TRUE
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 128, 1)
|
||||
INTO v_credential_name
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'credential_name';
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 128, 1)
|
||||
INTO v_region
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'region';
|
||||
|
||||
SELECT DBMS_LOB.SUBSTR(attribute_value, 4000, 1)
|
||||
INTO v_compartment_id
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'oci_compartment_id';
|
||||
|
||||
v_attribute_json := JSON_OBJECT_T();
|
||||
v_attribute_json.put('provider', 'oci');
|
||||
v_attribute_json.put('credential_name', v_credential_name);
|
||||
v_attribute_json.put('model', 'cohere.command-latest');
|
||||
v_attribute_json.put('region', v_region);
|
||||
v_attribute_json.put('oci_compartment_id', v_compartment_id);
|
||||
v_attributes := v_attribute_json.to_clob;
|
||||
|
||||
DBMS_CLOUD_AI.CREATE_PROFILE(
|
||||
profile_name => 'SGMP_POC_OCI_COHERE_COMMAND',
|
||||
attributes => v_attributes,
|
||||
description => 'Smilegate operational OCI Cohere Command profile for game scope extraction'
|
||||
);
|
||||
|
||||
FOR source_attribute IN (
|
||||
SELECT attribute_name, attribute_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
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_COHERE_COMMAND',
|
||||
attribute_name => source_attribute.attribute_name,
|
||||
attribute_value => source_attribute.attribute_value
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT profile_name, attribute_value AS model
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_COHERE_COMMAND'
|
||||
AND attribute_name = 'model';
|
||||
38
database/adb/119_sgmp_remove_invalid_std12_fewshot.sql
Normal file
38
database/adb/119_sgmp_remove_invalid_std12_fewshot.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- STD-12 is a multi-target orchestration case, not a reusable SQL few-shot.
|
||||
-- Preserve SG_AI_QA_QUESTION as the customer benchmark; remove only its
|
||||
-- invalid vector-example row so it cannot be managed as a few-shot.
|
||||
|
||||
DECLARE
|
||||
v_count PLS_INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_count
|
||||
FROM sg_qa_vector_example
|
||||
WHERE example_id = 51
|
||||
AND source_case_id = 'STD-12'
|
||||
AND source_type = 'CUSTOMER_QA_BENCHMARK';
|
||||
|
||||
IF v_count <> 1 THEN
|
||||
RAISE_APPLICATION_ERROR(-20051, 'Expected exactly one invalid STD-12 few-shot row.');
|
||||
END IF;
|
||||
|
||||
DELETE FROM sg_qa_vector_example
|
||||
WHERE example_id = 51
|
||||
AND source_case_id = 'STD-12'
|
||||
AND source_type = 'CUSTOMER_QA_BENCHMARK';
|
||||
|
||||
IF SQL%ROWCOUNT <> 1 THEN
|
||||
RAISE_APPLICATION_ERROR(-20052, 'Invalid STD-12 few-shot row was not deleted.');
|
||||
END IF;
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT COUNT(*) AS remaining_fewshot_rows
|
||||
FROM sg_qa_vector_example
|
||||
WHERE example_id = 51;
|
||||
|
||||
SELECT COUNT(*) AS preserved_question_rows
|
||||
FROM sg_ai_qa_question
|
||||
WHERE question_code = 'STD-12';
|
||||
84
database/adb/121_sg_game_catalog_identity_duality_view.sql
Normal file
84
database/adb/121_sg_game_catalog_identity_duality_view.sql
Normal file
@@ -0,0 +1,84 @@
|
||||
-- One DB-owned JSON identity document per game. No game value is hardcoded.
|
||||
-- The relational alias child makes aliases a nested Duality View array.
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE q'[
|
||||
CREATE TABLE sg_game_catalog_identity_alias (
|
||||
game_key VARCHAR2(128) NOT NULL,
|
||||
alias_value VARCHAR2(512) NOT NULL,
|
||||
CONSTRAINT sg_game_catalog_identity_alias_pk PRIMARY KEY (game_key, alias_value),
|
||||
CONSTRAINT sg_game_catalog_identity_alias_fk FOREIGN KEY (game_key)
|
||||
REFERENCES sg_game_catalog (game_key)
|
||||
)]';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -955 THEN RAISE; END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
MERGE INTO sg_game_catalog_identity_alias target
|
||||
USING (
|
||||
SELECT c.game_key, aliases.alias_value
|
||||
FROM sg_game_catalog c,
|
||||
JSON_TABLE(
|
||||
c.aliases_json,
|
||||
'$[*]' COLUMNS (alias_value VARCHAR2(512) PATH '$')
|
||||
) aliases
|
||||
WHERE c.active_yn = 'Y'
|
||||
) source
|
||||
ON (target.game_key = source.game_key AND target.alias_value = source.alias_value)
|
||||
WHEN NOT MATCHED THEN INSERT (game_key, alias_value)
|
||||
VALUES (source.game_key, source.alias_value);
|
||||
/
|
||||
|
||||
DELETE FROM sg_game_catalog_identity_alias target
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sg_game_catalog c,
|
||||
JSON_TABLE(
|
||||
c.aliases_json,
|
||||
'$[*]' COLUMNS (alias_value VARCHAR2(512) PATH '$')
|
||||
) aliases
|
||||
WHERE c.game_key = target.game_key
|
||||
AND c.active_yn = 'Y'
|
||||
AND aliases.alias_value = target.alias_value
|
||||
);
|
||||
/
|
||||
|
||||
CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW sg_game_catalog_identity_dv AS
|
||||
SELECT JSON {
|
||||
'_id' : c.game_key,
|
||||
'gameId' : c.game_id,
|
||||
'gamePrefix' : c.game_prefix,
|
||||
'gameName' : c.game_nm,
|
||||
'gameAliases' : [
|
||||
SELECT JSON {
|
||||
'_id' : { 'gameKey' : a.game_key, 'value' : a.alias_value }
|
||||
}
|
||||
FROM sg_game_catalog_identity_alias a
|
||||
WHERE a.game_key = c.game_key
|
||||
]
|
||||
}
|
||||
FROM sg_game_catalog c
|
||||
WHERE c.active_yn = 'Y'
|
||||
WITH CHECK OPTION;
|
||||
/
|
||||
|
||||
-- Serialize the DB JSON document itself before embedding. GAME_ID, GAME_PREFIX,
|
||||
-- names and every alias therefore share one vector search document.
|
||||
UPDATE sg_game_catalog c
|
||||
SET c.embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
(
|
||||
SELECT JSON_SERIALIZE(d.data RETURNING CLOB)
|
||||
FROM sg_game_catalog_identity_dv d
|
||||
WHERE JSON_VALUE(d.data, '$._id') = c.game_key
|
||||
),
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
),
|
||||
c.updated_at = SYSTIMESTAMP
|
||||
WHERE c.active_yn = 'Y';
|
||||
/
|
||||
|
||||
COMMENT ON TABLE sg_game_catalog_identity_dv IS
|
||||
'DB JSON identity document for each active game; the canonical embedding source for game-name, alias, GAME_ID and GAME_PREFIX resolution.';
|
||||
/
|
||||
144
database/adb/122_sgmp_std18_filtered_sales_aggregate_fewshot.sql
Normal file
144
database/adb/122_sgmp_std18_filtered_sales_aggregate_fewshot.sql
Normal file
@@ -0,0 +1,144 @@
|
||||
-- STD-18 asks for an aggregate over qualifying orders, not an individual
|
||||
-- transaction list. Remove the invalid customer-derived references and keep
|
||||
-- one reusable, data-neutral aggregate pattern for runtime retrieval.
|
||||
|
||||
DELETE FROM sg_qa_vector_example
|
||||
WHERE source_case_id IN ('STD-18', 'PAT-STD-18')
|
||||
AND source_type IN ('CUSTOMER_QA_BENCHMARK', 'GENERALIZED_QUESTION_PATTERN');
|
||||
/
|
||||
|
||||
DECLARE
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_exists NUMBER;
|
||||
BEGIN
|
||||
v_input := TO_CLOB('질문 패턴: 전체 매출에서 금액 조건을 만족하는 주문을 집계해줘.')
|
||||
|| CHR(10) || 'Question pattern: summarize whole-scope sales after a payment amount filter.'
|
||||
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|
||||
|| CHR(10) || 'Required result grain: one aggregate row with total sales amount, distinct buyer count, and order count.'
|
||||
|| CHR(10) || 'A reference to orders does not by itself request individual order detail.';
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO v_exists
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'FILTERED_SALES_AGGREGATE';
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, object_role,
|
||||
inspection_status, inspection_note, verified_at, verified_by,
|
||||
source_case_id, source_type
|
||||
) VALUES (
|
||||
'전체 매출에서 금액 조건을 만족하는 주문을 집계해줘.',
|
||||
TO_CLOB('SELECT SUM(CAST(s."PAYMT_AMT" AS NUMBER)) AS "TOTAL_SALES_AMOUNT",' || CHR(10)
|
||||
|| ' COUNT(DISTINCT s."GUID") AS "BUYER_COUNT",' || CHR(10)
|
||||
|| ' COUNT(*) AS "ORDER_COUNT"' || CHR(10)
|
||||
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|
||||
|| 'WHERE s."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|
||||
|| ' AND s."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|
||||
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|
||||
|| ' AND s."EXPT_USER_YN" = ''N'''),
|
||||
'Structural Few-shot: return one aggregate row containing total sales amount, distinct buyer count, and order count after the requested payment-amount filter. Do not return individual orders unless the user explicitly asks for a list or detail rows.',
|
||||
v_input,
|
||||
v_embedding,
|
||||
'cohere.embed-v4.0',
|
||||
'APPROVED', 'SQL_TEMPLATE', 'NONE', 'SALES_TRANSACTION',
|
||||
'VERIFIED',
|
||||
'Reusable whole-scope filtered-sales aggregate. No customer date, amount, game, or expected result is stored.',
|
||||
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
|
||||
'FILTERED_SALES_AGGREGATE', 'POLICY_TEMPLATE'
|
||||
);
|
||||
ELSE
|
||||
UPDATE sg_qa_vector_example
|
||||
SET question = '전체 매출에서 금액 조건을 만족하는 주문을 집계해줘.',
|
||||
answer_sql = TO_CLOB('SELECT SUM(CAST(s."PAYMT_AMT" AS NUMBER)) AS "TOTAL_SALES_AMOUNT",' || CHR(10)
|
||||
|| ' COUNT(DISTINCT s."GUID") AS "BUYER_COUNT",' || CHR(10)
|
||||
|| ' COUNT(*) AS "ORDER_COUNT"' || CHR(10)
|
||||
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|
||||
|| 'WHERE s."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|
||||
|| ' AND s."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|
||||
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|
||||
|| ' AND s."EXPT_USER_YN" = ''N'''),
|
||||
answer_text = 'Structural Few-shot: return one aggregate row containing total sales amount, distinct buyer count, and order count after the requested payment-amount filter. Do not return individual orders unless the user explicitly asks for a list or detail rows.',
|
||||
embedding_input = v_input,
|
||||
embedding = v_embedding,
|
||||
reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'NONE',
|
||||
object_role = 'SALES_TRANSACTION',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Reusable whole-scope filtered-sales aggregate. No customer date, amount, game, or expected result is stored.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'FILTERED_SALES_AGGREGATE';
|
||||
END IF;
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
-- A SQL template is prompt context, never an executable statement. Permit
|
||||
-- reviewed policy templates to retain logical placeholders while continuing
|
||||
-- to require executable SQL for automatically generalized patterns.
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
v_max_cosine_distance NUMBER;
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
|
||||
END IF;
|
||||
|
||||
SELECT number_value INTO v_max_cosine_distance
|
||||
FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
|
||||
AND active_yn = 'Y'
|
||||
AND number_value IS NOT NULL;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question, JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
cosine_distance
|
||||
FROM (
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND inspection_status = 'VERIFIED'
|
||||
AND answer_sql IS NOT NULL
|
||||
AND (source_type = 'POLICY_TEMPLATE'
|
||||
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
)
|
||||
WHERE cosine_distance <= v_max_cosine_distance
|
||||
ORDER BY cosine_distance, example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT example_id, source_case_id, source_type, reference_status, inspection_status
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_case_id IN ('STD-18', 'PAT-STD-18', 'FILTERED_SALES_AGGREGATE')
|
||||
ORDER BY example_id;
|
||||
33
database/adb/123_sgmp_filtered_sales_aggregate_any_scope.sql
Normal file
33
database/adb/123_sgmp_filtered_sales_aggregate_any_scope.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- This customer question is game-unscoped. Keep the existing NONE-compatible
|
||||
-- Few-shot path; game_query_plan remains responsible for scope resolution.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET question = '매출에서 금액 조건을 만족하는 주문의 총액, 구매자 수, 주문 수를 집계해줘.',
|
||||
answer_sql = TO_CLOB('SELECT' || CHR(10)
|
||||
|| ' SUM(CAST(s."PAYMT_AMT" AS NUMBER)) AS "TOTAL_SALES_AMOUNT",' || CHR(10)
|
||||
|| ' COUNT(DISTINCT s."GUID") AS "BUYER_COUNT",' || CHR(10)
|
||||
|| ' COUNT(*) AS "ORDER_COUNT"' || CHR(10)
|
||||
|| ' FROM <COMMON_SALES_TRANSACTION> s' || CHR(10)
|
||||
|| ' WHERE s."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|
||||
|| ' AND s."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|
||||
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|
||||
|| ' AND s."EXPT_USER_YN" = ''N'''),
|
||||
answer_text = 'Aggregate result-shape reference: return one row with total sales amount, distinct buyer count, and order count. The game plan separately supplies any game scope; use this pattern only when the question is semantically similar.',
|
||||
embedding_input = TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|
||||
|| 'Logical object role: SALES_TRANSACTION' || CHR(10)
|
||||
|| 'Result grain: one aggregate row with total sales amount, distinct buyer count, and order count.'),
|
||||
embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|
||||
|| 'Logical object role: SALES_TRANSACTION' || CHR(10)
|
||||
|| 'Result grain: one aggregate row with total sales amount, distinct buyer count, and order count.'),
|
||||
JSON(sg_qa_vector_params('search_document'))),
|
||||
target_type = 'NONE',
|
||||
source_case_id = 'PORTAL-STD-18',
|
||||
inspection_note = 'Generalized aggregate pattern for the current game-unscoped question; game_query_plan controls scope separately.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE example_id = 141
|
||||
AND source_type = 'POLICY_TEMPLATE';
|
||||
/
|
||||
|
||||
COMMIT;
|
||||
/
|
||||
23
database/adb/124_sgmp_std18_expected_answer_fewshot.sql
Normal file
23
database/adb/124_sgmp_std18_expected_answer_fewshot.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- The reviewed customer benchmark answer is part of this Few-shot guidance.
|
||||
-- It clarifies that the requested result is one aggregate row, not detail rows.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_text = TO_CLOB('Expected answer shape: return exactly one aggregate row, not individual order rows.' || CHR(10)
|
||||
|| 'Expected answer:' || CHR(10)
|
||||
|| 'TOTAL_SALES_AMOUNT BUYER_COUNT ORDER_COUNT' || CHR(10)
|
||||
|| '------------------ ----------- -----------' || CHR(10)
|
||||
|| ' 204720 6 6'),
|
||||
embedding_input = TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|
||||
|| 'Expected output: TOTAL_SALES_AMOUNT, BUYER_COUNT, ORDER_COUNT as one aggregate row.' || CHR(10)
|
||||
|| 'Expected result example: 204720, 6, 6.'),
|
||||
embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|
||||
|| 'Expected output: TOTAL_SALES_AMOUNT, BUYER_COUNT, ORDER_COUNT as one aggregate row.' || CHR(10)
|
||||
|| 'Expected result example: 204720, 6, 6.'),
|
||||
JSON(sg_qa_vector_params('search_document'))),
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE example_id = 141
|
||||
AND source_type = 'POLICY_TEMPLATE';
|
||||
/
|
||||
COMMIT;
|
||||
/
|
||||
@@ -0,0 +1,29 @@
|
||||
-- STD-25 is a period AU metric, not the daily AU_FLAG metric.
|
||||
-- Keep the evaluation evidence explicit so the LLM judge accepts the valid
|
||||
-- weekly result shape produced by the NL2SQL tool.
|
||||
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = 'Weekly AU: use one as-of snapshot (BASE_DT=2026-07-15), '
|
||||
|| 'count DISTINCT GUID whose LAST_CONN_DT is in the inclusive seven-day window '
|
||||
|| '(2026-07-09 through 2026-07-15), with STD_USER_YN=''Y'' and EXPT_USER_YN=''N''. '
|
||||
|| 'This is one aggregate result, not daily rows. Do not substitute daily AU_FLAG=1 for the period definition.',
|
||||
baseline_sql = TO_CLOB('SELECT COUNT(DISTINCT u."GUID") AS "RECENT_7DAY_AU"' || CHR(10)
|
||||
|| 'FROM "SGMP_POC"."CZN_COMN_USER_MST" u' || CHR(10)
|
||||
|| 'WHERE u."BASE_DT" = DATE ''2026-07-15''' || CHR(10)
|
||||
|| ' AND u."LAST_CONN_DT" BETWEEN DATE ''2026-07-09'' AND DATE ''2026-07-15''' || CHR(10)
|
||||
|| ' AND u."STD_USER_YN" = ''Y''' || CHR(10)
|
||||
|| ' AND u."EXPT_USER_YN" = ''N'''),
|
||||
baseline_answer = 'RECENT_7DAY_AU=0',
|
||||
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","LAST_CONN_DT","STD_USER_YN","EXPT_USER_YN","COUNT"],"recommended_sql_terms":["BASE_DT"],"forbidden_sql_terms":["AU_FLAG"],"required_result_shape":"SINGLE_AGGREGATE"}',
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHERE question_code = 'STD-25';
|
||||
|
||||
/
|
||||
|
||||
COMMIT;
|
||||
|
||||
/
|
||||
|
||||
SELECT question_code, expected_focus, baseline_sql, baseline_answer, evaluation_rule_json
|
||||
FROM sg_ai_qa_question
|
||||
WHERE question_code = 'STD-25';
|
||||
@@ -0,0 +1,40 @@
|
||||
-- CZN-02 customer sample marks STD_USER_YN='Y' as optional for daily
|
||||
-- standard-AU reporting. It must not turn an otherwise correct AU query into
|
||||
-- a failure merely because the condition is present.
|
||||
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = 'Daily standard AU: COUNT(DISTINCT GUID) from CZN_COMN_USER_MST '
|
||||
|| 'for BASE_DT=2026-07-15 with AU_FLAG=1 and EXPT_USER_YN=''N''. '
|
||||
|| 'STD_USER_YN=''Y'' is an allowed optional cohort filter in the customer sample; '
|
||||
|| 'its presence or absence is not a contradiction to this baseline.',
|
||||
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","AU_FLAG","EXPT_USER_YN","COUNT"],"recommended_sql_terms":["BASE_DT","STD_USER_YN"],"optional_sql_terms":["STD_USER_YN"]}',
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHERE question_code = 'CZN-02';
|
||||
|
||||
/
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_text = 'Expected focus: daily standard AU uses CZN_COMN_USER_MST, BASE_DT=2026-07-15, '
|
||||
|| 'AU_FLAG=1 and EXPT_USER_YN=''N''. The customer sample permits STD_USER_YN=''Y'' '
|
||||
|| 'as an optional standard-user cohort filter; do not treat its presence as a conflicting condition. '
|
||||
|| 'Historical answer: STD_AU_COUNT=0',
|
||||
inspection_note = 'Customer sample permits optional STD_USER_YN filtering for daily standard AU; AU_FLAG and excluded-user filtering remain mandatory.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-02'
|
||||
AND reference_status = 'APPROVED';
|
||||
|
||||
/
|
||||
|
||||
COMMIT;
|
||||
|
||||
/
|
||||
|
||||
SELECT q.question_code, q.expected_focus, q.evaluation_rule_json,
|
||||
e.example_id, e.answer_text
|
||||
FROM sg_ai_qa_question q
|
||||
LEFT JOIN sg_qa_vector_example e
|
||||
ON e.source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND e.source_case_id = q.question_code
|
||||
WHERE q.question_code = 'CZN-02';
|
||||
90
database/adb/131_sgmp_independent_au_comparison_pattern.sql
Normal file
90
database/adb/131_sgmp_independent_au_comparison_pattern.sql
Normal file
@@ -0,0 +1,90 @@
|
||||
-- Reusable SINGLE-scope pattern: two AU populations must be aggregated
|
||||
-- independently before comparison. A user-master LEFT JOIN may erase valid
|
||||
-- business-user rows and must not define the business population.
|
||||
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = 'Compare standard AU and business AU as two independent single-row aggregates for the same as-of date. '
|
||||
|| 'Standard AU uses the resolved game user master with AU_FLAG=1 and EXPT_USER_YN=''N''. '
|
||||
|| 'Business AU uses the resolved game business-user fact with BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. '
|
||||
|| 'Do not make the business count depend on a LEFT JOIN from the user-master population. '
|
||||
|| 'STD_USER_YN=''Y'' is an allowed optional cohort filter, not a reason to reject the result.',
|
||||
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","CZN_CUSTOM_BIZ_USER_TXN","AU_FLAG","BIZ_AU_FLAG","EXPT_USER_YN","COUNT"],"recommended_sql_terms":["BASE_DT","STD_USER_YN"],"optional_sql_terms":["STD_USER_YN"],"required_result_shape":"SINGLE_COMPARISON"}',
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHERE question_code = 'CZN-03';
|
||||
|
||||
/
|
||||
|
||||
DECLARE
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_exists NUMBER;
|
||||
BEGIN
|
||||
v_input := TO_CLOB('Question pattern: compare daily standard active users and business active users for one resolved game and one business date.')
|
||||
|| CHR(10) || 'Question pattern Korean: 한 게임의 기준일 일간 표준 AU와 사업 AU를 비교해줘.'
|
||||
|| CHR(10) || 'Logical object role: USER_BUSINESS_AU_COMPARISON'
|
||||
|| CHR(10) || 'Required result shape: one row with two independent aggregate metrics.'
|
||||
|| CHR(10) || 'Business population must be aggregated independently; a LEFT JOIN from the user-master population may not define it.';
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input, JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
SELECT COUNT(*) INTO v_exists
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'STD_BIZ_AU_COMPARE';
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, object_role,
|
||||
inspection_status, inspection_note, verified_at, verified_by,
|
||||
source_case_id, source_type
|
||||
) VALUES (
|
||||
'한 게임의 기준일 일간 표준 AU와 사업 AU를 비교해줘.',
|
||||
TO_CLOB('SELECT' || CHR(10)
|
||||
|| ' (SELECT COUNT(DISTINCT u."GUID")' || CHR(10)
|
||||
|| ' FROM <RESOLVED_GAME_USER_MASTER> u' || CHR(10)
|
||||
|| ' WHERE u."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|
||||
|| ' AND u."AU_FLAG" = 1' || CHR(10)
|
||||
|| ' AND u."EXPT_USER_YN" = ''N'') AS "STANDARD_AU_COUNT",' || CHR(10)
|
||||
|| ' (SELECT COUNT(DISTINCT b."GUID")' || CHR(10)
|
||||
|| ' FROM <RESOLVED_GAME_BUSINESS_USER> b' || CHR(10)
|
||||
|| ' WHERE b."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|
||||
|| ' AND b."BIZ_AU_FLAG" = 1' || CHR(10)
|
||||
|| ' AND b."EXPT_USER_YN" = ''N'') AS "BUSINESS_AU_COUNT"' || CHR(10)
|
||||
|| 'FROM DUAL'),
|
||||
'Applicable metric reference: for this daily AU comparison, the standard metric must use AU_FLAG=1 and EXPT_USER_YN=''N''; do not replace it with LAST_CONN_DT period logic or STD_USER_YN alone. The business metric must use BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. Produce one row from two independent aggregate subqueries, and do not count business users through a LEFT JOIN from the standard-user population. STD_USER_YN may be added only as an optional cohort filter.',
|
||||
v_input, v_embedding, 'cohere.embed-v4.0',
|
||||
'APPROVED', 'SQL_TEMPLATE', 'SINGLE', 'USER_BUSINESS_AU_COMPARISON',
|
||||
'VERIFIED',
|
||||
'Reusable comparison pattern with logical placeholders only; no customer game, date, result, or physical object is embedded.',
|
||||
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
|
||||
'STD_BIZ_AU_COMPARE', 'POLICY_TEMPLATE'
|
||||
);
|
||||
ELSE
|
||||
UPDATE sg_qa_vector_example
|
||||
SET question = '한 게임의 기준일 일간 표준 AU와 사업 AU를 비교해줘.',
|
||||
answer_sql = TO_CLOB('SELECT' || CHR(10)
|
||||
|| ' (SELECT COUNT(DISTINCT u."GUID")' || CHR(10)
|
||||
|| ' FROM <RESOLVED_GAME_USER_MASTER> u' || CHR(10)
|
||||
|| ' WHERE u."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|
||||
|| ' AND u."AU_FLAG" = 1' || CHR(10)
|
||||
|| ' AND u."EXPT_USER_YN" = ''N'') AS "STANDARD_AU_COUNT",' || CHR(10)
|
||||
|| ' (SELECT COUNT(DISTINCT b."GUID")' || CHR(10)
|
||||
|| ' FROM <RESOLVED_GAME_BUSINESS_USER> b' || CHR(10)
|
||||
|| ' WHERE b."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|
||||
|| ' AND b."BIZ_AU_FLAG" = 1' || CHR(10)
|
||||
|| ' AND b."EXPT_USER_YN" = ''N'') AS "BUSINESS_AU_COUNT"' || CHR(10)
|
||||
|| 'FROM DUAL'),
|
||||
answer_text = 'Applicable metric reference: for this daily AU comparison, the standard metric must use AU_FLAG=1 and EXPT_USER_YN=''N''; do not replace it with LAST_CONN_DT period logic or STD_USER_YN alone. The business metric must use BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. Produce one row from two independent aggregate subqueries, and do not count business users through a LEFT JOIN from the standard-user population. STD_USER_YN may be added only as an optional cohort filter.',
|
||||
embedding_input = v_input,
|
||||
embedding = v_embedding,
|
||||
reference_status = 'APPROVED', inspection_status = 'VERIFIED',
|
||||
verified_at = SYSTIMESTAMP, verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'STD_BIZ_AU_COMPARE';
|
||||
END IF;
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
/
|
||||
58
database/adb/132_sgmp_country_daily_au_pattern.sql
Normal file
58
database/adb/132_sgmp_country_daily_au_pattern.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- Reusable SINGLE-scope pattern for country-grouped daily AU.
|
||||
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = 'Country-grouped daily standard AU uses the resolved game user master with BASE_DT, AU_FLAG=1 and EXPT_USER_YN=''N''. '
|
||||
|| 'Group by LAST_CONN_COUNTRY_CD and use the approved country dimension only for display/classification. '
|
||||
|| 'STD_USER_YN is optional and cannot replace AU_FLAG for the daily metric.',
|
||||
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","AU_FLAG","EXPT_USER_YN","LAST_CONN_COUNTRY_CD","COUNT"],"recommended_sql_terms":["BASE_DT","COMN_COUNTRY_BAS","STD_USER_YN"],"optional_sql_terms":["STD_USER_YN"],"required_result_shape":"COUNTRY_GROUPED"}',
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHERE question_code = 'CZN-06';
|
||||
|
||||
/
|
||||
|
||||
DECLARE
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_exists NUMBER;
|
||||
BEGIN
|
||||
v_input := TO_CLOB('Question pattern: show daily active-user counts by country for one resolved game and one business date.')
|
||||
|| CHR(10) || 'Question pattern Korean: 한 게임의 기준일 주요 국가별 표준 AU 수를 알려줘.'
|
||||
|| CHR(10) || 'Logical object role: COUNTRY_GROUPED_DAILY_AU'
|
||||
|| CHR(10) || 'Required metric: AU_FLAG=1 and excluded-user filtering; group by the last connection country.';
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(v_input, JSON(sg_qa_vector_params('search_document')));
|
||||
SELECT COUNT(*) INTO v_exists FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE' AND source_case_id = 'COUNTRY_DAILY_AU';
|
||||
IF v_exists = 0 THEN
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, object_role,
|
||||
inspection_status, inspection_note, verified_at, verified_by, source_case_id, source_type
|
||||
) VALUES (
|
||||
'한 게임의 기준일 주요 국가별 표준 AU 수를 알려줘.',
|
||||
TO_CLOB('SELECT u."LAST_CONN_COUNTRY_CD" AS "COUNTRY_CD",' || CHR(10)
|
||||
|| ' c."COUNTRY_KR_NM" AS "COUNTRY_NAME",' || CHR(10)
|
||||
|| ' COUNT(DISTINCT u."GUID") AS "STANDARD_AU_COUNT"' || CHR(10)
|
||||
|| 'FROM <RESOLVED_GAME_USER_MASTER> u' || CHR(10)
|
||||
|| 'LEFT JOIN <APPROVED_COUNTRY_DIMENSION> c' || CHR(10)
|
||||
|| ' ON c."COUNTRY_2CHAR_CD" = u."LAST_CONN_COUNTRY_CD"' || CHR(10)
|
||||
|| 'WHERE u."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|
||||
|| ' AND u."AU_FLAG" = 1' || CHR(10)
|
||||
|| ' AND u."EXPT_USER_YN" = ''N''' || CHR(10)
|
||||
|| 'GROUP BY u."LAST_CONN_COUNTRY_CD", c."COUNTRY_KR_NM"'),
|
||||
'Applicable metric reference: country-grouped daily AU must use AU_FLAG=1 and EXPT_USER_YN=''N''; do not replace AU_FLAG with STD_USER_YN alone. Group by LAST_CONN_COUNTRY_CD. Use an approved country dimension for country display or a current approved major-country classification when the request requires it.',
|
||||
v_input, v_embedding, 'cohere.embed-v4.0',
|
||||
'APPROVED', 'SQL_TEMPLATE', 'SINGLE', 'COUNTRY_GROUPED_DAILY_AU',
|
||||
'VERIFIED', 'Reusable country-grouped daily-AU pattern; no customer game, date, result, or physical object is embedded.',
|
||||
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW', 'COUNTRY_DAILY_AU', 'POLICY_TEMPLATE'
|
||||
);
|
||||
ELSE
|
||||
UPDATE sg_qa_vector_example
|
||||
SET embedding_input = v_input, embedding = v_embedding,
|
||||
reference_status = 'APPROVED', inspection_status = 'VERIFIED',
|
||||
verified_at = SYSTIMESTAMP, verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'POLICY_TEMPLATE' AND source_case_id = 'COUNTRY_DAILY_AU';
|
||||
END IF;
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
/
|
||||
17
database/adb/133_sgmp_restore_czn06_exact_fewshot.sql
Normal file
17
database/adb/133_sgmp_restore_czn06_exact_fewshot.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- CZN-06 is a verified, exact customer question/SQL pair. It must be a
|
||||
-- runtime Few-shot when approved; RETIRED is the DB switch that excludes it.
|
||||
-- This is a single-game reference, so keep the retrieval scope explicit.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'COUNTRY_GROUPED_DAILY_AU',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Verified exact CZN-06 Few-shot restored for runtime retrieval. Daily country AU requires AU_FLAG=1 and EXPT_USER_YN=''N''; STD_USER_YN alone is insufficient.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-06'
|
||||
/
|
||||
|
||||
COMMIT
|
||||
69
database/adb/135_sgmp_semantic_fewshot_retrieval.sql
Normal file
69
database/adb/135_sgmp_semantic_fewshot_retrieval.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- Runtime Few-shots remain semantic vector retrieval. Customer examples are
|
||||
-- governed by their DB approval state, not restricted to exact text matches.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'GAME_GOODS_HOLDINGS',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Verified CZN-07 semantic Few-shot. Use goods holdings, crystal dimension, RU_FLAG=1, excluded-user filter, nonzero holdings, and daily grouping.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-07'
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
v_max_cosine_distance NUMBER;
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
|
||||
END IF;
|
||||
|
||||
SELECT number_value INTO v_max_cosine_distance
|
||||
FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
|
||||
AND active_yn = 'Y'
|
||||
AND number_value IS NOT NULL;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question, JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
cosine_distance
|
||||
FROM (
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND inspection_status = 'VERIFIED'
|
||||
AND answer_sql IS NOT NULL
|
||||
AND (source_type = 'POLICY_TEMPLATE'
|
||||
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
)
|
||||
WHERE cosine_distance <= v_max_cosine_distance
|
||||
ORDER BY cosine_distance, example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMIT
|
||||
87
database/adb/136_sgmp_fewshot_neighbor_margin.sql
Normal file
87
database/adb/136_sgmp_fewshot_neighbor_margin.sql
Normal file
@@ -0,0 +1,87 @@
|
||||
-- Keep semantic vector retrieval, but do not inject weak trailing neighbours
|
||||
-- when a materially stronger example has already been found.
|
||||
MERGE INTO sg_game_scope_policy t
|
||||
USING (
|
||||
SELECT 'QA_VECTOR_NEIGHBOR_DISTANCE_MARGIN' AS policy_key,
|
||||
0.120000 AS number_value,
|
||||
'Maximum additional cosine distance from the best runtime Few-shot candidate.' AS description
|
||||
FROM dual
|
||||
) s
|
||||
ON (t.policy_key = s.policy_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.number_value = s.number_value,
|
||||
t.description = s.description,
|
||||
t.active_yn = 'Y',
|
||||
t.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
policy_key, number_value, text_value, description, active_yn
|
||||
) VALUES (
|
||||
s.policy_key, s.number_value, NULL, s.description, 'Y'
|
||||
)
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
v_max_cosine_distance NUMBER;
|
||||
v_neighbor_margin NUMBER;
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
|
||||
END IF;
|
||||
|
||||
SELECT number_value INTO v_max_cosine_distance
|
||||
FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
|
||||
AND active_yn = 'Y'
|
||||
AND number_value IS NOT NULL;
|
||||
|
||||
SELECT number_value INTO v_neighbor_margin
|
||||
FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_NEIGHBOR_DISTANCE_MARGIN'
|
||||
AND active_yn = 'Y'
|
||||
AND number_value IS NOT NULL;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question, JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
cosine_distance
|
||||
FROM (
|
||||
SELECT c.*,
|
||||
MIN(c.cosine_distance) OVER () AS best_cosine_distance
|
||||
FROM (
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND inspection_status = 'VERIFIED'
|
||||
AND answer_sql IS NOT NULL
|
||||
AND (source_type = 'POLICY_TEMPLATE'
|
||||
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
) c
|
||||
WHERE c.cosine_distance <= v_max_cosine_distance
|
||||
)
|
||||
WHERE cosine_distance <= best_cosine_distance + v_neighbor_margin
|
||||
ORDER BY cosine_distance, example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMIT
|
||||
@@ -0,0 +1,16 @@
|
||||
-- CZN-08 is the reviewed semantic reference for daily standard-AU crystal
|
||||
-- total and per-user average holdings.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'GAME_GOODS_HOLDINGS',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Verified CZN-08 Few-shot. Standard-AU crystal holdings require AU_FLAG=1, excluded-user filtering, and per-user average as SUM(HAVE_CNT) / COUNT(DISTINCT GUID), not AVG(HAVE_CNT).',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-08'
|
||||
/
|
||||
|
||||
COMMIT
|
||||
11
database/adb/138_sgmp_tighten_fewshot_neighbor_margin.sql
Normal file
11
database/adb/138_sgmp_tighten_fewshot_neighbor_margin.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Calibrate the semantic-neighbour window using reviewed CZN patterns:
|
||||
-- retain close paraphrases, exclude adjacent metric shapes.
|
||||
UPDATE sg_game_scope_policy
|
||||
SET number_value = 0.100000,
|
||||
description = 'Maximum additional cosine distance from the best runtime Few-shot candidate.',
|
||||
active_yn = 'Y',
|
||||
updated_at = SYSTIMESTAMP
|
||||
WHERE policy_key = 'QA_VECTOR_NEIGHBOR_DISTANCE_MARGIN'
|
||||
/
|
||||
|
||||
COMMIT
|
||||
@@ -0,0 +1,15 @@
|
||||
-- CZN-05 is the reviewed reference for country-grouped business AU.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'COUNTRY_GROUPED_BUSINESS_AU',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Verified CZN-05 Few-shot. Join business-user data to user master on GUID and BASE_DT before grouping by user country; filter BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-05'
|
||||
/
|
||||
|
||||
COMMIT
|
||||
95
database/adb/140_sgmp_hybrid_fewshot_reranking.sql
Normal file
95
database/adb/140_sgmp_hybrid_fewshot_reranking.sql
Normal file
@@ -0,0 +1,95 @@
|
||||
-- Hybrid retrieval remains database-driven: dense vector similarity handles
|
||||
-- paraphrases, while lexical similarity distinguishes decisive request terms.
|
||||
MERGE INTO sg_game_scope_policy t
|
||||
USING (
|
||||
SELECT 'QA_VECTOR_LEXICAL_WEIGHT' AS policy_key, 0.350000 AS number_value,
|
||||
'Weight of normalized lexical question similarity in runtime Few-shot reranking.' AS description
|
||||
FROM dual
|
||||
UNION ALL
|
||||
SELECT 'QA_VECTOR_HYBRID_SCORE_MARGIN', 0.050000,
|
||||
'Maximum hybrid-score difference from the best runtime Few-shot candidate.'
|
||||
FROM dual
|
||||
) s
|
||||
ON (t.policy_key = s.policy_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.number_value = s.number_value,
|
||||
t.description = s.description,
|
||||
t.active_yn = 'Y',
|
||||
t.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
policy_key, number_value, text_value, description, active_yn
|
||||
) VALUES (
|
||||
s.policy_key, s.number_value, NULL, s.description, 'Y'
|
||||
)
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
v_max_cosine_distance NUMBER;
|
||||
v_lexical_weight NUMBER;
|
||||
v_hybrid_margin NUMBER;
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
|
||||
END IF;
|
||||
|
||||
SELECT number_value INTO v_max_cosine_distance FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE' AND active_yn = 'Y';
|
||||
SELECT number_value INTO v_lexical_weight FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_LEXICAL_WEIGHT' AND active_yn = 'Y';
|
||||
SELECT number_value INTO v_hybrid_margin FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'QA_VECTOR_HYBRID_SCORE_MARGIN' AND active_yn = 'Y';
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question, JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
cosine_distance
|
||||
FROM (
|
||||
SELECT s.*,
|
||||
MAX(s.hybrid_score) OVER () AS best_hybrid_score
|
||||
FROM (
|
||||
SELECT c.*,
|
||||
((1 - v_lexical_weight) * (1 - c.cosine_distance)
|
||||
+ v_lexical_weight * c.lexical_similarity) AS hybrid_score
|
||||
FROM (
|
||||
SELECT example_id, question, answer_sql, answer_text, embedding_model,
|
||||
reference_kind, target_type, object_role, source_case_id, source_type,
|
||||
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance,
|
||||
UTL_MATCH.JARO_WINKLER_SIMILARITY(
|
||||
DBMS_LOB.SUBSTR(question, 4000, 1),
|
||||
DBMS_LOB.SUBSTR(p_question, 4000, 1)
|
||||
) / 100 AS lexical_similarity
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND inspection_status = 'VERIFIED'
|
||||
AND answer_sql IS NOT NULL
|
||||
AND (source_type = 'POLICY_TEMPLATE'
|
||||
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
) c
|
||||
WHERE c.cosine_distance <= v_max_cosine_distance
|
||||
) s
|
||||
)
|
||||
WHERE hybrid_score >= best_hybrid_score - v_hybrid_margin
|
||||
ORDER BY hybrid_score DESC, cosine_distance, example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMIT
|
||||
15
database/adb/141_sgmp_restore_czn13_ether_usage_fewshot.sql
Normal file
15
database/adb/141_sgmp_restore_czn13_ether_usage_fewshot.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- CZN-13 is the reviewed reference for Ether usage and distinct users.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'GAME_GOODS_CHANGE',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Verified CZN-13 Few-shot. Ether usage requires goods-change data joined to the goods dimension and user master by GUID and BASE_DT, CHANGE_TYPE_CD=''USE'', active Ether dimension, excluded-user filter, and GOODS_CHANGE_CNT aggregation.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-13'
|
||||
/
|
||||
|
||||
COMMIT
|
||||
@@ -0,0 +1,15 @@
|
||||
-- CZN-16 is the reviewed reference for purchasers of a named package.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'SALES_PRODUCT_PURCHASER',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Verified CZN-16 Few-shot. Join sales transactions to product display by GAME_ID and PRODUCT_ID, filter the resolved package name and excluded users, and use the payment business date when counting distinct purchasers.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-16'
|
||||
/
|
||||
|
||||
COMMIT
|
||||
@@ -1,159 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 47_dds_ad_identity_test_setup.sql
|
||||
--
|
||||
-- Test-only bridge for the isolated dds.test Active Directory domain.
|
||||
-- Maps immutable AD objectGUID values to the existing HMM application users
|
||||
-- 1 and 2, then publishes only their passwordless local DDS END USERs.
|
||||
--
|
||||
-- This does NOT validate an AD/OIDC JWT. A verified issuer + subject must be
|
||||
-- resolved by the MCP application before it uses this bridge.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET DEFINE OFF
|
||||
|
||||
PROMPT === 1. Creating external identity bridge ===
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE q'[
|
||||
CREATE TABLE cb_external_identity_binding (
|
||||
issuer VARCHAR2(512) NOT NULL,
|
||||
subject VARCHAR2(512) NOT NULL,
|
||||
application_user_id NUMBER NOT NULL,
|
||||
display_name VARCHAR2(256),
|
||||
active CHAR(1) DEFAULT 'Y' NOT NULL
|
||||
CHECK (active IN ('Y', 'N')),
|
||||
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT cb_external_identity_binding_pk PRIMARY KEY (issuer, subject)
|
||||
)]';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE <> -955 THEN RAISE; END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'CREATE INDEX cb_external_identity_binding_user_ix '
|
||||
|| 'ON cb_external_identity_binding (application_user_id)';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE <> -955 THEN RAISE; END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 2. Creating DDS END USER map ===
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE q'[
|
||||
CREATE TABLE cb_dds_end_user_map (
|
||||
application_user_id NUMBER PRIMARY KEY,
|
||||
end_user_name VARCHAR2(128) NOT NULL UNIQUE,
|
||||
data_role_name VARCHAR2(128) NOT NULL UNIQUE,
|
||||
lookup_key_ref VARCHAR2(128) NOT NULL,
|
||||
grant_name VARCHAR2(128) NOT NULL,
|
||||
publish_status VARCHAR2(20) NOT NULL
|
||||
CHECK (publish_status IN ('PENDING', 'PUBLISHED', 'REVOKED', 'FAILED')),
|
||||
published_at TIMESTAMP,
|
||||
last_error VARCHAR2(1000)
|
||||
)]';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE <> -955 THEN RAISE; END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
DECLARE
|
||||
v_active_users NUMBER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_active_users
|
||||
FROM cb_app_user
|
||||
WHERE user_id IN (1, 2) AND active = 'Y';
|
||||
IF v_active_users <> 2 THEN
|
||||
RAISE_APPLICATION_ERROR(-20947, 'Expected active HMM application users 1 and 2.');
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 3. Binding dds.test immutable subjects to HMM users ===
|
||||
MERGE INTO cb_external_identity_binding target
|
||||
USING (
|
||||
SELECT 'urn:dds-ad:test' AS issuer,
|
||||
'fa7ebe4b-aca8-4049-a436-7f3da2d27a9f' AS subject,
|
||||
1 AS application_user_id,
|
||||
'dds-alice' AS display_name
|
||||
FROM dual
|
||||
UNION ALL
|
||||
SELECT 'urn:dds-ad:test',
|
||||
'128546cf-ec0a-4e99-8f51-dfce29280c91',
|
||||
2,
|
||||
'dds-bob'
|
||||
FROM dual
|
||||
) source
|
||||
ON (target.issuer = source.issuer AND target.subject = source.subject)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
target.application_user_id = source.application_user_id,
|
||||
target.display_name = source.display_name,
|
||||
target.active = 'Y',
|
||||
target.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
issuer, subject, application_user_id, display_name, active
|
||||
) VALUES (
|
||||
source.issuer, source.subject, source.application_user_id, source.display_name, 'Y'
|
||||
);
|
||||
|
||||
MERGE INTO cb_dds_end_user_map target
|
||||
USING (
|
||||
SELECT user_id AS application_user_id,
|
||||
'DDS_U_' || TO_CHAR(user_id) AS end_user_name,
|
||||
'DDS_U_' || TO_CHAR(user_id) || '_ROLE' AS data_role_name,
|
||||
'DDS_OCI_IAM_CLIENT_SECRET_DERIVED_V1' AS lookup_key_ref,
|
||||
'DDS_MCP_U_' || TO_CHAR(user_id) || '_VECTOR_GRANT' AS grant_name
|
||||
FROM cb_app_user
|
||||
WHERE user_id IN (1, 2)
|
||||
) source
|
||||
ON (target.application_user_id = source.application_user_id)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
target.end_user_name = source.end_user_name,
|
||||
target.data_role_name = source.data_role_name,
|
||||
target.lookup_key_ref = source.lookup_key_ref,
|
||||
target.grant_name = source.grant_name,
|
||||
target.publish_status = 'PENDING',
|
||||
target.last_error = NULL
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
application_user_id, end_user_name, data_role_name, lookup_key_ref,
|
||||
grant_name, publish_status
|
||||
) VALUES (
|
||||
source.application_user_id, source.end_user_name, source.data_role_name,
|
||||
source.lookup_key_ref, source.grant_name, 'PENDING'
|
||||
);
|
||||
|
||||
PROMPT === 4. Publishing DDS identities only (default deny until data grants exist) ===
|
||||
BEGIN
|
||||
FOR mapped_user IN (
|
||||
SELECT application_user_id, end_user_name, data_role_name
|
||||
FROM cb_dds_end_user_map
|
||||
WHERE application_user_id IN (1, 2)
|
||||
ORDER BY application_user_id
|
||||
) LOOP
|
||||
EXECUTE IMMEDIATE 'CREATE END USER IF NOT EXISTS "' || mapped_user.end_user_name || '"';
|
||||
EXECUTE IMMEDIATE 'CREATE DATA ROLE IF NOT EXISTS ' || mapped_user.data_role_name;
|
||||
EXECUTE IMMEDIATE 'GRANT DATA ROLE ' || mapped_user.data_role_name
|
||||
|| ' TO "' || mapped_user.end_user_name || '"';
|
||||
UPDATE cb_dds_end_user_map
|
||||
SET publish_status = 'PUBLISHED', published_at = SYSTIMESTAMP, last_error = NULL
|
||||
WHERE application_user_id = mapped_user.application_user_id;
|
||||
END LOOP;
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 5. Identity bridge inventory ===
|
||||
SELECT b.issuer, b.subject, b.display_name, b.application_user_id,
|
||||
m.end_user_name, m.data_role_name, m.publish_status
|
||||
FROM cb_external_identity_binding b
|
||||
JOIN cb_dds_end_user_map m ON m.application_user_id = b.application_user_id
|
||||
WHERE b.issuer = 'urn:dds-ad:test'
|
||||
ORDER BY b.application_user_id;
|
||||
|
||||
PROMPT === AD identity to DDS END USER test setup complete ===
|
||||
EXIT;
|
||||
62
database/adb/47_smilegate_qa_history.sql
Normal file
62
database/adb/47_smilegate_qa_history.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
-- Smilegate customer Excel QA benchmark history.
|
||||
-- This script is also applied by poc4_active_source_20260714/scripts/
|
||||
-- sync_smilegate_qa_history.py with existence checks for repeatable deployment.
|
||||
|
||||
CREATE TABLE SG_AI_QA_QUESTION (
|
||||
question_id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
|
||||
question_code VARCHAR2(30) UNIQUE,
|
||||
question_source VARCHAR2(30) NOT NULL,
|
||||
question_hash VARCHAR2(64) NOT NULL UNIQUE,
|
||||
category VARCHAR2(30) NOT NULL,
|
||||
title VARCHAR2(200) NOT NULL,
|
||||
question_text CLOB NOT NULL,
|
||||
source_document VARCHAR2(255),
|
||||
source_sheet VARCHAR2(255),
|
||||
source_row NUMBER,
|
||||
source_scenario CLOB,
|
||||
sample_sql CLOB,
|
||||
expected_focus CLOB,
|
||||
baseline_sql CLOB,
|
||||
baseline_answer CLOB,
|
||||
support_level VARCHAR2(20) NOT NULL,
|
||||
evaluation_rule_json CLOB CHECK (evaluation_rule_json IS JSON),
|
||||
active_yn CHAR(1) DEFAULT 'Y' NOT NULL CHECK (active_yn IN ('Y', 'N')),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT sg_ai_qa_question_source_ck
|
||||
CHECK (question_source IN ('CUSTOMER_EXCEL', 'FREE_TEXT'))
|
||||
);
|
||||
|
||||
CREATE TABLE SG_AI_QA_ANSWER (
|
||||
answer_seq NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
question_id NUMBER NOT NULL,
|
||||
answer_kind VARCHAR2(20) NOT NULL,
|
||||
run_key VARCHAR2(100),
|
||||
conversation_id VARCHAR2(100),
|
||||
requested_by VARCHAR2(100),
|
||||
requested_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
model_profile VARCHAR2(100),
|
||||
generated_sql CLOB,
|
||||
answer_text CLOB,
|
||||
result_json CLOB CHECK (result_json IS JSON),
|
||||
execution_output CLOB,
|
||||
execution_status VARCHAR2(40),
|
||||
judgment_status VARCHAR2(20) NOT NULL,
|
||||
judgment_reason CLOB,
|
||||
duration_ms NUMBER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT sg_ai_qa_answer_question_fk
|
||||
FOREIGN KEY (question_id)
|
||||
REFERENCES SG_AI_QA_QUESTION (question_id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT sg_ai_qa_answer_kind_ck
|
||||
CHECK (answer_kind IN ('HISTORICAL', 'LIVE')),
|
||||
CONSTRAINT sg_ai_qa_answer_judgment_ck
|
||||
CHECK (judgment_status IN ('PASS', 'WARN', 'FAIL', 'REVIEW'))
|
||||
);
|
||||
|
||||
CREATE INDEX sg_ai_qa_answer_question_ix
|
||||
ON SG_AI_QA_ANSWER (question_id, answer_seq DESC);
|
||||
|
||||
CREATE UNIQUE INDEX sg_ai_qa_answer_run_uk
|
||||
ON SG_AI_QA_ANSWER (question_id, run_key);
|
||||
@@ -130,6 +130,69 @@ begin
|
||||
)');
|
||||
create_if_missing('create sequence sg_permission_seq start with 1 increment by 1 nocache');
|
||||
create_if_missing('create sequence sg_permission_rule_seq start with 1 increment by 1 nocache');
|
||||
create_if_missing('create table sg_vpd_policy_note (
|
||||
object_owner varchar2(128) not null,
|
||||
object_name varchar2(128) not null,
|
||||
policy_name varchar2(128) not null,
|
||||
description varchar2(2000),
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_vpd_policy_note_pk primary key (object_owner, object_name, policy_name)
|
||||
)');
|
||||
create_if_missing('create table sg_vpd_filter_note (
|
||||
function_owner varchar2(128) not null,
|
||||
function_name varchar2(128) not null,
|
||||
description varchar2(2000),
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_vpd_filter_note_pk primary key (function_owner, function_name)
|
||||
)');
|
||||
create_if_missing('create table sg_masking_rule (
|
||||
rule_id number primary key,
|
||||
rule_code varchar2(100) not null unique,
|
||||
rule_name varchar2(200) not null,
|
||||
template_code varchar2(100) not null,
|
||||
description varchar2(2000),
|
||||
enabled_yn char(1) default ''Y'' not null,
|
||||
created_at timestamp default systimestamp not null,
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_masking_rule_enabled_ck check (enabled_yn in (''Y'', ''N''))
|
||||
)');
|
||||
create_if_missing('create table sg_access_bearer_token (
|
||||
key_id number primary key, user_id number not null, key_prefix varchar2(100) not null,
|
||||
key_hash varchar2(256) not null unique, expires_at timestamp not null, revoked_at timestamp,
|
||||
description varchar2(500), created_at timestamp default systimestamp not null,
|
||||
constraint sg_access_bearer_token_user_fk foreign key (user_id) references sg_app_user(user_id)
|
||||
)');
|
||||
create_if_missing('create table sg_backoffice_setting (
|
||||
setting_key varchar2(200) primary key, setting_value varchar2(4000),
|
||||
updated_at timestamp default systimestamp not null
|
||||
)');
|
||||
create_if_missing('create table sg_vector_document_chunk (
|
||||
chunk_id number primary key, document_id varchar2(200) not null, chunk_no number not null,
|
||||
content clob not null, embedding vector(1536, float32), created_at timestamp default systimestamp not null
|
||||
)');
|
||||
create_if_missing('create table sg_vector_document_tag (
|
||||
chunk_id number not null, tech_tag varchar2(200) not null,
|
||||
constraint sg_vector_document_tag_pk primary key (chunk_id, tech_tag),
|
||||
constraint sg_vector_document_tag_chunk_fk foreign key (chunk_id) references sg_vector_document_chunk(chunk_id)
|
||||
)');
|
||||
create_if_missing('create sequence sg_vector_chunk_seq start with 1 increment by 1 nocache');
|
||||
create_if_missing('create table sg_column_masking_rule (
|
||||
column_id number primary key,
|
||||
rule_id number not null,
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_column_masking_rule_column_fk foreign key (column_id) references sg_protected_column(column_id),
|
||||
constraint sg_column_masking_rule_rule_fk foreign key (rule_id) references sg_masking_rule(rule_id)
|
||||
)');
|
||||
create_if_missing('create table sg_user_masking_rule (
|
||||
user_id number not null,
|
||||
column_id number not null,
|
||||
decision varchar2(30) not null,
|
||||
active_yn char(1) default ''Y'' not null,
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_user_masking_rule_pk primary key (user_id, column_id),
|
||||
constraint sg_user_masking_rule_user_fk foreign key (user_id) references sg_app_user(user_id),
|
||||
constraint sg_user_masking_rule_column_fk foreign key (column_id) references sg_protected_column(column_id)
|
||||
)');
|
||||
end;
|
||||
/
|
||||
|
||||
@@ -168,3 +231,99 @@ merge into sg_group_role t using (select 2001 group_id, 3002 role_id from dual)
|
||||
on (t.group_id=s.group_id and t.role_id=s.role_id) when not matched then insert (group_id, role_id) values (s.group_id, s.role_id);
|
||||
|
||||
commit;
|
||||
|
||||
-- Compatibility layer for remaining backoffice modules. The data is stored
|
||||
-- only in SG_* tables; these views prevent older controller paths from
|
||||
-- querying non-existent CB_* physical tables during the Smilegate transition.
|
||||
create or replace view cb_app_user as
|
||||
select user_id, user_name, employee_no, dept_code, can_read_contents, active from sg_app_user;
|
||||
create or replace view cb_app_group as
|
||||
select group_id, group_code, group_name, description, active_yn from sg_app_group;
|
||||
create or replace view cb_app_role as
|
||||
select role_id, role_name, max_sensitivity_level from sg_app_role;
|
||||
create or replace view cb_user_role as select user_id, role_id from sg_user_role;
|
||||
create or replace view cb_user_group as select group_id, user_id from sg_user_group;
|
||||
create or replace view cb_group_role as select group_id, role_id from sg_group_role;
|
||||
create or replace view cb_protected_object as
|
||||
select object_id, owner, object_name, ords_path, enabled_yn, description from sg_protected_object;
|
||||
create or replace view cb_protected_column as
|
||||
select column_id, object_id, column_name, sensitive_yn, visible_role_id, sensitivity_level, redaction_method from sg_protected_column;
|
||||
create or replace view cb_permission as
|
||||
select perm_id, role_id, target_name, action_name, permission_effect from sg_permission;
|
||||
create or replace view cb_permission_rule as
|
||||
select rule_id, perm_id, rule_column, rule_type, rule_value from sg_permission_rule;
|
||||
create or replace view cb_permission_column as
|
||||
select permission_id, column_name from sg_permission_column;
|
||||
create or replace view cb_vpd_policy_note as
|
||||
select object_owner, object_name, policy_name, description, updated_at from sg_vpd_policy_note;
|
||||
create or replace view cb_vpd_filter_note as
|
||||
select function_owner, function_name, description, updated_at from sg_vpd_filter_note;
|
||||
create or replace view cb_masking_rule as
|
||||
select rule_id, rule_code, rule_name, template_code, description, enabled_yn from sg_masking_rule;
|
||||
create or replace view cb_column_masking_rule as
|
||||
select column_id, rule_id, updated_at from sg_column_masking_rule;
|
||||
create or replace view cb_user_masking_rule as
|
||||
select user_id, column_id, decision, active_yn, updated_at from sg_user_masking_rule;
|
||||
create or replace view cb_ords_probe_audit as
|
||||
select audit_id, event_type, key_id, object_id, status, row_count, error_code, message, created_at from sg_audit_event;
|
||||
create or replace view cb_backoffice_setting as
|
||||
select setting_key, setting_value, updated_at from sg_backoffice_setting;
|
||||
|
||||
-- PoC administrator access: both demo operators can manage and query every
|
||||
-- Smilegate game-data object registered in SGMP_POC.
|
||||
merge into sg_app_role t
|
||||
using (select 3099 role_id, 'DATA_AI_POC_ADMIN' role_name,
|
||||
'Full access to all Smilegate PoC game-data objects' description,
|
||||
'RESTRICTED' max_sensitivity_level from dual) s
|
||||
on (t.role_id = s.role_id)
|
||||
when matched then update set t.role_name=s.role_name, t.description=s.description,
|
||||
t.max_sensitivity_level=s.max_sensitivity_level, t.updated_at=systimestamp
|
||||
when not matched then insert (role_id, role_name, description, max_sensitivity_level)
|
||||
values (s.role_id, s.role_name, s.description, s.max_sensitivity_level);
|
||||
|
||||
merge into sg_user_role t
|
||||
using (select 1001 user_id, 3099 role_id from dual union all select 1002, 3099 from dual) s
|
||||
on (t.user_id=s.user_id and t.role_id=s.role_id)
|
||||
when not matched then insert (user_id, role_id) values (s.user_id, s.role_id);
|
||||
|
||||
declare
|
||||
l_object_id number;
|
||||
l_permission_id number;
|
||||
begin
|
||||
for source_object in (
|
||||
select table_name
|
||||
from all_tables
|
||||
where owner = 'SGMP_POC'
|
||||
and table_name not in ('SEMANTIC_METADATA_CHANGE_LOG', 'SGMP_TERM_CONTEXT_CACHE',
|
||||
'SGMP_TERM_DICTIONARY', 'SGMP_TERM_SEARCH_LOG', 'SGMP_TERM_SYNONYM')
|
||||
order by table_name
|
||||
) loop
|
||||
begin
|
||||
select object_id into l_object_id
|
||||
from sg_protected_object
|
||||
where owner = 'SGMP_POC' and object_name = source_object.table_name;
|
||||
exception
|
||||
when no_data_found then
|
||||
select nvl(max(object_id), 0) + 1 into l_object_id from sg_protected_object;
|
||||
insert into sg_protected_object (object_id, owner, object_name, ords_path, enabled_yn, description)
|
||||
values (l_object_id, 'SGMP_POC', source_object.table_name,
|
||||
'/sgmp-poc/' || lower(source_object.table_name), 'Y',
|
||||
'Smilegate PoC game-data object');
|
||||
end;
|
||||
|
||||
begin
|
||||
select perm_id into l_permission_id
|
||||
from sg_permission
|
||||
where role_id = 3099 and target_name = source_object.table_name;
|
||||
exception
|
||||
when no_data_found then
|
||||
l_permission_id := sg_permission_seq.nextval;
|
||||
insert into sg_permission (perm_id, role_id, target_name, action_name, permission_effect)
|
||||
values (l_permission_id, 3099, source_object.table_name, 'SELECT', 'ALLOW');
|
||||
insert into sg_permission_rule (rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
values (sg_permission_rule_seq.nextval, l_permission_id, null, 'ALL', null);
|
||||
end;
|
||||
end loop;
|
||||
commit;
|
||||
end;
|
||||
/
|
||||
|
||||
74
database/adb/72_sgmp_select_ai_oci_genai_profile.sql
Normal file
74
database/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;
|
||||
199
database/adb/75_sgmp_qa_vector_retrieval.sql
Normal file
199
database/adb/75_sgmp_qa_vector_retrieval.sql
Normal file
@@ -0,0 +1,199 @@
|
||||
-- SGMP QA example vector store.
|
||||
--
|
||||
-- Run as SGMP_POC after scripts/setup-sgmp-qa-vector.sh has registered the
|
||||
-- DBMS_VECTOR credential and granted the HTTPS ACL. No API key material is
|
||||
-- stored in this file.
|
||||
--
|
||||
-- Cohere Embed 4 is intentionally fixed to 1536 dimensions. Stored examples
|
||||
-- use search_document; incoming questions use search_query.
|
||||
|
||||
DECLARE
|
||||
v_count PLS_INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_count
|
||||
FROM user_tables
|
||||
WHERE table_name = 'SG_QA_VECTOR_CONFIG';
|
||||
|
||||
IF v_count = 0 THEN
|
||||
EXECUTE IMMEDIATE q'[
|
||||
CREATE TABLE sg_qa_vector_config (
|
||||
config_key VARCHAR2(64) PRIMARY KEY,
|
||||
config_value VARCHAR2(4000) NOT NULL,
|
||||
updated_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL
|
||||
)]';
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
MERGE INTO sg_qa_vector_config c
|
||||
USING (
|
||||
SELECT 'CREDENTIAL_NAME' AS config_key, 'SGMP_POC_QA_VECTOR_CRED' AS config_value FROM dual
|
||||
UNION ALL SELECT 'ENDPOINT_URL', 'https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText' FROM dual
|
||||
UNION ALL SELECT 'MODEL_NAME', 'cohere.embed-v4.0' FROM dual
|
||||
UNION ALL SELECT 'DIMENSION', '1536' FROM dual
|
||||
) s
|
||||
ON (c.config_key = s.config_key)
|
||||
WHEN MATCHED THEN UPDATE SET c.config_value = s.config_value, c.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (config_key, config_value) VALUES (s.config_key, s.config_value);
|
||||
/
|
||||
|
||||
DECLARE
|
||||
v_count PLS_INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_count
|
||||
FROM user_tables
|
||||
WHERE table_name = 'SG_QA_VECTOR_EXAMPLE';
|
||||
|
||||
IF v_count = 0 THEN
|
||||
EXECUTE IMMEDIATE q'[
|
||||
CREATE TABLE sg_qa_vector_example (
|
||||
example_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
question CLOB NOT NULL,
|
||||
answer_sql CLOB NOT NULL,
|
||||
answer_text CLOB,
|
||||
embedding_input CLOB NOT NULL,
|
||||
embedding VECTOR(1536, FLOAT32) NOT NULL,
|
||||
embedding_model VARCHAR2(128) DEFAULT 'cohere.embed-v4.0' NOT NULL,
|
||||
created_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL
|
||||
)]';
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_params(p_input_type IN VARCHAR2)
|
||||
RETURN CLOB
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_credential VARCHAR2(4000);
|
||||
v_endpoint VARCHAR2(4000);
|
||||
v_model VARCHAR2(4000);
|
||||
BEGIN
|
||||
SELECT MAX(CASE WHEN config_key = 'CREDENTIAL_NAME' THEN config_value END),
|
||||
MAX(CASE WHEN config_key = 'ENDPOINT_URL' THEN config_value END),
|
||||
MAX(CASE WHEN config_key = 'MODEL_NAME' THEN config_value END)
|
||||
INTO v_credential, v_endpoint, v_model
|
||||
FROM sg_qa_vector_config;
|
||||
|
||||
IF v_credential IS NULL OR v_endpoint IS NULL OR v_model IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20001, 'SG QA vector configuration is incomplete.');
|
||||
END IF;
|
||||
|
||||
RETURN TO_CLOB('{"provider":"ocigenai","credential_name":"')
|
||||
|| v_credential
|
||||
|| '","url":"' || v_endpoint
|
||||
|| '","model":"' || v_model
|
||||
|| '","truncate":"END"}';
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_store(
|
||||
p_question IN CLOB,
|
||||
p_answer_sql IN CLOB,
|
||||
p_answer IN CLOB DEFAULT NULL
|
||||
) RETURN NUMBER
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_example_id NUMBER;
|
||||
BEGIN
|
||||
IF p_question IS NULL OR p_answer_sql IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20002, 'question and answer_sql are required.');
|
||||
END IF;
|
||||
|
||||
v_input := TO_CLOB('Question: ') || p_question
|
||||
|| TO_CLOB(CHR(10) || 'Answer SQL: ') || p_answer_sql
|
||||
|| CASE WHEN p_answer IS NULL THEN NULL ELSE TO_CLOB(CHR(10) || 'Answer: ') || p_answer END;
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model
|
||||
) VALUES (
|
||||
p_question, p_answer_sql, p_answer, v_input, v_embedding, 'cohere.embed-v4.0'
|
||||
) RETURNING example_id INTO v_example_id;
|
||||
|
||||
COMMIT;
|
||||
RETURN v_example_id;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3
|
||||
) RETURN SYS_REFCURSOR
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
|
||||
END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question,
|
||||
JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id,
|
||||
question,
|
||||
answer_sql,
|
||||
answer_text,
|
||||
embedding_model,
|
||||
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_context(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3
|
||||
) RETURN CLOB
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_results SYS_REFCURSOR;
|
||||
v_id NUMBER;
|
||||
v_q CLOB;
|
||||
v_sql CLOB;
|
||||
v_answer CLOB;
|
||||
v_model VARCHAR2(128);
|
||||
v_dist NUMBER;
|
||||
v_context CLOB := EMPTY_CLOB();
|
||||
BEGIN
|
||||
v_results := sg_qa_vector_search(p_question, p_top_k);
|
||||
LOOP
|
||||
FETCH v_results INTO v_id, v_q, v_sql, v_answer, v_model, v_dist;
|
||||
EXIT WHEN v_results%NOTFOUND;
|
||||
v_context := v_context
|
||||
|| CASE WHEN DBMS_LOB.GETLENGTH(v_context) = 0 THEN NULL ELSE CHR(10) || CHR(10) END
|
||||
|| '[Example ' || v_id || ', cosine_distance=' || TO_CHAR(v_dist, 'FM0D000000') || ']' || CHR(10)
|
||||
|| 'Question: ' || v_q || CHR(10)
|
||||
|| 'Answer SQL: ' || v_sql
|
||||
|| CASE WHEN v_answer IS NULL THEN NULL ELSE CHR(10) || 'Answer: ' || v_answer END;
|
||||
END LOOP;
|
||||
CLOSE v_results;
|
||||
RETURN v_context;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMENT ON TABLE sg_qa_vector_example IS
|
||||
'Question-to-SQL QA examples embedded with OCI GenAI Cohere Embed 4 for retrieval-augmented prompt context.';
|
||||
|
||||
COMMENT ON COLUMN sg_qa_vector_example.embedding IS
|
||||
'1536-dimensional Cohere Embed 4 document embedding; generated through the dedicated SGMP vector API credential.';
|
||||
148
database/adb/76_sgmp_annotation_api.sql
Normal file
148
database/adb/76_sgmp_annotation_api.sql
Normal file
@@ -0,0 +1,148 @@
|
||||
-- SGMP PoC: validated table/view and column annotation API
|
||||
-- Issue: #734
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgmp_set_annotation(
|
||||
p_schema_name IN VARCHAR2,
|
||||
p_target_kind IN VARCHAR2,
|
||||
p_object_name IN VARCHAR2,
|
||||
p_column_name IN VARCHAR2 DEFAULT NULL,
|
||||
p_change_text IN VARCHAR2 DEFAULT NULL,
|
||||
p_annotation_name IN VARCHAR2 DEFAULT 'AI_GUIDANCE'
|
||||
) RETURN VARCHAR2
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
l_schema_name VARCHAR2(128);
|
||||
l_object_name VARCHAR2(128);
|
||||
l_column_name VARCHAR2(128);
|
||||
l_annotation_name VARCHAR2(128);
|
||||
l_target_kind VARCHAR2(20);
|
||||
l_object_type VARCHAR2(30);
|
||||
l_exists PLS_INTEGER := 0;
|
||||
l_action VARCHAR2(20);
|
||||
l_sql VARCHAR2(32767);
|
||||
|
||||
FUNCTION simple_name(p_value VARCHAR2, p_label VARCHAR2) RETURN VARCHAR2 IS
|
||||
l_value VARCHAR2(128) := UPPER(TRIM(p_value));
|
||||
BEGIN
|
||||
IF l_value IS NULL OR NOT REGEXP_LIKE(l_value, '^[A-Z][A-Z0-9_$#]{0,127}$') THEN
|
||||
RAISE_APPLICATION_ERROR(-20001, p_label || ' has an invalid format.');
|
||||
END IF;
|
||||
RETURN l_value;
|
||||
END;
|
||||
|
||||
FUNCTION qname(p_value VARCHAR2) RETURN VARCHAR2 IS
|
||||
BEGIN
|
||||
RETURN DBMS_ASSERT.ENQUOTE_NAME(p_value, FALSE);
|
||||
END;
|
||||
|
||||
FUNCTION literal(p_value VARCHAR2) RETURN VARCHAR2 IS
|
||||
BEGIN
|
||||
RETURN DBMS_ASSERT.ENQUOTE_LITERAL(p_value);
|
||||
END;
|
||||
BEGIN
|
||||
l_schema_name := simple_name(p_schema_name, 'schema_name');
|
||||
l_object_name := simple_name(p_object_name, 'object_name');
|
||||
l_target_kind := UPPER(TRIM(p_target_kind));
|
||||
IF l_target_kind NOT IN ('TABLE', 'COLUMN') THEN
|
||||
RAISE_APPLICATION_ERROR(-20002, 'target_kind must be TABLE or COLUMN.');
|
||||
END IF;
|
||||
l_annotation_name := simple_name(p_annotation_name, 'annotation_name');
|
||||
IF p_change_text IS NULL OR LENGTH(p_change_text) = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20003, 'change_text must not be empty.');
|
||||
END IF;
|
||||
IF LENGTH(p_change_text) > 4000 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'change_text must be 4000 characters or less.');
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
SELECT object_type
|
||||
INTO l_object_type
|
||||
FROM all_objects
|
||||
WHERE owner = l_schema_name
|
||||
AND object_name = l_object_name
|
||||
AND object_type IN ('TABLE', 'VIEW')
|
||||
AND ROWNUM = 1;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'TABLE or VIEW object was not found.');
|
||||
END;
|
||||
|
||||
IF l_target_kind = 'COLUMN' THEN
|
||||
IF p_column_name IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20006, 'column_name is required for COLUMN target.');
|
||||
END IF;
|
||||
IF l_object_type = 'VIEW' THEN
|
||||
RAISE_APPLICATION_ERROR(-20007, 'VIEW column annotations cannot be altered by Oracle.');
|
||||
END IF;
|
||||
l_column_name := simple_name(p_column_name, 'column_name');
|
||||
BEGIN
|
||||
SELECT 1 INTO l_exists
|
||||
FROM all_tab_columns
|
||||
WHERE owner = l_schema_name
|
||||
AND table_name = l_object_name
|
||||
AND column_name = l_column_name
|
||||
AND ROWNUM = 1;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
RAISE_APPLICATION_ERROR(-20008, 'column_name does not exist on the table.');
|
||||
END;
|
||||
ELSIF p_column_name IS NOT NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20009, 'column_name is not allowed for TABLE target.');
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO l_exists
|
||||
FROM all_annotations_usage
|
||||
WHERE annotation_owner = l_schema_name
|
||||
AND object_name = l_object_name
|
||||
AND object_type = l_object_type
|
||||
AND annotation_name = l_annotation_name
|
||||
AND (l_target_kind = 'TABLE' AND column_name IS NULL
|
||||
OR l_target_kind = 'COLUMN' AND column_name = l_column_name);
|
||||
|
||||
IF l_exists > 0 THEN
|
||||
IF l_target_kind = 'COLUMN' THEN
|
||||
l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name)
|
||||
|| ' MODIFY ' || qname(l_column_name) || ' ANNOTATIONS (DROP '
|
||||
|| qname(l_annotation_name) || ')';
|
||||
ELSIF l_object_type = 'VIEW' THEN
|
||||
l_sql := 'ALTER VIEW ' || qname(l_schema_name) || '.' || qname(l_object_name)
|
||||
|| ' ANNOTATIONS (DROP ' || qname(l_annotation_name) || ')';
|
||||
ELSE
|
||||
l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name)
|
||||
|| ' ANNOTATIONS (DROP ' || qname(l_annotation_name) || ')';
|
||||
END IF;
|
||||
EXECUTE IMMEDIATE l_sql;
|
||||
l_action := 'REPLACED';
|
||||
ELSE
|
||||
l_action := 'ADDED';
|
||||
END IF;
|
||||
|
||||
IF l_target_kind = 'COLUMN' THEN
|
||||
l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name)
|
||||
|| ' MODIFY ' || qname(l_column_name) || ' ANNOTATIONS (ADD '
|
||||
|| qname(l_annotation_name) || ' ' || literal(p_change_text) || ')';
|
||||
ELSIF l_object_type = 'VIEW' THEN
|
||||
l_sql := 'ALTER VIEW ' || qname(l_schema_name) || '.' || qname(l_object_name)
|
||||
|| ' ANNOTATIONS (ADD ' || qname(l_annotation_name) || ' '
|
||||
|| literal(p_change_text) || ')';
|
||||
ELSE
|
||||
l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name)
|
||||
|| ' ANNOTATIONS (ADD ' || qname(l_annotation_name) || ' '
|
||||
|| literal(p_change_text) || ')';
|
||||
END IF;
|
||||
EXECUTE IMMEDIATE l_sql;
|
||||
|
||||
RETURN l_action || ': ' || l_schema_name || '.' || l_object_name
|
||||
|| CASE WHEN l_column_name IS NULL THEN '' ELSE '.' || l_column_name END
|
||||
|| ' [' || l_annotation_name || ']';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE BETWEEN -20099 AND -20000 THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
RAISE_APPLICATION_ERROR(-20099, 'annotation change failed: ' || SQLERRM);
|
||||
END;
|
||||
/
|
||||
|
||||
SHOW ERRORS FUNCTION sgmp_set_annotation;
|
||||
175
database/adb/77_sgmp_game_scope_resolver.sql
Normal file
175
database/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
database/adb/78_sgmp_game_alias_logical_joins.sql
Normal file
36
database/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;
|
||||
/
|
||||
179
database/adb/79_sg_game_catalog_vector.sql
Normal file
179
database/adb/79_sg_game_catalog_vector.sql
Normal file
@@ -0,0 +1,179 @@
|
||||
-- Metadata-driven game catalog for deterministic scope resolution.
|
||||
-- No customer game names or prefixes are embedded in this script.
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE q'[
|
||||
CREATE TABLE sg_game_catalog (
|
||||
game_key VARCHAR2(128) PRIMARY KEY,
|
||||
game_id VARCHAR2(128) NOT NULL,
|
||||
game_prefix VARCHAR2(128),
|
||||
game_nm VARCHAR2(512),
|
||||
game_alias_nm VARCHAR2(512),
|
||||
user_master_object_name VARCHAR2(128),
|
||||
search_text CLOB NOT NULL,
|
||||
embedding VECTOR(1536, FLOAT32),
|
||||
active_yn CHAR(1) DEFAULT 'Y' NOT NULL,
|
||||
priority NUMBER DEFAULT 100 NOT NULL,
|
||||
updated_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL
|
||||
)]';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -955 THEN RAISE; END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
DECLARE
|
||||
v_count PLS_INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_count
|
||||
FROM user_tab_columns
|
||||
WHERE table_name = 'SG_GAME_CATALOG'
|
||||
AND column_name = 'USER_MASTER_OBJECT_NAME';
|
||||
IF v_count = 0 THEN
|
||||
EXECUTE IMMEDIATE
|
||||
'ALTER TABLE sg_game_catalog ADD (user_master_object_name VARCHAR2(128))';
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
MERGE INTO sg_game_catalog c
|
||||
USING (
|
||||
SELECT
|
||||
game_id AS game_key,
|
||||
game_id,
|
||||
MAX(game_prefix) AS game_prefix,
|
||||
MAX(game_nm) AS game_nm,
|
||||
LISTAGG(game_alias_nm, ' ') WITHIN GROUP (ORDER BY game_alias_nm) AS game_alias_nm,
|
||||
game_id || ' ' || MAX(NVL(game_nm, '')) || ' '
|
||||
|| LISTAGG(NVL(game_alias_nm, ''), ' ') WITHIN GROUP (ORDER BY game_alias_nm)
|
||||
|| ' ' || MAX(NVL(game_prefix, '')) AS search_text
|
||||
FROM comn_game_alias_bas
|
||||
WHERE use_yn = 'Y'
|
||||
GROUP BY game_id
|
||||
) s
|
||||
ON (c.game_key = s.game_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
c.game_id = s.game_id,
|
||||
c.game_prefix = s.game_prefix,
|
||||
c.game_nm = s.game_nm,
|
||||
c.game_alias_nm = s.game_alias_nm,
|
||||
c.search_text = s.search_text,
|
||||
c.active_yn = 'Y',
|
||||
c.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT
|
||||
(game_key, game_id, game_prefix, game_nm, game_alias_nm, search_text)
|
||||
VALUES
|
||||
(s.game_key, s.game_id, s.game_prefix, s.game_nm, s.game_alias_nm, s.search_text);
|
||||
/
|
||||
|
||||
-- Keep operator-maintained games in the same canonical catalog. These rows
|
||||
-- remain observable even when their current approved physical object is null.
|
||||
MERGE INTO sg_game_catalog c
|
||||
USING (
|
||||
SELECT
|
||||
r.game_key,
|
||||
r.game_key AS game_id,
|
||||
MAX(r.game_prefix) AS game_prefix,
|
||||
MAX(r.display_name) AS game_nm,
|
||||
LISTAGG(r.game_alias, ' ') WITHIN GROUP (ORDER BY r.game_alias) AS game_alias_nm,
|
||||
r.game_key || ' ' || MAX(NVL(r.display_name, '')) || ' '
|
||||
|| LISTAGG(NVL(r.game_alias, ''), ' ') WITHIN GROUP (ORDER BY r.game_alias)
|
||||
|| ' ' || MAX(NVL(r.game_prefix, '')) AS search_text
|
||||
FROM sg_game_scope_registry r
|
||||
WHERE r.active_yn = 'Y'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM comn_game_alias_bas a
|
||||
WHERE a.use_yn = 'Y'
|
||||
AND a.game_id = r.game_key
|
||||
)
|
||||
GROUP BY r.game_key
|
||||
) s
|
||||
ON (c.game_key = s.game_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
c.game_id = s.game_id,
|
||||
c.game_prefix = s.game_prefix,
|
||||
c.game_nm = s.game_nm,
|
||||
c.game_alias_nm = s.game_alias_nm,
|
||||
c.search_text = s.search_text,
|
||||
c.active_yn = 'Y',
|
||||
c.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT
|
||||
(game_key, game_id, game_prefix, game_nm, game_alias_nm, search_text)
|
||||
VALUES
|
||||
(s.game_key, s.game_id, s.game_prefix, s.game_nm, s.game_alias_nm, s.search_text);
|
||||
/
|
||||
|
||||
-- Resolve the physical user-master object from current valid objects and the
|
||||
-- current Select AI object lists. No game, prefix, or object name is embedded
|
||||
-- in this policy.
|
||||
MERGE INTO sg_game_catalog c
|
||||
USING (
|
||||
WITH approved_objects AS (
|
||||
SELECT DISTINCT UPPER(j.object_name) AS object_name
|
||||
FROM user_cloud_ai_profile_attributes p,
|
||||
JSON_TABLE(
|
||||
p.attribute_value,
|
||||
'$[*]' COLUMNS (object_name VARCHAR2(128) PATH '$.name')
|
||||
) j
|
||||
INNER JOIN user_objects o
|
||||
ON o.object_name = UPPER(j.object_name)
|
||||
AND o.object_type IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
|
||||
AND o.status = 'VALID'
|
||||
WHERE p.attribute_name = 'object_list'
|
||||
)
|
||||
SELECT c2.game_key,
|
||||
MIN(a.object_name) AS user_master_object_name
|
||||
FROM sg_game_catalog c2
|
||||
LEFT JOIN approved_objects a
|
||||
ON c2.game_prefix IS NOT NULL
|
||||
AND a.object_name = UPPER(c2.game_prefix || '_COMN_USER_MST')
|
||||
GROUP BY c2.game_key
|
||||
) s
|
||||
ON (c.game_key = s.game_key)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
c.user_master_object_name = s.user_master_object_name,
|
||||
c.updated_at = SYSTIMESTAMP;
|
||||
/
|
||||
|
||||
-- The catalog is small. Recompute embeddings after synchronization so changed
|
||||
-- aliases and registry-only games cannot retain a stale or null vector.
|
||||
UPDATE sg_game_catalog c
|
||||
SET c.embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
c.search_text,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
),
|
||||
c.updated_at = SYSTIMESTAMP
|
||||
WHERE c.active_yn = 'Y';
|
||||
/
|
||||
|
||||
COMMENT ON TABLE sg_game_catalog IS
|
||||
'Customer-owned game catalog used to resolve query scope before NL2SQL.';
|
||||
COMMENT ON COLUMN sg_game_catalog.embedding IS
|
||||
'Vector representation of game names and aliases, generated with the configured vector credential.';
|
||||
COMMENT ON COLUMN sg_game_catalog.user_master_object_name IS
|
||||
'Current valid Select AI-approved physical user-master object for this game; null when unavailable.';
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_game_catalog_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 5
|
||||
) RETURN SYS_REFCURSOR AUTHID DEFINER IS
|
||||
v_query VECTOR;
|
||||
v_result SYS_REFCURSOR;
|
||||
BEGIN
|
||||
v_query := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question,
|
||||
JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
OPEN v_result FOR
|
||||
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
|
||||
user_master_object_name,
|
||||
VECTOR_DISTANCE(embedding, v_query, COSINE) AS cosine_distance
|
||||
FROM sg_game_catalog
|
||||
WHERE active_yn = 'Y' AND embedding IS NOT NULL
|
||||
ORDER BY VECTOR_DISTANCE(embedding, v_query, COSINE), priority, game_key
|
||||
FETCH FIRST LEAST(GREATEST(NVL(p_top_k, 5), 1), 20) ROWS ONLY;
|
||||
RETURN v_result;
|
||||
END;
|
||||
/
|
||||
35
database/adb/80_sg_game_mention_extract.sql
Normal file
35
database/adb/80_sg_game_mention_extract.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- One short OCI Chat call extracts game-name mentions. Identity resolution is
|
||||
-- performed later by catalog vector score, not by another LLM call.
|
||||
CREATE OR REPLACE FUNCTION sg_game_extract_mentions(p_question IN CLOB)
|
||||
RETURN CLOB AUTHID DEFINER
|
||||
IS
|
||||
v_prompt CLOB;
|
||||
v_result CLOB;
|
||||
v_extract JSON_OBJECT_T;
|
||||
BEGIN
|
||||
v_prompt := 'Extract only game identity mentions from the user question. '
|
||||
|| 'A registered game title, alias, GAME_ID, GAME_PREFIX, or catalog key is a game mention and must be preserved exactly as written. '
|
||||
|| 'Metrics, acronyms, dates, filters, and database object or column names are not game mentions unless they are themselves an explicit registered game identity. '
|
||||
|| 'When a title-like noun directly qualifies a game data request such as user master, character, sales, AU, NRU, server, or game log, preserve that noun as a game-name mention even when it is not in a catalog. '
|
||||
|| 'Do not discard an unknown title merely because it cannot be resolved. General scope words such as common, overall, all, total, or every are not game-name mentions unless they are part of an explicit title. '
|
||||
|| 'Return exactly one JSON object with keys game_mentions (array of strings) '
|
||||
|| 'and scope_hint (GLOBAL, SINGLE_GAME, MULTI_GAME, ALL_GAMES, UNKNOWN). '
|
||||
|| 'Do not resolve one game identity to another and do not generate SQL. '
|
||||
|| 'Return raw JSON only: no prose, no Markdown, and no code fence. Question: '
|
||||
|| DBMS_LOB.SUBSTR(p_question, 4000, 1);
|
||||
|
||||
v_result := DBMS_CLOUD_AI.GENERATE(
|
||||
prompt => v_prompt,
|
||||
profile_name => 'SGMP_POC_OCI_COHERE_COMMAND',
|
||||
action => 'chat'
|
||||
);
|
||||
v_extract := JSON_OBJECT_T.parse(v_result);
|
||||
IF NOT v_extract.has('game_mentions') OR NOT v_extract.has('scope_hint') THEN
|
||||
RAISE_APPLICATION_ERROR(
|
||||
-20091,
|
||||
'Game mention extraction must return game_mentions and scope_hint JSON keys.'
|
||||
);
|
||||
END IF;
|
||||
RETURN v_result;
|
||||
END;
|
||||
/
|
||||
485
database/adb/81_sg_game_query_plan.sql
Normal file
485
database/adb/81_sg_game_query_plan.sql
Normal file
@@ -0,0 +1,485 @@
|
||||
-- Vector-score candidate selection. The closest catalog vector is accepted
|
||||
-- only when it is within the customer-managed policy threshold.
|
||||
CREATE OR REPLACE FUNCTION sg_game_match_candidate(
|
||||
p_mention IN CLOB,
|
||||
p_candidates_json IN CLOB
|
||||
) RETURN CLOB AUTHID DEFINER
|
||||
IS
|
||||
v_candidates JSON_ARRAY_T;
|
||||
v_candidate JSON_OBJECT_T;
|
||||
v_result JSON_OBJECT_T := JSON_OBJECT_T();
|
||||
v_threshold NUMBER;
|
||||
v_distance NUMBER;
|
||||
v_game_key VARCHAR2(128);
|
||||
BEGIN
|
||||
SELECT number_value
|
||||
INTO v_threshold
|
||||
FROM sg_game_scope_policy
|
||||
WHERE policy_key = 'GAME_ALIAS_MAX_COSINE_DISTANCE'
|
||||
AND active_yn = 'Y';
|
||||
|
||||
v_candidates := JSON_ARRAY_T.parse(p_candidates_json);
|
||||
IF v_candidates.get_size = 0 THEN
|
||||
v_result.put('status', 'UNMATCHED');
|
||||
v_result.put_null('game_key');
|
||||
v_result.put('reason', 'No active game catalog vector candidate was returned.');
|
||||
RETURN v_result.to_clob;
|
||||
END IF;
|
||||
|
||||
v_candidate := TREAT(v_candidates.get(0) AS JSON_OBJECT_T);
|
||||
v_distance := v_candidate.get_number('cosineDistance');
|
||||
v_game_key := v_candidate.get_string('gameKey');
|
||||
|
||||
IF v_distance <= v_threshold THEN
|
||||
v_result.put('status', 'MATCHED');
|
||||
v_result.put('game_key', v_game_key);
|
||||
v_result.put(
|
||||
'reason',
|
||||
'Closest catalog vector distance '
|
||||
|| TO_CHAR(v_distance, 'FM0D000000')
|
||||
|| ' is within configured maximum '
|
||||
|| TO_CHAR(v_threshold, 'FM0D000000') || '.'
|
||||
);
|
||||
ELSE
|
||||
v_result.put('status', 'UNMATCHED');
|
||||
v_result.put_null('game_key');
|
||||
v_result.put(
|
||||
'reason',
|
||||
'Closest catalog vector distance '
|
||||
|| TO_CHAR(v_distance, 'FM0D000000')
|
||||
|| ' exceeds configured maximum '
|
||||
|| TO_CHAR(v_threshold, 'FM0D000000') || '.'
|
||||
);
|
||||
END IF;
|
||||
RETURN v_result.to_clob;
|
||||
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
|
||||
-- function and returns its structured result.
|
||||
CREATE OR REPLACE FUNCTION sg_game_query_plan(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 5
|
||||
) RETURN CLOB AUTHID DEFINER
|
||||
IS
|
||||
TYPE t_seen_game_keys IS TABLE OF BOOLEAN INDEX BY VARCHAR2(128);
|
||||
|
||||
v_extract_raw CLOB;
|
||||
v_extract JSON_OBJECT_T;
|
||||
v_mentions JSON_ARRAY_T;
|
||||
v_scope_type VARCHAR2(30);
|
||||
v_target_type VARCHAR2(20);
|
||||
v_targets 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_mention_results JSON_ARRAY_T := JSON_ARRAY_T();
|
||||
v_execution_tasks JSON_ARRAY_T := JSON_ARRAY_T();
|
||||
v_seen_game_keys t_seen_game_keys;
|
||||
v_result JSON_OBJECT_T := JSON_OBJECT_T();
|
||||
v_reference_summary JSON_OBJECT_T := JSON_OBJECT_T();
|
||||
v_select_ai_reference CLOB;
|
||||
v_plan_status VARCHAR2(30);
|
||||
v_matched_count PLS_INTEGER := 0;
|
||||
v_data_eligible_count PLS_INTEGER := 0;
|
||||
v_unmatched_count PLS_INTEGER := 0;
|
||||
|
||||
v_mention VARCHAR2(1000);
|
||||
v_candidates JSON_ARRAY_T;
|
||||
v_candidate JSON_OBJECT_T;
|
||||
v_decision_raw CLOB;
|
||||
v_decision JSON_OBJECT_T;
|
||||
v_decision_status VARCHAR2(30);
|
||||
v_selected_key VARCHAR2(128);
|
||||
v_reason VARCHAR2(4000);
|
||||
v_matched_candidate JSON_OBJECT_T;
|
||||
v_mention_result JSON_OBJECT_T;
|
||||
v_target JSON_OBJECT_T;
|
||||
v_unmatched_item JSON_OBJECT_T;
|
||||
v_execution_task JSON_OBJECT_T;
|
||||
v_worker_arguments JSON_OBJECT_T;
|
||||
v_fewshot_arguments JSON_OBJECT_T;
|
||||
v_task_plan JSON_OBJECT_T;
|
||||
v_task_reference JSON_OBJECT_T;
|
||||
v_task_targets JSON_ARRAY_T;
|
||||
v_excluded_mentions JSON_ARRAY_T;
|
||||
v_task_question CLOB;
|
||||
|
||||
v_cursor SYS_REFCURSOR;
|
||||
v_game_key VARCHAR2(128);
|
||||
v_game_id VARCHAR2(128);
|
||||
v_game_prefix VARCHAR2(128);
|
||||
v_game_nm VARCHAR2(512);
|
||||
v_game_alias_nm VARCHAR2(512);
|
||||
v_user_master_object_name VARCHAR2(128);
|
||||
v_fact_scope_status VARCHAR2(30);
|
||||
v_cosine_distance NUMBER;
|
||||
BEGIN
|
||||
v_extract_raw := sg_game_extract_mentions(p_question);
|
||||
v_extract := JSON_OBJECT_T.parse(v_extract_raw);
|
||||
v_mentions := v_extract.get_array('game_mentions');
|
||||
v_scope_type := NVL(v_extract.get_string('scope_hint'), 'UNKNOWN');
|
||||
|
||||
IF v_scope_type = 'ALL_GAMES' THEN
|
||||
v_target_type := 'ALL';
|
||||
ELSIF v_mentions.get_size = 0 THEN
|
||||
v_target_type := 'NONE';
|
||||
ELSIF v_mentions.get_size = 1 THEN
|
||||
v_target_type := 'SINGLE';
|
||||
ELSE
|
||||
v_target_type := 'MULTI';
|
||||
END IF;
|
||||
|
||||
IF v_target_type = 'ALL' THEN
|
||||
FOR game_row IN (
|
||||
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
|
||||
user_master_object_name
|
||||
FROM SGMP_POC.sg_game_catalog
|
||||
WHERE active_yn = 'Y'
|
||||
ORDER BY priority, game_key
|
||||
) LOOP
|
||||
v_target := JSON_OBJECT_T();
|
||||
v_target.put_null('mention');
|
||||
v_target.put('status', 'CATALOG');
|
||||
v_target.put('matchStatus', 'MATCHED');
|
||||
v_target.put('gameKey', game_row.game_key);
|
||||
v_target.put('gameId', game_row.game_id);
|
||||
v_target.put('gamePrefix', game_row.game_prefix);
|
||||
v_target.put('gameName', game_row.game_nm);
|
||||
v_target.put('aliases', game_row.game_alias_nm);
|
||||
v_target.put(
|
||||
'userMasterObjectName',
|
||||
game_row.user_master_object_name
|
||||
);
|
||||
v_target.put(
|
||||
'objectStatus',
|
||||
CASE WHEN game_row.user_master_object_name IS NULL
|
||||
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('reasonCode', 'ALL_GAMES_CATALOG');
|
||||
v_target.put('reason', 'Active game returned from the database catalog.');
|
||||
v_targets.append(v_target);
|
||||
v_matched_count := v_matched_count + 1;
|
||||
IF game_row.user_master_object_name IS NOT NULL THEN
|
||||
v_supported.append(v_target);
|
||||
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;
|
||||
ELSIF v_target_type = 'NONE' THEN
|
||||
v_target := JSON_OBJECT_T();
|
||||
v_target.put_null('mention');
|
||||
v_target.put('status', 'NONE');
|
||||
v_target.put('matchStatus', 'NOT_APPLICABLE');
|
||||
v_target.put_null('gameKey');
|
||||
v_target.put_null('gameId');
|
||||
v_target.put_null('gamePrefix');
|
||||
v_target.put_null('gameName');
|
||||
v_target.put_null('aliases');
|
||||
v_target.put_null('userMasterObjectName');
|
||||
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('reasonCode', 'NO_GAME_TARGET');
|
||||
v_target.put(
|
||||
'reason',
|
||||
'The question does not select a particular game.'
|
||||
);
|
||||
v_targets.append(v_target);
|
||||
ELSE
|
||||
FOR i IN 0 .. v_mentions.get_size - 1 LOOP
|
||||
v_mention := v_mentions.get_string(i);
|
||||
v_candidates := JSON_ARRAY_T();
|
||||
v_cursor := SGMP_POC.sg_game_catalog_search(
|
||||
v_mention,
|
||||
LEAST(GREATEST(NVL(p_top_k, 5), 1), 20)
|
||||
);
|
||||
|
||||
LOOP
|
||||
FETCH v_cursor INTO
|
||||
v_game_key,
|
||||
v_game_id,
|
||||
v_game_prefix,
|
||||
v_game_nm,
|
||||
v_game_alias_nm,
|
||||
v_user_master_object_name,
|
||||
v_cosine_distance;
|
||||
EXIT WHEN v_cursor%NOTFOUND;
|
||||
|
||||
v_candidate := JSON_OBJECT_T();
|
||||
v_candidate.put('gameKey', v_game_key);
|
||||
v_candidate.put('gameId', v_game_id);
|
||||
v_candidate.put('gamePrefix', v_game_prefix);
|
||||
v_candidate.put('gameName', v_game_nm);
|
||||
v_candidate.put('aliases', v_game_alias_nm);
|
||||
v_candidate.put(
|
||||
'userMasterObjectName',
|
||||
v_user_master_object_name
|
||||
);
|
||||
v_candidate.put(
|
||||
'objectStatus',
|
||||
CASE WHEN v_user_master_object_name IS NULL
|
||||
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_candidates.append(v_candidate);
|
||||
END LOOP;
|
||||
CLOSE v_cursor;
|
||||
|
||||
v_decision_raw := sg_game_match_candidate(
|
||||
v_mention,
|
||||
v_candidates.to_clob
|
||||
);
|
||||
v_decision := JSON_OBJECT_T.parse(v_decision_raw);
|
||||
v_decision_status := UPPER(
|
||||
NVL(v_decision.get_string('status'), 'UNMATCHED')
|
||||
);
|
||||
v_selected_key := v_decision.get_string('game_key');
|
||||
v_reason := v_decision.get_string('reason');
|
||||
v_matched_candidate := NULL;
|
||||
|
||||
IF v_decision_status = 'MATCHED' AND v_selected_key IS NOT NULL THEN
|
||||
FOR j IN 0 .. v_candidates.get_size - 1 LOOP
|
||||
v_candidate := TREAT(v_candidates.get(j) AS JSON_OBJECT_T);
|
||||
IF v_candidate.get_string('gameKey') = v_selected_key THEN
|
||||
v_matched_candidate := v_candidate;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
v_mention_result := JSON_OBJECT_T();
|
||||
v_mention_result.put('mention', v_mention);
|
||||
v_mention_result.put('candidateGames', v_candidates);
|
||||
v_mention_result.put('reason', v_reason);
|
||||
|
||||
IF v_matched_candidate IS NOT NULL THEN
|
||||
v_mention_result.put('status', 'MATCHED');
|
||||
v_mention_result.put('reasonCode', 'VECTOR_SCORE_MATCH');
|
||||
v_mention_result.put('matchedGameKey', v_selected_key);
|
||||
|
||||
v_target := JSON_OBJECT_T.parse(v_matched_candidate.to_clob);
|
||||
v_target.put('mention', v_mention);
|
||||
v_target.put('status', 'MATCHED');
|
||||
v_target.put('matchStatus', 'MATCHED');
|
||||
v_target.put('reasonCode', 'VECTOR_SCORE_MATCH');
|
||||
v_target.put('reason', v_reason);
|
||||
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
|
||||
v_supported.append(v_target);
|
||||
v_seen_game_keys(v_selected_key) := TRUE;
|
||||
END IF;
|
||||
ELSE
|
||||
v_mention_result.put('status', 'UNMATCHED');
|
||||
v_mention_result.put('reasonCode', 'VECTOR_SCORE_OVER_THRESHOLD');
|
||||
|
||||
v_target := JSON_OBJECT_T();
|
||||
v_target.put('mention', v_mention);
|
||||
v_target.put('status', 'UNMATCHED');
|
||||
v_target.put('matchStatus', 'UNMATCHED');
|
||||
v_target.put_null('gameKey');
|
||||
v_target.put_null('gameId');
|
||||
v_target.put_null('gamePrefix');
|
||||
v_target.put_null('gameName');
|
||||
v_target.put_null('aliases');
|
||||
v_target.put_null('userMasterObjectName');
|
||||
v_target.put('objectStatus', 'UNAVAILABLE');
|
||||
v_target.put('factScopeStatus', 'UNAVAILABLE');
|
||||
v_target.put('dataStatus', 'UNAVAILABLE');
|
||||
v_target.put_null('cosineDistance');
|
||||
v_target.put(
|
||||
'reasonCode',
|
||||
v_mention_result.get_string('reasonCode')
|
||||
);
|
||||
v_target.put('reason', v_reason);
|
||||
v_targets.append(v_target);
|
||||
|
||||
v_unmatched_item := JSON_OBJECT_T();
|
||||
v_unmatched_item.put('mention', v_mention);
|
||||
v_unmatched_item.put(
|
||||
'reasonCode',
|
||||
v_mention_result.get_string('reasonCode')
|
||||
);
|
||||
v_unmatched_item.put('reason', v_reason);
|
||||
v_unmatched.append(v_unmatched_item);
|
||||
v_unresolved.append(v_target);
|
||||
v_unmatched_count := v_unmatched_count + 1;
|
||||
END IF;
|
||||
v_mention_results.append(v_mention_result);
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
IF v_target_type = 'NONE' THEN
|
||||
v_plan_status := 'NO_TARGET';
|
||||
ELSIF v_matched_count = 0 THEN
|
||||
v_plan_status := 'UNMATCHED';
|
||||
ELSIF v_data_eligible_count = 0 THEN
|
||||
v_plan_status := 'UNAVAILABLE';
|
||||
ELSIF v_unmatched_count > 0 THEN
|
||||
v_plan_status := 'PARTIAL';
|
||||
ELSE
|
||||
v_plan_status := 'SUPPORTED';
|
||||
END IF;
|
||||
|
||||
-- This is the sole Select AI handoff contract. It is deliberately generic:
|
||||
-- game values and physical objects come only from the database lookup above.
|
||||
v_reference_summary.put('targetType', v_target_type);
|
||||
v_reference_summary.put('status', v_plan_status);
|
||||
v_reference_summary.put('gameTargets', v_supported);
|
||||
v_select_ai_reference := '[GAME QUERY REFERENCE]' || CHR(10) || CHR(10)
|
||||
|| v_reference_summary.to_clob()
|
||||
|| CHR(10) || CHR(10)
|
||||
|| 'Field meanings:' || CHR(10) || CHR(10)
|
||||
|| '* targetType:' || CHR(10)
|
||||
|| ' * NONE: No game was resolved.' || CHR(10)
|
||||
|| ' * SINGLE: Exactly one game was resolved.' || CHR(10)
|
||||
|| ' * MULTI: Multiple specific games were resolved.' || CHR(10)
|
||||
|| ' * ALL: The query applies to all supported games.' || CHR(10)
|
||||
|| '* status: The result of game-target resolution.' || CHR(10)
|
||||
|| '* gameTargets: The exact games resolved by the DB lookup. Each item may include:' || CHR(10)
|
||||
|| ' * gameKey: Canonical game identifier.' || CHR(10)
|
||||
|| ' * gamePrefix: Prefix used for game-scoped objects.' || CHR(10)
|
||||
|| ' * userMasterObjectName: Resolved user-master object for that game.' || CHR(10) || CHR(10)
|
||||
|| 'Object-selection guidance:' || CHR(10) || CHR(10)
|
||||
|| '* Use only the targets listed in gameTargets.' || CHR(10)
|
||||
|| '* For NONE, use a game-neutral common object when it directly answers the question.' || CHR(10)
|
||||
|| '* If no suitable common object exists, state that a game name is required.' || CHR(10)
|
||||
|| '* Do not infer an unlisted game or game-scoped object.';
|
||||
|
||||
v_result.put('contractVersion', '2.0');
|
||||
v_result.put('targetType', v_target_type);
|
||||
v_result.put('scopeType', v_target_type);
|
||||
v_result.put('selectAiReference', v_select_ai_reference);
|
||||
v_result.put('extractScopeHint', v_scope_type);
|
||||
v_result.put('status', v_plan_status);
|
||||
-- `gameTargets` is the public downstream contract. The detailed `targets`
|
||||
-- collection remains diagnostic evidence only for the MCP response.
|
||||
v_result.put('gameTargets', v_supported);
|
||||
v_result.put('targets', v_targets);
|
||||
v_result.put('matchedGames', v_supported);
|
||||
v_result.put('mentionResults', v_mention_results);
|
||||
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);
|
||||
-- Dynamic ReAct work items. Every game identity and availability state comes
|
||||
-- from the catalog lookup above; clients must not infer their own targets.
|
||||
FOR i IN 0 .. v_targets.get_size - 1 LOOP
|
||||
v_target := TREAT(v_targets.get(i) AS JSON_OBJECT_T);
|
||||
IF v_target.get_string('gameKey') IS NOT NULL THEN
|
||||
v_execution_task := JSON_OBJECT_T();
|
||||
v_execution_task.put(
|
||||
'action',
|
||||
CASE
|
||||
WHEN v_target.get_string('dataStatus') = 'AVAILABLE'
|
||||
AND v_target.get_string('userMasterObjectName') IS NOT NULL
|
||||
THEN 'QUERY'
|
||||
ELSE 'REPORT_UNAVAILABLE'
|
||||
END
|
||||
);
|
||||
v_execution_task.put('scopeGameKey', v_target.get_string('gameKey'));
|
||||
v_execution_task.put('target', v_target);
|
||||
IF v_execution_task.get_string('action') = 'QUERY' THEN
|
||||
v_excluded_mentions := JSON_ARRAY_T();
|
||||
FOR j IN 0 .. v_targets.get_size - 1 LOOP
|
||||
v_candidate := TREAT(v_targets.get(j) AS JSON_OBJECT_T);
|
||||
IF v_candidate.get_string('gameKey') <> v_target.get_string('gameKey')
|
||||
AND v_candidate.get_string('mention') IS NOT NULL THEN
|
||||
v_excluded_mentions.append(v_candidate.get_string('mention'));
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- Preserve the original question verbatim. The target-specific
|
||||
-- queryPlan below is the separate, authoritative scope contract.
|
||||
v_task_question := p_question;
|
||||
v_task_reference := JSON_OBJECT_T();
|
||||
v_task_targets := JSON_ARRAY_T();
|
||||
v_task_targets.append(v_target);
|
||||
v_task_reference.put('targetType', 'SINGLE');
|
||||
v_task_reference.put('status', 'SUPPORTED');
|
||||
v_task_reference.put('gameTargets', v_task_targets);
|
||||
|
||||
v_task_plan := JSON_OBJECT_T();
|
||||
v_task_plan.put('contractVersion', '2.0');
|
||||
v_task_plan.put('targetType', 'SINGLE');
|
||||
v_task_plan.put('status', 'SUPPORTED');
|
||||
v_task_plan.put('gameTargets', v_task_targets);
|
||||
v_task_plan.put('selectAiReference',
|
||||
'[GAME QUERY REFERENCE]' || CHR(10) || CHR(10) || v_task_reference.to_clob());
|
||||
|
||||
v_worker_arguments := JSON_OBJECT_T();
|
||||
v_worker_arguments.put('prompt', v_task_question);
|
||||
v_worker_arguments.put('scopeGameKey', v_target.get_string('gameKey'));
|
||||
v_worker_arguments.put('queryPlan', v_task_plan);
|
||||
v_fewshot_arguments := JSON_OBJECT_T();
|
||||
v_fewshot_arguments.put('question', v_task_question);
|
||||
v_fewshot_arguments.put('topK', 3);
|
||||
v_execution_task.put('workerTool', 'oracle.select_ai.smilegate_fewshot_nl2sql');
|
||||
v_execution_task.put('workerArguments', v_worker_arguments);
|
||||
v_execution_task.put('fewShotArguments', v_fewshot_arguments);
|
||||
END IF;
|
||||
v_execution_tasks.append(v_execution_task);
|
||||
END IF;
|
||||
END LOOP;
|
||||
v_result.put('executionTasks', v_execution_tasks);
|
||||
v_result.put('nextAction', 'CALL_FEWSHOT');
|
||||
|
||||
CASE v_target_type
|
||||
WHEN 'NONE' THEN v_result.put('executionMode', 'UNSCOPED');
|
||||
WHEN 'SINGLE' THEN v_result.put('executionMode', 'SINGLE');
|
||||
WHEN 'MULTI' THEN v_result.put('executionMode', 'COMBINED');
|
||||
WHEN 'ALL' THEN v_result.put('executionMode', 'ALL');
|
||||
END CASE;
|
||||
|
||||
RETURN v_result.to_clob;
|
||||
END;
|
||||
/
|
||||
219
database/adb/82_sgmp_qa_fewshot_reference_governance.sql
Normal file
219
database/adb/82_sgmp_qa_fewshot_reference_governance.sql
Normal file
@@ -0,0 +1,219 @@
|
||||
-- Governed Few-shot examples for the Smilegate Select AI MCP.
|
||||
-- Existing rows are preserved for audit and only APPROVED rows are retrievable.
|
||||
|
||||
DECLARE
|
||||
PROCEDURE add_column(p_definition IN VARCHAR2) IS
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'ALTER TABLE sg_qa_vector_example ADD (' || p_definition || ')';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -1430 THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
BEGIN
|
||||
add_column('reference_status VARCHAR2(16) DEFAULT ''DRAFT'' NOT NULL');
|
||||
add_column('reference_kind VARCHAR2(32) DEFAULT ''SQL_TEMPLATE'' NOT NULL');
|
||||
add_column('target_type VARCHAR2(16) DEFAULT ''ANY'' NOT NULL');
|
||||
add_column('object_role VARCHAR2(64)');
|
||||
add_column('inspection_status VARCHAR2(16) DEFAULT ''PENDING'' NOT NULL');
|
||||
add_column('inspection_note CLOB');
|
||||
add_column('verified_at TIMESTAMP(6)');
|
||||
add_column('verified_by VARCHAR2(128)');
|
||||
END;
|
||||
/
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'RETIRED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
inspection_status = 'RETIRED',
|
||||
inspection_note = 'Executed successfully but maps BUBBLYZ to the CZN physical user-master object. Conflicts with the approved game-target contract.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_REVIEW'
|
||||
WHERE example_id = 2;
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'RETIRED',
|
||||
reference_kind = 'OBJECT_UNAVAILABLE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'GAME_ALIAS_CATALOG',
|
||||
inspection_status = 'RETIRED',
|
||||
inspection_note = 'Executed successfully with no BUBBLYZ alias rows, but the game literal is not a reusable object-unavailable template. The current game query plan is the authoritative boundary source.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_REVIEW'
|
||||
WHERE example_id = 3;
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'SINGLE',
|
||||
object_role = 'GAME_USER_MASTER',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Logical placeholder template. The resolved physical object must come only from the current game query plan.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_REVIEW'
|
||||
WHERE example_id = 4;
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
reference_kind = 'NO_TARGET',
|
||||
target_type = 'NONE',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Executed successfully with no rows. Canonical boundary for an unscoped game-user-master request.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_REVIEW'
|
||||
WHERE example_id = 5;
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'RETIRED',
|
||||
reference_kind = 'METADATA_POLICY',
|
||||
target_type = 'ANY',
|
||||
object_role = 'GAME_ALIAS_CATALOG',
|
||||
inspection_status = 'RETIRED',
|
||||
inspection_note = 'Not directly executable: requires an unbound GAME_TERM placeholder. Game resolution is now supplied by game_query_plan.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_REVIEW'
|
||||
WHERE example_id = 6;
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'RETIRED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = 'NONE',
|
||||
object_role = 'GAME_USER_MASTER',
|
||||
inspection_status = 'RETIRED',
|
||||
inspection_note = 'Executed successfully but chooses CZN_COMN_USER_MST for a game-unscoped question. Conflicts with the NONE target contract.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_REVIEW'
|
||||
WHERE example_id IN (7, 8);
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_store(
|
||||
p_question IN CLOB,
|
||||
p_answer_sql IN CLOB,
|
||||
p_answer IN CLOB DEFAULT NULL
|
||||
) RETURN NUMBER
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_example_id NUMBER;
|
||||
BEGIN
|
||||
IF p_question IS NULL OR p_answer_sql IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20002, 'question and answer_sql are required.');
|
||||
END IF;
|
||||
|
||||
v_input := TO_CLOB('Question: ') || p_question
|
||||
|| TO_CLOB(CHR(10) || 'Answer SQL: ') || p_answer_sql
|
||||
|| CASE WHEN p_answer IS NULL THEN NULL ELSE TO_CLOB(CHR(10) || 'Answer: ') || p_answer END;
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, inspection_status
|
||||
) VALUES (
|
||||
p_question, p_answer_sql, p_answer, v_input, v_embedding, 'cohere.embed-v4.0',
|
||||
'DRAFT', 'SQL_TEMPLATE', 'ANY', 'PENDING'
|
||||
) RETURNING example_id INTO v_example_id;
|
||||
|
||||
COMMIT;
|
||||
RETURN v_example_id;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
|
||||
END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'target_type must be NONE, SINGLE, MULTI, ALL, or ANY.');
|
||||
END IF;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question,
|
||||
JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id,
|
||||
question,
|
||||
answer_sql,
|
||||
answer_text,
|
||||
embedding_model,
|
||||
reference_kind,
|
||||
target_type,
|
||||
object_role,
|
||||
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_context(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN CLOB
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_results SYS_REFCURSOR;
|
||||
v_id NUMBER;
|
||||
v_q CLOB;
|
||||
v_sql CLOB;
|
||||
v_answer CLOB;
|
||||
v_model VARCHAR2(128);
|
||||
v_kind VARCHAR2(32);
|
||||
v_target VARCHAR2(16);
|
||||
v_role VARCHAR2(64);
|
||||
v_dist NUMBER;
|
||||
v_context CLOB := EMPTY_CLOB();
|
||||
BEGIN
|
||||
v_results := sg_qa_vector_search(p_question, p_top_k, p_target_type);
|
||||
LOOP
|
||||
FETCH v_results INTO v_id, v_q, v_sql, v_answer, v_model, v_kind, v_target, v_role, v_dist;
|
||||
EXIT WHEN v_results%NOTFOUND;
|
||||
v_context := v_context
|
||||
|| CASE WHEN DBMS_LOB.GETLENGTH(v_context) = 0 THEN NULL ELSE CHR(10) || CHR(10) END
|
||||
|| '[Example ' || v_id || ', kind=' || v_kind || ', target_type=' || v_target
|
||||
|| ', cosine_distance=' || TO_CHAR(v_dist, 'FM0D000000') || ']' || CHR(10)
|
||||
|| 'Question: ' || v_q || CHR(10)
|
||||
|| 'Answer SQL: ' || v_sql
|
||||
|| CASE WHEN v_answer IS NULL THEN NULL ELSE CHR(10) || 'Answer: ' || v_answer END;
|
||||
END LOOP;
|
||||
CLOSE v_results;
|
||||
RETURN v_context;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMENT ON COLUMN sg_qa_vector_example.reference_status IS
|
||||
'Few-shot retrieval lifecycle: DRAFT, APPROVED, or RETIRED. Only APPROVED is retrievable.';
|
||||
COMMENT ON COLUMN sg_qa_vector_example.reference_kind IS
|
||||
'Few-shot semantic kind: SQL_TEMPLATE, NO_TARGET, OBJECT_UNAVAILABLE, or METADATA_POLICY.';
|
||||
COMMENT ON COLUMN sg_qa_vector_example.target_type IS
|
||||
'Applicable game query-plan target type: NONE, SINGLE, MULTI, ALL, or ANY.';
|
||||
COMMENT ON COLUMN sg_qa_vector_example.object_role IS
|
||||
'Logical object role; physical object names must be sourced from the current game query plan.';
|
||||
@@ -1,72 +0,0 @@
|
||||
-- HMM carrier report template storage and DBMS_CLOUD_AI_AGENT custom tool.
|
||||
-- Run as ADMIN. Load the approved HTML template into HMM_REPORT_TEMPLATES
|
||||
-- through the deployment loader before enabling the MCP tool.
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
|
||||
SET DEFINE OFF
|
||||
|
||||
CREATE TABLE hmm_report_templates (
|
||||
template_key VARCHAR2(64) PRIMARY KEY,
|
||||
template_version VARCHAR2(32) NOT NULL,
|
||||
html_template CLOB NOT NULL,
|
||||
active_yn CHAR(1) DEFAULT 'Y' NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT hmm_report_templates_active_ck CHECK (active_yn IN ('Y', 'N'))
|
||||
);
|
||||
|
||||
CREATE OR REPLACE PACKAGE hmm_report_render_pkg AUTHID DEFINER AS
|
||||
FUNCTION render_carrier_report(p_report_json IN CLOB) RETURN CLOB;
|
||||
END hmm_report_render_pkg;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE PACKAGE BODY hmm_report_render_pkg AS
|
||||
FUNCTION render_carrier_report(p_report_json IN CLOB) RETURN CLOB IS
|
||||
l_template CLOB;
|
||||
l_data CLOB;
|
||||
l_html VARCHAR2(32767);
|
||||
l_result CLOB;
|
||||
BEGIN
|
||||
IF p_report_json IS NULL OR dbms_lob.getlength(p_report_json) > 64000 THEN
|
||||
raise_application_error(-20101, 'Invalid report payload size.');
|
||||
END IF;
|
||||
IF NOT json_exists(p_report_json, '$.report') OR NOT json_exists(p_report_json, '$.rows') THEN
|
||||
raise_application_error(-20102, 'Report payload requires report and rows.');
|
||||
END IF;
|
||||
SELECT html_template INTO l_template
|
||||
FROM hmm_report_templates
|
||||
WHERE template_key = 'hmm-carrier-performance'
|
||||
AND active_yn = 'Y';
|
||||
l_data := replace(p_report_json, '</', '<\/');
|
||||
l_html := dbms_lob.substr(replace(l_template, '__REPORT_DATA__', l_data), 32767, 1);
|
||||
SELECT json_object(
|
||||
'status' VALUE 'ok',
|
||||
'template' VALUE 'hmm-carrier-performance',
|
||||
'html' VALUE l_html
|
||||
RETURNING CLOB
|
||||
) INTO l_result FROM dual;
|
||||
RETURN l_result;
|
||||
EXCEPTION
|
||||
WHEN no_data_found THEN
|
||||
raise_application_error(-20103, 'Active report template is not installed.');
|
||||
END render_carrier_report;
|
||||
END hmm_report_render_pkg;
|
||||
/
|
||||
|
||||
BEGIN
|
||||
DBMS_CLOUD_AI_AGENT.DROP_TOOL('HMM_CARRIER_REPORT_RENDERER', force => TRUE);
|
||||
DBMS_CLOUD_AI_AGENT.CREATE_TOOL(
|
||||
tool_name => 'HMM_CARRIER_REPORT_RENDERER',
|
||||
attributes => q'~{
|
||||
"instruction": "Render the supplied carrier-performance payload with the approved HMM HTML template. Do not query data and do not alter the supplied values.",
|
||||
"function": "HMM_REPORT_RENDER_PKG.RENDER_CARRIER_REPORT",
|
||||
"tool_inputs": [{"name":"P_REPORT_JSON","description":"Normalized carrier performance report JSON."}]
|
||||
}~',
|
||||
status => 'ENABLED',
|
||||
description => 'Renders approved HMM carrier-performance HTML from already-authorized query results.'
|
||||
);
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT tool_name, status
|
||||
FROM user_ai_agent_tools
|
||||
WHERE tool_name = 'HMM_CARRIER_REPORT_RENDERER';
|
||||
304
database/adb/83_sgmp_qa_benchmark_fewshot_candidates.sql
Normal file
304
database/adb/83_sgmp_qa_benchmark_fewshot_candidates.sql
Normal file
@@ -0,0 +1,304 @@
|
||||
-- Import the 47 customer QA benchmark rows as governed Few-shot candidates.
|
||||
-- This migration deliberately creates DRAFT records only. Retrieval continues
|
||||
-- to use APPROVED rows only (see 82_sgmp_qa_fewshot_reference_governance.sql).
|
||||
|
||||
DECLARE
|
||||
PROCEDURE add_column(p_definition IN VARCHAR2) IS
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'ALTER TABLE sg_qa_vector_example ADD (' || p_definition || ')';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -1430 THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
BEGIN
|
||||
add_column('source_case_id VARCHAR2(30)');
|
||||
add_column('source_type VARCHAR2(30)');
|
||||
END;
|
||||
/
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE
|
||||
'CREATE UNIQUE INDEX sg_qa_vector_example_source_uk '
|
||||
|| 'ON sg_qa_vector_example (source_type, source_case_id)';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -955 THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_seed_benchmark
|
||||
RETURN NUMBER
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_answer_sql CLOB;
|
||||
v_answer_text CLOB;
|
||||
v_exists NUMBER;
|
||||
v_inserted NUMBER := 0;
|
||||
BEGIN
|
||||
FOR item IN (
|
||||
SELECT question_code,
|
||||
question_text,
|
||||
baseline_sql,
|
||||
baseline_answer,
|
||||
expected_focus,
|
||||
support_level
|
||||
FROM sg_ai_qa_question
|
||||
WHERE question_source = 'CUSTOMER_EXCEL'
|
||||
AND active_yn = 'Y'
|
||||
ORDER BY question_code
|
||||
) LOOP
|
||||
SELECT CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM sg_qa_vector_example existing
|
||||
WHERE existing.source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND existing.source_case_id = item.question_code
|
||||
) THEN 1 ELSE 0
|
||||
END
|
||||
INTO v_exists
|
||||
FROM dual;
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
v_answer_sql := CASE
|
||||
WHEN item.baseline_sql IS NULL OR DBMS_LOB.GETLENGTH(TRIM(item.baseline_sql)) = 0
|
||||
THEN TO_CLOB('SELECT CAST(NULL AS NUMBER) AS "NO_BASELINE" FROM DUAL WHERE 1 = 0')
|
||||
ELSE item.baseline_sql
|
||||
END;
|
||||
v_answer_text := TO_CLOB('Expected focus: ') || item.expected_focus
|
||||
|| CASE WHEN item.baseline_answer IS NULL THEN NULL
|
||||
ELSE TO_CLOB(CHR(10) || 'Historical answer: ') || item.baseline_answer END;
|
||||
v_input := TO_CLOB('Customer QA case: ') || item.question_code
|
||||
|| TO_CLOB(CHR(10) || 'Question: ') || item.question_text
|
||||
|| TO_CLOB(CHR(10) || 'Expected focus: ') || item.expected_focus
|
||||
|| TO_CLOB(CHR(10) || 'Candidate SQL: ') || v_answer_sql;
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
BEGIN
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, inspection_status,
|
||||
inspection_note, source_case_id, source_type
|
||||
) VALUES (
|
||||
item.question_text, v_answer_sql, v_answer_text, v_input, v_embedding, 'cohere.embed-v4.0',
|
||||
'DRAFT',
|
||||
CASE WHEN item.support_level = 'UNSUPPORTED' THEN 'OBJECT_UNAVAILABLE'
|
||||
ELSE 'SQL_TEMPLATE' END,
|
||||
'ANY',
|
||||
'PENDING',
|
||||
'Imported from the customer benchmark. A candidate cannot be retrieved until scope, logical object role, and SQL safety are reviewed.',
|
||||
item.question_code,
|
||||
'CUSTOMER_QA_BENCHMARK'
|
||||
);
|
||||
v_inserted := v_inserted + 1;
|
||||
EXCEPTION
|
||||
WHEN DUP_VAL_ON_INDEX THEN
|
||||
NULL;
|
||||
END;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
COMMIT;
|
||||
RETURN v_inserted;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_approve_verified_benchmark
|
||||
RETURN NUMBER
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||||
v_target_type VARCHAR2(16);
|
||||
v_input CLOB;
|
||||
v_answer_text CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_approved NUMBER := 0;
|
||||
BEGIN
|
||||
FOR item IN (
|
||||
WITH latest_answer AS (
|
||||
SELECT answer.question_id,
|
||||
answer.generated_sql,
|
||||
answer.answer_text,
|
||||
answer.result_json,
|
||||
answer.judgment_status,
|
||||
answer.execution_status,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY answer.question_id ORDER BY answer.answer_seq DESC
|
||||
) AS row_rank
|
||||
FROM sg_ai_qa_answer answer
|
||||
)
|
||||
SELECT example.example_id,
|
||||
question.question_text,
|
||||
question.expected_focus,
|
||||
question.support_level,
|
||||
latest.generated_sql,
|
||||
latest.answer_text AS live_answer,
|
||||
latest.result_json,
|
||||
latest.judgment_status,
|
||||
latest.execution_status
|
||||
FROM sg_qa_vector_example example
|
||||
JOIN sg_ai_qa_question question
|
||||
ON question.question_code = example.source_case_id
|
||||
LEFT JOIN latest_answer latest
|
||||
ON latest.question_id = question.question_id
|
||||
AND latest.row_rank = 1
|
||||
WHERE example.source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND example.reference_status = 'DRAFT'
|
||||
) LOOP
|
||||
IF item.support_level = 'SUPPORTED'
|
||||
AND item.judgment_status = 'PASS'
|
||||
AND item.execution_status = 'COMPLETED'
|
||||
AND item.generated_sql IS NOT NULL
|
||||
AND DBMS_LOB.GETLENGTH(TRIM(item.generated_sql)) > 0 THEN
|
||||
v_target_type := REGEXP_SUBSTR(
|
||||
DBMS_LOB.SUBSTR(item.result_json, 32767, 1),
|
||||
'"targetType"[[:space:]]*:[[:space:]]*"([A-Z]+)"',
|
||||
1, 1, NULL, 1
|
||||
);
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
v_target_type := 'ANY';
|
||||
END IF;
|
||||
v_answer_text := TO_CLOB('Expected focus: ') || item.expected_focus
|
||||
|| CASE WHEN item.live_answer IS NULL THEN NULL
|
||||
ELSE TO_CLOB(CHR(10) || 'Verified live answer: ') || item.live_answer END;
|
||||
v_input := TO_CLOB('Customer QA question: ') || item.question_text
|
||||
|| TO_CLOB(CHR(10) || 'Expected focus: ') || item.expected_focus
|
||||
|| TO_CLOB(CHR(10) || 'Verified SQL template: ') || item.generated_sql;
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_sql = item.generated_sql,
|
||||
answer_text = v_answer_text,
|
||||
embedding_input = v_input,
|
||||
embedding = v_embedding,
|
||||
reference_status = 'APPROVED',
|
||||
reference_kind = 'SQL_TEMPLATE',
|
||||
target_type = v_target_type,
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Promoted only after the latest Portal ReAct run completed with PASS for this customer benchmark case.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_BENCHMARK_REVIEW'
|
||||
WHERE example_id = item.example_id;
|
||||
v_approved := v_approved + 1;
|
||||
ELSE
|
||||
UPDATE sg_qa_vector_example
|
||||
SET inspection_status = 'REVIEW',
|
||||
inspection_note = 'Not retrievable: latest customer benchmark execution is unsupported, incomplete, WARN, FAIL, or lacks executable SQL.'
|
||||
WHERE example_id = item.example_id;
|
||||
END IF;
|
||||
END LOOP;
|
||||
COMMIT;
|
||||
RETURN v_approved;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
|
||||
END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'target_type must be NONE, SINGLE, MULTI, ALL, or ANY.');
|
||||
END IF;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question,
|
||||
JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id,
|
||||
question,
|
||||
answer_sql,
|
||||
answer_text,
|
||||
embedding_model,
|
||||
reference_kind,
|
||||
target_type,
|
||||
object_role,
|
||||
source_case_id,
|
||||
source_type,
|
||||
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_context(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN CLOB
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_results SYS_REFCURSOR;
|
||||
v_id NUMBER;
|
||||
v_q CLOB;
|
||||
v_sql CLOB;
|
||||
v_answer CLOB;
|
||||
v_model VARCHAR2(128);
|
||||
v_kind VARCHAR2(32);
|
||||
v_target VARCHAR2(16);
|
||||
v_role VARCHAR2(64);
|
||||
v_case_id VARCHAR2(30);
|
||||
v_source VARCHAR2(30);
|
||||
v_dist NUMBER;
|
||||
v_context CLOB := EMPTY_CLOB();
|
||||
BEGIN
|
||||
v_results := sg_qa_vector_search(p_question, p_top_k, p_target_type);
|
||||
LOOP
|
||||
FETCH v_results INTO v_id, v_q, v_sql, v_answer, v_model, v_kind, v_target, v_role,
|
||||
v_case_id, v_source, v_dist;
|
||||
EXIT WHEN v_results%NOTFOUND;
|
||||
v_context := v_context
|
||||
|| CASE WHEN DBMS_LOB.GETLENGTH(v_context) = 0 THEN NULL ELSE CHR(10) || CHR(10) END
|
||||
|| '[Example ' || v_id || ', kind=' || v_kind || ', target_type=' || v_target
|
||||
|| CASE WHEN v_case_id IS NULL THEN NULL ELSE ', source_case_id=' || v_case_id END
|
||||
|| ', cosine_distance=' || TO_CHAR(v_dist, 'FM0D000000') || ']' || CHR(10)
|
||||
|| 'Question: ' || v_q || CHR(10)
|
||||
|| 'Answer SQL: ' || v_sql
|
||||
|| CASE WHEN v_answer IS NULL THEN NULL ELSE CHR(10) || 'Answer: ' || v_answer END;
|
||||
END LOOP;
|
||||
CLOSE v_results;
|
||||
RETURN v_context;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMENT ON COLUMN sg_qa_vector_example.source_case_id IS
|
||||
'Customer QA benchmark case identifier, such as STD-01 or CZN-01.';
|
||||
COMMENT ON COLUMN sg_qa_vector_example.source_type IS
|
||||
'Candidate provenance. CUSTOMER_QA_BENCHMARK rows remain DRAFT until reviewed.';
|
||||
@@ -1,323 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 84_hmm_carrier_team_vpd.sql
|
||||
--
|
||||
-- Row security for HMM_CARRIER_ASSIGNMENTS_V:
|
||||
-- HMM_HR_VIEWER -> current employee only
|
||||
-- HMM_HR_MANAGER -> current employee and direct reports
|
||||
-- HMM_HR_ADMIN -> all assignments
|
||||
--
|
||||
-- Run as ADMIN after 72_hmm_leave_team_vpd.sql and
|
||||
-- 81_hmm_carrier_access_groups.sql. Plain bearer tokens are
|
||||
-- never stored by this script.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET DEFINE OFF
|
||||
|
||||
PROMPT === 1. Registering the protected carrier object ===
|
||||
DECLARE
|
||||
v_object_id NUMBER;
|
||||
BEGIN
|
||||
BEGIN
|
||||
SELECT object_id
|
||||
INTO v_object_id
|
||||
FROM hmm_access_objects
|
||||
WHERE owner = 'ADMIN'
|
||||
AND object_name = 'HMM_CARRIER_ASSIGNMENTS_V';
|
||||
|
||||
UPDATE hmm_access_objects
|
||||
SET enabled_yn = 'Y',
|
||||
description = 'Employee-to-carrier assignments protected by employee hierarchy',
|
||||
ords_path = 'cb-ords/cb-object-query/admin/hmm_carrier_assignments_v'
|
||||
WHERE object_id = v_object_id;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
SELECT NVL(MAX(object_id), 0) + 1
|
||||
INTO v_object_id
|
||||
FROM hmm_access_objects;
|
||||
|
||||
INSERT INTO hmm_access_objects (
|
||||
object_id, owner, object_name, ords_path, enabled_yn, description
|
||||
) VALUES (
|
||||
v_object_id,
|
||||
'ADMIN',
|
||||
'HMM_CARRIER_ASSIGNMENTS_V',
|
||||
'cb-ords/cb-object-query/admin/hmm_carrier_assignments_v',
|
||||
'Y',
|
||||
'Employee-to-carrier assignments protected by employee hierarchy'
|
||||
);
|
||||
END;
|
||||
|
||||
FOR c IN (
|
||||
SELECT column_name
|
||||
FROM user_tab_columns
|
||||
WHERE table_name = 'HMM_CARRIER_ASSIGNMENTS_V'
|
||||
ORDER BY column_id
|
||||
) LOOP
|
||||
MERGE INTO hmm_access_object_columns dst
|
||||
USING (
|
||||
SELECT v_object_id object_id, c.column_name column_name FROM dual
|
||||
) src
|
||||
ON (dst.object_id = src.object_id AND dst.column_name = src.column_name)
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
column_id, object_id, column_name, sensitive_yn,
|
||||
sensitivity_level, redaction_method
|
||||
) VALUES (
|
||||
(SELECT NVL(MAX(column_id), 0) + 1 FROM hmm_access_object_columns),
|
||||
src.object_id, src.column_name, 'N', 'PUBLIC', 'NONE'
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 2. Registering role rules ===
|
||||
DECLARE
|
||||
PROCEDURE ensure_permission(
|
||||
p_role_name IN VARCHAR2,
|
||||
p_rule_type IN VARCHAR2
|
||||
) AS
|
||||
v_role_id NUMBER;
|
||||
v_perm_id NUMBER;
|
||||
BEGIN
|
||||
SELECT role_id
|
||||
INTO v_role_id
|
||||
FROM hmm_access_roles
|
||||
WHERE role_name = p_role_name
|
||||
AND active_yn = 'Y';
|
||||
|
||||
BEGIN
|
||||
SELECT perm_id
|
||||
INTO v_perm_id
|
||||
FROM hmm_access_permissions
|
||||
WHERE role_id = v_role_id
|
||||
AND target_name = 'HMM_CARRIER_ASSIGNMENTS_V'
|
||||
AND action_name = 'SELECT';
|
||||
|
||||
UPDATE hmm_access_permissions
|
||||
SET permission_effect = 'ALLOW'
|
||||
WHERE perm_id = v_perm_id;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
SELECT cb_permission_seq.NEXTVAL INTO v_perm_id FROM dual;
|
||||
INSERT INTO hmm_access_permissions (
|
||||
perm_id, role_id, target_name, action_name, permission_effect
|
||||
) VALUES (
|
||||
v_perm_id, v_role_id, 'HMM_CARRIER_ASSIGNMENTS_V', 'SELECT', 'ALLOW'
|
||||
);
|
||||
END;
|
||||
|
||||
DELETE FROM hmm_access_permission_rules WHERE perm_id = v_perm_id;
|
||||
INSERT INTO hmm_access_permission_rules (
|
||||
rule_id, perm_id, rule_column, rule_type, rule_value
|
||||
) VALUES (
|
||||
cb_permission_rule_seq.NEXTVAL,
|
||||
v_perm_id,
|
||||
'EMPLOYEE_ID',
|
||||
p_rule_type,
|
||||
NULL
|
||||
);
|
||||
END;
|
||||
BEGIN
|
||||
ensure_permission('HMM_HR_VIEWER', 'SELF');
|
||||
ensure_permission('HMM_HR_MANAGER', 'MANAGED_TEAM');
|
||||
ensure_permission('HMM_HR_ADMIN', 'ALL');
|
||||
END;
|
||||
/
|
||||
|
||||
COMMIT;
|
||||
|
||||
PROMPT === 3. Creating the carrier VPD predicate ===
|
||||
CREATE OR REPLACE FUNCTION hmm_carrier_vpd_filter(
|
||||
p_schema IN VARCHAR2,
|
||||
p_object IN VARCHAR2
|
||||
) RETURN VARCHAR2
|
||||
AUTHID DEFINER
|
||||
AS
|
||||
v_employee_id NUMBER;
|
||||
v_all_rule NUMBER := 0;
|
||||
v_team_rule NUMBER := 0;
|
||||
v_self_rule NUMBER := 0;
|
||||
BEGIN
|
||||
IF UPPER(TRIM(p_schema)) != 'ADMIN'
|
||||
OR UPPER(TRIM(p_object)) != 'HMM_CARRIER_ASSIGNMENTS_V' THEN
|
||||
RETURN '1 = 0';
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
v_employee_id := TO_NUMBER(
|
||||
SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN '1 = 0';
|
||||
END;
|
||||
|
||||
IF v_employee_id IS NULL THEN
|
||||
RETURN '1 = 0';
|
||||
END IF;
|
||||
|
||||
SELECT NVL(MAX(CASE WHEN rule.rule_type = 'ALL' THEN 1 ELSE 0 END), 0),
|
||||
NVL(MAX(CASE WHEN rule.rule_type = 'MANAGED_TEAM' THEN 1 ELSE 0 END), 0),
|
||||
NVL(MAX(CASE WHEN rule.rule_type = 'SELF' THEN 1 ELSE 0 END), 0)
|
||||
INTO v_all_rule, v_team_rule, v_self_rule
|
||||
FROM (
|
||||
SELECT employee_role.role_id
|
||||
FROM hmm_employee_access_roles employee_role
|
||||
WHERE employee_role.employee_id = v_employee_id
|
||||
UNION
|
||||
SELECT group_role.role_id
|
||||
FROM hmm_access_group_members group_member
|
||||
JOIN hmm_access_groups access_group
|
||||
ON access_group.group_id = group_member.group_id
|
||||
AND access_group.active_yn = 'Y'
|
||||
JOIN hmm_group_access_roles group_role
|
||||
ON group_role.group_id = group_member.group_id
|
||||
WHERE group_member.employee_id = v_employee_id
|
||||
) effective_role
|
||||
JOIN hmm_access_roles role
|
||||
ON role.role_id = effective_role.role_id
|
||||
AND role.active_yn = 'Y'
|
||||
JOIN hmm_access_permissions permission
|
||||
ON permission.role_id = role.role_id
|
||||
AND permission.target_name = 'HMM_CARRIER_ASSIGNMENTS_V'
|
||||
AND permission.action_name = 'SELECT'
|
||||
AND permission.permission_effect = 'ALLOW'
|
||||
JOIN hmm_access_permission_rules rule
|
||||
ON rule.perm_id = permission.perm_id
|
||||
AND UPPER(TRIM(rule.rule_column)) = 'EMPLOYEE_ID';
|
||||
|
||||
IF v_all_rule = 1 THEN
|
||||
RETURN '1 = 1';
|
||||
END IF;
|
||||
|
||||
IF v_team_rule = 1 THEN
|
||||
RETURN 'EMPLOYEE_ID IN ('
|
||||
|| 'SELECT employee.employee_id '
|
||||
|| 'FROM ADMIN.HMM_HR_EMPLOYEES employee '
|
||||
|| 'WHERE employee.employee_id = '
|
||||
|| 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID'')) '
|
||||
|| 'OR employee.manager_employee_id = '
|
||||
|| 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))'
|
||||
|| ')';
|
||||
END IF;
|
||||
|
||||
IF v_self_rule = 1 THEN
|
||||
RETURN 'EMPLOYEE_ID = TO_NUMBER('
|
||||
|| 'SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))';
|
||||
END IF;
|
||||
|
||||
RETURN '1 = 0';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN '1 = 0';
|
||||
END;
|
||||
/
|
||||
|
||||
SHOW ERRORS FUNCTION hmm_carrier_vpd_filter
|
||||
|
||||
PROMPT === 4. Attaching the VPD policy ===
|
||||
BEGIN
|
||||
BEGIN
|
||||
DBMS_RLS.DROP_POLICY(
|
||||
object_schema => 'ADMIN',
|
||||
object_name => 'HMM_CARRIER_ASSIGNMENTS_V',
|
||||
policy_name => 'HMM_CARRIER_SCOPE_POLICY'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -28102 THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
|
||||
DBMS_RLS.ADD_POLICY(
|
||||
object_schema => 'ADMIN',
|
||||
object_name => 'HMM_CARRIER_ASSIGNMENTS_V',
|
||||
policy_name => 'HMM_CARRIER_SCOPE_POLICY',
|
||||
function_schema => 'ADMIN',
|
||||
policy_function => 'HMM_CARRIER_VPD_FILTER',
|
||||
statement_types => 'SELECT',
|
||||
update_check => FALSE,
|
||||
enable => TRUE,
|
||||
policy_type => DBMS_RLS.CONTEXT_SENSITIVE
|
||||
);
|
||||
END;
|
||||
/
|
||||
|
||||
PROMPT === 5. Granting the non-exempt runtime read boundary ===
|
||||
GRANT SELECT ON hmm_carrier_assignments_v TO cb_ords;
|
||||
GRANT SELECT ON hmm_rds_carriers_v TO cb_ords;
|
||||
GRANT SELECT ON hmm_rds_carrier_perf_v TO cb_ords;
|
||||
GRANT SELECT ON hmm_rds_carrier_latest_v TO cb_ords;
|
||||
|
||||
CREATE OR REPLACE SYNONYM cb_ords.hmm_carrier_assignments_v
|
||||
FOR admin.hmm_carrier_assignments_v;
|
||||
CREATE OR REPLACE SYNONYM cb_ords.hmm_rds_carriers_v
|
||||
FOR admin.hmm_rds_carriers_v;
|
||||
CREATE OR REPLACE SYNONYM cb_ords.hmm_rds_carrier_perf_v
|
||||
FOR admin.hmm_rds_carrier_perf_v;
|
||||
CREATE OR REPLACE SYNONYM cb_ords.hmm_rds_carrier_latest_v
|
||||
FOR admin.hmm_rds_carrier_latest_v;
|
||||
|
||||
PROMPT === 6. Recording policy notes ===
|
||||
MERGE INTO hmm_access_vpd_filter_notes dst
|
||||
USING (
|
||||
SELECT 'ADMIN' function_owner,
|
||||
'HMM_CARRIER_VPD_FILTER' function_name,
|
||||
'Filters carrier assignments by the authenticated employee role and employee hierarchy.' description
|
||||
FROM dual
|
||||
) src
|
||||
ON (dst.function_owner = src.function_owner AND dst.function_name = src.function_name)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
dst.description = src.description,
|
||||
dst.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
function_owner, function_name, description, updated_at
|
||||
) VALUES (
|
||||
src.function_owner, src.function_name, src.description, SYSTIMESTAMP
|
||||
);
|
||||
|
||||
MERGE INTO hmm_access_vpd_policy_notes dst
|
||||
USING (
|
||||
SELECT 'ADMIN' object_owner,
|
||||
'HMM_CARRIER_ASSIGNMENTS_V' object_name,
|
||||
'HMM_CARRIER_SCOPE_POLICY' policy_name,
|
||||
'Restricts carrier assignments to self, direct reports, or explicit HR administrator access.' description
|
||||
FROM dual
|
||||
) src
|
||||
ON (
|
||||
dst.object_owner = src.object_owner
|
||||
AND dst.object_name = src.object_name
|
||||
AND dst.policy_name = src.policy_name
|
||||
)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
dst.description = src.description,
|
||||
dst.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
object_owner, object_name, policy_name, description, updated_at
|
||||
) VALUES (
|
||||
src.object_owner, src.object_name, src.policy_name,
|
||||
src.description, SYSTIMESTAMP
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PROMPT === 7. Verification inventory ===
|
||||
SELECT object_name, policy_name, function, sel, enable, policy_type
|
||||
FROM all_policies
|
||||
WHERE object_owner = 'ADMIN'
|
||||
AND object_name = 'HMM_CARRIER_ASSIGNMENTS_V'
|
||||
AND policy_name = 'HMM_CARRIER_SCOPE_POLICY';
|
||||
|
||||
SELECT role.role_name,
|
||||
permission.target_name,
|
||||
rule.rule_column,
|
||||
rule.rule_type
|
||||
FROM hmm_access_permissions permission
|
||||
JOIN hmm_access_roles role ON role.role_id = permission.role_id
|
||||
JOIN hmm_access_permission_rules rule ON rule.perm_id = permission.perm_id
|
||||
WHERE permission.target_name = 'HMM_CARRIER_ASSIGNMENTS_V'
|
||||
ORDER BY role.role_name;
|
||||
|
||||
PROMPT === HMM carrier team VPD ready ===
|
||||
31
database/adb/84_sgmp_none_scope_common_object_guidance.sql
Normal file
31
database/adb/84_sgmp_none_scope_common_object_guidance.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- NONE means "no selected game", not "no executable query".
|
||||
-- Keep the boundary example scoped to its logical object role so it cannot
|
||||
-- be generalized to approved common-object questions.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET object_role = 'GAME_USER_MASTER',
|
||||
answer_text = 'This boundary applies only to a game-scoped user-master operation. '
|
||||
|| 'When no game identifier is resolved, do not select a prefix-specific user-master object. '
|
||||
|| 'This does not prohibit an approved common-object query.',
|
||||
inspection_note = 'Canonical boundary for an unscoped game-user-master request. '
|
||||
|| 'It applies only to GAME_USER_MASTER and must not suppress common-object queries.'
|
||||
WHERE example_id = 5
|
||||
AND reference_kind = 'NO_TARGET';
|
||||
|
||||
-- Populate logical roles from verified SQL. This is prompt metadata only;
|
||||
-- runtime physical-object selection remains governed by the query plan.
|
||||
UPDATE sg_qa_vector_example
|
||||
SET object_role = CASE
|
||||
WHEN REGEXP_LIKE(answer_sql, 'COMN_SALES_TXN', 'i') THEN 'SALES_TRANSACTION'
|
||||
WHEN REGEXP_LIKE(answer_sql, 'COMN_REFUND_TXN', 'i') THEN 'REFUND_TRANSACTION'
|
||||
WHEN REGEXP_LIKE(answer_sql, 'COMN_CHARACTER_MST', 'i') THEN 'GAME_CHARACTER_MASTER'
|
||||
WHEN REGEXP_LIKE(answer_sql, 'COMN_USER_MST', 'i') THEN 'GAME_USER_MASTER'
|
||||
ELSE object_role
|
||||
END
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND object_role IS NULL;
|
||||
|
||||
COMMENT ON COLUMN sg_qa_vector_example.object_role IS
|
||||
'Logical business object role used to bound Few-shot interpretation. It is not a runtime physical-object selector.';
|
||||
|
||||
COMMIT;
|
||||
@@ -1,159 +0,0 @@
|
||||
-- Redmine #749
|
||||
-- Create an isolated HMM HR Select AI profile with annotations disabled.
|
||||
-- The source profile is never modified.
|
||||
|
||||
SET SERVEROUTPUT ON SIZE UNLIMITED
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
|
||||
DECLARE
|
||||
c_source_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_PROFILE';
|
||||
c_target_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_NOANN_PROFILE';
|
||||
|
||||
l_attributes CLOB;
|
||||
l_object_list CLOB;
|
||||
l_provider VARCHAR2(4000);
|
||||
l_credential_name VARCHAR2(4000);
|
||||
l_model VARCHAR2(4000);
|
||||
l_region VARCHAR2(4000);
|
||||
l_compartment_id VARCHAR2(4000);
|
||||
l_max_tokens NUMBER;
|
||||
l_temperature NUMBER;
|
||||
l_profile_count PLS_INTEGER;
|
||||
l_difference_count PLS_INTEGER;
|
||||
l_annotations VARCHAR2(30);
|
||||
|
||||
FUNCTION attribute_clob(p_name IN VARCHAR2) RETURN CLOB IS
|
||||
l_value CLOB;
|
||||
BEGIN
|
||||
SELECT attribute_value
|
||||
INTO l_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = c_source_profile
|
||||
AND attribute_name = p_name;
|
||||
RETURN l_value;
|
||||
END attribute_clob;
|
||||
|
||||
FUNCTION attribute_text(p_name IN VARCHAR2) RETURN VARCHAR2 IS
|
||||
BEGIN
|
||||
RETURN DBMS_LOB.SUBSTR(attribute_clob(p_name), 4000, 1);
|
||||
END attribute_text;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO l_profile_count
|
||||
FROM user_cloud_ai_profiles
|
||||
WHERE profile_name = c_source_profile
|
||||
AND status = 'ENABLED';
|
||||
|
||||
IF l_profile_count <> 1 THEN
|
||||
RAISE_APPLICATION_ERROR(-20080, 'Enabled source Select AI profile was not found.');
|
||||
END IF;
|
||||
|
||||
l_provider := attribute_text('provider');
|
||||
l_credential_name := attribute_text('credential_name');
|
||||
l_model := attribute_text('model');
|
||||
l_region := attribute_text('region');
|
||||
l_compartment_id := attribute_text('oci_compartment_id');
|
||||
l_object_list := attribute_clob('object_list');
|
||||
l_max_tokens := TO_NUMBER(
|
||||
attribute_text('max_tokens'),
|
||||
'9999999999',
|
||||
'NLS_NUMERIC_CHARACTERS=''.,'''
|
||||
);
|
||||
l_temperature := TO_NUMBER(
|
||||
attribute_text('temperature'),
|
||||
'9999999990D999999999',
|
||||
'NLS_NUMERIC_CHARACTERS=''.,'''
|
||||
);
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO l_profile_count
|
||||
FROM user_cloud_ai_profiles
|
||||
WHERE profile_name = c_target_profile;
|
||||
|
||||
IF l_profile_count = 0 THEN
|
||||
SELECT JSON_OBJECT(
|
||||
'provider' VALUE l_provider,
|
||||
'credential_name' VALUE l_credential_name,
|
||||
'model' VALUE l_model,
|
||||
'region' VALUE l_region,
|
||||
'oci_compartment_id' VALUE l_compartment_id,
|
||||
'object_list' VALUE l_object_list FORMAT JSON,
|
||||
'max_tokens' VALUE l_max_tokens,
|
||||
'temperature' VALUE l_temperature,
|
||||
'annotations' VALUE 'false' FORMAT JSON
|
||||
RETURNING CLOB
|
||||
)
|
||||
INTO l_attributes
|
||||
FROM dual;
|
||||
|
||||
DBMS_CLOUD_AI.CREATE_PROFILE(
|
||||
profile_name => c_target_profile,
|
||||
attributes => l_attributes,
|
||||
status => 'enabled',
|
||||
description => 'HMM HR GPT-5.4 mini comparison profile with annotations disabled'
|
||||
);
|
||||
DBMS_OUTPUT.PUT_LINE('PROFILE_CREATED|' || c_target_profile);
|
||||
ELSE
|
||||
DBMS_OUTPUT.PUT_LINE('PROFILE_EXISTS|' || c_target_profile);
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO l_difference_count
|
||||
FROM (
|
||||
SELECT attribute_name, attribute_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = c_source_profile
|
||||
AND attribute_name <> 'annotations'
|
||||
) source_attributes
|
||||
FULL OUTER JOIN (
|
||||
SELECT attribute_name, attribute_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = c_target_profile
|
||||
AND attribute_name <> 'annotations'
|
||||
) target_attributes
|
||||
ON target_attributes.attribute_name = source_attributes.attribute_name
|
||||
WHERE source_attributes.attribute_name IS NULL
|
||||
OR target_attributes.attribute_name IS NULL
|
||||
OR DBMS_LOB.COMPARE(
|
||||
source_attributes.attribute_value,
|
||||
target_attributes.attribute_value
|
||||
) <> 0;
|
||||
|
||||
SELECT LOWER(TRIM(DBMS_LOB.SUBSTR(attribute_value, 30, 1)))
|
||||
INTO l_annotations
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = c_target_profile
|
||||
AND attribute_name = 'annotations';
|
||||
|
||||
IF l_difference_count <> 0 OR l_annotations <> 'false' THEN
|
||||
RAISE_APPLICATION_ERROR(
|
||||
-20081,
|
||||
'Comparison profile differs from source beyond annotations=false.'
|
||||
);
|
||||
END IF;
|
||||
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
'PROFILE_VALIDATED|source=' || c_source_profile
|
||||
|| '|target=' || c_target_profile
|
||||
|| '|annotations=false|other_attribute_differences=0'
|
||||
);
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT profile_name, status, description
|
||||
FROM user_cloud_ai_profiles
|
||||
WHERE profile_name IN (
|
||||
'HMM_HR_DATA_GPT54_PROFILE',
|
||||
'HMM_HR_DATA_GPT54_NOANN_PROFILE'
|
||||
)
|
||||
ORDER BY profile_name;
|
||||
|
||||
SELECT profile_name, attribute_name, attribute_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name IN (
|
||||
'HMM_HR_DATA_GPT54_PROFILE',
|
||||
'HMM_HR_DATA_GPT54_NOANN_PROFILE'
|
||||
)
|
||||
ORDER BY profile_name, attribute_name;
|
||||
|
||||
EXIT SUCCESS
|
||||
39
database/adb/85_sgmp_no_target_null_value_template.sql
Normal file
39
database/adb/85_sgmp_no_target_null_value_template.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- A no-target aggregate is a successful empty-value result, not a no-row
|
||||
-- execution failure. Keep the output alias from the verified logical metric.
|
||||
|
||||
DECLARE
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
BEGIN
|
||||
SELECT TO_CLOB('Question: ') || question
|
||||
|| TO_CLOB(CHR(10) || 'Answer SQL: SELECT CAST(NULL AS NUMBER) AS "USER_COUNT" FROM DUAL')
|
||||
|| TO_CLOB(CHR(10) || 'Answer: This boundary applies only to a game-scoped user-master operation. '
|
||||
|| 'When no game identifier is resolved, do not select a prefix-specific user-master object. '
|
||||
|| 'Return USER_COUNT as NULL. This does not prohibit an approved common-object query.')
|
||||
INTO v_input
|
||||
FROM sg_qa_vector_example
|
||||
WHERE example_id = 5;
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_sql = 'SELECT CAST(NULL AS NUMBER) AS "USER_COUNT" FROM DUAL',
|
||||
answer_text = 'This boundary applies only to a game-scoped user-master operation. '
|
||||
|| 'When no game identifier is resolved, do not select a prefix-specific user-master object. '
|
||||
|| 'Return USER_COUNT as NULL. This does not prohibit an approved common-object query.',
|
||||
embedding_input = v_input,
|
||||
embedding = v_embedding,
|
||||
embedding_model = 'cohere.embed-v4.0',
|
||||
inspection_note = 'Canonical GAME_USER_MASTER boundary: no game target returns a NULL metric value, '
|
||||
|| 'not an execution error and not a default game selection.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_REVIEW'
|
||||
WHERE example_id = 5
|
||||
AND reference_status = 'APPROVED'
|
||||
AND reference_kind = 'NO_TARGET'
|
||||
AND object_role = 'GAME_USER_MASTER';
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
@@ -1,103 +0,0 @@
|
||||
-- Redmine #749
|
||||
-- Compare Select AI prompt and SQL generation with annotations enabled/disabled.
|
||||
-- The Korean prompt is reconstructed from UTF-8 Base64 inside Oracle.
|
||||
|
||||
SET SERVEROUTPUT ON SIZE UNLIMITED
|
||||
SET FEEDBACK OFF
|
||||
SET VERIFY OFF
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
|
||||
DECLARE
|
||||
c_annotation_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_PROFILE';
|
||||
c_no_annotation_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_NOANN_PROFILE';
|
||||
c_prompt_base64 CONSTANT VARCHAR2(4000) :=
|
||||
'7J2067KIIOuLrCDtjIDsm5Drs4Qg7Zy06rCAIOyCrOyaqSDtmITtmansnYQg67O07Jes7KSY';
|
||||
|
||||
l_prompt VARCHAR2(4000) :=
|
||||
UTL_I18N.RAW_TO_CHAR(
|
||||
UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(c_prompt_base64)),
|
||||
'AL32UTF8'
|
||||
);
|
||||
|
||||
PROCEDURE run_one(
|
||||
p_run_no IN PLS_INTEGER,
|
||||
p_action IN VARCHAR2,
|
||||
p_profile_name IN VARCHAR2
|
||||
) IS
|
||||
l_started PLS_INTEGER;
|
||||
l_elapsed_ms PLS_INTEGER;
|
||||
l_result CLOB;
|
||||
l_result_head VARCHAR2(32767);
|
||||
l_result_hash VARCHAR2(128);
|
||||
l_valid_sql VARCHAR2(1) := '-';
|
||||
BEGIN
|
||||
l_started := DBMS_UTILITY.GET_TIME;
|
||||
l_result := DBMS_CLOUD_AI.GENERATE(
|
||||
l_prompt,
|
||||
p_profile_name,
|
||||
p_action
|
||||
);
|
||||
l_elapsed_ms := (DBMS_UTILITY.GET_TIME - l_started) * 10;
|
||||
l_result_head := DBMS_LOB.SUBSTR(l_result, 32767, 1);
|
||||
SELECT RAWTOHEX(STANDARD_HASH(l_result_head, 'SHA256'))
|
||||
INTO l_result_hash
|
||||
FROM dual;
|
||||
|
||||
IF p_action = 'showsql' THEN
|
||||
IF REGEXP_LIKE(LTRIM(l_result_head), '^(SELECT|WITH)[[:space:]]', 'i') THEN
|
||||
l_valid_sql := 'Y';
|
||||
ELSE
|
||||
l_valid_sql := 'N';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
'BENCHMARK|run=' || p_run_no
|
||||
|| '|action=' || p_action
|
||||
|| '|profile=' || p_profile_name
|
||||
|| '|elapsed_ms=' || l_elapsed_ms
|
||||
|| '|result_chars=' || DBMS_LOB.GETLENGTH(l_result)
|
||||
|| '|valid_sql=' || l_valid_sql
|
||||
|| '|result_sha256=' || l_result_hash
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
l_elapsed_ms := (DBMS_UTILITY.GET_TIME - l_started) * 10;
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
'BENCHMARK_ERROR|run=' || p_run_no
|
||||
|| '|action=' || p_action
|
||||
|| '|profile=' || p_profile_name
|
||||
|| '|elapsed_ms=' || l_elapsed_ms
|
||||
|| '|error=' || REPLACE(SUBSTR(SQLERRM, 1, 500), '|', '/')
|
||||
);
|
||||
END run_one;
|
||||
|
||||
PROCEDURE run_pair(p_run_no IN PLS_INTEGER, p_action IN VARCHAR2) IS
|
||||
BEGIN
|
||||
IF MOD(p_run_no, 2) = 1 THEN
|
||||
run_one(p_run_no, p_action, c_annotation_profile);
|
||||
run_one(p_run_no, p_action, c_no_annotation_profile);
|
||||
ELSE
|
||||
run_one(p_run_no, p_action, c_no_annotation_profile);
|
||||
run_one(p_run_no, p_action, c_annotation_profile);
|
||||
END IF;
|
||||
END run_pair;
|
||||
BEGIN
|
||||
DBMS_OUTPUT.PUT_LINE(
|
||||
'BENCHMARK_START|prompt_utf8_bytes=' ||
|
||||
UTL_RAW.LENGTH(UTL_I18N.STRING_TO_RAW(l_prompt, 'AL32UTF8'))
|
||||
);
|
||||
|
||||
FOR run_no IN 1 .. 3 LOOP
|
||||
run_pair(run_no, 'showprompt');
|
||||
END LOOP;
|
||||
|
||||
FOR run_no IN 1 .. 3 LOOP
|
||||
run_pair(run_no, 'showsql');
|
||||
END LOOP;
|
||||
|
||||
DBMS_OUTPUT.PUT_LINE('BENCHMARK_END');
|
||||
END;
|
||||
/
|
||||
|
||||
EXIT SUCCESS
|
||||
19
database/adb/86_sgmp_game_scope_profile_guidance.sql
Normal file
19
database/adb/86_sgmp_game_scope_profile_guidance.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Select AI profile instructions contain only common SQL-generation guidance.
|
||||
-- Game target routing and execution policy are supplied at runtime by
|
||||
-- SG_GAME_QUERY_PLAN; they do not belong in the profile-wide prompt.
|
||||
|
||||
BEGIN
|
||||
DBMS_CLOUD_AI.SET_ATTRIBUTE(
|
||||
profile_name => 'SGMP_POC_OCI_GPT54MINI',
|
||||
attribute_name => 'additional_instructions',
|
||||
attribute_value => q'~Generate Oracle SQL only for the listed approved objects. Do not reference external tables. Use English aliases only. Use database comments and annotations as the source of business rules.
|
||||
|
||||
~'
|
||||
);
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT attribute_name, attribute_value
|
||||
FROM user_cloud_ai_profile_attributes
|
||||
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
|
||||
AND attribute_name = 'additional_instructions';
|
||||
23
database/adb/87_sgmp_std09_catalog_fact_boundary.sql
Normal file
23
database/adb/87_sgmp_std09_catalog_fact_boundary.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Benchmark correction: game-catalog resolution and fact-row availability are distinct.
|
||||
-- Korean text is reconstructed from UTF-8 base64 so SQLcl cannot corrupt it.
|
||||
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'6rKM7J6EIOuzhOy5rSDsubTtg4jroZzqt7jsl5DshJwg7ZmV7J2465CcIOqyjOyehOydgCDqs7XthrUg66ek7LacIOqwneyytOydmCBHQU1FX0lE66W8IOuzhOy5rSDsubTtg4jroZzqt7ggRVhJU1RTL+yEnOu4jOy/vOumrOuhnCDsl7DqsrDtlZzri6QuIEdBTUVfSUTrgpggcHJlZml466W8IOyngeygkSDrpqzthLDrn7TroZwg6rOg7KCV7ZWY7KeAIOyViuuKlOuLpC4g7Lm07YOI66Gc6re4IOunpOy5reqzvCDtlbTri7kg66ek7LacIOyCrOyLpCDrjbDsnbTthLAg7KG07J6sIOyXrOu2gOuKlCDrs4TqsJzsnbTrqbAsIOunpOy5reuQmOyXiOyngOunjCDtlbTri7kg7J287J6QIOunpOy2nCDtlonsnbQg7JeG7Jy866m0IOynkeqzhCBOVUxMIOuYkOuKlCAw6rG07J2AIOygleyDgSDqsrDqs7zri6QuIOyehOydmOydmCDri6Trpbgg6rKM7J6E7Jy866GcIOuMgOyytO2VmOyngCDslYrripTri6Qu'
|
||||
)),
|
||||
'AL32UTF8'
|
||||
)
|
||||
WHERE question_code = 'STD-09';
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET inspection_status = 'REVIEW',
|
||||
inspection_note = 'Customer benchmark criterion updated: catalog resolution and common-fact availability are evaluated separately.'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-09';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT question_code, expected_focus
|
||||
FROM sg_ai_qa_question
|
||||
WHERE question_code = 'STD-09';
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Benchmark correction: common-fact eligibility is determined by the active
|
||||
-- alias source. Registry-only games are reported separately while eligible
|
||||
-- targets continue through the common-fact query.
|
||||
-- Korean baseline text and SQL are reconstructed from UTF-8 base64 for SQLcl safety.
|
||||
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'7Jes65+sIOqyjOyehCDruYTqtZDsl5DshJwg7Lm07YOI66Gc6re466GcIO2ZleyduOuQnCDrjIDsg4HsnYAg67OE7LmtIOy5tO2TiOuhnOq3uOulvCDqtazrj5kg7KeR7ZWp7Jy866GcIO2VmOqzoCDqs7XthrUg7IKs7IukIO2FjOydtOu4lOydhCBMRUZUIEpPSU7tlZjsl6wg6rKM7J6E67OEIOynkeqzhO2VnOuLpC4g7IKs7IukIO2WieydtCDsl4bripQg64yA7IOB7J2AIDDsnLzroZwg67O07KG07ZWY6rOgLCDrp6Tsua3rkJwg64uk66W4IOuMgOyDgeydmCDqsrDqs7zrpbwg7IOd65617ZWY7KeAIOyViuuKlOuLpC4gRFVBTCBVTklPTuycvOuhnCDrjIDsg4HrqoXqs7wg6rCS7J2EIO2VqeyEse2VmOyngCDslYrripTri6Qu'
|
||||
)),
|
||||
'AL32UTF8'
|
||||
),
|
||||
baseline_answer = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'7Lm07YOI66Gc6re4IOunpOy5rSDrjIDsg4Hrs4Qg66ek7Lac7J2EIOuwmO2ZmO2VnOuLpC4g7IKs7IukIO2WieydtCDsl4bripQg64yA7IOB7J2AIDAsIOuLpOuluCDrp6Tsua0g64yA7IOB7J2AIO2VtOuLuSDsnbzsnpDsnZgg7KeR6rOE6rCS7J2EIOuwmO2ZmO2VnOuLpC4='
|
||||
)),
|
||||
'AL32UTF8'
|
||||
),
|
||||
baseline_sql = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'V0lUSCByZXF1ZXN0ZWRfZ2FtZXMgQVMgKAogIFNFTEVDVCBESVNUSU5DVCBnLiJHQU1FX0lEIiwgZy4iR0FNRV9OTSIKICBGUk9NICJTR01QX1BPQyIuIkNPTU5fR0FNRV9BTElBU19CQVMiIGcKICBXSEVSRSBnLiJVU0VfWU4iID0gJ1knCiAgICBBTkQgKAogICAgICBVUFBFUihnLiJHQU1FX05NIikgTElLRSBVUFBFUignJUJ1YmJseXolJykKICAgICAgT1IgVVBQRVIoZy4iR0FNRV9BTElBU19OTSIpIExJS0UgVVBQRVIoJyVCdWJibHl6JScpCiAgICAgIE9SIFVQUEVSKGcuIkdBTUVfTk0iKSBMSUtFIFVQUEVSKCcl7Lm07KCc64KYJScpCiAgICAgIE9SIFVQUEVSKGcuIkdBTUVfQUxJQVNfTk0iKSBMSUtFIFVQUEVSKCcl7Lm07KCc64KYJScpCiAgICApCikKU0VMRUNUIHJnLiJHQU1FX05NIiBBUyAiR0FNRV9OQU1FIiwKICAgICAgIE5WTChTVU0oQ0FTRQogICAgICAgICBXSEVOIHMuIlBBWU1UX0RUTSIgPj0gVE9fREFURSgnMjAyNjA3MTUnLCAnWVlZWU1NREQnKQogICAgICAgICAgQU5EIHMuIlBBWU1UX0RUTSIgPCBUT19EQVRFKCcyMDI2MDcxNicsICdZWVlZTU1ERCcpCiAgICAgICAgICBBTkQgcy4iRVhQVF9VU0VSX1lOIiA9ICdOJwogICAgICAgICBUSEVOIENBU1Qocy4iUEFZTVRfQU1UIiBBUyBOVU1CRVIpIEVMU0UgMCBFTkQpLCAwKSBBUyAiU0FMRVNfQU1UIgpGUk9NIHJlcXVlc3RlZF9nYW1lcyByZwpMRUZUIEpPSU4gIlNHTVBfUE9DIi4iQ09NTl9TQUxFU19UWE4iIHMKICBPTiBzLiJHQU1FX0lEIiA9IHJnLiJHQU1FX0lEIgpHUk9VUCBCWSByZy4iR0FNRV9OTSIKT1JERVIgQlkgcmcuIkdBTUVfTk0iCg=='
|
||||
)),
|
||||
'AL32UTF8'
|
||||
)
|
||||
WHERE question_code = 'STD-11';
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET inspection_status = 'REVIEW',
|
||||
inspection_note = 'Customer benchmark criterion updated: multi-target common-fact comparisons preserve catalog-resolved zero-fact targets through a catalog-driven left join.'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-11';
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Correct the earlier catalog-only interpretation in this same migration. A
|
||||
-- registry can identify a game to the operator, but does not by itself make it
|
||||
-- an approved source for a common fact query.
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'67O17IiYIOqyjOyehCDqs7XthrUg7IKs7IukIOuNsOydtO2EsCDsp4jsnZjripQg7Zmc7ISxIOqyjOyehCDrs4Tsua0g7JuQ7LKc7JeQ7IScIO2ZleyduOuQnCDrjIDsg4Hrp4wg7IKs7IukIOyhsO2ajCDrjIDsg4HsnLzroZwg7IKs7Jqp7ZWY6rOgLCDrs4Tsua0g7JuQ7LKc7J20IOyXhuuKlCDroIjsp4DsiqTtirjrpqwg7KCE7JqpIOuMgOyDgeydgCDsobDtmowg67aI6rCAIOyDge2DnOuhnCDrs4Trj4Qg7JWI64K07ZWc64ukLiDsobDtmowg6rCA64ql7ZWcIOuMgOyDgeydmCDqsrDqs7zripQg7Jyg7KeA7ZWY66mwIOyghOyytCDsmpTssq3snYQg7LCo64uo7ZWY7KeAIOyViuuKlOuLpC4gRFVBTCBVTklPTuycvOuhnCDrjIDsg4Eg7ZaJ7J2064KYIOqwkuydhCDrp4zrk6Tsp4Ag7JWK64qU64ukLg=='
|
||||
)),
|
||||
'AL32UTF8'
|
||||
),
|
||||
baseline_answer = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'7Lm07KCc64KYIOunpOy2nOydgCAyMjcsNjgx7J6F64uI64ukLiBCdWJibHl664qUIO2ZnOyEsSDqsozsnoQg67OE7LmtIOybkOyynOydtCDsl4bslrQg66ek7LacIOyhsO2ajCDrjIDsg4HsnbQg7JWE64uZ64uI64ukLg=='
|
||||
)),
|
||||
'AL32UTF8'
|
||||
)
|
||||
WHERE question_code = 'STD-11';
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET inspection_status = 'REVIEW',
|
||||
inspection_note = 'Customer benchmark criterion updated: common-fact comparison queries use active-alias eligible targets; registry-only targets are separately unavailable and do not become synthetic fact rows.'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-11';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT question_code, expected_focus, baseline_answer
|
||||
FROM sg_ai_qa_question
|
||||
WHERE question_code = 'STD-11';
|
||||
22
database/adb/89_sgmp_common_fact_multi_target_guidance.sql
Normal file
22
database/adb/89_sgmp_common_fact_multi_target_guidance.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Object-specific, data-driven guidance for comparison queries on a common fact table.
|
||||
-- No game, prefix, ID, or physical per-game object is embedded in this annotation.
|
||||
|
||||
DECLARE
|
||||
v_result VARCHAR2(4000);
|
||||
BEGIN
|
||||
v_result := sgmp_set_annotation(
|
||||
'SGMP_POC',
|
||||
'TABLE',
|
||||
'COMN_SALES_TXN',
|
||||
NULL,
|
||||
'Game fact scope: query this table only for plan targets marked ACTIVE_ALIAS. Derive target GAME_ID values through active COMN_GAME_ALIAS_BAS aliases rather than direct identifier or prefix literals. For a mixed request, retain the eligible target results and report other plan statuses separately; do not substitute or manufacture a target result.',
|
||||
'MULTI_TARGET_COMPARISON'
|
||||
);
|
||||
DBMS_OUTPUT.PUT_LINE(v_result);
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT annotation_name, annotation_value
|
||||
FROM user_annotations_usage
|
||||
WHERE object_name = 'COMN_SALES_TXN'
|
||||
AND annotation_name = 'MULTI_TARGET_COMPARISON';
|
||||
92
database/adb/90_sgmp_multi_common_fact_fewshot_template.sql
Normal file
92
database/adb/90_sgmp_multi_common_fact_fewshot_template.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
-- Generic Few-shot structure for multi-target comparisons on a common fact object.
|
||||
-- The template is intentionally logical: no current game, prefix, ID, date, or result value is embedded.
|
||||
|
||||
DECLARE
|
||||
v_exists NUMBER;
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO v_exists
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'MULTI_COMMON_FACT_LEFT_JOIN';
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
v_input := TO_CLOB('Question pattern: Compare a common fact metric across multiple resolved games. Use only fact-query-eligible targets and separately report known registry-only or unavailable targets.')
|
||||
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|
||||
|| CHR(10) || 'Required structure: active alias catalog distinct game set, left join fact, aggregate by catalog display identifier.';
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, object_role,
|
||||
inspection_status, inspection_note, verified_at, verified_by,
|
||||
source_case_id, source_type
|
||||
) VALUES (
|
||||
'Compare a common fact metric across multiple resolved games, including games with no fact rows.',
|
||||
TO_CLOB('WITH resolved_games AS (' || CHR(10)
|
||||
|| ' SELECT DISTINCT a."GAME_ID", a."GAME_NM"' || CHR(10)
|
||||
|| ' FROM "SGMP_POC"."COMN_GAME_ALIAS_BAS" a' || CHR(10)
|
||||
|| ' WHERE a."USE_YN" = ''Y''' || CHR(10)
|
||||
|| ' AND (<ACTIVE_ALIAS_MATCHES_FOR_EACH_REQUESTED_GAME_TERM>)' || CHR(10)
|
||||
|| ')' || CHR(10)
|
||||
|| 'SELECT g."GAME_NM" AS "GAME_NAME",' || CHR(10)
|
||||
|| ' NVL(SUM(CASE WHEN <FACT_DATE_AND_EXCLUSION_CONDITION>' || CHR(10)
|
||||
|| ' THEN CAST(f."<METRIC_COLUMN>" AS NUMBER) ELSE 0 END), 0) AS "METRIC_VALUE"' || CHR(10)
|
||||
|| 'FROM resolved_games g' || CHR(10)
|
||||
|| 'LEFT JOIN "SGMP_POC"."<APPROVED_COMMON_FACT_OBJECT>" f' || CHR(10)
|
||||
|| ' ON f."GAME_ID" = g."GAME_ID"' || CHR(10)
|
||||
|| 'GROUP BY g."GAME_NM"' || CHR(10)
|
||||
|| 'ORDER BY g."GAME_NM"'),
|
||||
'Structural Few-shot only. Replace every angle-bracket placeholder from the current approved object metadata, the current game query plan, and the original question. Only ACTIVE_ALIAS targets enter the alias-driven LEFT JOIN and grouping; report registry-only or unavailable targets from the plan without manufacturing fact rows with DUAL/UNION.',
|
||||
v_input,
|
||||
v_embedding,
|
||||
'cohere.embed-v4.0',
|
||||
'APPROVED', 'SQL_TEMPLATE', 'MULTI', 'SALES_TRANSACTION',
|
||||
'VERIFIED',
|
||||
'Generic, non-customer-specific comparison structure. Verified against the game-alias and common-fact metadata contract; not an executable answer key.',
|
||||
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
|
||||
'MULTI_COMMON_FACT_LEFT_JOIN', 'POLICY_TEMPLATE'
|
||||
);
|
||||
END IF;
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Keep the approved template current when the policy text evolves. The vector
|
||||
-- is rebuilt from its generic retrieval text; no customer answer is embedded.
|
||||
DECLARE
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
BEGIN
|
||||
v_input := TO_CLOB('Question pattern: Compare a common fact metric across multiple resolved games. Use only fact-query-eligible targets and separately report known registry-only or unavailable targets.')
|
||||
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|
||||
|| CHR(10) || 'Required structure: active alias catalog distinct game set, left join fact, aggregate by catalog display identifier.';
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET question = 'Compare a common fact metric across multiple resolved games, using only fact-query-eligible targets.',
|
||||
answer_text = 'Structural Few-shot only. Replace every angle-bracket placeholder from the current approved object metadata, the current game query plan, and the original question. Only ACTIVE_ALIAS targets enter the alias-driven LEFT JOIN and grouping; report registry-only or unavailable targets from the plan without manufacturing fact rows with DUAL/UNION.',
|
||||
embedding_input = v_input,
|
||||
embedding = v_embedding,
|
||||
inspection_note = 'Generic, non-customer-specific comparison structure. Active-alias targets are fact-query eligible; registry-only targets are reported separately. Not an executable answer key.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'MULTI_COMMON_FACT_LEFT_JOIN';
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT example_id, reference_status, reference_kind, target_type, object_role,
|
||||
source_case_id, source_type
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'MULTI_COMMON_FACT_LEFT_JOIN';
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Retire the overly specific no-target template. The profile and table
|
||||
-- metadata carry this general scope policy without a case-shaped example.
|
||||
|
||||
DELETE FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'NO_ELIGIBLE_FACT_TARGET';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT COUNT(*) AS remaining_template_count
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'NO_ELIGIBLE_FACT_TARGET';
|
||||
70
database/adb/92_sgmp_transaction_detail_fewshot_template.sql
Normal file
70
database/adb/92_sgmp_transaction_detail_fewshot_template.sql
Normal file
@@ -0,0 +1,70 @@
|
||||
-- Generic Few-shot structure for a filtered transaction/order detail request.
|
||||
-- It fixes the output grain through an approved object pattern, not a global
|
||||
-- instruction or a customer-specific answer.
|
||||
|
||||
DECLARE
|
||||
v_input CLOB;
|
||||
v_embedding VECTOR;
|
||||
v_exists NUMBER;
|
||||
BEGIN
|
||||
v_input := TO_CLOB('Question pattern: List individual payment orders that match a business date and an amount condition.')
|
||||
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|
||||
|| CHR(10) || 'Required output: transaction identifiers, game, user, payment timestamp, and payment amount.';
|
||||
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
v_input,
|
||||
JSON(sg_qa_vector_params('search_document'))
|
||||
);
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO v_exists
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'FILTERED_TRANSACTION_DETAIL';
|
||||
|
||||
IF v_exists = 0 THEN
|
||||
INSERT INTO sg_qa_vector_example (
|
||||
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
|
||||
reference_status, reference_kind, target_type, object_role,
|
||||
inspection_status, inspection_note, verified_at, verified_by,
|
||||
source_case_id, source_type
|
||||
) VALUES (
|
||||
'List payment orders matching a date and amount condition.',
|
||||
TO_CLOB('SELECT t."PAYMT_TRANSAC_ID" AS "PAYMENT_TRANSACTION_ID",' || CHR(10)
|
||||
|| ' t."PAYMT_TRANSAC_DTL_ID" AS "PAYMENT_TRANSACTION_DETAIL_ID",' || CHR(10)
|
||||
|| ' t."GAME_ID" AS "GAME_ID",' || CHR(10)
|
||||
|| ' t."GUID" AS "USER_ID",' || CHR(10)
|
||||
|| ' t."PAYMT_DTM" AS "PAYMENT_DATETIME",' || CHR(10)
|
||||
|| ' t."PAYMT_AMT" AS "PAYMENT_AMOUNT"' || CHR(10)
|
||||
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" t' || CHR(10)
|
||||
|| 'WHERE t."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|
||||
|| ' AND t."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|
||||
|| ' AND CAST(t."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|
||||
|| ' AND t."EXPT_USER_YN" = ''N''' || CHR(10)
|
||||
|| 'ORDER BY t."PAYMT_DTM", t."PAYMT_TRANSAC_ID", t."PAYMT_TRANSAC_DTL_ID"'),
|
||||
'Structural Few-shot only. Replace placeholders using the original request and approved metadata. Use the business payment timestamp for a payment-date condition. This pattern is for individual transaction detail; do not substitute an aggregate-only result for a requested order list.',
|
||||
v_input,
|
||||
v_embedding,
|
||||
'cohere.embed-v4.0',
|
||||
'APPROVED', 'SQL_TEMPLATE', 'NONE', 'SALES_TRANSACTION',
|
||||
'VERIFIED',
|
||||
'Generic transaction-detail output shape with no customer date, amount, game, or result value; not an executable answer key.',
|
||||
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
|
||||
'FILTERED_TRANSACTION_DETAIL', 'POLICY_TEMPLATE'
|
||||
);
|
||||
ELSE
|
||||
UPDATE sg_qa_vector_example
|
||||
SET embedding_input = v_input,
|
||||
embedding = v_embedding,
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'FILTERED_TRANSACTION_DETAIL';
|
||||
END IF;
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
SELECT example_id, reference_status, target_type, object_role, source_case_id
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'POLICY_TEMPLATE'
|
||||
AND source_case_id = 'FILTERED_TRANSACTION_DETAIL';
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Customer question wording requests individual orders. Align the benchmark
|
||||
-- with the transaction-detail output pattern rather than forcing KPI summary.
|
||||
|
||||
UPDATE sg_ai_qa_question
|
||||
SET expected_focus = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'Q09NTl9TQUxFU19UWE7sl5DshJwgUEFZTVRfRFRNIOq4sOykgOydvCwgUEFZTVRfQU1UID4gMTAwMDAsIEVYUFRfVVNFUl9ZTj0nTicg7KGw6rG07J2YIOqwnOuzhCDso7zrrLgg7IOB7IS466W8IOyhsO2ajO2VnOuLpC4g7KO866y4IOyLneuzhOyekCwg6rKM7J6ELCDsgqzsmqnsnpAsIOqysOygnCDsnbzsi5zsmYAg6riI7JWh7J2EIOygnOqzte2VnOuLpC4='
|
||||
)),
|
||||
'AL32UTF8'
|
||||
),
|
||||
baseline_sql = TO_CLOB('SELECT s."PAYMT_TRANSAC_ID" AS "PAYMENT_TRANSACTION_ID",' || CHR(10)
|
||||
|| ' s."PAYMT_TRANSAC_DTL_ID" AS "PAYMENT_TRANSACTION_DETAIL_ID",' || CHR(10)
|
||||
|| ' s."GAME_ID" AS "GAME_ID",' || CHR(10)
|
||||
|| ' s."GUID" AS "USER_ID",' || CHR(10)
|
||||
|| ' s."PAYMT_DTM" AS "PAYMENT_DATETIME",' || CHR(10)
|
||||
|| ' s."PAYMT_AMT" AS "PAYMENT_AMOUNT"' || CHR(10)
|
||||
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|
||||
|| 'WHERE s."PAYMT_DTM" >= DATE ''2026-07-15''' || CHR(10)
|
||||
|| ' AND s."PAYMT_DTM" < DATE ''2026-07-16''' || CHR(10)
|
||||
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) > 10000' || CHR(10)
|
||||
|| ' AND s."EXPT_USER_YN" = ''N''' || CHR(10)
|
||||
|| 'ORDER BY s."PAYMT_DTM", s."PAYMT_TRANSAC_ID", s."PAYMT_TRANSAC_DTL_ID"'),
|
||||
baseline_answer = utl_i18n.raw_to_char(
|
||||
utl_encode.base64_decode(utl_raw.cast_to_raw(
|
||||
'6rKw7KCc6riI7JWhIDHrp4zsm5Ag7LSI6rO8IOyjvOusuCA26rG07J2EIOyjvOusuCDsi53rs4TsnpAsIOqyjOyehCwg7IKs7Jqp7J6QLCDqsrDsoJwg7J287IucLCDqsrDsoJzquIjslaHqs7wg7ZWo6ruYIOuwmO2ZmO2VnOuLpC4='
|
||||
)),
|
||||
'AL32UTF8'
|
||||
)
|
||||
WHERE question_code = 'STD-18';
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_sql = TO_CLOB('SELECT s."PAYMT_TRANSAC_ID" AS "PAYMENT_TRANSACTION_ID",' || CHR(10)
|
||||
|| ' s."PAYMT_TRANSAC_DTL_ID" AS "PAYMENT_TRANSACTION_DETAIL_ID",' || CHR(10)
|
||||
|| ' s."GAME_ID" AS "GAME_ID",' || CHR(10)
|
||||
|| ' s."GUID" AS "USER_ID",' || CHR(10)
|
||||
|| ' s."PAYMT_DTM" AS "PAYMENT_DATETIME",' || CHR(10)
|
||||
|| ' s."PAYMT_AMT" AS "PAYMENT_AMOUNT"' || CHR(10)
|
||||
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|
||||
|| 'WHERE s."PAYMT_DTM" >= DATE ''2026-07-15''' || CHR(10)
|
||||
|| ' AND s."PAYMT_DTM" < DATE ''2026-07-16''' || CHR(10)
|
||||
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) > 10000' || CHR(10)
|
||||
|| ' AND s."EXPT_USER_YN" = ''N''' || CHR(10)
|
||||
|| 'ORDER BY s."PAYMT_DTM", s."PAYMT_TRANSAC_ID", s."PAYMT_TRANSAC_DTL_ID"'),
|
||||
answer_text = 'Approved customer Few-shot: return individual qualifying payment orders with transaction identifiers, game, user, payment timestamp, and payment amount. Use PAYMT_DTM for the payment business date.',
|
||||
reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Customer benchmark aligned to detailed qualifying orders and the payment business timestamp.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-18';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT question_code, expected_focus, baseline_answer
|
||||
FROM sg_ai_qa_question
|
||||
WHERE question_code = 'STD-18';
|
||||
19
database/adb/94_sgmp_std21_country_continuous_fewshot.sql
Normal file
19
database/adb/94_sgmp_std21_country_continuous_fewshot.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Customer benchmark evidence is retained for evaluation only. It is not a
|
||||
-- runtime Few-shot because the vector store must not become a collection of
|
||||
-- case-specific benchmark overrides.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'RETIRED',
|
||||
inspection_status = 'RETIRED',
|
||||
inspection_note = 'Retired from runtime Few-shot retrieval; retained as customer QA evaluation evidence.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-21';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT example_id, reference_status, inspection_status, source_case_id
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-21';
|
||||
17
database/adb/95_sgmp_std22_nru_fewshot.sql
Normal file
17
database/adb/95_sgmp_std22_nru_fewshot.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Promote the reviewed customer QA example for the NRU metric.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Customer QA reviewed: NRU is measured with NRU_FLAG, with the stated date and excluded-user condition.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-22';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT example_id, reference_status, inspection_status, source_case_id
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'STD-22';
|
||||
17
database/adb/96_sgmp_czn02_standard_au_fewshot.sql
Normal file
17
database/adb/96_sgmp_czn02_standard_au_fewshot.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Promote the reviewed customer QA example for the standard AU metric.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
inspection_note = 'Customer QA reviewed: standard AU is measured with AU_FLAG and excluded-user filtering.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-02';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT example_id, reference_status, inspection_status, source_case_id
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-02';
|
||||
57
database/adb/97_sgmp_exact_qa_fewshot_priority.sql
Normal file
57
database/adb/97_sgmp_exact_qa_fewshot_priority.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- Prefer an exact approved customer QA question over semantically adjacent
|
||||
-- vector neighbours. This is a general retrieval rule; it does not encode
|
||||
-- a game, metric, table, or customer-case-specific SQL policy.
|
||||
|
||||
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
|
||||
p_question IN CLOB,
|
||||
p_top_k IN PLS_INTEGER DEFAULT 3,
|
||||
p_target_type IN VARCHAR2 DEFAULT 'ANY'
|
||||
) RETURN SYS_REFCURSOR
|
||||
AUTHID DEFINER
|
||||
IS
|
||||
v_query_vector VECTOR;
|
||||
v_results SYS_REFCURSOR;
|
||||
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
|
||||
BEGIN
|
||||
IF p_question IS NULL THEN
|
||||
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
|
||||
END IF;
|
||||
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
|
||||
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
|
||||
END IF;
|
||||
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
|
||||
RAISE_APPLICATION_ERROR(-20005, 'target_type must be NONE, SINGLE, MULTI, ALL, or ANY.');
|
||||
END IF;
|
||||
|
||||
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
|
||||
p_question,
|
||||
JSON(sg_qa_vector_params('search_query'))
|
||||
);
|
||||
|
||||
OPEN v_results FOR
|
||||
SELECT example_id,
|
||||
question,
|
||||
answer_sql,
|
||||
answer_text,
|
||||
embedding_model,
|
||||
reference_kind,
|
||||
target_type,
|
||||
object_role,
|
||||
source_case_id,
|
||||
source_type,
|
||||
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
|
||||
FROM sg_qa_vector_example
|
||||
WHERE reference_status = 'APPROVED'
|
||||
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
|
||||
ORDER BY CASE
|
||||
WHEN DBMS_LOB.COMPARE(
|
||||
LOWER(TRIM(question)), LOWER(TRIM(p_question))
|
||||
) = 0 THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
vector_distance(embedding, v_query_vector, COSINE),
|
||||
example_id
|
||||
FETCH FIRST p_top_k ROWS ONLY;
|
||||
RETURN v_results;
|
||||
END;
|
||||
/
|
||||
20
database/adb/98_sgmp_czn02_standard_au_semantics.sql
Normal file
20
database/adb/98_sgmp_czn02_standard_au_semantics.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Strengthen the approved customer QA example itself. The wording belongs
|
||||
-- to the benchmark Few-shot record, not to a global Select AI profile rule.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET answer_text = 'Expected focus: CZN_COMN_USER_MST, BASE_DT=2026-07-15, AU_FLAG=1, EXPT_USER_YN=''N''. '
|
||||
|| 'The phrase standard AU is the report metric label; do not add STD_USER_YN unless the question separately asks for the standard-user cohort. '
|
||||
|| 'Historical answer: STD_AU_COUNT=0',
|
||||
inspection_note = 'Customer QA verified: standard AU uses the AU flag and excluded-user filtering; standard-user cohort is a separate request.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-02'
|
||||
AND reference_status = 'APPROVED';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT example_id, reference_status, inspection_status, answer_text
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-02';
|
||||
23
database/adb/99_sgmp_czn03_standard_business_au_fewshot.sql
Normal file
23
database/adb/99_sgmp_czn03_standard_business_au_fewshot.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Approve the reviewed customer QA comparison example. Metric definitions
|
||||
-- stay in the exact Few-shot example rather than becoming global profile text.
|
||||
|
||||
UPDATE sg_qa_vector_example
|
||||
SET reference_status = 'APPROVED',
|
||||
inspection_status = 'VERIFIED',
|
||||
answer_text = 'Expected focus: compare two independently aggregated metrics for the same resolved game and date. '
|
||||
|| 'Standard AU: CZN_COMN_USER_MST with AU_FLAG=1 and EXPT_USER_YN=''N''. '
|
||||
|| 'Business AU: CZN_CUSTOM_BIZ_USER_TXN with BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. '
|
||||
|| 'The labels standard AU and business AU do not imply STD_USER_YN. '
|
||||
|| 'Historical answer: STD_AU_COUNT=0, BIZ_AU_COUNT=1.',
|
||||
inspection_note = 'Customer QA verified: standard and business AU are separate aggregates with their respective AU flags.',
|
||||
verified_at = SYSTIMESTAMP,
|
||||
verified_by = 'SGMP_POC_METADATA_REVIEW'
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-03';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT example_id, reference_status, inspection_status, source_case_id, answer_text
|
||||
FROM sg_qa_vector_example
|
||||
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
|
||||
AND source_case_id = 'CZN-03';
|
||||
@@ -42,10 +42,6 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.jdbc</groupId>
|
||||
<artifactId>ojdbc11</artifactId>
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.cloudhandson.ddsbackoffice;
|
||||
|
||||
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
||||
import com.cloudhandson.ddsbackoffice.config.DdsMcpIamProperties;
|
||||
import com.cloudhandson.ddsbackoffice.config.DdsMcpOidcProperties;
|
||||
import com.cloudhandson.vpdbackoffice.VpdBackofficeApplication;
|
||||
import com.cloudhandson.vpdbackoffice.web.DashboardController;
|
||||
import com.cloudhandson.vpdbackoffice.web.LoginController;
|
||||
@@ -16,7 +15,7 @@ import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties({DdsProperties.class, DdsMcpIamProperties.class, DdsMcpOidcProperties.class})
|
||||
@EnableConfigurationProperties({DdsProperties.class, DdsMcpIamProperties.class})
|
||||
@MapperScan("com.cloudhandson.vpdbackoffice.mapper")
|
||||
@ComponentScan(
|
||||
basePackages = {"com.cloudhandson.ddsbackoffice", "com.cloudhandson.vpdbackoffice"},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user