Added complete code documentation
This commit is contained in:
parent
63d2c1ddf1
commit
c74ad8d0fc
@ -7,32 +7,60 @@ import { Runnable } from '@/models/runnable';
|
|||||||
import { Logger } from '@/logger';
|
import { Logger } from '@/logger';
|
||||||
|
|
||||||
export class Api implements AI, Runnable {
|
export class Api implements AI, Runnable {
|
||||||
|
/**
|
||||||
|
* Logger instance
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private _logger: Logger;
|
private _logger: Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI API instance
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private _api!: OpenAIApi;
|
private _api!: OpenAIApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI API configuration
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private readonly _configuration: Configuration;
|
private readonly _configuration: Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create API instance
|
||||||
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this._logger = new Logger(Api.name);
|
this._logger = new Logger(Api.name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create OpenAI API configuration with API key
|
||||||
|
*/
|
||||||
this._configuration = new Configuration({
|
this._configuration = new Configuration({
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize OpenAI API service
|
||||||
|
*/
|
||||||
run(): void {
|
run(): void {
|
||||||
try {
|
try {
|
||||||
this._api = new OpenAIApi(this._configuration);
|
this._api = new OpenAIApi(this._configuration); // Create API instance
|
||||||
this._logger.service.info('OpenAI Service has been initialized successfully.');
|
this._logger.service.info('OpenAI Service has been initialized successfully.'); // Log service initialization
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._logger.service.error(`Failed to start OpenAI Service: ${error}`);
|
this._logger.service.error(`Failed to start OpenAI Service: ${error}`); // Log service initialization error
|
||||||
process.exit(1);
|
process.exit(1); // Exit process
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the chat completion from the OpenAI API
|
||||||
|
* @param chatHistory
|
||||||
|
*/
|
||||||
async chatCompletion(chatHistory: ChatCompletionRequestMessage[])
|
async chatCompletion(chatHistory: ChatCompletionRequestMessage[])
|
||||||
: Promise<ChatCompletionResponseMessage> {
|
: Promise<ChatCompletionResponseMessage> {
|
||||||
|
/**
|
||||||
|
* Create chat completion request and return response or throw error
|
||||||
|
*/
|
||||||
const request = await this._api.createChatCompletion({
|
const request = await this._api.createChatCompletion({
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
messages: chatHistory,
|
messages: chatHistory,
|
||||||
|
@ -9,26 +9,32 @@ import { Command } from '@/bot/models/command';
|
|||||||
|
|
||||||
export const ChatCommand: Command = {
|
export const ChatCommand: Command = {
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
description: 'Say anything to the Chat bot',
|
description: 'Chat with the bot',
|
||||||
type: ApplicationCommandType.ChatInput,
|
type: ApplicationCommandType.ChatInput,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'question',
|
name: 'question',
|
||||||
description: 'Question for the Chat bot',
|
description: 'The question you want to ask the bot',
|
||||||
required: true,
|
required: true,
|
||||||
type: ApplicationCommandOptionType.String,
|
type: ApplicationCommandOptionType.String,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ephemeral',
|
name: 'ephemeral',
|
||||||
description: 'If you set \'false\' the message will be persisted over time',
|
description: 'If the response should be ephemeral or not',
|
||||||
required: false,
|
required: false,
|
||||||
type: ApplicationCommandOptionType.Boolean,
|
type: ApplicationCommandOptionType.Boolean,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
execute: async (client: Client, interaction: CommandInteraction, ai) => {
|
execute: async (client: Client, interaction: CommandInteraction, ai) => {
|
||||||
|
/**
|
||||||
|
* Get the chat history from the channel
|
||||||
|
*/
|
||||||
const channel = client.channels.cache.get(interaction.channelId) as TextChannel;
|
const channel = client.channels.cache.get(interaction.channelId) as TextChannel;
|
||||||
const messages = await channel.messages.fetch({ limit: 100 });
|
const messages = await channel.messages.fetch({ limit: 100 }); // Get the last 100 messages from the channel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the messages from the user and get the embeds from the messages
|
||||||
|
*/
|
||||||
const chatHistory: ChatCompletionRequestMessage[] = [];
|
const chatHistory: ChatCompletionRequestMessage[] = [];
|
||||||
const consistentMessages = messages
|
const consistentMessages = messages
|
||||||
.filter((x) => x.interaction?.user.id === interaction.user.id);
|
.filter((x) => x.interaction?.user.id === interaction.user.id);
|
||||||
@ -38,18 +44,27 @@ export const ChatCommand: Command = {
|
|||||||
.flatMap((item) => item.data);
|
.flatMap((item) => item.data);
|
||||||
|
|
||||||
embed.forEach((item) => {
|
embed.forEach((item) => {
|
||||||
|
/**
|
||||||
|
* Create the message object from the embed and add it to the chat history
|
||||||
|
*/
|
||||||
const message: ChatCompletionRequestMessage = {
|
const message: ChatCompletionRequestMessage = {
|
||||||
role: item.footer?.text === 'embed-question' ? 'user' : 'assistant',
|
role: item.footer?.text === 'embed-question' ? 'user' : 'assistant',
|
||||||
content: item.description || 'An error occurred during the process, please try again later.',
|
content: item.description || 'An error occurred during the process, please try again later.',
|
||||||
};
|
};
|
||||||
|
|
||||||
chatHistory.push(message);
|
chatHistory.push(message); // Add the message to the chat history
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the options from the interaction
|
||||||
|
*/
|
||||||
const interactionResolver = (interaction.options as CommandInteractionOptionResolver);
|
const interactionResolver = (interaction.options as CommandInteractionOptionResolver);
|
||||||
const question = interactionResolver.getString('question') || undefined;
|
const question = interactionResolver.getString('question') || undefined; // Default to undefined
|
||||||
const ephemeral = interactionResolver.getBoolean('ephemeral') || true;
|
const ephemeral = interactionResolver.getBoolean('ephemeral') || true; // Default to true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the current question to the chat history
|
||||||
|
*/
|
||||||
const currentQuestion: ChatCompletionRequestMessage = {
|
const currentQuestion: ChatCompletionRequestMessage = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: question || 'An error occurred during the process, please try again later.',
|
content: question || 'An error occurred during the process, please try again later.',
|
||||||
@ -57,10 +72,16 @@ export const ChatCommand: Command = {
|
|||||||
|
|
||||||
chatHistory.push(currentQuestion);
|
chatHistory.push(currentQuestion);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the answer from the AI
|
||||||
|
*/
|
||||||
const answer = await ai?.chatCompletion(chatHistory)
|
const answer = await ai?.chatCompletion(chatHistory)
|
||||||
.then((response) => response.content)
|
.then((response) => response.content)
|
||||||
.catch((error: Error) => error.message);
|
.catch((error: Error) => error.message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the current answer to the chat history and reply to the user on the channel
|
||||||
|
*/
|
||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
ephemeral,
|
ephemeral,
|
||||||
fetchReply: true,
|
fetchReply: true,
|
||||||
@ -70,7 +91,7 @@ export const ChatCommand: Command = {
|
|||||||
title: '✥ Question',
|
title: '✥ Question',
|
||||||
description: question,
|
description: question,
|
||||||
footer: {
|
footer: {
|
||||||
text: 'embed-question',
|
text: 'embed-question', // embed-question is used to identify is a response from user to bot
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -78,7 +99,7 @@ export const ChatCommand: Command = {
|
|||||||
title: '✥ Answer',
|
title: '✥ Answer',
|
||||||
description: answer,
|
description: answer,
|
||||||
footer: {
|
footer: {
|
||||||
text: 'embed-answer',
|
text: 'embed-answer', // embed-answer is used to identify is a response from bot to user
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -8,14 +8,26 @@ export const ClearCommand: Command = {
|
|||||||
description: 'Delete your interactions with the Chat bot',
|
description: 'Delete your interactions with the Chat bot',
|
||||||
type: ApplicationCommandType.ChatInput,
|
type: ApplicationCommandType.ChatInput,
|
||||||
execute: async (client: Client, interaction: CommandInteraction) => {
|
execute: async (client: Client, interaction: CommandInteraction) => {
|
||||||
|
/**
|
||||||
|
* Get the chat history from the channel and filter the messages from the user
|
||||||
|
*/
|
||||||
const channel = client.channels.cache.get(interaction.channelId) as TextChannel;
|
const channel = client.channels.cache.get(interaction.channelId) as TextChannel;
|
||||||
const messages = await channel.messages.fetch({ limit: 100 });
|
const messages = await channel.messages.fetch({ limit: 100 }); // Get the last 100 messages from the channel
|
||||||
const consistentMessages = messages
|
const consistentMessages = messages
|
||||||
.filter((x) => x.interaction?.user.id === interaction.user.id);
|
.filter((x) => x.interaction?.user.id === interaction.user.id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the messages from the channel
|
||||||
|
*/
|
||||||
if (channel.type === ChannelType.GuildText) {
|
if (channel.type === ChannelType.GuildText) {
|
||||||
|
/**
|
||||||
|
* Bulk delete the messages if the channel is a guild text channel
|
||||||
|
*/
|
||||||
await channel.bulkDelete(consistentMessages);
|
await channel.bulkDelete(consistentMessages);
|
||||||
} else {
|
} else {
|
||||||
|
/**
|
||||||
|
* Delete the messages one by one if the channel is a DM channel
|
||||||
|
*/
|
||||||
await messages.forEach((message) => {
|
await messages.forEach((message) => {
|
||||||
if (message.author.id !== client.user?.id) return;
|
if (message.author.id !== client.user?.id) return;
|
||||||
message.delete();
|
message.delete();
|
||||||
|
@ -3,6 +3,9 @@ import { PingCommand } from '@/bot/commands/pingCommand';
|
|||||||
import { ChatCommand } from '@/bot/commands/chatCommand';
|
import { ChatCommand } from '@/bot/commands/chatCommand';
|
||||||
import { ClearCommand } from '@/bot/commands/clearCommand';
|
import { ClearCommand } from '@/bot/commands/clearCommand';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all the commands registered as an array for centralized management
|
||||||
|
*/
|
||||||
export const commands: Command[] = [
|
export const commands: Command[] = [
|
||||||
PingCommand,
|
PingCommand,
|
||||||
ChatCommand,
|
ChatCommand,
|
||||||
|
@ -7,6 +7,9 @@ export const PingCommand: Command = {
|
|||||||
type: ApplicationCommandType.ChatInput,
|
type: ApplicationCommandType.ChatInput,
|
||||||
execute: async (client: Client, interaction: CommandInteraction) => {
|
execute: async (client: Client, interaction: CommandInteraction) => {
|
||||||
const content = 'Pong';
|
const content = 'Pong';
|
||||||
|
/**
|
||||||
|
* Send a message to the channel
|
||||||
|
*/
|
||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
content,
|
content,
|
||||||
|
@ -8,16 +8,31 @@ import { AI } from '@/models/ai';
|
|||||||
import { commands } from '@/bot/commands';
|
import { commands } from '@/bot/commands';
|
||||||
|
|
||||||
export class Bot implements Runnable {
|
export class Bot implements Runnable {
|
||||||
|
/**
|
||||||
|
* Logger instance
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private _logger: Logger;
|
private _logger: Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI instance
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private readonly _ai: AI;
|
private readonly _ai: AI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord API client instance
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private readonly _client: Client;
|
private readonly _client: Client;
|
||||||
|
|
||||||
constructor(ai: AI) {
|
constructor(ai: AI) {
|
||||||
this._logger = new Logger(Bot.name);
|
this._logger = new Logger(Bot.name);
|
||||||
this._ai = ai;
|
this._ai = ai;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Discord API client instance with intents and partials
|
||||||
|
*/
|
||||||
this._client = new Client({
|
this._client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
IntentsBitField.Flags.Guilds,
|
IntentsBitField.Flags.Guilds,
|
||||||
@ -26,12 +41,20 @@ export class Bot implements Runnable {
|
|||||||
IntentsBitField.Flags.DirectMessages,
|
IntentsBitField.Flags.DirectMessages,
|
||||||
],
|
],
|
||||||
partials: [
|
partials: [
|
||||||
Partials.Channel,
|
Partials.Channel, // For DMs
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle slash commands from Discord API
|
||||||
|
* @param interaction
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private async handleSlashCommand(interaction: CommandInteraction): Promise<void> {
|
private async handleSlashCommand(interaction: CommandInteraction): Promise<void> {
|
||||||
|
/**
|
||||||
|
* Find command by name and execute it if found or return error message
|
||||||
|
*/
|
||||||
const slashCommand = commands.find((command) => command.name === interaction.commandName);
|
const slashCommand = commands.find((command) => command.name === interaction.commandName);
|
||||||
if (!slashCommand) {
|
if (!slashCommand) {
|
||||||
this._logger.service.warning(`SlashCommand [${interaction.commandName}] not found.`);
|
this._logger.service.warning(`SlashCommand [${interaction.commandName}] not found.`);
|
||||||
@ -39,36 +62,56 @@ export class Bot implements Runnable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply(); // Defer reply to show loading state
|
||||||
this._logger.service.debug(`SlashCommand [${interaction.commandName}] executed properly.`);
|
this._logger.service.debug(`SlashCommand [${interaction.commandName}] executed properly.`); // Log command execution
|
||||||
await slashCommand.execute(this._client, interaction, this._ai);
|
await slashCommand.execute(this._client, interaction, this._ai); // Execute command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Discord API service
|
||||||
|
*/
|
||||||
run(): void {
|
run(): void {
|
||||||
|
/**
|
||||||
|
* Login to Discord API and set status for show command if login was successful or exit process if failed
|
||||||
|
*/
|
||||||
this._client.login(process.env.DISCORD_API_KEY).then(() => {
|
this._client.login(process.env.DISCORD_API_KEY).then(() => {
|
||||||
this._logger.service.info('Discord Service has been initialized successfully.');
|
this._logger.service.info('Discord Service has been initialized successfully.'); // Log service initialization
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this._logger.service.error(`Failed to start Discord Service: ${error}`);
|
this._logger.service.error(`Failed to start Discord Service: ${error}`); // Log service initialization error
|
||||||
process.exit(1);
|
process.exit(1); // Exit process
|
||||||
});
|
});
|
||||||
|
|
||||||
this._client.on('ready', async () => {
|
this._client.on('ready', async () => {
|
||||||
|
/**
|
||||||
|
* Check if user and application are available before continue
|
||||||
|
*/
|
||||||
if (!this._client.user || !this._client.application) {
|
if (!this._client.user || !this._client.application) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set status for show command
|
/**
|
||||||
|
* Set status for show command
|
||||||
|
*/
|
||||||
this._client.user?.setActivity({
|
this._client.user?.setActivity({
|
||||||
name: '/chat',
|
name: '/chat',
|
||||||
type: ActivityType.Listening,
|
type: ActivityType.Listening,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set slash commands for bot application
|
||||||
|
*/
|
||||||
await this._client.application.commands.set(commands);
|
await this._client.application.commands.set(commands);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On interaction create event handler
|
||||||
|
*/
|
||||||
this._client.on('interactionCreate', async (interaction: Interaction) => {
|
this._client.on('interactionCreate', async (interaction: Interaction) => {
|
||||||
|
/**
|
||||||
|
* Check if interaction is command or chat input command
|
||||||
|
*/
|
||||||
if (interaction.isCommand() || interaction.isChatInputCommand()) {
|
if (interaction.isCommand() || interaction.isChatInputCommand()) {
|
||||||
await this.handleSlashCommand(interaction);
|
await this.handleSlashCommand(interaction); // Handle slash command
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,11 @@ import { CommandInteraction, ChatInputApplicationCommandData, Client } from 'dis
|
|||||||
import { AI } from '@/models/ai';
|
import { AI } from '@/models/ai';
|
||||||
|
|
||||||
export interface Command extends ChatInputApplicationCommandData {
|
export interface Command extends ChatInputApplicationCommandData {
|
||||||
|
/**
|
||||||
|
* Execute the command with the given parameters
|
||||||
|
* @param client
|
||||||
|
* @param interaction
|
||||||
|
* @param ai
|
||||||
|
*/
|
||||||
execute: (client: Client, interaction: CommandInteraction, ai?: AI) => Promise<void>;
|
execute: (client: Client, interaction: CommandInteraction, ai?: AI) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ import { Bot } from '@/bot';
|
|||||||
import { Api } from '@/api';
|
import { Api } from '@/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure dotenv.
|
* Load environment variables from .env file
|
||||||
*/
|
*/
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenAI contained in API Module.
|
* OpenAI contained in Api Module.
|
||||||
*/
|
*/
|
||||||
const api = new Api();
|
const api = new Api();
|
||||||
api.run();
|
api.run();
|
||||||
|
@ -4,8 +4,16 @@ import {
|
|||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
|
/**
|
||||||
|
* Winston logger instance
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected _logger: WinstonLogger;
|
protected _logger: WinstonLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create logger instance
|
||||||
|
* @param serviceName
|
||||||
|
*/
|
||||||
constructor(serviceName: string) {
|
constructor(serviceName: string) {
|
||||||
this._logger = createLogger({
|
this._logger = createLogger({
|
||||||
level: process.env.NODE_ENV === 'dev' ? 'debug' : 'info',
|
level: process.env.NODE_ENV === 'dev' ? 'debug' : 'info',
|
||||||
@ -23,6 +31,9 @@ export class Logger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get logger instance
|
||||||
|
*/
|
||||||
get service() {
|
get service() {
|
||||||
return this._logger;
|
return this._logger;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { ChatCompletionRequestMessage, ChatCompletionResponseMessage } from 'openai';
|
import { ChatCompletionRequestMessage, ChatCompletionResponseMessage } from 'openai';
|
||||||
|
|
||||||
export interface AI {
|
export interface AI {
|
||||||
|
/**
|
||||||
|
* Get the chat completion from the OpenAI API
|
||||||
|
* @param chatHistory
|
||||||
|
*/
|
||||||
chatCompletion(chatHistory: ChatCompletionRequestMessage[]):
|
chatCompletion(chatHistory: ChatCompletionRequestMessage[]):
|
||||||
Promise<ChatCompletionResponseMessage>;
|
Promise<ChatCompletionResponseMessage>;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
export interface Runnable {
|
export interface Runnable {
|
||||||
|
/**
|
||||||
|
* Run the service instance
|
||||||
|
*/
|
||||||
run(): void;
|
run(): void;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user