feat: Implement report generation service and add report route for investor profiles
This commit is contained in:
Binary file not shown.
+9
-1
@@ -5,7 +5,14 @@ from db.db import Base, db_dependency, engine
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, File, Form, UploadFile
|
||||
from pydantic import BaseModel
|
||||
from routers import companies, folk_crm, insight_route, investors, projects
|
||||
from routers import (
|
||||
companies,
|
||||
folk_crm,
|
||||
insight_route,
|
||||
investors,
|
||||
projects,
|
||||
report_route,
|
||||
)
|
||||
from schemas.router_schemas import InvestmentResponse, PaginatedResponse
|
||||
from services.llm_parser import InvestorProcessor
|
||||
from services.querying import QueryProcessor
|
||||
@@ -110,6 +117,7 @@ app.include_router(companies.router)
|
||||
app.include_router(projects.router)
|
||||
app.include_router(folk_crm.router)
|
||||
app.include_router(insight_route.router)
|
||||
app.include_router(report_route.router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,121 @@
|
||||
from typing import Optional
|
||||
|
||||
from db.db import get_db
|
||||
from db.models import FundTable, InvestorTable, ProjectTable
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import Response
|
||||
from services.report_gen import ReportGenerator
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
router = APIRouter(tags=["Report Generation"])
|
||||
|
||||
|
||||
@router.get("/report/investor/{investor_id}")
|
||||
async def generate_investor_report(
|
||||
investor_id: int,
|
||||
project_id: Optional[int] = Query(
|
||||
None, description="Optional project ID for compatibility analysis"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Generate a PDF report for an investor profile.
|
||||
|
||||
Args:
|
||||
investor_id: The ID of the investor to generate a report for
|
||||
project_id: Optional project ID to include mandate match analysis
|
||||
|
||||
Returns:
|
||||
PDF file as a downloadable response
|
||||
"""
|
||||
# Fetch investor data with all relationships
|
||||
investor = (
|
||||
db.query(InvestorTable)
|
||||
.options(
|
||||
selectinload(InvestorTable.portfolio_companies),
|
||||
selectinload(InvestorTable.team_members),
|
||||
selectinload(InvestorTable.sectors),
|
||||
selectinload(InvestorTable.funds).selectinload(FundTable.investment_stages),
|
||||
selectinload(InvestorTable.funds).selectinload(FundTable.sectors),
|
||||
)
|
||||
.filter(InvestorTable.id == investor_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not investor:
|
||||
raise HTTPException(status_code=404, detail="Investor not found")
|
||||
|
||||
# Prepare investor data dictionary
|
||||
investor_data = {
|
||||
"name": investor.name,
|
||||
"description": investor.description,
|
||||
"website": investor.website,
|
||||
"headquarters": investor.headquarters,
|
||||
"aum": investor.aum,
|
||||
"geographic_focus": investor.geographic_focus,
|
||||
"portfolio_highlights": investor.portfolio_highlights or [],
|
||||
"investment_thesis": investor.investment_thesis or [],
|
||||
"sectors": [sector.name for sector in investor.sectors],
|
||||
"team_members": [
|
||||
{
|
||||
"name": member.name,
|
||||
"role": member.role,
|
||||
"title": member.title,
|
||||
"email": member.email,
|
||||
}
|
||||
for member in investor.team_members
|
||||
],
|
||||
"check_size_lower": None,
|
||||
"check_size_upper": None,
|
||||
"investment_stages": [],
|
||||
}
|
||||
|
||||
# Get check sizes and stages from funds
|
||||
if investor.funds:
|
||||
# Use the first fund's data or aggregate
|
||||
fund = investor.funds[0]
|
||||
investor_data["check_size_lower"] = fund.check_size_lower
|
||||
investor_data["check_size_upper"] = fund.check_size_upper
|
||||
|
||||
# Aggregate all investment stages from all funds
|
||||
stages = set()
|
||||
for fund in investor.funds:
|
||||
for stage in fund.investment_stages:
|
||||
stages.add(stage.name)
|
||||
investor_data["investment_stages"] = list(stages)
|
||||
|
||||
# Fetch project data if project_id is provided
|
||||
project_data = None
|
||||
if project_id:
|
||||
project = (
|
||||
db.query(ProjectTable)
|
||||
.options(selectinload(ProjectTable.sector))
|
||||
.filter(ProjectTable.id == project_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
project_data = {
|
||||
"name": project.name,
|
||||
"description": project.description,
|
||||
"location": project.location,
|
||||
"valuation": project.valuation,
|
||||
"stage": project.stage.name if project.stage else None,
|
||||
"sectors": [sector.name for sector in project.sector],
|
||||
}
|
||||
|
||||
# Generate PDF report
|
||||
report_generator = ReportGenerator()
|
||||
pdf_bytes = await report_generator.generate_investor_report(
|
||||
investor_data, project_data
|
||||
)
|
||||
|
||||
# Return PDF as downloadable file
|
||||
filename = f"{investor.name.replace(' ', '_')}_Report.pdf"
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,247 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Investor Profile Report</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@page {
|
||||
margin: 0;
|
||||
size: A4;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, sans-serif;
|
||||
}
|
||||
.page {
|
||||
page-break-after: always;
|
||||
min-height: 100vh;
|
||||
background: white;
|
||||
}
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-white">
|
||||
<!-- Page 1: Investor Profile -->
|
||||
<div class="page p-12">
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 mb-2">Investor Profile</p>
|
||||
<h1 class="text-4xl font-bold text-gray-900">
|
||||
{{ investor.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded text-sm"
|
||||
>
|
||||
<a
|
||||
href="{{ investor.website }}"
|
||||
target="_blank"
|
||||
class="no-underline text-gray-700"
|
||||
>Visit Website →</a
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-8">
|
||||
<!-- Left Column -->
|
||||
<div>
|
||||
<div class="mb-8">
|
||||
<h2
|
||||
class="text-sm font-bold text-gray-900 uppercase mb-4"
|
||||
>
|
||||
Investor Description
|
||||
</h2>
|
||||
<p class="text-sm text-gray-700 leading-relaxed">
|
||||
{{ investor.description or 'No description
|
||||
available.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2
|
||||
class="text-sm font-bold text-gray-900 uppercase mb-4"
|
||||
>
|
||||
Portfolio Highlights
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if investor.portfolio_highlights %} {% for
|
||||
company in investor.portfolio_highlights[:5] %}
|
||||
<span class="tag">{{ company }}</span>
|
||||
{% endfor %} {% else %}
|
||||
<p class="text-sm text-gray-500">
|
||||
No portfolio highlights available
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2
|
||||
class="text-sm font-bold text-gray-900 uppercase mb-4"
|
||||
>
|
||||
Senior Leadership
|
||||
</h2>
|
||||
{% if investor.team_members %} {% for member in
|
||||
investor.team_members[:2] %}
|
||||
<div class="mb-3">
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ member.name }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ member.role or member.title or 'Team Member'
|
||||
}}
|
||||
</p>
|
||||
{% if member.email %}
|
||||
<p class="text-xs text-blue-600">
|
||||
{{ member.email }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %} {% else %}
|
||||
<p class="text-sm text-gray-500">
|
||||
No team information available
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||
Key Data
|
||||
</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">Headquarters:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ investor.headquarters or 'N/A' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">Sectors:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.sectors %} {{ investor.sectors |
|
||||
join(', ') }} {% else %} N/A {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">DACH Region:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ investor.geographic_focus or 'N/A' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">
|
||||
AUM: (EUR million) (as of Fund IX)
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.aum %} €{{
|
||||
'{:,.0f}'.format(investor.aum / 1000000) }}M {% else
|
||||
%} N/A {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">
|
||||
Investment Stage:
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.investment_stages %} {{
|
||||
investor.investment_stages | join(', ') }} {% else
|
||||
%} N/A {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">
|
||||
Est. Investment Size:
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.check_size_lower and
|
||||
investor.check_size_upper %} €{{
|
||||
'{:,.0f}'.format(investor.check_size_lower /
|
||||
1000000) }}M - €{{
|
||||
'{:,.0f}'.format(investor.check_size_upper /
|
||||
1000000) }}M {% elif investor.check_size_lower %}
|
||||
€{{ '{:,.0f}'.format(investor.check_size_lower /
|
||||
1000000) }}M+ {% else %} N/A {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">
|
||||
Select Deals, Series A, Series B:
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
Growth
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-xs text-gray-600 mb-1">Focus Areas:</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{% if investor.investment_thesis %} {{
|
||||
investor.investment_thesis[:3] | join(', ') }} {%
|
||||
else %} Disruptive Technologies, Entrepreneur-led,
|
||||
Sustainability {% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-12 right-12 text-xs text-gray-400">
|
||||
Page 3
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page 2: Mandate Match Analysis -->
|
||||
{% if project %}
|
||||
<div class="page p-12">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">
|
||||
{{ investor.name }}: Mandate Match Analysis
|
||||
</h1>
|
||||
|
||||
<!-- Overall Match Circle -->
|
||||
<div class="flex justify-center mb-12">
|
||||
<div class="text-center">
|
||||
<p class="text-sm font-bold text-gray-700 uppercase mb-4">
|
||||
Overall Mandate Match
|
||||
</p>
|
||||
<div
|
||||
class="w-48 h-48 rounded-full border-8 border-green-400 flex items-center justify-center bg-green-50 mx-auto"
|
||||
>
|
||||
<span class="text-5xl font-bold text-green-600"
|
||||
>{{ compatibility_score }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mandate Alignment Analysis Table -->
|
||||
<div class="mb-12">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-6">
|
||||
Mandate Alignment Analysis
|
||||
</h2>
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b-2 border-gray-300">
|
||||
<th
|
||||
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
|
||||
>
|
||||
Criterion
|
||||
</th>
|
||||
<th
|
||||
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
|
||||
>
|
||||
Mandate Requirement
|
||||
</th>
|
||||
<th
|
||||
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
|
||||
>
|
||||
Investor Evidence (from Database)
|
||||
</th>
|
||||
<th
|
||||
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
|
||||
>
|
||||
Match Score
|
||||
</th>
|
||||
<th
|
||||
class="text-left py-3 px-4 text-sm font-bold text-gray-700"
|
||||
>
|
||||
Weight
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion in match_criteria %}
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-4 px-4 text-sm text-gray-900">
|
||||
{{ criterion.name }}
|
||||
</td>
|
||||
<td class="py-4 px-4 text-sm text-gray-700">
|
||||
{{ criterion.requirement }}
|
||||
</td>
|
||||
<td class="py-4 px-4 text-sm text-gray-700">
|
||||
{{ criterion.evidence }}
|
||||
</td>
|
||||
<td class="py-4 px-4 text-sm">
|
||||
<span
|
||||
class="{% if criterion.match == 'Perfect' %}text-green-600{% elif criterion.match == 'Strong' %}text-blue-600{% else %}text-yellow-600{% endif %} font-semibold"
|
||||
>
|
||||
{{ criterion.match }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 px-4 text-sm text-gray-700">
|
||||
{{ criterion.weight }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Final Recommendation -->
|
||||
<div class="bg-blue-50 border-l-4 border-blue-500 p-6 rounded">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-3">
|
||||
Final Recommendation & Rationale
|
||||
</h3>
|
||||
<p class="text-sm text-gray-700 leading-relaxed">
|
||||
{{ recommendation or "High Priority. A strong target due to
|
||||
exceptional alignment on the most heavily-weighted criteria:
|
||||
Sector and Stage. The strong geographic fit in the DACH
|
||||
region further solidifies this recommendation." }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-12 right-12 text-xs text-gray-400">
|
||||
Page 4
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,6 +48,7 @@ jsonpointer==3.0.0
|
||||
jsonschema==4.25.1
|
||||
jsonschema-specifications==2025.4.1
|
||||
kubernetes==33.1.0
|
||||
playwright==1.48.0
|
||||
langchain==0.3.27
|
||||
langchain-community==0.3.29
|
||||
langchain-core==0.3.75
|
||||
|
||||
Reference in New Issue
Block a user