from __future__ import annotations import json import logging from typing import Any import models from config import SMART_MODEL, STREAMING_ENABLED from format_prompts import get_format_prompt from html_utils import inject_theme from llm_utils import run_ai, stream_ai from prompts import ( field_values_system_prompt, html_context_messages, html_edit_system_prompt, html_system_prompt, outline_generator_system_prompt, section_draft_system_prompt, template_fill_html_system_prompt, ) logger = logging.getLogger(__name__) def generate_outline_with_llm( prompt: str, document_type: str, constraints: models.Constraint | None = None, ) -> models.OutlineResponse: """ Generate outline for a specific document type. document_type should already be detected - this function only generates the outline. """ constraint_text = "" if constraints: tone = constraints.tone audience = constraints.audience pages = constraints.page_count constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." # Try to get a format-specific extraction prompt format_prompt, default_sections = get_format_prompt(document_type) system_prompt = outline_generator_system_prompt(document_type, constraint_text, format_prompt, default_sections) messages: list[models.ChatMessage] = [ models.ChatMessage(role="system", content=system_prompt), models.ChatMessage(role="user", content=prompt), ] parsed = run_ai( SMART_MODEL, messages, models.OutlineResponse, tag="outline", ) if parsed: logger.info( "[OUTLINE] Generated outline doc_type=%s sections=%d filename=%s", parsed.doc_type, len(parsed.sections), parsed.outline_filename, ) return parsed raise RuntimeError("AI outline generation failed.") def generate_field_values( prompt: str, document_type: str, fields: list[dict[str, Any]], constraints: models.Constraint | None = None, ) -> list[dict[str, str]]: constraint_text = "" if constraints: tone = constraints.tone audience = constraints.audience pages = constraints.page_count constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." system_prompt = field_values_system_prompt(constraint_text) messages: list[models.ChatMessage] = [ models.ChatMessage(role="system", content=system_prompt), models.ChatMessage(role="user", content=f"Document type: {document_type}"), models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"), models.ChatMessage(role="user", content=f"Fields:\n{json.dumps(fields, ensure_ascii=True)}"), ] parsed = run_ai( SMART_MODEL, messages, models.LLMFieldValuesResponse, tag="field_values", ) if parsed: return [{"label": item.label, "value": item.value} for item in parsed.fields] raise RuntimeError("AI field extraction failed.") def generate_section_draft( prompt: str, document_type: str, outline_text: str, constraints: models.Constraint | None = None, ) -> list[models.DraftSection]: constraint_text = "" if constraints: tone = constraints.tone audience = constraints.audience pages = constraints.page_count constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." system_prompt = section_draft_system_prompt(constraint_text) messages: list[models.ChatMessage] = [ models.ChatMessage(role="system", content=system_prompt), models.ChatMessage(role="user", content=f"Document type: {document_type}"), models.ChatMessage(role="user", content=f"Outline:\n{outline_text}"), models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"), ] parsed = run_ai( SMART_MODEL, messages, models.LLMDraftSectionsResponse, tag="section_draft", ) return parsed.sections # ── HTML generation ─────────────────────────────────────────────────────────── def generate_template_fill_html_stream( template_html: str, document_type: str, outline_text: str, draft_sections: list[models.DraftSection] | None = None, constraints: models.Constraint | None = None, theme: dict[str, str] | None = None, additional_instructions: str | None = None, has_logo: bool = False, ): """Fill an HTML template by replacing {{PLACEHOLDER}} tokens, streaming chunks.""" constraints_text = "" if constraints: tone = constraints.tone audience = constraints.audience pages = constraints.page_count constraints_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." if draft_sections: constraints_text += "\nUse the section content to inform placeholder values." # Inject theme before sending to AI if overrides are provided if theme: template_html = inject_theme(template_html, theme) system_prompt = template_fill_html_system_prompt(constraints_text) messages: list[models.ChatMessage] = [ models.ChatMessage(role="system", content=system_prompt), models.ChatMessage(role="user", content=f"Document type: {document_type}"), models.ChatMessage( role="user", content=( "CRITICAL EXAMPLE — What to preserve vs. what to change:\n" "If template has:\n" "
{{VENDOR_NAME}}
\n" "You MUST output:\n" "
Acme Corporation
← fill the token, keep the tag\n" "\n" "For multi-row content like {{LINEITEM_ROWS}}, generate full ... HTML.\n" "Match ONLY the columns present in the template's — do NOT add extra columns.\n" "Example (4-column table: #, Description, Qty, Line Total):\n" " 1Office chairs4$480.00\n" "\n" "Keep ALL HTML tags, CSS, classes, and IDs EXACTLY as-is.\n" "Do NOT modify any