vignette/infra/db/init/03_kb.sql

159 lines
8.6 KiB
SQL

-- =============================================================================
-- Vignette · 03_kb.sql
-- kb 스키마: 정적 지식 KB (DSM/이론/technique/taxonomy) — app 과 물리 분리
-- ⑤ KNOWLEDGE 계층. 3-AI 가 같은 물리 테이블을 다른 정책(visible_to+sensitivity)으로 검색.
-- 근거: MEMORY_KNOWLEDGE_PERSONA_DESIGN.md §3.6, §4.3 / MASTERPLAN §3.6
-- 전제: 02_schema.sql (app.technique_label_def 존재). 적용 순서: 02 → 03(본 파일).
-- 원칙: app 으로의 FK 없음(read-only 독립). license_class C/D = 외부 LLM 차단.
-- =============================================================================
CREATE SCHEMA IF NOT EXISTS kb;
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SET search_path TO kb, public;
-- =============================================================================
-- 1. 출처 + 라이선스 (DSM-5 저작권 1차 통제, 설계서 §3.6)
-- =============================================================================
CREATE TABLE IF NOT EXISTS kb.source (
source_id TEXT PRIMARY KEY, -- 'dsm5','theory','taxonomy_0615'
title TEXT NOT NULL,
kb_kind TEXT NOT NULL CHECK (kb_kind IN
('diagnostic','theory','technique','taxonomy',
'supervisor_pattern','template','ko_context','microskill')),
license_class CHAR(1) NOT NULL CHECK (license_class IN ('A','B','C','D')),
-- C=저작권민감 D=미성년파생
origin_path TEXT,
citation TEXT,
external_llm_ok BOOLEAN NOT NULL DEFAULT FALSE, -- C/D면 false → 국내 라우팅
CONSTRAINT ck_kb_source_external_license
CHECK (license_class IN ('A','B') OR external_llm_ok = FALSE),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- =============================================================================
-- 1-A. 관리자 등록 프로토콜 수명주기
-- draft 는 원문·메타데이터만 보관하고 검색 인덱스를 만들지 않는다.
-- active 전환이 성공한 뒤에만 같은 source_id 의 document/chunk 가 검색 후보가 된다.
-- =============================================================================
CREATE TABLE IF NOT EXISTS kb.protocol_registration (
protocol_id UUID PRIMARY KEY,
source_id TEXT NOT NULL UNIQUE,
title TEXT NOT NULL CHECK (btrim(title) <> ''),
source_ref TEXT NOT NULL CHECK (btrim(source_ref) <> ''),
version INT NOT NULL CHECK (version > 0),
license_class CHAR(1) NOT NULL CHECK (license_class IN ('A','B','C','D')),
external_llm_ok BOOLEAN NOT NULL DEFAULT FALSE,
content TEXT NOT NULL CHECK (btrim(content) <> ''),
content_hash CHAR(64) NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft','active','retired')),
registered_by UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT,
registered_at TIMESTAMPTZ NOT NULL DEFAULT now(),
activated_at TIMESTAMPTZ,
retired_at TIMESTAMPTZ,
CONSTRAINT ck_protocol_external_license
CHECK (license_class IN ('A','B') OR external_llm_ok = FALSE),
CONSTRAINT ck_protocol_lifecycle_timestamps CHECK (
(status = 'draft' AND activated_at IS NULL AND retired_at IS NULL)
OR (status = 'active' AND activated_at IS NOT NULL AND retired_at IS NULL)
OR (status = 'retired' AND activated_at IS NOT NULL AND retired_at IS NOT NULL)
)
);
CREATE INDEX IF NOT EXISTS idx_protocol_registration_status
ON kb.protocol_registration(status, registered_at DESC);
-- =============================================================================
-- 1-B. 원문 격리 아티팩트 (RAG 색인 금지)
-- raw_source_artifact 는 kb.chunk/embedding/FTS 로 들어가지 않는 별도 보관 증거다.
-- =============================================================================
CREATE TABLE IF NOT EXISTS kb.raw_source_artifact (
raw_source_id TEXT PRIMARY KEY,
derived_source_id TEXT REFERENCES kb.source(source_id),
owner_user_id UUID,
doc_uri TEXT NOT NULL,
content_hash TEXT NOT NULL,
storage_uri TEXT,
sealed_blob BYTEA,
license_class CHAR(1) NOT NULL CHECK (license_class IN ('A','B','C','D')),
redaction_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
external_llm_ok BOOLEAN NOT NULL DEFAULT FALSE CHECK (external_llm_ok = FALSE),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_raw_source_artifact_derived
ON kb.raw_source_artifact(derived_source_id);
-- =============================================================================
-- 2. 문서 + 버전 (graphify content_hash 증분 인덱싱 차용)
-- =============================================================================
CREATE TABLE IF NOT EXISTS kb.document (
doc_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
source_id TEXT NOT NULL REFERENCES kb.source(source_id),
doc_uri TEXT NOT NULL,
version INT NOT NULL DEFAULT 1,
content_hash TEXT NOT NULL, -- SHA256 → 변경감지(증분)
superseded_by BIGINT REFERENCES kb.document(doc_id),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
indexed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source_id, doc_uri, version)
);
CREATE INDEX IF NOT EXISTS idx_kb_doc_active
ON kb.document(source_id) WHERE is_active;
-- =============================================================================
-- 3. 청크 + 멀티벡터 (검색 본체, 설계서 §3.6)
-- visible_to + sensitivity 가 3-AI 정보비대칭을 DB WHERE 로 강제.
-- =============================================================================
CREATE TABLE IF NOT EXISTS kb.chunk (
chunk_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
doc_id BIGINT NOT NULL REFERENCES kb.document(doc_id) ON DELETE CASCADE,
source_id TEXT NOT NULL, -- 비정규화(필터 가속)
kb_kind TEXT NOT NULL, -- 비정규화
seq INT NOT NULL,
heading_path TEXT,
chunk_text TEXT NOT NULL, -- 표시·LLM 주입(prefix 미포함)
context_prefix TEXT, -- Contextual Retrieval 주입문
embedding vector(1024), -- BGE-M3 dense
sparse_vec JSONB, -- BGE-M3 sparse
colbert_vecs JSONB, -- 멀티벡터(단일 호출 산출)
visible_to TEXT[] NOT NULL DEFAULT '{client,counselor,evaluator}',
sensitivity SMALLINT NOT NULL DEFAULT 0
CHECK (sensitivity BETWEEN 0 AND 3), -- 0공개 1내부 2평가전용 3원천격리
label_id BIGINT REFERENCES app.technique_label_def(label_id), -- taxonomy 정답라벨
meta JSONB DEFAULT '{}', -- {dsm_category,theory,bias_weight,...}
token_count INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- dense ANN
CREATE INDEX IF NOT EXISTS idx_chunk_hnsw ON kb.chunk
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
-- sparse/BM25 (한국어: simple + pg_trgm 병용, M10 완화)
CREATE INDEX IF NOT EXISTS idx_chunk_fts ON kb.chunk
USING gin (to_tsvector('simple', chunk_text));
CREATE INDEX IF NOT EXISTS idx_chunk_trgm ON kb.chunk
USING gin (chunk_text gin_trgm_ops);
-- 정보비대칭 사전필터
CREATE INDEX IF NOT EXISTS idx_chunk_visible ON kb.chunk USING gin (visible_to);
CREATE INDEX IF NOT EXISTS idx_chunk_route ON kb.chunk (kb_kind, sensitivity);
CREATE INDEX IF NOT EXISTS idx_chunk_source ON kb.chunk (source_id);
-- =============================================================================
-- 4. KB 검색 로그 (환각측정·캐시검증, 람다 cost.json 차용, 설계서 §3.7)
-- =============================================================================
CREATE TABLE IF NOT EXISTS kb.retrieval_log (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
session_id UUID,
turn_id UUID,
ai_role TEXT NOT NULL, -- 'client'|'counselor'|'evaluator'
query_text TEXT,
policy TEXT NOT NULL, -- 4-튜플 정책명
hit_chunk_ids BIGINT[],
rerank_scores REAL[],
top1_score REAL, -- CRAG 게이트
used_in_answer BOOLEAN,
latency_ms INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_kb_retr_session ON kb.retrieval_log(session_id);