"""analyze_events 테이블 ORM — POST /documents/{id}/analyze 호출 관측 (Phase E.2) 목적: 분석 failure mode 분류 (timeout / parse / llm / missing_summary) + source 별 사용 패턴 (document_server / synology_chat / ui_search / ui_detail / eval). 단계 3 snapshot DB 설계 입력이 됨. """ from datetime import datetime from typing import Any from sqlalchemy import ARRAY, BigInteger, Boolean, DateTime, Float, ForeignKey, Integer, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from core.database import Base # FK("users.id") 해석에 users 테이블 메타데이터 필요 — fastapi 앱은 어차피 전 모델을 # import 하지만, CLI 단독 실행(queue_drain 등)은 본 모듈만 끌어와 INSERT 시 # "could not find table 'users'" 로 실패했다 (2026-06-12 drain 로그 실측). 명시 import. from models.user import User # noqa: F401 class AnalyzeEvent(Base): __tablename__ = "analyze_events" id: Mapped[int] = mapped_column(BigInteger, primary_key=True) doc_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False ) user_id: Mapped[int | None] = mapped_column( BigInteger, ForeignKey("users.id", ondelete="SET NULL") ) mode: Mapped[str] = mapped_column(Text, default="quick", nullable=False) # quick / full / summary_triage / summary_deep / retrieval_select / synthesis text_limit: Mapped[int | None] = mapped_column(Integer) truncated: Mapped[bool] = mapped_column(Boolean, default=False) layers_returned: Mapped[list[Any] | None] = mapped_column(JSONB, default=list) cached: Mapped[bool] = mapped_column(Boolean, default=False) latency_ms: Mapped[int | None] = mapped_column(Integer) model_name: Mapped[str | None] = mapped_column(Text) prompt_version: Mapped[str | None] = mapped_column(Text) # None (success) | "timeout" | "llm" | "parse" | "missing_summary" | "no_text" error_code: Mapped[str | None] = mapped_column(Text) # document_server / synology_chat / ui_search / ui_detail / eval / unknown source: Mapped[str] = mapped_column(Text, default="document_server", nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=datetime.now, nullable=False ) # PR-A (migration 153) — routing shadow observability subject_domain: Mapped[str | None] = mapped_column(Text) risk_flags: Mapped[list[str] | None] = mapped_column(ARRAY(Text)) high_impact_task: Mapped[bool | None] = mapped_column(Boolean) escalated_to_26b: Mapped[bool | None] = mapped_column(Boolean) escalation_reasons: Mapped[list[str] | None] = mapped_column(ARRAY(Text)) confidence: Mapped[float | None] = mapped_column(Float) policy_violation: Mapped[bool | None] = mapped_column(Boolean) policy_violation_ids: Mapped[list[str] | None] = mapped_column(ARRAY(Text)) shadow_would_route_to: Mapped[str | None] = mapped_column(Text) policy_version: Mapped[str | None] = mapped_column(Text) # PR-B (migration 159) — 실제 호출 tier 와 R2 backlog guard 이벤트 tier: Mapped[str | None] = mapped_column(Text) # 'triage' | 'primary' | 'fallback' suppressed_reason: Mapped[str | None] = mapped_column(Text) # 'backlog_guard(ratio=0.42,pending=7)' # PR-B B-2 (migration 161) — /ask 3-state answerability 독립 컬럼 answerability: Mapped[str | None] = mapped_column(Text) # 'direct' | 'partial' | 'insufficient' partial_basis: Mapped[bool | None] = mapped_column(Boolean) # partial 답변이 실제 생성됐는지 suggested_query_count: Mapped[int | None] = mapped_column(Integer)