-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
151 lines (121 loc) · 5.36 KB
/
main.py
File metadata and controls
151 lines (121 loc) · 5.36 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
import logging
from contextlib import asynccontextmanager
from importlib.metadata import version, PackageNotFoundError
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.responses import JSONResponse
from prometheus_fastapi_instrumentator import Instrumentator
from api.auth import verify_auth
from api.errors import AppError, ErrorCode
from api.middleware import RequestIdMiddleware, RequestIdFilter
from api.routes.analyze import router as analyze_router
from api.routes.debug import router as debug_router
from api.routes.documents import router as docs_router
from api.routes.health import router as health_router
from api.routes.images import router as images_router
from api.routes.knowledgebase import router as kb_router
from api.routes.query import router as query_router
from config import get_settings
# Importing the metrics module registers every metric in the default
# collector at import time — keep this import even if unused here so the
# /metrics scrape reports them from process start, not after the first
# observation.
import infra.metrics # noqa: F401
settings = get_settings()
try:
_VERSION = version("vectoria")
except PackageNotFoundError:
_VERSION = "0.1.0"
def _log_parser_config() -> None:
"""One-line summary of external parser routing on startup.
Cross-region misconfigurations (overseas worker pointing at a CN
mineru endpoint, etc.) cause silent quality degradation that's
hard to debug from alerts alone. Logging the resolved endpoints
once at boot makes it grep-able from ``docker logs`` immediately.
No secrets are emitted — only the URL host:port and model name.
"""
log = logging.getLogger("vectoria.startup")
cfg = get_settings()
mineru = cfg.mineru_api_url or "(unset → falls back to pdfium)"
vision = cfg.vision_base_url or "(unset → image uploads use ocr-native)"
budget = cfg.vision_daily_budget_usd
log.info(
"parser config: mineru=%s | vision=%s model=%s | "
"vision_budget=$%s/day (per-call ~$%s)",
mineru, vision, cfg.vision_model,
budget if budget else "uncapped", cfg.vision_cost_per_call_usd,
)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage shared resources: startup → yield → shutdown."""
_log_parser_config()
yield
# Shutdown: close vectorstore connection pool
from vectorstore.pgvector import close_pool
await close_pool()
# --- Structured logging ---
_log_format = "%(asctime)s %(levelname)s [%(request_id)s] %(name)s: %(message)s"
logging.basicConfig(level=logging.INFO, format=_log_format)
logging.getLogger().handlers[0].addFilter(RequestIdFilter())
app = FastAPI(title="Vectoria", version=_VERSION, root_path="/vectoria", lifespan=lifespan)
# --- Middleware ---
app.add_middleware(RequestIdMiddleware)
# --- Prometheus metrics ---
# Exposes GET /metrics (served at /vectoria/metrics behind the reverse proxy
# because of root_path). The endpoint is not wrapped by verify_auth since it
# sits at app-level, not inside an auth-gated router — scrape from the cluster
# network and restrict access with a NetworkPolicy in prod.
Instrumentator(
should_group_status_codes=False,
should_ignore_untemplated=True,
excluded_handlers=["/metrics", "/health.*"],
).instrument(app).expose(app, endpoint="/metrics", include_in_schema=False)
# --- CORS ---
if settings.cors_origins:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Public routes (no auth, no version prefix) ---
app.include_router(health_router)
# --- Protected routes (require JWT or API key when configured) ---
_auth = [Depends(verify_auth)]
app.include_router(analyze_router, prefix="/v1", dependencies=_auth)
app.include_router(kb_router, prefix="/v1", dependencies=_auth)
app.include_router(docs_router, prefix="/v1", dependencies=_auth)
app.include_router(query_router, prefix="/v1", dependencies=_auth)
app.include_router(images_router, prefix="/v1", dependencies=_auth)
app.include_router(debug_router, prefix="/v1", dependencies=_auth)
logger = logging.getLogger(__name__)
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.code, "detail": exc.detail},
)
@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={"code": ErrorCode.VALIDATION_ERROR, "detail": str(exc)},
)
@app.exception_handler(HTTPException)
async def http_error_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.status_code, "detail": exc.detail},
)
@app.exception_handler(Exception)
async def unhandled_error_handler(request: Request, exc: Exception):
logger.exception("Unhandled error")
return JSONResponse(
status_code=500,
content={"code": ErrorCode.INTERNAL_ERROR, "detail": "Internal server error"},
)
@app.get("/")
async def root():
return {"service": "vectoria", "version": _VERSION}