129 lines
5.4 KiB
Python
129 lines
5.4 KiB
Python
"""Shared data models. Every module talks to the others through these types only."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class PaperSummary(BaseModel):
|
|
"""Per-paper Korean summary derived from the abstract (generated once per DOI)."""
|
|
|
|
tldr: str = Field(description="한 줄 요약 (한 문장)")
|
|
problem: str = Field(description="다루는 문제 1~2문장")
|
|
approach: str = Field(description="방법·접근 1~2문장")
|
|
findings: str = Field(description="결과 1~2문장 - 초록에 있는 내용만, 저자 주장은 주장으로 표기")
|
|
implication: str = Field(description="실무 시사점 1문장 - 초록 근거 내로 한정")
|
|
|
|
|
|
class PaperSummaryItem(PaperSummary):
|
|
"""One entry of a batch summarisation reply, keyed back to its paper by DOI."""
|
|
|
|
doi: str = Field(description="논문 목록의 DOI를 그대로 복사")
|
|
|
|
|
|
class PaperSummaryBatch(BaseModel):
|
|
"""LLM output schema for one batch of per-paper summaries."""
|
|
|
|
summaries: list[PaperSummaryItem] = Field(description="목록의 모든 논문에 대해 하나씩")
|
|
|
|
|
|
class Paper(BaseModel):
|
|
"""One collected OA paper (abstract-level metadata only in Phase 1)."""
|
|
|
|
doi: str # normalised bare DOI, lowercase (e.g. "10.1145/1234567")
|
|
title: str
|
|
abstract: str = ""
|
|
authors: list[str] = Field(default_factory=list)
|
|
venue: str = ""
|
|
published_date: date | None = None
|
|
oa_url: str | None = None
|
|
pdf_url: str | None = None
|
|
topics: list[str] = Field(default_factory=list)
|
|
source: str = "openalex" # collector key ("openalex" | "rss:<이름>")
|
|
summary: PaperSummary | None = None # filled by the per-paper summarisation step
|
|
|
|
|
|
class WeekPayload(BaseModel):
|
|
"""Everything collected for one weekly digest, before LLM summarisation."""
|
|
|
|
week: str # ISO week id, e.g. "2026-W36" (the digest idempotency key)
|
|
week_start: date # Monday
|
|
week_end: date # Sunday
|
|
generated_at: datetime # tz-aware (Asia/Seoul)
|
|
papers: list[Paper] = Field(default_factory=list)
|
|
data_gaps: list[str] = Field(default_factory=list) # collectors that failed / partial
|
|
|
|
def paper_by_doi(self) -> dict[str, Paper]:
|
|
return {p.doi: p for p in self.papers}
|
|
|
|
|
|
class Highlight(BaseModel):
|
|
"""One featured paper: problem / approach / findings / practical implication."""
|
|
|
|
doi: str = Field(description="논문 목록의 DOI를 그대로 복사")
|
|
title: str = Field(description="논문 원제 그대로 (영문 유지)")
|
|
problem: str = Field(description="다루는 문제 1~2문장")
|
|
approach: str = Field(description="방법·접근 1~2문장")
|
|
findings: str = Field(description="결과 1~2문장 - 초록에 있는 내용만, 저자 주장은 주장으로 표기")
|
|
implication: str = Field(description="실무 시사점 1문장 - 초록 근거 내로 한정")
|
|
|
|
|
|
class TopicPaper(BaseModel):
|
|
doi: str = Field(description="논문 목록의 DOI를 그대로 복사")
|
|
title: str = Field(description="논문 원제 그대로")
|
|
note: str = Field(description="논문 요약 1~2문장")
|
|
|
|
|
|
class TopicGroup(BaseModel):
|
|
topic: str = Field(description="주제명 (예: LLM4SE, 테스팅·분석, 실증 연구)")
|
|
papers: list[TopicPaper] = Field(description="이 주제의 논문들")
|
|
|
|
|
|
class Pick(BaseModel):
|
|
doi: str = Field(description="논문 목록의 DOI를 그대로 복사")
|
|
title: str = Field(description="논문 원제 그대로")
|
|
reason: str = Field(description="읽어볼 이유 1~2문장")
|
|
|
|
|
|
class Digest(BaseModel):
|
|
"""LLM output schema. Used verbatim as the JSON Schema for structured output."""
|
|
|
|
overview: str = Field(description="이번 주 흐름 총평 2~3문장")
|
|
highlights: list[Highlight] = Field(description="가장 주목할 논문 최대 3편")
|
|
by_topic: list[TopicGroup] = Field(description="주제별 그룹 - 모든 논문을 정확히 한 그룹에 배치")
|
|
worth_reading: list[Pick] = Field(description="읽어볼 논문 픽 1~3편")
|
|
data_gaps: list[str] = Field(default_factory=list, description="수집 실패로 반영하지 못한 항목")
|
|
|
|
|
|
class VerificationIssue(BaseModel):
|
|
claim: str = Field(description="문제가 된 다이제스트 속 서술 (원문 그대로 인용)")
|
|
problem: str = Field(description="제공된 초록·메타데이터와 어떻게 다른지 한 문장")
|
|
severity: Literal["minor", "major"] = Field(
|
|
default="minor",
|
|
description="major: 초록과 모순되거나 초록에 없는 수치·결과를 사실처럼 서술 / minor: 근거 불충분·표현 문제",
|
|
)
|
|
|
|
|
|
class Verification(BaseModel):
|
|
"""LLM fact-check verdict for a generated Digest, judged only against the payload."""
|
|
|
|
ok: bool = Field(description="major 이슈가 하나도 없으면 true")
|
|
issues: list[VerificationIssue] = Field(default_factory=list, description="발견된 문제 목록 (없으면 빈 배열)")
|
|
|
|
@property
|
|
def has_major(self) -> bool:
|
|
return any(i.severity == "major" for i in self.issues)
|
|
|
|
|
|
class DigestResult(BaseModel):
|
|
payload: WeekPayload
|
|
digest: Digest | None
|
|
markdown: str
|
|
model_used: str | None = None
|
|
degraded: bool = False # True when the LLM failed (or --no-llm) and only the paper list was produced
|
|
error: str | None = None
|
|
verification: Verification | None = None # None: not run (disabled / degraded / verifier failed)
|