sqlsaber 0.19.0__tar.gz → 0.21.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.
Potentially problematic release.
This version of sqlsaber might be problematic. Click here for more details.
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/PKG-INFO +1 -1
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/changelog.md +15 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/guides/getting-started.mdx +17 -9
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/guides/queries.mdx +6 -3
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/index.mdx +2 -2
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/pyproject.toml +1 -1
- sqlsaber-0.21.0/sqlsaber.gif +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/agents/pydantic_ai_agent.py +5 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/commands.py +11 -41
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/database.py +3 -1
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/display.py +180 -13
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/interactive.py +34 -27
- sqlsaber-0.21.0/src/sqlsaber/cli/streaming.py +176 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/threads.py +14 -7
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_cli/test_threads.py +12 -8
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/uv.lock +902 -902
- sqlsaber-0.19.0/sqlsaber.gif +0 -0
- sqlsaber-0.19.0/src/sqlsaber/cli/streaming.py +0 -133
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/.github/workflows/claude-code-review.yml +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/.github/workflows/claude.yml +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/.github/workflows/deploy-docs.yml +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/.github/workflows/publish.yml +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/.github/workflows/test.yml +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/.gitignore +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/.python-version +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/AGENT.md +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/CLAUDE.md +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/LICENSE +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/README.md +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/.gitignore +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/.vscode/extensions.json +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/.vscode/launch.json +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/CLAUDE.md +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/astro.config.mjs +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/package-lock.json +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/package.json +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/public/CNAME +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/public/favicon.svg +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/assets/sqlsaber-hero.svg +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/guides/authentication.mdx +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/guides/database-setup.mdx +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/guides/memory.mdx +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/guides/models.mdx +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/guides/threads.md +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/installation.mdx +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content/docs/reference/commands.md +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/content.config.ts +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/src/styles/global.css +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/docs/tsconfig.json +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/legislators.db +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/pytest.ini +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/sqlsaber.svg +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/__main__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/agents/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/agents/base.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/agents/mcp.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/auth.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/completers.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/memory.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/cli/models.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/api_keys.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/auth.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/database.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/oauth_flow.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/oauth_tokens.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/providers.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/config/settings.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/database/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/database/connection.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/database/resolver.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/database/schema.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/mcp/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/mcp/mcp.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/memory/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/memory/manager.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/memory/storage.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/threads/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/threads/storage.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/tools/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/tools/base.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/tools/enums.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/tools/instructions.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/tools/registry.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/src/sqlsaber/tools/sql_tools.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/conftest.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_cli/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_cli/test_auth_reset.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_cli/test_commands.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_config/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_config/test_database.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_config/test_oauth.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_config/test_providers.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_config/test_settings.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_database/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_database/test_connection.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_database_resolver.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_threads_storage.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_tools/__init__.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_tools/test_base.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_tools/test_instructions.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_tools/test_registry.py +0 -0
- {sqlsaber-0.19.0 → sqlsaber-0.21.0}/tests/test_tools/test_sql_tools.py +0 -0
|
@@ -7,8 +7,23 @@ All notable changes to SQLsaber will be documented here.
|
|
|
7
7
|
|
|
8
8
|
### Unreleased
|
|
9
9
|
|
|
10
|
+
### v0.21.0 - 2025-09-15
|
|
11
|
+
|
|
12
|
+
#### Changed
|
|
13
|
+
|
|
14
|
+
- Use Responses API for OpenAI models
|
|
15
|
+
- Stream markdown while streaming response from models
|
|
16
|
+
|
|
10
17
|
---
|
|
11
18
|
|
|
19
|
+
### v0.20.0 - 2025-09-10
|
|
20
|
+
|
|
21
|
+
#### Fixed
|
|
22
|
+
|
|
23
|
+
- Subcommand help visibility for nested apps
|
|
24
|
+
- Removed mouse support for user prompt
|
|
25
|
+
- Enabling mouse support disables terminal scrolling when user prompt is focused
|
|
26
|
+
|
|
12
27
|
### v0.19.0 - 2025-09-09
|
|
13
28
|
|
|
14
29
|
#### Added
|
|
@@ -3,8 +3,7 @@ title: Getting Started
|
|
|
3
3
|
description: Your first steps with SQLsaber
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
import { Aside } from
|
|
7
|
-
|
|
6
|
+
import { Aside } from "@astrojs/starlight/components";
|
|
8
7
|
|
|
9
8
|
Welcome to SQLsaber! This guide will walk you through setting up SQLsaber and running your first query.
|
|
10
9
|
|
|
@@ -27,15 +26,18 @@ saber auth setup
|
|
|
27
26
|
```
|
|
28
27
|
|
|
29
28
|
This will prompt you to:
|
|
29
|
+
|
|
30
30
|
- Choose a provider (We recommend Anthropic)
|
|
31
31
|
- Enter your API key
|
|
32
32
|
|
|
33
33
|
<Aside type="tip">
|
|
34
|
-
If you have a Claude Pro or Max subscription, you can use it with SQLsaber. It
|
|
34
|
+
If you have a Claude Pro or Max subscription, you can use it with SQLsaber. It
|
|
35
|
+
is one of the most cost-effective options.
|
|
35
36
|
</Aside>
|
|
36
37
|
|
|
37
38
|
<Aside type="note">
|
|
38
|
-
|
|
39
|
+
If you choose a provider other than Anthropic, please select the appropriate
|
|
40
|
+
model for the provider by running `saber models set`.
|
|
39
41
|
</Aside>
|
|
40
42
|
|
|
41
43
|
#### 2. Add Your Database
|
|
@@ -47,10 +49,11 @@ saber db add my-database
|
|
|
47
49
|
```
|
|
48
50
|
|
|
49
51
|
This interactive command will ask you for:
|
|
52
|
+
|
|
50
53
|
- Database type
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
- PostgreSQL
|
|
55
|
+
- MySQL
|
|
56
|
+
- SQLite
|
|
54
57
|
- Connection details (host, port, database name, username)
|
|
55
58
|
- SSL configuration (if needed)
|
|
56
59
|
|
|
@@ -63,6 +66,7 @@ curl -L -o legislators.db https://github.com/SarthakJariwala/sqlsaber/raw/refs/h
|
|
|
63
66
|
# Add it to SQLsaber
|
|
64
67
|
saber db add legislators --type sqlite --database ./legislators.db
|
|
65
68
|
```
|
|
69
|
+
|
|
66
70
|
</Aside>
|
|
67
71
|
|
|
68
72
|
#### 3. Your First Query
|
|
@@ -82,14 +86,15 @@ Try asking a question:
|
|
|
82
86
|
```
|
|
83
87
|
|
|
84
88
|
SQLsaber will:
|
|
89
|
+
|
|
85
90
|
1. Analyze your database schema
|
|
86
91
|
2. Generate appropriate SQL
|
|
87
92
|
3. Execute the query
|
|
88
93
|
4. Present the results and analysis
|
|
89
94
|
|
|
90
95
|
<script
|
|
91
|
-
src="https://asciinema.org/a/
|
|
92
|
-
id="asciicast-
|
|
96
|
+
src="https://asciinema.org/a/739399.js"
|
|
97
|
+
id="asciicast-739399"
|
|
93
98
|
async="true"
|
|
94
99
|
></script>
|
|
95
100
|
|
|
@@ -142,18 +147,21 @@ Now that you have SQLsaber running, explore these features:
|
|
|
142
147
|
#### Common Issues
|
|
143
148
|
|
|
144
149
|
**"No database connections configured"**
|
|
150
|
+
|
|
145
151
|
```bash
|
|
146
152
|
saber db list # Check configured databases
|
|
147
153
|
saber db add my-db # Add a new database
|
|
148
154
|
```
|
|
149
155
|
|
|
150
156
|
**"Authentication not configured"**
|
|
157
|
+
|
|
151
158
|
```bash
|
|
152
159
|
saber auth status # Check auth status
|
|
153
160
|
saber auth setup # Set up authentication
|
|
154
161
|
```
|
|
155
162
|
|
|
156
163
|
**"Database connection failed"**
|
|
164
|
+
|
|
157
165
|
```bash
|
|
158
166
|
saber db test my-database # Test the connection
|
|
159
167
|
```
|
|
@@ -3,7 +3,7 @@ title: Running Queries
|
|
|
3
3
|
description: Learn different ways to query your data with SQLsaber
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
import { Aside } from
|
|
6
|
+
import { Aside } from "@astrojs/starlight/components";
|
|
7
7
|
|
|
8
8
|
SQLsaber offers multiple ways to query your database using natural language. This guide covers all the different query modes and their use cases.
|
|
9
9
|
|
|
@@ -16,8 +16,8 @@ saber
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
<script
|
|
19
|
-
src="https://asciinema.org/a/
|
|
20
|
-
id="asciicast-
|
|
19
|
+
src="https://asciinema.org/a/739399.js"
|
|
20
|
+
id="asciicast-739399"
|
|
21
21
|
async="true"
|
|
22
22
|
></script>
|
|
23
23
|
|
|
@@ -30,6 +30,7 @@ saber "How many orders were placed last week?"
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Useful for:
|
|
33
|
+
|
|
33
34
|
- Shell scripting
|
|
34
35
|
- Quick checks
|
|
35
36
|
- Automated reporting
|
|
@@ -50,6 +51,7 @@ curl -s https://api.example.com/query | saber
|
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
Useful for:
|
|
54
|
+
|
|
53
55
|
- Shell scripting
|
|
54
56
|
- Batch processing
|
|
55
57
|
- Integration with other tools
|
|
@@ -82,6 +84,7 @@ Type `@` followed by a table name to get autocomplete suggestions:
|
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
Supports schema-aware completions:
|
|
87
|
+
|
|
85
88
|
```
|
|
86
89
|
> @pub[TAB] → @public.customers
|
|
87
90
|
```
|
|
Binary file
|
|
@@ -8,6 +8,7 @@ import httpx
|
|
|
8
8
|
from pydantic_ai import Agent, RunContext
|
|
9
9
|
from pydantic_ai.models.anthropic import AnthropicModel
|
|
10
10
|
from pydantic_ai.models.google import GoogleModel
|
|
11
|
+
from pydantic_ai.models.openai import OpenAIResponsesModel
|
|
11
12
|
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
12
13
|
from pydantic_ai.providers.google import GoogleProvider
|
|
13
14
|
|
|
@@ -79,6 +80,10 @@ def build_sqlsaber_agent(
|
|
|
79
80
|
provider_obj = AnthropicProvider(api_key="placeholder", http_client=http_client)
|
|
80
81
|
model_obj = AnthropicModel(model_name_only, provider=provider_obj)
|
|
81
82
|
agent = Agent(model_obj, name="sqlsaber")
|
|
83
|
+
elif provider == "openai":
|
|
84
|
+
# Use OpenAI Responses Model for structured output capabilities
|
|
85
|
+
model_obj = OpenAIResponsesModel(model_name_only)
|
|
86
|
+
agent = Agent(model_obj, name="sqlsaber")
|
|
82
87
|
else:
|
|
83
88
|
agent = Agent(cfg.model_name, name="sqlsaber")
|
|
84
89
|
|
|
@@ -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()
|
|
@@ -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,12 +1,167 @@
|
|
|
1
|
-
"""Display utilities for the CLI interface.
|
|
1
|
+
"""Display utilities for the CLI interface.
|
|
2
|
+
|
|
3
|
+
All rendering occurs on the event loop thread.
|
|
4
|
+
Streaming segments use Live Markdown; transient status and SQL blocks are also
|
|
5
|
+
rendered with Live.
|
|
6
|
+
"""
|
|
2
7
|
|
|
3
8
|
import json
|
|
9
|
+
from typing import Sequence, Type
|
|
4
10
|
|
|
5
|
-
from
|
|
6
|
-
from rich.
|
|
11
|
+
from pydantic_ai.messages import ModelResponsePart, TextPart
|
|
12
|
+
from rich.columns import Columns
|
|
13
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
14
|
+
from rich.live import Live
|
|
15
|
+
from rich.markdown import CodeBlock, Markdown
|
|
7
16
|
from rich.panel import Panel
|
|
17
|
+
from rich.spinner import Spinner
|
|
8
18
|
from rich.syntax import Syntax
|
|
9
19
|
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _SimpleCodeBlock(CodeBlock):
|
|
24
|
+
def __rich_console__(
|
|
25
|
+
self, console: Console, options: ConsoleOptions
|
|
26
|
+
) -> RenderResult:
|
|
27
|
+
code = str(self.text).rstrip()
|
|
28
|
+
yield Syntax(
|
|
29
|
+
code,
|
|
30
|
+
self.lexer_name,
|
|
31
|
+
theme=self.theme,
|
|
32
|
+
background_color="default",
|
|
33
|
+
word_wrap=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LiveMarkdownRenderer:
|
|
38
|
+
"""Handles Live markdown rendering with segment separation.
|
|
39
|
+
|
|
40
|
+
Supports different segment kinds: 'assistant', 'thinking', 'sql'.
|
|
41
|
+
Adds visible paragraph breaks between segments and renders code fences
|
|
42
|
+
with nicer formatting.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_patched_fences = False
|
|
46
|
+
|
|
47
|
+
def __init__(self, console: Console):
|
|
48
|
+
self.console = console
|
|
49
|
+
self._live: Live | None = None
|
|
50
|
+
self._status_live: Live | None = None
|
|
51
|
+
self._buffer: str = ""
|
|
52
|
+
self._current_kind: Type[ModelResponsePart] | None = None
|
|
53
|
+
|
|
54
|
+
def prepare_code_blocks(self) -> None:
|
|
55
|
+
"""Patch rich Markdown fence rendering once for nicer code blocks."""
|
|
56
|
+
if LiveMarkdownRenderer._patched_fences:
|
|
57
|
+
return
|
|
58
|
+
# Guard with class check to avoid re-patching if already applied
|
|
59
|
+
if Markdown.elements.get("fence") is not _SimpleCodeBlock:
|
|
60
|
+
Markdown.elements["fence"] = _SimpleCodeBlock
|
|
61
|
+
LiveMarkdownRenderer._patched_fences = True
|
|
62
|
+
|
|
63
|
+
def ensure_segment(self, kind: Type[ModelResponsePart]) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Ensure a markdown Live segment is active for the given kind.
|
|
66
|
+
|
|
67
|
+
When switching kinds, end the previous segment and add a paragraph break.
|
|
68
|
+
"""
|
|
69
|
+
# If a transient status is showing, clear it first (no paragraph break)
|
|
70
|
+
if self._status_live is not None:
|
|
71
|
+
self.end_status()
|
|
72
|
+
if self._live is not None and self._current_kind == kind:
|
|
73
|
+
return
|
|
74
|
+
if self._live is not None:
|
|
75
|
+
self.end()
|
|
76
|
+
self.paragraph_break()
|
|
77
|
+
|
|
78
|
+
self._start()
|
|
79
|
+
self._current_kind = kind
|
|
80
|
+
|
|
81
|
+
def append(self, text: str | None) -> None:
|
|
82
|
+
"""Append text to the current markdown segment and refresh."""
|
|
83
|
+
if not text:
|
|
84
|
+
return
|
|
85
|
+
if self._live is None:
|
|
86
|
+
# default to assistant if no segment was ensured
|
|
87
|
+
self.ensure_segment(TextPart)
|
|
88
|
+
|
|
89
|
+
self._buffer += text
|
|
90
|
+
self._live.update(Markdown(self._buffer))
|
|
91
|
+
|
|
92
|
+
def end(self) -> None:
|
|
93
|
+
"""Finalize and stop the current Live segment, if any."""
|
|
94
|
+
if self._live is None:
|
|
95
|
+
return
|
|
96
|
+
if self._buffer:
|
|
97
|
+
self._live.update(Markdown(self._buffer))
|
|
98
|
+
self._live.stop()
|
|
99
|
+
self._live = None
|
|
100
|
+
self._buffer = ""
|
|
101
|
+
self._current_kind = None
|
|
102
|
+
|
|
103
|
+
def end_if_active(self) -> None:
|
|
104
|
+
self.end()
|
|
105
|
+
|
|
106
|
+
def paragraph_break(self) -> None:
|
|
107
|
+
self.console.print()
|
|
108
|
+
|
|
109
|
+
def start_sql_block(self, sql: str) -> None:
|
|
110
|
+
"""Render a SQL block using a transient Live markdown segment."""
|
|
111
|
+
if not sql or not isinstance(sql, str) or not sql.strip():
|
|
112
|
+
return
|
|
113
|
+
# Separate from surrounding content
|
|
114
|
+
self.end_if_active()
|
|
115
|
+
self.paragraph_break()
|
|
116
|
+
self._buffer = f"```sql\n{sql}\n```"
|
|
117
|
+
# Use context manager to auto-stop and persist final render
|
|
118
|
+
with Live(
|
|
119
|
+
Markdown(self._buffer),
|
|
120
|
+
console=self.console,
|
|
121
|
+
vertical_overflow="visible",
|
|
122
|
+
refresh_per_second=12,
|
|
123
|
+
):
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def start_status(self, message: str = "Crunching data...") -> None:
|
|
127
|
+
"""Show a transient status line with a spinner until streaming starts."""
|
|
128
|
+
if self._status_live is not None:
|
|
129
|
+
# Update existing status text
|
|
130
|
+
self._status_live.update(self._status_renderable(message))
|
|
131
|
+
return
|
|
132
|
+
live = Live(
|
|
133
|
+
self._status_renderable(message),
|
|
134
|
+
console=self.console,
|
|
135
|
+
transient=True, # disappear when stopped
|
|
136
|
+
refresh_per_second=12,
|
|
137
|
+
)
|
|
138
|
+
self._status_live = live
|
|
139
|
+
live.start()
|
|
140
|
+
|
|
141
|
+
def end_status(self) -> None:
|
|
142
|
+
live = self._status_live
|
|
143
|
+
if live is None:
|
|
144
|
+
return
|
|
145
|
+
live.stop()
|
|
146
|
+
self._status_live = None
|
|
147
|
+
|
|
148
|
+
def _status_renderable(self, message: str):
|
|
149
|
+
spinner = Spinner("dots", style="yellow")
|
|
150
|
+
text = Text(f" {message}", style="yellow")
|
|
151
|
+
return Columns([spinner, text], expand=False)
|
|
152
|
+
|
|
153
|
+
def _start(self, initial_markdown: str = "") -> None:
|
|
154
|
+
if self._live is not None:
|
|
155
|
+
self.end()
|
|
156
|
+
self._buffer = initial_markdown or ""
|
|
157
|
+
live = Live(
|
|
158
|
+
Markdown(self._buffer),
|
|
159
|
+
console=self.console,
|
|
160
|
+
vertical_overflow="visible",
|
|
161
|
+
refresh_per_second=12,
|
|
162
|
+
)
|
|
163
|
+
self._live = live
|
|
164
|
+
live.start()
|
|
10
165
|
|
|
11
166
|
|
|
12
167
|
class DisplayManager:
|
|
@@ -14,10 +169,11 @@ class DisplayManager:
|
|
|
14
169
|
|
|
15
170
|
def __init__(self, console: Console):
|
|
16
171
|
self.console = console
|
|
172
|
+
self.live = LiveMarkdownRenderer(console)
|
|
17
173
|
|
|
18
174
|
def _create_table(
|
|
19
175
|
self,
|
|
20
|
-
columns:
|
|
176
|
+
columns: Sequence[str | dict[str, str]],
|
|
21
177
|
header_style: str = "bold blue",
|
|
22
178
|
title: str | None = None,
|
|
23
179
|
) -> Table:
|
|
@@ -34,17 +190,24 @@ class DisplayManager:
|
|
|
34
190
|
|
|
35
191
|
def show_tool_executing(self, tool_name: str, tool_input: dict):
|
|
36
192
|
"""Display tool execution details."""
|
|
37
|
-
|
|
193
|
+
# Normalized leading blank line before tool headers
|
|
194
|
+
self.show_newline()
|
|
38
195
|
if tool_name == "list_tables":
|
|
39
|
-
self.console.print(
|
|
196
|
+
self.console.print(
|
|
197
|
+
"[dim bold]:gear: Discovering available tables[/dim bold]"
|
|
198
|
+
)
|
|
40
199
|
elif tool_name == "introspect_schema":
|
|
41
200
|
pattern = tool_input.get("table_pattern", "all tables")
|
|
42
|
-
self.console.print(
|
|
201
|
+
self.console.print(
|
|
202
|
+
f"[dim bold]:gear: Examining schema for: {pattern}[/dim bold]"
|
|
203
|
+
)
|
|
43
204
|
elif tool_name == "execute_sql":
|
|
205
|
+
# For streaming, we render SQL via LiveMarkdownRenderer; keep Syntax
|
|
206
|
+
# rendering for threads show/resume. Controlled by include_sql flag.
|
|
44
207
|
query = tool_input.get("query", "")
|
|
45
|
-
self.console.print("
|
|
208
|
+
self.console.print("[dim bold]:gear: Executing SQL:[/dim bold]")
|
|
46
209
|
self.show_newline()
|
|
47
|
-
syntax = Syntax(query, "sql")
|
|
210
|
+
syntax = Syntax(query, "sql", background_color="default", word_wrap=True)
|
|
48
211
|
self.console.print(syntax)
|
|
49
212
|
|
|
50
213
|
def show_text_stream(self, text: str):
|
|
@@ -99,10 +262,12 @@ class DisplayManager:
|
|
|
99
262
|
"""Display a newline for spacing."""
|
|
100
263
|
self.console.print()
|
|
101
264
|
|
|
102
|
-
def show_table_list(self, tables_data: str):
|
|
265
|
+
def show_table_list(self, tables_data: str | dict):
|
|
103
266
|
"""Display the results from list_tables tool."""
|
|
104
267
|
try:
|
|
105
|
-
data =
|
|
268
|
+
data = (
|
|
269
|
+
json.loads(tables_data) if isinstance(tables_data, str) else tables_data
|
|
270
|
+
)
|
|
106
271
|
|
|
107
272
|
# Handle error case
|
|
108
273
|
if "error" in data:
|
|
@@ -143,10 +308,12 @@ class DisplayManager:
|
|
|
143
308
|
except Exception as e:
|
|
144
309
|
self.show_error(f"Error displaying table list: {str(e)}")
|
|
145
310
|
|
|
146
|
-
def show_schema_info(self, schema_data: str):
|
|
311
|
+
def show_schema_info(self, schema_data: str | dict):
|
|
147
312
|
"""Display the results from introspect_schema tool."""
|
|
148
313
|
try:
|
|
149
|
-
data =
|
|
314
|
+
data = (
|
|
315
|
+
json.loads(schema_data) if isinstance(schema_data, str) else schema_data
|
|
316
|
+
)
|
|
150
317
|
|
|
151
318
|
# Handle error case
|
|
152
319
|
if "error" in data:
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from textwrap import dedent
|
|
5
6
|
|
|
6
7
|
import platformdirs
|
|
7
8
|
from prompt_toolkit import PromptSession
|
|
8
9
|
from prompt_toolkit.history import FileHistory
|
|
10
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
9
11
|
from prompt_toolkit.styles import Style
|
|
10
12
|
from pydantic_ai import Agent
|
|
11
13
|
from rich.console import Console
|
|
14
|
+
from rich.markdown import Markdown
|
|
12
15
|
from rich.panel import Panel
|
|
13
16
|
|
|
14
17
|
from sqlsaber.cli.completers import (
|
|
@@ -101,14 +104,14 @@ class InteractiveSession:
|
|
|
101
104
|
)
|
|
102
105
|
)
|
|
103
106
|
self.console.print(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
Markdown(
|
|
108
|
+
dedent("""
|
|
109
|
+
- Use `/` for slash commands
|
|
110
|
+
- Type `@` to get table name completions
|
|
111
|
+
- Start message with `#` to add something to agent's memory
|
|
112
|
+
- Use `Ctrl+C` to interrupt and `Ctrl+D` to exit
|
|
113
|
+
""")
|
|
114
|
+
)
|
|
112
115
|
)
|
|
113
116
|
|
|
114
117
|
self.console.print(
|
|
@@ -118,6 +121,15 @@ class InteractiveSession:
|
|
|
118
121
|
if self._thread_id:
|
|
119
122
|
self.console.print(f"[dim]Resuming thread:[/dim] {self._thread_id}\n")
|
|
120
123
|
|
|
124
|
+
async def _end_thread_and_display_resume_hint(self):
|
|
125
|
+
"""End thread and display command to resume thread"""
|
|
126
|
+
# Print resume hint if there is an active thread
|
|
127
|
+
if self._thread_id:
|
|
128
|
+
await self._threads.end_thread(self._thread_id)
|
|
129
|
+
self.console.print(
|
|
130
|
+
f"[dim]You can continue this thread using:[/dim] saber threads resume {self._thread_id}"
|
|
131
|
+
)
|
|
132
|
+
|
|
121
133
|
async def _update_table_cache(self):
|
|
122
134
|
"""Update the table completer cache with fresh data."""
|
|
123
135
|
try:
|
|
@@ -215,33 +227,27 @@ class InteractiveSession:
|
|
|
215
227
|
|
|
216
228
|
while True:
|
|
217
229
|
try:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
+
with patch_stdout():
|
|
231
|
+
user_query = await session.prompt_async(
|
|
232
|
+
"",
|
|
233
|
+
multiline=True,
|
|
234
|
+
completer=CompositeCompleter(
|
|
235
|
+
SlashCommandCompleter(), self.table_completer
|
|
236
|
+
),
|
|
237
|
+
show_frame=True,
|
|
238
|
+
bottom_toolbar=bottom_toolbar,
|
|
239
|
+
style=style,
|
|
240
|
+
)
|
|
230
241
|
|
|
231
242
|
if not user_query:
|
|
232
243
|
continue
|
|
233
244
|
|
|
234
245
|
if (
|
|
235
|
-
user_query in ["/exit", "/quit"]
|
|
246
|
+
user_query in ["/exit", "/quit", "exit", "quit"]
|
|
236
247
|
or user_query.startswith("/exit")
|
|
237
248
|
or user_query.startswith("/quit")
|
|
238
249
|
):
|
|
239
|
-
|
|
240
|
-
if self._thread_id:
|
|
241
|
-
await self._threads.end_thread(self._thread_id)
|
|
242
|
-
self.console.print(
|
|
243
|
-
f"[dim]You can continue this thread using:[/dim] saber threads resume {self._thread_id}"
|
|
244
|
-
)
|
|
250
|
+
await self._end_thread_and_display_resume_hint()
|
|
245
251
|
break
|
|
246
252
|
|
|
247
253
|
if user_query == "/clear":
|
|
@@ -313,6 +319,7 @@ class InteractiveSession:
|
|
|
313
319
|
)
|
|
314
320
|
except EOFError:
|
|
315
321
|
# Exit when Ctrl+D is pressed
|
|
322
|
+
await self._end_thread_and_display_resume_hint()
|
|
316
323
|
break
|
|
317
324
|
except Exception as e:
|
|
318
325
|
self.console.print(f"[bold red]Error:[/bold red] {str(e)}")
|