Case study · 2025
CogenticAI DB Agent: natural-language database queries for an enterprise SaaS
A production-grade NL-to-SQL agent built with Google ADK and FastAPI: a SQL Generator, an SQL Executor with retry-with-feedback, and a Response Generator orchestrated as a sequential + loop agent. Converts business questions into safe, read-only Postgres queries and natural-language answers.
Senior engineer · Toptal engagement (CogenticAI)
Stack
Python 3.12 · Google ADK · FastAPI · PostgreSQL · LLM provider (OpenAI / Anthropic) · uv · Pytest
Outcomes
- Multi-agent ADK pipeline: SQL Generator (LlmAgent), SQL Executor (Custom), Response Generator (LlmAgent), orchestrated by SequentialAgent + LoopAgent with bounded retry.
- Safe execution by default: read-only SELECT, statement timeouts, row caps, connection pooling, and schema-aware prompting.
- Session-aware conversation: maintains query history across turns so follow-ups ("and for last month?") resolve against the same context.
- Two reference implementations (Python with ADK, and TypeScript) to fit the client's preferred deployment stack.
What I owned
The agent design (which agents, what each one decides, what state lives between them), the retry-with-feedback loop that lets the SQL Generator self-correct on execution errors, the schema-context engineering that keeps generated queries grounded, and the production posture: connection pooling, timeouts, structured errors, and a RESTful FastAPI surface the client could embed in their existing product.
What shipped
A SequentialAgent orchestrating a LoopAgent (Generator, Executor, retry on failure) followed by a Response Generator. Read-only SELECT enforcement at the executor layer, statement timeouts, row limits, and a schema-aware system prompt so the model knows what tables, columns, and join paths exist. A RESTful API for embedding into the client’s product, session state for multi-turn follow-ups, and a parallel TypeScript implementation so the client could deploy on whichever runtime their team owned.
Lessons
The hard part of NL-to-SQL is not the SQL. It’s recovery. A single shot will fail on join paths the model didn’t see, ambiguous column names, or non-trivial filters. A retry loop with structured executor feedback (“you referenced users.email but that column doesn’t exist; the closest match is users.contact_email”) turns a 60%-correct agent into a 95%-correct one without any prompt tuning. Treat the database as the ground truth and let the model converge to it.