# Export Engine - Delivery Intelligence """ Generates executive-ready exports from chat responses. Supports PDF, PPTX, and Email HTML formats. No user rework required - one-click export with company branding. """ import io import base64 from datetime import datetime from typing import Dict, List, Optional, Any from dataclasses import dataclass import json # PDF generation try: from reportlab.lib import colors from reportlab.lib.pagesizes import letter, A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image from reportlab.lib.units import inch PDF_AVAILABLE = True except ImportError: PDF_AVAILABLE = False print("⚠️ ReportLab not installed - PDF export disabled") # PPTX generation try: from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN PPTX_AVAILABLE = True except ImportError as e: PPTX_AVAILABLE = False print(f"⚠️ python-pptx not installed - PPTX export disabled: {e}") @dataclass class ExportContent: """Content to export""" title: str answer: str charts: List[Dict] = None # List of chart data/images tables: List[Dict] = None # List of table data metadata: Dict = None # Additional metadata def __post_init__(self): self.charts = self.charts or [] self.tables = self.tables or [] self.metadata = self.metadata or {} @dataclass class CompanyBranding: """Company branding for exports""" company_name: str = "Business Report" primary_color: str = "#3B82F6" secondary_color: str = "#1E40AF" logo_base64: Optional[str] = None font_family: str = "Helvetica" class PDFExporter: """Generates executive PDF reports""" def __init__(self, branding: CompanyBranding = None): self.branding = branding or CompanyBranding() def export(self, content: ExportContent) -> bytes: """Generate PDF from content""" if not PDF_AVAILABLE: raise RuntimeError("ReportLab not installed. Run: pip install reportlab") buffer = io.BytesIO() doc = SimpleDocTemplate(buffer, pagesize=letter, topMargin=0.5*inch, bottomMargin=0.5*inch) styles = getSampleStyleSheet() # Custom styles title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontSize=18, textColor=colors.HexColor(self.branding.primary_color), spaceAfter=12, ) body_style = ParagraphStyle( 'CustomBody', parent=styles['Normal'], fontSize=10, spaceAfter=8, leading=14, ) # Build story story = [] # Header with branding story.append(Paragraph(self.branding.company_name, title_style)) story.append(Paragraph(content.title, styles['Heading2'])) story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", styles['Normal'])) story.append(Spacer(1, 0.25*inch)) # Main content # Split answer into paragraphs and format for paragraph in content.answer.split('\n\n'): if paragraph.strip(): # Handle markdown bold paragraph = paragraph.replace('**', '').replace('**', '') story.append(Paragraph(paragraph.strip(), body_style)) story.append(Spacer(1, 0.5*inch)) # Tables for table_data in content.tables: if table_data.get('rows'): headers = table_data.get('headers', []) rows = table_data.get('rows', []) table_rows = [headers] if headers else [] table_rows.extend(rows) if table_rows: t = Table(table_rows) t.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(self.branding.primary_color)), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black), ])) story.append(t) story.append(Spacer(1, 0.25*inch)) # Footer story.append(Spacer(1, 0.5*inch)) story.append(Paragraph( f"© {datetime.now().year} {self.branding.company_name} | DataVision", styles['Normal'] )) doc.build(story) buffer.seek(0) return buffer.getvalue() class PPTXExporter: """Generates executive PowerPoint presentations""" def __init__(self, branding: CompanyBranding = None): self.branding = branding or CompanyBranding() def export(self, content: ExportContent) -> bytes: """Generate PPTX from content""" if not PPTX_AVAILABLE: raise RuntimeError("python-pptx not installed. Run: pip install python-pptx") prs = Presentation() prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) # Title slide title_slide_layout = prs.slide_layouts[6] # Blank slide = prs.slides.add_slide(title_slide_layout) # Add title txBox = slide.shapes.add_textbox(Inches(0.5), Inches(2), Inches(12), Inches(1.5)) tf = txBox.text_frame p = tf.paragraphs[0] p.text = content.title p.font.size = Pt(44) p.font.bold = True p.alignment = PP_ALIGN.CENTER # Add subtitle with company name txBox2 = slide.shapes.add_textbox(Inches(0.5), Inches(3.5), Inches(12), Inches(1)) tf2 = txBox2.text_frame p2 = tf2.paragraphs[0] p2.text = f"{self.branding.company_name} | {datetime.now().strftime('%B %d, %Y')}" p2.font.size = Pt(24) p2.alignment = PP_ALIGN.CENTER # Content slide with KPIs and insights content_layout = prs.slide_layouts[6] # Blank slide2 = prs.slides.add_slide(content_layout) # Add content txBox3 = slide2.shapes.add_textbox(Inches(0.5), Inches(0.5), Inches(12), Inches(6.5)) tf3 = txBox3.text_frame tf3.word_wrap = True # Parse content (simplified) paragraphs = content.answer.split('\n\n')[:5] # Limit for slide for i, para in enumerate(paragraphs): if i == 0: p = tf3.paragraphs[0] else: p = tf3.add_paragraph() # Clean markdown para = para.replace('**', '').replace('*', '') p.text = para[:300] # Limit length p.font.size = Pt(18 if i == 0 else 14) p.font.bold = (i == 0) # Add chart slides if charts are provided if content.charts: for i, chart_data in enumerate(content.charts): chart_slide = prs.slides.add_slide(prs.slide_layouts[6]) # Chart title chart_title = chart_data.get('title', f'Chart {i+1}') title_box = chart_slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(0.8)) title_tf = title_box.text_frame title_p = title_tf.paragraphs[0] title_p.text = chart_title title_p.font.size = Pt(28) title_p.font.bold = True title_p.alignment = PP_ALIGN.CENTER # If chart has base64 image data, add it if chart_data.get('image_base64'): try: # Decode base64 image img_data = chart_data['image_base64'] if ',' in img_data: img_data = img_data.split(',')[1] # Remove data:image/png;base64, prefix img_bytes = base64.b64decode(img_data) img_stream = io.BytesIO(img_bytes) # Add image centered on slide chart_slide.shapes.add_picture( img_stream, Inches(1.5), # left Inches(1.2), # top Inches(10), # width Inches(5.5) # height ) except Exception as e: # If image fails, add placeholder text placeholder = chart_slide.shapes.add_textbox(Inches(2), Inches(3), Inches(9), Inches(1)) placeholder.text_frame.paragraphs[0].text = f"Chart: {chart_title}\n(Image not available)" placeholder.text_frame.paragraphs[0].font.size = Pt(18) else: # No image - add chart description as text desc_box = chart_slide.shapes.add_textbox(Inches(1), Inches(1.5), Inches(11), Inches(5)) desc_tf = desc_box.text_frame desc_tf.word_wrap = True # Add chart type info chart_type = chart_data.get('type', 'chart') desc_p = desc_tf.paragraphs[0] desc_p.text = f"Chart Type: {chart_type.replace('_', ' ').title()}" desc_p.font.size = Pt(16) # Add data summary if available if chart_data.get('data'): data_info = desc_tf.add_paragraph() data_p = chart_data.get('data', {}) if isinstance(data_p, list) and len(data_p) > 0: first_trace = data_p[0] x_vals = first_trace.get('x', [])[:5] y_vals = first_trace.get('y', [])[:5] data_info.text = f"\nSample Data:\nX: {x_vals}\nY: {y_vals}" data_info.font.size = Pt(12) # Save to bytes buffer = io.BytesIO() prs.save(buffer) buffer.seek(0) return buffer.getvalue() class EmailExporter: """Generates executive email HTML""" def __init__(self, branding: CompanyBranding = None): self.branding = branding or CompanyBranding() def export(self, content: ExportContent) -> str: """Generate email HTML from content""" # Convert markdown-ish to HTML body_html = self._markdown_to_html(content.answer) html = f"""
Business Intelligence Report | {datetime.now().strftime('%B %d, %Y')}
{para}
') return '\n'.join(html_parts) class ExportEngine: """ Unified export engine for all formats. """ def __init__(self, branding: CompanyBranding = None): self.branding = branding or CompanyBranding() self.pdf = PDFExporter(self.branding) self.pptx = PPTXExporter(self.branding) self.email = EmailExporter(self.branding) def export_pdf(self, content: ExportContent) -> bytes: """Export to PDF""" return self.pdf.export(content) def export_pptx(self, content: ExportContent) -> bytes: """Export to PowerPoint""" return self.pptx.export(content) def export_email(self, content: ExportContent) -> str: """Export to HTML email""" return self.email.export(content) def get_available_formats(self) -> List[str]: """Get list of available export formats""" formats = ['email'] # Email always available (pure HTML) if PDF_AVAILABLE: formats.append('pdf') if PPTX_AVAILABLE: formats.append('pptx') return formats def get_export_engine(workspace_id: str = None) -> ExportEngine: """ Get export engine with company branding if available. """ branding = CompanyBranding() if workspace_id: try: from core.company_profile import get_company_profile, get_company_branding profile = get_company_profile(workspace_id) if profile: company_branding = get_company_branding(workspace_id) branding = CompanyBranding( company_name=profile.company_name, primary_color=company_branding.primary_color, secondary_color=company_branding.secondary_color, logo_base64=company_branding.logo_base64, font_family=company_branding.font_family, ) except Exception as e: print(f"⚠️ Could not load company branding: {e}") return ExportEngine(branding)