dbagent-cli 0.11.1__tar.gz → 0.11.3__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 (100) hide show
  1. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/PKG-INFO +1 -1
  2. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/__init__.py +1 -1
  3. dbagent_cli-0.11.3/dbagent/agent/conversation.py +140 -0
  4. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/generator.py +10 -0
  5. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/pipeline.py +96 -17
  6. dbagent_cli-0.11.3/dbagent/schema/ground_truth.py +337 -0
  7. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/PKG-INFO +1 -1
  8. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/SOURCES.txt +2 -0
  9. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/pyproject.toml +1 -1
  10. dbagent_cli-0.11.3/tests/test_ground_truth_sql_validation.py +150 -0
  11. dbagent_cli-0.11.3/tests/test_multi_turn_conversation.py +102 -0
  12. dbagent_cli-0.11.1/dbagent/agent/conversation.py +0 -93
  13. dbagent_cli-0.11.1/dbagent/schema/ground_truth.py +0 -126
  14. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/README.md +0 -0
  15. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/confidence.py +0 -0
  16. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/doctor.py +0 -0
  17. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/error_classifier.py +0 -0
  18. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/recommender.py +0 -0
  19. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/validator.py +0 -0
  20. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/benchmark.py +0 -0
  21. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/cli.py +0 -0
  22. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/config.py +0 -0
  23. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/base.py +0 -0
  24. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/factory.py +0 -0
  25. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/mongo.py +0 -0
  26. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/relational.py +0 -0
  27. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/errors.py +0 -0
  28. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/__init__.py +0 -0
  29. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/cache.py +0 -0
  30. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/changeset.py +0 -0
  31. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/cost.py +0 -0
  32. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/prompt_queue.py +0 -0
  33. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/recovery.py +0 -0
  34. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/sandbox.py +0 -0
  35. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/streaming.py +0 -0
  36. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/anthropic_provider.py +0 -0
  37. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/auto_setup.py +0 -0
  38. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/base.py +0 -0
  39. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/deepseek_provider.py +0 -0
  40. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/factory.py +0 -0
  41. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/gemini_provider.py +0 -0
  42. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/groq_provider.py +0 -0
  43. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/mistral_provider.py +0 -0
  44. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/mock_provider.py +0 -0
  45. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/ollama_provider.py +0 -0
  46. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/openai_provider.py +0 -0
  47. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/openrouter_provider.py +0 -0
  48. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/plugins.py +0 -0
  49. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sample_db.py +0 -0
  50. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/cache.py +0 -0
  51. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/formatter.py +0 -0
  52. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/graph.py +0 -0
  53. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/models.py +0 -0
  54. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/retriever.py +0 -0
  55. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/selector.py +0 -0
  56. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/__init__.py +0 -0
  57. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/audit.py +0 -0
  58. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/environment.py +0 -0
  59. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/masking.py +0 -0
  60. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/modes.py +0 -0
  61. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/rbac.py +0 -0
  62. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/__init__.py +0 -0
  63. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/dictionary.py +0 -0
  64. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/knowledge.py +0 -0
  65. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/templates.py +0 -0
  66. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sql/__init__.py +0 -0
  67. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sql/optimizer.py +0 -0
  68. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sql/validator.py +0 -0
  69. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/chat_screen.py +0 -0
  70. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/console.py +0 -0
  71. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/grid_window.py +0 -0
  72. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/live_streamer.py +0 -0
  73. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/viewer.py +0 -0
  74. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/visualizer.py +0 -0
  75. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  76. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/entry_points.txt +0 -0
  77. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/requires.txt +0 -0
  78. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/top_level.txt +0 -0
  79. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/setup.cfg +0 -0
  80. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_benchmarks.py +0 -0
  81. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_cache.py +0 -0
  82. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_cli.py +0 -0
  83. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_connectors.py +0 -0
  84. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_demo_and_sample_db.py +0 -0
  85. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_direct_sql.py +0 -0
  86. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_doctor_and_viz.py +0 -0
  87. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_formats_and_exit_codes.py +0 -0
  88. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_generator.py +0 -0
  89. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_graph.py +0 -0
  90. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_knowledge.py +0 -0
  91. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_plugins.py +0 -0
  92. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_prompt_queue.py +0 -0
  93. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_prompt_queue_and_sql.py +0 -0
  94. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_providers.py +0 -0
  95. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_recovery.py +0 -0
  96. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_safety.py +0 -0
  97. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_sandbox.py +0 -0
  98. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_schema.py +0 -0
  99. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_security.py +0 -0
  100. {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.11.1
3
+ Version: 0.11.3
4
4
  Summary: The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)
5
5
  Author: Santhosh Gupta
6
6
  License: MIT
@@ -2,5 +2,5 @@
2
2
  DB-Agent: Universal Database Introspection and Script Generation AI Agent (CLI).
3
3
  """
4
4
 
5
- __version__ = "0.11.1"
5
+ __version__ = "0.11.3"
6
6
  __author__ = "Santhosh Gupta"
@@ -0,0 +1,140 @@
1
+ """
2
+ Multi-Turn Conversational Context Tracker.
3
+ Maintains active tables, filter predicates, entities, and generated SQL across chat turns
4
+ so users can refine, modify, rollback, or question queries naturally (e.g. 'modify the above', 'give rollback query', 'only active users').
5
+ """
6
+
7
+ import re
8
+ from typing import Dict, Any, List, Optional, Set
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class ConversationTurn:
14
+ """A single turn in the conversation."""
15
+ user_prompt: str
16
+ sql: str = ""
17
+ assistant_response: str = ""
18
+ tables: List[str] = field(default_factory=list)
19
+ filters: List[str] = field(default_factory=list)
20
+ entities: Dict[str, str] = field(default_factory=dict)
21
+ limit: Optional[int] = None
22
+ was_executed: bool = False
23
+ is_chat: bool = False
24
+ query_type: str = "read"
25
+
26
+
27
+ class ConversationContext:
28
+ """
29
+ Tracks state and multi-turn conversational history across interactive chat turns.
30
+ """
31
+
32
+ FOLLOWUP_INDICATORS = [
33
+ "above", "previous", "that", "this", "it", "rollback", "undo", "reverse",
34
+ "modify", "change it", "change that", "update that", "fix that", "correct this",
35
+ "instead", "also", "same query", "same table", "same as above", "again",
36
+ "for the above", "for that", "for this", "what about", "how about",
37
+ "now filter", "now show", "now sort", "add where", "add order", "add limit",
38
+ "remove limit", "remove where", "only active", "only inactive",
39
+ ]
40
+
41
+ def __init__(self, max_history: int = 15):
42
+ self.max_history = max_history
43
+ self.turns: List[ConversationTurn] = []
44
+ self.active_tables: Set[str] = set()
45
+ self.active_filters: List[str] = []
46
+
47
+ def add_turn(
48
+ self,
49
+ user_prompt: str,
50
+ sql: str = "",
51
+ assistant_response: str = "",
52
+ tables: Optional[List[str]] = None,
53
+ filters: Optional[List[str]] = None,
54
+ was_executed: bool = False,
55
+ is_chat: bool = False,
56
+ query_type: str = "read",
57
+ ) -> None:
58
+ """Records a completed or proposed turn and updates active context."""
59
+ cleaned_tables = [t.lower() for t in (tables or []) if t]
60
+
61
+ # If SQL is present, also extract table names from FROM/JOIN clauses
62
+ if sql:
63
+ for m in re.findall(r'\b(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+([a-zA-Z0-9_]+)', sql, re.IGNORECASE):
64
+ t_lower = m.lower()
65
+ if t_lower not in ("select", "where", "set", "values", "as") and t_lower not in cleaned_tables:
66
+ cleaned_tables.append(t_lower)
67
+
68
+ turn = ConversationTurn(
69
+ user_prompt=user_prompt,
70
+ sql=sql,
71
+ assistant_response=assistant_response,
72
+ tables=cleaned_tables,
73
+ filters=filters or [],
74
+ was_executed=was_executed,
75
+ is_chat=is_chat,
76
+ query_type=query_type,
77
+ )
78
+ self.turns.append(turn)
79
+ if len(self.turns) > self.max_history:
80
+ self.turns.pop(0)
81
+
82
+ if cleaned_tables:
83
+ self.active_tables.update(cleaned_tables)
84
+
85
+ def get_active_tables(self) -> List[str]:
86
+ """Returns currently active database table names discussed in this session."""
87
+ if not self.active_tables and self.turns:
88
+ for t in reversed(self.turns):
89
+ if t.tables:
90
+ return list(t.tables)
91
+ return list(self.active_tables)
92
+
93
+ def get_last_turn(self) -> Optional[ConversationTurn]:
94
+ """Returns the most recent conversation turn."""
95
+ if self.turns:
96
+ return self.turns[-1]
97
+ return None
98
+
99
+ def get_last_sql(self) -> Optional[str]:
100
+ """Returns the most recent SQL statement generated in the session."""
101
+ for t in reversed(self.turns):
102
+ if t.sql and t.sql.strip():
103
+ return t.sql.strip()
104
+ return None
105
+
106
+ def is_followup_prompt(self, user_prompt: str) -> bool:
107
+ """Checks if a user prompt is modifying, rolling back, or referencing previous turns."""
108
+ if not self.turns:
109
+ return False
110
+ p_lower = user_prompt.lower()
111
+ return any(ind in p_lower for ind in self.FOLLOWUP_INDICATORS)
112
+
113
+ def format_context_for_prompt(self, max_turns: int = 6) -> str:
114
+ """Formats multi-turn conversation memory with full SQL for inclusion in LLM prompt."""
115
+ if not self.turns:
116
+ return ""
117
+
118
+ recent = self.turns[-max_turns:]
119
+ lines = ["=== Previous Chat Session Memory (Use for follow-ups, modifications, rollbacks & context) ==="]
120
+ for i, t in enumerate(recent, 1):
121
+ lines.append(f"Turn {i}:")
122
+ lines.append(f" User Request: \"{t.user_prompt}\"")
123
+ if t.sql:
124
+ status = "Executed" if t.was_executed else "Generated / Proposed"
125
+ lines.append(f" Previous SQL ({status}):\n{t.sql.strip()}")
126
+ if t.assistant_response:
127
+ clean_resp = t.assistant_response.strip().replace("\n", " ")
128
+ preview = (clean_resp[:250] + "...") if len(clean_resp) > 250 else clean_resp
129
+ lines.append(f" Assistant Output: \"{preview}\"")
130
+ if t.tables:
131
+ lines.append(f" Tables Referenced: {', '.join(t.tables)}")
132
+ lines.append("=== End Chat Session Memory ===")
133
+
134
+ return "\n".join(lines)
135
+
136
+ def clear(self) -> None:
137
+ """Resets the conversation history."""
138
+ self.turns.clear()
139
+ self.active_tables.clear()
140
+ self.active_filters.clear()
@@ -63,6 +63,16 @@ Generate ONLY the raw executable SQL query for the given database. No markdown f
63
63
  - For PostgreSQL: Use a single Common Table Expression (CTE) with data-modifying statements deleting from all child, mapping, activity, profile, and preference tables first, and finally deleting from the parent table (e.g. `WITH target AS (SELECT pk_user_id FROM ivis_user WHERE ...), del_act AS (DELETE FROM user_activity WHERE user_id IN (SELECT pk_user_id FROM target)), del_map AS (DELETE FROM ivis_user_access WHERE fk_user_id IN (SELECT pk_user_id FROM target)) DELETE FROM ivis_user WHERE pk_user_id IN (SELECT pk_user_id FROM target);`).
64
64
  - For MySQL / SQLite: Generate clean, ordered DELETE statements deleting referencing child records before the parent record.
65
65
  - Always delete from all referencing tables present in the schema to avoid foreign key violations.
66
+ 11. Multi-Turn Session Memory & Query Refinement:
67
+ - When the user asks to modify, refine, change, or fix a query (e.g. "modify the above", "change it to X", "add a filter", "order by Y", "show site names instead", "fix the error"):
68
+ - Locate the previous SQL statement in the Chat Session Memory, apply the requested changes, and output the complete, valid modified SQL query.
69
+ 12. Rollback & Reversal Queries:
70
+ - When the user asks for a "rollback query", "undo query", or "reversal script" for a previous query (e.g. "give the rollback query for the above one", "undo that update", "how to revert this"):
71
+ - Inspect the previous SQL statement in the Chat Session Memory.
72
+ - For an UPDATE: Generate the inverse UPDATE statement restoring the original/previous values or setting them back to the previous keys.
73
+ - For an INSERT: Generate the corresponding DELETE statement.
74
+ - For a DELETE: If the previous query deleted specific records or used a CTE, generate the corresponding recovery script or reverse CTE.
75
+ - NEVER output dummy placeholders like `SELECT 1` when a prior query exists to be modified or rolled back.
66
76
  """
67
77
 
68
78
 
@@ -134,8 +134,13 @@ class QueryPipeline:
134
134
  # Database identifier
135
135
  self.db_name = "database"
136
136
  if hasattr(connector, "engine") and connector.engine:
137
- self.db_name = connector.engine.url.database or "database"
138
- self.db_id = str(connector.engine.url)
137
+ try:
138
+ db_val = getattr(getattr(connector.engine, "url", None), "database", None)
139
+ if db_val and isinstance(db_val, str):
140
+ self.db_name = db_val
141
+ self.db_id = str(getattr(connector.engine, "url", self.db_name))
142
+ except Exception:
143
+ self.db_id = self.db_name
139
144
  else:
140
145
  self.db_id = self.db_name
141
146
  self.cache_mgr = SchemaCacheManager()
@@ -160,7 +165,10 @@ class QueryPipeline:
160
165
 
161
166
  # 2. Targeted on-demand table introspection with automatic junction expansion
162
167
  target_names = list(set(exact + fuzzy))
163
- is_delete_or_mapping = any(w in user_prompt.lower() for w in ["delete", "remove", "drop", "cascade", "mapping", "map", "link", "clean", "without any mappings"])
168
+ is_delete_or_mapping = any(w in user_prompt.lower() for w in [
169
+ "delete", "remove", "drop", "cascade", "mapping", "map", "link", "clean",
170
+ "without any mappings", "change", "update", "rollback", "undo", "revert"
171
+ ])
164
172
  max_tables_limit = 25 if is_delete_or_mapping else 12
165
173
 
166
174
  if target_names:
@@ -442,7 +450,17 @@ class QueryPipeline:
442
450
  except AttributeError:
443
451
  exact, fuzzy = [], []
444
452
 
445
- if not exact and len(fuzzy) > 1:
453
+ # If user prompt is a follow-up or has no table mentions, inherit active tables from previous conversation turns
454
+ is_followup = self.conversation.is_followup_prompt(user_prompt)
455
+ if (not exact and not fuzzy) or (not exact and is_followup):
456
+ active_from_ctx = self.conversation.get_active_tables()
457
+ if active_from_ctx:
458
+ exact = active_from_ctx[:10]
459
+ result.exact_tables = exact
460
+ # Also prevent false ambiguity prompt if we have strong session context
461
+ result.ambiguous = False
462
+
463
+ if not exact and len(fuzzy) > 1 and not is_followup:
446
464
  result.ambiguous = True
447
465
  result.ambiguous_choices = fuzzy
448
466
  if self.choice_callback:
@@ -469,6 +487,14 @@ class QueryPipeline:
469
487
  conv_context = self.conversation.format_context_for_prompt()
470
488
 
471
489
  augmented_prompt = user_prompt
490
+ if is_followup and self.conversation.get_last_sql():
491
+ last_sql = self.conversation.get_last_sql()
492
+ followup_hint = (
493
+ f"[Follow-Up Note: The user's query refers directly to the previous query in the session.]\n"
494
+ f"[Target Previous SQL:\n{last_sql}\n]"
495
+ )
496
+ augmented_prompt = f"{followup_hint}\n\n{augmented_prompt}"
497
+
472
498
  if dict_context:
473
499
  augmented_prompt = f"{dict_context}\n\n{augmented_prompt}"
474
500
  if kb_context:
@@ -492,6 +518,42 @@ class QueryPipeline:
492
518
  result.error = "LLM returned empty SQL."
493
519
  return result
494
520
 
521
+ # --- 5.5. Schema Ground-Truth Column & Key Pre-Validation & Auto-Correction ---
522
+ if sub_schema and sub_schema.tables:
523
+ for _ in range(self.MAX_RETRIES):
524
+ is_schema_valid, schema_errors = SchemaGroundTruthValidator.validate_sql_against_schema(
525
+ result.sql, sub_schema
526
+ )
527
+ if is_schema_valid:
528
+ break
529
+
530
+ # Hallucinated columns / keys detected -> Auto-correct using physical schema definitions
531
+ result.retries += 1
532
+ error_feedback = (
533
+ "Schema Ground-Truth Column & Key Validation Discrepancy:\n"
534
+ + "\n".join(f"- {err}" for err in schema_errors)
535
+ + "\nPlease rewrite the query using ONLY the exact physical primary keys, foreign keys, and column names defined in the schema."
536
+ )
537
+ try:
538
+ corrected_sql = self.generator.generate_retry_sql(
539
+ schema=sub_schema,
540
+ user_prompt=augmented_prompt,
541
+ failed_sql=result.sql,
542
+ error_message=error_feedback,
543
+ model=self.model,
544
+ )
545
+ if corrected_sql and corrected_sql.strip():
546
+ result.sql = corrected_sql
547
+ else:
548
+ break
549
+ except Exception:
550
+ break
551
+
552
+ # Attach ground truth verification metadata
553
+ result.ground_truth = SchemaGroundTruthValidator.validate_response(
554
+ f"```sql\n{result.sql}\n```", sub_schema
555
+ )
556
+
495
557
  # --- 6. AST & Security Mode Validation ---
496
558
  result.query_type = SQLValidator.classify_query(result.sql)
497
559
  is_allowed, violations = SQLValidator.validate_for_mode(result.sql, mode=self.mode)
@@ -547,8 +609,28 @@ class QueryPipeline:
547
609
  if self.confirm_callback(result.sql, violations):
548
610
  should_execute = True
549
611
  else:
612
+ turn_tables = list(set((exact or []) + (fuzzy or []) + [t.name.lower() for t in (sub_schema.tables if sub_schema else [])]))
613
+ self.conversation.add_turn(
614
+ user_prompt=user_prompt,
615
+ sql=result.sql,
616
+ tables=turn_tables,
617
+ was_executed=False,
618
+ is_chat=False,
619
+ query_type=result.query_type,
620
+ )
621
+ result.execution_time_ms = (time.time() - t_start) * 1000
550
622
  return result
551
623
  else:
624
+ turn_tables = list(set((exact or []) + (fuzzy or []) + [t.name.lower() for t in (sub_schema.tables if sub_schema else [])]))
625
+ self.conversation.add_turn(
626
+ user_prompt=user_prompt,
627
+ sql=result.sql,
628
+ tables=turn_tables,
629
+ was_executed=False,
630
+ is_chat=False,
631
+ query_type=result.query_type,
632
+ )
633
+ result.execution_time_ms = (time.time() - t_start) * 1000
552
634
  return result
553
635
  elif self.mode == OperationMode.ADMIN:
554
636
  should_execute = True
@@ -620,19 +702,16 @@ class QueryPipeline:
620
702
  error=result.error,
621
703
  )
622
704
 
623
- if result.success:
624
- sql_tables = set()
625
- if result.sql:
626
- import re as _re
627
- for m in _re.findall(r'\b(?:FROM|JOIN)\s+([a-zA-Z0-9_]+)', result.sql, _re.IGNORECASE):
628
- sql_tables.add(m.lower())
629
- turn_tables = list(set((exact or []) + (fuzzy or []) + list(sql_tables)))
630
- self.conversation.add_turn(
631
- user_prompt=user_prompt,
632
- sql=result.sql,
633
- tables=turn_tables,
634
- is_chat=False,
635
- )
705
+ turn_tables = list(set((exact or []) + (fuzzy or []) + [t.name.lower() for t in (sub_schema.tables if sub_schema else [])]))
706
+ self.conversation.add_turn(
707
+ user_prompt=user_prompt,
708
+ sql=result.sql,
709
+ assistant_response=result.error or result.friendly_error or "",
710
+ tables=turn_tables,
711
+ was_executed=result.was_executed,
712
+ is_chat=False,
713
+ query_type=result.query_type,
714
+ )
636
715
 
637
716
  return result
638
717