Yash-dev-1744 commited on
Commit
9c68528
Β·
0 Parent(s):

Initial commit

Browse files
Files changed (4) hide show
  1. .gitignore +151 -0
  2. README.md +103 -0
  3. requirements.txt +38 -0
  4. server.py +1261 -0
.gitignore ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .nox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ *.py,cover
48
+ .hypothesis/
49
+ .pytest_cache/
50
+ cover/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff
57
+ e.g. *.log
58
+
59
+ # Flask stuff:
60
+ instance/
61
+ .webassets-cache
62
+
63
+ # Scrapy stuff:
64
+ .scrapy
65
+
66
+ # Sphinx documentation
67
+ docs/_build/
68
+
69
+ # PyBuilder
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
+ # For a library or package, you might want to ignore these files since the code is
82
+ # intended to be run from anywhere
83
+ .python-version
84
+
85
+ # pipenv
86
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
87
+ # However, in case of collaboration, you should exclude Pipfile.lock
88
+ Pipfile.lock
89
+
90
+ # poetry
91
+ # Similar to Pipfile.lock, it is generally recommended to not include poetry.lock in version control.
92
+ poetry.lock
93
+
94
+ # pdm
95
+ # Similar to Pipfile.lock, it is generally recommended to not include the pdm.lock in version control.
96
+ pdm.lock
97
+
98
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
99
+ __pypackages__/
100
+
101
+ # Celery stuff
102
+ celerybeat-schedule
103
+ celerybeat.pid
104
+
105
+ # SageMath parsed files
106
+ *.sage.py
107
+
108
+ # Environments
109
+ .env
110
+ .venv
111
+ env/
112
+ venv/
113
+ ENV/
114
+ env.bak/
115
+ venv.bak/
116
+
117
+ # Spyder project settings
118
+ .spyderproject
119
+ .spyproject
120
+
121
+ # Rope project settings
122
+ .ropeproject
123
+
124
+ # mkdocs documentation
125
+ /site
126
+
127
+ # mypy
128
+ .mypy_cache/
129
+ .dmypy.json
130
+ dmypy.json
131
+
132
+ # Pyre type checker
133
+ .pyre/
134
+
135
+ # pytype static type analyzer
136
+ .pytype/
137
+
138
+ # Cython debug symbols
139
+ cython_debug/
140
+
141
+ # PyCharm
142
+ .idea/
143
+
144
+ # VS Code
145
+ .vscode/
146
+
147
+ # Custom for this project
148
+ # Ignore any potential logs or outputs
149
+ logs/
150
+ output/
151
+ *.log
README.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <iframe width="560" height="315" src="https://www.youtube.com/embed/0kAYAxU7YsQ?si=6ENuemAOXeLNyh-I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
2
+
3
+ # πŸ€– AI Database Query Assistant
4
+
5
+ A powerful AI-powered database query assistant that allows you to interact with your MySQL database using natural language.
6
+
7
+ ## 🏷️ Tags
8
+ - agent-demo-track
9
+
10
+ ## ✨ Features
11
+
12
+ ### Database Connection
13
+ - Secure MySQL database connection with support for:
14
+ - Custom host and port configuration
15
+ - Username and password authentication
16
+ - Special character support in passwords
17
+ - Default port (3306) handling
18
+ - Connection status monitoring
19
+
20
+ ### AI-Powered Query Processing
21
+ - Natural language to SQL conversion using Google's Gemini model
22
+ - Intelligent query validation and error handling
23
+ - Automatic query correction and optimization
24
+ - Support for complex SQL operations including:
25
+ - Joins
26
+ - Aggregations
27
+ - Subqueries
28
+ - Complex functions
29
+
30
+ ### Database Analysis
31
+ - Comprehensive database structure analysis
32
+ - Table schema visualization
33
+ - Column information display
34
+ - Primary and foreign key relationship mapping
35
+ - Row count statistics
36
+
37
+ ### Query Features
38
+ - Natural language question processing
39
+ - SQL query generation and execution
40
+ - Query performance metrics:
41
+ - Execution time tracking
42
+ - Row count statistics
43
+ - Query complexity analysis
44
+ - Performance insights
45
+
46
+ ### Query History
47
+ - Track and store query history
48
+ - View past queries with:
49
+ - Timestamp
50
+ - Original question
51
+ - Generated SQL
52
+ - Execution metrics
53
+ - Complexity level
54
+ - Reuse previous queries
55
+ - Clear history functionality
56
+
57
+ ### Security Features
58
+ - Secure password handling
59
+ - SQL injection prevention
60
+ - Query validation
61
+ - API key management:
62
+ - Environment variable support
63
+ - User-provided key option
64
+ - Secure key storage
65
+
66
+ ### User Interface
67
+ - Modern Gradio-based interface
68
+ - Tabbed navigation
69
+ - Real-time status updates
70
+ - Interactive query input
71
+ - Formatted result display
72
+ - Example questions for guidance
73
+
74
+ ### Performance Monitoring
75
+ - Query execution time tracking
76
+ - Complexity analysis
77
+ - Performance insights
78
+ - Resource usage optimization
79
+
80
+ ### Error Handling
81
+ - Comprehensive error detection
82
+ - User-friendly error messages
83
+ - Automatic query correction
84
+ - Detailed error reporting
85
+
86
+ ## πŸš€ Getting Started
87
+
88
+ 1. Install the required dependencies
89
+ 2. Set up your Google API key (for Gemini model)
90
+ 3. Configure your MySQL database connection
91
+ 4. Launch the application
92
+ 5. Start querying your database using natural language!
93
+
94
+ ## πŸ”§ Requirements
95
+
96
+ - Python 3.x
97
+ - MySQL database
98
+ - Google API key for Gemini model
99
+ - Required Python packages (see requirements.txt)
100
+
101
+ ## πŸ“ License
102
+
103
+ This project is licensed under the MIT License - see the LICENSE file for details.
requirements.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core web framework
2
+ gradio>=4.0.0
3
+
4
+ # Database connectivity
5
+ mysql-connector-python>=8.2.0
6
+ psycopg2-binary>=2.9.7
7
+ PyMySQL>=1.1.0
8
+
9
+ # LangChain and AI
10
+ langchain>=0.1.0
11
+ langchain-google-genai>=1.0.0
12
+ langchain-community>=0.0.20
13
+ langchain-experimental>=0.0.50
14
+
15
+ # Database ORM and utilities
16
+ SQLAlchemy>=2.0.0
17
+ pandas>=2.0.0
18
+ numpy>=1.24.0
19
+
20
+ # Environment and configuration
21
+ python-dotenv>=1.0.0
22
+
23
+ # Additional utilities
24
+ requests>=2.31.0
25
+ pydantic>=2.0.0
26
+
27
+ # URL encoding for database connections
28
+ urllib3>=1.26.0
29
+
30
+ # Visualization and formatting
31
+ #matplotlib>=3.7.0
32
+ #seaborn>=0.12.0
33
+ #tabulate>=0.9.0
34
+
35
+ # Optional: For enhanced database support
36
+ # Uncomment if needed
37
+ # cx-Oracle>=8.3.0 # For Oracle database support
38
+ # pymongo>=4.0.0 # For MongoDB support
server.py ADDED
@@ -0,0 +1,1261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import os
4
+ import json
5
+ import re
6
+ from typing import Optional, Tuple, Dict, Any, List
7
+ import traceback
8
+ from datetime import datetime
9
+ import time
10
+
11
+ # Database imports
12
+ import mysql.connector
13
+ from sqlalchemy import create_engine, inspect, text
14
+ from sqlalchemy.exc import SQLAlchemyError
15
+
16
+ # LangChain imports
17
+ from langchain_community.agent_toolkits.sql.base import create_sql_agent
18
+ from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit
19
+ from langchain_community.utilities import SQLDatabase
20
+ from langchain.agents.agent_types import AgentType
21
+ from langchain_community.callbacks.manager import get_openai_callback
22
+ from langchain_google_genai import ChatGoogleGenerativeAI
23
+
24
+ # Environment setup
25
+ from dotenv import load_dotenv
26
+ load_dotenv()
27
+
28
+ class DatabaseManager:
29
+ def __init__(self):
30
+ self.db_connection = None
31
+ self.db_context = None
32
+ self.sql_agent = None
33
+ self.connection_status = "Not Connected"
34
+ self.db_type = None
35
+ self.query_history = [] # Store query history
36
+ self.max_history_items = 20 # Maximum number of history items to keep
37
+ self.user_api_key = None # Store user-provided API key
38
+
39
+ def set_api_key(self, api_key: str) -> str:
40
+ """Set user-provided API key"""
41
+ if not api_key or not api_key.strip():
42
+ self.user_api_key = None
43
+ return "❌ API key cleared. Using environment variable if available."
44
+
45
+ # Store the API key
46
+ self.user_api_key = api_key.strip()
47
+ return "βœ… API key set successfully!"
48
+
49
+ def get_api_key(self) -> str:
50
+ """Get API key with priority to user-provided key"""
51
+ if self.user_api_key:
52
+ return self.user_api_key
53
+ return os.getenv("GOOGLE_API_KEY", "")
54
+
55
+ def connect_mysql(self, host: str, port: str, username: str, password: str, database: str) -> Tuple[str, str]:
56
+ """Connect to MySQL database"""
57
+ try:
58
+ # Clean and validate inputs
59
+ host = host.strip() if host else "localhost"
60
+ port_num = int(port.strip()) if port and port.strip() else 3306
61
+ username = username.strip() if username else ""
62
+ password = str(password) if password else "" # Ensure password is treated as string
63
+ database = database.strip() if database else ""
64
+
65
+ if not username or not database:
66
+ return "❌ Missing required fields", "Please provide username and database name."
67
+
68
+ # Test connection first with mysql.connector
69
+ # Using raw credentials without URL encoding for direct connection
70
+ conn = mysql.connector.connect(
71
+ host=host,
72
+ port=port_num,
73
+ user=username,
74
+ password=password,
75
+ database=database,
76
+ autocommit=True
77
+ )
78
+ conn.close()
79
+
80
+ # Create SQLAlchemy engine with proper URL encoding
81
+ from urllib.parse import quote_plus
82
+ # Make sure to properly encode all special characters in password
83
+ encoded_password = quote_plus(str(password))
84
+ encoded_username = quote_plus(username)
85
+ encoded_database = quote_plus(database)
86
+
87
+ # Add binary_prefix=true to handle binary data warnings
88
+ connection_string = f"mysql+pymysql://{encoded_username}:{encoded_password}@{host}:{port_num}/{encoded_database}?binary_prefix=true"
89
+ engine = create_engine(connection_string, echo=False)
90
+
91
+ # Test SQLAlchemy connection
92
+ with engine.connect() as conn:
93
+ conn.execute(text("SELECT 1"))
94
+
95
+ # Create LangChain SQLDatabase
96
+ self.db_connection = SQLDatabase(engine)
97
+ self.db_type = "MySQL"
98
+ self.connection_status = f"βœ… Connected to MySQL: {host}:{port_num}/{database}"
99
+
100
+ return self.connection_status, "Connection successful! You can now analyze the database."
101
+
102
+ except Exception as e:
103
+ error_msg = f"❌ MySQL Connection Failed: {str(e)}"
104
+ self.connection_status = "Not Connected"
105
+ return error_msg, f"Connection failed. Please check your credentials.\nError details: {str(e)}"
106
+
107
+ def validate_sql_query(self, sql_query: str) -> Tuple[bool, str]:
108
+ """
109
+ Validate SQL query for common errors and security issues
110
+
111
+ Args:
112
+ sql_query: SQL query string to validate
113
+
114
+ Returns:
115
+ Tuple of (is_valid, message)
116
+ """
117
+ if not sql_query or not isinstance(sql_query, str):
118
+ return False, "Invalid or empty SQL query"
119
+
120
+ sql_query = sql_query.strip()
121
+
122
+ # Check for basic SQL injection patterns
123
+ dangerous_patterns = [
124
+ "DROP TABLE", "DROP DATABASE", "DELETE FROM", "TRUNCATE TABLE",
125
+ "ALTER TABLE", "UPDATE", "INSERT INTO", "CREATE TABLE", "GRANT",
126
+ "REVOKE", "--", ";--", ";", "/*", "*/"
127
+ ]
128
+
129
+ for pattern in dangerous_patterns:
130
+ if pattern.upper() in sql_query.upper():
131
+ return False, f"Potentially harmful SQL detected: {pattern}"
132
+
133
+ # Check for common SQL errors
134
+ common_errors = [
135
+ # NOT IN with NULL values
136
+ (r"NOT\s+IN.*NULL", "Using NOT IN with NULL values can lead to unexpected results"),
137
+ # BETWEEN for exclusive ranges
138
+ (r"BETWEEN.*AND", "Check BETWEEN usage for correct inclusive/exclusive ranges"),
139
+ # Potential data type mismatches
140
+ (r"CAST\(|CONVERT\(", "Verify data type casting is correct"),
141
+ # Potential quoting issues
142
+ (r"[^']'[^']|[^']'$", "Check for proper quoting of identifiers")
143
+ ]
144
+
145
+ import re
146
+ for pattern, message in common_errors:
147
+ if re.search(pattern, sql_query, re.IGNORECASE):
148
+ # This is just a warning, not an error
149
+ return True, f"Warning: {message}"
150
+
151
+ # Check for SELECT statement
152
+ if not sql_query.upper().startswith("SELECT"):
153
+ return False, "Only SELECT queries are allowed"
154
+
155
+ return True, "Query validation passed"
156
+
157
+ def fix_sql_query(self, sql_query: str, error_message: str, db_schema: Optional[dict] = None) -> str:
158
+ """
159
+ Use LLM to fix an invalid SQL query
160
+
161
+ Args:
162
+ sql_query: The original invalid SQL query
163
+ error_message: The error message from validation or execution
164
+ db_schema: Optional database schema information to help with correction
165
+
166
+ Returns:
167
+ Corrected SQL query
168
+ """
169
+ api_key = self.get_api_key()
170
+ if not api_key:
171
+ raise ValueError("No API key available. Please set a Google API key.")
172
+
173
+ # Initialize LLM
174
+ llm = ChatGoogleGenerativeAI(
175
+ model="gemini-2.5-flash-preview-05-20",
176
+ temperature=0,
177
+ google_api_key=api_key
178
+ )
179
+
180
+ # Prepare schema information if available
181
+ schema_info = ""
182
+ if db_schema and isinstance(db_schema, dict):
183
+ schema_info = "Database schema information:\n"
184
+ for table, info in db_schema.items():
185
+ schema_info += f"Table: {table}\n"
186
+ if "columns" in info:
187
+ schema_info += "Columns:\n"
188
+ for col in info["columns"]:
189
+ schema_info += f"- {col['name']} ({col['type']})\n"
190
+ schema_info += "\n"
191
+
192
+ # Build prompt for the LLM
193
+ prompt = f"""
194
+ Fix the following SQL query that has errors:
195
+
196
+ ```sql
197
+ {sql_query}
198
+ ```
199
+
200
+ Error message:
201
+ {error_message}
202
+
203
+ {schema_info}
204
+
205
+ Please provide ONLY the corrected SQL query with no additional text or explanation.
206
+ The query should be a valid SELECT statement.
207
+ """
208
+
209
+ # Get the corrected query
210
+ try:
211
+ response = llm.invoke(prompt)
212
+ corrected_query = response.content
213
+
214
+ # Extract SQL from response if needed
215
+ if "```sql" in corrected_query:
216
+ corrected_query = corrected_query.split("```sql")[1].split("```")[0].strip()
217
+ elif "```" in corrected_query:
218
+ corrected_query = corrected_query.split("```")[1].strip()
219
+
220
+ return corrected_query
221
+ except Exception as e:
222
+ # If correction fails, return the original query
223
+ return sql_query
224
+
225
+ def analyze_database(self) -> Tuple[str, str]:
226
+ """Analyze database structure and create context"""
227
+ if not self.db_connection:
228
+ return "❌ No database connection", "Please connect to a database first."
229
+
230
+ try:
231
+ # Get database schema information
232
+ inspector = inspect(self.db_connection._engine)
233
+ tables = inspector.get_table_names()
234
+
235
+ context_info = {
236
+ "database_type": self.db_type,
237
+ "total_tables": len(tables),
238
+ "tables": {},
239
+ "analysis_timestamp": datetime.now().isoformat()
240
+ }
241
+
242
+ # Analyze each table
243
+ for table in tables[:10]: # Limit to first 10 tables for performance
244
+ try:
245
+ columns = inspector.get_columns(table)
246
+ primary_keys = inspector.get_pk_constraint(table)
247
+ foreign_keys = inspector.get_foreign_keys(table)
248
+
249
+ # Get sample data count
250
+ with self.db_connection._engine.connect() as conn:
251
+ result = conn.execute(text(f"SELECT COUNT(*) FROM {table}"))
252
+ row_count = result.scalar()
253
+
254
+ context_info["tables"][table] = {
255
+ "columns": [{"name": col["name"], "type": str(col["type"])} for col in columns],
256
+ "primary_keys": primary_keys["constrained_columns"] if primary_keys else [],
257
+ "foreign_keys": [{"columns": fk["constrained_columns"], "refers_to": f"{fk['referred_table']}.{fk['referred_columns']}"} for fk in foreign_keys],
258
+ "row_count": row_count
259
+ }
260
+ except Exception as table_error:
261
+ context_info["tables"][table] = {"error": str(table_error)}
262
+
263
+ self.db_context = context_info
264
+
265
+ # Initialize Gemini LLM
266
+ api_key = self.get_api_key()
267
+ if not api_key:
268
+ return "❌ Analysis Failed", "Please set a Google API key in the settings or environment variables"
269
+
270
+ llm = ChatGoogleGenerativeAI(
271
+ model="gemini-2.5-flash-preview-05-20",
272
+ temperature=0,
273
+ google_api_key=api_key
274
+ )
275
+
276
+ # Create SQL agent
277
+ toolkit = SQLDatabaseToolkit(db=self.db_connection, llm=llm)
278
+ self.sql_agent = create_sql_agent(
279
+ llm=llm,
280
+ toolkit=toolkit,
281
+ agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
282
+ verbose=True,
283
+ handle_parsing_errors=True
284
+ )
285
+
286
+ summary = f"""
287
+ βœ… Database Analysis Complete!
288
+
289
+ πŸ“Š Database: {self.db_type}
290
+ πŸ“‹ Tables Found: {len(tables)}
291
+ πŸ” Analyzed Tables: {min(len(tables), 10)}
292
+
293
+ Ready for natural language queries!
294
+ """
295
+
296
+ detailed_info = json.dumps(context_info, indent=2)
297
+ return summary, f"Database context saved. You can now ask questions!\n\nDetailed Analysis:\n{detailed_info}"
298
+
299
+ except Exception as e:
300
+ error_msg = f"❌ Analysis Failed: {str(e)}"
301
+ return error_msg, f"Error during analysis: {traceback.format_exc()}"
302
+
303
+ def query_database(self, question: str) -> Tuple[str, str]:
304
+ """Process natural language query and return results"""
305
+ if not self.sql_agent:
306
+ # Convert error to table format with clean RDBMS style
307
+ df_error = pd.DataFrame({"Message": ["Please connect and analyze database first."]})
308
+ table_html = df_error.to_html(index=False, classes="table table-bordered table-striped", border=0)
309
+ return "❌ Not Ready", table_html
310
+
311
+ if not question.strip():
312
+ # Convert error to table format with clean RDBMS style
313
+ df_error = pd.DataFrame({"Message": ["Please enter a question about your database."]})
314
+ table_html = df_error.to_html(index=False, classes="table table-bordered table-striped", border=0)
315
+ return "❌ Empty Query", table_html
316
+
317
+ try:
318
+ # Track query start time for overall performance
319
+ start_time = time.time()
320
+
321
+ # Process the query with the agent
322
+ result = self.sql_agent.run(question)
323
+
324
+ # Try to extract and execute the SQL query for tabular display
325
+ try:
326
+ # Look for SQL in the result
327
+ if "SELECT" in result.upper():
328
+ # Extract SQL query (this is a simple extraction, could be improved)
329
+ lines = result.split('\n')
330
+ sql_lines = [line for line in lines if 'SELECT' in line.upper()]
331
+
332
+ if sql_lines:
333
+ sql_query = sql_lines[0].strip()
334
+ # Clean up the SQL query
335
+ sql_query = sql_query.replace('sql', '').replace('```', '').strip()
336
+
337
+ # Validate the SQL query before execution
338
+ is_valid, validation_message = self.validate_sql_query(sql_query)
339
+
340
+ # If query is invalid, try to fix it
341
+ correction_applied = False
342
+ if not is_valid:
343
+ # Get schema information for the correction agent
344
+ schema_info = self.db_context["tables"] if self.db_context else None
345
+
346
+ # Try to fix the query
347
+ corrected_query = self.fix_sql_query(sql_query, validation_message, schema_info)
348
+
349
+ # Validate the corrected query
350
+ is_valid_corrected, validation_message_corrected = self.validate_sql_query(corrected_query)
351
+
352
+ if is_valid_corrected:
353
+ sql_query = corrected_query
354
+ validation_message = validation_message_corrected
355
+ correction_applied = True
356
+ is_valid = True
357
+ else:
358
+ # If correction also failed, return both errors in table format
359
+ error_msg = f"The generated SQL query failed validation: {validation_message}\n\nAttempted correction also failed: {validation_message_corrected}\n\nOriginal result:\n{result}"
360
+ df_error = pd.DataFrame({"Error": [error_msg]})
361
+ table_html = df_error.to_html(index=False, classes="table table-bordered table-striped", border=0)
362
+ return "❌ Query Validation Failed", table_html
363
+
364
+ # If there's a warning but query is valid, add it to the result
365
+ warning_message = ""
366
+ if validation_message.startswith("Warning:"):
367
+ warning_message = f"\n\n⚠️ {validation_message}"
368
+
369
+ # Add correction notice if applicable
370
+ if correction_applied:
371
+ warning_message += f"\n\nπŸ”§ Query was automatically corrected. Original query had issues: {validation_message}"
372
+
373
+ # Execute the query to get structured data
374
+ try:
375
+ # Measure query performance
376
+ performance_metrics = self.measure_query_performance(sql_query)
377
+
378
+ if performance_metrics.get("success", False):
379
+ # Get the data from the metrics
380
+ with self.db_connection._engine.connect() as conn:
381
+ df = pd.read_sql(sql_query, conn)
382
+
383
+ # Calculate overall processing time
384
+ total_time_ms = round((time.time() - start_time) * 1000, 2)
385
+
386
+ # Add query to history
387
+ history_item = {
388
+ "question": question,
389
+ "sql_query": sql_query,
390
+ "execution_time_ms": performance_metrics["execution_time_ms"],
391
+ "total_time_ms": total_time_ms,
392
+ "row_count": performance_metrics["row_count"],
393
+ "complexity": performance_metrics["complexity"]["level"],
394
+ "timestamp": datetime.now().isoformat()
395
+ }
396
+ self.add_to_query_history(history_item)
397
+
398
+ # Generate performance and complexity insights
399
+ complexity = performance_metrics["complexity"]
400
+ perf_insights = f"\n\nπŸ“Š Query Metrics:\n"
401
+ perf_insights += f"β€’ Execution time: {performance_metrics['execution_time_ms']}ms\n"
402
+ perf_insights += f"β€’ Total processing time: {total_time_ms}ms\n"
403
+ perf_insights += f"β€’ Rows returned: {performance_metrics['row_count']}\n"
404
+ perf_insights += f"β€’ Complexity: {complexity['level']}\n"
405
+
406
+ if complexity["insights"]:
407
+ perf_insights += "\nπŸ” Insights:\n"
408
+ for insight in complexity["insights"]:
409
+ perf_insights += f"β€’ {insight}\n"
410
+
411
+ if not df.empty:
412
+ # Format table in RDBMS style
413
+ table_html = df.to_html(index=False, classes="table table-bordered table-striped", border=0)
414
+ # Add custom styling to make it look more like RDBMS output
415
+ table_html = f"""
416
+ <style>
417
+ .table-bordered {{
418
+ border-collapse: collapse;
419
+ width: 100%;
420
+ font-family: 'Courier New', Courier, monospace;
421
+ }}
422
+ .table-bordered th {{
423
+ background-color: #f2f2f2;
424
+ color: #333;
425
+ font-weight: bold;
426
+ text-align: left;
427
+ padding: 8px;
428
+ border: 1px solid #ddd;
429
+ }}
430
+ .table-bordered td {{
431
+ padding: 8px;
432
+ border: 1px solid #ddd;
433
+ }}
434
+ .table-striped tbody tr:nth-of-type(odd) {{
435
+ background-color: rgba(0,0,0,.05);
436
+ }}
437
+ </style>
438
+ {table_html}
439
+ """
440
+ return f"βœ… Query Successful\n\n{result}{warning_message}{perf_insights}", table_html
441
+ else:
442
+ # If performance measurement failed, continue with normal execution
443
+ with self.db_connection._engine.connect() as conn:
444
+ df = pd.read_sql(sql_query, conn)
445
+
446
+ if not df.empty:
447
+ # Format table in RDBMS style
448
+ table_html = df.to_html(index=False, classes="table table-bordered table-striped", border=0)
449
+ # Add custom styling
450
+ table_html = f"""
451
+ <style>
452
+ .table-bordered {{
453
+ border-collapse: collapse;
454
+ width: 100%;
455
+ font-family: 'Courier New', Courier, monospace;
456
+ }}
457
+ .table-bordered th {{
458
+ background-color: #f2f2f2;
459
+ color: #333;
460
+ font-weight: bold;
461
+ text-align: left;
462
+ padding: 8px;
463
+ border: 1px solid #ddd;
464
+ }}
465
+ .table-bordered td {{
466
+ padding: 8px;
467
+ border: 1px solid #ddd;
468
+ }}
469
+ .table-striped tbody tr:nth-of-type(odd) {{
470
+ background-color: rgba(0,0,0,.05);
471
+ }}
472
+ </style>
473
+ {table_html}
474
+ """
475
+ return f"βœ… Query Successful\n\n{result}{warning_message}", table_html
476
+
477
+ except SQLAlchemyError as exec_error:
478
+ # If execution fails, try to fix the query again with the specific error
479
+ if not correction_applied:
480
+ schema_info = self.db_context["tables"] if self.db_context else None
481
+ corrected_query = self.fix_sql_query(sql_query, str(exec_error), schema_info)
482
+
483
+ # Try executing the corrected query
484
+ try:
485
+ # Measure performance of corrected query
486
+ performance_metrics = self.measure_query_performance(corrected_query)
487
+
488
+ if performance_metrics.get("success", False):
489
+ # Get the data from the metrics
490
+ with self.db_connection._engine.connect() as conn:
491
+ df = pd.read_sql(corrected_query, conn)
492
+
493
+ # Calculate overall processing time
494
+ total_time_ms = round((time.time() - start_time) * 1000, 2)
495
+
496
+ # Add query to history
497
+ history_item = {
498
+ "question": question,
499
+ "sql_query": corrected_query,
500
+ "execution_time_ms": performance_metrics["execution_time_ms"],
501
+ "total_time_ms": total_time_ms,
502
+ "row_count": performance_metrics["row_count"],
503
+ "complexity": performance_metrics["complexity"]["level"],
504
+ "timestamp": datetime.now().isoformat(),
505
+ "corrected": True,
506
+ "original_query": sql_query
507
+ }
508
+ self.add_to_query_history(history_item)
509
+
510
+ # Generate performance and complexity insights
511
+ complexity = performance_metrics["complexity"]
512
+ perf_insights = f"\n\nπŸ“Š Query Metrics:\n"
513
+ perf_insights += f"β€’ Execution time: {performance_metrics['execution_time_ms']}ms\n"
514
+ perf_insights += f"β€’ Total processing time: {total_time_ms}ms\n"
515
+ perf_insights += f"β€’ Rows returned: {performance_metrics['row_count']}\n"
516
+ perf_insights += f"β€’ Complexity: {complexity['level']}\n"
517
+
518
+ if complexity["insights"]:
519
+ perf_insights += "\nπŸ” Insights:\n"
520
+ for insight in complexity["insights"]:
521
+ perf_insights += f"β€’ {insight}\n"
522
+
523
+ if not df.empty:
524
+ # Format table in RDBMS style
525
+ table_html = df.to_html(index=False, classes="table table-bordered table-striped", border=0)
526
+ # Add custom styling
527
+ table_html = f"""
528
+ <style>
529
+ .table-bordered {{
530
+ border-collapse: collapse;
531
+ width: 100%;
532
+ font-family: 'Courier New', Courier, monospace;
533
+ }}
534
+ .table-bordered th {{
535
+ background-color: #f2f2f2;
536
+ color: #333;
537
+ font-weight: bold;
538
+ text-align: left;
539
+ padding: 8px;
540
+ border: 1px solid #ddd;
541
+ }}
542
+ .table-bordered td {{
543
+ padding: 8px;
544
+ border: 1px solid #ddd;
545
+ }}
546
+ .table-striped tbody tr:nth-of-type(odd) {{
547
+ background-color: rgba(0,0,0,.05);
548
+ }}
549
+ </style>
550
+ {table_html}
551
+ """
552
+ return f"βœ… Query Successful (after correction)\n\n{result}\n\nπŸ”§ Query was automatically corrected due to execution error: {str(exec_error)}{perf_insights}", table_html
553
+ else:
554
+ # If performance measurement failed, continue with normal execution
555
+ with self.db_connection._engine.connect() as conn:
556
+ df = pd.read_sql(corrected_query, conn)
557
+
558
+ if not df.empty:
559
+ # Format table in RDBMS style
560
+ table_html = df.to_html(index=False, classes="table table-bordered table-striped", border=0)
561
+ # Add custom styling
562
+ table_html = f"""
563
+ <style>
564
+ .table-bordered {{
565
+ border-collapse: collapse;
566
+ width: 100%;
567
+ font-family: 'Courier New', Courier, monospace;
568
+ }}
569
+ .table-bordered th {{
570
+ background-color: #f2f2f2;
571
+ color: #333;
572
+ font-weight: bold;
573
+ text-align: left;
574
+ padding: 8px;
575
+ border: 1px solid #ddd;
576
+ }}
577
+ .table-bordered td {{
578
+ padding: 8px;
579
+ border: 1px solid #ddd;
580
+ }}
581
+ .table-striped tbody tr:nth-of-type(odd) {{
582
+ background-color: rgba(0,0,0,.05);
583
+ }}
584
+ </style>
585
+ {table_html}
586
+ """
587
+ return f"βœ… Query Successful (after correction)\n\n{result}\n\nπŸ”§ Query was automatically corrected due to execution error: {str(exec_error)}", table_html
588
+ except Exception:
589
+ # If correction fails, return the original error
590
+ pass
591
+
592
+ # Return the execution error in table format
593
+ error_msg = f"The query failed to execute:\n\n{str(exec_error)}\n\nOriginal result:\n{result}"
594
+ df_error = pd.DataFrame({"Error": [error_msg]})
595
+ table_html = df_error.to_html(index=False, classes="table table-bordered table-striped", border=0)
596
+ # Add custom styling
597
+ table_html = f"""
598
+ <style>
599
+ .table-bordered {{
600
+ border-collapse: collapse;
601
+ width: 100%;
602
+ font-family: 'Courier New', Courier, monospace;
603
+ }}
604
+ .table-bordered th {{
605
+ background-color: #f2f2f2;
606
+ color: #333;
607
+ font-weight: bold;
608
+ text-align: left;
609
+ padding: 8px;
610
+ border: 1px solid #ddd;
611
+ }}
612
+ .table-bordered td {{
613
+ padding: 8px;
614
+ border: 1px solid #ddd;
615
+ }}
616
+ .table-striped tbody tr:nth-of-type(odd) {{
617
+ background-color: rgba(0,0,0,.05);
618
+ }}
619
+ </style>
620
+ {table_html}
621
+ """
622
+ return "❌ SQL Execution Error", table_html
623
+
624
+ except SQLAlchemyError as sql_error:
625
+ # Handle SQL execution errors
626
+ error_details = str(sql_error)
627
+ error_msg = f"❌ SQL Execution Error"
628
+ details = f"The query failed to execute:\n\n{error_details}\n\nOriginal result:\n{result}"
629
+ df_error = pd.DataFrame({"Error": [details]})
630
+ table_html = df_error.to_html(index=False, classes="table table-bordered table-striped", border=0)
631
+ # Add custom styling
632
+ table_html = f"""
633
+ <style>
634
+ .table-bordered {{
635
+ border-collapse: collapse;
636
+ width: 100%;
637
+ font-family: 'Courier New', Courier, monospace;
638
+ }}
639
+ .table-bordered th {{
640
+ background-color: #f2f2f2;
641
+ color: #333;
642
+ font-weight: bold;
643
+ text-align: left;
644
+ padding: 8px;
645
+ border: 1px solid #ddd;
646
+ }}
647
+ .table-bordered td {{
648
+ padding: 8px;
649
+ border: 1px solid #ddd;
650
+ }}
651
+ .table-striped tbody tr:nth-of-type(odd) {{
652
+ background-color: rgba(0,0,0,.05);
653
+ }}
654
+ </style>
655
+ {table_html}
656
+ """
657
+ return error_msg, table_html
658
+ except Exception as table_error:
659
+ # If table extraction fails, just return the text result
660
+ pass
661
+
662
+ # If we got here, we just have the text result without structured data
663
+ # Convert to table format with RDBMS style
664
+ df_text = pd.DataFrame({"Result": [result]})
665
+ table_html = df_text.to_html(index=False, classes="table table-bordered table-striped", border=0)
666
+ # Add custom styling
667
+ table_html = f"""
668
+ <style>
669
+ .table-bordered {{
670
+ border-collapse: collapse;
671
+ width: 100%;
672
+ font-family: 'Courier New', Courier, monospace;
673
+ }}
674
+ .table-bordered th {{
675
+ background-color: #f2f2f2;
676
+ color: #333;
677
+ font-weight: bold;
678
+ text-align: left;
679
+ padding: 8px;
680
+ border: 1px solid #ddd;
681
+ }}
682
+ .table-bordered td {{
683
+ padding: 8px;
684
+ border: 1px solid #ddd;
685
+ }}
686
+ .table-striped tbody tr:nth-of-type(odd) {{
687
+ background-color: rgba(0,0,0,.05);
688
+ }}
689
+ </style>
690
+ {table_html}
691
+ """
692
+
693
+ # Add to history
694
+ history_item = {
695
+ "question": question,
696
+ "result": result,
697
+ "timestamp": datetime.now().isoformat()
698
+ }
699
+ self.add_to_query_history(history_item)
700
+
701
+ return f"βœ… Query Successful", table_html
702
+
703
+ except Exception as e:
704
+ # Convert exception to table format with RDBMS style
705
+ error_msg = f"❌ Query Failed: {str(e)}"
706
+ details = f"Error processing query: {traceback.format_exc()}"
707
+ df_error = pd.DataFrame({"Error": [details]})
708
+ table_html = df_error.to_html(index=False, classes="table table-bordered table-striped", border=0)
709
+ # Add custom styling
710
+ table_html = f"""
711
+ <style>
712
+ .table-bordered {{
713
+ border-collapse: collapse;
714
+ width: 100%;
715
+ font-family: 'Courier New', Courier, monospace;
716
+ }}
717
+ .table-bordered th {{
718
+ background-color: #f2f2f2;
719
+ color: #333;
720
+ font-weight: bold;
721
+ text-align: left;
722
+ padding: 8px;
723
+ border: 1px solid #ddd;
724
+ }}
725
+ .table-bordered td {{
726
+ padding: 8px;
727
+ border: 1px solid #ddd;
728
+ }}
729
+ .table-striped tbody tr:nth-of-type(odd) {{
730
+ background-color: rgba(0,0,0,.05);
731
+ }}
732
+ </style>
733
+ {table_html}
734
+ """
735
+ return error_msg, table_html
736
+
737
+ def analyze_query_complexity(self, sql_query: str) -> Dict[str, Any]:
738
+ """
739
+ Analyze SQL query complexity and provide insights
740
+
741
+ Args:
742
+ sql_query: SQL query to analyze
743
+
744
+ Returns:
745
+ Dictionary with complexity metrics and insights
746
+ """
747
+ if not sql_query or not isinstance(sql_query, str):
748
+ return {"error": "Invalid query provided"}
749
+
750
+ sql_query = sql_query.strip().upper()
751
+
752
+ # Initialize complexity metrics
753
+ complexity = {
754
+ "level": "Simple",
755
+ "score": 0,
756
+ "joins": 0,
757
+ "tables": [],
758
+ "aggregations": False,
759
+ "grouping": False,
760
+ "ordering": False,
761
+ "limiting": False,
762
+ "subqueries": 0,
763
+ "complex_functions": [],
764
+ "insights": []
765
+ }
766
+
767
+ # Count number of JOINs
768
+ join_count = len(re.findall(r'\bJOIN\b', sql_query))
769
+ complexity["joins"] = join_count
770
+ if join_count > 0:
771
+ complexity["score"] += join_count * 2
772
+ if join_count >= 3:
773
+ complexity["insights"].append(f"Query uses {join_count} joins, which may impact performance")
774
+
775
+ # Detect tables used
776
+ from_clause = re.search(r'\bFROM\b\s+(.*?)(?:\bWHERE\b|\bGROUP\b|\bHAVING\b|\bORDER\b|\bLIMIT\b|$)', sql_query)
777
+ if from_clause:
778
+ # Extract table names from FROM clause
779
+ tables_text = from_clause.group(1).strip()
780
+ # Handle JOIN syntax in FROM clause
781
+ tables = re.findall(r'([a-zA-Z0-9_]+)(?:\s+(?:AS\s+)?[a-zA-Z0-9_]+)?', tables_text)
782
+ complexity["tables"] = list(set(tables)) # Remove duplicates
783
+
784
+ # Check for aggregations
785
+ agg_functions = ["COUNT", "SUM", "AVG", "MIN", "MAX"]
786
+ for func in agg_functions:
787
+ if re.search(rf'\b{func}\s*\(', sql_query):
788
+ complexity["aggregations"] = True
789
+ complexity["score"] += 1
790
+ break
791
+
792
+ # Check for GROUP BY
793
+ if re.search(r'\bGROUP\s+BY\b', sql_query):
794
+ complexity["grouping"] = True
795
+ complexity["score"] += 2
796
+
797
+ # Check for ORDER BY
798
+ if re.search(r'\bORDER\s+BY\b', sql_query):
799
+ complexity["ordering"] = True
800
+ complexity["score"] += 1
801
+
802
+ # Check for LIMIT
803
+ if re.search(r'\bLIMIT\b', sql_query):
804
+ complexity["limiting"] = True
805
+ complexity["score"] += 0.5
806
+
807
+ # Check for subqueries
808
+ subquery_count = len(re.findall(r'\(\s*SELECT', sql_query))
809
+ complexity["subqueries"] = subquery_count
810
+ if subquery_count > 0:
811
+ complexity["score"] += subquery_count * 3
812
+ complexity["insights"].append(f"Query contains {subquery_count} subqueries, which may affect performance")
813
+
814
+ # Check for complex functions
815
+ complex_funcs = ["CASE", "COALESCE", "NULLIF", "CAST", "CONVERT", "SUBSTRING", "CONCAT", "DATE_FORMAT", "EXTRACT"]
816
+ for func in complex_funcs:
817
+ if re.search(rf'\b{func}\b', sql_query):
818
+ complexity["complex_functions"].append(func)
819
+ complexity["score"] += 1
820
+
821
+ # Determine complexity level
822
+ if complexity["score"] <= 2:
823
+ complexity["level"] = "Simple"
824
+ elif complexity["score"] <= 5:
825
+ complexity["level"] = "Moderate"
826
+ elif complexity["score"] <= 10:
827
+ complexity["level"] = "Complex"
828
+ else:
829
+ complexity["level"] = "Very Complex"
830
+ complexity["insights"].append("This is a highly complex query that may benefit from optimization")
831
+
832
+ # Add insights based on complexity
833
+ if complexity["level"] in ["Complex", "Very Complex"] and not complexity["limiting"]:
834
+ complexity["insights"].append("Consider adding a LIMIT clause to prevent large result sets")
835
+
836
+ if complexity["joins"] >= 2 and not any(idx for idx in complexity["insights"] if "index" in idx.lower()):
837
+ complexity["insights"].append("Ensure proper indexes exist on join columns")
838
+
839
+ return complexity
840
+
841
+ def add_to_query_history(self, query_data: Dict[str, Any]) -> None:
842
+ """
843
+ Add a query to the history
844
+
845
+ Args:
846
+ query_data: Dictionary containing query information
847
+ """
848
+ # Add timestamp if not present
849
+ if "timestamp" not in query_data:
850
+ query_data["timestamp"] = datetime.now().isoformat()
851
+
852
+ # Add to history (at the beginning for most recent first)
853
+ self.query_history.insert(0, query_data)
854
+
855
+ # Trim history if needed
856
+ if len(self.query_history) > self.max_history_items:
857
+ self.query_history = self.query_history[:self.max_history_items]
858
+
859
+ def get_query_history(self) -> List[Dict[str, Any]]:
860
+ """
861
+ Get the query history
862
+
863
+ Returns:
864
+ List of query history items
865
+ """
866
+ return self.query_history
867
+
868
+ def clear_query_history(self) -> None:
869
+ """Clear the query history"""
870
+ self.query_history = []
871
+
872
+ def measure_query_performance(self, sql_query: str) -> Dict[str, Any]:
873
+ """
874
+ Measure the performance of a SQL query
875
+
876
+ Args:
877
+ sql_query: SQL query to execute and measure
878
+
879
+ Returns:
880
+ Dictionary with performance metrics
881
+ """
882
+ if not self.db_connection:
883
+ return {"error": "No database connection"}
884
+
885
+ metrics = {
886
+ "query": sql_query,
887
+ "execution_time_ms": 0,
888
+ "row_count": 0,
889
+ "success": False,
890
+ "error": None
891
+ }
892
+
893
+ try:
894
+ # Measure execution time
895
+ start_time = time.time()
896
+
897
+ with self.db_connection._engine.connect() as conn:
898
+ result = conn.execute(text(sql_query))
899
+ # Convert to DataFrame to get row count
900
+ df = pd.DataFrame(result.fetchall(), columns=result.keys())
901
+
902
+ end_time = time.time()
903
+
904
+ # Calculate metrics
905
+ metrics["execution_time_ms"] = round((end_time - start_time) * 1000, 2)
906
+ metrics["row_count"] = len(df)
907
+ metrics["success"] = True
908
+
909
+ # Add complexity analysis
910
+ metrics["complexity"] = self.analyze_query_complexity(sql_query)
911
+
912
+ return metrics
913
+
914
+ except Exception as e:
915
+ metrics["error"] = str(e)
916
+ return metrics
917
+
918
+ def generate_schema_diagram(self, include_all_tables: bool = False) -> str:
919
+ """
920
+ Generate a Mermaid ER diagram for the database schema
921
+
922
+ Args:
923
+ include_all_tables: Whether to include all tables or just a subset
924
+
925
+ Returns:
926
+ Mermaid diagram code
927
+ """
928
+ # Return a message that this functionality is not available
929
+ return "This functionality has been removed"
930
+
931
+ # Initialize the database manager
932
+ db_manager = DatabaseManager()
933
+
934
+ def create_interface():
935
+ """Create the Gradio interface"""
936
+
937
+ with gr.Blocks(title="AI Database Query Assistant", theme=gr.themes.Soft()) as demo:
938
+ gr.Markdown("""
939
+ # πŸ€– AI Database Query Assistant
940
+
941
+ Connect to your MySQL database and query it using natural language!
942
+
943
+ ### Steps:
944
+ 1. **Connect** to your database
945
+ 2. **Analyze** your database structure
946
+ 3. **Ask questions** in natural language
947
+ """)
948
+
949
+ # Connection Status
950
+ connection_status = gr.Textbox(
951
+ label="Connection Status",
952
+ value="Not Connected",
953
+ interactive=False
954
+ )
955
+
956
+ with gr.Tabs():
957
+ # MySQL Connection Tab
958
+ with gr.TabItem("MySQL Connection"):
959
+ gr.Markdown("""
960
+ **MySQL Connection Details:**
961
+ - Enter your MySQL server connection details
962
+ - Password will be securely handled (not stored)
963
+ - Default port is 3306 if not specified
964
+ - Special characters in passwords are supported
965
+ """)
966
+
967
+ with gr.Row():
968
+ mysql_host = gr.Textbox(
969
+ label="Host",
970
+ value="localhost",
971
+ placeholder="localhost or IP address"
972
+ )
973
+ mysql_port = gr.Textbox(
974
+ label="Port",
975
+ value="3306",
976
+ placeholder="3306"
977
+ )
978
+
979
+ with gr.Row():
980
+ mysql_username = gr.Textbox(
981
+ label="Username",
982
+ placeholder="root or your username"
983
+ )
984
+ mysql_password = gr.Textbox(
985
+ label="DB Password(optional)",
986
+ type="password",
987
+ placeholder="Your MySQL password"
988
+ )
989
+
990
+ mysql_database = gr.Textbox(
991
+ label="Database Name",
992
+ placeholder="my_database"
993
+ )
994
+
995
+ mysql_connect_btn = gr.Button("Connect to MySQL", variant="primary")
996
+ mysql_message = gr.Textbox(label="Connection Message", interactive=False)
997
+
998
+ # API Key Section
999
+ gr.Markdown("""
1000
+ ### πŸ”‘ Google API Key
1001
+ Enter your Google API key for Gemini model. If not provided, will use environment variable.
1002
+ """)
1003
+
1004
+ with gr.Row():
1005
+ api_key_input = gr.Textbox(
1006
+ label="Google API Key",
1007
+ type="password",
1008
+ placeholder="Enter your Gemini API key here",
1009
+ info="Get your API key from: https://makersuite.google.com/app/apikey"
1010
+ )
1011
+ api_key_btn = gr.Button("Set API Key", variant="secondary")
1012
+
1013
+ api_key_message = gr.Textbox(label="API Key Status", interactive=False)
1014
+
1015
+ # Database Analysis Section
1016
+ with gr.Tabs():
1017
+ with gr.TabItem("Database Analysis"):
1018
+ gr.Markdown("## πŸ” Database Analysis")
1019
+ analyze_btn = gr.Button("Analyze Database", variant="secondary", size="lg")
1020
+
1021
+ with gr.Row():
1022
+ analysis_status = gr.Textbox(label="Analysis Status", interactive=False)
1023
+ analysis_details = gr.Textbox(label="Analysis Details", lines=10, interactive=False)
1024
+
1025
+ # Schema Visualization
1026
+ gr.Markdown("### πŸ“Š Database Schema Visualization")
1027
+ with gr.Row():
1028
+ schema_table_select = gr.Dropdown(label="Select Table", choices=[], interactive=True)
1029
+ visualize_schema_btn = gr.Button("Visualize Schema", variant="secondary")
1030
+
1031
+ schema_output = gr.HTML(label="Schema Visualization")
1032
+
1033
+ # Removed ER Diagram Visualization section
1034
+
1035
+ # Query Section
1036
+ with gr.TabItem("Query Database"):
1037
+ gr.Markdown("## πŸ’¬ Ask Questions")
1038
+
1039
+ question_input = gr.Textbox(
1040
+ label="Your Question",
1041
+ placeholder="Example: Show me all customers from New York, What are the top 5 selling products?",
1042
+ lines=2
1043
+ )
1044
+
1045
+ query_btn = gr.Button("Ask Question", variant="primary", size="lg")
1046
+
1047
+ with gr.Row():
1048
+ query_status = gr.Textbox(label="Query Result", lines=5, interactive=False)
1049
+ query_output = gr.HTML(label="Data Output")
1050
+
1051
+ # Example questions
1052
+ gr.Markdown("""
1053
+ ### πŸ’‘ Example Questions:
1054
+ - "Show me all users registered in the last month"
1055
+ - "What are the top 5 products by sales?"
1056
+ - "How many orders were placed yesterday?"
1057
+ - "Show me customers with more than 10 orders"
1058
+ - "What's the average order value?"
1059
+ """)
1060
+
1061
+ # Query History Tab
1062
+ with gr.TabItem("Query History"):
1063
+ gr.Markdown("## πŸ“œ Query History")
1064
+
1065
+ with gr.Row():
1066
+ refresh_history_btn = gr.Button("Refresh History", variant="secondary")
1067
+ clear_history_btn = gr.Button("Clear History", variant="secondary")
1068
+
1069
+ history_output = gr.HTML(label="Query History")
1070
+
1071
+ # Reuse Query Section
1072
+ gr.Markdown("### πŸ”„ Reuse Previous Query")
1073
+ with gr.Row():
1074
+ history_question_select = gr.Dropdown(label="Select Previous Question", choices=[], interactive=True)
1075
+ reuse_query_btn = gr.Button("Use Selected Query", variant="primary")
1076
+
1077
+ # Event handlers
1078
+ mysql_connect_btn.click(
1079
+ fn=lambda h, p, u, pw, d: db_manager.connect_mysql(h, p, u, pw, d) + (db_manager.connection_status,),
1080
+ inputs=[mysql_host, mysql_port, mysql_username, mysql_password, mysql_database],
1081
+ outputs=[mysql_message, connection_status]
1082
+ )
1083
+
1084
+ # API Key event handler
1085
+ api_key_btn.click(
1086
+ fn=db_manager.set_api_key,
1087
+ inputs=[api_key_input],
1088
+ outputs=[api_key_message]
1089
+ )
1090
+
1091
+ # Database analysis event handler
1092
+ def on_analyze_database():
1093
+ status, details = db_manager.analyze_database()
1094
+
1095
+ # Update schema table dropdown if analysis was successful
1096
+ table_choices = []
1097
+ if "βœ…" in status and db_manager.db_context:
1098
+ table_choices = list(db_manager.db_context.get("tables", {}).keys())
1099
+
1100
+ return status, details, gr.Dropdown(choices=table_choices)
1101
+
1102
+ analyze_btn.click(
1103
+ fn=on_analyze_database,
1104
+ outputs=[analysis_status, analysis_details, schema_table_select]
1105
+ )
1106
+
1107
+ # Schema visualization event handler
1108
+ def visualize_table_schema(table_name):
1109
+ if not table_name or not db_manager.db_context or table_name not in db_manager.db_context.get("tables", {}):
1110
+ return "<p>Please select a valid table</p>"
1111
+
1112
+ table_info = db_manager.db_context["tables"][table_name]
1113
+
1114
+ # Create HTML visualization
1115
+ html = f"<h3>Table: {table_name}</h3>"
1116
+ html += f"<p>Row count: {table_info.get('row_count', 'Unknown')}</p>"
1117
+
1118
+ # Create table for columns
1119
+ html += "<table class='table table-bordered table-striped'>"
1120
+ html += "<thead><tr><th>Column</th><th>Type</th><th>Key</th></tr></thead>"
1121
+ html += "<tbody>"
1122
+
1123
+ # Add columns
1124
+ primary_keys = table_info.get("primary_keys", [])
1125
+ foreign_keys_flat = []
1126
+
1127
+ # Flatten foreign key references
1128
+ for fk in table_info.get("foreign_keys", []):
1129
+ for col in fk.get("columns", []):
1130
+ foreign_keys_flat.append(col)
1131
+
1132
+ for col in table_info.get("columns", []):
1133
+ col_name = col.get("name", "")
1134
+ col_type = col.get("type", "")
1135
+
1136
+ # Determine key type
1137
+ key_type = ""
1138
+ if col_name in primary_keys:
1139
+ key_type = "πŸ”‘ Primary"
1140
+ elif col_name in foreign_keys_flat:
1141
+ key_type = "πŸ”— Foreign"
1142
+
1143
+ html += f"<tr><td>{col_name}</td><td>{col_type}</td><td>{key_type}</td></tr>"
1144
+
1145
+ html += "</tbody></table>"
1146
+
1147
+ # Add foreign key relationships
1148
+ if table_info.get("foreign_keys"):
1149
+ html += "<h4>Foreign Key Relationships</h4>"
1150
+ html += "<ul>"
1151
+ for fk in table_info.get("foreign_keys", []):
1152
+ cols = ", ".join(fk.get("columns", []))
1153
+ refs = fk.get("refers_to", "")
1154
+ html += f"<li>{cols} β†’ {refs}</li>"
1155
+ html += "</ul>"
1156
+
1157
+ return html
1158
+
1159
+ visualize_schema_btn.click(
1160
+ fn=visualize_table_schema,
1161
+ inputs=[schema_table_select],
1162
+ outputs=[schema_output]
1163
+ )
1164
+
1165
+ # Removed ER Diagram event handler
1166
+
1167
+ # Query event handler
1168
+ query_btn.click(
1169
+ fn=db_manager.query_database,
1170
+ inputs=[question_input],
1171
+ outputs=[query_status, query_output]
1172
+ )
1173
+
1174
+ # Query history event handlers
1175
+ def format_query_history():
1176
+ history = db_manager.get_query_history()
1177
+ if not history:
1178
+ return "<p>No queries in history</p>", gr.Dropdown(choices=[])
1179
+
1180
+ # Format history as HTML table
1181
+ html = "<table class='table table-bordered table-striped'>"
1182
+ html += "<thead><tr><th>Time</th><th>Question</th><th>SQL Query</th><th>Execution Time</th><th>Rows</th><th>Complexity</th></tr></thead>"
1183
+ html += "<tbody>"
1184
+
1185
+ # Collect questions for dropdown
1186
+ questions = []
1187
+
1188
+ for i, item in enumerate(history):
1189
+ # Format timestamp
1190
+ timestamp = item.get("timestamp", "")
1191
+ if timestamp:
1192
+ try:
1193
+ dt = datetime.fromisoformat(timestamp)
1194
+ timestamp = dt.strftime("%Y-%m-%d %H:%M:%S")
1195
+ except:
1196
+ pass
1197
+
1198
+ question = item.get("question", "")
1199
+ sql_query = item.get("sql_query", "")
1200
+ exec_time = f"{item.get('execution_time_ms', 0)}ms" if "execution_time_ms" in item else "-"
1201
+ row_count = item.get("row_count", "-")
1202
+ complexity = item.get("complexity", "-")
1203
+
1204
+ # Add question to dropdown options
1205
+ if question:
1206
+ questions.append(question)
1207
+
1208
+ # Format row with corrected query highlight
1209
+ row_class = " class='table-warning'" if item.get("corrected", False) else ""
1210
+ html += f"<tr{row_class}>"
1211
+ html += f"<td>{timestamp}</td>"
1212
+ html += f"<td>{question}</td>"
1213
+ html += f"<td><code>{sql_query}</code></td>"
1214
+ html += f"<td>{exec_time}</td>"
1215
+ html += f"<td>{row_count}</td>"
1216
+ html += f"<td>{complexity}</td>"
1217
+ html += "</tr>"
1218
+
1219
+ html += "</tbody></table>"
1220
+
1221
+ return html, gr.Dropdown(choices=questions)
1222
+
1223
+ refresh_history_btn.click(
1224
+ fn=format_query_history,
1225
+ outputs=[history_output, history_question_select]
1226
+ )
1227
+
1228
+ clear_history_btn.click(
1229
+ fn=lambda: (db_manager.clear_query_history(), "<p>History cleared</p>", gr.Dropdown(choices=[])),
1230
+ outputs=[history_output, history_question_select]
1231
+ )
1232
+
1233
+ # Reuse query event handler
1234
+ def reuse_question(selected_question):
1235
+ if not selected_question:
1236
+ return gr.Textbox(value="")
1237
+ return gr.Textbox(value=selected_question)
1238
+
1239
+ reuse_query_btn.click(
1240
+ fn=reuse_question,
1241
+ inputs=[history_question_select],
1242
+ outputs=[question_input]
1243
+ )
1244
+
1245
+ return demo
1246
+
1247
+ if __name__ == "__main__":
1248
+ # Check for required environment variables
1249
+ if not os.getenv("GOOGLE_API_KEY"):
1250
+ print("⚠️ Warning: GOOGLE_API_KEY not found in environment variables")
1251
+ print("You will need to provide an API key in the interface or set the environment variable.")
1252
+ print("Get your API key from: https://makersuite.google.com/app/apikey")
1253
+
1254
+ # Create and launch the interface
1255
+ demo = create_interface()
1256
+ demo.launch(
1257
+ #server_name="0.0.0.0",
1258
+ server_port=7860,
1259
+ share=False,
1260
+ debug=True
1261
+ )