Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__
sentence-transformers
.tmp/*
!.tmp/prebuild.sh
node_modules
124 changes: 115 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Scikit RAG + OpenAI & Discord App for Defang

This repository contains two projects:

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.

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.

---

# Scikit RAG + OpenAI

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.
### Overview

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.

## Prerequisites

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

1. Clone the repository.
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:
3. Run the command `docker compose -f compose.dev.yaml up --build` to spin up a docker container for this RAG chatbot
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.
3. Run the command:

```bash
docker compose -f compose.dev.yaml up --build
```

This spins up a Docker container for the RAG chatbot.

## Configuration

- 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'.
- 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`.
- The file `get_knowledge_base.py` parses every webpage as specified into paragraphs and writes to `knowledge_base.json` for the RAG retrieval.
- To obtain your own knowledge base, please feel free to implement your own parsing scheme.
- for local development, please use the compose.dev.yaml file where as for production, please use the compose.yaml.
- for local development, please use the `compose.dev.yaml` file where as for production, please use the `compose.yaml`.

---

Title: Scikit RAG + OpenAI
# Discord App for Defang

### Overview

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).

## Features

### Slash Commands

`/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.

`/test`: A basic command to test functionality using the Discord API, without relying on external APIs.

## Development

### Project structure

Below is a basic overview of the project structure:

```
├── .env. -> .env file (not shown)
├── app.js -> main entrypoint for app
├── commands.js -> slash command payloads + helpers
├── utils.js -> utility functions and enums
├── package.json
├── README.md
└── .gitignore
```

### Setup project

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:

- `applications.commands`
- `bot` (with Send Messages enabled)
Configuring the app is covered in detail in the [getting started guide](https://discord.com/developers/docs/getting-started).

### Install dependencies

```
cd discord-bot
npm install
```

### Get app credentials

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`).
You will also need an `ASK_TOKEN` to authenticate API calls to the Ask Defang endpoint.

### Install slash commands

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`:

```
cd discord-bot
npm run register
```

### Running the app locally

After your credentials are added, go ahead and run the app:

```
cd discord-bot
npm run start
```

### Set up interactivity

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.

Install ngrok if you haven't already, then start listening on port `3000` in a separate terminal:

```
ngrok http 3000
```

You should see your connection open:

```
Tunnel Status online
Version 2.0/2.0
Web Interface http://127.0.0.1:4040
Forwarding https://1234-someurl.ngrok.io -> localhost:3000

Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
```

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.
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).

Tags: Flask, Scikit, Python, RAG, OpenAI, GPT, Machine Learning
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).

Languages: python
Click **Save Changes**, and your app should be ready to run 🚀
8 changes: 8 additions & 0 deletions compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ services:
- type: bind
source: ~/.aws
target: /root/.aws

discord-bot:
restart: unless-stopped
extends:
file: compose.yaml
service: discord-bot
env_file:
- .env
21 changes: 21 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,24 @@ services:
mode: host
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}

discord-bot:
restart: unless-stopped
build:
context: ./discord-bot
dockerfile: Dockerfile
ports:
- mode: ingress
target: 3000
published: 3000
environment:
DISCORD_APP_ID:
DISCORD_TOKEN:
DISCORD_PUBLIC_KEY:
ASK_TOKEN:
deploy:
resources:
reservations:
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
25 changes: 25 additions & 0 deletions discord-bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use the slim version of Node.js on Debian Bookworm as the base image
FROM node:20-bookworm-slim

RUN apt-get update -qq \
&& apt-get install -y curl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and package-lock.json to the container
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application code to the container
COPY . .

# Expose the port the app runs on (adjust if necessary)
EXPOSE 3000

# Define the command to run the application
CMD ["npm", "run", "start"]
21 changes: 21 additions & 0 deletions discord-bot/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Shay DeWael

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
160 changes: 160 additions & 0 deletions discord-bot/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import dotenv from 'dotenv';
import path from 'path';

// Load environment variables from the parent folder
const __dirname = path.dirname(new URL(import.meta.url).pathname);
dotenv.config({ path: path.resolve(__dirname, '../.env') });

import express from 'express';
import {
InteractionResponseFlags,
InteractionResponseType,
InteractionType,
verifyKeyMiddleware,
} from 'discord-interactions';
import { getRandomEmoji } from './utils.js';

// Create an express app
const app = express();
// Get port, or default to 3000
const PORT = process.env.PORT || 3000;

// Add health check endpoint
app.get('/', (req, res) => {
res.status(200).send('OK');
});

// Helper functions below
async function sendPlaceholderResponse(res, placeholderResponse) {
await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: placeholderResponse,
flags: InteractionResponseFlags.EPHEMERAL,
components: [],
},
});
}

async function fetchAnswer(question) {
const response = await fetch('https://ask.defang.io/v1/ask', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ASK_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: question }),
});

const rawResponse = await response.text();
console.log('Raw API response:', rawResponse);

if (!response.ok) {
throw new Error(`API error! Status: ${response.status}`);
}

return rawResponse || 'No answer provided.';
}

async function sendFollowUpResponse(endpoint, content) {
await fetch(`https://discord.com/api/v10/${endpoint}`, {
method: 'PATCH',
headers: {
'Authorization': `Bot ${process.env.DISCORD_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content,
flags: InteractionResponseFlags.EPHEMERAL,
components: [],
}),
});
}

/**
* Interactions endpoint URL where Discord will send HTTP requests
* Parse request body and verifies incoming requests using discord-interactions package
*/
app.post('/interactions', verifyKeyMiddleware(process.env.DISCORD_PUBLIC_KEY), async function (req, res) {
// Interaction id, type and data
const { id, type, data } = req.body;

/**
* Handle verification requests
*/
if (type === InteractionType.PING) {
return res.send({ type: InteractionResponseType.PONG });
}

/**
* Handle slash command requests
* See https://discord.com/developers/docs/interactions/application-commands#slash-commands
*/
if (type === InteractionType.APPLICATION_COMMAND) {
const { name } = data;

// "ask command"
if (name === 'ask') {
const context = req.body.context;
const userId = context === 0 ? req.body.member.user.id : req.body.user.id

const question = data.options[0]?.value || 'No question provided';
const endpoint = `webhooks/${process.env.DISCORD_APP_ID}/${req.body.token}/messages/@original`;
const initialMessage = `\n> ${question}\n\nLet me find the answer for you. This might take a moment`

// Send a placeholder response
await sendPlaceholderResponse(res, initialMessage);

// Show animated dots in the message while waiting
let dotCount = 0;
const maxDots = 4;
let isFetching = true;

const interval = setInterval(() => {
if (isFetching) {
dotCount = (dotCount % maxDots) + 1;
sendFollowUpResponse(endpoint, `${initialMessage}${'.'.repeat(dotCount)}`);
}
}, 500);

// Create the follow-up response
let followUpMessage;
try {
// Call an external API to fetch the answer
const answer = await fetchAnswer(question);
followUpMessage = `\n> ${question}\n\nHere's what I found, <@${userId}>:\n\n${answer}`;
} catch (error) {
console.error('Error fetching answer:', error);
followUpMessage = `\n> ${question}\n\nSorry <@${userId}>, I couldn't fetch an answer to your question. Please try again later.`;
} finally {
// Ensure cleanup and state updates
isFetching = false; // Mark fetching as complete
clearInterval(interval); // Stop the dot interval
}

return sendFollowUpResponse(endpoint, followUpMessage);
}

// "test" command
if (name === 'test') {
// Send a message into the channel where command was triggered from
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
// Fetches a random emoji to send from a helper function
content: `Develop Anything, Deploy Anywhere ${getRandomEmoji()}`,
},
});
}

console.error(`unknown command: ${name}`);
return res.status(400).json({ error: 'unknown command' });
}

console.error('unknown interaction type', type);
return res.status(400).json({ error: 'unknown interaction type' });
});

app.listen(PORT, () => {
console.log('Listening on port', PORT);
});
Loading