noise -> signal

000

press any key to skip

Text-to-SQL agent chat over your own database, with hybrid schema retrieval and memory

Problem

Data analysts and non-technical users must hand-write SQL against unfamiliar databases and re-derive context (schema, join paths, past queries) every session.

What it does

Users register a PostgreSQL/MySQL/Oracle/MSSQL database, then ask questions in plain English. A LangGraph agent retrieves relevant schema via hybrid dense+BM25 search in Qdrant, writes and executes validated read-only SQL, and streams an analysis, SQL, and rows back over SSE. Durable facts and user preferences are stored in Mem0; conversations persist in Postgres via a Celery worker.

Architecture

[web] Next.js web app (Clerk auth, chat workspace) (client)
[api] FastAPI backend with LangGraph agent (api)
[worker] Celery worker (turn persistence, memory writes, schema indexing) (worker)
[pg] PostgreSQL (users, databases, conversations, turns) (store)
[redis] Redis (checkpoints, caches, Celery broker) (store)
[qdrant] Qdrant (dense + BM25 schema index, tenant-filtered) (store)
[mem0] Mem0 long-term memory (facts, preferences) (store)
[llm] DeepSeek chat model via langchain-deepseek (model)
[embed] OpenRouter-hosted embedding model (Qdrant inference) (external)
[userdb] User's database (PostgreSQL/MySQL/Oracle/SQL Server) (external)

web ──▶ api · REST + SSE chat stream with Clerk bearer token
api ──▶ llm · agent runs with tools
api ──▶ qdrant · retrieve_context: RRF-fused dense+sparse hybrid search
api ──▶ mem0 · search_db_facts / load user prefs
api ──▶ redis · db-config cache, checkpointer state restore
api ──▶ pg · conversation/turn lookups (SQLModel)
api ──▶ userdb · sqlglot-validated read-only SQL, Fernet-decrypted creds
api ──▶ worker · enqueue turn persistence and memory writes
worker ──▶ pg · insert_chats_in_db
worker ──▶ mem0 · memory_write (infer=False)
worker ──▶ qdrant · ingest_schema upserts
worker ──▶ redis · broker / result backend
worker ──▶ embed · embed schema documents on ingest

Simplified — box-and-arrow sketch of the repo structure, not a system diagram.

Decisions

  1. 01

    Single compiled agent with per-run Context + middleware instead of per-tenant prompts

    Dynamic prompt middleware injects engine, dense schema and user preferences at every model call, while ToolCallLimitMiddleware caps retrieve_context at 2 and run_sql at 4 calls per turn; one agent serves all users and databases.

    Evidence: apps/backend/agent/init.py, apps/backend/agent/middleware.py, apps/backend/agent/context.py

  2. 02

    sqlglot AST validation as a guardrail, least-privilege DB role as the real boundary

    run_sql parses in the target dialect, allows only single SELECT/WITH/UNION statements, rejects SELECT INTO and any write/DDL/grant/copy node, and returns max 51 rows; README explicitly states validation is not the security boundary and ships per-engine read-only setup guides.

    Evidence: apps/backend/agent/tools/run_sql.py, README.md

  3. 03

    Hybrid dense+BM25 schema retrieval with RRF fusion and tenant filtering

    Every query prefetches dense (1024-d, OpenRouter-hosted embedding) and BM25 sparse vectors then fuses with FusionQuery(RRF), filtered by a user__db tenant payload key, so retrieval is scoped per user and per database.

    Evidence: apps/backend/agent/tools/retriever.py, apps/task_queue/task_queue/tasks.py

  4. 04

    Two-layer memory with write-behind via Celery

    Short-term state lives in AsyncRedisSaver checkpoints (falling back to last 5 Postgres turns); long-term facts/preferences go to Mem0 via a Celery task with infer=False so stored text is exactly what the agent produced, keeping chat responses off the write path.

    Evidence: apps/backend/agent/memory/short.py, apps/task_queue/task_queue/tasks.py, apps/backend/api/endpoints/chat.py

  5. 05

    Agent quality measured with a DeepEval harness on BIRD-Interact-style data

    packages/evals wraps the agent's SQL tool with stricter checks, intercepts Celery memory dispatch so evals don't mutate memory, and scores 15 metrics (SQL correctness, tool ordering, memory quality, grounding) with timestamped JSON reports.

    Evidence: packages/evals/runner.py, packages/evals/metrics/, README.md

  6. 06

    One image, whole monorepo, ARM CI with SSH deploy

    Root DockerFile installs both Bun and uv, builds with turbo, and starts web (3000) + API (8000) via one turbo command; GitHub Actions builds on ubuntu-24.04-arm, pushes to Docker Hub, then SSH-deploys by resetting the VM checkout and re-running docker compose.

    Evidence: DockerFile, .github/workflows/main.yml, docker-compose.yml

The hard parts

  • Multi-dialect read-only SQL safety: sqlglot parsing and validation must be correct across postgres/mssql/mysql/oracle dialects (apps/backend/agent/tools/run_sql.py)
  • Streaming a LangGraph agent over SSE with tool_call/tool_result/output events, including checkpointer restore and Postgres turn-rebuild fallback when checkpoints expire (apps/backend/api/endpoints/chat.py)
  • Tenant isolation across every store: Qdrant tenant payloads, Mem0 user/app/run filters, owner-scoped Postgres lookups, Fernet-encrypted credentials in Redis cache
  • Dynamic single-agent prompting that must adapt to arbitrary schemas at every model call (apps/backend/agent/middleware.py, apps/backend/agent/init.py)
  • Live-service evaluation harness with mocked memory dispatch and stricter SQL interception (packages/evals/runner.py)

Known limitations

  • No unit or integration tests for application code; the only pytest file is the live-service eval harness requiring LLM/Redis/Qdrant/Mem0 and a test tenant (README.md Known limitations, packages/evals/test_eval.py)
  • Final answer is emitted as one SSE output event and faked word-by-word in the frontend; no token streaming from the backend (README.md, apps/backend/api/endpoints/chat.py)
  • Celery runs with the solo pool, one task at a time per worker process (README.md)
  • Database rename/description editing is client-side only; no backend update endpoint (README.md)
  • Landing-page demo workspace is simulated and touches no real database (README.md)

Command palette

Jump to a section, project or link