enhanced generation and rendering chain
This commit is contained in:
parent
c135321aee
commit
11bb127a43
13 changed files with 423 additions and 424 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -95,32 +95,16 @@ 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):
|
||||
"""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")
|
||||
sourceJson: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
|
||||
)
|
||||
|
||||
|
||||
class ExtractContentParameters(BaseModel):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:"""
|
||||
|
|
@ -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)
|
||||
|
||||
# Validate and fix contrast issues
|
||||
return self._validateStylesContrast(styles)
|
||||
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()
|
||||
|
||||
# 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."""
|
||||
try:
|
||||
# Set default font
|
||||
style = doc.styles['Normal']
|
||||
font = style.font
|
||||
font.name = 'Calibri'
|
||||
font.size = Pt(11)
|
||||
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:
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
# Validate and fix contrast issues
|
||||
return self._validateHtmlStylesContrast(styles)
|
||||
Returns:
|
||||
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."""
|
||||
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 = []
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
@ -136,20 +209,6 @@ class RendererPdf(BaseRenderer):
|
|||
"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."""
|
||||
if not ai_service:
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
@ -323,13 +385,6 @@ class RendererPptx(BaseRenderer):
|
|||
"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."""
|
||||
import json
|
||||
|
|
@ -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]]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
@ -261,13 +334,6 @@ class RendererXlsx(BaseRenderer):
|
|||
"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."""
|
||||
if not aiService:
|
||||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue