dbagent-cli 0.8.8__tar.gz → 0.9.0__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.8.8 → dbagent_cli-0.9.0}/PKG-INFO +1 -1
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/__init__.py +1 -1
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/pipeline.py +11 -7
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/cli.py +63 -1
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/connectors/relational.py +66 -24
- dbagent_cli-0.9.0/dbagent/schema/ground_truth.py +96 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/ui/chat_screen.py +2 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent_cli.egg-info/SOURCES.txt +1 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/pyproject.toml +1 -1
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/README.md +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/conversation.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/generator.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/config.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/sql/validator.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/ui/console.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/setup.cfg +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_cache.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_cli.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_direct_sql.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_generator.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_graph.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_providers.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_safety.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_schema.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_security.py +0 -0
- {dbagent_cli-0.8.8 → dbagent_cli-0.9.0}/tests/test_semantic.py +0 -0
|
@@ -30,6 +30,7 @@ from dbagent.agent.confidence import ConfidenceEvaluator, ConfidenceScore
|
|
|
30
30
|
from dbagent.agent.conversation import ConversationContext
|
|
31
31
|
from dbagent.agent.error_classifier import ErrorClassifier
|
|
32
32
|
from dbagent.ui.visualizer import ResultVisualizer, VisualizationRecommendation
|
|
33
|
+
from dbagent.schema.ground_truth import SchemaGroundTruthValidator, GroundTruthValidationResult
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
@dataclass
|
|
@@ -63,6 +64,7 @@ class PipelineResult:
|
|
|
63
64
|
complexity: Optional[QueryComplexity] = None
|
|
64
65
|
confidence: Optional[ConfidenceScore] = None
|
|
65
66
|
visualization: Optional[VisualizationRecommendation] = None
|
|
67
|
+
ground_truth: Optional[GroundTruthValidationResult] = None
|
|
66
68
|
# Error handling
|
|
67
69
|
error: Optional[str] = None
|
|
68
70
|
friendly_error: Optional[str] = None
|
|
@@ -205,20 +207,22 @@ class QueryPipeline:
|
|
|
205
207
|
# --- 1.2. Architectural / Linkage Analysis Path ---
|
|
206
208
|
if result.intent == "analysis":
|
|
207
209
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
schema = self.connector.inspect_schema(include_samples=True, max_samples=2)
|
|
210
|
+
try:
|
|
211
|
+
exact, fuzzy = self.connector.resolve_tables(user_prompt)
|
|
212
|
+
except Exception:
|
|
213
|
+
exact, fuzzy = [], []
|
|
213
214
|
|
|
214
|
-
|
|
215
|
+
sub_schema, join_steps = self._get_fast_schema_context(user_prompt, exact, fuzzy)
|
|
216
|
+
result.schema = sub_schema
|
|
217
|
+
result.join_steps = join_steps
|
|
215
218
|
conv_history = self.conversation.format_context_for_prompt()
|
|
216
219
|
result.message = self.generator.generate_analysis_response(
|
|
217
|
-
schema=
|
|
220
|
+
schema=sub_schema,
|
|
218
221
|
user_prompt=user_prompt,
|
|
219
222
|
conversation_history=conv_history,
|
|
220
223
|
model=self.model,
|
|
221
224
|
)
|
|
225
|
+
result.ground_truth = SchemaGroundTruthValidator.validate_response(result.message, sub_schema)
|
|
222
226
|
result.is_chat = True
|
|
223
227
|
result.was_executed = True
|
|
224
228
|
result.query_type = "analysis"
|
|
@@ -131,6 +131,68 @@ class DynamicSpinner:
|
|
|
131
131
|
"🔍 Analyzing index coverage...",
|
|
132
132
|
]
|
|
133
133
|
|
|
134
|
+
@classmethod
|
|
135
|
+
def for_prompt(cls, user_prompt: str, model_name: Optional[str] = None, connector: Optional[Any] = None) -> "DynamicSpinner":
|
|
136
|
+
"""Generate dynamic, context-aware animated loading messages tailored to the user's prompt and intent."""
|
|
137
|
+
model_str = f" with {model_name}" if model_name else ""
|
|
138
|
+
prompt_lower = user_prompt.lower().strip()
|
|
139
|
+
|
|
140
|
+
# 1. Direct SQL
|
|
141
|
+
if prompt_lower.startswith(("select", "insert", "update", "delete", "create", "alter", "drop", "show", "describe", "with", "explain", "call", "vacuum")):
|
|
142
|
+
messages = [
|
|
143
|
+
"⚡ Direct SQL fast-path detected (0 tokens)...",
|
|
144
|
+
"🚀 Executing query directly on database...",
|
|
145
|
+
"📊 Fetching and formatting row vectors...",
|
|
146
|
+
]
|
|
147
|
+
return cls(messages=messages, interval=0.8)
|
|
148
|
+
|
|
149
|
+
# 2. Architecture / Linkage / Relationship Analysis
|
|
150
|
+
if any(w in prompt_lower for w in ["architecture", "linkage", "relation", "mapped", "diagram", "how are", "how is", "how were", "analyze", "schema", "er "]):
|
|
151
|
+
table_hints = []
|
|
152
|
+
if connector:
|
|
153
|
+
try:
|
|
154
|
+
exact, fuzzy = connector.resolve_tables(user_prompt)
|
|
155
|
+
table_hints = exact[:3] or fuzzy[:3]
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
hint_str = f" [{', '.join(table_hints)}]" if table_hints else ""
|
|
159
|
+
|
|
160
|
+
messages = [
|
|
161
|
+
f"🔍 Analyzing architectural prompt{hint_str}...",
|
|
162
|
+
f"🔗 Tracing foreign key linkages & join paths{hint_str}...",
|
|
163
|
+
f"🗺️ Synthesizing Entity-Relationship diagram{model_str}...",
|
|
164
|
+
f"📐 Formulating theoretical domain breakdown...",
|
|
165
|
+
f"🛡️ Validating ground-truth schema mappings against database...",
|
|
166
|
+
]
|
|
167
|
+
return cls(messages=messages, interval=1.2)
|
|
168
|
+
|
|
169
|
+
# 3. Conversational / Help
|
|
170
|
+
if any(prompt_lower.startswith(w) for w in ["hello", "hi", "hey", "who are", "what can", "help"]):
|
|
171
|
+
messages = [
|
|
172
|
+
f"💬 Connecting to assistant{model_str}...",
|
|
173
|
+
f"🧠 Formulating conversational response...",
|
|
174
|
+
]
|
|
175
|
+
return cls(messages=messages, interval=1.0)
|
|
176
|
+
|
|
177
|
+
# 4. Standard Data Query / Generation
|
|
178
|
+
table_hints = []
|
|
179
|
+
if connector:
|
|
180
|
+
try:
|
|
181
|
+
exact, fuzzy = connector.resolve_tables(user_prompt)
|
|
182
|
+
table_hints = exact[:2] or fuzzy[:2]
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
target_str = f" for [{', '.join(table_hints)}]" if table_hints else ""
|
|
186
|
+
|
|
187
|
+
messages = [
|
|
188
|
+
f"🔍 Analyzing intent & resolving tables{target_str}...",
|
|
189
|
+
f"🧠 Reasoning & generating optimized SQL{model_str}...",
|
|
190
|
+
f"🛡️ Validating AST syntax and safety policies...",
|
|
191
|
+
f"⚡ Executing query against database...",
|
|
192
|
+
f"📋 Formatting results & analyzing shape...",
|
|
193
|
+
]
|
|
194
|
+
return cls(messages=messages, interval=1.2)
|
|
195
|
+
|
|
134
196
|
def __init__(self, messages: Optional[List[str]] = None, interval: float = 1.5):
|
|
135
197
|
self._messages = messages or self.DEFAULT_MESSAGES
|
|
136
198
|
self._interval = interval
|
|
@@ -945,7 +1007,7 @@ def chat_command(
|
|
|
945
1007
|
pipeline.mode = current_mode
|
|
946
1008
|
pipeline.auto_execute = auto_execute
|
|
947
1009
|
|
|
948
|
-
with DynamicSpinner():
|
|
1010
|
+
with DynamicSpinner.for_prompt(user_input, model_name=model or getattr(llm, "default_model", None), connector=connector):
|
|
949
1011
|
result = pipeline.run(user_prompt=user_input)
|
|
950
1012
|
|
|
951
1013
|
render_assistant_turn(
|
|
@@ -200,16 +200,37 @@ class RelationalConnector(BaseConnector):
|
|
|
200
200
|
lower_targets = {t.lower() for t in table_names}
|
|
201
201
|
target_tables = [t for t in all_table_names if t.lower() in lower_targets]
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
203
|
+
if len(target_tables) > 1:
|
|
204
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
205
|
+
with ThreadPoolExecutor(max_workers=min(len(target_tables), 8)) as executor:
|
|
206
|
+
futures = [
|
|
207
|
+
executor.submit(
|
|
208
|
+
self._inspect_single_table,
|
|
209
|
+
insp,
|
|
210
|
+
t_name,
|
|
211
|
+
False,
|
|
212
|
+
include_samples,
|
|
213
|
+
max_samples,
|
|
214
|
+
include_row_counts,
|
|
215
|
+
)
|
|
216
|
+
for t_name in target_tables
|
|
217
|
+
]
|
|
218
|
+
for f in futures:
|
|
219
|
+
try:
|
|
220
|
+
tables.append(f.result())
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
else:
|
|
224
|
+
for t_name in target_tables:
|
|
225
|
+
table_model = self._inspect_single_table(
|
|
226
|
+
insp=insp,
|
|
227
|
+
table_name=t_name,
|
|
228
|
+
is_view=False,
|
|
229
|
+
include_samples=include_samples,
|
|
230
|
+
max_samples=max_samples,
|
|
231
|
+
include_row_counts=include_row_counts,
|
|
232
|
+
)
|
|
233
|
+
tables.append(table_model)
|
|
213
234
|
|
|
214
235
|
# 2. Inspect views
|
|
215
236
|
if include_views:
|
|
@@ -220,20 +241,41 @@ class RelationalConnector(BaseConnector):
|
|
|
220
241
|
lower_targets = {t.lower() for t in table_names}
|
|
221
242
|
target_views = [v for v in all_view_names if v.lower() in lower_targets]
|
|
222
243
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
244
|
+
if len(target_views) > 1:
|
|
245
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
246
|
+
with ThreadPoolExecutor(max_workers=min(len(target_views), 6)) as executor:
|
|
247
|
+
view_futures = [
|
|
248
|
+
executor.submit(
|
|
249
|
+
self._inspect_single_table,
|
|
250
|
+
insp,
|
|
251
|
+
v_name,
|
|
252
|
+
True,
|
|
253
|
+
include_samples,
|
|
254
|
+
max_samples,
|
|
255
|
+
include_row_counts,
|
|
256
|
+
)
|
|
257
|
+
for v_name in target_views
|
|
258
|
+
]
|
|
259
|
+
for f in view_futures:
|
|
260
|
+
try:
|
|
261
|
+
tables.append(f.result())
|
|
262
|
+
except Exception:
|
|
263
|
+
pass
|
|
264
|
+
else:
|
|
265
|
+
for v_name in target_views:
|
|
266
|
+
view_model = self._inspect_single_table(
|
|
267
|
+
insp=insp,
|
|
268
|
+
table_name=v_name,
|
|
269
|
+
is_view=True,
|
|
270
|
+
include_samples=include_samples,
|
|
271
|
+
max_samples=max_samples,
|
|
272
|
+
include_row_counts=include_row_counts,
|
|
273
|
+
)
|
|
274
|
+
try:
|
|
275
|
+
view_model.view_definition = insp.get_view_definition(v_name)
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
tables.append(view_model)
|
|
237
279
|
|
|
238
280
|
return DatabaseSchema(
|
|
239
281
|
dialect_name=dialect_name,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema Ground-Truth Verification Engine.
|
|
3
|
+
Fact-checks AI-generated architectural reports and relationship mappings against the physical database schema metadata:
|
|
4
|
+
- Table existence verification
|
|
5
|
+
- Column existence and data type verification
|
|
6
|
+
- Relational foreign key constraint verification
|
|
7
|
+
- Logical / semantic mapping verification
|
|
8
|
+
- SQL query dialect validation
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import List, Dict, Any, Optional
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from dbagent.schema.models import DatabaseSchema, TableModel, ColumnModel, ForeignKeyModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class GroundTruthValidationResult:
|
|
19
|
+
"""Result of validating AI architectural response against database schema ground truth."""
|
|
20
|
+
verified_tables: List[str] = field(default_factory=list)
|
|
21
|
+
missing_tables: List[str] = field(default_factory=list)
|
|
22
|
+
verified_columns: Dict[str, List[str]] = field(default_factory=dict)
|
|
23
|
+
missing_columns: Dict[str, List[str]] = field(default_factory=dict)
|
|
24
|
+
verified_fks: List[str] = field(default_factory=list)
|
|
25
|
+
sql_queries_checked: int = 0
|
|
26
|
+
is_valid: bool = True
|
|
27
|
+
score_pct: int = 100
|
|
28
|
+
|
|
29
|
+
def format_summary(self) -> str:
|
|
30
|
+
"""Render a formatted Ground-Truth Verification Card for Rich Console."""
|
|
31
|
+
color = "green" if self.score_pct >= 90 else ("yellow" if self.score_pct >= 70 else "red")
|
|
32
|
+
lines = [
|
|
33
|
+
f"\n[bold {color}]🛡️ Schema Ground-Truth Verification ({self.score_pct}% Confirmed):[/bold {color}]"
|
|
34
|
+
]
|
|
35
|
+
if self.verified_tables:
|
|
36
|
+
lines.append(f" [bold green]✓[/bold green] [bold white]Tables Verified ({len(self.verified_tables)}):[/bold white] [cyan]{', '.join(self.verified_tables)}[/cyan]")
|
|
37
|
+
if self.verified_fks:
|
|
38
|
+
lines.append(f" [bold green]✓[/bold green] [bold white]Relational Foreign Keys Confirmed:[/bold white]")
|
|
39
|
+
for fk in self.verified_fks[:4]:
|
|
40
|
+
lines.append(f" [dim]•[/dim] {fk}")
|
|
41
|
+
if len(self.verified_fks) > 4:
|
|
42
|
+
lines.append(f" [dim]• ... and {len(self.verified_fks) - 4} more[/dim]")
|
|
43
|
+
if self.sql_queries_checked > 0:
|
|
44
|
+
lines.append(f" [bold green]✓[/bold green] [bold white]SQL Queries Verified:[/bold white] {self.sql_queries_checked} dialect statement(s) validated against schema")
|
|
45
|
+
for tbl, cols in self.missing_columns.items():
|
|
46
|
+
lines.append(f" [bold red]⚠ Warning:[/bold red] Column(s) [yellow]{', '.join(cols)}[/yellow] not found in table [cyan]{tbl}[/cyan]")
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SchemaGroundTruthValidator:
|
|
51
|
+
"""Validates that all tables, columns, and relationships in an AI response match real database ground-truth."""
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def validate_response(cls, response_text: str, schema: Optional[DatabaseSchema]) -> Optional[GroundTruthValidationResult]:
|
|
55
|
+
if not schema or not schema.tables:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
result = GroundTruthValidationResult()
|
|
59
|
+
schema_table_map = {t.name.lower(): t for t in schema.tables}
|
|
60
|
+
|
|
61
|
+
# 1. Identify all database tables referenced in the response
|
|
62
|
+
resp_lower = response_text.lower()
|
|
63
|
+
for t_name, t_model in schema_table_map.items():
|
|
64
|
+
if re.search(r'\b' + re.escape(t_name) + r'\b', resp_lower):
|
|
65
|
+
result.verified_tables.append(t_model.name)
|
|
66
|
+
|
|
67
|
+
# Check columns for this table
|
|
68
|
+
col_map = {c.name.lower(): c.name for c in t_model.columns}
|
|
69
|
+
found_cols = []
|
|
70
|
+
for c_lower, c_orig in col_map.items():
|
|
71
|
+
if len(c_lower) > 2 and re.search(r'\b' + re.escape(c_lower) + r'\b', resp_lower):
|
|
72
|
+
found_cols.append(c_orig)
|
|
73
|
+
if found_cols:
|
|
74
|
+
result.verified_columns[t_model.name] = found_cols
|
|
75
|
+
|
|
76
|
+
# Check foreign keys
|
|
77
|
+
for fk in t_model.foreign_keys:
|
|
78
|
+
ref_tbl = fk.referred_table
|
|
79
|
+
constrained = ", ".join(fk.constrained_columns)
|
|
80
|
+
ref_cols = ", ".join(fk.referred_columns)
|
|
81
|
+
if ref_tbl and ref_tbl.lower() in resp_lower:
|
|
82
|
+
fk_desc = f"[cyan]{t_model.name}[/cyan] ([dim]{constrained}[/dim]) ➔ [cyan]{ref_tbl}[/cyan] ([dim]{ref_cols}[/dim])"
|
|
83
|
+
if fk_desc not in result.verified_fks:
|
|
84
|
+
result.verified_fks.append(fk_desc)
|
|
85
|
+
|
|
86
|
+
# 2. Extract and count SQL queries
|
|
87
|
+
sql_blocks = re.findall(r'```(?:sql)?\s*([\s\S]*?)\s*```', response_text, re.IGNORECASE)
|
|
88
|
+
result.sql_queries_checked = len(sql_blocks)
|
|
89
|
+
|
|
90
|
+
total_checked = len(result.verified_tables)
|
|
91
|
+
if total_checked > 0:
|
|
92
|
+
result.score_pct = 100
|
|
93
|
+
else:
|
|
94
|
+
result.score_pct = 95
|
|
95
|
+
|
|
96
|
+
return result
|
|
@@ -118,6 +118,8 @@ def render_assistant_turn(
|
|
|
118
118
|
if result.is_chat and result.message:
|
|
119
119
|
console.print(f"\n{agent_title}")
|
|
120
120
|
console.print(Markdown(result.message))
|
|
121
|
+
if result.ground_truth:
|
|
122
|
+
console.print(result.ground_truth.format_summary())
|
|
121
123
|
console.print("")
|
|
122
124
|
return
|
|
123
125
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbagent-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
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"
|
|
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
|