| .. | ||
| bootstrap | ||
| common | ||
| config | ||
| 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. 여러 회사(ktcommerce·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 ktcommerce # 기본 3턴 시나리오
APP_ENV=local python -m tools.console_demo --tenant imarketkorea # 다른 테넌트(다른 카드셋·임계값)
APP_ENV=local python -m tools.console_demo --tenant ktcommerce --interactive # 직접 입력
APP_ENV=local python -m tools.console_demo --tenant ktcommerce --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: ktcommerce' # 통과(라우트 미존재라 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: ktcommerce' -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 ktcommerce. (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 ktcommerce
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 비교표/리포트.
상세 계획·검증 기준은 실행 계획서 참조.