Skip to content

Commit 47056da

Browse files
Merge pull request austinLorenzMccoy#1 from austinLorenzMccoy/backend-revamp
Backend: migrate to pydantic-settings, update Groq model, add env ign…
2 parents 940e5ce + f32b487 commit 47056da

27 files changed

+553
-138
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ celerybeat.pid
121121

122122
# Environments
123123
.env
124+
backend/.env
124125
.venv
125126
env/
126127
venv/

app.py

Lines changed: 0 additions & 95 deletions
This file was deleted.

backend/.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copy this file to .env and fill in the values
2+
APP_NAME=Text-to-SQL Backend
3+
SQLITE_PATH=../student.db
4+
5+
# Groq settings
6+
GROQ_API_KEY="your_groq_api_key_here"
7+
GROQ_MODEL="llama-3.3-70b-versatile"

backend/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Text-to-SQL Backend (FastAPI + Groq)
2+
3+
A modular FastAPI backend that converts natural language to SQL for the `STUDENT` table in `student.db` and executes queries safely.
4+
5+
## Project Structure
6+
7+
```
8+
backend/
9+
app/
10+
api/v1/
11+
__init__.py
12+
routes.py # /health, /students, /sql, /nl2sql
13+
core/
14+
config.py # Settings via Pydantic + .env
15+
logging.py # Basic logging setup
16+
models/
17+
schemas.py # Pydantic models
18+
services/
19+
db.py # SQLite access helpers
20+
nl2sql.py # Google Gemini integration
21+
utils/
22+
sql_cleaner.py # Remove markdown, enforce semicolon
23+
__init__.py
24+
main.py # FastAPI factory + include router
25+
tests/
26+
conftest.py # Temp DB per test session
27+
test_health.py
28+
test_students.py
29+
test_sql_cleaner.py
30+
test_run_sql.py
31+
test_nl2sql_endpoint.py
32+
.env.example
33+
api_demo.py
34+
pyproject.toml
35+
setup.py
36+
README.md
37+
```
38+
39+
## Requirements
40+
41+
- Python 3.9+
42+
- Conda (for environment management)
43+
- SQLite database at `TextToSQLapp/student.db` (already in repo)
44+
45+
## Quickstart (Conda + Uvicorn)
46+
47+
```bash
48+
# from repo root
49+
# Option A: create from environment.yml
50+
conda env create -f backend/environment.yml
51+
conda activate text2sql-backend
52+
53+
# Option B: create manually
54+
# conda create -n text2sql-backend python=3.11 -y
55+
# conda activate text2sql-backend
56+
# pip install -e backend[dev]
57+
58+
# prepare env
59+
cp backend/.env.example backend/.env
60+
# then edit backend/.env and set GROQ_API_KEY
61+
62+
# run API
63+
uvicorn app.main:app --reload --port 8000 --app-dir backend
64+
```
65+
66+
Open: http://127.0.0.1:8000/docs
67+
68+
## Endpoints
69+
70+
- GET `/api/v1/health``{ "status": "ok" }`
71+
- GET `/api/v1/students` → list students
72+
- POST `/api/v1/sql` → body `{ "sql": "SELECT ...;" }`
73+
- POST `/api/v1/nl2sql` → body `{ "question": "..." }` returns `{ "sql": "...;" }`
74+
75+
## Curl Demo
76+
77+
```bash
78+
python backend/api_demo.py
79+
```
80+
81+
Or manually:
82+
83+
```bash
84+
curl http://127.0.0.1:8000/api/v1/health
85+
curl http://127.0.0.1:8000/api/v1/students
86+
curl -X POST http://127.0.0.1:8000/api/v1/sql \
87+
-H 'Content-Type: application/json' \
88+
-d '{"sql":"SELECT COUNT(*) FROM STUDENT;"}'
89+
90+
curl -X POST http://127.0.0.1:8000/api/v1/nl2sql \
91+
-H 'Content-Type: application/json' \
92+
-d '{"question":"How many entries of records are present?"}'
93+
```
94+
95+
## Testing & Coverage
96+
97+
```bash
98+
# from backend/
99+
pytest
100+
# or with full output
101+
pytest -q --cov=app --cov-report=term-missing --cov-report=xml:coverage.xml
102+
```
103+
104+
The test suite uses a temporary SQLite database and does not touch your real `student.db`.
105+
106+
## Configuration
107+
108+
Configuration is handled by `app/core/config.py` using Pydantic BaseSettings:
109+
110+
- `APP_NAME` (default: Text-to-SQL Backend)
111+
- `SQLITE_PATH` (default: student.db)
112+
- `GROQ_API_KEY` (required for /nl2sql)
113+
- `GROQ_MODEL` (default: llama-3.1-70b-versatile)
114+
115+
Env file: `backend/.env` (copy from `.env.example`).
116+
117+
## Notes on NL→SQL
118+
119+
- `app/services/nl2sql.py` integrates Groq via its OpenAI-compatible Chat Completions API.
120+
- `app/utils/sql_cleaner.py` strips markdown and enforces trailing semicolon.
121+
- Tests mock the model so CI can run without an API key.
122+
123+
## Developing
124+
125+
- Linting (optional):
126+
```bash
127+
pip install ruff
128+
ruff check backend/app
129+
```
130+
131+
- Packaging:
132+
- `pyproject.toml` defines project metadata, test, and coverage settings.
133+
- `setup.py` provided for compatibility.
134+

backend/api_demo.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simple demo commands to exercise the FastAPI backend.
4+
Run the server first:
5+
6+
uvicorn app.main:app --reload --port 8000
7+
8+
Then run:
9+
10+
python backend/api_demo.py
11+
"""
12+
import subprocess
13+
14+
BASE = "http://127.0.0.1:8000/api/v1"
15+
16+
17+
def run(cmd: list[str]):
18+
print("$", " ".join(cmd))
19+
out = subprocess.run(cmd, capture_output=True, text=True)
20+
print(out.stdout or out.stderr)
21+
22+
23+
if __name__ == "__main__":
24+
run(["curl", f"{BASE}/health"]) # Health
25+
run(["curl", f"{BASE}/students"]) # List students
26+
run(["curl", "-X", "POST", f"{BASE}/sql", "-H", "Content-Type: application/json", "-d", '{"sql":"SELECT COUNT(*) FROM STUDENT;"}'])
27+
run(["curl", "-X", "POST", f"{BASE}/nl2sql", "-H", "Content-Type: application/json", "-d", '{"question":"How many entries of records are present?"}'])

backend/app/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = [
2+
"main",
3+
]

backend/app/api/v1/__init__.py

Whitespace-only changes.

backend/app/api/v1/routes.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from fastapi import APIRouter, HTTPException
2+
from typing import List
3+
4+
from ...models.schemas import NLQuery, SQLQuery, QueryResult, Student
5+
from ...services import db as db_service
6+
from ...services import nl2sql as nl2sql_service
7+
from ...utils.sql_cleaner import clean_sql_query
8+
9+
router = APIRouter()
10+
11+
12+
@router.get("/health")
13+
def health() -> dict:
14+
return {"status": "ok"}
15+
16+
17+
@router.get("/students", response_model=List[Student])
18+
def list_students() -> List[Student]:
19+
try:
20+
rows = db_service.fetch_all("SELECT NAME, CLASS, SECTION, MARKS FROM STUDENT;")
21+
return [Student(name=r[0], class_name=r[1], section=r[2], marks=r[3]) for r in rows]
22+
except Exception as e:
23+
raise HTTPException(status_code=500, detail=str(e))
24+
25+
26+
@router.post("/sql", response_model=QueryResult)
27+
def run_sql(query: SQLQuery) -> QueryResult:
28+
try:
29+
rows = db_service.fetch_all(query.sql)
30+
return QueryResult(rows=rows)
31+
except Exception as e:
32+
raise HTTPException(status_code=400, detail=str(e))
33+
34+
35+
@router.post("/nl2sql", response_model=SQLQuery)
36+
def nl_to_sql(payload: NLQuery) -> SQLQuery:
37+
try:
38+
raw = nl2sql_service.generate_sql(payload.question)
39+
cleaned = clean_sql_query(raw)
40+
return SQLQuery(sql=cleaned)
41+
except Exception as e:
42+
raise HTTPException(status_code=500, detail=str(e))

backend/app/core/__init__.py

Whitespace-only changes.

backend/app/core/config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from functools import lru_cache
2+
from pydantic import Field
3+
from pydantic_settings import BaseSettings, SettingsConfigDict
4+
from dotenv import load_dotenv
5+
6+
# Load .env early
7+
load_dotenv()
8+
9+
10+
class Settings(BaseSettings):
11+
app_name: str = Field("Text-to-SQL Backend", env="APP_NAME")
12+
sqlite_path: str = Field("student.db", env="SQLITE_PATH")
13+
14+
# Groq (primary)
15+
groq_api_key: str | None = Field(default=None, env="GROQ_API_KEY")
16+
groq_model: str = Field("llama-3.1-70b-versatile", env="GROQ_MODEL")
17+
18+
# Deprecated/unused (kept for compatibility)
19+
google_api_key: str | None = Field(default=None, env="GOOGLE_API_KEY")
20+
gemini_model: str = Field("gemini-1.5-flash", env="GEMINI_MODEL")
21+
22+
# Pydantic v2 settings config
23+
model_config = SettingsConfigDict(env_file=".env", case_sensitive=False)
24+
25+
26+
@lru_cache(maxsize=1)
27+
def get_settings() -> Settings:
28+
return Settings()

0 commit comments

Comments
 (0)