Chat with any SQLite database using natural language. Built on AnyLanguageModel (HuggingFace) for LLM-agnostic provider support and GRDB for SQLite access. Core features: - Auto schema introspection from sqlite_master (zero config) - NL → SQL generation via any AnyLanguageModel provider - Three rendering modes: text summary, data table, Swift Charts - Drop-in DataChatView (SwiftUI) and headless ChatEngine - Operation allowlist with read-only default - Mutation policy with per-table control - ToolExecutionDelegate for destructive operation confirmation - Multi-turn conversation context - 352 tests across 24 suites, all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
255 lines
6.6 KiB
Markdown
255 lines
6.6 KiB
Markdown
# SwiftDBAI
|
|
|
|
Chat with any SQLite database using natural language.
|
|
|
|
<!-- badges -->
|
|

|
|

|
|

|
|
|
|
## Features
|
|
|
|
- Drop-in SwiftUI chat view (`DataChatView`) -- one line to add a database chat UI
|
|
- Headless `ChatEngine` for programmatic / non-UI use
|
|
- LLM-agnostic via [AnyLanguageModel](https://github.com/huggingface/AnyLanguageModel) -- works with OpenAI, Anthropic, Gemini, Ollama, llama.cpp, or any OpenAI-compatible endpoint
|
|
- Automatic schema introspection -- no manual annotations required
|
|
- Safety-first: read-only by default, operation allowlists, table-level mutation policies, destructive operation confirmation delegate
|
|
- Configurable query timeouts, context windows, and custom validators
|
|
|
|
## Installation
|
|
|
|
Add SwiftDBAI via Swift Package Manager:
|
|
|
|
```swift
|
|
dependencies: [
|
|
.package(url: "https://github.com/<org>/SwiftDBAI.git", from: "1.0.0"),
|
|
]
|
|
```
|
|
|
|
Then add the dependency to your target:
|
|
|
|
```swift
|
|
.target(
|
|
name: "MyApp",
|
|
dependencies: ["SwiftDBAI"]
|
|
)
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
Drop a full chat UI into any SwiftUI view with `DataChatView`:
|
|
|
|
```swift
|
|
import SwiftDBAI
|
|
import AnyLanguageModel
|
|
|
|
struct ContentView: View {
|
|
var body: some View {
|
|
DataChatView(
|
|
databasePath: "/path/to/mydata.sqlite",
|
|
model: OllamaLanguageModel(model: "llama3")
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
That's it. `DataChatView` opens the database, introspects the schema, and renders a chat interface. The default mode is **read-only** (SELECT only).
|
|
|
|
To pass an existing GRDB connection and customize behavior:
|
|
|
|
```swift
|
|
DataChatView(
|
|
database: myDatabasePool,
|
|
model: OpenAILanguageModel(apiKey: "sk-...", model: "gpt-4o"),
|
|
allowlist: .standard,
|
|
additionalContext: "This database stores a recipe app's data.",
|
|
maxSummaryRows: 100
|
|
)
|
|
```
|
|
|
|
## Headless / Programmatic Use
|
|
|
|
Use `ChatEngine` directly when you don't need a UI:
|
|
|
|
```swift
|
|
import SwiftDBAI
|
|
import AnyLanguageModel
|
|
import GRDB
|
|
|
|
let pool = try DatabasePool(path: "/path/to/mydata.sqlite")
|
|
let engine = ChatEngine(
|
|
database: pool,
|
|
model: OpenAILanguageModel(apiKey: "sk-...", model: "gpt-4o")
|
|
)
|
|
|
|
let response = try await engine.send("How many users signed up this week?")
|
|
print(response.summary) // "There were 42 new signups this week."
|
|
print(response.sql) // Optional("SELECT COUNT(*) FROM users WHERE ...")
|
|
```
|
|
|
|
`ChatEngine` also accepts a `ProviderConfiguration` for convenience:
|
|
|
|
```swift
|
|
let engine = ChatEngine(
|
|
database: pool,
|
|
provider: .anthropic(apiKey: "sk-ant-...", model: "claude-sonnet-4-20250514")
|
|
)
|
|
```
|
|
|
|
For fine-grained control, pass a `ChatEngineConfiguration`:
|
|
|
|
```swift
|
|
var config = ChatEngineConfiguration(
|
|
queryTimeout: 10,
|
|
contextWindowSize: 20,
|
|
maxSummaryRows: 100,
|
|
additionalContext: "The 'status' column uses: 'active', 'inactive', 'suspended'."
|
|
)
|
|
|
|
let engine = ChatEngine(
|
|
database: pool,
|
|
model: model,
|
|
allowlist: .standard,
|
|
configuration: config
|
|
)
|
|
```
|
|
|
|
## Choosing a Provider
|
|
|
|
SwiftDBAI works with any provider supported by AnyLanguageModel. Use `ProviderConfiguration` factory methods or construct model instances directly.
|
|
|
|
```swift
|
|
// OpenAI
|
|
let config = ProviderConfiguration.openAI(apiKey: "sk-...", model: "gpt-4o")
|
|
|
|
// Anthropic
|
|
let config = ProviderConfiguration.anthropic(apiKey: "sk-ant-...", model: "claude-sonnet-4-20250514")
|
|
|
|
// Gemini
|
|
let config = ProviderConfiguration.gemini(apiKey: "AIza...", model: "gemini-2.0-flash")
|
|
|
|
// Ollama (local, no API key needed)
|
|
let config = ProviderConfiguration.ollama(model: "llama3.2")
|
|
|
|
// llama.cpp (local)
|
|
let config = ProviderConfiguration.llamaCpp(model: "default")
|
|
|
|
// Any OpenAI-compatible endpoint
|
|
let config = ProviderConfiguration.openAICompatible(
|
|
apiKey: "your-key",
|
|
model: "llama-3.1-70b",
|
|
baseURL: URL(string: "https://api.together.xyz/v1/")!
|
|
)
|
|
```
|
|
|
|
Use with ChatEngine:
|
|
|
|
```swift
|
|
let engine = ChatEngine(database: pool, provider: config)
|
|
// or
|
|
let engine = ChatEngine(database: pool, model: config.makeModel())
|
|
```
|
|
|
|
API keys can also come from environment variables:
|
|
|
|
```swift
|
|
let config = ProviderConfiguration.fromEnvironment(
|
|
provider: .openAI,
|
|
environmentVariable: "OPENAI_API_KEY",
|
|
model: "gpt-4o"
|
|
)
|
|
```
|
|
|
|
## Safety and Mutation Control
|
|
|
|
### Operation Allowlist
|
|
|
|
By default, only SELECT queries are allowed. Opt in to writes explicitly:
|
|
|
|
| Preset | Allowed Operations |
|
|
|---|---|
|
|
| `.readOnly` (default) | SELECT |
|
|
| `.standard` | SELECT, INSERT, UPDATE |
|
|
| `.unrestricted` | SELECT, INSERT, UPDATE, DELETE |
|
|
|
|
```swift
|
|
// Custom allowlist
|
|
let allowlist = OperationAllowlist([.select, .insert])
|
|
```
|
|
|
|
### Mutation Policy
|
|
|
|
For table-level control, use `MutationPolicy`:
|
|
|
|
```swift
|
|
// Allow INSERT and UPDATE only on specific tables
|
|
let policy = MutationPolicy(
|
|
allowedOperations: [.insert, .update],
|
|
allowedTables: ["orders", "order_items"]
|
|
)
|
|
|
|
let engine = ChatEngine(
|
|
database: pool,
|
|
model: model,
|
|
mutationPolicy: policy
|
|
)
|
|
```
|
|
|
|
Presets: `.readOnly`, `.readWrite`, `.unrestricted`.
|
|
|
|
### Confirmation Delegate
|
|
|
|
Destructive operations (DELETE, DROP, ALTER, TRUNCATE) require confirmation through a `ToolExecutionDelegate`:
|
|
|
|
```swift
|
|
struct MyDelegate: ToolExecutionDelegate {
|
|
func confirmDestructiveOperation(
|
|
_ context: DestructiveOperationContext
|
|
) async -> Bool {
|
|
// Present confirmation UI, return true to proceed
|
|
return await showConfirmationDialog(context.description)
|
|
}
|
|
}
|
|
|
|
let engine = ChatEngine(
|
|
database: pool,
|
|
model: model,
|
|
allowlist: .unrestricted,
|
|
delegate: MyDelegate()
|
|
)
|
|
```
|
|
|
|
Without a delegate, destructive operations throw `SwiftDBAIError.confirmationRequired` so you can handle confirmation in your own flow.
|
|
|
|
Built-in delegates: `AutoApproveDelegate` (testing only), `RejectAllDelegate` (safest).
|
|
|
|
## Architecture
|
|
|
|
```
|
|
User Question
|
|
|
|
|
v
|
|
ChatEngine
|
|
|-- SchemaIntrospector (auto-discovers tables, columns, keys, indexes)
|
|
|-- PromptBuilder (builds LLM system prompt with schema context)
|
|
|-- LanguageModel (generates SQL via AnyLanguageModel)
|
|
|-- SQLQueryParser (parses and validates against allowlist/policy)
|
|
|-- QueryValidator (optional custom validators)
|
|
|-- GRDB (executes SQL against SQLite)
|
|
|-- TextSummaryRenderer (summarizes results via LLM)
|
|
v
|
|
ChatResponse { summary, sql, queryResult }
|
|
```
|
|
|
|
`DataChatView` wraps this pipeline in a SwiftUI view with `ChatViewModel` managing state.
|
|
|
|
## Requirements
|
|
|
|
- iOS 17.0+ / macOS 14.0+ / visionOS 1.0+
|
|
- Swift 6.1+
|
|
- Xcode 16+
|
|
|
|
## License
|
|
|
|
MIT. See [LICENSE](LICENSE) for details.
|