[feat] 세션 step desync 동기화
This commit is contained in:
parent
5d9eedf2f6
commit
fd2e5c1cc2
@ -3,7 +3,7 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from router.deps import get_tenant_engine
|
from router.deps import get_tenant_engine
|
||||||
from router.v1.chat.protocol import Req_Chat, Res_Chat
|
from router.v1.chat.protocol import Req_Chat, Res_Chat, Res_ChatSession
|
||||||
from services.chat_service import ChatService
|
from services.chat_service import ChatService
|
||||||
from tenancy.registry import TenantEngine
|
from tenancy.registry import TenantEngine
|
||||||
|
|
||||||
@ -22,3 +22,17 @@ async def chat(
|
|||||||
service: ChatService = Depends(),
|
service: ChatService = Depends(),
|
||||||
):
|
):
|
||||||
return await service.chat(engine, req)
|
return await service.chat(engine, req)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
path="/sessions/{session_id}",
|
||||||
|
response_model=Res_ChatSession,
|
||||||
|
summary="세션 상태 조회 (desync 리싱크)",
|
||||||
|
description="agent 세션의 현재 step/ended 를 반환한다. backend 가 타임아웃·재진입 시 정합을 맞출 때 사용. X-Tenant-ID 헤더 필수.",
|
||||||
|
)
|
||||||
|
async def get_session(
|
||||||
|
session_id: str,
|
||||||
|
engine: TenantEngine = Depends(get_tenant_engine),
|
||||||
|
service: ChatService = Depends(),
|
||||||
|
):
|
||||||
|
return await service.get_session_state(engine, session_id)
|
||||||
|
|||||||
@ -13,6 +13,10 @@ class Req_Chat(Req_WebPacketProtocol):
|
|||||||
session_id: Optional[str] = Field(None, description="없으면 새 세션 생성")
|
session_id: Optional[str] = Field(None, description="없으면 새 세션 생성")
|
||||||
rq_type: str = Field("재협상", description="재협상 | 재견적")
|
rq_type: str = Field("재협상", description="재협상 | 재견적")
|
||||||
user_input: Optional[str] = Field(None, description="버튼 선택 텍스트 또는 가격(price 모드)")
|
user_input: Optional[str] = Field(None, description="버튼 선택 텍스트 또는 가격(price 모드)")
|
||||||
|
# ① desync 감지: backend 가 보는 직전 봇 step(내부 step 또는 표시 step). 없으면 검사 생략.
|
||||||
|
# agent 는 자기 세션 step 을 정답으로 보고 진행하되, 불일치 시 경고 로깅하고 응답에 desynced 를 실어
|
||||||
|
# backend/front 가 agent 응답의 step/client_step 으로 리싱크하게 한다.
|
||||||
|
client_step: Optional[str] = Field(None, description="backend 가 본 직전 봇 step (desync 감지용)")
|
||||||
# 새 세션 시작 시 협상 컨텍스트 (옵션, 기본값 제공)
|
# 새 세션 시작 시 협상 컨텍스트 (옵션, 기본값 제공)
|
||||||
revenue_amount: float = 20_000_000
|
revenue_amount: float = 20_000_000
|
||||||
distribution_code: str = "A"
|
distribution_code: str = "A"
|
||||||
@ -32,6 +36,8 @@ class Res_Chat(Res_WebPacketProtocol):
|
|||||||
input_options: Optional[List[str]] = None
|
input_options: Optional[List[str]] = None
|
||||||
chat_end: bool = False
|
chat_end: bool = False
|
||||||
outcome: Optional[str] = None
|
outcome: Optional[str] = None
|
||||||
|
# ① step desync 신호: 요청 client_step 이 agent 세션 step 과 달랐음을 알린다(agent step 이 정답).
|
||||||
|
desynced: bool = False
|
||||||
# 가격협상 턴에서 선택된 협상 카드 + 학습 메타
|
# 가격협상 턴에서 선택된 협상 카드 + 학습 메타
|
||||||
card_id: Optional[str] = None
|
card_id: Optional[str] = None
|
||||||
policy: Optional[str] = None
|
policy: Optional[str] = None
|
||||||
@ -39,3 +45,14 @@ class Res_Chat(Res_WebPacketProtocol):
|
|||||||
updated_q: Optional[float] = None
|
updated_q: Optional[float] = None
|
||||||
visit_count: Optional[int] = None
|
visit_count: Optional[int] = None
|
||||||
reward_total: Optional[float] = None
|
reward_total: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Res_ChatSession(Res_WebPacketProtocol):
|
||||||
|
"""① 세션 상태 조회 응답. backend 가 타임아웃/재진입 시 정합을 맞출 때 사용."""
|
||||||
|
|
||||||
|
session_id: Optional[str] = None
|
||||||
|
step: Optional[str] = None
|
||||||
|
client_step: Optional[str] = None
|
||||||
|
rq_type: Optional[str] = None
|
||||||
|
ended: bool = False
|
||||||
|
found: bool = False
|
||||||
|
|||||||
@ -20,7 +20,7 @@ from negotiation.qtable.domain.model.snapshot import NegotiationOutcome, Negotia
|
|||||||
from negotiation.qtable.domain.service.reward_calculator import RewardCalculator
|
from negotiation.qtable.domain.service.reward_calculator import RewardCalculator
|
||||||
from negotiation.qtable.domain.service.state_calculator import state_index
|
from negotiation.qtable.domain.service.state_calculator import state_index
|
||||||
from negotiation.qtable.infra.repository.learning_repository import LearningRepository
|
from negotiation.qtable.infra.repository.learning_repository import LearningRepository
|
||||||
from router.v1.chat.protocol import Req_Chat, Res_Chat
|
from router.v1.chat.protocol import Req_Chat, Res_Chat, Res_ChatSession
|
||||||
from tenancy.registry import TenantEngine
|
from tenancy.registry import TenantEngine
|
||||||
|
|
||||||
|
|
||||||
@ -34,6 +34,20 @@ class ChatService:
|
|||||||
session = await sess_repo.get(req.session_id) if req.session_id else None
|
session = await sess_repo.get(req.session_id) if req.session_id else None
|
||||||
chat_engine = ChatEngine(repo, rq_type=(session.rq_type if session else req.rq_type))
|
chat_engine = ChatEngine(repo, rq_type=(session.rq_type if session else req.rq_type))
|
||||||
|
|
||||||
|
# ① step desync 감지: backend 가 본 직전 봇 step(client_step)이 agent 세션 step 과 다르면 경고.
|
||||||
|
# agent 가 자기 step 을 정답으로 보고 진행하고(응답의 step/client_step 으로 backend 가 따라옴),
|
||||||
|
# 추적/리싱크 트리거를 위해 로깅 + 응답 desynced 플래그로 알린다.
|
||||||
|
desynced = False
|
||||||
|
if session is not None and req.client_step:
|
||||||
|
agent_step = session.step
|
||||||
|
agent_client_step = chat_engine.step_map.get(agent_step, agent_step)
|
||||||
|
if req.client_step not in (agent_step, agent_client_step):
|
||||||
|
desynced = True
|
||||||
|
LOG.w(
|
||||||
|
f"[ChatService] step desync: backend client_step={req.client_step!r} != "
|
||||||
|
f"agent step={agent_step!r}(client={agent_client_step!r}) session={session.session_id}"
|
||||||
|
)
|
||||||
|
|
||||||
if session is None:
|
if session is None:
|
||||||
# session_id honoring: backend 가 보낸 session_id(= negotiation.sessions.session_id)를
|
# session_id honoring: backend 가 보낸 session_id(= negotiation.sessions.session_id)를
|
||||||
# 새 uuid 발급 없이 그대로 세션 키로 쓴다. 없으면(직접 호출/데모) 생성.
|
# 새 uuid 발급 없이 그대로 세션 키로 쓴다. 없으면(직접 호출/데모) 생성.
|
||||||
@ -67,6 +81,7 @@ class ChatService:
|
|||||||
res.input_options = view.input_options
|
res.input_options = view.input_options
|
||||||
res.chat_end = view.chat_end
|
res.chat_end = view.chat_end
|
||||||
res.outcome = view.outcome
|
res.outcome = view.outcome
|
||||||
|
res.desynced = desynced
|
||||||
if view.error:
|
if view.error:
|
||||||
res.result.SetResult(ErrorType.NEGO_INVALID_STEP)
|
res.result.SetResult(ErrorType.NEGO_INVALID_STEP)
|
||||||
res.msg = view.error
|
res.msg = view.error
|
||||||
@ -74,6 +89,24 @@ class ChatService:
|
|||||||
await sess_repo.save(session) # 진행 상태 영속화 (재시작/멀티워커 안전)
|
await sess_repo.save(session) # 진행 상태 영속화 (재시작/멀티워커 안전)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
# ---- 세션 상태 조회 (① desync 리싱크용) ----------------------------
|
||||||
|
async def get_session_state(self, engine: TenantEngine, session_id: str) -> "Res_ChatSession":
|
||||||
|
"""backend 가 타임아웃/재진입 시 agent 의 현재 step 을 읽어 정합을 맞춘다."""
|
||||||
|
res = Res_ChatSession()
|
||||||
|
repo = ScriptRepository(engine.config, agent_config.tenants_dir)
|
||||||
|
session = await ChatSessionRepository(engine.company_id).get(session_id)
|
||||||
|
if session is None:
|
||||||
|
res.result.SetResult(ErrorType.NEGO_SESSION_NOT_FOUND)
|
||||||
|
return res
|
||||||
|
step_map = repo.client_step_mapping()
|
||||||
|
res.session_id = session.session_id
|
||||||
|
res.step = session.step
|
||||||
|
res.client_step = step_map.get(session.step, session.step)
|
||||||
|
res.rq_type = session.rq_type
|
||||||
|
res.ended = session.ended
|
||||||
|
res.found = True
|
||||||
|
return res
|
||||||
|
|
||||||
# ---- 학습 ----------------------------------------------------------
|
# ---- 학습 ----------------------------------------------------------
|
||||||
def _snapshot(self, session: ChatSession, outcome: NegotiationOutcome) -> NegotiationSnapshot:
|
def _snapshot(self, session: ChatSession, outcome: NegotiationOutcome) -> NegotiationSnapshot:
|
||||||
c = session.context
|
c = session.context
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
\connect negosium_db
|
\connect negosium_db
|
||||||
|
|
||||||
-- 회사 1개 (고정 UUID). status 1=active. code/industry 는 코드값이라 비워둠(nullable).
|
-- 회사 1개 (고정 UUID). status 1=active. code/industry 는 코드값이라 비워둠(nullable).
|
||||||
INSERT INTO company.companies (company_id, name, business_number, representative_name, email, contact_number, website_url, status)
|
INSERT INTO company.companies (name, business_number, representative_name, email, contact_number, website_url, status)
|
||||||
SELECT '00000000-0000-0000-0000-000000000001', '아이마켓코리아', '220-88-21724', '홍길동',
|
SELECT '아이마켓코리아', '220-88-21724', '홍길동',
|
||||||
'admin@imarketkorea.com', '02-3708-5000', 'https://www.imarketkorea.com', 1
|
'admin@imarketkorea.com', '02-3708-5000', 'https://www.imarketkorea.com', 1
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM company.companies WHERE company_id = '00000000-0000-0000-0000-000000000001'
|
SELECT 1 FROM company.companies WHERE company_id = '00000000-0000-0000-0000-000000000001'
|
||||||
@ -17,9 +17,7 @@ WHERE NOT EXISTS (
|
|||||||
|
|
||||||
-- admin 유저. password 는 'admin123' 의 bcrypt 해시(백엔드 GetHashedPW 와 동일 알고리즘, checkpw 로 검증됨).
|
-- admin 유저. password 는 'admin123' 의 bcrypt 해시(백엔드 GetHashedPW 와 동일 알고리즘, checkpw 로 검증됨).
|
||||||
-- role 2=manager (UserRole.MANAGER; ADMIN 코드는 enum 에 없어 최상위인 MANAGER 사용). status 1=active.
|
-- role 2=manager (UserRole.MANAGER; ADMIN 코드는 enum 에 없어 최상위인 MANAGER 사용). status 1=active.
|
||||||
INSERT INTO company.users (user_id, company_id, id, password, name, email, contact_number, last_accessed_at, status, role)
|
INSERT INTO company.users (id, password, name, email, contact_number, last_accessed_at, status, role)
|
||||||
SELECT '00000000-0000-0000-0000-000000000002',
|
|
||||||
'00000000-0000-0000-0000-000000000001',
|
|
||||||
'admin',
|
'admin',
|
||||||
'$2b$12$KY4T0kXQ2npvvt71iWZG0.JZHlMNt9angIkE/7.lBC4vta4dHgrj2',
|
'$2b$12$KY4T0kXQ2npvvt71iWZG0.JZHlMNt9angIkE/7.lBC4vta4dHgrj2',
|
||||||
'관리자', 'admin@imarketkorea.com', '02-3708-5000', now(), 1, 2
|
'관리자', 'admin@imarketkorea.com', '02-3708-5000', now(), 1, 2
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user