from pathlib import Path from typing import Any, Dict, List, Optional from jinja2 import Environment, FileSystemLoader from playwright.async_api import async_playwright class ReportGenerator: """Service for generating PDF reports from HTML templates""" def __init__(self): # Set up Jinja2 environment template_dir = Path(__file__).parent.parent / "templates" self.env = Environment(loader=FileSystemLoader(str(template_dir))) async def generate_investor_report( self, investor_data: Dict[str, Any], project_data: Optional[Dict[str, Any]] = None, ) -> bytes: """ Generate a PDF report for an investor profile. Args: investor_data: Dictionary containing investor information project_data: Optional dictionary containing project information for compatibility analysis Returns: bytes: PDF file content """ # Prepare template context context = self._prepare_context(investor_data, project_data) # Render HTML from template template = self.env.get_template("report.html") html_content = template.render(**context) # Convert HTML to PDF using Playwright pdf_bytes = await self._html_to_pdf(html_content) return pdf_bytes def _prepare_context( self, investor_data: Dict[str, Any], project_data: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Prepare the context dictionary for template rendering""" context = { "investor": investor_data, "project": project_data, "compatibility_score": 0, "match_criteria": [], "recommendation": None, } # If project data is provided, calculate compatibility if project_data: context["compatibility_score"] = self._calculate_compatibility_score( investor_data, project_data ) context["match_criteria"] = self._generate_match_criteria( investor_data, project_data ) context["recommendation"] = self._generate_recommendation( context["compatibility_score"], context["match_criteria"] ) return context def _calculate_compatibility_score( self, investor_data: Dict[str, Any], project_data: Dict[str, Any] ) -> int: """Calculate overall compatibility score between investor and project""" score = 0 weights = { "sector": 30, "stage": 30, "geography": 20, "check_size": 15, "thesis": 5, } # Sector match investor_sectors = set(investor_data.get("sectors", [])) project_sectors = set(project_data.get("sectors", [])) if investor_sectors and project_sectors: if investor_sectors & project_sectors: score += weights["sector"] # Stage match investor_stages = set(investor_data.get("investment_stages", [])) project_stage = project_data.get("stage") if project_stage and project_stage in investor_stages: score += weights["stage"] # Geography match investor_geo = investor_data.get("geographic_focus", "").lower() project_geo = project_data.get("location", "").lower() if investor_geo and project_geo and investor_geo in project_geo: score += weights["geography"] # Check size match project_valuation = project_data.get("valuation", 0) check_lower = investor_data.get("check_size_lower", 0) check_upper = investor_data.get("check_size_upper", float("inf")) if check_lower <= project_valuation <= check_upper: score += weights["check_size"] # Thesis alignment (simplified) score += weights["thesis"] return min(score, 100) def _generate_match_criteria( self, investor_data: Dict[str, Any], project_data: Dict[str, Any] ) -> List[Dict[str, str]]: """Generate detailed match criteria table""" criteria = [] # Sector criterion investor_sectors = investor_data.get("sectors", []) project_sectors = project_data.get("sectors", []) sector_match = ( "Perfect" if set(investor_sectors) & set(project_sectors) else "Mismatch" ) criteria.append( { "name": "Sector", "requirement": "Cybersecurity, B2B SaaS" if project_sectors else "N/A", "evidence": ", ".join(investor_sectors[:3]) if investor_sectors else "N/A", "match": sector_match, "weight": "30%", } ) # Stage criterion investor_stages = investor_data.get("investment_stages", []) project_stage = project_data.get("stage", "N/A") stage_match = "Perfect" if project_stage in investor_stages else "Mismatch" criteria.append( { "name": "Stage", "requirement": str(project_stage), "evidence": ", ".join(investor_stages) if investor_stages else "N/A", "match": stage_match, "weight": "30%", } ) # Geography criterion investor_geo = investor_data.get("geographic_focus", "N/A") project_geo = project_data.get("location", "N/A") geo_match = ( "Strong" if investor_geo.lower() in project_geo.lower() or project_geo.lower() in investor_geo.lower() else "Mismatch" ) criteria.append( { "name": "Geography", "requirement": project_geo, "evidence": investor_geo, "match": geo_match, "weight": "20%", } ) # Check Size criterion check_lower = investor_data.get("check_size_lower", 0) check_upper = investor_data.get("check_size_upper", 0) project_val = project_data.get("valuation", 0) check_evidence = "N/A" if check_lower and check_upper: check_evidence = ( f"€{check_lower / 1000000:.0f}M - €{check_upper / 1000000:.0f}M" ) elif check_lower: check_evidence = f"€{check_lower / 1000000:.0f}M+" check_match = ( "Perfect" if check_lower <= project_val <= check_upper else "Strong" if project_val > 0 else "N/A" ) criteria.append( { "name": "Check Size", "requirement": f"€{project_val / 1000000:.0f}M" if project_val else "N/A", "evidence": check_evidence, "match": check_match, "weight": "15%", } ) # Thesis criterion thesis = investor_data.get("investment_thesis", []) criteria.append( { "name": "Thesis", "requirement": "Founder-led, ESG focus", "evidence": ", ".join(thesis[:2]) if thesis else "Entrepreneur-led", "match": "Strong", "weight": "5%", } ) return criteria def _generate_recommendation( self, score: int, criteria: List[Dict[str, str]] ) -> str: """Generate recommendation text based on score and criteria""" if score >= 85: return "High Priority. A strong target due to exceptional alignment on the most heavily-weighted criteria: Sector and Stage. The strong geographic fit further solidifies this recommendation." elif score >= 70: return "Medium Priority. Good alignment on key criteria with some areas of strong fit. The geographic fit in the target region supports this recommendation." else: return "Low Priority. Limited alignment on key investment criteria. Consider for future evaluation if circumstances change." async def _html_to_pdf(self, html_content: str) -> bytes: """Convert HTML content to PDF using Playwright""" async with async_playwright() as p: browser = await p.chromium.launch() page = await browser.new_page() # Set content and wait for any dynamic content to load await page.set_content(html_content, wait_until="networkidle") # Generate PDF with proper settings pdf_bytes = await page.pdf( format="A4", print_background=True, margin={"top": "0", "right": "0", "bottom": "0", "left": "0"}, ) await browser.close() return pdf_bytes