db-chat-widget 0.1.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.
- db_chat_widget-0.1.0/.github/workflows/ci.yml +28 -0
- db_chat_widget-0.1.0/.gitignore +18 -0
- db_chat_widget-0.1.0/LICENSE +21 -0
- db_chat_widget-0.1.0/PKG-INFO +233 -0
- db_chat_widget-0.1.0/README.md +187 -0
- db_chat_widget-0.1.0/examples/demo_app.py +78 -0
- db_chat_widget-0.1.0/examples/embed_example.html +32 -0
- db_chat_widget-0.1.0/pyproject.toml +80 -0
- db_chat_widget-0.1.0/src/db_chat_widget/__init__.py +6 -0
- db_chat_widget-0.1.0/src/db_chat_widget/chat/__init__.py +0 -0
- db_chat_widget-0.1.0/src/db_chat_widget/chat/engine.py +99 -0
- db_chat_widget-0.1.0/src/db_chat_widget/cli.py +89 -0
- db_chat_widget-0.1.0/src/db_chat_widget/config.py +27 -0
- db_chat_widget-0.1.0/src/db_chat_widget/db/__init__.py +0 -0
- db_chat_widget-0.1.0/src/db_chat_widget/db/connection.py +24 -0
- db_chat_widget-0.1.0/src/db_chat_widget/db/executor.py +76 -0
- db_chat_widget-0.1.0/src/db_chat_widget/db/introspection.py +71 -0
- db_chat_widget-0.1.0/src/db_chat_widget/db/safety.py +60 -0
- db_chat_widget-0.1.0/src/db_chat_widget/llm/__init__.py +0 -0
- db_chat_widget-0.1.0/src/db_chat_widget/llm/anthropic_provider.py +30 -0
- db_chat_widget-0.1.0/src/db_chat_widget/llm/base.py +49 -0
- db_chat_widget-0.1.0/src/db_chat_widget/llm/factory.py +40 -0
- db_chat_widget-0.1.0/src/db_chat_widget/llm/groq_provider.py +32 -0
- db_chat_widget-0.1.0/src/db_chat_widget/llm/ollama_provider.py +40 -0
- db_chat_widget-0.1.0/src/db_chat_widget/llm/openai_provider.py +32 -0
- db_chat_widget-0.1.0/src/db_chat_widget/server/__init__.py +0 -0
- db_chat_widget-0.1.0/src/db_chat_widget/server/app.py +69 -0
- db_chat_widget-0.1.0/src/db_chat_widget/server/static/widget.css +253 -0
- db_chat_widget-0.1.0/src/db_chat_widget/server/static/widget.js +221 -0
- db_chat_widget-0.1.0/src/db_chat_widget/server/templates/demo.html +35 -0
- db_chat_widget-0.1.0/tests/conftest.py +51 -0
- db_chat_widget-0.1.0/tests/test_api.py +36 -0
- db_chat_widget-0.1.0/tests/test_connection.py +16 -0
- db_chat_widget-0.1.0/tests/test_engine.py +60 -0
- db_chat_widget-0.1.0/tests/test_executor.py +28 -0
- db_chat_widget-0.1.0/tests/test_introspection.py +25 -0
- db_chat_widget-0.1.0/tests/test_safety.py +54 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
|
|
21
|
+
- name: Install package with dev extras
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
|
|
24
|
+
- name: Lint
|
|
25
|
+
run: ruff check .
|
|
26
|
+
|
|
27
|
+
- name: Test
|
|
28
|
+
run: pytest -v
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Armand Kouassi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: db-chat-widget
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Embeddable chatbot widget that answers natural-language questions about your database.
|
|
5
|
+
Project-URL: Homepage, https://github.com/armandkouassi/db-chat-widget
|
|
6
|
+
Project-URL: Issues, https://github.com/armandkouassi/db-chat-widget/issues
|
|
7
|
+
Author: Armand Kouassi
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: chatbot,database,llm,nl2sql,sql,widget
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: anthropic>=0.34
|
|
23
|
+
Requires-Dist: groq>=0.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: openai>=1.30
|
|
26
|
+
Requires-Dist: psycopg2-binary>=2.9
|
|
27
|
+
Requires-Dist: pymysql>=1.1
|
|
28
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
29
|
+
Requires-Dist: sqlglot>=23.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: fastapi>=0.110; extra == 'dev'
|
|
32
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
33
|
+
Requires-Dist: jinja2>=3.1; extra == 'dev'
|
|
34
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
35
|
+
Requires-Dist: pydantic>=2.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
39
|
+
Requires-Dist: uvicorn>=0.29; extra == 'dev'
|
|
40
|
+
Provides-Extra: server
|
|
41
|
+
Requires-Dist: fastapi>=0.110; extra == 'server'
|
|
42
|
+
Requires-Dist: jinja2>=3.1; extra == 'server'
|
|
43
|
+
Requires-Dist: pydantic>=2.0; extra == 'server'
|
|
44
|
+
Requires-Dist: uvicorn>=0.29; extra == 'server'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# db-chat-widget
|
|
48
|
+
|
|
49
|
+
Embeddable chatbot widget that lets end users ask natural-language questions
|
|
50
|
+
about a SQL database and get back answers with the underlying query and data.
|
|
51
|
+
|
|
52
|
+
- **Any SQLAlchemy-compatible database**: PostgreSQL, MySQL, SQLite, etc.
|
|
53
|
+
- **Pluggable LLM backend**: Anthropic Claude, OpenAI, Groq, or a local Ollama model.
|
|
54
|
+
- **Read-only by default**: generated SQL is parsed and only `SELECT`
|
|
55
|
+
statements are allowed unless you explicitly opt into write access.
|
|
56
|
+
- **Embeddable widget**: a small vanilla-JS chat component you can drop into
|
|
57
|
+
any existing web page with a single `<script>` tag, backed by a FastAPI
|
|
58
|
+
server.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install "db-chat-widget[server]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This includes every LLM provider (Anthropic, OpenAI, Groq, Ollama), every
|
|
67
|
+
database driver (PostgreSQL, MySQL), and the standalone FastAPI server + CLI
|
|
68
|
+
`serve` command used below. Pick which provider and database to use
|
|
69
|
+
afterwards, at configuration time, via `--llm-provider` / `Settings.llm_provider`
|
|
70
|
+
and `--db-url` / `Settings.db_url`.
|
|
71
|
+
|
|
72
|
+
If you only need the `ChatEngine` as a library — e.g. from
|
|
73
|
+
[django-db-chat-widget](https://github.com/krak225/django-db-chat-widget),
|
|
74
|
+
which brings its own web layer — `pip install db-chat-widget` (without
|
|
75
|
+
`[server]`) skips FastAPI/uvicorn/Jinja2 entirely.
|
|
76
|
+
|
|
77
|
+
## Quickstart
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export DB_CHAT_LLM_API_KEY=gsk_... # your Groq API key
|
|
81
|
+
|
|
82
|
+
db-chat-widget serve \
|
|
83
|
+
--db-url "postgresql://user:pass@localhost/mydb" \
|
|
84
|
+
--llm-provider groq \
|
|
85
|
+
--port 8000
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
(`--llm-provider anthropic` / `openai` work the same way, with the matching API key.)
|
|
89
|
+
|
|
90
|
+
Open http://127.0.0.1:8000 for a demo chat page, or embed the widget in your
|
|
91
|
+
own site.
|
|
92
|
+
|
|
93
|
+
### Popup mode (floating chat bubble)
|
|
94
|
+
|
|
95
|
+
Drops a chat bubble launcher into the bottom-right corner of the page — the
|
|
96
|
+
most common way to bolt this onto an existing template with zero markup
|
|
97
|
+
changes. Clicking it opens/closes the chat panel.
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<link rel="stylesheet" href="http://127.0.0.1:8000/static/widget.css" />
|
|
101
|
+
<script
|
|
102
|
+
src="http://127.0.0.1:8000/static/widget.js"
|
|
103
|
+
data-mode="bubble"
|
|
104
|
+
data-api-url="http://127.0.0.1:8000/api/chat"
|
|
105
|
+
data-title="Ask about our data"
|
|
106
|
+
></script>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Use `data-position="bottom-left"` to anchor it to the bottom-left instead.
|
|
110
|
+
|
|
111
|
+
### Inline mode
|
|
112
|
+
|
|
113
|
+
Embeds the chat panel directly into an element already on the page, instead
|
|
114
|
+
of floating:
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<link rel="stylesheet" href="http://127.0.0.1:8000/static/widget.css" />
|
|
118
|
+
<div id="my-db-chat"></div>
|
|
119
|
+
<script
|
|
120
|
+
src="http://127.0.0.1:8000/static/widget.js"
|
|
121
|
+
data-target="#my-db-chat"
|
|
122
|
+
data-api-url="http://127.0.0.1:8000/api/chat"
|
|
123
|
+
data-title="Ask about our data"
|
|
124
|
+
></script>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Programmatic API
|
|
128
|
+
|
|
129
|
+
Both modes are also available from JavaScript, e.g. after dynamically
|
|
130
|
+
injecting the script, or to control the popup (`.open()` / `.close()`):
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<script src="http://127.0.0.1:8000/static/widget.js"></script>
|
|
134
|
+
<script>
|
|
135
|
+
const chat = DBChatWidget.init({
|
|
136
|
+
mode: "bubble", // or "inline" (requires `target`)
|
|
137
|
+
apiUrl: "http://127.0.0.1:8000/api/chat",
|
|
138
|
+
title: "Ask about our data",
|
|
139
|
+
position: "bottom-right", // or "bottom-left"
|
|
140
|
+
});
|
|
141
|
+
// chat.open(); chat.close(); chat.ask("How many orders today?");
|
|
142
|
+
</script>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
See [`examples/embed_example.html`](examples/embed_example.html) and
|
|
146
|
+
[`examples/demo_app.py`](examples/demo_app.py) for a runnable, self-contained
|
|
147
|
+
example with a seeded SQLite database.
|
|
148
|
+
|
|
149
|
+
## Using it as a library
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from db_chat_widget import ChatEngine, Settings
|
|
153
|
+
|
|
154
|
+
settings = Settings(
|
|
155
|
+
db_url="sqlite:///mydb.sqlite",
|
|
156
|
+
llm_provider="groq",
|
|
157
|
+
llm_api_key="gsk_...",
|
|
158
|
+
read_only=True, # default: only SELECT statements are allowed
|
|
159
|
+
max_rows=200, # cap on rows returned per query
|
|
160
|
+
allowed_tables=None, # optionally restrict which tables can be queried
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
engine = ChatEngine(settings)
|
|
164
|
+
result = engine.ask("How many orders were placed last month?")
|
|
165
|
+
|
|
166
|
+
print(result.answer)
|
|
167
|
+
print(result.sql)
|
|
168
|
+
print(result.rows)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
To mount just the FastAPI app (e.g. behind your own ASGI server or reverse
|
|
172
|
+
proxy):
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from db_chat_widget.config import Settings
|
|
176
|
+
from db_chat_widget.server.app import create_app
|
|
177
|
+
|
|
178
|
+
app = create_app(Settings(db_url="...", llm_provider="openai", llm_api_key="..."))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## How it works
|
|
182
|
+
|
|
183
|
+
1. The database schema (tables, columns, types, foreign keys) is introspected
|
|
184
|
+
via SQLAlchemy and sent to the configured LLM as context.
|
|
185
|
+
2. The LLM is asked to translate the user's question into a single SQL
|
|
186
|
+
`SELECT` statement (or reply `CANNOT_ANSWER`).
|
|
187
|
+
3. The generated SQL is parsed with `sqlglot` and rejected if it contains
|
|
188
|
+
anything other than a single read-only `SELECT` (no `INSERT`/`UPDATE`/
|
|
189
|
+
`DELETE`/`DROP`/multiple statements/disallowed tables).
|
|
190
|
+
4. The query is executed with a row limit and the result, generated SQL, and
|
|
191
|
+
a short natural-language summary are returned to the widget.
|
|
192
|
+
|
|
193
|
+
## Default encoding
|
|
194
|
+
|
|
195
|
+
MySQL/MariaDB servers commonly default their client connection to `latin1`,
|
|
196
|
+
which mangles accented characters (e.g. French text) unless the client asks
|
|
197
|
+
for UTF-8 explicitly. When your `db_url` is a MySQL URL without a `charset`
|
|
198
|
+
query parameter, `db-chat-widget` automatically connects with
|
|
199
|
+
`charset=utf8mb4`:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
mysql+pymysql://user:pass@localhost:3306/mydb
|
|
203
|
+
# connects as:
|
|
204
|
+
mysql+pymysql://user:pass@localhost:3306/mydb?charset=utf8mb4
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
If your URL already specifies a `charset` (e.g. `?charset=latin1`), it is left
|
|
208
|
+
untouched. Other database backends (PostgreSQL, SQLite) already default to
|
|
209
|
+
UTF-8 and are not affected.
|
|
210
|
+
|
|
211
|
+
## Safety notes
|
|
212
|
+
|
|
213
|
+
- `read_only=True` (the default) is enforced at the SQL-parsing level, not
|
|
214
|
+
just by prompting the model — treat it as the actual security boundary.
|
|
215
|
+
- Use `allowed_tables` to scope the chatbot to specific tables (e.g. exclude
|
|
216
|
+
a `users`/`secrets` table containing sensitive columns).
|
|
217
|
+
- The demo server enables permissive CORS (`*`) for convenience; pass
|
|
218
|
+
`cors_origins=[...]` (or `--cors-origin` on the CLI) to restrict this in
|
|
219
|
+
production.
|
|
220
|
+
- Run the database user backing `db_url` with least-privilege, read-only
|
|
221
|
+
grants where possible, as defense in depth.
|
|
222
|
+
|
|
223
|
+
## Development
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
pip install -e ".[dev]"
|
|
227
|
+
pytest
|
|
228
|
+
ruff check .
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# db-chat-widget
|
|
2
|
+
|
|
3
|
+
Embeddable chatbot widget that lets end users ask natural-language questions
|
|
4
|
+
about a SQL database and get back answers with the underlying query and data.
|
|
5
|
+
|
|
6
|
+
- **Any SQLAlchemy-compatible database**: PostgreSQL, MySQL, SQLite, etc.
|
|
7
|
+
- **Pluggable LLM backend**: Anthropic Claude, OpenAI, Groq, or a local Ollama model.
|
|
8
|
+
- **Read-only by default**: generated SQL is parsed and only `SELECT`
|
|
9
|
+
statements are allowed unless you explicitly opt into write access.
|
|
10
|
+
- **Embeddable widget**: a small vanilla-JS chat component you can drop into
|
|
11
|
+
any existing web page with a single `<script>` tag, backed by a FastAPI
|
|
12
|
+
server.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install "db-chat-widget[server]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This includes every LLM provider (Anthropic, OpenAI, Groq, Ollama), every
|
|
21
|
+
database driver (PostgreSQL, MySQL), and the standalone FastAPI server + CLI
|
|
22
|
+
`serve` command used below. Pick which provider and database to use
|
|
23
|
+
afterwards, at configuration time, via `--llm-provider` / `Settings.llm_provider`
|
|
24
|
+
and `--db-url` / `Settings.db_url`.
|
|
25
|
+
|
|
26
|
+
If you only need the `ChatEngine` as a library — e.g. from
|
|
27
|
+
[django-db-chat-widget](https://github.com/krak225/django-db-chat-widget),
|
|
28
|
+
which brings its own web layer — `pip install db-chat-widget` (without
|
|
29
|
+
`[server]`) skips FastAPI/uvicorn/Jinja2 entirely.
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export DB_CHAT_LLM_API_KEY=gsk_... # your Groq API key
|
|
35
|
+
|
|
36
|
+
db-chat-widget serve \
|
|
37
|
+
--db-url "postgresql://user:pass@localhost/mydb" \
|
|
38
|
+
--llm-provider groq \
|
|
39
|
+
--port 8000
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
(`--llm-provider anthropic` / `openai` work the same way, with the matching API key.)
|
|
43
|
+
|
|
44
|
+
Open http://127.0.0.1:8000 for a demo chat page, or embed the widget in your
|
|
45
|
+
own site.
|
|
46
|
+
|
|
47
|
+
### Popup mode (floating chat bubble)
|
|
48
|
+
|
|
49
|
+
Drops a chat bubble launcher into the bottom-right corner of the page — the
|
|
50
|
+
most common way to bolt this onto an existing template with zero markup
|
|
51
|
+
changes. Clicking it opens/closes the chat panel.
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<link rel="stylesheet" href="http://127.0.0.1:8000/static/widget.css" />
|
|
55
|
+
<script
|
|
56
|
+
src="http://127.0.0.1:8000/static/widget.js"
|
|
57
|
+
data-mode="bubble"
|
|
58
|
+
data-api-url="http://127.0.0.1:8000/api/chat"
|
|
59
|
+
data-title="Ask about our data"
|
|
60
|
+
></script>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use `data-position="bottom-left"` to anchor it to the bottom-left instead.
|
|
64
|
+
|
|
65
|
+
### Inline mode
|
|
66
|
+
|
|
67
|
+
Embeds the chat panel directly into an element already on the page, instead
|
|
68
|
+
of floating:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<link rel="stylesheet" href="http://127.0.0.1:8000/static/widget.css" />
|
|
72
|
+
<div id="my-db-chat"></div>
|
|
73
|
+
<script
|
|
74
|
+
src="http://127.0.0.1:8000/static/widget.js"
|
|
75
|
+
data-target="#my-db-chat"
|
|
76
|
+
data-api-url="http://127.0.0.1:8000/api/chat"
|
|
77
|
+
data-title="Ask about our data"
|
|
78
|
+
></script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Programmatic API
|
|
82
|
+
|
|
83
|
+
Both modes are also available from JavaScript, e.g. after dynamically
|
|
84
|
+
injecting the script, or to control the popup (`.open()` / `.close()`):
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<script src="http://127.0.0.1:8000/static/widget.js"></script>
|
|
88
|
+
<script>
|
|
89
|
+
const chat = DBChatWidget.init({
|
|
90
|
+
mode: "bubble", // or "inline" (requires `target`)
|
|
91
|
+
apiUrl: "http://127.0.0.1:8000/api/chat",
|
|
92
|
+
title: "Ask about our data",
|
|
93
|
+
position: "bottom-right", // or "bottom-left"
|
|
94
|
+
});
|
|
95
|
+
// chat.open(); chat.close(); chat.ask("How many orders today?");
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [`examples/embed_example.html`](examples/embed_example.html) and
|
|
100
|
+
[`examples/demo_app.py`](examples/demo_app.py) for a runnable, self-contained
|
|
101
|
+
example with a seeded SQLite database.
|
|
102
|
+
|
|
103
|
+
## Using it as a library
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from db_chat_widget import ChatEngine, Settings
|
|
107
|
+
|
|
108
|
+
settings = Settings(
|
|
109
|
+
db_url="sqlite:///mydb.sqlite",
|
|
110
|
+
llm_provider="groq",
|
|
111
|
+
llm_api_key="gsk_...",
|
|
112
|
+
read_only=True, # default: only SELECT statements are allowed
|
|
113
|
+
max_rows=200, # cap on rows returned per query
|
|
114
|
+
allowed_tables=None, # optionally restrict which tables can be queried
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
engine = ChatEngine(settings)
|
|
118
|
+
result = engine.ask("How many orders were placed last month?")
|
|
119
|
+
|
|
120
|
+
print(result.answer)
|
|
121
|
+
print(result.sql)
|
|
122
|
+
print(result.rows)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
To mount just the FastAPI app (e.g. behind your own ASGI server or reverse
|
|
126
|
+
proxy):
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from db_chat_widget.config import Settings
|
|
130
|
+
from db_chat_widget.server.app import create_app
|
|
131
|
+
|
|
132
|
+
app = create_app(Settings(db_url="...", llm_provider="openai", llm_api_key="..."))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
1. The database schema (tables, columns, types, foreign keys) is introspected
|
|
138
|
+
via SQLAlchemy and sent to the configured LLM as context.
|
|
139
|
+
2. The LLM is asked to translate the user's question into a single SQL
|
|
140
|
+
`SELECT` statement (or reply `CANNOT_ANSWER`).
|
|
141
|
+
3. The generated SQL is parsed with `sqlglot` and rejected if it contains
|
|
142
|
+
anything other than a single read-only `SELECT` (no `INSERT`/`UPDATE`/
|
|
143
|
+
`DELETE`/`DROP`/multiple statements/disallowed tables).
|
|
144
|
+
4. The query is executed with a row limit and the result, generated SQL, and
|
|
145
|
+
a short natural-language summary are returned to the widget.
|
|
146
|
+
|
|
147
|
+
## Default encoding
|
|
148
|
+
|
|
149
|
+
MySQL/MariaDB servers commonly default their client connection to `latin1`,
|
|
150
|
+
which mangles accented characters (e.g. French text) unless the client asks
|
|
151
|
+
for UTF-8 explicitly. When your `db_url` is a MySQL URL without a `charset`
|
|
152
|
+
query parameter, `db-chat-widget` automatically connects with
|
|
153
|
+
`charset=utf8mb4`:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
mysql+pymysql://user:pass@localhost:3306/mydb
|
|
157
|
+
# connects as:
|
|
158
|
+
mysql+pymysql://user:pass@localhost:3306/mydb?charset=utf8mb4
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
If your URL already specifies a `charset` (e.g. `?charset=latin1`), it is left
|
|
162
|
+
untouched. Other database backends (PostgreSQL, SQLite) already default to
|
|
163
|
+
UTF-8 and are not affected.
|
|
164
|
+
|
|
165
|
+
## Safety notes
|
|
166
|
+
|
|
167
|
+
- `read_only=True` (the default) is enforced at the SQL-parsing level, not
|
|
168
|
+
just by prompting the model — treat it as the actual security boundary.
|
|
169
|
+
- Use `allowed_tables` to scope the chatbot to specific tables (e.g. exclude
|
|
170
|
+
a `users`/`secrets` table containing sensitive columns).
|
|
171
|
+
- The demo server enables permissive CORS (`*`) for convenience; pass
|
|
172
|
+
`cors_origins=[...]` (or `--cors-origin` on the CLI) to restrict this in
|
|
173
|
+
production.
|
|
174
|
+
- Run the database user backing `db_url` with least-privilege, read-only
|
|
175
|
+
grants where possible, as defense in depth.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install -e ".[dev]"
|
|
181
|
+
pytest
|
|
182
|
+
ruff check .
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Run a local demo: creates a small SQLite database and serves the chatbot.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
export DB_CHAT_LLM_API_KEY=sk-...
|
|
5
|
+
python examples/demo_app.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import create_engine, text
|
|
14
|
+
|
|
15
|
+
DB_PATH = Path(__file__).parent / "demo.db"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def seed_database() -> str:
|
|
19
|
+
db_url = f"sqlite:///{DB_PATH}"
|
|
20
|
+
engine = create_engine(db_url)
|
|
21
|
+
with engine.begin() as conn:
|
|
22
|
+
conn.execute(text("DROP TABLE IF EXISTS customers"))
|
|
23
|
+
conn.execute(text("DROP TABLE IF EXISTS orders"))
|
|
24
|
+
conn.execute(
|
|
25
|
+
text("CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, country TEXT)")
|
|
26
|
+
)
|
|
27
|
+
conn.execute(
|
|
28
|
+
text(
|
|
29
|
+
"CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, "
|
|
30
|
+
"amount REAL, status TEXT)"
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
customers = [
|
|
34
|
+
(1, "Awa Diallo", "Senegal"),
|
|
35
|
+
(2, "Kofi Mensah", "Ghana"),
|
|
36
|
+
(3, "Amina Traore", "Cote d'Ivoire"),
|
|
37
|
+
]
|
|
38
|
+
for row in customers:
|
|
39
|
+
conn.execute(
|
|
40
|
+
text("INSERT INTO customers (id, name, country) VALUES (:id, :name, :country)"),
|
|
41
|
+
{"id": row[0], "name": row[1], "country": row[2]},
|
|
42
|
+
)
|
|
43
|
+
orders = [
|
|
44
|
+
(1, 1, 120.0, "paid"),
|
|
45
|
+
(2, 1, 45.5, "pending"),
|
|
46
|
+
(3, 2, 300.0, "paid"),
|
|
47
|
+
]
|
|
48
|
+
for row in orders:
|
|
49
|
+
conn.execute(
|
|
50
|
+
text(
|
|
51
|
+
"INSERT INTO orders (id, customer_id, amount, status) "
|
|
52
|
+
"VALUES (:id, :customer_id, :amount, :status)"
|
|
53
|
+
),
|
|
54
|
+
{"id": row[0], "customer_id": row[1], "amount": row[2], "status": row[3]},
|
|
55
|
+
)
|
|
56
|
+
return db_url
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
import uvicorn
|
|
61
|
+
|
|
62
|
+
from db_chat_widget.config import Settings
|
|
63
|
+
from db_chat_widget.server.app import create_app
|
|
64
|
+
|
|
65
|
+
db_url = seed_database()
|
|
66
|
+
|
|
67
|
+
settings = Settings(
|
|
68
|
+
db_url=db_url,
|
|
69
|
+
llm_provider=os.environ.get("DB_CHAT_LLM_PROVIDER", "anthropic"),
|
|
70
|
+
llm_api_key=os.environ.get("DB_CHAT_LLM_API_KEY"),
|
|
71
|
+
read_only=True,
|
|
72
|
+
)
|
|
73
|
+
app = create_app(settings)
|
|
74
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Embedding db-chat-widget</title>
|
|
6
|
+
<link rel="stylesheet" href="http://127.0.0.1:8000/static/widget.css" />
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>My existing website</h1>
|
|
10
|
+
<p>
|
|
11
|
+
A floating chat bubble is injected in the bottom-right corner by the
|
|
12
|
+
script below — no extra markup needed in this template.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<!-- Popup mode: no data-target needed, the widget builds its own
|
|
16
|
+
floating launcher button + panel and appends them to <body>. -->
|
|
17
|
+
<script src="http://127.0.0.1:8000/static/widget.js"
|
|
18
|
+
data-mode="bubble"
|
|
19
|
+
data-api-url="http://127.0.0.1:8000/api/chat"
|
|
20
|
+
data-title="Ask about our data"></script>
|
|
21
|
+
|
|
22
|
+
<!-- Alternative: inline mode, embedded directly into a page element
|
|
23
|
+
instead of floating. Swap the script above for this one:
|
|
24
|
+
|
|
25
|
+
<div id="my-db-chat"></div>
|
|
26
|
+
<script src="http://127.0.0.1:8000/static/widget.js"
|
|
27
|
+
data-target="#my-db-chat"
|
|
28
|
+
data-api-url="http://127.0.0.1:8000/api/chat"
|
|
29
|
+
data-title="Ask about our data"></script>
|
|
30
|
+
-->
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "db-chat-widget"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Embeddable chatbot widget that answers natural-language questions about your database."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Armand Kouassi" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["chatbot", "sql", "database", "llm", "nl2sql", "widget"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
dependencies = [
|
|
30
|
+
"sqlalchemy>=2.0",
|
|
31
|
+
"sqlglot>=23.0",
|
|
32
|
+
# LLM providers
|
|
33
|
+
"anthropic>=0.34",
|
|
34
|
+
"openai>=1.30",
|
|
35
|
+
"groq>=0.11",
|
|
36
|
+
"httpx>=0.27",
|
|
37
|
+
# database drivers
|
|
38
|
+
"psycopg2-binary>=2.9",
|
|
39
|
+
"pymysql>=1.1",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
# Only needed for the standalone FastAPI server / CLI `serve` command and the
|
|
44
|
+
# embeddable demo page. Not required to use ChatEngine as a library (e.g. from
|
|
45
|
+
# django-db-chat-widget, which brings its own web layer).
|
|
46
|
+
server = [
|
|
47
|
+
"fastapi>=0.110",
|
|
48
|
+
"uvicorn>=0.29",
|
|
49
|
+
"pydantic>=2.0",
|
|
50
|
+
"jinja2>=3.1",
|
|
51
|
+
]
|
|
52
|
+
dev = [
|
|
53
|
+
"db-chat-widget[server]",
|
|
54
|
+
"pytest>=8.0",
|
|
55
|
+
"pytest-asyncio>=0.23",
|
|
56
|
+
"httpx>=0.27",
|
|
57
|
+
"ruff>=0.4",
|
|
58
|
+
"mypy>=1.10",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[project.urls]
|
|
62
|
+
Homepage = "https://github.com/armandkouassi/db-chat-widget"
|
|
63
|
+
Issues = "https://github.com/armandkouassi/db-chat-widget/issues"
|
|
64
|
+
|
|
65
|
+
[project.scripts]
|
|
66
|
+
db-chat-widget = "db_chat_widget.cli:main"
|
|
67
|
+
|
|
68
|
+
[tool.hatch.build.targets.wheel]
|
|
69
|
+
packages = ["src/db_chat_widget"]
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
asyncio_mode = "auto"
|
|
74
|
+
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
line-length = 100
|
|
77
|
+
target-version = "py39"
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
File without changes
|