o2o-negosium-original/agent/tools/reset_learning.py
hbyang b368a19f79 [feat] agent: 카드번호 기반 Q-table 마이그레이션 + 학습 리셋 스크립트
카탈로그 카드 추가/삭제/재정렬 시 action_id 가 밀려도 학습이 카드를 따라가도록,
Q-table 버전에 카탈로그 스냅샷(카드번호)을 저장하고 카드번호로 리맵한다.

- q_table_versions.action_cards JSONB 신설(카드번호 목록, index=action_id).
  models.py + init.sql(DDL·ALTER) + 로컬 DB ALTER.
- 버전 생성(get_or_create/warm_start/migrate)이 action_cards 저장.
- migrate_active_version_dim: 옛 action_cards ↔ 새 카탈로그를 카드번호로 리맵
  (중간 삽입/삭제 보정, 사라진 카드 버림, 새 카드 fresh). 레거시(스냅샷 없음)는 위치 폴백.
  version_name 은 vid 접미로 유니크.
- model_store.load: card_list 계산 → 차원변경 OR 동일차원 내용변경 시 마이그레이션,
  레거시 버전 action_cards backfill(set_version_action_cards).
- tools/reset_learning.py: learning 스키마만 비우는 리셋(카드·협상 데이터 보존),
  --company/--yes 옵션. 카탈로그 바꾸고 학습 처음부터 할 때 사용.
- 테스트: 중간 카드 삭제 시 카드번호 리맵으로 학습 보존 검증. agent 100/100.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:19:55 +09:00

66 lines
2.6 KiB
Python

"""로컬 학습 리셋 — learning 스키마를 비운다 (카드 카탈로그·협상 데이터는 보존).
카탈로그(카드) 구성을 바꾼 뒤 Q-table 을 처음부터 다시 학습시키고 싶을 때 사용한다.
learning.* (버전/Q값/방문수/경험로그/세션/카드매핑) 만 삭제 → 다음 협상부터 fresh 재학습.
card.*·negotiation.*·quotation.* 등 실제 데이터는 건드리지 않는다.
사용 (agent 디렉토리에서):
APP_ENV=local python -m tools.reset_learning # 전체 회사 학습 리셋
APP_ENV=local python -m tools.reset_learning --company <id> # 특정 회사만
APP_ENV=local python -m tools.reset_learning --yes # 확인 프롬프트 생략
"""
import argparse
import asyncio
import asyncpg
from config.server_configs import main_db_config
# 삭제 대상(learning 스키마). 전부 company_id 스코프라 --company 로 회사별 리셋 가능.
_LEARNING_TABLES = [
"q_values", "visit_counts", "experience_logs", "chat_sessions",
"tenant_action_cards", "q_table_versions",
]
async def _reset(company_id: str | None) -> None:
cfg = main_db_config
conn = await asyncpg.connect(
host=cfg.write_host, port=cfg.write_port,
user=cfg.write_id, password=cfg.write_pw, database=cfg.name,
)
try:
total = 0
for t in _LEARNING_TABLES:
if company_id:
res = await conn.execute(f"DELETE FROM learning.{t} WHERE company_id = $1", company_id)
else:
res = await conn.execute(f"DELETE FROM learning.{t}")
n = int(res.split()[-1]) if res else 0
total += n
print(f" learning.{t}: {n} 행 삭제")
scope = f"회사 {company_id}" if company_id else "전체 회사"
print(f"== 학습 리셋 완료: 총 {total} 행 삭제 (scope={scope}) ==")
finally:
await conn.close()
def main() -> None:
p = argparse.ArgumentParser(description="learning 스키마 리셋 (카드/협상 데이터는 보존)")
p.add_argument("--company", help="특정 company_id 만 리셋. 생략 시 전체")
p.add_argument("--yes", action="store_true", help="확인 프롬프트 생략")
args = p.parse_args()
scope = f"회사 {args.company}" if args.company else "전체 회사"
if not args.yes:
ans = input(f"[{main_db_config.name}] learning 스키마({scope})를 비웁니다. 계속? [y/N] ")
if ans.strip().lower() != "y":
print("취소됨.")
return
asyncio.run(_reset(args.company))
if __name__ == "__main__":
main()