"""Brief Router — API endpoints for content brief v2. POST /api/v1/content/briefs Create brief GET /api/v1/content/briefs/{id} Get brief POST /api/v1/content/briefs/{id}/validate Validate fields POST /api/v1/content/briefs/{id}/submit Submit for approval POST /api/v1/content/briefs/{id}/approve Approve (Matt only) POST /api/v1/content/briefs/{id}/reject Reject (Matt only) GET /api/v1/content/briefs/{id}/output Get generated content """ import uuid import logging from datetime import datetime from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from sqlalchemy.orm import Session from sqlalchemy import desc from ...database import get_db from ..models.brief import ContentBrief from ..validation.brief import validate_brief from ..notifications.telegram import ( notify_brief_pending, notify_brief_approved, notify_brief_rejected, notify_author_brief_submitted, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/content/briefs", tags=["content-briefs"]) # --- Request/Response Models --- class BriefCreateRequest(BaseModel): title: str = Field(..., min_length=10, max_length=200) struggle_angle: str = Field(..., min_length=50) origin_story: str = Field(..., min_length=100) attempts: List[dict] = Field(..., min_length=2) the_moment: str = Field(..., min_length=50) the_fix: str = Field(..., min_length=50) reflection: str = Field(..., min_length=50) voice_checklist: dict = Field(default_factory=dict) style_reference: Optional[str] = None target_length: Optional[int] = Field(None, ge=500, le=5000) class BriefResponse(BaseModel): id: str title: str struggle_angle: str origin_story: str attempts: List[dict] the_moment: str the_fix: str reflection: str voice_checklist: dict status: str style_reference: Optional[str] target_length: Optional[int] created_by: str created_at: str approved_at: Optional[str] approved_by: Optional[str] struggle_score: Optional[float] class BriefValidationRequest(BaseModel): field: Optional[str] = None value: Optional[str] = None class BriefValidationResponse(BaseModel): valid: bool errors: List[dict] warnings: List[dict] voice_score: int class BriefApproveRequest(BaseModel): approved_by: str class BriefRejectRequest(BaseModel): reason: str # --- Authentication Helper --- def get_current_user(request: Request) -> str: """Extract current user from request.""" # For now, use header or default user = request.headers.get("X-User-Id", "anonymous") return user def require_matt(user_id: str) -> bool: """Check if user is Matt or Aundrea.""" # Matt's Telegram ID and Aundrea's (if known) allowed = ["8386527252", "matt", "aundrea"] return user_id in allowed # --- Endpoints --- @router.post("", response_model=BriefResponse) async def create_brief( request: BriefCreateRequest, db: Session = Depends(get_db), current_user: str = Depends(get_current_user), ): """Create a new content brief (draft status).""" brief_id = str(uuid.uuid4()) # Validate first validation = validate_brief(request.dict()) if not validation["valid"]: raise HTTPException( status_code=400, detail={ "message": "Brief validation failed", "errors": validation["errors"] } ) brief = ContentBrief( id=brief_id, title=request.title, struggle_angle=request.struggle_angle, origin_story=request.origin_story, attempts=request.attempts, the_moment=request.the_moment, the_fix=request.the_fix, reflection=request.reflection, voice_checklist=request.voice_checklist, status="draft", style_reference=request.style_reference, target_length=request.target_length, created_by=current_user, ) db.add(brief) db.commit() db.refresh(brief) return brief.to_dict() @router.get("/{brief_id}", response_model=BriefResponse) async def get_brief( brief_id: str, db: Session = Depends(get_db), ): """Get a brief by ID.""" brief = db.query(ContentBrief).filter(ContentBrief.id == brief_id).first() if not brief: raise HTTPException(status_code=404, detail="Brief not found") return brief.to_dict() @router.post("/{brief_id}/validate", response_model=BriefValidationResponse) async def validate_brief_endpoint( brief_id: str, request: Request, db: Session = Depends(get_db), ): """Validate a brief (real-time validation).""" body = await request.json() validation = validate_brief(body) return BriefValidationResponse(**validation) @router.post("/{brief_id}/submit") async def submit_brief( brief_id: str, db: Session = Depends(get_db), current_user: str = Depends(get_current_user), ): """Submit brief for approval (sends Telegram to Matt).""" brief = db.query(ContentBrief).filter(ContentBrief.id == brief_id).first() if not brief: raise HTTPException(status_code=404, detail="Brief not found") if brief.status != "draft": raise HTTPException( status_code=400, detail=f"Brief already in status: {brief.status}" ) # Update status brief.status = "pending" db.commit() # Send Telegram notification to Matt notify_brief_pending( brief_id=brief.id, title=brief.title, struggle_angle=brief.struggle_angle, created_by=brief.created_by ) # Notify author # Note: In real implementation, get author's chat_id from user system notify_author_brief_submitted( chat_id=current_user, # This would be actual chat_id title=brief.title ) return {"status": "pending", "message": "Brief submitted for approval"} @router.post("/{brief_id}/approve") async def approve_brief( brief_id: str, request: BriefApproveRequest, db: Session = Depends(get_db), current_user: str = Depends(get_current_user), ): """Approve brief and trigger generation (Matt/Aundrea only).""" if not require_matt(current_user): raise HTTPException(status_code=403, detail="Only household can approve") brief = db.query(ContentBrief).filter(ContentBrief.id == brief_id).first() if not brief: raise HTTPException(status_code=404, detail="Brief not found") if brief.status != "pending": raise HTTPException( status_code=400, detail=f"Brief must be pending, current status: {brief.status}" ) # Update status brief.status = "approved" brief.approved_by = request.approved_by brief.approved_at = datetime.utcnow() db.commit() # Notify notify_brief_approved( brief_id=brief.id, title=brief.title, approved_by=request.approved_by ) # TODO: Trigger actual generation (Phase 1) # For now, just mark as approved return {"status": "approved", "message": "Brief approved, generation starting"} @router.post("/{brief_id}/reject") async def reject_brief( brief_id: str, request: BriefRejectRequest, db: Session = Depends(get_db), current_user: str = Depends(get_current_user), ): """Reject brief with feedback (Matt/Aundrea only).""" if not require_matt(current_user): raise HTTPException(status_code=403, detail="Only household can reject") brief = db.query(ContentBrief).filter(ContentBrief.id == brief_id).first() if not brief: raise HTTPException(status_code=404, detail="Brief not found") if brief.status != "pending": raise HTTPException( status_code=400, detail=f"Brief must be pending, current status: {brief.status}" ) # Update status brief.status = "rejected" db.commit() # Notify notify_brief_rejected( brief_id=brief.id, title=brief.title, reason=request.reason ) return {"status": "rejected", "message": "Brief rejected with feedback"} @router.get("/{brief_id}/output") async def get_brief_output( brief_id: str, db: Session = Depends(get_db), ): """Get generated content for a brief.""" brief = db.query(ContentBrief).filter(ContentBrief.id == brief_id).first() if not brief: raise HTTPException(status_code=404, detail="Brief not found") if brief.status != "completed": raise HTTPException( status_code=400, detail=f"Content not ready, status: {brief.status}" ) return { "brief_id": brief.id, "title": brief.title, "content": brief.content_output, "struggle_score": brief.struggle_score, } @router.get("") async def list_briefs( status: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: str = Depends(get_current_user), ): """List briefs, optionally filtered by status.""" query = db.query(ContentBrief) if status: query = query.filter(ContentBrief.status == status) # Non-Matt users only see their own briefs if not require_matt(current_user): query = query.filter(ContentBrief.created_by == current_user) briefs = query.order_by(desc(ContentBrief.created_at)).all() return { "briefs": [b.to_dict() for b in briefs], "count": len(briefs) }