feat(fastapi): changes presentation generation flow for documents to chunk it and extract as outlines
This commit is contained in:
parent
089e620482
commit
ca72a652ee
21 changed files with 163 additions and 286 deletions
|
|
@ -7,7 +7,10 @@ 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, SSEResponse, SSEStatusResponse
|
||||
from services import TEMP_FILE_SERVICE
|
||||
from services.database import get_async_session
|
||||
from services.documents_loader import DocumentsLoader
|
||||
from services.score_based_chunker import ScoreBasedChunker
|
||||
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
|
||||
|
||||
OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"])
|
||||
|
|
@ -22,38 +25,59 @@ async def stream_outlines(
|
|||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
|
||||
|
||||
async def inner():
|
||||
yield SSEStatusResponse(
|
||||
status="Generating presentation outlines..."
|
||||
).to_string()
|
||||
|
||||
presentation_content_text = ""
|
||||
async for chunk in generate_ppt_outline(
|
||||
presentation.prompt,
|
||||
presentation.n_slides,
|
||||
presentation.language,
|
||||
presentation.summary,
|
||||
):
|
||||
# Give control to the event loop
|
||||
await asyncio.sleep(0)
|
||||
presentation_outlines = None
|
||||
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 = documents[0]
|
||||
chunker = ScoreBasedChunker()
|
||||
try:
|
||||
chunks = await chunker.get_n_chunks(
|
||||
documents[0], presentation.n_slides
|
||||
)
|
||||
presentation_outlines = PresentationOutlineModel(
|
||||
slides=[chunk.to_slide_outline() for chunk in chunks]
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
yield SSEResponse(
|
||||
event="response",
|
||||
data=json.dumps({"type": "chunk", "chunk": chunk}),
|
||||
).to_string()
|
||||
presentation_content_text += chunk
|
||||
if not presentation_outlines:
|
||||
presentation_outlines_text = ""
|
||||
async for chunk in generate_ppt_outline(
|
||||
presentation.prompt,
|
||||
presentation.n_slides,
|
||||
presentation.language,
|
||||
additional_context,
|
||||
):
|
||||
# Give control to the event loop
|
||||
await asyncio.sleep(0)
|
||||
|
||||
presentation_content_json = json.loads(presentation_content_text)
|
||||
yield SSEResponse(
|
||||
event="response",
|
||||
data=json.dumps({"type": "chunk", "chunk": chunk}),
|
||||
).to_string()
|
||||
presentation_outlines_text += chunk
|
||||
|
||||
presentation_content = PresentationOutlineModel(**presentation_content_json)
|
||||
presentation_content.slides = presentation_content.slides[
|
||||
presentation_outlines_json = json.loads(presentation_outlines_text)
|
||||
presentation_outlines = PresentationOutlineModel(
|
||||
**presentation_outlines_json
|
||||
)
|
||||
|
||||
presentation_outlines.slides = presentation_outlines.slides[
|
||||
: presentation.n_slides
|
||||
]
|
||||
|
||||
presentation.title = presentation_content.title
|
||||
presentation.outlines = [
|
||||
each.model_dump() for each in presentation_content.slides
|
||||
]
|
||||
presentation.outlines = presentation_outlines.model_dump()
|
||||
|
||||
sql_session.add(presentation)
|
||||
await sql_session.commit()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import asyncio
|
|||
import json
|
||||
import os
|
||||
import random
|
||||
import importlib
|
||||
from typing import Annotated, List, Literal, Optional
|
||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
|
@ -12,14 +11,12 @@ from sqlmodel import select
|
|||
from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES
|
||||
from models.presentation_and_path import PresentationPathAndEditPath
|
||||
from models.presentation_from_template import GetPresentationUsingTemplateRequest
|
||||
from models.presentation_outline_model import (
|
||||
PresentationOutlineModel,
|
||||
SlideOutlineModel,
|
||||
)
|
||||
from models.presentation_outline_model import PresentationOutlineModel
|
||||
from models.pptx_models import PptxPresentationModel
|
||||
from models.presentation_layout import PresentationLayoutModel
|
||||
from models.presentation_structure_model import PresentationStructureModel
|
||||
from models.presentation_with_slides import PresentationWithSlides
|
||||
from services.score_based_chunker import ScoreBasedChunker
|
||||
from utils.get_layout_by_name import get_layout_by_name
|
||||
from services.icon_finder_service import IconFinderService
|
||||
from services.image_generation_service import ImageGenerationService
|
||||
|
|
@ -34,7 +31,6 @@ from services.documents_loader import DocumentsLoader
|
|||
from models.sql.presentation import PresentationModel
|
||||
from services.pptx_presentation_creator import PptxPresentationCreator
|
||||
from utils.asset_directory_utils import get_exports_directory, get_images_directory
|
||||
from utils.llm_calls.generate_document_summary import generate_document_summary
|
||||
from utils.llm_calls.generate_presentation_structure import (
|
||||
generate_presentation_structure,
|
||||
)
|
||||
|
|
@ -113,20 +109,12 @@ async def create_presentation(
|
|||
):
|
||||
presentation_id = get_random_uuid()
|
||||
|
||||
summary = None
|
||||
if file_paths:
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(presentation_id)
|
||||
documents_loader = DocumentsLoader(file_paths=file_paths)
|
||||
await documents_loader.load_documents(temp_dir)
|
||||
|
||||
summary = await generate_document_summary(documents_loader.documents)
|
||||
|
||||
presentation = PresentationModel(
|
||||
id=presentation_id,
|
||||
prompt=prompt,
|
||||
n_slides=n_slides,
|
||||
language=language,
|
||||
summary=summary,
|
||||
file_paths=file_paths,
|
||||
)
|
||||
|
||||
sql_session.add(presentation)
|
||||
|
|
@ -138,7 +126,7 @@ async def create_presentation(
|
|||
@PRESENTATION_ROUTER.post("/prepare", response_model=PresentationModel)
|
||||
async def prepare_presentation(
|
||||
presentation_id: Annotated[str, Body()],
|
||||
outlines: Annotated[List[SlideOutlineModel], Body()],
|
||||
outlines: Annotated[List[str], Body()],
|
||||
layout: Annotated[PresentationLayoutModel, Body()],
|
||||
title: Annotated[Optional[str], Body()] = None,
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
|
|
@ -173,7 +161,7 @@ async def prepare_presentation(
|
|||
presentation_structure.slides[index] = random_slide_index
|
||||
|
||||
sql_session.add(presentation)
|
||||
presentation.outlines = [each.model_dump() for each in outlines]
|
||||
presentation.outlines = PresentationOutlineModel(slides=outlines).model_dump()
|
||||
presentation.title = title or presentation.title
|
||||
presentation.set_layout(layout)
|
||||
presentation.set_structure(presentation_structure)
|
||||
|
|
@ -328,37 +316,48 @@ async def generate_presentation_api(
|
|||
|
||||
presentation_id = get_random_uuid()
|
||||
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
|
||||
|
||||
# 1. Save uploaded files
|
||||
file_paths = []
|
||||
if files:
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
|
||||
for upload in files:
|
||||
file_path = os.path.join(temp_dir, upload.filename)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(await upload.read())
|
||||
file_paths.append(file_path)
|
||||
|
||||
# 2. Create Presentation Summary (if documents are provided)
|
||||
summary = None
|
||||
# 3. Generate Outlines
|
||||
presentation_outlines = None
|
||||
additional_context = ""
|
||||
if file_paths:
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(presentation_id)
|
||||
documents_loader = DocumentsLoader(file_paths=file_paths)
|
||||
await documents_loader.load_documents(temp_dir)
|
||||
summary = await generate_document_summary(documents_loader.documents)
|
||||
documents = documents_loader.documents
|
||||
if documents:
|
||||
additional_context = documents[0]
|
||||
chunker = ScoreBasedChunker()
|
||||
try:
|
||||
chunks = await chunker.get_n_chunks(documents[0], n_slides)
|
||||
presentation_outlines = PresentationOutlineModel(
|
||||
slides=[chunk.to_slide_outline() for chunk in chunks]
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# 3. Generate Outlines
|
||||
presentation_content_text = ""
|
||||
async for chunk in generate_ppt_outline(
|
||||
prompt,
|
||||
n_slides,
|
||||
language,
|
||||
summary,
|
||||
):
|
||||
presentation_content_text += chunk
|
||||
if not presentation_outlines:
|
||||
presentation_outlines_text = ""
|
||||
async for chunk in generate_ppt_outline(
|
||||
prompt,
|
||||
n_slides,
|
||||
language,
|
||||
additional_context,
|
||||
):
|
||||
presentation_outlines_text += chunk
|
||||
|
||||
presentation_content_json = json.loads(presentation_content_text)
|
||||
presentation_content = PresentationOutlineModel(**presentation_content_json)
|
||||
outlines = presentation_content.slides[:n_slides]
|
||||
presentation_outlines_json = json.loads(presentation_outlines_text)
|
||||
presentation_outlines = PresentationOutlineModel(**presentation_outlines_json)
|
||||
outlines = presentation_outlines.slides[:n_slides]
|
||||
total_outlines = len(outlines)
|
||||
|
||||
print("-" * 40)
|
||||
|
|
@ -374,11 +373,8 @@ async def generate_presentation_api(
|
|||
else:
|
||||
presentation_structure: PresentationStructureModel = (
|
||||
await generate_presentation_structure(
|
||||
presentation_outline=PresentationOutlineModel(
|
||||
title=presentation_content.title,
|
||||
slides=outlines,
|
||||
),
|
||||
presentation_layout=layout_model,
|
||||
presentation_outlines,
|
||||
layout_model,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -397,9 +393,7 @@ async def generate_presentation_api(
|
|||
prompt=prompt,
|
||||
n_slides=n_slides,
|
||||
language=language,
|
||||
title=presentation_content.title,
|
||||
summary=summary,
|
||||
outlines=[each.model_dump() for each in outlines],
|
||||
outlines=presentation_outlines.model_dump(),
|
||||
layout=layout_model.model_dump(),
|
||||
structure=presentation_structure.model_dump(),
|
||||
)
|
||||
|
|
@ -445,7 +439,7 @@ async def generate_presentation_api(
|
|||
|
||||
# 9. Export
|
||||
presentation_and_path = await export_presentation(
|
||||
presentation_id, presentation_content.title, export_as
|
||||
presentation_id, presentation.title or get_random_uuid(), export_as
|
||||
)
|
||||
|
||||
return PresentationPathAndEditPath(
|
||||
|
|
@ -482,7 +476,7 @@ async def from_template(
|
|||
await sql_session.commit()
|
||||
|
||||
presentation_and_path = await export_presentation(
|
||||
new_presentation.id, new_presentation.title, data.export_as
|
||||
new_presentation.id, new_presentation.title or get_random_uuid(), data.export_as
|
||||
)
|
||||
|
||||
return PresentationPathAndEditPath(
|
||||
|
|
|
|||
Binary file not shown.
11
servers/fastapi/models/document_chunk.py
Normal file
11
servers/fastapi/models/document_chunk.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DocumentChunk(BaseModel):
|
||||
heading: str
|
||||
content: str
|
||||
heading_index: int
|
||||
score: float
|
||||
|
||||
def to_slide_outline(self) -> str:
|
||||
return f"{self.heading}\n{self.content}"
|
||||
|
|
@ -1,31 +1,13 @@
|
|||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SlideOutlineModel(BaseModel):
|
||||
title: str = Field(
|
||||
description="Title of the slide in about 3 to 5 words",
|
||||
)
|
||||
body: str = Field(
|
||||
description="Content of the slide in markdown format",
|
||||
)
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PresentationOutlineModel(BaseModel):
|
||||
title: str = Field(
|
||||
description="Title of the presentation in about 3 to 8 words",
|
||||
)
|
||||
slides: List[SlideOutlineModel] = Field(description="List of slides")
|
||||
slides: List[str]
|
||||
|
||||
def to_string(self):
|
||||
message = f"# Presentation Title: {self.title} \n\n"
|
||||
message = ""
|
||||
for i, slide in enumerate(self.slides):
|
||||
message += f"## Slide {i+1}:\n"
|
||||
message += f" - Title: {slide.title} \n"
|
||||
message += f" - Body: {slide.body} \n"
|
||||
|
||||
# if self.notes:
|
||||
# message += f"# Notes: \n"
|
||||
# for note in self.notes:
|
||||
# message += f" - {note} \n"
|
||||
message += f" - Content: {slide} \n"
|
||||
return message
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
|||
from pydantic import BaseModel
|
||||
|
||||
from models.presentation_layout import PresentationLayoutModel
|
||||
from models.presentation_outline_model import SlideOutlineModel
|
||||
from models.presentation_outline_model import PresentationOutlineModel
|
||||
from models.presentation_structure_model import PresentationStructureModel
|
||||
from models.sql.presentation import PresentationModel
|
||||
from models.sql.slide import SlideModel
|
||||
|
|
@ -16,9 +16,7 @@ class PresentationWithSlides(BaseModel):
|
|||
n_slides: int
|
||||
language: str
|
||||
title: Optional[str] = None
|
||||
notes: Optional[List[str]]
|
||||
outlines: Optional[List[SlideOutlineModel]]
|
||||
summary: Optional[str]
|
||||
outlines: Optional[PresentationOutlineModel]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
layout: Optional[PresentationLayoutModel]
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ from sqlalchemy import JSON, Column, DateTime
|
|||
from sqlmodel import SQLModel, Field
|
||||
|
||||
from models.presentation_layout import PresentationLayoutModel
|
||||
from models.presentation_outline_model import (
|
||||
PresentationOutlineModel,
|
||||
SlideOutlineModel,
|
||||
)
|
||||
from models.presentation_outline_model import PresentationOutlineModel
|
||||
from models.presentation_structure_model import PresentationStructureModel
|
||||
from utils.randomizers import get_random_uuid
|
||||
|
||||
|
|
@ -18,9 +15,8 @@ class PresentationModel(SQLModel, table=True):
|
|||
n_slides: int
|
||||
language: str
|
||||
title: Optional[str] = None
|
||||
notes: Optional[List[str]] = Field(sa_column=Column(JSON), default=None)
|
||||
outlines: Optional[List[dict]] = Field(sa_column=Column(JSON), default=None)
|
||||
summary: Optional[str] = None
|
||||
file_paths: Optional[List[str]] = Field(sa_column=Column(JSON), default=None)
|
||||
outlines: Optional[dict] = Field(sa_column=Column(JSON), default=None)
|
||||
created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now))
|
||||
updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now))
|
||||
layout: Optional[dict] = Field(sa_column=Column(JSON), default=None)
|
||||
|
|
@ -33,9 +29,8 @@ class PresentationModel(SQLModel, table=True):
|
|||
n_slides=self.n_slides,
|
||||
language=self.language,
|
||||
title=self.title,
|
||||
notes=self.notes,
|
||||
file_paths=self.file_paths,
|
||||
outlines=self.outlines,
|
||||
summary=self.summary,
|
||||
layout=self.layout,
|
||||
structure=self.structure,
|
||||
)
|
||||
|
|
@ -43,11 +38,7 @@ class PresentationModel(SQLModel, table=True):
|
|||
def get_presentation_outline(self):
|
||||
if not self.outlines:
|
||||
return None
|
||||
return PresentationOutlineModel(
|
||||
title=self.title,
|
||||
slides=[SlideOutlineModel(**each) for each in self.outlines],
|
||||
# notes=self.notes,
|
||||
)
|
||||
return PresentationOutlineModel(**self.outlines)
|
||||
|
||||
def get_layout(self):
|
||||
return PresentationLayoutModel(**self.layout)
|
||||
|
|
|
|||
|
|
@ -55,37 +55,22 @@ huggingface-hub==0.34.3
|
|||
humanfriendly==10.0
|
||||
idna==3.10
|
||||
imageio==2.37.0
|
||||
<<<<<<< HEAD
|
||||
importlib_metadata==8.7.0
|
||||
importlib_resources==6.5.2
|
||||
Jinja2==3.1.6
|
||||
jiter==0.10.0
|
||||
=======
|
||||
importlib-metadata==8.7.0
|
||||
importlib-resources==6.5.2
|
||||
jinja2==3.1.6
|
||||
jiter==0.10.0
|
||||
joblib==1.5.1
|
||||
>>>>>>> main
|
||||
jsonlines==3.1.0
|
||||
jsonref==1.1.0
|
||||
jsonschema==4.25.0
|
||||
jsonschema-specifications==2025.4.1
|
||||
kubernetes==33.1.0
|
||||
latex2mathml==3.78.0
|
||||
<<<<<<< HEAD
|
||||
lazy_loader==0.4
|
||||
lxml==5.4.0
|
||||
markdown-it-py==3.0.0
|
||||
marko==2.1.4
|
||||
MarkupSafe==3.0.2
|
||||
=======
|
||||
lazy-loader==0.4
|
||||
lxml==5.4.0
|
||||
markdown-it-py==3.0.0
|
||||
marko==2.1.4
|
||||
markupsafe==3.0.1
|
||||
>>>>>>> main
|
||||
mdurl==0.1.2
|
||||
mmh3==5.2.0
|
||||
mpire==2.10.2
|
||||
|
|
@ -94,20 +79,12 @@ multidict==6.6.3
|
|||
multiprocess==0.70.18
|
||||
networkx==3.5
|
||||
ninja==1.11.1.4
|
||||
<<<<<<< HEAD
|
||||
numpy==2.2.6
|
||||
oauthlib==3.3.1
|
||||
onnxruntime==1.22.1
|
||||
openai==1.98.0
|
||||
opencv-python-headless==4.12.0.88
|
||||
=======
|
||||
nltk==3.9.1
|
||||
numpy==2.3.2
|
||||
oauthlib==3.3.1
|
||||
onnxruntime==1.22.1
|
||||
openai==1.98.0
|
||||
opencv-python-headless==4.11.0.86
|
||||
>>>>>>> main
|
||||
openpyxl==3.1.5
|
||||
opentelemetry-api==1.36.0
|
||||
opentelemetry-exporter-otlp-proto-common==1.36.0
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import asyncio
|
|||
from typing import List
|
||||
import nltk
|
||||
|
||||
from models.document_chunk import DocumentChunk
|
||||
|
||||
try:
|
||||
nltk.data.find("tokenizers/punkt")
|
||||
except LookupError:
|
||||
|
|
@ -104,7 +106,7 @@ class ScoreBasedChunker:
|
|||
|
||||
def get_chunks(
|
||||
self, sentences: List[str], sentences_scores: List[float], top_k: int = 10
|
||||
) -> List[dict]:
|
||||
) -> List[DocumentChunk]:
|
||||
if not sentences_scores:
|
||||
sentences_scores = self.score_sentences_for_heading(sentences)
|
||||
|
||||
|
|
@ -175,16 +177,16 @@ class ScoreBasedChunker:
|
|||
content_sentences = sentences[heading_idx + 1 : content_end]
|
||||
content = " ".join(content_sentences).strip()
|
||||
|
||||
chunk = {
|
||||
"heading": heading,
|
||||
"content": content,
|
||||
"heading_index": heading_idx,
|
||||
"score": sentences_scores[heading_idx],
|
||||
}
|
||||
chunk = DocumentChunk(
|
||||
heading=heading,
|
||||
content=content,
|
||||
heading_index=heading_idx,
|
||||
score=sentences_scores[heading_idx],
|
||||
)
|
||||
chunks.append(chunk)
|
||||
return chunks
|
||||
|
||||
async def get_n_chunks(self, text: str, n: int) -> List[dict]:
|
||||
async def get_n_chunks(self, text: str, n: int) -> List[DocumentChunk]:
|
||||
sentences = await asyncio.to_thread(self.extract_sentences, text, n)
|
||||
sentences_scores = await asyncio.to_thread(
|
||||
self.score_sentences_for_heading, sentences
|
||||
|
|
|
|||
|
|
@ -1,29 +1,15 @@
|
|||
from typing import List, Optional
|
||||
from typing import List
|
||||
from pydantic import Field
|
||||
from models.presentation_outline_model import (
|
||||
PresentationOutlineModel,
|
||||
SlideOutlineModel,
|
||||
)
|
||||
from models.presentation_outline_model import PresentationOutlineModel
|
||||
from models.presentation_structure_model import PresentationStructureModel
|
||||
|
||||
|
||||
class SlideOutlineModelWithValidation(SlideOutlineModel):
|
||||
title: str = Field(
|
||||
description="Title of the slide in about 3 to 5 words",
|
||||
min_length=10,
|
||||
max_length=50,
|
||||
)
|
||||
|
||||
|
||||
def get_presentation_outline_model_with_n_slides(n_slides: int):
|
||||
class PresentationOutlineModelWithNSlides(PresentationOutlineModel):
|
||||
title: str = Field(
|
||||
description="Title of the presentation in about 3 to 8 words",
|
||||
min_length=10,
|
||||
max_length=50,
|
||||
)
|
||||
slides: List[SlideOutlineModelWithValidation] = Field(
|
||||
description="List of slides", min_items=n_slides, max_items=n_slides
|
||||
slides: List[str] = Field(
|
||||
description="Markdown content for each slide",
|
||||
min_items=n_slides,
|
||||
max_items=n_slides,
|
||||
)
|
||||
|
||||
return PresentationOutlineModelWithNSlides
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import asyncio
|
||||
from typing import List
|
||||
|
||||
from models.llm_message import LLMMessage
|
||||
from services.llm_client import LLMClient
|
||||
from utils.llm_provider import get_model
|
||||
|
||||
|
||||
sysmte_prompt = """
|
||||
Generate a blog-style summary of the provided document in **more than 2000 words**.
|
||||
Maintain as much information as possible.
|
||||
|
||||
### Output Format
|
||||
|
||||
- Provide the summary in a **blog format** with an **engaging introduction** and a **clear structure**.
|
||||
- Ensure the **logical flow** of the document is preserved.
|
||||
|
||||
### Notes
|
||||
|
||||
- **Retain the main ideas and essential details** from the document.
|
||||
- **Show line-breaks** clearly.
|
||||
- If **slides structure is mentioned** in document, structure the summary in the same way.
|
||||
"""
|
||||
|
||||
|
||||
async def generate_document_summary(documents: List[str]):
|
||||
client = LLMClient()
|
||||
model = get_model()
|
||||
|
||||
coroutines = []
|
||||
for document in documents:
|
||||
truncated_text = document[:200000]
|
||||
coroutine = client.generate(
|
||||
model=model,
|
||||
messages=[
|
||||
LLMMessage(role="system", content=sysmte_prompt),
|
||||
LLMMessage(role="user", content=truncated_text),
|
||||
],
|
||||
)
|
||||
coroutines.append(coroutine)
|
||||
|
||||
completions: List[str] = await asyncio.gather(*coroutines)
|
||||
combined = "\n\n\n\n".join(completions)
|
||||
return combined
|
||||
|
|
@ -7,42 +7,12 @@ from utils.get_dynamic_models import get_presentation_outline_model_with_n_slide
|
|||
from utils.llm_provider import get_model
|
||||
|
||||
system_prompt = """
|
||||
You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content.
|
||||
You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content.
|
||||
|
||||
## Core Requirements
|
||||
|
||||
### Input Processing
|
||||
1. **Extract key information** from the user's prompt:
|
||||
- Main topic/subject matter
|
||||
- Required number of slides
|
||||
- Target language for output
|
||||
- Specific content requirements or focus areas
|
||||
- Target audience (if specified)
|
||||
- Presentation style or tone preferences
|
||||
|
||||
|
||||
## Content Generation Guidelines
|
||||
|
||||
### Presentation Title
|
||||
- Create a **concise, descriptive title** that captures the essence of the topic
|
||||
- Use **plain text format** (no markdown formatting)
|
||||
- Make it **engaging and professional**
|
||||
- Ensure it reflects the main theme and target audience
|
||||
|
||||
### Slide Titles
|
||||
- Generate **clear, specific titles** for each slide
|
||||
- Use **plain text format** (no markdown, no "Slide 1", "Slide 2" prefixes)
|
||||
- Make each title **descriptive and informative**
|
||||
- Ensure titles create a **logical flow** through the presentation
|
||||
- Keep titles **concise but meaningful**
|
||||
|
||||
|
||||
## Special Considerations
|
||||
|
||||
### Slide Count Compliance
|
||||
- Generate **exactly** the number of slides requested
|
||||
- Distribute content **evenly** across slides
|
||||
- Create **balanced information flow**
|
||||
- Provide content for each slide in markdown format.
|
||||
- Make sure that flow of the presentation is logical and consistent.
|
||||
- If Additional Information is provided, divide it into slides.
|
||||
- Make sure that content follows language guidelines.
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
from models.llm_message import LLMMessage
|
||||
from models.presentation_layout import SlideLayoutModel
|
||||
from models.presentation_outline_model import SlideOutlineModel
|
||||
from services.llm_client import LLMClient
|
||||
from utils.llm_provider import get_model
|
||||
from utils.schema_utils import remove_fields_from_schema
|
||||
|
||||
system_prompt = """
|
||||
Generate structured slide based on provided title and outline, follow mentioned steps and notes and provide structured output.
|
||||
Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output.
|
||||
|
||||
# Steps
|
||||
1. Analyze the outline and title.
|
||||
2. Generate structured slide based on the outline and title.
|
||||
1. Analyze the outline.
|
||||
2. Generate structured slide based on the outline.
|
||||
|
||||
# Notes
|
||||
- Slide body should not use words like "This slide", "This presentation".
|
||||
- Rephrase the slide body to make it flow naturally.
|
||||
- Provide prompt to generate image on "__image_prompt__" property.
|
||||
- Provide query to search icon on "__icon_query__" property.
|
||||
- Do not use markdown formatting in slide body.
|
||||
- Only use markdown to highlight important points.
|
||||
- Make sure to follow language guidelines.
|
||||
**Strictly follow the max and min character limit for every property in the slide.**
|
||||
"""
|
||||
|
||||
|
||||
def get_user_prompt(title: str, outline: str, language: str):
|
||||
def get_user_prompt(outline: str, language: str):
|
||||
return f"""
|
||||
## Icon Query And Image Prompt Language
|
||||
English
|
||||
|
|
@ -31,15 +30,12 @@ def get_user_prompt(title: str, outline: str, language: str):
|
|||
## Slide Content Language
|
||||
{language}
|
||||
|
||||
## Slide Title
|
||||
{title}
|
||||
|
||||
## Slide Outline
|
||||
{outline}
|
||||
"""
|
||||
|
||||
|
||||
def get_messages(title: str, outline: str, language: str):
|
||||
def get_messages(outline: str, language: str):
|
||||
|
||||
return [
|
||||
LLMMessage(
|
||||
|
|
@ -48,13 +44,13 @@ def get_messages(title: str, outline: str, language: str):
|
|||
),
|
||||
LLMMessage(
|
||||
role="user",
|
||||
content=get_user_prompt(title, outline, language),
|
||||
content=get_user_prompt(outline, language),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def get_slide_content_from_type_and_outline(
|
||||
slide_layout: SlideLayoutModel, outline: SlideOutlineModel, language: str
|
||||
slide_layout: SlideLayoutModel, outline: str, language: str
|
||||
):
|
||||
client = LLMClient()
|
||||
model = get_model()
|
||||
|
|
@ -66,8 +62,7 @@ async def get_slide_content_from_type_and_outline(
|
|||
response = await client.generate_structured(
|
||||
model=model,
|
||||
messages=get_messages(
|
||||
outline.title,
|
||||
outline.body,
|
||||
outline,
|
||||
language,
|
||||
),
|
||||
response_format=response_schema,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { useEditor, EditorContent } from "@tiptap/react"
|
||||
import StarterKit from "@tiptap/starter-kit"
|
||||
import { Markdown } from "tiptap-markdown"
|
||||
import { useEffect } from "react"
|
||||
|
||||
|
||||
export default function MarkdownEditor({ content, onChange }: { content: string; onChange: (content: string) => void }) {
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit, Markdown],
|
||||
content: content,
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@ import {
|
|||
} from "@dnd-kit/sortable";
|
||||
import { OutlineItem } from "./OutlineItem";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SlideOutline } from "@/store/slices/presentationGeneration";
|
||||
import { FileText } from "lucide-react";
|
||||
|
||||
interface OutlineContentProps {
|
||||
outlines: SlideOutline[] | null;
|
||||
outlines: string[] | null;
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean;
|
||||
onDragEnd: (event: any) => void;
|
||||
|
|
@ -33,6 +32,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
|
|||
onDragEnd,
|
||||
onAddSlide
|
||||
}) => {
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
|
|
@ -84,12 +84,12 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
|
|||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={outlines?.map((item, index) => ({ id: item.title || `slide-${index}` })) || []}
|
||||
items={outlines?.map((item, index) => ({ id: `slide-${index}` })) || []}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{outlines?.map((item, index) => (
|
||||
<OutlineItem
|
||||
key={item.title || `slide-${index}`}
|
||||
key={`slide-${index}`}
|
||||
index={index + 1}
|
||||
slideOutline={item}
|
||||
isStreaming={isStreaming}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import { CSS } from "@dnd-kit/utilities"
|
|||
import { Trash2 } from "lucide-react"
|
||||
import { RootState } from "@/store/store"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import { deleteSlideOutline, setOutlines, SlideOutline } from "@/store/slices/presentationGeneration"
|
||||
import { deleteSlideOutline, setOutlines } from "@/store/slices/presentationGeneration"
|
||||
import ToolTip from "@/components/ToolTip"
|
||||
import MarkdownEditor from "../../components/MarkdownEditor"
|
||||
import { useEffect } from "react"
|
||||
|
||||
|
||||
interface OutlineItemProps {
|
||||
slideOutline: SlideOutline,
|
||||
slideOutline: string,
|
||||
index: number
|
||||
isStreaming: boolean
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ export function OutlineItem({
|
|||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (isStreaming && slideOutline.body) {
|
||||
if (isStreaming && slideOutline) {
|
||||
const outlineItem = document.getElementById(`outline-item-${index}`);
|
||||
if (outlineItem) {
|
||||
outlineItem.scrollIntoView({
|
||||
|
|
@ -38,7 +38,7 @@ export function OutlineItem({
|
|||
}
|
||||
}, [outlines.length]);
|
||||
|
||||
const handleSlideChange = (newOutline: SlideOutline) => {
|
||||
const handleSlideChange = (newOutline: string) => {
|
||||
if (isStreaming) return;
|
||||
const newData = outlines?.map((each, idx) => {
|
||||
if (idx === index - 1) {
|
||||
|
|
@ -60,7 +60,7 @@ export function OutlineItem({
|
|||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: slideOutline.title || index })
|
||||
} = useSortable({ id: index })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
|
|
@ -96,24 +96,16 @@ export function OutlineItem({
|
|||
|
||||
{/* Main Title Input - Add onFocus handler */}
|
||||
<div id={`outline-item-${index}`} className="flex flex-col basis-full gap-2">
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={slideOutline.title || ''}
|
||||
onBlur={(e) => handleSlideChange({ ...slideOutline, title: e.target.value })}
|
||||
className="text-lg mt-4 sm:text-xl flex-1 font-semibold bg-transparent outline-none"
|
||||
placeholder="Title goes here"
|
||||
/>
|
||||
|
||||
{/* Editable Markdown Content */}
|
||||
{isStreaming ? <textarea
|
||||
defaultValue={slideOutline.body || ''}
|
||||
onBlur={(e) => handleSlideChange({ ...slideOutline, body: e.target.value })}
|
||||
defaultValue={slideOutline || ''}
|
||||
onBlur={(e) => handleSlideChange(e.target.value)}
|
||||
className="text-sm flex-1 font-normal bg-transparent outline-none overflow-y-hidden"
|
||||
placeholder="Content goes here"
|
||||
/> : <MarkdownEditor
|
||||
key={index}
|
||||
content={slideOutline.body || ''}
|
||||
onChange={(content) => handleSlideChange({ ...slideOutline, body: content })}
|
||||
content={slideOutline || ''}
|
||||
onChange={(content) => handleSlideChange(content)}
|
||||
/>}
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ const OutlinePage: React.FC = () => {
|
|||
selectedLayoutGroup,
|
||||
setActiveTab
|
||||
);
|
||||
|
||||
if (!presentation_id) {
|
||||
return <EmptyStateView />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { toast } from "sonner";
|
||||
import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration";
|
||||
import { setOutlines } from "@/store/slices/presentationGeneration";
|
||||
import { jsonrepair } from "jsonrepair";
|
||||
import { StreamState } from "../types/index";
|
||||
import { RootState } from "@/store/store";
|
||||
|
|
@ -49,7 +49,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
|
||||
case "complete":
|
||||
try {
|
||||
const outlinesData: SlideOutline[] = data.presentation.outlines;
|
||||
const outlinesData: string[] = data.presentation.outlines.slides;
|
||||
dispatch(setOutlines(outlinesData));
|
||||
setStreamState({ isStreaming: false, isLoading: false });
|
||||
eventSource.close();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const usePresentationData = (
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const fetchUserSlides = useCallback(async () => {
|
||||
console.log("fetching user slides inside usePresentationData");
|
||||
try {
|
||||
const data = await DashboardApi.getPresentation(presentationId);
|
||||
if (data) {
|
||||
|
|
@ -26,9 +27,9 @@ export const usePresentationData = (
|
|||
}
|
||||
}, [presentationId, dispatch, setLoading, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserSlides();
|
||||
}, [fetchUserSlides]);
|
||||
// useEffect(() => {
|
||||
// fetchUserSlides();
|
||||
// }, [fetchUserSlides]);
|
||||
|
||||
return {
|
||||
fetchUserSlides,
|
||||
|
|
|
|||
|
|
@ -102,7 +102,10 @@ export const usePresentationStreaming = (
|
|||
if (stream) {
|
||||
initializeStream();
|
||||
} else {
|
||||
console.log("stream is null", stream);
|
||||
console.log("presentationData", presentationData);
|
||||
if(!presentationData || presentationData.slides.length === 0){
|
||||
console.log("fetching user slides");
|
||||
fetchUserSlides();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||
|
||||
|
||||
|
||||
export interface SlideOutline {
|
||||
title?: string;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface PresentationData {
|
||||
|
|
@ -26,7 +23,7 @@ interface PresentationGenerationState {
|
|||
presentation_id: string | null;
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean | null;
|
||||
outlines: SlideOutline[];
|
||||
outlines: string[];
|
||||
error: string | null;
|
||||
presentationData: PresentationData | null;
|
||||
isSlidesRendered: boolean;
|
||||
|
|
@ -63,7 +60,7 @@ const presentationGenerationSlice = createSlice({
|
|||
state.presentation_id = action.payload;
|
||||
state.error = null;
|
||||
},
|
||||
// Slides rendered
|
||||
// Slides rendereimport { useEffect } from "react"d
|
||||
setSlidesRendered: (state, action: PayloadAction<boolean>) => {
|
||||
state.isSlidesRendered = action.payload;
|
||||
},
|
||||
|
|
@ -80,7 +77,7 @@ const presentationGenerationSlice = createSlice({
|
|||
state.outlines = [];
|
||||
},
|
||||
// Set outlines
|
||||
setOutlines: (state, action: PayloadAction<SlideOutline[]>) => {
|
||||
setOutlines: (state, action: PayloadAction<string[]>) => {
|
||||
state.outlines = action.payload;
|
||||
},
|
||||
// Set presentation data
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue