import csv from fpdf import FPDF import tempfile import os def _safe_text_for_pdf(s: object) -> str: """ Ensure text passed to FPDF only contains characters encodable in latin-1. FPDF (classic) writes using latin-1 encoding by default and will raise an error when encountering characters outside that range (e.g. •). This helper replaces unsupported characters with a best-effort replacement so PDF generation doesn't fail. """ if s is None: return "" try: text = str(s) except Exception: text = "" # encode -> decode using latin-1 with replacement to avoid exceptions return text.encode('latin-1', 'replace').decode('latin-1') def export_evidence_pack_pdf(data, filename=None): """ Export evidence pack as PDF. Data should include clause, summary, checklist, scenario, metadata. Returns path to PDF file. """ pdf = FPDF() pdf.add_page() pdf.set_font("Arial", "B", size=16) pdf.cell(200, 10, txt="Evidence Pack", ln=True, align='C') pdf.ln(10) # Clause section pdf.set_font("Arial", "B", size=12) pdf.cell(0, 8, txt="Clause:", ln=True) pdf.set_font("Arial", size=10) clause_text = data.get('clause_text', 'No clause information available') pdf.multi_cell(0, 6, _safe_text_for_pdf(clause_text)) pdf.ln(3) # Summary section pdf.set_font("Arial", "B", size=12) pdf.cell(0, 8, txt="Summary:", ln=True) pdf.set_font("Arial", size=10) summary_text = data.get('summary', 'No summary available') pdf.multi_cell(0, 6, _safe_text_for_pdf(summary_text)) pdf.ln(3) # Checklist section pdf.set_font("Arial", "B", size=12) pdf.cell(0, 8, txt="Checklist:", ln=True) pdf.set_font("Arial", size=10) checklist = data.get('role_checklist', []) if checklist: for item in checklist: # use a simple hyphen bullet and sanitize text pdf.multi_cell(0, 6, _safe_text_for_pdf(f"- {item}")) else: pdf.multi_cell(0, 6, "No checklist items available") pdf.ln(3) # Source information section pdf.set_font("Arial", "B", size=12) pdf.cell(0, 8, txt="Source Information:", ln=True) pdf.set_font("Arial", size=10) pdf.multi_cell(0, 6, _safe_text_for_pdf(f"Source: {data.get('source_title', 'Not specified')}")) pdf.multi_cell(0, 6, _safe_text_for_pdf(f"Clause ID: {data.get('clause_id', 'Not assigned')}")) pdf.multi_cell(0, 6, _safe_text_for_pdf(f"Date: {data.get('date', 'Not specified')}")) pdf.multi_cell(0, 6, _safe_text_for_pdf(f"URL: {data.get('url', 'Not available')}")) # User role and context information if data.get('user_role'): pdf.multi_cell(0, 6, _safe_text_for_pdf(f"User Role: {data.get('user_role', '').title()}")) if data.get('language_preference'): pdf.multi_cell(0, 6, _safe_text_for_pdf(f"Language: {data.get('language_preference', '').title()}")) pdf.ln(5) scenario = data.get('scenario_analysis',{}) if scenario: pdf.multi_cell(0, 8, _safe_text_for_pdf(f"Scenario Analysis:")) pdf.multi_cell(0, 8, _safe_text_for_pdf(f"Yearly Results: {scenario.get('yearly_results','')}")) pdf.multi_cell(0, 8, _safe_text_for_pdf(f"Cumulative Base: {scenario.get('cumulative_base','')}")) pdf.multi_cell(0, 8, _safe_text_for_pdf(f"Cumulative Scenario: {scenario.get('cumulative_scenario','')}")) pdf.multi_cell(0, 8, _safe_text_for_pdf(f"Optimistic: {scenario.get('optimistic','')}")) pdf.multi_cell(0, 8, _safe_text_for_pdf(f"Pessimistic: {scenario.get('pessimistic','')}")) pdf.multi_cell(0, 8, _safe_text_for_pdf(f"Driver Breakdown: {scenario.get('driver_breakdown','')}")) if not filename: filename = os.path.join(tempfile.gettempdir(), f"evidence_pack_{os.getpid()}.pdf") pdf.output(filename) return filename def export_evidence_pack_csv(data, filename=None): """ Export evidence pack as CSV. Data should include clause, summary, checklist, scenario, metadata. Returns path to CSV file. """ if not filename: filename = os.path.join(tempfile.gettempdir(), f"evidence_pack_{os.getpid()}.csv") with open(filename, 'w', newline='', encoding='utf-8') as csvfile: writer = csv.writer(csvfile) writer.writerow(["Field", "Value"]) writer.writerow(["Clause", data.get('clause_text', 'No clause information available')]) writer.writerow(["Summary", data.get('summary', 'No summary available')]) # Handle checklist properly checklist = data.get('role_checklist', []) if checklist: checklist_text = '; '.join(checklist) else: checklist_text = "No checklist items available" writer.writerow(["Checklist", checklist_text]) writer.writerow(["Source", data.get('source_title', 'Not specified')]) writer.writerow(["Clause ID", data.get('clause_id', 'Not assigned')]) writer.writerow(["Date", data.get('date', 'Not specified')]) writer.writerow(["URL", data.get('url', 'Not available')]) # Add timestamp if available if 'timestamp' in data: writer.writerow(["Generated At", data.get('timestamp', '')]) # Add original query if available if 'original_query' in data: writer.writerow(["Original Query", data.get('original_query', '')]) # Add user context information if 'user_role' in data: writer.writerow(["User Role", data.get('user_role', '').title()]) if 'language_preference' in data: writer.writerow(["Language Preference", data.get('language_preference', '').title()]) scenario = data.get('scenario_analysis', {}) if scenario: writer.writerow(["=== SCENARIO ANALYSIS ===", ""]) writer.writerow(["Yearly Results", scenario.get('yearly_results', '')]) writer.writerow(["Cumulative Base", scenario.get('cumulative_base', '')]) writer.writerow(["Cumulative Scenario", scenario.get('cumulative_scenario', '')]) writer.writerow(["Optimistic", scenario.get('optimistic', '')]) writer.writerow(["Pessimistic", scenario.get('pessimistic', '')]) writer.writerow(["Driver Breakdown", scenario.get('driver_breakdown', '')]) return filename