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.
Files changed (74) hide show
  1. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/PKG-INFO +1 -1
  2. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/generator.py +14 -5
  4. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/cli.py +120 -18
  5. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/gemini_provider.py +10 -2
  6. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/console.py +17 -0
  7. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/grid_window.py +12 -3
  8. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/PKG-INFO +1 -1
  9. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/pyproject.toml +1 -1
  10. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/README.md +0 -0
  11. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/confidence.py +0 -0
  12. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/conversation.py +0 -0
  13. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/doctor.py +0 -0
  14. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/error_classifier.py +0 -0
  15. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/pipeline.py +0 -0
  16. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/recommender.py +0 -0
  17. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/agent/validator.py +0 -0
  18. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/config.py +0 -0
  19. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/base.py +0 -0
  20. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/factory.py +0 -0
  21. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/mongo.py +0 -0
  22. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/connectors/relational.py +0 -0
  23. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/__init__.py +0 -0
  24. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/cache.py +0 -0
  25. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/changeset.py +0 -0
  26. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/cost.py +0 -0
  27. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/recovery.py +0 -0
  28. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/sandbox.py +0 -0
  29. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/execution/streaming.py +0 -0
  30. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/auto_setup.py +0 -0
  31. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/base.py +0 -0
  32. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/factory.py +0 -0
  33. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/groq_provider.py +0 -0
  34. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/mock_provider.py +0 -0
  35. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/ollama_provider.py +0 -0
  36. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/llm/openrouter_provider.py +0 -0
  37. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/cache.py +0 -0
  38. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/formatter.py +0 -0
  39. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/graph.py +0 -0
  40. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/models.py +0 -0
  41. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/retriever.py +0 -0
  42. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/schema/selector.py +0 -0
  43. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/__init__.py +0 -0
  44. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/audit.py +0 -0
  45. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/environment.py +0 -0
  46. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/masking.py +0 -0
  47. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/modes.py +0 -0
  48. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/security/rbac.py +0 -0
  49. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/semantic/__init__.py +0 -0
  50. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/semantic/dictionary.py +0 -0
  51. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/semantic/templates.py +0 -0
  52. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/sql/__init__.py +0 -0
  53. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/sql/optimizer.py +0 -0
  54. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/sql/validator.py +0 -0
  55. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/viewer.py +0 -0
  56. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent/ui/visualizer.py +0 -0
  57. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/SOURCES.txt +0 -0
  58. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  59. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/entry_points.txt +0 -0
  60. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/requires.txt +0 -0
  61. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/dbagent_cli.egg-info/top_level.txt +0 -0
  62. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/setup.cfg +0 -0
  63. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_cache.py +0 -0
  64. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_cli.py +0 -0
  65. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_connectors.py +0 -0
  66. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_doctor_and_viz.py +0 -0
  67. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_generator.py +0 -0
  68. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_graph.py +0 -0
  69. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_recovery.py +0 -0
  70. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_safety.py +0 -0
  71. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_sandbox.py +0 -0
  72. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_schema.py +0 -0
  73. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_security.py +0 -0
  74. {dbagent_cli-0.4.1 → dbagent_cli-0.5.0}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -2,5 +2,5 @@
2
2
  DB-Agent: Universal Database Introspection and Script Generation AI Agent (CLI).
3
3
  """
4
4
 
5
- __version__ = "0.4.1"
5
+ __version__ = "0.5.0"
6
6
  __author__ = "Santhosh Gupta"
@@ -132,16 +132,25 @@ class ScriptGenerator:
132
132
  if kw in prompt_lower:
133
133
  return "ddl"
134
134
 
135
- # Write indicators
136
- write_keywords = [
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", "change", "modify", "set ",
139
- "delete", "remove", "drop record",
138
+ "update", "delete", "drop record",
140
139
  "increment", "decrement",
141
140
  ]
142
- for kw in write_keywords:
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 console.status("[bold cyan]Connecting to database...[/bold cyan]"):
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 console.status("[bold cyan]Thinking & generating SQL...[/bold cyan]"):
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 console.status("[bold cyan]Connecting to database...[/bold cyan]"):
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 console.status("[bold cyan]Running Database Doctor diagnostics...[/bold cyan]"):
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
- sql_preview = (r.sql[:80] + "...") if len(r.sql) > 80 else r.sql
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 = QueryPipeline(
708
- connector=connector,
709
- llm=llm,
710
- model=model,
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 console.status("[bold cyan]Thinking...[/bold cyan]"):
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 console.status("[bold cyan]Connecting and introspecting database schema...[/bold cyan]"):
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 console.status("[bold cyan]Running Database Doctor health check...[/bold cyan]"):
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 = requests.post(endpoint, json=payload, stream=True, timeout=10)
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 = requests.post(endpoint, json=payload, timeout=10)
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
- for r in rows:
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
- status_lbl.config(text=f"Showing: {count} of {len(rows)} rows ({len(columns)} columns)")
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
- webbrowser.open(f"file://{temp_path}")
253
+ from pathlib import Path as _Path
254
+ webbrowser.open(_Path(temp_path).as_uri())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbagent-cli"
7
- version = "0.4.1"
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