🛍️ 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, andapply_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")andget_price("shoes"). - Backend Execution: The Python backend intercepts this request, looks up the real functions in its
TOOL_FUNCTIONSregistry, 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
Code flow
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 functionimport 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