-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_server.py
More file actions
172 lines (145 loc) · 5.47 KB
/
Copy pathapi_server.py
File metadata and controls
172 lines (145 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""
FastAPI server for the Multilingual Scientific RAG system.
Production-ready REST API with authentication, validation, and monitoring.
Route handlers live in routes/*.py; shared auth/state in deps.py.
"""
from contextlib import asynccontextmanager
import asyncio
import logging
import os
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
import config
from deps import (
STATIC_DIR,
limiter,
verify_api_key,
)
from routes import query, chat, ingest, agent, management, feedback, watch, report, models as models_route
from middleware import RequestIdFilter, RequestIdMiddleware
# Configure logging
logging.basicConfig(
level=getattr(logging, config.LOG_LEVEL),
format='%(asctime)s - %(name)s - [%(request_id)s] - %(levelname)s - %(message)s'
)
logging.getLogger().addFilter(RequestIdFilter())
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app):
import embeddings
import vector_store
embeddings.load_embedding_model()
vector_store.get_or_create_collection()
if config.USE_RERANKER:
import rerank
rerank._load()
if config.USE_HYBRID_SEARCH:
import bm25_search
bm25_search.get_or_build_index()
# Phase 6 Increment 4: background schedule loop (single-worker, in-process).
watch_task = None
if config.WATCH_ENABLE:
import watch_runner
watch_task = asyncio.create_task(watch_runner.watch_loop())
def _log_watch_task_result(task: asyncio.Task) -> None:
# Surface a crashed schedule loop immediately instead of waiting for
# GC to log "Task exception was never retrieved".
if not task.cancelled() and task.exception() is not None:
logger.error("[Watch] schedule loop died unexpectedly",
exc_info=task.exception())
watch_task.add_done_callback(_log_watch_task_result)
yield
if watch_task:
watch_task.cancel()
try:
await watch_task
except asyncio.CancelledError:
pass
logger.info("Shutting down: draining in-flight requests complete.")
# Initialize FastAPI app
app = FastAPI(
title="Multilingual Scientific RAG API",
description="Retrieval-Augmented Generation system for multilingual scientific Q&A",
version=config.VERSION,
docs_url="/api/docs",
redoc_url="/api/redoc",
lifespan=lifespan,
)
# Rate limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Prometheus Monitoring
#
# prometheus_fastapi_instrumentator==7.1.0 walks app.routes expecting flat
# Route/Mount objects. fastapi>=0.130's app.include_router() wraps included
# sub-routers in an internal lazy _IncludedRouter node (no .path attribute),
# which crashes the instrumentator's route-name walker on every request once
# routes are split across routers. Degrade gracefully instead of 500ing:
# fall back to the raw URL path (unmatched-route grouping) when route-name
# resolution can't handle the wrapper node.
import prometheus_fastapi_instrumentator.routing as _pi_routing # noqa: E402 — must patch before Instrumentator import
_original_get_route_name = _pi_routing.get_route_name
def _safe_get_route_name(request):
try:
return _original_get_route_name(request)
except AttributeError:
return None
_pi_routing.get_route_name = _safe_get_route_name
from prometheus_fastapi_instrumentator import Instrumentator # noqa: E402 — after routing monkeypatch above
# /metrics requires the same X-API-Key as the rest of the API (no-op when
# API_KEYS is unset) — route latency/count data shouldn't be public.
Instrumentator().instrument(app).expose(
app,
include_in_schema=False,
should_gzip=True,
dependencies=[Depends(verify_api_key)],
)
# Mount static files directory
if STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# Phase 3: serve figure/table crops so the UI can render cited figures.
# StaticFiles is path-traversal safe; read-only. Only mounted when the dir exists.
if config.FIGURES_DIR.exists():
app.mount("/figures", StaticFiles(directory=str(config.FIGURES_DIR)), name="figures")
# CORS configuration — env-driven for deployment flexibility
_cors_origins_env = os.getenv("CORS_ORIGINS")
_cors_origins = (
[o.strip() for o in _cors_origins_env.split(",") if o.strip()]
if _cors_origins_env
else [
"http://localhost:8080",
"http://localhost:8000",
"http://127.0.0.1:8080",
"http://127.0.0.1:8000",
]
)
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "DELETE", "PATCH", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "X-API-Key"],
)
app.add_middleware(RequestIdMiddleware)
# Mount routers
app.include_router(query.router)
app.include_router(chat.router)
app.include_router(ingest.router)
app.include_router(agent.router)
app.include_router(management.router)
app.include_router(feedback.router)
app.include_router(watch.router)
app.include_router(report.router)
app.include_router(models_route.router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"api_server:app",
host="0.0.0.0",
port=8000,
reload=False,
log_level=config.LOG_LEVEL.lower()
)