Back to Blog
This post is Part 2 of 18 in the series: Agentic Commerce with ADK 2 — Building & Shipping CartPilot

Giving Agents Tools: Python Functions as First-Class Capabilities

Equip your ADK 2 agents with typed Python tools. Learn schema auto-generation, tool payload optimization, and cost-conscious tool design.

Share Editorial
Giving Agents Tools: Python Functions as First-Class Capabilities

Giving Agents Tools: Python Functions as First-Class Capabilities

📦 Companion Code: View the complete, runnable code on GitHub: part02 in the cartpilot-adk2-series repository.

In Part 1, CartPilot's search agent could talk but not act. Today it gets real tools — and we treat tool design as a cost decision from day one, not just a capability one.

How ADK turns functions into tools

ADK inspects a plain Python function — its name, docstring, type hints, and parameter names — and builds the function-calling schema the model sees automatically. No manual JSON schema authoring.

python
# cartpilot_search/catalog.py
CATALOG = {
    "sku-001": {"sku": "sku-001", "name": "Trail Runner Jacket", "category": "outdoor",
                "price": 89.99, "stock": 14, "color": "forest green", "sizes": ["S", "M", "L", "XL"]},
    "sku-002": {"sku": "sku-002", "name": "Insulated Water Bottle 1L", "category": "outdoor",
                "price": 24.50, "stock": 42, "color": "steel", "sizes": ["one-size"]},
    "sku-003": {"sku": "sku-003", "name": "Wireless Trail Earbuds", "category": "electronics",
                "price": 59.00, "stock": 0, "color": "black", "sizes": ["one-size"]},
    "sku-004": {"sku": "sku-004", "name": "Merino Wool Hiking Socks (3-pack)", "category": "apparel",
                "price": 18.75, "stock": 60, "color": "grey", "sizes": ["S", "M", "L"]},
    "sku-005": {"sku": "sku-005", "name": "Compact Camp Stove", "category": "outdoor",
                "price": 45.00, "stock": 7, "color": "silver", "sizes": ["one-size"]},
}

def find_products(query: str = "", category: str = "") -> list[dict]:
    query = query.lower().strip()
    return [
        item for item in CATALOG.values()
        if (query in item["name"].lower() if query else True)
        and (item["category"] == category if category else True)
    ]

def get_product(sku: str) -> dict | None:
    return CATALOG.get(sku)

In production, swap this module for a real product-service client — the tool functions below don't change.

python
# cartpilot_search/agent.py
from google.adk.agents import Agent
from google.adk.tools import ToolContext
from .catalog import find_products, get_product


def search_products(query: str, category: str, tool_context: ToolContext) -> dict:
    """Searches the CartPilot catalog for products.

    Args:
        query: Free-text search term, e.g. "water bottle". Pass an empty
            string to browse by category only.
        category: Optional category filter, e.g. "outdoor", "electronics",
            "apparel". Pass an empty string to search all categories.

    Returns:
        A dict with a "results" list of matching products, each including
        sku, name, price, and stock count.
    """
    results = find_products(query=query, category=category)
    tool_context.state["last_search_results"] = [r["sku"] for r in results]
    return {"status": "success", "results": results}


def get_product_details(sku: str) -> dict:
    """Fetches full details for a single product by SKU.

    Args:
        sku: The product SKU, e.g. "sku-002".
    """
    product = get_product(sku)
    if product is None:
        return {"status": "not_found", "sku": sku}
    return {"status": "success", "product": product}


root_agent = Agent(
    model="gemini-3.5-flash",
    name="product_search_agent",
    description=(
        "Helps shoppers find products in the CartPilot store by name, "
        "category, or general description."
    ),
    instruction="""
You are the CartPilot product search assistant.

- Use `search_products` to find items matching what the shopper describes.
- Use `get_product_details` when the shopper wants more info on one item.
- If `search_products` returns no results, say so plainly and suggest
  broadening the search (drop the category filter, try a different term).
- Don't call a tool more than once for the same query — read the results
  you already have before deciding you need another search.
- Keep responses short (2-4 sentences).
""",
    tools=[search_products, get_product_details],
)

A few things worth calling out:

  • ToolContext is injected, not requested. It appears in the function signature but never in the docstring's Args: — the model never fills it in; ADK passes it automatically. This is how a tool reads and writes session state (more in Part 3).
  • Return dicts with a status field, always. We're setting up the convention Part 9 builds resilience on — the model needs a machine-readable way to tell "found it" from "not found" apart from parsing prose.
  • The docstring is the API contract the model reasons from. A vague docstring produces vague tool calls — wrong arguments, redundant calls, or the model guessing instead of calling the tool at all.

Tool design as a cost lever

This is the part that's easy to miss: every tool call is a full model round-trip — output tokens for the call, input tokens for the result, and (usually) another generation to turn the result into a response. Three habits keep that under control:

  1. Design tools to return everything the model is likely to need next, so it doesn't have to chain a second call to get a detail it could've had the first time. search_products returns price and stock, not just names — because "is it in stock?" is an obvious follow-up.
  2. Nudge against redundant calls in the instruction. We explicitly tell the agent not to re-call search_products for a query it's already answered. Left unsaid, some models will re-search "just to be sure" on every turn — quietly doubling your tool-call cost per conversation.
  3. Prefer one tool with parameters over many narrow tools. search_products(query, category) beats separate search_by_name and search_by_category tools — fewer schema entries competing for the model's attention, and one call covers more shopper intents.

Try it

bash
adk web

Ask: "Do you have anything for hiking under $30?" — watch the event inspector show the search_products call, its arguments, and the raw tool result before the model turns it into a sentence.

What's next

Right now, every new chat starts from zero — CartPilot doesn't remember what it just searched for once the conversation moves on. Part 3 adds session state and a shopping cart, and we look at what multi-turn memory actually costs in tokens as a conversation grows.