Spaces:
Runtime error
Runtime error
| """ | |
| Lightweight Hugging Face runtime for IRIS. | |
| This server exposes the same public API paths the frontend uses, but answers | |
| from precomputed cache files so the Space can run without rebuilding vector | |
| indexes on every cold start. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import json | |
| import re | |
| import time | |
| from pathlib import Path | |
| from typing import Any | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse | |
| from pydantic import BaseModel | |
| BASE_DIR = Path(__file__).resolve().parent | |
| DATA_DIR = BASE_DIR / "data" | |
| PRIMARY_DOC_ID = "emiratesnbd_investor_presentation_2026_q1" | |
| CACHE_DIR = DATA_DIR / "response_cache" / PRIMARY_DOC_ID | |
| DOCS_FILE = DATA_DIR / "documents.json" | |
| PAGES_DIR = DATA_DIR / "pages" | |
| INTENT_CACHE_FILES = { | |
| "capital": "capital.json", | |
| "cost_efficiency": "cost-efficiency.json", | |
| "credit_quality": "credit-quality.json", | |
| "deposits": "deposits.json", | |
| "ecl_scenario": "ecl-scenario.json", | |
| "esg": "esg.json", | |
| "hyperinflation": "hyperinflation.json", | |
| "income": "income.json", | |
| "liquidity": "liquidity.json", | |
| "loans_sector": "loans-sector.json", | |
| "loans": "loans.json", | |
| "macro": "macro.json", | |
| "net_interest_margin": "net-interest-margin.json", | |
| "non_funded_income": "non-funded-income.json", | |
| "profitability": "profitability.json", | |
| "segment": "segment.json", | |
| } | |
| INTENT_PATTERNS = [ | |
| ("esg", [ | |
| r"\besg\b", r"\benvironmental social governance\b", r"\bsustainab", | |
| r"\bgreen\b", r"\bclimate\b", r"\bemissions?\b", r"\bghg\b", | |
| r"\bscope\s*[12]\b", r"\btransition finance\b", r"\bdiversity\b", | |
| r"\bgovernance\b", r"\bsustainable finance\b", r"\bsustainalytics\b", | |
| r"\bmsci\b", r"\bs&p\b", r"\bfemale leadership\b", r"\bnet[- ]?zero\b", | |
| r"\bdecarboni", r"\bcarbon reduction\b", r"\bgreen bond\b", | |
| r"\bsustainable issuance\b", r"\buse of proceeds\b", r"\bicma\b", | |
| r"\bsecond[- ]party opinions?\b", | |
| ]), | |
| ("segment", [ | |
| r"\bbusiness segment", r"\bsegment performance\b", r"\bdivisional\b", | |
| r"\bsegmental performance\b", r"\bdivision\b", r"\brbwm\b", r"\bcib\b", | |
| r"\bgm&t\b", r"\bglobal markets\b", r"\btreasury\b", r"\bdenizbank\b", | |
| r"\bretail banking\b", r"\bwealth management\b", r"\bsbu\b", | |
| r"\bstrategic business unit\b", r"\bbusiness unit\b", | |
| r"\bsegment breakdown\b", r"\bdivisional financial contribution", | |
| r"\bsegment.*operating income\b", r"\bsegment.*\bpbt\b", | |
| r"\bcontributed most.*\bpbt\b", r"\bcontributed most.*operating income\b", | |
| ]), | |
| ("non_funded_income", [ | |
| r"\bnon[- ]?funded\b", r"\bnfi\b", r"\bfee", r"\bcommission", | |
| r"\bclient flow", r"\btrading income", r"\bfx\b", r"\bderivative", | |
| ]), | |
| ("net_interest_margin", [ | |
| r"\bnim\b", r"\bnet interest margin\b", r"\binterest margin\b", | |
| r"\bmargins remain\b", | |
| ]), | |
| ("capital", [ | |
| r"\bcapital adequacy\b", r"\bcet[- ]?1\b", r"\bcar\b", r"\brwa\b", | |
| r"\bbasel\b", r"\bcapital ratio", | |
| ]), | |
| ("liquidity", [ | |
| r"\bliquidity\b", r"\bliquidity coverage ratio\b", r"\blcr\b", | |
| r"\badr\b", r"\bfunding\b", r"\bdebt maturit", r"\bwhat is the lcr\b", | |
| r"\blcr performance\b", | |
| ]), | |
| ("credit_quality", [ | |
| r"\bcost of risk\b", r"\bcredit quality\b", r"\bnpl\b", | |
| r"\bcoverage ratio\b", r"\bimpairment", r"\bprovision", | |
| ]), | |
| ("ecl_scenario", [ | |
| r"\becl\b", r"\bexpected credit loss\b", r"\bscenario", r"\bstage\s*[123]\b", | |
| ]), | |
| ("cost_efficiency", [ | |
| r"\bcost[- ]?to[- ]?income\b", r"\bcir\b", r"\bcost efficiency\b", | |
| r"\boperating expense", | |
| ]), | |
| ("loans_sector", [ | |
| r"\bloans? by sector\b", r"\bsector mix\b", r"\bsector concentration", | |
| r"\bgross loan.*sector", r"\bloans? by sector distribution\b", | |
| r"\bsector.*distribution\b", r"\bloan portfolio.*sector\b", | |
| ]), | |
| ("loans", [ | |
| r"\bloans?\b", r"\badvances?\b", r"\blending\b", r"\bloan growth\b", | |
| r"\bloan growth.*deposit growth\b", r"\bloans? and deposits?\b", | |
| r"\bloan.*deposit\b", | |
| ]), | |
| ("deposits", [ | |
| r"\bdeposits?\b", r"\bcasa\b", r"\btime deposits?\b", r"\bfunding base\b", | |
| ]), | |
| ("hyperinflation", [ | |
| r"\bhyperinflation\b", r"\bias\s*29\b", r"\bturkiye cpi\b", | |
| r"\bmonetary correction\b", | |
| ]), | |
| ("macro", [ | |
| r"\bmacro", r"\beconomic environment\b", r"\bgdp\b", r"\binflation\b", | |
| r"\btourism\b", r"\breal estate\b", r"\bpopulation\b", r"\bproject awards?\b", | |
| ]), | |
| ("profitability", [ | |
| r"\bnet profit\b", r"\btotal profit\b", r"\bprofitability\b", r"\bprofit perform\b", | |
| r"\bprofit growth\b", r"\bprofit before tax\b", r"\bpbt\b", r"\brote\b", | |
| r"\bmain drivers?\b", r"\bkey drivers?\b", r"\bperformance highlights?\b", | |
| r"\bresults? highlights?\b", r"\bq1\s*2026\s+performance\b", | |
| r"\bq1\s*2026\s+results?\b", r"\boverall performance\b", | |
| ]), | |
| ("income", [ | |
| r"\btotal income\b", r"\bincome statement\b", r"\boperating income\b", | |
| r"\bnet interest income\b", r"\bnii\b", r"\brevenue\b", | |
| ]), | |
| ] | |
| class ChatRequest(BaseModel): | |
| question: str | |
| doc_ids: list[str] = [PRIMARY_DOC_ID] | |
| app = FastAPI(title="IRIS IR Intelligence API", version="1.0.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=False, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def _load_json(path: Path) -> Any: | |
| with path.open("r", encoding="utf-8") as handle: | |
| return json.load(handle) | |
| def _detect_cached_intent(question: str) -> str | None: | |
| q_lower = question.lower() | |
| for intent, patterns in INTENT_PATTERNS: | |
| if any(re.search(pattern, q_lower) for pattern in patterns): | |
| return intent | |
| return None | |
| def _load_cached_response(intent: str) -> dict[str, Any] | None: | |
| filename = INTENT_CACHE_FILES.get(intent) | |
| if not filename: | |
| return None | |
| path = CACHE_DIR / filename | |
| if not path.exists(): | |
| return None | |
| return _load_json(path) | |
| def _insufficient_response(question: str, latency_ms: int) -> dict[str, Any]: | |
| return { | |
| "response_type": "insufficient", | |
| "question": question, | |
| "executive_summary": ( | |
| "I do not have enough evidence in the indexed Emirates NBD Q1 2026 " | |
| "presentation cache to answer this question." | |
| ), | |
| "sources": [], | |
| "financial_kpis": [], | |
| "key_drivers": [], | |
| "visual_evidence": [], | |
| "latency_ms": latency_ms, | |
| "model_used": "cache-retrieval", | |
| } | |
| def _page_image_candidates(doc_id: str, page_number: int) -> list[Path]: | |
| pages_dir = PAGES_DIR / doc_id / "pages" | |
| page_stem = f"page_{page_number:04d}" | |
| return [ | |
| pages_dir / f"{page_stem}.png", | |
| pages_dir / f"{page_stem}_colpali.png", | |
| pages_dir / f"{page_stem}_colpali_index.png", | |
| ] | |
| async def health() -> dict[str, str]: | |
| return { | |
| "status": "ok", | |
| "service": "IRIS IR Intelligence", | |
| "institution": "Emirates NBD", | |
| "mode": "hf-cache", | |
| } | |
| async def chat_query(request: ChatRequest) -> dict[str, Any]: | |
| start = time.time() | |
| intent = _detect_cached_intent(request.question) | |
| if intent: | |
| cached = _load_cached_response(intent) | |
| if cached: | |
| response = copy.deepcopy(cached) | |
| response["question"] = request.question | |
| response["latency_ms"] = max(1, int((time.time() - start) * 1000)) | |
| response["model_used"] = response.get("model_used") or "cache-retrieval" | |
| return response | |
| return _insufficient_response(request.question, max(1, int((time.time() - start) * 1000))) | |
| async def list_documents() -> list[dict[str, Any]]: | |
| if DOCS_FILE.exists(): | |
| return _load_json(DOCS_FILE) | |
| return [ | |
| { | |
| "doc_id": PRIMARY_DOC_ID, | |
| "name": "Emiratesnbd Investor Presentation 2026 Q1", | |
| "doc_type": "Investor Presentation", | |
| "period": "2026", | |
| "institution": "Emirates NBD", | |
| "total_pages": 36, | |
| "status": "indexed", | |
| "filename": "emiratesnbd_investor_presentation_2026_q1.pdf", | |
| } | |
| ] | |
| async def get_page_image(doc_id: str, page_number: int) -> FileResponse: | |
| img_path = next( | |
| (candidate for candidate in _page_image_candidates(doc_id, page_number) if candidate.exists()), | |
| None, | |
| ) | |
| if img_path is None: | |
| raise HTTPException(status_code=404, detail=f"Page image not found: {doc_id}/{page_number}") | |
| return FileResponse( | |
| str(img_path), | |
| media_type="image/png", | |
| headers={"Cache-Control": "public, max-age=3600"}, | |
| ) | |