Vanna is an open-source Python framework that turns natural-language questions into SQL queries and, ultimately, into charts and summaries. You feed it your schema (DDL), business documentation, and sample queries; it embeds and stores them, then retrieves the most relevant context at question time to prompt the LLM. The current main branch carries a major turning point: version 2.0 is a ground-up rewrite that turns the old “generate SQL and return it” library into a user-aware, streaming, tool-based agent platform.
That rewrite is exactly why the source tree is worth a tour. Most LLM wrappers keep their simplicity by refusing to grow up; Vanna instead reorganized itself into a clean layered architecture — a framework core with seven explicit extension points, a set of capability contracts, a large integrations directory, and a preserved legacy layer for 0.x users. Reading the code, you can watch a mature project execute a migration strategy in public: the old API still runs, an adapter bridges it into the new runtime, and the new design is opinionated about identity, permissions, and streaming from day one.
In this tour we walk the actual main branch of Vanna-AI/vanna, module by module, with verified file paths and line anchors. Two diagrams anchor the discussion: a compact overview of the moving parts, and a detailed map covering the extension interfaces, integrations, and the legacy subsystem. Everything below links to the real source on GitHub.
Overview of Vanna 2.0 on the main branch: the Agent orchestrates an LLM service, a tool registry, and a user resolver; integrations supply OpenAI, Anthropic, and database adapters; servers expose the chat over HTTP; and the legacy 0.x surface plugs in through an adapter.
Reading the overview from left to right:
- Agent, L56 — the orchestrator. Its constructor at L82 wires an LLM service, a tool registry, a user resolver, memory, and seven optional extension points, while send_message, L141 streams UI components back to the caller.
- DefaultWorkflowHandler, L32 — the built-in chat-turn logic. The
Agentdelegates each incoming message here; this implementation decides when to call tools, when to ask the LLM to summarize, and what starter UI a new conversation renders. - ToolRegistry, L46 — where tools live. Every capability the agent can use is registered here, with access groups checked against the resolved user.
- Tool, L16 — the typed contract every tool implements: a name, a description, a Pydantic args schema, and an async
executethat receives a ToolContext, L28 carrying the user and returns a ToolResult, L47 with aresult_for_llmand an optional UI component. - RunSqlTool, L18 — the flagship tool. It takes any injected
SqlRunner, executes the query, saves the result as a CSV artifact for downstream tools, and streams aDataFrameComponentso the frontend renders a real table. - LlmService, L13 — the provider seam. Concrete adapters include OpenAILlmService, L27, an OpenAIResponsesService, L14 variant on the newer Responses API, and AnthropicLlmService, L27.
- register_chat_routes, L17 — the delivery seam. A ChatHandler, L12 binds the agent to HTTP routes, exposing an SSE streaming chat endpoint that the prebuilt
<vanna-chat>web component consumes. - VannaBase, L72 — the old world, kept alive under
src/vanna/legacy/. The classicask/train/generate_sqlsurface still works there, and LegacyVannaAdapter, L66 wraps an existing 0.x instance so it can serve as the tool registry and memory of a 2.0 agent.
The detailed map: the agent core exposes pluggable interfaces for prompts, storage, caching middleware, hooks, context enrichment, and observability; capability contracts define what a tool, a memory, or a database runner must look like; integrations fill those contracts with real providers; and the legacy layer preserves the 0.x API.
What Vanna Is and Why It Changed
The 0.x design was famously simple: you mixed a vector-store class with an LLM class and got an object that could train() and ask(). Behind the scenes, DDL statements, documentation, and question-SQL pairs were embedded and stored, and at question time the most similar items were retrieved and injected into the prompt. That design made text-to-SQL accessible to anyone who could write two lines of Python.
But production deployments need things a class mixin cannot express cleanly: who is asking, which rows they may see, how results stream to a browser, and how to audit every query. Vanna 2.0 answers those questions structurally. The README’s own summary of the rewrite names the shift: agent-based instead of VannaBase class methods, user-aware at every layer, streaming rich UI components instead of text and dataframes, and web-first with a bundled <vanna-chat> component and server. The MIGRATION_GUIDE.md at the repo root documents the break and offers a genuinely incremental path: wrap your old vn object, ship the new UI, then migrate tool by tool.
The Agent Core: One Orchestrator, Seven Hooks
src/vanna/core/ is the framework’s heart, and its design rewards a close read. The Agent class itself is surprisingly thin — it mostly stores dependencies and defaults. When no conversation store is passed, it imports and instantiates a MemoryConversationStore automatically; when no context enhancer is given, it builds a DefaultLlmContextEnhancer from the agent memory.
The seven documented extension points are each a single abstract base class in its own subpackage: LlmMiddleware, L15 intercepts LLM requests and responses for caching or cost tracking; LifecycleHook, L17 fires at message and tool-execution boundaries for quota checks and logging; ToolContextEnricher, L15 adds data to tool context; LlmContextEnhancer, L16 folds retrieved memories into prompts; ConversationStore, L14 persists history; ObservabilityProvider, L14 collects traces and metrics; and SystemPromptBuilder, L15 shapes the system prompt. Configuration lives in AgentConfig, L113, covering streaming, temperature, UI feature flags, and audit settings. The default workflow handler actually consumes all of it: it runs the agent loop — status updates, tool calls through the registry, streamed UI components — with each phase interceptable by a different hook. A textbook ports-and-adapters layout, applied to an LLM stack.
Tools, Users, and Row-Level Awareness
The capability contracts live in two places: src/vanna/core/tool/ for the tool protocol and src/vanna/capabilities/ for the database-facing interfaces. A tool is generic machinery — arguments, execution, results — while a capability such as SqlRunner, L18 or AgentMemory, L23 describes a need the framework has. Integrations fill those needs: SqliteRunner, L10 for SQLite, MemoryConversationStore for conversations, and ChromaAgentMemory, L42 for embedding-backed memories.
User-awareness is the headline feature, and it flows through identity first. A UserResolver, L14 extracts a User, L12 — an id, an email, and group memberships — from cookies, JWTs, or whatever your app already uses. That user object rides inside every ToolContext, tools declare which access groups they require, and the README documents the intended end state: SQL filtered per user before it ever runs. The RunSqlTool path shows the mechanics: it receives the context, calls the injected runner with it, and leaves enforcement to the runner implementation — where your database-side row-level policies plug in.
The Legacy Layer: Where 0.x Lives On
src/vanna/legacy/ is the entire 0.x codebase, relocated but not abandoned. The abstract base class VannaBase, L72 still declares the classic surface: generate_sql, L93 builds the retrieval-augmented prompt and submits it; ask, L1692 chains SQL generation, execution, and Plotly visualization into one call; train, L1807 accepts DDL, documentation, question-SQL pairs, or a full plan; and the retrieval primitives — get_similar_question_sql, L407, get_related_ddl, L420, get_related_documentation, L433 — define what any vector store must provide. Storage training data flows through add_question_sql, L446, and generate_questions, L356 mines stored pairs for suggested follow-ups.
Two mixes complete the classic stack. ChromaDB_VectorStore, L15 implements the storage side — its add_question_sql, L67 persists a question-SQL pair and its get_similar_question_sql, L237 retrieves neighbors at query time. OpenAI_Chat, L8 implements the prompt side, with a system_message, L44 hook and a submit_prompt, L53 that token-counts and calls the client. Training payloads are shaped by dataclasses in legacy/types/__init__.py — notably TrainingPlanItem, L215 and TrainingPlan, L234 — and the old Flask UI survives as VannaFlaskApp, L1208, with deterministic content-hash ids from deterministic_uuid, L51.
Vector-store choices matter: we have covered Chroma and pgvector in earlier tours, and Vanna’s legacy tree ships connectors for both alongside Marqo, Qdrant, Weaviate, FAISS, and Pinecone.
From Install to Your First Model
The classic flow from the README needs nothing more than pip and an API key:
pip install vanna
Then build an instance by mixing a vector store with an LLM. In the current tree these classes live under the legacy subpackage, so the imports below use those paths:
from vanna.legacy.chromadb.chromadb_vector import ChromaDB_VectorStore
from vanna.legacy.openai.openai_chat import OpenAI_Chat
class MyVanna(ChromaDB_VectorStore, OpenAI_Chat):
def __init__(self, config=None):
ChromaDB_VectorStore.__init__(self, config=config)
OpenAI_Chat.__init__(self, config=config)
vn = MyVanna(config={'api_key': 'sk-...', 'model': 'gpt-4o'})
Next, teach it your schema. The README’s training section shows three kinds of knowledge — DDL statements, business documentation, and sample SQL queries that demonstrate the dialect and conventions you expect back:
vn.train(ddl="CREATE TABLE customers (id INT PRIMARY KEY, name VARCHAR(100), age INT)")
vn.train(documentation="Our business defines XYZ as ...")
vn.train(sql="SELECT name, age FROM customers WHERE name = 'John Doe'")
Finally, connect a database (or set vn.run_sql yourself) and ask a question in plain English — the README’s example is vn.ask("What are the top 10 customers by sales?") — and you get the generated SQL, a results table, and a Plotly chart in one call.
The 2.0 flow trades the mixin for composition, per the README’s production example: create an AnthropicLlmService or OpenAILlmService, register a RunSqlTool over a SqliteRunner (or another database runner), subclass UserResolver to plug in your auth, construct the Agent, and hand a ChatHandler to register_chat_routes on your existing FastAPI app. The frontend is then a single <vanna-chat> tag pointed at the SSE endpoint. If you already have a 0.x instance trained and running, the migration guide’s quick path is to wrap it with LegacyVannaAdapter and serve it through the new agent runtime — no retraining required. The agent core is framework-agnostic, so it also pairs naturally with the ecosystems in our LangChain and FastChat tours.
Honest Limits
The rewrite is real, and so is the disruption. Here is what the source and docs actually say:
- The legacy import paths have drifted.
README_LEGACY.mdstill showsfrom vanna.openai.openai_chat import OpenAI_Chat, but in the current tree those modules live undersrc/vanna/legacy/, and the migration guide itself importsfrom vanna.legacy.adapter import LegacyVannaAdapter. Follow the migration guide’s paths, not the legacy README’s, if you are on a recent build. - Defaults are in-memory. When you construct an
Agentwithout a conversation store, you getMemoryConversationStore— perfect for development, gone on restart. Production deployments need to inject a realConversationStore. - Row-level security is a pattern, not a switch. Identity and groups flow to every tool, but the actual SQL filtering is your job inside a
SqlRunnerimplementation. The README describes the mechanism; your policy is your code. - No independent benchmarks. The repo’s own materials (README, migration guide, papers directory) describe capability, not measured accuracy. Text-to-SQL accuracy depends heavily on your DDL quality, documentation, and example queries — evaluate on your own schema.
- 0.x code is maintained, not developed. Everything under
src/vanna/legacy/exists to keep existing users running and to feed the adapter. New capabilities land in the agent core.
Enjoyed this post? Never miss out on future posts by following us