first commit

This commit is contained in:
jsahn
2026-09-02 14:57:29 +09:00
commit c0b140d27f
60 changed files with 7520 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
"""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)