Close

Sign up to Gadget

Build a product recommendation chatbot for a Shopify store using OpenAI and Gadget

-

About

Build a chat bot that recommends products to shoppers and explore some of the Gadget features that enable you to build AI-powered Shopify apps.

Problem

Solution

Result

Build a product recommendation chatbot for a Shopify store using OpenAI and Gadget

Riley Draward
May 12, 2023

Build a product recommendation chatbot for a Shopify store using OpenAI and Gadget

Topics covered: Shopify connections, AI + vector embeddings, HTTP routes, React frontends
Time to build: ~30 minutes

Large Language Model (LLM) APIs allow developers to build apps that can understand and generate text.

In this tutorial, you will build a product recommendation chatbot for a Shopify store using OpenAI's API and Gadget. The chatbot will utilize OpenAI's text embedding API to generate vector embeddings for product descriptions, which will then be stored in your Gadget database. These embeddings will help to identify the products that best match a shopper's chat message. With Gadget, you can easily sync data from Shopify, build the backend for generating recommendations, and create the chatbot UI.

A screenshot of the chatbot in action. The shopper asks about backpacks, and the bot responds with 3 suggestions. The suggested product titles and images are then displayed below the chatbot's response
Requirements

To get the most out of this tutorial, you will need:
 - a Shopify Partners account
  - a development store
 - an OpenAI account and API Key that can be used to make requests to the OpenAI API

You can fork this Gadget project and try it out yourself.

You will still need to set up the Shopify Connection after forking. Read on to learn how to connect Gadget to a Shopify store!

Fork on Gadget

Want to learn more about building AI apps before getting started? Watch our online learning session where Harry explains LLMs, vector embeddings, and more!

Step 1: Create a Gadget app and connect to Shopify

Our first step will be to set up a Gadget project and connect our backend to a Shopify store via the Shopify connection. Create a new Gadget application at gadget.new and select the Shopify app template.

Because we are adding an embedded frontend, we are going to build an app using the Partners connection.

Connect to Shopify through the Partners dashboard

Both the Shopify store Admin and the Shopify Partner Dashboard have an Apps section. Ensure that you are on the Shopify Partner Dashboard before continuing.

  • Click the Create App button
  • Click the Create app manually button and enter a name for your Shopify app
  • Go to the Connections page in your Gadget app and click on Shopify
  • Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget Connections page
  • Click Connect on the Gadget Connections page to move to scope and model selection

Now we get to select what Shopify scopes we give our application access to, while also picking what Shopify data models we want to import into our Gadget app.

  • Enable the read scope for the Shopify Products API, and select the underlying Product and Product Image models that we want to import into Gadget
  • Click Confirm

Now we want to connect our Gadget app to our custom app in the Partners dashboard.

  • In your Shopify app in the Partners dashboard, click on App setup in the side nav bar so you can edit the App URL and Allowed redirection URL(s) fields
  • Copy the App URL and Allowed redirection URL from the Gadget Connections page and paste them into your custom Shopify App
Screenshot of the connected app, with the App URL and Allowed redirection URL(s) fields

Now you can install your app on a store from the Partners dashboard. Do not sync data yet! You're going to add some code to generate vector embeddings for your products before the sync is run.

Step 2: Set up OpenAI connection

Now that you are connected to Shopify, you can also set up the OpenAI connection that will be used to fetch embeddings for product descriptions. Gadget provides OpenAI credits for testing while developing, however, you will need to use your own OpenAI API key for this tutorial, as the Gadget-provided credentials are rate-limited.

  1. Click on Connections in the nav bar
  2. Click on the OpenAI connection tile
  3. Select the Use your own API keys option in the modal that appears
  4. Paste your OpenAI API key into the Development key field

Your connection is now ready to be used!

Step 3: Add vector field to Shopify Product model

Before you add code to create the embeddings from product descriptions, you need a place to store the generated embeddings. You can add a vector field to the Shopify Product model to store the embeddings.

The vector field types store a vector, or array, of floats. It is useful for storing embeddings and will allow you to perform vector operations like cosine similarity, which helps you find the most similar products to a given chat message.

To add a vector field to the Shopify Product model:

  • Go to the shopifyProduct model in the navigation bar
  • Click on + in the FIELDS section to add a new field
  • Name the field descriptionEmbedding
  • Set the field type to vector

Now you are set up to store embeddings for products! The next step is adding code to generate these embeddings.

Step 4: Write code effect to create vector embedding

Now you can add some code to create vector embeddings for all products in your store. You will want to run this code when Shopify fires a products/create or products/update webhook. To do this, you will create a code effect that runs when a Shopify Product is created or updated.

  1. Go to the shopifyProduct model in the navigation bar
  2. Click on the create action in the ACTION selection
  3. Paste the following code into shopifyProduct/create.js to update the onSuccess function:

In this snippet, the OpenAI connection is accessed through connections.openai and the embeddings.create() API is called.

The internal API is used in the onSuccess function to update the shopifyProduct model and set the descriptionEmbedding field. The internal API needs to be used because the shopifyProduct model does not have Gadget API set as a trigger on this action by default. You can read more about the internal API in the Gadget docs.

Generate embeddings for existing products

Now that the code is in place to generate vector embeddings for products, you can sync existing Shopify products into your Gadget app's database. To do this:

  • Go to the Connections page in the navigation bar
  • Click on the Shopify connection
  • Click on Shop Installs for the connection
Screenshot of the connected app on the Shopify connections page, with the Shop Installs button highlighted
  • Click on the Sync button for the store you want to sync products from
Screenshot of the shop installs page, with an arrow added to the screenshot to highlight the Sync button for the connected store

Product and product image data will be synced from Shopify to your Gadget app's database. The code effect you added will run for each product and generate a vector embedding for the product. You can see these vector embeddings by going to the Data page for the Shopify Product model. The vector embeddings will be stored in the descriptionEmbedding field.

A screenshot of the Data page for the Shopify Product model. The descriptionEmbedding column is highlighted, with vector data generated for products.

Step 5: Add HTTP route to handle incoming chat messages

To complete your app backend, you will use cosine similarity on the stored vector embeddings to extract products that are closely related to a shopper's query. These products, along with a prompt, will be passed into LangChain, which will then use an OpenAI model to respond to the shopper's question. In addition, you will include product information to display recommended products by LangChain and provide a link to the store page for these products.

You will also stream the response from LangChain to the shopper's chat window. This will allow you to show the shopper that the chatbot is typing while it is generating a response.

Install LangChain and zod npm packages

To start, install the LangChain and zod npm packages. The zod package will be used to provide a parser to LangChain for reliably extracting structured data from the LLM response.

  • Open the Gadget command palette using P or Ctrl P
  • Enter > in the palette to allow you to run yarn commands
  • Enter yarn add langchain@0.0.66 zod to install the LangChain client and zod
A screenshot of Gadget command palette with 'yarn add langchain@0.0.66 zod' ready to be run!

Add code to handle incoming chat messages

Now you are ready to add some more code. You will start by adding a new HTTP route to handle incoming chat messages. To add a new HTTP route to your Gadget backend:

  • Hover over the routes folder in the FILES explorer and click on + to create a new file
  • Name the file POST-chat.js

Your app now has a new HTTP route that will be triggered when a POST request is made to /chat. You can add code to this file to handle incoming chat messages.

This is the complete code file for POST-chat.js. You can copy and paste this code into the file you just created. A step-by-step explanation of the code is below.


Step-by-step instructions for building this route are below.

Set up LangChain

The first thing you need to do when building this route is to set up LangChain. LangChain needs a couple of things defined before it can be used to respond to a user's chat message, including a parser to format the response from OpenAI, a prompt template that contains some text defining the purpose of the prompt and variables that will be passed in, and finally, the OpenAI model that will be used.

Begin with setting up a StructuredOutputParser. This parser will format the response from OpenAI into a structured JSON object that can be utilized by the frontend to display the response to the user. The parser uses zod to structure the response, which will consist of a string answer and an array of product IDs productIds recommend to shoppers.


Now that you have a parser, you can set up the prompt template. The prompt template is a string that contains the text that will be used to prompt the OpenAI model to respond to the user's chat message. The prompt template can also include variables that will be passed in when the prompt is invoked. In this case, the prompt template includes a variable for the user's question and a variable for the products that will be passed in as initial recommendations. Formatting instructions created with the parser are also passed into the prompt.


Once the parser and prompt are both defined, you can set up LangChain's OpenAI model and the chain that will be called in the route.


The temperature parameter is set to 0 to ensure that the response from OpenAI is deterministic. The streaming parameter is set to true to ensure that the response from OpenAI is streamed in as it is generated, rather than waiting for the entire response to be generated before returning it. The chain is defined using the model, prompt, and parser.

LangChain model selection
The OpenAI model and LLMChain are used in this tutorial as an example of how to use chains and prompt templates with LangChain. LangChain has a variety of different models, including chat-specific models, that might be worth investigating for your app.

Define route parameters

Now that LangChain is set up, you can define the route parameters. The route will accept a message from the shopper as part of a request body. The message will be passed into the prompt template.

To define this message parameter, you can use the schema option in the route module's options object. The schema option is used to define the JSON schema for the request body.

Define the parameter at the bottom of routes/POST-chat.js:


Now you can start to write the actual route code that runs when a shopper asks a question.

Find similar products

The first thing that your route will do is create an embedding vector for the shopper's question and use that vector to find similar products. The embedMessage function created earlier will be used to embed the shopper's question.

Gadget includes a cosineSimilarityTo operator that can be used to sort the results of a read query by cosine similarity to a given vector. The cosineSimilarityTo operator is used in the sort parameter of the findMany query to sort the results by cosine similarity to the embedded message. The first parameter is used to limit the number of results returned to 4.

In other words, the query will return the 4 products that are most similar to the shopper's question.


A product string is then created from the list of returned products:


You are now ready to invoke the chain and stream the response back to the shopper.

Stream response from LangChain

Now that the chain is defined, you can invoke it and stream the response back to the shopper. The call method of the chain is used to invoke the chain. The call method takes two parameters: an object containing the input variables for the prompt and an array of callback handlers.

You define a new Readable stream and immediately send it as a response using await reply.send(stream);. Once you have done this, any additional data pushed to the stream will be sent back to the route called.

Finally, chain.call() is invoked to generate a response to the shopper's question, and takes the request.body.message and productString as input for the shopper's question and the products with the closest descriptions matching the question, respectively.

The ConsoleCallbackHandler will output the response from LangChain to the Gadget Logs, so you can see the exact input and output from LangChain. An additional callback is defined using handleLLMNewToken to stream the response from LangChain to the shopper. The handleLLMNewToken callback is invoked every time a new token is generated by LangChain. The token will contain a fragment of the complete response, which will be pushed to the stream and returned to the shopper.

Some additional token handling is also done using tokenText. In the parser, a JSON object was defined for a response. You do not want to push the JSON object keys to the shopper, so they are filtered out using tokenText.includes('"answer": "') && !tokenText.includes('",\n'). The token is then pushed to the stream.

The stream is closed using stream.push(null);. In the case of an error, stream.destroy() is called to close the stream.


This will stream the entire chat response back to the shopper. But this isn't all you want to return, you also want additional product info so you can display product listings and provide links to the products from your frontend. To do this, you can use the productIds returned from the chain. Not all product IDs that were passed in will be used in the response, so it is important to use the IDs returned from the chain and not the IDs grabbed using the cosine similarity operation.

The returned productIds can be used as a filter on the shopifyProduct model to grab the product details for the recommended products. The await api.shopifyProduct.findMany() is a read operation that will return the fields defined in the select GraphQL query. The product title and handle are returned, as well as the product image source and the product's shop domain. The filter GraphQL query will filter the results to only include products with an ID that is in the productIds array.

Once the product details are returned, they are also sent to the route's caller via the stream.


Your route is now complete! Now all that is needed is a frontend app that allows shoppers to ask a question and displays the response along with product recommendations. You're going to use Gadget's hosted React frontends to build this UI.

Build a frontend

All Gadget apps come with hosted Vite frontends that can be used to build your UI. You can use these frontends as a starting point for your UI, or you can build your UI from scratch. For this tutorial, you're going to use the hosted React frontend to build a chat widget that can be embedded on any page of your Shopify store.

Build the chat widget

In your Gadget app's frontend folder, create a new file called Chat.jsx. This file will contain the React component for the chat widget. The complete code for this file is below, but you'll walk through each piece of the code in the following sections.


Step-by-step chat widget build

The first thing to set up when building the chat widget is the useFetch hook. This hook is provided by Gadget and is used to make requests to the backend HTTP route. The useFetch hook takes two arguments: the backend route to call and an options object. The options object is used to configure the request. In this case, you're setting the request method to POST and the content-type header to application/json. You're also setting the stream option to true. This option tells the useFetch hook to return a stream that can be used to read the response from the backend route. The useFetch hook returns an object containing response and fetching info, and a function that can be called to make the actual request to your chat route. This function is named sendChat.


React state is also defined above to manage the text that the shopper enters into the chat widget, as well as the streamed chat response and recommended product info. The next thing to add is a <form> that makes use of this state. A shopper will use the form to ask a question. The form's onSubmit callback will handle the streamed response from the backend route using Node's built-in streaming tooling.


This code will send the user's message to the backend route and then read the response from the stream. The response will be a JSON string that contains the chat response and any recommended products. The while loop will read the response from the stream until the stream is done. The while loop will also handle the response by setting the chat response and recommended products in React state.

The next thing to do is render the chat response and recommended products in the chat widget. The chat response is rendered in a <p> tag, and the recommended products are rendered in an <a> tag. The <a> tag will link to the product's page on the shop's storefront.


Adding loading and error messaging is also nice. This can be done by rendering a <span> with a loading message when the fetching variable is true, and rendering a <p> with an error message when the error variable is true.


Hook up chat widget to frontend project

Now you can add the chat widget to your app's frontend. Because this is not an embedded Shopify app, you can simplify frontend/App.jsx with the following code that imports the chat widget and renders it at your app's default route:


This isn't an admin-embedded Shopify app, so you can use the Provider from @gadgetinc/react instead of the App Bridge Provider. The Provider enables you to make authenticated requests with your Gadget app's api client defined in frontend/api.js.

You can overwrite the default code in frontend/main.jsx with the following:


Finally, add some styles to make the chat widget look nice. You can copy-paste the following css into frontend/App.css:


Step 7: Test out your chatbot

To view your app:

  • Click on your app name in the top left corner of the Gadget dashboard
  • Hover over Go to app and select your Development app

Congrats! You have built a chatbot that uses the OpenAI API to find the best products to recommend to shoppers. Test it out by asking a question. The chatbot should respond with a list of products that are relevant to the question.

Screenshot of the finished chatbot, with a question entered (asking about backpacks for sale) and a response. The response includes a text response and product recommendations, both generated by langchain

Next steps

Have questions about the tutorial? Join Gadget's developer Discord to ask Gadget employees and join the Gadget developer community!

Want to learn more about building AI apps in Gadget? Check out our building AI apps documentation.

Keep reading

No items found.
Keep reading to learn about how it's built

Under the hood

We're on Discord, drop in and say hi!
Join Discord
Bouncing Arrow