nilotpaldhar2004 commited on
Commit
fe2e2be
·
unverified ·
1 Parent(s): 5cf9270

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -82
app.py CHANGED
@@ -2,10 +2,19 @@ import os
2
  import re
3
  import io
4
  import json
 
5
  import sqlite3
6
  import tempfile
 
7
  import pandas as pd
8
  import urllib.request
 
 
 
 
 
 
 
9
 
10
  from fastapi import FastAPI, File, UploadFile, HTTPException, Request
11
  from fastapi.staticfiles import StaticFiles
@@ -41,89 +50,85 @@ class QueryRequest(BaseModel):
41
  question: str
42
 
43
 
44
- # ─────────────────────────────
45
- # HEURISTIC SQL — disabled, Gemini handles everything
46
- # ─────────────────────────────
47
-
48
- def _heuristic_sql(question: str, table: str, cols: list = None):
49
- return None
50
-
51
-
52
  # ─────────────────────────────
53
  # GEMINI CALL
 
 
54
  # ─────────────────────────────
55
 
56
- def _call_gemini(question: str, schema: str, columns: list, table: str):
 
57
 
58
  if not GEMINI_API_KEY:
59
  return ""
60
 
61
  prompt = f"""
62
  You are an expert SQLite data analyst.
63
-
64
  Return ONLY a valid SQL query. No explanation, no markdown, no text before or after.
65
-
66
  STRICT RULES:
67
  - Use only the table "{table}"
68
  - Never use DROP or DELETE
69
  - For GROUP BY queries, always include the grouped column in SELECT
70
  - For filtering, use the exact case as in the data
71
  - Return only one SQL statement ending without semicolon
72
-
73
  COLUMNS AVAILABLE:
74
  {", ".join(columns)}
75
-
76
  SCHEMA:
77
  {schema}
78
-
79
  QUESTION:
80
  {question}
81
  """
82
 
83
  url = (
84
- f"https://generativelanguage.googleapis.com"
85
- f"/v1beta/models/gemini-2.5-flash"
86
  f":generateContent?key={GEMINI_API_KEY}"
87
  )
88
 
89
  payload = json.dumps({
90
- "contents": [
91
- {
92
- "role": "user",
93
- "parts": [{"text": prompt}]
94
- }
95
- ]
96
  }).encode("utf-8")
97
 
98
- try:
99
- req = urllib.request.Request(
100
- url,
101
- data=payload,
102
- headers={"Content-Type": "application/json"}
103
- )
104
-
105
- with urllib.request.urlopen(req, timeout=30) as resp:
106
- data = json.loads(resp.read().decode())
107
-
108
  try:
109
- sql = data["candidates"][0]["content"]["parts"][0]["text"]
110
- except Exception:
111
- return ""
112
-
113
- if not sql:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  return ""
115
 
116
- sql = sql.replace("```sql", "").replace("```", "").strip()
117
- sql = sql.split(";")[0].strip()
118
-
119
- if "drop" in sql.lower() or "delete" in sql.lower():
120
- return f'SELECT * FROM "{table}" LIMIT 10'
121
-
122
- return sql
123
-
124
- except Exception as e:
125
- print("❌ GEMINI ERROR:", e)
126
- return ""
127
 
128
 
129
  # ─────────────────────────────
@@ -173,7 +178,6 @@ async def upload_csv(file: UploadFile = File(...)):
173
  table_name = re.sub(r"[^a-zA-Z0-9_]", "_", file.filename)
174
  if table_name and table_name[0].isdigit():
175
  table_name = "t_" + table_name
176
-
177
  table_name = table_name[:40]
178
 
179
  with tempfile.NamedTemporaryFile(delete=False) as tf:
@@ -185,7 +189,6 @@ async def upload_csv(file: UploadFile = File(...)):
185
  ).fetchone()[0]
186
 
187
  conn.close()
188
-
189
  db_bytes = open(tf.name, "rb").read()
190
 
191
  os.remove(tf.name)
@@ -195,7 +198,6 @@ async def upload_csv(file: UploadFile = File(...)):
195
  "table": table_name,
196
  "cols": list(df.columns)
197
  }
198
-
199
  _schema_store[session_id] = schema
200
 
201
  return {
@@ -217,14 +219,12 @@ async def upload_csv(file: UploadFile = File(...)):
217
  async def query(req: QueryRequest):
218
 
219
  data = _db_store.get(req.session_id)
220
-
221
  if not data and _db_store:
222
  data = list(_db_store.values())[-1]
223
-
224
  if not data:
225
  raise HTTPException(status_code=404, detail="No dataset loaded")
226
 
227
- table = data["table"]
228
  schema = (
229
  _schema_store.get(req.session_id)
230
  or list(_schema_store.values())[-1]
@@ -232,15 +232,17 @@ async def query(req: QueryRequest):
232
 
233
  sql = _call_gemini(req.question, schema, data["cols"], table)
234
 
235
- if not sql or "error" in sql.lower():
236
- sql = f'SELECT * FROM "{table}" LIMIT 10'
 
 
 
 
 
237
 
238
  results = execute_sql(sql, data["bytes"])
239
 
240
- return {
241
- "sql": sql,
242
- "results": results
243
- }
244
 
245
 
246
  # ─────────────────────────────
@@ -257,32 +259,23 @@ def health():
257
 
258
 
259
  # ─────────────────────────────
260
- # TELEGRAM WEBHOOK
 
 
261
  # ─────────────────────────────
262
 
263
- @app.on_event("startup")
264
- async def on_startup():
265
- try:
266
- from bot import setup_webhook
267
- setup_webhook()
268
- except Exception as e:
269
- print(f"⚠️ Webhook setup skipped: {e}")
270
 
 
 
 
271
 
272
- @app.post("/webhook/{token}")
273
- async def telegram_webhook(token: str, request: Request):
274
- expected = os.getenv("BOT_TOKEN", "")
275
- if token != expected:
276
- raise HTTPException(status_code=403, detail="Invalid token")
277
- try:
278
- import telebot
279
- from bot import bot
280
- json_data = await request.json()
281
- update = telebot.types.Update.de_json(json_data)
282
- bot.process_new_updates([update])
283
- except Exception as e:
284
- print(f"❌ Webhook error: {e}")
285
- return {"ok": True}
286
 
287
 
288
  # ─────────────────────────────
 
2
  import re
3
  import io
4
  import json
5
+ import time
6
  import sqlite3
7
  import tempfile
8
+ import threading
9
  import pandas as pd
10
  import urllib.request
11
+ import urllib.error
12
+
13
+ try:
14
+ import requests as _requests
15
+ HAS_REQUESTS = True
16
+ except ImportError:
17
+ HAS_REQUESTS = False
18
 
19
  from fastapi import FastAPI, File, UploadFile, HTTPException, Request
20
  from fastapi.staticfiles import StaticFiles
 
50
  question: str
51
 
52
 
 
 
 
 
 
 
 
 
53
  # ─────────────────────────────
54
  # GEMINI CALL
55
+ # retries=1 → fail fast, tell user immediately
56
+ # do NOT change retries back to 4
57
  # ─────────────────────────────
58
 
59
+ def _call_gemini(question: str, schema: str, columns: list, table: str,
60
+ retries: int = 1):
61
 
62
  if not GEMINI_API_KEY:
63
  return ""
64
 
65
  prompt = f"""
66
  You are an expert SQLite data analyst.
 
67
  Return ONLY a valid SQL query. No explanation, no markdown, no text before or after.
 
68
  STRICT RULES:
69
  - Use only the table "{table}"
70
  - Never use DROP or DELETE
71
  - For GROUP BY queries, always include the grouped column in SELECT
72
  - For filtering, use the exact case as in the data
73
  - Return only one SQL statement ending without semicolon
 
74
  COLUMNS AVAILABLE:
75
  {", ".join(columns)}
 
76
  SCHEMA:
77
  {schema}
 
78
  QUESTION:
79
  {question}
80
  """
81
 
82
  url = (
83
+ "https://generativelanguage.googleapis.com"
84
+ "/v1beta/models/gemini-2.5-flash"
85
  f":generateContent?key={GEMINI_API_KEY}"
86
  )
87
 
88
  payload = json.dumps({
89
+ "contents": [{"role": "user", "parts": [{"text": prompt}]}]
 
 
 
 
 
90
  }).encode("utf-8")
91
 
92
+ for attempt in range(1, retries + 1):
 
 
 
 
 
 
 
 
 
93
  try:
94
+ req = urllib.request.Request(
95
+ url,
96
+ data=payload,
97
+ headers={"Content-Type": "application/json"}
98
+ )
99
+ with urllib.request.urlopen(req, timeout=30) as resp:
100
+ data = json.loads(resp.read().decode())
101
+
102
+ try:
103
+ sql = data["candidates"][0]["content"]["parts"][0]["text"]
104
+ except Exception:
105
+ return ""
106
+
107
+ if not sql:
108
+ return ""
109
+
110
+ sql = sql.replace("```sql", "").replace("```", "").strip()
111
+ sql = sql.split(";")[0].strip()
112
+
113
+ if "drop" in sql.lower() or "delete" in sql.lower():
114
+ return f'SELECT * FROM "{table}" LIMIT 10'
115
+
116
+ return sql
117
+
118
+ except urllib.error.HTTPError as e:
119
+ if e.code == 429:
120
+ print(f"⚠️ Gemini 429 — rate limited (attempt {attempt}/{retries})")
121
+ # fail fast — do not retry, tell user immediately
122
+ return ""
123
+ else:
124
+ print(f"❌ GEMINI HTTP ERROR {e.code}: {e}")
125
+ return ""
126
+
127
+ except Exception as e:
128
+ print(f"❌ GEMINI ERROR: {e}")
129
  return ""
130
 
131
+ return ""
 
 
 
 
 
 
 
 
 
 
132
 
133
 
134
  # ─────────────────────────────
 
178
  table_name = re.sub(r"[^a-zA-Z0-9_]", "_", file.filename)
179
  if table_name and table_name[0].isdigit():
180
  table_name = "t_" + table_name
 
181
  table_name = table_name[:40]
182
 
183
  with tempfile.NamedTemporaryFile(delete=False) as tf:
 
189
  ).fetchone()[0]
190
 
191
  conn.close()
 
192
  db_bytes = open(tf.name, "rb").read()
193
 
194
  os.remove(tf.name)
 
198
  "table": table_name,
199
  "cols": list(df.columns)
200
  }
 
201
  _schema_store[session_id] = schema
202
 
203
  return {
 
219
  async def query(req: QueryRequest):
220
 
221
  data = _db_store.get(req.session_id)
 
222
  if not data and _db_store:
223
  data = list(_db_store.values())[-1]
 
224
  if not data:
225
  raise HTTPException(status_code=404, detail="No dataset loaded")
226
 
227
+ table = data["table"]
228
  schema = (
229
  _schema_store.get(req.session_id)
230
  or list(_schema_store.values())[-1]
 
232
 
233
  sql = _call_gemini(req.question, schema, data["cols"], table)
234
 
235
+ # If Gemini failed (429 or error), return clear error to Render bot
236
+ if not sql:
237
+ return {
238
+ "sql": "",
239
+ "results": [],
240
+ "error": "⚠️ AI busy (rate limit). Wait 60s and retry."
241
+ }
242
 
243
  results = execute_sql(sql, data["bytes"])
244
 
245
+ return {"sql": sql, "results": results}
 
 
 
246
 
247
 
248
  # ─────────────────────────────
 
259
 
260
 
261
  # ─────────────────────────────
262
+ # TELEGRAM WEBHOOK — DISABLED
263
+ # Telegram is now handled by Render bot (polling)
264
+ # Do not re-enable — causes conflict with Render polling
265
  # ─────────────────────────────
266
 
267
+ # @app.on_event("startup")
268
+ # async def on_startup():
269
+ # thread = threading.Thread(target=_background_webhook_setup, daemon=True)
270
+ # thread.start()
 
 
 
271
 
272
+ # @app.get("/set-webhook")
273
+ # async def set_webhook_endpoint():
274
+ # ...
275
 
276
+ # @app.post("/webhook/{token}")
277
+ # async def telegram_webhook(token: str, request: Request):
278
+ # ...
 
 
 
 
 
 
 
 
 
 
 
279
 
280
 
281
  # ─────────────────────────────