Skip to content

Commit b083840

Browse files
author
GAIA Framework Bot
committed
Adding Discord Bot example
1 parent a5bd42b commit b083840

25 files changed

+1271
-3
lines changed

README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ For the tool to execute properly the docker image must be built first:
117117
docker build -t jupyter-runtime .
118118
```
119119

120-
### Run Typescript Example
120+
### Run Typescript Console Example
121121

122-
1. Navigate to `examples/typescript` in terminal
122+
1. Navigate to `examples/typescript/console` in terminal
123123

124124
2. Create a `.env` file based on the `.env.example` and add your value for the `OPENAI_API_KEY`
125125

@@ -134,6 +134,37 @@ For the tool to execute properly the docker image must be built first:
134134
4. Enter a prompt that would make GPT choose the tool
135135
- e.g., `execute a python script to add two numbers together and show the result`
136136

137+
### Run Typescript Discord Bot Example
138+
139+
1. Setup a Bot on the Discord Developer Portal: <https://discord.com/developers/applications>
140+
141+
2. Navigate to `examples/typescript/discordbot` in terminal
142+
143+
3. Create a `.env` file based on the `.env.example` and add your value for the `OPENAI_API_KEY`
144+
145+
- (Optional) To connect to Azure Blob storage, provide values for the additional following keys, assuming Azure Blob Storage is already configured in your Azure subscription (see [Microsofts Quickstart](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-portal))
146+
147+
- `AZURE_STORAGE_CONNECTION_STRING` - The connection string to your Azure Storage resource
148+
149+
- `AZURE_BLOB_CONTAINER_NAME` - The name of your blob container
150+
151+
- `AZURE_STORAGE_ACCOUNT_NAME` - The name of the Azure Storage account
152+
153+
- `AZURE_STORAGE_ACCOUNT_KEY` - The account key of the Azure Storage account
154+
155+
4. Run the following commands sequentially:
156+
157+
```bash
158+
yarn install
159+
yarn build
160+
yarn start
161+
```
162+
163+
5. Start a chat with your bot using its username
164+
165+
6. Enter a prompt to your bot in Discord that would make GPT choose the tool
166+
- e.g., `execute a python script to do something cool and display it`
167+
137168
## Example Run Outputs
138169

139170
View [output examples](docs/output_examples.md) to see example run outputs using the tool with `.runTools(...)` from the [OpenAI Node API](https://www.npmjs.com/package/openai) for easy usage and handling tool errors

docs/output_examples.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ This plot showcases a simple, yet elegant, sine wave which is a fundamental conc
343343

344344
## Example 4 - Utilizing the tool with a Discord Bot
345345

346-
In this example, a discord bot setup with GPT is configured to use the tool. After GPT responds with a url similarly to the examples above, it displays the message including a link to download the file if one is created. Which this case it returns a downloadable link to a blob storage where the file is uploaded to. Since discord supports displaying markdown, its shown nicely to the user compared to the standard CLI output. The code for this is not included in the repo, but a demonstration of what can be done using the tool in your application depending on the architecture.
346+
In this example, a discord bot setup with GPT is configured to use the tool. After GPT responds with a url similarly to the examples above, it displays the message including a link to download the file if one is created. Which this case it returns a downloadable link to a blob storage where the file is uploaded to. Since discord supports displaying markdown, its shown nicely to the user compared to the standard CLI output.
347347

348348
<details>
349349
<summary>View example</summary>
File renamed without changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
OPENAI_API_KEY=
2+
DISCORD_BOT_TOKEN=
3+
AZURE_BLOB_CONTAINER_NAME=
4+
AZURE_STORAGE_CONNECTION_STRING=
5+
AZURE_STORAGE_ACCOUNT_KEY=
6+
AZURE_STORAGE_ACCOUNT_NAME=
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "typescript-discordbot",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"build": "tsc",
9+
"start": "node ./dist/index.js"
10+
},
11+
"author": "",
12+
"license": "ISC",
13+
"dependencies": {
14+
"@azure/storage-blob": "^12.17.0",
15+
"discord.js": "^14.14.1",
16+
"dotenv": "^16.4.5",
17+
"openai": "^4.35.0"
18+
}
19+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import {
2+
BlobServiceClient,
3+
BlobSASPermissions,
4+
StorageSharedKeyCredential,
5+
generateBlobSASQueryParameters
6+
} from "@azure/storage-blob";
7+
import path from "path";
8+
import { promises as fs } from 'fs';
9+
import { config } from "../config";
10+
import { BlobType } from "../utils/types";
11+
12+
/**
13+
* Represents a service for interacting with Azure Blob Storage.
14+
*/
15+
export class AzureBlobService {
16+
private blobServiceClient: BlobServiceClient;
17+
18+
constructor() {
19+
this.blobServiceClient = BlobServiceClient.fromConnectionString(config.azureStorageConnectionString);
20+
}
21+
22+
static isBlobStorageAvailable(): boolean {
23+
return Boolean(config.azureStorageConnectionString);
24+
}
25+
26+
/**
27+
* Generates a Shared Access Signature (SAS) URL for a blob in Azure Blob Storage.
28+
* @param blobServiceClient - The BlobServiceClient instance.
29+
* @param containerName - The name of the container.
30+
* @param blobName - The name of the blob.
31+
* @returns A Promise that resolves to the SAS URL for the blob.
32+
*/
33+
async generateBlobSasUrl(blobServiceClient: BlobServiceClient, containerName: string, blobName: string): Promise<string> {
34+
const containerClient = blobServiceClient.getContainerClient(containerName);
35+
const blobClient = containerClient.getBlobClient(blobName);
36+
37+
const sasOptions = {
38+
containerName,
39+
blobName,
40+
permissions: BlobSASPermissions.parse("r"),
41+
startsOn: new Date(),
42+
expiresOn: new Date(new Date().valueOf() + 3600 * 1000), // 1 hour from now
43+
};
44+
45+
const sharedKeyCredential = new StorageSharedKeyCredential(
46+
config.azureStorageAccountName,
47+
config.azureStorageAccountKey
48+
);
49+
50+
const sasToken = generateBlobSASQueryParameters(sasOptions, sharedKeyCredential).toString();
51+
52+
return `${blobClient.url}?${sasToken}`;
53+
}
54+
55+
/**
56+
* Uploads data to a blob container and returns a shared access signature (SAS) token for the uploaded blob.
57+
* @param data - The data to be uploaded as a string.
58+
* @param type - The type of the blob.
59+
* @param fileExtension - The file extension of the blob.
60+
* @returns A Promise that resolves to the SAS token for the uploaded blob.
61+
*/
62+
async uploadToBlob(data: string, type: BlobType, fileExtension: string): Promise<string> {
63+
const blobContainerClient = this.blobServiceClient.getContainerClient(config.azureBlobContainerName);
64+
const blobName = `${type}-${Date.now()}.${fileExtension}`;
65+
const blockBlobClient = blobContainerClient.getBlockBlobClient(blobName);
66+
67+
console.log(`Uploading ${type} to blob: ${blobName}`);
68+
69+
await blockBlobClient.upload(data, data.length);
70+
71+
const sasToken = await blockBlobClient.generateSasUrl({
72+
expiresOn: new Date(new Date().valueOf() + 3600 * 1000), // 1 hour from now
73+
permissions: BlobSASPermissions.parse("r")
74+
});
75+
76+
return sasToken;
77+
}
78+
79+
/**
80+
* Uploads a file to the Azure Blob storage.
81+
* @param filePath - The path of the file to upload.
82+
* @returns A Promise that resolves to the SAS (Shared Access Signature) URL of the uploaded file.
83+
*/
84+
async uploadFileToBlob(filePath: string): Promise<string> {
85+
const blobType: BlobType = BlobType.File;
86+
const originalFileName = path.basename(filePath);
87+
const blobContainerClient = this.blobServiceClient.getContainerClient(config.azureBlobContainerName);
88+
const fileExtension = path.extname(filePath);
89+
const blobName = `${blobType}-${originalFileName.split('.')[0]}-${Date.now()}${fileExtension}`;
90+
const blockBlobClient = blobContainerClient.getBlockBlobClient(blobName);
91+
92+
console.log(`Uploading ${blobType} to blob: ${blobName}`);
93+
94+
// Read the file as a buffer to handle both text and binary files correctly
95+
const data = await fs.readFile(filePath);
96+
await blockBlobClient.upload(data, data.length);
97+
98+
const sasUrl = await blockBlobClient.generateSasUrl({
99+
expiresOn: new Date(new Date().valueOf() + 3600 * 1000), // 1 hour from now
100+
permissions: BlobSASPermissions.parse("r")
101+
});
102+
103+
return sasUrl;
104+
}
105+
106+
async deleteBlob(blobName: string): Promise<void> {
107+
const blobContainerClient = this.blobServiceClient.getContainerClient(config.azureBlobContainerName);
108+
await blobContainerClient.deleteBlob(blobName);
109+
}
110+
111+
/**
112+
* Uploads a base64 encoded image to an Azure Blob storage container.
113+
* @param base64Data - The base64 encoded image data.
114+
* @param type - The type of the image (e.g., 'image', 'file', 'folder').
115+
* @returns A Promise that resolves to the SAS (Shared Access Signature) URL of the uploaded image.
116+
*/
117+
async uploadBase64ImageToBlob(base64Data: string, type: BlobType): Promise<string> {
118+
const data = Buffer.from(base64Data, 'base64');
119+
120+
const blobContainerClient = this.blobServiceClient.getContainerClient(config.azureBlobContainerName);
121+
const blobName = `${type}-${Date.now()}.png`;
122+
const blockBlobClient = blobContainerClient.getBlockBlobClient(blobName);
123+
124+
console.info(`Uploading ${type} image to blob: ${blobName}`);
125+
126+
await blockBlobClient.upload(data, data.length, {
127+
blobHTTPHeaders: { blobContentType: 'image/png' }
128+
});
129+
130+
const sasUrl = await this.generateBlobSasUrl(this.blobServiceClient, config.azureBlobContainerName, blobName);
131+
return sasUrl;
132+
}
133+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Client, GatewayIntentBits, Partials } from "discord.js";
2+
import { getBotResponse } from "../example_openai";
3+
4+
/**
5+
* Represents a Discord bot that interacts with the Discord API.
6+
*/
7+
export class DiscordBot {
8+
private client: Client;
9+
10+
constructor() {
11+
this.client = new Client({
12+
intents: [
13+
GatewayIntentBits.Guilds,
14+
GatewayIntentBits.GuildMessages,
15+
GatewayIntentBits.MessageContent,
16+
GatewayIntentBits.GuildMessageTyping,
17+
GatewayIntentBits.DirectMessages
18+
],
19+
partials: [Partials.Channel],
20+
});
21+
}
22+
23+
/**
24+
* Starts the Discord bot and sets up event listeners for message creation and bot readiness.
25+
*/
26+
start() {
27+
this.client.on("ready", async () => {
28+
console.log(`Discord bot logged in as ${this.client.user?.tag}!`);
29+
});
30+
31+
this.client.on("messageCreate", async (message) => {
32+
if (message.author.bot) return;
33+
34+
message.channel.sendTyping();
35+
36+
// Query open AI with the message content, utilizing
37+
// discord for the input and output of chats
38+
const response = await getBotResponse(message.content);
39+
await message.channel.send(response ?? "Failed to get response from bot");
40+
});
41+
42+
this.client.login(process.env.DISCORD_BOT_TOKEN);
43+
}
44+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as dotenv from "dotenv";
2+
3+
dotenv.config();
4+
5+
export const config = {
6+
discordBotToken: process.env.DISCORD_BOT_TOKEN,
7+
openaiApiKey: process.env.OPENAI_API_KEY,
8+
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING || "",
9+
azureBlobContainerName: process.env.AZURE_BLOB_CONTAINER_NAME || "",
10+
azureStorageAccountName: process.env.AZURE_STORAGE_ACCOUNT_NAME || "",
11+
azureStorageAccountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY || "",
12+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import OpenAI from 'openai';
2+
import { ChatCompletionMessageParam } from 'openai/resources';
3+
import { executePythonInNotebook } from './tools/pythonTool';
4+
5+
const client = new OpenAI({
6+
apiKey: process.env.OPENAI_API_KEY
7+
});
8+
9+
const chatHistory: ChatCompletionMessageParam[] = [];
10+
11+
/**
12+
* Runs an example using OpenAI.
13+
* This function executes Python code in a Jupyter notebook environment using OpenAI's GPT model.
14+
* It prompts the user for a query, sends the query to the GPT model along with previous chat history,
15+
* and displays the response from the GPT model.
16+
*/
17+
export async function getBotResponse(input: string) {
18+
const executePythonSchema = {
19+
"description": "Execute python in a jupyter notebook environment",
20+
"parameters": {
21+
"type": "object",
22+
"properties": {
23+
"input": {
24+
"type": "string",
25+
"description": "The code content to execute in the runtime execution environment"
26+
}
27+
},
28+
"required": ["input"],
29+
}
30+
};
31+
32+
const instructions = `You are a Discord Bot GPT that has a tool to execute Python code. Here are the instructions for the python tool:
33+
- When you send a message containing Python code to python, it will be executed in a non-stateful Jupyter notebook environment. Python will respond with the output of the execution and path to file if one exists. No timeout exists. The drive at '/mnt/data' can be used to save and persist user files. Internet access for this session is enabled, if needed or requested.
34+
35+
- On errors of tool calls, try 3 times to fix the error if the tool response results in an error. If an error persists, let the user know.`;
36+
37+
chatHistory.push({
38+
role: 'user',
39+
content: input
40+
});
41+
42+
// Note: remove chat history if you want a clear chat history for each query
43+
// and just use instructions + user input as the query messages
44+
const queryMessages: ChatCompletionMessageParam[] = [
45+
{
46+
role: "system",
47+
content: instructions
48+
},
49+
...chatHistory
50+
];
51+
52+
const runner = await client.beta.chat.completions
53+
.runTools({
54+
model: 'gpt-4-turbo',
55+
messages: queryMessages,
56+
tools: [
57+
{
58+
type: 'function',
59+
function: {
60+
function: executePythonInNotebook,
61+
parse: JSON.parse,
62+
...executePythonSchema,
63+
},
64+
}
65+
],
66+
});
67+
68+
runner.on('message', (message) => {
69+
console.log(message);
70+
chatHistory.push(message);
71+
});
72+
73+
const finalContent = await runner.finalContent();
74+
return finalContent;
75+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as dotenv from "dotenv";
2+
dotenv.config();
3+
4+
import { DiscordBot } from "./bot/DiscordBot";
5+
new DiscordBot().start();

0 commit comments

Comments
 (0)