Query helpers as tools

Subagents do not write SQL directly. They call small, well-named helper functions that take a few typed arguments and return serializable dicts. This keeps the graph code clean and lets you unit test the database layer without spinning up a graph.

db_utils.py
python
def get_customer(customer_id: str) -> dict | None:
    with _conn() as con:
        row = con.execute(
            'SELECT * FROM customers WHERE customer_id = ?', (customer_id,)
        ).fetchone()
        if row is None:
            return None
        record = dict(row)
        record['claims'] = list_claims(customer_id)
        return record


def list_claims(customer_id: str) -> list[dict]:
    with _conn() as con:
        rows = con.execute(
            'SELECT * FROM claims WHERE customer_id = ? ORDER BY opened_at DESC',
            (customer_id,),
        ).fetchall()
        return [dict(r) for r in rows]

get_customer returns the customer row with their claim history nested inside. The billing subagent gets everything it needs from one function call. The claims subagent uses list_claims directly when it does not need customer identity.

db_utils.py
python
from datetime import date


def open_claim(customer_id: str, claim_type: str, amount: float) -> dict:
    """Open a new claim in 'review' status and return it."""
    with _conn() as con:
        row = con.execute('SELECT COUNT(*) AS n FROM claims').fetchone()
        claim_id = f'CLM-{5000 + row["n"] + 1}'
        record = {
            'claim_id': claim_id,
            'customer_id': customer_id,
            'type': claim_type,
            'status': 'review',
            'amount': float(amount),
            'opened_at': date.today().isoformat(),
        }
        con.execute(
            'INSERT INTO claims (claim_id, customer_id, type, status, amount, opened_at) '
            'VALUES (:claim_id, :customer_id, :type, :status, :amount, :opened_at)',
            record,
        )
        return record

New claims always enter in 'review' status. The subagent never gets to mark a claim approved or paid. That is a safety rule: the AI can file a claim on behalf of the user, but approval goes through a human.

The @tool decorator is useful when you want the LLM to choose which function to call from a list. In the supervisor pattern, the LLM does not choose. The supervisor picks the subagent and the subagent decides which helpers to call from code, not from a prompt. That means we get faster, cheaper, more deterministic behavior at the cost of giving up one layer of LLM reasoning we do not actually need here.

Matching exercise: Match each helper to the subagent that uses it

Loading practice…

Checkpoint: Data foundations checkpoint

Loading practice…