dbagent-cli 0.8.2__tar.gz → 0.8.4__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 (82) hide show
  1. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/PKG-INFO +1 -1
  2. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/cli.py +0 -22
  4. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/ui/console.py +54 -19
  5. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent_cli.egg-info/PKG-INFO +1 -1
  6. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent_cli.egg-info/SOURCES.txt +0 -1
  7. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/pyproject.toml +1 -1
  8. dbagent_cli-0.8.2/dbagent/ui/fullscreen_chat.py +0 -392
  9. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/README.md +0 -0
  10. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/confidence.py +0 -0
  11. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/conversation.py +0 -0
  12. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/doctor.py +0 -0
  13. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/error_classifier.py +0 -0
  14. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/generator.py +0 -0
  15. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/pipeline.py +0 -0
  16. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/recommender.py +0 -0
  17. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/agent/validator.py +0 -0
  18. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/config.py +0 -0
  19. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/connectors/base.py +0 -0
  20. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/connectors/factory.py +0 -0
  21. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/connectors/mongo.py +0 -0
  22. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/connectors/relational.py +0 -0
  23. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/execution/__init__.py +0 -0
  24. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/execution/cache.py +0 -0
  25. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/execution/changeset.py +0 -0
  26. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/execution/cost.py +0 -0
  27. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/execution/recovery.py +0 -0
  28. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/execution/sandbox.py +0 -0
  29. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/execution/streaming.py +0 -0
  30. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/anthropic_provider.py +0 -0
  31. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/auto_setup.py +0 -0
  32. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/base.py +0 -0
  33. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/deepseek_provider.py +0 -0
  34. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/factory.py +0 -0
  35. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/gemini_provider.py +0 -0
  36. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/groq_provider.py +0 -0
  37. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/mistral_provider.py +0 -0
  38. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/mock_provider.py +0 -0
  39. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/ollama_provider.py +0 -0
  40. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/openai_provider.py +0 -0
  41. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/llm/openrouter_provider.py +0 -0
  42. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/schema/cache.py +0 -0
  43. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/schema/formatter.py +0 -0
  44. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/schema/graph.py +0 -0
  45. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/schema/models.py +0 -0
  46. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/schema/retriever.py +0 -0
  47. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/schema/selector.py +0 -0
  48. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/security/__init__.py +0 -0
  49. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/security/audit.py +0 -0
  50. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/security/environment.py +0 -0
  51. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/security/masking.py +0 -0
  52. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/security/modes.py +0 -0
  53. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/security/rbac.py +0 -0
  54. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/semantic/__init__.py +0 -0
  55. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/semantic/dictionary.py +0 -0
  56. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/semantic/templates.py +0 -0
  57. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/sql/__init__.py +0 -0
  58. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/sql/optimizer.py +0 -0
  59. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/sql/validator.py +0 -0
  60. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/ui/chat_screen.py +0 -0
  61. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/ui/grid_window.py +0 -0
  62. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/ui/viewer.py +0 -0
  63. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent/ui/visualizer.py +0 -0
  64. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  65. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent_cli.egg-info/entry_points.txt +0 -0
  66. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent_cli.egg-info/requires.txt +0 -0
  67. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/dbagent_cli.egg-info/top_level.txt +0 -0
  68. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/setup.cfg +0 -0
  69. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_cache.py +0 -0
  70. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_cli.py +0 -0
  71. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_connectors.py +0 -0
  72. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_direct_sql.py +0 -0
  73. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_doctor_and_viz.py +0 -0
  74. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_generator.py +0 -0
  75. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_graph.py +0 -0
  76. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_providers.py +0 -0
  77. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_recovery.py +0 -0
  78. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_safety.py +0 -0
  79. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_sandbox.py +0 -0
  80. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_schema.py +0 -0
  81. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_security.py +0 -0
  82. {dbagent_cli-0.8.2 → dbagent_cli-0.8.4}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.8.2
3
+ Version: 0.8.4
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.8.2"
5
+ __version__ = "0.8.4"
6
6
  __author__ = "Santhosh Gupta"
@@ -523,28 +523,6 @@ def chat_command(
523
523
  conversation_context=conversation_ctx,
524
524
  )
525
525
 
526
- # Launch Full-Screen TUI (Fixed top header, middle scroller, static bottom chat bar)
527
- if not inline and not os.getenv("DBAGENT_INLINE_CHAT"):
528
- try:
529
- from dbagent.ui.fullscreen_chat import FullScreenChatApp
530
- app_tui = FullScreenChatApp(
531
- connector=connector,
532
- llm=llm,
533
- pipeline=pipeline,
534
- db_name=db_name,
535
- dialect=dialect,
536
- table_count=len(table_names),
537
- config_mgr=config_mgr,
538
- cache_mgr=cache_mgr,
539
- model=model,
540
- mode=current_mode,
541
- )
542
- app_tui.run()
543
- connector.close()
544
- return
545
- except Exception:
546
- pass
547
-
548
526
  def _get_bottom_toolbar():
549
527
  curr_model = model or getattr(llm, "default_model", "default")
550
528
  return HTML(
@@ -81,16 +81,18 @@ def print_results_table(
81
81
  col_page: int = 1,
82
82
  cols_per_page: Optional[int] = None,
83
83
  show_all: bool = False,
84
+ max_rows_display: int = 15,
84
85
  ) -> None:
85
86
  """
86
87
  Print query results in a clean, readable Rich table.
87
- Dynamically scales column count based on terminal zoom-out/width.
88
+ Dynamically scales column count and limits vertical rows to fit viewport.
88
89
  """
89
90
  if not columns or not rows:
90
91
  console.print("[dim]Query returned 0 rows.[/dim]\n")
91
92
  return
92
93
 
93
94
  total_cols = len(columns)
95
+ total_rows = len(rows)
94
96
  term_width = console.width or 120
95
97
  auto_cols = max(8, term_width // 16)
96
98
 
@@ -110,12 +112,14 @@ def print_results_table(
110
112
  if total_pages > 1 and not show_all:
111
113
  tbl_title += f" [dim](Cols {start_idx + 1}–{end_idx} of {total_cols} | Page {current_page}/{total_pages})[/dim]"
112
114
 
113
- table = Table(title=tbl_title, show_header=True, header_style="bold magenta", border_style="dim")
115
+ table = Table(title=tbl_title, show_header=True, header_style="bold cyan", border_style="dim")
114
116
 
115
117
  for col in display_cols:
116
118
  table.add_column(str(col), overflow="ellipsis", max_width=35)
117
119
 
118
- for row in rows:
120
+ display_rows = rows if (show_all or total_rows <= max_rows_display) else rows[:max_rows_display]
121
+
122
+ for row in display_rows:
119
123
  row_vals = []
120
124
  for col in display_cols:
121
125
  v = row.get(col)
@@ -128,11 +132,17 @@ def print_results_table(
128
132
  console.print("")
129
133
  console.print(table)
130
134
 
135
+ footer_items = []
136
+ if total_rows > max_rows_display and not show_all:
137
+ footer_items.append(f"[bold green]Showing {len(display_rows)} of {total_rows} rows[/bold green]")
138
+ footer_items.append("[dim]⚡ Type [bold white]:scroll[/bold white] for scroller · [bold white]:all[/bold white] to show all · [bold white]:window[/bold white] for GUI[/dim]")
139
+ else:
140
+ footer_items.append(f"[bold green]Total rows:[/bold green] {total_rows}")
141
+
131
142
  if total_pages > 1 and not show_all:
132
- console.print(
133
- f"[dim]⚡ [bold]:window[/bold] (GUI scroll window) | [bold]:page <1-{total_pages}>[/bold] | [bold]:card[/bold] (vertical) | [bold]:wide[/bold] (zoom-out)[/dim]"
134
- )
135
- console.print(f"[bold green]Total rows:[/bold green] {len(rows)}\n")
143
+ footer_items.append(f"[dim]· [bold]:page <1-{total_pages}>[/bold] | [bold]:card[/bold] (vertical)[/dim]")
144
+
145
+ console.print(" ".join(footer_items) + "\n")
136
146
 
137
147
 
138
148
  def print_results_vertical(
@@ -162,35 +172,60 @@ def print_results_vertical(
162
172
 
163
173
  def open_interactive_scroller(columns: List[str], rows: List[Dict[str, Any]]) -> None:
164
174
  """
165
- Interactive Column Scroller allowing seamless navigation through wide tables.
175
+ Interactive 2D Scroller allowing seamless navigation through both long rows and wide columns.
166
176
  """
167
177
  if not columns or not rows:
168
178
  console.print("[dim]No data to display.[/dim]\n")
169
179
  return
170
180
 
171
181
  cols_per_page = 6
182
+ rows_per_page = 15
172
183
  total_cols = len(columns)
173
- total_pages = max(1, math.ceil(total_cols / cols_per_page))
174
- current_page = 1
184
+ total_rows = len(rows)
185
+ total_col_pages = max(1, math.ceil(total_cols / cols_per_page))
186
+ total_row_pages = max(1, math.ceil(total_rows / rows_per_page))
187
+
188
+ current_col_page = 1
189
+ current_row_page = 1
175
190
 
176
191
  from rich.prompt import Prompt
177
192
 
178
- console.print(f"\n[bold cyan]─── Interactive Multi-Column Scroller ({total_cols} columns, {len(rows)} rows) ───[/bold cyan]")
193
+ console.print(f"\n[bold cyan]─── 📜 Interactive 2D Scroller ({total_rows} rows × {total_cols} columns) ───[/bold cyan]")
179
194
  while True:
180
- print_results_table(columns, rows, title="Column Scroller", col_page=current_page, cols_per_page=cols_per_page)
195
+ start_row = (current_row_page - 1) * rows_per_page
196
+ end_row = min(start_row + rows_per_page, total_rows)
197
+ page_rows = rows[start_row:end_row]
198
+
199
+ print_results_table(
200
+ columns=columns,
201
+ rows=page_rows,
202
+ title=f"Scroller [dim](Rows {start_row + 1}–{end_row} of {total_rows} · Page {current_row_page}/{total_row_pages})[/dim]",
203
+ col_page=current_col_page,
204
+ cols_per_page=cols_per_page,
205
+ show_all=False,
206
+ max_rows_display=rows_per_page,
207
+ )
181
208
 
182
- console.print(f"[bold cyan]Scroller Navigation:[/bold cyan] [bold]n[/bold]=Next Cols | [bold]p[/bold]=Prev Cols | [bold]1-{total_pages}[/bold]=Jump to Page | [bold]c[/bold]=Vertical Cards | [bold]q[/bold]=Exit Scroller")
183
- choice = Prompt.ask("[dim]scroller>[/dim]", default="n").strip().lower()
209
+ console.print(f"[bold cyan]Navigation:[/bold cyan] [bold]d[/bold]=Next Rows | [bold]u[/bold]=Prev Rows | [bold]n[/bold]=Next Cols | [bold]p[/bold]=Prev Cols | [bold]c[/bold]=Cards | [bold]q[/bold]=Exit")
210
+ choice = Prompt.ask("[dim]scroller>[/dim]", default="d").strip().lower()
184
211
 
185
212
  if choice in ("q", "quit", "exit", "back"):
186
213
  console.print("[dim]Exited scroller.[/dim]\n")
187
214
  break
188
- elif choice in ("n", "next", ""):
189
- current_page = 1 if current_page >= total_pages else current_page + 1
190
- elif choice in ("p", "prev", "previous"):
191
- current_page = total_pages if current_page <= 1 else current_page - 1
215
+ elif choice in ("d", "down", "next-row", ""):
216
+ current_row_page = 1 if current_row_page >= total_row_pages else current_row_page + 1
217
+ elif choice in ("u", "up", "prev-row"):
218
+ current_row_page = total_row_pages if current_row_page <= 1 else current_row_page - 1
219
+ elif choice in ("n", "next", "right", ">"):
220
+ current_col_page = 1 if current_col_page >= total_col_pages else current_col_page + 1
221
+ elif choice in ("p", "prev", "left", "<"):
222
+ current_col_page = total_col_pages if current_col_page <= 1 else current_col_page - 1
223
+ elif choice.isdigit():
224
+ val = int(choice)
225
+ if 1 <= val <= total_row_pages:
226
+ current_row_page = val
192
227
  elif choice in ("c", "card", "v", "vertical"):
193
- print_results_vertical(columns, rows)
228
+ print_results_vertical(columns, page_rows)
194
229
  Prompt.ask("[dim]Press Enter to return to scroller...[/dim]")
195
230
  else:
196
231
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.8.2
3
+ Version: 0.8.4
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
@@ -54,7 +54,6 @@ dbagent/sql/optimizer.py
54
54
  dbagent/sql/validator.py
55
55
  dbagent/ui/chat_screen.py
56
56
  dbagent/ui/console.py
57
- dbagent/ui/fullscreen_chat.py
58
57
  dbagent/ui/grid_window.py
59
58
  dbagent/ui/viewer.py
60
59
  dbagent/ui/visualizer.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbagent-cli"
7
- version = "0.8.2"
7
+ version = "0.8.4"
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"
@@ -1,392 +0,0 @@
1
- """
2
- Full-Screen TUI for DB-Agent.
3
- Features:
4
- - Static pinned top Header with live connection and AI status.
5
- - Middle scrollable conversation history with Rich-rendered tables, SQL syntax boxes, and markdown.
6
- - Static pinned bottom Chat Bar with prompt input and shortcut footer.
7
- """
8
-
9
- import io
10
- import os
11
- import sys
12
- import time
13
- from typing import Optional, List, Dict, Any, Callable
14
- from prompt_toolkit.application import Application
15
- from prompt_toolkit.layout.containers import HSplit, VSplit, Window
16
- from prompt_toolkit.layout.controls import FormattedTextControl
17
- from prompt_toolkit.layout.layout import Layout
18
- from prompt_toolkit.widgets import TextArea, Frame
19
- from prompt_toolkit.key_binding import KeyBindings
20
- from prompt_toolkit.formatted_text import HTML, ANSI
21
- from prompt_toolkit.styles import Style
22
- from rich.console import Console
23
- from rich.panel import Panel
24
- from rich.table import Table
25
- from rich.syntax import Syntax
26
- from rich.markdown import Markdown
27
-
28
- from dbagent import __version__
29
- from dbagent.agent.pipeline import QueryPipeline, PipelineResult
30
- from dbagent.connectors.base import BaseConnector
31
- from dbagent.llm.base import BaseLLMProvider
32
- from dbagent.llm.factory import get_llm_provider
33
- from dbagent.security.modes import OperationMode
34
- from dbagent.config import ConfigManager
35
- from dbagent.schema.cache import SchemaCacheManager
36
- from dbagent.schema.formatter import SchemaFormatter
37
- from dbagent.agent.recommender import QueryRecommender
38
- from dbagent.agent.doctor import DatabaseDoctor
39
- from dbagent.ui.chat_screen import MASCOT_ART, render_session_history
40
-
41
-
42
- class FullScreenChatApp:
43
- """
44
- Full-Screen interactive TUI with fixed Top Header, middle Scrollable History,
45
- and fixed Bottom Chat Bar.
46
- """
47
-
48
- def __init__(
49
- self,
50
- connector: BaseConnector,
51
- llm: BaseLLMProvider,
52
- pipeline: QueryPipeline,
53
- db_name: str,
54
- dialect: str,
55
- table_count: int,
56
- config_mgr: ConfigManager,
57
- cache_mgr: SchemaCacheManager,
58
- model: Optional[str] = None,
59
- mode: OperationMode = OperationMode.CONFIRM,
60
- ):
61
- self.connector = connector
62
- self.llm = llm
63
- self.pipeline = pipeline
64
- self.db_name = db_name
65
- self.dialect = dialect
66
- self.table_count = table_count
67
- self.config_mgr = config_mgr
68
- self.cache_mgr = cache_mgr
69
- self.model = model or getattr(llm, "default_model", "default")
70
- self.mode = mode
71
- self.query_history: List[PipelineResult] = []
72
- self.last_result: Optional[PipelineResult] = None
73
- self.auto_execute = True
74
-
75
- # Rich console for buffer rendering
76
- self.rich_io = io.StringIO()
77
- self.rich_console = Console(
78
- file=self.rich_io,
79
- force_terminal=True,
80
- color_system="truecolor",
81
- width=110,
82
- )
83
-
84
- # Build UI layout components
85
- self._setup_ui()
86
-
87
- def _render_rich_to_ansi(self, renderable: Any) -> str:
88
- """Helper to render any Rich renderable (Table, Panel, etc.) to ANSI string."""
89
- self.rich_io.seek(0)
90
- self.rich_io.truncate(0)
91
- self.rich_console.print(renderable)
92
- return self.rich_io.getvalue()
93
-
94
- def _setup_ui(self) -> None:
95
- """Create the prompt_toolkit UI containers and keybindings."""
96
- kb = KeyBindings()
97
-
98
- @kb.add("c-c")
99
- @kb.add("c-q")
100
- def _exit(event):
101
- event.app.exit()
102
-
103
- @kb.add("pageup")
104
- def _pageup(event):
105
- self.history_area.buffer.cursor_up(count=10)
106
-
107
- @kb.add("pagedown")
108
- def _pagedown(event):
109
- self.history_area.buffer.cursor_down(count=10)
110
-
111
- @kb.add("enter")
112
- def _submit(event):
113
- text = self.input_area.text.strip()
114
- if not text:
115
- return
116
- self.input_area.text = ""
117
- self._handle_input(text)
118
-
119
- # 1. Top Fixed Header Window
120
- def get_header_text():
121
- return HTML(
122
- f"<style bg='#0f172a' fg='#00ffff'><b> ⚡ DB-AGENT v{__version__} </b></style> "
123
- f"<style bg='#0f172a' fg='#00ff88'><b>🟢 Connected:</b> {self.db_name} <i>({self.dialect} • {self.table_count} tables)</i></style> │ "
124
- f"<style bg='#0f172a' fg='#00ffff'><b>🤖 AI:</b> {self.llm.name} ({self.model})</style> │ "
125
- f"<style bg='#0f172a' fg='#fbbf24'><b>🛡️ {self.mode.value}</b></style>"
126
- )
127
-
128
- self.header_window = Window(
129
- content=FormattedTextControl(get_header_text),
130
- height=1,
131
- style="bg:#0f172a",
132
- )
133
-
134
- # 2. Middle Scrollable Conversation History
135
- self.history_area = TextArea(
136
- text="",
137
- read_only=True,
138
- scrollbar=True,
139
- wrap_lines=True,
140
- style="bg:#020617 fg:#f8fafc",
141
- )
142
-
143
- # 3. Bottom Static Input Bar
144
- self.input_area = TextArea(
145
- height=1,
146
- prompt="❯ ",
147
- style="bg:#0f172a fg:#00ffff bold",
148
- multiline=False,
149
- wrap_lines=False,
150
- )
151
-
152
- self.input_frame = Frame(
153
- body=self.input_area,
154
- title="💬 Ask plain English or paste SQL",
155
- style="bg:#0f172a fg:#00ffff",
156
- )
157
-
158
- # 4. Bottom Static Toolbar
159
- def get_footer_text():
160
- return HTML(
161
- "<style bg='#0f172a' fg='#94a3b8'> <b>Enter</b> Send │ "
162
- "<b>PgUp/PgDn</b> Scroll History │ "
163
- "<b>:help</b> Commands │ "
164
- "<b>:models</b> Switch Model │ "
165
- "<b>:doctor</b> Health │ "
166
- "<b>:clear</b> Clear │ "
167
- "<b>:exit</b> Quit </style>"
168
- )
169
-
170
- self.footer_window = Window(
171
- content=FormattedTextControl(get_footer_text),
172
- height=1,
173
- style="bg:#0f172a",
174
- )
175
-
176
- # Assemble FullScreen Layout
177
- self.root_container = HSplit([
178
- self.header_window,
179
- self.history_area,
180
- self.input_frame,
181
- self.footer_window,
182
- ])
183
-
184
- self.custom_style = Style.from_dict({
185
- "frame.label": "#00ffff bold",
186
- "frame.border": "#00ffff",
187
- })
188
-
189
- self.app = Application(
190
- layout=Layout(self.root_container, focused_element=self.input_area),
191
- key_bindings=kb,
192
- style=self.custom_style,
193
- full_screen=True,
194
- mouse_support=True,
195
- )
196
-
197
- # Print initial welcome banner into history
198
- self._print_welcome()
199
-
200
- def _append_text(self, text: str) -> None:
201
- """Append text to the scrollable middle conversation buffer and auto-scroll to end."""
202
- current = self.history_area.text
203
- if current:
204
- self.history_area.text = current + "\n" + text
205
- else:
206
- self.history_area.text = text
207
- self.history_area.buffer.cursor_position = len(self.history_area.text)
208
-
209
- def _print_welcome(self) -> None:
210
- """Render initial split-card welcome screen into history area."""
211
- left_grid = Table.grid(padding=(0, 1))
212
- left_grid.add_column(justify="center", min_width=28)
213
- left_grid.add_row("[bold white]Welcome to DB-Agent![/bold white]")
214
- left_grid.add_row(f"[bold cyan]{MASCOT_ART.strip()}[/bold cyan]")
215
- left_grid.add_row(f"[bold cyan]{self.model}[/bold cyan] [dim]({self.llm.name})[/dim]")
216
- left_grid.add_row(f"[bold green]🟢 Connected:[/bold green] [bold white]{self.db_name}[/bold white] [dim]({self.dialect} • {self.table_count} tables)[/dim]")
217
-
218
- right_grid = Table.grid(padding=(0, 1))
219
- right_grid.add_column()
220
- right_grid.add_row("[bold cyan]💡 Quick Database Tips[/bold cyan]")
221
- right_grid.add_row("[dim]Ask in plain English or paste direct SQL for 0ms execution[/dim]")
222
- right_grid.add_row("[dim]Type :help to inspect tables (:tables), relations (:fk), or diagnostics (:doctor)[/dim]")
223
- right_grid.add_row("[dim]────────────────────────────────────────────────────────────────────────[/dim]")
224
- right_grid.add_row(f"[bold green]🚀 Features in v{__version__}[/bold green]")
225
- right_grid.add_row("[dim]Universal AI: Claude 3.7 Sonnet, OpenAI GPT-4o, DeepSeek R1, Mistral, Gemini[/dim]")
226
- right_grid.add_row("[dim]Zero-Latency Direct SQL Fast-Path with autonomous AI auto-repair on error[/dim]")
227
- right_grid.add_row("[dim]Multi-turn conversational session memory graph & persistent connection pooling[/dim]")
228
-
229
- layout_table = Table.grid(expand=True, padding=(0, 2))
230
- layout_table.add_column(ratio=2)
231
- layout_table.add_column(ratio=3)
232
- layout_table.add_row(left_grid, right_grid)
233
-
234
- panel = Panel(
235
- layout_table,
236
- title=f"[bold cyan]⚡ DB-AGENT [/bold cyan][bold green]v{__version__}[/bold green]",
237
- title_align="left",
238
- border_style="cyan",
239
- padding=(1, 1),
240
- )
241
-
242
- ansi_banner = self._render_rich_to_ansi(panel)
243
- self._append_text(ansi_banner)
244
-
245
- # Add sample recommendation prompts
246
- try:
247
- cached = self.cache_mgr.load_cached_schema(self.connector.connection_url)
248
- if cached:
249
- recs = QueryRecommender.generate_recommendations(cached, max_suggestions=3)
250
- if recs:
251
- rec_lines = ["[dim italic]💡 Try asking:[/dim italic]"]
252
- for r in recs:
253
- rec_lines.append(f" [dim]→[/dim] [cyan]{r}[/cyan]")
254
- self._append_text(self._render_rich_to_ansi("\n".join(rec_lines)))
255
- except Exception:
256
- pass
257
-
258
- def _handle_input(self, user_input: str) -> None:
259
- """Handle incoming prompt or REPL command."""
260
- # 1. REPL Commands
261
- if user_input.startswith(":"):
262
- cmd_parts = user_input.split(maxsplit=1)
263
- cmd_name = cmd_parts[0].lower()
264
- cmd_arg = cmd_parts[1].strip() if len(cmd_parts) > 1 else ""
265
-
266
- if cmd_name in [":exit", ":quit", ":q"]:
267
- self.app.exit()
268
- return
269
-
270
- if cmd_name == ":clear":
271
- self.history_area.text = ""
272
- self._print_welcome()
273
- return
274
-
275
- if cmd_name in [":help", ":h", ":?"]:
276
- cmd_table = Table(box=None, padding=(0, 2), show_header=False)
277
- cmd_table.add_column("Command", style="bold cyan", width=22)
278
- cmd_table.add_column("Description", style="dim")
279
- cmd_table.add_row(":models, :model", "Switch active AI model (Claude 3.7, GPT-4o, DeepSeek, Gemini...)")
280
- cmd_table.add_row(":provider <name>", "Switch AI provider (claude, openai, deepseek, mistral, gemini)")
281
- cmd_table.add_row(":tables, :dt", "List all tables & schemas in this database")
282
- cmd_table.add_row(":table <name>", "Inspect columns, primary keys, data types & sample data")
283
- cmd_table.add_row(":fk <name>", "View foreign key relationships & dependency graph")
284
- cmd_table.add_row(":path <t1> <t2>", "Discover shortest SQL join path between two tables")
285
- cmd_table.add_row(":doctor", "Run database health, index, and query performance diagnostics")
286
- cmd_table.add_row(":mode <name>", "Switch safety mode (SAFE, CONFIRM, ADMIN, READ_ONLY)")
287
- cmd_table.add_row(":history", "View session query history, execution timing & row counts")
288
- cmd_table.add_row(":explain", "Explain last generated SQL query with execution plan")
289
- cmd_table.add_row(":run", "Re-execute the last generated SQL query")
290
- cmd_table.add_row(":clear", "Clear conversation memory & redraw screen")
291
- cmd_table.add_row(":exit, :q", "Exit DB-Agent session")
292
- ansi_help = self._render_rich_to_ansi(Panel(cmd_table, title="[bold cyan]⌨️ Interactive Commands[/bold cyan]", border_style="cyan"))
293
- self._append_text(f"\n[cyan]❯ {user_input}[/cyan]\n" + ansi_help)
294
- return
295
-
296
- if cmd_name in [":models", ":model"]:
297
- avail = self.llm.list_models() if hasattr(self.llm, "list_models") else []
298
- if not cmd_arg or cmd_name == ":models":
299
- lines = [f"[bold cyan]Available Models for {self.llm.name}:[/bold cyan]\n"]
300
- for idx, m in enumerate(avail, 1):
301
- marker = "❯" if m == self.model else " "
302
- active = " [bold green](Active)[/bold green]" if m == self.model else ""
303
- lines.append(f" {marker} [bold cyan]{idx}.[/bold cyan] [bold white]{m}[/bold white]{active}")
304
- lines.append("\n[dim]Switch with: :model <number|name> (e.g. :model 1 or :model 3.7)[/dim]")
305
- self._append_text(f"\n[cyan]❯ {user_input}[/cyan]\n" + self._render_rich_to_ansi("\n".join(lines)))
306
- return
307
- else:
308
- # Switch model
309
- target = cmd_arg
310
- if target.isdigit() and avail:
311
- idx = int(target) - 1
312
- if 0 <= idx < len(avail):
313
- target = avail[idx]
314
- elif target in ["3.7", "37"]:
315
- target = "claude-3-7-sonnet-20250219" if self.llm.name == "claude" else "gemini-3.7-flash"
316
- elif target in ["3.6", "36"]:
317
- target = "gemini-3.6-flash"
318
- self.model = target
319
- self.pipeline.model = target
320
- self._append_text(f"\n[cyan]❯ {user_input}[/cyan]\n[bold green]✓ Switched model to {self.model}[/bold green]\n")
321
- return
322
-
323
- if cmd_name == ":doctor":
324
- doc = DatabaseDoctor(self.connector, self.llm)
325
- diag = doc.diagnose()
326
- self._append_text(f"\n[cyan]❯ {user_input}[/cyan]\n" + self._render_rich_to_ansi(diag.to_markdown()))
327
- return
328
-
329
- if cmd_name == ":history":
330
- self._append_text(f"\n[cyan]❯ {user_input}[/cyan]")
331
- render_session_history(self.query_history)
332
- return
333
-
334
- # Other commands
335
- self._append_text(f"\n[cyan]❯ {user_input}[/cyan]\n[dim]Command executed.[/dim]")
336
- return
337
-
338
- # 2. Natural Language or Direct SQL Query
339
- self._append_text(f"\n[bold blue]💬 You:[/bold blue] [bold white]{user_input}[/bold white]")
340
-
341
- # Run via pipeline
342
- t0 = time.time()
343
- self.pipeline.model = self.model
344
- self.pipeline.mode = self.mode
345
- result = self.pipeline.run(user_prompt=user_input)
346
-
347
- self.last_result = result
348
- self.query_history.append(result)
349
-
350
- timing_str = f"⚡ {result.execution_time_ms / 1000:.2f}s" if result.execution_time_ms > 0 else ""
351
-
352
- # Chat response
353
- if result.is_chat and result.message:
354
- self._append_text(f"\n[bold cyan]🤖 DB-Agent ({self.model})[/bold cyan] [dim]{timing_str}[/dim]\n" + result.message)
355
- return
356
-
357
- # Error
358
- if result.error:
359
- self._append_text(f"\n[bold red]❌ Error ({timing_str}):[/bold red] {result.friendly_error or result.error}")
360
- return
361
-
362
- # Executed SQL results
363
- if result.was_executed:
364
- blocks = []
365
- if result.is_direct_sql and result.retries == 0:
366
- header_line = f"[bold cyan]🤖 DB-Agent[/bold cyan] [dim](Direct SQL • 0 tokens · {timing_str})[/dim]"
367
- else:
368
- header_line = f"[bold cyan]🤖 DB-Agent ({self.model})[/bold cyan] [dim]{timing_str}[/dim]"
369
- blocks.append(header_line)
370
-
371
- if result.sql:
372
- syntax = Syntax(result.sql.strip(), "sql", theme="monokai", line_numbers=False)
373
- blocks.append(self._render_rich_to_ansi(Panel(syntax, title="[dim]SQL Statement[/dim]", border_style="dim")))
374
-
375
- if result.retries > 0:
376
- blocks.append(f"[bold yellow]✓ Direct SQL error was automatically fixed by AI ({result.retries} retry)[/bold yellow]")
377
-
378
- if result.has_results:
379
- t = Table(box=None, padding=(0, 2), show_header=True, header_style="bold cyan")
380
- for col in result.columns:
381
- t.add_column(str(col), style="white")
382
- for row in result.rows[:50]:
383
- t.add_row(*[str(row.get(col, "")) for col in result.columns])
384
- blocks.append(self._render_rich_to_ansi(t))
385
- if len(result.rows) > 50:
386
- blocks.append(f"[dim]... and {len(result.rows) - 50} more rows[/dim]")
387
-
388
- self._append_text("\n".join(blocks))
389
-
390
- def run(self) -> None:
391
- """Start the full-screen interactive TUI."""
392
- self.app.run()
File without changes
File without changes