# AI Agents That Speak SQL: Text-to-SQL with Hugging Face smolagents

> Source: <https://dev.to/marymar_danytzacallotico/agentes-de-ia-que-hablan-sql-text-to-sql-con-hugging-face-smolagents-5gmd>
> Published: 2026-07-09 23:19:35+00:00

The simplest way to connect an LLM to a database is a single-pass pipeline: the user writes a question in natural language, the model generates a SQL query, and that query is executed directly against the database. This is, for example, the approach followed in freeCodeCamp's tutorial *"How to Talk to Any Database Using AI"*, where a Python script builds a prompt with the table schema and asks a model to return raw SQL.

The problem is that this pipeline is fragile. If the model generates a query with a syntax error, or worse, a syntactically valid but semantically incorrect query, there's nobody reviewing the result before showing it to the user. There's no error correction, no verification that the answer actually makes sense.

That's where **agents** come in. Instead of a model that just "translates" text into SQL, an agent can execute the query, observe the result, and decide whether it needs to fix something before responding. That's exactly what `smolagents`

, Hugging Face's library for building agents with very little code, does.

`smolagents`

is an open source library from Hugging Face designed so an agent "thinks in code": instead of the LLM returning a JSON blob describing the action to take, it writes actual Python snippets that the agent executes step by step, observing the result before continuing. This pattern is called `CodeAgent`

and follows the ReAct framework (reason → act → observe).

Official repository: [https://github.com/huggingface/smolagents](https://github.com/huggingface/smolagents)

``` python
from sqlalchemy import (
    create_engine, MetaData, Table, Column,
    String, Integer, Float, insert, inspect, text
)

engine = create_engine("sqlite:///:memory:")
metadata_obj = MetaData()

# Define the "receipts" table
table_name = "receipts"
receipts = Table(
    table_name,
    metadata_obj,
    Column("receipt_id", Integer, primary_key=True),
    Column("customer_name", String(16), primary_key=True),
    Column("price", Float),
    Column("tip", Float),
)
metadata_obj.create_all(engine)

# Insert sample data
rows = [
    {"receipt_id": 1, "customer_name": "Alan Payne", "price": 12.06, "tip": 1.20},
    {"receipt_id": 2, "customer_name": "Alex Mason", "price": 23.86, "tip": 0.24},
    {"receipt_id": 3, "customer_name": "Woodrow Wilson", "price": 53.43, "tip": 5.43},
    {"receipt_id": 4, "customer_name": "Margaret James", "price": 21.11, "tip": 1.00},
]
for row in rows:
    stmt = insert(receipts).values(**row)
    with engine.begin() as connection:
        connection.execute(stmt)
inspector = inspect(engine)
columns_info = [(col["name"], col["type"]) for col in inspector.get_columns("receipts")]

table_description = "Columns:\n" + "\n".join(
    [f"  - {name}: {col_type}" for name, col_type in columns_info]
)
print(table_description)
```

This gives us something like:

```
Columns:
  - receipt_id: INTEGER
  - customer_name: VARCHAR(16)
  - price: FLOAT
  - tip: FLOAT
```

In smolagents, a tool is just a Python function decorated with `@tool`

. The docstring is key: the agent reads it to know when and how to use the tool.

``` php
from smolagents import tool

@tool
def sql_engine(query: str) -> str:
    """
    Allows you to perform SQL queries on the table 'receipts'.
    Returns a string representation of the result.
    The table has the following columns:
    Columns:
      - receipt_id: INTEGER
      - customer_name: VARCHAR(16)
      - price: FLOAT
      - tip: FLOAT

    Args:
        query: the SQL query to execute. Must be valid SQL.
    """
    output = ""
    with engine.connect() as con:
        rows = con.execute(text(query))
        for row in rows:
            output += "\n" + str(row)
    return output
python
from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[sql_engine],
    model=InferenceClientModel(model_id="meta-llama/Llama-3.1-8B-Instruct"),
)

agent.run("Can you give me the name of the client who got the most expensive receipt?")
```

The agent doesn't just generate the SQL: it executes it, reads the result, and if something doesn't add up (say, a column that doesn't exist), it can retry with a corrected query before giving you the final answer. That's the real difference compared to the single-pass pipeline.

The same pattern holds as the schema grows. We add a second table for waiters and update the tool's description:

```
table_name = "waiters"
waiters = Table(
    table_name,
    metadata_obj,
    Column("receipt_id", Integer, primary_key=True),
    Column("waiter_name", String(16), primary_key=True),
)
metadata_obj.create_all(engine)

rows = [
    {"receipt_id": 1, "waiter_name": "Corey Johnson"},
    {"receipt_id": 2, "waiter_name": "Michael Watts"},
    {"receipt_id": 3, "waiter_name": "Michael Watts"},
    {"receipt_id": 4, "waiter_name": "Margaret James"},
]
for row in rows:
    stmt = insert(waiters).values(**row)
    with engine.begin() as connection:
        connection.execute(stmt)

agent.run("Which waiter got the most tips in total?")
```

The agent now has to reason about a JOIN between `receipts`

and `waiters`

, and it does so without us changing a single line of the agent's logic — only the tool's description was updated.

If you want to see this pattern taken into a project with production-style structure (REST API, result validation, explicit self-correction), this repository implements exactly that idea on top of smolagents:

[https://github.com/Sakeeb91/text2sql-agent](https://github.com/Sakeeb91/text2sql-agent)

There, the flow is: the agent inspects the schema, generates the SQL, validates that the result makes sense, and if it detects a problem, self-corrects before answering — all exposed behind an API with JWT or API key authentication.

The "classic" Text-to-SQL pipeline (like freeCodeCamp's) is a great starting point for understanding the problem, but it falls short in production as soon as questions get more complex. Wrapping the SQL query as a tool inside a smolagents `CodeAgent`

gives the system the ability to review its own work, which translates into fewer silently wrong answers — which is, in the end, the worst kind of error a system that talks to your database can make.
