# 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..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 비교표/리포트. 상세 계획·검증 기준은 실행 계획서 참조.