""" 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