Emannn2004 commited on
Commit
0d3d494
·
verified ·
1 Parent(s): 71d8105

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +20 -0
  2. business_database.db +0 -0
  3. main.py +122 -0
  4. requirements.txt +0 -0
  5. schema.py +104 -0
  6. sql_analyst.py +472 -0
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image
2
+ FROM python:3.12-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy project files
8
+ COPY . /app
9
+
10
+ # Upgrade pip
11
+ RUN pip install --upgrade pip
12
+
13
+ # Install dependencies
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Expose FastAPI port
17
+ EXPOSE 7860
18
+
19
+ # Set command for HF Spaces to run your app
20
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
business_database.db ADDED
Binary file (32.8 kB). View file
 
main.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import sqlite3
4
+ from fastapi import FastAPI, UploadFile, File, Form
5
+ from pydantic import BaseModel
6
+ from typing import Optional
7
+
8
+ # Import schema extraction logic
9
+ from schema import (
10
+ get_sqlite_schema,
11
+ get_uploaded_sqlite_schema,
12
+ get_mysql_schema,
13
+ get_postgresql_schema,
14
+ )
15
+
16
+ # Import your AI SQL logic
17
+ from sql_analyst import generate_and_execute_sql
18
+
19
+ app = FastAPI(title="AI SQL Analyst API")
20
+
21
+ # ----------------------------
22
+ # Health / Root Endpoint
23
+ # ----------------------------
24
+ @app.get("/")
25
+ def root():
26
+ return {"status": "AI SQL Analyst API is running"}
27
+
28
+ # ----------------------------
29
+ # POST /sqlite
30
+ # Accept a SQLite database file + user query
31
+ # ----------------------------
32
+ @app.post("/sqlite")
33
+ async def analyze_sqlite(
34
+ file: UploadFile = File(...),
35
+ user_query: str = Form(...)
36
+ ):
37
+ """
38
+ Upload a SQLite file and run a natural‑language query against it.
39
+ """
40
+ # Save uploaded file to a temp path
41
+ temp_dir = tempfile.gettempdir()
42
+ sqlite_path = os.path.join(temp_dir, file.filename)
43
+
44
+ with open(sqlite_path, "wb") as f:
45
+ f.write(await file.read())
46
+
47
+ # Extract schema
48
+ schema = get_uploaded_sqlite_schema(sqlite_path)
49
+
50
+ # Build SQLite config
51
+ db_info = {"type": "sqlite", "path": sqlite_path}
52
+
53
+ # Generate + execute SQL
54
+ sql, result = generate_and_execute_sql(user_query, schema, db_info)
55
+
56
+ # Optionally remove the file (cleanup)
57
+ try:
58
+ os.remove(sqlite_path)
59
+ except:
60
+ pass
61
+
62
+ return {"sql": sql, "result": result}
63
+
64
+ # ----------------------------
65
+ # POST /builtinsqlite
66
+ # Run query on built‑in business SQLite
67
+ # ----------------------------
68
+ @app.post("/builtinsqlite")
69
+ async def analyze_business_sqlite(user_query: str = Form(...)):
70
+ """
71
+ Run a query against the built‑in business SQLite database.
72
+ """
73
+ # Get business SQLite schema
74
+ schema = get_sqlite_schema()
75
+ db_info = {"type": "sqlite", "path": "business_database.db"}
76
+
77
+ sql, result = generate_and_execute_sql(user_query, schema, db_info)
78
+ return {"sql": sql, "result": result}
79
+
80
+ # ----------------------------
81
+ # POST /remote
82
+ # Accept remote DB connection details + user query
83
+ # ----------------------------
84
+ class RemoteRequest(BaseModel):
85
+ host: str
86
+ port: str
87
+ user: str
88
+ password: str
89
+ database: str
90
+ db_type: str # "mysql" or "postgresql"
91
+ user_query: str
92
+
93
+ @app.post("/remote")
94
+ def analyze_remote(body: RemoteRequest):
95
+ """
96
+ Connect to a remote MySQL/PostgreSQL and run the natural language query.
97
+ """
98
+ db_type = body.db_type.lower()
99
+ if db_type not in ("mysql", "postgresql"):
100
+ return {"error": "db_type must be 'mysql' or 'postgresql'"}
101
+
102
+ # Extract schema
103
+ if db_type == "mysql":
104
+ schema = get_mysql_schema(
105
+ body.host, body.port, body.user, body.password, body.database
106
+ )
107
+ else: # postgresql
108
+ schema = get_postgresql_schema(
109
+ body.host, body.port, body.user, body.password, body.database
110
+ )
111
+
112
+ db_info = {
113
+ "type": db_type,
114
+ "host": body.host,
115
+ "port": body.port,
116
+ "user": body.user,
117
+ "password": body.password,
118
+ "database": body.database,
119
+ }
120
+
121
+ sql, result = generate_and_execute_sql(body.user_query, schema, db_info)
122
+ return {"sql": sql, "result": result}
requirements.txt ADDED
Binary file (5.98 kB). View file
 
schema.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import sqliteschema
3
+ from typing import Dict, Any
4
+
5
+ # -------------------------
6
+ # 1) Business SQLite Schema
7
+ # -------------------------
8
+
9
+ def get_sqlite_schema() -> Dict[str, Any]:
10
+ """
11
+ Extracts the schema of the hardcoded business SQLite database
12
+ and returns it as a dictionary.
13
+ """
14
+ db_path = "business_database.db"
15
+ extractor = sqliteschema.SQLiteSchemaExtractor(db_path)
16
+ schema_dict = extractor.fetch_database_schema_as_dict()
17
+ return schema_dict
18
+
19
+ # -------------------------
20
+ # 2) Uploaded SQLite Schema
21
+ # -------------------------
22
+
23
+ def get_uploaded_sqlite_schema(db_path: str) -> Dict[str, Any]:
24
+ """
25
+ Extracts the schema from a user uploaded SQLite file.
26
+ """
27
+ extractor = sqliteschema.SQLiteSchemaExtractor(db_path)
28
+ schema_dict = extractor.fetch_database_schema_as_dict()
29
+ return schema_dict
30
+
31
+ # -------------------------
32
+ # 3) MySQL Schema
33
+ # -------------------------
34
+
35
+ def get_mysql_schema(host: str, port: str, user: str, password: str, database: str) -> Dict[str, Any]:
36
+ """
37
+ Connects to a MySQL database and extracts its schema.
38
+ """
39
+ import mysql.connector
40
+
41
+ conn = mysql.connector.connect(
42
+ host=host,
43
+ port=int(port),
44
+ user=user,
45
+ password=password,
46
+ database=database
47
+ )
48
+ cursor = conn.cursor()
49
+ cursor.execute("SHOW TABLES")
50
+ tables = [t[0] for t in cursor.fetchall()]
51
+
52
+ schema: Dict[str, Any] = {}
53
+ for table in tables:
54
+ cursor.execute(f"DESCRIBE {table}")
55
+ cols = cursor.fetchall()
56
+ schema[table] = [
57
+ {"Field": col[0], "Type": col[1], "Null": col[2], "Key": col[3], "Default": col[4], "Extra": col[5]}
58
+ for col in cols
59
+ ]
60
+
61
+ conn.close()
62
+ return schema
63
+
64
+ # -------------------------
65
+ # 4) PostgreSQL Schema
66
+ # -------------------------
67
+
68
+ def get_postgresql_schema(host: str, port: str, user: str, password: str, database: str) -> Dict[str, Any]:
69
+ """
70
+ Connects to a PostgreSQL database and extracts its schema.
71
+ """
72
+ import psycopg2
73
+
74
+ conn = psycopg2.connect(
75
+ host=host,
76
+ port=int(port),
77
+ user=user,
78
+ password=password,
79
+ dbname=database
80
+ )
81
+ cursor = conn.cursor()
82
+
83
+ # list all public tables
84
+ cursor.execute("""
85
+ SELECT table_name
86
+ FROM information_schema.tables
87
+ WHERE table_schema = 'public'
88
+ AND table_type = 'BASE TABLE';
89
+ """)
90
+ tables = [t[0] for t in cursor.fetchall()]
91
+
92
+ schema: Dict[str, Any] = {}
93
+ for table in tables:
94
+ cursor.execute(f"""
95
+ SELECT column_name, data_type
96
+ FROM information_schema.columns
97
+ WHERE table_name = '{table}';
98
+ """)
99
+ cols = cursor.fetchall()
100
+ schema[table] = [{"column_name": col[0], "data_type": col[1]} for col in cols]
101
+
102
+ conn.close()
103
+ return schema
104
+
sql_analyst.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ # import sqlite3
3
+ # from langchain_ollama import ChatOllama
4
+ # from langchain_core.prompts import PromptTemplate
5
+ # from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
6
+ # from dotenv import load_dotenv
7
+
8
+ # load_dotenv()
9
+
10
+ # print("DEBUG: Initializing LLM")
11
+ # llm = HuggingFaceEndpoint(
12
+ # model="Qwen/Qwen3-Coder-Next",
13
+ # task="text-generation"
14
+ # )
15
+ # model = ChatHuggingFace(llm=llm)
16
+
17
+ # # ---------------------------------------------------------------------------- #
18
+ # # Pilot Few-Shot SQL Examples (must be defined before use)
19
+ # # ---------------------------------------------------------------------------- #
20
+ # COMPLEX_EXAMPLES = """
21
+ # -- Example A: Basic SELECT
22
+ # User: "List all data from orders."
23
+ # SQL:
24
+ # SELECT * FROM orders;
25
+
26
+ # -- Example B: Lookup by ID
27
+ # User: "Get customer with ID 5."
28
+ # SQL:
29
+ # SELECT * FROM customers WHERE customer_id = 5;
30
+
31
+ # -- Example C: Aggregation with Group
32
+ # User: "Count number of products per category."
33
+ # SQL:
34
+ # SELECT category, COUNT(*) AS count_per_category
35
+ # FROM products
36
+ # GROUP BY category;
37
+
38
+ # -- Example D: Update record
39
+ # User: "Update customer ID 10's email."
40
+ # SQL:
41
+ # UPDATE customers SET email = 'new@example.com' WHERE customer_id = 10;
42
+
43
+ # -- Example E: Delete specific records
44
+ # User: "Delete cancelled orders before 2025."
45
+ # SQL:
46
+ # DELETE FROM orders WHERE status = 'cancelled' AND order_date < '2025-01-01';
47
+ # """
48
+
49
+ # # ---------------------------------------------------------------------------- #
50
+ # # Prompt Template (stores DDL, examples, and user query)
51
+ # # ---------------------------------------------------------------------------- #
52
+ # prompt_template = PromptTemplate(
53
+ # template="""
54
+ # -- You are an expert SQL generator.
55
+ # -- Generate valid SQL (SELECT, INSERT, UPDATE, DELETE) to answer the user's request.
56
+ # -- Avoid hallucinating tables/columns outside the schema.
57
+ # -- Do not output CREATE, DROP, ALTER, or other schema changes.
58
+ # -- If the question cannot be answered with the given schema, output:
59
+ # -- Cannot answer with current schema.
60
+
61
+ # -- SQL Dialect: {sql_dialect}
62
+
63
+ # -- Schema DDL --
64
+ # {schema_ddl}
65
+
66
+ # -- Examples --
67
+ # {few_shot_examples}
68
+
69
+ # -- User Request --
70
+ # {user_query}
71
+
72
+ # -- OUTPUT ONLY the SQL:
73
+ # """,
74
+ # input_variables=["schema_ddl", "few_shot_examples", "user_query", "sql_dialect"]
75
+ # )
76
+
77
+ # # ---------------------------------------------------------------------------- #
78
+ # # Main: Generate + Execute SQL
79
+ # # ---------------------------------------------------------------------------- #
80
+ # def generate_and_execute_sql(user_query: str, schema: dict, db_config: dict = None):
81
+ # print("\nDEBUG: generate_and_execute_sql called")
82
+ # print("DEBUG: User query:", user_query)
83
+ # print("DEBUG: DB config:", db_config)
84
+
85
+ # # Determine SQL dialect
86
+ # if db_config is None or db_config.get("type") == "sqlite":
87
+ # dialect = "SQLite"
88
+ # elif db_config["type"] == "mysql":
89
+ # dialect = "MySQL"
90
+ # elif db_config["type"] == "postgresql":
91
+ # dialect = "PostgreSQL"
92
+ # else:
93
+ # dialect = "SQL"
94
+
95
+ # print("DEBUG: Using dialect:", dialect)
96
+
97
+ # ddl = convert_schema_to_ddl(schema)
98
+
99
+ # # Build final prompt with the schema and examples
100
+ # final_prompt = prompt_template.format(
101
+ # schema_ddl=ddl,
102
+ # few_shot_examples=COMPLEX_EXAMPLES.strip(),
103
+ # user_query=user_query,
104
+ # sql_dialect=dialect
105
+ # )
106
+
107
+ # print("DEBUG: Prompt sent to model:\n", final_prompt)
108
+
109
+ # # Ask LLM for SQL
110
+ # try:
111
+ # response = model.invoke([
112
+ # ("system", "Generate only SQL as instructed."),
113
+ # ("human", final_prompt),
114
+ # ])
115
+ # except Exception as e:
116
+ # print("ERROR: LLM invocation error:", e)
117
+ # return "", f"LLM invocation error: {str(e)}"
118
+
119
+ # generated_sql = response.content.strip()
120
+ # print("DEBUG: Model generated SQL:\n", generated_sql)
121
+
122
+ # # If the LLM says it cannot answer:
123
+ # if generated_sql.lower().startswith("cannot answer"):
124
+ # print("DEBUG: Model cannot answer with current schema")
125
+ # return generated_sql, "No execution (cannot answer)"
126
+
127
+ # # Execute the generated SQL
128
+ # print("DEBUG: Executing SQL...")
129
+ # result = execute_query(generated_sql, db_config)
130
+ # print("DEBUG: Execution result:", result)
131
+
132
+ # return generated_sql, result
133
+
134
+ # # ---------------------------------------------------------------------------- #
135
+ # # Unified SQL Executor (with column names)
136
+ # # ---------------------------------------------------------------------------- #
137
+ # def execute_query(query: str, db_config: dict):
138
+ # print("\nDEBUG: execute_query called")
139
+ # print("DEBUG: Query to execute:", query)
140
+ # print("DEBUG: DB Config:", db_config)
141
+
142
+ # db_type = db_config.get("type")
143
+
144
+ # try:
145
+ # if db_type == "mysql":
146
+ # print("DEBUG: Connecting to MySQL")
147
+ # import mysql.connector
148
+ # conn = mysql.connector.connect(
149
+ # host=db_config["host"],
150
+ # port=int(db_config["port"]),
151
+ # user=db_config["user"],
152
+ # password=db_config["password"],
153
+ # database=db_config["database"],
154
+ # )
155
+
156
+ # elif db_type == "postgresql":
157
+ # print("DEBUG: Connecting to PostgreSQL")
158
+ # import psycopg2
159
+ # conn = psycopg2.connect(
160
+ # host=db_config["host"],
161
+ # port=int(db_config["port"]),
162
+ # user=db_config["user"],
163
+ # password=db_config["password"],
164
+ # dbname=db_config["database"],
165
+ # )
166
+ # print("DEBUG: PostgreSQL connection successful")
167
+
168
+ # else:
169
+ # print("DEBUG: Connecting to SQLite")
170
+ # conn = sqlite3.connect(db_config.get("path"))
171
+
172
+ # cursor = conn.cursor()
173
+ # print("DEBUG: Cursor acquired, executing now...")
174
+
175
+ # cursor.execute(query)
176
+
177
+ # # SELECT → fetch results + columns
178
+ # if query.strip().lower().startswith("select"):
179
+ # print("DEBUG: SELECT query — fetching rows")
180
+ # rows = cursor.fetchall()
181
+
182
+ # # Get column names from cursor.description
183
+ # columns = [col[0] for col in cursor.description] # first element is column name :contentReference[oaicite:1]{index=1}
184
+
185
+ # print("DEBUG: Column names:", columns)
186
+ # print("DEBUG: SELECT result rows:", rows)
187
+
188
+ # conn.close()
189
+ # return {"columns": columns, "rows": rows}
190
+
191
+ # # For INSERT/UPDATE/DELETE
192
+ # print("DEBUG: Non‑SELECT — commit and return affected count")
193
+ # conn.commit()
194
+ # affected = cursor.rowcount
195
+ # print("DEBUG: Affected rows:", affected)
196
+ # conn.close()
197
+ # return {"affected_rows": affected}
198
+
199
+ # except Exception as e:
200
+ # print("ERROR: Exception in execute_query:", e)
201
+ # try:
202
+ # conn.close()
203
+ # except:
204
+ # pass
205
+
206
+ # return f"SQL execution error: {str(e)}"
207
+
208
+ # # ---------------------------------------------------------------------------- #
209
+ # # Schema to DDL
210
+ # # ---------------------------------------------------------------------------- #
211
+ # def convert_schema_to_ddl(schema: dict) -> str:
212
+ # print("DEBUG: Converting schema to DDL")
213
+ # ddl_lines = []
214
+ # for table, cols in schema.items():
215
+ # col_defs = []
216
+ # for col in cols:
217
+ # col_name = (
218
+ # col.get("name")
219
+ # or col.get("Field")
220
+ # or col.get("column_name")
221
+ # or col.get("COLUMN_NAME")
222
+ # or str(col)
223
+ # )
224
+ # col_type = (
225
+ # col.get("type")
226
+ # or col.get("Type")
227
+ # or col.get("data_type")
228
+ # or "TEXT"
229
+ # )
230
+ # col_defs.append(f"{col_name} {col_type}")
231
+ # ddl_lines.append(f"CREATE TABLE {table} ({', '.join(col_defs)});")
232
+ # ddl_text = "\n".join(ddl_lines)
233
+ # print("DEBUG: DDL:\n", ddl_text)
234
+ # return ddl_text
235
+ import json
236
+ import sqlite3
237
+ from langchain_ollama import ChatOllama
238
+ from langchain_core.prompts import PromptTemplate
239
+ from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
240
+ import os # <-- for environment variables
241
+
242
+ # Instead of load_dotenv(), get token from environment
243
+ hf_token = os.environ.get("HUGGINGFACEHUB_ACCESS_TOKEN")
244
+ if not hf_token:
245
+ raise ValueError("HUGGINGFACEHUB_ACCESS_TOKEN not found in environment variables!")
246
+
247
+ print("DEBUG: Initializing LLM")
248
+ llm = HuggingFaceEndpoint(
249
+ model="Qwen/Qwen3-Coder-Next",
250
+ task="text-generation",
251
+ huggingfacehub_api_token=hf_token # <-- use environment token
252
+ )
253
+ model = ChatHuggingFace(llm=llm)
254
+
255
+ # ---------------------------------------------------------------------------- #
256
+ # Pilot Few-Shot SQL Examples (must be defined before use)
257
+ # ---------------------------------------------------------------------------- #
258
+ COMPLEX_EXAMPLES = """
259
+ -- Example A: Basic SELECT
260
+ User: "List all data from orders."
261
+ SQL:
262
+ SELECT * FROM orders;
263
+
264
+ -- Example B: Lookup by ID
265
+ User: "Get customer with ID 5."
266
+ SQL:
267
+ SELECT * FROM customers WHERE customer_id = 5;
268
+
269
+ -- Example C: Aggregation with Group
270
+ User: "Count number of products per category."
271
+ SQL:
272
+ SELECT category, COUNT(*) AS count_per_category
273
+ FROM products
274
+ GROUP BY category;
275
+
276
+ -- Example D: Update record
277
+ User: "Update customer ID 10's email."
278
+ SQL:
279
+ UPDATE customers SET email = 'new@example.com' WHERE customer_id = 10;
280
+
281
+ -- Example E: Delete specific records
282
+ User: "Delete cancelled orders before 2025."
283
+ SQL:
284
+ DELETE FROM orders WHERE status = 'cancelled' AND order_date < '2025-01-01';
285
+ """
286
+
287
+ # ---------------------------------------------------------------------------- #
288
+ # Prompt Template (stores DDL, examples, and user query)
289
+ # ---------------------------------------------------------------------------- #
290
+ prompt_template = PromptTemplate(
291
+ template="""
292
+ -- You are an expert SQL generator.
293
+ -- Generate valid SQL (SELECT, INSERT, UPDATE, DELETE) to answer the user's request.
294
+ -- Avoid hallucinating tables/columns outside the schema.
295
+ -- Do not output CREATE, DROP, ALTER, or other schema changes.
296
+ -- If the question cannot be answered with the given schema, output:
297
+ -- Cannot answer with current schema.
298
+
299
+ -- SQL Dialect: {sql_dialect}
300
+
301
+ -- Schema DDL --
302
+ {schema_ddl}
303
+
304
+ -- Examples --
305
+ {few_shot_examples}
306
+
307
+ -- User Request --
308
+ {user_query}
309
+
310
+ -- OUTPUT ONLY the SQL:
311
+ """,
312
+ input_variables=["schema_ddl", "few_shot_examples", "user_query", "sql_dialect"]
313
+ )
314
+
315
+ # ---------------------------------------------------------------------------- #
316
+ # Main: Generate + Execute SQL
317
+ # ---------------------------------------------------------------------------- #
318
+ def generate_and_execute_sql(user_query: str, schema: dict, db_config: dict = None):
319
+ print("\nDEBUG: generate_and_execute_sql called")
320
+ print("DEBUG: User query:", user_query)
321
+ print("DEBUG: DB config:", db_config)
322
+
323
+ # Determine SQL dialect
324
+ if db_config is None or db_config.get("type") == "sqlite":
325
+ dialect = "SQLite"
326
+ elif db_config["type"] == "mysql":
327
+ dialect = "MySQL"
328
+ elif db_config["type"] == "postgresql":
329
+ dialect = "PostgreSQL"
330
+ else:
331
+ dialect = "SQL"
332
+
333
+ print("DEBUG: Using dialect:", dialect)
334
+
335
+ ddl = convert_schema_to_ddl(schema)
336
+
337
+ # Build final prompt with the schema and examples
338
+ final_prompt = prompt_template.format(
339
+ schema_ddl=ddl,
340
+ few_shot_examples=COMPLEX_EXAMPLES.strip(),
341
+ user_query=user_query,
342
+ sql_dialect=dialect
343
+ )
344
+
345
+ print("DEBUG: Prompt sent to model:\n", final_prompt)
346
+
347
+ # Ask LLM for SQL
348
+ try:
349
+ response = model.invoke([
350
+ ("system", "Generate only SQL as instructed."),
351
+ ("human", final_prompt),
352
+ ])
353
+ except Exception as e:
354
+ print("ERROR: LLM invocation error:", e)
355
+ return "", f"LLM invocation error: {str(e)}"
356
+
357
+ generated_sql = response.content.strip()
358
+ print("DEBUG: Model generated SQL:\n", generated_sql)
359
+
360
+ # If the LLM says it cannot answer:
361
+ if generated_sql.lower().startswith("cannot answer"):
362
+ print("DEBUG: Model cannot answer with current schema")
363
+ return generated_sql, "No execution (cannot answer)"
364
+
365
+ # Execute the generated SQL
366
+ print("DEBUG: Executing SQL...")
367
+ result = execute_query(generated_sql, db_config)
368
+ print("DEBUG: Execution result:", result)
369
+
370
+ return generated_sql, result
371
+
372
+ # ---------------------------------------------------------------------------- #
373
+ # Unified SQL Executor (with column names)
374
+ # ---------------------------------------------------------------------------- #
375
+ def execute_query(query: str, db_config: dict):
376
+ print("\nDEBUG: execute_query called")
377
+ print("DEBUG: Query to execute:", query)
378
+ print("DEBUG: DB Config:", db_config)
379
+
380
+ db_type = db_config.get("type")
381
+
382
+ try:
383
+ if db_type == "mysql":
384
+ print("DEBUG: Connecting to MySQL")
385
+ import mysql.connector
386
+ conn = mysql.connector.connect(
387
+ host=db_config["host"],
388
+ port=int(db_config["port"]),
389
+ user=db_config["user"],
390
+ password=db_config["password"],
391
+ database=db_config["database"],
392
+ )
393
+
394
+ elif db_type == "postgresql":
395
+ print("DEBUG: Connecting to PostgreSQL")
396
+ import psycopg2
397
+ conn = psycopg2.connect(
398
+ host=db_config["host"],
399
+ port=int(db_config["port"]),
400
+ user=db_config["user"],
401
+ password=db_config["password"],
402
+ dbname=db_config["database"],
403
+ )
404
+ print("DEBUG: PostgreSQL connection successful")
405
+
406
+ else:
407
+ print("DEBUG: Connecting to SQLite")
408
+ conn = sqlite3.connect(db_config.get("path"))
409
+
410
+ cursor = conn.cursor()
411
+ print("DEBUG: Cursor acquired, executing now...")
412
+
413
+ cursor.execute(query)
414
+
415
+ # SELECT → fetch results + columns
416
+ if query.strip().lower().startswith("select"):
417
+ print("DEBUG: SELECT query — fetching rows")
418
+ rows = cursor.fetchall()
419
+
420
+ # Get column names from cursor.description
421
+ columns = [col[0] for col in cursor.description]
422
+
423
+ print("DEBUG: Column names:", columns)
424
+ print("DEBUG: SELECT result rows:", rows)
425
+
426
+ conn.close()
427
+ return {"columns": columns, "rows": rows}
428
+
429
+ # For INSERT/UPDATE/DELETE
430
+ print("DEBUG: Non‑SELECT — commit and return affected count")
431
+ conn.commit()
432
+ affected = cursor.rowcount
433
+ print("DEBUG: Affected rows:", affected)
434
+ conn.close()
435
+ return {"affected_rows": affected}
436
+
437
+ except Exception as e:
438
+ print("ERROR: Exception in execute_query:", e)
439
+ try:
440
+ conn.close()
441
+ except:
442
+ pass
443
+
444
+ return f"SQL execution error: {str(e)}"
445
+
446
+ # ---------------------------------------------------------------------------- #
447
+ # Schema to DDL
448
+ # ---------------------------------------------------------------------------- #
449
+ def convert_schema_to_ddl(schema: dict) -> str:
450
+ print("DEBUG: Converting schema to DDL")
451
+ ddl_lines = []
452
+ for table, cols in schema.items():
453
+ col_defs = []
454
+ for col in cols:
455
+ col_name = (
456
+ col.get("name")
457
+ or col.get("Field")
458
+ or col.get("column_name")
459
+ or col.get("COLUMN_NAME")
460
+ or str(col)
461
+ )
462
+ col_type = (
463
+ col.get("type")
464
+ or col.get("Type")
465
+ or col.get("data_type")
466
+ or "TEXT"
467
+ )
468
+ col_defs.append(f"{col_name} {col_type}")
469
+ ddl_lines.append(f"CREATE TABLE {table} ({', '.join(col_defs)});")
470
+ ddl_text = "\n".join(ddl_lines)
471
+ print("DEBUG: DDL:\n", ddl_text)
472
+ return ddl_text