How to Build a Self-Correcting AI Agent for Product Search in E-Commerce

Shopify just launched AI agents that let shoppers search, explore, and purchase using natural language.

If you’ve tried retrieval-augmented generation (RAG) pipelines for product search, you’ve probably hit the usual walls: vague results, brittle prom…


This content originally appeared on DEV Community and was authored by Chris Zhang

Shopify just launched AI agents that let shoppers search, explore, and purchase using natural language.

If you’ve tried retrieval-augmented generation (RAG) pipelines for product search, you’ve probably hit the usual walls: vague results, brittle prompts, and silent failures when the data isn’t structured just right. When your catalog involves complex product descriptions, categorizations and multiple supporting documents, a basic retrieval or prompt-based approach just doesn’t cut it.

In the age of agentic commerce, how can we enable users to say things like “I have a small family of four. We live in Munich. What’s the best internet plan for us?” and have the system identify relevant products, draft an initial proposal, review and refine it based on available data, and engage in a meaningful conversation?

In this post, you’ll learn how to build a practical AI agent for searching product catalogs using Enthusiast, an AI toolkit designed for e-commerce and knowledge-intensive tasks. We will cover setting up the environment, customizing the agent, and quickly testing it on sample data.

But first, let’s look at how agentic workflows differ from traditional pipelines and why that matters.

Non-Agentic Workflow vs. Agentic Workflow

In a traditional (non-agentic) workflow, product search is driven by fixed queries or rigid filter logic. It’s simple and fast, but struggles with nuanced language or evolving user intent. The system can’t adapt on the fly. It just follows predefined instructions.
On the other hand, an agentic workflow introduces flexibility and adaptability. AI agents dynamically interpret user inputs, construct queries intelligently, and adjust their approach based on the context of the interaction and feedback received. This allows them to handle more complex, ambiguous requests while improving reliability and user experience.

What Makes Up an AI Agent

To build an effective AI agent for product catalog search, the following components are essential:

  • Input Handling: Accepts and interprets user requests.
  • Feedback Handling and Memory: Incorporates user and system feedback to improve future interactions and maintains memory of past interactions.
  • Tools: Interfaces with external tools or databases to execute tasks.
  • Reasoning: Analyzes input and feedback to make informed decisions.

To build such an agent, we need an execution environment. Let’s explore how Enthusiast can serve as an effective option.

Introducing Enthusiast

Most LangChain tutorials stop at toy examples or require heavy customization to support real-world workflows. Enthusiast changes that. It’s built from the ground up to support:

  • Tool-based agents with LangChain and ReAct
  • SQL-backed querying with Django or external sources
  • Structured memory and retry logic out of the box
  • Open-source, customizable behavior
  • Self-hosting with cloud/local model support

Whether you're debugging search in a product catalog or surfacing relevant documents across internal departments, Enthusiast gives you a working foundation in minutes with real production logic, not just playground demos.

Build in action

Alright, now let’s bring that to life. We’ll walk through a real case: spinning up a local environment, loading data, and creating a self-correcting LangChain agent that actually understands and interacts with your product catalog.

Setting Up the Development Environment

To get started, you need to set up your development environment by cloning the Enthusiast starter repository and using its Docker configuration.

  1. Clone the repository:
    git clone https://github.com/upsidelab/enthusiast-starter

  2. Navigate into the repository directory:
    cd enthusiast-starter

  3. Copy default configuration file and add your own OpenAI API key:
    cp config/env.sample config/env
    echo OPENAI_API_KEY=xxxx >> config/env

  4. Build and run the Docker containers:
    docker compose up

Once it’s running, open your browser and go to: http://localhost:10001 Log in with the default credentials:****

You’ll be prompted to create your first dataset. Give it a name, for example, “My Dataset”.

Import a Sample Product Dataset
Enthusiast comes with a sample set of products that can be useful if you want to get started quickly. In this case, we have a set of products that represent different phone and mobile plans - with varying internet speeds, data limits, landline access, cable TV options, and more. They make a great test case for experimenting with different approaches to agentic product recommendations.

Let’s import this into our dataset:

  1. Click on “Add Source” in the top-right corner of the screen.
  2. From the dropdown, select “Product source”.
  3. A popup will appear for configuring the source.
  4. Select “Sample Product Source” from the list and click “Add”.
  5. You should now see it listed under configured sources.
  6. Repeat the same process for documents by selecting “Document source” from the dropdown.
  7. This time, choose “Sample Document Source” as the type and add it as well.

Enthusiast will automatically index the dataset so it’s searchable right away.

Once the data is loaded, you can go to the Products tab to verify that the sample data was successfully imported and indexed. This ensures that your dataset is ready for querying by the agent.

Create a Custom Agent Structure

Now that your product catalog is loaded, it’s time to build an agent that can operate on it. Enthusiast supports extending and modifying agent behavior through the enthusiast_custom directory in the project.

  1. Inside the enthusiast-starter repository, locate the src/enthusiast_custom directory. This is the package that contains your custom agents and plugins. This code will be bundled by the Dockerfile and automatically installed into your Enthusiast instance.

  2. Let’s also install a plugin that provides a reusable base implementation for a ReAct-style agent. Run the following command inside the src/ directory to add the plugin:
    poetry add enthusiast-agent-re-act

  3. Then, create a new directory inside enthusiast_custom, calling it for example product_search. Inside this directory, add an empty init.py file to make it a Python package. This is where you’ll define your agent’s implementation.

  4. Add your new agent to the config/settings_override.py file so that Enthusiast can recognize it. Update the AVAILABLE_AGENTS dictionary to include your custom module:

AVAILABLE_AGENTS = {
    "Product Search": "enthusiast_custom.product_search"
}
  1. You can now rebuild and restart your Docker Compose setup to apply these changes: docker compose up --build

Once the application is restarted, you’ll see your new agent listed in the UI on the left. Time to give it some logic.

Step 1 – Generate an SQL Query

We’ll start with a basic implementation that generates an SQL query and executes it on the product catalog indexed in Enthusiast. The agent will reason through user queries and interact with the catalog to retrieve relevant results.

To do this, we’ll use the enthusiast-agent-re-act plugin that we added earlier. It provides a BaseReActAgent class, which defines the core structure of a ReAct-style agent, including how it connects prompts, tools, memory, and output processing.

Here’s how we’ll structure the product_search agent module:
product_search/agent.py

Start by defining the agent class. In a basic scenario, no overrides are required - agent’s default implementation will respond to user’s queries by creating an agent executor configured with tools and memory, and will pass the user’s request there.
Here’s what the simplest implementation looks like:

from enthusiast_agent_re_act import BaseReActAgent

class ProductSearchAgent(BaseReActAgent):
    pass

product_search/product_search_tool.py

Next, implement a tool the agent can use to run SQL queries against your product catalog.

Let’s first declare the expected input schema using a Pydantic model. This schema will be provided to the agent together with the tool definition, to let the agent determine what’s needed to call this tool. Since we specify that the tool requires an SQL query, the agent will try to produce one based on everything it knows so far in order to invoke it.

from pydantic import BaseModel, Field

class ProductSearchToolInput(BaseModel):
    sql_query: str = Field(description="sql select query to execute on the catalog_product table. include both where and order clauses")

This tool receives an SQL string from the agent, executes it using Django’s ORM, serializes the resulting product objects, and returns a message with the result. The NAME and DESCRIPTION fields in the tool definition help the agent determine when this tool is relevant to the current task.

Here’s a basic version of the tool implementation:

from enthusiast_common.tools import BaseLLMTool
from catalog.models import Product
from django.core import serializers

class ProductSearchTool(BaseLLMTool):
    NAME = "products_search_tool"
    DESCRIPTION = "use it to search for products"
    ARGS_SCHEMA = ProductSearchToolInput
    RETURN_DIRECT = False

    def run(self, sql_query: str):
        products = Product.objects.raw(self._sanitize_sql(sql_query))
        serialized = serializers.serialize("json", list(products))
        return f"Found the following products: {serialized}. Notify the user."
    def _sanitize_sql(self, sql_query: str):
               # This is where you should add some extra safety checks to the SQL query before you execute it on a live database
        return sql_query

product_search/prompt.py

Then, create the system prompt that will guide how the agent reasons and interacts with tools. Add the following:

PRODUCT_FINDER_AGENT_PROMPT = """
I want you to help finding products using the ReACT (Reasoning and Acting) approach.
Always verify your answer
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).

Valid "action" values: {tool_names}

Provide only ONE action per $JSON_BLOB, as shown:


{{
  "action": $TOOL_NAME,
  "action_input": $INPUT
}}

For each step, follow the format:
User query: the user's question or request
Thought: what you should do next
Action: 
{{
  "action": "<tool>",
  "action_input": {{"<tool_argument_name>": "<tool_argument_value>", ...}}
}}
Observation: the result returned by the tool
... (repeat Thought/Action/Action Input/Observation as needed)
Thought: I now have the necessary information
Final Answer: the response to the user

Here are the tools you can use:
{tools}

Do not come up with any other types of JSON than specified above.
Your output to user should always begin with '''Final Answer: <output>'''

Begin!
Chat history: {chat_history}
User query: {input}
{agent_scratchpad}"""

product_search/config.py

Finally, wire everything together in the config file. This tells Enthusiast which components make up your agent:

def get_config(conversation_id: int, streaming: bool) -> AgentConfigWithDefaults:
    return AgentConfigWithDefaults(
        conversation_id=conversation_id,
        agent_class=ProductSearchAgent,
        llm_tools=[
            LLMToolConfig(
                tool_class=AgentProductSearchTool,
            ),
        ],
        prompt_template=ChatPromptTemplate.from_messages(
            [
                (
                    "system",
                    PRODUCT_FINDER_AGENT_PROMPT,
                ),
            ]
        ),
        llm=LLMConfig(
            callbacks=[ReactAgentWebsocketCallbackHandler(conversation_id)],
            streaming=streaming,
        ),
        agent_callback_handler=AgentCallbackHandlerConfig(
            handler_class=AgentActionWebsocketCallbackHandler, args={"conversation_id": conversation_id}
        ),
    )

Once these components are in place and the Docker container is rebuilt, try executing a sample query:
What’s the best plan for a small family?
The agent will reason about the input, construct an SQL query, and invoke the search tool, likely failing due to invalid schema or search criteria. Let’s see what we can do with that.

Step 2 – Let the Agent Handle Its Own Errors

In the initial version, if the SQL query generated by the agent was incorrect, the tool would simply fail without giving the agent any indication of what went wrong. We can improve this by modifying the tool to catch SQL errors and return the error message as part of the response.

This way, the agent can treat the error as feedback and make another attempt, refining the query on its own.

To do this, update the run method in ProductSearchTool as follows:

from django.core import serializers
import logging

logger = logging.getLogger(__name__)

class ProductSearchTool(BaseLLMTool):
    NAME = "products_search_tool"
    DESCRIPTION = "use it to search for products"
    ARGS_SCHEMA = ProductSearchToolInput
    RETURN_DIRECT = False

    def run(self, sql_query: str):
        try:
            products = Product.objects.raw(self._sanitize_sql(sql_query))
            serialized = serializers.serialize("json", list(products))
            return f"Found the following products: {serialized}. Notify the user."
        except Exception as e:
            logger.info(e)
            return f"The query you've generated is incorrect. Error {type(e).__name__} - {e}. Please fix it and try again. Make sure it's a valid SQL."

With this change, when the SQL query fails, the agent gets the error message and can use it to revise its approach. Since the agent maintains memory of previous steps, it can iterate on its output to try and produce a valid query.

Try running the same query again:
What’s the best plan for a small family?

If the first attempt fails, the agent will receive the error, analyze it, and try to generate a better query.

Step 3 – Help the Agent Understand the Data

Letting the agent correct its own mistakes is helpful, but trial and error can be inefficient. Instead of waiting for the agent to fail and recover, we can give it a clearer understanding of the data structure up front.

One simple way to do this is by including a few sample rows from the product catalog directly in the prompt. This helps the agent understand both the schema and the shape of the data, which improves its chances of generating valid queries from the start.

To add this context, let’s override the get_answer method in your agent like this:

class ProductSearchAgent(BaseReActAgent):
    def get_answer(self, input_text: str) -> str:
        sample_products = self._injector.product_retriever._get_sample_products_json()
        agent_executor = self._build_agent_executor()
        agent_output = agent_executor.invoke(
            {"input": input_text, "sample_products": sample_products},
            config=self._build_invoke_config()
        )
        return agent_output["output"]

This method will use functionality provided by the base class to build a LangChain-based agent executor, pass the input to it, and return the response to the user. One important change here is that besides user’s input (passed as input_text ), it will also pull a few sample products from the database and will inject them into the agent’s system prompt as sample_products.

In your prompt template (prompt.py), add this placeholder at the end:
Here are some sample products in the database: {sample_products}

This additional context will be included with every call to the agent. It initializes the agent with a basic understanding of the structure and shape of the data, which makes it easier for the agent to construct accurate queries from the start.
Let’s give it a try.

You should notice that the agent now constructs queries that better match how the data is shaped. For example, it may use the category column to search for plans labeled as “Home,” or rely on the properties column to filter for plans with specific internet speeds.

Step 4 – Retry When No Results Are Found

Even if the agent is capable of generating valid SQL queries and has seen sample data, there’s still a chance it will produce a query that technically runs but returns no results.

In the current implementation, when that happens, the tool simply returns an empty list, and the agent assumes there are no relevant options. But in reality, the issue may be with how the agent built the query, not with a lack of products.

To address this, we can update the tool to return a clear message when no products are found—encouraging the agent to try a different approach. Here’s how the updated run method might look:

class ProductSearchTool(BaseLLMTool):
    NAME = "products_search_tool"
    DESCRIPTION = "use it to search for products"
    ARGS_SCHEMA = ProductSearchToolInput
    RETURN_DIRECT = False

    def run(self, sql_query: str):
        try:
            products = Product.objects.raw(self._sanitize_sql(sql_query))
            products = list(products)
        except Exception as e:
            logger.info(e)
            return f"The query you've generated is incorrect. Error {type(e).__name__} - {e}. Please fix it and try again. Make sure it's a valid SQL."

        if len(products) > 0:
            serialized = serializers.serialize("json", products)
            return f"Found the following products: {serialized}. Notify the user."
        else:
            return "No products found using this search criteria. Try using a different query and try again."

With this change, the agent receives explicit feedback when a query returns no matches. It can then choose to revise the query and try again with broader or alternative criteria.

This gives the agent an opportunity to step back and reconsider its assumptions, leading to better resilience and more accurate results when dealing with uncertain or ambiguous user requests.

Step 5 – Respect the Expected Number of Results

In some cases, a user might indicate how many products they want to see—perhaps just one recommendation or the top three matches. Right now, the agent doesn’t take that into account. It may return a long list of results, even if the user only wanted a few.

We can improve this by passing the expected number of results as part of the tool input. The tool will then check whether the number of matches exceeds this limit. If it does, it will prompt the agent to follow up and narrow the criteria.

First, update the input schema to include this new parameter:

class AgentProductSearchToolInput(BaseModel):
    sql_query: str = Field(description="sql select query to execute on the catalog_product table. include both where and order clauses")
    result_number: int = Field(description="the number of results that the user wants to get")
Then, update the run method in the tool to handle this input:
class ProductSearchTool(BaseLLMTool):
    NAME = "products_search_tool"
    DESCRIPTION = "use it to search for products"
    ARGS_SCHEMA = AgentProductSearchToolInput
    RETURN_DIRECT = False

    def run(self, sql_query: str, result_number: int):
        try:
            products = Product.objects.raw(self._sanitize_sql(sql_query))
            products = list(products)
        except Exception as e:
            logger.info(e)
            return f"The query you've generated is incorrect. Error {type(e).__name__} - {e}. Please fix it and try again. Make sure it's a valid SQL."

        if len(products) > 0:
            serialized_products = serializers.serialize("json", products)
            if len(products) > result_number:
                return f"Found the following products: {serialized_products}. However, the user would like to get at most {result_number}. Ask a follow up question that will make it possible to limit the amount of results further. Don't return these products to the user yet."
            else:
                return f"Found the following products: {serialized_products}. Notify the user."
        else:
            return "No products found using this search criteria. Try using a different query and try again."

This addition helps turn the agent into a more effective product search assistant. Instead of assuming that the initial results are appropriate, the agent now reflects on the quantity of data returned, checks it against user expectations, and adjusts accordingly. This creates a more collaborative flow where the agent and user refine the query together to land on a relevant result.

Step 6 – Enable the Agent to Finalize a Purchase

Once the user finds a plan that matches their needs, the next logical step is to help them act on it. Right now, our agent can recommend products but doesn’t support any kind of checkout process.

To make this possible, we’ll give the agent the ability to generate a contract URL the user can follow to finalize their purchase. This effectively allows the agent to transition from discovery to action.

Start by creating a new tool, PurchaseTool, which accepts a plan_sku and returns a contract finalization link:

from enthusiast_common.tools import BaseLLMTool
from pydantic import BaseModel, Field

class PurchaseToolInput(BaseModel):
    plan_sku: str = Field("SKU of the plan that the customer wants to purchase")

class PurchaseTool(BaseLLMTool):
    NAME = "purchase_tool"
    DESCRIPTION = "use when the user wants to sign a contract for a plan"
    ARGS_SCHEMA = PurchaseToolInput
    RETURN_DIRECT = False

    def run(self, plan_sku: str):
        url = f"https://fiberup.upsidelab.net/finalize_contract?{plan_sku}"
        return f"Show the following url to the user, and ask them to use it to finalize their signature {url}"
Then, register this tool in your agents config:
from .purchase_tool import PurchaseTool
...
llm_tools=[
    LLMToolConfig(tool_class=AgentProductSearchTool),
    LLMToolConfig(tool_class=PurchaseTool),
],

Lastly, modify the search tool’s return message slightly to encourage the agent to propose a contract. The agent will likely figure it out even without this hint, but there’s no harm in pushing it more explicitly:

return f"Found the following products: {serialized_products}. Notify the user and propose signing a contract for this plan."

With this addition, your agent becomes a guided assistant that helps the user discover a suitable plan and smoothly transition into completing the purchase.

Step 7 – Ask for Additional Customer Details

Before the agent pushes the user to sign a contract, it can also ensure that it collects any additional information needed to complete the process—such as the customer’s name and location.
To support this, update the PurchaseToolInput schema with two new fields:

class PurchaseToolInput(BaseModel):
    plan_sku: str = Field("SKU of the plan that the customer wants to purchase")
    customer_name: str = Field("Name of the customer")
    zipcode: str = Field("Zipcode of the customer")
Then modify the run method to include this information in the contract URL:
import urllib.parse

class PurchaseTool(BaseLLMTool):
    NAME = "purchase_tool"
    DESCRIPTION = "use when the user wants to sign a contract for a plan"
    ARGS_SCHEMA = PurchaseToolInput
    RETURN_DIRECT = False

    def run(self, plan_sku: str, customer_name: str, zipcode: str):
        url = f"https://fiberup.upsidelab.net/finalize_contract?{plan_sku}&name={urllib.parse.quote_plus(customer_name)}&zipcode={zipcode}"
        return f"Show the following url to the user, and ask them to use it to finalize their signature {url}"

Thanks to the structured schema and tool description, the agent will know that it must collect these inputs from the user before invoking the tool. If the information isn’t provided initially, the agent can follow up with questions like:

Could you tell me your name and zip code so I can finalize the contract?

This closes the loop and ensures that the agent not only helps discover the right plan but can also guide the user through to a complete and personalized purchase process.

Summary

In this walkthrough, we explored how to build a practical AI agent for product catalog search using Enthusiast. Starting from a basic ReAct-style agent capable of generating SQL queries, we incrementally introduced more sophisticated behaviors:

  • Error recovery through exception feedback
  • Schema-aware reasoning via sample data
  • Retry logic when no results are found
  • Adapting results to match user expectations
  • Finalizing user purchases with structured follow-up
  • Collecting required customer details before contract generation

Each step was designed to bring the agent closer to an experience that feels like a helpful, iterative assistant.

To try this yourself, check out the Enthusiast starter repository: https://github.com/upsidelab/enthusiast

For full documentation and examples, visit: https://github.com/upsidelab/enthusiast


This content originally appeared on DEV Community and was authored by Chris Zhang


Print Share Comment Cite Upload Translate Updates
APA

Chris Zhang | Sciencx (2025-08-21T20:26:11+00:00) How to Build a Self-Correcting AI Agent for Product Search in E-Commerce. Retrieved from https://www.scien.cx/2025/08/21/how-to-build-a-self-correcting-ai-agent-for-product-search-in-e-commerce/

MLA
" » How to Build a Self-Correcting AI Agent for Product Search in E-Commerce." Chris Zhang | Sciencx - Thursday August 21, 2025, https://www.scien.cx/2025/08/21/how-to-build-a-self-correcting-ai-agent-for-product-search-in-e-commerce/
HARVARD
Chris Zhang | Sciencx Thursday August 21, 2025 » How to Build a Self-Correcting AI Agent for Product Search in E-Commerce., viewed ,<https://www.scien.cx/2025/08/21/how-to-build-a-self-correcting-ai-agent-for-product-search-in-e-commerce/>
VANCOUVER
Chris Zhang | Sciencx - » How to Build a Self-Correcting AI Agent for Product Search in E-Commerce. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/21/how-to-build-a-self-correcting-ai-agent-for-product-search-in-e-commerce/
CHICAGO
" » How to Build a Self-Correcting AI Agent for Product Search in E-Commerce." Chris Zhang | Sciencx - Accessed . https://www.scien.cx/2025/08/21/how-to-build-a-self-correcting-ai-agent-for-product-search-in-e-commerce/
IEEE
" » How to Build a Self-Correcting AI Agent for Product Search in E-Commerce." Chris Zhang | Sciencx [Online]. Available: https://www.scien.cx/2025/08/21/how-to-build-a-self-correcting-ai-agent-for-product-search-in-e-commerce/. [Accessed: ]
rf:citation
» How to Build a Self-Correcting AI Agent for Product Search in E-Commerce | Chris Zhang | Sciencx | https://www.scien.cx/2025/08/21/how-to-build-a-self-correcting-ai-agent-for-product-search-in-e-commerce/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.