presenton/servers/fastapi/utils/llm_calls/generate_slide_content.py

215 lines
6.2 KiB
Python

import asyncio
from datetime import datetime
import json
from typing import Optional
from fastapi import HTTPException
from llmai import get_client
from llmai.shared import JSONSchemaResponse, Message, SystemMessage, UserMessage
from models.presentation_layout import SlideLayoutModel
from models.presentation_outline_model import SlideOutlineModel
from utils.llm_config import get_llm_config
from utils.llm_client_error_handler import handle_llm_client_exceptions
from utils.llm_utils import extract_structured_content, get_generate_kwargs
from utils.llm_provider import get_model
from utils.schema_utils import add_field_in_schema, remove_fields_from_schema
SLIDE_CONTENT_SYSTEM_PROMPT = """
You will be given slide content and response schema.
You need to generate structured content json based on the schema.
# Steps
1. Analyze the content.
2. Analyze the response schema.
3. Generate structured content json based on the schema.
4. Generate speaker note if required.
5. Provide structured content json as output.
# General Rules
- Make sure to follow language guidelines.
- Speaker note should be normal text, not markdown.
- Never ever go over the max character limit.
- Do not add emoji in the content.
- Don't provide $schema field in content json.
{markdown_emphasis_rules}
{user_instructions}
{tone_instructions}
{verbosity_instructions}
{output_fields_instructions}
"""
SLIDE_CONTENT_USER_PROMPT = """
# Current Date and Time:
{current_date_time}
# Icon Query And Image Prompt Language:
English
# Slide Language:
{language}
# SLIDE CONTENT: START
{content}
# SLIDE CONTENT: END
"""
def _resolve_prompt_language(language: Optional[str]) -> str:
if language is None:
return "auto-detect"
s = str(language).strip()
if not s:
return "auto-detect"
if s.lower() in {"auto", "auto-detect"}:
return "auto-detect"
return s
def _get_schema_markdown(response_schema: Optional[dict]) -> str:
if not response_schema:
return "- Follow the provided response schema strictly."
try:
schema_text = json.dumps(response_schema, ensure_ascii=False)
except Exception:
return "- Follow the provided response schema strictly."
return f"- Follow this response schema exactly: {schema_text}"
def get_system_prompt(
tone: Optional[str] = None,
verbosity: Optional[str] = None,
instructions: Optional[str] = None,
response_schema: Optional[dict] = None,
):
markdown_emphasis_rules = (
"- Strictly use markdown to emphasize important points, by bolding or "
"italicizing the part of text."
)
user_instructions = f"# User Instructions:\n{instructions}" if instructions else ""
tone_instructions = (
f"# Tone Instructions:\nMake slide as {tone} as possible." if tone else ""
)
verbosity_instructions = ""
if verbosity:
verbosity_instructions = "# Verbosity Instructions:\n"
if verbosity == "concise":
verbosity_instructions += "Make slide as concise as possible."
elif verbosity == "standard":
verbosity_instructions += "Make slide as standard as possible."
elif verbosity == "text-heavy":
verbosity_instructions += "Make slide as text-heavy as possible."
output_fields_instructions = "# Output Fields:\n" + _get_schema_markdown(
response_schema
)
return SLIDE_CONTENT_SYSTEM_PROMPT.format(
markdown_emphasis_rules=markdown_emphasis_rules,
user_instructions=user_instructions,
tone_instructions=tone_instructions,
verbosity_instructions=verbosity_instructions,
output_fields_instructions=output_fields_instructions,
)
def get_user_prompt(outline: str, language: Optional[str]):
return SLIDE_CONTENT_USER_PROMPT.format(
current_date_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
language=_resolve_prompt_language(language),
content=outline,
)
def get_messages(
outline: str,
language: Optional[str],
tone: Optional[str] = None,
verbosity: Optional[str] = None,
instructions: Optional[str] = None,
response_schema: Optional[dict] = None,
) -> list[Message]:
return [
SystemMessage(
content=get_system_prompt(
tone,
verbosity,
instructions,
response_schema,
),
),
UserMessage(
content=get_user_prompt(outline, language),
),
]
async def get_slide_content_from_type_and_outline(
slide_layout: SlideLayoutModel,
outline: SlideOutlineModel,
language: Optional[str],
tone: Optional[str] = None,
verbosity: Optional[str] = None,
instructions: Optional[str] = None,
):
client = get_client(config=get_llm_config())
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,
)
try:
response_format = JSONSchemaResponse(
name="response",
json_schema=response_schema,
strict=False,
)
messages = get_messages(
outline.content,
language,
tone,
verbosity,
instructions,
response_schema,
)
for attempt in range(3):
response = await asyncio.to_thread(
client.generate,
**get_generate_kwargs(
model=model,
messages=messages,
response_format=response_format,
),
)
content = extract_structured_content(response.content)
if content is not None:
return content
if attempt < 2:
await asyncio.sleep(0.5 * (attempt + 1))
raise HTTPException(status_code=400, detail="LLM did not return any content")
except Exception as e:
raise handle_llm_client_exceptions(e)