AI Agents That Speak SQL: Text-to-SQL with Hugging Face smolagents Hugging Face's smolagents library enables AI agents to generate and execute SQL queries against databases, using a code-based reasoning approach that allows error correction and verification. The agent writes Python snippets to interact with the database, observe results, and refine its actions, overcoming the fragility of single-pass text-to-SQL pipelines. 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.