first commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user