diff --git a/docs/docs/event-triggers/recipes/_category_.json b/docs/docs/event-triggers/recipes/_category_.json new file mode 100644 index 00000000000..eecb1e4cb87 --- /dev/null +++ b/docs/docs/event-triggers/recipes/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Recipes", + "position": 10 +} diff --git a/docs/docs/event-triggers/recipes/index.mdx b/docs/docs/event-triggers/recipes/index.mdx new file mode 100644 index 00000000000..0949b58c736 --- /dev/null +++ b/docs/docs/event-triggers/recipes/index.mdx @@ -0,0 +1,27 @@ +--- +title: 'Recipes: Event Triggers' +description: Succinct, tested, and reusable code recipes for common use cases in Hasura. +keywords: + - hasura + - docs + - recipes + - event triggers + - event-driven + - automated +slug: index +--- + +# Recipes + +## Introduction + +This section contains recipes for common use cases of **Event Triggers**. Each recipe is a succinct, tested, and +reusable piece of code that can be used to solve a common use case. However, these recipes are not exhaustive and you +can use them as a starting point to build your own custom logic. + +### Moderate user content with ChatGPT + +A common use case for integrating AI-powered bots into applications is to take care of mundane tasks like moderating +user content. This can be done with specialized sentiment analysis tool or a general large language model like [ChatGPT](https://platform.openai.com/docs/guides/gpt). This recipe shows how to use ChatGPT to moderate reviews +submitted by users. We'll build a webhook that parses Event Trigger data, sends it to ChatGPT, and then performs +operations on our data based on the results. [Check it out](/event-triggers/recipes/moderate-user-content-with-gpt.mdx)! diff --git a/docs/docs/event-triggers/recipes/moderate-user-content-with-gpt.mdx b/docs/docs/event-triggers/recipes/moderate-user-content-with-gpt.mdx new file mode 100644 index 00000000000..0dd4577bee6 --- /dev/null +++ b/docs/docs/event-triggers/recipes/moderate-user-content-with-gpt.mdx @@ -0,0 +1,359 @@ +--- +title: Using Event Triggers to moderate user-generated content with ChatGPT +description: Succinct, tested, and reusable code recipes for common use cases in Hasura. +sidebar_label: Moderate content with ChatGPT +keywords: + - hasura + - docs + - recipes + - event triggers + - moderate + - chatgpt + - openai +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import Thumbnail from '@site/src/components/Thumbnail'; +import SampleAppBlock from '@site/src/components/SampleAppBlock'; + +# Moderate User-Generated Content with ChatGPT + +## Introduction + +Using Event Triggers allows you to call a webhook with a contextual payload whenever a specific event occurs in your database. In this recipe, +we'll create an Event Trigger that will fire whenever a new review is inserted into our `reviews` table. We'll then send +it to [ChatGPT](https://openai.com/blog/openai-api/) to determine if the review contains inappropriate content. If it +does, we'll mark the review as unavailable and send a notification to the user. + + + +## Prerequisites + +Before getting started, ensure that you have the following in place: + +- The docs e-commerce sample app deployed to Hasura Cloud. +- An [OpenAI API key](https://openai.com/blog/openai-api). + +:::info Tunneling your webhook endpoint from your local machine + +If you plan on using a webhook endpoint hosted on your own machine, ensure that you have a tunneling service such as +[ngrok](https://ngrok.com/) set up so that your Cloud Project can communicate with your local machine. + +::: + +## Our model + +Event Triggers are designed to run when specific operations occur on a table, such as insertions, updates, and +deletions. When architecting your own Event Trigger, you need to consider the following: + +- Which table's changes will initiate the Event Trigger? +- Which operation(s) on that table will initiate the Event Trigger? +- What should my webhook do with the data it receives? + +## Step 1: Create the Event Trigger + +Head to the `Events` tab of the Hasura Console and click `Create`: + + + +## Step 2: Configure the Event Trigger + +First, provide a name for your trigger, e.g., `moderate_product_review`. Then, enter a webhook URL that will be called +when the event is triggered. This webhook will be responsible for sending the new review to ChatGPT and determining +whether or not its content is appropriate; it can be hosted anywhere, and written in any language you like. + +The route on our webhook we'll use is `/check-review`. Below, we'll see what this looks like with a service like +[ngrok](https://ngrok.com/), but the format will follow this template: + +```text +https:///check-review +``` + +:::info Tunneling your webhook endpoint + +Since our project is running on Hasura Cloud, and our handler will run on our local machine, we'll use ngrok to expose +the webhook endpoint to the internet. This will allow us to expose a public URL that will forward requests to our local +machine and the server we'll configure below. + +You'll need to modify your webhook URL to use the public URL provided by ngrok. + +After installing ngrok and +[authenticating](https://ngrok.com/docs/secure-tunnels/ngrok-agent/tunnel-authtokens/#:~:text=Once%20you've%20signed%20up,make%20installing%20the%20authtoken%20simple.), +you can do this by running: + +```bash +ngrok http 4000 +``` + +Then, copy the `Forwarding` value for use in our webhook šŸŽ‰ + +::: + +Under `Advanced Settings`, we can configure the headers that will be sent with the request. We'll add an +`authentication` header to prevent abuse of the endpoint and ensure that only Hasura can trigger the event. Set the +`Key` as `secret-authorization-string` and the `Value` as `super_secret_string_123`: + + + +Before exiting, open the `Add Request Options Transform` section and check `POST`. Then, click `Create Event Trigger`. + +## Step 3: Create a webhook to handle the request + +Whenever new data is inserted into our `reviews` table, the Event Trigger fires. Hasura will send a request to the +webhook URL you provided. In this example, we're simply going to send a `POST` request. Our webhook will parse the +request, ensure the header is correct, and then pass our data to ChatGPT. Depending on the response from ChatGPT, we'll +either allow the review to be visible, or we'll mark it as flagged and send a notification to the user. + +:::info Our prompt for ChatGPT + +A large part of creating accurate and useful machine learning models is providing them with the right data. Much of that +comes down to how you engineer your prompt. This can take some experimentation, but this is the prompt that we use in +the webhook's code below: + +```text +You are a content moderator for SuperStore.com. A customer has left a review for a product they purchased. Your response should only be a JSON object with two properties: "feedback" and "is_appropriate". The "feedback" property should be a string containing your response to the customer only if the review "is_appropriate" value is false. The feedback should be on why their review was flagged as inappropriate, not a response to their review. The "is_appropriate" property should be a boolean indicating whether or not the review contains inappropriate content. The review is as follows: +``` + +::: + +Event Triggers sent by Hasura to your webhook as a request include a [payload](/event-triggers/payload.mdx) with `event` +data nested inside the `body` object of the request. This `event` object can then be parsed and values extracted from it +to be used in your webhook. + +Below, we've written an example of webhook in JavaScript that uses `body-parser` to parse the request. As we established +earlier, this runs on port `4000`. If you're attempting to run this locally, follow the instructions below. If you're +running this in a hosted environment, use this code as a guide to write your own webhook. + +Init a new project with `npm init` and install the following dependencies: + +```bash +npm install express body-parser openai +``` + +
+ +Then, create a new file called index.js, add the following code, and update the config values: + + +```javascript +const express = require('express'); +const bodyParser = require('body-parser'); +const openai = require('openai'); + +// Hasura and OpenAI config +const config = { + url: '', + secret: '', + openAIKey: '', +}; + +// OpenAI API config and client +const newOpenAI = new openai.OpenAI({ + apiKey: config.openAIKey, +}); + +const prompt = `You are a content moderator for SuperStore.com. A customer has left a review for a product they purchased. Your response should only be a JSON object with two properties: "feedback" and "is_appropriate". The "feedback" property should be a string containing your response to the customer only if the review "is_appropriate" value is false. The feedback should be on why their review was flagged as inappropriate, not a response to their review. The "is_appropriate" property should be a boolean indicating whether or not the review contains inappropriate content. The review is as follows:`; + +// Send a request to ChatGPT to see if the review contains inappropriate content +async function checkReviewWithChatGPT(reviewText) { + try { + const moderationReport = await newOpenAI.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'user', + content: `${prompt} ${reviewText}}`, + }, + ], + }); + return JSON.parse(moderationReport.choices[0].message.content); + } catch (err) { + return err; + } +} + +// Mark their review as visible if there's no feedback +async function markReviewAsVisible(userReview, reviewId) { + const response = await fetch(config.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-hasura-admin-secret': config.secret, + }, + body: JSON.stringify({ + query: ` + mutation UpdateReviewToVisible($review_id: uuid!) { + update_reviews_by_pk(pk_columns: {id: $review_id}, _set: {is_visible: true}) { + id + } + } + `, + variables: { + review_id: reviewId, + }, + }), + }); + console.log(`šŸŽ‰ Review approved: ${userReview}`); + const { data } = await response.json(); + return data.update_reviews_by_pk; +} + +// Send a notification to the user if their review is flagged +async function sendNotification(userReview, userId, reviewFeedback) { + const response = await fetch(config.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-hasura-admin-secret': config.secret, + }, + body: JSON.stringify({ + query: ` + mutation InsertNotification($user_id: uuid!, $review_feedback: String!) { + insert_notifications_one(object: {user_id: $user_id, message: $review_feedback}) { + id + } + } + `, + variables: { + user_id: userId, + review_feedback: reviewFeedback, + }, + }), + }); + console.log( + `šŸš© Review flagged. This is not visible to users: ${userReview}\nšŸ”” The user has received the following notification: ${reviewFeedback}` + ); + const { data } = await response.json(); + return data.insert_notifications_one; +} + +const app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// Our route for the webhook +app.post('/check-review', async (req, res) => { + // confirm the auth header is correct ā€” ideally, you'd keep the secret in an environment variable + const authHeader = req.headers['secret-authorization-string']; + if (authHeader !== 'super_secret_string_123') { + return res.status(401).json({ + message: 'Unauthorized', + }); + } + + // we'll parse the review from the event payload + const userReview = req.body.event.data.new.text; + const userId = req.body.event.data.new.user_id; + + // Then check the review with ChatGPT + const moderationReport = await checkReviewWithChatGPT(userReview); + + // if the review is appropriate, mark it as visible; if not, send a notification to the user + if (moderationReport.is_appropriate) { + await markReviewAsVisible(userReview, req.body.event.data.new.id); + } else { + await sendNotification(userReview, userId, moderationReport.feedback); + } + + // Return a JSON response to the client + res.json({ + GPTResponse: moderationReport, + }); +}); + +// Start the server +app.listen(4000, () => { + console.log('Server started on port 4000'); +}); +``` + +
+ +You can run the server by running `node index.js` in your terminal. If you see the message +`Webhook server is running on port 4000`, you're good to go! + +## Step 4: Test the setup + +### Testing with appropriate content + +With your server running, Hasura should be able to hit the endpoint. We can test this by inserting a new row into our +`reviews` table. Let's do this with the following mutation from the `API` tab of the Console: + +```graphql +mutation InsertReview { + insert_reviews_one( + object: { + product_id: "7992fdfa-65b5-11ed-8612-6a8b11ef7372" + user_id: "7cf0a66c-65b7-11ed-b904-fb49f034fbbb" + text: "I love this shirt! It's so comfortable and easy to wear." + rating: 5 + } + ) { + id + } +} +``` + +As this review doesn't contain any questionable content, it should be marked as visible. We should see output similar to +this in our terminal: + +```text +šŸŽ‰ Review approved: I love this shirt! It's so comfortable and easy to wear. +``` + +### Testing with inappropriate content + +Now, let's try inserting a review that contains inappropriate content. We'll do this by running the following mutation +in our Hasura Console: + +```graphql +mutation InsertReview { + insert_reviews_one( + object: { + product_id: "7992fdfa-65b5-11ed-8612-6a8b11ef7372" + user_id: "7cf0a66c-65b7-11ed-b904-fb49f034fbbb" + text: "" + rating: 1 + } + ) { + id + } +} +``` + +While this mutation will succeed and insert the review into our database, it will not be marked as visible. Our terminal +will return something like this: + +```text +šŸš© Review flagged. This is not visible to users: +šŸ”” The user has received the following notification: Your review has been flagged as inappropriate due to . +``` + +And we can head to the `Events` tab and see the response from this newest mutation: + + + +:::info Don't forget about the new notification! + +Additionally, Hasura created a new notification for the user, alerting them to the status of their review based on the +response from ChatGPT. You can find this by heading to the `Data` tab and clicking on the `notifications` table. + +::: + +Feel free to customize the webhook implementation based on your specific requirements and identified need for a bot. +Remember to handle error scenarios, implement necessary validations, and add appropriate security measures to your +webhook endpoint. diff --git a/docs/static/img/event-triggers/recipes/review-moderation/click-create-event-trigger.png b/docs/static/img/event-triggers/recipes/review-moderation/click-create-event-trigger.png new file mode 100644 index 00000000000..a53beb41ea2 Binary files /dev/null and b/docs/static/img/event-triggers/recipes/review-moderation/click-create-event-trigger.png differ diff --git a/docs/static/img/event-triggers/recipes/review-moderation/event-trigger-gpt-response.png b/docs/static/img/event-triggers/recipes/review-moderation/event-trigger-gpt-response.png new file mode 100644 index 00000000000..5e2906503c1 Binary files /dev/null and b/docs/static/img/event-triggers/recipes/review-moderation/event-trigger-gpt-response.png differ diff --git a/docs/static/img/event-triggers/recipes/review-moderation/event-trigger-secret-header.png b/docs/static/img/event-triggers/recipes/review-moderation/event-trigger-secret-header.png new file mode 100644 index 00000000000..c5ff4ae5e44 Binary files /dev/null and b/docs/static/img/event-triggers/recipes/review-moderation/event-trigger-secret-header.png differ