-- ============================================================ -- 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);