Merge branch 'version_three' of http://23.29.118.76:3000/bolade/Anton_wireframe into version_three

This commit is contained in:
2025-10-21 08:40:17 +00:00
8 changed files with 233 additions and 162 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -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")
+56 -14
View File
@@ -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.
+9 -4
View File
@@ -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
View File
@@ -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">