카드 카탈로그(negodata)가 Q-table action space 를 정의하는 정본이 되고, 카드 변경이
config 수정·학습 손실 없이 agent 에 자동 반영되는 고리를 완성.
- action space 정리: 카탈로그 전체(NGC-001~011, 11장) 고정, 견적별 선택은 축소가 아니라
available_mask(_selection_mask) 로 처리 — action_id↔카드 대응을 견적마다 일정하게 유지해
Q-table 학습 일관성 보장. 구 인덱스 방식(selected[action_id]) 폐기.
- ① 카탈로그 DB 정본화: action_mapping.type=db 면 registry 가 card.nego_cards(user_id NULL,
number 순) 조회로 action_to_card 동적 구성(파일은 폴백). port/adapter(card_catalog_*).
_base=type:db. → negodata 카드 추가/삭제 시 config 수정 불필요.
- ② 차원 변경 학습 보존 마이그레이션: migrate_active_version_dim — 겹치는 셀 복사
(append/truncate 안전) + 새 카드 fresh. model_store.load 가 차원 불일치 시 호출.
- ③ reload 엔드포인트: /v1/catalog-refresh(테넌트) · /v1/catalog-refresh-all(전역, 화이트리스트).
- ④ 브랜드: company_profile_repo — 자동 온보딩 고객사(company_id UUID)는
company.companies.name 으로 {company_name} 채움. 데모 테넌트는 파일 유지.
- 크로스서비스: negodata card_service 가 공용 nego 카드 변경 시 agent_notify 로 전역 리로드 알림
(best-effort, is_test skip). config 에 agent_base_url.
- 하니스 episodes 400→600(action 11 수렴). 테스트 갱신·추가로 agent 98/98.
알려진 갭(후속): per-company 카탈로그 스코프(회사 카드도 action space 포함), 카탈로그 중간
삭제 시 카드번호 기반 마이그레이션.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
2.7 KiB
Python
64 lines
2.7 KiB
Python
"""테넌트 라우팅 미들웨어.
|
|
|
|
규약 (계획서 B):
|
|
- 헤더 `X-Tenant-ID`(1순위) / 경로 `/t/{id}/...`(폴백)로 테넌트 식별.
|
|
- 부재 시 400(TENANT_HEADER_MISSING), 미등록 404(TENANT_NOT_REGISTERED).
|
|
- default tenant 금지 (KT 사고 방지) — allow_default_tenant=false.
|
|
- 요청 본문엔 tenant 미포함(위조 방지). 식별된 tenant_id 는 request.state.tenant_id 로만 전달.
|
|
|
|
NOTE(P0): 여기서는 헤더 추출 + 부재 검증까지만 한다.
|
|
미등록 404 판정(레지스트리 조회)·company_id 매핑·엔진 resolve 는 P4(TenantEngineRegistry)에서 채운다.
|
|
health/docs 등 화이트리스트 경로는 검사를 건너뛴다.
|
|
"""
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import JSONResponse
|
|
|
|
from common.enums import ErrorType
|
|
from config.server_configs import agent_config
|
|
|
|
_TENANT_HEADER = "X-Tenant-ID"
|
|
|
|
# 테넌트 식별이 필요 없는 경로 (헬스/문서/스키마/데모 UI + 전역 카탈로그 리로드=내부 운영).
|
|
_WHITELIST_PREFIXES = ("/healthz", "/health", "/v1/health", "/docs", "/redoc", "/openapi.json", "/demo", "/v1/catalog-refresh-all")
|
|
|
|
class TenantMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
# CORS preflight(OPTIONS)는 테넌트 헤더 없이 오므로 통과시킨다(CORSMiddleware 가 처리).
|
|
if request.method == "OPTIONS":
|
|
return await call_next(request)
|
|
|
|
path = request.url.path
|
|
if any(path.startswith(p) for p in _WHITELIST_PREFIXES):
|
|
return await call_next(request)
|
|
|
|
tenant_id = request.headers.get(_TENANT_HEADER)
|
|
|
|
# 경로 폴백: /t/{tenant_id}/...
|
|
if not tenant_id and path.startswith("/t/"):
|
|
parts = path.split("/", 3)
|
|
if len(parts) >= 3 and parts[2]:
|
|
tenant_id = parts[2]
|
|
|
|
if not tenant_id:
|
|
if agent_config.allow_default_tenant:
|
|
tenant_id = "_base"
|
|
else:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"result": {"success": False, "code": ErrorType.TENANT_HEADER_MISSING.value, "desc": ErrorType.TENANT_HEADER_MISSING.name}},
|
|
)
|
|
|
|
# 미등록 테넌트는 404 (default tenant 금지 — KT 사고 방지).
|
|
from tenancy.registry import tenant_registry
|
|
|
|
if not tenant_registry.is_registered(tenant_id):
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={"result": {"success": False, "code": ErrorType.TENANT_NOT_REGISTERED.value, "desc": ErrorType.TENANT_NOT_REGISTERED.name}},
|
|
)
|
|
|
|
request.state.tenant_id = tenant_id
|
|
return await call_next(request)
|