o2o-negosium-original/backend/router/v1/negotiation/chat.py
민헌 dcce82a630 feat(backend): 협상 채팅(chat) API + agent 위임
- /v1/negotiation/sessions/{id}/chat/{init,messages,send} 추가
- agent(9500) 위임 어댑터(IAgentClient) + mock(use_mock) 격리 → agent 미연동 시 1402 graceful degrade
- negotiation.chats 메시지 영속화(meta JSONB) + 종료 시 세션 입찰/거부 확정(단일 트랜잭션)
- 동시전송 가드(유저 메시지 pre-claim/CHAT_IN_PROGRESS) + 실패 시 롤백, 마감/만료 분기, init 만료 정리
- ChatSender enum, chat 에러코드(1400~1403), chats ORM 모델, AgentConfig
- 테스트 10건(test_chat.py), AGENT_INTEGRATION.md 연동 규약 문서

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:24:57 +09:00

58 lines
2.3 KiB
Python

from fastapi import APIRouter, Depends
from fastapi.security import HTTPAuthorizationCredentials
from common.models.gmodel import UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse, security
from services.chat_service import ChatService
from .chat_protocol import Req_ChatSend, Res_ChatInit, Res_ChatMessages, Res_ChatSend
router = APIRouter(prefix="/v1/negotiation", tags=["Negotiation Chat"], responses={404: {"description": "Not found"}})
@router.get(
path="/sessions/{session_id}/chat/init",
response_model=Res_ChatInit,
summary="채팅 진입(상품·견적 메타)",
description="채팅 화면 진입용. 상품/견적 정보 + 현재 세션 상태 + 마감 시각(타이머)을 반환한다. 소유(공급사) 검증.",
)
async def chat_init(
session_id: str,
user_info: UserInfo = Depends(IsValidAccessToken),
credentials: HTTPAuthorizationCredentials = Depends(security),
service: ChatService = Depends(),
):
return RemoveNoneResponse(await service.init(user_info, credentials.credentials, session_id))
@router.get(
path="/sessions/{session_id}/chat/messages",
response_model=Res_ChatMessages,
summary="대화 히스토리",
description="세션의 대화 말풍선 목록(seq 오름차순). 비어 있고 협상중이면 오프닝 메시지를 생성해 포함한다.",
)
async def chat_messages(
session_id: str,
user_info: UserInfo = Depends(IsValidAccessToken),
credentials: HTTPAuthorizationCredentials = Depends(security),
service: ChatService = Depends(),
):
return RemoveNoneResponse(await service.messages(user_info, credentials.credentials, session_id))
@router.post(
path="/sessions/{session_id}/chat/send",
response_model=Res_ChatSend,
summary="협상 한 턴 전송",
description="유저 입력을 보내고 agent 가 만든 봇 응답 1건을 반환한다(append-only). 종료 시 세션 입찰을 확정한다.",
)
async def chat_send(
session_id: str,
req: Req_ChatSend,
user_info: UserInfo = Depends(IsValidAccessToken),
credentials: HTTPAuthorizationCredentials = Depends(security),
service: ChatService = Depends(),
):
return RemoveNoneResponse(
await service.send(user_info, credentials.credentials, session_id, req.user_input_type, req.user_input)
)