LangChain Mastery

Course Content

LangChain Mastery

7 sections · 109 lessons

Write a function to integrate a LangChain agent with a database tool.


Safety rails around a text-to-SQL tool, strongest firstRead-only role with row-level securityTable allowlist: orders, stores, productsStatement timeout of 5 secondsLIMIT 100 wrapped around every queryPrompt rule and SELECT check: advice only
The prototype's DELETE ran because only the prompt said no; the database role is the only layer a model cannot talk its way past.

What you need to know

Two designs

Narrow, parameterised tools

  • orders_by_status(status, days)
  • Fixed SQL, model fills parameters only
  • Safe and fast; cannot answer new questions
  • Best for the top 80% of questions

Text-to-SQL tool

  • run_sql(query) — the model writes SQL
  • Flexible: any question over allowed tables
  • Needs strong guard rails
  • Best for analysts and internal tools

Many production systems use both: narrow tools for customers, text-to-SQL only for trusted internal users.

The code

Python
from langchain_community.utilities import SQLDatabasefrom langchain.agents import create_agentfrom langchain_core.tools import tool, ToolException# Connect as a read-only role with a 5-second statement timeout set on the role.db = SQLDatabase.from_uri(    "postgresql+psycopg://analyst_ro:***@db/sales",    include_tables=["orders", "stores", "products"],   # allowlist    sample_rows_in_table_info=2,)@tooldef orders_by_status(status: str, days: int = 7) -> str:    """Count orders with a status ('delivered', 'returned', 'cancelled') in the last N days."""    return str(db.run(        "SELECT status, count(*) FROM orders WHERE status = :s "        "AND created_at > now() - make_interval(days => :d) GROUP BY status",        parameters={"s": status, "d": days}))@tooldef run_sql(query: str) -> str:    """Run one read-only SELECT on orders, stores, products. Returns at most 100 rows."""    if not query.lstrip().lower().startswith(("select", "with")):        raise ToolException("Only SELECT queries are allowed.")    try:        return str(db.run(f"SELECT * FROM ({query.rstrip(';')}) AS q LIMIT 100"))    except Exception as e:        raise ToolException(f"SQL error: {e}. Fix the query and try once more.")orders_by_status.handle_tool_error = run_sql.handle_tool_error = Truedef build_sql_agent(model):    return create_agent(model, tools=[orders_by_status, run_sql],        system_prompt="You answer sales questions. Schema:\n" + db.get_table_info()                      + "\nPrefer orders_by_status when it fits.")

Why each piece is there

  • Read-only role — the real protection. Even if the model writes DELETE, the database refuses it. The startswith("select") check only gives a friendlier error.
  • include_tables — the model only sees and can only query the allowed tables. Keep salary or customer-PII tables out.
  • Schema in the prompt — get_table_info() returns the CREATE TABLE statements plus 2 sample rows, so the model writes correct column names first time.
  • LIMIT 100 — one careless SELECT * could return a million rows into the context window.
  • SQL errors returned to the model — a wrong column name comes back as a message, and the model usually fixes it on the next try.
  • Parameters, not string formatting — in the narrow tool, values go in as bind parameters, which prevents SQL injection.

SQLDatabaseToolkit from langchain-community gives a ready set of tools (list tables, get schema, check query, run query) if you prefer not to hand-roll them. create_sql_agent builds a legacy AgentExecutor, so for new code pass the toolkit's tools to create_agent.

Row-level access

If users may only see their own store's data, enforce that in the database with row-level security or a view filtered by the session's store ID. Never rely on "only query store BLR-04" in the prompt.

A real-life example

An electronics chain gives its 40 store managers a chat assistant over sales data. The first prototype connected as the application's main database user. In testing, a question phrased as "clear out last week's test orders" produced a DELETE statement — and it ran.

The rebuild uses a role with only SELECT on three tables, a 5-second statement_timeout, row-level security so each manager sees only their store, and the two tools above. About 70% of questions now go to orders_by_status, answering in about 2 seconds. The rest use run_sql; 12% of those need a second try after a SQL error, which the agent handles on its own.

Follow-up questions to expect

  • "How do you stop SQL injection?" — Bind parameters in fixed queries, and for generated SQL rely on a read-only role and allowlisted tables, not string checks.
  • "The model keeps using wrong column names. What do you do?" — Put the schema and sample rows in the prompt, add column descriptions, and return SQL errors to the model.
  • "How do you handle huge tables?" — Force a LIMIT, set a statement timeout, and prefer aggregate queries; summarise results before returning them.