enhanced generation and rendering chain

This commit is contained in:
ValueOn AG 2025-11-30 11:03:23 +01:00
parent c135321aee
commit 11bb127a43
13 changed files with 423 additions and 424 deletions

View file

@ -222,13 +222,6 @@ class AiCallPromptImage(BaseModel):
style: Optional[str] = Field(default="vivid", description="Image style (vivid, natural)") style: Optional[str] = Field(default="vivid", description="Image style (vivid, natural)")
class DocumentData(BaseModel):
"""Single document in response."""
documentName: str = Field(description="Document name")
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
mimeType: str = Field(description="MIME type of the document")
class AiProcessParameters(BaseModel): class AiProcessParameters(BaseModel):
"""Parameters for AI processing action.""" """Parameters for AI processing action."""
aiPrompt: str = Field(description="AI instruction prompt") aiPrompt: str = Field(description="AI instruction prompt")
@ -242,91 +235,6 @@ class AiProcessParameters(BaseModel):
) )
class AiResponseMetadata(BaseModel): # NOTE: DocumentData, AiResponseMetadata, and AiResponse are defined in datamodelWorkflow.py
"""Metadata for AI response (varies by operation type).""" # Import them from there if needed: from modules.datamodels.datamodelWorkflow import DocumentData, AiResponseMetadata, AiResponse
# Document Generation Metadata
title: Optional[str] = Field(None, description="Document title")
filename: Optional[str] = Field(None, description="Document filename")
# Operation-Specific Metadata
operationType: Optional[str] = Field(None, description="Type of operation performed")
schemaVersion: Optional[str] = Field(None, description="Schema version (e.g., 'parameters_v1')", alias="schema")
extractionMethod: Optional[str] = Field(None, description="Method used for extraction")
sourceDocuments: Optional[List[str]] = Field(None, description="Source document references")
# Additional metadata (for extensibility)
additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
@classmethod
def fromDict(cls, data: Optional[Dict[str, Any]]) -> Optional["AiResponseMetadata"]:
"""Create AiResponseMetadata from dict with camelCase field names."""
if not data or not isinstance(data, dict):
return None
knownFields = {"title", "filename", "operationType", "schema", "extractionMethod", "sourceDocuments", "additionalData"}
mappedData = {k: v for k, v in data.items() if k in knownFields}
additionalFields = {k: v for k, v in data.items() if k not in knownFields}
if additionalFields:
mappedData["additionalData"] = additionalFields
try:
return cls(**mappedData)
except Exception:
return None
class AiResponse(BaseModel):
"""Unified response from all AI calls (planning, text, documents)."""
content: str = Field(description="Response content (JSON string for planning, text for analysis, unified JSON for documents)")
metadata: Optional[AiResponseMetadata] = Field(
None,
description="Response metadata (varies by operation type)"
)
documents: Optional[List[DocumentData]] = Field(
None,
description="Generated documents (only for document generation operations)"
)
def toJson(self) -> Dict[str, Any]:
"""
Convert AI response content to JSON using enhanced stabilizing failsafe conversion methods.
Centralizes AI result to JSON conversion in one place.
Returns:
Dict containing the parsed JSON content, or a safe fallback structure if parsing fails.
"""
if not self.content:
return {}
# Use enhanced stabilizing failsafe JSON conversion methods from jsonUtils
# First, try to extract and parse JSON using the safe methods
obj, err, cleaned = tryParseJson(self.content)
if err is None and isinstance(obj, dict):
# Successfully parsed as dict
return obj
elif err is None and isinstance(obj, list):
# Successfully parsed as list - wrap in dict for consistency
return {"data": obj}
# If parsing failed, try to repair broken JSON
repaired = repairBrokenJson(self.content)
if repaired is not None:
return repaired
# If all else fails, return a safe structure with the cleaned content
# Extract JSON string even if it's not fully parseable
extracted = extractJsonString(self.content)
if extracted and extracted != self.content:
# Try one more time with extracted string
obj, err, _ = tryParseJson(extracted)
if err is None and isinstance(obj, (dict, list)):
return obj if isinstance(obj, dict) else {"data": obj}
# Final fallback: return safe structure with raw content
return {
"content": self.content,
"parseError": True
}

View file

@ -396,6 +396,10 @@ class ActionDocument(BaseModel):
documentName: str = Field(description="Name of the document") documentName: str = Field(description="Name of the document")
documentData: Any = Field(description="Content/data of the document") documentData: Any = Field(description="Content/data of the document")
mimeType: str = Field(description="MIME type of the document") mimeType: str = Field(description="MIME type of the document")
sourceJson: Optional[Dict[str, Any]] = Field(
None,
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
)
registerModelLabels( registerModelLabels(

View file

@ -95,32 +95,16 @@ class AiResponseMetadata(BaseModel):
# Additional metadata (for extensibility) # Additional metadata (for extensibility)
additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata") additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
@classmethod
def fromDict(cls, data: Optional[Dict[str, Any]]) -> Optional["AiResponseMetadata"]:
"""Create AiResponseMetadata from dict with camelCase field names"""
if not data:
return None
# Convert snake_case keys to camelCase if needed (for backward compatibility)
convertedData = {}
for key, value in data.items():
# Keep camelCase as-is, convert snake_case if present
if '_' in key:
# Convert snake_case to camelCase
parts = key.split('_')
camelKey = parts[0] + ''.join(word.capitalize() for word in parts[1:])
convertedData[camelKey] = value
else:
convertedData[key] = value
return cls(**convertedData)
class DocumentData(BaseModel): class DocumentData(BaseModel):
"""Single document in response""" """Single document in response"""
documentName: str = Field(description="Document name") documentName: str = Field(description="Document name")
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)") documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
mimeType: str = Field(description="MIME type of the document") mimeType: str = Field(description="MIME type of the document")
sourceJson: Optional[Dict[str, Any]] = Field(
None,
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
)
class ExtractContentParameters(BaseModel): class ExtractContentParameters(BaseModel):

View file

@ -1354,7 +1354,8 @@ Respond with ONLY a JSON object in this exact format:
docData = DocumentData( docData = DocumentData(
documentName=documentName, documentName=documentName,
documentData=rendered_content, documentData=rendered_content,
mimeType=mime_type mimeType=mime_type,
sourceJson=generated_data # Preserve source JSON for structure validation
) )
metadata = AiResponseMetadata( metadata = AiResponseMetadata(

View file

@ -479,14 +479,16 @@ class BaseRenderer(ABC):
return f"""You are a professional document styling expert. Generate a complete JSON styling configuration for {formatName.upper()} documents. return f"""You are a professional document styling expert. Generate a complete JSON styling configuration for {formatName.upper()} documents.
Use this schema as a template and customize the values for professional document styling: User request: {userPrompt}
Use this schema as a template:
{schemaJson} {schemaJson}
Requirements: Requirements:
- Return ONLY the complete JSON object (no markdown, no explanations) - Return ONLY the complete JSON object (no markdown, no explanations)
- Customize colors, fonts, and spacing for professional appearance - If the user request contains style/formatting/design instructions (in any language), customize the styling accordingly (adapt styles and add styles if needed)
- If the user request has NO style instructions, return the default schema values unchanged
- Ensure all objects are properly closed with closing braces - Ensure all objects are properly closed with closing braces
- Make the styling modern and professional - Only modify styles if style instructions are present in the user request
Return the complete JSON:""" Return the complete JSON:"""

View file

@ -57,17 +57,17 @@ class RendererDocx(BaseRenderer):
return f"DOCX Generation Error: {str(e)}", "text/plain" return f"DOCX Generation Error: {str(e)}", "text/plain"
async def _generateDocxFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str: async def _generateDocxFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
"""Generate DOCX content from structured JSON document using AI-generated styling.""" """Generate DOCX content from structured JSON document."""
try: try:
# Create new document # Create new document
doc = Document() doc = Document()
# Get AI-generated styling definitions # Get style set: default styles, enhanced with AI if style instructions present
self.logger.info(f"About to call AI styling with user_prompt: {userPrompt[:100] if userPrompt else 'None'}...") styleSet = await self._getStyleSet(userPrompt, aiService)
styles = await self._getDocxStyles(userPrompt, aiService)
# Apply basic document setup # Setup basic document styles and create all styles from style set
self._setupBasicDocumentStyles(doc) self._setupBasicDocumentStyles(doc)
self._setupDocumentStyles(doc, styleSet)
# Validate JSON structure # Validate JSON structure
if not isinstance(json_content, dict): if not isinstance(json_content, dict):
@ -79,15 +79,14 @@ class RendererDocx(BaseRenderer):
# Use title from JSON metadata if available, otherwise use provided title # Use title from JSON metadata if available, otherwise use provided title
document_title = json_content.get("metadata", {}).get("title", title) document_title = json_content.get("metadata", {}).get("title", title)
# Add document title using analyzed styles # Add document title using Title style
if document_title: if document_title:
title_heading = doc.add_heading(document_title, level=1) doc.add_paragraph(document_title, style='Title')
title_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Process each section in order # Process each section in order
sections = json_content.get("sections", []) sections = json_content.get("sections", [])
for section in sections: for section in sections:
self._renderJsonSection(doc, section, styles) self._renderJsonSection(doc, section, styleSet)
# Save to buffer # Save to buffer
buffer = io.BytesIO() buffer = io.BytesIO()
@ -104,25 +103,44 @@ class RendererDocx(BaseRenderer):
self.logger.error(f"Error generating DOCX from JSON: {str(e)}") self.logger.error(f"Error generating DOCX from JSON: {str(e)}")
raise Exception(f"DOCX generation failed: {str(e)}") raise Exception(f"DOCX generation failed: {str(e)}")
async def _getDocxStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]: async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get DOCX styling definitions using base template AI styling.""" """Get style set - default styles, enhanced with AI if userPrompt provided.
style_schema = {
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center"},
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left"},
"heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left"},
"paragraph": {"font_size": 11, "color": "#2F2F2F", "bold": False, "align": "left"},
"table_header": {"background": "#4F4F4F", "text_color": "#FFFFFF", "bold": True, "align": "center"},
"table_cell": {"background": "#FFFFFF", "text_color": "#2F2F2F", "bold": False, "align": "left"},
"table_border": {"style": "horizontal_only", "color": "#000000", "thickness": "thin"},
"bullet_list": {"font_size": 11, "color": "#2F2F2F", "indent": 20},
"code_block": {"font": "Courier New", "font_size": 10, "color": "#2F2F2F", "background": "#F5F5F5"}
}
style_template = self._createAiStyleTemplate("docx", userPrompt, style_schema) Args:
styles = await self._getAiStyles(aiService, style_template, self._getDefaultStyles()) userPrompt: User's prompt (AI will detect style instructions in any language)
aiService: AI service (used only if userPrompt provided)
templateName: Name of template style set (None = default)
# Validate and fix contrast issues Returns:
return self._validateStylesContrast(styles) Dict with style definitions for all document styles
"""
# Get default style set
if templateName == "corporate":
defaultStyleSet = self._getCorporateStyleSet()
elif templateName == "minimal":
defaultStyleSet = self._getMinimalStyleSet()
else:
defaultStyleSet = self._getDefaultStyleSet()
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
if userPrompt and aiService:
# AI will naturally detect style instructions in any language
self.logger.info(f"Enhancing styles with AI based on user prompt...")
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
return self._validateStylesContrast(enhancedStyleSet)
else:
# Use default styles only
return defaultStyleSet
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
"""Enhance default styles with AI based on user prompt."""
try:
style_template = self._createAiStyleTemplate("docx", userPrompt, defaultStyleSet)
enhanced_styles = await self._getAiStyles(aiService, style_template, defaultStyleSet)
return enhanced_styles
except Exception as e:
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
return defaultStyleSet
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]: def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix contrast issues in AI-generated styles.""" """Validate and fix contrast issues in AI-generated styles."""
@ -159,10 +177,10 @@ class RendererDocx(BaseRenderer):
except Exception as e: except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}") self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultStyles() return self._getDefaultStyleSet()
def _getDefaultStyles(self) -> Dict[str, Any]: def _getDefaultStyleSet(self) -> Dict[str, Any]:
"""Default DOCX styles.""" """Default DOCX style set - used when no style instructions present."""
return { return {
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center"}, "title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center"},
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left"}, "heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left"},
@ -613,25 +631,69 @@ class RendererDocx(BaseRenderer):
return "" return ""
def _setupDocumentStyles(self, doc): def _setupDocumentStyles(self, doc: Document, styleSet: Dict[str, Any]) -> None:
"""Set up document styles.""" """Create all styles in document from style set.
try:
# Set default font Creates styles BEFORE rendering so they're available for use.
style = doc.styles['Normal'] """
font = style.font try:
font.name = 'Calibri' from docx.enum.style import WD_STYLE_TYPE
font.size = Pt(11)
# Create Title style
if "title" in styleSet:
self._createStyle(doc, "Title", styleSet["title"], WD_STYLE_TYPE.PARAGRAPH)
# Create Heading styles (Heading 1, Heading 2)
if "heading1" in styleSet:
self._createStyle(doc, "Heading 1", styleSet["heading1"], WD_STYLE_TYPE.PARAGRAPH)
if "heading2" in styleSet:
self._createStyle(doc, "Heading 2", styleSet["heading2"], WD_STYLE_TYPE.PARAGRAPH)
# Note: List Bullet and List Number are built-in Word styles, no need to create
# Set heading styles
for i in range(1, 4):
heading_style = doc.styles[f'Heading {i}']
heading_font = heading_style.font
heading_font.name = 'Calibri'
heading_font.size = Pt(16 - i * 2)
heading_font.bold = True
except Exception as e: except Exception as e:
self.logger.warning(f"Could not set up document styles: {str(e)}") self.logger.warning(f"Could not set up document styles: {str(e)}")
def _createStyle(self, doc: Document, styleName: str, styleConfig: Dict[str, Any], styleType) -> None:
"""Create or update a style in the document styles collection."""
try:
from docx.enum.style import WD_STYLE_TYPE
# Try to get existing style, or create new one
try:
doc_style = doc.styles[styleName]
except KeyError:
# Create new style based on Normal
doc_style = doc.styles.add_style(styleName, styleType)
# Base it on Normal style
doc_style.base_style = doc.styles['Normal']
# Apply font configuration
font = doc_style.font
if "font_size" in styleConfig:
font.size = Pt(styleConfig["font_size"])
if "bold" in styleConfig:
font.bold = styleConfig["bold"]
if "color" in styleConfig:
color_hex = styleConfig["color"].lstrip('#')
font.color.rgb = RGBColor(int(color_hex[0:2], 16), int(color_hex[2:4], 16), int(color_hex[4:6], 16))
if "font" in styleConfig:
font.name = styleConfig["font"]
# Set paragraph formatting for alignment
if "align" in styleConfig:
para_format = doc_style.paragraph_format
align = styleConfig["align"]
if align == "center":
para_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif align == "right":
para_format.alignment = WD_ALIGN_PARAGRAPH.RIGHT
else:
para_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
except Exception as e:
self.logger.warning(f"Could not create style '{styleName}': {str(e)}")
def _processSection(self, doc, lines: list): def _processSection(self, doc, lines: list):
"""Process a section of content into DOCX elements.""" """Process a section of content into DOCX elements."""
for line in lines: for line in lines:

View file

@ -39,8 +39,8 @@ class RendererHtml(BaseRenderer):
async def _generateHtmlFromJson(self, jsonContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str: async def _generateHtmlFromJson(self, jsonContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
"""Generate HTML content from structured JSON document using AI-generated styling.""" """Generate HTML content from structured JSON document using AI-generated styling."""
try: try:
# Get AI-generated styling definitions # Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getHtmlStyles(userPrompt, aiService) styles = await self._getStyleSet(userPrompt, aiService)
# Validate JSON structure # Validate JSON structure
if not isinstance(jsonContent, dict): if not isinstance(jsonContent, dict):
@ -97,29 +97,41 @@ class RendererHtml(BaseRenderer):
self.logger.error(f"Error generating HTML from JSON: {str(e)}") self.logger.error(f"Error generating HTML from JSON: {str(e)}")
raise Exception(f"HTML generation failed: {str(e)}") raise Exception(f"HTML generation failed: {str(e)}")
async def _getHtmlStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]: async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get HTML styling definitions using base template AI styling.""" """Get style set - default styles, enhanced with AI if userPrompt provided.
styleSchema = {
"title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"},
"heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"},
"heading2": {"font_size": "1.5em", "color": "#4F4F4F", "font_weight": "bold", "text_align": "left", "margin": "1em 0 0.5em 0"},
"paragraph": {"font_size": "1em", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "margin": "0 0 1em 0", "line_height": "1.6"},
"table": {"border": "1px solid #ddd", "border_collapse": "collapse", "width": "100%", "margin": "1em 0"},
"table_header": {"background": "#4F4F4F", "color": "#FFFFFF", "font_weight": "bold", "text_align": "center", "padding": "12px"},
"table_cell": {"background": "#FFFFFF", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "padding": "8px", "border": "1px solid #ddd"},
"bullet_list": {"font_size": "1em", "color": "#2F2F2F", "margin": "0 0 1em 0", "padding_left": "20px"},
"code_block": {"font_family": "Courier New, monospace", "font_size": "0.9em", "color": "#2F2F2F", "background": "#F5F5F5", "padding": "1em", "border": "1px solid #ddd", "border_radius": "4px", "margin": "1em 0"},
"image": {"max_width": "100%", "height": "auto", "margin": "1em 0", "border_radius": "4px"},
"body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"}
}
styleTemplate = self._createAiStyleTemplate("html", userPrompt, styleSchema) Args:
styles = await self._getAiStyles(aiService, styleTemplate, self._getDefaultHtmlStyles()) userPrompt: User's prompt (AI will detect style instructions in any language)
aiService: AI service (used only if userPrompt provided)
templateName: Name of template style set (None = default)
# Validate and fix contrast issues Returns:
return self._validateHtmlStylesContrast(styles) Dict with style definitions for all document styles
"""
# Get default style set
defaultStyleSet = self._getDefaultStyleSet()
def _validateHtmlStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]: # Enhance with AI if userPrompt provided (AI handles multilingual style detection)
if userPrompt and aiService:
# AI will naturally detect style instructions in any language
self.logger.info(f"Enhancing styles with AI based on user prompt...")
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
return self._validateStylesContrast(enhancedStyleSet)
else:
# Use default styles only
return defaultStyleSet
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
"""Enhance default styles with AI based on user prompt."""
try:
style_template = self._createAiStyleTemplate("html", userPrompt, defaultStyleSet)
enhanced_styles = await self._getAiStyles(aiService, style_template, defaultStyleSet)
return enhanced_styles
except Exception as e:
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
return defaultStyleSet
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix contrast issues in AI-generated styles.""" """Validate and fix contrast issues in AI-generated styles."""
try: try:
# Fix table header contrast # Fix table header contrast
@ -154,11 +166,10 @@ class RendererHtml(BaseRenderer):
except Exception as e: except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}") self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultHtmlStyles() return self._getDefaultStyleSet()
def _getDefaultStyleSet(self) -> Dict[str, Any]:
def _getDefaultHtmlStyles(self) -> Dict[str, Any]: """Default HTML style set - used when no style instructions present."""
"""Default HTML styles."""
return { return {
"title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"}, "title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"},
"heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"}, "heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"},
@ -173,6 +184,7 @@ class RendererHtml(BaseRenderer):
"body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"} "body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"}
} }
def _generateCssStyles(self, styles: Dict[str, Any]) -> str: def _generateCssStyles(self, styles: Dict[str, Any]) -> str:
"""Generate CSS from style definitions.""" """Generate CSS from style definitions."""
css_parts = [] css_parts = []

View file

@ -59,8 +59,8 @@ class RendererPdf(BaseRenderer):
async def _generatePdfFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str: async def _generatePdfFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
"""Generate PDF content from structured JSON document using AI-generated styling.""" """Generate PDF content from structured JSON document using AI-generated styling."""
try: try:
# Get AI-generated styling definitions # Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getPdfStyles(userPrompt, aiService) styles = await self._getStyleSet(userPrompt, aiService)
# Validate JSON structure # Validate JSON structure
if not isinstance(json_content, dict): if not isinstance(json_content, dict):
@ -123,9 +123,82 @@ class RendererPdf(BaseRenderer):
self.logger.error(f"Error generating PDF from JSON: {str(e)}") self.logger.error(f"Error generating PDF from JSON: {str(e)}")
raise Exception(f"PDF generation failed: {str(e)}") raise Exception(f"PDF generation failed: {str(e)}")
async def _getPdfStyles(self, user_prompt: str, ai_service=None) -> Dict[str, Any]: async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get PDF styling definitions using base template AI styling.""" """Get style set - default styles, enhanced with AI if userPrompt provided.
style_schema = {
Args:
userPrompt: User's prompt (AI will detect style instructions in any language)
aiService: AI service (used only if userPrompt provided)
templateName: Name of template style set (None = default)
Returns:
Dict with style definitions for all document styles
"""
# Get default style set
defaultStyleSet = self._getDefaultStyleSet()
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
if userPrompt and aiService:
# AI will naturally detect style instructions in any language
self.logger.info(f"Enhancing styles with AI based on user prompt...")
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
# Convert colors to PDF format after getting styles
enhancedStyleSet = self._convertColorsFormat(enhancedStyleSet)
return self._validateStylesContrast(enhancedStyleSet)
else:
# Use default styles only
return defaultStyleSet
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
"""Enhance default styles with AI based on user prompt."""
try:
style_template = self._createAiStyleTemplate("pdf", userPrompt, defaultStyleSet)
enhanced_styles = await self._getAiStyles(aiService, style_template, defaultStyleSet)
return enhanced_styles
except Exception as e:
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
return defaultStyleSet
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix contrast issues in AI-generated styles."""
try:
# Fix table header contrast
if "table_header" in styles:
header = styles["table_header"]
bg_color = header.get("background", "#FFFFFF")
text_color = header.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
header["background"] = "#4F4F4F"
header["text_color"] = "#FFFFFF"
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
header["background"] = "#4F4F4F"
header["text_color"] = "#FFFFFF"
# Fix table cell contrast
if "table_cell" in styles:
cell = styles["table_cell"]
bg_color = cell.get("background", "#FFFFFF")
text_color = cell.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
cell["background"] = "#FFFFFF"
cell["text_color"] = "#2F2F2F"
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
cell["background"] = "#FFFFFF"
cell["text_color"] = "#2F2F2F"
return styles
except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultStyleSet()
def _getDefaultStyleSet(self) -> Dict[str, Any]:
"""Default PDF style set - used when no style instructions present."""
return {
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center", "space_after": 30}, "title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center", "space_after": 30},
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left", "space_after": 12, "space_before": 12}, "heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left", "space_after": 12, "space_before": 12},
"heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left", "space_after": 8, "space_before": 8}, "heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left", "space_after": 8, "space_before": 8},
@ -136,20 +209,6 @@ class RendererPdf(BaseRenderer):
"code_block": {"font": "Courier", "font_size": 9, "color": "#2F2F2F", "background": "#F5F5F5", "space_after": 6} "code_block": {"font": "Courier", "font_size": 9, "color": "#2F2F2F", "background": "#F5F5F5", "space_after": 6}
} }
style_template = self._createAiStyleTemplate("pdf", user_prompt, style_schema)
# Use base template method like DOCX does (this works!)
styles = await self._getAiStyles(ai_service, style_template, self._getDefaultPdfStyles())
if styles is None:
return self._getDefaultPdfStyles()
# Convert colors to PDF format after getting styles
styles = self._convertColorsFormat(styles)
# Validate and fix contrast issues
return self._validatePdfStylesContrast(styles)
async def _getAiStylesWithPdfColors(self, ai_service, style_template: str, default_styles: Dict[str, Any]) -> Dict[str, Any]: async def _getAiStylesWithPdfColors(self, ai_service, style_template: str, default_styles: Dict[str, Any]) -> Dict[str, Any]:
"""Get AI styles with proper PDF color conversion.""" """Get AI styles with proper PDF color conversion."""
if not ai_service: if not ai_service:
@ -313,55 +372,6 @@ class RendererPdf(BaseRenderer):
return color_value return color_value
return default return default
def _validatePdfStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix contrast issues in AI-generated styles."""
try:
# Fix table header contrast
if "table_header" in styles:
header = styles["table_header"]
bg_color = header.get("background", "#FFFFFF")
text_color = header.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
header["background"] = "#4F4F4F"
header["text_color"] = "#FFFFFF"
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
header["background"] = "#4F4F4F"
header["text_color"] = "#FFFFFF"
# Fix table cell contrast
if "table_cell" in styles:
cell = styles["table_cell"]
bg_color = cell.get("background", "#FFFFFF")
text_color = cell.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
cell["background"] = "#FFFFFF"
cell["text_color"] = "#2F2F2F"
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
cell["background"] = "#FFFFFF"
cell["text_color"] = "#2F2F2F"
return styles
except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultPdfStyles()
def _getDefaultPdfStyles(self) -> Dict[str, Any]:
"""Default PDF styles."""
return {
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center", "space_after": 30},
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left", "space_after": 12, "space_before": 12},
"heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left", "space_after": 8, "space_before": 8},
"paragraph": {"font_size": 11, "color": "#2F2F2F", "bold": False, "align": "left", "space_after": 6, "line_height": 1.2},
"table_header": {"background": "#4F4F4F", "text_color": "#FFFFFF", "bold": True, "align": "center", "font_size": 12},
"table_cell": {"background": "#FFFFFF", "text_color": "#2F2F2F", "bold": False, "align": "left", "font_size": 10},
"bullet_list": {"font_size": 11, "color": "#2F2F2F", "space_after": 3},
"code_block": {"font": "Courier", "font_size": 9, "color": "#2F2F2F", "background": "#F5F5F5", "space_after": 6}
}
def _createTitleStyle(self, styles: Dict[str, Any]) -> ParagraphStyle: def _createTitleStyle(self, styles: Dict[str, Any]) -> ParagraphStyle:
"""Create title style from style definitions.""" """Create title style from style definitions."""

View file

@ -42,8 +42,8 @@ class RendererPptx(BaseRenderer):
from pptx.dml.color import RGBColor from pptx.dml.color import RGBColor
import re import re
# Get AI-generated styling definitions first # Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getPptxStyles(userPrompt, aiService) styles = await self._getStyleSet(userPrompt, aiService)
# Create new presentation # Create new presentation
prs = Presentation() prs = Presentation()
@ -303,9 +303,71 @@ class RendererPptx(BaseRenderer):
"""Get MIME type for rendered output.""" """Get MIME type for rendered output."""
return self.outputMimeType return self.outputMimeType
async def _getPptxStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]: async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get PowerPoint styling definitions using base template AI styling.""" """Get style set - default styles, enhanced with AI if userPrompt provided.
style_schema = {
Args:
userPrompt: User's prompt (AI will detect style instructions in any language)
aiService: AI service (used only if userPrompt provided)
templateName: Name of template style set (None = default)
Returns:
Dict with style definitions for all document styles
"""
# Get default style set
defaultStyleSet = self._getDefaultStyleSet()
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
if userPrompt and aiService:
# AI will naturally detect style instructions in any language
self.logger.info(f"Enhancing styles with AI based on user prompt...")
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
# Convert colors to PPTX format after getting styles
enhancedStyleSet = self._convertColorsFormat(enhancedStyleSet)
return self._validateStylesReadability(enhancedStyleSet)
else:
# Use default styles only
return defaultStyleSet
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
"""Enhance default styles with AI based on user prompt."""
try:
style_template = self._createProfessionalPptxTemplate(userPrompt, defaultStyleSet)
enhanced_styles = await self._getAiStylesWithPptxColors(aiService, style_template, defaultStyleSet)
return enhanced_styles
except Exception as e:
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
return defaultStyleSet
def _validateStylesReadability(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix readability issues in AI-generated styles."""
try:
# Ensure minimum font sizes for PowerPoint readability
min_font_sizes = {
"title": 36,
"heading": 24,
"subheading": 20,
"paragraph": 14,
"bullet_list": 14,
"table_header": 12,
"table_cell": 12
}
for style_name, min_size in min_font_sizes.items():
if style_name in styles:
current_size = styles[style_name].get("font_size", 12)
if current_size < min_size:
styles[style_name]["font_size"] = min_size
return styles
except Exception as e:
logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultStyleSet()
def _getDefaultStyleSet(self) -> Dict[str, Any]:
"""Default PowerPoint style set - used when no style instructions present."""
return {
"title": {"font_size": 52, "color": "#1B365D", "bold": True, "align": "center"}, "title": {"font_size": 52, "color": "#1B365D", "bold": True, "align": "center"},
"heading": {"font_size": 36, "color": "#2C5F2D", "bold": True, "align": "left"}, "heading": {"font_size": 36, "color": "#2C5F2D", "bold": True, "align": "left"},
"subheading": {"font_size": 28, "color": "#4A90E2", "bold": True, "align": "left"}, "subheading": {"font_size": 28, "color": "#4A90E2", "bold": True, "align": "left"},
@ -323,13 +385,6 @@ class RendererPptx(BaseRenderer):
"executive_ready": True "executive_ready": True
} }
style_template = self._createProfessionalPptxTemplate(userPrompt, style_schema)
# Use our own _getAiStylesWithPptxColors method to ensure proper color conversion
styles = await self._getAiStylesWithPptxColors(aiService, style_template, self._getDefaultPptxStyles())
# Validate PowerPoint-specific requirements
return self._validatePptxStylesReadability(styles)
def _createProfessionalPptxTemplate(self, userPrompt: str, style_schema: Dict[str, Any]) -> str: def _createProfessionalPptxTemplate(self, userPrompt: str, style_schema: Dict[str, Any]) -> str:
"""Create a professional PowerPoint-specific AI style template for corporate-quality slides.""" """Create a professional PowerPoint-specific AI style template for corporate-quality slides."""
import json import json
@ -495,51 +550,6 @@ JSON ONLY. NO OTHER TEXT."""
return (r, g, b) return (r, g, b)
return default return default
def _validatePptxStylesReadability(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix readability issues in AI-generated styles."""
try:
# Ensure minimum font sizes for PowerPoint readability
min_font_sizes = {
"title": 36,
"heading": 24,
"subheading": 20,
"paragraph": 14,
"bullet_list": 14,
"table_header": 12,
"table_cell": 12
}
for style_name, min_size in min_font_sizes.items():
if style_name in styles:
current_size = styles[style_name].get("font_size", 12)
if current_size < min_size:
styles[style_name]["font_size"] = min_size
return styles
except Exception as e:
logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultPptxStyles()
def _getDefaultPptxStyles(self) -> Dict[str, Any]:
"""Default PowerPoint styles with corporate professional color scheme."""
return {
"title": {"font_size": 52, "color": (27, 54, 93), "bold": True, "align": "center"},
"heading": {"font_size": 36, "color": (44, 95, 45), "bold": True, "align": "left"},
"subheading": {"font_size": 28, "color": (74, 144, 226), "bold": True, "align": "left"},
"paragraph": {"font_size": 20, "color": (47, 47, 47), "bold": False, "align": "left"},
"bullet_list": {"font_size": 20, "color": (47, 47, 47), "indent": 20},
"table_header": {"font_size": 18, "color": (255, 255, 255), "bold": True, "background": (27, 54, 93)},
"table_cell": {"font_size": 16, "color": (47, 47, 47), "bold": False, "background": (248, 249, 250)},
"slide_size": "16:9",
"content_per_slide": "concise",
"design_theme": "corporate",
"color_scheme": "professional",
"background_style": "clean",
"accent_colors": [(27, 54, 93), (44, 95, 45), (74, 144, 226), (107, 114, 128)],
"professional_grade": True,
"executive_ready": True
}
async def _parseJsonToSlides(self, json_content: Dict[str, Any], title: str, styles: Dict[str, Any]) -> List[Dict[str, Any]]: async def _parseJsonToSlides(self, json_content: Dict[str, Any], title: str, styles: Dict[str, Any]) -> List[Dict[str, Any]]:
""" """

View file

@ -205,8 +205,8 @@ class RendererXlsx(BaseRenderer):
self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT TYPE: {type(jsonContent)}", "EXCEL_RENDERER") self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT TYPE: {type(jsonContent)}", "EXCEL_RENDERER")
self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT KEYS: {list(jsonContent.keys()) if isinstance(jsonContent, dict) else 'Not a dict'}", "EXCEL_RENDERER") self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT KEYS: {list(jsonContent.keys()) if isinstance(jsonContent, dict) else 'Not a dict'}", "EXCEL_RENDERER")
# Get AI-generated styling definitions # Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getExcelStyles(userPrompt, aiService) styles = await self._getStyleSet(userPrompt, aiService)
# Validate JSON structure # Validate JSON structure
if not isinstance(jsonContent, dict): if not isinstance(jsonContent, dict):
@ -249,9 +249,82 @@ class RendererXlsx(BaseRenderer):
self.logger.error(f"Error generating Excel from JSON: {str(e)}") self.logger.error(f"Error generating Excel from JSON: {str(e)}")
raise Exception(f"Excel generation failed: {str(e)}") raise Exception(f"Excel generation failed: {str(e)}")
async def _getExcelStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]: async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get Excel styling definitions using base template AI styling.""" """Get style set - default styles, enhanced with AI if userPrompt provided.
styleSchema = {
Args:
userPrompt: User's prompt (AI will detect style instructions in any language)
aiService: AI service (used only if userPrompt provided)
templateName: Name of template style set (None = default)
Returns:
Dict with style definitions for all document styles
"""
# Get default style set
defaultStyleSet = self._getDefaultStyleSet()
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
if userPrompt and aiService:
# AI will naturally detect style instructions in any language
self.logger.info(f"Enhancing styles with AI based on user prompt...")
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
# Convert colors to Excel format after getting styles
enhancedStyleSet = self._convertColorsFormat(enhancedStyleSet)
return self._validateStylesContrast(enhancedStyleSet)
else:
# Use default styles only
return defaultStyleSet
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
"""Enhance default styles with AI based on user prompt."""
try:
style_template = self._createAiStyleTemplate("xlsx", userPrompt, defaultStyleSet)
enhanced_styles = await self._getAiStylesWithExcelColors(aiService, style_template, defaultStyleSet)
return enhanced_styles
except Exception as e:
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
return defaultStyleSet
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix contrast issues in AI-generated styles."""
try:
# Fix table header contrast
if "table_header" in styles:
header = styles["table_header"]
bgColor = header.get("background", "#FFFFFF")
textColor = header.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
header["background"] = "#FF4F4F4F"
header["text_color"] = "#FFFFFFFF"
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
header["background"] = "#FF4F4F4F"
header["text_color"] = "#FFFFFFFF"
# Fix table cell contrast
if "table_cell" in styles:
cell = styles["table_cell"]
bgColor = cell.get("background", "#FFFFFF")
textColor = cell.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
cell["background"] = "#FFFFFFFF"
cell["text_color"] = "#FF2F2F2F"
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
cell["background"] = "#FFFFFFFF"
cell["text_color"] = "#FF2F2F2F"
return styles
except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultStyleSet()
def _getDefaultStyleSet(self) -> Dict[str, Any]:
"""Default Excel style set - used when no style instructions present."""
return {
"title": {"font_size": 16, "color": "#FF1F4E79", "bold": True, "align": "center"}, "title": {"font_size": 16, "color": "#FF1F4E79", "bold": True, "align": "center"},
"heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"}, "heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"},
"table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"}, "table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"},
@ -261,13 +334,6 @@ class RendererXlsx(BaseRenderer):
"code_block": {"font": "Courier New", "font_size": 10, "color": "#FF2F2F2F", "background": "#FFF5F5F5"} "code_block": {"font": "Courier New", "font_size": 10, "color": "#FF2F2F2F", "background": "#FFF5F5F5"}
} }
styleTemplate = self._createAiStyleTemplate("xlsx", userPrompt, styleSchema)
# Use our own _getAiStylesWithExcelColors method to ensure proper color conversion
styles = await self._getAiStylesWithExcelColors(aiService, styleTemplate, self._getDefaultExcelStyles())
# Validate and fix contrast issues
return self._validateExcelStylesContrast(styles)
async def _getAiStylesWithExcelColors(self, aiService, styleTemplate: str, defaultStyles: Dict[str, Any]) -> Dict[str, Any]: async def _getAiStylesWithExcelColors(self, aiService, styleTemplate: str, defaultStyles: Dict[str, Any]) -> Dict[str, Any]:
"""Get AI styles with proper Excel color conversion.""" """Get AI styles with proper Excel color conversion."""
if not aiService: if not aiService:
@ -360,55 +426,6 @@ class RendererXlsx(BaseRenderer):
except Exception as e: except Exception as e:
return styles return styles
def _validateExcelStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and fix contrast issues in AI-generated styles."""
try:
# Fix table header contrast
if "table_header" in styles:
header = styles["table_header"]
bgColor = header.get("background", "#FFFFFF")
textColor = header.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
header["background"] = "#4F4F4F"
header["text_color"] = "#FFFFFF"
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
header["background"] = "#4F4F4F"
header["text_color"] = "#FFFFFF"
# Fix table cell contrast
if "table_cell" in styles:
cell = styles["table_cell"]
bgColor = cell.get("background", "#FFFFFF")
textColor = cell.get("text_color", "#000000")
# If both are white or both are dark, fix it
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
cell["background"] = "#FFFFFF"
cell["text_color"] = "#2F2F2F"
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
cell["background"] = "#FFFFFF"
cell["text_color"] = "#2F2F2F"
return styles
except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultExcelStyles()
def _getDefaultExcelStyles(self) -> Dict[str, Any]:
"""Default Excel styles with aRGB color format."""
return {
"title": {"font_size": 16, "color": "#FF1F4E79", "bold": True, "align": "center"},
"heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"},
"table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"},
"table_cell": {"background": "#FFFFFFFF", "text_color": "#FF2F2F2F", "bold": False, "align": "left"},
"bullet_list": {"font_size": 11, "color": "#FF2F2F2F", "indent": 2},
"paragraph": {"font_size": 11, "color": "#FF2F2F2F", "bold": False, "align": "left"},
"code_block": {"font": "Courier New", "font_size": 10, "color": "#FF2F2F2F", "background": "#FFF5F5F5"}
}
def _createExcelSheets(self, wb: Workbook, jsonContent: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]: def _createExcelSheets(self, wb: Workbook, jsonContent: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]:
"""Create Excel sheets based on content structure and user intent.""" """Create Excel sheets based on content structure and user intent."""
sheets = {} sheets = {}

View file

@ -175,7 +175,8 @@ class MethodAi(MethodBase):
action_documents.append(ActionDocument( action_documents.append(ActionDocument(
documentName=doc.documentName, documentName=doc.documentName,
documentData=doc.documentData, documentData=doc.documentData,
mimeType=doc.mimeType or output_mime_type mimeType=doc.mimeType or output_mime_type,
sourceJson=getattr(doc, 'sourceJson', None) # Preserve source JSON for structure validation
)) ))
final_documents = action_documents final_documents = action_documents
@ -600,7 +601,8 @@ class MethodAi(MethodBase):
actionDoc = ActionDocument( actionDoc = ActionDocument(
documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}", documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}",
documentData=rendered_content, documentData=rendered_content,
mimeType=mime_type mimeType=mime_type,
sourceJson=jsonData # Preserve source JSON for structure validation
) )
return ActionResult.isSuccess(documents=[actionDoc]) return ActionResult.isSuccess(documents=[actionDoc])
@ -807,7 +809,8 @@ class MethodAi(MethodBase):
actionDoc = ActionDocument( actionDoc = ActionDocument(
documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}", documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}",
documentData=rendered_content, documentData=rendered_content,
mimeType=mime_type mimeType=mime_type,
sourceJson=jsonData # Preserve source JSON for structure validation
) )
return ActionResult.isSuccess(documents=[actionDoc]) return ActionResult.isSuccess(documents=[actionDoc])

View file

@ -250,9 +250,16 @@ class ContentValidator:
"size": sizeInfo["readable"] "size": sizeInfo["readable"]
} }
# Extract JSON structure summary if documentData is available # Extract JSON structure summary - prioritize sourceJson for rendered documents
sourceJson = getattr(doc, 'sourceJson', None)
data = getattr(doc, 'documentData', None) data = getattr(doc, 'documentData', None)
if data is not None:
if sourceJson and isinstance(sourceJson, dict):
# Use source JSON for structure analysis (for rendered documents like xlsx/docx/pdf)
jsonSummary = self._summarizeJsonStructure(sourceJson)
summary["jsonStructure"] = jsonSummary
elif data is not None:
# Fallback: try to parse documentData as JSON (for non-rendered documents)
if isinstance(data, dict): if isinstance(data, dict):
# Summarize JSON structure # Summarize JSON structure
jsonSummary = self._summarizeJsonStructure(data) jsonSummary = self._summarizeJsonStructure(data)
@ -436,43 +443,27 @@ EXPECTED DATA TYPE: {dataType}
EXPECTED FORMATS: {expectedFormats if expectedFormats else ['any']} EXPECTED FORMATS: {expectedFormats if expectedFormats else ['any']}
SUCCESS CRITERIA ({criteriaCount} items): {criteriaDisplay}{actionContext} SUCCESS CRITERIA ({criteriaCount} items): {criteriaDisplay}{actionContext}
VALIDATION RULES: VALIDATION CONTEXT:
You have document METADATA (filename, format, size, mimeType) AND JSON STRUCTURE SUMMARY (sections, tables with captions, IDs, statistics). You have METADATA (filename, format, size, mimeType) and STRUCTURE SUMMARY (if available: sections, tables, captions, IDs, statistics).
What CAN be validated: VALIDATION PRINCIPLES:
- Format compatibility: Check if delivered format matches expected format (e.g., xlsx matches xlsx, docx matches docx) 1. Format compatibility: Match delivered format to expected format
- Filename appropriateness: Check if filename suggests correct content type (e.g., "employee_data.xlsx" suggests employee data) 2. Structure validation: Use structure summary to verify requirements (section count, table captions, IDs, section types, etc.)
- Document structure: Use JSON structure summary to validate: 3. Filename appropriateness: Check if filename suggests correct content type
* Number of sections/tables matches requirements 4. Document count: Verify number matches expectations
* Table captions are present and meaningful (if task requires specific tables) 5. Size sanity: Only flag if clearly wrong (<1KB for complex content or suspiciously large)
* Section IDs are present (if needed)
* Table row/column counts are reasonable for the task
* Section types match expectations (e.g., task asks for tables, check if tables are present)
- Document count: Check if number of documents matches expectations
- Basic size sanity: Only flag size if EXTREMELY small (<1KB) or suspiciously large for the task type
What CANNOT be validated: LIMITATIONS:
- Content quality, accuracy, or completeness of actual data values - Cannot validate: Content accuracy, data correctness, formatting details, or requirements requiring full content reading
- Whether specific data values are correct - If structure summary unavailable, validate only metadata (format, filename, count, size)
- Whether formatting details are perfect
- Whether content meets very detailed requirements that require reading actual data
Validation approach: SCORING GUIDELINES:
1. Format matching is PRIMARY - if format matches, qualityScore should be at least 0.7 - Format matches + reasonable structure qualityScore: 0.8-1.0
2. Structure validation using JSON summary is SECONDARY - check if structure matches requirements: - Format matches but structure issues qualityScore: 0.7-0.8
- If task asks for "two sheets" or "two tables", verify section count or table count from JSON summary - Format mismatch qualityScore: <0.7
- If task asks for specific table captions, verify they exist in JSON summary - Only suggest improvements for CLEAR metadata/structure issues
- If task asks for specific structure (e.g., "Employees table" and "Departments table"), verify section titles/captions match
3. Filename appropriateness is TERTIARY - meaningful filenames increase score
4. Size checks should be VERY conservative - only flag if clearly wrong (e.g., 0 bytes or <1KB for complex documents)
5. For successCriteriaMet: Evaluate each criterion using metadata AND JSON structure:
- Format-related criteria: Can be evaluated (e.g., "Excel file" check format)
- Structure-related criteria: Can be evaluated using JSON summary (e.g., "two sheets" check section count, "table with caption X" check JSON summary for caption)
- Content-related criteria: Set to false if cannot be determined from structure (don't guess data values)
6. Only suggest improvements if there are CLEAR issues (wrong format, missing structure elements, etc.)
7. If format matches, structure matches requirements (from JSON summary), and filename is reasonable, qualityScore should be 0.8-1.0
OUTPUT FORMAT - JSON ONLY (no prose): OUTPUT FORMAT (JSON only):
{{ {{
"overallSuccess": false, "overallSuccess": false,
"qualityScore": 0.0, "qualityScore": 0.0,
@ -480,25 +471,17 @@ OUTPUT FORMAT - JSON ONLY (no prose):
"formatMatch": false, "formatMatch": false,
"documentCount": {len(documents)}, "documentCount": {len(documents)},
"successCriteriaMet": {criteriaMetExample}, "successCriteriaMet": {criteriaMetExample},
"gapAnalysis": "Describe what is missing or incorrect based ONLY on metadata (format, filename, count, size). If format matches and filename is reasonable, state that validation is limited by metadata-only access.", "gapAnalysis": "Brief description of gaps based on metadata/structure only. If validation is limited, state this clearly.",
"improvementSuggestions": [], "improvementSuggestions": [],
"validationDetails": [ "validationDetails": [
{{ {{
"documentName": "document.ext", "documentName": "document.ext",
"issues": ["Issue inferred from metadata ONLY"], "issues": ["Issue inferred from metadata/structure only"],
"suggestions": ["Specific fix based on metadata analysis"] "suggestions": ["Specific fix based on metadata/structure analysis"]
}} }}
] ]
}} }}
Field explanations:
- "successCriteriaMet": Array of {criteriaCount} boolean values, one per success criterion. Evaluate each based ONLY on metadata. If a criterion cannot be evaluated from metadata, set to false and explain in gapAnalysis.
- "qualityScore": 0.0-1.0 score. If format matches and filename is reasonable, score should be 0.8-1.0. Only reduce score for clear metadata issues.
- "overallSuccess": true if format matches AND (qualityScore >= 0.8 OR no clear metadata issues)
- "improvementSuggestions": Only include if there are CLEAR metadata issues that can be fixed. If format matches and filename is reasonable, leave empty array [].
- "gapAnalysis": Be honest about limitations - if validation is limited by metadata-only access, state this clearly.
- IMPORTANT: Do NOT suggest improvements based on assumptions about content quality. Only suggest fixes for clear metadata problems (wrong format, missing documents, etc.).
DELIVERED DOCUMENTS ({len(documents)} items): DELIVERED DOCUMENTS ({len(documents)} items):
""" """
@ -511,9 +494,8 @@ DELIVERED DOCUMENTS ({len(documents)} items):
documentSummaries = self._analyzeDocumentsWithSizeLimit(documents, availableBytes) documentSummaries = self._analyzeDocumentsWithSizeLimit(documents, availableBytes)
# Build final prompt with summaries at the end # Build final prompt with summaries at the end
# Format document summaries with JSON structure prominently displayed
documentsJson = json.dumps(documentSummaries, indent=2, ensure_ascii=False) documentsJson = json.dumps(documentSummaries, indent=2, ensure_ascii=False)
validationPrompt = promptBase + documentsJson + "\n\nNOTE: The 'jsonStructure' field in each document summary contains the document structure (sections, tables with captions, IDs, statistics). Use this to validate structure requirements like number of tables, table captions, section types, etc." validationPrompt = promptBase + documentsJson
# Call AI service for validation # Call AI service for validation
response = await self.services.ai.callAiPlanning( response = await self.services.ai.callAiPlanning(
@ -570,6 +552,7 @@ DELIVERED DOCUMENTS ({len(documents)} items):
"overallSuccess": overall if isinstance(overall, bool) else None, "overallSuccess": overall if isinstance(overall, bool) else None,
"qualityScore": float(quality) if isinstance(quality, (int, float)) else None, "qualityScore": float(quality) if isinstance(quality, (int, float)) else None,
"documentCount": len(documentSummaries), "documentCount": len(documentSummaries),
"gapAnalysis": gap if gap else "",
"validationDetails": details if isinstance(details, list) else [{ "validationDetails": details if isinstance(details, list) else [{
"documentName": "AI Validation", "documentName": "AI Validation",
"gapAnalysis": gap, "gapAnalysis": gap,

View file

@ -832,6 +832,9 @@ class DynamicMode(BaseMode):
if quality_score is None: if quality_score is None:
quality_score = 0.0 quality_score = 0.0
enhancedReviewContent += f"Quality Score: {quality_score:.2f}\n" enhancedReviewContent += f"Quality Score: {quality_score:.2f}\n"
gap_analysis = validation.get('gapAnalysis', '')
if gap_analysis:
enhancedReviewContent += f"Gap Analysis: {gap_analysis}\n"
if validation.get('improvementSuggestions'): if validation.get('improvementSuggestions'):
enhancedReviewContent += f"Improvement Suggestions: {', '.join(validation['improvementSuggestions'])}\n" enhancedReviewContent += f"Improvement Suggestions: {', '.join(validation['improvementSuggestions'])}\n"