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)")
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):
"""Parameters for AI processing action."""
aiPrompt: str = Field(description="AI instruction prompt")
@ -242,91 +235,6 @@ class AiProcessParameters(BaseModel):
)
class AiResponseMetadata(BaseModel):
"""Metadata for AI response (varies by operation type)."""
# 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
}
# NOTE: DocumentData, AiResponseMetadata, and AiResponse are defined in datamodelWorkflow.py
# Import them from there if needed: from modules.datamodels.datamodelWorkflow import DocumentData, AiResponseMetadata, AiResponse

View file

@ -396,6 +396,10 @@ class ActionDocument(BaseModel):
documentName: str = Field(description="Name of the document")
documentData: Any = Field(description="Content/data 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(

View file

@ -94,26 +94,6 @@ class AiResponseMetadata(BaseModel):
# 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:
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):
@ -121,6 +101,10 @@ class DocumentData(BaseModel):
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")
sourceJson: Optional[Dict[str, Any]] = Field(
None,
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
)
class ExtractContentParameters(BaseModel):

View file

@ -1354,7 +1354,8 @@ Respond with ONLY a JSON object in this exact format:
docData = DocumentData(
documentName=documentName,
documentData=rendered_content,
mimeType=mime_type
mimeType=mime_type,
sourceJson=generated_data # Preserve source JSON for structure validation
)
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.
Use this schema as a template and customize the values for professional document styling:
User request: {userPrompt}
Use this schema as a template:
{schemaJson}
Requirements:
- 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
- Make the styling modern and professional
- Only modify styles if style instructions are present in the user request
Return the complete JSON:"""

View file

@ -57,17 +57,17 @@ class RendererDocx(BaseRenderer):
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:
"""Generate DOCX content from structured JSON document using AI-generated styling."""
"""Generate DOCX content from structured JSON document."""
try:
# Create new document
doc = Document()
# Get AI-generated styling definitions
self.logger.info(f"About to call AI styling with user_prompt: {userPrompt[:100] if userPrompt else 'None'}...")
styles = await self._getDocxStyles(userPrompt, aiService)
# Get style set: default styles, enhanced with AI if style instructions present
styleSet = await self._getStyleSet(userPrompt, aiService)
# Apply basic document setup
# Setup basic document styles and create all styles from style set
self._setupBasicDocumentStyles(doc)
self._setupDocumentStyles(doc, styleSet)
# Validate JSON structure
if not isinstance(json_content, dict):
@ -79,15 +79,14 @@ class RendererDocx(BaseRenderer):
# Use title from JSON metadata if available, otherwise use provided 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:
title_heading = doc.add_heading(document_title, level=1)
title_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.add_paragraph(document_title, style='Title')
# Process each section in order
sections = json_content.get("sections", [])
for section in sections:
self._renderJsonSection(doc, section, styles)
self._renderJsonSection(doc, section, styleSet)
# Save to buffer
buffer = io.BytesIO()
@ -104,25 +103,44 @@ class RendererDocx(BaseRenderer):
self.logger.error(f"Error generating DOCX from JSON: {str(e)}")
raise Exception(f"DOCX generation failed: {str(e)}")
async def _getDocxStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
"""Get DOCX styling definitions using base template AI styling."""
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"}
}
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get style set - default styles, enhanced with AI if userPrompt provided.
style_template = self._createAiStyleTemplate("docx", userPrompt, style_schema)
styles = await self._getAiStyles(aiService, style_template, self._getDefaultStyles())
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
if templateName == "corporate":
defaultStyleSet = self._getCorporateStyleSet()
elif templateName == "minimal":
defaultStyleSet = self._getMinimalStyleSet()
else:
defaultStyleSet = self._getDefaultStyleSet()
# Validate and fix contrast issues
return self._validateStylesContrast(styles)
# 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]:
"""Validate and fix contrast issues in AI-generated styles."""
@ -159,10 +177,10 @@ class RendererDocx(BaseRenderer):
except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultStyles()
return self._getDefaultStyleSet()
def _getDefaultStyles(self) -> Dict[str, Any]:
"""Default DOCX styles."""
def _getDefaultStyleSet(self) -> Dict[str, Any]:
"""Default DOCX style set - used when no style instructions present."""
return {
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center"},
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left"},
@ -613,25 +631,69 @@ class RendererDocx(BaseRenderer):
return ""
def _setupDocumentStyles(self, doc):
"""Set up document styles."""
def _setupDocumentStyles(self, doc: Document, styleSet: Dict[str, Any]) -> None:
"""Create all styles in document from style set.
Creates styles BEFORE rendering so they're available for use.
"""
try:
# Set default font
style = doc.styles['Normal']
font = style.font
font.name = 'Calibri'
font.size = Pt(11)
from docx.enum.style import WD_STYLE_TYPE
# 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:
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):
"""Process a section of content into DOCX elements."""
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:
"""Generate HTML content from structured JSON document using AI-generated styling."""
try:
# Get AI-generated styling definitions
styles = await self._getHtmlStyles(userPrompt, aiService)
# Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getStyleSet(userPrompt, aiService)
# Validate JSON structure
if not isinstance(jsonContent, dict):
@ -97,29 +97,41 @@ class RendererHtml(BaseRenderer):
self.logger.error(f"Error generating HTML from JSON: {str(e)}")
raise Exception(f"HTML generation failed: {str(e)}")
async def _getHtmlStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
"""Get HTML styling definitions using base template AI styling."""
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"}
}
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get style set - default styles, enhanced with AI if userPrompt provided.
styleTemplate = self._createAiStyleTemplate("html", userPrompt, styleSchema)
styles = await self._getAiStyles(aiService, styleTemplate, self._getDefaultHtmlStyles())
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()
# Validate and fix contrast issues
return self._validateHtmlStylesContrast(styles)
# 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
def _validateHtmlStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
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."""
try:
# Fix table header contrast
@ -154,11 +166,10 @@ class RendererHtml(BaseRenderer):
except Exception as e:
self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultHtmlStyles()
return self._getDefaultStyleSet()
def _getDefaultHtmlStyles(self) -> Dict[str, Any]:
"""Default HTML styles."""
def _getDefaultStyleSet(self) -> Dict[str, Any]:
"""Default HTML style set - used when no style instructions present."""
return {
"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"},
@ -173,6 +184,7 @@ class RendererHtml(BaseRenderer):
"body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"}
}
def _generateCssStyles(self, styles: Dict[str, Any]) -> str:
"""Generate CSS from style definitions."""
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:
"""Generate PDF content from structured JSON document using AI-generated styling."""
try:
# Get AI-generated styling definitions
styles = await self._getPdfStyles(userPrompt, aiService)
# Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getStyleSet(userPrompt, aiService)
# Validate JSON structure
if not isinstance(json_content, dict):
@ -123,9 +123,82 @@ class RendererPdf(BaseRenderer):
self.logger.error(f"Error generating PDF from JSON: {str(e)}")
raise Exception(f"PDF generation failed: {str(e)}")
async def _getPdfStyles(self, user_prompt: str, ai_service=None) -> Dict[str, Any]:
"""Get PDF styling definitions using base template AI styling."""
style_schema = {
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get style set - default styles, enhanced with AI if userPrompt provided.
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},
"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},
@ -135,20 +208,6 @@ class RendererPdf(BaseRenderer):
"bullet_list": {"font_size": 11, "color": "#2F2F2F", "space_after": 3},
"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]:
"""Get AI styles with proper PDF color conversion."""
@ -313,55 +372,6 @@ class RendererPdf(BaseRenderer):
return color_value
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:
"""Create title style from style definitions."""

View file

@ -42,8 +42,8 @@ class RendererPptx(BaseRenderer):
from pptx.dml.color import RGBColor
import re
# Get AI-generated styling definitions first
styles = await self._getPptxStyles(userPrompt, aiService)
# Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getStyleSet(userPrompt, aiService)
# Create new presentation
prs = Presentation()
@ -303,9 +303,71 @@ class RendererPptx(BaseRenderer):
"""Get MIME type for rendered output."""
return self.outputMimeType
async def _getPptxStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
"""Get PowerPoint styling definitions using base template AI styling."""
style_schema = {
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get style set - default styles, enhanced with AI if userPrompt provided.
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"},
"heading": {"font_size": 36, "color": "#2C5F2D", "bold": True, "align": "left"},
"subheading": {"font_size": 28, "color": "#4A90E2", "bold": True, "align": "left"},
@ -322,13 +384,6 @@ class RendererPptx(BaseRenderer):
"professional_grade": 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:
"""Create a professional PowerPoint-specific AI style template for corporate-quality slides."""
@ -495,51 +550,6 @@ JSON ONLY. NO OTHER TEXT."""
return (r, g, b)
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]]:
"""

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 KEYS: {list(jsonContent.keys()) if isinstance(jsonContent, dict) else 'Not a dict'}", "EXCEL_RENDERER")
# Get AI-generated styling definitions
styles = await self._getExcelStyles(userPrompt, aiService)
# Get style set: default styles, enhanced with AI if userPrompt provided
styles = await self._getStyleSet(userPrompt, aiService)
# Validate JSON structure
if not isinstance(jsonContent, dict):
@ -249,9 +249,82 @@ class RendererXlsx(BaseRenderer):
self.logger.error(f"Error generating Excel from JSON: {str(e)}")
raise Exception(f"Excel generation failed: {str(e)}")
async def _getExcelStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
"""Get Excel styling definitions using base template AI styling."""
styleSchema = {
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
"""Get style set - default styles, enhanced with AI if userPrompt provided.
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"},
"heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"},
"table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"},
@ -260,13 +333,6 @@ class RendererXlsx(BaseRenderer):
"paragraph": {"font_size": 11, "color": "#FF2F2F2F", "bold": False, "align": "left"},
"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]:
"""Get AI styles with proper Excel color conversion."""
@ -360,55 +426,6 @@ class RendererXlsx(BaseRenderer):
except Exception as e:
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]:
"""Create Excel sheets based on content structure and user intent."""
sheets = {}

View file

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

View file

@ -250,9 +250,16 @@ class ContentValidator:
"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)
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):
# Summarize JSON structure
jsonSummary = self._summarizeJsonStructure(data)
@ -436,43 +443,27 @@ EXPECTED DATA TYPE: {dataType}
EXPECTED FORMATS: {expectedFormats if expectedFormats else ['any']}
SUCCESS CRITERIA ({criteriaCount} items): {criteriaDisplay}{actionContext}
VALIDATION RULES:
You have document METADATA (filename, format, size, mimeType) AND JSON STRUCTURE SUMMARY (sections, tables with captions, IDs, statistics).
VALIDATION CONTEXT:
You have METADATA (filename, format, size, mimeType) and STRUCTURE SUMMARY (if available: sections, tables, captions, IDs, statistics).
What CAN be validated:
- Format compatibility: Check if delivered format matches expected format (e.g., xlsx matches xlsx, docx matches docx)
- Filename appropriateness: Check if filename suggests correct content type (e.g., "employee_data.xlsx" suggests employee data)
- Document structure: Use JSON structure summary to validate:
* Number of sections/tables matches requirements
* Table captions are present and meaningful (if task requires specific tables)
* 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
VALIDATION PRINCIPLES:
1. Format compatibility: Match delivered format to expected format
2. Structure validation: Use structure summary to verify requirements (section count, table captions, IDs, section types, etc.)
3. Filename appropriateness: Check if filename suggests correct content type
4. Document count: Verify number matches expectations
5. Size sanity: Only flag if clearly wrong (<1KB for complex content or suspiciously large)
What CANNOT be validated:
- Content quality, accuracy, or completeness of actual data values
- Whether specific data values are correct
- Whether formatting details are perfect
- Whether content meets very detailed requirements that require reading actual data
LIMITATIONS:
- Cannot validate: Content accuracy, data correctness, formatting details, or requirements requiring full content reading
- If structure summary unavailable, validate only metadata (format, filename, count, size)
Validation approach:
1. Format matching is PRIMARY - if format matches, qualityScore should be at least 0.7
2. Structure validation using JSON summary is SECONDARY - check if structure matches requirements:
- If task asks for "two sheets" or "two tables", verify section count or table count from JSON summary
- If task asks for specific table captions, verify they exist in JSON summary
- 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
SCORING GUIDELINES:
- Format matches + reasonable structure qualityScore: 0.8-1.0
- Format matches but structure issues qualityScore: 0.7-0.8
- Format mismatch qualityScore: <0.7
- Only suggest improvements for CLEAR metadata/structure issues
OUTPUT FORMAT - JSON ONLY (no prose):
OUTPUT FORMAT (JSON only):
{{
"overallSuccess": false,
"qualityScore": 0.0,
@ -480,25 +471,17 @@ OUTPUT FORMAT - JSON ONLY (no prose):
"formatMatch": false,
"documentCount": {len(documents)},
"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": [],
"validationDetails": [
{{
"documentName": "document.ext",
"issues": ["Issue inferred from metadata ONLY"],
"suggestions": ["Specific fix based on metadata analysis"]
"issues": ["Issue inferred from metadata/structure only"],
"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):
"""
@ -511,9 +494,8 @@ DELIVERED DOCUMENTS ({len(documents)} items):
documentSummaries = self._analyzeDocumentsWithSizeLimit(documents, availableBytes)
# 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)
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
response = await self.services.ai.callAiPlanning(
@ -570,6 +552,7 @@ DELIVERED DOCUMENTS ({len(documents)} items):
"overallSuccess": overall if isinstance(overall, bool) else None,
"qualityScore": float(quality) if isinstance(quality, (int, float)) else None,
"documentCount": len(documentSummaries),
"gapAnalysis": gap if gap else "",
"validationDetails": details if isinstance(details, list) else [{
"documentName": "AI Validation",
"gapAnalysis": gap,

View file

@ -832,6 +832,9 @@ class DynamicMode(BaseMode):
if quality_score is None:
quality_score = 0.0
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'):
enhancedReviewContent += f"Improvement Suggestions: {', '.join(validation['improvementSuggestions'])}\n"