Files
Anton_wireframe/app/services/report_gen.py
T

260 lines
9.2 KiB
Python

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") or "").lower()
project_geo = (project_data.get("location") or "").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") or 0
check_upper = investor_data.get("check_size_upper") or float("inf")
if (
check_lower
and check_upper
and 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") or "N/A"
project_geo = project_data.get("location") or "N/A"
# Safe comparison handling None values
if investor_geo == "N/A" or project_geo == "N/A":
geo_match = (
"N/A" if investor_geo == "N/A" and project_geo == "N/A" else "Mismatch"
)
else:
investor_geo_lower = investor_geo.lower()
project_geo_lower = project_geo.lower()
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") or 0
check_upper = investor_data.get("check_size_upper") or 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 and check_upper and 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