From 135a8a442d0e1f493c8658d01a15b4e63462d5c6 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Thu, 17 Jul 2025 00:27:49 +0545 Subject: [PATCH] feat(fastapi): adds get presentation from id endpoint --- servers/fastapi/api/__init__.py | 0 .../api/v1/ppt/endpoints/presentation.py | 16 +++++ servers/fastapi/constants/__init__.py | 0 servers/fastapi/enums/__init__.py | 0 servers/fastapi/models/__init__.py | 0 servers/fastapi/models/sql/slide.py | 1 + servers/fastapi/requirements.txt | 4 ++ servers/fastapi/tests/__init__.py | 0 .../tests/test_gemini_schema_support.py | 70 +++++++++++++++++++ .../tests/test_openai_schema_support.py | 66 +++++++++++++++++ servers/fastapi/utils/__init__.py | 0 .../generate_presentation_structure.py | 13 ---- servers/fastapi/utils/llm_provider.py | 10 ++- 13 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 servers/fastapi/api/__init__.py create mode 100644 servers/fastapi/constants/__init__.py create mode 100644 servers/fastapi/enums/__init__.py create mode 100644 servers/fastapi/models/__init__.py create mode 100644 servers/fastapi/tests/__init__.py create mode 100644 servers/fastapi/tests/test_gemini_schema_support.py create mode 100644 servers/fastapi/tests/test_openai_schema_support.py create mode 100644 servers/fastapi/utils/__init__.py diff --git a/servers/fastapi/api/__init__.py b/servers/fastapi/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index 8d91cf6e..d812ad8a 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -4,6 +4,7 @@ from typing import Annotated, List, Optional import uuid from fastapi import APIRouter, Body, HTTPException from fastapi.responses import StreamingResponse +from sqlmodel import select from models.presentation_outline_model import SlideOutlineModel from models.presentation_layout import PresentationLayoutModel @@ -144,6 +145,7 @@ async def stream_presentation(presentation_id: str): slide = SlideModel( presentation=presentation_id, layout=slide_layout.id, + index=i, content=slide_content, ) slides.append(slide) @@ -176,3 +178,17 @@ async def stream_presentation(presentation_id: str): ).to_string() return StreamingResponse(inner(), media_type="text/event-stream") + + +@PRESENTATION_ROUTER.get("/", response_model=PresentationWithSlides) +def get_presentation(id: str): + with get_sql_session() as sql_session: + presentation = sql_session.get(PresentationModel, id) + slides = sql_session.exec( + select(SlideModel).where(SlideModel.presentation == id) + ) + slides = sorted(slides, key=lambda x: x.index) + return PresentationWithSlides( + **presentation.model_dump(), + slides=slides, + ) diff --git a/servers/fastapi/constants/__init__.py b/servers/fastapi/constants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/servers/fastapi/enums/__init__.py b/servers/fastapi/enums/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/servers/fastapi/models/__init__.py b/servers/fastapi/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/servers/fastapi/models/sql/slide.py b/servers/fastapi/models/sql/slide.py index 00576197..68d9bdf0 100644 --- a/servers/fastapi/models/sql/slide.py +++ b/servers/fastapi/models/sql/slide.py @@ -7,4 +7,5 @@ class SlideModel(SQLModel, table=True): id: str = Field(primary_key=True, default_factory=get_random_uuid) presentation: str layout: str + index: int content: dict = Field(sa_column=Column(JSON)) diff --git a/servers/fastapi/requirements.txt b/servers/fastapi/requirements.txt index 6e946e6e..3edb9b66 100644 --- a/servers/fastapi/requirements.txt +++ b/servers/fastapi/requirements.txt @@ -26,6 +26,7 @@ httpcore==1.0.9 httptools==0.6.4 httpx==0.28.1 idna==3.10 +iniconfig==2.1.0 Jinja2==3.1.6 jiter==0.10.0 lxml==6.0.0 @@ -34,10 +35,12 @@ MarkupSafe==3.0.2 mdurl==0.1.2 multidict==6.6.3 openai==1.95.1 +packaging==25.0 pathvalidate==3.3.1 pdfminer.six==20250506 pdfplumber==0.11.7 pillow==11.3.0 +pluggy==1.6.0 propcache==0.3.2 pyasn1==0.6.1 pyasn1_modules==0.4.2 @@ -46,6 +49,7 @@ pydantic==2.11.7 pydantic_core==2.33.2 Pygments==2.19.2 pypdfium2==4.30.1 +pytest==8.4.1 python-docx==1.2.0 python-dotenv==1.1.1 python-multipart==0.0.20 diff --git a/servers/fastapi/tests/__init__.py b/servers/fastapi/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/servers/fastapi/tests/test_gemini_schema_support.py b/servers/fastapi/tests/test_gemini_schema_support.py new file mode 100644 index 00000000..44844cd2 --- /dev/null +++ b/servers/fastapi/tests/test_gemini_schema_support.py @@ -0,0 +1,70 @@ +import json +from typing import Optional +from pydantic import BaseModel, Field +from google.genai.types import GenerateContentResponse, GenerateContentConfig + + +from utils.llm_provider import get_google_llm_client, get_large_model + + +class HeadingDescription(BaseModel): + heading: str = Field( + description="Heading of the slide", min_length=10, max_length=20 + ) + description: str = Field( + description="Description of the slide", min_length=40, max_length=200 + ) + + +class SlideContentTest(BaseModel): + title: str = Field(description="Title of the slide", min_length=10, max_length=20) + first_content: HeadingDescription = Field(description="First content of the slide") + second_content: HeadingDescription = Field( + description="Second content of the slide" + ) + third_content: HeadingDescription = Field(description="Third content of the slide") + + +class ColumnContentModel(BaseModel): + title: str = Field(min_length=3, max_length=100, description="Column title") + content: str = Field(min_length=10, max_length=800, description="Column content") + + +class TwoColumnSlideModel(BaseModel): + title: str = Field( + min_length=3, + max_length=100, + description="Title of the slide", + ) + subtitle: Optional[str] = Field( + min_length=3, + max_length=150, + description="Optional subtitle or description", + ) + leftColumn: ColumnContentModel = Field( + description="Left column content", + ) + rightColumn: ColumnContentModel = Field( + description="Right column content", + ) + backgroundImage: Optional[str] = Field( + description="URL to background image for the slide" + ) + + +def test_gemini_schema_support(): + response: GenerateContentResponse = get_google_llm_client().models.generate_content( + model=get_large_model(), + contents=[ + "Generate a slide for a presentation", + "The slide should have a title and two contents", + "The title should be a short title for the slide", + "The contents should be a heading and a description", + "The heading should be a short heading for the slide", + ], + config=GenerateContentConfig( + response_mime_type="application/json", + response_schema=TwoColumnSlideModel.model_json_schema(), + ), + ) + print(response.parsed) diff --git a/servers/fastapi/tests/test_openai_schema_support.py b/servers/fastapi/tests/test_openai_schema_support.py new file mode 100644 index 00000000..f7d00b75 --- /dev/null +++ b/servers/fastapi/tests/test_openai_schema_support.py @@ -0,0 +1,66 @@ +import asyncio +import json +from typing import Optional +from pydantic import BaseModel, Field + + +from utils.llm_provider import get_llm_client, get_large_model + + +class HeadingDescription(BaseModel): + heading: str = Field( + description="Heading of the slide", min_length=10, max_length=20 + ) + description: str = Field( + description="Description of the slide", min_length=40, max_length=200 + ) + + +class SlideContentTest(BaseModel): + title: str = Field(description="Title of the slide", min_length=10, max_length=20) + first_content: HeadingDescription = Field(description="First content of the slide") + second_content: HeadingDescription = Field( + description="Second content of the slide" + ) + third_content: HeadingDescription = Field(description="Third content of the slide") + + +class ColumnContentModel(BaseModel): + title: str = Field(min_length=3, max_length=100, description="Column title") + content: str = Field(min_length=10, max_length=800, description="Column content") + + +class TwoColumnSlideModel(BaseModel): + title: str = Field( + min_length=3, + max_length=100, + description="Title of the slide", + ) + subtitle: Optional[str] = Field( + min_length=3, + max_length=150, + description="Optional subtitle or description", + ) + leftColumn: ColumnContentModel = Field( + description="Left column content", + ) + rightColumn: ColumnContentModel = Field( + description="Right column content", + ) + backgroundImage: Optional[str] = Field( + description="URL to background image for the slide" + ) + + +def test_openai_schema_support(): + response = asyncio.run( + get_llm_client().beta.chat.completions.parse( + model=get_large_model(), + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Generate a slide for a presentation"}, + ], + response_format=TwoColumnSlideModel, + ) + ) + print(response.choices[0].message.parsed) diff --git a/servers/fastapi/utils/__init__.py b/servers/fastapi/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/servers/fastapi/utils/llm_calls/generate_presentation_structure.py b/servers/fastapi/utils/llm_calls/generate_presentation_structure.py index 34880943..85deb581 100644 --- a/servers/fastapi/utils/llm_calls/generate_presentation_structure.py +++ b/servers/fastapi/utils/llm_calls/generate_presentation_structure.py @@ -13,19 +13,6 @@ def get_prompt(presentation_layout: PresentationLayoutModel, n_slides: int, data return [ { "role": "system", - # "content": f""" - # You're a professional presentation designer with years of experience in designing clear and engaging presentations. - # {presentation_layout.to_string()} - # # Steps - # 1. Analyze provided Number of slides, Presentation title, Slides content and Presentation Layout. - # 2. Select appropriate slide layout **index** for each slide. - # # Notes - # - Slide layout should be selected based on provided content for slide and notes. - # - Don't fall into patterns like always using layout 2 and after layout 1. - # - Each presentation should have its own unique flow and rhythm. - # - Select type for {n_slides} slides. - # **Go through notes and steps and make sure they are all followed. Rule breaks are strictly not allowed.** - # """, "content": f""" You're a professional presentation designer. {presentation_layout.to_string()} diff --git a/servers/fastapi/utils/llm_provider.py b/servers/fastapi/utils/llm_provider.py index 9f333c5e..bb069773 100644 --- a/servers/fastapi/utils/llm_provider.py +++ b/servers/fastapi/utils/llm_provider.py @@ -1,5 +1,5 @@ -from http.client import HTTPException import os +from fastapi import HTTPException from openai import AsyncOpenAI from google import genai @@ -15,7 +15,13 @@ from utils.get_env import ( def get_llm_provider(): - return LLMProvider(get_llm_provider_env()) + try: + return LLMProvider(get_llm_provider_env()) + except: + raise HTTPException( + status_code=500, + detail=f"Invalid LLM provider. Please select one of: openai, google, ollama, custom", + ) def get_ollama_url():