🛍️ Shop Agent

tool-using agent via OpenAI function calling

Concept

A tool-using agent — the LLM decides on its own when to call a tool. Three tools are available: get_price, check_stock, and apply_discount. The model uses OpenAI's function-calling protocol: it may request tool calls in its response, the server runs the real functions and appends the results, and the model is called again to phrase the final answer.

Theory & Concepts

The Shop Agent demo illustrates how Large Language Models (LLMs) can transition from passive chatbots into active problem-solvers using Tool Calling (Function Calling). By default, an LLM doesn't know the current price of shoes or if a bag is in stock. Instead of guessing, the Shop Agent is given a specific set of abilities it can use to find out.

The demo operates in an autonomous loop using the OpenAI function-calling protocol:

  • Providing the Tools: Before asking GPT-4o-mini a question, the application sends a JSON "menu" describing three available Python functions: get_price, check_stock, and apply_discount. The model knows what these tools do and what arguments they require (like the item name).
  • The Model's Decision: When you ask, "Do you have shoes and how much are they?", the LLM realizes it needs external data. Instead of replying with a text answer, it returns a structured request asking the backend to run check_stock("shoes") and get_price("shoes").
  • Backend Execution: The Python backend intercepts this request, looks up the real functions in its TOOL_FUNCTIONS registry, and executes them against its mock database. The LLM never runs code itself; it only asks the backend to do it.
  • Final Synthesis: The backend appends the results from those functions (e.g., "12 units in stock", "₹799") back into the conversation history as "tool" messages. The LLM is called one last time with this new context, allowing it to formulate a natural, accurate response based on the live shop data.

Request flow

Browser question
app.py POST /agent
agent.py ask()
OpenAI API with tools
Tool Calls dispatch results
Browser JSON answer

Code flow

flowchart TD A["Browser
shopping question"] -->|POST /agent| B["app.py
shop_agent"] B -->|user_message| C["agent.py
ask"] C -->|messages + TOOLS| D["OpenAI API
GPT-4o mini"] D -->|tool_calls request| E["Tool dispatch
TOOL_FUNCTIONS registry"] E -->|call get_price| F["get_price
item"] E -->|call check_stock| G["check_stock
item"] E -->|call apply_discount| H["apply_discount
item"] F -->|price string| E G -->|stock string| E H -->|discounted price| E E -->|tool results| C C -->|messages + results| D D -->|final answer text| C C -->|answer| B B -->|JSON result| A

Backend

agent.py — tool definitions & ask function
import json
import random

from config import CHAT_MODEL, get_openai_client

# Our tiny "database" — dicts standing in for a real product catalog.
PRICES = {"shoes": 799, "hat": 399, "bag": 1420, "shorts": 1299, "pants": 1699}

STOCK = {"shoes": 12, "hat": 5, "bag": 0, "shorts": 8, "pants": 3}

DISCOUNT_PERCENT = 10


def get_price(item: str) -> str:
    """Look up the price of a shop item.

    Args:
        item: The item name the user asked about.

    Returns:
        A rupee price string, or "₹unknown" if the item is not stocked.
    """
    return f"₹{PRICES.get(item.lower(), 'unknown')}"


def check_stock(item: str) -> str:
    """Check the stock availability of a shop item.

    Args:
        item: The item name to check stock for.

    Returns:
        A string describing the stock level, or "unknown item" if not found.
    """
    item_lower = item.lower()
    if item_lower not in STOCK:
        return f"{item} is not a known item in our shop."
    qty = STOCK[item_lower]
    if qty == 0:
        return f"{item} is currently out of stock."
    return f"{item} has {qty} units in stock."


def apply_discount(item: str) -> str:
    """Apply a 10% discount to a shop item and return the discounted price.

    Args:
        item: The item name to apply the discount to.

    Returns:
        A string with the original and discounted price, or an error message.
    """
    item_lower = item.lower()
    if item_lower not in PRICES:
        return f"{item} is not a known item in our shop."
    original = PRICES[item_lower]
    discounted = original - (original * DISCOUNT_PERCENT // 100)
    return f"{item}: ₹{original} → ₹{discounted} ({DISCOUNT_PERCENT}% off)"


# A registry mapping tool names to their Python functions.
TOOL_FUNCTIONS = {
    "get_price": get_price,
    "check_stock": check_stock,
    "apply_discount": apply_discount,
}

# Describe the tools so the model knows they exist and how to call them.
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_price",
            "description": "Get the price of a shop item the user asks about.",
            "parameters": {
                "type": "object",
                "properties": {
                    "item": {"type": "string", "description": "the item name"}
                },
                "required": ["item"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "check_stock",
            "description": "Check the stock availability of a shop item.",
            "parameters": {
                "type": "object",
                "properties": {
                    "item": {"type": "string", "description": "the item name"}
                },
                "required": ["item"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "apply_discount",
            "description": "Apply a 10% discount to a shop item and return the discounted price.",
            "parameters": {
                "type": "object",
                "properties": {
                    "item": {"type": "string", "description": "the item name"}
                },
                "required": ["item"],
            },
        },
    },
]


def ask(user_message: str) -> str:
    """Answer a shopping question, using the available tools when needed.

    The flow mirrors the OpenAI function-calling protocol:
      1. Send the message plus the tools menu; the model may request a tool.
      2. If it did, run the real function and append the result.
      3. Send everything back so the model can phrase a final answer.

    Args:
        user_message: The shopper's question.

    Returns:
        The assistant's final natural-language reply.
    """
    client = get_openai_client()
    messages = [{"role": "user", "content": user_message}]

    # 1. First call — the model sees the tools menu and may ask for a tool.
    response = client.chat.completions.create(
        model=CHAT_MODEL, messages=messages, tools=TOOLS
    )
    msg = response.choices[0].message

    # 2. Did it request one or more tool calls?
    if msg.tool_calls:

        # add the tool REQUEST first — required: every "tool" result must follow the assistant message that asked for it (matched by tool_call_id), or the API rejects the next call for context
        messages.append(msg)

        for call in msg.tool_calls:
            fn_name = call.function.name

            # the model's arguments arrive as a JSON string, e.g. '{"item": "shoes"}' — parse it into a Python dict so we can read args["item"]
            args = json.loads(call.function.arguments)
            # Look up the tool function from the registry and call it.
            fn = TOOL_FUNCTIONS.get(fn_name)
            result = fn(args["item"]) if fn else f"Unknown tool: {fn_name}"
            messages.append(
                {
                    "role": "tool",  # the third role, alongside user/assistant
                    "tool_call_id": call.id,
                    "content": result,
                }
            )
        # 3. Send tool results back so the model can answer in plain language.
        response = client.chat.completions.create(
            model=CHAT_MODEL, messages=messages
        )
        msg = response.choices[0].message

    return msg.content

API route

app.py
@bp.route("/agent", methods=["POST"])
def shop_agent():
    """Answer a shopping question, letting the agent call its price tool."""
    data = request.get_json(force=True)
    message = (data.get("message") or "").strip()
    if not message:
        return jsonify({"error": "A message is required."}), 400
    try:
        text = ask(message)
        return jsonify({"result": text})
    except Exception as e:
        return jsonify({"error": str(e)}), 500