Based on PPTAgent (EMNLP 2025) and DocPres research findings:
1. Brief summarization (summarize_brief.py)
- For content >800 chars: single LLM call extracts {overview, sections[{title,
key_points, data_points}]} before outline generation
- Prevents "lost middle" context loss in long documents
- BriefStructure.to_outline_context() formats sections for outline prompt
- BriefStructure.get_section_text(idx) returns targeted excerpt per slide
2. Section attribution in SlideOutlineModel
- Added source_section_idx: Optional[int] field
- LLM sets this during outline generation to map each slide → brief section
- Used to pass targeted section text to per-slide content generation
instead of full brief (reduces hallucination, improves accuracy)
3. Narrative continuity in slide content generation
- prev_slide_title passed to each content generation call
- Injected in user prompt: "ensure this slide continues naturally from..."
- Batch-safe: titles collected from completed batch before next starts
4. Source section text in content generation
- source_section_text parameter added to get_slide_content_from_type_and_outline
- Injected as "Source Material for This Slide" in user prompt
- Only data points present in the excerpt should be used
5. Richer layout catalog
- PresentationLayoutModel.to_catalog_string() added
- Includes field names + maxLength constraints alongside layout descriptions
- Helps LLM make informed layout choices based on content type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
255 lines
9 KiB
Python
255 lines
9 KiB
Python
from datetime import datetime
|
|
from typing import Optional
|
|
from models.llm_message import LLMSystemMessage, LLMUserMessage
|
|
from models.presentation_layout import SlideLayoutModel
|
|
from models.presentation_outline_model import SlideOutlineModel
|
|
from services.llm_client import LLMClient
|
|
from utils.llm_client_error_handler import handle_llm_client_exceptions
|
|
from utils.llm_provider import get_model
|
|
from utils.schema_utils import add_field_in_schema, remove_fields_from_schema
|
|
|
|
|
|
def get_system_prompt(
|
|
tone: Optional[str] = None,
|
|
verbosity: Optional[str] = None,
|
|
instructions: Optional[str] = None,
|
|
brand_context: Optional[str] = None,
|
|
attachment_context: Optional[str] = None,
|
|
):
|
|
brand_section = ""
|
|
if brand_context:
|
|
brand_section = f"""
|
|
## Brand Guidelines
|
|
{brand_context}
|
|
Ensure all generated text follows these brand voice and tone guidelines.
|
|
"""
|
|
|
|
attachment_section = ""
|
|
if attachment_context:
|
|
attachment_section = f"""
|
|
## Attachment Data
|
|
The following data from attachments (tables, charts) is available for this slide. Use it directly — do not invent data points.
|
|
{attachment_context}
|
|
"""
|
|
|
|
return f"""
|
|
Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output.
|
|
|
|
You are extracting and restructuring content from a brief. Do NOT invent facts, statistics, or claims not present in the source material or attachment data.
|
|
|
|
{"# User Instructions:" if instructions else ""}
|
|
{instructions or ""}
|
|
|
|
{"# Tone:" if tone else ""}
|
|
{tone or ""}
|
|
|
|
{"# Verbosity:" if verbosity else ""}
|
|
{verbosity or ""}
|
|
|
|
{brand_section}
|
|
|
|
{attachment_section}
|
|
|
|
# Steps
|
|
1. Analyze the outline.
|
|
2. Generate structured slide based on the outline.
|
|
3. Generate speaker note that is simple, clear, concise and to the point.
|
|
|
|
# Notes
|
|
- Slide body should not use words like "This slide", "This presentation".
|
|
- Rephrase the slide body to make it flow naturally.
|
|
- Only use markdown to highlight important points.
|
|
- Make sure to follow language guidelines.
|
|
- Speaker note should be normal text, not markdown.
|
|
- Strictly follow the max and min character limit for every property in the slide.
|
|
- Never ever go over the max character limit. Limit your narration to make sure you never go over the max character limit.
|
|
- Number of items should not be more than max number of items specified in slide schema. If you have to put multiple points then merge them to obey max numebr of items.
|
|
- Generate content as per the given tone.
|
|
- Be very careful with number of words to generate for given field. As generating more than max characters will overflow in the design. So, analyze early and never generate more characters than allowed.
|
|
- Do not add emoji in the content.
|
|
- Metrics should be in abbreviated form with least possible characters. Do not add long sequence of words for metrics.
|
|
- For verbosity:
|
|
- If verbosity is 'concise', then generate description as 1/3 or lower of the max character limit. Don't worry if you miss content or context.
|
|
- If verbosity is 'standard', then generate description as 2/3 of the max character limit.
|
|
- If verbosity is 'text-heavy', then generate description as 3/4 or higher of the max character limit. Make sure it does not exceed the max character limit.
|
|
|
|
User instructions, tone and verbosity should always be followed and should supercede any other instruction, except for max and min character limit, slide schema and number of items.
|
|
|
|
- Provide output in json format and **don't include <parameters> tags**.
|
|
|
|
# Image and Icon Output Format
|
|
image: {{
|
|
__image_prompt__: string,
|
|
}}
|
|
icon: {{
|
|
__icon_query__: string,
|
|
}}
|
|
|
|
# Diagram Output Format (optional)
|
|
When the slide content naturally contains a process/workflow, comparison data, or proportions,
|
|
optionally include one of these fields:
|
|
- __diagram__: for simple data viz — flowchart (3-6 steps), bar_chart (3-6 items), pie_chart (3-6 slices)
|
|
- __mermaid__: for complex structural diagrams — sequence diagrams, class diagrams, state machines, ER diagrams, gantt charts
|
|
Only include __diagram__ or __mermaid__ when the content truly calls for it. Never include both.
|
|
These fields are completely optional — most slides should not include them.
|
|
|
|
"""
|
|
|
|
|
|
def get_user_prompt(
|
|
outline: str,
|
|
language: str,
|
|
prev_slide_title: Optional[str] = None,
|
|
source_section_text: Optional[str] = None,
|
|
):
|
|
prev_slide_section = ""
|
|
if prev_slide_title:
|
|
prev_slide_section = f"""
|
|
## Previous Slide
|
|
"{prev_slide_title}" — ensure this slide continues naturally from it.
|
|
"""
|
|
|
|
source_section = ""
|
|
if source_section_text:
|
|
source_section = f"""
|
|
## Source Material for This Slide
|
|
Use the following excerpt from the brief as the primary source of facts for this slide.
|
|
Do NOT invent data points not present here.
|
|
{source_section_text}
|
|
"""
|
|
|
|
return f"""
|
|
## Current Date and Time
|
|
{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
|
|
## Icon Query And Image Prompt Language
|
|
English
|
|
|
|
## Slide Content Language
|
|
{language}
|
|
{prev_slide_section}
|
|
{source_section}
|
|
## Slide Outline
|
|
{outline}
|
|
"""
|
|
|
|
|
|
def get_messages(
|
|
outline: str,
|
|
language: str,
|
|
tone: Optional[str] = None,
|
|
verbosity: Optional[str] = None,
|
|
instructions: Optional[str] = None,
|
|
brand_context: Optional[str] = None,
|
|
attachment_context: Optional[str] = None,
|
|
prev_slide_title: Optional[str] = None,
|
|
source_section_text: Optional[str] = None,
|
|
):
|
|
|
|
return [
|
|
LLMSystemMessage(
|
|
content=get_system_prompt(
|
|
tone, verbosity, instructions, brand_context, attachment_context,
|
|
),
|
|
),
|
|
LLMUserMessage(
|
|
content=get_user_prompt(outline, language, prev_slide_title, source_section_text),
|
|
),
|
|
]
|
|
|
|
|
|
async def get_slide_content_from_type_and_outline(
|
|
slide_layout: SlideLayoutModel,
|
|
outline: SlideOutlineModel,
|
|
language: str,
|
|
tone: Optional[str] = None,
|
|
verbosity: Optional[str] = None,
|
|
instructions: Optional[str] = None,
|
|
brand_context: Optional[str] = None,
|
|
attachment_context: Optional[str] = None,
|
|
prev_slide_title: Optional[str] = None,
|
|
source_section_text: Optional[str] = None,
|
|
):
|
|
client = LLMClient()
|
|
model = get_model()
|
|
|
|
response_schema = remove_fields_from_schema(
|
|
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
|
|
)
|
|
response_schema = add_field_in_schema(
|
|
response_schema,
|
|
{
|
|
"__speaker_note__": {
|
|
"type": "string",
|
|
"minLength": 100,
|
|
"maxLength": 250,
|
|
"description": "Speaker note for the slide",
|
|
}
|
|
},
|
|
True,
|
|
)
|
|
response_schema = add_field_in_schema(
|
|
response_schema,
|
|
{
|
|
"__diagram__": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {"type": "string"},
|
|
"title": {"type": "string"},
|
|
"flow_steps": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"label": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
"bar_items": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"label": {"type": "string"},
|
|
"value": {"type": "number"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
False,
|
|
)
|
|
response_schema = add_field_in_schema(
|
|
response_schema,
|
|
{
|
|
"__mermaid__": {
|
|
"type": "string",
|
|
"description": "Mermaid diagram syntax for complex structural diagrams",
|
|
}
|
|
},
|
|
False,
|
|
)
|
|
|
|
try:
|
|
response = await client.generate_structured(
|
|
model=model,
|
|
messages=get_messages(
|
|
outline.content,
|
|
language,
|
|
tone,
|
|
verbosity,
|
|
instructions,
|
|
brand_context,
|
|
attachment_context,
|
|
prev_slide_title,
|
|
source_section_text,
|
|
),
|
|
response_format=response_schema,
|
|
strict=False,
|
|
)
|
|
return response
|
|
|
|
except Exception as e:
|
|
raise handle_llm_client_exceptions(e)
|