dbagent-cli 0.4.1__tar.gz → 0.5.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.4.1 → dbagent_cli-0.5.0}/PKG-INFO +1 -1
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/__init__.py +1 -1
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/generator.py +14 -5
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/cli.py +120 -18
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/gemini_provider.py +10 -2
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/console.py +17 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/grid_window.py +12 -3
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/pyproject.toml +1 -1
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/README.md +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/conversation.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/pipeline.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/config.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/sql/validator.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/SOURCES.txt +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/setup.cfg +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_cache.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_cli.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_generator.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_graph.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_safety.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_schema.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_security.py +0 -0
- {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_semantic.py +0 -0
|
@@ -132,16 +132,25 @@ class ScriptGenerator:
|
|
|
132
132
|
if kw in prompt_lower:
|
|
133
133
|
return "ddl"
|
|
134
134
|
|
|
135
|
-
# Write indicators
|
|
136
|
-
|
|
135
|
+
# Write indicators (word-boundary matching to avoid false positives like "show dataset")
|
|
136
|
+
write_phrases = [
|
|
137
137
|
"insert", "add record", "add row", "add new",
|
|
138
|
-
"update", "
|
|
139
|
-
"delete", "remove", "drop record",
|
|
138
|
+
"update", "delete", "drop record",
|
|
140
139
|
"increment", "decrement",
|
|
141
140
|
]
|
|
142
|
-
for kw in
|
|
141
|
+
for kw in write_phrases:
|
|
143
142
|
if kw in prompt_lower:
|
|
144
143
|
return "write"
|
|
144
|
+
# Single-word write verbs need word boundary to avoid false positives
|
|
145
|
+
# e.g. "set" should not match "dataset", "change" should not match "show changes"
|
|
146
|
+
import re as _re
|
|
147
|
+
write_verbs = ["set", "change", "modify", "remove"]
|
|
148
|
+
for verb in write_verbs:
|
|
149
|
+
if _re.search(r'\b' + verb + r'\b', prompt_lower):
|
|
150
|
+
# Exclude when preceded by 'show', 'list', 'display', 'view', 'get'
|
|
151
|
+
read_context = _re.search(r'\b(?:show|list|display|view|get|count)\b.*\b' + verb, prompt_lower)
|
|
152
|
+
if not read_context:
|
|
153
|
+
return "write"
|
|
145
154
|
|
|
146
155
|
# Default: read
|
|
147
156
|
return "read"
|
|
@@ -6,6 +6,8 @@ Ask questions in natural language, get instant results, explore relationships, a
|
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
8
|
import json
|
|
9
|
+
import time
|
|
10
|
+
import threading
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Optional, List, Dict, Any
|
|
11
13
|
import typer
|
|
@@ -77,6 +79,87 @@ template_mgr = TemplateManager()
|
|
|
77
79
|
audit_logger = AuditLogger()
|
|
78
80
|
|
|
79
81
|
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Dynamic Animated Spinner with Rotating Messages
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
class DynamicSpinner:
|
|
87
|
+
"""
|
|
88
|
+
Context manager that shows an animated spinner with rotating contextual messages.
|
|
89
|
+
Messages cycle every ~1.5s so the user always sees progress.
|
|
90
|
+
Displays elapsed time suffix like (2.3s).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
DEFAULT_MESSAGES = [
|
|
94
|
+
"🔍 Analyzing your question...",
|
|
95
|
+
"📊 Resolving tables & schema...",
|
|
96
|
+
"🧠 Generating SQL with AI...",
|
|
97
|
+
"⚡ Executing query...",
|
|
98
|
+
"📋 Formatting results...",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
CONNECT_MESSAGES = [
|
|
102
|
+
"🔌 Connecting to database...",
|
|
103
|
+
"🔐 Authenticating credentials...",
|
|
104
|
+
"📡 Establishing connection pool...",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
SCHEMA_MESSAGES = [
|
|
108
|
+
"📂 Inspecting database schema...",
|
|
109
|
+
"🔗 Mapping foreign key relationships...",
|
|
110
|
+
"📊 Cataloging tables & columns...",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
DOCTOR_MESSAGES = [
|
|
114
|
+
"🏥 Running health diagnostics...",
|
|
115
|
+
"📡 Testing connectivity & latency...",
|
|
116
|
+
"🔍 Analyzing index coverage...",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
def __init__(self, messages: Optional[List[str]] = None, interval: float = 1.5):
|
|
120
|
+
self._messages = messages or self.DEFAULT_MESSAGES
|
|
121
|
+
self._interval = interval
|
|
122
|
+
self._stop_event = threading.Event()
|
|
123
|
+
self._thread: Optional[threading.Thread] = None
|
|
124
|
+
self._start_time = 0.0
|
|
125
|
+
self._status = None
|
|
126
|
+
|
|
127
|
+
def __enter__(self):
|
|
128
|
+
self._start_time = time.time()
|
|
129
|
+
self._stop_event.clear()
|
|
130
|
+
self._status = console.status(f"[bold cyan]{self._messages[0]}[/bold cyan]")
|
|
131
|
+
self._status.__enter__()
|
|
132
|
+
self._thread = threading.Thread(target=self._rotate, daemon=True)
|
|
133
|
+
self._thread.start()
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __exit__(self, *args):
|
|
137
|
+
self._stop_event.set()
|
|
138
|
+
if self._thread:
|
|
139
|
+
self._thread.join(timeout=2)
|
|
140
|
+
if self._status:
|
|
141
|
+
self._status.__exit__(*args)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def elapsed_ms(self) -> float:
|
|
145
|
+
return (time.time() - self._start_time) * 1000
|
|
146
|
+
|
|
147
|
+
def _rotate(self):
|
|
148
|
+
idx = 0
|
|
149
|
+
while not self._stop_event.is_set():
|
|
150
|
+
self._stop_event.wait(self._interval)
|
|
151
|
+
if self._stop_event.is_set():
|
|
152
|
+
break
|
|
153
|
+
idx = (idx + 1) % len(self._messages)
|
|
154
|
+
elapsed = time.time() - self._start_time
|
|
155
|
+
msg = f"[bold cyan]{self._messages[idx]}[/bold cyan] [dim]({elapsed:.1f}s)[/dim]"
|
|
156
|
+
try:
|
|
157
|
+
if self._status and self._status._live:
|
|
158
|
+
self._status.update(msg)
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
80
163
|
# ---------------------------------------------------------------------------
|
|
81
164
|
# Helper Functions
|
|
82
165
|
# ---------------------------------------------------------------------------
|
|
@@ -131,9 +214,13 @@ def _choice_callback(prompt_text: str, choices: List[str]) -> Optional[str]:
|
|
|
131
214
|
|
|
132
215
|
def _display_pipeline_result(result: PipelineResult, show_sql: bool = True) -> None:
|
|
133
216
|
"""Display a pipeline result with SQL, results table, confidence, or conversational chat message."""
|
|
217
|
+
from dbagent.ui.console import print_timing
|
|
218
|
+
|
|
134
219
|
if result.is_chat and result.message:
|
|
135
220
|
console.print("")
|
|
136
221
|
console.print(Markdown(result.message))
|
|
222
|
+
if result.execution_time_ms > 0:
|
|
223
|
+
print_timing(result.execution_time_ms)
|
|
137
224
|
console.print("")
|
|
138
225
|
return
|
|
139
226
|
|
|
@@ -182,6 +269,10 @@ def _display_pipeline_result(result: PipelineResult, show_sql: bool = True) -> N
|
|
|
182
269
|
if result.retries > 0:
|
|
183
270
|
console.print(f"[dim](Auto-corrected after {result.retries} retry(ies))[/dim]")
|
|
184
271
|
|
|
272
|
+
# Show execution timing
|
|
273
|
+
if result.execution_time_ms > 0:
|
|
274
|
+
print_timing(result.execution_time_ms)
|
|
275
|
+
|
|
185
276
|
# Display recovery info for write changes
|
|
186
277
|
if result.change_id:
|
|
187
278
|
console.print(f"\n[bold green]✓ Change committed successfully[/bold green]")
|
|
@@ -285,7 +376,7 @@ def ask_command(
|
|
|
285
376
|
print_banner()
|
|
286
377
|
db_url = resolve_db_url(db)
|
|
287
378
|
|
|
288
|
-
with
|
|
379
|
+
with DynamicSpinner(DynamicSpinner.CONNECT_MESSAGES):
|
|
289
380
|
try:
|
|
290
381
|
connector = create_connector(db_url)
|
|
291
382
|
success, msg = connector.test_connection()
|
|
@@ -315,7 +406,7 @@ def ask_command(
|
|
|
315
406
|
choice_callback=_choice_callback,
|
|
316
407
|
)
|
|
317
408
|
|
|
318
|
-
with
|
|
409
|
+
with DynamicSpinner():
|
|
319
410
|
result = pipeline.run(user_prompt=prompt, force=force, bypass_cache=no_cache)
|
|
320
411
|
|
|
321
412
|
_display_pipeline_result(result, show_sql=True)
|
|
@@ -336,7 +427,7 @@ def chat_command(
|
|
|
336
427
|
print_banner()
|
|
337
428
|
db_url = resolve_db_url(db)
|
|
338
429
|
|
|
339
|
-
with
|
|
430
|
+
with DynamicSpinner(DynamicSpinner.CONNECT_MESSAGES):
|
|
340
431
|
try:
|
|
341
432
|
connector = create_connector(db_url)
|
|
342
433
|
success, msg = connector.test_connection()
|
|
@@ -383,6 +474,18 @@ def chat_command(
|
|
|
383
474
|
query_history: List[PipelineResult] = []
|
|
384
475
|
last_result: Optional[PipelineResult] = None
|
|
385
476
|
|
|
477
|
+
# Create pipeline ONCE and reuse across all queries (avoids re-init overhead)
|
|
478
|
+
pipeline = QueryPipeline(
|
|
479
|
+
connector=connector,
|
|
480
|
+
llm=llm,
|
|
481
|
+
model=model,
|
|
482
|
+
mode=current_mode,
|
|
483
|
+
auto_execute=auto_execute,
|
|
484
|
+
confirm_callback=_confirm_callback,
|
|
485
|
+
choice_callback=_choice_callback,
|
|
486
|
+
conversation_context=conversation_ctx,
|
|
487
|
+
)
|
|
488
|
+
|
|
386
489
|
while True:
|
|
387
490
|
try:
|
|
388
491
|
user_input = session.prompt("db-agent> ").strip()
|
|
@@ -414,7 +517,10 @@ def chat_command(
|
|
|
414
517
|
console.print(" [bold]:models[/bold] -> Show active AI provider and model status")
|
|
415
518
|
console.print(" [bold]:page <n>[/bold] -> View column page <n> for wide multi-column tables")
|
|
416
519
|
console.print(" [bold]:card[/bold] -> View last result in expanded vertical card format (all columns)")
|
|
520
|
+
console.print(" [bold]:wide[/bold] -> Force-show all columns in terminal (zoom out)")
|
|
417
521
|
console.print(" [bold]:scroll[/bold] -> Open full-screen interactive horizontal/vertical scroller")
|
|
522
|
+
console.print(" [bold]:window[/bold] -> Open desktop GUI window with scrollbars & search")
|
|
523
|
+
console.print(" [bold]:web[/bold] -> Open browser-based interactive data grid")
|
|
418
524
|
console.print(" [bold]:explain[/bold] -> Show generated SQL & query analysis for last turn")
|
|
419
525
|
console.print(" [bold]:history[/bold] -> View history of queries in this session")
|
|
420
526
|
console.print(" [bold]:auto[/bold] / [bold]:noauto[/bold] -> Toggle automatic execution of read queries")
|
|
@@ -522,7 +628,7 @@ def chat_command(
|
|
|
522
628
|
continue
|
|
523
629
|
|
|
524
630
|
if cmd_name == ":doctor":
|
|
525
|
-
with
|
|
631
|
+
with DynamicSpinner(DynamicSpinner.DOCTOR_MESSAGES):
|
|
526
632
|
report = DatabaseDoctor.run_diagnostics(connector, llm, current_mode.value)
|
|
527
633
|
console.print(f"\n[bold cyan]Database Doctor Health Report: {report.database_name} ({report.dialect})[/bold cyan]")
|
|
528
634
|
for check in report.checks:
|
|
@@ -568,6 +674,7 @@ def chat_command(
|
|
|
568
674
|
if prov_target in ["ollama", "gemini", "groq", "openrouter", "mock"]:
|
|
569
675
|
llm = get_llm_provider(provider_name=prov_target, config_mgr=config_mgr)
|
|
570
676
|
model = None
|
|
677
|
+
pipeline.generator.llm = llm # Update reused pipeline
|
|
571
678
|
print_success(f"Switched AI provider to [bold cyan]{llm.name}[/bold cyan]")
|
|
572
679
|
else:
|
|
573
680
|
print_error(f"Unknown provider '{cmd_arg}'. Supported: ollama, gemini, groq, openrouter")
|
|
@@ -608,7 +715,8 @@ def chat_command(
|
|
|
608
715
|
continue
|
|
609
716
|
for i, r in enumerate(query_history[-10:], 1):
|
|
610
717
|
status = "[green]OK[/green]" if r.success else "[red]FAIL[/red]"
|
|
611
|
-
|
|
718
|
+
sql_str = r.sql or ""
|
|
719
|
+
sql_preview = (sql_str[:80] + "...") if len(sql_str) > 80 else sql_str
|
|
612
720
|
console.print(f" {i}. {status} [{r.query_type}] {sql_preview}")
|
|
613
721
|
continue
|
|
614
722
|
|
|
@@ -704,18 +812,12 @@ def chat_command(
|
|
|
704
812
|
break
|
|
705
813
|
|
|
706
814
|
# --- Natural language query via pipeline ---
|
|
707
|
-
pipeline
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
mode=current_mode,
|
|
712
|
-
auto_execute=auto_execute,
|
|
713
|
-
confirm_callback=_confirm_callback,
|
|
714
|
-
choice_callback=_choice_callback,
|
|
715
|
-
conversation_context=conversation_ctx,
|
|
716
|
-
)
|
|
815
|
+
# Update pipeline settings in-place (pipeline was created once before the loop)
|
|
816
|
+
pipeline.model = model
|
|
817
|
+
pipeline.mode = current_mode
|
|
818
|
+
pipeline.auto_execute = auto_execute
|
|
717
819
|
|
|
718
|
-
with
|
|
820
|
+
with DynamicSpinner():
|
|
719
821
|
result = pipeline.run(user_prompt=user_input)
|
|
720
822
|
|
|
721
823
|
_display_pipeline_result(result, show_sql=True)
|
|
@@ -774,7 +876,7 @@ def scan_command(
|
|
|
774
876
|
print_banner()
|
|
775
877
|
db_url = resolve_db_url(db)
|
|
776
878
|
|
|
777
|
-
with
|
|
879
|
+
with DynamicSpinner(DynamicSpinner.SCHEMA_MESSAGES):
|
|
778
880
|
try:
|
|
779
881
|
connector = create_connector(db_url)
|
|
780
882
|
success, msg = connector.test_connection()
|
|
@@ -833,7 +935,7 @@ def doctor_command(
|
|
|
833
935
|
print_banner()
|
|
834
936
|
db_url = resolve_db_url(db)
|
|
835
937
|
|
|
836
|
-
with
|
|
938
|
+
with DynamicSpinner(DynamicSpinner.DOCTOR_MESSAGES):
|
|
837
939
|
try:
|
|
838
940
|
connector = create_connector(db_url)
|
|
839
941
|
llm = get_llm_provider(config_mgr=config_mgr)
|
|
@@ -25,6 +25,8 @@ class GeminiProvider(BaseLLMProvider):
|
|
|
25
25
|
def __init__(self, api_key: Optional[str] = None, default_model: str = "gemini-3.6-flash"):
|
|
26
26
|
self.api_key = api_key or os.getenv("GEMINI_API_KEY")
|
|
27
27
|
self.default_model = default_model
|
|
28
|
+
self._session = requests.Session()
|
|
29
|
+
self._session.headers.update({"User-Agent": "DB-Agent/0.5.0"})
|
|
28
30
|
|
|
29
31
|
@property
|
|
30
32
|
def name(self) -> str:
|
|
@@ -84,7 +86,7 @@ class GeminiProvider(BaseLLMProvider):
|
|
|
84
86
|
|
|
85
87
|
try:
|
|
86
88
|
if stream_callback:
|
|
87
|
-
response =
|
|
89
|
+
response = self._session.post(endpoint, json=payload, stream=True, timeout=15)
|
|
88
90
|
if response.status_code == 200:
|
|
89
91
|
full_text = []
|
|
90
92
|
for line in response.iter_lines():
|
|
@@ -106,11 +108,14 @@ class GeminiProvider(BaseLLMProvider):
|
|
|
106
108
|
return "".join(full_text)
|
|
107
109
|
elif response.status_code in (404, 400, 503, 429):
|
|
108
110
|
last_error = f"Model {target_model} status {response.status_code}"
|
|
111
|
+
if response.status_code == 429:
|
|
112
|
+
import time
|
|
113
|
+
time.sleep(min(2 ** models_to_try.index(target_model), 4))
|
|
109
114
|
continue
|
|
110
115
|
else:
|
|
111
116
|
raise RuntimeError(f"Gemini API error ({response.status_code}): {response.text}")
|
|
112
117
|
else:
|
|
113
|
-
response =
|
|
118
|
+
response = self._session.post(endpoint, json=payload, timeout=30)
|
|
114
119
|
if response.status_code == 200:
|
|
115
120
|
data = response.json()
|
|
116
121
|
candidates = data.get("candidates", [])
|
|
@@ -120,6 +125,9 @@ class GeminiProvider(BaseLLMProvider):
|
|
|
120
125
|
return ""
|
|
121
126
|
elif response.status_code in (404, 400, 503, 429):
|
|
122
127
|
last_error = f"Model {target_model} status {response.status_code}"
|
|
128
|
+
if response.status_code == 429:
|
|
129
|
+
import time
|
|
130
|
+
time.sleep(min(2 ** models_to_try.index(target_model), 4))
|
|
123
131
|
continue
|
|
124
132
|
else:
|
|
125
133
|
raise RuntimeError(f"Gemini API error ({response.status_code}): {response.text}")
|
|
@@ -26,8 +26,10 @@ console = Console()
|
|
|
26
26
|
|
|
27
27
|
def print_banner() -> None:
|
|
28
28
|
"""Print the DB-Agent terminal banner."""
|
|
29
|
+
from dbagent import __version__
|
|
29
30
|
banner_text = Text()
|
|
30
31
|
banner_text.append("[*] DB-AGENT ", style="bold cyan")
|
|
32
|
+
banner_text.append(f"v{__version__} ", style="bold green")
|
|
31
33
|
banner_text.append("- Universal Database Introspector & AI Script Generator\n", style="bold white")
|
|
32
34
|
banner_text.append("Supports PostgreSQL | MySQL | SQLite | MSSQL | Oracle | DuckDB | MongoDB", style="dim")
|
|
33
35
|
panel = Panel(banner_text, border_style="cyan", padding=(0, 2))
|
|
@@ -57,6 +59,21 @@ def print_code(code: str, language: str = "sql", title: Optional[str] = None) ->
|
|
|
57
59
|
console.print(panel)
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
def print_timing(elapsed_ms: float) -> None:
|
|
63
|
+
"""Display execution timing with color coding."""
|
|
64
|
+
elapsed_s = elapsed_ms / 1000
|
|
65
|
+
if elapsed_s < 1.0:
|
|
66
|
+
style = "bold green"
|
|
67
|
+
icon = "⚡"
|
|
68
|
+
elif elapsed_s < 5.0:
|
|
69
|
+
style = "bold yellow"
|
|
70
|
+
icon = "⏱️"
|
|
71
|
+
else:
|
|
72
|
+
style = "bold red"
|
|
73
|
+
icon = "🐢"
|
|
74
|
+
console.print(f"[{style}]{icon} Completed in {elapsed_s:.1f}s[/{style}]")
|
|
75
|
+
|
|
76
|
+
|
|
60
77
|
def print_results_table(
|
|
61
78
|
columns: List[str],
|
|
62
79
|
rows: List[Dict[str, Any]],
|
|
@@ -24,6 +24,10 @@ def open_desktop_grid_window(
|
|
|
24
24
|
def _launch_gui():
|
|
25
25
|
try:
|
|
26
26
|
import tkinter as tk
|
|
27
|
+
except ImportError:
|
|
28
|
+
print("[DB-Agent] Desktop GUI requires tkinter. Install it with: sudo apt-get install python3-tk (Linux) or it's included with Python on Windows/macOS.")
|
|
29
|
+
return
|
|
30
|
+
try:
|
|
27
31
|
from tkinter import ttk, messagebox, filedialog
|
|
28
32
|
import csv
|
|
29
33
|
|
|
@@ -106,18 +110,22 @@ def open_desktop_grid_window(
|
|
|
106
110
|
col_w = max(120, min(280, len(str(col)) * 12))
|
|
107
111
|
tree.column(col, width=col_w, minwidth=80, stretch=False)
|
|
108
112
|
|
|
113
|
+
MAX_GUI_ROWS = 5000
|
|
114
|
+
|
|
109
115
|
def populate_table(filter_text=""):
|
|
110
116
|
tree.delete(*tree.get_children())
|
|
111
117
|
f_lower = filter_text.strip().lower()
|
|
112
118
|
count = 0
|
|
113
|
-
|
|
119
|
+
source_rows = rows[:MAX_GUI_ROWS]
|
|
120
|
+
for r in source_rows:
|
|
114
121
|
row_vals = [str(r.get(c, "")) if r.get(c) is not None else "NULL" for c in columns]
|
|
115
122
|
if f_lower:
|
|
116
123
|
if not any(f_lower in v.lower() for v in row_vals):
|
|
117
124
|
continue
|
|
118
125
|
tree.insert("", tk.END, values=row_vals)
|
|
119
126
|
count += 1
|
|
120
|
-
|
|
127
|
+
truncated = f" (limited to {MAX_GUI_ROWS})" if len(rows) > MAX_GUI_ROWS else ""
|
|
128
|
+
status_lbl.config(text=f"Showing: {count} of {len(rows)} rows ({len(columns)} columns){truncated}")
|
|
121
129
|
|
|
122
130
|
populate_table()
|
|
123
131
|
|
|
@@ -242,4 +250,5 @@ def open_browser_grid_window(
|
|
|
242
250
|
with open(temp_path, "w", encoding="utf-8") as f:
|
|
243
251
|
f.write(html_content)
|
|
244
252
|
|
|
245
|
-
|
|
253
|
+
from pathlib import Path as _Path
|
|
254
|
+
webbrowser.open(_Path(temp_path).as_uri())
|
|
@@ -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.5.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
|