IMK QA 2건(BB9A 카드 중복·8AB0 중간값 오계산)의 근본 원인이 전술 하드코딩(_TACTICS 번호 매칭)이라 전술을 데이터로 옮기고, 발동을 유효성 검사로 바꿨다. 전술 정본 = 카드 스크립트의 마지막 가격 변수(파싱), 문장으로 알 수 없는 운영 규칙(closing·min_round)만 card.*.tactic JSONB. 세션 시작 시 card_specs 스냅샷 박제. 발동 유효성(하나라도 걸리면 그 라운드 미발동 — 클램프 폐지): 목표가 초과 / 협력사 제시가 이상 / 당사 직전 제안 미만(역행 금지, IMK 논의) / 재료 결측 / 필수 변수 결측(시장가 카드 requires — 토큰 노출 방지) / 이미 쓴 카드(played_card_numbers 공용 이력) - agent: 와일드=비종결·종결=전용 풀 분리(같은 카드 2회 구조적 차단), 발동 시 자기 제안가 기록(절충가 수렴), 진입 존 프로브(빈 덱 재사용 교착 방지), 낼 카드 전무 시 소진→종결, 무효 금액 카드는 설득 폴백도 금지(playable), 에디터 anchor_price 별칭 등록, 에러 재렌더 변수 치환 - backend: 카드 사용 기록을 step 휴리스틱→번호 prefix 판정(종결 발동 card:null 누락 해소) - negodata: 카드 상세 "협상 전술" 섹션(제시 가격 파싱 표시·종결 전용·최소 라운드) + tactic API 배선 - postgres-init: tactic 컬럼·시드(WC-03/05 closing, WC-04 min_round 2), 멱등 alter 로 dev 정본화 (번호 WC-0x 정규화, WC-01·03·NGC-010 구멘트 교체, WC-05 변수 middle_price 교정) 검증: agent 178 통과 · 시나리오 하네스 14케이스(BB9A·8AB0·역행 실수치 재현) · 랜덤 퍼즈 50협상 불변식 위반 0 (불변식: 카드 중복 금지·종결 카드 자리·타결가≤목표가·표시가=타결가·토큰 잔존 금지·종료 보장) |
||
|---|---|---|
| .. | ||
| bootstrap | ||
| common | ||
| config | ||
| docs | ||
| eval_harness | ||
| negotiation | ||
| router | ||
| services | ||
| tenancy | ||
| tenants | ||
| tests | ||
| tools | ||
| .dockerignore | ||
| .gitignore | ||
| conftest.py | ||
| Dockerfile | ||
| pytest.ini | ||
| README.md | ||
| requirements.txt | ||
| web_main.py | ||
Negosium Agent
범용 멀티테넌트 협상 솔루션 PoC. 여러 회사(imarketkorea 등)가 각자 데이터로 분기 학습하는
협상 카드 선택 에이전트. Q-Learning(UCB) 기반 Chat_server(단일 테넌트)를 참고해 신규 구축한다.
PoC 목표 두 가지:
- 학습 알고리즘이 실제 협상 성과를 개선하는지 정량 검증 (Q-Table vs LinUCB vs Offline RL 비교 하네스).
- 멀티테넌트 분기 학습 + 기존 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로 선택).
- MVC:
- 협상 엔진 내부는 헥사고날 구조(
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.
디렉토리 구조
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/
실행 / 테스트
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 격리를 눈으로 확인:
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:
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):
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 별 로그 건수 조회:
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.py5/5) - ✅ P1 TenantConfig & 로더: pydantic config + YAML/
_basedeep-merge + TTL 캐시, 클린룸 적용(중립 데모값·NGC-*카드). (tests/test_p1_tenant_config.py7/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.py5/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.py5/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 비교표/리포트.
상세 계획·검증 기준은 실행 계획서 참조.