o2o-negosium-original/postgres-init/02-learning-schema.sql
Mina Choi bfdc0c3047 [fix] db: 04-alter close_reason 계열 ALTER 보정 + 스키마 파일 날짜 rename
- 04-alter 에 quotations.close_reason · quotation_settings.mid_action/over_action/regen_limit ALTER 누락분 append. 기존 DB 마이그레이션이 close_reason 개편을 안 담고 있어 UndefinedColumn 나던 것 보정(멱등 IF NOT EXISTS).
- postgres-init/01-schema.sql·04-alter.sql → *_20260702.sql 로 rename(스키마 최종 변경일 = 파일명). 사람들이 DB 최신 여부를 파일명으로 판단.
- 주석·문서 참조(README·docker-compose·02-learning·agent models·anchoring doc)는 글롭 01-schema*.sql·04-alter*.sql 로 바꿔 다음 rename 때 수정 불필요.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:41:52 +09:00

142 lines
8.4 KiB
SQL

-- ============================================================
-- learning : 협상 에이전트(agent) RL 학습 자산 (Q-Table / 경험로그)
-- ============================================================
-- negosium_db 안의 7번째 schema. agent 서비스가 소유한다(backend 는 미사용).
-- 01-schema*.sql 과 동일 컨벤션: FK 미사용(앱 레이어 무결성), TIMESTAMPTZ(UTC), 코드값 SMALLINT(1부터).
--
-- 멀티테넌트 논리 격리: 모든 테이블에 company_id 컬럼. company.companies.company_id(uuid)를
-- 문자열로 보관하되, 공유 베이스 정책은 예약어 '_base' 를 쓴다(uuid/sentinel 혼용 → VARCHAR).
-- 모든 유니크/인덱스는 company_id 선두 복합으로 둔다(테넌트 간 충돌 방지 + 스코프 조회).
\connect negosium_db
CREATE SCHEMA IF NOT EXISTS learning;
-- ------------------------------------------------------------
-- Q-Table 버전 (학습 스냅샷의 헤더)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS learning.q_table_versions (
version_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
company_id VARCHAR(64) NOT NULL, -- 테넌트 키(company uuid 문자열 또는 '_base')
version_name VARCHAR(50) NOT NULL, -- 버전명 (예: v000_warmstart_from_base)
scope SMALLINT NOT NULL DEFAULT 2, -- 1=base, 2=tenant
base_version_id uuid NULL, -- warm-start 출처 추적(베이스 버전)
state_space_size INTEGER NOT NULL, -- 차원 정합성 체크용
action_space_size INTEGER NOT NULL,
learning_rate NUMERIC(6,4) NOT NULL DEFAULT 0.1000,
discount_factor NUMERIC(6,4) NOT NULL DEFAULT 0.9500,
epochs INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT FALSE, -- 활성 버전 포인터(테넌트당 1개)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
-- version_name 은 테넌트 스코프에서만 유니크 (계획서 C: UniqueConstraint(tenant, version_name))
CREATE UNIQUE INDEX IF NOT EXISTS uq_qtv_company_version
ON learning.q_table_versions (company_id, version_name);
-- 테넌트별 활성 버전은 최대 1개 (부분 유니크)
CREATE UNIQUE INDEX IF NOT EXISTS uq_qtv_company_active
ON learning.q_table_versions (company_id) WHERE is_active AND NOT deleted;
-- ------------------------------------------------------------
-- Q 값 (state_index, action_id) -> q_value
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS learning.q_values (
id BIGSERIAL PRIMARY KEY,
company_id VARCHAR(64) NOT NULL,
version_id uuid NOT NULL,
state_index INTEGER NOT NULL,
action_id INTEGER NOT NULL,
q_value DOUBLE PRECISION NOT NULL DEFAULT 0.0
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_qval_company_version_sa
ON learning.q_values (company_id, version_id, state_index, action_id);
CREATE INDEX IF NOT EXISTS idx_qval_company_version_state
ON learning.q_values (company_id, version_id, state_index);
-- ------------------------------------------------------------
-- 방문 횟수 (UCB 탐색용)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS learning.visit_counts (
id BIGSERIAL PRIMARY KEY,
company_id VARCHAR(64) NOT NULL,
version_id uuid NOT NULL,
state_index INTEGER NOT NULL,
action_id INTEGER NOT NULL,
count BIGINT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_visit_company_version_sa
ON learning.visit_counts (company_id, version_id, state_index, action_id);
CREATE INDEX IF NOT EXISTS idx_visit_company_version_state
ON learning.visit_counts (company_id, version_id, state_index);
-- ------------------------------------------------------------
-- 경험 로그 (transition). OPE/오프라인RL 의 데이터 소스.
-- propensity / turn / available_actions / settled_price 는 신규 로깅(소급 불가, 계획서 H0).
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS learning.experience_logs (
id BIGSERIAL PRIMARY KEY,
company_id VARCHAR(64) NOT NULL,
transition_id uuid NOT NULL DEFAULT gen_random_uuid(),
session_id uuid NULL, -- negotiation.sessions.session_id 연결
state_index INTEGER NOT NULL,
action_id INTEGER NOT NULL,
card_id VARCHAR(40) NULL, -- 사용된 카드(테넌트 카탈로그)
q_value_at_selection DOUBLE PRECISION NULL,
reward DOUBLE PRECISION NULL, -- 보상 산출 후 update
next_state_index INTEGER NULL,
done BOOLEAN NOT NULL DEFAULT FALSE,
snapshot JSONB NULL, -- NegotiationSnapshot 전체(연속 feature)
propensity DOUBLE PRECISION NULL, -- 행동정책 선택확률 (OPE 필수)
turn INTEGER NULL, -- 협상 라운드(iteration)
available_actions JSONB NULL, -- 선택 시점 가용 액션(마스킹)
settled_price BIGINT NULL, -- 타결가(원)
visit_count_at_selection BIGINT NULL,
total_visits_at_selection BIGINT NULL,
ucb_score_at_selection DOUBLE PRECISION NULL,
is_new_quote BOOLEAN NOT NULL DEFAULT FALSE, -- 학습 격리(신규견적은 UCB 비활성)
is_invalidated BOOLEAN NOT NULL DEFAULT FALSE,
invalidated_reason VARCHAR(255) NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_exp_company_transition
ON learning.experience_logs (company_id, transition_id);
CREATE INDEX IF NOT EXISTS idx_exp_company_session
ON learning.experience_logs (company_id, session_id);
CREATE INDEX IF NOT EXISTS idx_exp_company_state_action
ON learning.experience_logs (company_id, state_index, action_id);
-- ------------------------------------------------------------
-- 테넌트별 action_id -> card 매핑 (계획서 C: tenant_action_cards)
-- PoC 는 카드 매핑 고정. P6 에서 동기화 소스로 사용.
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS learning.tenant_action_cards (
id BIGSERIAL PRIMARY KEY,
company_id VARCHAR(64) NOT NULL,
action_id INTEGER NOT NULL,
card_id VARCHAR(40) NOT NULL, -- card.nego_cards.number 등 테넌트 카탈로그 식별자
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_tac_company_action
ON learning.tenant_action_cards (company_id, action_id) WHERE NOT deleted;
-- ------------------------------------------------------------
-- 대화 세션 상태 (P8-A: /chat 진행 상태 영속화 — 재시작/멀티워커 안전)
-- 채팅 '로그'(메시지)가 아니라 진행 '상태'(현재 step·맥락·사용카드·라운드)다.
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS learning.chat_sessions (
session_id uuid PRIMARY KEY,
company_id VARCHAR(64) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
rq_type VARCHAR(10) NOT NULL DEFAULT '재협상',
step VARCHAR(40) NOT NULL DEFAULT '시작', -- 현재 대기 중인 step
context JSONB NOT NULL DEFAULT '{}'::jsonb, -- 앵커/목표가·라운드·last_state 등
used_action_ids JSONB NOT NULL DEFAULT '[]'::jsonb, -- 사용한 카드(중복방지/소진 판정)
action_space_size INTEGER NOT NULL DEFAULT 0,
ended BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_chat_sessions_company ON learning.chat_sessions (company_id);