o2o-negosium-original/agent/README.md
hbyang 1682481b01 [feat] agent: 협상 고도화 — LLM 표현/이해층 + 카드 전술 실행계층 + ktcommerce 정리
카드 재설계("멘트 카드 → 전술 카드"):
- tactics.py 신설: 카드번호→전술(카운터 산식) 레지스트리, min(counter,target) 클램프
- 카운터 수락=즉시 타결(pending_counter_price 일반화, 구 offer_1pct 흡수)
- 목표가 초과 타결 금지(성공스텝 진입 가드) + "카드 소진=실패" 폐지→종결 국면
- 선택형 와일드카드(WC-*) 발동 + card.wild_cards 멘트 DB 어댑터

LLM 계층:
- Phase 2 표현층 ScriptNaturalizer(카드 멘트 자연화, 마커·치환자·숫자 보존 검증)
- Phase 3 이해층 InputInterpreter(자유발화 NLU→기대입력, 한국어 가격 파서)
- OPENAI_API_KEY env override(server_configs) + 전역 자격증명 게이트

결정 스택(Phase 1):
- 협상 규칙 데이터화(negotiation.wildcard_*_ratio/max_counter_rounds)
- 선택카드 우선순위 prior(UCB 방문수 감쇠, Q-table 오염 없음)

버그픽스:
- 인하율 음수 표기 제거 + 인상/동일/인하 구분(discount_phrase)
- 자연화 강조마커 보존(볼드/색 소실 시 원본 폴백)
- 카드 시드 가격변수(prev_partner_price·target_mid_price·middle_price 등) 치환

정리:
- ktcommerce 테넌트 삭제 + 테스트 21파일 imarketkorea/_base 로 마이그레이션
- 실 LLM 호출 차단 conftest 가드

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:37:36 +09:00

193 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Negosium Agent
범용 멀티테넌트 협상 솔루션 PoC. 여러 회사(imarketkorea 등)가 **각자 데이터로 분기 학습**하는
협상 카드 선택 에이전트. Q-Learning(UCB) 기반 `Chat_server`(단일 테넌트)를 참고해 신규 구축한다.
PoC 목표 두 가지:
1. **학습 알고리즘이 실제 협상 성과를 개선하는지 정량 검증** (Q-Table vs LinUCB vs Offline RL 비교 하네스).
2. **멀티테넌트 분기 학습 + 기존 14개 API/step 체계 무손실 보존**.
## o2o-negosium 서버군 내 위치
`agent` 는 서버군의 한 서비스다. **backend 와 같은 컨벤션·같은 `negosium_db`** 를 공유한다.
| 서비스 | 포트 | DB | 역할 |
|---|---|---|---|
| backend | 9300 | negosium_db | 회사/공급사/견적/카드 도메인 + 인증 |
| negodata/backend | 9400 | negodata_db | 데이터 도구 |
| **agent** | **9500** | **negosium_db (learning 스키마 + 기존 스키마 읽기/쓰기)** | **협상 챗봇·학습** |
### 통합 토폴로지 (확정: 하이브리드)
- agent 는 **독립 FastAPI 서비스**(자체 포트)로 뜨되, backend 의 구조적 컨벤션을 그대로 채택한다:
- **MVC**: `router/`(컨트롤러) → `services/`(로직, 추가 예정) → `crud/`(쿼리, 추가 예정).
- **람다 DB**: `common/database/db_session_manager.py` — `execute_lambda` / `execute_lambda_run`, Read/Write 엔진 분리.
- **Protocol**: `common/models/gmodel.py` — `WebPacketProtocol`/`Req_*`/`Res_*`/`result: ErrorInfo`.
- **config**: `config/config.<APP_ENV>.toml` (`APP_ENV` 로 선택).
- 협상 엔진 내부는 **헥사고날 구조**(`negotiation/` 패키지)로 유지한다 — 도메인은 tenant-agnostic, config 주입.
- backend 가 `/chat` 을 agent 로 위임하거나, 프런트가 agent 를 직접 호출(인증 토큰은 backend 발급분 검증).
**장단점**
- (+) 무거운 RL 의존성(torch/d3rlpy/obp)이 인증 backend 를 오염시키지 않음 — 격리 + 독립 스케일.
- (+) backend 컨벤션·`negosium_db` 단일 소스 재사용 → 학습 곡선·중복 제거.
- (+) PoC 알고리즘 실험의 잦은 재학습이 운영 backend 배포에 묶이지 않음.
- (−) `common/` 을 backend 와 별도 유지(현재는 복제) → 추후 공유 라이브러리화 검토.
- (−) 서비스 간 Protocol 버전 정합 관리 필요.
### 테넌트 = company_id
계획서의 문자열 `tenant_id` 는 이 레포에서 **`company.companies.company_id`(uuid)** 로 매핑한다(이미 1급 시민).
공유 베이스 정책은 예약 식별자(`_base`)로 표현한다. 모든 학습 테이블은 `company_id` 로 논리 격리한다.
### RL 학습 자산 저장 위치 = learning 스키마
Q-Table·q_values·visit_counts·experience_logs(propensity/turn 포함)는 `negosium_db` 안의
**신설 `learning` 스키마**(7번째)에 둔다. 기존 `company`/`card`/`negotiation`/`quotation` 스키마는 재사용한다.
### 저작권 분리 (클린룸) — `CLEANROOM.md`
플랫폼은 특정 고객사 독점 자산과 분리해 독립 설계한다. 참고 엔진(`Chat_server`)은 **기능 구조(아이디어·방법)** 이해용으로만 열람하고, **독점 표현물(협상 스크립트·고유 카드 코드·verbatim 라벨·튜닝값)은 반입하지 않는다.** 레포의 테넌트 프로파일은 **합성/중립 데모값**이며, 실제 운영값은 테넌트 비공개 소스(YAML/DB)에서 런타임 주입한다. 카드 코드는 우리 중립 스킴(`NGC-*`). 상세는 [CLEANROOM.md](CLEANROOM.md).
## 디렉토리 구조
```
agent/
├── web_main.py # 엔트리포인트 (uvicorn, 포트 9500)
├── config/ # APP_ENV 별 toml (backend 컨벤션)
├── common/ # logger·enums·singleton·gmodel·gtime·db_session_manager (backend 이식)
├── router/ # FastAPI app + 미들웨어 + v1 라우터
│ ├── router.py # app + lifespan + TenantMiddleware
│ ├── middleware/tenant_middleware.py # X-Tenant-ID → request.state.tenant_id (P4 완성)
│ └── v1/health/health.py
├── negotiation/ # 협상 엔진 (Chat_server 이식, 헥사고날)
│ ├── chat/service/ # chat_engine·step 핸들러 (P7/P8)
│ ├── orchestrator/ # negotiation_orchestrator (P4 팩토리화)
│ ├── policy/ policies/ # UCBPolicy + 상위 NegotiationPolicy 추상 (H0)
│ ├── qtable/ # state·q_table·reward·usecase (P2 config 주입)
│ ├── cards/ # CardSourcePort + card.* 어댑터 (P6)
│ └── profiling/ # ← 구 N-profiling 개명 (동적 import 해킹 제거) ✅ P0 완료
├── tenancy/ # TenantConfig·로더·레지스트리 (P1/P4)
├── eval_harness/ # 알고리즘 비교 하네스 (PoC 본체, H1~H7)
├── tools/ # init_base·train·export 스크립트
└── tests/
```
## 실행 / 테스트
```bash
cd agent
pip install -r requirements.txt
pip install pytest pytest-asyncio httpx # 테스트 도구
# config 는 config.local.toml 하나만 사용(.gitignore — DB 비번·OpenAI 키 포함).
# 없으면 직접 생성(DB·OpenAI 값 입력). test/docker 도 이 파일로 폴백된다.
python web_main.py # APP_ENV 기본 local, http://localhost:9500/docs
APP_ENV=test python -m pytest # 테스트 (config.local.toml 사용)
```
### 브라우저 데모 (스크립트 기반 협상 채팅)
서버를 띄우고 **http://localhost:9500/demo** 접속 (`tests/negotiation_demo.html`, `/v1/chat` 구동).
테넌트/유형을 고르면 **서비스안내→담당자확인→협상품목안내→가격협상→와일드카드→협상완료** 대화가
스크립트로 진행되고, 버튼/가격 입력이 step 에 따라 동적 렌더링. **가격협상 턴에서 UCB Q-Table 이 카드를
선택하고 학습**(card_id·Q·visit 표시), 종료 시 성공/실패 보상 반영. 스크립트 브랜드는 테넌트별 치환(데모상사 A/B).
### 콘솔 테스트 (프론트 없이)
현재 구현된 부분을 콘솔로 확인하는 방법:
**1) 의사결정 루프 데모** — 테넌트별 config 주입·상태분류·보상·DB 격리를 눈으로 확인:
```bash
APP_ENV=local python -m tools.console_demo --tenant imarketkorea # 기본 3턴 시나리오
APP_ENV=local python -m tools.console_demo --tenant imarketkorea # 다른 테넌트(다른 카드셋·임계값)
APP_ENV=local python -m tools.console_demo --tenant imarketkorea --interactive # 직접 입력
APP_ENV=local python -m tools.console_demo --tenant imarketkorea --no-db # DB 없이
```
> ⚠️ 카드선택은 임시 placeholder 정책(실제 UCB Q-Table 은 H1/P5). 학습은 아직 일어나지 않는다.
**2) 서버 헬스/테넌트 라우팅** — 서버를 띄우고 curl:
```bash
APP_ENV=local python web_main.py # 다른 터미널에서:
curl localhost:9500/healthz # 200
curl localhost:9500/v1/health # {"status":"ok",...}
curl localhost:9500/v1/foo # 400 TENANT_HEADER_MISSING (헤더 없음)
curl localhost:9500/v1/foo -H 'X-Tenant-ID: nonexistent' # 404 TENANT_NOT_REGISTERED
curl localhost:9500/v1/foo -H 'X-Tenant-ID: imarketkorea' # 통과(라우트 미존재라 404 Not Found)
```
**2-1) 협상 한 라운드 (HTTP 프리뷰)** — `POST /v1/negotiation/step` (Swagger: http://localhost:9500/docs):
```bash
curl -s -X POST localhost:9500/v1/negotiation/step \
-H 'X-Tenant-ID: imarketkorea' -H 'Content-Type: application/json' \
-d '{"revenue_amount":20000000,"distribution_code":"A","partner_count":1,
"acceptance_ratio":0.11,"input_price":990,"anchor_price":800,"target_price":1000,
"round_number":3,"outcome":"success"}'
# → state_index / card_id(NGC-A*) / reward / logged. 테넌트를 imarketkorea 로 바꾸면 다른 상태·카드(NGC-B*).
```
> ⚠️ 카드선택은 임시 placeholder. 실제 대화 `/chat`·step 체계·학습형 정책은 P7/H1.
**3) DB 격리 확인** — 콘솔 데모 실행 후 company_id 별 로그 건수 조회:
```bash
APP_ENV=local python -m tools.show_logs # learning.experience_logs 를 company_id 별로 집계
```
## 진행 상황 (Phase)
- ✅ **P0 스캐폴딩**: 디렉토리/config/common/람다DB/Protocol 골격, profiling 개명(동적 import 제거),
앱 순환 import 없이 로드 + 테넌트 미들웨어 골격. (`tests/test_p0_scaffold.py` 5/5)
- ✅ **P1 TenantConfig & 로더**: pydantic config + YAML/`_base` deep-merge + TTL 캐시,
**클린룸 적용**(중립 데모값·`NGC-*` 카드). (`tests/test_p1_tenant_config.py` 7/7)
- ✅ **P2 State/Reward/Mapper config 주입**: `build_state`(IntEnum→주입, mixed-radix index),
`RewardCalculator`(자체 공식), `ActionCardMapper`(+중복방지 마스킹). 결정론·주입효과 검증. (`tests/test_p2_*` 8/8)
- ✅ **P3 learning 스키마**: `postgres-init/02-learning-schema.sql`(company_id 격리·복합유니크·propensity/turn),
ORM 모델, `LearningRepository`(company_id 생성자 박기·리셋 스코프). **실DB 검증**: 2테넌트 version_name 공존,
reset_all 타테넌트 무영향(파괴 테스트), company_id 위조방지. (`tests/test_p3_*` 5/5)
- ✅ **P4 Registry/Factory & 미들웨어**: 전역 싱글톤 제거 → `TenantEngineRegistry`(테넌트별 지연생성+lock 캐시),
`EngineFactory`, **episode_state 외부화**(`EpisodeState` 요청스코프 — 동시성 오염 방지), 미들웨어 미등록 404.
검증: 두 테넌트 다른 엔진/카드, 동시요청 1회조립, 헤더 400·미등록 404. (`tests/test_p4_*` 7/7)
- ✅ **H0 Policy 추상 + H1 UCB Q-Table 정책**: `NegotiationPolicy`(ABC), `QTable`(numpy, Q-learning),
`UCBQTablePolicy`(UCB 선택+propensity 근사+마스킹), `QTablePolicyStore`(learning 스키마 로드/영속).
**`/v1/negotiation/step` 이 실제 학습형으로 교체** — 반복 호출 시 UCB 탐색 + Q 갱신 + DB 누적 + 테넌트 격리.
(`tests/test_h1_*` 7/7)
- ✅ **대화 스크립트 + ScriptRepository**: Chat_server 스크립트 구조 이식(재협상/재견적/와일드카드/step·변수맵),
**KT 브랜드→`{company_name}`/`{service_name}` 변수화 + 표현 중립 재작성**(클린룸). (`tests/test_scripts_*` 7/7)
- ✅ **P7 슬라이스 `/v1/chat`**: 인메모리 세션 + `ChatEngine`(step 전이·조건분기·와일드카드 진입) +
**가격협상 턴 UCB 카드선택·학습 + 종료보상 역전파**. 브라우저 채팅 UI. (`tests/test_p7_chat.py` 5/5)
- ✅ **H5 학습검증 하네스 (PoC 본체)**: 카드별 효과가 다른 시뮬 구매자(`eval_harness/`) → 정책 비교.
**학습형(qtable_ucb)이 random/static 대비 평균보상 우위(95%CI 분리)·좋은카드 적중 0.9 vs 0.32** →
"학습하면 성과가 오른다" 정량 입증. `python -m eval_harness.runner --config configs/exp_default.yaml --tenant imarketkorea`. (`tests/test_h5_*` 5/5)
- ✅ **P7 14개 API**: chat / q-table(versions·switch·current) / experience-logs / reset-learning ·
reset-all(타테넌트 무영향) / invalidate-session / **train**(오프라인 Q-learning) / verification-report /
card-update · card-search. 전부 X-Tenant-ID 격리. (`tests/test_p7_apis.py` 5/5)
- ✅ **P5 베이스 warm-start / cold-start 3단**: `warm_start_from_base`(차원 호환 시 base Q값/방문수 복제,
visit 감쇠·base_version_id 추적), cold-start 3단(warm-start→차원불일치 휴리스틱 폴백→첫 버전),
`tools/init_base.py`(시뮬레이터로 베이스 시드). 신규 테넌트 첫 협상 → `v000_warmstart_from_base`. (`tests/test_p5_*` 5/5)
- ✅ **P8-A 세션 상태 DB화**: `learning.chat_sessions` + `ChatSessionRepository`(인메모리 제거).
**서버 재시작/멀티워커에도 협상 진행 상태 복원**(라이브 검증: kill 후 재기동→같은 session_id 이어짐). (`tests/test_p8_*` 3/3)
- ⬜ H3 LinUCB / H4 OPE(IPS/DR/SNIPS) / H6 CQL+FQE / P8-B 채팅로그(negotiation.chats) 적재.
**누적 테스트 73/73** (`cd agent && APP_ENV=test python -m pytest`). DB 테스트는 로컬 postgres 필요(미가용 시 skip).
### 협상 경제 모델 (KT 구매자 관점)
- **KT커머스/아이마켓코리아 = 구매자(갑)**. 목표 = **싸게 매입**. 협력사(판매자)가 제시가를 낸다.
- **앵커링가(anchor) < 목표가(target).** 앵커링값은 **갑이 직접 입력**(UI 기본 제안 = `target×(1−0.01)`, 편집 가능).
- **협력사 제시가 ≤ 앵커가 → 우선협상(타결)**, 더 낮을수록 **KT 보상↑**. 앵커가 초과 → 카드로 인하 협상, **설정 카드 모두 소진 시 결렬**.
- 와일드카드: 앵커가 살짝 초과 구간에서 1% 인하/목표가 매칭 압박.
### PoC 본체 결과 (H5, 위 경제모델 기준 / target=10000·anchor=8000 시나리오)
```
python -m eval_harness.runner --config configs/exp_default.yaml --tenant imarketkorea
policy success settled/tgt turns mean_rwd ±95%CI good_hit
random 0.988 0.904 2.02 1.1176 0.0206 0.350
static 1.000 0.923 2.56 0.9995 0.0020 0.000
qtable_ucb 0.998 0.881 1.46 1.2686 0.0088 1.000 ← 학습형(최저 매입가)
판정 ✅ PASS: 학습형이 baseline 대비 평균보상 우위(95%CI 분리) + 좋은카드 적중 우위 → 학습 루프 유효
(settled/tgt 낮을수록 = 더 싸게 매입 = KT 이득. 학습형이 가장 낮음)
```
> **학습 한계(정직)**: 현재 보상은 snapshot 만으로 산출돼 **어떤 카드를 골랐는지에 무관**하다. 즉 UCB 탐색·Q갱신·영속·격리 '머신'은 실동작하지만, "어떤 카드가 더 좋은가"를 학습하려면 **행동-의존 보상**이 필요하다 → H5(LLM 구매자 시뮬레이터) 또는 실 협상결과 로그. 그게 PoC 본체의 다음 핵심.
- ⬜ 트랙2(하네스): H0 로깅 보강 → H1 QTable 어댑터 → … → H7 비교표/리포트.
상세 계획·검증 기준은 실행 계획서 참조.