dbagent-cli 0.11.0__tar.gz → 0.11.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.11.0 → dbagent_cli-0.11.2}/PKG-INFO +1 -1
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/__init__.py +1 -1
- dbagent_cli-0.11.2/dbagent/agent/conversation.py +140 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/agent/generator.py +14 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/agent/pipeline.py +68 -22
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/prompt_queue.py +17 -5
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/sql/validator.py +12 -6
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/ui/console.py +2 -2
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent_cli.egg-info/SOURCES.txt +2 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/pyproject.toml +1 -1
- dbagent_cli-0.11.2/tests/test_multi_turn_conversation.py +102 -0
- dbagent_cli-0.11.2/tests/test_prompt_queue_and_sql.py +48 -0
- dbagent_cli-0.11.0/dbagent/agent/conversation.py +0 -93
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/README.md +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/benchmark.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/cli.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/config.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/errors.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/plugins.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/sample_db.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/schema/ground_truth.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/semantic/knowledge.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/ui/chat_screen.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/ui/live_streamer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/setup.cfg +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_benchmarks.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_cache.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_cli.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_demo_and_sample_db.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_direct_sql.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_formats_and_exit_codes.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_generator.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_graph.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_knowledge.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_plugins.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_prompt_queue.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_providers.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_safety.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_schema.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/tests/test_security.py +0 -0
- {dbagent_cli-0.11.0 → dbagent_cli-0.11.2}/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()
|
|
@@ -59,6 +59,20 @@ Generate ONLY the raw executable SQL query for the given database. No markdown f
|
|
|
59
59
|
- Check the column data type and sample values shown in the schema.
|
|
60
60
|
- If the column is VARCHAR(1) or CHAR(1) or text, check if values use 'Y'/'N', 'YES'/'NO', '1'/'0', 'ACTIVE'/'INACTIVE', 'TRUE'/'FALSE'.
|
|
61
61
|
- If sample values show 'Y'/'N' (or if it's a single-character VARCHAR/CHAR column), use `active = 'Y'` or `active IN ('Y', '1', 'true', 'TRUE')` for active/enabled status.
|
|
62
|
+
10. For Deletions & Cascading Mapping Cleanup (when deleting a record/user without foreign key constraint violations):
|
|
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
|
+
- For MySQL / SQLite: Generate clean, ordered DELETE statements deleting referencing child records before the parent record.
|
|
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.
|
|
62
76
|
"""
|
|
63
77
|
|
|
64
78
|
|
|
@@ -160,6 +160,12 @@ class QueryPipeline:
|
|
|
160
160
|
|
|
161
161
|
# 2. Targeted on-demand table introspection with automatic junction expansion
|
|
162
162
|
target_names = list(set(exact + fuzzy))
|
|
163
|
+
is_delete_or_mapping = any(w in user_prompt.lower() for w in [
|
|
164
|
+
"delete", "remove", "drop", "cascade", "mapping", "map", "link", "clean",
|
|
165
|
+
"without any mappings", "change", "update", "rollback", "undo", "revert"
|
|
166
|
+
])
|
|
167
|
+
max_tables_limit = 25 if is_delete_or_mapping else 12
|
|
168
|
+
|
|
163
169
|
if target_names:
|
|
164
170
|
try:
|
|
165
171
|
all_table_names = self.connector.get_table_names()
|
|
@@ -174,24 +180,29 @@ class QueryPipeline:
|
|
|
174
180
|
t_lower = t.lower()
|
|
175
181
|
if t in target_names:
|
|
176
182
|
continue
|
|
177
|
-
is_map = any(m in t_lower for m in ["_map", "mapping", "_rel", "_junction", "_link", "_access", "_config"])
|
|
183
|
+
is_map = any(m in t_lower for m in ["_map", "mapping", "_rel", "_junction", "_link", "_access", "_config", "_preference", "_profile", "_activity", "_log"])
|
|
178
184
|
matches_stem = any(s in t_lower for s in stems)
|
|
179
|
-
if
|
|
180
|
-
if
|
|
181
|
-
target_names
|
|
182
|
-
|
|
185
|
+
if is_delete_or_mapping:
|
|
186
|
+
if matches_stem or (is_map and matches_stem) or sum(1 for s in stems if s in t_lower) >= 1:
|
|
187
|
+
if t not in target_names:
|
|
188
|
+
target_names.append(t)
|
|
189
|
+
else:
|
|
190
|
+
if (is_map and matches_stem) or sum(1 for s in stems if s in t_lower) >= 2:
|
|
191
|
+
if t not in target_names:
|
|
192
|
+
target_names.append(t)
|
|
193
|
+
if len(target_names) >= max_tables_limit:
|
|
183
194
|
break
|
|
184
195
|
except Exception:
|
|
185
196
|
pass
|
|
186
197
|
|
|
187
198
|
targeted = self.connector.inspect_schema(table_names=target_names, include_samples=True, max_samples=2)
|
|
188
199
|
retriever = ContextRetriever(targeted)
|
|
189
|
-
return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=
|
|
200
|
+
return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=max_tables_limit)
|
|
190
201
|
|
|
191
202
|
# 3. Fallback: inspect targeted
|
|
192
|
-
targeted = self.connector.inspect_targeted(user_prompt, max_tables=
|
|
203
|
+
targeted = self.connector.inspect_targeted(user_prompt, max_tables=max_tables_limit, include_samples=True)
|
|
193
204
|
retriever = ContextRetriever(targeted)
|
|
194
|
-
return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=
|
|
205
|
+
return retriever.retrieve_context(user_prompt, exact_tables=exact, fuzzy_tables=fuzzy, max_tables=max_tables_limit)
|
|
195
206
|
|
|
196
207
|
def run(
|
|
197
208
|
self,
|
|
@@ -434,7 +445,17 @@ class QueryPipeline:
|
|
|
434
445
|
except AttributeError:
|
|
435
446
|
exact, fuzzy = [], []
|
|
436
447
|
|
|
437
|
-
|
|
448
|
+
# If user prompt is a follow-up or has no table mentions, inherit active tables from previous conversation turns
|
|
449
|
+
is_followup = self.conversation.is_followup_prompt(user_prompt)
|
|
450
|
+
if (not exact and not fuzzy) or (not exact and is_followup):
|
|
451
|
+
active_from_ctx = self.conversation.get_active_tables()
|
|
452
|
+
if active_from_ctx:
|
|
453
|
+
exact = active_from_ctx[:10]
|
|
454
|
+
result.exact_tables = exact
|
|
455
|
+
# Also prevent false ambiguity prompt if we have strong session context
|
|
456
|
+
result.ambiguous = False
|
|
457
|
+
|
|
458
|
+
if not exact and len(fuzzy) > 1 and not is_followup:
|
|
438
459
|
result.ambiguous = True
|
|
439
460
|
result.ambiguous_choices = fuzzy
|
|
440
461
|
if self.choice_callback:
|
|
@@ -461,6 +482,14 @@ class QueryPipeline:
|
|
|
461
482
|
conv_context = self.conversation.format_context_for_prompt()
|
|
462
483
|
|
|
463
484
|
augmented_prompt = user_prompt
|
|
485
|
+
if is_followup and self.conversation.get_last_sql():
|
|
486
|
+
last_sql = self.conversation.get_last_sql()
|
|
487
|
+
followup_hint = (
|
|
488
|
+
f"[Follow-Up Note: The user's query refers directly to the previous query in the session.]\n"
|
|
489
|
+
f"[Target Previous SQL:\n{last_sql}\n]"
|
|
490
|
+
)
|
|
491
|
+
augmented_prompt = f"{followup_hint}\n\n{augmented_prompt}"
|
|
492
|
+
|
|
464
493
|
if dict_context:
|
|
465
494
|
augmented_prompt = f"{dict_context}\n\n{augmented_prompt}"
|
|
466
495
|
if kb_context:
|
|
@@ -539,8 +568,28 @@ class QueryPipeline:
|
|
|
539
568
|
if self.confirm_callback(result.sql, violations):
|
|
540
569
|
should_execute = True
|
|
541
570
|
else:
|
|
571
|
+
turn_tables = list(set((exact or []) + (fuzzy or []) + [t.name.lower() for t in (sub_schema.tables if sub_schema else [])]))
|
|
572
|
+
self.conversation.add_turn(
|
|
573
|
+
user_prompt=user_prompt,
|
|
574
|
+
sql=result.sql,
|
|
575
|
+
tables=turn_tables,
|
|
576
|
+
was_executed=False,
|
|
577
|
+
is_chat=False,
|
|
578
|
+
query_type=result.query_type,
|
|
579
|
+
)
|
|
580
|
+
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
542
581
|
return result
|
|
543
582
|
else:
|
|
583
|
+
turn_tables = list(set((exact or []) + (fuzzy or []) + [t.name.lower() for t in (sub_schema.tables if sub_schema else [])]))
|
|
584
|
+
self.conversation.add_turn(
|
|
585
|
+
user_prompt=user_prompt,
|
|
586
|
+
sql=result.sql,
|
|
587
|
+
tables=turn_tables,
|
|
588
|
+
was_executed=False,
|
|
589
|
+
is_chat=False,
|
|
590
|
+
query_type=result.query_type,
|
|
591
|
+
)
|
|
592
|
+
result.execution_time_ms = (time.time() - t_start) * 1000
|
|
544
593
|
return result
|
|
545
594
|
elif self.mode == OperationMode.ADMIN:
|
|
546
595
|
should_execute = True
|
|
@@ -612,19 +661,16 @@ class QueryPipeline:
|
|
|
612
661
|
error=result.error,
|
|
613
662
|
)
|
|
614
663
|
|
|
615
|
-
if
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
tables=turn_tables,
|
|
626
|
-
is_chat=False,
|
|
627
|
-
)
|
|
664
|
+
turn_tables = list(set((exact or []) + (fuzzy or []) + [t.name.lower() for t in (sub_schema.tables if sub_schema else [])]))
|
|
665
|
+
self.conversation.add_turn(
|
|
666
|
+
user_prompt=user_prompt,
|
|
667
|
+
sql=result.sql,
|
|
668
|
+
assistant_response=result.error or result.friendly_error or "",
|
|
669
|
+
tables=turn_tables,
|
|
670
|
+
was_executed=result.was_executed,
|
|
671
|
+
is_chat=False,
|
|
672
|
+
query_type=result.query_type,
|
|
673
|
+
)
|
|
628
674
|
|
|
629
675
|
return result
|
|
630
676
|
|
|
@@ -83,15 +83,27 @@ class PromptQueueManager:
|
|
|
83
83
|
if not raw_clean:
|
|
84
84
|
return raw_clean
|
|
85
85
|
|
|
86
|
-
# Do not split direct SQL statements like "SELECT 1; SELECT 2;"
|
|
87
|
-
if raw_clean.lower().startswith(("select ", "insert ", "update ", "delete ", "create ", "alter ", "drop ", "with ")):
|
|
88
|
-
return raw_clean
|
|
89
|
-
|
|
90
86
|
# Do not split special REPL commands
|
|
91
87
|
if raw_clean.startswith(":"):
|
|
92
88
|
return raw_clean
|
|
93
89
|
|
|
94
|
-
#
|
|
90
|
+
# Do not split multi-line text with newlines containing SQL statements/keywords
|
|
91
|
+
if "\n" in raw_clean and any(kw in raw_clean.upper() for kw in [
|
|
92
|
+
"SELECT", "INSERT", "UPDATE", "DELETE", "FROM", "WHERE", "JOIN", "COMMIT", "BEGIN", "WITH", "CREATE", "ALTER", "DROP"
|
|
93
|
+
]):
|
|
94
|
+
return raw_clean
|
|
95
|
+
|
|
96
|
+
# Do not split direct SQL statements or code blocks containing SQL DML/DDL
|
|
97
|
+
if re.search(r'\b(?:DELETE\s+FROM|INSERT\s+INTO|UPDATE\s+[a-zA-Z0-9_\"`.]+\s+SET|SELECT\s+.*?FROM|CREATE\s+TABLE|ALTER\s+TABLE|DROP\s+TABLE|COMMIT|ROLLBACK|BEGIN)\b', raw_clean, re.IGNORECASE | re.DOTALL):
|
|
98
|
+
return raw_clean
|
|
99
|
+
|
|
100
|
+
# Do not split prompts that ask to review, fix, or correct a code snippet (e.g. "... ; correct this")
|
|
101
|
+
if any(phrase in raw_clean.lower() for phrase in [
|
|
102
|
+
"correct this", "fix this", "check this", "review this", "optimize this", "explain this", "what does this do", "run this", "execute this"
|
|
103
|
+
]):
|
|
104
|
+
return raw_clean
|
|
105
|
+
|
|
106
|
+
# Split by ';' or '&&' for distinct natural language prompts (e.g. "count users ; show top 5 sites")
|
|
95
107
|
delimiter_pattern = r'\s*(?:;|&&)\s*'
|
|
96
108
|
parts = [p.strip() for p in re.split(delimiter_pattern, raw_clean) if p.strip()]
|
|
97
109
|
|
|
@@ -150,17 +150,23 @@ class SQLValidator:
|
|
|
150
150
|
|
|
151
151
|
stmt = statements[0]
|
|
152
152
|
|
|
153
|
-
for
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
# Check for data-modifying CTEs (WITH ... DELETE|INSERT|UPDATE ...)
|
|
154
|
+
if re.match(r"^\s*WITH\b", stmt, re.IGNORECASE):
|
|
155
|
+
if re.search(r"\b(?:DELETE\s+FROM|INSERT\s+INTO|UPDATE\s+[a-zA-Z0-9_\"`.]+\s+SET)\b", stmt, re.IGNORECASE):
|
|
156
|
+
return "write"
|
|
157
|
+
return "read"
|
|
158
|
+
|
|
159
|
+
for pat in cls.WRITE_PATTERNS:
|
|
160
|
+
if re.search(pat, stmt, re.IGNORECASE):
|
|
161
|
+
return "write"
|
|
156
162
|
|
|
157
163
|
for pat in cls.DDL_PATTERNS:
|
|
158
164
|
if re.search(pat, stmt, re.IGNORECASE):
|
|
159
165
|
return "ddl"
|
|
160
166
|
|
|
161
|
-
for pat in cls.
|
|
162
|
-
if re.search(pat, stmt, re.IGNORECASE):
|
|
163
|
-
return "
|
|
167
|
+
for pat in cls.READ_PATTERNS:
|
|
168
|
+
if re.search(pat, stmt, re.IGNORECASE | re.DOTALL):
|
|
169
|
+
return "read"
|
|
164
170
|
|
|
165
171
|
return "unknown"
|
|
166
172
|
|
|
@@ -53,8 +53,8 @@ def print_info(message: str) -> None:
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def print_code(code: str, language: str = "sql", title: Optional[str] = None) -> None:
|
|
56
|
-
"""Print syntax-highlighted code block."""
|
|
57
|
-
syntax = Syntax(code, language, theme="monokai", line_numbers=
|
|
56
|
+
"""Print syntax-highlighted code block without line numbers for easy copy-pasting."""
|
|
57
|
+
syntax = Syntax(code, language, theme="monokai", line_numbers=False)
|
|
58
58
|
panel = Panel(syntax, title=f"[bold cyan]{title or language.upper()}[/bold cyan]", border_style="dim")
|
|
59
59
|
console.print(panel)
|
|
60
60
|
|
|
@@ -82,8 +82,10 @@ tests/test_formats_and_exit_codes.py
|
|
|
82
82
|
tests/test_generator.py
|
|
83
83
|
tests/test_graph.py
|
|
84
84
|
tests/test_knowledge.py
|
|
85
|
+
tests/test_multi_turn_conversation.py
|
|
85
86
|
tests/test_plugins.py
|
|
86
87
|
tests/test_prompt_queue.py
|
|
88
|
+
tests/test_prompt_queue_and_sql.py
|
|
87
89
|
tests/test_providers.py
|
|
88
90
|
tests/test_recovery.py
|
|
89
91
|
tests/test_safety.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbagent-cli"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.2"
|
|
8
8
|
description = "The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Multi-Turn Conversational Memory, Query Refinements, and Rollback Generation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Callable
|
|
6
|
+
from dbagent.agent.conversation import ConversationContext
|
|
7
|
+
from dbagent.agent.pipeline import QueryPipeline
|
|
8
|
+
from dbagent.connectors.relational import RelationalConnector
|
|
9
|
+
from dbagent.llm.base import BaseLLMProvider
|
|
10
|
+
from dbagent.sample_db import init_sample_database
|
|
11
|
+
from dbagent.security.modes import OperationMode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MockWriteProvider(BaseLLMProvider):
|
|
15
|
+
"""Custom provider returning specific SQL for multi-turn test."""
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.call_count = 0
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return "mock-write"
|
|
22
|
+
|
|
23
|
+
def is_available(self) -> bool:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
def list_models(self) -> List[str]:
|
|
27
|
+
return ["mock-v1"]
|
|
28
|
+
|
|
29
|
+
def generate(
|
|
30
|
+
self,
|
|
31
|
+
prompt: str,
|
|
32
|
+
system_prompt: Optional[str] = None,
|
|
33
|
+
model: Optional[str] = None,
|
|
34
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
35
|
+
) -> str:
|
|
36
|
+
self.call_count += 1
|
|
37
|
+
if self.call_count == 1:
|
|
38
|
+
return "UPDATE customers SET first_name = 'Alice' WHERE customer_id = 1;"
|
|
39
|
+
else:
|
|
40
|
+
return "UPDATE customers SET first_name = 'Original' WHERE customer_id = 1;"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_conversation_context_tracking_and_followup():
|
|
44
|
+
"""Verify that ConversationContext detects follow-ups and records unexecuted proposals."""
|
|
45
|
+
ctx = ConversationContext()
|
|
46
|
+
assert ctx.is_followup_prompt("give the rollback query for the above one") is False # Empty history
|
|
47
|
+
|
|
48
|
+
# Add a proposed write query that was not executed
|
|
49
|
+
ctx.add_turn(
|
|
50
|
+
user_prompt="Update customer 1 to inactive",
|
|
51
|
+
sql="UPDATE customers SET status = 'INACTIVE' WHERE customer_id = 1;",
|
|
52
|
+
tables=["customers"],
|
|
53
|
+
was_executed=False,
|
|
54
|
+
query_type="write",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert len(ctx.turns) == 1
|
|
58
|
+
assert "customers" in ctx.get_active_tables()
|
|
59
|
+
assert ctx.get_last_sql() == "UPDATE customers SET status = 'INACTIVE' WHERE customer_id = 1;"
|
|
60
|
+
assert ctx.is_followup_prompt("okay also give the rollback query for the above one") is True
|
|
61
|
+
assert ctx.is_followup_prompt("modify that to customer_id = 2") is True
|
|
62
|
+
|
|
63
|
+
prompt_context = ctx.format_context_for_prompt()
|
|
64
|
+
assert "UPDATE customers SET status = 'INACTIVE'" in prompt_context
|
|
65
|
+
assert "Generated / Proposed" in prompt_context
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_pipeline_multi_turn_followup_table_inheritance():
|
|
69
|
+
"""Verify that QueryPipeline automatically inherits active tables and targets for follow-up prompts."""
|
|
70
|
+
_, db_url = init_sample_database(force_recreate=False)
|
|
71
|
+
connector = RelationalConnector(db_url)
|
|
72
|
+
llm = MockWriteProvider()
|
|
73
|
+
ctx = ConversationContext()
|
|
74
|
+
|
|
75
|
+
# Reject confirmation to simulate user answering [n]
|
|
76
|
+
def _reject_confirmation(sql, violations):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
pipeline = QueryPipeline(
|
|
80
|
+
connector=connector,
|
|
81
|
+
llm=llm,
|
|
82
|
+
mode=OperationMode.CONFIRM,
|
|
83
|
+
confirm_callback=_reject_confirmation,
|
|
84
|
+
conversation_context=ctx,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Turn 1: Propose update on customers (user says [n] so it is not executed)
|
|
88
|
+
res1 = pipeline.run("change customer 1 name to Alice")
|
|
89
|
+
assert res1.needs_confirmation is True
|
|
90
|
+
assert res1.was_executed is False
|
|
91
|
+
assert len(ctx.turns) == 1
|
|
92
|
+
assert "customers" in ctx.get_active_tables()
|
|
93
|
+
assert ctx.get_last_sql() == "UPDATE customers SET first_name = 'Alice' WHERE customer_id = 1;"
|
|
94
|
+
|
|
95
|
+
# Turn 2: Follow-up prompt with no table names in the text
|
|
96
|
+
res2 = pipeline.run("give the rollback query for the above one")
|
|
97
|
+
# Exact tables and schema context should be inherited from previous turn
|
|
98
|
+
assert "customers" in res2.exact_tables
|
|
99
|
+
assert res2.schema is not None
|
|
100
|
+
assert "customers" in [t.name.lower() for t in res2.schema.tables]
|
|
101
|
+
|
|
102
|
+
connector.close()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for PromptQueueManager SQL script handling and SQLValidator CTE classification.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dbagent.execution.prompt_queue import PromptQueueManager
|
|
6
|
+
from dbagent.sql.validator import SQLValidator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_queue_does_not_split_multiline_sql_with_semicolons():
|
|
10
|
+
"""Verify that multi-line SQL scripts with internal semicolons are not split into queued fragments."""
|
|
11
|
+
queue_mgr = PromptQueueManager()
|
|
12
|
+
|
|
13
|
+
sql_input = """
|
|
14
|
+
DELETE FROM ivis_customer_user_map WHERE fk_user_id = 5201;
|
|
15
|
+
DELETE FROM ivis_user_access WHERE fk_user_id = 5201;
|
|
16
|
+
DELETE FROM user_activity WHERE user_id = 5201;
|
|
17
|
+
DELETE FROM ivis_user WHERE pk_user_id = 5201;
|
|
18
|
+
COMMIT; correct this
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
res = queue_mgr.split_and_enqueue_chained(sql_input)
|
|
22
|
+
assert res == sql_input.strip()
|
|
23
|
+
assert queue_mgr.has_next() is False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_classify_query_with_cte_delete():
|
|
27
|
+
"""Verify that data-modifying WITH CTE statements are classified as 'write'."""
|
|
28
|
+
cte_delete = """
|
|
29
|
+
WITH target_user AS (
|
|
30
|
+
SELECT pk_user_id FROM ivis_user WHERE username = 'target_user'
|
|
31
|
+
),
|
|
32
|
+
del_access AS (
|
|
33
|
+
DELETE FROM ivis_user_access WHERE fk_user_id IN (SELECT pk_user_id FROM target_user)
|
|
34
|
+
)
|
|
35
|
+
DELETE FROM ivis_user WHERE pk_user_id IN (SELECT pk_user_id FROM target_user);
|
|
36
|
+
"""
|
|
37
|
+
assert SQLValidator.classify_query(cte_delete) == "write"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_classify_query_with_cte_read():
|
|
41
|
+
"""Verify that regular SELECT WITH CTE statements remain classified as 'read'."""
|
|
42
|
+
cte_read = """
|
|
43
|
+
WITH active_users AS (
|
|
44
|
+
SELECT pk_user_id, username FROM ivis_user WHERE active = 'Y'
|
|
45
|
+
)
|
|
46
|
+
SELECT * FROM active_users LIMIT 10;
|
|
47
|
+
"""
|
|
48
|
+
assert SQLValidator.classify_query(cte_read) == "read"
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Multi-Turn Conversational Context Tracker.
|
|
3
|
-
Maintains active tables, filter predicates, entities, and sort orders across chat turns
|
|
4
|
-
so users can refine queries naturally (e.g. 'Which sites?', 'Only Hyderabad', 'Top 10?').
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from typing import Dict, Any, List, Optional, Set
|
|
8
|
-
from dataclasses import dataclass, field
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class ConversationTurn:
|
|
13
|
-
"""A single turn in the conversation."""
|
|
14
|
-
user_prompt: str
|
|
15
|
-
sql: str = ""
|
|
16
|
-
assistant_response: str = ""
|
|
17
|
-
tables: List[str] = field(default_factory=list)
|
|
18
|
-
filters: List[str] = field(default_factory=list)
|
|
19
|
-
entities: Dict[str, str] = field(default_factory=dict)
|
|
20
|
-
limit: Optional[int] = None
|
|
21
|
-
is_chat: bool = False
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ConversationContext:
|
|
25
|
-
"""
|
|
26
|
-
Tracks state and multi-turn conversational history across interactive chat turns.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self, max_history: int = 15):
|
|
30
|
-
self.max_history = max_history
|
|
31
|
-
self.turns: List[ConversationTurn] = []
|
|
32
|
-
self.active_tables: Set[str] = set()
|
|
33
|
-
self.active_filters: List[str] = []
|
|
34
|
-
|
|
35
|
-
def add_turn(
|
|
36
|
-
self,
|
|
37
|
-
user_prompt: str,
|
|
38
|
-
sql: str = "",
|
|
39
|
-
assistant_response: str = "",
|
|
40
|
-
tables: Optional[List[str]] = None,
|
|
41
|
-
filters: Optional[List[str]] = None,
|
|
42
|
-
is_chat: bool = False,
|
|
43
|
-
) -> None:
|
|
44
|
-
"""Records a completed turn and updates active context."""
|
|
45
|
-
turn = ConversationTurn(
|
|
46
|
-
user_prompt=user_prompt,
|
|
47
|
-
sql=sql,
|
|
48
|
-
assistant_response=assistant_response,
|
|
49
|
-
tables=tables or [],
|
|
50
|
-
filters=filters or [],
|
|
51
|
-
is_chat=is_chat,
|
|
52
|
-
)
|
|
53
|
-
self.turns.append(turn)
|
|
54
|
-
if len(self.turns) > self.max_history:
|
|
55
|
-
self.turns.pop(0)
|
|
56
|
-
|
|
57
|
-
if tables:
|
|
58
|
-
self.active_tables.update(t.lower() for t in tables)
|
|
59
|
-
|
|
60
|
-
def get_active_tables(self) -> List[str]:
|
|
61
|
-
"""Returns currently active database table names discussed in this session."""
|
|
62
|
-
return list(self.active_tables)
|
|
63
|
-
|
|
64
|
-
def get_last_turn(self) -> Optional[ConversationTurn]:
|
|
65
|
-
if self.turns:
|
|
66
|
-
return self.turns[-1]
|
|
67
|
-
return None
|
|
68
|
-
|
|
69
|
-
def format_context_for_prompt(self, max_turns: int = 5) -> str:
|
|
70
|
-
"""Formats multi-turn conversation memory for inclusion in LLM prompt."""
|
|
71
|
-
if not self.turns:
|
|
72
|
-
return ""
|
|
73
|
-
|
|
74
|
-
recent = self.turns[-max_turns:]
|
|
75
|
-
lines = ["=== Previous Chat Session Memory (Use for follow-ups & context) ==="]
|
|
76
|
-
for i, t in enumerate(recent, 1):
|
|
77
|
-
lines.append(f"Turn {i}:")
|
|
78
|
-
lines.append(f" User: \"{t.user_prompt}\"")
|
|
79
|
-
if t.sql:
|
|
80
|
-
lines.append(f" Assistant (SQL): {t.sql.strip()}")
|
|
81
|
-
elif t.assistant_response:
|
|
82
|
-
clean_resp = t.assistant_response.strip().replace("\n", " ")
|
|
83
|
-
preview = (clean_resp[:120] + "...") if len(clean_resp) > 120 else clean_resp
|
|
84
|
-
lines.append(f" Assistant: \"{preview}\"")
|
|
85
|
-
lines.append("=== End Chat Session Memory ===")
|
|
86
|
-
|
|
87
|
-
return "\n".join(lines)
|
|
88
|
-
|
|
89
|
-
def clear(self) -> None:
|
|
90
|
-
"""Resets the conversation history."""
|
|
91
|
-
self.turns.clear()
|
|
92
|
-
self.active_tables.clear()
|
|
93
|
-
self.active_filters.clear()
|
|
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
|
|
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
|