Al1Abdullah commited on
Commit
e05b320
·
verified ·
1 Parent(s): 41b903e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -25
app.py CHANGED
@@ -6,11 +6,16 @@ from groq import Groq
6
  from dotenv import load_dotenv
7
  import re
8
  import uuid
 
9
 
10
  app = Flask(__name__)
11
  app.secret_key = os.urandom(24) # Required for session management
12
  load_dotenv()
13
 
 
 
 
 
14
  # Default database configuration from .env
15
  default_db_config = {
16
  'host': os.getenv('DB_HOST'),
@@ -21,14 +26,20 @@ default_db_config = {
21
 
22
  # Validate default config at startup
23
  if not all([default_db_config['host'], default_db_config['user'], default_db_config['password']]):
24
- print("Warning: Incomplete default MySQL credentials in Secrets. Default connection may fail.")
 
 
 
 
 
25
 
26
  # Groq API configuration with error handling
27
  try:
28
  groq_client = Groq(api_key=os.getenv('GROQ_API_KEY'))
 
29
  except Exception as e:
30
  groq_client = None
31
- print(f"Failed to initialize Groq client: {str(e)}")
32
 
33
  # Temporary storage for current database name and schema
34
  current_db_name = None
@@ -36,21 +47,30 @@ current_schema = {}
36
  current_summary = {}
37
 
38
  def get_db_connection(db_name=None):
39
- """Establish a database connection using session or default config."""
40
- # Try session config first, but only if complete
41
- config = session.get('db_config', default_db_config).copy()
42
- if not all([config.get('host'), config.get('user'), config.get('password')]):
43
- # Fallback to default_db_config if session config is incomplete
 
 
44
  config = default_db_config.copy()
45
- if not all([config.get('host'), config.get('user'), config.get('password')]):
46
- return None, "No valid MySQL credentials provided. Ensure Secrets (DB_HOST, DB_USER, DB_PASSWORD, DB_PORT) are set correctly in Hugging Face Space settings, or use the 'Configure MySQL Connection' modal."
47
-
 
 
 
 
 
48
  if db_name:
49
  config['database'] = db_name
50
  try:
51
  conn = mysql.connector.connect(**config)
 
52
  return conn, None
53
  except Error as e:
 
54
  return None, f"Database connection failed: {str(e)}. Verify credentials, ensure the MySQL/TiDB server is running, and check network settings (e.g., IP whitelist, TLS)."
55
 
56
  def parse_sql_file(file_content):
@@ -63,6 +83,7 @@ def parse_sql_file(file_content):
63
  # Extract database name
64
  db_name_match = re.search(r"CREATE\s+DATABASE\s+[`']?(\w+)[`']?", file_content, re.IGNORECASE)
65
  db_name = db_name_match.group(1) if db_name_match else f"temp_db_{uuid.uuid4().hex[:8]}"
 
66
 
67
  # Split SQL into statements
68
  for line in file_content.splitlines():
@@ -85,6 +106,7 @@ def parse_sql_file(file_content):
85
 
86
  def generate_schema_summary(schema, db_name):
87
  """Generate a dynamic summary of any MySQL database schema."""
 
88
  summary = {
89
  'description': '',
90
  'main_tables': {},
@@ -169,41 +191,66 @@ def generate_schema_summary(schema, db_name):
169
  cursor.close()
170
  conn.close()
171
  except Error as e:
 
172
  summary['suggestions']['note'] = f'Analysis limited due to: {str(e)}'
173
  if not summary['relationships']:
174
  summary['relationships'] = ['Unable to detect relationships due to limited metadata access.']
 
 
 
175
 
176
  return summary
177
 
178
  def load_sql_file(file):
179
  """Load SQL file into MySQL database and generate schema summary."""
180
  global current_db_name, current_schema, current_summary
 
181
  try:
182
  file_content = file.read()
 
 
 
 
183
  db_name, statements = parse_sql_file(file_content)
184
 
185
  # Connect without specifying a database
186
  conn, error = get_db_connection()
187
  if error:
 
188
  return False, error, None
189
  cursor = conn.cursor()
190
 
191
  # Drop existing database if it exists
192
- cursor.execute(f"DROP DATABASE IF EXISTS `{db_name}`")
193
- cursor.execute(f"CREATE DATABASE `{db_name}`")
194
- conn.commit()
 
 
 
 
 
 
 
195
  cursor.close()
196
  conn.close()
197
 
198
  # Connect to the new database
199
  conn, error = get_db_connection(db_name)
200
  if error:
 
201
  return False, error, None
202
  cursor = conn.cursor()
203
 
204
  # Execute SQL statements
205
  for statement in statements:
206
- cursor.execute(statement)
 
 
 
 
 
 
 
207
  conn.commit()
208
 
209
  # Extract schema
@@ -214,6 +261,7 @@ def load_sql_file(file):
214
  cursor.execute(f"SHOW COLUMNS FROM `{table}`")
215
  columns = [row[0] for row in cursor.fetchall()]
216
  schema[table] = columns
 
217
 
218
  # Generate summary
219
  summary = generate_schema_summary(schema, db_name)
@@ -224,13 +272,16 @@ def load_sql_file(file):
224
 
225
  cursor.close()
226
  conn.close()
 
227
  return True, schema, summary
228
- except Error as e:
229
- return False, f"Failed to load SQL file: {str(e)}", None
 
230
 
231
  def generate_sql_query(question, schema):
232
  """Generate SQL query using Groq API with user-friendly aliases."""
233
  if not groq_client:
 
234
  return "ERROR: Groq client not initialized. Check API key and try again."
235
 
236
  schema_text = "\n".join([f"Table: {table}\nColumns: {', '.join(columns)}" for table, columns in schema.items()])
@@ -250,26 +301,32 @@ User Question: {question}
250
  query = response.choices[0].message.content.strip()
251
  query = re.sub(r'```(?:sql)?\n?', '', query) # Remove any markdown
252
  query = query.strip()
 
253
  return query
254
  except Exception as e:
 
255
  return f"ERROR: Failed to generate SQL query: {str(e)}"
256
 
257
  def execute_sql_query(query):
258
  """Execute SQL query on the current database."""
259
  if not current_db_name:
 
260
  return False, "No database loaded. Please upload an SQL file.", None
261
  conn, error = get_db_connection(current_db_name)
262
  if error:
 
263
  return False, error, None
264
  try:
265
  cursor = conn.cursor(dictionary=True)
266
  cursor.execute(query)
267
  results = cursor.fetchall()
268
  conn.commit()
 
269
  cursor.close()
270
  conn.close()
271
  return True, results, None
272
  except Error as e:
 
273
  return False, f"SQL execution failed: {str(e)}", None
274
 
275
  @app.route('/', methods=['GET', 'POST'])
@@ -282,67 +339,85 @@ def index():
282
 
283
  if not groq_client:
284
  error = "Groq client not initialized. Please check GROQ_API_KEY and restart the app."
 
285
 
286
  if request.method == 'POST':
 
287
  if 'sql_file' in request.files:
288
  file = request.files['sql_file']
 
 
 
289
  if file and file.filename.endswith('.sql'):
290
  success, result, summary = load_sql_file(file)
291
  if success:
292
  schema = result
 
293
  else:
294
  error = result
 
295
  else:
296
  error = "Please upload a valid .sql file."
 
297
  elif 'question' in request.form:
298
  question = request.form['question']
 
299
  if not current_db_name or not current_schema:
300
  error = "No database loaded. Please upload an SQL file first."
 
301
  else:
302
  generated_query = generate_sql_query(question, current_schema)
303
  if not generated_query.startswith('ERROR:'):
304
  success, result, _ = execute_sql_query(generated_query)
305
  if success:
306
  results = result
 
307
  else:
308
  error = result
 
309
  else:
310
  error = generated_query
 
311
 
 
 
312
  return render_template('index.html', error=error, schema=schema, summary=summary, results=results, query=generated_query)
313
 
314
  @app.route('/configure_db', methods=['POST'])
315
  def configure_db():
316
  """Handle MySQL connection configuration."""
 
317
  host = request.form.get('host', '').strip()
318
  user = request.form.get('user', '').strip()
319
  password = request.form.get('password', '')
320
  port = request.form.get('port', '4000').strip()
321
 
322
  if not host or not user:
323
- return render_template('index.html', error="Host and user are required for custom MySQL configuration.", schema=current_schema, summary=current_summary)
 
 
324
 
325
  try:
326
  port = int(port)
327
  except ValueError:
328
- return render_template('index.html', error="Port must be a valid number.", schema=current_schema, summary=current_summary)
 
 
329
 
330
  # Test connection
331
  test_config = {'host': host, 'user': user, 'password': password, 'port': port}
332
  conn, error = get_db_connection()
333
  if error:
 
334
  return render_template('index.html', error=error, schema=current_schema, summary=current_summary)
335
 
336
  # Store in session
337
  session['db_config'] = test_config
338
  conn.close()
339
- return render_template('index.html', error=None, schema=current_schema, summary=current_summary, success="Custom MySQL connection configured successfully. You can now upload .sql files and query your database.")
340
-
341
- @app.route('/reset_db', methods=['POST'])
342
- def reset_db():
343
- """Reset to default TiDB Serverless connection."""
344
- session.pop('db_config', None)
345
- return render_template('index.html', error=None, schema=current_schema, summary=current_summary, success="Reverted to default TiDB Serverless connection. You can now upload .sql files using the default database.")
346
 
347
  if __name__ == '__main__':
348
  app.run(host='0.0.0.0', port=int(os.getenv('PORT', 7860)), debug=False)
 
6
  from dotenv import load_dotenv
7
  import re
8
  import uuid
9
+ import logging
10
 
11
  app = Flask(__name__)
12
  app.secret_key = os.urandom(24) # Required for session management
13
  load_dotenv()
14
 
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
  # Default database configuration from .env
20
  default_db_config = {
21
  'host': os.getenv('DB_HOST'),
 
26
 
27
  # Validate default config at startup
28
  if not all([default_db_config['host'], default_db_config['user'], default_db_config['password']]):
29
+ logger.error("Incomplete default MySQL credentials in Secrets: host=%s, user=%s, password=%s, port=%s",
30
+ default_db_config['host'], default_db_config['user'], '***' if default_db_config['password'] else None,
31
+ default_db_config['port'])
32
+ else:
33
+ logger.info("Default MySQL credentials loaded successfully: host=%s, user=%s, port=%s",
34
+ default_db_config['host'], default_db_config['user'], default_db_config['port'])
35
 
36
  # Groq API configuration with error handling
37
  try:
38
  groq_client = Groq(api_key=os.getenv('GROQ_API_KEY'))
39
+ logger.info("Groq client initialized successfully")
40
  except Exception as e:
41
  groq_client = None
42
+ logger.error("Failed to initialize Groq client: %s", str(e))
43
 
44
  # Temporary storage for current database name and schema
45
  current_db_name = None
 
47
  current_summary = {}
48
 
49
  def get_db_connection(db_name=None):
50
+ """Establish a database connection using default config unless session config is explicitly set."""
51
+ # Use session config only if explicitly set and complete
52
+ if 'db_config' in session and all([session['db_config'].get('host'), session['db_config'].get('user'), session['db_config'].get('password')]):
53
+ config = session['db_config'].copy()
54
+ logger.info("Using session config for DB connection: host=%s, user=%s, port=%s",
55
+ config['host'], config['user'], config['port'])
56
+ else:
57
  config = default_db_config.copy()
58
+ logger.info("Using default config for DB connection: host=%s, user=%s, port=%s",
59
+ config['host'], config['user'], config['port'])
60
+
61
+ if not all([config.get('host'), config.get('user'), config.get('password')]):
62
+ logger.error("No valid MySQL credentials: host=%s, user=%s, password=%s",
63
+ config.get('host'), config.get('user'), '***' if config.get('password') else None)
64
+ return None, "No valid MySQL credentials provided. Ensure Secrets (DB_HOST, DB_USER, DB_PASSWORD, DB_PORT) are set in Hugging Face Space settings or configure a custom connection."
65
+
66
  if db_name:
67
  config['database'] = db_name
68
  try:
69
  conn = mysql.connector.connect(**config)
70
+ logger.info("Database connection successful%s", f" to {db_name}" if db_name else "")
71
  return conn, None
72
  except Error as e:
73
+ logger.error("Database connection failed: %s", str(e))
74
  return None, f"Database connection failed: {str(e)}. Verify credentials, ensure the MySQL/TiDB server is running, and check network settings (e.g., IP whitelist, TLS)."
75
 
76
  def parse_sql_file(file_content):
 
83
  # Extract database name
84
  db_name_match = re.search(r"CREATE\s+DATABASE\s+[`']?(\w+)[`']?", file_content, re.IGNORECASE)
85
  db_name = db_name_match.group(1) if db_name_match else f"temp_db_{uuid.uuid4().hex[:8]}"
86
+ logger.info("Parsed SQL file: database name=%s", db_name)
87
 
88
  # Split SQL into statements
89
  for line in file_content.splitlines():
 
106
 
107
  def generate_schema_summary(schema, db_name):
108
  """Generate a dynamic summary of any MySQL database schema."""
109
+ logger.info("Generating schema summary for database: %s", db_name)
110
  summary = {
111
  'description': '',
112
  'main_tables': {},
 
191
  cursor.close()
192
  conn.close()
193
  except Error as e:
194
+ logger.error("Schema summary error: %s", str(e))
195
  summary['suggestions']['note'] = f'Analysis limited due to: {str(e)}'
196
  if not summary['relationships']:
197
  summary['relationships'] = ['Unable to detect relationships due to limited metadata access.']
198
+ else:
199
+ logger.error("Failed to connect for schema summary: %s", error)
200
+ summary['suggestions']['note'] = 'Unable to analyze schema due to connection issues.'
201
 
202
  return summary
203
 
204
  def load_sql_file(file):
205
  """Load SQL file into MySQL database and generate schema summary."""
206
  global current_db_name, current_schema, current_summary
207
+ logger.info("Attempting to load SQL file")
208
  try:
209
  file_content = file.read()
210
+ if not file_content:
211
+ logger.error("Empty SQL file uploaded")
212
+ return False, "Uploaded SQL file is empty.", None
213
+
214
  db_name, statements = parse_sql_file(file_content)
215
 
216
  # Connect without specifying a database
217
  conn, error = get_db_connection()
218
  if error:
219
+ logger.error("Connection failed in load_sql_file: %s", error)
220
  return False, error, None
221
  cursor = conn.cursor()
222
 
223
  # Drop existing database if it exists
224
+ try:
225
+ cursor.execute(f"DROP DATABASE IF EXISTS `{db_name}`")
226
+ cursor.execute(f"CREATE DATABASE `{db_name}`")
227
+ conn.commit()
228
+ logger.info("Created database: %s", db_name)
229
+ except Error as e:
230
+ logger.error("Failed to create database %s: %s", db_name, str(e))
231
+ cursor.close()
232
+ conn.close()
233
+ return False, f"Failed to create database: {str(e)}", None
234
  cursor.close()
235
  conn.close()
236
 
237
  # Connect to the new database
238
  conn, error = get_db_connection(db_name)
239
  if error:
240
+ logger.error("Connection to %s failed: %s", db_name, error)
241
  return False, error, None
242
  cursor = conn.cursor()
243
 
244
  # Execute SQL statements
245
  for statement in statements:
246
+ try:
247
+ cursor.execute(statement)
248
+ logger.info("Executed statement: %s", statement[:50])
249
+ except Error as e:
250
+ logger.error("Failed to execute statement: %s, error: %s", statement[:50], str(e))
251
+ cursor.close()
252
+ conn.close()
253
+ return False, f"Failed to execute SQL statement: {str(e)}", None
254
  conn.commit()
255
 
256
  # Extract schema
 
261
  cursor.execute(f"SHOW COLUMNS FROM `{table}`")
262
  columns = [row[0] for row in cursor.fetchall()]
263
  schema[table] = columns
264
+ logger.info("Extracted schema for table: %s", table)
265
 
266
  # Generate summary
267
  summary = generate_schema_summary(schema, db_name)
 
272
 
273
  cursor.close()
274
  conn.close()
275
+ logger.info("SQL file loaded successfully: %s", db_name)
276
  return True, schema, summary
277
+ except Exception as e:
278
+ logger.error("Unexpected error in load_sql_file: %s", str(e))
279
+ return False, f"Unexpected error while loading SQL file: {str(e)}", None
280
 
281
  def generate_sql_query(question, schema):
282
  """Generate SQL query using Groq API with user-friendly aliases."""
283
  if not groq_client:
284
+ logger.error("Groq client not initialized")
285
  return "ERROR: Groq client not initialized. Check API key and try again."
286
 
287
  schema_text = "\n".join([f"Table: {table}\nColumns: {', '.join(columns)}" for table, columns in schema.items()])
 
301
  query = response.choices[0].message.content.strip()
302
  query = re.sub(r'```(?:sql)?\n?', '', query) # Remove any markdown
303
  query = query.strip()
304
+ logger.info("Generated SQL query: %s", query[:100])
305
  return query
306
  except Exception as e:
307
+ logger.error("Failed to generate SQL query: %s", str(e))
308
  return f"ERROR: Failed to generate SQL query: {str(e)}"
309
 
310
  def execute_sql_query(query):
311
  """Execute SQL query on the current database."""
312
  if not current_db_name:
313
+ logger.error("No database loaded for query execution")
314
  return False, "No database loaded. Please upload an SQL file.", None
315
  conn, error = get_db_connection(current_db_name)
316
  if error:
317
+ logger.error("Connection failed for query execution: %s", error)
318
  return False, error, None
319
  try:
320
  cursor = conn.cursor(dictionary=True)
321
  cursor.execute(query)
322
  results = cursor.fetchall()
323
  conn.commit()
324
+ logger.info("Query executed successfully: %s", query[:50])
325
  cursor.close()
326
  conn.close()
327
  return True, results, None
328
  except Error as e:
329
+ logger.error("SQL execution failed: %s", str(e))
330
  return False, f"SQL execution failed: {str(e)}", None
331
 
332
  @app.route('/', methods=['GET', 'POST'])
 
339
 
340
  if not groq_client:
341
  error = "Groq client not initialized. Please check GROQ_API_KEY and restart the app."
342
+ logger.error(error)
343
 
344
  if request.method == 'POST':
345
+ logger.info("Received POST request")
346
  if 'sql_file' in request.files:
347
  file = request.files['sql_file']
348
+ logger.info("SQL file upload detected: %s", file.filename if file else "No file")
349
+ # Clear session config to ensure default TiDB backend is used for uploads
350
+ session.pop('db_config', None)
351
  if file and file.filename.endswith('.sql'):
352
  success, result, summary = load_sql_file(file)
353
  if success:
354
  schema = result
355
+ logger.info("SQL file loaded successfully")
356
  else:
357
  error = result
358
+ logger.error("Failed to load SQL file: %s", error)
359
  else:
360
  error = "Please upload a valid .sql file."
361
+ logger.error(error)
362
  elif 'question' in request.form:
363
  question = request.form['question']
364
+ logger.info("Received question: %s", question)
365
  if not current_db_name or not current_schema:
366
  error = "No database loaded. Please upload an SQL file first."
367
+ logger.error(error)
368
  else:
369
  generated_query = generate_sql_query(question, current_schema)
370
  if not generated_query.startswith('ERROR:'):
371
  success, result, _ = execute_sql_query(generated_query)
372
  if success:
373
  results = result
374
+ logger.info("Query executed successfully, results: %d rows", len(results))
375
  else:
376
  error = result
377
+ logger.error("Query execution failed: %s", error)
378
  else:
379
  error = generated_query
380
+ logger.error("Query generation failed: %s", error)
381
 
382
+ logger.info("Rendering index.html: error=%s, schema=%s, summary=%s, results=%s",
383
+ error, bool(schema), bool(summary), bool(results))
384
  return render_template('index.html', error=error, schema=schema, summary=summary, results=results, query=generated_query)
385
 
386
  @app.route('/configure_db', methods=['POST'])
387
  def configure_db():
388
  """Handle MySQL connection configuration."""
389
+ logger.info("Received configure_db request")
390
  host = request.form.get('host', '').strip()
391
  user = request.form.get('user', '').strip()
392
  password = request.form.get('password', '')
393
  port = request.form.get('port', '4000').strip()
394
 
395
  if not host or not user:
396
+ logger.error("Missing host or user in configure_db")
397
+ return render_template('index.html', error="Host and user are required for custom MySQL configuration.",
398
+ schema=current_schema, summary=current_summary)
399
 
400
  try:
401
  port = int(port)
402
  except ValueError:
403
+ logger.error("Invalid port number: %s", port)
404
+ return render_template('index.html', error="Port must be a valid number.",
405
+ schema=current_schema, summary=current_summary)
406
 
407
  # Test connection
408
  test_config = {'host': host, 'user': user, 'password': password, 'port': port}
409
  conn, error = get_db_connection()
410
  if error:
411
+ logger.error("Test connection failed in configure_db: %s", error)
412
  return render_template('index.html', error=error, schema=current_schema, summary=current_summary)
413
 
414
  # Store in session
415
  session['db_config'] = test_config
416
  conn.close()
417
+ logger.info("Custom MySQL connection configured: host=%s, user=%s, port=%s",
418
+ host, user, port)
419
+ return render_template('index.html', error=None, schema=current_schema, summary=current_summary,
420
+ success="Custom MySQL connection configured successfully. You can now upload .sql files and query your database.")
 
 
 
421
 
422
  if __name__ == '__main__':
423
  app.run(host='0.0.0.0', port=int(os.getenv('PORT', 7860)), debug=False)