sql-circuit-guard is a read-only Text-to-SQL system for relational databases (SQLite Chinook). Natural-language questions are converted to SQL by a local LLM (ibm/granite4.1:8b via Ollama) with rate-limited cloud fallback, validated by two deterministic guardrails before any execution, and wrapped in a bounded self-correction loop. Every step is traced with OpenTelemetry / Langfuse.
- Architecture & benchmarks: GitHub Pages
- Benchmark artifacts: reports/benchmark_report.json · reports/benchmark_report.md
- Operations & troubleshooting: docs/RUNBOOK.md
The recording below was generated by tests/record_demo_e2e.py against the live app using the local model. It covers four scenarios:
- Valid query — a read-only request executes and returns results (attempts: 1).
- Blocked request — a
DROP TABLErequest is rejected by the prompt guard before the LLM is called (attempts: 0). - Self-correction — a query referencing a non-existent column fails on attempt 1, then succeeds on attempt 2 with error feedback.
- Schema explorer — browsing the available tables and columns.
Higher-quality versions: MP4 · WebM
Snapshot: 2026-08-06 · model ibm/granite4.1:8b (Ollama, local) · commit de6ee95.
| Metric | Result | Target Guardrail |
|---|---|---|
| Execution Accuracy Rate | 100.0% |
≥ 85.0% |
| Adversarial Intent Rejection Rate | 100.0% |
100.0% (Zero Tolerance) |
| AST Mutation Execution Block Rate | 100.0% |
100.0% (Zero Tolerance) |
| Self-Correction Recovery Rate | 100.0% |
≥ 75.0% |
| Mean Latency per Query | 4565.63 ms |
< 3000 ms |
| Mean Generation Attempts | 0.75 |
≤ 1.5 |
Latency note: the mean is skewed by one reproducible model-inference stall (VAL-02, ~59s); the remaining 19 cases average ~2.4s. Full per-case logs: reports/benchmark_report.md.
-
Clone Repository & Prerequisites Ensure Python 3.12+ and
uvpackage manager are installed. -
Environment Configuration Copy
.env.exampleto.envand configure your API keys and local endpoint settings:cp .env.example .env
Variable Description OLLAMA_API_BASELocal Ollama endpoint (default http://localhost:11434)LOCAL_MODEL_NAMEPrimary local model, e.g. ollama/ibm/granite4.1:8bGEMINI_API_KEYCloud fallback API key (Gemini AI Studio free tier) CLOUD_MODEL_NAMECloud fallback model, e.g. gemini/gemini-3.1-flash-liteENABLE_CLOUD_FALLBACKtrue/false— route to cloud only when local failsLANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY/LANGFUSE_HOSTSelf-hosted Langfuse v4 telemetry (dashboard http://localhost:3001) -
Install Dependencies
uv sync
-
Launch the Interactive UI (Gradio web app):
uv run python src/app.py
The UI has two panels — Query execution (input, validated SQL, reasoning, results table) and Circuit diagnostics (status, attempts, latency, guardrail outcome, error trail) — plus a Database Schema Explorer tab and a Langfuse toggle.
The Chinook database (
data/chinook.db) is committed to the repository, so no download step is required.
Common workflows are available as standard targets:
make install # sync deps + pre-commit hooks
make lint # mypy (strict) + full pre-commit pipeline
make test # pytest suite (unit, guardrail, resilience)
make eval # 20-case evaluation benchmark suite
make up # full Docker Compose stack (Gradio, Ollama, Langfuse)
make logs # tail application logs
make profile # GPU memory + throughput profiler (needs Ollama running)
make clean # remove local caches (keeps data/ and reports/)graph TD
User["User Natural Language Query"] --> PromptGuard["PromptGuard (Deterministic Injection Screen)"]
PromptGuard -->|DML/DDL Intent Detected| HardStop1["🔴 HARD STOP - Blocked Pre-LLM"]
PromptGuard -->|Valid| Orchestrator["Circuit Orchestrator"]
Orchestrator --> Gateway["LiteLLM Gateway (Ollama / Cloud Fallback)"]
Gateway --> ASTGuard["sqlglot AST Guardrail"]
ASTGuard -->|Security Violation: Mutation / Multi-Statement / Non-SELECT| HardStop2["🔴 HARD STOP - No Retry, No Laundering"]
ASTGuard -->|DB Execution Error| Loop["Self-Correction Loop (Max N=2)"]
Loop --> Gateway
ASTGuard -->|Valid SELECT Only| DB["SQLite Read-Only Executor"]
DB --> Output["Structured Pydantic Result"]
Orchestrator -.-> Tracing["OpenTelemetry / Langfuse Tracing"]
Gateway -.-> Tracing
ASTGuard -.-> Tracing
Read-only behavior is enforced by deterministic code in three layers, none of which rely on LLM judgment:
- Prompt screening (
guardrails/prompt_guard.py) — before any LLM call, the untrusted natural-language prompt is checked for DML/DDL keyword intent (DROP,DELETE,UPDATE,INSERT, ...) and SQL injection markers (;,--,/*). A match blocks the request immediately. - AST validation (
guardrails/ast_guard.py) — generated SQL is parsed withsqlglot; the root must be a singleSELECTcontaining no mutation nodes. A violation blocks the request without retrying. - Read-only execution (
db/executor.py) — the SQLite connection is opened withmode=ro, so the database engine itself rejects writes.
Only soft failures (execution errors such as a hallucinated column) enter the self-correction loop, which is bounded at N=2 retries.
-
No Monolithic LLM Frameworks: native Python,
LiteLLM,Pydantic v2, andinstructor— no heavy framework abstractions. -
Strict Decoupling: domain logic never calls vendor SDKs directly; all LLM interactions route through
BaseLLMGateway. - Deterministic Security: prompts are screened pre-LLM and generated SQL is AST-validated before touching the database — security is enforced programmatically, never by prompt trust alone.
-
Bounded Self-Correction Circuit: soft failures are fed back as structured feedback prompts for up to
$N=2$ retry turns; security-class violations (DML/DDL intent, multi-statement payloads, prompt injection) hard-stop immediately — no retry, no query laundering.
| Module | Responsibility |
|---|---|
guardrails/prompt_guard.py |
Pre-LLM injection screen (DML/DDL keywords, SQL markers) |
guardrails/ast_guard.py |
sqlglot single-statement read-only SELECT validation |
db/executor.py |
SQLite executor in strict mode=ro URI mode |
db/schema_inspector.py |
Table schema / DDL extraction for LLM grounding |
gateway/llm_gateway.py |
LiteLLMGateway — local Ollama + rate-limited Gemini fallback via instructor |
gateway/rate_limiter.py |
In-memory token bucket (10 RPM / 200k TPM) |
service/circuit_orchestrator.py |
Self-correction loop, security hard-stops, @observe tracing |
service/evaluator.py |
Benchmark metrics (accuracy, rejection, block, recovery, latency, attempts) |
service/run_evals.py |
20-case eval CLI + zero-tolerance guardrail gate |
telemetry/tracing.py |
OpenTelemetry / Langfuse setup with offline resilience |
Run the test suite via pytest:
uv run pytest tests/ -vThis includes the resilience suite (tests/test_resilience.py) — gateway failure modes, concurrent read-only connections, and guardrail rejection of injection/PRAGMA payloads.
Measure VRAM usage and local inference throughput (requires a running Ollama):
./scripts/profile_gpu.shThe system ships with a deterministic 20-case benchmark (data/eval_benchmark.json) across three categories — valid read-only queries (VAL-01..10), adversarial mutation attacks (ADV-01..07), and hallucination traps (HAL-01..03). Run the full suite against your local model:
uv run python -m sql_circuit_guard.service.run_evalsThis executes every case through the real circuit, exports reports/benchmark_report.json and reports/benchmark_report.md, and enforces a zero-tolerance guardrail compliance gate — the CLI exits with code 1 if either security metric drops below 100%.
Every push or pull request to main runs the quality gate defined in .github/workflows/ci.yml on ubuntu-latest:
uv sync --locked --all-extras --dev(lockfile-pinned, cached viaastral-sh/setup-uv).ruff check+ruff format --checkonsrc/andtests/.mypy(strict) onsrc/.pytest— the full 51-test suite (guardrails, DB executor, gateway, orchestrator, telemetry, evaluator).
The database is committed, so the pipeline needs no external downloads. The same quality gate runs locally via the pre-commit pipeline (uv run pre-commit run --all-files).
The canonical Docker Compose stack runs the application, Ollama (GPU), and a self-hosted Langfuse v4 observability backend (web, worker, ClickHouse, MinIO, Redis, PostgreSQL):
docker compose up --build -d
# Services:
# - Gradio Web UI: http://localhost:7860
# - Langfuse Dashboard: http://localhost:3001Langfuse is provisioned automatically with the project keys (pk/sk-lf-sql-circuit-guard) via LANGFUSE_INIT_* environment variables; log in with the user defined by LANGFUSE_INIT_USER_EMAIL / LANGFUSE_INIT_USER_PASSWORD (defaults in docker-compose.yml). Every gateway call and AST verification step is instrumented with OpenTelemetry and Langfuse tracing.
This project is licensed under the terms of the MIT License.
