I built a custom bot for my Discord server (join at fullstack.chat) and one thing I’ve always wanted to do is integrate it with AI in some way. Turns out, the process wasn’t nearly as difficult as I thought it would be.

This article outlines how I did it, and what I plan to build on going forward.

⚠️
Everything mentioned here is open source! https://github.com/fullstack-chat/walter

Using the Ollama API

Discord bots appear in the server just like another user, meaning you can mention them directly.

When a user mentions any other user in a server, the message object will container an array of user IDs called mentions. This can be used in conjunction with the bot’s user ID to determine if it’s in the list:

const tag = `<@${client.user!.id}>`
if(message.mentions.has(client.user!.id)) {
	// Logic here
}

Inside the if block, I then remove the mention (so the prompt is clean) and trim any whitespace. That message is then sent to Ollama using the generate API simply using fetch:

// Remove the mention from the message
let msg = message.content.replace(tag, "").trim();

// Send the message to the Ollama API
let res = await fetch(`${process.env.OLLAMA_URL}/api/generate`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    prompt: msg,
    model: "llama3",
    stream: false
  })
})
let data = await res.json();

I chose not to stream the response back because the Discord API has a rate limit and I’d need to constantly edit the previous message to continue streaming the response. So instead I just wait for Ollama to finish doing it’s thing and send the whole message:

await message.reply(data.response.trim());

Handling long responses

The Discord API has a limit of 2000 characters in a single message. To send long responses, I instead create a thread based on the initial request. This also creates a trimmed title and mentions the sender of the original message:

let destination = message.channel;
// Create a thread ONLY if were not in one already
if(message.channel.type === ChannelType.GuildText) {
  let threadname = `"${msg.length > 50 ? msg : `${msg.slice(0, 70)}...`}" by @${message.author.username}`
  destination = await message.startThread({
    name: threadname,
    autoArchiveDuration: 1440
  })
}

Finally, send an individual message on each line break. This also handles if Ollama response with a code block so those are rendered properly in Discord as well:

let spl = response.split("\n");
let isWritingCodeBlock = false
let agg = ""
for(const chunk of spl) {
  await destination.sendTyping();
  if(chunk !== "") {
    if(chunk.startsWith("```")) {
      isWritingCodeBlock = !isWritingCodeBlock
    }
    agg += `${chunk}\n`
    if(!isWritingCodeBlock)  {
      await destination.send(agg)
      agg = ""
    }
  }
}