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)")
|
style: Optional[str] = Field(default="vivid", description="Image style (vivid, natural)")
|
||||||
|
|
||||||
|
|
||||||
class DocumentData(BaseModel):
|
|
||||||
"""Single document in response."""
|
|
||||||
documentName: str = Field(description="Document name")
|
|
||||||
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
|
|
||||||
mimeType: str = Field(description="MIME type of the document")
|
|
||||||
|
|
||||||
|
|
||||||
class AiProcessParameters(BaseModel):
|
class AiProcessParameters(BaseModel):
|
||||||
"""Parameters for AI processing action."""
|
"""Parameters for AI processing action."""
|
||||||
aiPrompt: str = Field(description="AI instruction prompt")
|
aiPrompt: str = Field(description="AI instruction prompt")
|
||||||
|
|
@ -242,91 +235,6 @@ class AiProcessParameters(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AiResponseMetadata(BaseModel):
|
# NOTE: DocumentData, AiResponseMetadata, and AiResponse are defined in datamodelWorkflow.py
|
||||||
"""Metadata for AI response (varies by operation type)."""
|
# Import them from there if needed: from modules.datamodels.datamodelWorkflow import DocumentData, AiResponseMetadata, AiResponse
|
||||||
# Document Generation Metadata
|
|
||||||
title: Optional[str] = Field(None, description="Document title")
|
|
||||||
filename: Optional[str] = Field(None, description="Document filename")
|
|
||||||
|
|
||||||
# Operation-Specific Metadata
|
|
||||||
operationType: Optional[str] = Field(None, description="Type of operation performed")
|
|
||||||
schemaVersion: Optional[str] = Field(None, description="Schema version (e.g., 'parameters_v1')", alias="schema")
|
|
||||||
extractionMethod: Optional[str] = Field(None, description="Method used for extraction")
|
|
||||||
sourceDocuments: Optional[List[str]] = Field(None, description="Source document references")
|
|
||||||
|
|
||||||
# Additional metadata (for extensibility)
|
|
||||||
additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromDict(cls, data: Optional[Dict[str, Any]]) -> Optional["AiResponseMetadata"]:
|
|
||||||
"""Create AiResponseMetadata from dict with camelCase field names."""
|
|
||||||
if not data or not isinstance(data, dict):
|
|
||||||
return None
|
|
||||||
|
|
||||||
knownFields = {"title", "filename", "operationType", "schema", "extractionMethod", "sourceDocuments", "additionalData"}
|
|
||||||
mappedData = {k: v for k, v in data.items() if k in knownFields}
|
|
||||||
|
|
||||||
additionalFields = {k: v for k, v in data.items() if k not in knownFields}
|
|
||||||
if additionalFields:
|
|
||||||
mappedData["additionalData"] = additionalFields
|
|
||||||
|
|
||||||
try:
|
|
||||||
return cls(**mappedData)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class AiResponse(BaseModel):
|
|
||||||
"""Unified response from all AI calls (planning, text, documents)."""
|
|
||||||
content: str = Field(description="Response content (JSON string for planning, text for analysis, unified JSON for documents)")
|
|
||||||
metadata: Optional[AiResponseMetadata] = Field(
|
|
||||||
None,
|
|
||||||
description="Response metadata (varies by operation type)"
|
|
||||||
)
|
|
||||||
documents: Optional[List[DocumentData]] = Field(
|
|
||||||
None,
|
|
||||||
description="Generated documents (only for document generation operations)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def toJson(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Convert AI response content to JSON using enhanced stabilizing failsafe conversion methods.
|
|
||||||
Centralizes AI result to JSON conversion in one place.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict containing the parsed JSON content, or a safe fallback structure if parsing fails.
|
|
||||||
"""
|
|
||||||
if not self.content:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Use enhanced stabilizing failsafe JSON conversion methods from jsonUtils
|
|
||||||
# First, try to extract and parse JSON using the safe methods
|
|
||||||
obj, err, cleaned = tryParseJson(self.content)
|
|
||||||
|
|
||||||
if err is None and isinstance(obj, dict):
|
|
||||||
# Successfully parsed as dict
|
|
||||||
return obj
|
|
||||||
elif err is None and isinstance(obj, list):
|
|
||||||
# Successfully parsed as list - wrap in dict for consistency
|
|
||||||
return {"data": obj}
|
|
||||||
|
|
||||||
# If parsing failed, try to repair broken JSON
|
|
||||||
repaired = repairBrokenJson(self.content)
|
|
||||||
if repaired is not None:
|
|
||||||
return repaired
|
|
||||||
|
|
||||||
# If all else fails, return a safe structure with the cleaned content
|
|
||||||
# Extract JSON string even if it's not fully parseable
|
|
||||||
extracted = extractJsonString(self.content)
|
|
||||||
if extracted and extracted != self.content:
|
|
||||||
# Try one more time with extracted string
|
|
||||||
obj, err, _ = tryParseJson(extracted)
|
|
||||||
if err is None and isinstance(obj, (dict, list)):
|
|
||||||
return obj if isinstance(obj, dict) else {"data": obj}
|
|
||||||
|
|
||||||
# Final fallback: return safe structure with raw content
|
|
||||||
return {
|
|
||||||
"content": self.content,
|
|
||||||
"parseError": True
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -396,6 +396,10 @@ class ActionDocument(BaseModel):
|
||||||
documentName: str = Field(description="Name of the document")
|
documentName: str = Field(description="Name of the document")
|
||||||
documentData: Any = Field(description="Content/data of the document")
|
documentData: Any = Field(description="Content/data of the document")
|
||||||
mimeType: str = Field(description="MIME type of the document")
|
mimeType: str = Field(description="MIME type of the document")
|
||||||
|
sourceJson: Optional[Dict[str, Any]] = Field(
|
||||||
|
None,
|
||||||
|
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
registerModelLabels(
|
||||||
|
|
|
||||||
|
|
@ -95,32 +95,16 @@ class AiResponseMetadata(BaseModel):
|
||||||
# Additional metadata (for extensibility)
|
# Additional metadata (for extensibility)
|
||||||
additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
|
additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fromDict(cls, data: Optional[Dict[str, Any]]) -> Optional["AiResponseMetadata"]:
|
|
||||||
"""Create AiResponseMetadata from dict with camelCase field names"""
|
|
||||||
if not data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Convert snake_case keys to camelCase if needed (for backward compatibility)
|
|
||||||
convertedData = {}
|
|
||||||
for key, value in data.items():
|
|
||||||
# Keep camelCase as-is, convert snake_case if present
|
|
||||||
if '_' in key:
|
|
||||||
# Convert snake_case to camelCase
|
|
||||||
parts = key.split('_')
|
|
||||||
camelKey = parts[0] + ''.join(word.capitalize() for word in parts[1:])
|
|
||||||
convertedData[camelKey] = value
|
|
||||||
else:
|
|
||||||
convertedData[key] = value
|
|
||||||
|
|
||||||
return cls(**convertedData)
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentData(BaseModel):
|
class DocumentData(BaseModel):
|
||||||
"""Single document in response"""
|
"""Single document in response"""
|
||||||
documentName: str = Field(description="Document name")
|
documentName: str = Field(description="Document name")
|
||||||
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
|
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
|
||||||
mimeType: str = Field(description="MIME type of the document")
|
mimeType: str = Field(description="MIME type of the document")
|
||||||
|
sourceJson: Optional[Dict[str, Any]] = Field(
|
||||||
|
None,
|
||||||
|
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExtractContentParameters(BaseModel):
|
class ExtractContentParameters(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -1354,7 +1354,8 @@ Respond with ONLY a JSON object in this exact format:
|
||||||
docData = DocumentData(
|
docData = DocumentData(
|
||||||
documentName=documentName,
|
documentName=documentName,
|
||||||
documentData=rendered_content,
|
documentData=rendered_content,
|
||||||
mimeType=mime_type
|
mimeType=mime_type,
|
||||||
|
sourceJson=generated_data # Preserve source JSON for structure validation
|
||||||
)
|
)
|
||||||
|
|
||||||
metadata = AiResponseMetadata(
|
metadata = AiResponseMetadata(
|
||||||
|
|
|
||||||
|
|
@ -479,14 +479,16 @@ class BaseRenderer(ABC):
|
||||||
|
|
||||||
return f"""You are a professional document styling expert. Generate a complete JSON styling configuration for {formatName.upper()} documents.
|
return f"""You are a professional document styling expert. Generate a complete JSON styling configuration for {formatName.upper()} documents.
|
||||||
|
|
||||||
Use this schema as a template and customize the values for professional document styling:
|
User request: {userPrompt}
|
||||||
|
|
||||||
|
Use this schema as a template:
|
||||||
{schemaJson}
|
{schemaJson}
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- Return ONLY the complete JSON object (no markdown, no explanations)
|
- Return ONLY the complete JSON object (no markdown, no explanations)
|
||||||
- Customize colors, fonts, and spacing for professional appearance
|
- If the user request contains style/formatting/design instructions (in any language), customize the styling accordingly (adapt styles and add styles if needed)
|
||||||
|
- If the user request has NO style instructions, return the default schema values unchanged
|
||||||
- Ensure all objects are properly closed with closing braces
|
- Ensure all objects are properly closed with closing braces
|
||||||
- Make the styling modern and professional
|
- Only modify styles if style instructions are present in the user request
|
||||||
|
|
||||||
Return the complete JSON:"""
|
Return the complete JSON:"""
|
||||||
|
|
@ -57,17 +57,17 @@ class RendererDocx(BaseRenderer):
|
||||||
return f"DOCX Generation Error: {str(e)}", "text/plain"
|
return f"DOCX Generation Error: {str(e)}", "text/plain"
|
||||||
|
|
||||||
async def _generateDocxFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
|
async def _generateDocxFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
|
||||||
"""Generate DOCX content from structured JSON document using AI-generated styling."""
|
"""Generate DOCX content from structured JSON document."""
|
||||||
try:
|
try:
|
||||||
# Create new document
|
# Create new document
|
||||||
doc = Document()
|
doc = Document()
|
||||||
|
|
||||||
# Get AI-generated styling definitions
|
# Get style set: default styles, enhanced with AI if style instructions present
|
||||||
self.logger.info(f"About to call AI styling with user_prompt: {userPrompt[:100] if userPrompt else 'None'}...")
|
styleSet = await self._getStyleSet(userPrompt, aiService)
|
||||||
styles = await self._getDocxStyles(userPrompt, aiService)
|
|
||||||
|
|
||||||
# Apply basic document setup
|
# Setup basic document styles and create all styles from style set
|
||||||
self._setupBasicDocumentStyles(doc)
|
self._setupBasicDocumentStyles(doc)
|
||||||
|
self._setupDocumentStyles(doc, styleSet)
|
||||||
|
|
||||||
# Validate JSON structure
|
# Validate JSON structure
|
||||||
if not isinstance(json_content, dict):
|
if not isinstance(json_content, dict):
|
||||||
|
|
@ -79,15 +79,14 @@ class RendererDocx(BaseRenderer):
|
||||||
# Use title from JSON metadata if available, otherwise use provided title
|
# Use title from JSON metadata if available, otherwise use provided title
|
||||||
document_title = json_content.get("metadata", {}).get("title", title)
|
document_title = json_content.get("metadata", {}).get("title", title)
|
||||||
|
|
||||||
# Add document title using analyzed styles
|
# Add document title using Title style
|
||||||
if document_title:
|
if document_title:
|
||||||
title_heading = doc.add_heading(document_title, level=1)
|
doc.add_paragraph(document_title, style='Title')
|
||||||
title_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
|
|
||||||
# Process each section in order
|
# Process each section in order
|
||||||
sections = json_content.get("sections", [])
|
sections = json_content.get("sections", [])
|
||||||
for section in sections:
|
for section in sections:
|
||||||
self._renderJsonSection(doc, section, styles)
|
self._renderJsonSection(doc, section, styleSet)
|
||||||
|
|
||||||
# Save to buffer
|
# Save to buffer
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|
@ -104,25 +103,44 @@ class RendererDocx(BaseRenderer):
|
||||||
self.logger.error(f"Error generating DOCX from JSON: {str(e)}")
|
self.logger.error(f"Error generating DOCX from JSON: {str(e)}")
|
||||||
raise Exception(f"DOCX generation failed: {str(e)}")
|
raise Exception(f"DOCX generation failed: {str(e)}")
|
||||||
|
|
||||||
async def _getDocxStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
|
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
|
||||||
"""Get DOCX styling definitions using base template AI styling."""
|
"""Get style set - default styles, enhanced with AI if userPrompt provided.
|
||||||
style_schema = {
|
|
||||||
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center"},
|
|
||||||
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left"},
|
|
||||||
"heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left"},
|
|
||||||
"paragraph": {"font_size": 11, "color": "#2F2F2F", "bold": False, "align": "left"},
|
|
||||||
"table_header": {"background": "#4F4F4F", "text_color": "#FFFFFF", "bold": True, "align": "center"},
|
|
||||||
"table_cell": {"background": "#FFFFFF", "text_color": "#2F2F2F", "bold": False, "align": "left"},
|
|
||||||
"table_border": {"style": "horizontal_only", "color": "#000000", "thickness": "thin"},
|
|
||||||
"bullet_list": {"font_size": 11, "color": "#2F2F2F", "indent": 20},
|
|
||||||
"code_block": {"font": "Courier New", "font_size": 10, "color": "#2F2F2F", "background": "#F5F5F5"}
|
|
||||||
}
|
|
||||||
|
|
||||||
style_template = self._createAiStyleTemplate("docx", userPrompt, style_schema)
|
Args:
|
||||||
styles = await self._getAiStyles(aiService, style_template, self._getDefaultStyles())
|
userPrompt: User's prompt (AI will detect style instructions in any language)
|
||||||
|
aiService: AI service (used only if userPrompt provided)
|
||||||
|
templateName: Name of template style set (None = default)
|
||||||
|
|
||||||
# Validate and fix contrast issues
|
Returns:
|
||||||
return self._validateStylesContrast(styles)
|
Dict with style definitions for all document styles
|
||||||
|
"""
|
||||||
|
# Get default style set
|
||||||
|
if templateName == "corporate":
|
||||||
|
defaultStyleSet = self._getCorporateStyleSet()
|
||||||
|
elif templateName == "minimal":
|
||||||
|
defaultStyleSet = self._getMinimalStyleSet()
|
||||||
|
else:
|
||||||
|
defaultStyleSet = self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
|
||||||
|
if userPrompt and aiService:
|
||||||
|
# AI will naturally detect style instructions in any language
|
||||||
|
self.logger.info(f"Enhancing styles with AI based on user prompt...")
|
||||||
|
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
|
||||||
|
return self._validateStylesContrast(enhancedStyleSet)
|
||||||
|
else:
|
||||||
|
# Use default styles only
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
|
||||||
|
"""Enhance default styles with AI based on user prompt."""
|
||||||
|
try:
|
||||||
|
style_template = self._createAiStyleTemplate("docx", userPrompt, defaultStyleSet)
|
||||||
|
enhanced_styles = await self._getAiStyles(aiService, style_template, defaultStyleSet)
|
||||||
|
return enhanced_styles
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Validate and fix contrast issues in AI-generated styles."""
|
"""Validate and fix contrast issues in AI-generated styles."""
|
||||||
|
|
@ -159,10 +177,10 @@ class RendererDocx(BaseRenderer):
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Style validation failed: {str(e)}")
|
self.logger.warning(f"Style validation failed: {str(e)}")
|
||||||
return self._getDefaultStyles()
|
return self._getDefaultStyleSet()
|
||||||
|
|
||||||
def _getDefaultStyles(self) -> Dict[str, Any]:
|
def _getDefaultStyleSet(self) -> Dict[str, Any]:
|
||||||
"""Default DOCX styles."""
|
"""Default DOCX style set - used when no style instructions present."""
|
||||||
return {
|
return {
|
||||||
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center"},
|
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center"},
|
||||||
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left"},
|
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left"},
|
||||||
|
|
@ -613,25 +631,69 @@ class RendererDocx(BaseRenderer):
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _setupDocumentStyles(self, doc):
|
def _setupDocumentStyles(self, doc: Document, styleSet: Dict[str, Any]) -> None:
|
||||||
"""Set up document styles."""
|
"""Create all styles in document from style set.
|
||||||
try:
|
|
||||||
# Set default font
|
Creates styles BEFORE rendering so they're available for use.
|
||||||
style = doc.styles['Normal']
|
"""
|
||||||
font = style.font
|
try:
|
||||||
font.name = 'Calibri'
|
from docx.enum.style import WD_STYLE_TYPE
|
||||||
font.size = Pt(11)
|
|
||||||
|
# Create Title style
|
||||||
|
if "title" in styleSet:
|
||||||
|
self._createStyle(doc, "Title", styleSet["title"], WD_STYLE_TYPE.PARAGRAPH)
|
||||||
|
|
||||||
|
# Create Heading styles (Heading 1, Heading 2)
|
||||||
|
if "heading1" in styleSet:
|
||||||
|
self._createStyle(doc, "Heading 1", styleSet["heading1"], WD_STYLE_TYPE.PARAGRAPH)
|
||||||
|
if "heading2" in styleSet:
|
||||||
|
self._createStyle(doc, "Heading 2", styleSet["heading2"], WD_STYLE_TYPE.PARAGRAPH)
|
||||||
|
|
||||||
|
# Note: List Bullet and List Number are built-in Word styles, no need to create
|
||||||
|
|
||||||
# Set heading styles
|
|
||||||
for i in range(1, 4):
|
|
||||||
heading_style = doc.styles[f'Heading {i}']
|
|
||||||
heading_font = heading_style.font
|
|
||||||
heading_font.name = 'Calibri'
|
|
||||||
heading_font.size = Pt(16 - i * 2)
|
|
||||||
heading_font.bold = True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Could not set up document styles: {str(e)}")
|
self.logger.warning(f"Could not set up document styles: {str(e)}")
|
||||||
|
|
||||||
|
def _createStyle(self, doc: Document, styleName: str, styleConfig: Dict[str, Any], styleType) -> None:
|
||||||
|
"""Create or update a style in the document styles collection."""
|
||||||
|
try:
|
||||||
|
from docx.enum.style import WD_STYLE_TYPE
|
||||||
|
|
||||||
|
# Try to get existing style, or create new one
|
||||||
|
try:
|
||||||
|
doc_style = doc.styles[styleName]
|
||||||
|
except KeyError:
|
||||||
|
# Create new style based on Normal
|
||||||
|
doc_style = doc.styles.add_style(styleName, styleType)
|
||||||
|
# Base it on Normal style
|
||||||
|
doc_style.base_style = doc.styles['Normal']
|
||||||
|
|
||||||
|
# Apply font configuration
|
||||||
|
font = doc_style.font
|
||||||
|
if "font_size" in styleConfig:
|
||||||
|
font.size = Pt(styleConfig["font_size"])
|
||||||
|
if "bold" in styleConfig:
|
||||||
|
font.bold = styleConfig["bold"]
|
||||||
|
if "color" in styleConfig:
|
||||||
|
color_hex = styleConfig["color"].lstrip('#')
|
||||||
|
font.color.rgb = RGBColor(int(color_hex[0:2], 16), int(color_hex[2:4], 16), int(color_hex[4:6], 16))
|
||||||
|
if "font" in styleConfig:
|
||||||
|
font.name = styleConfig["font"]
|
||||||
|
|
||||||
|
# Set paragraph formatting for alignment
|
||||||
|
if "align" in styleConfig:
|
||||||
|
para_format = doc_style.paragraph_format
|
||||||
|
align = styleConfig["align"]
|
||||||
|
if align == "center":
|
||||||
|
para_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
elif align == "right":
|
||||||
|
para_format.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
else:
|
||||||
|
para_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Could not create style '{styleName}': {str(e)}")
|
||||||
|
|
||||||
def _processSection(self, doc, lines: list):
|
def _processSection(self, doc, lines: list):
|
||||||
"""Process a section of content into DOCX elements."""
|
"""Process a section of content into DOCX elements."""
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ class RendererHtml(BaseRenderer):
|
||||||
async def _generateHtmlFromJson(self, jsonContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
|
async def _generateHtmlFromJson(self, jsonContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
|
||||||
"""Generate HTML content from structured JSON document using AI-generated styling."""
|
"""Generate HTML content from structured JSON document using AI-generated styling."""
|
||||||
try:
|
try:
|
||||||
# Get AI-generated styling definitions
|
# Get style set: default styles, enhanced with AI if userPrompt provided
|
||||||
styles = await self._getHtmlStyles(userPrompt, aiService)
|
styles = await self._getStyleSet(userPrompt, aiService)
|
||||||
|
|
||||||
# Validate JSON structure
|
# Validate JSON structure
|
||||||
if not isinstance(jsonContent, dict):
|
if not isinstance(jsonContent, dict):
|
||||||
|
|
@ -97,29 +97,41 @@ class RendererHtml(BaseRenderer):
|
||||||
self.logger.error(f"Error generating HTML from JSON: {str(e)}")
|
self.logger.error(f"Error generating HTML from JSON: {str(e)}")
|
||||||
raise Exception(f"HTML generation failed: {str(e)}")
|
raise Exception(f"HTML generation failed: {str(e)}")
|
||||||
|
|
||||||
async def _getHtmlStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
|
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
|
||||||
"""Get HTML styling definitions using base template AI styling."""
|
"""Get style set - default styles, enhanced with AI if userPrompt provided.
|
||||||
styleSchema = {
|
|
||||||
"title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"},
|
|
||||||
"heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"},
|
|
||||||
"heading2": {"font_size": "1.5em", "color": "#4F4F4F", "font_weight": "bold", "text_align": "left", "margin": "1em 0 0.5em 0"},
|
|
||||||
"paragraph": {"font_size": "1em", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "margin": "0 0 1em 0", "line_height": "1.6"},
|
|
||||||
"table": {"border": "1px solid #ddd", "border_collapse": "collapse", "width": "100%", "margin": "1em 0"},
|
|
||||||
"table_header": {"background": "#4F4F4F", "color": "#FFFFFF", "font_weight": "bold", "text_align": "center", "padding": "12px"},
|
|
||||||
"table_cell": {"background": "#FFFFFF", "color": "#2F2F2F", "font_weight": "normal", "text_align": "left", "padding": "8px", "border": "1px solid #ddd"},
|
|
||||||
"bullet_list": {"font_size": "1em", "color": "#2F2F2F", "margin": "0 0 1em 0", "padding_left": "20px"},
|
|
||||||
"code_block": {"font_family": "Courier New, monospace", "font_size": "0.9em", "color": "#2F2F2F", "background": "#F5F5F5", "padding": "1em", "border": "1px solid #ddd", "border_radius": "4px", "margin": "1em 0"},
|
|
||||||
"image": {"max_width": "100%", "height": "auto", "margin": "1em 0", "border_radius": "4px"},
|
|
||||||
"body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"}
|
|
||||||
}
|
|
||||||
|
|
||||||
styleTemplate = self._createAiStyleTemplate("html", userPrompt, styleSchema)
|
Args:
|
||||||
styles = await self._getAiStyles(aiService, styleTemplate, self._getDefaultHtmlStyles())
|
userPrompt: User's prompt (AI will detect style instructions in any language)
|
||||||
|
aiService: AI service (used only if userPrompt provided)
|
||||||
|
templateName: Name of template style set (None = default)
|
||||||
|
|
||||||
# Validate and fix contrast issues
|
Returns:
|
||||||
return self._validateHtmlStylesContrast(styles)
|
Dict with style definitions for all document styles
|
||||||
|
"""
|
||||||
|
# Get default style set
|
||||||
|
defaultStyleSet = self._getDefaultStyleSet()
|
||||||
|
|
||||||
def _validateHtmlStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
|
||||||
|
if userPrompt and aiService:
|
||||||
|
# AI will naturally detect style instructions in any language
|
||||||
|
self.logger.info(f"Enhancing styles with AI based on user prompt...")
|
||||||
|
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
|
||||||
|
return self._validateStylesContrast(enhancedStyleSet)
|
||||||
|
else:
|
||||||
|
# Use default styles only
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
|
||||||
|
"""Enhance default styles with AI based on user prompt."""
|
||||||
|
try:
|
||||||
|
style_template = self._createAiStyleTemplate("html", userPrompt, defaultStyleSet)
|
||||||
|
enhanced_styles = await self._getAiStyles(aiService, style_template, defaultStyleSet)
|
||||||
|
return enhanced_styles
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Validate and fix contrast issues in AI-generated styles."""
|
"""Validate and fix contrast issues in AI-generated styles."""
|
||||||
try:
|
try:
|
||||||
# Fix table header contrast
|
# Fix table header contrast
|
||||||
|
|
@ -154,11 +166,10 @@ class RendererHtml(BaseRenderer):
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Style validation failed: {str(e)}")
|
self.logger.warning(f"Style validation failed: {str(e)}")
|
||||||
return self._getDefaultHtmlStyles()
|
return self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
def _getDefaultStyleSet(self) -> Dict[str, Any]:
|
||||||
def _getDefaultHtmlStyles(self) -> Dict[str, Any]:
|
"""Default HTML style set - used when no style instructions present."""
|
||||||
"""Default HTML styles."""
|
|
||||||
return {
|
return {
|
||||||
"title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"},
|
"title": {"font_size": "2.5em", "color": "#1F4E79", "font_weight": "bold", "text_align": "center", "margin": "0 0 1em 0"},
|
||||||
"heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"},
|
"heading1": {"font_size": "2em", "color": "#2F2F2F", "font_weight": "bold", "text_align": "left", "margin": "1.5em 0 0.5em 0"},
|
||||||
|
|
@ -173,6 +184,7 @@ class RendererHtml(BaseRenderer):
|
||||||
"body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"}
|
"body": {"font_family": "Arial, sans-serif", "background": "#FFFFFF", "color": "#2F2F2F", "margin": "0", "padding": "20px"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _generateCssStyles(self, styles: Dict[str, Any]) -> str:
|
def _generateCssStyles(self, styles: Dict[str, Any]) -> str:
|
||||||
"""Generate CSS from style definitions."""
|
"""Generate CSS from style definitions."""
|
||||||
css_parts = []
|
css_parts = []
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ class RendererPdf(BaseRenderer):
|
||||||
async def _generatePdfFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
|
async def _generatePdfFromJson(self, json_content: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> str:
|
||||||
"""Generate PDF content from structured JSON document using AI-generated styling."""
|
"""Generate PDF content from structured JSON document using AI-generated styling."""
|
||||||
try:
|
try:
|
||||||
# Get AI-generated styling definitions
|
# Get style set: default styles, enhanced with AI if userPrompt provided
|
||||||
styles = await self._getPdfStyles(userPrompt, aiService)
|
styles = await self._getStyleSet(userPrompt, aiService)
|
||||||
|
|
||||||
# Validate JSON structure
|
# Validate JSON structure
|
||||||
if not isinstance(json_content, dict):
|
if not isinstance(json_content, dict):
|
||||||
|
|
@ -123,9 +123,82 @@ class RendererPdf(BaseRenderer):
|
||||||
self.logger.error(f"Error generating PDF from JSON: {str(e)}")
|
self.logger.error(f"Error generating PDF from JSON: {str(e)}")
|
||||||
raise Exception(f"PDF generation failed: {str(e)}")
|
raise Exception(f"PDF generation failed: {str(e)}")
|
||||||
|
|
||||||
async def _getPdfStyles(self, user_prompt: str, ai_service=None) -> Dict[str, Any]:
|
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
|
||||||
"""Get PDF styling definitions using base template AI styling."""
|
"""Get style set - default styles, enhanced with AI if userPrompt provided.
|
||||||
style_schema = {
|
|
||||||
|
Args:
|
||||||
|
userPrompt: User's prompt (AI will detect style instructions in any language)
|
||||||
|
aiService: AI service (used only if userPrompt provided)
|
||||||
|
templateName: Name of template style set (None = default)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with style definitions for all document styles
|
||||||
|
"""
|
||||||
|
# Get default style set
|
||||||
|
defaultStyleSet = self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
|
||||||
|
if userPrompt and aiService:
|
||||||
|
# AI will naturally detect style instructions in any language
|
||||||
|
self.logger.info(f"Enhancing styles with AI based on user prompt...")
|
||||||
|
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
|
||||||
|
# Convert colors to PDF format after getting styles
|
||||||
|
enhancedStyleSet = self._convertColorsFormat(enhancedStyleSet)
|
||||||
|
return self._validateStylesContrast(enhancedStyleSet)
|
||||||
|
else:
|
||||||
|
# Use default styles only
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
|
||||||
|
"""Enhance default styles with AI based on user prompt."""
|
||||||
|
try:
|
||||||
|
style_template = self._createAiStyleTemplate("pdf", userPrompt, defaultStyleSet)
|
||||||
|
enhanced_styles = await self._getAiStyles(aiService, style_template, defaultStyleSet)
|
||||||
|
return enhanced_styles
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Validate and fix contrast issues in AI-generated styles."""
|
||||||
|
try:
|
||||||
|
# Fix table header contrast
|
||||||
|
if "table_header" in styles:
|
||||||
|
header = styles["table_header"]
|
||||||
|
bg_color = header.get("background", "#FFFFFF")
|
||||||
|
text_color = header.get("text_color", "#000000")
|
||||||
|
|
||||||
|
# If both are white or both are dark, fix it
|
||||||
|
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
|
||||||
|
header["background"] = "#4F4F4F"
|
||||||
|
header["text_color"] = "#FFFFFF"
|
||||||
|
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
|
||||||
|
header["background"] = "#4F4F4F"
|
||||||
|
header["text_color"] = "#FFFFFF"
|
||||||
|
|
||||||
|
# Fix table cell contrast
|
||||||
|
if "table_cell" in styles:
|
||||||
|
cell = styles["table_cell"]
|
||||||
|
bg_color = cell.get("background", "#FFFFFF")
|
||||||
|
text_color = cell.get("text_color", "#000000")
|
||||||
|
|
||||||
|
# If both are white or both are dark, fix it
|
||||||
|
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
|
||||||
|
cell["background"] = "#FFFFFF"
|
||||||
|
cell["text_color"] = "#2F2F2F"
|
||||||
|
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
|
||||||
|
cell["background"] = "#FFFFFF"
|
||||||
|
cell["text_color"] = "#2F2F2F"
|
||||||
|
|
||||||
|
return styles
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Style validation failed: {str(e)}")
|
||||||
|
return self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
def _getDefaultStyleSet(self) -> Dict[str, Any]:
|
||||||
|
"""Default PDF style set - used when no style instructions present."""
|
||||||
|
return {
|
||||||
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center", "space_after": 30},
|
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center", "space_after": 30},
|
||||||
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left", "space_after": 12, "space_before": 12},
|
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left", "space_after": 12, "space_before": 12},
|
||||||
"heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left", "space_after": 8, "space_before": 8},
|
"heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left", "space_after": 8, "space_before": 8},
|
||||||
|
|
@ -136,20 +209,6 @@ class RendererPdf(BaseRenderer):
|
||||||
"code_block": {"font": "Courier", "font_size": 9, "color": "#2F2F2F", "background": "#F5F5F5", "space_after": 6}
|
"code_block": {"font": "Courier", "font_size": 9, "color": "#2F2F2F", "background": "#F5F5F5", "space_after": 6}
|
||||||
}
|
}
|
||||||
|
|
||||||
style_template = self._createAiStyleTemplate("pdf", user_prompt, style_schema)
|
|
||||||
|
|
||||||
# Use base template method like DOCX does (this works!)
|
|
||||||
styles = await self._getAiStyles(ai_service, style_template, self._getDefaultPdfStyles())
|
|
||||||
|
|
||||||
if styles is None:
|
|
||||||
return self._getDefaultPdfStyles()
|
|
||||||
|
|
||||||
# Convert colors to PDF format after getting styles
|
|
||||||
styles = self._convertColorsFormat(styles)
|
|
||||||
|
|
||||||
# Validate and fix contrast issues
|
|
||||||
return self._validatePdfStylesContrast(styles)
|
|
||||||
|
|
||||||
async def _getAiStylesWithPdfColors(self, ai_service, style_template: str, default_styles: Dict[str, Any]) -> Dict[str, Any]:
|
async def _getAiStylesWithPdfColors(self, ai_service, style_template: str, default_styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get AI styles with proper PDF color conversion."""
|
"""Get AI styles with proper PDF color conversion."""
|
||||||
if not ai_service:
|
if not ai_service:
|
||||||
|
|
@ -313,55 +372,6 @@ class RendererPdf(BaseRenderer):
|
||||||
return color_value
|
return color_value
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def _validatePdfStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""Validate and fix contrast issues in AI-generated styles."""
|
|
||||||
try:
|
|
||||||
# Fix table header contrast
|
|
||||||
if "table_header" in styles:
|
|
||||||
header = styles["table_header"]
|
|
||||||
bg_color = header.get("background", "#FFFFFF")
|
|
||||||
text_color = header.get("text_color", "#000000")
|
|
||||||
|
|
||||||
# If both are white or both are dark, fix it
|
|
||||||
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
|
|
||||||
header["background"] = "#4F4F4F"
|
|
||||||
header["text_color"] = "#FFFFFF"
|
|
||||||
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
|
|
||||||
header["background"] = "#4F4F4F"
|
|
||||||
header["text_color"] = "#FFFFFF"
|
|
||||||
|
|
||||||
# Fix table cell contrast
|
|
||||||
if "table_cell" in styles:
|
|
||||||
cell = styles["table_cell"]
|
|
||||||
bg_color = cell.get("background", "#FFFFFF")
|
|
||||||
text_color = cell.get("text_color", "#000000")
|
|
||||||
|
|
||||||
# If both are white or both are dark, fix it
|
|
||||||
if bg_color.upper() == "#FFFFFF" and text_color.upper() == "#FFFFFF":
|
|
||||||
cell["background"] = "#FFFFFF"
|
|
||||||
cell["text_color"] = "#2F2F2F"
|
|
||||||
elif bg_color.upper() == "#000000" and text_color.upper() == "#000000":
|
|
||||||
cell["background"] = "#FFFFFF"
|
|
||||||
cell["text_color"] = "#2F2F2F"
|
|
||||||
|
|
||||||
return styles
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.warning(f"Style validation failed: {str(e)}")
|
|
||||||
return self._getDefaultPdfStyles()
|
|
||||||
|
|
||||||
def _getDefaultPdfStyles(self) -> Dict[str, Any]:
|
|
||||||
"""Default PDF styles."""
|
|
||||||
return {
|
|
||||||
"title": {"font_size": 24, "color": "#1F4E79", "bold": True, "align": "center", "space_after": 30},
|
|
||||||
"heading1": {"font_size": 18, "color": "#2F2F2F", "bold": True, "align": "left", "space_after": 12, "space_before": 12},
|
|
||||||
"heading2": {"font_size": 14, "color": "#4F4F4F", "bold": True, "align": "left", "space_after": 8, "space_before": 8},
|
|
||||||
"paragraph": {"font_size": 11, "color": "#2F2F2F", "bold": False, "align": "left", "space_after": 6, "line_height": 1.2},
|
|
||||||
"table_header": {"background": "#4F4F4F", "text_color": "#FFFFFF", "bold": True, "align": "center", "font_size": 12},
|
|
||||||
"table_cell": {"background": "#FFFFFF", "text_color": "#2F2F2F", "bold": False, "align": "left", "font_size": 10},
|
|
||||||
"bullet_list": {"font_size": 11, "color": "#2F2F2F", "space_after": 3},
|
|
||||||
"code_block": {"font": "Courier", "font_size": 9, "color": "#2F2F2F", "background": "#F5F5F5", "space_after": 6}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _createTitleStyle(self, styles: Dict[str, Any]) -> ParagraphStyle:
|
def _createTitleStyle(self, styles: Dict[str, Any]) -> ParagraphStyle:
|
||||||
"""Create title style from style definitions."""
|
"""Create title style from style definitions."""
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ class RendererPptx(BaseRenderer):
|
||||||
from pptx.dml.color import RGBColor
|
from pptx.dml.color import RGBColor
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Get AI-generated styling definitions first
|
# Get style set: default styles, enhanced with AI if userPrompt provided
|
||||||
styles = await self._getPptxStyles(userPrompt, aiService)
|
styles = await self._getStyleSet(userPrompt, aiService)
|
||||||
|
|
||||||
# Create new presentation
|
# Create new presentation
|
||||||
prs = Presentation()
|
prs = Presentation()
|
||||||
|
|
@ -303,9 +303,71 @@ class RendererPptx(BaseRenderer):
|
||||||
"""Get MIME type for rendered output."""
|
"""Get MIME type for rendered output."""
|
||||||
return self.outputMimeType
|
return self.outputMimeType
|
||||||
|
|
||||||
async def _getPptxStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
|
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
|
||||||
"""Get PowerPoint styling definitions using base template AI styling."""
|
"""Get style set - default styles, enhanced with AI if userPrompt provided.
|
||||||
style_schema = {
|
|
||||||
|
Args:
|
||||||
|
userPrompt: User's prompt (AI will detect style instructions in any language)
|
||||||
|
aiService: AI service (used only if userPrompt provided)
|
||||||
|
templateName: Name of template style set (None = default)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with style definitions for all document styles
|
||||||
|
"""
|
||||||
|
# Get default style set
|
||||||
|
defaultStyleSet = self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
|
||||||
|
if userPrompt and aiService:
|
||||||
|
# AI will naturally detect style instructions in any language
|
||||||
|
self.logger.info(f"Enhancing styles with AI based on user prompt...")
|
||||||
|
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
|
||||||
|
# Convert colors to PPTX format after getting styles
|
||||||
|
enhancedStyleSet = self._convertColorsFormat(enhancedStyleSet)
|
||||||
|
return self._validateStylesReadability(enhancedStyleSet)
|
||||||
|
else:
|
||||||
|
# Use default styles only
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
|
||||||
|
"""Enhance default styles with AI based on user prompt."""
|
||||||
|
try:
|
||||||
|
style_template = self._createProfessionalPptxTemplate(userPrompt, defaultStyleSet)
|
||||||
|
enhanced_styles = await self._getAiStylesWithPptxColors(aiService, style_template, defaultStyleSet)
|
||||||
|
return enhanced_styles
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
def _validateStylesReadability(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Validate and fix readability issues in AI-generated styles."""
|
||||||
|
try:
|
||||||
|
# Ensure minimum font sizes for PowerPoint readability
|
||||||
|
min_font_sizes = {
|
||||||
|
"title": 36,
|
||||||
|
"heading": 24,
|
||||||
|
"subheading": 20,
|
||||||
|
"paragraph": 14,
|
||||||
|
"bullet_list": 14,
|
||||||
|
"table_header": 12,
|
||||||
|
"table_cell": 12
|
||||||
|
}
|
||||||
|
|
||||||
|
for style_name, min_size in min_font_sizes.items():
|
||||||
|
if style_name in styles:
|
||||||
|
current_size = styles[style_name].get("font_size", 12)
|
||||||
|
if current_size < min_size:
|
||||||
|
styles[style_name]["font_size"] = min_size
|
||||||
|
|
||||||
|
return styles
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Style validation failed: {str(e)}")
|
||||||
|
return self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
def _getDefaultStyleSet(self) -> Dict[str, Any]:
|
||||||
|
"""Default PowerPoint style set - used when no style instructions present."""
|
||||||
|
return {
|
||||||
"title": {"font_size": 52, "color": "#1B365D", "bold": True, "align": "center"},
|
"title": {"font_size": 52, "color": "#1B365D", "bold": True, "align": "center"},
|
||||||
"heading": {"font_size": 36, "color": "#2C5F2D", "bold": True, "align": "left"},
|
"heading": {"font_size": 36, "color": "#2C5F2D", "bold": True, "align": "left"},
|
||||||
"subheading": {"font_size": 28, "color": "#4A90E2", "bold": True, "align": "left"},
|
"subheading": {"font_size": 28, "color": "#4A90E2", "bold": True, "align": "left"},
|
||||||
|
|
@ -323,13 +385,6 @@ class RendererPptx(BaseRenderer):
|
||||||
"executive_ready": True
|
"executive_ready": True
|
||||||
}
|
}
|
||||||
|
|
||||||
style_template = self._createProfessionalPptxTemplate(userPrompt, style_schema)
|
|
||||||
# Use our own _getAiStylesWithPptxColors method to ensure proper color conversion
|
|
||||||
styles = await self._getAiStylesWithPptxColors(aiService, style_template, self._getDefaultPptxStyles())
|
|
||||||
|
|
||||||
# Validate PowerPoint-specific requirements
|
|
||||||
return self._validatePptxStylesReadability(styles)
|
|
||||||
|
|
||||||
def _createProfessionalPptxTemplate(self, userPrompt: str, style_schema: Dict[str, Any]) -> str:
|
def _createProfessionalPptxTemplate(self, userPrompt: str, style_schema: Dict[str, Any]) -> str:
|
||||||
"""Create a professional PowerPoint-specific AI style template for corporate-quality slides."""
|
"""Create a professional PowerPoint-specific AI style template for corporate-quality slides."""
|
||||||
import json
|
import json
|
||||||
|
|
@ -495,51 +550,6 @@ JSON ONLY. NO OTHER TEXT."""
|
||||||
return (r, g, b)
|
return (r, g, b)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def _validatePptxStylesReadability(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""Validate and fix readability issues in AI-generated styles."""
|
|
||||||
try:
|
|
||||||
# Ensure minimum font sizes for PowerPoint readability
|
|
||||||
min_font_sizes = {
|
|
||||||
"title": 36,
|
|
||||||
"heading": 24,
|
|
||||||
"subheading": 20,
|
|
||||||
"paragraph": 14,
|
|
||||||
"bullet_list": 14,
|
|
||||||
"table_header": 12,
|
|
||||||
"table_cell": 12
|
|
||||||
}
|
|
||||||
|
|
||||||
for style_name, min_size in min_font_sizes.items():
|
|
||||||
if style_name in styles:
|
|
||||||
current_size = styles[style_name].get("font_size", 12)
|
|
||||||
if current_size < min_size:
|
|
||||||
styles[style_name]["font_size"] = min_size
|
|
||||||
|
|
||||||
return styles
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Style validation failed: {str(e)}")
|
|
||||||
return self._getDefaultPptxStyles()
|
|
||||||
|
|
||||||
def _getDefaultPptxStyles(self) -> Dict[str, Any]:
|
|
||||||
"""Default PowerPoint styles with corporate professional color scheme."""
|
|
||||||
return {
|
|
||||||
"title": {"font_size": 52, "color": (27, 54, 93), "bold": True, "align": "center"},
|
|
||||||
"heading": {"font_size": 36, "color": (44, 95, 45), "bold": True, "align": "left"},
|
|
||||||
"subheading": {"font_size": 28, "color": (74, 144, 226), "bold": True, "align": "left"},
|
|
||||||
"paragraph": {"font_size": 20, "color": (47, 47, 47), "bold": False, "align": "left"},
|
|
||||||
"bullet_list": {"font_size": 20, "color": (47, 47, 47), "indent": 20},
|
|
||||||
"table_header": {"font_size": 18, "color": (255, 255, 255), "bold": True, "background": (27, 54, 93)},
|
|
||||||
"table_cell": {"font_size": 16, "color": (47, 47, 47), "bold": False, "background": (248, 249, 250)},
|
|
||||||
"slide_size": "16:9",
|
|
||||||
"content_per_slide": "concise",
|
|
||||||
"design_theme": "corporate",
|
|
||||||
"color_scheme": "professional",
|
|
||||||
"background_style": "clean",
|
|
||||||
"accent_colors": [(27, 54, 93), (44, 95, 45), (74, 144, 226), (107, 114, 128)],
|
|
||||||
"professional_grade": True,
|
|
||||||
"executive_ready": True
|
|
||||||
}
|
|
||||||
|
|
||||||
async def _parseJsonToSlides(self, json_content: Dict[str, Any], title: str, styles: Dict[str, Any]) -> List[Dict[str, Any]]:
|
async def _parseJsonToSlides(self, json_content: Dict[str, Any], title: str, styles: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -205,8 +205,8 @@ class RendererXlsx(BaseRenderer):
|
||||||
self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT TYPE: {type(jsonContent)}", "EXCEL_RENDERER")
|
self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT TYPE: {type(jsonContent)}", "EXCEL_RENDERER")
|
||||||
self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT KEYS: {list(jsonContent.keys()) if isinstance(jsonContent, dict) else 'Not a dict'}", "EXCEL_RENDERER")
|
self.services.utils.debugLogToFile(f"EXCEL JSON CONTENT KEYS: {list(jsonContent.keys()) if isinstance(jsonContent, dict) else 'Not a dict'}", "EXCEL_RENDERER")
|
||||||
|
|
||||||
# Get AI-generated styling definitions
|
# Get style set: default styles, enhanced with AI if userPrompt provided
|
||||||
styles = await self._getExcelStyles(userPrompt, aiService)
|
styles = await self._getStyleSet(userPrompt, aiService)
|
||||||
|
|
||||||
# Validate JSON structure
|
# Validate JSON structure
|
||||||
if not isinstance(jsonContent, dict):
|
if not isinstance(jsonContent, dict):
|
||||||
|
|
@ -249,9 +249,82 @@ class RendererXlsx(BaseRenderer):
|
||||||
self.logger.error(f"Error generating Excel from JSON: {str(e)}")
|
self.logger.error(f"Error generating Excel from JSON: {str(e)}")
|
||||||
raise Exception(f"Excel generation failed: {str(e)}")
|
raise Exception(f"Excel generation failed: {str(e)}")
|
||||||
|
|
||||||
async def _getExcelStyles(self, userPrompt: str, aiService=None) -> Dict[str, Any]:
|
async def _getStyleSet(self, userPrompt: str = None, aiService=None, templateName: str = None) -> Dict[str, Any]:
|
||||||
"""Get Excel styling definitions using base template AI styling."""
|
"""Get style set - default styles, enhanced with AI if userPrompt provided.
|
||||||
styleSchema = {
|
|
||||||
|
Args:
|
||||||
|
userPrompt: User's prompt (AI will detect style instructions in any language)
|
||||||
|
aiService: AI service (used only if userPrompt provided)
|
||||||
|
templateName: Name of template style set (None = default)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with style definitions for all document styles
|
||||||
|
"""
|
||||||
|
# Get default style set
|
||||||
|
defaultStyleSet = self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
# Enhance with AI if userPrompt provided (AI handles multilingual style detection)
|
||||||
|
if userPrompt and aiService:
|
||||||
|
# AI will naturally detect style instructions in any language
|
||||||
|
self.logger.info(f"Enhancing styles with AI based on user prompt...")
|
||||||
|
enhancedStyleSet = await self._enhanceStylesWithAI(userPrompt, defaultStyleSet, aiService)
|
||||||
|
# Convert colors to Excel format after getting styles
|
||||||
|
enhancedStyleSet = self._convertColorsFormat(enhancedStyleSet)
|
||||||
|
return self._validateStylesContrast(enhancedStyleSet)
|
||||||
|
else:
|
||||||
|
# Use default styles only
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
async def _enhanceStylesWithAI(self, userPrompt: str, defaultStyleSet: Dict[str, Any], aiService) -> Dict[str, Any]:
|
||||||
|
"""Enhance default styles with AI based on user prompt."""
|
||||||
|
try:
|
||||||
|
style_template = self._createAiStyleTemplate("xlsx", userPrompt, defaultStyleSet)
|
||||||
|
enhanced_styles = await self._getAiStylesWithExcelColors(aiService, style_template, defaultStyleSet)
|
||||||
|
return enhanced_styles
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"AI style enhancement failed: {str(e)}, using default styles")
|
||||||
|
return defaultStyleSet
|
||||||
|
|
||||||
|
def _validateStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Validate and fix contrast issues in AI-generated styles."""
|
||||||
|
try:
|
||||||
|
# Fix table header contrast
|
||||||
|
if "table_header" in styles:
|
||||||
|
header = styles["table_header"]
|
||||||
|
bgColor = header.get("background", "#FFFFFF")
|
||||||
|
textColor = header.get("text_color", "#000000")
|
||||||
|
|
||||||
|
# If both are white or both are dark, fix it
|
||||||
|
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
|
||||||
|
header["background"] = "#FF4F4F4F"
|
||||||
|
header["text_color"] = "#FFFFFFFF"
|
||||||
|
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
|
||||||
|
header["background"] = "#FF4F4F4F"
|
||||||
|
header["text_color"] = "#FFFFFFFF"
|
||||||
|
|
||||||
|
# Fix table cell contrast
|
||||||
|
if "table_cell" in styles:
|
||||||
|
cell = styles["table_cell"]
|
||||||
|
bgColor = cell.get("background", "#FFFFFF")
|
||||||
|
textColor = cell.get("text_color", "#000000")
|
||||||
|
|
||||||
|
# If both are white or both are dark, fix it
|
||||||
|
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
|
||||||
|
cell["background"] = "#FFFFFFFF"
|
||||||
|
cell["text_color"] = "#FF2F2F2F"
|
||||||
|
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
|
||||||
|
cell["background"] = "#FFFFFFFF"
|
||||||
|
cell["text_color"] = "#FF2F2F2F"
|
||||||
|
|
||||||
|
return styles
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Style validation failed: {str(e)}")
|
||||||
|
return self._getDefaultStyleSet()
|
||||||
|
|
||||||
|
def _getDefaultStyleSet(self) -> Dict[str, Any]:
|
||||||
|
"""Default Excel style set - used when no style instructions present."""
|
||||||
|
return {
|
||||||
"title": {"font_size": 16, "color": "#FF1F4E79", "bold": True, "align": "center"},
|
"title": {"font_size": 16, "color": "#FF1F4E79", "bold": True, "align": "center"},
|
||||||
"heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"},
|
"heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"},
|
||||||
"table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"},
|
"table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"},
|
||||||
|
|
@ -261,13 +334,6 @@ class RendererXlsx(BaseRenderer):
|
||||||
"code_block": {"font": "Courier New", "font_size": 10, "color": "#FF2F2F2F", "background": "#FFF5F5F5"}
|
"code_block": {"font": "Courier New", "font_size": 10, "color": "#FF2F2F2F", "background": "#FFF5F5F5"}
|
||||||
}
|
}
|
||||||
|
|
||||||
styleTemplate = self._createAiStyleTemplate("xlsx", userPrompt, styleSchema)
|
|
||||||
# Use our own _getAiStylesWithExcelColors method to ensure proper color conversion
|
|
||||||
styles = await self._getAiStylesWithExcelColors(aiService, styleTemplate, self._getDefaultExcelStyles())
|
|
||||||
|
|
||||||
# Validate and fix contrast issues
|
|
||||||
return self._validateExcelStylesContrast(styles)
|
|
||||||
|
|
||||||
async def _getAiStylesWithExcelColors(self, aiService, styleTemplate: str, defaultStyles: Dict[str, Any]) -> Dict[str, Any]:
|
async def _getAiStylesWithExcelColors(self, aiService, styleTemplate: str, defaultStyles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get AI styles with proper Excel color conversion."""
|
"""Get AI styles with proper Excel color conversion."""
|
||||||
if not aiService:
|
if not aiService:
|
||||||
|
|
@ -360,55 +426,6 @@ class RendererXlsx(BaseRenderer):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return styles
|
return styles
|
||||||
|
|
||||||
def _validateExcelStylesContrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""Validate and fix contrast issues in AI-generated styles."""
|
|
||||||
try:
|
|
||||||
# Fix table header contrast
|
|
||||||
if "table_header" in styles:
|
|
||||||
header = styles["table_header"]
|
|
||||||
bgColor = header.get("background", "#FFFFFF")
|
|
||||||
textColor = header.get("text_color", "#000000")
|
|
||||||
|
|
||||||
# If both are white or both are dark, fix it
|
|
||||||
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
|
|
||||||
header["background"] = "#4F4F4F"
|
|
||||||
header["text_color"] = "#FFFFFF"
|
|
||||||
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
|
|
||||||
header["background"] = "#4F4F4F"
|
|
||||||
header["text_color"] = "#FFFFFF"
|
|
||||||
|
|
||||||
# Fix table cell contrast
|
|
||||||
if "table_cell" in styles:
|
|
||||||
cell = styles["table_cell"]
|
|
||||||
bgColor = cell.get("background", "#FFFFFF")
|
|
||||||
textColor = cell.get("text_color", "#000000")
|
|
||||||
|
|
||||||
# If both are white or both are dark, fix it
|
|
||||||
if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
|
|
||||||
cell["background"] = "#FFFFFF"
|
|
||||||
cell["text_color"] = "#2F2F2F"
|
|
||||||
elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
|
|
||||||
cell["background"] = "#FFFFFF"
|
|
||||||
cell["text_color"] = "#2F2F2F"
|
|
||||||
|
|
||||||
return styles
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.warning(f"Style validation failed: {str(e)}")
|
|
||||||
return self._getDefaultExcelStyles()
|
|
||||||
|
|
||||||
def _getDefaultExcelStyles(self) -> Dict[str, Any]:
|
|
||||||
"""Default Excel styles with aRGB color format."""
|
|
||||||
return {
|
|
||||||
"title": {"font_size": 16, "color": "#FF1F4E79", "bold": True, "align": "center"},
|
|
||||||
"heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"},
|
|
||||||
"table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"},
|
|
||||||
"table_cell": {"background": "#FFFFFFFF", "text_color": "#FF2F2F2F", "bold": False, "align": "left"},
|
|
||||||
"bullet_list": {"font_size": 11, "color": "#FF2F2F2F", "indent": 2},
|
|
||||||
"paragraph": {"font_size": 11, "color": "#FF2F2F2F", "bold": False, "align": "left"},
|
|
||||||
"code_block": {"font": "Courier New", "font_size": 10, "color": "#FF2F2F2F", "background": "#FFF5F5F5"}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _createExcelSheets(self, wb: Workbook, jsonContent: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]:
|
def _createExcelSheets(self, wb: Workbook, jsonContent: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Create Excel sheets based on content structure and user intent."""
|
"""Create Excel sheets based on content structure and user intent."""
|
||||||
sheets = {}
|
sheets = {}
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,8 @@ class MethodAi(MethodBase):
|
||||||
action_documents.append(ActionDocument(
|
action_documents.append(ActionDocument(
|
||||||
documentName=doc.documentName,
|
documentName=doc.documentName,
|
||||||
documentData=doc.documentData,
|
documentData=doc.documentData,
|
||||||
mimeType=doc.mimeType or output_mime_type
|
mimeType=doc.mimeType or output_mime_type,
|
||||||
|
sourceJson=getattr(doc, 'sourceJson', None) # Preserve source JSON for structure validation
|
||||||
))
|
))
|
||||||
|
|
||||||
final_documents = action_documents
|
final_documents = action_documents
|
||||||
|
|
@ -600,7 +601,8 @@ class MethodAi(MethodBase):
|
||||||
actionDoc = ActionDocument(
|
actionDoc = ActionDocument(
|
||||||
documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}",
|
documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}",
|
||||||
documentData=rendered_content,
|
documentData=rendered_content,
|
||||||
mimeType=mime_type
|
mimeType=mime_type,
|
||||||
|
sourceJson=jsonData # Preserve source JSON for structure validation
|
||||||
)
|
)
|
||||||
|
|
||||||
return ActionResult.isSuccess(documents=[actionDoc])
|
return ActionResult.isSuccess(documents=[actionDoc])
|
||||||
|
|
@ -807,7 +809,8 @@ class MethodAi(MethodBase):
|
||||||
actionDoc = ActionDocument(
|
actionDoc = ActionDocument(
|
||||||
documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}",
|
documentName=f"{doc.documentName.rsplit('.', 1)[0] if '.' in doc.documentName else doc.documentName}.{normalizedOutputFormat}",
|
||||||
documentData=rendered_content,
|
documentData=rendered_content,
|
||||||
mimeType=mime_type
|
mimeType=mime_type,
|
||||||
|
sourceJson=jsonData # Preserve source JSON for structure validation
|
||||||
)
|
)
|
||||||
|
|
||||||
return ActionResult.isSuccess(documents=[actionDoc])
|
return ActionResult.isSuccess(documents=[actionDoc])
|
||||||
|
|
|
||||||
|
|
@ -250,9 +250,16 @@ class ContentValidator:
|
||||||
"size": sizeInfo["readable"]
|
"size": sizeInfo["readable"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract JSON structure summary if documentData is available
|
# Extract JSON structure summary - prioritize sourceJson for rendered documents
|
||||||
|
sourceJson = getattr(doc, 'sourceJson', None)
|
||||||
data = getattr(doc, 'documentData', None)
|
data = getattr(doc, 'documentData', None)
|
||||||
if data is not None:
|
|
||||||
|
if sourceJson and isinstance(sourceJson, dict):
|
||||||
|
# Use source JSON for structure analysis (for rendered documents like xlsx/docx/pdf)
|
||||||
|
jsonSummary = self._summarizeJsonStructure(sourceJson)
|
||||||
|
summary["jsonStructure"] = jsonSummary
|
||||||
|
elif data is not None:
|
||||||
|
# Fallback: try to parse documentData as JSON (for non-rendered documents)
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
# Summarize JSON structure
|
# Summarize JSON structure
|
||||||
jsonSummary = self._summarizeJsonStructure(data)
|
jsonSummary = self._summarizeJsonStructure(data)
|
||||||
|
|
@ -436,43 +443,27 @@ EXPECTED DATA TYPE: {dataType}
|
||||||
EXPECTED FORMATS: {expectedFormats if expectedFormats else ['any']}
|
EXPECTED FORMATS: {expectedFormats if expectedFormats else ['any']}
|
||||||
SUCCESS CRITERIA ({criteriaCount} items): {criteriaDisplay}{actionContext}
|
SUCCESS CRITERIA ({criteriaCount} items): {criteriaDisplay}{actionContext}
|
||||||
|
|
||||||
VALIDATION RULES:
|
VALIDATION CONTEXT:
|
||||||
You have document METADATA (filename, format, size, mimeType) AND JSON STRUCTURE SUMMARY (sections, tables with captions, IDs, statistics).
|
You have METADATA (filename, format, size, mimeType) and STRUCTURE SUMMARY (if available: sections, tables, captions, IDs, statistics).
|
||||||
|
|
||||||
What CAN be validated:
|
VALIDATION PRINCIPLES:
|
||||||
- Format compatibility: Check if delivered format matches expected format (e.g., xlsx matches xlsx, docx matches docx)
|
1. Format compatibility: Match delivered format to expected format
|
||||||
- Filename appropriateness: Check if filename suggests correct content type (e.g., "employee_data.xlsx" suggests employee data)
|
2. Structure validation: Use structure summary to verify requirements (section count, table captions, IDs, section types, etc.)
|
||||||
- Document structure: Use JSON structure summary to validate:
|
3. Filename appropriateness: Check if filename suggests correct content type
|
||||||
* Number of sections/tables matches requirements
|
4. Document count: Verify number matches expectations
|
||||||
* Table captions are present and meaningful (if task requires specific tables)
|
5. Size sanity: Only flag if clearly wrong (<1KB for complex content or suspiciously large)
|
||||||
* Section IDs are present (if needed)
|
|
||||||
* Table row/column counts are reasonable for the task
|
|
||||||
* Section types match expectations (e.g., task asks for tables, check if tables are present)
|
|
||||||
- Document count: Check if number of documents matches expectations
|
|
||||||
- Basic size sanity: Only flag size if EXTREMELY small (<1KB) or suspiciously large for the task type
|
|
||||||
|
|
||||||
What CANNOT be validated:
|
LIMITATIONS:
|
||||||
- Content quality, accuracy, or completeness of actual data values
|
- Cannot validate: Content accuracy, data correctness, formatting details, or requirements requiring full content reading
|
||||||
- Whether specific data values are correct
|
- If structure summary unavailable, validate only metadata (format, filename, count, size)
|
||||||
- Whether formatting details are perfect
|
|
||||||
- Whether content meets very detailed requirements that require reading actual data
|
|
||||||
|
|
||||||
Validation approach:
|
SCORING GUIDELINES:
|
||||||
1. Format matching is PRIMARY - if format matches, qualityScore should be at least 0.7
|
- Format matches + reasonable structure → qualityScore: 0.8-1.0
|
||||||
2. Structure validation using JSON summary is SECONDARY - check if structure matches requirements:
|
- Format matches but structure issues → qualityScore: 0.7-0.8
|
||||||
- If task asks for "two sheets" or "two tables", verify section count or table count from JSON summary
|
- Format mismatch → qualityScore: <0.7
|
||||||
- If task asks for specific table captions, verify they exist in JSON summary
|
- Only suggest improvements for CLEAR metadata/structure issues
|
||||||
- If task asks for specific structure (e.g., "Employees table" and "Departments table"), verify section titles/captions match
|
|
||||||
3. Filename appropriateness is TERTIARY - meaningful filenames increase score
|
|
||||||
4. Size checks should be VERY conservative - only flag if clearly wrong (e.g., 0 bytes or <1KB for complex documents)
|
|
||||||
5. For successCriteriaMet: Evaluate each criterion using metadata AND JSON structure:
|
|
||||||
- Format-related criteria: Can be evaluated (e.g., "Excel file" → check format)
|
|
||||||
- Structure-related criteria: Can be evaluated using JSON summary (e.g., "two sheets" → check section count, "table with caption X" → check JSON summary for caption)
|
|
||||||
- Content-related criteria: Set to false if cannot be determined from structure (don't guess data values)
|
|
||||||
6. Only suggest improvements if there are CLEAR issues (wrong format, missing structure elements, etc.)
|
|
||||||
7. If format matches, structure matches requirements (from JSON summary), and filename is reasonable, qualityScore should be 0.8-1.0
|
|
||||||
|
|
||||||
OUTPUT FORMAT - JSON ONLY (no prose):
|
OUTPUT FORMAT (JSON only):
|
||||||
{{
|
{{
|
||||||
"overallSuccess": false,
|
"overallSuccess": false,
|
||||||
"qualityScore": 0.0,
|
"qualityScore": 0.0,
|
||||||
|
|
@ -480,25 +471,17 @@ OUTPUT FORMAT - JSON ONLY (no prose):
|
||||||
"formatMatch": false,
|
"formatMatch": false,
|
||||||
"documentCount": {len(documents)},
|
"documentCount": {len(documents)},
|
||||||
"successCriteriaMet": {criteriaMetExample},
|
"successCriteriaMet": {criteriaMetExample},
|
||||||
"gapAnalysis": "Describe what is missing or incorrect based ONLY on metadata (format, filename, count, size). If format matches and filename is reasonable, state that validation is limited by metadata-only access.",
|
"gapAnalysis": "Brief description of gaps based on metadata/structure only. If validation is limited, state this clearly.",
|
||||||
"improvementSuggestions": [],
|
"improvementSuggestions": [],
|
||||||
"validationDetails": [
|
"validationDetails": [
|
||||||
{{
|
{{
|
||||||
"documentName": "document.ext",
|
"documentName": "document.ext",
|
||||||
"issues": ["Issue inferred from metadata ONLY"],
|
"issues": ["Issue inferred from metadata/structure only"],
|
||||||
"suggestions": ["Specific fix based on metadata analysis"]
|
"suggestions": ["Specific fix based on metadata/structure analysis"]
|
||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
|
|
||||||
Field explanations:
|
|
||||||
- "successCriteriaMet": Array of {criteriaCount} boolean values, one per success criterion. Evaluate each based ONLY on metadata. If a criterion cannot be evaluated from metadata, set to false and explain in gapAnalysis.
|
|
||||||
- "qualityScore": 0.0-1.0 score. If format matches and filename is reasonable, score should be 0.8-1.0. Only reduce score for clear metadata issues.
|
|
||||||
- "overallSuccess": true if format matches AND (qualityScore >= 0.8 OR no clear metadata issues)
|
|
||||||
- "improvementSuggestions": Only include if there are CLEAR metadata issues that can be fixed. If format matches and filename is reasonable, leave empty array [].
|
|
||||||
- "gapAnalysis": Be honest about limitations - if validation is limited by metadata-only access, state this clearly.
|
|
||||||
- IMPORTANT: Do NOT suggest improvements based on assumptions about content quality. Only suggest fixes for clear metadata problems (wrong format, missing documents, etc.).
|
|
||||||
|
|
||||||
DELIVERED DOCUMENTS ({len(documents)} items):
|
DELIVERED DOCUMENTS ({len(documents)} items):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -511,9 +494,8 @@ DELIVERED DOCUMENTS ({len(documents)} items):
|
||||||
documentSummaries = self._analyzeDocumentsWithSizeLimit(documents, availableBytes)
|
documentSummaries = self._analyzeDocumentsWithSizeLimit(documents, availableBytes)
|
||||||
|
|
||||||
# Build final prompt with summaries at the end
|
# Build final prompt with summaries at the end
|
||||||
# Format document summaries with JSON structure prominently displayed
|
|
||||||
documentsJson = json.dumps(documentSummaries, indent=2, ensure_ascii=False)
|
documentsJson = json.dumps(documentSummaries, indent=2, ensure_ascii=False)
|
||||||
validationPrompt = promptBase + documentsJson + "\n\nNOTE: The 'jsonStructure' field in each document summary contains the document structure (sections, tables with captions, IDs, statistics). Use this to validate structure requirements like number of tables, table captions, section types, etc."
|
validationPrompt = promptBase + documentsJson
|
||||||
|
|
||||||
# Call AI service for validation
|
# Call AI service for validation
|
||||||
response = await self.services.ai.callAiPlanning(
|
response = await self.services.ai.callAiPlanning(
|
||||||
|
|
@ -570,6 +552,7 @@ DELIVERED DOCUMENTS ({len(documents)} items):
|
||||||
"overallSuccess": overall if isinstance(overall, bool) else None,
|
"overallSuccess": overall if isinstance(overall, bool) else None,
|
||||||
"qualityScore": float(quality) if isinstance(quality, (int, float)) else None,
|
"qualityScore": float(quality) if isinstance(quality, (int, float)) else None,
|
||||||
"documentCount": len(documentSummaries),
|
"documentCount": len(documentSummaries),
|
||||||
|
"gapAnalysis": gap if gap else "",
|
||||||
"validationDetails": details if isinstance(details, list) else [{
|
"validationDetails": details if isinstance(details, list) else [{
|
||||||
"documentName": "AI Validation",
|
"documentName": "AI Validation",
|
||||||
"gapAnalysis": gap,
|
"gapAnalysis": gap,
|
||||||
|
|
|
||||||
|
|
@ -832,6 +832,9 @@ class DynamicMode(BaseMode):
|
||||||
if quality_score is None:
|
if quality_score is None:
|
||||||
quality_score = 0.0
|
quality_score = 0.0
|
||||||
enhancedReviewContent += f"Quality Score: {quality_score:.2f}\n"
|
enhancedReviewContent += f"Quality Score: {quality_score:.2f}\n"
|
||||||
|
gap_analysis = validation.get('gapAnalysis', '')
|
||||||
|
if gap_analysis:
|
||||||
|
enhancedReviewContent += f"Gap Analysis: {gap_analysis}\n"
|
||||||
if validation.get('improvementSuggestions'):
|
if validation.get('improvementSuggestions'):
|
||||||
enhancedReviewContent += f"Improvement Suggestions: {', '.join(validation['improvementSuggestions'])}\n"
|
enhancedReviewContent += f"Improvement Suggestions: {', '.join(validation['improvementSuggestions'])}\n"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue