📄 models.py 4,781 bytes Apr 22, 2026 📋 Raw

"""
Pydantic models for the tiered content generation pipeline.
"""

from datetime import datetime
from enum import Enum
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field

class ContentType(str, Enum):
"""Types of content the pipeline can generate."""
ROUNDUP = "roundup"
HOW_I_SOLVED = "how_i_solved"
BUILD_LOG = "build_log"
ESSAY = "essay"
TUTORIAL = "tutorial"

class PipelineStage(str, Enum):
"""Stages in the content generation pipeline."""
QUEUED = "queued"
STRATEGY = "strategy" # Llama 3.1 8B - brief + angle
STRUCTURE = "structure" # Qwen 2.5 7B - validate angle
DRAFT = "draft" # Phi-4 14B - full draft (~90s)
SEO = "seo" # Llama 3.1 8B - excerpt, tags, meta
COMPLIANCE = "compliance" # Python - strip banned words, flag dates
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"

class JobStatus(BaseModel):
"""Status of a pipeline job."""
job_id: str
status: PipelineStage
stage_number: int = Field(default=0, description="Current stage number (0-5)")
total_stages: int = Field(default=5)
progress_percent: int = Field(default=0, ge=0, le=100)
current_stage_name: Optional[str] = None
estimated_seconds_remaining: Optional[int] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
error: Optional[str] = None

class GenerateRequest(BaseModel):
"""Request to start content generation."""
topic: str = Field(..., description="Topic or title idea")
content_type: ContentType = Field(default=ContentType.HOW_I_SOLVED)
outline: Optional[List[str]] = Field(default=None, description="Optional bullet points")
context: Optional[str] = Field(default=None, description="Additional context or notes")

class GenerateResponse(BaseModel):
"""Immediate response after queuing generation."""
job_id: str
status: PipelineStage
estimated_seconds: int = Field(default=90, description="Estimated total time")

class StrategyOutput(BaseModel):
"""Output from Stage 1: Strategy (Llama 3.1 8B)."""
title: str
angle: str
target_audience: str
key_takeaways: List[str]
tone_notes: str

class StruggleFirstBrief(BaseModel):
"""V2: Struggle-first content brief."""
struggle_angle: str = Field(..., description="What broke — the human moment")
origin_story: str = Field(..., description="Why you were trying this")
attempts: List[Dict[str, str]] = Field(..., description="Failed attempts with why they failed")
the_moment: str = Field(..., description="The realization or breaking point")
the_fix: str = Field(..., description="What worked, with caveats")
reflection: str = Field(..., description="Honest reflection, not preachy")
target_length: int = Field(default=1200)

class StructureOutput(BaseModel):
"""Output from Stage 2: Structure (Qwen 2.5 7B)."""
validated: bool
structure: List[str] # Section headers / flow
estimated_word_count: int
concerns: Optional[List[str]] = None # Issues with the angle

class DraftOutput(BaseModel):
"""Output from Stage 3: Draft (Phi-4 14B)."""
content: str # Full markdown draft
word_count: int
reading_time_minutes: int

class SEOOutput(BaseModel):
"""Output from Stage 4: SEO (Llama 3.1 8B)."""
excerpt: str
tags: List[str]
meta_description: str
keywords: List[str]

class ComplianceReport(BaseModel):
"""Output from Stage 5: Compliance filter."""
replacements_made: int
warnings: List[str]
is_compliant: bool
banned_words_found: List[str]
dates_flagged: List[str]
names_flagged: List[str]

class PipelineResult(BaseModel):
"""Final result of the complete pipeline."""
title: str
content: str
excerpt: str
tags: List[str]
meta_description: str
word_count: int
reading_time_minutes: int
compliance: ComplianceReport

class Config:
    from_attributes = True

class JobDetailResponse(BaseModel):
"""Full job status including partial or complete results."""
job_id: str
status: PipelineStage
stage_number: int
total_stages: int
progress_percent: int
current_stage_name: Optional[str]
estimated_seconds_remaining: Optional[int]
started_at: Optional[datetime]
completed_at: Optional[datetime]
error: Optional[str]
result: Optional[PipelineResult] = None
partial_outputs: Optional[Dict[str, Any]] = Field(
default=None,
description="Intermediate outputs from completed stages"
)

class CancelResponse(BaseModel):
"""Response after attempting to cancel a job."""
job_id: str
was_cancelled: bool
message: str