From 228a67cc49f50e85163135cf95ddcb9082735e02 Mon Sep 17 00:00:00 2001 From: bolade Date: Fri, 17 Oct 2025 23:15:57 +0100 Subject: [PATCH] feat: Add InvestorInsightCache model and implement caching for investor insights --- app/__pycache__/main.cpython-312.pyc | Bin 5225 -> 5212 bytes app/db/__pycache__/db.cpython-312.pyc | Bin 1749 -> 1749 bytes app/db/__pycache__/models.cpython-312.pyc | Bin 9028 -> 9678 bytes app/db/models.py | 21 ++ app/routers/insight_route.py | 70 ++++- .../__pycache__/querying.cpython-312.pyc | Bin 8315 -> 8594 bytes app/services/report_gen.py | 13 +- app/templates/report.html | 291 +++++++++--------- investors.db | Bin 27054080 -> 27066368 bytes 9 files changed, 233 insertions(+), 162 deletions(-) diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index a03bc929ab23772b706d998d4472ae29b145e415..ef94b2959a3913ba0da87acc8ea966b36959d9a2 100644 GIT binary patch delta 112 zcmaEnL-CZ&r-k|%d9SfpePCx` Kkt&h~N&o;{SRADQ delta 125 zcmcbk@lu2LG%qg~0}!O@yvvZB$ScX{v{9XhnNfPO3A36=iozPj)hr-s1_nlkO2$e? zO{L8_%(r-DLNr-!F((!j++xYdFD|*oQjlL%a*M4fH77qYWpkupEF)JUSPuh3@wCkc Xg!&oz52##YasR;1z#>&74U_@^4G1A< diff --git a/app/db/__pycache__/db.cpython-312.pyc b/app/db/__pycache__/db.cpython-312.pyc index c852d80ecbaa5298160b8697f1207ed64e2b4c48..f367c0da854209e96ed71e99c2661405caf4345d 100644 GIT binary patch delta 20 acmcc0dzF{_G%qg~0}xnl`?Qh!6dM3O{st@n delta 20 acmcc0dzF{_G%qg~0}xC|dAgDN6dM3PG6p07 diff --git a/app/db/__pycache__/models.cpython-312.pyc b/app/db/__pycache__/models.cpython-312.pyc index bccb5bde5244e3986c4a86462cdb88149afcea96..5bcb5acd548b3e41a1918eab4718d69247394e79 100644 GIT binary patch delta 2231 zcmbVN%}*Og6yG)A4=^?cXIVQNpp=$uF%3$i9tfo(;ZqQks`Lw~br$ac8|<~2T~pan zWv7ZFwU-8s8gb|$haQ5=fy7A_<-nBYcTi`QseF1e?h6w`y+p->fC-f zpuZ&fvb=q8Kk$O>);uAbJ5#0aQ^09(ahzfeSXnAz)bA3LN9}V_;G! z*sK_zGBp<1d1|tP6D))z(#s4O0YZXNhDqCnj8%x83!EVp#` zybXqT0CX_;jZ_B5Q7ny9E1HTheiux3aAd3HtuTUhRfsH(F>*ZklO5t1`8ymUkwbrp z<8Y=Yo=T<_T|-Y^p9~4o^l3P17=T3^2Wc-ZJVlpLSvW{=qCV&p8{WqIdQG(4on zvPi-B4LBDfuw3OLz;mEFNp^$|9+5!nbKwP0R7C~c@i>`ky=zA`P7`;N&ylZ1G5;Fa zSPk`3X%sWxJSZV&QQ3cR+SK22Cp3KtshWu&huh*ZR5xL!VjBjIJ$b5N6qecaF;5XQFAd$-wbYw3U=N%#>ZpWV5G(WLr)H#c8WDf>F8oe{fUbu9E--fGh>lCmrcx>HQE? zPb1D4PLgG*VDp}Yrz|xcaq)TJg<*FEdA5O(XJJ4DQ91lfd@aw4lE^5gmeR2gE`bSY z)fv#?_#;qwC^+z0vewr1%BP_C1YjP3MR*Y;h_E^jjPn%vt?es2oGBPM{5|q06d^l- zKwfohIQPc9+rgn)M6q9 zq~ENcdN--{#e8}5Y#`epd;cA8DLkPY+QOoFT8S?rOW2#+nyrq-L0>T^FVj=)9&83F z2mtx`kJ6!CYESSdBN3k3I&5DFb2gS%OcP-}rszr{YiNe$Pbzo`QA5hmSkB;Q64wJ9 zhRwc&Vwf?E<}orBky;*@V6Ny}4zF_WQ{^hP4A6HlOe$RB9t0zWGrxNLS6tU-HiO%q zR|}^eHj5A3{?+hW^nRdc6MZwhTjOYP7A7CpbI;4ygLmq-I|qNLZGR|8YctnJwuR$` z>Bmk^kjVGWR!MLcqSUcTTARM_k6awLe~95y`9-qX{&Aian>&e2MO9429*3-V3e*R) zYGZ}QYK%2JYaUkZL*T+@2~0!2 z0wWF-Zor5`|ALAeuu>0IrAk$AsJ&KNdgVU=4oHcEXw$HOMLG-qRzuis=!!9 zZBIWQF6eWz2$X;^U|ju{E)9>utN`19DnY$Xf0-g@HRPG4p285^Ne!Ni(w4Yt22EcM zsh|2ic0z58LA_X)hp~mzKH!qZ z6DCjSFM|)r4-#PYG|&U2fHOcEIICW7<`cVNW`UzXSCKXNYUR3|{&iRU5E>w^|+iYH9)Id7!BgUdGyIRK7BzP7eQDsv~A!!);wR9b0&D5n8{xSQzab zL#X2bm14qrUx}R8j|MM%PC4-<>-|y2cC5hjWjo3=-G=Ggt|JXBWUsn`V+m`qElerT zA=MD7iqb97x+0_}FT;00Wr|hSP`o%+7I1V`ih5*SS;b%CO6zK(^t`j8y-_}{vc0$# zaDg*<2-|6frnFU;bo?fqOBx2xud}+Q?R+aN+mSY6P#3>QB*xYn)P}Q5Fr>*9wLEe; z7Q8~Oc{6gc<6ym<~l<&&`_ zha~OY`eO$ZEHPm3=L8NK4w?(PKFmdw@r>)M;hefZdOOxW7v?Kf)`2F8lOGAE%_N*kfO>{Q$>Zzyhec4lGpH-3QVJoBAua{NxGs?8|V6257f zz8Sw6bYFB;l!X#dY-%)J7*$zADw-YY7R?{EoNj}XemtV@GT*dZD`<)%Qt7iSk?2Wy T_rkq9?j|?BNfcR%{t5mAwWK)# diff --git a/app/db/models.py b/app/db/models.py index a3e0774..983533b 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -311,3 +311,24 @@ class ProjectTable(Base, TimestampMixin): companies = relationship( "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") diff --git a/app/routers/insight_route.py b/app/routers/insight_route.py index 632bde2..4967c32 100644 --- a/app/routers/insight_route.py +++ b/app/routers/insight_route.py @@ -1,7 +1,8 @@ +from datetime import datetime, timedelta from typing import Optional 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 schemas.insight_schema import InsightResponse 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" ) - # 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, + # Check if we have cached insights + cached_insights = ( + db.query(InvestorInsightCache) + .filter(InvestorInsightCache.investor_id == investor_id) + .first() ) + # 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 compatibility_score = None if project_id: @@ -74,7 +116,7 @@ async def get_insights( compatibility_score = "Select a project to see compatibility analysis" return InsightResponse( - investment_pattern_analysis=insights["investment_pattern_analysis"], - market_position=insights["market_position"], + investment_pattern_analysis=cached_insights.investment_pattern_analysis, + market_position=cached_insights.market_position, compatibility_score=compatibility_score, ) diff --git a/app/services/__pycache__/querying.cpython-312.pyc b/app/services/__pycache__/querying.cpython-312.pyc index d5a560a4efbd55cbd8a91cf50e8d46ca372a68d2..39750b291c41ef069c8f84e2139a9f96c78d1eef 100644 GIT binary patch delta 1206 zcmY*YO>7%Q6rS;}_s?G2v7I<}NW3XY+#i*uV5wFscsFLv&Tcg8 zqJmPThe`y5M9YY_Qia5^C>$zTf@^P;K$UDIQYx22PDn)+4FZG&2bfu766WyD_ulvB zo1OQxf0aL*)4$hs4X{=G>DfEq+|aYDB?!PRU;-050QGi=U=W8;i0vUqz#Tp6v0sx4RT>e$1xNOxa7of0wn@2I|De0k^xto6i%bnbP%Y{ zARab93DlZ0gpOkIFIrWgn$JbW&Wc7FKc|AJ&Z53 zkJZE)ul1t6XzJ_m#*Tb^f)ykiXZK1yZpr_dOohNaFk{z%8D9_eR8(k9!%u`%(p$FN zs(qxs+&o#J4@S;@H@3K6#N@9)Mk`=DACD{mI{KeGof+##pfYWzY4k6NYv`CWly6 zSF+vrymzgx2UTV9WQbb*sX>nwHz7A8PYbk)8B!(oJn* z^-+5AOVUmq+zEs9yYFQSKP! zwleyI5({d)_SDPm$x=H#yQ!SqPGz=I$2zHF?V0l%sn^$(9SJ1TTk%3CUbw5=JGc>_ zZ7Z{6ntytvYF8BU?~__@ME-!0S6QCSbC{zS^b$NpZ|X0>^YlCYDV%G48%wLwTU@Qs zPUij*PP^eHoA^z_=SMDb;FBR1m!eIpidh5jqD6eNMDGtDf))C7IG=c%*Hk$ya;VXf zY%Vj$DQ|sOGZ!m9>l>R0LG)a$d zEYoM%0<5(%xt4JJ3|9v@BsjdnLEvzTLz%%>AZQ7BfhUIgFiM3nn%LhJ8@}guWjgIk&~;uzC<77pFetyQ{LRb=})DEJ@b-dog|;L9RDZ%6$U#3gzzzl jJO(j3XiN`otGTtzmYVCRx!W@v>cmxPJDHF|9LL|A=gjWT%+9Vmo9-WN-7-X6Q_B)ztrZK!dqvMm$DL7kowqA9nKX~2v~6QI@3eE*h_raM?@^T zHQtnnjAd5~rk^NSaka+`kRT4a+H2O65DvN8XR0KO!;U@!bkRIQ8gK(?#Em3^BP5EW zZj)rjNF2vqEt^fG88<_~0YlEGsREn(6OIn%Gwe10hrzEqk79w792f?=au4W%b?$HN zSVf1AP<`-LrReE}%Zw?UPbeG261L*MuK z*WXT5LAzjeICE7YSC}u(8dWrHl<1Z?CQDjw+%oLBV!<|OS{fF5oHl!&-j`a>{Xkq; zT=Ldr(9{1~ekE_>Pu(L+BP)1EZh02kkz4oV)UKR*KUqV-7p{pQ9NAO*cGSL2>7#mq zN^+COI`6E@a&!WPE2HukD6!lcV#v_5N*boAt(=CV^trMRGnFTSuus6u9itnOH(g9t z`Kg>`mn;t}Ut#bvXv~sJhDOGTK_(5$wkGM*Xa^jlAEJrDYpmxw!wrUU)-7mrb4Ee0 z@>#QJ8||u - - -
-
-
-

Investor Profile

-

- {{ investor.name }} -

-
- -
- -
- -
-
-

- Investor Description -

-

- {{ investor.description or 'No description - available.' }} -

-
- -
-

- Portfolio Highlights -

-
- {% if investor.portfolio_highlights %} {% for - company in investor.portfolio_highlights[:5] %} - {{ company }} - {% endfor %} {% else %} -

- No portfolio highlights available -

- {% endif %} -
-
- -
-

- Senior Leadership -

- {% if investor.team_members %} {% for member in - investor.team_members[:2] %} -
-

- {{ member.name }} -

-

- {{ member.role or member.title or 'Team Member' - }} -

- {% if member.email %} -

- {{ member.email }} -

- {% endif %} -
- {% endfor %} {% else %} -

- No team information available -

- {% endif %} -
- -
-

- Key Data -

+
+ +
+
+

+ Investor Description +

+

+ {{ investor.description or 'No description available.' }} +

+
-
-

Headquarters:

-

- {{ investor.headquarters or 'N/A' }} -

+
+

+ Portfolio Highlights +

+
+ {% if investor.portfolio_highlights %} + {% for company in investor.portfolio_highlights[:5] %} + {{ company }} + {% endfor %} + {% else %} +

+ No portfolio highlights available +

+ {% endif %} +
+
+ +
+

+ Senior Leadership +

+ {% if investor.team_members %} + {% for member in investor.team_members[:2] %} +
+

+ {{ member.name }} +

+

+ {{ member.role or member.title or 'Team Member' }} +

+ {% if member.email %} +

+ {{ member.email }} +

+ {% endif %} +
+ {% endfor %} + {% else %} +

No team information available

+ {% endif %} +
-
-

Sectors:

-

- {% if investor.sectors %} {{ investor.sectors | - join(', ') }} {% else %} N/A {% endif %} -

-
+ +
+

+ Key Data +

+
+
+

Headquarters:

+

+ {{ investor.headquarters or 'N/A' }} +

+
-
-

DACH Region:

-

- {{ investor.geographic_focus or 'N/A' }} -

-
+
+

Sectors:

+

+ {% if investor.sectors %} + {{ investor.sectors | join(', ') }} + {% else %} + N/A + {% endif %} +

+
-
-

- AUM: (EUR million) (as of Fund IX) -

-

- {% if investor.aum %} €{{ - '{:,.0f}'.format(investor.aum / 1000000) }}M {% else - %} N/A {% endif %} -

-
+
+

DACH Region:

+

+ {{ investor.geographic_focus or 'N/A' }} +

+
+ +
+

AUM (EUR million):

+

+ {% if investor.aum %} + €{{ '{:,.0f}'.format(investor.aum / 1000000) }}M + {% else %} + N/A + {% endif %} +

+

@@ -182,41 +203,23 @@ 1000000) }}M {% elif investor.check_size_lower %} €{{ '{:,.0f}'.format(investor.check_size_lower / 1000000) }}M+ {% else %} N/A {% endif %} -

-
- -
-

- Select Deals, Series A, Series B: -

-

- Growth -

-
- -
-

Focus Areas:

-

- {% if investor.investment_thesis %} {{ - investor.investment_thesis[:3] | join(', ') }} {% - else %} Disruptive Technologies, Entrepreneur-led, - Sustainability {% endif %} -

+

+
+
-
-
- Page 1 +
- + {% if project %} -
-

- {{ investor.name }}: Mandate Match Analysis -

+
+
+

+ {{ investor.name }}: Mandate Match Analysis +

diff --git a/investors.db b/investors.db index 59ed697527cff793d423e3fa59d167e40eba2b28..4e8c7d338c4a6b261b9d3bde65eeb128f9b50859 100644 GIT binary patch delta 3426 zcmaLa349dQ0mt#3J=kOkCJ5vp2|N^#BMF-g2;mS21PF2k5&?sT$?nVUklEQ~W;S6t zB!F^?fY+c=7kkob6>BBcwj63(k5-}gVXdvzwpuJ!dx@?6{}*aMwA6e)-ycbKcIM4{ zv-v!_551Sy-Ti*v{?h*QU9OT-uK#=uK2TBPoA0XX-1S(bCtoSpT5z;L@diBw?zejV zGnCiey>%_hF>mkTxys6%A?`L;o@=Tr(2{HUKk)DJJ>~24j`U1$uW?Ov56sUr9$u)F zPutc!*k4pz>)t-gR@-!`8J*Iy6K1PsSXwk@w}#bl?En0>?4N9r= z%nD_I^Y&b&->UN`j7!6AO{%snO`}ycR6T8JmT0P9(IgfvhUNteDnuYRu9_VZ2PQ1d z))L159-yn1-D=8qQ(7?@aUN+IprK%u9S?q=#n^EEM6oo{f-^yS`~1E2fW~eKH!HO$b|s( zg*@m7mq9)hK!3O#2EafV1cgup#ZUr+p%luX9EQM95HJj`fGc4*jDV4F6^w$>Fb2j# z1yn*6jDsLlLkMbMJWPPAVIoX|T9^z|U@A<5YhXIefSFJSvtTyV!yK3k^PmCd!?myg z7Q!M}3`?L9mclY{Y6TSuChUegUcmZC7 z@4$EAC3qQ5!D;v&d>?)QKZGB_kKrfqQ}`MD9A1H6z^m{}cny99zlPW0H}D3$3BQHk z!SCTMcpLrzXW$+9BbF`!;EDrm`bLK8OH>fY9_?gFyomC%+<_9W)f4&OlGDqQ<-VZ zHOzEo1~ZeXV`ee4nR;dpGnbjiG%)j-YncViLS_-Om|4O!GE13dOcT@0T*oYDRxm4> z>zNj26>|e~Ba>a))%;Fn+L$mCVI9C(MrY!T!6cX@vz9R#i?Nv$)5&x( z>zFjNp4q@`WHvE3F`JogW(%{G*~Z+=+`?>Ub}&1cUCeG~4|6NCm${AE$LwbgFt;;z zFn2O{F?TZ#bC9`*xtHltj^j_0;!8eIpea6Y;9*z58Fr#%&vEhEdZjopZ+D{-aOLhU z-#xvi#?@d5J0=Bp3!*wySv^U_6NVkLDnzI{Bx0&1+7scFB_jfx49y6eQnfTADm2@2 zP7L!5%+#(6Df!Ox=R5_@6UTxDn=3?@X2*nT2<(ouxN76AYNs^Ss1(>Ag*$|$t(U@7 zjVRC4ZYJVlc=bYIC-6^jAueYMOY%)dL@bl0CM_{@tS}QPBVtvFCJeS+GvnEz=~{=> z(>&Y|s<2IIL~?MVF4c@!J*TH&cXf7t+V4y|b0*+)uX(m`WQu-2}Hg^`N4Nz>wVM5Lf+4mCw+>qRt<-xjtRhC(Gnc8n5knX;u@yhN=2iXCkw+* znh7;*Yn@W4$?W&B%-T&oN@=ESa!M(gg;mnb3zyUrU7{^5Lcz+K;D{Sn?sjGtsW|{^h0D##TG9IJ#6LSVA3d*9=?|mvT;? z+&dss;tnZ&bGYU_SLWm!JC$PRNaaRf-uXqr6Q`#lnbYSsD21D|b61b|irHz*64o$b z$f|ZsS%nDWsWr8>l+7E!5P3RNCR4VW-5zIWB$}w?D;GAQ*(Z%tP-&^1ooW1trvfXB z70*7v=O28ovy4j<53BQH#2d&Q*$`5^Q&kbcWlc+B64m(m4-3|yOX_+hCOeEpz@@@! zQnOW^&pS?I$|!c~s_wk|+>$}(SGA183@74AX=8G+^fH2vi5gY0<+U?g9y+KLI->l3 z|4hspKc~!>h0(8t4S8FGd#RO8@`> delta 1520 zcmYMwcbFFf0LJnAU3adot1Buc+6oQPG8;#oJrZrc$S9SFjHoO6QnVy1BRji{j*z|g z&d%ODEA#c2_jx|g^ZxVx^VZa?tEgG}Q^kUojjM}_T0B?uf0{4s(xbGxsN1yRE5JlkPvDA_DQrSStq^{JH za;YyB(m)zYr8JVpvY~7wO{A$blZ~ahw2)1trEDsjNh@hBZKSQVllIa!kR#RQErl(qz@~}K2 zkIG~6xI7_G%2V>RJR{G_bMm~rATP>G^0K@lugYuky1XH4+=pNg|w$UScMz81{ePX-VK6Z#5W2e|T z`bNL#9|NK)c8OhMU<``t*e!OCJz~$;EB20kV&B*=_K(4FKn#fkbFo9%JH&I5LikqhoB0i(_JZOo)kbY)p#d;`o>xC&Y;{B~FTyTinC*O%!za2+?X5lVty=$g>haiit}S}To4z=MR9Rl M5=#oVa%o}Gzub;U@c;k-