n4nAI

Building a shopping assistant agent with LangGraph

Build a shopping assistant agent langgraph in this hands-on tutorial: product search, recommendation reasoning, and tool-calling with LangGraph.

n4n Team3 min read601 words

Audio narration

Coming soon — every post will get a voice note here.

A shopping assistant agent langgraph implementation lets you combine retrieval, tool use, and multi-step reasoning without wiring a brittle prompt chain by hand. This tutorial builds a working agent that searches a product catalog and explains its recommendations, using LangGraph’s stateful graph and an OpenAI-compatible chat model.

Prerequisites

  • Python 3.10 or newer.
  • pip install langgraph langchain-openai langchain-core
  • An API key for an OpenAI-compatible endpoint. You can use OpenAI directly, or point at n4n.ai’s single endpoint that addresses 240+ models with automatic fallback when a provider is degraded.
  • Familiarity with Python typing and basic LangChain message objects.

Set the key in your environment:

export OPENAI_API_KEY=sk-...
export BASE_URL=https://api.openai.com/v1   # or https://api.n4n.ai/v1

Design goals for the shopping assistant agent langgraph

The agent must accept natural language, call a search tool when it needs inventory data, and loop back to the model to synthesize a final answer. We keep the tool interface strict so the LLM can’t hallucinate fields, and we persist message history in graph state to avoid re-implementing conversation memory.

The product catalog and search tool

We’ll embed a tiny catalog as JSON. In production you’d query a vector store or SQL, but the tool interface stays identical.

[
  {"id": 1, "name": "SoundCore Wireless Headphones", "category": "audio", "price": 79.99, "rating": 4.5},
  {"id": 2, "name": "Studio Over-Ear Headphones", "category": "audio", "price": 149.00, "rating": 4.2},
  {"id": 3, "name": "Travel Bluetooth Earbuds", "category": "audio", "price": 49.99, "rating": 4.0},
  {"id": 4, "name": "Mechanical Keyboard", "category": "accessories", "price": 89.99, "rating": 4.7}
]

Load it and wrap a search function as a LangChain tool. The decorator handles schema extraction.

import json
from langchain_core.tools import tool

CATALOG = json.loads(open("catalog.json").read())

@tool
def search_products(query: str, max_price: float = None) -> str:
    """Search the catalog for products by name or category, optionally capping price."""
    matches = [p for p in CATALOG if query.lower() in p["name"].lower() or query.lower() in p["category"].lower()]
    if max_price is not None:
        matches = [p for p in matches if p["price"] <= max_price]
    return json.dumps(matches)

The tool returns a stringified list. Keeping the payload as JSON makes the model’s post-processing deterministic.

Agent state and graph scaffolding

LangGraph tracks conversation state as a typed dictionary. We use operator.add so each node appends messages rather than replacing them.

from typing import TypedDict, Annotated
import operator

class State(TypedDict):
    messages: Annotated[list, operator.add]

A StateGraph defines nodes and edges. Our nodes: model (calls LLM), tools (executes tool calls). The graph loops between them until the model stops requesting tools.

Model node and binding

Instantiate the chat model with bind_tools. Temperature zero keeps extraction factual.

from langchain_openai import ChatOpenAI
import os

model = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
    base_url=os.environ["BASE_URL"],
    api_key=os.environ["OPENAI_API_KEY"],
).bind_tools([search_products])

def model_node(state: State):
    response = model.invoke(state["messages"])
    return {"messages": [response]}

Tool execution node

The model node may emit tool_calls. We map each call to a ToolMessage keyed by tool_call_id, which the model consumes on the next pass.

from langchain_core.messages import ToolMessage

def tool_node(state: State):
    last_msg = state["messages"][-1]
    tool_messages = []
    for tc in last_msg.tool_calls:
        if tc["name"] == "search_products":
            result = search_products.invoke(tc["args"])
            tool_messages.append(
                ToolMessage(content=result, tool_call_id=tc["id"])
            )
    return {"messages": tool_messages}

Conditional routing

After the model speaks, we check whether it requested a tool. If yes, run tools; otherwise end.

from langgraph.graph import StateGraph, END

def should_continue(state: State):
    last = state["messages"][-1]
    if getattr(last, "tool_calls", None):
        return "tools"
    return END

graph = StateGraph(State)
graph.add_node("model", model_node)
graph.add_node("tools", tool_node)
graph.add_conditional_edges("model", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "model")
app = graph.compile()

Running the shopping assistant agent langgraph

Invoke with a user question. The agent will call search_products, receive JSON, and synthesize an answer.

from langchain_core.messages import HumanMessage

query = "Find wireless headphones under $100"
result = app.invoke({"messages": [HumanMessage(content=query)]})

for m in result["messages"]:
    print(f"{m.type}: {m.content}")

Expected output (abridged):

human: Find wireless headphones under $100
ai: (tool_call: search_products args={'query':'wireless headphones','max_price':100})
tool: [{"id":1,"name":"SoundCore Wireless Headphones","price":79.99,"rating":4.5},{"id":3,"name":"Travel Bluetooth Earbuds","price":49.99,"rating":4.0}]
ai: You have two options under $100: SoundCore Wireless Headphones at $79.99 (4.5★) and Travel Bluetooth Earbuds at $49.99 (4.0★). The SoundCore offers better rating for home use; the earbuds win on portability.

The loop ran model → tools → model. No manual prompt engineering for the second turn; LangGraph persisted state.

Adding recommendation scoring

A pure search isn’t a shopping assistant agent langgraph strength unless it ranks. Extend the tool to weight rating and price:

@tool
def search_and_rank(query: str, max_price: float = None) -> str:
    """Return top products by rating/price value."""
    matches = json.loads(search_products.invoke({"query": query, "max_price": max_price}))
    for m in matches:
        m["score"] = m["rating"] / (m["price"]/10)
    matches.sort(key=lambda x: x["score"], reverse=True)
    return json.dumps(matches[:2])

Bind the new tool, swap search_products for search_and_rank in model.bind_tools, and the same graph works. The model now receives pre-ranked candidates and can focus on explaining trade-offs.

Handling empty results

Production catalogs return zero matches. Modify tool_node to emit a clear signal:

def tool_node(state: State):
    last_msg = state["messages"][-1]
    tool_messages = []
    for tc in last_msg.tool_calls:
        if tc["name"] == "search_products":
            result = search_products.invoke(tc["args"])
            if not json.loads(result):
                result = json.dumps({"error": "no matches"})
            tool_messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
    return {"messages": tool_messages}

The model will then tell the user no products fit, instead of inventing specs.

Streaming and debugging

For UX, stream tokens:

for chunk in app.stream({"messages": [HumanMessage(content=query)]}):
    print(chunk)

To debug tool calls, log last_msg.tool_calls inside tool_node. If the model repeatedly calls the tool with bad args, tighten the tool description or add Pydantic validation.

Production considerations

Streaming: wrap app.stream to yield tokens per node. Durability: LangGraph persists to a checkpointer; pass checkpointer=memory for dev, Postgres for prod. Observability: log state["messages"] lengths to catch runaway loops.

If you point BASE_URL at n4n.ai, the gateway forwards provider cache-control hints and honors client routing directives, so repeated catalog queries hit cache and cut cost. The automatic fallback also keeps the agent alive when a single model provider returns 429s.

Complete script

import json, os
from typing import TypedDict, Annotated
import operator
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END

CATALOG = json.loads(open("catalog.json").read())

@tool
def search_products(query: str, max_price: float = None) -> str:
    matches = [p for p in CATALOG if query.lower() in p["name"].lower() or query.lower() in p["category"].lower()]
    if max_price is not None:
        matches = [p for p in matches if p["price"] <= max_price]
    return json.dumps(matches)

class State(TypedDict):
    messages: Annotated[list, operator.add]

model = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
    base_url=os.environ["BASE_URL"],
    api_key=os.environ["OPENAI_API_KEY"],
).bind_tools([search_products])

def model_node(state: State):
    return {"messages": [model.invoke(state["messages"])]}

def tool_node(state: State):
    last = state["messages"][-1]
    msgs = []
    for tc in last.tool_calls:
        if tc["name"] == "search_products":
            res = search_products.invoke(tc["args"])
            msgs.append(ToolMessage(content=res, tool_call_id=tc["id"]))
    return {"messages": msgs}

def should_continue(state: State):
    if getattr(state["messages"][-1], "tool_calls", None):
        return "tools"
    return END

g = StateGraph(State)
g.add_node("model", model_node)
g.add_node("tools", tool_node)
g.add_conditional_edges("model", should_continue, {"tools": "tools", END: END})
g.add_edge("tools", "model")
app = g.compile()

print(app.invoke({"messages": [HumanMessage(content="Find wireless headphones under $100")]})["messages"][-1].content)

Where to take it next

Replace the JSON file with a real search backend (Elasticsearch, pgvector). Add a cart node that calls an order API. Use LangGraph’s subgraphs to isolate the shopping assistant agent langgraph from a larger support bot. The pattern—model proposes, tools act, graph loops—scales to most e-commerce copilots.

Tagslanggraphecommerceshopping-assistant

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All framework tutorials: e-commerce search & recommendations posts →