import asyncio import json import math import traceback import uuid import dirtyjson from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from models.presentation_outline_model import PresentationOutlineModel from models.sql.presentation import PresentationModel from models.sse_response import ( SSECompleteResponse, SSEErrorResponse, SSEResponse, SSEStatusResponse, ) from services.temp_file_service import TEMP_FILE_SERVICE from services.database import get_async_session, async_session_maker from services.documents_loader import DocumentsLoader from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline from utils.ppt_utils import get_presentation_title_from_outlines OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"]) @OUTLINES_ROUTER.get("/stream/{id}") async def stream_outlines( id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session) ): presentation = await sql_session.get(PresentationModel, id) if not presentation: raise HTTPException(status_code=404, detail="Presentation not found") # Capture all data from the presentation before the endpoint returns # (Depends session closes when endpoint function returns, before StreamingResponse runs) presentation_id = presentation.id presentation_content = presentation.content presentation_file_paths = presentation.file_paths presentation_n_slides = presentation.n_slides presentation_language = presentation.language presentation_tone = presentation.tone presentation_verbosity = presentation.verbosity presentation_instructions = presentation.instructions presentation_include_title_slide = presentation.include_title_slide presentation_include_table_of_contents = presentation.include_table_of_contents presentation_web_search = presentation.web_search temp_dir = TEMP_FILE_SERVICE.create_temp_dir() async def inner(): yield SSEStatusResponse( status="Generating presentation outlines..." ).to_string() additional_context = "" if presentation_file_paths: documents_loader = DocumentsLoader(file_paths=presentation_file_paths) await documents_loader.load_documents(temp_dir) documents = documents_loader.documents if documents: additional_context = "\n\n".join(documents) presentation_outlines_text = "" n_slides_to_generate = presentation_n_slides if presentation_include_table_of_contents: needed_toc_count = math.ceil((presentation_n_slides - 1) / 10) n_slides_to_generate -= math.ceil( (presentation_n_slides - needed_toc_count) / 10 ) async for chunk in generate_ppt_outline( presentation_content, n_slides_to_generate, presentation_language, additional_context, presentation_tone, presentation_verbosity, presentation_instructions, presentation_include_title_slide, presentation_web_search, ): # Give control to the event loop await asyncio.sleep(0) if isinstance(chunk, HTTPException): yield SSEErrorResponse(detail=chunk.detail).to_string() return yield SSEResponse( event="response", data=json.dumps({"type": "chunk", "chunk": chunk}), ).to_string() presentation_outlines_text += chunk try: presentation_outlines_json = dict( dirtyjson.loads(presentation_outlines_text) ) # Fix: LLM sometimes returns slides as JSON string instead of list if "slides" in presentation_outlines_json and isinstance(presentation_outlines_json["slides"], str): print("[OUTLINE] Warning: slides field is a string, parsing as JSON...") presentation_outlines_json["slides"] = dirtyjson.loads(presentation_outlines_json["slides"]) except Exception as e: traceback.print_exc() yield SSEErrorResponse( detail=f"Failed to generate presentation outlines. Please try again. {str(e)}", ).to_string() return presentation_outlines = PresentationOutlineModel(**presentation_outlines_json) presentation_outlines.slides = presentation_outlines.slides[ :n_slides_to_generate ] outlines_data = presentation_outlines.model_dump() title = get_presentation_title_from_outlines(presentation_outlines) # Use a fresh session for the DB write — Depends session is already closed async with async_session_maker() as db: pres = await db.get(PresentationModel, presentation_id) if pres: pres.outlines = outlines_data pres.title = title db.add(pres) await db.commit() await db.refresh(pres) yield SSECompleteResponse( key="presentation", value=pres.model_dump(mode="json") ).to_string() else: yield SSEErrorResponse(detail="Presentation not found after outline generation").to_string() return StreamingResponse(inner(), media_type="text/event-stream")