File size: 9,966 Bytes
2c5c978
 
 
 
 
 
 
 
 
 
ce7ed21
 
2c5c978
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce7ed21
2c5c978
 
 
 
 
 
 
 
 
f6ae268
5d574b8
 
 
f6ae268
 
 
 
36d8598
f6ae268
 
 
2dd3e7a
 
2c5c978
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce7ed21
2c5c978
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce7ed21
 
 
 
 
 
 
 
 
2c5c978
 
 
ce7ed21
2c5c978
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d574b8
 
 
 
 
 
 
 
 
 
 
 
 
2c5c978
 
ce7ed21
2c5c978
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6ae268
 
 
 
 
 
 
 
 
2c5c978
 
 
 
 
 
 
 
 
 
 
 
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
"""
FastAPI backend -- expose pipeline (Schema Linker -> Model sinh SQL -> SQL
Validator -> Executor -> Auto Visualization) qua REST API, de frontend rieng
(Stitch hoac bat ky frontend nao khac) goi qua fetch/AJAX. Tai su dung dung
cac module da viet cho ban Streamlit (schema_builder/inference/sql_validator/
executor/visualizer) -- 2 frontend (Streamlit + Stitch) dung chung 1 backend
logic, khong code lap lai.

Ho tro 2 nguon du lieu, chuyen doi qua _active["mode"] (state toan cuc don
gian -- demo 1-nguoi-dung, KHONG can session/multi-user thuc su):
  - "fixed"    : database Spider co dinh (retail_store -- 7-table relational DB)
  - "uploaded" : file nguoi dung upload (CSV/SQLite/Excel -- nhieu bang duoc ho tro)

mode chi quyet dinh SCHEMA STRING duoc build nhu the nao (Spider-style co
PK/FK vs WikiSQL-style khong PK/FK) -- KHONG con ep checkpoint nao phai dung
theo mode. Nguoi dung TU CHON model_id (1 trong 12 checkpoint cua
inference.MODEL_CATALOG, span 4 stage x 3 architecture) qua /api/model, doc
lap voi mode -- da verify checkpoint Stage 2/3 van sinh dung SQL tren CA HAI
dang schema (xem inference.py).

Chay:  ./venv/Scripts/python.exe -m uvicorn src.app.api:app --reload --port 8000
Docs interactif (Swagger UI, tham khao luc thiet ke Stitch prompt): http://localhost:8000/docs
"""

import json
import sys
import time
from pathlib import Path
from typing import Any, List, Optional

from fastapi import FastAPI, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel

_STATIC_DIR = Path(__file__).resolve().parent / "giao_dien" / "text_to_sql_dashboard"

sys.path.insert(0, str(Path(__file__).resolve().parent))
import csv_loader  # noqa: E402
import executor  # noqa: E402
import inference  # noqa: E402
import schema_builder  # noqa: E402
import sql_validator  # noqa: E402
import visualizer  # noqa: E402

DEFAULT_DB_ID = "retail_store"
DEFAULT_DB_PATH = str(
    Path(__file__).resolve().parents[2]
    / "spider_data"
    / "database"
    / DEFAULT_DB_ID
    / f"{DEFAULT_DB_ID}.sqlite"
)

EXAMPLE_QUESTIONS = [
    # Simple — single metric
    "How many orders are there in total?",
    "How many customers are from hanoi?",
    "What is the average product price?",
    # Simple — bar chart (GROUP BY, single table)
    "How many orders are there for each status?",
    "How many customers live in each city?",
    # Medium — bar chart (no JOIN)
    "Show each region and the number of stores in that region.",
    # JOIN — bar chart
    "Show each category name and the number of products in it.",
    "For each store, show the store name and the number of orders.",
    "Show each category name and the total cost of all products in it.",
    "Show the 5 customers with the most orders. List customer name and order count.",
]

def _default_active():
    return {
        "mode": "fixed",
        "db_id": DEFAULT_DB_ID,
        "db_path": DEFAULT_DB_PATH,
        "schema_string": schema_builder.get_schema_string(DEFAULT_DB_ID),
        "model_id": inference.DEFAULT_MODEL_ID_FOR_MODE["fixed"],
    }


_active = _default_active()

app = FastAPI(
    title="Text-to-SQL Demo API",
    description="Question (English) -> SQL -> SQLite result, backed by fine-tuned T5/CodeT5/BART checkpoints.",
)

# allow_origins=["*"] -- demo noi bo/luan van, khong co auth/du lieu nhay cam.
# Sieta lai thanh domain cu the neu deploy public lau dai.
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)


class QueryRequest(BaseModel):
    question: str


class QueryResponse(BaseModel):
    question: str
    sql: str
    gen_time: float
    exec_time: float = 0.0
    is_safe: bool
    validator_message: str
    exec_error: Optional[str] = None
    columns: List[str] = []
    rows: List[List[Any]] = []
    chart_type: str = "table"


class UploadResponse(BaseModel):
    db_id: str
    schema_text: str
    columns: List[str]
    row_count: int
    rows: List[List[Any]]
    truncated: bool
    model_id: str


class ModelRequest(BaseModel):
    model_id: str


@app.get("/")
def serve_frontend():
    return FileResponse(str(_STATIC_DIR / "code.html"), media_type="text/html")


@app.get("/usth_logo.png")
def serve_logo():
    return FileResponse(str(_STATIC_DIR / "usth_logo.png"), media_type="image/png")


@app.get("/api/health")
def health():
    return {"status": "ok"}


@app.get("/api/meta")
def meta():
    """Thong tin nguon du lieu dang active (schema, model, vi du cau hoi) --
    frontend goi lai sau moi /api/upload, /api/reset, hoac /api/model de cap
    nhat UI."""
    cfg = inference.get_checkpoint_config(_active["model_id"])
    return {
        "mode": _active["mode"],
        "db_id": _active["db_id"],
        "schema": _active["schema_string"],
        "model_id": _active["model_id"],
        "model": cfg["label"],
        # Vi du cau hoi chi hop ly cho database co dinh (retail_store) --
        # frontend nen an phan nay khi mode="uploaded".
        "examples": EXAMPLE_QUESTIONS if _active["mode"] == "fixed" else [],
    }


@app.get("/api/models")
def models():
    """Toan bo catalog 12 checkpoint (4 stage x 3 architecture) de frontend
    render dropdown -- nguoi dung tu chon model_id bat ky, khong gioi han theo
    mode dang active."""
    options = [
        {
            "model_id": model_id,
            "label": cfg["label"],
            "architecture": cfg["architecture"],
            "stage": cfg["stage"],
        }
        for model_id, cfg in inference.MODEL_CATALOG.items()
    ]
    options.sort(key=lambda o: (o["stage"] if isinstance(o["stage"], int) else 99, o["architecture"]))
    return {"current_model_id": _active["model_id"], "options": options}


@app.post("/api/model")
def set_model(req: ModelRequest):
    """Doi model_id (1 trong 12 checkpoint cua MODEL_CATALOG). Mode (fixed/
    uploaded) KHONG doi qua endpoint nay."""
    if req.model_id not in inference.MODEL_CATALOG:
        return {"status": "error", "message": f"Unknown model_id '{req.model_id}'"}
    _active["model_id"] = req.model_id
    return {"status": "ok", "model_id": _active["model_id"]}


@app.post("/api/upload", response_model=UploadResponse)
def upload(file: UploadFile):
    filename = file.filename or "uploaded"
    ext = Path(filename).suffix.lower()

    if ext in (".sqlite", ".db"):
        result = csv_loader.load_sqlite_file(file.file)
    elif ext in (".xlsx", ".xls"):
        result = csv_loader.load_excel_to_sqlite(file.file)
    else:
        result = csv_loader.load_csv_to_sqlite(file.file)

    _active.update(
        mode="uploaded",
        db_id=filename,
        db_path=result["db_path"],
        schema_string=result["schema_string"],
    )

    return UploadResponse(
        db_id=_active["db_id"],
        schema_text=result["schema_string"],
        columns=result["columns"],
        row_count=result["row_count"],
        rows=result["rows"],
        truncated=result["truncated"],
        model_id=_active["model_id"],
    )


@app.get("/api/preview")
def preview_table(table: str = ""):
    """Return rows from a specific table (or first available) for the active database."""
    db_path = _active["db_path"]
    table_names = csv_loader.get_table_names(db_path)
    if not table_names:
        return {"table_names": [], "active_table": "", "columns": [], "rows": [], "row_count": 0, "truncated": False}
    if table not in table_names:
        table = table_names[0]
    preview = csv_loader.get_preview_for_table(db_path, table)
    return {"table_names": table_names, "active_table": table, **preview}


@app.post("/api/reset")
def reset():
    """Quay lai database mac dinh (retail_store). Giu nguyen model_id nguoi
    dung dang chon (vd van la 1 checkpoint BIRD Stage3 sau reset)."""
    model_id = _active["model_id"]
    _active.clear()
    _active.update(_default_active())
    _active["model_id"] = model_id
    return {"status": "ok", "db_id": _active["db_id"]}


@app.post("/api/query", response_model=QueryResponse)
def query(req: QueryRequest):
    question = req.question.strip()
    model_id = _active["model_id"]

    if _active["mode"] == "fixed":
        model_input = schema_builder.build_input(question, _active["db_id"])
    else:
        model_input = csv_loader.build_input(question, _active["schema_string"])

    t0 = time.time()
    sql = inference.generate_sql(model_input, model_id=model_id)
    gen_time = time.time() - t0

    is_safe, msg = sql_validator.validate_sql(sql)
    resp = QueryResponse(
        question=question, sql=sql, gen_time=gen_time,
        is_safe=is_safe, validator_message=msg,
    )
    if not is_safe:
        return resp

    t_exec = time.time()
    try:
        df = executor.run_query(_active["db_path"], sql)
    except Exception as e:
        resp.exec_time = time.time() - t_exec
        resp.exec_error = str(e)
        return resp
    resp.exec_time = time.time() - t_exec

    chart_type, _chart_df = visualizer.make_chart_data(df)
    # Khi chart la bar/line, frontend dung r[0] lam label truc X.
    # Neu model sinh aggregate truoc (vd SELECT COUNT(*), city), reorder de
    # cot text (label) luon o vi tri 0 truoc khi serialize.
    if chart_type in ("bar", "line"):
        text_cols = df.select_dtypes(exclude="number").columns.tolist()
        if text_cols and df.columns.tolist()[0] != text_cols[0]:
            label = text_cols[0]
            rest = [c for c in df.columns if c != label]
            df = df[[label] + rest]
    # df.to_json() xu ly dung numpy int64/float64/NaN -> JSON-safe, khac voi
    # .values.tolist() truc tiep (co the tra ve kieu numpy khong serialize duoc).
    resp.columns = [str(c) for c in df.columns]
    resp.rows = json.loads(df.to_json(orient="values"))
    resp.chart_type = chart_type
    return resp


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)