카드 카탈로그(negodata)가 Q-table action space 를 정의하는 정본이 되고, 카드 변경이
config 수정·학습 손실 없이 agent 에 자동 반영되는 고리를 완성.
- action space 정리: 카탈로그 전체(NGC-001~011, 11장) 고정, 견적별 선택은 축소가 아니라
available_mask(_selection_mask) 로 처리 — action_id↔카드 대응을 견적마다 일정하게 유지해
Q-table 학습 일관성 보장. 구 인덱스 방식(selected[action_id]) 폐기.
- ① 카탈로그 DB 정본화: action_mapping.type=db 면 registry 가 card.nego_cards(user_id NULL,
number 순) 조회로 action_to_card 동적 구성(파일은 폴백). port/adapter(card_catalog_*).
_base=type:db. → negodata 카드 추가/삭제 시 config 수정 불필요.
- ② 차원 변경 학습 보존 마이그레이션: migrate_active_version_dim — 겹치는 셀 복사
(append/truncate 안전) + 새 카드 fresh. model_store.load 가 차원 불일치 시 호출.
- ③ reload 엔드포인트: /v1/catalog-refresh(테넌트) · /v1/catalog-refresh-all(전역, 화이트리스트).
- ④ 브랜드: company_profile_repo — 자동 온보딩 고객사(company_id UUID)는
company.companies.name 으로 {company_name} 채움. 데모 테넌트는 파일 유지.
- 크로스서비스: negodata card_service 가 공용 nego 카드 변경 시 agent_notify 로 전역 리로드 알림
(best-effort, is_test skip). config 에 agent_base_url.
- 하니스 episodes 400→600(action 11 수렴). 테스트 갱신·추가로 agent 98/98.
알려진 갭(후속): per-company 카탈로그 스코프(회사 카드도 action space 포함), 카탈로그 중간
삭제 시 카드번호 기반 마이그레이션.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
4.0 KiB
Python
80 lines
4.0 KiB
Python
"""QTablePolicyStore — learning 스키마에서 테넌트 UCBQTablePolicy 를 로드/영속화 (H1).
|
|
|
|
요청마다 활성 버전의 q_values/visit_counts 를 numpy QTable 로 적재해 정책을 조립한다(상태는 DB 가
|
|
단일 소스). 갱신은 touched 셀만 write-through → 재시작/요청 간 학습이 보존된다.
|
|
|
|
PoC 단순화: 온라인 single-step 갱신(다음상태 부트스트랩은 outcome 종료 시 생략). 시퀀스 보상링크는
|
|
P5/H 트랙에서 transition_id 기반으로 정교화한다.
|
|
"""
|
|
|
|
import math
|
|
from typing import Tuple
|
|
|
|
from common.logger import LOG
|
|
from negotiation.policies.qtable_policy import UCBQTablePolicy
|
|
from negotiation.qtable.domain.model.q_table import QTable
|
|
from negotiation.qtable.infra.repository.learning_repository import LearningRepository
|
|
from tenancy.registry import TenantEngine
|
|
|
|
|
|
class QTablePolicyStore:
|
|
@staticmethod
|
|
async def load(engine: TenantEngine) -> Tuple[UCBQTablePolicy, object, LearningRepository]:
|
|
repo = LearningRepository(engine.company_id)
|
|
S = engine.state_space_size
|
|
A = engine.action_space_size
|
|
pol_cfg = engine.config.policy
|
|
lr = pol_cfg.learning_rate
|
|
gamma = pol_cfg.gamma
|
|
|
|
# cold-start 3단 (계획서 D):
|
|
# ① 활성 버전 있으면 그대로 ② 없고 inherits_base 면 base warm-start 복제(차원 호환 시)
|
|
# ③ 차원 불일치/base 없음 → 휴리스틱 빈 버전
|
|
err, active = await repo.read(lambda s: repo.get_active_version(s))
|
|
if active is not None:
|
|
if active.action_space_size != A or active.state_space_size != S:
|
|
# 카탈로그 카드 수(또는 상태 차원) 변경 → 학습 보존 마이그레이션.
|
|
# 겹치는 셀 복사(append/truncate 안전) + 새 카드 fresh. 실패 시 기존 버전 유지(load 가 reshape).
|
|
migrated = await repo.migrate_active_version_dim(
|
|
old_version=active, state_space_size=S, action_space_size=A,
|
|
learning_rate=lr, discount_factor=gamma, version_name=f"v_migrated_a{A}")
|
|
if migrated is not None:
|
|
LOG.i(f"[QTablePolicyStore] 차원 변경 마이그레이션 company={engine.company_id} "
|
|
f"{active.action_space_size}→{A} action (학습 보존)")
|
|
version_id = migrated or active.version_id
|
|
else:
|
|
version_id = active.version_id
|
|
else:
|
|
version_id = None
|
|
if engine.config.inherits_base:
|
|
version_id = await repo.warm_start_from_base(
|
|
state_space_size=S, action_space_size=A, learning_rate=lr, discount_factor=gamma)
|
|
if version_id is None:
|
|
version_id = await repo.get_or_create_active_version(
|
|
state_space_size=S, action_space_size=A, learning_rate=lr, discount_factor=gamma)
|
|
|
|
qtable = QTable(S, A, learning_rate=lr, discount_factor=gamma)
|
|
qrows, vrows = await repo.load_cells(version_id)
|
|
for st, a, q in qrows:
|
|
if 0 <= st < S and 0 <= a < A:
|
|
qtable.q[st, a] = q
|
|
for st, a, c in vrows:
|
|
if 0 <= st < S and 0 <= a < A:
|
|
qtable.visits[st, a] = c
|
|
|
|
params = pol_cfg.params or {}
|
|
policy = UCBQTablePolicy(
|
|
qtable,
|
|
exploration_constant=params.get("exploration_constant", math.sqrt(2.0)),
|
|
epsilon=params.get("propensity_epsilon", 0.1),
|
|
)
|
|
return policy, version_id, repo
|
|
|
|
@staticmethod
|
|
async def persist_cell(repo: LearningRepository, version_id, policy: UCBQTablePolicy,
|
|
state_index: int, action_id: int):
|
|
"""(state, action) 셀 write-through (select 의 visit 증가 + update 의 Q 변화 반영)."""
|
|
q_value = float(policy.qtable.q[state_index, action_id])
|
|
count = int(policy.qtable.visits[state_index, action_id])
|
|
await repo.upsert_cell(version_id, state_index, action_id, q_value, count)
|