dbagent-cli 0.7.0__tar.gz → 0.7.2__tar.gz
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.
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/PKG-INFO +1 -1
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/__init__.py +1 -1
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/conversation.py +21 -14
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/generator.py +7 -2
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/pipeline.py +125 -1
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/sql/validator.py +55 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/chat_screen.py +12 -4
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/SOURCES.txt +1 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/pyproject.toml +1 -1
- dbagent_cli-0.7.2/tests/test_direct_sql.py +83 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/README.md +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/cli.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/config.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/console.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/setup.cfg +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_cache.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_cli.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_generator.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_graph.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_providers.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_safety.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_schema.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_security.py +0 -0
- {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_semantic.py +0 -0
|
@@ -12,7 +12,8 @@ from dataclasses import dataclass, field
|
|
|
12
12
|
class ConversationTurn:
|
|
13
13
|
"""A single turn in the conversation."""
|
|
14
14
|
user_prompt: str
|
|
15
|
-
sql: str
|
|
15
|
+
sql: str = ""
|
|
16
|
+
assistant_response: str = ""
|
|
16
17
|
tables: List[str] = field(default_factory=list)
|
|
17
18
|
filters: List[str] = field(default_factory=list)
|
|
18
19
|
entities: Dict[str, str] = field(default_factory=dict)
|
|
@@ -22,10 +23,10 @@ class ConversationTurn:
|
|
|
22
23
|
|
|
23
24
|
class ConversationContext:
|
|
24
25
|
"""
|
|
25
|
-
Tracks state across interactive chat turns.
|
|
26
|
+
Tracks state and multi-turn conversational history across interactive chat turns.
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
|
-
def __init__(self, max_history: int =
|
|
29
|
+
def __init__(self, max_history: int = 15):
|
|
29
30
|
self.max_history = max_history
|
|
30
31
|
self.turns: List[ConversationTurn] = []
|
|
31
32
|
self.active_tables: Set[str] = set()
|
|
@@ -34,7 +35,8 @@ class ConversationContext:
|
|
|
34
35
|
def add_turn(
|
|
35
36
|
self,
|
|
36
37
|
user_prompt: str,
|
|
37
|
-
sql: str,
|
|
38
|
+
sql: str = "",
|
|
39
|
+
assistant_response: str = "",
|
|
38
40
|
tables: Optional[List[str]] = None,
|
|
39
41
|
filters: Optional[List[str]] = None,
|
|
40
42
|
is_chat: bool = False,
|
|
@@ -43,6 +45,7 @@ class ConversationContext:
|
|
|
43
45
|
turn = ConversationTurn(
|
|
44
46
|
user_prompt=user_prompt,
|
|
45
47
|
sql=sql,
|
|
48
|
+
assistant_response=assistant_response,
|
|
46
49
|
tables=tables or [],
|
|
47
50
|
filters=filters or [],
|
|
48
51
|
is_chat=is_chat,
|
|
@@ -59,19 +62,23 @@ class ConversationContext:
|
|
|
59
62
|
return self.turns[-1]
|
|
60
63
|
return None
|
|
61
64
|
|
|
62
|
-
def format_context_for_prompt(self) -> str:
|
|
63
|
-
"""Formats
|
|
65
|
+
def format_context_for_prompt(self, max_turns: int = 5) -> str:
|
|
66
|
+
"""Formats multi-turn conversation memory for inclusion in LLM prompt."""
|
|
64
67
|
if not self.turns:
|
|
65
68
|
return ""
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
recent = self.turns[-max_turns:]
|
|
71
|
+
lines = ["=== Previous Chat Session Memory (Use for follow-ups & context) ==="]
|
|
72
|
+
for i, t in enumerate(recent, 1):
|
|
73
|
+
lines.append(f"Turn {i}:")
|
|
74
|
+
lines.append(f" User: \"{t.user_prompt}\"")
|
|
75
|
+
if t.sql:
|
|
76
|
+
lines.append(f" Assistant (SQL): {t.sql.strip()}")
|
|
77
|
+
elif t.assistant_response:
|
|
78
|
+
clean_resp = t.assistant_response.strip().replace("\n", " ")
|
|
79
|
+
preview = (clean_resp[:120] + "...") if len(clean_resp) > 120 else clean_resp
|
|
80
|
+
lines.append(f" Assistant: \"{preview}\"")
|
|
81
|
+
lines.append("=== End Chat Session Memory ===")
|
|
75
82
|
|
|
76
83
|
return "\n".join(lines)
|
|
77
84
|
|
|
@@ -169,9 +169,10 @@ class ScriptGenerator:
|
|
|
169
169
|
self,
|
|
170
170
|
schema: DatabaseSchema,
|
|
171
171
|
user_prompt: str,
|
|
172
|
+
conversation_history: Optional[str] = None,
|
|
172
173
|
model: Optional[str] = None,
|
|
173
174
|
) -> str:
|
|
174
|
-
"""Generate a natural conversational response to the user's message."""
|
|
175
|
+
"""Generate a natural conversational response to the user's message with full session memory."""
|
|
175
176
|
table_names = [t.name for t in schema.tables]
|
|
176
177
|
table_summary = ", ".join(table_names[:30])
|
|
177
178
|
if len(table_names) > 30:
|
|
@@ -183,8 +184,12 @@ class ScriptGenerator:
|
|
|
183
184
|
table_summary=table_summary if table_summary else "No tables found",
|
|
184
185
|
)
|
|
185
186
|
|
|
187
|
+
full_prompt = user_prompt
|
|
188
|
+
if conversation_history:
|
|
189
|
+
full_prompt = f"{conversation_history}\n\nUser: {user_prompt}"
|
|
190
|
+
|
|
186
191
|
return self.llm.generate(
|
|
187
|
-
prompt=
|
|
192
|
+
prompt=full_prompt,
|
|
188
193
|
system_prompt=system_prompt,
|
|
189
194
|
model=model,
|
|
190
195
|
)
|
|
@@ -53,6 +53,7 @@ class PipelineResult:
|
|
|
53
53
|
expires_at: Optional[str] = None
|
|
54
54
|
# Conversational chat response
|
|
55
55
|
is_chat: bool = False
|
|
56
|
+
is_direct_sql: bool = False
|
|
56
57
|
message: str = ""
|
|
57
58
|
# Query classification
|
|
58
59
|
query_type: str = "unknown" # read, write, ddl, chat, unknown
|
|
@@ -183,22 +184,145 @@ class QueryPipeline:
|
|
|
183
184
|
schema = self.connector.inspect_schema(table_names=quick_tables, include_samples=False, max_samples=0)
|
|
184
185
|
|
|
185
186
|
result.schema = schema
|
|
187
|
+
conv_history = self.conversation.format_context_for_prompt()
|
|
186
188
|
result.message = self.generator.generate_chat_response(
|
|
187
189
|
schema=schema,
|
|
188
190
|
user_prompt=user_prompt,
|
|
191
|
+
conversation_history=conv_history,
|
|
189
192
|
model=self.model,
|
|
190
193
|
)
|
|
191
194
|
result.is_chat = True
|
|
192
195
|
result.was_executed = True
|
|
193
196
|
result.query_type = "chat"
|
|
194
197
|
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
195
|
-
self.conversation.add_turn(user_prompt=user_prompt, sql="", is_chat=True)
|
|
198
|
+
self.conversation.add_turn(user_prompt=user_prompt, sql="", assistant_response=result.message, is_chat=True)
|
|
196
199
|
return result
|
|
197
200
|
except Exception as e:
|
|
198
201
|
result.error = f"Chat generation failed: {str(e)}"
|
|
199
202
|
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
200
203
|
return result
|
|
201
204
|
|
|
205
|
+
# --- 1.5. Direct Raw SQL Fast-Path (0ms AI latency, 0 tokens) ---
|
|
206
|
+
if SQLValidator.is_direct_sql(user_prompt):
|
|
207
|
+
raw_sql = user_prompt.strip().rstrip(";") + ";"
|
|
208
|
+
result.sql = raw_sql
|
|
209
|
+
result.is_direct_sql = True
|
|
210
|
+
result.query_type = SQLValidator.classify_query(raw_sql)
|
|
211
|
+
result.intent = result.query_type
|
|
212
|
+
|
|
213
|
+
# AST & Security Mode Validation
|
|
214
|
+
is_allowed, violations = SQLValidator.validate_for_mode(raw_sql, mode=self.mode)
|
|
215
|
+
result.safety_violations = violations
|
|
216
|
+
if not is_allowed:
|
|
217
|
+
result.error = f"Security Policy Violation in {self.mode.value} mode: " + "; ".join(violations)
|
|
218
|
+
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
# Check Result Cache
|
|
222
|
+
if self.use_cache and not bypass_cache and result.query_type == "read":
|
|
223
|
+
cached = self.result_cache.get(self.db_name, raw_sql)
|
|
224
|
+
if cached:
|
|
225
|
+
cols, rows, age = cached
|
|
226
|
+
result.columns = cols
|
|
227
|
+
result.rows = rows
|
|
228
|
+
result.was_executed = True
|
|
229
|
+
result.is_cached = True
|
|
230
|
+
result.cache_age_seconds = age
|
|
231
|
+
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
232
|
+
result.visualization = ResultVisualizer.analyze_result_shape(cols, rows)
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
# Decide Execute or Confirm
|
|
236
|
+
should_execute = False
|
|
237
|
+
if force:
|
|
238
|
+
should_execute = True
|
|
239
|
+
elif self.mode in (OperationMode.SAFE, OperationMode.READ_ONLY):
|
|
240
|
+
should_execute = (result.query_type == "read")
|
|
241
|
+
result.was_auto_executed = should_execute
|
|
242
|
+
elif self.mode == OperationMode.CONFIRM:
|
|
243
|
+
if result.query_type == "read" and not violations:
|
|
244
|
+
should_execute = True
|
|
245
|
+
result.was_auto_executed = True
|
|
246
|
+
else:
|
|
247
|
+
result.needs_confirmation = True
|
|
248
|
+
if self.confirm_callback:
|
|
249
|
+
if self.confirm_callback(result.sql, violations):
|
|
250
|
+
should_execute = True
|
|
251
|
+
else:
|
|
252
|
+
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
253
|
+
return result
|
|
254
|
+
else:
|
|
255
|
+
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
256
|
+
return result
|
|
257
|
+
elif self.mode == OperationMode.ADMIN:
|
|
258
|
+
should_execute = True
|
|
259
|
+
|
|
260
|
+
if should_execute:
|
|
261
|
+
# 1. Attempt direct execution without AI (Zero AI Latency)
|
|
262
|
+
cols, rows, error = self.connector.execute_query(raw_sql, limit=100)
|
|
263
|
+
if error is None:
|
|
264
|
+
result.columns = cols
|
|
265
|
+
result.rows = rows
|
|
266
|
+
result.was_executed = True
|
|
267
|
+
result.error = None
|
|
268
|
+
if self.mask_sensitive_data and result.rows:
|
|
269
|
+
result.rows = SensitiveDataMasker.mask_row_set(result.columns, result.rows)
|
|
270
|
+
if result.rows:
|
|
271
|
+
result.visualization = ResultVisualizer.analyze_result_shape(result.columns, result.rows)
|
|
272
|
+
if self.use_cache and result.query_type == "read":
|
|
273
|
+
self.result_cache.set(self.db_name, raw_sql, result.columns, result.rows)
|
|
274
|
+
exec_duration = (time.time() - t_start) * 1000
|
|
275
|
+
result.execution_time_ms = exec_duration
|
|
276
|
+
self.audit_logger.log_execution(
|
|
277
|
+
database=self.db_name,
|
|
278
|
+
user=self.user_id,
|
|
279
|
+
question=user_prompt,
|
|
280
|
+
sql=result.sql,
|
|
281
|
+
mode=self.mode.value,
|
|
282
|
+
status="SUCCESS",
|
|
283
|
+
execution_time_ms=exec_duration,
|
|
284
|
+
rows_returned=len(result.rows),
|
|
285
|
+
risk_level="LOW",
|
|
286
|
+
)
|
|
287
|
+
self.conversation.add_turn(user_prompt=user_prompt, sql=result.sql, is_chat=False)
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
# 2. Direct query failed with DB error -> Fallback to AI Auto-Repair
|
|
291
|
+
try:
|
|
292
|
+
exact, fuzzy = self.connector.resolve_tables(raw_sql)
|
|
293
|
+
except Exception:
|
|
294
|
+
exact, fuzzy = [], []
|
|
295
|
+
sub_schema, join_steps = self._get_fast_schema_context(raw_sql, exact, fuzzy)
|
|
296
|
+
result.schema = sub_schema
|
|
297
|
+
result.join_steps = join_steps
|
|
298
|
+
repair_prompt = f"The following direct SQL query failed with a database error. Please correct the query using the database schema:\nFailed SQL: {raw_sql}\nError: {error}"
|
|
299
|
+
self._execute_with_retry(result, repair_prompt, sub_schema)
|
|
300
|
+
|
|
301
|
+
if result.success:
|
|
302
|
+
if self.mask_sensitive_data and result.rows:
|
|
303
|
+
result.rows = SensitiveDataMasker.mask_row_set(result.columns, result.rows)
|
|
304
|
+
if result.rows:
|
|
305
|
+
result.visualization = ResultVisualizer.analyze_result_shape(result.columns, result.rows)
|
|
306
|
+
if self.use_cache and result.query_type == "read":
|
|
307
|
+
self.result_cache.set(self.db_name, result.sql, result.columns, result.rows)
|
|
308
|
+
self.conversation.add_turn(user_prompt=user_prompt, sql=result.sql, is_chat=False)
|
|
309
|
+
|
|
310
|
+
exec_duration = (time.time() - t_start) * 1000
|
|
311
|
+
result.execution_time_ms = exec_duration
|
|
312
|
+
self.audit_logger.log_execution(
|
|
313
|
+
database=self.db_name,
|
|
314
|
+
user=self.user_id,
|
|
315
|
+
question=user_prompt,
|
|
316
|
+
sql=result.sql,
|
|
317
|
+
mode=self.mode.value,
|
|
318
|
+
status="SUCCESS" if result.success else "FAILED",
|
|
319
|
+
execution_time_ms=exec_duration,
|
|
320
|
+
rows_returned=len(result.rows),
|
|
321
|
+
risk_level=result.complexity.risk_level if result.complexity else "LOW",
|
|
322
|
+
error=result.error,
|
|
323
|
+
)
|
|
324
|
+
return result
|
|
325
|
+
|
|
202
326
|
# --- 2. Resolve Tables & Detect Ambiguity ---
|
|
203
327
|
try:
|
|
204
328
|
exact, fuzzy = self.connector.resolve_tables(user_prompt)
|
|
@@ -52,6 +52,61 @@ class SQLValidator:
|
|
|
52
52
|
r"^\s*REVOKE\b",
|
|
53
53
|
]
|
|
54
54
|
|
|
55
|
+
DIRECT_SQL_REGEX = re.compile(
|
|
56
|
+
r"^\s*(?:"
|
|
57
|
+
r"WITH\b\s+[a-zA-Z0-9_\"`]+\s+AS\b|"
|
|
58
|
+
r"INSERT\s+(?:INTO\s+)?[a-zA-Z0-9_\"`.]+|"
|
|
59
|
+
r"UPDATE\s+[a-zA-Z0-9_\"`.]+\s+SET\b|"
|
|
60
|
+
r"DELETE\s+FROM\s+[a-zA-Z0-9_\"`.]+|"
|
|
61
|
+
r"CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|VIEW|DATABASE|SCHEMA|UNIQUE\s+INDEX)\b|"
|
|
62
|
+
r"ALTER\s+(?:TABLE|VIEW)\b|"
|
|
63
|
+
r"DROP\s+(?:TABLE|VIEW|INDEX|DATABASE|SCHEMA)\b|"
|
|
64
|
+
r"TRUNCATE\s+(?:TABLE\s+)?[a-zA-Z0-9_\"`.]+|"
|
|
65
|
+
r"SHOW\s+(?:TABLES|DATABASES|COLUMNS|SCHEMAS|PROCESSLIST|VARIABLES|STATUS|CREATE\s+TABLE)\b|"
|
|
66
|
+
r"DESCRIBE\s+[a-zA-Z0-9_\"`.]+|"
|
|
67
|
+
r"DESC\s+[a-zA-Z0-9_\"`.]+|"
|
|
68
|
+
r"EXPLAIN\s+(?:ANALYZE\s+)?(?:SELECT|INSERT|UPDATE|DELETE|WITH)\b|"
|
|
69
|
+
r"PRAGMA\s+[a-zA-Z0-9_\"`]+"
|
|
70
|
+
r")",
|
|
71
|
+
re.IGNORECASE | re.DOTALL,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def is_direct_sql(cls, text: str) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Determines if the input string is directly a raw SQL query (bypassing AI generation).
|
|
78
|
+
"""
|
|
79
|
+
if not text or not text.strip():
|
|
80
|
+
return False
|
|
81
|
+
clean = re.sub(r"--[^\n]*\n?", "", text).strip()
|
|
82
|
+
clean = re.sub(r"/\*.*?\*/", "", clean, flags=re.DOTALL).strip()
|
|
83
|
+
clean_lower = clean.lower()
|
|
84
|
+
|
|
85
|
+
# Check explicit non-SELECT DDL/DML/Utility SQL patterns
|
|
86
|
+
if cls.DIRECT_SQL_REGEX.search(clean):
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
# Check SELECT statements
|
|
90
|
+
if clean_lower.startswith("select"):
|
|
91
|
+
tokens = clean.split()
|
|
92
|
+
if len(tokens) < 2:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
# Natural language conversational words indicate prompt rather than raw SQL
|
|
96
|
+
nl_words = ["the ", " who ", " which ", " that ", " whose ", " please ", " me ", " their ", " those "]
|
|
97
|
+
if any(w in clean_lower for w in nl_words):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Single expression queries: SELECT 1;, SELECT NOW();, SELECT version();
|
|
101
|
+
if len(tokens) == 2 and (tokens[1].rstrip(";").isdigit() or tokens[1].endswith("()") or tokens[1].startswith("@@")):
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
# Standard SQL SELECT ... FROM ...
|
|
105
|
+
if " from " in clean_lower:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
return False
|
|
109
|
+
|
|
55
110
|
@classmethod
|
|
56
111
|
def split_statements(cls, sql: str) -> List[str]:
|
|
57
112
|
"""Splits SQL script into individual statements, ignoring semicolons inside quotes/literals."""
|
|
@@ -87,12 +87,17 @@ def render_assistant_turn(
|
|
|
87
87
|
ai_model: Optional[str] = None,
|
|
88
88
|
) -> None:
|
|
89
89
|
"""Render the assistant response card with SQL, data grid, and execution timing."""
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
if result.is_direct_sql and result.retries == 0:
|
|
91
|
+
agent_title = "[bold cyan]🤖 DB-Agent[/bold cyan] [dim](Direct SQL • 0 tokens)[/dim]"
|
|
92
|
+
else:
|
|
93
|
+
model_display = f" ({ai_model})" if ai_model else ""
|
|
94
|
+
agent_title = f"[bold cyan]🤖 DB-Agent{model_display}[/bold cyan]"
|
|
95
|
+
|
|
96
|
+
timing_str = f"⚡ {result.execution_time_ms / 1000:.2f}s" if result.execution_time_ms > 0 else ""
|
|
92
97
|
|
|
93
98
|
# Conversational Chat Response
|
|
94
99
|
if result.is_chat and result.message:
|
|
95
|
-
console.print(f"\n
|
|
100
|
+
console.print(f"\n{agent_title} [dim]{timing_str}[/dim]")
|
|
96
101
|
console.print(Markdown(result.message))
|
|
97
102
|
console.print("")
|
|
98
103
|
return
|
|
@@ -119,8 +124,11 @@ def render_assistant_turn(
|
|
|
119
124
|
|
|
120
125
|
# Executed Result Response
|
|
121
126
|
if result.was_executed:
|
|
122
|
-
console.print(f"\n
|
|
127
|
+
console.print(f"\n{agent_title} [dim]{timing_str}[/dim]")
|
|
123
128
|
|
|
129
|
+
if result.is_direct_sql and result.retries > 0:
|
|
130
|
+
console.print(f"[bold green]✓ Direct SQL failed on database and was automatically fixed by AI ({result.retries} retry)[/bold green]")
|
|
131
|
+
|
|
124
132
|
if show_sql and result.sql:
|
|
125
133
|
sql_clean = result.sql.strip()
|
|
126
134
|
syntax = Syntax(sql_clean, "sql", theme="monokai", line_numbers=False)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbagent-cli"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Direct SQL Fast-Path and Self-Healing AI Auto-Correction.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dbagent.sql.validator import SQLValidator
|
|
6
|
+
from dbagent.agent.pipeline import QueryPipeline
|
|
7
|
+
from dbagent.connectors.factory import create_connector
|
|
8
|
+
from dbagent.llm.mock_provider import MockProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_is_direct_sql_detection():
|
|
12
|
+
# Direct SQL queries
|
|
13
|
+
assert SQLValidator.is_direct_sql("SELECT * FROM users;") is True
|
|
14
|
+
assert SQLValidator.is_direct_sql("select pk_user_id, username from ivis_user where active = 'Y'") is True
|
|
15
|
+
assert SQLValidator.is_direct_sql("SELECT count(*) FROM orders") is True
|
|
16
|
+
assert SQLValidator.is_direct_sql("SELECT 1;") is True
|
|
17
|
+
assert SQLValidator.is_direct_sql("WITH cte AS (SELECT 1) SELECT * FROM cte") is True
|
|
18
|
+
assert SQLValidator.is_direct_sql("INSERT INTO users (id, name) VALUES (1, 'Alice')") is True
|
|
19
|
+
assert SQLValidator.is_direct_sql("UPDATE users SET active = 'Y' WHERE id = 1") is True
|
|
20
|
+
assert SQLValidator.is_direct_sql("DELETE FROM users WHERE id = 1") is True
|
|
21
|
+
assert SQLValidator.is_direct_sql("CREATE TABLE test (id INT);") is True
|
|
22
|
+
assert SQLValidator.is_direct_sql("SHOW TABLES;") is True
|
|
23
|
+
assert SQLValidator.is_direct_sql("DESCRIBE users;") is True
|
|
24
|
+
assert SQLValidator.is_direct_sql("EXPLAIN SELECT * FROM users") is True
|
|
25
|
+
assert SQLValidator.is_direct_sql("PRAGMA table_info(users)") is True
|
|
26
|
+
|
|
27
|
+
# Natural language queries (must NOT be detected as direct SQL)
|
|
28
|
+
assert SQLValidator.is_direct_sql("list top 10 most recent active users") is False
|
|
29
|
+
assert SQLValidator.is_direct_sql("show me the active users") is False
|
|
30
|
+
assert SQLValidator.is_direct_sql("how many users registered today") is False
|
|
31
|
+
assert SQLValidator.is_direct_sql("select the active users who joined last week") is False
|
|
32
|
+
assert SQLValidator.is_direct_sql("hello") is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_pipeline_direct_sql_fast_path():
|
|
36
|
+
# Set up in-memory sqlite database
|
|
37
|
+
connector = create_connector("sqlite:///:memory:")
|
|
38
|
+
connector.execute_query("CREATE TABLE users (id INT, name VARCHAR, active VARCHAR(1));")
|
|
39
|
+
connector.execute_query("INSERT INTO users VALUES (1, 'Alice', 'Y'), (2, 'Bob', 'N');")
|
|
40
|
+
|
|
41
|
+
mock_llm = MockProvider()
|
|
42
|
+
pipeline = QueryPipeline(
|
|
43
|
+
connector=connector,
|
|
44
|
+
llm=mock_llm,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# 1. Direct valid query executes without LLM (0 retries)
|
|
48
|
+
result = pipeline.run("SELECT * FROM users WHERE active = 'Y';")
|
|
49
|
+
assert result.was_executed is True
|
|
50
|
+
assert result.is_direct_sql is True
|
|
51
|
+
assert result.retries == 0
|
|
52
|
+
assert len(result.rows) == 1
|
|
53
|
+
assert result.rows[0]["name"] == "Alice"
|
|
54
|
+
assert result.error is None
|
|
55
|
+
|
|
56
|
+
connector.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_pipeline_direct_sql_auto_repair():
|
|
60
|
+
connector = create_connector("sqlite:///:memory:")
|
|
61
|
+
connector.execute_query("CREATE TABLE users (id INT, name VARCHAR, active VARCHAR(1));")
|
|
62
|
+
connector.execute_query("INSERT INTO users VALUES (1, 'Alice', 'Y'), (2, 'Bob', 'N');")
|
|
63
|
+
|
|
64
|
+
class RepairMockLLM(MockProvider):
|
|
65
|
+
def generate(self, prompt: str, system_prompt: str = None, model: str = None, stream_callback=None) -> str:
|
|
66
|
+
# Emulate AI fixing the typo 'usrs' -> 'users'
|
|
67
|
+
return "SELECT * FROM users WHERE active = 'Y';"
|
|
68
|
+
|
|
69
|
+
pipeline = QueryPipeline(
|
|
70
|
+
connector=connector,
|
|
71
|
+
llm=RepairMockLLM(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Direct query with typo in table name 'usrs' -> fails initially on DB, then AI auto-repairs to 'users'
|
|
75
|
+
result = pipeline.run("SELECT * FROM usrs WHERE active = 'Y';")
|
|
76
|
+
assert result.was_executed is True
|
|
77
|
+
assert result.is_direct_sql is True
|
|
78
|
+
assert result.retries == 1
|
|
79
|
+
assert "users" in result.sql
|
|
80
|
+
assert len(result.rows) == 1
|
|
81
|
+
assert result.rows[0]["name"] == "Alice"
|
|
82
|
+
|
|
83
|
+
connector.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|