nikeshn commited on
Commit
5753811
Β·
verified Β·
1 Parent(s): eb7f039

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -30
app.py CHANGED
@@ -1,14 +1,16 @@
1
- """LibBee - Main Application Entry Point
2
- Phase 3: production hardening, persistence, metrics, and safer staff routing
3
-
4
- Fixes applied (v3.1):
5
- 1. _request_log replaced with cachetools.TTLCache β€” no more unbounded IP dict
6
- 2. MetricsService background flush task started in lifespan
7
- 3. Circular import risk reduced β€” rag_service / metrics_service exposed via
8
- module-level accessors get_rag_service() / get_metrics_service() so routers
9
- can import the accessor instead of doing a deferred 'from app import ...'
10
- 4. Startup errors no longer silently swallow RAG init exceptions
11
  """
 
 
 
 
 
 
 
 
 
 
 
 
12
  import asyncio
13
  import logging
14
  import time
@@ -32,23 +34,30 @@ from src.services.rag_service import RAGService
32
  logging.basicConfig(level=logging.INFO)
33
  logger = logging.getLogger(__name__)
34
 
35
- rag_service = RAGService()
36
- cache_service = CacheService()
 
 
 
37
  metrics_service = MetricsService(get_settings().metrics_path)
38
 
39
- # ── Rate-limit request log ─────────────────────────────────────────────────────
40
- # TTLCache(maxsize, ttl) auto-evicts entries older than ttl seconds.
41
- # Falls back to a plain dict with manual sweep if cachetools not installed.
 
 
42
  if _ttl_cache_available:
43
- _request_log = TTLCache(maxsize=10_000, ttl=120)
44
  _request_log_lock = asyncio.Lock()
45
  else:
46
  from collections import defaultdict, deque
47
- _request_log = defaultdict(deque) # type: ignore[assignment]
48
  _request_log_lock = asyncio.Lock()
49
 
50
 
51
- # ── Public accessors (used by routers to avoid circular imports) ───────────────
 
 
52
 
53
  def get_rag_service() -> RAGService:
54
  return rag_service
@@ -58,7 +67,7 @@ def get_metrics_service() -> MetricsService:
58
  return metrics_service
59
 
60
 
61
- # ── Background flush task ──────────────────────────────────────────────────────
62
 
63
  async def _metrics_flush_loop(interval: int = 30) -> None:
64
  """Flush in-memory metric counters to disk every `interval` seconds."""
@@ -70,6 +79,8 @@ async def _metrics_flush_loop(interval: int = 30) -> None:
70
  logger.warning("Metrics flush error: %s", exc)
71
 
72
 
 
 
73
  @asynccontextmanager
74
  async def lifespan(app: FastAPI):
75
  from src.services.staff_service import build_staff_index
@@ -83,7 +94,7 @@ async def lifespan(app: FastAPI):
83
  except Exception as exc:
84
  logger.error("Staff index build failed: %s", exc)
85
 
86
- # Initialize RAG
87
  if settings.openai_api_key:
88
  try:
89
  await rag_service.initialize(openai_api_key=settings.openai_api_key)
@@ -108,17 +119,23 @@ async def lifespan(app: FastAPI):
108
  logger.info("LibBee shutting down")
109
 
110
 
 
 
111
  app = FastAPI(
112
  title="LibBee - KU Library AI",
113
  description="Khalifa University Library AI Assistant",
114
- version="3.1.0",
115
  lifespan=lifespan,
116
  )
117
 
 
 
 
118
  app.add_middleware(
119
  CORSMiddleware,
120
  allow_origins=[
121
  "https://nikeshn.github.io",
 
122
  "http://localhost:8080",
123
  "http://localhost:3000",
124
  "http://127.0.0.1:5500",
@@ -130,24 +147,38 @@ app.add_middleware(
130
  )
131
 
132
 
 
 
133
  @app.middleware("http")
134
  async def operational_guardrails(request: Request, call_next):
135
  settings = get_settings()
136
 
137
- # Maintenance mode
138
- if settings.maintenance_mode and request.url.path not in {"/", "/config"}:
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  return JSONResponse(
140
  status_code=503,
141
  content={"detail": "LibBee is in maintenance mode. Please try again shortly."},
142
  )
143
 
144
- # Rate limiting
145
  client_ip = request.client.host if request.client else "unknown"
146
  now = time.time()
147
 
148
  async with _request_log_lock:
149
  if _ttl_cache_available:
150
- # TTLCache stores a list of timestamps per IP
151
  timestamps = _request_log.get(client_ip, [])
152
  timestamps = [t for t in timestamps if now - t < 60]
153
  if len(timestamps) >= settings.rate_limit_per_minute:
@@ -159,7 +190,6 @@ async def operational_guardrails(request: Request, call_next):
159
  timestamps.append(now)
160
  _request_log[client_ip] = timestamps
161
  else:
162
- # Fallback: deque-based sliding window
163
  from collections import deque
164
  bucket = _request_log[client_ip] # type: ignore[index]
165
  while bucket and now - bucket[0] > 60:
@@ -172,6 +202,7 @@ async def operational_guardrails(request: Request, call_next):
172
  )
173
  bucket.append(now)
174
 
 
175
  try:
176
  response = await call_next(request)
177
  return response
@@ -181,20 +212,24 @@ async def operational_guardrails(request: Request, call_next):
181
  return JSONResponse(status_code=500, content={"detail": "Unexpected server error."})
182
 
183
 
 
 
184
  from src.api import admin, agent, feedback, search # noqa: E402
185
 
186
- app.include_router(agent.router, prefix="/agent", tags=["Agent"])
187
- app.include_router(search.router, prefix="/search", tags=["Search"])
188
- app.include_router(admin.router, prefix="/admin", tags=["Admin"])
189
  app.include_router(feedback.router, prefix="/feedback", tags=["Feedback"])
190
 
191
 
 
 
192
  @app.get("/")
193
  def health_check():
194
  settings = get_settings()
195
  return {
196
  "status": "ok",
197
- "version": "3.1.0",
198
  "service": "LibBee KU Library AI",
199
  "rag_ready": rag_service.is_ready(),
200
  "maintenance_mode": settings.maintenance_mode,
@@ -204,6 +239,7 @@ def health_check():
204
 
205
  @app.get("/config")
206
  def public_config():
 
207
  settings = get_settings()
208
  return {
209
  "welcome_message": "Hi! I'm LibBee, the Khalifa University Library AI Assistant.",
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ app.py β€” LibBee v3.5
3
+ FastAPI entry point: CORS, middleware, lifespan, router mounting.
4
+
5
+ Changes over v3.1:
6
+ - Maintenance mode middleware now reads from JsonRuntimeStore (admin-editable)
7
+ instead of settings.maintenance_mode (env var, static at startup).
8
+ - CORS origins updated: added ku-library.github.io.
9
+ - Section headers added throughout.
10
+ """
11
+
12
+ # ── Imports ────────────────────────────────────────────────────────────────────
13
+
14
  import asyncio
15
  import logging
16
  import time
 
34
  logging.basicConfig(level=logging.INFO)
35
  logger = logging.getLogger(__name__)
36
 
37
+
38
+ # ── Service Instances ──────────────────────────────────────────────────────────
39
+
40
+ rag_service = RAGService()
41
+ cache_service = CacheService()
42
  metrics_service = MetricsService(get_settings().metrics_path)
43
 
44
+
45
+ # ── Rate-limit Request Log ─────────────────────────────────────────────────────
46
+ # TTLCache auto-evicts entries older than ttl seconds.
47
+ # Falls back to a plain deque-based dict if cachetools is not installed.
48
+
49
  if _ttl_cache_available:
50
+ _request_log = TTLCache(maxsize=10_000, ttl=120)
51
  _request_log_lock = asyncio.Lock()
52
  else:
53
  from collections import defaultdict, deque
54
+ _request_log = defaultdict(deque) # type: ignore[assignment]
55
  _request_log_lock = asyncio.Lock()
56
 
57
 
58
+ # ── Public Accessors ───────────────────────────────────────────────────────────
59
+ # Routers import these instead of importing module-level globals directly,
60
+ # which avoids circular-import issues at import time.
61
 
62
  def get_rag_service() -> RAGService:
63
  return rag_service
 
67
  return metrics_service
68
 
69
 
70
+ # ── Background Tasks ───────────────────────────────────────────────────────────
71
 
72
  async def _metrics_flush_loop(interval: int = 30) -> None:
73
  """Flush in-memory metric counters to disk every `interval` seconds."""
 
79
  logger.warning("Metrics flush error: %s", exc)
80
 
81
 
82
+ # ── Lifespan ───────────────────────────────────────────────────────────────────
83
+
84
  @asynccontextmanager
85
  async def lifespan(app: FastAPI):
86
  from src.services.staff_service import build_staff_index
 
94
  except Exception as exc:
95
  logger.error("Staff index build failed: %s", exc)
96
 
97
+ # Initialise RAG service
98
  if settings.openai_api_key:
99
  try:
100
  await rag_service.initialize(openai_api_key=settings.openai_api_key)
 
119
  logger.info("LibBee shutting down")
120
 
121
 
122
+ # ── FastAPI App ──────────────────────────────────────────────────────��─────────
123
+
124
  app = FastAPI(
125
  title="LibBee - KU Library AI",
126
  description="Khalifa University Library AI Assistant",
127
+ version="3.5.0",
128
  lifespan=lifespan,
129
  )
130
 
131
+
132
+ # ── CORS Middleware ────────────────────────────────────────────────────────────
133
+
134
  app.add_middleware(
135
  CORSMiddleware,
136
  allow_origins=[
137
  "https://nikeshn.github.io",
138
+ "https://ku-library.github.io", # added v3.5
139
  "http://localhost:8080",
140
  "http://localhost:3000",
141
  "http://127.0.0.1:5500",
 
147
  )
148
 
149
 
150
+ # ── Operational Guardrails Middleware ──────────────────────────────────────────
151
+
152
  @app.middleware("http")
153
  async def operational_guardrails(request: Request, call_next):
154
  settings = get_settings()
155
 
156
+ # ── Maintenance mode ───────────────────────────────────────────────────────
157
+ # FIX v3.5: read from JsonRuntimeStore (admin-editable) rather than
158
+ # settings.maintenance_mode (env var, static at startup). This means
159
+ # toggling maintenance via the admin dashboard actually takes effect.
160
+ _maintenance_on = False
161
+ try:
162
+ from src.services.runtime_store import JsonRuntimeStore
163
+ _store = JsonRuntimeStore(settings.config_path, default={"maintenance_mode": False})
164
+ _maintenance_on = bool(_store.load().get("maintenance_mode", False))
165
+ except Exception:
166
+ # If store is unreadable fall back to env-var setting
167
+ _maintenance_on = settings.maintenance_mode
168
+
169
+ _exempt_paths = {"/", "/config", "/admin", "/admin/login", "/admin/auth"}
170
+ if _maintenance_on and request.url.path not in _exempt_paths:
171
  return JSONResponse(
172
  status_code=503,
173
  content={"detail": "LibBee is in maintenance mode. Please try again shortly."},
174
  )
175
 
176
+ # ── Rate limiting ──────────────────────────────────────────────────────────
177
  client_ip = request.client.host if request.client else "unknown"
178
  now = time.time()
179
 
180
  async with _request_log_lock:
181
  if _ttl_cache_available:
 
182
  timestamps = _request_log.get(client_ip, [])
183
  timestamps = [t for t in timestamps if now - t < 60]
184
  if len(timestamps) >= settings.rate_limit_per_minute:
 
190
  timestamps.append(now)
191
  _request_log[client_ip] = timestamps
192
  else:
 
193
  from collections import deque
194
  bucket = _request_log[client_ip] # type: ignore[index]
195
  while bucket and now - bucket[0] > 60:
 
202
  )
203
  bucket.append(now)
204
 
205
+ # ── Request execution ──────────────────────────────────────────────────────
206
  try:
207
  response = await call_next(request)
208
  return response
 
212
  return JSONResponse(status_code=500, content={"detail": "Unexpected server error."})
213
 
214
 
215
+ # ── Router Mounting ────────────────────────────────────────────────────────────
216
+
217
  from src.api import admin, agent, feedback, search # noqa: E402
218
 
219
+ app.include_router(agent.router, prefix="/agent", tags=["Agent"])
220
+ app.include_router(search.router, prefix="/search", tags=["Search"])
221
+ app.include_router(admin.router, prefix="/admin", tags=["Admin"])
222
  app.include_router(feedback.router, prefix="/feedback", tags=["Feedback"])
223
 
224
 
225
+ # ── Health & Public Endpoints ──────────────────────────────────────────────────
226
+
227
  @app.get("/")
228
  def health_check():
229
  settings = get_settings()
230
  return {
231
  "status": "ok",
232
+ "version": "3.5.0",
233
  "service": "LibBee KU Library AI",
234
  "rag_ready": rag_service.is_ready(),
235
  "maintenance_mode": settings.maintenance_mode,
 
239
 
240
  @app.get("/config")
241
  def public_config():
242
+ """Legacy public config endpoint β€” frontend uses /admin/public-config instead."""
243
  settings = get_settings()
244
  return {
245
  "welcome_message": "Hi! I'm LibBee, the Khalifa University Library AI Assistant.",