Spaces:
Sleeping
Sleeping
initial commit for huggingface
Browse files- .gitignore +168 -0
- backend/.dockerfile +17 -0
- backend/app/api/endpoints.py +82 -0
- backend/app/db/db_connector.py +51 -0
- backend/app/db/schema_reader.py +50 -0
- backend/app/services/query_executor.py +43 -0
- backend/app/services/result_formatter.py +21 -0
- backend/app/services/sql_generator.py +60 -0
- backend/app/services/utility.py +105 -0
- backend/app/services/utility_sql.py +21 -0
- backend/config/settings.py +13 -0
- backend/main.py +26 -0
- backend/requirements.txt +15 -0
.gitignore
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
env/
|
| 12 |
+
build/
|
| 13 |
+
develop-eggs/
|
| 14 |
+
dist/
|
| 15 |
+
downloads/
|
| 16 |
+
eggs/
|
| 17 |
+
.eggs/
|
| 18 |
+
lib/
|
| 19 |
+
lib64/
|
| 20 |
+
parts/
|
| 21 |
+
sdist/
|
| 22 |
+
var/
|
| 23 |
+
*.egg-info/
|
| 24 |
+
.installed.cfg
|
| 25 |
+
*.egg
|
| 26 |
+
|
| 27 |
+
# PyInstaller
|
| 28 |
+
# Usually these files are written by a python script from a template
|
| 29 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 30 |
+
*.manifest
|
| 31 |
+
*.spec
|
| 32 |
+
|
| 33 |
+
# Installer logs
|
| 34 |
+
debug.log
|
| 35 |
+
pip-log.txt
|
| 36 |
+
pip-delete-this-directory.txt
|
| 37 |
+
|
| 38 |
+
# Unit test / coverage reports
|
| 39 |
+
htmlcov/
|
| 40 |
+
.tox/
|
| 41 |
+
.nox/
|
| 42 |
+
.coverage
|
| 43 |
+
.coverage.*
|
| 44 |
+
.cache
|
| 45 |
+
nosetests.xml
|
| 46 |
+
coverage.xml
|
| 47 |
+
*.cover
|
| 48 |
+
.hypothesis/
|
| 49 |
+
.pytest_cache/
|
| 50 |
+
|
| 51 |
+
# Translations
|
| 52 |
+
*.mo
|
| 53 |
+
*.pot
|
| 54 |
+
|
| 55 |
+
# Django stuff:
|
| 56 |
+
*.log
|
| 57 |
+
local_settings.py
|
| 58 |
+
db.sqlite3
|
| 59 |
+
|
| 60 |
+
# Flask stuff:
|
| 61 |
+
instance/
|
| 62 |
+
.webassets-cache
|
| 63 |
+
|
| 64 |
+
# Scrapy stuff:
|
| 65 |
+
.scrapy
|
| 66 |
+
|
| 67 |
+
# Sphinx documentation
|
| 68 |
+
docs/_build/
|
| 69 |
+
|
| 70 |
+
# PyBuilder
|
| 71 |
+
.target/
|
| 72 |
+
|
| 73 |
+
# Jupyter Notebook
|
| 74 |
+
.ipynb_checkpoints
|
| 75 |
+
|
| 76 |
+
# IPython
|
| 77 |
+
profile_default/
|
| 78 |
+
ipython_config.py
|
| 79 |
+
|
| 80 |
+
# pyenv
|
| 81 |
+
.python-version
|
| 82 |
+
|
| 83 |
+
# pipenv
|
| 84 |
+
Pipfile.lock
|
| 85 |
+
|
| 86 |
+
# poetry
|
| 87 |
+
poetry.lock
|
| 88 |
+
|
| 89 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
| 90 |
+
__pypackages__/
|
| 91 |
+
|
| 92 |
+
# Celery stuff
|
| 93 |
+
celerybeat-schedule
|
| 94 |
+
celerybeat.pid
|
| 95 |
+
|
| 96 |
+
# SageMath parsed files
|
| 97 |
+
*.sage.py
|
| 98 |
+
|
| 99 |
+
# Environments
|
| 100 |
+
.env
|
| 101 |
+
.venv
|
| 102 |
+
env/
|
| 103 |
+
venv/
|
| 104 |
+
ENV/
|
| 105 |
+
env.bak/
|
| 106 |
+
venv.bak/
|
| 107 |
+
|
| 108 |
+
# Spyder project settings
|
| 109 |
+
.spyderproject
|
| 110 |
+
.spyderproject.*
|
| 111 |
+
|
| 112 |
+
# Rope project settings
|
| 113 |
+
.ropeproject
|
| 114 |
+
|
| 115 |
+
# mkdocs documentation
|
| 116 |
+
/site
|
| 117 |
+
|
| 118 |
+
# mypy
|
| 119 |
+
.mypy_cache/
|
| 120 |
+
.dmypy.json
|
| 121 |
+
|
| 122 |
+
# Pyre type checker
|
| 123 |
+
.pyre/
|
| 124 |
+
|
| 125 |
+
# pytype static type analyzer
|
| 126 |
+
.pytype/
|
| 127 |
+
|
| 128 |
+
# Cython debug symbols
|
| 129 |
+
cython_debug/
|
| 130 |
+
|
| 131 |
+
# VS Code
|
| 132 |
+
.vscode/
|
| 133 |
+
|
| 134 |
+
# Windows
|
| 135 |
+
Thumbs.db
|
| 136 |
+
Desktop.ini
|
| 137 |
+
|
| 138 |
+
# macOS
|
| 139 |
+
.DS_Store
|
| 140 |
+
.AppleDouble
|
| 141 |
+
.LSOverride
|
| 142 |
+
|
| 143 |
+
# Linux
|
| 144 |
+
*~
|
| 145 |
+
|
| 146 |
+
# Test artifacts
|
| 147 |
+
test/
|
| 148 |
+
artifacts/
|
| 149 |
+
|
| 150 |
+
# Database
|
| 151 |
+
*.db
|
| 152 |
+
*.sqlite
|
| 153 |
+
|
| 154 |
+
# Logs
|
| 155 |
+
*.log
|
| 156 |
+
|
| 157 |
+
# Excel
|
| 158 |
+
*.xlsx
|
| 159 |
+
*.xls
|
| 160 |
+
|
| 161 |
+
# Misc
|
| 162 |
+
*.bak
|
| 163 |
+
*.tmp
|
| 164 |
+
*.swp
|
| 165 |
+
*.swo
|
| 166 |
+
|
| 167 |
+
# Python virtual environment
|
| 168 |
+
backend_env/
|
backend/.dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official Python 3.11.5 image
|
| 2 |
+
FROM python:3.11.5
|
| 3 |
+
|
| 4 |
+
# Set the working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy the backend folder into the container
|
| 8 |
+
COPY backend /app
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Expose the port FastAPI runs on
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
# Run FastAPI app
|
| 17 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
backend/app/api/endpoints.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_openai import ChatOpenAI
|
| 2 |
+
from config.settings import settings
|
| 3 |
+
|
| 4 |
+
# --- LLM-based classifier for user input ---
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
# Use LLMClassifier from services.utility
|
| 8 |
+
from app.services.utility import UtilityClass
|
| 9 |
+
from fastapi import APIRouter
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from app.services.sql_generator import generate_sql_query
|
| 12 |
+
from app.services.query_executor import execute_sql_query
|
| 13 |
+
from app.services.result_formatter import df_to_chart
|
| 14 |
+
from app.services.query_executor import execute_sql_query
|
| 15 |
+
from app.db.schema_reader import get_schema
|
| 16 |
+
from app.services.query_executor import run_and_handle_sql_query
|
| 17 |
+
|
| 18 |
+
router = APIRouter()
|
| 19 |
+
|
| 20 |
+
class QueryRequest(BaseModel):
|
| 21 |
+
question: str
|
| 22 |
+
|
| 23 |
+
from fastapi import HTTPException
|
| 24 |
+
|
| 25 |
+
@router.post("/ask")
|
| 26 |
+
def ask_query(req: QueryRequest):
|
| 27 |
+
try:
|
| 28 |
+
sql = generate_sql_query(req.question)
|
| 29 |
+
|
| 30 |
+
# If generate_sql_query returns a chat_message, treat as normal chat or empty SQL result
|
| 31 |
+
if isinstance(sql, dict) and "chat_message" in sql:
|
| 32 |
+
return {"message": sql["chat_message"]}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# Use utility method to check if the string is a valid SQL query
|
| 36 |
+
formattedSqlQuery = UtilityClass.is_valid_sql_query(sql)
|
| 37 |
+
if not formattedSqlQuery:
|
| 38 |
+
return {"message": str(sql) if sql else "No SQL query could be generated for your question."}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
df = run_and_handle_sql_query(formattedSqlQuery, req.question)
|
| 42 |
+
|
| 43 |
+
# If run_and_handle_sql_query returns a chat_message (for empty SQL results), return it
|
| 44 |
+
if isinstance(df, dict) and "chat_message" in df:
|
| 45 |
+
return {"message": df["chat_message"]}
|
| 46 |
+
|
| 47 |
+
chart = None
|
| 48 |
+
# if len(df) > 0 and isinstance(df, list) and len(df[0]) > 0 and len(df[0].keys()) >= 2:
|
| 49 |
+
# chart = df_to_chart(pd.DataFrame(df))
|
| 50 |
+
|
| 51 |
+
# Prepare heading and records JSON using LLM
|
| 52 |
+
result_json = UtilityClass.prepare_llm_heading_and_records(req.question, df)
|
| 53 |
+
|
| 54 |
+
return {
|
| 55 |
+
"sql": sql,
|
| 56 |
+
"rows": result_json["records"],
|
| 57 |
+
"heading": result_json["heading"],
|
| 58 |
+
"chart": chart
|
| 59 |
+
}
|
| 60 |
+
except Exception as e:
|
| 61 |
+
raise HTTPException(status_code=400, detail=f"An error occurred: {str(e)}")
|
| 62 |
+
|
| 63 |
+
@router.get("/schema")
|
| 64 |
+
def read_schema():
|
| 65 |
+
return get_schema()
|
| 66 |
+
|
| 67 |
+
@router.get("/query")
|
| 68 |
+
def run_query(question: str):
|
| 69 |
+
try:
|
| 70 |
+
sql_query = generate_sql_query(question)
|
| 71 |
+
results = execute_sql_query(sql_query)
|
| 72 |
+
return {
|
| 73 |
+
"question": question,
|
| 74 |
+
"sql_query": sql_query,
|
| 75 |
+
"results": results
|
| 76 |
+
}
|
| 77 |
+
except Exception as e:
|
| 78 |
+
raise HTTPException(status_code=400, detail=f"An error occurred: {str(e)}")
|
| 79 |
+
|
| 80 |
+
@router.get("/")
|
| 81 |
+
def root():
|
| 82 |
+
return {"message": "Welcome to AI SQL Query Generator!"}
|
backend/app/db/db_connector.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.engine import Engine
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
import sqlite3
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
# Path to SQLite DB file, configurable via environment variable or .env
|
| 11 |
+
print("DB_PATH from env:", os.getenv("DB_PATH"))
|
| 12 |
+
DB_PATH = os.getenv("DB_PATH")
|
| 13 |
+
if not DB_PATH:
|
| 14 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
DB_PATH = os.path.join(BASE_DIR, "BankingDb.db")
|
| 16 |
+
|
| 17 |
+
DATABASE_URL = f"sqlite:///{DB_PATH}"
|
| 18 |
+
|
| 19 |
+
def get_connection():
|
| 20 |
+
db_path = DB_PATH
|
| 21 |
+
# Only allow connection if DB file exists
|
| 22 |
+
if not os.path.isfile(db_path):
|
| 23 |
+
raise FileNotFoundError(f"Database file not found at {db_path}. Set DB_PATH env variable or .env to the correct location.")
|
| 24 |
+
try:
|
| 25 |
+
conn = sqlite3.connect(db_path, timeout=30, check_same_thread=False)
|
| 26 |
+
conn.row_factory = sqlite3.Row # access columns by name
|
| 27 |
+
return conn
|
| 28 |
+
except sqlite3.OperationalError as e:
|
| 29 |
+
raise Exception(f"Error connecting to DB at {db_path}: {e}")
|
| 30 |
+
|
| 31 |
+
# Create engine
|
| 32 |
+
engine = create_engine(
|
| 33 |
+
DATABASE_URL,
|
| 34 |
+
connect_args={"check_same_thread": False} # Needed for SQLite threading
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Session factory
|
| 38 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 39 |
+
|
| 40 |
+
def get_db_engine():
|
| 41 |
+
"""Return SQLAlchemy engine (used by LangChain SQLDatabase)."""
|
| 42 |
+
return engine
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_db():
|
| 46 |
+
"""Provide DB session for queries."""
|
| 47 |
+
db = SessionLocal()
|
| 48 |
+
try:
|
| 49 |
+
yield db
|
| 50 |
+
finally:
|
| 51 |
+
db.close()
|
backend/app/db/schema_reader.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import inspect
|
| 2 |
+
from .db_connector import get_db_engine
|
| 3 |
+
from .db_connector import get_connection
|
| 4 |
+
|
| 5 |
+
def get_schema():
|
| 6 |
+
"""Fetch database schema information (tables & columns)."""
|
| 7 |
+
schema = {}
|
| 8 |
+
conn = get_connection()
|
| 9 |
+
try:
|
| 10 |
+
cursor = conn.cursor()
|
| 11 |
+
|
| 12 |
+
# Get all tables
|
| 13 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
|
| 14 |
+
tables = cursor.fetchall()
|
| 15 |
+
|
| 16 |
+
for t in tables:
|
| 17 |
+
table_name = t[0]
|
| 18 |
+
cursor.execute(f"PRAGMA table_info({table_name});")
|
| 19 |
+
columns = cursor.fetchall()
|
| 20 |
+
schema[table_name] = [col[1] for col in columns]
|
| 21 |
+
|
| 22 |
+
return schema
|
| 23 |
+
finally:
|
| 24 |
+
cursor.close()
|
| 25 |
+
conn.close()
|
| 26 |
+
|
| 27 |
+
def get_schema2():
|
| 28 |
+
"""Fetch database schema information (tables & columns)."""
|
| 29 |
+
engine = get_db_engine()
|
| 30 |
+
inspector = inspect(engine)
|
| 31 |
+
|
| 32 |
+
schema = {}
|
| 33 |
+
for table_name in inspector.get_table_names():
|
| 34 |
+
columns = [
|
| 35 |
+
{"name": col["name"], "type": str(col["type"])}
|
| 36 |
+
for col in inspector.get_columns(table_name)
|
| 37 |
+
]
|
| 38 |
+
schema[table_name] = columns
|
| 39 |
+
|
| 40 |
+
return schema
|
| 41 |
+
|
| 42 |
+
# def get_schema():
|
| 43 |
+
# engine = get_db_engine()
|
| 44 |
+
# inspector = inspect(engine)
|
| 45 |
+
|
| 46 |
+
# schema_info = {}
|
| 47 |
+
# for table in inspector.get_table_names():
|
| 48 |
+
# columns = [col["name"] for col in inspector.get_columns(table)]
|
| 49 |
+
# schema_info[table] = columns
|
| 50 |
+
# return schema_info
|
backend/app/services/query_executor.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from sqlalchemy import text
|
| 3 |
+
from app.db.db_connector import get_db_engine
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
from langchain_openai import ChatOpenAI
|
| 8 |
+
from config.settings import settings
|
| 9 |
+
|
| 10 |
+
def execute_sql_query(sql_query: str):
|
| 11 |
+
"""Run a SQL query and return results as list of dicts."""
|
| 12 |
+
engine = get_db_engine()
|
| 13 |
+
with engine.connect() as conn:
|
| 14 |
+
result = conn.execute(text(sql_query))
|
| 15 |
+
rows = [dict(row) for row in result.mappings()]
|
| 16 |
+
return rows
|
| 17 |
+
|
| 18 |
+
def run_and_handle_sql_query(sql_query: str, user_question: str):
|
| 19 |
+
"""
|
| 20 |
+
Executes the SQL query, and if no results, uses LLM to generate a friendly message.
|
| 21 |
+
Returns either a list of dicts (rows) or {"chat_message": ...}.
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
rows = execute_sql_query(sql_query)
|
| 25 |
+
if not rows:
|
| 26 |
+
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 27 |
+
no_result_prompt = (
|
| 28 |
+
f"The following SQL query was generated for the user's question, but it returned no results. "
|
| 29 |
+
f"User question: {user_question}\nSQL query: {sql_query}\n"
|
| 30 |
+
"Please explain to the user in a friendly way that no matching records were found for their request."
|
| 31 |
+
)
|
| 32 |
+
no_result_response = llm.invoke([{"role": "user", "content": no_result_prompt}]).content.strip()
|
| 33 |
+
return {"chat_message": no_result_response}
|
| 34 |
+
return rows
|
| 35 |
+
except Exception as e:
|
| 36 |
+
return {"error": str(e), "sql_query": sql_query}
|
| 37 |
+
|
| 38 |
+
# def run_query(sql: str):
|
| 39 |
+
# engine = get_db_engine()
|
| 40 |
+
# with engine.connect() as conn:
|
| 41 |
+
# result = conn.execute(text(sql))
|
| 42 |
+
# df = pd.DataFrame(result.fetchall(), columns=result.keys())
|
| 43 |
+
# return df
|
backend/app/services/result_formatter.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib.pyplot as plt
|
| 2 |
+
import io
|
| 3 |
+
import base64
|
| 4 |
+
|
| 5 |
+
def df_to_chart(df, chart_type="bar"):
|
| 6 |
+
import pandas as pd
|
| 7 |
+
plt.clf()
|
| 8 |
+
y_col = df.columns[1] if len(df.columns) > 1 else None
|
| 9 |
+
if y_col is not None and pd.api.types.is_numeric_dtype(df[y_col]):
|
| 10 |
+
# Numeric y-column: plot as usual
|
| 11 |
+
ax = df.plot(kind=chart_type, x=df.columns[0], y=y_col, legend=False)
|
| 12 |
+
else:
|
| 13 |
+
# Non-numeric: plot value counts of the first column
|
| 14 |
+
value_counts = df[df.columns[0]].value_counts()
|
| 15 |
+
ax = value_counts.plot(kind="bar")
|
| 16 |
+
ax.set_xlabel(df.columns[0])
|
| 17 |
+
ax.set_ylabel("Count")
|
| 18 |
+
buf = io.BytesIO()
|
| 19 |
+
plt.savefig(buf, format="png")
|
| 20 |
+
buf.seek(0)
|
| 21 |
+
return base64.b64encode(buf.read()).decode("utf-8")
|
backend/app/services/sql_generator.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_community.utilities import SQLDatabase
|
| 2 |
+
from langchain.chains import create_sql_query_chain
|
| 3 |
+
from langchain_openai import ChatOpenAI # or your LLaMA adapter
|
| 4 |
+
from config.settings import settings
|
| 5 |
+
from langchain.prompts import PromptTemplate, ChatPromptTemplate
|
| 6 |
+
from app.db.schema_reader import get_schema
|
| 7 |
+
from app.db.db_connector import get_db_engine
|
| 8 |
+
from app.db.db_connector import get_connection
|
| 9 |
+
|
| 10 |
+
# llm = ChatOpenAI(model="gpt-4", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 11 |
+
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def generate_sql_query(user_question: str):
|
| 15 |
+
schema = get_schema()
|
| 16 |
+
schema_str = "\n".join(
|
| 17 |
+
[f"{table}: {', '.join(cols)}" for table, cols in schema.items()]
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
prompt = ChatPromptTemplate.from_template("""
|
| 21 |
+
You are an expert SQLite query generator.
|
| 22 |
+
Use ONLY the following tables and columns:
|
| 23 |
+
|
| 24 |
+
{schema}
|
| 25 |
+
|
| 26 |
+
Generate a correct SQLite query (no explanations, only SQL).
|
| 27 |
+
Do not add single quotes or any quotes around table names or column names.
|
| 28 |
+
If the user asks for data, generate a SELECT query and that outcome should have only
|
| 29 |
+
sqlite compatible, don't include any other text.
|
| 30 |
+
If you cannot answer, say: "I cannot generate this query."
|
| 31 |
+
|
| 32 |
+
User question: {question}
|
| 33 |
+
""")
|
| 34 |
+
|
| 35 |
+
messages = prompt.format_messages(schema=schema_str, question=user_question)
|
| 36 |
+
sql_query = llm.invoke(messages).content.strip()
|
| 37 |
+
|
| 38 |
+
# If the LLM says it cannot generate a query, treat as normal chat
|
| 39 |
+
if sql_query.lower().startswith("i cannot generate this query"):
|
| 40 |
+
# Use the LLM as a chatbot for normal conversation
|
| 41 |
+
chat_prompt = f"You are a helpful assistant. Respond conversationally to: {user_question}"
|
| 42 |
+
chat_response = llm.invoke([{"role": "user", "content": chat_prompt}]).content.strip()
|
| 43 |
+
return {"chat_message": chat_response}
|
| 44 |
+
|
| 45 |
+
return sql_query
|
| 46 |
+
|
| 47 |
+
def generate_sql_query2(natural_language_query: str) -> str:
|
| 48 |
+
"""Generate SQL query from natural language using LangChain."""
|
| 49 |
+
engine = get_db_engine()
|
| 50 |
+
db = SQLDatabase(engine)
|
| 51 |
+
|
| 52 |
+
# You can swap ChatOpenAI with LLaMA wrapper
|
| 53 |
+
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 54 |
+
|
| 55 |
+
sql_chain = create_sql_query_chain(llm, db)
|
| 56 |
+
|
| 57 |
+
sql_query = sql_chain.invoke({"question": natural_language_query})
|
| 58 |
+
return sql_query
|
| 59 |
+
|
| 60 |
+
|
backend/app/services/utility.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import openai
|
| 2 |
+
from config.settings import settings
|
| 3 |
+
from langchain_openai import ChatOpenAI
|
| 4 |
+
import sqlparse
|
| 5 |
+
import re
|
| 6 |
+
class UtilityClass:
|
| 7 |
+
"""
|
| 8 |
+
Classifies user input as 'sql' (SQL/data context) or 'chat' (normal conversation) using an LLM.
|
| 9 |
+
"""
|
| 10 |
+
@staticmethod
|
| 11 |
+
def classify_user_input(text: str) -> str:
|
| 12 |
+
prompt = (
|
| 13 |
+
"Classify the following message as 'sql' if it is a database/data/SQL question, "
|
| 14 |
+
"or 'chat' if it is normal conversation.\n"
|
| 15 |
+
f"Message: {text}\n"
|
| 16 |
+
"Respond with only 'sql' or 'chat'."
|
| 17 |
+
)
|
| 18 |
+
response = openai.ChatCompletion.create(
|
| 19 |
+
model="gpt-3.5-turbo",
|
| 20 |
+
messages=[{"role": "user", "content": prompt}],
|
| 21 |
+
api_key=settings.OPENAI_API_KEY
|
| 22 |
+
)
|
| 23 |
+
label = response.choices[0].message.content.strip().lower()
|
| 24 |
+
return label if label in ("sql", "chat") else "chat"
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def chat_response(text: str) -> str:
|
| 28 |
+
chat_prompt = f"You are a helpful assistant. Respond conversationally to: {text}"
|
| 29 |
+
response = openai.ChatCompletion.create(
|
| 30 |
+
model="gpt-3.5-turbo",
|
| 31 |
+
messages=[{"role": "user", "content": chat_prompt}],
|
| 32 |
+
api_key=settings.OPENAI_API_KEY
|
| 33 |
+
)
|
| 34 |
+
return response.choices[0].message.content.strip()
|
| 35 |
+
|
| 36 |
+
# Helper to prepare JSON with heading from LLM and records
|
| 37 |
+
@staticmethod
|
| 38 |
+
def prepare_llm_heading_and_records(user_question, rows):
|
| 39 |
+
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=settings.OPENAI_API_KEY)
|
| 40 |
+
record_count = len(rows)
|
| 41 |
+
column_names = list(rows[0].keys()) if rows and isinstance(rows[0], dict) else []
|
| 42 |
+
heading_prompt = (
|
| 43 |
+
"Given the user's question and the following column names and record count, generate:\n"
|
| 44 |
+
"1. A short, clear, and specific heading for the results (do NOT use generic phrases like 'Here are the top X results I found').\n"
|
| 45 |
+
"2. A 1-2 sentence summary or description of what the results represent, using the question and columns for context.\n"
|
| 46 |
+
f"User question: {user_question}\nColumns: {column_names}\nRecord count: {record_count}\n"
|
| 47 |
+
"Return your answer as JSON with keys 'heading' and 'summary'. Do not mention the actual data."
|
| 48 |
+
)
|
| 49 |
+
heading = llm.invoke([{"role": "user", "content": heading_prompt}]).content.strip()
|
| 50 |
+
return {"heading": heading, "records": rows}
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def is_valid_sql_query33(sql: str) -> bool:
|
| 54 |
+
"""
|
| 55 |
+
Returns True if the string is a valid SQL statement (not just a keyword in text).
|
| 56 |
+
Uses sqlparse to check for a valid statement structure.
|
| 57 |
+
Accepts queries starting with 'sql ' followed by a valid SQL statement.
|
| 58 |
+
"""
|
| 59 |
+
if not sql or not isinstance(sql, str):
|
| 60 |
+
return False
|
| 61 |
+
sql_strip = sql.strip()
|
| 62 |
+
# Remove leading 'sql' if present
|
| 63 |
+
if sql_strip.lower().startswith('sql'):
|
| 64 |
+
sql_strip = sql_strip[4:].lstrip()
|
| 65 |
+
parsed = sqlparse.parse(sql_strip)
|
| 66 |
+
if not parsed or not parsed[0].tokens:
|
| 67 |
+
return False
|
| 68 |
+
stmt = parsed[0]
|
| 69 |
+
first_token = stmt.token_first(skip_cm=True, skip_ws=True)
|
| 70 |
+
if first_token is None:
|
| 71 |
+
return False
|
| 72 |
+
# Accept only if the first token is a SQL keyword and there is more than one token
|
| 73 |
+
return first_token.ttype in sqlparse.tokens.Keyword.DML and len(stmt.tokens) > 1
|
| 74 |
+
|
| 75 |
+
@staticmethod
|
| 76 |
+
def is_valid_sql_query(text: str):
|
| 77 |
+
if not text or not text.strip():
|
| 78 |
+
return False
|
| 79 |
+
|
| 80 |
+
# Remove markdown formatting like ```sql ... ```
|
| 81 |
+
text = re.sub(r"```sql|```", "", text, flags=re.IGNORECASE).strip()
|
| 82 |
+
|
| 83 |
+
# Collapse multiple spaces & line breaks into a single space
|
| 84 |
+
normalized = re.sub(r"\s+", " ", text).strip().upper()
|
| 85 |
+
|
| 86 |
+
# Common SQL query structure patterns (heuristics)
|
| 87 |
+
sql_patterns = [
|
| 88 |
+
r"^SELECT\s+.+\s+FROM\s+.+", # SELECT ... FROM ...
|
| 89 |
+
r"^INSERT\s+INTO\s+.+\s+VALUES\s*\(", # INSERT INTO ... VALUES (...)
|
| 90 |
+
r"^UPDATE\s+.+\s+SET\s+.+", # UPDATE ... SET ...
|
| 91 |
+
r"^DELETE\s+FROM\s+.+", # DELETE FROM ...
|
| 92 |
+
r"^CREATE\s+(TABLE|DATABASE)\s+.+", # CREATE TABLE/DATABASE ...
|
| 93 |
+
r"^DROP\s+(TABLE|DATABASE)\s+.+", # DROP TABLE/DATABASE ...
|
| 94 |
+
r"^ALTER\s+TABLE\s+.+", # ALTER TABLE ...
|
| 95 |
+
r"^WITH\s+.+\s+AS\s*\(.+\)" # WITH ... AS (...)
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
for pattern in sql_patterns:
|
| 99 |
+
# Format the SQL for readability
|
| 100 |
+
formatted_sql = sqlparse.format(text, reindent=True, keyword_case='upper')
|
| 101 |
+
return formatted_sql
|
| 102 |
+
|
| 103 |
+
return False
|
| 104 |
+
|
| 105 |
+
|
backend/app/services/utility_sql.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlparse
|
| 2 |
+
|
| 3 |
+
class UtilityClass:
|
| 4 |
+
@staticmethod
|
| 5 |
+
def is_valid_sql_query(sql: str) -> bool:
|
| 6 |
+
"""
|
| 7 |
+
Returns True if the string is a valid SQL statement (not just a keyword in text).
|
| 8 |
+
Uses sqlparse to check for a valid statement structure.
|
| 9 |
+
"""
|
| 10 |
+
if not sql or not isinstance(sql, str):
|
| 11 |
+
return False
|
| 12 |
+
parsed = sqlparse.parse(sql)
|
| 13 |
+
if not parsed or not parsed[0].tokens:
|
| 14 |
+
return False
|
| 15 |
+
# Check if the first token is a DML/DDL keyword and the statement is not just a keyword
|
| 16 |
+
stmt = parsed[0]
|
| 17 |
+
first_token = stmt.token_first(skip_cm=True, skip_ws=True)
|
| 18 |
+
if first_token is None:
|
| 19 |
+
return False
|
| 20 |
+
# Accept only if the first token is a SQL keyword and there is more than one token
|
| 21 |
+
return first_token.ttype in sqlparse.tokens.Keyword.DML and len(stmt.tokens) > 1
|
backend/config/settings.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
class Settings:
|
| 7 |
+
ENV = os.getenv("ENV") # Default to "local" if ENV is not set
|
| 8 |
+
# LLM API Token
|
| 9 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
| 10 |
+
|
| 11 |
+
# GEMENI_API_KEY_TOKEN = os.getenv("OPENAI_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
| 12 |
+
|
| 13 |
+
settings = Settings()
|
backend/main.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from app.api.endpoints import router
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
|
| 5 |
+
app = FastAPI(title="AI Query Generator")
|
| 6 |
+
# app.include_router(router)
|
| 7 |
+
|
| 8 |
+
# Allowed origins (Update this for production)
|
| 9 |
+
|
| 10 |
+
#router = APIRouter()
|
| 11 |
+
app.add_middleware(
|
| 12 |
+
CORSMiddleware,
|
| 13 |
+
allow_origins=["*"], # Replace "*" with your frontend domain in production
|
| 14 |
+
allow_credentials=True,
|
| 15 |
+
allow_methods=["*"],
|
| 16 |
+
allow_headers=["*"],
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Include API routes from endpoints.py
|
| 20 |
+
app.include_router(router, prefix="/api") # You can remove prefix if not needed
|
| 21 |
+
|
| 22 |
+
@app.get("/")
|
| 23 |
+
async def root():
|
| 24 |
+
return {"message": "Hello from FastAPI"}
|
| 25 |
+
|
| 26 |
+
#app.include_router(router)
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
sqlparse
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn
|
| 4 |
+
streamlit
|
| 5 |
+
requests
|
| 6 |
+
langchain
|
| 7 |
+
langchain-openai
|
| 8 |
+
langchain-community
|
| 9 |
+
sqlalchemy
|
| 10 |
+
pydantic
|
| 11 |
+
faiss-cpu # optional for embeddings
|
| 12 |
+
pandas
|
| 13 |
+
matplotlib
|
| 14 |
+
python-dotenv
|
| 15 |
+
|