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.
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/PKG-INFO +1 -1
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/__init__.py +1 -1
- dbagent_cli-0.11.3/dbagent/agent/conversation.py +140 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/generator.py +10 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/pipeline.py +96 -17
- dbagent_cli-0.11.3/dbagent/schema/ground_truth.py +337 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/SOURCES.txt +2 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/pyproject.toml +1 -1
- dbagent_cli-0.11.3/tests/test_ground_truth_sql_validation.py +150 -0
- dbagent_cli-0.11.3/tests/test_multi_turn_conversation.py +102 -0
- dbagent_cli-0.11.1/dbagent/agent/conversation.py +0 -93
- dbagent_cli-0.11.1/dbagent/schema/ground_truth.py +0 -126
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/README.md +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/benchmark.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/cli.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/config.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/errors.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/prompt_queue.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/plugins.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sample_db.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/knowledge.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/sql/validator.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/chat_screen.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/console.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/live_streamer.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/setup.cfg +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_benchmarks.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_cache.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_cli.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_demo_and_sample_db.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_direct_sql.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_formats_and_exit_codes.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_generator.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_graph.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_knowledge.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_plugins.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_prompt_queue.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_prompt_queue_and_sql.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_providers.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_safety.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_schema.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_security.py +0 -0
- {dbagent_cli-0.11.1 → dbagent_cli-0.11.3}/tests/test_semantic.py +0 -0
|
@@ -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
|
-
|
|
138
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|