GuidesBuild a shopping assistant

Build a shopping assistant

This guide builds a shopping assistant for a fictional outdoor-gear shop, Ridgeway. Customers describe what they need (“a tent for two for spring hikes in Scotland, under 250 pounds”); the assistant searches the live catalog, compares a few products using real specifications and prices, and adds an item to the basket only after the customer confirms. It covers the parts that make commerce different from support: grounding every price and claim in catalog data, keeping the customer in control of the basket, and measuring whether recommendations help.

You will need: an API key in SYNTIC_API_KEY, Python 3.9 or later, pip install openai, and a product search you can call from code.

Ground rules for a commerce assistant

RuleWhy
Prices, stock and specifications come only from tool resultsA model’s memory of a product is out of date or wrong; an invented price is a legal problem.
The basket changes only after explicit confirmationCustomers must stay in control of what they buy.
Comparisons cite the specification they rely on”Lighter” is checkable when it says “1.6 kg against 2.1 kg”.
Out-of-stock items are said to be out of stockRecommending what cannot be bought wastes the customer’s time.

The tools

TOOLS = [
    {"type": "function", "function": {
        "name": "search_products",
        "description": (
            "Search the Ridgeway catalog. Returns up to 8 products with id, name, price_gbp, in_stock and key specs. "
            "Use for every product question; never describe a product you have not retrieved."
        ),
        "parameters": {"type": "object", "properties": {
            "query": {"type": "string", "description": "What the customer wants, in catalog terms, e.g. '2 person tent 3 season'"},
            "max_price_gbp": {"type": "number"},
            "category": {"type": "string", "enum": ["tents", "sleeping", "packs", "footwear", "clothing", "cooking"]},
        }, "required": ["query"]}}},
    {"type": "function", "function": {
        "name": "get_product",
        "description": "Full specification, price and stock for one product id from search results.",
        "parameters": {"type": "object", "properties": {"product_id": {"type": "string"}}, "required": ["product_id"]}}},
    {"type": "function", "function": {
        "name": "add_to_basket",
        "description": (
            "Add a product to the customer's basket. Only call this after the customer has clearly said yes to "
            "a specific product and quantity in their latest message."
        ),
        "parameters": {"type": "object", "properties": {
            "product_id": {"type": "string"}, "quantity": {"type": "integer", "minimum": 1, "maximum": 5},
        }, "required": ["product_id", "quantity"]}}},
]

The add_to_basket description states the confirmation rule, and the code below enforces it again, because a description is guidance, not a guarantee.

The instructions

SYSTEM = """
You are Ridgeway's shopping assistant. Help customers choose outdoor gear.
 
- Use search_products for every product question. Quote prices, stock and specifications only from tool results.
- Recommend at most three products. For each, give the name, price, and the one or two specs that matter
  for the customer's need. Say plainly if something is out of stock.
- Ask one short question when the need is unclear (number of people, season, budget), then search.
- Never add to the basket unless the customer's latest message clearly confirms a product and quantity.
  If unsure, ask: "Shall I add the <name> to your basket?"
- If nothing in the catalog fits, say so and suggest the closest alternative or relaxing one requirement.
- Do not discuss competitors' products or prices.
""".strip()

The loop, with a confirmation check

import json
import os
import re
 
from openai import OpenAI
 
client = OpenAI(base_url="https://app.syntic.ai/v1", api_key=os.environ["SYNTIC_API_KEY"])
YES = re.compile(r"\b(yes|yeah|yep|please do|add it|go ahead|sure)\b", re.I)
 
 
def run_tool(name, args, session):
    if name == "search_products":
        return catalog.search(args["query"], args.get("max_price_gbp"), args.get("category"))   # your search
    if name == "get_product":
        return catalog.get(args["product_id"])
    if name == "add_to_basket":
        last_user = next(m["content"] for m in reversed(session["messages"]) if m["role"] == "user")
        if not YES.search(last_user):
            return {"error": "Not confirmed. Ask the customer to confirm the product and quantity first."}
        product = catalog.get(args["product_id"])
        if not product or not product["in_stock"]:
            return {"error": "That product is not in stock."}
        session["basket"].append({"product_id": product["id"], "quantity": args["quantity"]})
        return {"ok": True, "basket_items": len(session["basket"])}
    return {"error": f"unknown tool {name}"}
 
 
def reply(session, text):
    msgs = session["messages"]
    msgs.append({"role": "user", "content": text})
    for _ in range(6):
        r = client.chat.completions.create(model="Amara S1.2", stream=False, max_tokens=700,
                                           tools=TOOLS, messages=[{"role": "system", "content": SYSTEM}] + msgs)
        m = r.choices[0].message
        msgs.append(m.model_dump(exclude_none=True))
        if not m.tool_calls:
            return m.content
        for call in m.tool_calls:
            try:
                result = run_tool(call.function.name, json.loads(call.function.arguments or "{}"), session)
            except Exception as err:
                result = {"error": str(err)}
            msgs.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
    return "Sorry, I couldn't finish that. Could you rephrase?"
 
 
session = {"messages": [], "basket": []}
print(reply(session, "I need a tent for two for spring hikes in Scotland, under 250 pounds."))

A simple keyword check is enough to block the most common failure (adding to the basket while still comparing). For production, show an Add to basket button next to each recommendation and let the click, not the model, change the basket; the assistant then only suggests.

Make search results model-friendly

The assistant is only as good as what search_products returns:

  • Return 5 to 8 products, not 50. Ranking is your search engine’s job.
  • Include the specs that drive decisions in your category (weight, packed size, season rating for tents) as named fields, with units.
  • Include in_stock and the current price in every result; never let the model fill them in.
  • Normalise names: the model will repeat them to customers.

Measure whether it helps

MetricHow
GroundingSample 100 conversations; check every price and spec mentioned against the catalog. Target: no mismatches.
ConfirmationCount add_to_basket calls rejected by the check; each one is a near miss to investigate.
UsefulnessConversion rate of sessions with the assistant against those without, and basket returns.
CoverageShare of conversations ending with “nothing fits”; these show catalog gaps or search problems.

Prices quoted by an assistant are part of your shop’s offer in many jurisdictions. Keep the grounding rule strict, log what the assistant showed, and have someone review the setup with whoever owns consumer law compliance for your shop.

Was this page helpful?