DEV Community

Alain Airom
Alain Airom

Posted on

How to Use Vanna.ai to Query Your Database with Open-Source Language Models

Yet another Text2SQL exercise with Vanna.ai, Ollama, Granite and gpt-oss.

Introduction - What is Vanna.ai?

In the rapidly evolving world of data analytics, the search for the perfect Text-to-SQL generation tool often feels like a quest for the ‘Holy Grail’. I recently stumbled upon Vanna.ai, and it immediately stood out. At its core, Vanna.ai is a powerful agent designed to turn natural language questions into data insights. It achieves this by acting as a translator: taking a question from a user, converting it into valid SQL, executing that query against your database, and delivering the answer back in a rich format. More than just a simple query generator, Vanna 2.0 seems to be built for production, offering features like user-aware security, streaming responses (including interactive data tables and charts), and seamless integration with virtually any LLM (including open-source models like Granite and gpt-oss (among others…) and (almost) any database.

As I mentioned, Vanna.ai seems to be fit for more than Text2SQL… but I stick to this part for my tests. You can refer to their site provided in “Links” to discover more. Last but not least; I have no affiliation with them!

Implementation and Tests

The magic of Vanna’s solution resides in their powerful, agent-based package, which handles everything from authenticating the user to selecting the right tools — like the RunSqlTool—to safely query the database and generate the final visualization. This robust architecture is the foundation of the sample application we'll be exploring from their site. So, let's jump into the test and coding steps!

To properly test Vanna’s capabilities and demonstrate its Text-to-SQL functionality, we first need a working environment with a database containing meaningful data. For maximum simplicity and portability, I chose to create and populate a SQLite database. As a file-based SQL engine, SQLite eliminates the need for a separate server setup, making it the fastest and easiest way to get our Users and Commands tables ready for the Vanna agent to query.

So let’s jump into coding… 🪂

Prepare a / your Database

  • Preparing the environment 🧑‍🍳
python3 -m venv venv
source venv/bin/activate

pip install --upgrade pip

# to populate and generate data this package is nice!
pip install faker
Enter fullscreen mode Exit fullscreen mode
  • Create your tables in SQLite
# setup_database.py
import sqlite3
import os

# Define the database file name
DB_NAME = 'users_commands.db'

def setup_database():
    """
    Creates the SQLite database file and defines the Users and Commands tables.
    """
    if os.path.exists(DB_NAME):
        os.remove(DB_NAME)

    conn = sqlite3.connect(DB_NAME)
    cursor = conn.cursor()

    cursor.execute('''
        CREATE TABLE Users (
            ID INTEGER PRIMARY KEY,
            FirstName TEXT NOT NULL,
            LastName TEXT NOT NULL,
            DateOfBirth TEXT NOT NULL, 
            Address TEXT,
            CommandList TEXT UNIQUE NOT NULL
        )
    ''')

    cursor.execute('''
        CREATE TABLE Commands (
            ProductID TEXT PRIMARY KEY,
            ProductName TEXT NOT NULL,
            CommandNumber TEXT NOT NULL,
            FOREIGN KEY (CommandNumber) REFERENCES Users(CommandList)
        )
    ''')

    conn.commit()
    conn.close()
    print(f"✅ Successfully created database '{DB_NAME}' and tables 'Users' and 'Commands'.")

if __name__ == "__main__":
    setup_database()
Enter fullscreen mode Exit fullscreen mode
  • Insert some data in the DB!
# insert_data.py
import sqlite3
from faker import Faker
import random
import uuid

DB_NAME = 'users_commands.db'

def insert_random_data(num_records=10):
    """
    Generates and inserts random user and command data into the database.
    """
    try:
        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()
        fake = Faker()

        user_data = []
        command_data = []

        for _ in range(num_records):
            command_ref = str(uuid.uuid4())

            user_data.append((
                fake.first_name(),
                fake.last_name(),
                fake.date_of_birth(minimum_age=18, maximum_age=65).strftime('%Y-%m-%d'),
                fake.address().replace('\n', ', '), 
                command_ref # The link field
            ))

            product_id = f"PROD-{random.randint(1000, 9999)}" 
            product_name = fake.word().capitalize() + " " + fake.word().capitalize()

            command_data.append((
                product_id,
                product_name,
                command_ref 
            ))

        cursor.executemany('''
            INSERT INTO Users (FirstName, LastName, DateOfBirth, Address, CommandList)
            VALUES (?, ?, ?, ?, ?)
        ''', user_data)

        cursor.executemany('''
            INSERT INTO Commands (ProductID, ProductName, CommandNumber)
            VALUES (?, ?, ?)
        ''', command_data)

        conn.commit()
        print(f"✅ Successfully inserted {len(user_data)} random records into **'Users'** and 'Commands'.")

    except sqlite3.Error as e:
        print(f"An error occurred: {e}")
    finally:
        if conn:
            conn.close()

if __name__ == "__main__":
    insert_random_data(10) # Insert 10 lines of data!!!
Enter fullscreen mode Exit fullscreen mode
  • Test and Query the Database 🧪
# query_database.py
import sqlite3
import os
from typing import List, Tuple

DB_NAME = 'users_commands.db'
OUTPUT_DIR = 'output'
REPORT_PATH = os.path.join(OUTPUT_DIR, 'database_report.md')


def format_table_data(title: str, headers: List[str], data: List[Tuple], is_markdown: bool, col_widths: List[int] = None) -> str:
    """Formats data into a printable string, either for console or Markdown."""
    output = ""

    if is_markdown:
        output += f"## {title}\n\n"

        # Markdown Header Row
        output += "| " + " | ".join(headers) + " |\n"
        # Markdown Separator Row
        output += "|-" + "-|-".join(['-' * len(h) for h in headers]) + "-|\n"

        # Markdown Data Rows
        for row in data:
            # Ensure all elements are strings for joining
            str_row = [str(col) for col in row]
            output += "| " + " | ".join(str_row) + " |\n"
        output += "\n"

    else: # Console Formatting
        output += "-" * 70 + "\n"
        output += f"--- {title} ---\n"
        output += "-" * 70 + "\n"

        if not data:
            output += "The table is empty.\n"
            return output

        if not col_widths or len(col_widths) != len(headers):
            col_widths = [len(h) for h in headers] # Simple fallback

        header_row = ""
        for i, header in enumerate(headers):
            width = [4, 15, 15, 12, 25, 36][i] if len(headers) == 6 else [15, 15, 25, 15][i]
            header_row += f"{header:<{width}} | "
        output += header_row.strip() + "\n"
        output += "-" * (len(header_row) + 5) + "\n"

        for row in data:
            data_row = ""
            for i, item in enumerate(row):
                width = [4, 15, 15, 12, 25, 36][i] if len(headers) == 6 else [15, 15, 25, 15][i]

                display_item = str(item)
                if i == 4 and len(headers) == 6: # Address column in Users table
                    display_item = display_item[:width - 3] + '...' if len(display_item) > width else display_item

                data_row += f"{display_item:<{width}} | "

            output += data_row.strip() + "\n"
        output += "\n"

    return output

def query_and_display_data():
    """
    Connects to the database, queries the data, prints to console, 
    and saves the results to a Markdown file.
    """
    conn = None
    try:
        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()

        cursor.execute("SELECT ID, FirstName, LastName, DateOfBirth, Address, CommandList FROM Users")
        users_data = cursor.fetchall()
        users_headers = ['ID', 'First Name', 'Last Name', 'DOB', 'Address', 'CommandList Ref']

        if not users_data:
            console_output = "The Users table is empty. Please ensure 'insert_data.py' was run."
            markdown_content = "# Database Report\n\n## Users and Commands Data\n\n" + console_output
        else:
            join_query = """
                SELECT
                    U.FirstName,
                    U.LastName,
                    C.ProductName,
                    C.ProductID
                FROM
                    Users U
                INNER JOIN
                    Commands C ON U.CommandList = C.CommandNumber
                ORDER BY
                    U.ID;
            """
            cursor.execute(join_query)
            joined_data = cursor.fetchall()
            joined_headers = ['First Name', 'Last Name', 'Product Name', 'Product ID']

            console_output = format_table_data("1. Listing All Records from the 'Users' Table", users_headers, users_data, is_markdown=False)
            console_output += "\n" * 2
            console_output += format_table_data("2. Listing Joined Data ('User' and 'Command') using INNER JOIN", joined_headers, joined_data, is_markdown=False)

            markdown_content = "# Database Query Report\n\n"
            markdown_content += format_table_data("User Records (Users Table)", users_headers, users_data, is_markdown=True)
            markdown_content += "\n---\n\n"
            markdown_content += format_table_data("User Commands (Joined Data)", joined_headers, joined_data, is_markdown=True)


        # Console Display
        print(console_output)

        # File Save
        os.makedirs(OUTPUT_DIR, exist_ok=True)

        with open(REPORT_PATH, 'w') as f:
            f.write(markdown_content)

        print(f"\n✅ Successfully saved database report to: {REPORT_PATH}")

    except sqlite3.Error as e:
        print(f"\n❌ Database Error: {e}")
        print(f"Please ensure the database file '{DB_NAME}' exists and the tables were correctly set up.")
    except Exception as e:
        print(f"\n❌ An unexpected error occurred: {e}")
    finally:
        if conn:
            conn.close()

if __name__ == "__main__":
    query_and_display_data()
Enter fullscreen mode Exit fullscreen mode
  • Populate and build your database.
python setup_database.py
python insert_data.py
python query_database.py
Enter fullscreen mode Exit fullscreen mode
  • This will give you both a console output and a markdown file.
> python query_database.py
----------------------------------------------------------------------
--- 1. Listing All Records from the 'Users' Table ---
----------------------------------------------------------------------
ID   | First Name      | Last Name       | DOB          | Address                   | CommandList Ref                      |
----------------------------------------------------------------------------------------------------------------------------------
1    | David           | Frank           | 1983-03-22   | 3973 Carmen Gateway Su... | 173e986d-77b0-4c6e-9314-52813419f108 |
2    | Sydney          | Middleton       | 1981-09-24   | 4080 Regina Lake Apt. ... | 5bd1cf9a-2cfc-46e5-b150-77c368efaccb |
3    | Joshua          | Holder          | 2000-06-19   | 0072 Sanchez Hollow, P... | 68009684-34a9-4be0-8f67-ef26155d6cfd |
4    | Robert          | Walker          | 1968-01-10   | USS Lee, FPO AP 42065     | dc60fd66-f59b-486b-8f04-4162bdf9c99e |
5    | Douglas         | Johnson         | 1999-08-18   | 51189 Jacqueline Shore... | 80c13c6f-c5f7-4df7-abfe-f91ced3e1237 |
6    | James           | Hernandez       | 1976-10-28   | Unit 7218 Box 1614, DP... | 9b4dee77-37ad-4fa4-8672-a7c053e71429 |
7    | Diana           | Thomas          | 2004-02-29   | 84300 Vaughn Crossroad... | 7062b085-18b4-40c3-8e4e-c3bd6c214e08 |
8    | Jamie           | Torres          | 1961-06-22   | 857 Pennington Flats S... | 31725920-e0c3-4fac-bd42-31d113e6e107 |
9    | Renee           | Rice            | 2006-09-04   | 963 Marvin Underpass, ... | 8536b45b-3502-480a-a9be-89189e68478e |
10   | Henry           | Wallace         | 1962-04-18   | 79093 Danielle Haven S... | 83c466e4-239f-4844-8a79-6286be0edb30 |



----------------------------------------------------------------------
--- 2. Listing Joined Data ('User' and 'Command') using INNER JOIN ---
----------------------------------------------------------------------
First Name      | Last Name       | Product Name              | Product ID      |
---------------------------------------------------------------------------------------
David           | Frank           | Wife Choose               | PROD-3937       |
Sydney          | Middleton       | Produce Fall              | PROD-5279       |
Joshua          | Holder          | Plan Sell                 | PROD-5104       |
Robert          | Walker          | Interesting Guy           | PROD-3216       |
Douglas         | Johnson         | Time At                   | PROD-8672       |
James           | Hernandez       | Someone Mouth             | PROD-4010       |
Diana           | Thomas          | Pressure Move             | PROD-3587       |
Jamie           | Torres          | It Every                  | PROD-8192       |
Renee           | Rice            | Writer Still              | PROD-7510       |
Henry           | Wallace         | Professional Scene        | PROD-7318       |



✅ Successfully saved database report to: output/database_report.md
Enter fullscreen mode Exit fullscreen mode
  • Also, I made a GUI interface to see my database and the data (pretty hard-coded stuff, but we get the idea) 🈁
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SQLite Database Browser (Users & Commands)</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
    <style>
        body { font-family: 'Inter', sans-serif; background-color: #f7f9fb; }
        .container { max-width: 1200px; }
        .table-container { max-height: 400px; overflow-y: auto; background-color: white; border-radius: 0.5rem; }
        .sql-textarea { font-family: monospace; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }
        th { background-color: #1e40af; color: white; position: sticky; top: 0; }
        .btn { transition: background-color 0.2s; }
        .btn:hover { filter: brightness(1.1); }
    </style>
</head>
<body class="p-6">

    <div id="app" class="container mx-auto space-y-8">
        <h1 class="text-4xl font-bold text-gray-800 border-b-4 border-indigo-600 pb-2">Database Viewer Interface</h1>

        <div class="bg-white p-6 rounded-xl shadow-lg flex flex-col md:flex-row justify-between items-start md:items-center space-y-4 md:space-y-0">
            <div id="status" class="text-lg font-semibold text-green-700">Database initializing...</div>
            <button id="resetDbBtn" class="btn bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg shadow-md" onclick="initDb(true)">
                Reset Database & Reload Data
            </button>
        </div>

        <!-- Predefined Queries -->
        <div class="bg-white p-6 rounded-xl shadow-lg space-y-4">
            <h2 class="text-2xl font-semibold text-gray-700">Predefined Queries</h2>
            <div class="flex flex-wrap gap-4">
                <button class="btn bg-indigo-500 hover:bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md" onclick="displayUsers()">
                    Show All Users
                </button>
                <button class="btn bg-indigo-500 hover:bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md" onclick="displayJoinedData()">
                    Show Users & Commands (JOIN)
                </button>
            </div>
        </div>

        <div class="bg-white p-6 rounded-xl shadow-lg space-y-4">
            <h2 class="text-2xl font-semibold text-gray-700">Custom SQL Query</h2>
            <textarea id="sqlInput" class="sql-textarea w-full p-3 border-2 border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500" rows="4" placeholder="e.g., SELECT * FROM Users WHERE FirstName = 'Alex'"></textarea>
            <button class="btn bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-lg shadow-md" onclick="executeCustomQuery()">
                Execute Query
            </button>
        </div>

        <div class="space-y-4">
            <h2 id="resultTitle" class="text-3xl font-bold text-gray-800">Query Results</h2>
            <div id="results" class="table-container border-2 border-gray-200">
                <p class="p-4 text-gray-500">Run a query above to see results.</p>
            </div>
        </div>

    </div>

    <script>
        let db = null;
        let SQL = null;
        const DB_NAME = 'users_commands.db';
        const NUM_RECORDS = 10;
        const resultDiv = document.getElementById('results');
        const resultTitle = document.getElementById('resultTitle');
        const statusDiv = document.getElementById('status');

        const MOCK_DATA = {
            firstNames: ["Alex", "Bella", "Chris", "Dana", "Ethan", "Fiona", "George", "Hannah", "Ivan", "Jasmine"],
            lastNames: ["Smith", "Jones", "Williams", "Brown", "Davis", "Miller", "Wilson", "Moore", "Taylor", "Anderson"],
            addresses: [
                "123 Main St, Anytown", "45 Oak Ave, Smallville", "78 Pine Ln, Big City",
                "90 Maple Rd, Suburbia", "11 Elm Dr, Metropolis", "22 Birch Blvd, Village",
                "33 Cedar Ct, Hamlet", "44 Spruce Sq, Town", "55 Willow Wy, County",
                "66 Poplar Pk, District"
            ],
            productNames: ["Laptop", "Monitor", "Keyboard", "Mouse", "Webcam", "Headset", "Router", "Speaker", "Printer", "Scanner"]
        };

        function generateRandomData() {
            const data = [];
            for (let i = 0; i < NUM_RECORDS; i++) {
                const commandRef = crypto.randomUUID(); // Unique link ID

                const year = 1960 + Math.floor(Math.random() * 30);
                const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
                const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0');
                const dob = `${year}-${month}-${day}`;

                data.push({
                    firstName: MOCK_DATA.firstNames[i],
                    lastName: MOCK_DATA.lastNames[i],
                    dob: dob,
                    address: MOCK_DATA.addresses[i],
                    commandList: commandRef,
                    // Command data
                    productID: `PROD-${Math.floor(Math.random() * 9000) + 1000}`,
                    productName: MOCK_DATA.productNames[i] + ' Pro',
                    commandNumber: commandRef
                });
            }
            return data;
        }

        async function initDb(reset = false) {
            statusDiv.textContent = "Loading WebAssembly SQLite engine...";
            try {
                if (!SQL) {
                    SQL = await initSqlJs({ locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}` });
                }

                if (db) {
                    db.close();
                }

                db = new SQL.Database();

                // --- A. Create Users Table ---
                db.run(`
                    CREATE TABLE Users (
                        ID INTEGER PRIMARY KEY,
                        FirstName TEXT NOT NULL,
                        LastName TEXT NOT NULL,
                        DateOfBirth TEXT NOT NULL, 
                        Address TEXT,
                        CommandList TEXT UNIQUE NOT NULL
                    );
                `);

                db.run(`
                    CREATE TABLE Commands (
                        ProductID TEXT PRIMARY KEY,
                        ProductName TEXT NOT NULL,
                        CommandNumber TEXT NOT NULL,
                        FOREIGN KEY (CommandNumber) REFERENCES Users(CommandList)
                    );
                `);

                const mockData = generateRandomData();
                mockData.forEach(item => {
                    db.run("INSERT INTO Users (FirstName, LastName, DateOfBirth, Address, CommandList) VALUES (?, ?, ?, ?, ?)",
                        [item.firstName, item.lastName, item.dob, item.address, item.commandList]);

                    db.run("INSERT INTO Commands (ProductID, ProductName, CommandNumber) VALUES (?, ?, ?)",
                        [item.productID, item.productName, item.commandNumber]);
                });

                statusDiv.textContent = reset 
                    ? `✅ Database Reset & ${NUM_RECORDS} records inserted successfully!`
                    : `✅ Database initialized with ${NUM_RECORDS} records.`;
                statusDiv.classList.remove('text-red-700');
                statusDiv.classList.add('text-green-700');

                displayUsers();

            } catch (error) {
                statusDiv.textContent = `❌ Error initializing database: ${error.message}`;
                statusDiv.classList.remove('text-green-700');
                statusDiv.classList.add('text-red-700');
                console.error("DB Initialization Error:", error);
            }
        }


        function renderResults(results) {
            resultDiv.innerHTML = '';

            if (!results || results.length === 0) {
                resultDiv.innerHTML = '<p class="p-4 text-orange-500">Query executed successfully, but returned no rows.</p>';
                return;
            }

            const table = document.createElement('table');
            table.classList.add('min-w-full', 'divide-y', 'divide-gray-200');

            const thead = document.createElement('thead');
            const headerRow = document.createElement('tr');
            results[0].columns.forEach(col => {
                const th = document.createElement('th');
                th.textContent = col;
                headerRow.appendChild(th);
            });
            thead.appendChild(headerRow);
            table.appendChild(thead);

           const tbody = document.createElement('tbody');
            results[0].values.forEach(row => {
                const tr = document.createElement('tr');
                row.forEach(cell => {
                    const td = document.createElement('td');
                    td.textContent = cell === null ? 'NULL' : cell;
                    tr.appendChild(td);
                });
                tbody.appendChild(tr);
            });
            table.appendChild(tbody);

            resultDiv.appendChild(table);
        }

        function runQuery(sql, title) {
            resultTitle.textContent = title;
            try {
                if (!db) {
                    resultDiv.innerHTML = '<p class="p-4 text-red-500">Database not initialized. Please click "Reset Database & Reload Data".</p>';
                    return;
                }
                const results = db.exec(sql);
                renderResults(results);
            } catch (error) {
                resultDiv.innerHTML = `<div class="p-4 bg-red-100 text-red-700 rounded-lg">❌ SQL Error: ${error.message}</div>`;
            }
        }


        function displayUsers() {
            const sql = "SELECT ID, FirstName, LastName, DateOfBirth, Address, CommandList FROM Users";
            runQuery(sql, "Table: Users (All Fields)");
        }

        function displayJoinedData() {
            const sql = `
                SELECT
                    U.ID,
                    U.FirstName,
                    U.LastName,
                    C.ProductName,
                    C.ProductID,
                    U.CommandList as CommandRef
                FROM
                    Users U
                INNER JOIN
                    Commands C ON U.CommandList = C.CommandNumber
                ORDER BY
                    U.ID;
            `;
            runQuery(sql, "Joined Data: Users linked to Commands");
        }

        function executeCustomQuery() {
            const sql = document.getElementById('sqlInput').value.trim();
            if (!sql) {
                resultDiv.innerHTML = '<p class="p-4 text-orange-500">Please enter an SQL query to execute.</p>';
                return;
            }
            runQuery(sql, "Custom Query Results");
        }

        window.onload = () => {
            initDb();
        };

    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Text-2-SQL Tests

Now that the database and our data are readyn we will start using the solution.

  • What is extremely convenient is the way Vanna.ai supports setup: depending on the specific LLM provider (like Ollama) and database type (like SQLite) you select, their documentation provides the exact installation requirements and configuration snippets you need, making the initial setup frictionless.
pip install 'vanna[fastapi,httpx,ollama]'
Enter fullscreen mode Exit fullscreen mode

  • The site provides the full sample code to build an application, just copy/paste it!
# vanna-ai-app.py
# All imports at the top
from vanna import Agent
from vanna.core.registry import ToolRegistry
from vanna.core.user import UserResolver, User, RequestContext
from vanna.tools import RunSqlTool, VisualizeDataTool
from vanna.tools.agent_memory import SaveQuestionToolArgsTool, SearchSavedCorrectToolUsesTool, SaveTextMemoryTool
from vanna.servers.fastapi import VannaFastAPIServer
from vanna.integrations.ollama import OllamaLlmService
from vanna.integrations.sqlite import SqliteRunner
from vanna.integrations.local.agent_memory import DemoAgentMemory

# Configure your LLM
llm = OllamaLlmService(
    model="gpt-oss:latest",
    host="http://localhost:11434"
)

# Configure your database
db_tool = RunSqlTool(
    sql_runner=SqliteRunner(database_path="./users_commands.db")
)

# Configure your agent memory
agent_memory = DemoAgentMemory(max_items=1000)

# Configure user authentication
class SimpleUserResolver(UserResolver):
    async def resolve_user(self, request_context: RequestContext) -> User:
        user_email = request_context.get_cookie('vanna_email') or 'guest@example.com'
        group = 'admin' if user_email == 'admin@example.com' else 'user'
        return User(id=user_email, email=user_email, group_memberships=[group])

user_resolver = SimpleUserResolver()

# Create your agent
tools = ToolRegistry()
tools.register_local_tool(db_tool, access_groups=['admin', 'user'])
tools.register_local_tool(SaveQuestionToolArgsTool(), access_groups=['admin'])
tools.register_local_tool(SearchSavedCorrectToolUsesTool(), access_groups=['admin', 'user'])
tools.register_local_tool(SaveTextMemoryTool(), access_groups=['admin', 'user'])
tools.register_local_tool(VisualizeDataTool(), access_groups=['admin', 'user'])

agent = Agent(
    llm_service=llm,
    tool_registry=tools,
    user_resolver=user_resolver,
    agent_memory=agent_memory
)

# Run the server
server = VannaFastAPIServer(agent)
server.run()  # Access at http://localhost:8000
Enter fullscreen mode Exit fullscreen mode
  • The sample application from the site uses “gpt-oss” LLM, but you can put whatever LLM you choose. I tested both “granite” and “gpt-oss”.
# Configure your LLM
llm = OllamaLlmService(
    model="granite4:latest",
    host="http://localhost:11434"
)
Enter fullscreen mode Exit fullscreen mode
  • As the interface is generated by “FastAPI” the defaut port number is “http://localhost:8000” but you can adapt it to your needs and for instance run different instance by changing the port number.
# Run the server
server = VannaFastAPIServer(agent)

# Setting port=9000 
server.run(port=9000)
Enter fullscreen mode Exit fullscreen mode
  • During my initial testing, I found that running queries using both the Granite and GPT-OSS models consistently yielded accurate answers and correctly generated SQL queries. While I wasn’t aiming to conduct rigorous, side-by-side benchmarking, this functionality test confirms the core power of using Vanna.ai with these locally hosted open-source models. The primary goal was to validate the integration and core functionality, and in that regard, the results were highly successful. We leave it to the user community to dive deeper into performance comparisons, extended tests, and complex benchmarking scenarios. The first screens come from “gpt-oss”.

  • The following are generated queries and outputs from granite 🪨

  • The output ⬇️

  • A very handy feature is that every query generates a CSV file, meaning the user not only gets the beautifully presented output on the UI but also receives a tangible, exportable data file as a direct result. We leave it to the user community to dive deeper into performance comparisons, extended tests, and complex benchmarking scenarios.
ID,FirstName,LastName,DateOfBirth,Address,CommandList
10,Henry,Wallace,1962-04-18,"79093 Danielle Haven Suite 823, New Kevinton, PR 23752",83c466e4-239f-4844-8a79-6286be0edb30
Enter fullscreen mode Exit fullscreen mode
ID,FirstName,LastName,DateOfBirth,Address,CommandList
1,David,Frank,1983-03-22,"3973 Carmen Gateway Suite 371, Timothytown, NY 51959",173e986d-77b0-4c6e-9314-52813419f108
2,Sydney,Middleton,1981-09-24,"4080 Regina Lake Apt. 187, Kathrynchester, MN 66872",5bd1cf9a-2cfc-46e5-b150-77c368efaccb
3,Joshua,Holder,2000-06-19,"0072 Sanchez Hollow, Port Jackfurt, NC 88639",68009684-34a9-4be0-8f67-ef26155d6cfd
4,Robert,Walker,1968-01-10,"USS Lee, FPO AP 42065",dc60fd66-f59b-486b-8f04-4162bdf9c99e
5,Douglas,Johnson,1999-08-18,"51189 Jacqueline Shores, East Denise, ME 11103",80c13c6f-c5f7-4df7-abfe-f91ced3e1237
6,James,Hernandez,1976-10-28,"Unit 7218 Box 1614, DPO AP 28470",9b4dee77-37ad-4fa4-8672-a7c053e71429
7,Diana,Thomas,2004-02-29,"84300 Vaughn Crossroad, Port Sharon, VA 67416",7062b085-18b4-40c3-8e4e-c3bd6c214e08
8,Jamie,Torres,1961-06-22,"857 Pennington Flats Suite 672, Rushville, DC 37245",31725920-e0c3-4fac-bd42-31d113e6e107
9,Renee,Rice,2006-09-04,"963 Marvin Underpass, Rebeccachester, KS 45025",8536b45b-3502-480a-a9be-89189e68478e
10,Henry,Wallace,1962-04-18,"79093 Danielle Haven Suite 823, New Kevinton, PR 23752",83c466e4-239f-4844-8a79-6286be0edb30
Enter fullscreen mode Exit fullscreen mode

That’s a wrap! ✌️

Conclusion

In conclusion, I guess these tests validated Vanna.ai’s strength as a comprehensive Text-to-SQL solution, moving beyond simple query generation. This entire setup, running within a custom-ported FastAPI server, demonstrated core functionalities: accurate SQL generation from natural language questions, seamless execution against a database tables, rich visual output on the UI, and the practical utility of immediate CSV export. The ease of configuration, coupled with the accuracy achieved using either local or cloud based models with a variety of database engines. I really give a “thumb-up” to Venna.ai’s solution.

Thanks for reading! 🤗

Links

Top comments (0)