createsonline 0.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- createsonline/__init__.py +46 -0
- createsonline/admin/__init__.py +7 -0
- createsonline/admin/content.py +526 -0
- createsonline/admin/crud.py +805 -0
- createsonline/admin/field_builder.py +559 -0
- createsonline/admin/integration.py +482 -0
- createsonline/admin/interface.py +2562 -0
- createsonline/admin/model_creator.py +513 -0
- createsonline/admin/model_manager.py +388 -0
- createsonline/admin/modern_dashboard.py +498 -0
- createsonline/admin/permissions.py +264 -0
- createsonline/admin/user_forms.py +594 -0
- createsonline/ai/__init__.py +202 -0
- createsonline/ai/fields.py +1226 -0
- createsonline/ai/orm.py +325 -0
- createsonline/ai/services.py +1244 -0
- createsonline/app.py +506 -0
- createsonline/auth/__init__.py +8 -0
- createsonline/auth/management.py +228 -0
- createsonline/auth/models.py +552 -0
- createsonline/cli/__init__.py +5 -0
- createsonline/cli/commands/__init__.py +122 -0
- createsonline/cli/commands/database.py +416 -0
- createsonline/cli/commands/info.py +173 -0
- createsonline/cli/commands/initdb.py +218 -0
- createsonline/cli/commands/project.py +545 -0
- createsonline/cli/commands/serve.py +173 -0
- createsonline/cli/commands/shell.py +93 -0
- createsonline/cli/commands/users.py +148 -0
- createsonline/cli/main.py +2041 -0
- createsonline/cli/manage.py +274 -0
- createsonline/config/__init__.py +9 -0
- createsonline/config/app.py +2577 -0
- createsonline/config/database.py +179 -0
- createsonline/config/docs.py +384 -0
- createsonline/config/errors.py +160 -0
- createsonline/config/orm.py +43 -0
- createsonline/config/request.py +93 -0
- createsonline/config/settings.py +176 -0
- createsonline/data/__init__.py +23 -0
- createsonline/data/dataframe.py +925 -0
- createsonline/data/io.py +453 -0
- createsonline/data/series.py +557 -0
- createsonline/database/__init__.py +60 -0
- createsonline/database/abstraction.py +440 -0
- createsonline/database/assistant.py +585 -0
- createsonline/database/fields.py +442 -0
- createsonline/database/migrations.py +132 -0
- createsonline/database/models.py +604 -0
- createsonline/database.py +438 -0
- createsonline/http/__init__.py +28 -0
- createsonline/http/client.py +535 -0
- createsonline/ml/__init__.py +55 -0
- createsonline/ml/classification.py +552 -0
- createsonline/ml/clustering.py +680 -0
- createsonline/ml/metrics.py +542 -0
- createsonline/ml/neural.py +560 -0
- createsonline/ml/preprocessing.py +784 -0
- createsonline/ml/regression.py +501 -0
- createsonline/performance/__init__.py +19 -0
- createsonline/performance/cache.py +444 -0
- createsonline/performance/compression.py +335 -0
- createsonline/performance/core.py +419 -0
- createsonline/project_init.py +789 -0
- createsonline/routing.py +528 -0
- createsonline/security/__init__.py +34 -0
- createsonline/security/core.py +811 -0
- createsonline/security/encryption.py +349 -0
- createsonline/server.py +295 -0
- createsonline/static/css/admin.css +263 -0
- createsonline/static/css/common.css +358 -0
- createsonline/static/css/dashboard.css +89 -0
- createsonline/static/favicon.ico +0 -0
- createsonline/static/icons/icon-128x128.png +0 -0
- createsonline/static/icons/icon-128x128.webp +0 -0
- createsonline/static/icons/icon-16x16.png +0 -0
- createsonline/static/icons/icon-16x16.webp +0 -0
- createsonline/static/icons/icon-180x180.png +0 -0
- createsonline/static/icons/icon-180x180.webp +0 -0
- createsonline/static/icons/icon-192x192.png +0 -0
- createsonline/static/icons/icon-192x192.webp +0 -0
- createsonline/static/icons/icon-256x256.png +0 -0
- createsonline/static/icons/icon-256x256.webp +0 -0
- createsonline/static/icons/icon-32x32.png +0 -0
- createsonline/static/icons/icon-32x32.webp +0 -0
- createsonline/static/icons/icon-384x384.png +0 -0
- createsonline/static/icons/icon-384x384.webp +0 -0
- createsonline/static/icons/icon-48x48.png +0 -0
- createsonline/static/icons/icon-48x48.webp +0 -0
- createsonline/static/icons/icon-512x512.png +0 -0
- createsonline/static/icons/icon-512x512.webp +0 -0
- createsonline/static/icons/icon-64x64.png +0 -0
- createsonline/static/icons/icon-64x64.webp +0 -0
- createsonline/static/image/android-chrome-192x192.png +0 -0
- createsonline/static/image/android-chrome-512x512.png +0 -0
- createsonline/static/image/apple-touch-icon.png +0 -0
- createsonline/static/image/favicon-16x16.png +0 -0
- createsonline/static/image/favicon-32x32.png +0 -0
- createsonline/static/image/favicon.ico +0 -0
- createsonline/static/image/favicon.svg +17 -0
- createsonline/static/image/icon-128x128.png +0 -0
- createsonline/static/image/icon-128x128.webp +0 -0
- createsonline/static/image/icon-16x16.png +0 -0
- createsonline/static/image/icon-16x16.webp +0 -0
- createsonline/static/image/icon-180x180.png +0 -0
- createsonline/static/image/icon-180x180.webp +0 -0
- createsonline/static/image/icon-192x192.png +0 -0
- createsonline/static/image/icon-192x192.webp +0 -0
- createsonline/static/image/icon-256x256.png +0 -0
- createsonline/static/image/icon-256x256.webp +0 -0
- createsonline/static/image/icon-32x32.png +0 -0
- createsonline/static/image/icon-32x32.webp +0 -0
- createsonline/static/image/icon-384x384.png +0 -0
- createsonline/static/image/icon-384x384.webp +0 -0
- createsonline/static/image/icon-48x48.png +0 -0
- createsonline/static/image/icon-48x48.webp +0 -0
- createsonline/static/image/icon-512x512.png +0 -0
- createsonline/static/image/icon-512x512.webp +0 -0
- createsonline/static/image/icon-64x64.png +0 -0
- createsonline/static/image/icon-64x64.webp +0 -0
- createsonline/static/image/logo-header-h100.png +0 -0
- createsonline/static/image/logo-header-h100.webp +0 -0
- createsonline/static/image/logo-header-h200@2x.png +0 -0
- createsonline/static/image/logo-header-h200@2x.webp +0 -0
- createsonline/static/image/logo.png +0 -0
- createsonline/static/js/admin.js +274 -0
- createsonline/static/site.webmanifest +35 -0
- createsonline/static/templates/admin/base.html +87 -0
- createsonline/static/templates/admin/dashboard.html +217 -0
- createsonline/static/templates/admin/model_form.html +270 -0
- createsonline/static/templates/admin/model_list.html +202 -0
- createsonline/static/test_script.js +15 -0
- createsonline/static/test_styles.css +59 -0
- createsonline/static_files.py +365 -0
- createsonline/templates/404.html +100 -0
- createsonline/templates/admin_login.html +169 -0
- createsonline/templates/base.html +102 -0
- createsonline/templates/index.html +151 -0
- createsonline/templates.py +205 -0
- createsonline/testing.py +322 -0
- createsonline/utils.py +448 -0
- createsonline/validation/__init__.py +49 -0
- createsonline/validation/fields.py +598 -0
- createsonline/validation/models.py +504 -0
- createsonline/validation/validators.py +561 -0
- createsonline/views.py +184 -0
- createsonline-0.1.26.dist-info/METADATA +46 -0
- createsonline-0.1.26.dist-info/RECORD +152 -0
- createsonline-0.1.26.dist-info/WHEEL +5 -0
- createsonline-0.1.26.dist-info/entry_points.txt +2 -0
- createsonline-0.1.26.dist-info/licenses/LICENSE +21 -0
- createsonline-0.1.26.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# createsonline/database/assistant.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE Database Assistant - AI-Powered SQL Generation
|
|
4
|
+
|
|
5
|
+
Converts natural language prompts into safe SQLAlchemy queries.
|
|
6
|
+
Focuses on SELECT operations with strict safety guards against destructive operations.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Natural language → SQLAlchemy object conversion
|
|
10
|
+
- Safety filters for destructive operations
|
|
11
|
+
- Audit logging for all operations
|
|
12
|
+
- Rollback capabilities
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
assistant = DatabaseAssistant(db_connection)
|
|
16
|
+
query = assistant.generate_query("show last 10 error logs")
|
|
17
|
+
result = assistant.execute_safe(query)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Dict, Any, List, Optional, Tuple, Union
|
|
23
|
+
from datetime import datetime, timedelta
|
|
24
|
+
from enum import Enum
|
|
25
|
+
import json
|
|
26
|
+
|
|
27
|
+
# Core imports - using absolute imports to avoid relative import issues
|
|
28
|
+
try:
|
|
29
|
+
from createsonline.database import DatabaseConnection
|
|
30
|
+
except ImportError:
|
|
31
|
+
# Fallback for direct execution
|
|
32
|
+
import sys
|
|
33
|
+
import os
|
|
34
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
35
|
+
|
|
36
|
+
import importlib.util
|
|
37
|
+
db_spec = importlib.util.spec_from_file_location(
|
|
38
|
+
"database_connection",
|
|
39
|
+
os.path.join(os.path.dirname(__file__), '..', 'database.py')
|
|
40
|
+
)
|
|
41
|
+
db_module = importlib.util.module_from_spec(db_spec)
|
|
42
|
+
db_spec.loader.exec_module(db_module)
|
|
43
|
+
DatabaseConnection = db_module.DatabaseConnection
|
|
44
|
+
|
|
45
|
+
# Setup logging
|
|
46
|
+
logger = logging.getLogger("createsonline.database.assistant")
|
|
47
|
+
|
|
48
|
+
class QueryType(Enum):
|
|
49
|
+
"""Supported query types with safety levels"""
|
|
50
|
+
SELECT = "select" # Always safe
|
|
51
|
+
COUNT = "count" # Always safe
|
|
52
|
+
UPDATE = "update" # Requires confirmation
|
|
53
|
+
INSERT = "insert" # Requires confirmation
|
|
54
|
+
DELETE = "delete" # Requires explicit approval
|
|
55
|
+
DROP = "drop" # Blocked by default
|
|
56
|
+
CREATE = "create" # Requires confirmation
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SafetyLevel(Enum):
|
|
60
|
+
"""Safety levels for different operations"""
|
|
61
|
+
SAFE = "safe" # No confirmation needed
|
|
62
|
+
CONFIRMATION = "confirm" # Requires user confirmation
|
|
63
|
+
EXPLICIT = "explicit" # Requires explicit --allow-destructive flag
|
|
64
|
+
BLOCKED = "blocked" # Always blocked
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DatabaseAssistant:
|
|
68
|
+
"""AI-powered database query assistant with safety controls"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, db, safety_mode: str = "strict"):
|
|
71
|
+
self.db = db
|
|
72
|
+
self.safety_mode = safety_mode
|
|
73
|
+
|
|
74
|
+
# Safety mappings
|
|
75
|
+
self.safety_map = {
|
|
76
|
+
QueryType.SELECT: SafetyLevel.SAFE,
|
|
77
|
+
QueryType.COUNT: SafetyLevel.SAFE,
|
|
78
|
+
QueryType.UPDATE: SafetyLevel.CONFIRMATION,
|
|
79
|
+
QueryType.INSERT: SafetyLevel.CONFIRMATION,
|
|
80
|
+
QueryType.DELETE: SafetyLevel.EXPLICIT,
|
|
81
|
+
QueryType.DROP: SafetyLevel.BLOCKED,
|
|
82
|
+
QueryType.CREATE: SafetyLevel.CONFIRMATION
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Natural language patterns for different query types
|
|
86
|
+
self.nl_patterns = {
|
|
87
|
+
QueryType.DROP: [
|
|
88
|
+
r'\b(drop)\b.*\b(table|database|index)\b',
|
|
89
|
+
r'\b(drop table|drop database|drop index)\b'
|
|
90
|
+
],
|
|
91
|
+
QueryType.DELETE: [
|
|
92
|
+
r'\b(delete)\b.*\b(from|users?|records?|entries?)\b',
|
|
93
|
+
r'\b(remove|drop)\b.*\b(user|record|entry)\b'
|
|
94
|
+
],
|
|
95
|
+
QueryType.UPDATE: [
|
|
96
|
+
r'\b(update)\b.*\b(set|users?|records?)\b',
|
|
97
|
+
r'\b(modify|change|set)\b.*\b(password|email|status)\b',
|
|
98
|
+
r'\b(make|turn)\b.*\b(active|inactive)\b'
|
|
99
|
+
],
|
|
100
|
+
QueryType.INSERT: [
|
|
101
|
+
r'\b(insert)\b.*\b(into|values?|users?)\b',
|
|
102
|
+
r'\b(add|create|new)\b.*\b(user|record|entry)\b'
|
|
103
|
+
],
|
|
104
|
+
QueryType.COUNT: [
|
|
105
|
+
r'\bhow many\b',
|
|
106
|
+
r'\bcount\b.*\b(users|records|entries|rows)\b',
|
|
107
|
+
r'\btotal\b.*\b(number|count)\b'
|
|
108
|
+
],
|
|
109
|
+
QueryType.SELECT: [
|
|
110
|
+
r'\b(show|display|list|get|find|fetch|retrieve)\b',
|
|
111
|
+
r'\b(what|which)\b.*\b(users|records|entries|rows)\b',
|
|
112
|
+
r'\b(last|recent|latest)\b.*\b(\d+)\b',
|
|
113
|
+
r'\bselect\b'
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Common table mapping for natural language
|
|
118
|
+
self.table_mapping = {
|
|
119
|
+
'users': 'createsonline_users',
|
|
120
|
+
'user': 'createsonline_users',
|
|
121
|
+
'sessions': 'admin_sessions',
|
|
122
|
+
'session': 'admin_sessions',
|
|
123
|
+
'settings': 'app_settings',
|
|
124
|
+
'setting': 'app_settings',
|
|
125
|
+
'conversations': 'ai_conversations',
|
|
126
|
+
'conversation': 'ai_conversations',
|
|
127
|
+
'logs': 'audit_logs',
|
|
128
|
+
'log': 'audit_logs',
|
|
129
|
+
'errors': 'audit_logs',
|
|
130
|
+
'error': 'audit_logs'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Common column mapping
|
|
134
|
+
self.column_mapping = {
|
|
135
|
+
'name': 'username',
|
|
136
|
+
'email': 'email',
|
|
137
|
+
'active': 'is_active',
|
|
138
|
+
'staff': 'is_staff',
|
|
139
|
+
'admin': 'is_superuser',
|
|
140
|
+
'created': 'date_joined',
|
|
141
|
+
'joined': 'date_joined',
|
|
142
|
+
'login': 'last_login'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def parse_natural_language(self, prompt: str) -> Dict[str, Any]:
|
|
146
|
+
"""Parse natural language prompt into structured query components"""
|
|
147
|
+
prompt_lower = prompt.lower().strip()
|
|
148
|
+
|
|
149
|
+
# Detect query type
|
|
150
|
+
query_type = self._detect_query_type(prompt_lower)
|
|
151
|
+
|
|
152
|
+
# Extract table name
|
|
153
|
+
table_name = self._extract_table_name(prompt_lower)
|
|
154
|
+
|
|
155
|
+
# Extract conditions
|
|
156
|
+
conditions = self._extract_conditions(prompt_lower)
|
|
157
|
+
|
|
158
|
+
# Extract limit/order
|
|
159
|
+
limit = self._extract_limit(prompt_lower)
|
|
160
|
+
order_by = self._extract_order(prompt_lower)
|
|
161
|
+
|
|
162
|
+
# Extract columns (for SELECT)
|
|
163
|
+
columns = self._extract_columns(prompt_lower, query_type)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
'query_type': query_type,
|
|
167
|
+
'table': table_name,
|
|
168
|
+
'columns': columns,
|
|
169
|
+
'conditions': conditions,
|
|
170
|
+
'limit': limit,
|
|
171
|
+
'order_by': order_by,
|
|
172
|
+
'original_prompt': prompt,
|
|
173
|
+
'safety_level': self.safety_map.get(query_type, SafetyLevel.BLOCKED)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
def _detect_query_type(self, prompt: str) -> QueryType:
|
|
177
|
+
"""Detect the type of query from natural language"""
|
|
178
|
+
for query_type, patterns in self.nl_patterns.items():
|
|
179
|
+
for pattern in patterns:
|
|
180
|
+
if re.search(pattern, prompt, re.IGNORECASE):
|
|
181
|
+
return query_type
|
|
182
|
+
|
|
183
|
+
# Default to SELECT for safety
|
|
184
|
+
return QueryType.SELECT
|
|
185
|
+
|
|
186
|
+
def _extract_table_name(self, prompt: str) -> str:
|
|
187
|
+
"""Extract table name from natural language"""
|
|
188
|
+
# Look for known table keywords
|
|
189
|
+
for nl_name, db_name in self.table_mapping.items():
|
|
190
|
+
if re.search(rf'\b{re.escape(nl_name)}\b', prompt):
|
|
191
|
+
return db_name
|
|
192
|
+
|
|
193
|
+
# Default to users table (most common)
|
|
194
|
+
return 'createsonline_users'
|
|
195
|
+
|
|
196
|
+
def _extract_conditions(self, prompt: str) -> List[Dict[str, Any]]:
|
|
197
|
+
"""Extract WHERE conditions from natural language"""
|
|
198
|
+
conditions = []
|
|
199
|
+
|
|
200
|
+
# Pattern: "where X is Y" or "with X = Y"
|
|
201
|
+
where_patterns = [
|
|
202
|
+
r'\b(?:where|with)\s+(\w+)\s+(?:is|=|equals?)\s+(["\']?\w+["\']?)',
|
|
203
|
+
r'\b(\w+)\s+(?:is|=|equals?)\s+(["\']?\w+["\']?)',
|
|
204
|
+
r'\bactive\b', # Special case for is_active = true
|
|
205
|
+
r'\binactive\b', # Special case for is_active = false
|
|
206
|
+
r'\badmin\b', # Special case for is_superuser = true
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
for pattern in where_patterns:
|
|
210
|
+
matches = re.finditer(pattern, prompt, re.IGNORECASE)
|
|
211
|
+
for match in matches:
|
|
212
|
+
if 'active' in match.group().lower():
|
|
213
|
+
conditions.append({
|
|
214
|
+
'column': 'is_active',
|
|
215
|
+
'operator': '=',
|
|
216
|
+
'value': not ('inactive' in match.group().lower())
|
|
217
|
+
})
|
|
218
|
+
elif 'admin' in match.group().lower():
|
|
219
|
+
conditions.append({
|
|
220
|
+
'column': 'is_superuser',
|
|
221
|
+
'operator': '=',
|
|
222
|
+
'value': True
|
|
223
|
+
})
|
|
224
|
+
elif match.groups() and len(match.groups()) >= 2:
|
|
225
|
+
column = self.column_mapping.get(match.group(1), match.group(1))
|
|
226
|
+
value = match.group(2).strip('"\'')
|
|
227
|
+
conditions.append({
|
|
228
|
+
'column': column,
|
|
229
|
+
'operator': '=',
|
|
230
|
+
'value': value
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return conditions
|
|
234
|
+
|
|
235
|
+
def _extract_limit(self, prompt: str) -> Optional[int]:
|
|
236
|
+
"""Extract LIMIT from natural language"""
|
|
237
|
+
# Pattern: "last 10", "first 5", "limit 20"
|
|
238
|
+
limit_patterns = [
|
|
239
|
+
r'\b(?:last|first|top|limit)\s+(\d+)\b',
|
|
240
|
+
r'\b(\d+)\s+(?:users|records|entries|rows)\b'
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
for pattern in limit_patterns:
|
|
244
|
+
match = re.search(pattern, prompt)
|
|
245
|
+
if match:
|
|
246
|
+
return int(match.group(1))
|
|
247
|
+
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def _extract_order(self, prompt: str) -> Optional[Dict[str, str]]:
|
|
251
|
+
"""Extract ORDER BY from natural language"""
|
|
252
|
+
order_patterns = [
|
|
253
|
+
r'\b(?:order by|sort by)\s+(\w+)\s*(asc|desc)?\b',
|
|
254
|
+
r'\b(newest|latest|recent)\b', # ORDER BY created DESC
|
|
255
|
+
r'\b(oldest|first)\b', # ORDER BY created ASC
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
for pattern in order_patterns:
|
|
259
|
+
match = re.search(pattern, prompt, re.IGNORECASE)
|
|
260
|
+
if match:
|
|
261
|
+
if 'newest' in match.group().lower() or 'latest' in match.group().lower() or 'recent' in match.group().lower():
|
|
262
|
+
return {'column': 'date_joined', 'direction': 'DESC'}
|
|
263
|
+
elif 'oldest' in match.group().lower() or 'first' in match.group().lower():
|
|
264
|
+
return {'column': 'date_joined', 'direction': 'ASC'}
|
|
265
|
+
else:
|
|
266
|
+
column = self.column_mapping.get(match.group(1), match.group(1))
|
|
267
|
+
direction = match.group(2).upper() if len(match.groups()) > 1 and match.group(2) else 'ASC'
|
|
268
|
+
return {'column': column, 'direction': direction}
|
|
269
|
+
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
def _extract_columns(self, prompt: str, query_type: QueryType) -> List[str]:
|
|
273
|
+
"""Extract columns to select"""
|
|
274
|
+
if query_type == QueryType.COUNT:
|
|
275
|
+
return ['COUNT(*)']
|
|
276
|
+
|
|
277
|
+
# Look for specific column mentions
|
|
278
|
+
columns = []
|
|
279
|
+
column_patterns = [
|
|
280
|
+
r'\b(username|email|name)\b',
|
|
281
|
+
r'\b(first_name|last_name|full name)\b',
|
|
282
|
+
r'\b(active|status)\b',
|
|
283
|
+
r'\b(admin|superuser)\b',
|
|
284
|
+
r'\b(joined|created|date)\b'
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
for pattern in column_patterns:
|
|
288
|
+
if re.search(pattern, prompt, re.IGNORECASE):
|
|
289
|
+
match = re.search(pattern, prompt, re.IGNORECASE).group()
|
|
290
|
+
mapped_column = self.column_mapping.get(match.lower(), match.lower())
|
|
291
|
+
if mapped_column not in columns:
|
|
292
|
+
columns.append(mapped_column)
|
|
293
|
+
|
|
294
|
+
# Default to all columns if none specified
|
|
295
|
+
if not columns:
|
|
296
|
+
columns = ['*']
|
|
297
|
+
|
|
298
|
+
return columns
|
|
299
|
+
|
|
300
|
+
def generate_sql(self, natural_query_or_parsed) -> str:
|
|
301
|
+
"""Generate safe SQL from natural language query or parsed query
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
natural_query_or_parsed: Either a string (natural language) or dict (parsed query)
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
SQL string
|
|
308
|
+
"""
|
|
309
|
+
# Handle both string input and parsed dict input
|
|
310
|
+
if isinstance(natural_query_or_parsed, str):
|
|
311
|
+
parsed_query = self.parse_natural_language(natural_query_or_parsed)
|
|
312
|
+
else:
|
|
313
|
+
parsed_query = natural_query_or_parsed
|
|
314
|
+
|
|
315
|
+
query_type = parsed_query['query_type']
|
|
316
|
+
table = parsed_query['table']
|
|
317
|
+
columns = parsed_query['columns']
|
|
318
|
+
conditions = parsed_query['conditions']
|
|
319
|
+
limit = parsed_query['limit']
|
|
320
|
+
order_by = parsed_query['order_by']
|
|
321
|
+
safety_level = parsed_query['safety_level']
|
|
322
|
+
|
|
323
|
+
# Safety check - block dangerous operations
|
|
324
|
+
if safety_level == SafetyLevel.BLOCKED:
|
|
325
|
+
raise ValueError(f"Query type {query_type.value} is blocked for safety reasons")
|
|
326
|
+
|
|
327
|
+
if safety_level in [SafetyLevel.EXPLICIT, SafetyLevel.CONFIRMATION]:
|
|
328
|
+
raise ValueError(f"Query type {query_type.value} requires {safety_level.value} approval")
|
|
329
|
+
|
|
330
|
+
# Validate table name for safety
|
|
331
|
+
safe_table = self.db._validate_identifier(table)
|
|
332
|
+
|
|
333
|
+
# Build SQL based on query type
|
|
334
|
+
if query_type in [QueryType.SELECT, QueryType.COUNT]:
|
|
335
|
+
# Build SELECT query
|
|
336
|
+
column_str = ', '.join(columns) if columns != ['*'] else '*'
|
|
337
|
+
sql = f"SELECT {column_str} FROM {safe_table}"
|
|
338
|
+
|
|
339
|
+
# Add WHERE conditions
|
|
340
|
+
if conditions:
|
|
341
|
+
where_parts = []
|
|
342
|
+
for condition in conditions:
|
|
343
|
+
safe_column = self.db._validate_identifier(condition['column'])
|
|
344
|
+
if isinstance(condition['value'], bool):
|
|
345
|
+
value_str = 'TRUE' if condition['value'] else 'FALSE'
|
|
346
|
+
elif isinstance(condition['value'], str):
|
|
347
|
+
value_str = f"'{condition['value']}'"
|
|
348
|
+
else:
|
|
349
|
+
value_str = str(condition['value'])
|
|
350
|
+
|
|
351
|
+
where_parts.append(f"{safe_column} {condition['operator']} {value_str}")
|
|
352
|
+
|
|
353
|
+
sql += " WHERE " + " AND ".join(where_parts)
|
|
354
|
+
|
|
355
|
+
# Add ORDER BY
|
|
356
|
+
if order_by:
|
|
357
|
+
safe_order_column = self.db._validate_identifier(order_by['column'])
|
|
358
|
+
sql += f" ORDER BY {safe_order_column} {order_by['direction']}"
|
|
359
|
+
|
|
360
|
+
# Add LIMIT
|
|
361
|
+
if limit:
|
|
362
|
+
sql += f" LIMIT {limit}"
|
|
363
|
+
|
|
364
|
+
else:
|
|
365
|
+
# For now, only support SELECT queries in safe mode
|
|
366
|
+
raise ValueError(f"Query type {query_type.value} requires explicit approval")
|
|
367
|
+
|
|
368
|
+
return sql
|
|
369
|
+
|
|
370
|
+
def execute_safe(self, sql: str, parsed_query: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
371
|
+
"""Execute SQL with safety checks and audit logging"""
|
|
372
|
+
|
|
373
|
+
# Safety check - only allow SELECT statements in safe mode
|
|
374
|
+
sql_upper = sql.strip().upper()
|
|
375
|
+
if not sql_upper.startswith('SELECT'):
|
|
376
|
+
raise ValueError("Only SELECT queries are allowed in safe mode")
|
|
377
|
+
|
|
378
|
+
# Log the query attempt
|
|
379
|
+
audit_entry = {
|
|
380
|
+
'timestamp': datetime.now().isoformat(),
|
|
381
|
+
'query_type': 'SELECT',
|
|
382
|
+
'sql': sql,
|
|
383
|
+
'original_prompt': parsed_query.get('original_prompt', '') if parsed_query else '',
|
|
384
|
+
'status': 'pending'
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
# Execute the query
|
|
389
|
+
result = self.db.execute(sql)
|
|
390
|
+
|
|
391
|
+
# Update audit log
|
|
392
|
+
audit_entry['status'] = 'success'
|
|
393
|
+
audit_entry['rows_returned'] = len(result)
|
|
394
|
+
|
|
395
|
+
# Log successful execution
|
|
396
|
+
self._log_audit_entry(audit_entry)
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
'success': True,
|
|
400
|
+
'data': result,
|
|
401
|
+
'sql': sql,
|
|
402
|
+
'rows_returned': len(result),
|
|
403
|
+
'execution_time': audit_entry['timestamp']
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
except Exception as e:
|
|
407
|
+
# Update audit log with error
|
|
408
|
+
audit_entry['status'] = 'error'
|
|
409
|
+
audit_entry['error'] = str(e)
|
|
410
|
+
|
|
411
|
+
# Log failed execution
|
|
412
|
+
self._log_audit_entry(audit_entry)
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
'success': False,
|
|
416
|
+
'error': str(e),
|
|
417
|
+
'sql': sql,
|
|
418
|
+
'execution_time': audit_entry['timestamp']
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def _log_audit_entry(self, entry: Dict[str, Any]):
|
|
422
|
+
"""Log audit entry to database"""
|
|
423
|
+
try:
|
|
424
|
+
# Create audit_logs table if it doesn't exist
|
|
425
|
+
self._ensure_audit_table()
|
|
426
|
+
|
|
427
|
+
# Insert audit entry
|
|
428
|
+
self.db.insert('audit_logs', {
|
|
429
|
+
'timestamp': entry['timestamp'],
|
|
430
|
+
'query_type': entry['query_type'],
|
|
431
|
+
'sql_query': entry['sql'],
|
|
432
|
+
'original_prompt': entry['original_prompt'],
|
|
433
|
+
'status': entry['status'],
|
|
434
|
+
'rows_affected': entry.get('rows_returned', entry.get('rows_affected', 0)),
|
|
435
|
+
'error_message': entry.get('error', ''),
|
|
436
|
+
'user_id': 1 # TODO: Get from current session
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
logger.error(f"Failed to log audit entry: {e}")
|
|
441
|
+
|
|
442
|
+
def _ensure_audit_table(self):
|
|
443
|
+
"""Ensure audit_logs table exists"""
|
|
444
|
+
create_sql = f'''
|
|
445
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
446
|
+
id {'SERIAL' if self.db.db_type == 'postgresql' else 'INTEGER'} PRIMARY KEY{' AUTOINCREMENT' if self.db.db_type == 'sqlite' else ''},
|
|
447
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
448
|
+
query_type VARCHAR(20) NOT NULL,
|
|
449
|
+
sql_query TEXT NOT NULL,
|
|
450
|
+
original_prompt TEXT,
|
|
451
|
+
status VARCHAR(20) NOT NULL,
|
|
452
|
+
rows_affected INTEGER DEFAULT 0,
|
|
453
|
+
error_message TEXT,
|
|
454
|
+
user_id INTEGER REFERENCES createsonline_users(id),
|
|
455
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
456
|
+
)
|
|
457
|
+
'''
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
cursor = self.db.connection.cursor()
|
|
461
|
+
cursor.execute(create_sql)
|
|
462
|
+
self.db.connection.commit()
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.error(f"Failed to create audit_logs table: {e}")
|
|
465
|
+
|
|
466
|
+
def query_from_natural_language(self, prompt: str) -> Dict[str, Any]:
|
|
467
|
+
"""Complete workflow: natural language → SQL → execution → results"""
|
|
468
|
+
|
|
469
|
+
# Parse the natural language prompt
|
|
470
|
+
parsed_query = self.parse_natural_language(prompt)
|
|
471
|
+
|
|
472
|
+
# Check safety level
|
|
473
|
+
safety_level = parsed_query['safety_level']
|
|
474
|
+
if safety_level == SafetyLevel.BLOCKED:
|
|
475
|
+
return {
|
|
476
|
+
'success': False,
|
|
477
|
+
'error': f"Query type {parsed_query['query_type'].value} is blocked for safety",
|
|
478
|
+
'prompt': prompt
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
# Generate SQL
|
|
483
|
+
sql = self.generate_sql(parsed_query)
|
|
484
|
+
|
|
485
|
+
# Execute safely
|
|
486
|
+
result = self.execute_safe(sql, parsed_query)
|
|
487
|
+
|
|
488
|
+
# Add parsed query info to result
|
|
489
|
+
result['parsed_query'] = parsed_query
|
|
490
|
+
result['prompt'] = prompt
|
|
491
|
+
|
|
492
|
+
return result
|
|
493
|
+
|
|
494
|
+
except Exception as e:
|
|
495
|
+
return {
|
|
496
|
+
'success': False,
|
|
497
|
+
'error': str(e),
|
|
498
|
+
'prompt': prompt,
|
|
499
|
+
'parsed_query': parsed_query
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
def explain_query(self, prompt: str) -> Dict[str, Any]:
|
|
503
|
+
"""Explain what SQL would be generated without executing it"""
|
|
504
|
+
try:
|
|
505
|
+
parsed_query = self.parse_natural_language(prompt)
|
|
506
|
+
sql = self.generate_sql(parsed_query)
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
'success': True,
|
|
510
|
+
'prompt': prompt,
|
|
511
|
+
'parsed_query': parsed_query,
|
|
512
|
+
'generated_sql': sql,
|
|
513
|
+
'explanation': self._generate_explanation(parsed_query, sql),
|
|
514
|
+
'safety_level': parsed_query['safety_level'].value
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
except Exception as e:
|
|
518
|
+
return {
|
|
519
|
+
'success': False,
|
|
520
|
+
'error': str(e),
|
|
521
|
+
'prompt': prompt
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
def _generate_explanation(self, parsed_query: Dict[str, Any], sql: str) -> str:
|
|
525
|
+
"""Generate human-readable explanation of the query"""
|
|
526
|
+
query_type = parsed_query['query_type']
|
|
527
|
+
table = parsed_query['table']
|
|
528
|
+
conditions = parsed_query['conditions']
|
|
529
|
+
limit = parsed_query['limit']
|
|
530
|
+
|
|
531
|
+
explanation = f"This will {query_type.value.upper()} data from the '{table}' table"
|
|
532
|
+
|
|
533
|
+
if conditions:
|
|
534
|
+
condition_strs = []
|
|
535
|
+
for condition in conditions:
|
|
536
|
+
condition_strs.append(f"{condition['column']} {condition['operator']} {condition['value']}")
|
|
537
|
+
explanation += f" where {' and '.join(condition_strs)}"
|
|
538
|
+
|
|
539
|
+
if limit:
|
|
540
|
+
explanation += f", limited to {limit} rows"
|
|
541
|
+
|
|
542
|
+
explanation += f". Generated SQL: {sql}"
|
|
543
|
+
|
|
544
|
+
return explanation
|
|
545
|
+
|
|
546
|
+
def get_recent_queries(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
547
|
+
"""Get recent query history from audit logs"""
|
|
548
|
+
try:
|
|
549
|
+
self._ensure_audit_table()
|
|
550
|
+
|
|
551
|
+
sql = f"""
|
|
552
|
+
SELECT timestamp, original_prompt, sql_query, status, rows_affected, error_message
|
|
553
|
+
FROM audit_logs
|
|
554
|
+
ORDER BY timestamp DESC
|
|
555
|
+
LIMIT {limit}
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
result = self.db.execute(sql)
|
|
559
|
+
return result
|
|
560
|
+
|
|
561
|
+
except Exception as e:
|
|
562
|
+
logger.error(f"Failed to get recent queries: {e}")
|
|
563
|
+
return []
|
|
564
|
+
|
|
565
|
+
# Convenience function for quick access
|
|
566
|
+
def create_assistant(db=None):
|
|
567
|
+
"""Create a database assistant instance"""
|
|
568
|
+
if db is None:
|
|
569
|
+
# Import the main database module to get DatabaseConnection
|
|
570
|
+
import sys
|
|
571
|
+
import importlib.util
|
|
572
|
+
import os
|
|
573
|
+
|
|
574
|
+
# Get the path to the main database.py file
|
|
575
|
+
db_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database.py')
|
|
576
|
+
|
|
577
|
+
# Load the database module
|
|
578
|
+
spec = importlib.util.spec_from_file_location("database_main", db_path)
|
|
579
|
+
db_module = importlib.util.module_from_spec(spec)
|
|
580
|
+
spec.loader.exec_module(db_module)
|
|
581
|
+
|
|
582
|
+
# Get the database connection
|
|
583
|
+
db = db_module.get_database()
|
|
584
|
+
|
|
585
|
+
return DatabaseAssistant(db)
|