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.
Files changed (81) hide show
  1. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/PKG-INFO +1 -1
  2. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/conversation.py +21 -14
  4. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/generator.py +7 -2
  5. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/pipeline.py +125 -1
  6. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/sql/validator.py +55 -0
  7. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/chat_screen.py +12 -4
  8. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/PKG-INFO +1 -1
  9. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/SOURCES.txt +1 -0
  10. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/pyproject.toml +1 -1
  11. dbagent_cli-0.7.2/tests/test_direct_sql.py +83 -0
  12. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/README.md +0 -0
  13. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/confidence.py +0 -0
  14. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/doctor.py +0 -0
  15. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/error_classifier.py +0 -0
  16. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/recommender.py +0 -0
  17. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/agent/validator.py +0 -0
  18. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/cli.py +0 -0
  19. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/config.py +0 -0
  20. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/base.py +0 -0
  21. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/factory.py +0 -0
  22. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/mongo.py +0 -0
  23. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/connectors/relational.py +0 -0
  24. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/__init__.py +0 -0
  25. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/cache.py +0 -0
  26. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/changeset.py +0 -0
  27. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/cost.py +0 -0
  28. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/recovery.py +0 -0
  29. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/sandbox.py +0 -0
  30. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/execution/streaming.py +0 -0
  31. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/anthropic_provider.py +0 -0
  32. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/auto_setup.py +0 -0
  33. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/base.py +0 -0
  34. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/deepseek_provider.py +0 -0
  35. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/factory.py +0 -0
  36. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/gemini_provider.py +0 -0
  37. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/groq_provider.py +0 -0
  38. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/mistral_provider.py +0 -0
  39. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/mock_provider.py +0 -0
  40. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/ollama_provider.py +0 -0
  41. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/openai_provider.py +0 -0
  42. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/llm/openrouter_provider.py +0 -0
  43. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/cache.py +0 -0
  44. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/formatter.py +0 -0
  45. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/graph.py +0 -0
  46. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/models.py +0 -0
  47. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/retriever.py +0 -0
  48. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/schema/selector.py +0 -0
  49. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/__init__.py +0 -0
  50. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/audit.py +0 -0
  51. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/environment.py +0 -0
  52. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/masking.py +0 -0
  53. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/modes.py +0 -0
  54. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/security/rbac.py +0 -0
  55. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/semantic/__init__.py +0 -0
  56. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/semantic/dictionary.py +0 -0
  57. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/semantic/templates.py +0 -0
  58. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/sql/__init__.py +0 -0
  59. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/sql/optimizer.py +0 -0
  60. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/console.py +0 -0
  61. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/grid_window.py +0 -0
  62. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/viewer.py +0 -0
  63. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent/ui/visualizer.py +0 -0
  64. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  65. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/entry_points.txt +0 -0
  66. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/requires.txt +0 -0
  67. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/dbagent_cli.egg-info/top_level.txt +0 -0
  68. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/setup.cfg +0 -0
  69. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_cache.py +0 -0
  70. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_cli.py +0 -0
  71. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_connectors.py +0 -0
  72. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_doctor_and_viz.py +0 -0
  73. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_generator.py +0 -0
  74. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_graph.py +0 -0
  75. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_providers.py +0 -0
  76. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_recovery.py +0 -0
  77. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_safety.py +0 -0
  78. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_sandbox.py +0 -0
  79. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_schema.py +0 -0
  80. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_security.py +0 -0
  81. {dbagent_cli-0.7.0 → dbagent_cli-0.7.2}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -2,5 +2,5 @@
2
2
  DB-Agent: Universal Database Introspection and Script Generation AI Agent (CLI).
3
3
  """
4
4
 
5
- __version__ = "0.7.0"
5
+ __version__ = "0.7.2"
6
6
  __author__ = "Santhosh Gupta"
@@ -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 = 10):
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 recent context for inclusion in LLM prompt."""
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
- recent_valid = [t for t in self.turns[-3:] if t.sql and not t.is_chat]
68
- if not recent_valid:
69
- return ""
70
-
71
- lines = ["Previous Conversation Context (Use for follow-up refinements):"]
72
- for i, t in enumerate(recent_valid, 1):
73
- lines.append(f" {i}. User: \"{t.user_prompt}\"")
74
- lines.append(f" Last SQL: `{t.sql.strip()}`")
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=user_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
- model_display = f" ({ai_model})" if ai_model else ""
91
- timing_str = f" {result.execution_time_ms / 1000:.1f}s" if result.execution_time_ms > 0 else ""
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[bold cyan]🤖 DB-Agent{model_display}[/bold cyan] [dim]{timing_str}[/dim]")
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[bold cyan]🤖 DB-Agent{model_display}[/bold cyan] [dim]{timing_str}[/dim]")
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -66,6 +66,7 @@ dbagent_cli.egg-info/top_level.txt
66
66
  tests/test_cache.py
67
67
  tests/test_cli.py
68
68
  tests/test_connectors.py
69
+ tests/test_direct_sql.py
69
70
  tests/test_doctor_and_viz.py
70
71
  tests/test_generator.py
71
72
  tests/test_graph.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbagent-cli"
7
- version = "0.7.0"
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