Building a Discord Bot with Node-RED and OpenAI for Seamless User Interaction

python in the universe

In this post, we'll walk you through how to create a smart and interactive Discord bot using Node-RED and OpenAI. Our bot listens to user commands, processes them with OpenAI's powerful language model, and responds intelligently in your Discord channel. Let's dive into how all of this comes together!

Overview

Our flow is designed to accomplish these tasks:

• Listen to messages in a specific Discord channel (e.g., "support").
• Identify when users are addressing the bot.
• Manage conversations intelligently, keeping track of new and ongoing sessions.
• Use OpenAI to generate responses to user queries.
• Handle different statuses of the conversation to ensure smooth interaction.

Nodes Breakdown

Here's a peek into the main components of our Node-RED flow:

1.) Listening to Discord Messages:

 (apivisBot): This node picks up every message in the Discord server. It’s our bot's ears in the support channel.



2.) Filtering Relevant Messages:

 switch (name of the discord channel): This node verifies that messages are from the 'support' channel.

 switch (Filter Messages for Bot Response): This smart filter ensures only messages directed at the bot (e.g., containing "Hey Bot") are processed further. In this flow all messages are passed on with the otherwise filter. Remove that one so that the bot is not answer all conversations.

3.) Processing Messages:

 template (json for role and content): Prepares the message content for our AI assistant by creating a JSON structure.

 change nodes: Assist in setting necessary fields and managing session IDs.

 OpenAI API Integration Nodes:

listAssistants: Fetches available assistants from OpenAI.

createThreadAndRun: Initiates a conversation thread with OpenAI.

createMessage: Sends the user’s message to OpenAI.

retrieveRun: Checks the status of the conversation.

4.) Handling Conversations:

• switch (session exist or not?) and switch (new session or continue?): These nodes manage whether the conversation is a new one or an ongoing session.

• function nodes: These craft the necessary payloads for each step and manage conversation threads.

5.) Responding to Users:

• discordMessageManager: Sends OpenAI’s responses back to the user in Discord.

discordTyping: Simulates typing status to let users know the bot is processing their request.

How It All Connects

1.) Initialization:

• The flow starts when a message is detected in the Discord 'support' channel.

• The switch nodes filter out irrelevant messages and ensure only commands for the bot are processed.

2.) Session Management:

• If it’s a brand-new conversation, the bot kicks off a new session with OpenAI using the createThreadAndRun node.

• For ongoing conversations, the bot retrieves the existing thread and adds to it. This is managed by the switch (session exist or not?) node.

3.) Creating and Sending Messages:

• The template node structures the user’s input, and createMessage sends it to OpenAI.

• The status of this message is tracked using retrieveRun, and based on the status (queued, in progress, completed), appropriate actions are taken.

4.) Generating Responses:

• OpenAI processes the input, and the bot fetches the response with listMessages.

• To handle Discord's message limits, messages longer than 2000 characters are split and sent in chunks by the splitLongMessages function.

5.) User Feedback:

• Finally, discordMessageManager ensures the bot’s response reaches the user, and discordTyping simulates real-time typing, enhancing the user experience.

How to setup Discord

To start using Discord I found the following site very helpful: https://www.writebots.com

The flow

[{"id":"95c94e0b20b62cc5","type":"tab","label":"My Discord Bot","disabled":false,"info":"","env":[]},{"id":"e35bc33174b516e1","type":"switch","z":"95c94e0b20b62cc5","name":"Run Status Loop\\n queued\\n in progress\\n completed\\n exception","property":"payload.status","propertyType":"msg","rules":[{"t":"eq","v":"queued","vt":"str"},{"t":"eq","v":"in_progress","vt":"str"},{"t":"eq","v":"completed","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":4,"x":1050,"y":820,"wires":[["03824a6fa797e561"],["03824a6fa797e561"],["ae0a1f2eb1dda4e2"],[]],"inputLabels":["Run Details"],"outputLabels":["Run Queued","Run In Progress","Run Complete","Run Exception"]},{"id":"f490e04616a4c1e3","type":"change","z":"95c94e0b20b62cc5","name":"Create Run Payload","rules":[{"t":"set","p":"payload","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.thread_id","pt":"msg","to":"payload.thread_id","tot":"flow"},{"t":"set","p":"payload.assistant_id","pt":"msg","to":"payload.assistant_id","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":680,"y":780,"wires":[["88281ee04095186d"]]},{"id":"76bd5072b349fbc4","type":"function","z":"95c94e0b20b62cc5","name":"Create Run Payload","func":"let assistants = msg.payload;\nlet assistant = assistants.find(assistant => assistant.name === msg.assistant.name);\nmsg.payload = {};\nif (assistant) {\n  // Retrieved the assistant, now create the payload for the next node\n  msg.payload.assistant_id = assistant.id;\n  msg.payload.thread = msg.assistant.thread;\n} else {\n  // No assistant found with the given name\n  msg.payload = null;\n}\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":520,"wires":[["f346367be7ea755c"]]},{"id":"03824a6fa797e561","type":"function","z":"95c94e0b20b62cc5","name":"Create Run Status Payload","func":"let thread_id = msg.payload.thread_id;\n\nlet run_id = msg.payload.id;\n\nmsg.payload = {\n  thread_id: thread_id,\n  run_id: run_id\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":620,"wires":[["faaa655ee247cd64"]]},{"id":"515d4baa1a9ff994","type":"function","z":"95c94e0b20b62cc5","name":"Extract Thread Id","func":"msg.payload = {\n  thread_id: msg.payload.thread_id\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":230,"y":980,"wires":[["b390ac5f29990eeb"]]},{"id":"f351af28511642c5","type":"function","z":"95c94e0b20b62cc5","name":"Store Conversation Details","func":"flow.set(\"payload.thread_id\", msg.payload[0].thread_id);\n\nflow.set(\"payload.assistant_id\", msg.payload[0].assistant_id);\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":660,"y":1040,"wires":[[]]},{"id":"3e19767be4912027","type":"change","z":"95c94e0b20b62cc5","name":"","rules":[{"t":"set","p":"assistant.name","pt":"msg","to":"_node-red-assistant_","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":750,"y":80,"wires":[["fb67c5a72cc3d992"]]},{"id":"b6bed72d23b6907b","type":"template","z":"95c94e0b20b62cc5","name":"json for role and content","field":"assistant.thread","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"{{data.cleanContent}}\"\n    }\n  ]\n}","output":"json","x":250,"y":400,"wires":[["97b13084c09ae872"]]},{"id":"3a91799be127a7f6","type":"OpenAI API","z":"95c94e0b20b62cc5","name":"Create Thread & Run","property":"payload","propertyType":"msg","service":"155bf6b44322f868","method":"createThreadAndRun","x":240,"y":620,"wires":[["03824a6fa797e561"]]},{"id":"faaa655ee247cd64","type":"OpenAI API","z":"95c94e0b20b62cc5","name":"Retrieve Run","property":"payload","propertyType":"msg","service":"155bf6b44322f868","method":"getRun","x":730,"y":620,"wires":[["e35bc33174b516e1"]]},{"id":"b390ac5f29990eeb","type":"OpenAI API","z":"95c94e0b20b62cc5","name":"List Messages","property":"payload","propertyType":"msg","service":"155bf6b44322f868","method":"listMessages","x":420,"y":980,"wires":[["d1d86114340006d7","f351af28511642c5"]]},{"id":"0bdbf8ab7abc31c2","type":"OpenAI API","z":"95c94e0b20b62cc5","name":"Create Message","property":"payload","propertyType":"msg","service":"155bf6b44322f868","method":"createMessage","x":480,"y":780,"wires":[["f490e04616a4c1e3"]]},{"id":"88281ee04095186d","type":"OpenAI API","z":"95c94e0b20b62cc5","name":"Create Run","property":"payload","propertyType":"msg","service":"155bf6b44322f868","method":"createRun","x":870,"y":780,"wires":[["e35bc33174b516e1"]]},{"id":"35a08e5a7e88ca67","type":"OpenAI API","z":"95c94e0b20b62cc5","name":"List Assistants","property":"payload","propertyType":"msg","service":"155bf6b44322f868","method":"listAssistants","x":220,"y":520,"wires":[["76bd5072b349fbc4"]],"outputLabels":["Assistant Objects"]},{"id":"b02093886b6b7f48","type":"switch","z":"95c94e0b20b62cc5","name":"new session or continue?","property":"_session.id","propertyType":"msg","rules":[{"t":"eq","v":"_session.id","vt":"flow"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":710,"y":380,"wires":[["37395105b817f19b"],["ea7790779cdffc5f"]]},{"id":"f346367be7ea755c","type":"change","z":"95c94e0b20b62cc5","name":"set flow._session.id","rules":[{"t":"set","p":"_session.id","pt":"flow","to":"_session.id","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":520,"wires":[["ba7ef04e22ee604f"]]},{"id":"152a9e9b11ef9d5e","type":"change","z":"95c94e0b20b62cc5","name":"set msg.payload.thread_id","rules":[{"t":"set","p":"payload.thread_id","pt":"msg","to":"payload.thread_id","tot":"flow"},{"t":"set","p":"payload.role","pt":"msg","to":"user","tot":"str"},{"t":"set","p":"payload.content","pt":"msg","to":"payload.textField","tot":"msg"},{"t":"delete","p":"payload.textField","pt":"msg"},{"t":"delete","p":"payload.host","pt":"msg"},{"t":"delete","p":"payload.kb","pt":"msg"},{"t":"delete","p":"payload.imageClassification","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":780,"wires":[["0bdbf8ab7abc31c2"]]},{"id":"81fcfd91c1a5a878","type":"discordMessage","z":"95c94e0b20b62cc5","name":"apivisBot","channelIdFilter":"","token":"","x":160,"y":100,"wires":[["9e623708288e743e"]]},{"id":"0e316ec9e15ae219","type":"switch","z":"95c94e0b20b62cc5","name":"Filter Messages for Bot Response","property":"data.content","propertyType":"msg","rules":[{"t":"cont","v":"Hey Bot","vt":"str"},{"t":"cont","v":"hey bot","vt":"str"},{"t":"regex","v":"\\!Hey\\sBot","vt":"str","case":true},{"t":"else"}],"checkall":"false","repair":true,"outputs":4,"x":280,"y":200,"wires":[["3d2796e8ed3ecac4"],["951e095ea6f2d1f4"],["adc550621be4d43b"],["a1ac66c1b29ba54e"]]},{"id":"d1d86114340006d7","type":"function","z":"95c94e0b20b62cc5","name":"function 9","func":"// Get the text content from the message payload\nconst text = msg.payload[0].content[0].text.value || '';\n\n// Initialize an array to hold the new messages\nconst newMessages = [];\n\n// Check the length of the text\nif (text.length > 2000) {\n  // Split the text into chunks of 2000 characters\n  const maxLength = 2000;\n  const chunks = [];\n  for (let i = 0; i < text.length; i += maxLength) {\n    chunks.push(text.substring(i, i + maxLength));\n  }\n\n  // Create new messages for each chunk\n  chunks.forEach((chunk, index) => {\n    // Create a deep copy of the original message to maintain all other data\n    const newMsg = RED.util.cloneMessage(msg);\n\n    // Update the _msgid to be unique for each chunk\n    newMsg._msgid = `${msg._msgid}_${index}`;\n\n    // Update the text content with the chunk\n    newMsg.payload[0].content[0].text.value = chunk;\n\n    // Send the new message\n    node.send(newMsg);\n  });\n\n  // Return null to indicate that the original message has been processed\n  return null;\n}\n\n// If the text is not longer than 2000 characters, return the original message\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":980,"wires":[["1fe05e16e32b75eb"]]},{"id":"84d4199b0ac63d8b","type":"discordMessageManager","z":"95c94e0b20b62cc5","name":"Response","channel":"","token":"","x":940,"y":980,"wires":[["7202e3272bb4d901"]]},{"id":"7202e3272bb4d901","type":"debug","z":"95c94e0b20b62cc5","name":"debug 107","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1090,"y":980,"wires":[]},{"id":"020ad446c488be26","type":"discordTyping","z":"95c94e0b20b62cc5","name":"","channel":"","token":"","x":780,"y":180,"wires":[]},{"id":"97b13084c09ae872","type":"switch","z":"95c94e0b20b62cc5","name":"session exist or not?","property":"_session.id","propertyType":"msg","rules":[{"t":"nnull"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":480,"y":400,"wires":[["b02093886b6b7f48"],["e8956df02b5c50ff"]]},{"id":"ed639d51e07cc132","type":"template","z":"95c94e0b20b62cc5","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n  \"content\": \"{{payload}}\"\n}","output":"json","x":560,"y":80,"wires":[["3e19767be4912027"]]},{"id":"1fe05e16e32b75eb","type":"change","z":"95c94e0b20b62cc5","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[0].content[0].text.value","tot":"msg"},{"t":"set","p":"request","pt":"msg","to":"payload[1].content[0].text.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":760,"y":980,"wires":[["84d4199b0ac63d8b"]]},{"id":"37395105b817f19b","type":"link out","z":"95c94e0b20b62cc5","name":"continue","mode":"link","links":["37e930a01616d311"],"x":865,"y":360,"wires":[]},{"id":"ea7790779cdffc5f","type":"link out","z":"95c94e0b20b62cc5","name":"link out 2","mode":"link","links":["1e4b3f1d11b89198"],"x":865,"y":400,"wires":[]},{"id":"37e930a01616d311","type":"link in","z":"95c94e0b20b62cc5","name":"link in 1","links":["37395105b817f19b"],"x":115,"y":780,"wires":[["152a9e9b11ef9d5e"]]},{"id":"1e4b3f1d11b89198","type":"link in","z":"95c94e0b20b62cc5","name":"link in 2","links":["ea7790779cdffc5f","e8956df02b5c50ff"],"x":115,"y":520,"wires":[["35a08e5a7e88ca67"]]},{"id":"e8956df02b5c50ff","type":"link out","z":"95c94e0b20b62cc5","name":"link out 3","mode":"link","links":["76d2bbf2a56c9d07","1e4b3f1d11b89198"],"x":615,"y":420,"wires":[]},{"id":"7397aae6ede41b4b","type":"catch","z":"95c94e0b20b62cc5","name":"","scope":null,"uncaught":false,"x":160,"y":300,"wires":[["6d9ffb0b28d49c58"]]},{"id":"6d9ffb0b28d49c58","type":"debug","z":"95c94e0b20b62cc5","name":"CatchtestDiscordFlow","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":340,"y":300,"wires":[]},{"id":"9e623708288e743e","type":"switch","z":"95c94e0b20b62cc5","name":"name of the discord channel","property":"channel.name","propertyType":"msg","rules":[{"t":"eq","v":"support","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":360,"y":100,"wires":[["ed639d51e07cc132"],[]]},{"id":"3d2796e8ed3ecac4","type":"link out","z":"95c94e0b20b62cc5","name":"link out 369","mode":"link","links":["69dc4b4d1e01327d","fef2651f90accdc3"],"x":475,"y":180,"wires":[]},{"id":"951e095ea6f2d1f4","type":"link out","z":"95c94e0b20b62cc5","name":"link out 370","mode":"link","links":["69dc4b4d1e01327d"],"x":515,"y":200,"wires":[]},{"id":"a1ac66c1b29ba54e","type":"link out","z":"95c94e0b20b62cc5","name":"link out 371","mode":"link","links":["69dc4b4d1e01327d","fef2651f90accdc3"],"x":595,"y":240,"wires":[]},{"id":"69dc4b4d1e01327d","type":"link in","z":"95c94e0b20b62cc5","name":"link in 307","links":["3d2796e8ed3ecac4","951e095ea6f2d1f4","a1ac66c1b29ba54e"],"x":115,"y":400,"wires":[["b6bed72d23b6907b"]]},{"id":"fef2651f90accdc3","type":"link in","z":"95c94e0b20b62cc5","name":"link in 308","links":["3d2796e8ed3ecac4","a1ac66c1b29ba54e","adc550621be4d43b"],"x":675,"y":180,"wires":[["020ad446c488be26"]]},{"id":"adc550621be4d43b","type":"link out","z":"95c94e0b20b62cc5","name":"link out 372","mode":"link","links":["fef2651f90accdc3"],"x":555,"y":220,"wires":[]},{"id":"ba7ef04e22ee604f","type":"link out","z":"95c94e0b20b62cc5","name":"link out 373","mode":"link","links":["604ebf162e54cefb"],"x":755,"y":520,"wires":[]},{"id":"604ebf162e54cefb","type":"link in","z":"95c94e0b20b62cc5","name":"link in 309","links":["ba7ef04e22ee604f"],"x":115,"y":620,"wires":[["3a91799be127a7f6"]]},{"id":"ae0a1f2eb1dda4e2","type":"link out","z":"95c94e0b20b62cc5","name":"link out 374","mode":"link","links":["14c58d79b5dea4b9"],"x":1195,"y":840,"wires":[]},{"id":"14c58d79b5dea4b9","type":"link in","z":"95c94e0b20b62cc5","name":"link in 310","links":["ae0a1f2eb1dda4e2"],"x":115,"y":980,"wires":[["515d4baa1a9ff994"]]},{"id":"ba41b71f8868f9f1","type":"comment","z":"95c94e0b20b62cc5","name":"new session","info":"","x":170,"y":460,"wires":[]},{"id":"03e9d971972f6ae9","type":"comment","z":"95c94e0b20b62cc5","name":"continue session","info":"","x":180,"y":700,"wires":[]},{"id":"85854c5d6cd9e960","type":"link in","z":"95c94e0b20b62cc5","name":"link in 311","links":["fb67c5a72cc3d992"],"x":115,"y":200,"wires":[["0e316ec9e15ae219"]]},{"id":"fb67c5a72cc3d992","type":"link out","z":"95c94e0b20b62cc5","name":"link out 375","mode":"link","links":["85854c5d6cd9e960"],"x":885,"y":80,"wires":[]},{"id":"155bf6b44322f868","type":"Service Host","apiBase":"https://api.openai.com/v1","secureApiKeyHeaderOrQueryName":"Authorization","organizationId":"org-ldadft66yHlGaxjfue648","name":"mykey"}]