feat(engine): claude -p 상주 멀티턴 엔진 게이트웨이 + 실동작 검증
- engine_gateway/gateway.py: 회기당 claude -p 상주 프로세스(stream-json), 턴 직렬, budget 제한 - 세션 생성/턴/종료 HTTP API(FastAPI, :9099) - 검증: 멀티턴 컨텍스트 유지 + prompt caching 재사용(턴2 +$0.07) 실동작 확인
This commit is contained in:
parent
d5b86c5f89
commit
84eb6e2173
20 changed files with 2038 additions and 1 deletions
371
infra/db/init/02_schema.sql
Normal file
371
infra/db/init/02_schema.sql
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
-- =============================================================================
|
||||
-- Vignette · 02_schema.sql
|
||||
-- app 스키마: 세션 + 라벨 코드테이블 + 페르소나(불변 카드) + 메모리 4계층
|
||||
-- (① working / ② episodic / ③ summary / ④ semantic)
|
||||
-- 근거: MEMORY_KNOWLEDGE_PERSONA_DESIGN.md §3.1~3.5, MASTERPLAN.md §3.2
|
||||
-- 전제: 01_extensions.sql 이 vector 확장 + 스키마(app/audit/kb) 선생성.
|
||||
-- 적용 순서: 01 → 02(본 파일) → 03_kb → 04_audit_eval_rls
|
||||
-- 원칙(설계서 §0.3): 원본 append-only, 상태수치 결정론 carry-over, visible_to DB 강제.
|
||||
-- Phase 0 에 전체 선반영(대부분 nullable), cross-session 채우기는 Phase 2a.
|
||||
-- =============================================================================
|
||||
|
||||
SET search_path TO app, public;
|
||||
|
||||
-- 01_extensions.sql 미적용 환경에서도 단독 실행 가능하도록 방어적 보강.
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- gen_random_uuid()
|
||||
CREATE SCHEMA IF NOT EXISTS app;
|
||||
CREATE SCHEMA IF NOT EXISTS audit;
|
||||
CREATE SCHEMA IF NOT EXISTS kb;
|
||||
|
||||
-- =============================================================================
|
||||
-- 0. 인적 주체 (RBAC 앵커) — 학습자/교수자/관리자
|
||||
-- visible_to / RLS 가 참조하는 최소 사용자 테이블. SSO 매핑은 BFF 소유.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.app_user (
|
||||
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
external_id TEXT UNIQUE, -- Google OIDC sub 등
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
role TEXT NOT NULL DEFAULT 'learner'
|
||||
CHECK (role IN ('learner','instructor','admin')),
|
||||
cohort TEXT, -- 교수자 담당 코호트 매칭
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
consent_at TIMESTAMPTZ, -- 사전동의 하드게이트(IRB)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_app_user_cohort ON app.app_user(cohort);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. 페르소나 — 불변 카드 (설계서 §3.1)
|
||||
-- 람다 IDENTITY/SOUL 차용. 버전드, 교수만 편집. 진행 세션은 옛 version 핀.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.persona_card (
|
||||
persona_id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
code TEXT NOT NULL, -- 'P1','P2','P3'
|
||||
version INT NOT NULL DEFAULT 1, -- 편집 시 +1
|
||||
status TEXT NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft','review','approved','archived')),
|
||||
display_name TEXT NOT NULL,
|
||||
difficulty TEXT NOT NULL CHECK (difficulty IN ('easy','moderate','hard')),
|
||||
theory_target TEXT[], -- {'humanistic','cbt'}
|
||||
demographics JSONB NOT NULL, -- 범주화(재식별 방지 F-25)
|
||||
presenting JSONB NOT NULL, -- 표층 호소
|
||||
history JSONB NOT NULL, -- 과거사
|
||||
big5 JSONB NOT NULL, -- {O,C,E,A,N} 0~1
|
||||
resistance JSONB NOT NULL, -- {base_resistance,unlock_rate,decay_floor,...}
|
||||
speech_style JSONB NOT NULL, -- {register,avg_sentence_len,fillers,...}
|
||||
affect_baseline JSONB NOT NULL, -- {...,suicide_ideation_stage} hard상한=3
|
||||
ccd JSONB NOT NULL, -- Patient-Ψ 8요소 (★직접발화 금지 R4)
|
||||
dsm5_dimensional JSONB NOT NULL, -- criteria_behavior_matrix (진단명 비노출)
|
||||
source_provenance TEXT NOT NULL, -- '0615 합성변형'
|
||||
is_synthetic BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_by UUID REFERENCES app.app_user(user_id),
|
||||
approved_by UUID REFERENCES app.app_user(user_id),
|
||||
approved_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (persona_id, version),
|
||||
UNIQUE (code, version)
|
||||
);
|
||||
-- 승인된 최신 카드 빠른 조회 (회기 시작 시 카드 핀).
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_card_status
|
||||
ON app.persona_card(status) WHERE status = 'approved';
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_card_code ON app.persona_card(code, version DESC);
|
||||
|
||||
-- 음성 매핑 (provider-agnostic, 카드 버전 정합)
|
||||
CREATE TABLE IF NOT EXISTS app.persona_voice_map (
|
||||
persona_id UUID NOT NULL,
|
||||
version INT NOT NULL,
|
||||
voice_id TEXT NOT NULL, -- 'voice_p1_teen_m'
|
||||
provider TEXT NOT NULL CHECK (provider IN ('openai','higgs','melotts')),
|
||||
base_params JSONB NOT NULL,
|
||||
prosody_map JSONB NOT NULL, -- 상태→prosody 함수
|
||||
PRIMARY KEY (persona_id, version),
|
||||
FOREIGN KEY (persona_id, version)
|
||||
REFERENCES app.persona_card(persona_id, version) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 상담사 AI 페르소나 (데모/self-play용, 이론 충실도 일관)
|
||||
CREATE TABLE IF NOT EXISTS app.counselor_profile (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code TEXT,
|
||||
theory TEXT CHECK (theory IN ('humanistic','cbt','integrative')),
|
||||
skill_level TEXT,
|
||||
reasoning_chain JSONB, -- CoE: PCT/CBT 추론체인
|
||||
allowed_microskills TEXT[],
|
||||
speech_style JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. 라벨 코드테이블 (MASTERPLAN §3.2, taxonomy 3축 분리)
|
||||
-- 버전드 코드테이블 + 발화 다대다. KB taxonomy 청크가 label_id 로 참조.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.stage_def (
|
||||
stage_code TEXT PRIMARY KEY, -- '라포','탐색','개입','정리'
|
||||
display_name TEXT NOT NULL,
|
||||
seq INT NOT NULL,
|
||||
base_openness REAL NOT NULL DEFAULT 0.15
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app.technique_label_def (
|
||||
label_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
code TEXT NOT NULL, -- '공감','반영','탐색','홀딩'...
|
||||
display_name TEXT NOT NULL,
|
||||
category TEXT, -- 마이크로스킬 분류
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
UNIQUE (code, version)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app.client_state_def (
|
||||
label_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
code TEXT NOT NULL, -- '비자발','방어','자살사고인정'...
|
||||
display_name TEXT NOT NULL,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
UNIQUE (code, version)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app.scale_def (
|
||||
scale_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE, -- 'empathy','open_q','CTRS'...
|
||||
min_score REAL NOT NULL DEFAULT 0,
|
||||
max_score REAL NOT NULL DEFAULT 5, -- CTRS 는 0-6
|
||||
description TEXT
|
||||
);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. 세션 (MASTERPLAN §3.2) — 케이스/회기 앵커
|
||||
-- case_profile 보다 먼저 정의(FK 의존: session_state → sessions).
|
||||
-- case_id 는 (persona_id, learner_id) 복합 인스턴스 식별(설계서 §3.5 핵심결정).
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
case_id UUID, -- 다회기 연속체(FK는 04에서 보강)
|
||||
learner_id UUID NOT NULL REFERENCES app.app_user(user_id),
|
||||
persona_id UUID, -- 시드 카드 persona_id
|
||||
persona_version INT, -- 핀된 카드 버전
|
||||
session_no INT, -- 케이스 내 N번째 회기
|
||||
theory_mode TEXT, -- 'humanistic'|'cbt'|'integrative'
|
||||
stage_path JSONB NOT NULL DEFAULT '[]', -- 단계 전이 이력
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
ended_at TIMESTAMPTZ,
|
||||
FOREIGN KEY (persona_id, persona_version)
|
||||
REFERENCES app.persona_card(persona_id, version)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_case ON app.sessions(case_id, session_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_learner ON app.sessions(learner_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. 발화 (MASTERPLAN §3.2 + 설계서 §3.3 연속성 컬럼) — ② EPISODIC 원자단위
|
||||
-- append-only. PII 마스킹 후 적재. visible_to 는 표면이라 기본 전체 가시.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.turns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE CASCADE,
|
||||
seq INT NOT NULL,
|
||||
speaker TEXT NOT NULL CHECK (speaker IN ('counselor','client')),
|
||||
stage TEXT REFERENCES app.stage_def(stage_code),
|
||||
text TEXT, -- 저장 전 Presidio PII 마스킹된 원문
|
||||
text_masked TEXT, -- 외부 LLM 전송용(추가 마스킹)
|
||||
actor_kind TEXT, -- 'client_ai'|'counselor_ai'|'evaluator_ai'|'human_learner'|'human_supervisor'
|
||||
llm_provider TEXT, model TEXT,
|
||||
tokens_in INT, tokens_out INT, cost_usd NUMERIC(12,6),
|
||||
visible_to TEXT[] NOT NULL DEFAULT '{client,counselor,evaluator}',
|
||||
-- 설계서 §3.3 연속성 컬럼
|
||||
salience REAL NOT NULL DEFAULT 0.0, -- 망각/회상 우선순위
|
||||
is_pinned BOOLEAN NOT NULL DEFAULT FALSE, -- pinned_fact 승격 발화
|
||||
contradicts UUID[] NOT NULL DEFAULT '{}', -- 뒤집은 과거 turn_id[]
|
||||
-- 음성 paralinguistic (MASTERPLAN_REVISIONS §3.5 선반영, nullable)
|
||||
audio_ref TEXT,
|
||||
silence_ms INT,
|
||||
speech_rate REAL,
|
||||
barge_in BOOLEAN,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (session_id, seq)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_turns_session ON app.turns(session_id, seq);
|
||||
CREATE INDEX IF NOT EXISTS idx_turns_salient
|
||||
ON app.turns(session_id) WHERE salience >= 0.5;
|
||||
|
||||
-- 발화 라벨 다대다 (복수 라벨)
|
||||
CREATE TABLE IF NOT EXISTS app.turn_technique (
|
||||
turn_id UUID NOT NULL REFERENCES app.turns(id) ON DELETE CASCADE,
|
||||
label_id BIGINT NOT NULL REFERENCES app.technique_label_def(label_id),
|
||||
PRIMARY KEY (turn_id, label_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS app.turn_client_state (
|
||||
turn_id UUID NOT NULL REFERENCES app.turns(id) ON DELETE CASCADE,
|
||||
label_id BIGINT NOT NULL REFERENCES app.client_state_def(label_id),
|
||||
PRIMARY KEY (turn_id, label_id)
|
||||
);
|
||||
|
||||
-- 슈퍼바이저 논평 (MASTERPLAN §3.2, 윤찬 "의도와 다른 부분" 1급 시민)
|
||||
CREATE TABLE IF NOT EXISTS app.supervisor_comment (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
turn_id UUID NOT NULL REFERENCES app.turns(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('rationale','critique')),
|
||||
text TEXT NOT NULL,
|
||||
intent_deviation JSONB, -- {expected,actual,severity,dimension}
|
||||
visible_to TEXT[] NOT NULL DEFAULT '{evaluator}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sup_comment_turn ON app.supervisor_comment(turn_id);
|
||||
|
||||
-- 안전 이벤트 (MASTERPLAN §3.2)
|
||||
CREATE TABLE IF NOT EXISTS app.safety_events (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
session_id UUID REFERENCES app.sessions(id) ON DELETE CASCADE,
|
||||
turn_id UUID REFERENCES app.turns(id) ON DELETE SET NULL,
|
||||
trigger_type TEXT NOT NULL, -- 'suicide_means'|'real_crisis'|'ideation_cap'...
|
||||
ko_risk_level SMALLINT, -- 한국어 자살분류 1~5
|
||||
escalated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
latency_ms INT,
|
||||
detail JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_safety_session ON app.safety_events(session_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 5. 메모리 ① WORKING (설계서 §3.2) — 체크포인트
|
||||
-- 상태머신 결정론 수치 + 단기버퍼. 매 턴 UPSERT(프로세스 죽어도 복원).
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.session_state (
|
||||
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
|
||||
stage TEXT NOT NULL DEFAULT '라포'
|
||||
CHECK (stage IN ('라포','탐색','개입','정리')),
|
||||
turn_seq INT NOT NULL DEFAULT 0,
|
||||
effective_openness REAL NOT NULL DEFAULT 0.15
|
||||
CHECK (effective_openness BETWEEN 0 AND 1),
|
||||
rapport_credit REAL NOT NULL DEFAULT 0.0, -- 회기말 0.7 이월 대상
|
||||
resistance REAL NOT NULL DEFAULT 0.65
|
||||
CHECK (resistance BETWEEN 0 AND 1),
|
||||
ideation_stage SMALLINT NOT NULL DEFAULT 1
|
||||
CHECK (ideation_stage BETWEEN 1 AND 5),
|
||||
affect_state JSONB NOT NULL DEFAULT '{}',
|
||||
short_buffer JSONB NOT NULL DEFAULT '[]', -- 최근 K턴 rolling window
|
||||
recall_context JSONB, -- 회기시작 1회 로드 캐시
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 6. 메모리 ② EPISODIC 임베딩 (설계서 §3.3) — BGE-M3 멀티벡터 + HNSW
|
||||
-- case_id/session_id 비정규화: 케이스 스코프 회상(cross-trainee 누수 차단).
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.turn_embedding (
|
||||
turn_id UUID PRIMARY KEY REFERENCES app.turns(id) ON DELETE CASCADE,
|
||||
case_id UUID NOT NULL, -- denormalized 회상 스코프
|
||||
session_id UUID NOT NULL,
|
||||
seq INT NOT NULL,
|
||||
dense vector(1024) NOT NULL, -- BGE-M3 dense
|
||||
sparse JSONB, -- BGE-M3 sparse {token_id:weight}
|
||||
context_prefix TEXT, -- Contextual Retrieval 1문장
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_turn_emb_hnsw ON app.turn_embedding
|
||||
USING hnsw (dense vector_cosine_ops) WITH (m = 16, ef_construction = 64);
|
||||
CREATE INDEX IF NOT EXISTS idx_turn_emb_case ON app.turn_embedding (case_id, session_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 7. 메모리 ③ SUMMARY (설계서 §3.4) — 회기당 1행
|
||||
-- (A) end_state 무손실 carry-over(코드복사) / (B) digest narrative(LLM) 분리.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.session_summary (
|
||||
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
|
||||
case_id UUID NOT NULL,
|
||||
session_no INT NOT NULL,
|
||||
-- (A) 무손실 carry-over [P2]
|
||||
end_state JSONB NOT NULL, -- {stage,openness,rapport_credit,...}
|
||||
rapport_delta REAL,
|
||||
-- (B) narrative 압축 (LLM, 손실 허용)
|
||||
digest TEXT NOT NULL, -- 6~10문장, 다음 회기 주입
|
||||
open_threads JSONB NOT NULL DEFAULT '[]', -- [{topic,last_stance,raised_session_no}]
|
||||
homework JSONB,
|
||||
emotional_arc TEXT,
|
||||
-- (C) 검색용
|
||||
digest_embedding vector(1024),
|
||||
token_count INT,
|
||||
compressed_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (case_id, session_no)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sess_sum_case
|
||||
ON app.session_summary (case_id, session_no DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sess_sum_emb ON app.session_summary
|
||||
USING hnsw (digest_embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 8. 메모리 ④ SEMANTIC (설계서 §3.5) — case_profile(evolving) + pinned_fact
|
||||
-- 케이스 = (persona_id 템플릿) × (learner_id 인스턴스). 학습자별 독립 연속체.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS app.case_profile (
|
||||
case_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
persona_id UUID NOT NULL, -- 시드 페르소나(카드 persona_id)
|
||||
learner_id UUID NOT NULL REFERENCES app.app_user(user_id),
|
||||
last_session_no INT NOT NULL DEFAULT 0,
|
||||
ccd_estimate JSONB NOT NULL DEFAULT '{}', -- CCD 8요소 현재추정 +confidence
|
||||
presenting_arc JSONB NOT NULL DEFAULT '[]', -- 호소문제 궤적
|
||||
rapport_trajectory JSONB NOT NULL DEFAULT '[]', -- [{session_no,end_rapport,end_openness}]
|
||||
alliance_level REAL DEFAULT 0.2, -- 치료동맹 누적(EWMA)
|
||||
case_digest TEXT NOT NULL DEFAULT '', -- 전체 궤적 8~12문장(큰그림)
|
||||
digest_embedding vector(1024),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (persona_id, learner_id) -- 핵심 복합키(§3.5)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_case_learner ON app.case_profile(learner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_case_emb ON app.case_profile
|
||||
USING hnsw (digest_embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
|
||||
|
||||
-- pinned facts (람다 MEMORY.md, 절대 흘리면 안 되는 핵심, 모순 기준점)
|
||||
CREATE TABLE IF NOT EXISTS app.pinned_fact (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
case_id UUID NOT NULL REFERENCES app.case_profile(case_id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL, -- 'name','family',...
|
||||
value TEXT NOT NULL, -- 가명처리/마스킹된
|
||||
fact_type TEXT NOT NULL
|
||||
CHECK (fact_type IN ('identity','history','relationship','clinical','agreement')),
|
||||
status TEXT NOT NULL DEFAULT 'stable'
|
||||
CHECK (status IN ('stable','evolving','contradicted','locked')),
|
||||
source_turn UUID REFERENCES app.turns(id) ON DELETE SET NULL,
|
||||
confidence REAL NOT NULL DEFAULT 1.0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
updated_session_no INT,
|
||||
visible_to TEXT[] NOT NULL DEFAULT '{client,evaluator}', -- 모순처리 정답신호
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (case_id, key)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pinned_case ON app.pinned_fact (case_id, status);
|
||||
|
||||
-- pinned 변경 이력 (append-only, 모순/갱신 audit + 평가AI 정답신호)
|
||||
CREATE TABLE IF NOT EXISTS app.pinned_fact_history (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
fact_id UUID NOT NULL, -- pinned_fact 삭제돼도 이력 보존(FK 무)
|
||||
case_id UUID NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
reason TEXT, -- 'contradiction'|'clarification'|'progression'
|
||||
session_no INT,
|
||||
turn_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pinned_hist_fact ON app.pinned_fact_history(fact_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 9. 지연 FK 보강 — sessions.case_id, summary/embedding case 정합
|
||||
-- case_profile 가 sessions 보다 뒤에 정의되므로 여기서 FK 연결.
|
||||
-- =============================================================================
|
||||
ALTER TABLE app.sessions
|
||||
DROP CONSTRAINT IF EXISTS fk_sessions_case;
|
||||
ALTER TABLE app.sessions
|
||||
ADD CONSTRAINT fk_sessions_case
|
||||
FOREIGN KEY (case_id) REFERENCES app.case_profile(case_id) ON DELETE SET NULL;
|
||||
Loading…
Add table
Add a link
Reference in a new issue