sqlsaber 0.18.0__py3-none-any.whl → 0.20.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sqlsaber might be problematic. Click here for more details.

sqlsaber/cli/commands.py CHANGED
@@ -7,6 +7,12 @@ from typing import Annotated
7
7
  import cyclopts
8
8
  from rich.console import Console
9
9
 
10
+ from sqlsaber.cli.auth import create_auth_app
11
+ from sqlsaber.cli.database import create_db_app
12
+ from sqlsaber.cli.memory import create_memory_app
13
+ from sqlsaber.cli.models import create_models_app
14
+ from sqlsaber.cli.threads import create_threads_app
15
+
10
16
  # Lazy imports - only import what's needed for CLI parsing
11
17
  from sqlsaber.config.database import DatabaseConfigManager
12
18
 
@@ -24,6 +30,11 @@ app = cyclopts.App(
24
30
  help="SQLsaber - Open-source agentic SQL assistant for your database",
25
31
  )
26
32
 
33
+ app.command(create_auth_app(), name="auth")
34
+ app.command(create_db_app(), name="db")
35
+ app.command(create_memory_app(), name="memory")
36
+ app.command(create_models_app(), name="models")
37
+ app.command(create_threads_app(), name="threads")
27
38
 
28
39
  console = Console()
29
40
  config_manager = DatabaseConfigManager()
@@ -195,47 +206,6 @@ def query(
195
206
  sys.exit(e.exit_code)
196
207
 
197
208
 
198
- # Use lazy imports for fast CLI startup time
199
- @app.command(name="auth")
200
- def auth(*args, **kwargs):
201
- """Manage authentication configuration."""
202
- from sqlsaber.cli.auth import create_auth_app
203
-
204
- return create_auth_app()(*args, **kwargs)
205
-
206
-
207
- @app.command(name="db")
208
- def db(*args, **kwargs):
209
- """Manage database connections."""
210
- from sqlsaber.cli.database import create_db_app
211
-
212
- return create_db_app()(*args, **kwargs)
213
-
214
-
215
- @app.command(name="memory")
216
- def memory(*args, **kwargs):
217
- """Manage database-specific memories."""
218
- from sqlsaber.cli.memory import create_memory_app
219
-
220
- return create_memory_app()(*args, **kwargs)
221
-
222
-
223
- @app.command(name="models")
224
- def models(*args, **kwargs):
225
- """Select and manage models."""
226
- from sqlsaber.cli.models import create_models_app
227
-
228
- return create_models_app()(*args, **kwargs)
229
-
230
-
231
- @app.command(name="threads")
232
- def threads(*args, **kwargs):
233
- """Manage SQLsaber threads."""
234
- from sqlsaber.cli.threads import create_threads_app
235
-
236
- return create_threads_app()(*args, **kwargs)
237
-
238
-
239
209
  def main():
240
210
  """Entry point for the CLI application."""
241
211
  app()
sqlsaber/cli/database.py CHANGED
@@ -12,7 +12,6 @@ from rich.console import Console
12
12
  from rich.table import Table
13
13
 
14
14
  from sqlsaber.config.database import DatabaseConfig, DatabaseConfigManager
15
- from sqlsaber.database.connection import DatabaseConnection
16
15
 
17
16
  # Global instances for CLI commands
18
17
  console = Console()
@@ -343,6 +342,9 @@ def test(
343
342
  """Test a database connection."""
344
343
 
345
344
  async def test_connection():
345
+ # Lazy import to keep CLI startup fast
346
+ from sqlsaber.database.connection import DatabaseConnection
347
+
346
348
  if name:
347
349
  db_config = config_manager.get_database(name)
348
350
  if not db_config:
@@ -1,8 +1,13 @@
1
1
  """Interactive mode handling for the CLI."""
2
2
 
3
3
  import asyncio
4
+ from pathlib import Path
4
5
 
5
- import questionary
6
+ import platformdirs
7
+ from prompt_toolkit import PromptSession
8
+ from prompt_toolkit.history import FileHistory
9
+ from prompt_toolkit.patch_stdout import patch_stdout
10
+ from prompt_toolkit.styles import Style
6
11
  from pydantic_ai import Agent
7
12
  from rich.console import Console
8
13
  from rich.panel import Panel
@@ -24,6 +29,23 @@ from sqlsaber.database.schema import SchemaManager
24
29
  from sqlsaber.threads import ThreadStorage
25
30
 
26
31
 
32
+ def bottom_toolbar():
33
+ return [
34
+ (
35
+ "class:bottom-toolbar",
36
+ " Use 'Esc-Enter' or 'Meta-Enter' to submit.",
37
+ )
38
+ ]
39
+
40
+
41
+ style = Style.from_dict(
42
+ {
43
+ "frame.border": "#ebbcba",
44
+ "bottom-toolbar": "#ebbcba bg:#21202e",
45
+ }
46
+ )
47
+
48
+
27
49
  class InteractiveSession:
28
50
  """Manages interactive CLI sessions."""
29
51
 
@@ -82,7 +104,7 @@ class InteractiveSession:
82
104
  self.console.print(
83
105
  "\n",
84
106
  "[dim] > Use '/clear' to reset conversation",
85
- "[dim] > Use '/exit' or '/quit' to leave[/dim]",
107
+ "[dim] > Use 'Ctrl+D', '/exit' or '/quit' to leave[/dim]",
86
108
  "[dim] > Use 'Ctrl+C' to interrupt and return to prompt\n\n",
87
109
  "[dim] > Start message with '#' to add something to agent's memory for this database",
88
110
  "[dim] > Type '@' to get table name completions",
@@ -97,6 +119,15 @@ class InteractiveSession:
97
119
  if self._thread_id:
98
120
  self.console.print(f"[dim]Resuming thread:[/dim] {self._thread_id}\n")
99
121
 
122
+ async def _end_thread_and_display_resume_hint(self):
123
+ """End thread and display command to resume thread"""
124
+ # Print resume hint if there is an active thread
125
+ if self._thread_id:
126
+ await self._threads.end_thread(self._thread_id)
127
+ self.console.print(
128
+ f"[dim]You can continue this thread using:[/dim] saber threads resume {self._thread_id}"
129
+ )
130
+
100
131
  async def _update_table_cache(self):
101
132
  """Update the table completer cache with fresh data."""
102
133
  try:
@@ -137,7 +168,10 @@ class InteractiveSession:
137
168
  # Create the query task
138
169
  query_task = asyncio.create_task(
139
170
  self.streaming_handler.execute_streaming_query(
140
- user_query, self.agent, self.cancellation_token, self.message_history
171
+ user_query,
172
+ self.agent,
173
+ self.cancellation_token,
174
+ self.message_history,
141
175
  )
142
176
  )
143
177
  self.current_task = query_task
@@ -183,32 +217,35 @@ class InteractiveSession:
183
217
  # Initialize table cache
184
218
  await self._update_table_cache()
185
219
 
220
+ session = PromptSession(
221
+ history=FileHistory(
222
+ Path(platformdirs.user_config_dir("sqlsaber")) / "history"
223
+ )
224
+ )
225
+
186
226
  while True:
187
227
  try:
188
- user_query = await questionary.text(
189
- ">",
190
- qmark="",
191
- multiline=True,
192
- instruction="",
193
- completer=CompositeCompleter(
194
- SlashCommandCompleter(), self.table_completer
195
- ),
196
- ).ask_async()
228
+ with patch_stdout():
229
+ user_query = await session.prompt_async(
230
+ "",
231
+ multiline=True,
232
+ completer=CompositeCompleter(
233
+ SlashCommandCompleter(), self.table_completer
234
+ ),
235
+ show_frame=True,
236
+ bottom_toolbar=bottom_toolbar,
237
+ style=style,
238
+ )
197
239
 
198
240
  if not user_query:
199
241
  continue
200
242
 
201
243
  if (
202
- user_query in ["/exit", "/quit"]
244
+ user_query in ["/exit", "/quit", "exit", "quit"]
203
245
  or user_query.startswith("/exit")
204
246
  or user_query.startswith("/quit")
205
247
  ):
206
- # Print resume hint if there is an active thread
207
- if self._thread_id:
208
- await self._threads.end_thread(self._thread_id)
209
- self.console.print(
210
- f"[dim]You can continue this thread using:[/dim] saber threads resume {self._thread_id}"
211
- )
248
+ await self._end_thread_and_display_resume_hint()
212
249
  break
213
250
 
214
251
  if user_query == "/clear":
@@ -276,7 +313,11 @@ class InteractiveSession:
276
313
  self.console.print("\n[yellow]Query interrupted[/yellow]")
277
314
  else:
278
315
  self.console.print(
279
- "\n[yellow]Use '/exit' or '/quit' to leave.[/yellow]"
316
+ "\n[yellow]Press Ctrl+D to exit. Or use '/exit' or '/quit' slash command.[/yellow]"
280
317
  )
318
+ except EOFError:
319
+ # Exit when Ctrl+D is pressed
320
+ await self._end_thread_and_display_resume_hint()
321
+ break
281
322
  except Exception as e:
282
323
  self.console.print(f"[bold red]Error:[/bold red] {str(e)}")
sqlsaber/cli/threads.py CHANGED
@@ -12,17 +12,10 @@ from rich.markdown import Markdown
12
12
  from rich.panel import Panel
13
13
  from rich.table import Table
14
14
 
15
- from sqlsaber.agents import build_sqlsaber_agent
16
- from sqlsaber.cli.display import DisplayManager
17
- from sqlsaber.cli.interactive import InteractiveSession
18
- from sqlsaber.config.database import DatabaseConfigManager
19
- from sqlsaber.database.connection import DatabaseConnection
20
- from sqlsaber.database.resolver import DatabaseResolutionError, resolve_database
21
15
  from sqlsaber.threads import ThreadStorage
22
16
 
23
17
  # Globals consistent with other CLI modules
24
18
  console = Console()
25
- config_manager = DatabaseConfigManager()
26
19
 
27
20
 
28
21
  threads_app = cyclopts.App(
@@ -41,6 +34,9 @@ def _render_transcript(
41
34
  console: Console, all_msgs: list[ModelMessage], last_n: int | None = None
42
35
  ) -> None:
43
36
  """Render conversation turns from ModelMessage[] using DisplayManager."""
37
+ # Lazy import to avoid pulling UI helpers at startup
38
+ from sqlsaber.cli.display import DisplayManager
39
+
44
40
  dm = DisplayManager(console)
45
41
 
46
42
  # Locate indices of user prompts
@@ -237,6 +233,16 @@ def resume(
237
233
  store = ThreadStorage()
238
234
 
239
235
  async def _run() -> None:
236
+ # Lazy imports to avoid heavy modules at CLI startup
237
+ from sqlsaber.agents import build_sqlsaber_agent
238
+ from sqlsaber.cli.interactive import InteractiveSession
239
+ from sqlsaber.config.database import DatabaseConfigManager
240
+ from sqlsaber.database.connection import DatabaseConnection
241
+ from sqlsaber.database.resolver import (
242
+ DatabaseResolutionError,
243
+ resolve_database,
244
+ )
245
+
240
246
  thread = await store.get_thread(thread_id)
241
247
  if not thread:
242
248
  console.print(f"[red]Thread not found:[/red] {thread_id}")
@@ -248,6 +254,7 @@ def resume(
248
254
  )
249
255
  return
250
256
  try:
257
+ config_manager = DatabaseConfigManager()
251
258
  resolved = resolve_database(db_selector, config_manager)
252
259
  connection_string = resolved.connection_string
253
260
  db_name = resolved.name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.18.0
3
+ Version: 0.20.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -13,6 +13,7 @@ Requires-Dist: httpx>=0.28.1
13
13
  Requires-Dist: keyring>=25.6.0
14
14
  Requires-Dist: pandas>=2.0.0
15
15
  Requires-Dist: platformdirs>=4.0.0
16
+ Requires-Dist: prompt-toolkit>3.0.51
16
17
  Requires-Dist: pydantic-ai
17
18
  Requires-Dist: questionary>=2.1.0
18
19
  Requires-Dist: rich>=13.7.0
@@ -6,15 +6,15 @@ sqlsaber/agents/mcp.py,sha256=GcJTx7YDYH6aaxIADEIxSgcWAdWakUx395JIzVnf17U,768
6
6
  sqlsaber/agents/pydantic_ai_agent.py,sha256=dGdsgyxCZvfK-v-MH8KimKOr-xb2aSfSWY8CMcOUCT8,6795
7
7
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
8
8
  sqlsaber/cli/auth.py,sha256=jTsRgbmlGPlASSuIKmdjjwfqtKvjfKd_cTYxX0-QqaQ,7400
9
- sqlsaber/cli/commands.py,sha256=CmCqDC6KiE8JD6Vkpsry4lBQInCiS8TBeKKx3gdxZcM,8689
9
+ sqlsaber/cli/commands.py,sha256=mjLG9i1bXf0TEroxkIxq5O7Hhjufz3Ad72cyJz7vE1k,8128
10
10
  sqlsaber/cli/completers.py,sha256=HsUPjaZweLSeYCWkAcgMl8FylQ1xjWBWYTEL_9F6xfU,6430
11
- sqlsaber/cli/database.py,sha256=atwg3l8acQ3YTDuhq7vNrBN6tpOv0syz6V62KTF-Bh8,12910
11
+ sqlsaber/cli/database.py,sha256=JKtHSN-BFzBa14REf0phFVQB7d67m1M5FFaD8N6DdrY,12966
12
12
  sqlsaber/cli/display.py,sha256=wa7BjTBwXwqLT145Q1AEL0C28pQJTrvDN10mnFMjqsg,8554
13
- sqlsaber/cli/interactive.py,sha256=QqBjSsjtb6XoBVRyGS520cQrm7DvWC-obQ_EflcygbU,12051
13
+ sqlsaber/cli/interactive.py,sha256=suTZ-EvbaB21BsFsRc4MkjM89lZ2iJlYH4G1iYjW7PI,13213
14
14
  sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
15
15
  sqlsaber/cli/models.py,sha256=ZewtwGQwhd9b-yxBAPKePolvI1qQG-EkmeWAGMqtWNQ,8986
16
16
  sqlsaber/cli/streaming.py,sha256=WNqBYYbWtL5CNQkRg5YWhYpWKI8qz7JmqneB2DXTOHY,5259
17
- sqlsaber/cli/threads.py,sha256=xti7_kvh3loQfLb7_GC8wSULJ4Oj56jXY8GQp69CQCI,11111
17
+ sqlsaber/cli/threads.py,sha256=XUnLcCUe2wa_85IKdKmryqfiHTQu_IylET2Qo8oy1nk,11324
18
18
  sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
19
19
  sqlsaber/config/api_keys.py,sha256=RqWQCko1tY7sES7YOlexgBH5Hd5ne_kGXHdBDNqcV2U,3649
20
20
  sqlsaber/config/auth.py,sha256=b5qB2h1doXyO9Bn8z0CcL8LAR2jF431gGXBGKLgTmtQ,2756
@@ -40,8 +40,8 @@ sqlsaber/tools/enums.py,sha256=CH32mL-0k9ZA18911xLpNtsgpV6tB85TktMj6uqGz54,411
40
40
  sqlsaber/tools/instructions.py,sha256=X-x8maVkkyi16b6Tl0hcAFgjiYceZaSwyWTfmrvx8U8,9024
41
41
  sqlsaber/tools/registry.py,sha256=HWOQMsNIdL4XZS6TeNUyrL-5KoSDH6PHsWd3X66o-18,3211
42
42
  sqlsaber/tools/sql_tools.py,sha256=hM6tKqW5MDhFUt6MesoqhTUqIpq_5baIIDoN1MjDCXY,9647
43
- sqlsaber-0.18.0.dist-info/METADATA,sha256=HAXen2RBeZ4wIdxoLIMPKiNgOXJolWFqqXl5_cCOsJA,6141
44
- sqlsaber-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
- sqlsaber-0.18.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
46
- sqlsaber-0.18.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
47
- sqlsaber-0.18.0.dist-info/RECORD,,
43
+ sqlsaber-0.20.0.dist-info/METADATA,sha256=LJgfLWIWvI8ZgeLjZMLakKCmHn4EywL0MS9XeKLy63E,6178
44
+ sqlsaber-0.20.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ sqlsaber-0.20.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
46
+ sqlsaber-0.20.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
47
+ sqlsaber-0.20.0.dist-info/RECORD,,