Skip to content

Commit 8136ff2

Browse files
authored
Merge pull request #44 from DefangLabs/linda-discord-bot
Add Discord Bot for Ask Defang
2 parents aa23f05 + 7d4f369 commit 8136ff2

File tree

12 files changed

+1708
-9
lines changed

12 files changed

+1708
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ __pycache__
33
sentence-transformers
44
.tmp/*
55
!.tmp/prebuild.sh
6+
node_modules

README.md

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
# Scikit RAG + OpenAI & Discord App for Defang
2+
3+
This repository contains two projects:
4+
5+
1. **Scikit RAG + OpenAI** in `/app`: A Flask-based Retrieval-Augmented Generation (RAG) chatbot using OpenAI's GPT model, scikit-learn, and Sentence Transformers for dynamic knowledge retrieval.
6+
7+
2. **Discord App for Defang** in `/discord-bot`: A Discord bot designed for Defang Software Labs, providing helpful resources and interacting with users via slash commands.
8+
9+
---
10+
111
# Scikit RAG + OpenAI
212

3-
This sample demonstrates how to deploy a Flask-based Retrieval-Augmented Generation (RAG) chatbot using OpenAI's GPT model. The chatbot retrieves relevant documents from a knowledge base using scikit-learn and Sentence Transformers and then generates responses using OpenAI's GPT model.
13+
### Overview
14+
15+
This application demonstrates how to deploy a Flask-based Retrieval-Augmented Generation (RAG) chatbot using OpenAI's GPT model. The chatbot retrieves relevant documents from a knowledge base using scikit-learn and Sentence Transformers and then generates responses using OpenAI's GPT model.
416

517
## Prerequisites
618

@@ -17,22 +29,116 @@ This sample demonstrates how to deploy a Flask-based Retrieval-Augmented Generat
1729
## Local Development
1830

1931
1. Clone the repository.
20-
2. Create a `.env` file in the root directory and set your OpenAI API key or add the OPENAI_API_KEY into your .zshrc or .bashrc file:
21-
3. Run the command `docker compose -f compose.dev.yaml up --build` to spin up a docker container for this RAG chatbot
32+
2. Create a `.env` file in the root directory and set your OpenAI API key, or add the `OPENAI_API_KEY` to your `.zshrc` or `.bashrc` file.
33+
3. Run the command:
34+
35+
```bash
36+
docker compose -f compose.dev.yaml up --build
37+
```
38+
39+
This spins up a Docker container for the RAG chatbot.
2240

2341
## Configuration
2442

25-
- The knowledge base is the all the markdown files in the defang docs [website](https://docs.defang.io/docs/intro). The logic for parsing can be found in './app/get_knowledge_base.py'.
43+
- The knowledge base is the all the markdown files in the Defang docs [website](https://docs.defang.io/docs/intro). The logic for parsing can be found in `./app/get_knowledge_base.py`.
2644
- The file `get_knowledge_base.py` parses every webpage as specified into paragraphs and writes to `knowledge_base.json` for the RAG retrieval.
2745
- To obtain your own knowledge base, please feel free to implement your own parsing scheme.
28-
- for local development, please use the compose.dev.yaml file where as for production, please use the compose.yaml.
46+
- for local development, please use the `compose.dev.yaml` file where as for production, please use the `compose.yaml`.
2947

3048
---
3149

32-
Title: Scikit RAG + OpenAI
50+
# Discord App for Defang
51+
52+
### Overview
53+
54+
This is a Discord bot developed for [Defang Software Labs](https://github.com/DefangLabs). It provides helpful resources in a Discord server and interacts with users via slash commands. The bot is built using Discord's official [template](https://github.com/discord/discord-example-app).
55+
56+
## Features
57+
58+
### Slash Commands
59+
60+
`/ask`: A command to ask Defang-related questions to the bot. The bot accesses the Ask Defang (ask.defang.io) API endpoint for retrieving responses.
61+
62+
`/test`: A basic command to test functionality using the Discord API, without relying on external APIs.
63+
64+
## Development
65+
66+
### Project structure
67+
68+
Below is a basic overview of the project structure:
69+
70+
```
71+
├── .env. -> .env file (not shown)
72+
├── app.js -> main entrypoint for app
73+
├── commands.js -> slash command payloads + helpers
74+
├── utils.js -> utility functions and enums
75+
├── package.json
76+
├── README.md
77+
└── .gitignore
78+
```
79+
80+
### Setup project
81+
82+
Before you start, you'll need to install [NodeJS](https://nodejs.org/en/download/) and [create a Discord app](https://discord.com/developers/applications) with the proper permissions:
83+
84+
- `applications.commands`
85+
- `bot` (with Send Messages enabled)
86+
Configuring the app is covered in detail in the [getting started guide](https://discord.com/developers/docs/getting-started).
87+
88+
### Install dependencies
89+
90+
```
91+
cd discord-bot
92+
npm install
93+
```
94+
95+
### Get app credentials
96+
97+
Fetch the credentials from your app's settings and add them to a `.env` file. You'll need your app ID (`DISCORD_APP_ID`), bot token (`DISCORD_TOKEN`), and public key (`DISCORD_PUBLIC_KEY`).
98+
You will also need an `ASK_TOKEN` to authenticate API calls to the Ask Defang endpoint.
99+
100+
### Install slash commands
101+
102+
The commands for the example app are set up in `commands.js`. All of the commands in the `ALL_COMMANDS` array at the bottom of `commands.js` will be installed when you run the `register` command configured in `package.json`:
103+
104+
```
105+
cd discord-bot
106+
npm run register
107+
```
108+
109+
### Running the app locally
110+
111+
After your credentials are added, go ahead and run the app:
112+
113+
```
114+
cd discord-bot
115+
npm run start
116+
```
117+
118+
### Set up interactivity
119+
120+
The project needs a public endpoint where Discord can send requests. To develop and test locally, you can use something like [`ngrok`](https://ngrok.com/) to tunnel HTTP traffic.
121+
122+
Install ngrok if you haven't already, then start listening on port `3000` in a separate terminal:
123+
124+
```
125+
ngrok http 3000
126+
```
127+
128+
You should see your connection open:
129+
130+
```
131+
Tunnel Status online
132+
Version 2.0/2.0
133+
Web Interface http://127.0.0.1:4040
134+
Forwarding https://1234-someurl.ngrok.io -> localhost:3000
135+
136+
Connections ttl opn rt1 rt5 p50 p90
137+
0 0 0.00 0.00 0.00 0.00
138+
```
33139

34-
Description: An application demonstrating a GPT-4-based chatbot enhanced with a Retrieval-Augmented Generation (RAG) framework, leveraging scikit-learn for efficient contextual embeddings and dynamic knowledge retrieval.
140+
Copy the forwarding address that starts with `https`, in this case `https://1234-someurl.ngrok.io`, then go to your [app's settings](https://discord.com/developers/applications).
35141

36-
Tags: Flask, Scikit, Python, RAG, OpenAI, GPT, Machine Learning
142+
On the **General Information** tab, there will be an **Interactions Endpoint URL**. Paste your ngrok address there, and append `/interactions` to it (`https://1234-someurl.ngrok.io/interactions` in the example).
37143

38-
Languages: python
144+
Click **Save Changes**, and your app should be ready to run 🚀

compose.dev.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@ services:
3838
- type: bind
3939
source: ~/.aws
4040
target: /root/.aws
41+
42+
discord-bot:
43+
restart: unless-stopped
44+
extends:
45+
file: compose.yaml
46+
service: discord-bot
47+
env_file:
48+
- .env

compose.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,24 @@ services:
4343
mode: host
4444
environment:
4545
- OPENAI_API_KEY=${OPENAI_API_KEY}
46+
47+
discord-bot:
48+
restart: unless-stopped
49+
build:
50+
context: ./discord-bot
51+
dockerfile: Dockerfile
52+
ports:
53+
- mode: ingress
54+
target: 3000
55+
published: 3000
56+
environment:
57+
DISCORD_APP_ID:
58+
DISCORD_TOKEN:
59+
DISCORD_PUBLIC_KEY:
60+
ASK_TOKEN:
61+
deploy:
62+
resources:
63+
reservations:
64+
memory: 256M
65+
healthcheck:
66+
test: ["CMD", "curl", "-f", "http://localhost:3000/"]

discord-bot/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use the slim version of Node.js on Debian Bookworm as the base image
2+
FROM node:20-bookworm-slim
3+
4+
RUN apt-get update -qq \
5+
&& apt-get install -y curl \
6+
&& apt-get clean \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
# Set the working directory inside the container
10+
WORKDIR /app
11+
12+
# Copy package.json and package-lock.json to the container
13+
COPY package*.json ./
14+
15+
# Install dependencies
16+
RUN npm install
17+
18+
# Copy the rest of the application code to the container
19+
COPY . .
20+
21+
# Expose the port the app runs on (adjust if necessary)
22+
EXPOSE 3000
23+
24+
# Define the command to run the application
25+
CMD ["npm", "run", "start"]

discord-bot/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Shay DeWael
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

discord-bot/app.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import dotenv from 'dotenv';
2+
import path from 'path';
3+
4+
// Load environment variables from the parent folder
5+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
6+
dotenv.config({ path: path.resolve(__dirname, '../.env') });
7+
8+
import express from 'express';
9+
import {
10+
InteractionResponseFlags,
11+
InteractionResponseType,
12+
InteractionType,
13+
verifyKeyMiddleware,
14+
} from 'discord-interactions';
15+
import { getRandomEmoji } from './utils.js';
16+
17+
// Create an express app
18+
const app = express();
19+
// Get port, or default to 3000
20+
const PORT = process.env.PORT || 3000;
21+
22+
// Add health check endpoint
23+
app.get('/', (req, res) => {
24+
res.status(200).send('OK');
25+
});
26+
27+
// Helper functions below
28+
async function sendPlaceholderResponse(res, placeholderResponse) {
29+
await res.send({
30+
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
31+
data: {
32+
content: placeholderResponse,
33+
flags: InteractionResponseFlags.EPHEMERAL,
34+
components: [],
35+
},
36+
});
37+
}
38+
39+
async function fetchAnswer(question) {
40+
const response = await fetch('https://ask.defang.io/v1/ask', {
41+
method: 'POST',
42+
headers: {
43+
'Authorization': `Bearer ${process.env.ASK_TOKEN}`,
44+
'Content-Type': 'application/json',
45+
},
46+
body: JSON.stringify({ query: question }),
47+
});
48+
49+
const rawResponse = await response.text();
50+
console.log('Raw API response:', rawResponse);
51+
52+
if (!response.ok) {
53+
throw new Error(`API error! Status: ${response.status}`);
54+
}
55+
56+
return rawResponse || 'No answer provided.';
57+
}
58+
59+
async function sendFollowUpResponse(endpoint, content) {
60+
await fetch(`https://discord.com/api/v10/${endpoint}`, {
61+
method: 'PATCH',
62+
headers: {
63+
'Authorization': `Bot ${process.env.DISCORD_TOKEN}`,
64+
'Content-Type': 'application/json',
65+
},
66+
body: JSON.stringify({
67+
content,
68+
flags: InteractionResponseFlags.EPHEMERAL,
69+
components: [],
70+
}),
71+
});
72+
}
73+
74+
/**
75+
* Interactions endpoint URL where Discord will send HTTP requests
76+
* Parse request body and verifies incoming requests using discord-interactions package
77+
*/
78+
app.post('/interactions', verifyKeyMiddleware(process.env.DISCORD_PUBLIC_KEY), async function (req, res) {
79+
// Interaction id, type and data
80+
const { id, type, data } = req.body;
81+
82+
/**
83+
* Handle verification requests
84+
*/
85+
if (type === InteractionType.PING) {
86+
return res.send({ type: InteractionResponseType.PONG });
87+
}
88+
89+
/**
90+
* Handle slash command requests
91+
* See https://discord.com/developers/docs/interactions/application-commands#slash-commands
92+
*/
93+
if (type === InteractionType.APPLICATION_COMMAND) {
94+
const { name } = data;
95+
96+
// "ask command"
97+
if (name === 'ask') {
98+
const context = req.body.context;
99+
const userId = context === 0 ? req.body.member.user.id : req.body.user.id
100+
101+
const question = data.options[0]?.value || 'No question provided';
102+
const endpoint = `webhooks/${process.env.DISCORD_APP_ID}/${req.body.token}/messages/@original`;
103+
const initialMessage = `\n> ${question}\n\nLet me find the answer for you. This might take a moment`
104+
105+
// Send a placeholder response
106+
await sendPlaceholderResponse(res, initialMessage);
107+
108+
// Show animated dots in the message while waiting
109+
let dotCount = 0;
110+
const maxDots = 4;
111+
let isFetching = true;
112+
113+
const interval = setInterval(() => {
114+
if (isFetching) {
115+
dotCount = (dotCount % maxDots) + 1;
116+
sendFollowUpResponse(endpoint, `${initialMessage}${'.'.repeat(dotCount)}`);
117+
}
118+
}, 500);
119+
120+
// Create the follow-up response
121+
let followUpMessage;
122+
try {
123+
// Call an external API to fetch the answer
124+
const answer = await fetchAnswer(question);
125+
followUpMessage = `\n> ${question}\n\nHere's what I found, <@${userId}>:\n\n${answer}`;
126+
} catch (error) {
127+
console.error('Error fetching answer:', error);
128+
followUpMessage = `\n> ${question}\n\nSorry <@${userId}>, I couldn't fetch an answer to your question. Please try again later.`;
129+
} finally {
130+
// Ensure cleanup and state updates
131+
isFetching = false; // Mark fetching as complete
132+
clearInterval(interval); // Stop the dot interval
133+
}
134+
135+
return sendFollowUpResponse(endpoint, followUpMessage);
136+
}
137+
138+
// "test" command
139+
if (name === 'test') {
140+
// Send a message into the channel where command was triggered from
141+
return res.send({
142+
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
143+
data: {
144+
// Fetches a random emoji to send from a helper function
145+
content: `Develop Anything, Deploy Anywhere ${getRandomEmoji()}`,
146+
},
147+
});
148+
}
149+
150+
console.error(`unknown command: ${name}`);
151+
return res.status(400).json({ error: 'unknown command' });
152+
}
153+
154+
console.error('unknown interaction type', type);
155+
return res.status(400).json({ error: 'unknown interaction type' });
156+
});
157+
158+
app.listen(PORT, () => {
159+
console.log('Listening on port', PORT);
160+
});

0 commit comments

Comments
 (0)