mlbench123 commited on
Commit
2c7d54f
·
verified ·
1 Parent(s): d96162f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +306 -0
app.py CHANGED
@@ -1681,6 +1681,312 @@ async def health_check():
1681
  return {"status": "healthy", "service": "Real Estate Financial Model API"}
1682
 
1683
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1684
  if __name__ == "__main__":
1685
 
1686
  # Hardcoded API Key
 
1681
  return {"status": "healthy", "service": "Real Estate Financial Model API"}
1682
 
1683
 
1684
+ @app.post("/api/generate-model-with-summary")
1685
+ async def generate_model_with_summary(files: List[UploadFile] = File(...)):
1686
+ """
1687
+ API endpoint that returns JSON summary with key metrics
1688
+ AND includes base64 encoded Excel file
1689
+
1690
+ Returns JSON with:
1691
+ - Key financial metrics
1692
+ - Deal summary
1693
+ - Market analysis
1694
+ - Excel file as base64
1695
+ """
1696
+ if not files:
1697
+ raise HTTPException(status_code=400, detail="No files uploaded")
1698
+
1699
+ temp_dir = None
1700
+ try:
1701
+ # Create temporary directory
1702
+ temp_dir = tempfile.mkdtemp()
1703
+ print(f"📁 Created temp directory: {temp_dir}")
1704
+
1705
+ # Save uploaded files
1706
+ for upload_file in files:
1707
+ file_path = Path(temp_dir) / upload_file.filename
1708
+ print(f"💾 Saving file: {upload_file.filename}")
1709
+
1710
+ with open(file_path, "wb") as f:
1711
+ content = await upload_file.read()
1712
+ f.write(content)
1713
+
1714
+ print(f"✅ Saved {len(files)} files")
1715
+
1716
+ # Initialize pipeline
1717
+ print("🔧 Initializing pipeline...")
1718
+ pipeline = RealEstateModelPipeline(GEMINI_API_KEY)
1719
+
1720
+ # Create output file
1721
+ output_file = Path(temp_dir) / "Real_Estate_Financial_Model.xlsx"
1722
+
1723
+ # Run pipeline
1724
+ print("🚀 Running pipeline...")
1725
+ pipeline.run_full_pipeline(temp_dir, str(output_file))
1726
+
1727
+ # Check if file was created
1728
+ if not output_file.exists():
1729
+ raise Exception("Excel file was not generated")
1730
+
1731
+ print(f"✅ Excel file created: {output_file}")
1732
+
1733
+ # Get formula results
1734
+ r = pipeline.formula_results
1735
+ d = pipeline.structured_data
1736
+
1737
+ # Build comprehensive JSON response
1738
+ response_data = {
1739
+ "status": "success",
1740
+ "message": "Financial model generated successfully",
1741
+
1742
+ # KEY METRICS (Dashboard Top Cards)
1743
+ "key_metrics": {
1744
+ "purchase_price": {
1745
+ "value": r.get('PRICE', 0),
1746
+ "formatted": f"${r.get('PRICE', 0):,.0f}",
1747
+ "label": "Purchase Price"
1748
+ },
1749
+ "noi": {
1750
+ "value": r.get('NET_OPERATING_INCOME', 0),
1751
+ "formatted": f"${r.get('NET_OPERATING_INCOME', 0):,.0f}",
1752
+ "label": "NOI"
1753
+ },
1754
+ "cap_rate": {
1755
+ "value": r.get('CAP_RATE', 0),
1756
+ "formatted": f"{r.get('CAP_RATE', 0):.1f}%",
1757
+ "label": "Cap Rate"
1758
+ },
1759
+ "irr_10yr": {
1760
+ "value": float(r.get('IRR_TO_LP', 0).real if isinstance(r.get('IRR_TO_LP', 0), complex) else r.get('IRR_TO_LP', 0)),
1761
+ "formatted": f"{float(r.get('IRR_TO_LP', 0).real if isinstance(r.get('IRR_TO_LP', 0), complex) else r.get('IRR_TO_LP', 0)):.1f}%",
1762
+ "label": "IRR (10 yr)"
1763
+ },
1764
+ "dscr": {
1765
+ "value": r.get('DEBT_SERVICE_COVERAGE_RATIO', 0),
1766
+ "formatted": f"{r.get('DEBT_SERVICE_COVERAGE_RATIO', 0):.2f}x",
1767
+ "label": "DSCR"
1768
+ }
1769
+ },
1770
+
1771
+ # T12 SUMMARY (Extracted Summary)
1772
+ "t12_summary": {
1773
+ "gross_income": {
1774
+ "annual": r.get('EFFECTIVE_GROSS_INCOME', 0),
1775
+ "monthly": r.get('EFFECTIVE_GROSS_INCOME', 0) / 12,
1776
+ "source": "Calculated from rent roll"
1777
+ },
1778
+ "operating_expenses": {
1779
+ "annual": r.get('TOTAL_OPERATING_EXPENSES', 0),
1780
+ "monthly": r.get('TOTAL_OPERATING_EXPENSES', 0) / 12,
1781
+ "expense_ratio": f"{(r.get('TOTAL_OPERATING_EXPENSES', 0) / r.get('EFFECTIVE_GROSS_INCOME', 1) if r.get('EFFECTIVE_GROSS_INCOME', 0) > 0 else 0):.1%}"
1782
+ },
1783
+ "noi": {
1784
+ "annual": r.get('NET_OPERATING_INCOME', 0),
1785
+ "monthly": r.get('NET_OPERATING_INCOME', 0) / 12,
1786
+ "verified": True
1787
+ },
1788
+ "occupancy": {
1789
+ "rate": f"{(1 - r.get('VACANCY_RATE', 0)):.0%}",
1790
+ "value": 1 - r.get('VACANCY_RATE', 0)
1791
+ }
1792
+ },
1793
+
1794
+ # PROPERTY INFO
1795
+ "property_info": {
1796
+ "address": d.get('property_info', {}).get('address', 'N/A'),
1797
+ "property_type": d.get('property_info', {}).get('property_type', 'N/A'),
1798
+ "units": r.get('UNITS', 0),
1799
+ "gross_sf": r.get('GROSS_SF', 0),
1800
+ "rentable_sf": r.get('RENTABLE_SF', 0),
1801
+ "building_efficiency": f"{r.get('BUILDING_EFFICIENCY', 0):.1%}"
1802
+ },
1803
+
1804
+ # DEAL VS MARKET AVERAGES
1805
+ "market_comparison": {
1806
+ "your_deal": {
1807
+ "cap_rate": r.get('CAP_RATE', 0) / 100,
1808
+ "irr": float(r.get('IRR_TO_LP', 0).real if isinstance(r.get('IRR_TO_LP', 0), complex) else r.get('IRR_TO_LP', 0)) / 100,
1809
+ "dscr": r.get('DEBT_SERVICE_COVERAGE_RATIO', 0)
1810
+ },
1811
+ "market_average": {
1812
+ "cap_rate": 0.055, # Example market average
1813
+ "irr": 0.12,
1814
+ "dscr": 1.25
1815
+ }
1816
+ },
1817
+
1818
+ # AI OBSERVATIONS
1819
+ "ai_observations": [
1820
+ {
1821
+ "observation": f"Cap Rate {'above' if r.get('CAP_RATE', 0) > 5.5 else 'below'} area average",
1822
+ "recommendation": "undervalued potential" if r.get('CAP_RATE', 0) > 5.5 else "premium pricing"
1823
+ },
1824
+ {
1825
+ "observation": f"Expense ratio at {(r.get('TOTAL_OPERATING_EXPENSES', 0) / r.get('EFFECTIVE_GROSS_INCOME', 1) if r.get('EFFECTIVE_GROSS_INCOME', 0) > 0 else 0):.1%}",
1826
+ "recommendation": "within normal range" if 0.3 <= (r.get('TOTAL_OPERATING_EXPENSES', 0) / r.get('EFFECTIVE_GROSS_INCOME', 1)) <= 0.45 else "review expenses"
1827
+ },
1828
+ {
1829
+ "observation": "DSCR meets financing standards" if r.get('DEBT_SERVICE_COVERAGE_RATIO', 0) >= 1.2 else "DSCR below standards",
1830
+ "recommendation": "approved" if r.get('DEBT_SERVICE_COVERAGE_RATIO', 0) >= 1.2 else "increase equity"
1831
+ },
1832
+ {
1833
+ "observation": "NOI verification",
1834
+ "recommendation": "Full underwriting review recommended"
1835
+ }
1836
+ ],
1837
+
1838
+ # CAPITAL STACK
1839
+ "capital_stack": {
1840
+ "total_project_cost": r.get('TOTAL_FINANCING_CONTINGENCY_AND_RESERVES', 0),
1841
+ "total_debt": r.get('TOTAL_DEBT', 0),
1842
+ "total_equity": r.get('TOTAL_EQUITY', 0),
1843
+ "debt_percentage": r.get('DEBT_PERCENTAGE', 0),
1844
+ "equity_percentage": r.get('EQUITY_PERCENTAGE', 0),
1845
+ "ltc_ratio": r.get('LTC_RATIO', 0)
1846
+ },
1847
+
1848
+ # RETURNS ANALYSIS
1849
+ "returns": {
1850
+ "yield_on_cost": r.get('YIELD_ON_COST_PERCENTAGE', 0),
1851
+ "stabilized_yield_on_cost": r.get('STABILIZED_YIELD_ON_COST_PERCENTAGE', 0),
1852
+ "cash_on_cash_return": r.get('CASH_ON_CASH_RETURN', 0),
1853
+ "lp_multiple": r.get('LP_MULTIPLE', 0),
1854
+ "lp_irr": float(r.get('IRR_TO_LP', 0).real if isinstance(r.get('IRR_TO_LP', 0), complex) else r.get('IRR_TO_LP', 0)),
1855
+ "hold_period_years": r.get('HOLD_PERIOD_MONTHS', 0) / 12
1856
+ },
1857
+
1858
+ # OPERATING METRICS
1859
+ "operating_metrics": {
1860
+ "gross_potential_rent": r.get('GROSS_POTENTIAL_FREE_MARKET_RENT', 0) + r.get('GROSS_POTENTIAL_AFFORDABLE_RENT', 0),
1861
+ "vacancy_loss": r.get('VACANCY_LOSS', 0),
1862
+ "effective_gross_income": r.get('EFFECTIVE_GROSS_INCOME', 0),
1863
+ "operating_expenses": r.get('TOTAL_OPERATING_EXPENSES', 0),
1864
+ "noi": r.get('NET_OPERATING_INCOME', 0),
1865
+ "noi_per_unit": r.get('NOI_PER_UNIT', 0),
1866
+ "noi_per_sf": r.get('NOI_PER_GSF', 0)
1867
+ },
1868
+
1869
+ # COST BREAKDOWN
1870
+ "cost_breakdown": {
1871
+ "acquisition": {
1872
+ "total": r.get('TOTAL_ACQUISITION_COST', 0),
1873
+ "per_unit": r.get('TOTAL_ACQUISITION_COST_PER_UNIT', 0),
1874
+ "per_sf": r.get('TOTAL_ACQUISITION_COST_PER_GSF', 0)
1875
+ },
1876
+ "construction": {
1877
+ "total": r.get('TOTAL_CONSTRUCTION_GMP', 0),
1878
+ "per_unit": r.get('CONSTRUCTION_GMP_PER_UNIT', 0),
1879
+ "per_sf": r.get('CONSTRUCTION_GMP_PER_GSF', 0)
1880
+ },
1881
+ "soft_costs": {
1882
+ "total": r.get('TOTAL_SOFT_COST', 0),
1883
+ "per_sf": r.get('TOTAL_SOFT_COST_PER_GSF', 0)
1884
+ },
1885
+ "contingency": r.get('CONTINGENCY_COST', 0),
1886
+ "reserves": r.get('OPERATING_RESERVE', 0)
1887
+ },
1888
+
1889
+ # SOURCE FILES
1890
+ "source_files": [
1891
+ {
1892
+ "filename": file.filename,
1893
+ "size": len(await file.read()) if hasattr(file, 'read') else 0,
1894
+ "type": file.filename.split('.')[-1].upper()
1895
+ }
1896
+ for file in files
1897
+ ],
1898
+
1899
+ # EXCEL FILE (BASE64 ENCODED)
1900
+ "excel_file": {
1901
+ "filename": "Real_Estate_Financial_Model.xlsx",
1902
+ "size_bytes": output_file.stat().st_size,
1903
+ "base64_data": base64.b64encode(output_file.read_bytes()).decode('utf-8')
1904
+ }
1905
+ }
1906
+
1907
+ print("✅ Response JSON created")
1908
+
1909
+ return JSONResponse(content=response_data)
1910
+
1911
+ except Exception as e:
1912
+ import traceback
1913
+ error_details = traceback.format_exc()
1914
+ print("❌ ERROR OCCURRED:")
1915
+ print(error_details)
1916
+
1917
+ raise HTTPException(
1918
+ status_code=500,
1919
+ detail={
1920
+ "error": str(e),
1921
+ "type": type(e).__name__,
1922
+ "traceback": error_details
1923
+ }
1924
+ )
1925
+
1926
+
1927
+
1928
+ # NEW: Endpoint that returns ONLY JSON (no Excel file)
1929
+ @app.post("/api/analyze-only")
1930
+ async def analyze_only(files: List[UploadFile] = File(...)):
1931
+ """
1932
+ Returns only JSON summary without Excel file
1933
+ Faster response, good for dashboards
1934
+ """
1935
+ if not files:
1936
+ raise HTTPException(status_code=400, detail="No files uploaded")
1937
+
1938
+ temp_dir = None
1939
+ try:
1940
+ temp_dir = tempfile.mkdtemp()
1941
+
1942
+ for upload_file in files:
1943
+ file_path = Path(temp_dir) / upload_file.filename
1944
+ with open(file_path, "wb") as f:
1945
+ content = await upload_file.read()
1946
+ f.write(content)
1947
+
1948
+ pipeline = RealEstateModelPipeline(GEMINI_API_KEY)
1949
+
1950
+ # Extract and process data (no Excel generation)
1951
+ pipeline.extract_all_pdfs(temp_dir)
1952
+ structured_data = pipeline.extract_structured_data()
1953
+ structured_data = pipeline.post_process_extracted_data(structured_data)
1954
+ pipeline.calculate_all_formulas(structured_data)
1955
+
1956
+ r = pipeline.formula_results
1957
+ d = pipeline.structured_data
1958
+
1959
+ # Return lightweight JSON
1960
+ return JSONResponse(content={
1961
+ "status": "success",
1962
+ "key_metrics": {
1963
+ "purchase_price": r.get('PRICE', 0),
1964
+ "noi": r.get('NET_OPERATING_INCOME', 0),
1965
+ "cap_rate": r.get('CAP_RATE', 0),
1966
+ "irr": float(r.get('IRR_TO_LP', 0).real if isinstance(r.get('IRR_TO_LP', 0), complex) else r.get('IRR_TO_LP', 0)),
1967
+ "dscr": r.get('DEBT_SERVICE_COVERAGE_RATIO', 0)
1968
+ },
1969
+ "property_info": {
1970
+ "address": d.get('property_info', {}).get('address', 'N/A'),
1971
+ "units": r.get('UNITS', 0),
1972
+ "gross_sf": r.get('GROSS_SF', 0)
1973
+ },
1974
+ "capital_stack": {
1975
+ "total_project_cost": r.get('TOTAL_FINANCING_CONTINGENCY_AND_RESERVES', 0),
1976
+ "total_debt": r.get('TOTAL_DEBT', 0),
1977
+ "total_equity": r.get('TOTAL_EQUITY', 0)
1978
+ },
1979
+ "returns": {
1980
+ "yield_on_cost": r.get('YIELD_ON_COST_PERCENTAGE', 0),
1981
+ "cash_on_cash": r.get('CASH_ON_CASH_RETURN', 0),
1982
+ "lp_multiple": r.get('LP_MULTIPLE', 0)
1983
+ }
1984
+ })
1985
+
1986
+ except Exception as e:
1987
+ raise HTTPException(status_code=500, detail=str(e))
1988
+
1989
+
1990
  if __name__ == "__main__":
1991
 
1992
  # Hardcoded API Key