108 lines
3.2 KiB
Python
108 lines
3.2 KiB
Python
"""ai-draft data model.
|
|
|
|
Per functional-design entities (unit: ai-draft): DraftBrief, DraftArticle,
|
|
DraftBundle, StatusMessage. Lightweight plain-data shapes owned by the
|
|
ai-draft library; per Contract 1/5 the DraftBundle is the per-article review
|
|
handoff.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Literal
|
|
|
|
ArticleKind = Literal["lead", "article", "filler", "wellwish"]
|
|
BundleStatus = Literal["drafted", "empty", "partial_error"]
|
|
|
|
|
|
@dataclass
|
|
class MastheadConfig:
|
|
"""Dynamic masthead/metadata input read from config.json (US7/Q3=A).
|
|
|
|
Owned by the generator (functional-design), but carried here as a shape the
|
|
ai-draft brief can reference for on-theme context (couple names, occasion).
|
|
"""
|
|
|
|
title: str
|
|
couple_names: str
|
|
date_line: str
|
|
issue_number: str
|
|
volume: str
|
|
|
|
|
|
@dataclass
|
|
class DraftBrief:
|
|
"""The concise input ai-draft builds and hands to the cloud model (Q4=A)."""
|
|
|
|
content_context: str
|
|
requested_article_types: list[str] = field(default_factory=lambda: ["lead", "filler"])
|
|
model: str = "deepseek-v4-flash:cloud"
|
|
tone_instruction: str = (
|
|
"Write warm, keepsake-appropriate newspaper copy for a wedding issue."
|
|
)
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.content_context.strip():
|
|
raise ValueError("content_context must be non-empty when a draft is requested")
|
|
if not self.requested_article_types:
|
|
raise ValueError("requested_article_types must contain at least one type")
|
|
|
|
|
|
@dataclass
|
|
class DraftArticle:
|
|
"""One drafted article, the per-article unit of the review handoff (Q2=A)."""
|
|
|
|
article_id: str
|
|
type: ArticleKind
|
|
headline: str
|
|
body: str
|
|
byline: str = ""
|
|
section: str = ""
|
|
|
|
def round_trip(self) -> bool:
|
|
"""All string fields round-trip without dropping/escaping (NFR7-style)."""
|
|
return (
|
|
isinstance(self.article_id, str)
|
|
and isinstance(self.type, str)
|
|
and isinstance(self.headline, str)
|
|
and isinstance(self.body, str)
|
|
and isinstance(self.byline, str)
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class StatusMessage:
|
|
"""A clear, human-readable status note (Q3=A)."""
|
|
|
|
code: str
|
|
message: str
|
|
|
|
|
|
@dataclass
|
|
class DraftBundle:
|
|
"""The per-article review handoff (Contract 5). status=empty means the model
|
|
was unreachable/failed and the generator proceeds without AI (Q3=A / BR2.1)."""
|
|
|
|
draft_bundle_id: str
|
|
articles: list[DraftArticle]
|
|
generated_at: str
|
|
status: BundleStatus = "drafted"
|
|
status_message: StatusMessage | None = None
|
|
|
|
def article(self, article_id: str) -> DraftArticle | None:
|
|
"""Return one article by id, keeping each article addressable (BR1.3)."""
|
|
return next((a for a in self.articles if a.article_id == article_id), None)
|
|
|
|
|
|
def empty_draft_bundle(
|
|
draft_bundle_id: str, generated_at: str, code: str, message: str
|
|
) -> DraftBundle:
|
|
"""A fail-soft empty DraftBundle with a clear StatusMessage (BR2.1, Q3=A)."""
|
|
return DraftBundle(
|
|
draft_bundle_id=draft_bundle_id,
|
|
articles=[],
|
|
generated_at=generated_at,
|
|
status="empty",
|
|
status_message=StatusMessage(code=code, message=message),
|
|
)
|