feat(review): 검토 대기 자동검토 워커 — 고신뢰 자동승인 + 저신뢰 잔류 #42
@@ -78,6 +78,7 @@ async def lifespan(app: FastAPI):
|
||||
from workers.tier_backfill import run as tier_backfill_run
|
||||
from workers.upload_cleanup import cleanup_orphan_uploads
|
||||
from workers.memo_draft_worker import run as memo_draft_run
|
||||
from workers.auto_review_worker import run as auto_review_run
|
||||
|
||||
# 시작: DB 연결 확인
|
||||
await init_db()
|
||||
@@ -108,6 +109,8 @@ async def lifespan(app: FastAPI):
|
||||
scheduler.add_job(cleanup_orphan_uploads, "interval", minutes=10, id="upload_cleanup")
|
||||
# P2: 메모→문서 승격분 26B 문서화 (needs_draft 마커 → md_content). 26B 콜이라 소량·2분 간격.
|
||||
scheduler.add_job(memo_draft_run, "interval", minutes=2, id="memo_draft", max_instances=1)
|
||||
# 검토 대기 자동검토: 고신뢰(ai_confidence>=0.9) 자동승인 + 저신뢰 수동 잔류. 순수 DB(LLM 없음).
|
||||
scheduler.add_job(auto_review_run, "interval", minutes=3, id="auto_review", max_instances=1)
|
||||
# PR-4: study_questions 자동 임베딩 (status='none/failed/stale' 행을 batch=10 처리).
|
||||
# 별도 큐 테이블 없이 status 자체가 큐. backfill 도 cron 이 'none' 행을 자연스럽게 처리.
|
||||
scheduler.add_job(study_q_embed_run, "interval", minutes=1, id="study_q_embed")
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""검토 대기(review_status='pending') 자동 검토 — 고신뢰 자동승인 + 저신뢰 수동 잔류.
|
||||
|
||||
classify 가 이미 부여한 ai_confidence 를 게이트로 사용 — **재-LLM 호출 없음**(대량 2천건에
|
||||
맥미니/GPU 부하 0, 분류 confidence 가 곧 AI 의 자기-신뢰도). ai_domain 보유 +
|
||||
ai_confidence >= THRESHOLD 인 pending 문서를 review_status='approved' 로 자동승인하고
|
||||
audit(source_metadata.auto_reviewed)를 남긴다. 저신뢰/미분류는 그대로 두어 수동 검토
|
||||
큐(/inbox)에 잔류.
|
||||
|
||||
설계 근거(게이트 실측):
|
||||
- review_status 는 inbox 카운트(dashboard) + 수집기 ingest 에서만 사용, 검색/RAG/digest/
|
||||
ask 경로 필터에 **미사용** → 자동승인은 노출(검색결과) 변동 없이 검토 큐만 비운다.
|
||||
- pending 2,161 중 ai_suggestion 보유 0 → 이 큐는 '분류 변경 제안'(accept_suggestion)이
|
||||
아니라 '미검토 자동분류'. 승인 = review_status 플립.
|
||||
배치·interval 점진 드레인(관찰·중단 가능). 되돌리기 = source_metadata.auto_reviewed 마커로
|
||||
대상 식별 후 review_status='pending' 복원.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.database import async_session
|
||||
from models.document import Document
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 고신뢰 자동승인 바 (튜닝 가능). 실측 분포: >=0.9 → 1,981건 자동 / 저신뢰·미분류 ~180건 수동 잔류.
|
||||
_CONFIDENCE_THRESHOLD = 0.9
|
||||
# 한 틱 처리량 — 순수 DB UPDATE(LLM 없음)라 가볍지만, 2천 행 일괄 락 회피 위해 배치.
|
||||
_BATCH = 300
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
"""pending 고신뢰 문서를 배치 자동승인 (interval job, no-arg)."""
|
||||
async with async_session() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(Document)
|
||||
.where(
|
||||
Document.review_status == "pending",
|
||||
Document.deleted_at.is_(None),
|
||||
Document.ai_domain.isnot(None),
|
||||
Document.ai_confidence.isnot(None),
|
||||
Document.ai_confidence >= _CONFIDENCE_THRESHOLD,
|
||||
)
|
||||
.order_by(Document.id)
|
||||
.limit(_BATCH)
|
||||
)
|
||||
).scalars().all()
|
||||
if not rows:
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
for doc in rows:
|
||||
doc.review_status = "approved"
|
||||
doc.source_metadata = {
|
||||
**(doc.source_metadata or {}),
|
||||
"auto_reviewed": {
|
||||
"by": "confidence_gate",
|
||||
"confidence": float(doc.ai_confidence),
|
||||
"threshold": _CONFIDENCE_THRESHOLD,
|
||||
"at": now.isoformat(),
|
||||
},
|
||||
}
|
||||
doc.updated_at = now
|
||||
await session.commit()
|
||||
logger.info(
|
||||
"auto_review: approved %d pending docs (ai_confidence >= %.2f)",
|
||||
len(rows),
|
||||
_CONFIDENCE_THRESHOLD,
|
||||
)
|
||||
Reference in New Issue
Block a user