Retail Analytics Agent with Neo4j

Build a GraphRAG agent that answers retail questions using Neo4j.

Introduction

30 Second Summary

Every retail business sits on a web of connections: products belong to categories, suppliers provide products, customers place orders, and those orders contain specific items.

In this project, you will build an AI agent that understands these connections by storing retail data in a Neo4j graph database, then answering plain English questions about the business using LangGraph and OpenAI.

What You'll Build

A retail analytics agent that translates natural language questions into Cypher queries, executes them against a Neo4j knowledge graph, and returns human-readable answers using the ReAct agent pattern.

By the end of this project, you'll have:

  • A Neo4j AuraDB Free instance loaded with 1,035 nodes and 3,139 relationships of real retail data.
  • Custom Python tools that query the graph database using the Cypher query language.
  • A LangGraph ReAct agent with conversation memory that orchestrates the LLM and tools across multi-turn conversations.
  • A product recommendation engine that uses graph traversal to suggest products based on co-purchase patterns.

Are there any prerequisites?

You need basic Python knowledge and an OpenAI API key. You'll also need Cursor installed as your code editor. No prior experience with graph databases or LangGraph is required.

Set Up Your Environment

Before you can build your retail analytics agent, you need the right tools in place. This step gets your Python environment ready, creates a free Neo4j graph database in the cloud, and wires up the credentials your agent will use to connect to everything.

In This Step, Get Ready To:
  • Install the Python packages for LangChain, LangGraph, and Neo4j.
  • Create a free Neo4j AuraDB cloud database instance.
  • Configure environment variables so your code can securely access Neo4j and the OpenAI API.
Install Python Packages

Your agent relies on several Python libraries that work together. LangChain provides the tool and LLM abstractions. LangGraph adds the agent reasoning loop. langchain-neo4j connects everything to your graph database.

  • Open Cursor and create a new project folder called retail-agent.
  • Open the integrated terminal in Cursor.
  • Create and activate a virtual environment.

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

What is a virtual environment?

A virtual environment is an isolated Python workspace. It keeps the packages you install for this project separate from your system Python, so nothing conflicts with other projects on your machine.

  • Install the required packages.
pip install langchain langchain-openai langchain-neo4j langgraph python-dotenv

What does this command install?

  • langchain is the core framework for building LLM-powered applications with tools and chains.
  • langchain-openai connects LangChain to OpenAI models like gpt-4o-mini.
  • langchain-neo4j is the official Neo4j integration. It includes the neo4j Python driver as a dependency, so you don't need to install it separately.
  • langgraph provides the ReAct agent pattern your analytics agent will use.
  • python-dotenv loads environment variables from a .env file so you can keep credentials out of your code.

Seeing installation errors?

Make sure your virtual environment is active. You should see (venv) at the beginning of your terminal prompt. If not, re-run the activation command above. You also need Python 3.10 or higher for LangGraph. Check your version with python --version. If you're stuck, help me debug my installation error.

Create a Neo4j AuraDB Free Instance

Your agent needs a graph database to query. Neo4j AuraDB Free gives you a fully managed cloud database at no cost, with no credit card required.

  • Go to the Neo4j Aura console and sign up for a free account.
  • Click New Instance.
  • Select the Free tier.
  • Choose any cloud provider and region. The choice doesn't matter for this project.
  • Click Create Instance.

Save your credentials now

When the instance is created, Neo4j displays a connection URI, username, and password. These are shown only once. Copy and save all three values somewhere safe before closing the dialog. You will need them in the next substep.

Wait for your instance status to show Running. This usually takes about a minute.

Configure Environment Variables

Your code needs credentials for both Neo4j and OpenAI. Storing them in a .env file keeps secrets out of your source code.

  • Create a new file called .env in the root of your retail-agent folder.
  • Add the following content, replacing the placeholder values with your actual credentials.
NEO4J_URI=neo4j+s://your-aura-uri-here
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your-password-here
OPENAI_API_KEY=your-openai-api-key-here

Where do these values come from?

  • NEO4J_URI is the connection URI from the dialog you saved earlier. It starts with neo4j+s://.
  • NEO4J_USERNAME is typically neo4j by default.
  • NEO4J_PASSWORD is the generated password from the instance creation dialog.
  • OPENAI_API_KEY is your existing OpenAI API key.
  • Create a new file called main.py in the root of your project folder.
  • Add the following code to load your environment variables.
import os
from dotenv import load_dotenv

# Load variables from .env into the environment
load_dotenv()

# Verify the variables loaded correctly
print("Neo4j URI:", os.getenv("NEO4J_URI"))
print("OpenAI API Key loaded:", "Yes" if os.getenv("OPENAI_API_KEY") else "No")

What does this code do?

The load_dotenv() function reads your .env file and makes each key-value pair available through os.getenv(). The print statements confirm everything loaded without exposing your full API key.

  • Run the file to verify your environment variables are loading correctly.
python main.py

I see my Neo4j URI and 'Yes' for the API key

Your environment is configured correctly. You should see your Neo4j URI printed and Yes for the API key confirmation.

I see 'None' or 'No'

This means your .env file isn't being found or read correctly. Check the following:

  • Make sure .env is in the same folder as main.py.
  • Make sure the file is named exactly .env (with the leading dot), not env.txt or .env.txt.
  • Make sure there are no spaces around the = signs in your .env file.

If you're still stuck, help me troubleshoot my .env file.

Your environment is ready to go. Next up, you'll load a real retail dataset into your Neo4j graph database and see how products, categories, suppliers, and customers connect together.

Load the Northwind Retail Dataset into Neo4j

Your environment is configured and ready to connect to Neo4j. Now it's time to fill your graph database with real retail data so your agent has something meaningful to query.

The Northwind dataset is Neo4j's official retail example dataset. It models a complete retail business with products, categories, suppliers, customers, and orders. You'll load it from publicly hosted CSV files directly into your Neo4j instance using Cypher queries.

In This Step, Get Ready To:
  • Create uniqueness constraints to keep your graph data clean.
  • Load 1,035 nodes and 3,139 relationships from the Northwind CSV files.
  • Verify the graph by running sample Cypher queries.
Connect to Neo4j and Create Constraints

Before loading any data, you need to set up uniqueness constraints. Constraints prevent duplicate nodes from being created if you accidentally run the load script twice. They also improve query performance by creating indexes on the constrained properties.

  • Open main.py in Cursor.
  • Add the following code to connect to your Neo4j instance and create constraints for each node type.
import os
from dotenv import load_dotenv
from neo4j import GraphDatabase

load_dotenv()

# Connect to Neo4j using the credentials from your .env file
driver = GraphDatabase.driver(
    os.getenv("NEO4J_URI"),
    auth=(os.getenv("NEO4J_USERNAME"), os.getenv("NEO4J_PASSWORD"))
)

def run_query(query, parameters=None):
    """Execute a Cypher query and return the results."""
    with driver.session() as session:
        result = session.run(query, parameters)
        return [record.data() for record in result]

# Create uniqueness constraints for each node type
constraints = [
    "CREATE CONSTRAINT product_productID IF NOT EXISTS FOR (p:Product) REQUIRE (p.productID) IS UNIQUE",
    "CREATE CONSTRAINT category_categoryID IF NOT EXISTS FOR (c:Category) REQUIRE (c.categoryID) IS UNIQUE",
    "CREATE CONSTRAINT supplier_supplierID IF NOT EXISTS FOR (s:Supplier) REQUIRE (s.supplierID) IS UNIQUE",
    "CREATE CONSTRAINT customer_customerID IF NOT EXISTS FOR (c:Customer) REQUIRE (c.customerID) IS UNIQUE",
    "CREATE CONSTRAINT order_orderID IF NOT EXISTS FOR (o:Order) REQUIRE (o.orderID) IS UNIQUE",
]

for constraint in constraints:
    run_query(constraint)

print("Constraints created successfully.")

What does this code do?

The run_query helper function opens a session with Neo4j, runs a Cypher query, and returns the results as a list of dictionaries. You'll reuse this function throughout the project.

Each CREATE CONSTRAINT statement tells Neo4j that a specific property (like productID) must be unique across all nodes of that label. The IF NOT EXISTS clause makes the script safe to run multiple times.

  • Run the script in your terminal.
python main.py

What should I see?

You should see Constraints created successfully. printed in your terminal. This confirms that your Neo4j connection is working and the constraints are in place.

Seeing a connection error?

Double-check the values in your .env file. The NEO4J_URI should start with neo4j+s:// for AuraDB. Make sure your AuraDB instance is in a running state in the Neo4j console. If it auto-paused, click Resume and wait a moment before retrying. Help me debug my Neo4j connection error.

Load Nodes and Relationships from CSV

Neo4j can load data directly from CSV files hosted on the web using LOAD CSV statements. The Northwind CSV files are publicly hosted at https://data.neo4j.com/northwind/. You'll load nodes first, then create the relationships between them.

  • Replace the constraint code in main.py with the following data loading script. Keep the import statements, driver connection, and run_query function at the top.
# Load product nodes
run_query("""
LOAD CSV WITH HEADERS FROM "https://data.neo4j.com/northwind/products.csv" AS row
CREATE (n:Product)
SET n = row,
  n.unitPrice = toFloat(row.unitPrice),
  n.unitsInStock = toInteger(row.unitsInStock),
  n.unitsOnOrder = toInteger(row.unitsOnOrder),
  n.reorderLevel = toInteger(row.reorderLevel),
  n.discontinued = (row.discontinued <> "0")
""")
print("Products loaded.")

# Load category nodes
run_query("""
LOAD CSV WITH HEADERS FROM "https://data.neo4j.com/northwind/categories.csv" AS row
CREATE (n:Category)
SET n = row
""")
print("Categories loaded.")

# Load supplier nodes
run_query("""
LOAD CSV WITH HEADERS FROM "https://data.neo4j.com/northwind/suppliers.csv" AS row
CREATE (n:Supplier)
SET n = row
""")
print("Suppliers loaded.")

What is LOAD CSV doing?

LOAD CSV WITH HEADERS reads each row from the CSV file and makes the column values available as row.columnName. The SET n = row shortcut copies all columns as properties on the node. Then specific properties like unitPrice are converted to the correct data types using toFloat() and toInteger().

  • Add the following code below the node loading section to create relationships between products, categories, and suppliers.
# Create PART_OF relationships (Product -> Category)
run_query("""
MATCH (p:Product), (c:Category)
WHERE p.categoryID = c.categoryID
CREATE (p)-[:PART_OF]->(c)
""")
print("PART_OF relationships created.")

# Create SUPPLIES relationships (Supplier -> Product)
run_query("""
MATCH (s:Supplier), (p:Product)
WHERE s.supplierID = p.supplierID
CREATE (s)-[:SUPPLIES]->(p)
""")
print("SUPPLIES relationships created.")

What is a relationship in a graph database?

In a knowledge graph, relationships are the connections between nodes. They have a direction and a type. For example, (Product)-[:PART_OF]->(Category) means a product belongs to a category. These relationships are what make graph databases powerful for answering questions like "Which suppliers provide beverages?" in a single traversal.

  • Add the following code to load customers, orders, and order details.
# Load customer nodes
run_query("""
LOAD CSV WITH HEADERS FROM "https://data.neo4j.com/northwind/customers.csv" AS row
CREATE (n:Customer)
SET n = row
""")
print("Customers loaded.")

# Load order nodes
run_query("""
LOAD CSV WITH HEADERS FROM "https://data.neo4j.com/northwind/orders.csv" AS row
CREATE (n:Order)
SET n = row
""")
print("Orders loaded.")

# Create PURCHASED relationships (Customer -> Order)
run_query("""
MATCH (c:Customer), (o:Order)
WHERE c.customerID = o.customerID
CREATE (c)-[:PURCHASED]->(o)
""")
print("PURCHASED relationships created.")

# Create ORDERS relationships (Order -> Product) with quantity data
run_query("""
LOAD CSV WITH HEADERS FROM "https://data.neo4j.com/northwind/order-details.csv" AS row
MATCH (o:Order), (p:Product)
WHERE o.orderID = row.orderID AND p.productID = row.productID
CREATE (o)-[details:ORDERS]->(p)
SET details.quantity = toInteger(row.quantity)
""")
print("ORDERS relationships created.")

print("\nAll data loaded successfully!")

Why does ORDERS have a quantity property?

Relationships in Neo4j can carry their own properties. The ORDERS relationship stores the quantity of each product in an order. This lets you answer questions like "What are the top 5 most ordered products by total quantity?" without needing a separate join table.

  • Run the updated script.
python main.py

What should I see?

You should see each loading step print a confirmation message, ending with All data loaded successfully! at the bottom. The entire load process typically takes 10-30 seconds depending on your internet connection.

Verify the Graph

Your graph should now contain approximately 1,035 nodes and 3,139 relationships. Let's run some verification queries to confirm everything loaded correctly.

  • Replace the data loading code in main.py with the following verification queries. Keep the imports, driver connection, and run_query function.
# Count all nodes and relationships in the graph
count_result = run_query("""
MATCH (n)
WITH count(n) as nodeCount
MATCH ()-[r]->()
RETURN nodeCount, count(r) as relationshipCount
""")
print(f"Nodes: {count_result[0]['nodeCount']}, Relationships: {count_result[0]['relationshipCount']}")

# Test query: find suppliers and their product categories
test_result = run_query("""
MATCH (s:Supplier)-->(:Product)-->(c:Category)
RETURN s.companyName as Company, collect(distinct c.categoryName) as Categories
LIMIT 5
""")

print("\nSample supplier-category data:")
for record in test_result:
    print(f"  {record['Company']}: {record['Categories']}")
  • Run the verification script.
python main.py

The first line should show approximately 1,035 nodes and 3,139 relationships. The sample query should display supplier company names alongside the product categories they supply.

Why is this query powerful?

The test query MATCH (s:Supplier)-->(:Product)-->(c:Category) traverses two relationships in a single statement. It starts at a Supplier, follows the SUPPLIES relationship to a Product, then follows the PART_OF relationship to a Category. In a traditional relational database, this would require two JOIN operations. In a graph database, it reads like a natural sentence.

Seeing 0 nodes or unexpected counts?

If your node count is 0, the data loading step may have failed silently. Re-run the loading script from the previous substep.

If your counts are doubled (e.g., 2,070 nodes), the script ran twice without constraints catching duplicates. Go to the Neo4j console, open your instance's query editor, and run MATCH (n) DETACH DELETE n to clear all data. Then re-run both the constraint and loading scripts. Help me fix my Northwind data loading issue.

Your knowledge graph is loaded and verified. You have a complete retail data model with products, categories, suppliers, customers, and orders all connected through meaningful relationships. Next up, you'll build the Python tools that let your AI agent query this graph.

Build the Neo4j Query Tools

Your graph database is loaded with over a thousand nodes and three thousand relationships. Now you need a way for the AI agent to actually interact with that data. In this step, you'll build two LangChain tools that the agent will call on its own: one to inspect the graph's structure and one to run Cypher queries against it.

In This Step, Get Ready To:
  • Create a schema retrieval tool so the agent can discover what data is available.
  • Create a Cypher query execution tool so the agent can run queries against Neo4j.
  • Test both tools manually to confirm they return correct results.
Create a Graph Schema Retrieval Tool

Before the agent can write a Cypher query, it needs to know what nodes, relationships, and properties exist in the graph. Without this context, the LLM would be guessing at label names and property keys. The Neo4jGraph class from langchain-neo4j provides a get_schema property that returns exactly this information.

  • Open main.py in Cursor.
  • Add the following imports and create a Neo4jGraph connection, then define the schema tool.
from langchain_neo4j import Neo4jGraph
from langchain_core.tools import tool

# Connect to Neo4j using the same credentials from your .env file
graph = Neo4jGraph(
    url=os.environ["NEO4J_URI"],
    username=os.environ["NEO4J_USERNAME"],
    password=os.environ["NEO4J_PASSWORD"]
)

@tool
def get_graph_schema() -> str:
    """Returns the schema of the Neo4j graph database, including node labels,
    relationship types, and property keys. Use this to understand what data
    is available before writing Cypher queries."""
    return graph.get_schema

What Is a LangChain Tool?

The @tool decorator turns a regular Python function into something an LLM can call. The function's name becomes the tool name, and the docstring becomes the tool description. The agent reads that description to decide when to use each tool.

Why a Schema Tool Instead of Hardcoding the Schema?

You could paste the schema directly into the system prompt, but making it a tool is more flexible. If the graph structure changes, the agent automatically picks up the new schema. It also keeps the system prompt shorter and lets the agent decide when it needs schema information.

Create a Cypher Query Execution Tool

The schema tool tells the agent what data exists. This second tool lets the agent actually query that data. It accepts a Cypher query string, runs it against Neo4j, and returns the results. Error handling is critical here. If the LLM generates an invalid query, the tool should return the error message so the agent can try again with a corrected query.

  • Add the following tool function below your get_graph_schema tool in main.py.
@tool
def run_cypher_query(query: str) -> str:
    """Executes a Cypher query against the Neo4j database and returns the results.
    Use this to answer questions about the retail data after checking the schema.
    Input should be a valid Cypher query string."""
    try:
        results = graph.query(query)
        if not results:
            return "Query returned no results."
        # Format results as a readable string
        return str(results)
    except Exception as e:
        return f"Error executing query: {str(e)}"

What Does This Code Do?

The run_cypher_query tool takes a Cypher query string from the LLM, runs it using the graph.query() method, and returns the results as a string. The try/except block catches any errors from invalid queries. Instead of crashing, it returns the error message to the agent. This is what enables the ReAct pattern's self-correction ability.

Test the Tools Manually

Before handing these tools to an AI agent, you should verify they work on their own. This saves you from debugging agent behavior when the real issue is a broken tool.

  • Add the following test code at the bottom of main.py.
if __name__ == "__main__":
    # Test the schema tool
    print("=== Graph Schema ===")
    print(get_graph_schema.invoke({}))

    # Test the query tool with a simple Cypher query
    print("\n=== Top 5 Products by Units in Stock ===")
    result = run_cypher_query.invoke(
        {"query": "MATCH (p:Product) RETURN p.productName AS product, p.unitsInStock AS stock ORDER BY p.unitsInStock DESC LIMIT 5"}
    )
    print(result)

What Does This Code Do?

The .invoke({}) method is how LangChain tools are called programmatically. For get_graph_schema, you pass an empty dictionary because it takes no arguments. For run_cypher_query, you pass a dictionary with the query key containing your Cypher string.

  • Run the file in your Cursor terminal.
python main.py

I see schema and query results

Your schema output should show node labels like Product, Category, Supplier, Customer, and Order, along with relationship types like PART_OF, SUPPLIES, PURCHASED, and ORDERS. The query results should list five product names with their stock quantities. Both tools are working correctly.

I see an error

Here are some common issues:

  • If you see a connection error, check that your .env file has the correct NEO4J_URI, NEO4J_USERNAME, and NEO4J_PASSWORD values. Make sure the URI starts with neo4j+s://.
  • If you see ModuleNotFoundError, confirm your virtual environment is activated and that you installed langchain-neo4j.
  • If the schema is empty, your AuraDB instance may have auto-paused. Go to https://console.neo4j.io and check that your instance is running.

Still stuck? Help me debug my Neo4j tool connection.

Your two tools are tested and ready. The agent now has everything it needs to explore the graph schema and query the data. Next up, you'll build the LangGraph ReAct agent that ties these tools together with an LLM to answer natural language questions.

Build the LangGraph ReAct Agent

You have two working tools that can inspect the graph schema and execute Cypher queries. Now you need an LLM that can decide when and how to use those tools. This is where the ReAct agent pattern comes in. The agent reasons about a question, picks the right tool, observes the result, and repeats until it has a complete answer.

In this step, you will wire your tools into a LangGraph ReAct agent and test it with real retail analytics questions.

In This Step, Get Ready To:
  • Initialize an OpenAI LLM and create a ReAct agent with your Neo4j tools.
  • Write a system prompt that teaches the agent how to query the graph database.
  • Test the agent with real retail analytics questions and verify its reasoning.
Initialize the LLM and Create the Agent

The ReAct pattern stands for Reason + Act. The agent thinks about what it needs to do, takes an action (calling a tool), observes the result, and then decides whether to act again or respond. LangGraph provides a prebuilt create_react_agent function that handles this entire loop for you.

  • Add the following imports and agent setup code to your main.py file, below your tool definitions.
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

# Initialize the LLM with a low temperature for consistent query generation
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# Collect the tools the agent can use
tools = [get_graph_schema, run_cypher_query]

# System prompt that teaches the agent how to work with the graph database
system_prompt = """
You are a retail analytics assistant with access to a Neo4j graph database
containing the Northwind retail dataset.

When answering questions, ALWAYS follow these steps:
1. First, call the get_graph_schema tool to understand the available
   node labels, relationship types, and properties in the database.
2. Based on the schema, write a Cypher query to answer the user's question.
3. Call the run_cypher_query tool to execute your Cypher query.
4. If the query returns an error, analyze the error message, fix your
   Cypher query, and try again.
5. Format the results into a clear, human-readable answer.

Always use the schema to verify node labels and relationship types before
writing queries. Never guess at property names or relationship directions.
"""

# Create the ReAct agent
agent = create_react_agent(llm, tools, prompt=system_prompt)

print("Agent created successfully!")

What Does This Code Do?

This code creates three things. First, it initializes gpt-4o-mini as the LLM with temperature=0 so it generates consistent, deterministic Cypher queries. Second, it bundles your two tools into a list. Third, create_react_agent wires everything together into an agent that can reason, call tools, and loop until it has an answer.

Why gpt-4o-mini Instead of gpt-4o?

The gpt-4o-mini model is roughly 10x cheaper than gpt-4o while still being capable of generating accurate Cypher queries. For a learning project where you will run many test queries, this keeps your total cost under $0.10.

Understand the System Prompt

The system prompt is the most important piece of your agent. It defines the agent's behavior and strategy. Take a closer look at what each instruction in the prompt accomplishes.

  • Step 1 tells the agent to always check the schema first. This prevents the agent from guessing at node labels or property names.
  • Steps 2-3 establish the core workflow: write a Cypher query based on the schema, then execute it.
  • Step 4 enables self-correction. If a query fails, the agent analyzes the error and retries with a fixed query instead of giving up.
  • Step 5 ensures the raw database results get formatted into a readable answer for the user.

Pro tip: schema-first prompting

Telling the agent to always retrieve the schema before writing queries is a key pattern in Text2Cypher applications. Without the schema, the LLM might hallucinate property names like name when the actual property is companyName. Grounding the LLM in the real schema dramatically improves query accuracy.

Test the Agent With Sample Questions

Time to see the agent in action. You will invoke it with a real retail analytics question and watch it reason through the problem step by step.

  • Add the following test code at the bottom of main.py.
# Test the agent with a retail analytics question
test_question = "Which suppliers provide products in the Beverages category?"

print(f"\nQuestion: {test_question}")
print("-" * 50)

# Invoke the agent and get the response
response = agent.invoke({"messages": [{"role": "user", "content": test_question}]})

# Print the agent's final answer
for message in response["messages"]:
    if hasattr(message, 'content') and message.content:
        print(f"\n[{message.type}]: {message.content}")
  • Run the script in your terminal.
python main.py

What Should I See?

You should see the agent's reasoning chain printed out. First, it calls get_graph_schema to learn the data model. Then it writes a Cypher query to find suppliers connected to Beverages products. Finally, it returns a human-readable list of supplier names.

The output will include messages of type human (your question), ai (the agent's reasoning and tool calls), tool (tool results), and a final ai message with the formatted answer.

Now test with a more complex aggregation question to make sure the agent handles different query types.

  • Replace your test question with the following and run the script again.
# Test with an aggregation question
test_question = "What are the top 5 most ordered products by total quantity?"

print(f"\nQuestion: {test_question}")
print("-" * 50)

response = agent.invoke({"messages": [{"role": "user", "content": test_question}]})

for message in response["messages"]:
    if hasattr(message, 'content') and message.content:
        print(f"\n[{message.type}]: {message.content}")

The agent should generate a Cypher query that traverses the ORDERS relationship, sums up the quantity property, and returns the top 5 products sorted by total quantity. This confirms the agent can handle aggregation and sorting in its queries.

Getting an Error or Unexpected Results?

If the agent returns an error about invalid property names or relationship types, check that your system prompt includes the instruction to call get_graph_schema first. Without schema grounding, the LLM may guess incorrect names.

If you see an authentication error, verify your .env file has the correct OPENAI_API_KEY value. Help me debug my agent setup.

Your agent can now take a plain English question, figure out the right Cypher query, execute it against your Neo4j graph, and return a clear answer. Next up, you will wrap this agent in a conversational loop so you can have a back-and-forth dialogue with follow-up questions.

Build the Conversational Loop

Your agent can answer individual questions, but a real analytics tool needs to handle back-and-forth conversations. Think about how you'd naturally explore data: you ask a broad question, then drill down based on the answer. Right now, each query to the agent starts from scratch with no memory of what came before.

In this step, you'll wrap the agent in an interactive CLI and add conversation memory so it can handle follow-up questions.

In This Step, Get Ready To:
  • Create an interactive CLI loop that accepts natural language questions and prints the agent's answers.
  • Add conversation memory so the agent remembers context from previous turns.
  • Test a multi-turn conversation to verify follow-up questions work correctly.
Create an Interactive CLI

The CLI is a simple input loop. It prompts you for a question, sends it to the agent, and prints the final answer. You also need a way to exit gracefully.

  • Add the following code to the bottom of your main.py file.
def main():
    print("Retail Analytics Agent")
    print("Ask questions about products, categories, suppliers, customers, and orders.")
    print("Type 'quit' or 'exit' to stop.\n")

    while True:
        user_input = input("You: ").strip()

        # Check for exit commands
        if user_input.lower() in ["quit", "exit"]:
            print("Goodbye!")
            break

        # Skip empty input
        if not user_input:
            continue

        # Send the question to the agent
        response = agent.invoke(
            {"messages": [{"role": "user", "content": user_input}]}
        )

        # Extract and print the final answer
        final_message = response["messages"][-1].content
        print(f"\nAgent: {final_message}\n")

if __name__ == "__main__":
    main()

What does this code do?

The while True loop keeps the CLI running until you type quit or exit. Each question is sent to the agent via agent.invoke(), and the last message in the response is the agent's final answer. Everything in between (tool calls, intermediate reasoning) is handled internally by the ReAct loop.

  • Run your script to test the basic loop.
python main.py
  • Try asking a question like "How many products are in the database?" and verify you get a clear answer.
  • Type quit to exit.

The CLI works, but try this: ask "What product categories do you have?" and then follow up with "Which one has the most products?" The agent won't know what "which one" refers to because each invocation is independent. Time to fix that.

Add Conversation Memory

LangGraph supports conversation memory through a concept called a checkpointer. A checkpointer saves the agent's state (including all previous messages) so that each new invocation can access the full conversation history. InMemorySaver is a simple checkpointer that stores everything in memory for the duration of your session.

  • Add the following import near the top of your main.py file.
from langgraph.checkpoint.memory import InMemorySaver
  • Update your create_react_agent call to include the checkpointer. Find the line where you created the agent and modify it.
# Create a checkpointer for conversation memory
memory = InMemorySaver()

# Update the agent to use the checkpointer
agent = create_react_agent(
    model,
    tools=[get_graph_schema, run_cypher_query],
    checkpointer=memory,
    prompt=system_prompt
)

What is a checkpointer?

A checkpointer saves snapshots of the agent's state after each interaction. When you invoke the agent again with the same thread_id, it loads the previous state and the agent sees the full conversation history. Different thread_id values create separate conversations.

  • Now update the main() function to pass a thread_id in the config. This tells the checkpointer which conversation thread to use.
def main():
    print("Retail Analytics Agent")
    print("Ask questions about products, categories, suppliers, customers, and orders.")
    print("Type 'quit' or 'exit' to stop.\n")

    # Config with thread_id for conversation memory
    config = {"configurable": {"thread_id": "1"}}

    while True:
        user_input = input("You: ").strip()

        if user_input.lower() in ["quit", "exit"]:
            print("Goodbye!")
            break

        if not user_input:
            continue

        # Pass config so the agent remembers previous turns
        response = agent.invoke(
            {"messages": [{"role": "user", "content": user_input}]},
            config=config
        )

        final_message = response["messages"][-1].content
        print(f"\nAgent: {final_message}\n")

if __name__ == "__main__":
    main()

What does this code do?

The only change is the config dictionary with a thread_id. Every call to agent.invoke() now includes this config. The checkpointer uses the thread_id to look up all previous messages in this conversation and include them automatically.

Test a Multi-Turn Conversation

The real test of conversation memory is whether the agent can resolve references like "that category" or "which of those" based on earlier answers.

  • Run the script again.
python main.py
  • Try this three-turn conversation:
  1. Ask: "What product categories do you have?"
  2. Follow up with: "Which one has the most products?"
  3. Then ask: "Show me the suppliers for that category."

What should I see?

The agent should list all categories (like Beverages, Condiments, Confections, etc.) in the first response. For the second question, it should identify which category has the most products without you needing to repeat the category names. For the third question, it should know which category you mean by "that category" and list the suppliers.

If the agent asks you to clarify which category you mean, the memory is not working correctly. Double-check that you're passing the config to agent.invoke() and that the checkpointer is set on the agent.

Agent not remembering previous questions?

Make sure the checkpointer=memory parameter is included in your create_react_agent() call, and that config is passed to every agent.invoke() call. Both pieces are required. Help me debug my conversation memory setup.

You now have a fully conversational retail analytics agent. It translates natural language into Cypher queries, executes them against your Neo4j knowledge graph, and remembers what you talked about. Next up, take on the Secret Mission and build a product recommendation tool that uses graph traversal to suggest products based on real purchase patterns.

Secret mission

Add a Product Recommendation Tool

Your agent can answer questions about the retail data, but it can't yet make recommendations. What if a customer wants to know what else they should buy based on a product they already love? Build a graph traversal tool that finds co-purchased products and gives your agent the power to recommend.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Neo4j AuraDB Free has no ongoing costs, so there is nothing to worry about financially.

Resources you used:

  • Neo4j AuraDB Free instance (free tier, no credit card required, no ongoing cost).
  • Local project folder retail-agent with Python virtual environment and source files.
  • OpenAI API key (stored in your .env file).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with your retail analytics agent.

  • Your AuraDB Free instance persists indefinitely at no cost. You can continue querying it and building on this project.
  • Your local project files and virtual environment remain on your machine.
  • Your OpenAI API key stays in your .env file. You are only charged when you run queries.

Pause - I'll come back to this later

Free up local resources while keeping your cloud database and project files intact.

  • Your AuraDB Free instance auto-pauses after inactivity. It resumes automatically when you reconnect, so no manual action is needed.
  • Deactivate your Python virtual environment by running deactivate in your terminal.
  • Keep your project folder and .env file so you can pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Go to https://console.neo4j.io, select your instance, and click Delete to remove it entirely.
  • Delete your local project folder retail-agent to remove all source files and the virtual environment.
  • If you created the OpenAI API key specifically for this project, revoke it in your OpenAI dashboard to prevent unintended usage.

Mission accomplished

That's a Wrap!

Nice work! You just built a retail analytics agent powered by a Neo4j knowledge graph and a LangGraph ReAct agent. You can now ask plain English questions about a retail business and get answers backed by real graph data.

Here's what you learned:

  • Loaded the Northwind retail dataset (1,035 nodes, 3,139 relationships) into a Neo4j AuraDB Free instance and queried it using the Cypher query language.
  • Built a LangGraph ReAct agent with custom Python tools that translates natural language questions into Cypher queries and returns human-readable answers.
  • Added conversation memory using LangGraph's InMemorySaver so the agent handles follow-up questions across a multi-turn conversation.
  • Built a product recommendation engine using graph traversal patterns to suggest co-purchased products based on real order data.

Ready to quiz yourself?