from abc import ABC, abstractmethod from typing import Tuple from sqlalchemy import and_, select, update from sqlalchemy.ext.asyncio import AsyncSession from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import faqs from common.enums import ErrorType, FactStatus from common.logger import LOG from common.utils.gtime import GTime _PUBLISHABLE = (FactStatus.VERIFIED.value, FactStatus.CORRECTED.value) _ACTIVE = (FactStatus.UNVERIFIED.value, FactStatus.PENDING_OWNER.value) + _PUBLISHABLE # FAQ CRUD. fact 와 같은 검증 상태 흐름을 탄다 — 생성된 문장도 사람이 확인해야 나간다. class IFaqCRUD(ABC): @abstractmethod async def list_faqs(self, cdb: AsyncSession, place_id, publishable_only: bool) -> Tuple[ErrorType, list]: pass @abstractmethod async def add_faq(self, cdb: AsyncSession, faq: faqs) -> ErrorType: pass @abstractmethod async def expire_generated(self, cdb: AsyncSession, place_id, ts) -> Tuple[ErrorType, int]: pass @abstractmethod async def transition(self, cdb: AsyncSession, faq_id, from_statuses, to_status, data: dict) -> Tuple[ErrorType, int]: pass @abstractmethod async def get_faq(self, cdb: AsyncSession, place_id, faq_id) -> Tuple[ErrorType, faqs]: pass class FaqCRUD(IFaqCRUD): async def list_faqs(self, cdb: AsyncSession, place_id, publishable_only: bool = False) -> Tuple[ErrorType, list]: try: conds = [faqs.place_id == place_id, faqs.deleted == False] # noqa: E712 conds.append(faqs.status.in_(_PUBLISHABLE if publishable_only else _ACTIVE)) query = select(faqs).where(and_(*conds)).order_by(faqs.sort_order.asc(), faqs.created_at.asc()) err_type, rows = await DB_SESSION_MNG.execute(cdb, query) return (err_type, list(rows) if err_type == ErrorType.SUCCESS else []) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, [] async def get_faq(self, cdb: AsyncSession, place_id, faq_id) -> Tuple[ErrorType, faqs]: try: query = select(faqs).where( faqs.faq_id == faq_id, faqs.place_id == place_id, faqs.deleted == False # noqa: E712 ).limit(1) err_type, rows = await DB_SESSION_MNG.execute(cdb, query) if err_type != ErrorType.SUCCESS: return err_type, None if len(rows) != 1: return ErrorType.DB_INVALID_KEY, None return ErrorType.SUCCESS, rows[0] except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, None async def add_faq(self, cdb: AsyncSession, faq: faqs) -> ErrorType: try: return await DB_SESSION_MNG.insert(cdb, faq) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED async def expire_generated(self, cdb: AsyncSession, place_id, ts) -> Tuple[ErrorType, int]: """재생성 전에 **아직 확인 안 된** LLM 생성 FAQ 만 내린다. ★ 사람이 확인·정정한 FAQ(VERIFIED/CORRECTED)는 건드리지 않는다 — 재생성이 사람의 판단을 덮어쓰면 fact 쪽 규칙과 어긋난다.""" try: query = ( update(faqs) .where( faqs.place_id == place_id, faqs.deleted == False, # noqa: E712 faqs.status.in_((FactStatus.UNVERIFIED.value, FactStatus.PENDING_OWNER.value)), ) .values(status=FactStatus.EXPIRED.value, updated_at=ts) ) return await DB_SESSION_MNG.add_with_rowcount(cdb, query) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, 0 async def transition(self, cdb: AsyncSession, faq_id, from_statuses, to_status: int, data: dict) -> Tuple[ErrorType, int]: try: query = ( update(faqs) .where(faqs.faq_id == faq_id, faqs.status.in_(tuple(from_statuses)), faqs.deleted == False) # noqa: E712 .values(status=to_status, updated_at=GTime.UTC(), **data) ) return await DB_SESSION_MNG.add_with_rowcount(cdb, query) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, 0