Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/api/admin_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@

from app.api.dependencies.admin_auth import require_admin_token
from app.db.session import get_db
from app.schemas.admin_chat import AdminChatAdminReview
from app.schemas.admin_chat import AdminChatLogDetail
from app.schemas.admin_chat import AdminChatReviewQueueReason
from app.schemas.admin_chat import AdminChatReviewQueueResult
from app.schemas.admin_chat import AdminChatReviewSaveRequest
from app.schemas.admin_chat import AdminChatSessionsByDateResult
from app.schemas.common import ApiResponse
from app.services.admin_chat_service import get_admin_chat_review_queue
from app.services.admin_chat_service import get_admin_chat_sessions_by_date
from app.services.admin_chat_service import get_admin_chat_log_detail
from app.services.admin_chat_service import save_admin_chat_review

router = APIRouter(
prefix="/admin/chat",
Expand Down Expand Up @@ -72,6 +75,29 @@ def get_admin_chat_log_api(
)


@router.put(
"/logs/{chat_log_id}/review",
response_model=ApiResponse[AdminChatAdminReview],
status_code=status.HTTP_200_OK,
summary="관리자 채팅 검수 저장",
description=(
"관리자가 특정 채팅 로그에 대한 검수 결과를 저장하거나 수정합니다.\n\n"
"`chat_log_id`에 기존 검수 결과가 있으면 최신 검수 row를 수정하고, 없으면 새로 생성합니다."
),
)
def save_admin_chat_review_api(
request: AdminChatReviewSaveRequest,
chat_log_id: int = Path(ge=1),
db: Session = Depends(get_db),
) -> ApiResponse[AdminChatAdminReview]:
result = save_admin_chat_review(db, chat_log_id=chat_log_id, request=request)
Comment on lines +88 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

AdminChatReviewSaveRequest에서 admin_id를 클라이언트로부터 직접 입력받는 방식은 보안상 취약할 수 있습니다. 인증된 관리자가 다른 관리자의 ID로 리뷰를 저장하는 등의 오용을 방지하기 위해, admin_id는 require_admin_token 종속성을 통해 얻은 인증 정보에서 추출하여 서버 측에서 처리하는 것을 권장합니다.

return ApiResponse(
status=status.HTTP_200_OK,
message="chat admin review saved",
data=result,
)


@router.get(
"/sessions",
response_model=ApiResponse[AdminChatSessionsByDateResult],
Expand Down
68 changes: 68 additions & 0 deletions app/repositories/admin_chat_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,74 @@ def list_chat_error_logs_by_chat_log_id(
return list(db.execute(statement).scalars().all())


def list_chat_feedbacks_by_chat_log_id(
db: Session,
chat_log_id: int,
) -> list[ChatFeedback]:
statement = (
select(ChatFeedback)
.where(ChatFeedback.chat_log_id == chat_log_id)
.order_by(ChatFeedback.created_at.desc(), ChatFeedback.feedback_id.desc())
)
return list(db.execute(statement).scalars().all())


def list_chat_admin_reviews_by_chat_log_id(
db: Session,
chat_log_id: int,
) -> list[ChatAdminReview]:
statement = (
select(ChatAdminReview)
.where(ChatAdminReview.chat_log_id == chat_log_id)
.order_by(ChatAdminReview.created_at.desc(), ChatAdminReview.review_id.desc())
)
return list(db.execute(statement).scalars().all())


def get_latest_chat_admin_review_by_chat_log_id(
db: Session,
chat_log_id: int,
) -> Optional[ChatAdminReview]:
statement = (
select(ChatAdminReview)
.where(ChatAdminReview.chat_log_id == chat_log_id)
.order_by(ChatAdminReview.created_at.desc(), ChatAdminReview.review_id.desc())
.limit(1)
)
return db.execute(statement).scalar_one_or_none()


def save_chat_admin_review(
db: Session,
*,
chat_log_id: int,
reviewer_id: int,
correctness_label: str,
citation_label: Optional[str],
root_cause: Optional[str],
correction_required: bool,
corrected_answer: Optional[str],
review_note: Optional[str],
) -> ChatAdminReview:
admin_review = get_latest_chat_admin_review_by_chat_log_id(db, chat_log_id)
if admin_review is None:
admin_review = ChatAdminReview(
chat_log_id=chat_log_id,
reviewer_id=reviewer_id,
)
db.add(admin_review)

admin_review.reviewer_id = reviewer_id
admin_review.correctness_label = correctness_label
admin_review.citation_label = citation_label
admin_review.root_cause = root_cause
admin_review.correction_required = correction_required
admin_review.corrected_answer = corrected_answer
admin_review.review_note = review_note
db.flush()
return admin_review


def count_chat_review_queue_items(db: Session, reason: Optional[str] = None) -> int:
statement = select(func.count(ChatLog.chat_log_id)).where(*_build_chat_review_queue_conditions(reason))
return int(db.execute(statement).scalar_one())
Expand Down
46 changes: 46 additions & 0 deletions app/schemas/admin_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@


AdminChatReviewQueueReason = Literal["ERROR", "NO_ANSWER", "NEGATIVE_FEEDBACK"]
AdminChatCorrectnessLabel = Literal["CORRECT", "PARTIAL", "INCORRECT"]
AdminChatCitationLabel = Literal["APPROPRIATE", "WEAK", "WRONG", "NONE"]
AdminChatRootCause = Literal[
"RETRIEVAL_FAIL",
"DOC_OUTDATED",
"NO_RELEVANT_DOC",
"PROMPT_OVERGENERATION",
"QUESTION_AMBIGUOUS",
"MODEL_HALLUCINATION",
]


class AdminChatLogDetail(BaseModel):
Expand All @@ -25,6 +35,8 @@ class AdminChatLogDetail(BaseModel):
created_at: datetime
retrieval_results: list["AdminChatRetrievalResult"] = Field(default_factory=list)
error_logs: list["AdminChatErrorLog"] = Field(default_factory=list)
feedbacks: list["AdminChatFeedback"] = Field(default_factory=list)
admin_reviews: list["AdminChatAdminReview"] = Field(default_factory=list)


class AdminChatRetrievalResult(BaseModel):
Expand Down Expand Up @@ -52,6 +64,40 @@ class AdminChatErrorLog(BaseModel):
created_at: datetime


class AdminChatFeedback(BaseModel):
feedback_id: int
user_id: Optional[int] = None
feedback_type: str
is_helpful: Optional[bool] = None
rating: Optional[int] = None
reason_code: Optional[str] = None
feedback_comment: Optional[str] = None
feature_type: Optional[str] = None
created_at: datetime


class AdminChatAdminReview(BaseModel):
review_id: int
admin_id: int
correctness_label: str
citation_label: Optional[str] = None
root_cause: Optional[str] = None
correction_required: bool
corrected_answer: Optional[str] = None
review_note: Optional[str] = None
created_at: datetime


class AdminChatReviewSaveRequest(BaseModel):
admin_id: int = Field(ge=1)
correctness_label: AdminChatCorrectnessLabel
citation_label: Optional[AdminChatCitationLabel] = None
root_cause: Optional[AdminChatRootCause] = None
correction_required: bool = False
corrected_answer: Optional[str] = Field(default=None, max_length=5000)
review_note: Optional[str] = Field(default=None, max_length=2000)


class AdminChatSessionSummary(BaseModel):
session_id: str
user_id: Optional[int] = None
Expand Down
76 changes: 76 additions & 0 deletions app/services/admin_chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@
from app.repositories.admin_chat_repository import count_chat_review_queue_items
from app.repositories.admin_chat_repository import count_chat_sessions_by_started_date
from app.repositories.admin_chat_repository import list_chat_error_logs_by_chat_log_id
from app.repositories.admin_chat_repository import list_chat_feedbacks_by_chat_log_id
from app.repositories.admin_chat_repository import list_chat_admin_reviews_by_chat_log_id
from app.repositories.admin_chat_repository import list_chat_retrieval_results_by_chat_log_id
from app.repositories.admin_chat_repository import list_chat_review_queue_items
from app.repositories.admin_chat_repository import list_chat_sessions_by_started_date
from app.repositories.admin_chat_repository import save_chat_admin_review
from app.schemas.admin_chat import AdminChatReviewQueueReason
from app.schemas.admin_chat import AdminChatReviewQueueResult
from app.schemas.admin_chat import AdminChatReviewQueueItem
from app.schemas.admin_chat import AdminChatErrorLog
from app.schemas.admin_chat import AdminChatFeedback
from app.schemas.admin_chat import AdminChatAdminReview
from app.schemas.admin_chat import AdminChatReviewSaveRequest
from app.schemas.admin_chat import AdminChatSessionsByDateResult
from app.schemas.admin_chat import AdminChatLogDetail
from app.schemas.admin_chat import AdminChatRetrievalResult
Expand All @@ -34,6 +40,8 @@ def get_admin_chat_log_detail(db: Session, chat_log_id: int) -> AdminChatLogDeta
raise AppException(CHAT_LOG_NOT_FOUND)
retrieval_results = list_chat_retrieval_results_by_chat_log_id(db, chat_log_id)
error_logs = list_chat_error_logs_by_chat_log_id(db, chat_log_id)
feedbacks = list_chat_feedbacks_by_chat_log_id(db, chat_log_id)
admin_reviews = list_chat_admin_reviews_by_chat_log_id(db, chat_log_id)

return AdminChatLogDetail(
chat_log_id=chat_log.chat_log_id,
Expand Down Expand Up @@ -77,6 +85,34 @@ def get_admin_chat_log_detail(db: Session, chat_log_id: int) -> AdminChatLogDeta
)
for item in error_logs
],
feedbacks=[
AdminChatFeedback(
feedback_id=item.feedback_id,
user_id=item.user_id,
feedback_type=item.feedback_type,
is_helpful=item.is_helpful,
rating=item.rating,
reason_code=item.reason_code,
feedback_comment=item.feedback_comment,
feature_type=item.feature_type,
created_at=item.created_at,
)
for item in feedbacks
],
admin_reviews=[
AdminChatAdminReview(
review_id=item.review_id,
admin_id=item.reviewer_id,
correctness_label=item.correctness_label,
citation_label=item.citation_label,
root_cause=item.root_cause,
correction_required=item.correction_required,
corrected_answer=item.corrected_answer,
review_note=item.review_note,
created_at=item.created_at,
)
for item in admin_reviews
],
)


Expand Down Expand Up @@ -147,6 +183,32 @@ def get_admin_chat_review_queue(
)


def save_admin_chat_review(
db: Session,
*,
chat_log_id: int,
request: AdminChatReviewSaveRequest,
) -> AdminChatAdminReview:
chat_log = get_chat_log_by_id(db, chat_log_id)
if chat_log is None:
raise AppException(CHAT_LOG_NOT_FOUND)

admin_review = save_chat_admin_review(
db,
chat_log_id=chat_log_id,
reviewer_id=request.admin_id,
correctness_label=request.correctness_label,
citation_label=request.citation_label,
root_cause=request.root_cause,
correction_required=request.correction_required,
corrected_answer=request.corrected_answer,
review_note=request.review_note,
)
db.commit()
db.refresh(admin_review)
return _build_admin_review_schema(admin_review)


def _is_chat_session_expired(last_activity_at: datetime, expiration_threshold: datetime) -> bool:
return last_activity_at < expiration_threshold

Expand Down Expand Up @@ -176,3 +238,17 @@ def _resolve_review_reason(answer_status) -> AdminChatReviewQueueReason:

def _get_answer_status_value(answer_status) -> str:
return getattr(answer_status, "value", answer_status)


def _build_admin_review_schema(item) -> AdminChatAdminReview:
return AdminChatAdminReview(
review_id=item.review_id,
admin_id=item.reviewer_id,
correctness_label=item.correctness_label,
citation_label=item.citation_label,
root_cause=item.root_cause,
correction_required=item.correction_required,
corrected_answer=item.corrected_answer,
review_note=item.review_note,
created_at=item.created_at,
)
Loading
Loading