Updated dir structure to put the actual source code in the general /src dir
Some checks failed
Update Wiki from JSDoc / update-wiki (pull_request) Has been cancelled
Lint JavaScript/Node.js / lint-js (pull_request) Failing after 11s
DRB Tests / drb_mocha_tests (pull_request) Has been cancelled

This commit is contained in:
Logan Cusano
2024-08-17 18:44:18 -04:00
parent 8f2891f5d8
commit 066404dd10
44 changed files with 2493 additions and 1678 deletions

View File

@@ -12,7 +12,7 @@ RUN npm install -g node-gyp
RUN npm install
# Copy the rest of the application code to the working directory
COPY . .
COPY ./src ./src
# Expose the port on which your Node.js application will run
EXPOSE 3420

View File

@@ -1,117 +0,0 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.modules.gptHandler");
import dotenv from "dotenv";
dotenv.config();
import { OpenAI } from "openai";
import { EventEmitter } from "events";
const openai = new OpenAI(process.env.OPENAI_API_KEY);
const assistant = await openai.beta.assistants.create({
name: "Emmelia",
instructions: process.env.DRB_SERVER_INITIAL_PROMPT,
model: "gpt-4o",
});
class EventHandler extends EventEmitter {
constructor(client) {
super();
this.client = client;
}
async onEvent(event) {
try {
console.log(event);
// Retrieve events that are denoted with 'requires_action'
// since these will have our tool_calls
if (event.event === "thread.run.requires_action") {
await this.handleRequiresAction(
event.data,
event.data.id,
event.data.thread_id,
);
}
} catch (error) {
console.error("Error handling event:", error);
}
}
async handleRequiresAction(data, runId, threadId) {
try {
const toolOutputs =
data.required_action.submit_tool_outputs.tool_calls.map((toolCall) => {
// Call the function
switch (toolCall.function.name) {
case "getCurrentTemperature":
return {
tool_call_id: toolCall.id,
output: "57",
};
}
});
// Submit all the tool outputs at the same time
await this.submitToolOutputs(toolOutputs, runId, threadId);
} catch (error) {
console.error("Error processing required action:", error);
}
}
async submitToolOutputs(toolOutputs, runId, threadId) {
try {
// Use the submitToolOutputsStream helper
const stream = this.client.beta.threads.runs.submitToolOutputsStream(
threadId,
runId,
{ tool_outputs: toolOutputs },
);
for await (const event of stream) {
this.emit("event", event);
}
} catch (error) {
console.error("Error submitting tool outputs:", error);
}
}
}
const eventHandler = new EventHandler(openai);
eventHandler.on("event", eventHandler.onEvent.bind(eventHandler));
export const gptHandler = async (additionalMessages) => {
const thread = await openai.beta.threads.create();
// Add the additional messages to the conversation
for (const msgObj of additionalMessages) {
await openai.beta.threads.messages.create(thread.id, msgObj);
}
log.DEBUG("AI Conversation:", thread);
// Run the thread to get a response
try {
const stream = await openai.beta.threads.runs.stream(
thread.id,
{ assistant_id: assistant.id },
eventHandler,
);
for await (const event of stream) {
eventHandler.emit("event", event);
}
let response;
const messages = await openai.beta.threads.messages.list(thread.id);
response = messages.data[0].content[0].text.value;
log.DEBUG("AI Response:", response);
if (!response) {
return false;
}
return response;
} catch (error) {
console.error("Error generating response:", error);
return false;
}
};

View File

@@ -17,7 +17,7 @@ export default [
// Apply ESLint recommended settings first
...compat.extends().map((config) => ({
...config,
files: ["**/*.mjs", "**/*.js", "**/*.cjs"],
files: ["src/**/*.mjs", "src/**/*.js", "src/**/*.cjs"],
})),
// Custom rules and plugin configuration

View File

@@ -1,14 +0,0 @@
{
"source": {
"include": ["addons", "discordBot", "modules", "rss-manager", "test"],
"exclude": ["node_modules"],
"includePattern": ".+\\.[mc]?js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/better-docs"
}
}

View File

@@ -21,10 +21,6 @@ run:
-e MONGO_URL=${MONGO_URL} \
-e DISCORD_TOKEN=${DISCORD_TOKEN} \
-e RSS_REFRESH_INTERVAL=${RSS_REFRESH_INTERVAL} \
-e WELCOME_CHANNEL_ID=${WELCOME_CHANNEL_ID} \
-e IGNORED_CHANNEL_IDS=${IGNORED_CHANNEL_IDS} \
-e LINKCOP_RESTRICTED_CHANNEL_IDS=${LINKCOP_RESTRICTED_CHANNEL_IDS} \
-e DRB_SERVER_INITIAL_PROMPT=${DRB_SERVER_INITIAL_PROMPT} \
-e OPENAI_API_KEY=${OPENAI_API_KEY} \
-e LOG_LOCATION="./logs/server.log" \
-p ${SERVER_PORT}:${SERVER_PORT} \

3755
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,31 +2,30 @@
"name": "drb-server",
"version": "3.0.0",
"description": "",
"main": "server.js",
"main": "src/server.js",
"scripts": {
"docs": "jsdoc -c jsdoc.json -d docs -t ./node_modules/better-docs",
"docs": "jsdoc2md --files \"src/**/*.js\" > docs/api.md",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "mocha --timeout 5000",
"start": "node server.js"
"start": "node src/server.js"
},
"author": "Logan Cusano",
"license": "ISC",
"type": "module",
"devDependencies": {
"better-docs": "^2.7.3",
"chai": "^5.1.1",
"eslint": "^9.9.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^4.1.3",
"jsdoc": "^4.0.3",
"mocha": "^10.4.0",
"prettier": "^3.3.3",
"socket.io-client": "^4.7.5"
},
"dependencies": {
"discord.js": "^14.15.2",
"documentation": "^14.0.3",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"mongodb": "^6.7.0",

View File

@@ -0,0 +1,254 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
import { requestNodeJoinSystem } from "../../modules/socketServerWrappers.mjs";
import { getSystemByName } from "../../modules/mongo-wrappers/mongoSystemsWrappers.mjs";
import { getConfig } from "../../modules/mongo-wrappers/mongoConfigWrappers.mjs";
import { getAvailableTokensInGuild } from "../modules/wrappers.mjs";
import dotenv from "dotenv";
import { OpenAI } from "openai";
import { EventEmitter } from "events";
// Initialize environment variables
dotenv.config();
const log = new DebugBuilder("server", "discordBot.modules.gptHandler");
let assistant;
let eventHandler;
(async () => {
try {
const openai = new OpenAI(process.env.OPENAI_API_KEY);
assistant = await openai.beta.assistants.create({
name: "Emmelia",
instructions: (await getConfig("emmeliaInitialPrompt")) || "",
model: "gpt-4o-mini",
tools: [
{
type: "function",
function: {
name: "checkUserVoiceChannel",
description: "Check if the user is in a voice channel",
parameters: {
type: "object",
properties: {
userId: {
type: "string",
description: "The ID of the user",
},
guildId: {
type: "string",
description: "The ID of the guild",
},
},
required: ["userId", "guildId"],
},
},
},
{
type: "function",
function: {
name: "getSelectedSystem",
description: "Retrieve the selected system details",
parameters: {
type: "object",
properties: {
systemName: {
type: "string",
description: "The name of the system",
},
},
required: ["systemName"],
},
},
},
{
type: "function",
function: {
name: "joinSelectedSystem",
description: "Join the selected system in the user's voice channel",
parameters: {
type: "object",
properties: {
userId: {
type: "string",
description: "The ID of the user",
},
guildId: {
type: "string",
description: "The ID of the guild",
},
systemName: {
type: "string",
description: "The name of the system",
},
channelId: {
type: "string",
description: "The ID of the voice channel",
},
},
required: ["userId", "guildId", "systemName", "channelId"],
},
},
},
],
});
class EventHandler extends EventEmitter {
constructor(client) {
super();
this.client = client;
}
async onEvent(event) {
try {
console.log(event);
if (event.event === "thread.run.requires_action") {
await this.handleRequiresAction(
event.data,
event.data.id,
event.data.thread_id,
);
}
} catch (error) {
console.error("Error handling event:", error);
}
}
async handleRequiresAction(data, runId, threadId) {
try {
const toolOutputs = await Promise.all(
data.required_action.submit_tool_outputs.tool_calls.map(
async (toolCall) => {
switch (toolCall.function.name) {
case "checkUserVoiceChannel":
return await this.checkUserVoiceChannel(toolCall);
case "getSelectedSystem":
return await this.getSelectedSystem(toolCall);
case "joinSelectedSystem":
return await this.joinSelectedSystem(toolCall);
default:
throw new Error(
`Unknown function: ${toolCall.function.name}`,
);
}
},
),
);
await this.submitToolOutputs(toolOutputs, runId, threadId);
} catch (error) {
console.error("Error processing required action:", error);
}
}
async checkUserVoiceChannel(toolCall) {
const { userId, guildId } = JSON.parse(toolCall.function.arguments);
const guild = await this.client.guilds.get(guildId);
const member = await guild.members.get(userId);
const isInVoiceChannel = !!member.voice.channel;
return {
tool_call_id: toolCall.id,
output: JSON.stringify({
isInVoiceChannel,
channelId: member.voice.channelId,
}),
};
}
async getSelectedSystem(toolCall) {
const { systemName } = JSON.parse(toolCall.function.arguments);
const system = await getSystemByName(systemName);
return {
tool_call_id: toolCall.id,
output: JSON.stringify(system),
};
}
async joinSelectedSystem(toolCall) {
const { userId, guildId, systemName, channelId } = JSON.parse(
toolCall.function.arguments,
);
const system = await getSystemByName(systemName);
const guild = await this.client.guilds.fetch(guildId);
const discordToken = await getAvailableTokensInGuild(guildId);
if (discordToken) {
const result = await requestNodeJoinSystem(
system,
channelId,
discordToken,
);
return {
tool_call_id: toolCall.id,
output: JSON.stringify({ success: true, result }),
};
} else {
return {
tool_call_id: toolCall.id,
output: JSON.stringify({
success: false,
message: "No available bots.",
}),
};
}
}
async submitToolOutputs(toolOutputs, runId, threadId) {
try {
const stream =
this.client.beta.threads.runs.submitToolOutputsStream(
threadId,
runId,
{ tool_outputs: toolOutputs },
);
for await (const event of stream) {
this.emit("event", event);
}
} catch (error) {
console.error("Error submitting tool outputs:", error);
}
}
}
eventHandler = new EventHandler(openai);
eventHandler.on("event", eventHandler.onEvent.bind(eventHandler));
} catch (error) {
console.error("Initialization error:", error);
}
})();
export const gptHandler = async (additionalMessages) => {
try {
const thread = await openai.beta.threads.create();
for (const msgObj of additionalMessages) {
await openai.beta.threads.messages.create(thread.id, msgObj);
}
const stream = await openai.beta.threads.runs.stream(
thread.id,
{ assistant_id: assistant.id },
eventHandler,
);
for await (const event of stream) {
eventHandler.emit("event", event);
}
const messages = await openai.beta.threads.messages.list(thread.id);
const response = messages.data[0].content[0].text.value;
log.DEBUG("AI Response:", response);
if (!response) {
return false;
}
return response;
} catch (error) {
console.error("Error generating response:", error);
return false;
}
};

View File

@@ -4,7 +4,6 @@ import {
checkIfNodeIsConnectedToVC,
getNodeDiscordID,
getNodeDiscordUsername,
checkIfNodeHasOpenDiscordClient,
getNodeCurrentListeningSystem,
requestNodeJoinSystem,
} from "../../modules/socketServerWrappers.mjs";
@@ -86,9 +85,8 @@ export const getAvailableNodes = async (nodeIo, guildId, system) => {
openSockets.map(async (openSocket) => {
openSocket = await nodeIo.sockets.sockets.get(openSocket);
// Check if the node has an existing open client (meaning the radio is already being listened to)
const hasOpenClient = await checkIfNodeHasOpenDiscordClient(openSocket);
if (hasOpenClient) {
let currentSystem = await getNodeCurrentListeningSystem(openSocket);
let currentSystem = await getNodeCurrentListeningSystem(openSocket);
if (currentSystem) {
if (currentSystem != system.name) {
log.INFO(
"Node is listening to a different system than requested",

View File

@@ -4,11 +4,11 @@ import ioClient from "socket.io-client";
import {
deleteNodeByNuid,
getNodeByNuid,
} from "../modules/mongo-wrappers/mongoNodesWrappers.mjs";
} from "../src/modules/mongo-wrappers/mongoNodesWrappers.mjs";
import {
deleteSystemByName,
getSystemByName,
} from "../modules/mongo-wrappers/mongoSystemsWrappers.mjs";
} from "../src/modules/mongo-wrappers/mongoSystemsWrappers.mjs";
import {
nodeDisconnectWrapper,
checkIfNodeHasOpenDiscordClient,
@@ -19,8 +19,8 @@ import {
requestBotLeaveServer,
requestNodeJoinSystem,
requestNodeUpdate,
} from "../modules/socketServerWrappers.mjs";
import { nodeIo } from "../modules/socketServer.mjs";
} from "../src/modules/socketServerWrappers.mjs";
import { nodeIo } from "../src/modules/socketServer.mjs";
import dotenv from "dotenv";
dotenv.config();