nikeshn commited on
Commit
f99fd18
Β·
verified Β·
1 Parent(s): 5c3f33a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +71 -38
app.py CHANGED
@@ -9,67 +9,102 @@ Changes over v3.5:
9
  header is not needed and actively harmful in this deployment context.
10
  """
11
 
 
12
  import logging
13
- import os
14
  import time
15
  from contextlib import asynccontextmanager
16
- from typing import Optional
17
 
18
- from fastapi import FastAPI, Request, Response
19
  from fastapi.middleware.cors import CORSMiddleware
20
  from fastapi.responses import JSONResponse
21
 
 
 
 
 
 
 
22
  from src.config import get_settings
23
  from src.services.metrics_service import MetricsService
24
  from src.services.rag_service import RAGService
25
 
 
26
  logger = logging.getLogger(__name__)
27
 
28
- # ── Singletons ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- _rag_service: Optional[RAGService] = None
31
- _metrics_service: Optional[MetricsService] = None
32
 
 
33
 
34
  def get_rag_service() -> RAGService:
35
- global _rag_service
36
- if _rag_service is None:
37
- raise RuntimeError("RAG service not initialised")
38
- return _rag_service
39
 
40
 
41
  def get_metrics_service() -> MetricsService:
42
- global _metrics_service
43
- if _metrics_service is None:
44
- raise RuntimeError("Metrics service not initialised")
45
- return _metrics_service
 
 
 
 
 
 
 
 
46
 
47
 
48
  # ── Lifespan ──────────────────────────────────────────────────────────────────
49
 
50
  @asynccontextmanager
51
  async def lifespan(app: FastAPI):
52
- global _rag_service, _metrics_service
53
-
54
  settings = get_settings()
55
  logger.info("LibBee startup β€” initialising services")
56
 
57
- # Metrics (in-memory)
58
- _metrics_service = MetricsService()
59
- logger.info("MetricsService ready")
60
-
61
- # RAG service
62
- _rag_service = RAGService(settings)
63
  try:
64
- await _rag_service.initialise()
65
- stats = _rag_service.get_stats()
66
- logger.info("RAGService ready β€” %s", stats)
67
  except Exception as exc:
68
- logger.error("RAGService init failed: %s", exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  yield
71
 
72
- logger.info("LibBee shutdown")
 
 
 
 
 
 
 
73
 
74
 
75
  # ── App factory ───────────────────────────────────────────────────────────────
@@ -107,11 +142,10 @@ def create_app() -> FastAPI:
107
  # ── Security headers middleware ────────────────────────────────────────────
108
  @app.middleware("http")
109
  async def security_headers(request: Request, call_next):
110
- response: Response = await call_next(request)
111
- # X-Frame-Options: SAMEORIGIN is intentionally NOT set here.
112
- # HuggingFace Spaces renders the app inside an iframe; setting
113
- # SAMEORIGIN would block the HF Space preview panel from loading.
114
- # Frame embedding is already controlled by the CSP header below.
115
  response.headers["X-Content-Type-Options"] = "nosniff"
116
  response.headers["X-XSS-Protection"] = "1; mode=block"
117
  response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
@@ -128,16 +162,15 @@ def create_app() -> FastAPI:
128
  )
129
  return response
130
 
131
- # ── Request timing middleware ─────────────────────────────────────────────
132
  @app.middleware("http")
133
  async def add_process_time_header(request: Request, call_next):
134
  start_time = time.time()
135
  response = await call_next(request)
136
- process_time = time.time() - start_time
137
- response.headers["X-Process-Time"] = str(round(process_time, 4))
138
  return response
139
 
140
- # ── Health / root endpoints ───────────────────────────────────────────────
141
  @app.get("/")
142
  async def root():
143
  return {
@@ -165,9 +198,9 @@ def create_app() -> FastAPI:
165
  )
166
 
167
  @app.get("/metrics")
168
- async def metrics():
169
  try:
170
- return get_metrics_service().get_summary()
171
  except Exception as exc:
172
  return {"error": str(exc)}
173
 
 
9
  header is not needed and actively harmful in this deployment context.
10
  """
11
 
12
+ import asyncio
13
  import logging
 
14
  import time
15
  from contextlib import asynccontextmanager
 
16
 
17
+ from fastapi import FastAPI, Request
18
  from fastapi.middleware.cors import CORSMiddleware
19
  from fastapi.responses import JSONResponse
20
 
21
+ try:
22
+ from cachetools import TTLCache
23
+ _ttl_cache_available = True
24
+ except ImportError:
25
+ _ttl_cache_available = False
26
+
27
  from src.config import get_settings
28
  from src.services.metrics_service import MetricsService
29
  from src.services.rag_service import RAGService
30
 
31
+ logging.basicConfig(level=logging.INFO)
32
  logger = logging.getLogger(__name__)
33
 
34
+ # ── Module-level singletons (matches original working pattern) ────────────────
35
+ rag_service = RAGService()
36
+ metrics_service = MetricsService(get_settings().metrics_path)
37
+
38
+ # ── Rate-limit request log ────────────────────────────────────────────────────
39
+ if _ttl_cache_available:
40
+ _request_log = TTLCache(maxsize=10_000, ttl=120)
41
+ _request_log_lock = asyncio.Lock()
42
+ else:
43
+ from collections import defaultdict, deque
44
+ _request_log = defaultdict(deque) # type: ignore[assignment]
45
+ _request_log_lock = asyncio.Lock()
46
 
 
 
47
 
48
+ # ── Public accessors ──────────────────────────────────────────────────────────
49
 
50
  def get_rag_service() -> RAGService:
51
+ return rag_service
 
 
 
52
 
53
 
54
  def get_metrics_service() -> MetricsService:
55
+ return metrics_service
56
+
57
+
58
+ # ── Background metrics flush ──────────────────────────────────────────────────
59
+
60
+ async def _metrics_flush_loop(interval: int = 30) -> None:
61
+ while True:
62
+ await asyncio.sleep(interval)
63
+ try:
64
+ await metrics_service.flush()
65
+ except Exception as exc:
66
+ logger.warning("Metrics flush error: %s", exc)
67
 
68
 
69
  # ── Lifespan ──────────────────────────────────────────────────────────────────
70
 
71
  @asynccontextmanager
72
  async def lifespan(app: FastAPI):
 
 
73
  settings = get_settings()
74
  logger.info("LibBee startup β€” initialising services")
75
 
76
+ # Staff index
 
 
 
 
 
77
  try:
78
+ from src.services.staff_service import build_staff_index
79
+ build_staff_index()
80
+ logger.info("Staff index built")
81
  except Exception as exc:
82
+ logger.error("Staff index build failed: %s", exc)
83
+
84
+ # RAG service
85
+ if settings.openai_api_key:
86
+ try:
87
+ await rag_service.initialize(openai_api_key=settings.openai_api_key)
88
+ logger.info("RAG service ready β€” chunks: %d", len(rag_service.bm25_corpus))
89
+ except Exception as exc:
90
+ logger.error("RAG initialization failed: %s", exc, exc_info=True)
91
+ else:
92
+ logger.warning("OPENAI_API_KEY not set β€” RAG disabled")
93
+
94
+ # Background metrics flush
95
+ flush_task = asyncio.create_task(_metrics_flush_loop(interval=30))
96
+ logger.info("LibBee startup complete")
97
 
98
  yield
99
 
100
+ # Shutdown
101
+ flush_task.cancel()
102
+ try:
103
+ await asyncio.wait_for(metrics_service.flush(), timeout=5.0)
104
+ logger.info("Metrics flushed on shutdown")
105
+ except Exception as exc:
106
+ logger.warning("Metrics flush on shutdown failed: %s", exc)
107
+ logger.info("LibBee shutdown complete")
108
 
109
 
110
  # ── App factory ───────────────────────────────────────────────────────────────
 
142
  # ── Security headers middleware ────────────────────────────────────────────
143
  @app.middleware("http")
144
  async def security_headers(request: Request, call_next):
145
+ response = await call_next(request)
146
+ # X-Frame-Options: SAMEORIGIN intentionally NOT set.
147
+ # HuggingFace Spaces runs inside an iframe; SAMEORIGIN blocks the
148
+ # HF Space preview panel. Frame embedding is controlled by CSP below.
 
149
  response.headers["X-Content-Type-Options"] = "nosniff"
150
  response.headers["X-XSS-Protection"] = "1; mode=block"
151
  response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
 
162
  )
163
  return response
164
 
165
+ # ── Request timing ────────────────────────────────────────────────────────
166
  @app.middleware("http")
167
  async def add_process_time_header(request: Request, call_next):
168
  start_time = time.time()
169
  response = await call_next(request)
170
+ response.headers["X-Process-Time"] = str(round(time.time() - start_time, 4))
 
171
  return response
172
 
173
+ # ── Health / utility endpoints ────────────────────────────────────────────
174
  @app.get("/")
175
  async def root():
176
  return {
 
198
  )
199
 
200
  @app.get("/metrics")
201
+ async def metrics_endpoint():
202
  try:
203
+ return get_metrics_service().snapshot()
204
  except Exception as exc:
205
  return {"error": str(exc)}
206