Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add OpenAI GPT model integration #39

Merged
merged 13 commits into from
Apr 14, 2023
Merged
4 changes: 2 additions & 2 deletions app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from "express";
import {api} from "./src/api";
import {errorHandler} from "./src/api/middlewares/errorHandler";
import { api } from "./src/api";
import { errorHandler } from "./src/api/middlewares/errorHandler";

export const app = express();

Expand Down
6 changes: 6 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ if (!process.env.GRAPHQL_API)
if (!process.env.GUILD_ID)
throw new Error(errMessage("GUILD_ID is required"))

if (!process.env.OPENAI_API_KEY) {
throw new Error(errMessage("OPENAI_API_KEY is required"))
}

interface Config {
prefix: string;
discordToken: string;
Expand All @@ -25,6 +29,7 @@ interface Config {
lessonChannels: { [key: string]: string };
channels: { [key: string]: string };
port: number;
openaiApiKey: string;
}

export const config: Config = {
Expand Down Expand Up @@ -66,4 +71,5 @@ export const config: Config = {
welcome: "831750041445203979",
},
port: parseInt(process.env.PORT ?? "") || 5623,
openaiApiKey: process.env.OPENAI_API_KEY,
} as const;
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@discordjs/builders": "^0.12.0",
"@discordjs/rest": "^0.3.0",
"discord.js": "^13.6.0",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"zod": "^3.7.3",
"@discordjs/builders": "^0.12.0",
"@discordjs/rest": "^0.3.0",
"graphql": "^16.3.0",
"graphql-request": "^4.2.0"
"graphql-request": "^4.2.0",
"openai": "^3.2.1",
"zod": "^3.7.3"
},
"devDependencies": {
"@types/express": "^4.17.12",
Expand Down
31 changes: 21 additions & 10 deletions src/Bot/commands/commands.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { SlashCommandBuilder } from '@discordjs/builders'

const userSlashCommand =
new SlashCommandBuilder()
.setName("lookup")
.setDescription('Get the user Discord username by their C0D3 username')
.addStringOption(option =>
option.setName('username')
.setDescription("The user's username on C0D3")
.setRequired(true))
new SlashCommandBuilder()
.setName("lookup")
flacial marked this conversation as resolved.
Show resolved Hide resolved
.setDescription('Get the user Discord username by their C0D3 username')
.addStringOption(option =>
option.setName('username')
.setDescription("The user's username on C0D3")
.setRequired(true))



export const commands = [userSlashCommand].map((command) => command.toJSON());

const gptSlashCommand = new SlashCommandBuilder()
.setName('ask')
.setDescription('Ask the assistant a question')
.addStringOption((option) =>
option
.setName('question')
.setDescription('The question you want to ask the assistant')
.setRequired(true)
)


export const commands = [userSlashCommand, gptSlashCommand].map((command) => command.toJSON())
33 changes: 26 additions & 7 deletions src/Bot/commands/commandsReplies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommandInteraction } from "discord.js"
import { GraphQLClient } from "graphql-request"
import { USER_INFO } from "../../graphql"
import { config } from "../../../config"
import { sendPrompt } from "./externals/gpt"

type UserInfoQuery = {
userInfo: {
Expand All @@ -13,13 +14,6 @@ type UserInfoQuery = {

const graphQLClient = new GraphQLClient(config.graphqlAPI)

/*
Reply functions format: <command><subCommand>Reply

Examples:
With subCommand: infoSubmissionsReply
Without subCommand: infoReply
*/
export const lookupReply = async (interaction: CommandInteraction) => {
try {
const usernameArg = interaction.options.getString('username')
Expand All @@ -44,3 +38,28 @@ export const lookupReply = async (interaction: CommandInteraction) => {
await interaction.editReply({ content: 'We could not find the user.' })
}
}

export const assistantAskReply = async (interaction: CommandInteraction) => {
const questionArg = interaction.options.getString('question')

if (!questionArg) {
await interaction.reply({ content: 'You need to provide a question.', ephemeral: true })
return
}

try {
const promptPromise = sendPrompt(questionArg)

// Sometimes, the model takes more than 3 seconds to respond, so we need to defer the reply
// to let the user know that the bot is processing the request and it won't be rejected
// https://discordjs.guide/slash-commands/response-methods.html#deferred-responses
await interaction.deferReply()
JasirZaeem marked this conversation as resolved.
Show resolved Hide resolved

const { completion } = await promptPromise

await interaction.editReply({ content: completion || 'Sorry, I had an issue while responding. Please try again!' })
return
} catch (e) {
await interaction.editReply({ content: 'We could not reach the assistant.' })
}
}
35 changes: 35 additions & 0 deletions src/Bot/commands/externals/gpt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Configuration, OpenAIApi } from 'openai'
import { config } from '../../../../config'

const configuration = new Configuration({
apiKey: config.openaiApiKey,
})
const openai = new OpenAIApi(configuration)

export const sendPrompt = async (question: string) => {
const system = "You're the best coding assistant in the universe. You help students and explain things to them in a simple, clear, and super-friendly way, and you only respond to questions related to coding or programming.\n\n##EXAMPLE\n\nuser: What's const in JavaScript?\ncoding assistant: In JavaScript, const is a keyword used to declare a constant variable. A constant variable, as the name suggests, is a variable whose value cannot be changed once it has been assigned.\n\nTo declare a constant variable in JavaScript, you can use the const keyword followed by the variable name and its initial value. For example, the following code declares a constant variable named PI and assigns it the value of the mathematical constant pi:\n\n```js\nconst PI = 3.141592653589793;\n```\nOnce a constant variable has been declared, any attempt to reassign a new value to it will result in a TypeError. For example, the following code will produce an error:\n\n```js\nconst PI = 3.141592653589793;\nPI = 3.14; // TypeError: Assignment to constant variable.\n```\n\nNote that while a constant variable's value cannot be changed, its properties can still be modified if it is an object or array. In that case, the object or array itself is still considered constant, but its properties can be changed."
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: system },
{ role: 'user', content: question }
],
temperature: 0.7,
max_tokens: 756,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
})

if (response.status !== 200) {
throw new Error('OpenAI API request failed')
}

const completion = response.data.choices[0].message?.content

return {
statusText: response.statusText,
status: response.status,
completion
}
}
13 changes: 11 additions & 2 deletions src/Bot/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { Awaitable, Interaction } from "discord.js";
import { lookupReply } from "./commandsReplies";
import { assistantAskReply, lookupReply } from "./commandsReplies";

export const onInteractionCreate = (interaction: Interaction): Awaitable<void> => {
if (!interaction.isCommand()) return;

const { commandName } = interaction;

if (commandName === "lookup") lookupReply(interaction)
switch (commandName) {
case "lookup":
lookupReply(interaction);
break;
case "ask":
assistantAskReply(interaction);
break;
default:
break;
}
}
Loading