File size: 4,705 Bytes
e7d7c61
 
 
c24bfe8
 
 
 
e7d7c61
99fa656
 
e7d7c61
99fa656
 
 
e7d7c61
99fa656
e7d7c61
c24bfe8
 
 
 
570f7bd
e7d7c61
 
570f7bd
 
c24bfe8
 
570f7bd
c24bfe8
 
 
 
 
 
e7d7c61
 
 
 
 
 
 
 
c24bfe8
e7d7c61
 
 
 
c1bc4eb
99fa656
e7d7c61
 
 
 
 
c24bfe8
 
 
e7d7c61
c24bfe8
 
 
e7d7c61
c24bfe8
e7d7c61
 
 
c24bfe8
 
 
99fa656
e7d7c61
 
 
 
99fa656
e7d7c61
 
 
 
 
 
c24bfe8
 
 
 
e7d7c61
 
 
4c2cf14
 
 
c24bfe8
 
 
e7d7c61
 
 
570f7bd
c1bc4eb
99fa656
570f7bd
 
 
c1bc4eb
99fa656
570f7bd
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
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

try:
    from dotenv import load_dotenv

    load_dotenv()
except Exception:
    pass

from app.routers import nl2sql

# ----------------------------------------------------------------------------
#  App definition
# ----------------------------------------------------------------------------
application = FastAPI(
    title="NL2SQL Copilot Prototype",
    version=os.getenv("APP_VERSION", "0.1.0"),
    description="Convert natural language to safe & verified SQL",
)

# 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:
    mode = os.getenv("DB_MODE", "sqlite").lower()
    try:
        if mode == "postgres":
            from adapters.db.postgres_adapter import PostgresAdapter

            pg = PostgresAdapter(os.environ["POSTGRES_DSN"])
            ping_fn = getattr(pg, "ping", None)
            if callable(ping_fn):
                ping_fn()
        else:
            from adapters.db.sqlite_adapter import SQLiteAdapter

            sq = SQLiteAdapter(
                os.getenv("DEFAULT_SQLITE_PATH", "data/Chinook_Sqlite.sqlite")
            )
            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():
    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"]