File size: 5,317 Bytes
e7d7c61
 
82e122c
e7d7c61
c24bfe8
 
82e122c
c24bfe8
82e122c
 
 
e7d7c61
99fa656
 
e7d7c61
99fa656
 
82e122c
99fa656
e7d7c61
82e122c
 
e7d7c61
c24bfe8
 
 
 
570f7bd
82e122c
e7d7c61
570f7bd
82e122c
570f7bd
c24bfe8
 
570f7bd
c24bfe8
 
 
82e122c
c24bfe8
 
 
e7d7c61
 
 
 
 
 
 
 
c24bfe8
e7d7c61
 
 
 
c1bc4eb
99fa656
e7d7c61
 
 
 
 
c24bfe8
 
 
e7d7c61
c24bfe8
 
 
e7d7c61
c24bfe8
e7d7c61
 
 
c24bfe8
 
 
99fa656
e7d7c61
 
 
 
99fa656
e7d7c61
82e122c
 
 
 
 
 
 
e7d7c61
 
 
 
82e122c
 
 
 
 
c24bfe8
 
 
e7d7c61
 
 
82e122c
 
c24bfe8
 
 
e7d7c61
 
 
570f7bd
c1bc4eb
99fa656
570f7bd
 
 
c1bc4eb
99fa656
570f7bd
82e122c
c1bc4eb
e7d7c61
 
99fa656
e7d7c61
 
 
ccefd8e
 
c24bfe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccefd8e
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
import os
import time

from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.responses import PlainTextResponse, RedirectResponse
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST

from nl2sql.prom import REGISTRY
from app.routers import dev, nl2sql
from app.settings import get_settings
from app.exception_handlers import register_exception_handlers

try:
    from dotenv import load_dotenv

    load_dotenv()
except Exception:
    # Best-effort .env loading; app must not crash if dotenv is missing.
    pass


settings = get_settings()

# ----------------------------------------------------------------------------
#  App definition
# ----------------------------------------------------------------------------
application = FastAPI(
    title="NL2SQL Copilot Prototype",
    version=settings.app_version,
    description="Convert natural language to safe & verified SQL",
)
register_exception_handlers(application)

# Register only versioned API
application.include_router(nl2sql.router, prefix="/api/v1")

# Register Dev-only routes (only when APP_ENV=dev)
if os.getenv("APP_ENV", "dev").lower() == "dev":
    application.include_router(dev.router, prefix="/api/v1")

# ----------------------------------------------------------------------------
#  Prometheus Metrics Middleware
# ----------------------------------------------------------------------------
REQUEST_COUNT = Counter(
    "http_requests_total",
    "Total HTTP requests",
    ["path", "method", "status_code"],
    registry=REGISTRY,
)
REQUEST_LATENCY = Histogram(
    "http_request_latency_seconds",
    "Request latency (seconds)",
    ["path", "method"],
    registry=REGISTRY,
)


@application.middleware("http")
async def metrics_middleware(request: Request, call_next):
    start = time.perf_counter()
    response: Response = await call_next(request)
    elapsed = time.perf_counter() - start
    route = request.scope.get("route")
    path = getattr(route, "path", None) or request.url.path
    name = getattr(route, "name", None) or path

    REQUEST_COUNT.labels(
        path=name,
        method=request.method,
        status_code=str(getattr(response, "status_code", 500)),
    ).inc()
    REQUEST_LATENCY.labels(path=name, method=request.method).observe(elapsed)
    return response


# ----------------------------------------------------------------------------
#  System Endpoints
# ----------------------------------------------------------------------------
@application.get("/healthz", response_class=PlainTextResponse, tags=["system"])
def healthz() -> str:
    return "ok"


@application.get("/readyz", response_class=PlainTextResponse, tags=["system"])
def readyz() -> str:
    """
    Lightweight readiness probe:

    - For postgres mode → ping PostgresAdapter using configured DSN.
    - For sqlite mode   → ping SQLiteAdapter using configured default path.
    """
    mode = settings.db_mode.lower()
    try:
        if mode == "postgres":
            from adapters.db.postgres_adapter import PostgresAdapter

            dsn = (settings.postgres_dsn or "").strip()
            if not dsn:
                raise RuntimeError("POSTGRES_DSN is not configured for readiness check")

            pg = PostgresAdapter(dsn)
            ping_fn = getattr(pg, "ping", None)
            if callable(ping_fn):
                ping_fn()
        else:
            from adapters.db.sqlite_adapter import SQLiteAdapter

            db_path = settings.default_sqlite_path or "data/Chinook_Sqlite.sqlite"
            sq = SQLiteAdapter(db_path)
            ping_fn = getattr(sq, "ping", None)
            if callable(ping_fn):
                ping_fn()
        return "ready"
    except Exception:
        raise HTTPException(status_code=503, detail="not ready")


@application.get("/")
def root():
    return {"status": "ok", "message": "NL2SQL Copilot API is running"}


@application.get("/health")
def health():
    # This is a higher-level health stub; real checks can be wired later
    return {"status": "ok", "db": "connected", "llm": "reachable", "uptime_sec": 123.4}


@application.get("/metrics", tags=["system"])
def metrics():
    data = generate_latest(REGISTRY)
    return Response(content=data, media_type=CONTENT_TYPE_LATEST)


# ----------------------------------------------------------------------------
#  Legacy Redirects (clean compatibility)
# ----------------------------------------------------------------------------
@application.api_route("/nl2sql", methods=["GET", "POST"])
async def legacy_nl2sql_redirect(request: Request):
    return RedirectResponse(url="/api/v1/nl2sql", status_code=307)


@application.api_route(
    "/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
)
async def legacy_catch_all(request: Request, path: str):
    """Redirect old root-level endpoints to versioned API."""
    if path.startswith("api/v1"):
        return RedirectResponse(url=f"/{path}", status_code=307)
    return RedirectResponse(url=f"/api/v1/{path}", status_code=307)


# ----------------------------------------------------------------------------
#  Backward-compatible alias for uvicorn
# ----------------------------------------------------------------------------
app = application
__all__ = ["application", "app"]