Merge branch 'version_three' of http://23.29.118.76:3000/bolade/Anton_wireframe into version_three
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -311,3 +311,24 @@ class ProjectTable(Base, TimestampMixin):
|
|||||||
companies = relationship(
|
companies = relationship(
|
||||||
"CompanyTable", secondary=project_company_association, back_populates="projects"
|
"CompanyTable", secondary=project_company_association, back_populates="projects"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvestorInsightCache(Base, TimestampMixin):
|
||||||
|
__tablename__ = "investor_insight_cache"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
investor_id = Column(
|
||||||
|
Integer, ForeignKey("investors.id"), nullable=False, unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cached insights
|
||||||
|
investment_pattern_analysis = Column(Text, nullable=False)
|
||||||
|
market_position = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
# Cache management
|
||||||
|
last_refreshed = Column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationship to investor
|
||||||
|
investor = relationship("InvestorTable")
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from db.db import get_db
|
from db.db import get_db
|
||||||
from db.models import InvestorTable, ProjectTable
|
from db.models import InvestorInsightCache, InvestorTable, ProjectTable
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from schemas.insight_schema import InsightResponse
|
from schemas.insight_schema import InsightResponse
|
||||||
from services.compatibility_score import (
|
from services.compatibility_score import (
|
||||||
@@ -39,19 +40,60 @@ async def get_insights(
|
|||||||
status_code=404, detail=f"Investor with id {investor_id} not found"
|
status_code=404, detail=f"Investor with id {investor_id} not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize the query processor for insights
|
# Check if we have cached insights
|
||||||
query_processor = QueryProcessor()
|
cached_insights = (
|
||||||
|
db.query(InvestorInsightCache)
|
||||||
# Get investment pattern analysis and market position using web search
|
.filter(InvestorInsightCache.investor_id == investor_id)
|
||||||
insights = await query_processor.get_investor_insights(
|
.first()
|
||||||
investor_name=investor.name,
|
|
||||||
investor_website=investor.website,
|
|
||||||
investor_description=investor.description,
|
|
||||||
investor_headquarters=investor.headquarters,
|
|
||||||
investment_thesis=investor.investment_thesis,
|
|
||||||
portfolio_highlights=investor.portfolio_highlights,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Determine if cache needs refresh (older than 1 month)
|
||||||
|
needs_refresh = True
|
||||||
|
if cached_insights:
|
||||||
|
# Calculate if cache is older than 1 month
|
||||||
|
cache_age = (
|
||||||
|
datetime.now(cached_insights.last_refreshed.tzinfo)
|
||||||
|
- cached_insights.last_refreshed
|
||||||
|
)
|
||||||
|
needs_refresh = cache_age > timedelta(days=30)
|
||||||
|
|
||||||
|
# Fetch new insights if needed
|
||||||
|
if needs_refresh:
|
||||||
|
# Initialize the query processor for insights
|
||||||
|
query_processor = QueryProcessor()
|
||||||
|
|
||||||
|
# Get investment pattern analysis and market position using web search
|
||||||
|
insights = await query_processor.get_investor_insights(
|
||||||
|
investor_name=investor.name,
|
||||||
|
investor_website=investor.website,
|
||||||
|
investor_description=investor.description,
|
||||||
|
investor_headquarters=investor.headquarters,
|
||||||
|
investment_thesis=investor.investment_thesis,
|
||||||
|
portfolio_highlights=investor.portfolio_highlights,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update or create cache entry
|
||||||
|
if cached_insights:
|
||||||
|
# Update existing cache
|
||||||
|
cached_insights.investment_pattern_analysis = insights[
|
||||||
|
"investment_pattern_analysis"
|
||||||
|
]
|
||||||
|
cached_insights.market_position = insights["market_position"]
|
||||||
|
cached_insights.last_refreshed = datetime.now(
|
||||||
|
cached_insights.last_refreshed.tzinfo
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Create new cache entry
|
||||||
|
cached_insights = InvestorInsightCache(
|
||||||
|
investor_id=investor_id,
|
||||||
|
investment_pattern_analysis=insights["investment_pattern_analysis"],
|
||||||
|
market_position=insights["market_position"],
|
||||||
|
)
|
||||||
|
db.add(cached_insights)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(cached_insights)
|
||||||
|
|
||||||
# Calculate compatibility score if project_id is provided
|
# Calculate compatibility score if project_id is provided
|
||||||
compatibility_score = None
|
compatibility_score = None
|
||||||
if project_id:
|
if project_id:
|
||||||
@@ -74,7 +116,7 @@ async def get_insights(
|
|||||||
compatibility_score = "Select a project to see compatibility analysis"
|
compatibility_score = "Select a project to see compatibility analysis"
|
||||||
|
|
||||||
return InsightResponse(
|
return InsightResponse(
|
||||||
investment_pattern_analysis=insights["investment_pattern_analysis"],
|
investment_pattern_analysis=cached_insights.investment_pattern_analysis,
|
||||||
market_position=insights["market_position"],
|
market_position=cached_insights.market_position,
|
||||||
compatibility_score=compatibility_score,
|
compatibility_score=compatibility_score,
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
@@ -34,7 +34,6 @@ class ReportGenerator:
|
|||||||
# Render HTML from template
|
# Render HTML from template
|
||||||
template = self.env.get_template("report.html")
|
template = self.env.get_template("report.html")
|
||||||
html_content = template.render(**context)
|
html_content = template.render(**context)
|
||||||
|
|
||||||
# Convert HTML to PDF using Playwright
|
# Convert HTML to PDF using Playwright
|
||||||
pdf_bytes = await self._html_to_pdf(html_content)
|
pdf_bytes = await self._html_to_pdf(html_content)
|
||||||
|
|
||||||
@@ -104,7 +103,11 @@ class ReportGenerator:
|
|||||||
project_valuation = project_data.get("valuation", 0)
|
project_valuation = project_data.get("valuation", 0)
|
||||||
check_lower = investor_data.get("check_size_lower") or 0
|
check_lower = investor_data.get("check_size_lower") or 0
|
||||||
check_upper = investor_data.get("check_size_upper") or float("inf")
|
check_upper = investor_data.get("check_size_upper") or float("inf")
|
||||||
if check_lower and check_upper and check_lower <= project_valuation <= check_upper:
|
if (
|
||||||
|
check_lower
|
||||||
|
and check_upper
|
||||||
|
and check_lower <= project_valuation <= check_upper
|
||||||
|
):
|
||||||
score += weights["check_size"]
|
score += weights["check_size"]
|
||||||
|
|
||||||
# Thesis alignment (simplified)
|
# Thesis alignment (simplified)
|
||||||
@@ -153,10 +156,12 @@ class ReportGenerator:
|
|||||||
# Geography criterion
|
# Geography criterion
|
||||||
investor_geo = investor_data.get("geographic_focus") or "N/A"
|
investor_geo = investor_data.get("geographic_focus") or "N/A"
|
||||||
project_geo = project_data.get("location") or "N/A"
|
project_geo = project_data.get("location") or "N/A"
|
||||||
|
|
||||||
# Safe comparison handling None values
|
# Safe comparison handling None values
|
||||||
if investor_geo == "N/A" or project_geo == "N/A":
|
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"
|
geo_match = (
|
||||||
|
"N/A" if investor_geo == "N/A" and project_geo == "N/A" else "Mismatch"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
investor_geo_lower = investor_geo.lower()
|
investor_geo_lower = investor_geo.lower()
|
||||||
project_geo_lower = project_geo.lower()
|
project_geo_lower = project_geo.lower()
|
||||||
|
|||||||
+147
-144
@@ -7,23 +7,43 @@
|
|||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<style>
|
<style>
|
||||||
@page {
|
@page {
|
||||||
margin: 0;
|
|
||||||
size: A4;
|
size: A4;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
Roboto, sans-serif;
|
Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Each page is exactly one A4 sheet */
|
||||||
.page {
|
.page {
|
||||||
page-break-after: always;
|
width: 210mm;
|
||||||
min-height: 100vh;
|
height: 297mm;
|
||||||
|
position: relative;
|
||||||
background: white;
|
background: white;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.page:last-child {
|
|
||||||
page-break-after: auto;
|
/* Adds a break between pages (for print/PDF) */
|
||||||
|
.page-with-break {
|
||||||
|
page-break-after: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inner content wrapper for consistent padding */
|
||||||
|
.page-content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 48px; /* equivalent to Tailwind p-12 */
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
@@ -32,131 +52,132 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure the footer text stays inside page bounds */
|
||||||
|
.page-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 48px;
|
||||||
|
right: 48px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af; /* Tailwind gray-400 */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white">
|
<body>
|
||||||
<!-- Page 1: Investor Profile -->
|
<!-- Page 1 -->
|
||||||
<div class="page p-12">
|
<div class="page page-with-break">
|
||||||
<div class="flex justify-between items-start mb-8">
|
<div class="page-content">
|
||||||
<div>
|
<div class="flex justify-between items-start mb-8">
|
||||||
<p class="text-sm text-gray-600 mb-2">Investor Profile</p>
|
<div>
|
||||||
<h1 class="text-4xl font-bold text-gray-900">
|
<p class="text-sm text-gray-600 mb-2">Investor Profile</p>
|
||||||
{{ investor.name }}
|
<h1 class="text-4xl font-bold text-gray-900">
|
||||||
</h1>
|
{{ investor.name }}
|
||||||
</div>
|
</h1>
|
||||||
<button
|
</div>
|
||||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded text-sm"
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href="{{ investor.website }}"
|
href="{{ investor.website }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="no-underline text-gray-700"
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded text-sm no-underline"
|
||||||
>Visit Website →</a
|
>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>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column -->
|
<div class="grid grid-cols-2 gap-8 flex-grow">
|
||||||
<div class="bg-gray-50 p-6 rounded-lg">
|
<!-- Left Column -->
|
||||||
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
<div>
|
||||||
Key Data
|
<div class="mb-4">
|
||||||
</h2>
|
<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-4">
|
<div class="mb-4">
|
||||||
<p class="text-xs text-gray-600 mb-1">Headquarters:</p>
|
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||||
<p class="text-sm font-semibold text-gray-900">
|
Portfolio Highlights
|
||||||
{{ investor.headquarters or 'N/A' }}
|
</h2>
|
||||||
</p>
|
<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-4">
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<!-- Right Column -->
|
||||||
<p class="text-xs text-gray-600 mb-1">Sectors:</p>
|
<div class="bg-gray-50 p-6 rounded-lg">
|
||||||
<p class="text-sm font-semibold text-gray-900">
|
<h2 class="text-sm font-bold text-gray-900 uppercase mb-4">
|
||||||
{% if investor.sectors %} {{ investor.sectors |
|
Key Data
|
||||||
join(', ') }} {% else %} N/A {% endif %}
|
</h2>
|
||||||
</p>
|
<div class="space-y-3 text-sm">
|
||||||
</div>
|
<div>
|
||||||
|
<p class="text-xs text-gray-600">Headquarters:</p>
|
||||||
|
<p class="font-semibold text-gray-900">
|
||||||
|
{{ investor.headquarters or 'N/A' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<p class="text-xs text-gray-600 mb-1">DACH Region:</p>
|
<p class="text-xs text-gray-600">Sectors:</p>
|
||||||
<p class="text-sm font-semibold text-gray-900">
|
<p class="font-semibold text-gray-900">
|
||||||
{{ investor.geographic_focus or 'N/A' }}
|
{% if investor.sectors %}
|
||||||
</p>
|
{{ investor.sectors | join(', ') }}
|
||||||
</div>
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<p class="text-xs text-gray-600 mb-1">
|
<p class="text-xs text-gray-600">DACH Region:</p>
|
||||||
AUM: (EUR million) (as of Fund IX)
|
<p class="font-semibold text-gray-900">
|
||||||
</p>
|
{{ investor.geographic_focus or 'N/A' }}
|
||||||
<p class="text-sm font-semibold text-gray-900">
|
</p>
|
||||||
{% if investor.aum %} €{{
|
</div>
|
||||||
'{:,.0f}'.format(investor.aum / 1000000) }}M {% else
|
|
||||||
%} N/A {% endif %}
|
<div>
|
||||||
</p>
|
<p class="text-xs text-gray-600">AUM (EUR million):</p>
|
||||||
</div>
|
<p class="font-semibold text-gray-900">
|
||||||
|
{% if investor.aum %}
|
||||||
|
€{{ '{:,.0f}'.format(investor.aum / 1000000) }}M
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-xs text-gray-600 mb-1">
|
<p class="text-xs text-gray-600 mb-1">
|
||||||
@@ -182,41 +203,23 @@
|
|||||||
1000000) }}M {% elif investor.check_size_lower %}
|
1000000) }}M {% elif investor.check_size_lower %}
|
||||||
€{{ '{:,.0f}'.format(investor.check_size_lower /
|
€{{ '{:,.0f}'.format(investor.check_size_lower /
|
||||||
1000000) }}M+ {% else %} N/A {% endif %}
|
1000000) }}M+ {% else %} N/A {% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="absolute bottom-12 right-12 text-xs text-gray-400">
|
<div class="page-footer">Page 1</div>
|
||||||
Page 1
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page 2: Mandate Match Analysis -->
|
<!-- Page 2 -->
|
||||||
{% if project %}
|
{% if project %}
|
||||||
<div class="page p-12">
|
<div class="page">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">
|
<div class="page-content">
|
||||||
{{ investor.name }}: Mandate Match Analysis
|
<h1 class="text-3xl font-bold text-gray-900 mb-8">
|
||||||
</h1>
|
{{ investor.name }}: Mandate Match Analysis
|
||||||
|
</h1>
|
||||||
|
|
||||||
<!-- Overall Match Circle -->
|
<!-- Overall Match Circle -->
|
||||||
<div class="flex justify-center mb-12">
|
<div class="flex justify-center mb-12">
|
||||||
|
|||||||
Reference in New Issue
Block a user