Build a storefront chatbot with OpenAI 

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

Large Language Model (LLM) APIs allow developers to build apps that can understand and generate text. We can use OpenAI's APIs to build a chatbot that can understand a shopper's question and respond with product recommendations.

In this tutorial, you will learn how to:

Prerequisites 

To get the most out of this tutorial, you will need:

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

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

Fork on Gadget

Step 1: Create a Gadget app and connect to Shopify 

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

Now we will set up a custom Shopify application in the Partners dashboard.

  1. Go to the Shopify Partner dashboard
  2. Click on the link to the Apps page

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 on Apps link in Shopify Partners Dashboard
  1. Click the Create App button
Click on Create app button
  1. Click the Create app manually button and enter a name for your Shopify app
Shopify's app creation landing page in the Partners Dashboard
  1. Click on Settings in the side nav bar
  2. Click on Plugins in the modal that opens
  3. Select Shopify from the list of plugins and connections
The Gadget homescreen, with the Connections link highlighted
  1. Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget Connections page
Screenshot of the Partners card selected on the Connections page
  1. 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.

  1. Select the scopes and models listed below and click Confirm to connect to the custom Shopify app
  • Enable the Products Read API scope, and select the underlying Product and Product media models that we want to import into Gadget
Select Product API scope + model

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

  1. In your Shopify app in the Partner dashboard, click on Configuration in the side nav bar so you can edit the App URL and Allowed redirection URL(s) fields
  2. 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 we need to install our Shopify app on a store.

  1. Go back to the Shopify Partner dashboard
  2. Click on Apps to go to the Apps page again
  3. Click on your custom app
  4. Click on Select store
Click on the Select store button
  1. Click on the store we want to use to develop our app
  2. You may be prompted about Store transfer being disabled. This is okay, click Install anyway
  3. Click Install app to install your Gadget app on your Shopify store
Having an issue installing?

If you are getting a permissions denied error when installing your app, try logging in to the Shopify store Admin!

Click Install app to authorize our Gadget app with our store

You will be redirected to an embedded admin app that has been generated for you. The code for this app template can be found in web/routes/index.jsx.

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 building your app, so you don't need a personal OpenAI API key to get started.

  1. Click on Settings in the nav bar
  2. Click on Plugins
  3. Click on the OpenAI connection
  4. Select the Use Gadget's API keys option in the modal that appears OR enter your own OpenAI API key
  5. Click Add connection at the bottom of the page

Your OpenAI connection is now ready to be used!

Step 3: Add data models for chat responses 

Now we need a new data model to capture the chat responses from OpenAI, and the products recommended to shoppers. We need to use a has many through relationship to relate the chat response to the recommended products.

ERD of the chatbot data models, showing the has many through relationship between chatLog and shopifyProduct, through recommendedProduct. The diagram also shows that shopifyProduct has many shopifyProductMedia
  1. Click + next to the api/models folder and add a new chatLog model
  2. Add the following fields to chatlog:
Field nameField type
responsestring
productshas many through

Now you need to define the products relationship.

  1. Set the sibling to shopifyProduct and the joinModel to recommendedProduct, this will create a new recommendedProduct model
  2. Name the field in recommendedProduct that relates to the chatLog model, chatLog
  3. Name the field in recommendedProduct that relates to the shopifyProduct model, product
  4. Name the field in the shopifyProduct model that relates to the recommendedProduct model, chatRecommendations
A screenshot of the has many through relationship defined between the chatlog and shopifyProduct models

Step 4: Add vector field to shopifyProduct 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 shopifyProduct model to store the embeddings.

The vector field types store a vector, or array, of floats. It is useful for storing vector 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 shopifyProduct model:

  1. Go to api/models/shopifyProduct/schema and add the following field:
Field nameField type
descriptionEmbeddingvector
A screenshot of the Description Embedding vector field defined on the Shopify Product model

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

Step 5: 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 api/models/shopifyProduct/actions folder and add a new createEmbedding.js action
  2. Paste the following code into api/models/shopifyProduct/createEmbedding.js:
api/models/shopifyProduct/createEmbedding.js
JavaScript
1import { applyParams, save, ActionOptions } from "gadget-server";
2import { preventCrossShopDataAccess } from "gadget-server/shopify";
3
4export const run: ActionRun = async ({
5 params,
6 record,
7 logger,
8 api,
9 connections,
10}) => {
11 applyParams(params, record);
12 await preventCrossShopDataAccess(params, record);
13 await save(record);
14};
15
16export const onSuccess: ActionOnSuccess = async ({
17 params,
18 record,
19 logger,
20 api,
21 connections,
22}) => {
23 try {
24 // get an embedding for the product title + description using the OpenAI connection
25 const response = await connections.openai.embeddings.create({
26 input: `${record.title}: ${record.body}`,
27 model: "text-embedding-ada-002",
28 });
29 const embedding = response.data[0].embedding;
30
31 // write to the Gadget Logs
32 logger.info({ id: record.id }, "got product embedding");
33
34 // use the internal API to store vector embedding in Gadget database, on shopifyProduct model
35 await api.internal.shopifyProduct.update(record.id, {
36 shopifyProduct: { descriptionEmbedding: embedding },
37 });
38 } catch (error) {
39 logger.error({ error }, "error creating embedding");
40 }
41};
42
43export const options: ActionOptions = {
44 actionType: "update",
45 triggers: {
46 api: true,
47 },
48};
1import { applyParams, save, ActionOptions } from "gadget-server";
2import { preventCrossShopDataAccess } from "gadget-server/shopify";
3
4export const run: ActionRun = async ({
5 params,
6 record,
7 logger,
8 api,
9 connections,
10}) => {
11 applyParams(params, record);
12 await preventCrossShopDataAccess(params, record);
13 await save(record);
14};
15
16export const onSuccess: ActionOnSuccess = async ({
17 params,
18 record,
19 logger,
20 api,
21 connections,
22}) => {
23 try {
24 // get an embedding for the product title + description using the OpenAI connection
25 const response = await connections.openai.embeddings.create({
26 input: `${record.title}: ${record.body}`,
27 model: "text-embedding-ada-002",
28 });
29 const embedding = response.data[0].embedding;
30
31 // write to the Gadget Logs
32 logger.info({ id: record.id }, "got product embedding");
33
34 // use the internal API to store vector embedding in Gadget database, on shopifyProduct model
35 await api.internal.shopifyProduct.update(record.id, {
36 shopifyProduct: { descriptionEmbedding: embedding },
37 });
38 } catch (error) {
39 logger.error({ error }, "error creating embedding");
40 }
41};
42
43export const options: ActionOptions = {
44 actionType: "update",
45 triggers: {
46 api: true,
47 },
48};

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 a Gadget API trigger on this action by default. You can read more about the internal API in the Gadget docs.

Now enqueue this action from your api/models/shopifyProduct/actions/create.js and api/models/shopifyProduct/actions/update.js actions:

  1. Open api/models/shopifyProduct/actions/create.js and paste the following code:
api/models/shopifyProduct/actions/create.js
JavaScript
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7
8export const run: ActionRun = async ({ params, record }) => {
9 applyParams(params, record);
10 await preventCrossShopDataAccess(params, record);
11 await save(record);
12};
13
14export const onSuccess: ActionOnSuccess = async ({ record }) => {
15 // only run if the product does not have an embedding, or if the title or body have changed
16 if (!record.descriptionEmbedding && record.body && record.title) {
17 await api.enqueue(api.shopifyProduct.createEmbedding, { id: record.id });
18 }
19};
20
21export const options: ActionOptions = {
22 actionType: "create",
23};
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7
8export const run: ActionRun = async ({ params, record }) => {
9 applyParams(params, record);
10 await preventCrossShopDataAccess(params, record);
11 await save(record);
12};
13
14export const onSuccess: ActionOnSuccess = async ({ record }) => {
15 // only run if the product does not have an embedding, or if the title or body have changed
16 if (!record.descriptionEmbedding && record.body && record.title) {
17 await api.enqueue(api.shopifyProduct.createEmbedding, { id: record.id });
18 }
19};
20
21export const options: ActionOptions = {
22 actionType: "create",
23};
  1. Open api/models/shopifyProduct/actions/update.js and paste the following code:
api/models/shopifyProduct/actions/update.js
JavaScript
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7
8export const run: ActionRun = async ({ params, record }) => {
9 applyParams(params, record);
10 await preventCrossShopDataAccess(params, record);
11 await save(record);
12};
13
14export const onSuccess: ActionOnSuccess = async ({ record }) => {
15 if (
16 !record.descriptionEmbedding &&
17 record.body &&
18 record.title &&
19 record.changed("body") &&
20 record.changed("title")
21 ) {
22 await api.enqueue(api.shopifyProduct.createEmbedding, { id: record.id });
23 }
24};
25
26export const options: ActionOptions = {
27 actionType: "update",
28};
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7
8export const run: ActionRun = async ({ params, record }) => {
9 applyParams(params, record);
10 await preventCrossShopDataAccess(params, record);
11 await save(record);
12};
13
14export const onSuccess: ActionOnSuccess = async ({ record }) => {
15 if (
16 !record.descriptionEmbedding &&
17 record.body &&
18 record.title &&
19 record.changed("body") &&
20 record.changed("title")
21 ) {
22 await api.enqueue(api.shopifyProduct.createEmbedding, { id: record.id });
23 }
24};
25
26export const options: ActionOptions = {
27 actionType: "update",
28};

Gadget's background actions are used so you can manage OpenAI's rate limits using the built-in concurrency control and retries!

Now vector embeddings will be generated when a new product is created, or an existing product is updated.

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:

  1. Click on Settings in the nav bar
  2. Click on the Plugins page
  3. Select the Shopify connection
  4. Click on Installs for the connected Development app
Screenshot of the connected app on the Shopify connections page, with the Shop Installs button highlighted
  1. 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 media 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 shopifyProduct model. The vector embeddings will be stored in the descriptionEmbedding field.

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

Step 6: Add /chat HTTP route 

We will use an HTTP route to handle incoming chat messages from the storefront. The route will take a message from the shopper, use cosine similarity to determine what products to recommend, and stream a response from OpenAI back to the client.

  1. Hover over the api folder and right-click on it to create a folder named routes
  2. Click + next to api/routes create a new file
  3. 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.

  1. Paste the following code in api/routes/POST-chat.js:
api/routes/POST-chat.js
JavaScript
1import { RouteContext, RouteHandler } from "gadget-server";
2import { openAIResponseStream } from "gadget-server/ai";
3
4const route: RouteHandler<{
5 Body: { message: string };
6}> = async ({ request, reply, api, logger, connections }) => {
7 // get input from shopper
8 const { message } = request.body;
9
10 // embed the incoming message from the user
11 const embeddingResponse = await connections.openai.embeddings.create({
12 input: message,
13 model: "text-embedding-ada-002",
14 });
15
16 // find similar product descriptions
17 const products = await api.shopifyProduct.findMany({
18 sort: {
19 descriptionEmbedding: {
20 cosineSimilarityTo: embeddingResponse.data[0].embedding,
21 },
22 },
23 first: 2,
24 filter: {
25 status: {
26 equals: "active",
27 },
28 },
29 select: {
30 id: true,
31 title: true,
32 body: true,
33 handle: true,
34 shop: {
35 domain: true,
36 },
37 featuredMedia: {
38 file: {
39 image: true,
40 },
41 },
42 },
43 });
44
45 // capture products in Gadget's Logs
46 logger.info(
47 { products, message: request.body.message },
48 "found products most similar to user input"
49 );
50
51 const prompt = `You are a helpful shopping assistant trying to match customers with the right product. You will be given a question from a customer and some JSON objects with the id, title, handle, and description (body) of products available for sale that roughly match the customer's question, as well as the store domain. Respond in HTML markup, with an anchor tag at the end with images that link to the product pages and <br /> tags between your text response and product recommendations. The anchor should be of the format: <a href={"https://" + {domain} + "/products/" + {handle}} target="_blank">{title}<img style={border: "1px black solid"} width="200px" src={product.featuredMedia.file.image.url} /></a> but with the domain, handle, and title replaced with passed-in variables. If you have recommended products, end your response with "Click on a product to learn more!" If you are unsure or if the question seems unrelated to shopping, say "Sorry, I don't know how to help with that", and include some suggestions for better questions to ask. Here are the json products you can use to generate a response: ${JSON.stringify(
52 products
53 )}`;
54
55 // send prompt and similar products to OpenAI to generate a response
56 // using GPT-4 Turbo model
57 const chatResponse = await connections.openai.chat.completions.create({
58 model: "gpt-4-1106-preview",
59 messages: [
60 {
61 role: "system",
62 content: prompt,
63 },
64 { role: "user", content: message },
65 ],
66 stream: true,
67 });
68
69 // function fired after the steam is finished
70 const onComplete = (content: string) => {
71 // store the response from OpenAI, and the products that were recommended
72 const recommendedProducts = products.map((product) => ({
73 create: {
74 product: {
75 _link: product.id,
76 },
77 },
78 }));
79 void api.chatLog.create({
80 response: content,
81 recommendedProducts,
82 });
83 };
84
85 await reply.send(openAIResponseStream(chatResponse, { onComplete }));
86};
87
88export default route;
1import { RouteContext, RouteHandler } from "gadget-server";
2import { openAIResponseStream } from "gadget-server/ai";
3
4const route: RouteHandler<{
5 Body: { message: string };
6}> = async ({ request, reply, api, logger, connections }) => {
7 // get input from shopper
8 const { message } = request.body;
9
10 // embed the incoming message from the user
11 const embeddingResponse = await connections.openai.embeddings.create({
12 input: message,
13 model: "text-embedding-ada-002",
14 });
15
16 // find similar product descriptions
17 const products = await api.shopifyProduct.findMany({
18 sort: {
19 descriptionEmbedding: {
20 cosineSimilarityTo: embeddingResponse.data[0].embedding,
21 },
22 },
23 first: 2,
24 filter: {
25 status: {
26 equals: "active",
27 },
28 },
29 select: {
30 id: true,
31 title: true,
32 body: true,
33 handle: true,
34 shop: {
35 domain: true,
36 },
37 featuredMedia: {
38 file: {
39 image: true,
40 },
41 },
42 },
43 });
44
45 // capture products in Gadget's Logs
46 logger.info(
47 { products, message: request.body.message },
48 "found products most similar to user input"
49 );
50
51 const prompt = `You are a helpful shopping assistant trying to match customers with the right product. You will be given a question from a customer and some JSON objects with the id, title, handle, and description (body) of products available for sale that roughly match the customer's question, as well as the store domain. Respond in HTML markup, with an anchor tag at the end with images that link to the product pages and <br /> tags between your text response and product recommendations. The anchor should be of the format: <a href={"https://" + {domain} + "/products/" + {handle}} target="_blank">{title}<img style={border: "1px black solid"} width="200px" src={product.featuredMedia.file.image.url} /></a> but with the domain, handle, and title replaced with passed-in variables. If you have recommended products, end your response with "Click on a product to learn more!" If you are unsure or if the question seems unrelated to shopping, say "Sorry, I don't know how to help with that", and include some suggestions for better questions to ask. Here are the json products you can use to generate a response: ${JSON.stringify(
52 products
53 )}`;
54
55 // send prompt and similar products to OpenAI to generate a response
56 // using GPT-4 Turbo model
57 const chatResponse = await connections.openai.chat.completions.create({
58 model: "gpt-4-1106-preview",
59 messages: [
60 {
61 role: "system",
62 content: prompt,
63 },
64 { role: "user", content: message },
65 ],
66 stream: true,
67 });
68
69 // function fired after the steam is finished
70 const onComplete = (content: string) => {
71 // store the response from OpenAI, and the products that were recommended
72 const recommendedProducts = products.map((product) => ({
73 create: {
74 product: {
75 _link: product.id,
76 },
77 },
78 }));
79 void api.chatLog.create({
80 response: content,
81 recommendedProducts,
82 });
83 };
84
85 await reply.send(openAIResponseStream(chatResponse, { onComplete }));
86};
87
88export default route;

This code will:

  • create a vector embedding from the shopper's message using the OpenAI connection using connections.openai.embeddings.create()
  • use cosine similarity to find the 2 most similar products to the shopper's message using the Gadget API:
cosine similarity in api/routes/POST-chat.js
JavaScript
1const products = await api.shopifyProduct.findMany({
2 sort: {
3 descriptionEmbedding: {
4 // cosine similarity to the embedding of the shopper's message
5 cosineSimilarityTo: embeddingResponse.data[0].embedding,
6 },
7 },
8 first: 2,
9 select: {
10 // ...
11 },
12});
1const products = await api.shopifyProduct.findMany({
2 sort: {
3 descriptionEmbedding: {
4 // cosine similarity to the embedding of the shopper's message
5 cosineSimilarityTo: embeddingResponse.data[0].embedding,
6 },
7 },
8 first: 2,
9 select: {
10 // ...
11 },
12});
  • use the OpenAI chat API to generate a response using the gpt-4-1106-preview model
stream chat completion in api/routes/POST-chat.js
JavaScript
1const chatResponse = await connections.openai.chat.completions.create({
2 model: "gpt-4-1106-preview",
3 messages: [
4 {
5 role: "system",
6 content: prompt,
7 },
8 { role: "user", content: message },
9 ],
10 stream: true,
11});
1const chatResponse = await connections.openai.chat.completions.create({
2 model: "gpt-4-1106-preview",
3 messages: [
4 {
5 role: "system",
6 content: prompt,
7 },
8 { role: "user", content: message },
9 ],
10 stream: true,
11});
  • stream the response back to the client and save the records to the database using reply.send(openAIResponseStream(chatResponse, { onComplete }))
    • onComplete is a callback function that is called after the stream is finished

Set up CORS handling 

Before we can call our /chat route from a theme extension, we need to enable CORS handling. To do this we will use the @fastify/cors plugin.

  1. Open the Gadget command palette using P or Ctrl P
  2. Enter > in the palette to change to command-mode
  3. Run the following command to install the @fastify/cors plugin:
Run in the Gadget command palette
yarn add @fastify/[email protected]

Once the plugin is installed, you can add it to your app's configuration:

  1. Add a new file in the api/routes folder called +scope.js
  2. Paste the following code into api/routes/+scope.js:
api/routes/+scope.js
JavaScript
1import { Server } from "gadget-server";
2import cors from "@fastify/cors";
3
4/**
5 * Route plugin for *
6 *
7 * @param { Server } server - server instance to customize, with customizations scoped to descendant paths
8 *
9 * @see {@link https://www.fastify.dev/docs/latest/Reference/Server}
10 */
11export default async function (server: Server) {
12 await server.register(cors, {
13 // allow requests from any domain
14 origin: true,
15 });
16}
1import { Server } from "gadget-server";
2import cors from "@fastify/cors";
3
4/**
5 * Route plugin for *
6 *
7 * @param { Server } server - server instance to customize, with customizations scoped to descendant paths
8 *
9 * @see {@link https://www.fastify.dev/docs/latest/Reference/Server}
10 */
11export default async function (server: Server) {
12 await server.register(cors, {
13 // allow requests from any domain
14 origin: true,
15 });
16}

This will allow requests from ANY domain! For your actual, production app, you probably want to set the origin option to a specific domain - the domain of your store. Read more about CORS in Gadget in our documentation.

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.

Step 7: Use Shopify theme app extension to embed chatbot 

We make use of an app embed block for our storefront chatbot theme extension. This means that our extension works with any Shopify theme, both vintage and Online Store 2.0 themes.

  1. Use git to clone the Shopify CLI app

You can also build Shopify extensions inside your Gadget project directory so you can manage your entire app with a single Git repo. This tutorial skips this step due to all the assets that need to be copied for the chatbot!

  1. cd to the cloned directory
  2. Update the direct script tag in extensions/theme-extension/blocks/chatbot.liquid to include your app's script tag URL
Direct script tag
The script tag URL can be found in the Installing section of your app's API Reference.
Your script tag needs --development added to your app-specific subdomain when working on your Development environment:
html
<script src="https://example-app--development.gadget.appapi/client/web.min.js" defer="defer"></script>
  1. Run shopify app dev to start your app and connect to your existing Partners app and development store

You should now be set up and ready to add the chatbot theme app extension to the storefront theme.

  1. Navigate to your storefront theme editor and click on App embeds in the left sidebar
  2. Enable your app - the chatbot should appear in your storefront preview
  3. Click Save in the top right corner of the page
Theme extension overview

Theme extensions allow you to create custom blocks that merchants can place in the storefront theme editor. You can read more about theme extensions in the Shopify docs. This chatbot extension is an app embed block, so it works with both vintage and Online Store 2.0 themes.

In this theme there are 3 main files:

  • blocks/chatbot.liquid - the main file that renders the chatbot
  • assets/chatbot.js - handles the chatbot logic and calls your Gadget app's API
  • assets/chatbot.css - the CSS file that styles the chatbot

Your chatbot should now be available in your storefront - try it out!

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

Next steps 

You now have a chatbot that can respond to shopper questions with product recommendations! You can continue to build on this app by:

  • customizing the embedded React frontend, found in the frontend folder, to give merchants installation instructions
  • editing the prompt in api/routes/POST-chat.js to customize the chatbot's response
  • change the look, feel, and merchant customization options for the chatbot by editing the theme extension files
  • maintain full chat context by passing messages back and forth between the client and server
    • also maintain chat context between browser sessions and/or windows by storing the full chat context in the database

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.

Was this page helpful?