From 52f16c9d0adbd75638628c83ab61f9338acfe04f Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Thu, 28 Sep 2023 11:01:35 +0200 Subject: [PATCH 01/23] toOpenAIMessages: also return the username to user id map This will be used for fetching user descriptions very soon --- src/execution.ts | 2 +- src/toOpenAIMessages.ts | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 3b8e2fd..dada0a4 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -237,7 +237,7 @@ async function executeFromQueue(channel: string) { return b.createdTimestamp - a.createdTimestamp; }); - OpenAImessages = toOpenAIMessages(messages.values()); + [OpenAImessages] = toOpenAIMessages(messages.values()); let generatedMessage: ChatCompletionMessage | undefined = undefined; let answer: Awaited>; diff --git a/src/toOpenAIMessages.ts b/src/toOpenAIMessages.ts index f93d235..ccb8421 100644 --- a/src/toOpenAIMessages.ts +++ b/src/toOpenAIMessages.ts @@ -95,28 +95,34 @@ function getAuthorUsername(message: DiscordMessage): string | undefined { /** * Converts the Iterable of Discord Messages to array of OpenAI Messages to send * first message in the interable will be the last of the returned array (reverse order) + * (first should be newest) * @param messages the iterable to convert - * @returns the converted messages + * @returns [0] the converted messages + * @returns [1] username mappings to id */ export default function toOpenAIMessages( messages: Iterable, -): OpenAIMessage[] { - const rvalue: OpenAIMessage[] = []; +): [OpenAIMessage[], Map] { + const rmessages: OpenAIMessage[] = []; + const rUserMap = new Map(); let tokenCount = 0; for (const message of messages) { + // FIXME: multiple users could have the same name here. const content = formatMessage(message); // FIXME: tokens are not being counted properly (it's lower than it is) but it's enough for me for now. tokenCount += countTokens(content); if (tokenCount > config.readLimits.tokens) break; - rvalue.push({ + const name = getAuthorUsername(message); + rmessages.push({ role: message.author.id === message.client.user.id ? "assistant" : "user", content: content, - name: getAuthorUsername(message), + name: name, }); + if (name && rUserMap.has(name)) rUserMap.set(name, message.author.id); } - rvalue.push(...config.systemPrompt([...messages][0]).reverse()); + rmessages.push(...config.systemPrompt([...messages][0]).reverse()); - return rvalue.reverse(); + return [rmessages.reverse(), rUserMap]; } From 6e0a5e72eaf15025421ac71b2a2d9fe487d45492 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Thu, 28 Sep 2023 11:42:01 +0200 Subject: [PATCH 02/23] functionManager: Forward username mapping here --- src/execution.ts | 5 +++-- src/funcitonManager.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index dada0a4..1f4b282 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -212,6 +212,7 @@ async function executeFromQueue(channel: string) { const message = channelQueue.at(0) as RequestMessage; let functionRanCounter = 0; let OpenAImessages: ChatCompletionMessageParam[] = []; + let modelUsernameMap: Map; // ignore if we can't even send anything to reply if (!canReplyToRequest(message)) return; @@ -237,7 +238,7 @@ async function executeFromQueue(channel: string) { return b.createdTimestamp - a.createdTimestamp; }); - [OpenAImessages] = toOpenAIMessages(messages.values()); + [OpenAImessages, modelUsernameMap] = toOpenAIMessages(messages.values()); let generatedMessage: ChatCompletionMessage | undefined = undefined; let answer: Awaited>; @@ -259,7 +260,7 @@ async function executeFromQueue(channel: string) { OpenAImessages.push(generatedMessage); // FIXME: don't use new instance of FunctionManager OpenAImessages.push( - new FunctionManager().handleFunction(generatedMessage.function_call) + new FunctionManager().handleFunction(generatedMessage.function_call, modelUsernameMap) ); } } while (generatedMessage.function_call); diff --git a/src/funcitonManager.ts b/src/funcitonManager.ts index 9bcb0ec..9af00d5 100644 --- a/src/funcitonManager.ts +++ b/src/funcitonManager.ts @@ -43,7 +43,10 @@ export abstract class OpenAIFunction { }; } - abstract execute(data: OpenAIFunctionRequestData): string; + abstract execute( + data: OpenAIFunctionRequestData, + modelUsernameMap: Map, + ): string; } /* @@ -64,7 +67,10 @@ export default class FunctionManager { return rvalue; } - public handleFunction(request: ChatCompletionFunctionCall): ChatCompletionMessageParam { + public handleFunction( + request: ChatCompletionFunctionCall, + modelUsernameMap: Map = new Map(), + ): ChatCompletionMessageParam { // eslint-disable-next-line @typescript-eslint/no-explicit-any let parsedArguments: any; @@ -91,7 +97,7 @@ export default class FunctionManager { role: "function", name: request.name, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - content: functionToRun.execute(parsedArguments), + content: functionToRun.execute(parsedArguments, modelUsernameMap), }; } } From 02a5b0f7b97cc71e61f7fd811ec652a4995315cf Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Wed, 13 Dec 2023 17:42:59 +0100 Subject: [PATCH 03/23] Remove getTime model function it lead to higher usage of tokens. --- src/configDefault.ts | 10 ---------- src/funcitonManager.ts | 19 +------------------ 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/src/configDefault.ts b/src/configDefault.ts index 65785a9..12ce515 100644 --- a/src/configDefault.ts +++ b/src/configDefault.ts @@ -8,7 +8,6 @@ import IQuota from "./IQuota"; import MessageCount from "./quota/messageCount"; export interface IConfigRequired { - readonly calendarParams: Intl.DateTimeFormatOptions; /** Tokens to authentiate with */ readonly tokens: { readonly Discord: string; @@ -52,15 +51,6 @@ function envAsNumber(key: string): number | undefined { } const defaultConfig: IConfigRequired = { - calendarParams: { - weekday: "short", - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }, tokens: { Discord: envAsString("TOKENS__DISCORD") ?? "", OpenAI: envAsString("TOKENS__OPENAI") ?? "", diff --git a/src/funcitonManager.ts b/src/funcitonManager.ts index 9bcb0ec..41e35c1 100644 --- a/src/funcitonManager.ts +++ b/src/funcitonManager.ts @@ -1,7 +1,5 @@ import { ChatCompletionCreateParams, ChatCompletionMessage, ChatCompletionMessageParam } from "openai/resources/chat"; -import { config } from "./index"; - type parameterMap = { string: string, number: number, @@ -53,7 +51,7 @@ export default class FunctionManager { store = new Map(); constructor() { - this.store.set("getTime", new GetTime()); + // TODO: import functions from functions directory } public getFunctions(): ChatCompletionFunctions[] { @@ -95,18 +93,3 @@ export default class FunctionManager { }; } } - -// buildins - -class GetTime extends OpenAIFunction> { - name = "getTime"; - description = "Gets current date and time with a timezone attached"; - parameters = { - type: "object" as const, - properties: {} as Record, - }; - - execute(): string { - return `${Intl.DateTimeFormat().resolvedOptions().timeZone}): ${new Date().toLocaleString("en-US", config.calendarParams)}`; - } -} From 24e85f535a7fd14796eae6c9f9286751c5ac1b31 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Wed, 13 Dec 2023 19:48:30 +0100 Subject: [PATCH 04/23] "Gracefully" exit when receiving sigint --- Dockerfile | 1 + src/index.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9ef0693..32d0e7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,3 +17,4 @@ RUN npx tsc # Run the app CMD ["node", "dist/src/index.js"] +STOPSIGNAL SIGINT diff --git a/src/index.ts b/src/index.ts index 27e5c45..e0506ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,4 +63,17 @@ discord.on("messageCreate", message => { return queueRequest(message); }); -if (require.main === module) void discord.login(config.tokens.Discord); +if (require.main === module) { + void discord.login(config.tokens.Discord); + process.on("SIGINT", () => { + console.log("got SIGINT, exiting"); + //FIXME: finish executing requests then exit + discord.destroy() + .then(() => process.exit()) + .catch((e) => { + console.error("Failed to gracefully exit"); + console.error(e); + process.exit(); + }); + }); +} From 74359067d0e31c7a5ad80a72f01a219407e33ca7 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Wed, 13 Dec 2023 21:08:09 +0100 Subject: [PATCH 05/23] functionManager: don't return empty function array for openai openai doesn't accept empty function parameter, it was discovered after I removed the testing buildin function --- src/execution.ts | 2 +- src/funcitonManager.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/execution.ts b/src/execution.ts index 3b8e2fd..44e5710 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -246,7 +246,7 @@ async function executeFromQueue(channel: string) { ...config.chatCompletionParams, messages: OpenAImessages, // FIXME: don't use new instance of FunctionManager - functions: new FunctionManager().getFunctions(), + functions: new FunctionManager().getFunctionsForOpenAi(), }); logUsedTokens(answer, message, ++functionRanCounter); diff --git a/src/funcitonManager.ts b/src/funcitonManager.ts index 41e35c1..11b1bac 100644 --- a/src/funcitonManager.ts +++ b/src/funcitonManager.ts @@ -62,6 +62,11 @@ export default class FunctionManager { return rvalue; } + public getFunctionsForOpenAi(): ChatCompletionFunctions[] | undefined { + const rvalue = this.getFunctions(); + return rvalue.length > 0 ? rvalue : undefined; + } + public handleFunction(request: ChatCompletionFunctionCall): ChatCompletionMessageParam { // eslint-disable-next-line @typescript-eslint/no-explicit-any let parsedArguments: any; From dc01146ee82e05f5656ae7cb54d074d21ba0be6a Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Mon, 25 Dec 2023 16:09:35 +0100 Subject: [PATCH 06/23] ignore @everyone mentions --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index e0506ab..a565a3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,7 +58,7 @@ discord.on("ready", event => { discord.on("messageCreate", message => { if (message.author.bot) return; - if (!message.mentions.has(message.client.user)) return; + if (!message.mentions.has(message.client.user, { ignoreEveryone: true })) return; return queueRequest(message); }); From 91232e99a7b251aea3b013d4461550b8e223ce6e Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Tue, 23 Apr 2024 16:47:08 +0200 Subject: [PATCH 07/23] Update dependencies --- package-lock.json | 510 +++++++++++++++++++++++++--------------------- package.json | 16 +- shell.nix | 25 +-- 3 files changed, 293 insertions(+), 258 deletions(-) diff --git a/package-lock.json b/package-lock.json index f35449d..85b46e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,21 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@prisma/client": "5.7.0", - "discord.js": "^14.8.0", + "@prisma/client": "5.13.0", + "discord.js": "14.14.1", "fold-to-ascii": "^5.0.1", "gpt-3-encoder": "^1.1.4", - "openai": "^4.10.0", + "openai": "^4.38.3", "require-directory": "^2.1.1", - "typescript": "^5.1.6" + "typescript": "^5.4.5" }, "devDependencies": { "@types/fold-to-ascii": "^5.0.0", "@types/require-directory": "^2.1.2", - "@typescript-eslint/eslint-plugin": "^6.2.0", - "@typescript-eslint/parser": "^6.2.0", - "eslint": "^8.46.0", - "prisma": "5.7.0" + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "eslint": "^8.57.0", + "prisma": "5.13.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -180,37 +180,81 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "engines": { "node": ">=14" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -225,9 +269,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "node_modules/@nodelib/fs.scandir": { @@ -266,9 +310,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.0.tgz", - "integrity": "sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.13.0.tgz", + "integrity": "sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -283,70 +327,69 @@ } }, "node_modules/@prisma/debug": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz", - "integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.13.0.tgz", + "integrity": "sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz", - "integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.13.0.tgz", + "integrity": "sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.7.0", - "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", - "@prisma/fetch-engine": "5.7.0", - "@prisma/get-platform": "5.7.0" + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/fetch-engine": "5.13.0", + "@prisma/get-platform": "5.13.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz", - "integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==", + "version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b.tgz", + "integrity": "sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz", - "integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.13.0.tgz", + "integrity": "sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.7.0", - "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", - "@prisma/get-platform": "5.7.0" + "@prisma/debug": "5.13.0", + "@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b", + "@prisma/get-platform": "5.13.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz", - "integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.13.0.tgz", + "integrity": "sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.7.0" + "@prisma/debug": "5.13.0" } }, "node_modules/@sapphire/async-queue": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.1.tgz", - "integrity": "sha512-1RdpsmDQR/aWfp8oJzPtn4dNQrbpqSL5PIA0uAB/XwerPXUf994Ug1au1e7uGcD7ei8/F63UDjr5GWps1g/HxQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.2.tgz", + "integrity": "sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/shapeshift": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.4.tgz", - "integrity": "sha512-SiOoCBmm8O7QuadLJnX4V0tAkhC54NIOZJtmvw+5zwnHaiulGkjY02wxCuK8Gf4V540ILmGz+UulC0U8mrOZjg==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.7.tgz", + "integrity": "sha512-4It2mxPSr4OGn4HSQWGmhFMsNFGfFVhWeRPCRwbH972Ek2pzfGRZtb0pJ4Ze6oIzcyh2jw7nUDa6qGlWofgd9g==", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" }, "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" + "node": ">=v16" } }, "node_modules/@sapphire/snowflake": { @@ -371,17 +414,17 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", - "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", "dependencies": { "@types/node": "*", "form-data": "^4.0.0" @@ -397,9 +440,9 @@ } }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/ws": { @@ -411,33 +454,33 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz", - "integrity": "sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.14.0", - "@typescript-eslint/type-utils": "6.14.0", - "@typescript-eslint/utils": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -446,26 +489,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", - "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.14.0", - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/typescript-estree": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -474,16 +517,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz", - "integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0" + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -491,25 +534,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz", - "integrity": "sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.14.0", - "@typescript-eslint/utils": "6.14.0", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -518,12 +561,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz", - "integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -531,21 +574,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz", - "integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -558,41 +602,41 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.14.0.tgz", - "integrity": "sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.14.0", - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/typescript-estree": "6.14.0", - "semver": "^7.5.4" + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "semver": "^7.6.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz", - "integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.14.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.7.1", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -626,9 +670,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -723,19 +767,13 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" - }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -775,14 +813,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -832,14 +862,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -871,15 +893,6 @@ "node": ">=0.4.0" } }, - "node_modules/digest-fetch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", - "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", - "dependencies": { - "base-64": "^0.1.0", - "md5": "^2.3.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -946,16 +959,16 @@ } }, "node_modules/eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -1028,6 +1041,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -1141,9 +1176,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -1204,9 +1239,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/fold-to-ascii": { @@ -1293,6 +1328,28 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -1357,9 +1414,9 @@ } }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -1406,11 +1463,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1552,19 +1604,9 @@ } }, "node_modules/magic-bytes.js": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.7.0.tgz", - "integrity": "sha512-YzVU2+/hrjwx8xcgAw+ffNq3jkactpj+f1iSL4LonrFKhvnwDzHSqtFdk/MMRP53y9ScouJ7cKEnqYsJwsHoYA==" - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", + "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==" }, "node_modules/merge2": { "version": "1.4.1", @@ -1608,15 +1650,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { @@ -1677,15 +1722,14 @@ } }, "node_modules/openai": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.21.0.tgz", - "integrity": "sha512-HT0Jm2iGI5+Maq7Da/DPTeIAxNvpa5pamkhlNnJJAqJgVjaFDvMUBjGhFJoVohkYWwZjM9oSyfSC0eoVNPioaQ==", + "version": "4.38.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.38.3.tgz", + "integrity": "sha512-mIL9WtrFNOanpx98mJ+X/wkoepcxdqqu0noWFoNQHl/yODQ47YM7NEYda7qp8JfjqpLFVxY9mQhshoS/Fqac0A==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", - "digest-fetch": "^1.3.0", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7", @@ -1696,9 +1740,9 @@ } }, "node_modules/openai/node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "version": "18.19.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", + "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", "dependencies": { "undici-types": "~5.26.4" } @@ -1820,13 +1864,13 @@ } }, "node_modules/prisma": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz", - "integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.13.0.tgz", + "integrity": "sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.7.0" + "@prisma/engines": "5.13.0" }, "bin": { "prisma": "build/index.js" @@ -1930,9 +1974,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2034,21 +2078,21 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" } }, "node_modules/ts-mixer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", - "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" }, "node_modules/tslib": { "version": "2.6.2", @@ -2080,9 +2124,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2117,9 +2161,9 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "engines": { "node": ">= 8" } diff --git a/package.json b/package.json index 4af91ed..af0fdf1 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,20 @@ "author": "Wroclaw", "license": "MIT", "dependencies": { - "@prisma/client": "5.7.0", - "discord.js": "^14.8.0", + "@prisma/client": "5.13.0", + "discord.js": "14.14.1", "fold-to-ascii": "^5.0.1", "gpt-3-encoder": "^1.1.4", - "openai": "^4.10.0", + "openai": "^4.38.3", "require-directory": "^2.1.1", - "typescript": "^5.1.6" + "typescript": "^5.4.5" }, "devDependencies": { "@types/fold-to-ascii": "^5.0.0", "@types/require-directory": "^2.1.2", - "@typescript-eslint/eslint-plugin": "^6.2.0", - "@typescript-eslint/parser": "^6.2.0", - "eslint": "^8.46.0", - "prisma": "5.7.0" + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "eslint": "^8.57.0", + "prisma": "5.13.0" } } diff --git a/shell.nix b/shell.nix index b520ade..1759471 100644 --- a/shell.nix +++ b/shell.nix @@ -1,17 +1,16 @@ -{ pkgs ? import {} }: +{ pkgs ? import {} +, unstable ? import {} +}: let - # Updating this package will force an update for nodePackages.prisma. The - # version of prisma-engines and nodePackages.prisma must be the same for them to - # function correctly. - prisma-version = "5.7.0"; + prisma-version = "5.13.0"; prisma-src = pkgs.fetchFromGitHub { owner = "prisma"; repo = "prisma-engines"; rev = prisma-version; - hash = "sha256-gZEz0UtgNwumsZbweAyx3TOVHJshpBigc9pzWN7Gb/A="; + hash = "sha256-8LC2RV3FRr1F0TZxQNxvvEoTyhKusgzB5omlxLAnHG0="; }; - new-prisma-engines = pkgs.rustPlatform.buildRustPackage { + new-prisma-engines = unstable.rustPlatform.buildRustPackage { pname = "prisma-engines"; version = prisma-version; @@ -30,10 +29,10 @@ let cargoLock = { lockFile = "${prisma-src}/Cargo.lock"; outputHashes = { - "cuid-1.3.2" = "sha256-ZihFrLerEIOdbJggaBbByRbC1sZRvF4M0LN2albB7vA="; + "cuid-1.3.2" = "sha256-qBu1k/dJiA6rWBwk4nOOqouIneD9h2TTBT8tvs0TDfA="; "barrel-0.6.6-alpha.0" = "sha256-USh0lQ1z+3Spgc69bRFySUzhuY79qprLlEExTmYWFN8="; "graphql-parser-0.3.0" = "sha256-0ZAsj2mW6fCLhwTETucjbu4rPNzfbNiHu2wVTBlTNe4="; - "mysql_async-0.31.3" = "sha256-QIO9s0Upc0/1W7ux1RNJNGKqzO4gB4gMV3NoakAbxkQ="; + "mysql_async-0.31.3" = "sha256-2wOupQ/LFV9pUifqBLwTvA0tySv+XWbxHiqs7iTzvvg="; "postgres-native-tls-0.5.0" = "sha256-UYPsxhCkXXWk8yPbqjNS0illwjS5mVm3Z/jFwpVwqfw="; }; }; @@ -62,14 +61,6 @@ let # Tests are long to compile doCheck = false; - - # meta = with lib; { - # description = "A collection of engines that power the core stack for Prisma"; - # homepage = "https://www.prisma.io/"; - # license = licenses.asl20; - # platforms = platforms.unix; - # maintainers = with maintainers; [ pimeys tomhoule ivan aqrln ]; - # }; }; in pkgs.mkShell { From 0e5c8d22ccb3623060f541b55f26ef800f04d8e7 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Tue, 23 Apr 2024 17:46:24 +0200 Subject: [PATCH 08/23] functionManager: retrofit for tool calls api --- src/execution.ts | 10 +++--- src/funcitonManager.ts | 75 ++++++++++++++++++++++++++++-------------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 44e5710..d68f7c0 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -246,7 +246,7 @@ async function executeFromQueue(channel: string) { ...config.chatCompletionParams, messages: OpenAImessages, // FIXME: don't use new instance of FunctionManager - functions: new FunctionManager().getFunctionsForOpenAi(), + tools: new FunctionManager().getToolsForOpenAi(), }); logUsedTokens(answer, message, ++functionRanCounter); @@ -254,13 +254,11 @@ async function executeFromQueue(channel: string) { generatedMessage = answer.choices[0].message; if (!generatedMessage) throw new Error("Empty message received"); - // handle function calls - if (generatedMessage.function_call) { + // handle tool calls + if (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0) { OpenAImessages.push(generatedMessage); // FIXME: don't use new instance of FunctionManager - OpenAImessages.push( - new FunctionManager().handleFunction(generatedMessage.function_call) - ); + OpenAImessages.push(...(await new FunctionManager().handleToolCalls(generatedMessage.tool_calls))); } } while (generatedMessage.function_call); diff --git a/src/funcitonManager.ts b/src/funcitonManager.ts index 11b1bac..6ab27d0 100644 --- a/src/funcitonManager.ts +++ b/src/funcitonManager.ts @@ -1,4 +1,9 @@ -import { ChatCompletionCreateParams, ChatCompletionMessage, ChatCompletionMessageParam } from "openai/resources/chat"; +import { FunctionDefinition } from "openai/resources"; +import { + ChatCompletionMessageParam +, ChatCompletionMessageToolCall +, ChatCompletionTool +} from "openai/resources/chat"; type parameterMap = { string: string, @@ -11,8 +16,10 @@ type OpenAIFunctionRequestData = { [name in keyof T]: T[name]; }; -type ChatCompletionFunctions = ChatCompletionCreateParams.Function; -type ChatCompletionFunctionCall = ChatCompletionMessage.FunctionCall; +type ChatCompletionToolDefinition = ChatCompletionTool; +type ChatCompletionToolCall = ChatCompletionMessageToolCall; + +type ChatCompletionFunctionDefinition = FunctionDefinition; /** * Represents the function that can be ran by the OpenAI model @@ -33,7 +40,7 @@ export interface OpenAIFunction { } export abstract class OpenAIFunction { - getSettings(): ChatCompletionFunctions { + getSettings(): ChatCompletionFunctionDefinition { return { name: this.name, description: this.description, @@ -41,7 +48,7 @@ export abstract class OpenAIFunction { }; } - abstract execute(data: OpenAIFunctionRequestData): string; + abstract execute(data: OpenAIFunctionRequestData): Promise; } /* @@ -54,47 +61,67 @@ export default class FunctionManager { // TODO: import functions from functions directory } - public getFunctions(): ChatCompletionFunctions[] { - const rvalue: ChatCompletionFunctions[] = []; + public getTools(): ChatCompletionToolDefinition[] { + const rvalue: ChatCompletionToolDefinition[] = []; for (const [, value] of this.store) { - rvalue.push(value.getSettings()); + rvalue.push({type: "function", function: value.getSettings()}); } return rvalue; } - public getFunctionsForOpenAi(): ChatCompletionFunctions[] | undefined { - const rvalue = this.getFunctions(); + public getToolsForOpenAi(): ChatCompletionTool[] | undefined { + const rvalue = this.getTools(); return rvalue.length > 0 ? rvalue : undefined; } - public handleFunction(request: ChatCompletionFunctionCall): ChatCompletionMessageParam { + public handleFunction(request: ChatCompletionToolCall): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any let parsedArguments: any; - const functionToRun = this.store.get(request.name ?? ""); + const functionToRun = this.store.get(request.function.name); // check if the function is registered if (!functionToRun) { - return { + return Promise.resolve({ role: "system", - content: "Only use functions that were provided to you", - }; + content: `Only use functions that were provided to you (response for tool call ID: ${request.id})`, + }); } try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - parsedArguments = JSON.parse(request.arguments ?? ""); + parsedArguments = JSON.parse(request.function.arguments); } catch (e) { - console.error("Function arguments raw: " + request.arguments); - throw new Error(`Failed to parse the function JSON arguments when running function [${request.name}]`, {cause: e}); + console.error("Function arguments raw: " + request.function.arguments); + throw new Error(`Failed to parse the function JSON arguments when running function [${request.function.name}]`, {cause: e}); } // FIXME: Verify if the parsedArguments matches the requested function argument declaration. - return { - role: "function", - name: request.name, - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - content: functionToRun.execute(parsedArguments), - }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return functionToRun.execute(parsedArguments).then(content => { + return { + role: "tool", + tool_call_id: request.id, + content: content, + }; + }); + } + + public handleToolCall(call: ChatCompletionToolCall): Promise { + if (call.type === "function") { + return this.handleFunction(call); + } + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unsupported tool call type: ${call.type || "never"}`); + } + + public handleToolCalls(calls: ChatCompletionToolCall[]) { + const rvalue: Promise[] = []; + for (const call of calls) { + if (call.type === "function") { + rvalue.push(this.handleToolCall(call)); + } + } + return Promise.all(rvalue); } } From 63cb52e7f47e9c6da3e1342369d7c5101f7ed633 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Tue, 23 Apr 2024 20:56:55 +0200 Subject: [PATCH 09/23] Command: Add integration and context types While discord api supports that, discord api types, which discord.js depends on does not. --- scripts/pushCommands.ts | 18 ++++++++++++++++- src/command.ts | 44 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/scripts/pushCommands.ts b/scripts/pushCommands.ts index 25ee04e..3c5f093 100644 --- a/scripts/pushCommands.ts +++ b/scripts/pushCommands.ts @@ -4,13 +4,25 @@ import { REST, RESTGetAPIOAuth2CurrentApplicationResult, RESTPostAPIApplicationC import { config } from "../src/index"; import requireDirectory from "require-directory"; -import Command from "../src/command"; +import + Command +, { + ApplicationIntegrationType +, InteractionContextTypes +, InteractionTypeMap +} from "../src/command"; const post: RESTPostAPIApplicationCommandsJSONBody[] = []; const guildId = process.argv.slice(2)[0]; const importedCommands = requireDirectory(module, "../src/commands"); +function isGuildCommand(command: Command): boolean { + // guild Commmand is when it's a guild install and context is guild (and these are defaults if not provided) + return (command.integration_types?.includes(ApplicationIntegrationType.Guild_Install) ?? true) + && (command.contexts?.includes(InteractionContextTypes.Guild) ?? true); +} + for (const obj in importedCommands) { try { const allExports = importedCommands[obj] as {default: unknown}; @@ -19,6 +31,10 @@ for (const obj in importedCommands) { // @ts-expect-error const constructedExport = new defaultExport() as unknown; if (!(constructedExport instanceof Command)) throw new Error(`${obj}'s default does not extends Command`); + if (guildId && guildId !== "" && isGuildCommand(constructedExport as Command)) { + console.log(`Skipping ${obj} because it's not a guild command`); + continue; + } post.push(constructedExport.toRESTPostApplicationCommands()); } catch (e) { console.error(e); diff --git a/src/command.ts b/src/command.ts index d053e72..35be22a 100644 --- a/src/command.ts +++ b/src/command.ts @@ -2,12 +2,47 @@ import { AutocompleteInteraction, PermissionsBitField } from "discord.js"; import { RESTPostAPIApplicationCommandsJSONBody } from "discord.js"; import { APIApplicationCommandOption, ApplicationCommandType, ChatInputCommandInteraction, LocalizationMap, MessageInteraction, PermissionResolvable, UserSelectMenuInteraction } from "discord.js"; -type InteractionTypeMap = { +export type InteractionTypeMap = { + // [CommandType]: [Interaction, Description] [ApplicationCommandType.ChatInput]: [ChatInputCommandInteraction, string]; [ApplicationCommandType.Message]: [MessageInteraction, never]; [ApplicationCommandType.User]: [UserSelectMenuInteraction, never]; }; +// TODO: At time of coding, Discord api types doesn't support user installations of bot/application yet +// replace this with the types from the discord api types when it's available +/** + * https://discord.com/developers/docs/resources/application#application-object-application-integration-types + */ +export enum ApplicationIntegrationType { + Guild_Install = 0, + User_Install = 1, +} + +/** + * https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types + */ +export enum InteractionContextTypes { + Guild = 0, + BotDM = 1, + PrivateChannel = 2, +} + +/** + * https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure + */ +export type FutureRESTPostAPIApplicationCommandsJSONBody = + RESTPostAPIApplicationCommandsJSONBody +& { + /** + * @deprecated use contexts instead + */ + dm_permission?: boolean; + integration_types?: ApplicationIntegrationType[]; + contexts?: InteractionContextTypes[]; +}; + + interface Command { readonly name: string; readonly name_localizations?: LocalizationMap; @@ -17,7 +52,10 @@ interface Command { @@ -25,7 +63,7 @@ abstract class Command; - toRESTPostApplicationCommands(): RESTPostAPIApplicationCommandsJSONBody { + toRESTPostApplicationCommands(): FutureRESTPostAPIApplicationCommandsJSONBody { return { name: this.name, name_localizations: this.name_localizations, @@ -36,6 +74,8 @@ abstract class Command Date: Tue, 23 Apr 2024 21:10:52 +0200 Subject: [PATCH 10/23] Allow chatting with bot through direct messages Added new command to make the bot see direct messages, because by default these are not visible, unless bot opens direct message with the user. --- src/commands/listen.ts | 26 ++++++++++++++++++++++++++ src/index.ts | 5 ++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/commands/listen.ts diff --git a/src/commands/listen.ts b/src/commands/listen.ts new file mode 100644 index 0000000..3f1c39d --- /dev/null +++ b/src/commands/listen.ts @@ -0,0 +1,26 @@ +import {ApplicationCommandType, ChatInputCommandInteraction } from "discord.js"; + +import Command, { ApplicationIntegrationType, InteractionContextTypes } from "../command"; + +export default class Listen extends Command implements Command { + // This command exists because Discord bots don't receive direct messages + // unless they explicitly open a DM channel with the user + name = "listen"; + description = "Makes the bot listen on your direct messages"; + type = ApplicationCommandType.ChatInput; + + options = []; + integration_types = [ + ApplicationIntegrationType.Guild_Install, + ApplicationIntegrationType.User_Install + ]; + contexts = [InteractionContextTypes.BotDM]; + + async execute(interaction: ChatInputCommandInteraction) { + await interaction.user.createDM(); + await interaction.reply({ + content: "I'm now listening to your direct messages", + ephemeral: true, + }); + } +} diff --git a/src/index.ts b/src/index.ts index a565a3f..eb7a9f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ const discord = new DiscordApi.Client({ intents: [ DiscordApi.GatewayIntentBits.Guilds, DiscordApi.GatewayIntentBits.GuildMessages, + DiscordApi.GatewayIntentBits.DirectMessages, DiscordApi.GatewayIntentBits.MessageContent, ] }); @@ -58,7 +59,9 @@ discord.on("ready", event => { discord.on("messageCreate", message => { if (message.author.bot) return; - if (!message.mentions.has(message.client.user, { ignoreEveryone: true })) return; + if (!message.channel.isDMBased()) { + if (!message.mentions.has(message.client.user, { ignoreEveryone: true })) return; + } return queueRequest(message); }); From d9bee2dcf2e1e4e42c0fbb0f518efebe55799d39 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Tue, 23 Apr 2024 21:11:40 +0200 Subject: [PATCH 11/23] check-quota: define integration types and context to any --- src/commands/check-quota.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/commands/check-quota.ts b/src/commands/check-quota.ts index 3a2f5ef..4cc61d4 100644 --- a/src/commands/check-quota.ts +++ b/src/commands/check-quota.ts @@ -1,6 +1,10 @@ import { ApplicationCommandType, ChatInputCommandInteraction, APIApplicationCommandOption, ApplicationCommandOptionType, APIEmbedField } from "discord.js"; -import Command from "../command"; +import + Command +,{ApplicationIntegrationType +, InteractionContextTypes +} from "../command"; import { config } from "../index"; export default class MyLimit extends Command implements Command { @@ -21,6 +25,15 @@ export default class MyLimit extends Command implements Command { required: false, } ]; + integration_types = [ + ApplicationIntegrationType.Guild_Install, + ApplicationIntegrationType.User_Install + ]; + contexts = [ + InteractionContextTypes.Guild, + InteractionContextTypes.BotDM, + InteractionContextTypes.PrivateChannel + ]; async execute(interaction: ChatInputCommandInteraction) { let recoveryFor = interaction.options.getInteger("recovery-for", false); From b567e13f2a8b4bdbbb7bebe57a95196e5f93f186 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Wed, 24 Apr 2024 02:27:31 +0200 Subject: [PATCH 12/23] functionManager: use json-schema-to-ts to derive arguments for OpenAIFunctions --- package-lock.json | 38 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/funcitonManager.ts | 37 +++++++++++++------------------------ 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85b46e3..ec40cc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", + "json-schema-to-ts": "^3.0.1", "prisma": "5.13.0" } }, @@ -35,6 +36,18 @@ "node": ">=0.10.0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discordjs/builders": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz", @@ -1526,6 +1539,19 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-schema-to-ts": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.0.1.tgz", + "integrity": "sha512-ANphQxnKbzLWPeYDmdoci8C9g9ttpfMx8etTlJJ8UCEmNXH9jxGkn3AAbMe+lR4N5OG/01nYxPrDyugLdsRt+A==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^1.2.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1908,6 +1934,12 @@ } ] }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2077,6 +2109,12 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-algebra": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz", + "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==", + "dev": true + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", diff --git a/package.json b/package.json index af0fdf1..870f9c7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", + "json-schema-to-ts": "^3.0.1", "prisma": "5.13.0" } } diff --git a/src/funcitonManager.ts b/src/funcitonManager.ts index 6ab27d0..4860c0c 100644 --- a/src/funcitonManager.ts +++ b/src/funcitonManager.ts @@ -4,17 +4,11 @@ import { , ChatCompletionMessageToolCall , ChatCompletionTool } from "openai/resources/chat"; +import { type FromSchema, type JSONSchema } from "json-schema-to-ts"; -type parameterMap = { - string: string, - number: number, -}; - -type nameTypeMap = {[name: string]: keyof parameterMap} | Record; - -type OpenAIFunctionRequestData = { - [name in keyof T]: T[name]; -}; +type OpenAIFunctionRequestData = (JSONSchema & { + type: "object" +}); type ChatCompletionToolDefinition = ChatCompletionTool; type ChatCompletionToolCall = ChatCompletionMessageToolCall; @@ -24,31 +18,26 @@ type ChatCompletionFunctionDefinition = FunctionDefinition; /** * Represents the function that can be ran by the OpenAI model */ -export interface OpenAIFunction { +export interface OpenAIFunction< + T extends Readonly = Readonly +> { name: string, description?: string, - parameters: { - type: "object", - properties: T extends Record ? Record : { - [name in T[string]]: { - type: T[name], - description?: string, - } - }, - required?: Array, - }, + parameters: T, } -export abstract class OpenAIFunction { +export abstract class OpenAIFunction< + T extends Readonly = Readonly +> { getSettings(): ChatCompletionFunctionDefinition { return { name: this.name, description: this.description, - parameters: this.parameters, + parameters: this.parameters as Record, }; } - abstract execute(data: OpenAIFunctionRequestData): Promise; + abstract execute(data: FromSchema): Promise; } /* From 370b7623b5a6f189de96f4f877ca59216c784b4a Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Wed, 24 Apr 2024 02:38:52 +0200 Subject: [PATCH 13/23] Dockerfile: chmod dist to 777 for config.ts compilation --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dockerfile b/Dockerfile index 32d0e7f..52b9fb1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,11 @@ COPY src ./src COPY scripts ./scripts RUN npx tsc +# Permissions for dist directory, +# so config.ts can be compiled there during runtime +# regardless of the user of the container +RUN chmod 777 dist + # Run the app CMD ["node", "dist/src/index.js"] STOPSIGNAL SIGINT From 67a6e4d4864dd699dcfe14fb7634662d6b03fd2f Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Thu, 25 Apr 2024 01:10:30 +0200 Subject: [PATCH 14/23] execution+configDefault: retrofit for tooll_calls api --- src/configDefault.ts | 2 +- src/execution.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/configDefault.ts b/src/configDefault.ts index 12ce515..620a363 100644 --- a/src/configDefault.ts +++ b/src/configDefault.ts @@ -16,7 +16,7 @@ export interface IConfigRequired { /** Messages to append at the start of every chat history when sending to API */ systemPrompt(context: Message): OpenAIMessage[]; /** OpenAI model config */ - readonly chatCompletionParams: Omit; + readonly chatCompletionParams: Omit; /** Limits for message selection */ readonly readLimits: { /** Maximum message age to include (in miliseconds) */ diff --git a/src/execution.ts b/src/execution.ts index d68f7c0..1cfcaa8 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -180,10 +180,13 @@ function logUsedTokens( functionRan: number, ) { const usage = answer.usage; - const functionName = answer.choices[0].message?.function_call?.name; + const functionNames = + answer.choices[0].message.tool_calls?.map( + v => v.type === "function" ? v.function.name : `[unknown type]` + ); if (usage !== undefined) { const channelName: string = !message.channel.isDMBased() ? `${message.channel.name} (${message.guild?.name})` : `@${getAuthor(message).tag}`; - console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in #${channelName}${functionName ? " [Function: " + functionName + "]" : ""}`); + console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in #${channelName}${functionNames && functionNames.length > 0 ? " [Tools: " + functionNames.join(", ") + "]" : ""}`); database.usage.create({ data: { @@ -193,8 +196,8 @@ function logUsedTokens( guild: message.guildId ? BigInt(message.guildId) : null, usageRequest: usage.prompt_tokens, usageResponse: usage.completion_tokens, - functionName: functionName ?? null, - functionRan: functionName ? functionRan : 0, + functionName: functionNames?.join(", ") ?? null, + functionRan: functionNames ? functionRan : 0, } }).catch((e => { console.error("Failed to push to a database"); @@ -249,6 +252,7 @@ async function executeFromQueue(channel: string) { tools: new FunctionManager().getToolsForOpenAi(), }); + functionRanCounter += answer.choices[0].message?.tool_calls?.length ?? 0; logUsedTokens(answer, message, ++functionRanCounter); generatedMessage = answer.choices[0].message; @@ -260,7 +264,7 @@ async function executeFromQueue(channel: string) { // FIXME: don't use new instance of FunctionManager OpenAImessages.push(...(await new FunctionManager().handleToolCalls(generatedMessage.tool_calls))); } - } while (generatedMessage.function_call); + } while (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0); channelQueue.stopTyping(); From d3567c36070b0c3443d24ecb2182b6f97e8d1b81 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 04:02:09 +0200 Subject: [PATCH 15/23] execution: handle undefined message in logUsedTokens --- src/execution.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 1cfcaa8..63679fa 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -172,12 +172,12 @@ export async function queueRequest(request: apiRequest) { * Logs used tokens to the terminal and to the database * @param answer the response that OpenAI returned * @param message the message that initiated the execution - * @param functionRan counter of how many function have been ran + * @param functionRan counter of how many function have been ran (to distinct records in database) */ function logUsedTokens( answer: ChatCompletion, - message: RequestMessage, - functionRan: number, + message: RequestMessage | undefined = undefined, + functionRan: number = 0, ) { const usage = answer.usage; const functionNames = @@ -185,6 +185,12 @@ function logUsedTokens( v => v.type === "function" ? v.function.name : `[unknown type]` ); if (usage !== undefined) { + if (!message) { + // log usage to stdout even if we can't store it in database + console.warn(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens from unknown call`); + // it doesn't make sense to store usage in database if we don't know where it came from + return; + } const channelName: string = !message.channel.isDMBased() ? `${message.channel.name} (${message.guild?.name})` : `@${getAuthor(message).tag}`; console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in #${channelName}${functionNames && functionNames.length > 0 ? " [Tools: " + functionNames.join(", ") + "]" : ""}`); @@ -197,7 +203,7 @@ function logUsedTokens( usageRequest: usage.prompt_tokens, usageResponse: usage.completion_tokens, functionName: functionNames?.join(", ") ?? null, - functionRan: functionNames ? functionRan : 0, + functionRan: functionRan, } }).catch((e => { console.error("Failed to push to a database"); From 482f72a4d12f85815f99eb0de89f132eb67185b3 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 04:03:34 +0200 Subject: [PATCH 16/23] execution: factor out chat completion process --- src/execution.ts | 65 +++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 63679fa..4010b51 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -219,7 +219,6 @@ function logUsedTokens( async function executeFromQueue(channel: string) { const channelQueue = channelsRunning.get(channel) as ChannelsRunningValue; const message = channelQueue.at(0) as RequestMessage; - let functionRanCounter = 0; let OpenAImessages: ChatCompletionMessageParam[] = []; // ignore if we can't even send anything to reply @@ -247,30 +246,7 @@ async function executeFromQueue(channel: string) { }); OpenAImessages = toOpenAIMessages(messages.values()); - let generatedMessage: ChatCompletionMessage | undefined = undefined; - let answer: Awaited>; - - do { - answer = await openai.chat.completions.create({ - ...config.chatCompletionParams, - messages: OpenAImessages, - // FIXME: don't use new instance of FunctionManager - tools: new FunctionManager().getToolsForOpenAi(), - }); - - functionRanCounter += answer.choices[0].message?.tool_calls?.length ?? 0; - logUsedTokens(answer, message, ++functionRanCounter); - - generatedMessage = answer.choices[0].message; - if (!generatedMessage) throw new Error("Empty message received"); - - // handle tool calls - if (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0) { - OpenAImessages.push(generatedMessage); - // FIXME: don't use new instance of FunctionManager - OpenAImessages.push(...(await new FunctionManager().handleToolCalls(generatedMessage.tool_calls))); - } - } while (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0); + const answer = await executeChatCompletion(OpenAImessages, message); channelQueue.stopTyping(); @@ -349,3 +325,42 @@ async function executeFromQueue(channel: string) { else return executeFromQueue(channel); } + +/** + * Executes the chat completion process. + * + * @param OpenAImessages An array of ChatCompletionMessageParam objects representing the messages for chat completion. + * @param message An optional RequestMessage object representing the request message, used for logging. + * @returns A Promise that resolves to the answer from the chat completion process. + */ +async function executeChatCompletion( + OpenAImessages: ChatCompletionMessageParam[], + message: RequestMessage | undefined, +) { + let generatedMessage: ChatCompletionMessage | undefined = undefined; + let answer: Awaited>; + let functionRanCounter = 0; + + do { + answer = await openai.chat.completions.create({ + ...config.chatCompletionParams, + messages: OpenAImessages, + // FIXME: don't use new instance of FunctionManager + tools: new FunctionManager().getToolsForOpenAi(), + }); + + functionRanCounter += answer.choices[0].message?.tool_calls?.length ?? 0; + logUsedTokens(answer, message, ++functionRanCounter); + + generatedMessage = answer.choices[0].message; + if (!generatedMessage) throw new Error("Empty message received"); + + // handle tool calls + if (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0) { + OpenAImessages.push(generatedMessage); + // FIXME: don't use new instance of FunctionManager + OpenAImessages.push(...(await new FunctionManager().handleToolCalls(generatedMessage.tool_calls))); + } + } while (generatedMessage.tool_calls !== undefined && generatedMessage.tool_calls.length > 0); + return answer; +} From 2fab1b1b428fa9fcd9b8c02e18cdb3423e2abd51 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 05:17:32 +0200 Subject: [PATCH 17/23] execution: factor out replying code to it's own fuction --- src/execution.ts | 49 ++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 4010b51..a78131f 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -252,26 +252,7 @@ async function executeFromQueue(channel: string) { const answerContent = answer.choices[0].message?.content; - if (answerContent === null || answerContent === "") { - if (message instanceof DiscordApi.Message) message.react("😶").catch(() => {/* GRACEFAIL: It's okay if the bot won't reply */}); - } - else { - const answerMessagesContent :string[] = [""]; - for (const i of answerContent.split(/\n\n/)) { - if (answerMessagesContent[answerMessagesContent.length-1].length + i.length < 2000) { - answerMessagesContent[answerMessagesContent.length-1] += "\n\n" + i; - } - else { - answerMessagesContent.push(i); - } - } - - for (const i of answerMessagesContent) { - const response = requestReply(message, {content: i}, {allowedMentions: { repliedUser: false }}); - - await response.then(rval => Moderation.checkMessageNoReturn(rval)); - } - } + await replyInMultiMessage(answerContent, message); } catch (e) { let errorText: string = ""; channelQueue.stopTyping(); @@ -326,6 +307,34 @@ async function executeFromQueue(channel: string) { return executeFromQueue(channel); } +/** + * Replies to a message and splits to multiple messages if needed. + * @param answerContent - The content of the answer. + * @param message - The request message to reply to. + */ +async function replyInMultiMessage(answerContent: string | null, message: RequestMessage) { + if (answerContent === null || answerContent === "") { + if (message instanceof DiscordApi.Message) message.react("😶").catch(() => { }); + } + else { + const answerMessagesContent: string[] = [""]; + for (const i of answerContent.split(/\n\n/)) { + if (answerMessagesContent[answerMessagesContent.length - 1].length + i.length < 2000) { + answerMessagesContent[answerMessagesContent.length - 1] += "\n\n" + i; + } + else { + answerMessagesContent.push(i); + } + } + + for (const i of answerMessagesContent) { + const response = requestReply(message, { content: i }, { allowedMentions: { repliedUser: false } }); + + await response.then(rval => Moderation.checkMessageNoReturn(rval)); + } + } +} + /** * Executes the chat completion process. * From 9f5dfefb31448839826bb5dc7010cd2ee2ce2920 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 05:38:41 +0200 Subject: [PATCH 18/23] execution: support for requests that don't have channel set Interactions initiated outside of servers where bot is don't have channel assigned --- src/configDefault.ts | 4 ++-- src/execution.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/configDefault.ts b/src/configDefault.ts index 620a363..dcd4dd9 100644 --- a/src/configDefault.ts +++ b/src/configDefault.ts @@ -1,4 +1,3 @@ -import { Message } from "discord.js"; import { ChatCompletionMessageParam as OpenAIMessage, ChatCompletionCreateParamsNonStreaming as ChatCompletionRequestData, @@ -6,6 +5,7 @@ import { import IQuota from "./IQuota"; import MessageCount from "./quota/messageCount"; +import { apiRequest } from "./execution"; export interface IConfigRequired { /** Tokens to authentiate with */ @@ -14,7 +14,7 @@ export interface IConfigRequired { readonly OpenAI: string; }; /** Messages to append at the start of every chat history when sending to API */ - systemPrompt(context: Message): OpenAIMessage[]; + systemPrompt(context: apiRequest): OpenAIMessage[]; /** OpenAI model config */ readonly chatCompletionParams: Omit; /** Limits for message selection */ diff --git a/src/execution.ts b/src/execution.ts index a78131f..f715564 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -73,7 +73,7 @@ export function getAuthor(request: apiRequest) { * @returns Promise of the done action */ function requestReply( - request: RequestMessage, + request: apiRequest, message: DiscordApi.MessageReplyOptions & DiscordApi.InteractionReplyOptions, // TODO: add support for these below replyOptions: DiscordApi.MessageReplyOptions = {}, @@ -176,7 +176,7 @@ export async function queueRequest(request: apiRequest) { */ function logUsedTokens( answer: ChatCompletion, - message: RequestMessage | undefined = undefined, + message: apiRequest | undefined = undefined, functionRan: number = 0, ) { const usage = answer.usage; @@ -191,14 +191,17 @@ function logUsedTokens( // it doesn't make sense to store usage in database if we don't know where it came from return; } - const channelName: string = !message.channel.isDMBased() ? `${message.channel.name} (${message.guild?.name})` : `@${getAuthor(message).tag}`; - console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in #${channelName}${functionNames && functionNames.length > 0 ? " [Tools: " + functionNames.join(", ") + "]" : ""}`); + const channelName: string = !message.channel ? "[No channel]" + : !message.channel.isDMBased() ? `#${message.channel.name} (${message.guild?.name})` + : `#@${getAuthor(message).tag}` + ; + console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in ${channelName}${functionNames && functionNames.length > 0 ? " [Tools: " + functionNames.join(", ") + "]" : ""}`); database.usage.create({ data: { timestamp: message.createdAt, user: BigInt(getAuthor(message).id), - channel: BigInt(message.channelId), + channel: BigInt(message.channelId ?? 0), guild: message.guildId ? BigInt(message.guildId) : null, usageRequest: usage.prompt_tokens, usageResponse: usage.completion_tokens, @@ -312,7 +315,7 @@ async function executeFromQueue(channel: string) { * @param answerContent - The content of the answer. * @param message - The request message to reply to. */ -async function replyInMultiMessage(answerContent: string | null, message: RequestMessage) { +async function replyInMultiMessage(answerContent: string | null, message: apiRequest) { if (answerContent === null || answerContent === "") { if (message instanceof DiscordApi.Message) message.react("😶").catch(() => { }); } @@ -344,7 +347,7 @@ async function replyInMultiMessage(answerContent: string | null, message: Reques */ async function executeChatCompletion( OpenAImessages: ChatCompletionMessageParam[], - message: RequestMessage | undefined, + message: apiRequest | undefined, ) { let generatedMessage: ChatCompletionMessage | undefined = undefined; let answer: Awaited>; From 6efb6e587679cd678739a3f8ea1362f34c4b5e4f Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 05:41:07 +0200 Subject: [PATCH 19/23] commands/ask: create Allows to interact with the bot when bot is user installed in discord. --- src/commands/ask.ts | 74 +++++++++++++++++++++++++++++++++++++++++ src/execution.ts | 4 +-- src/toOpenAIMessages.ts | 2 +- 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/commands/ask.ts diff --git a/src/commands/ask.ts b/src/commands/ask.ts new file mode 100644 index 0000000..ea39da7 --- /dev/null +++ b/src/commands/ask.ts @@ -0,0 +1,74 @@ +import { + APIApplicationCommandOption +, ApplicationCommandOptionType +, ApplicationCommandType +, ChatInputCommandInteraction +} from "discord.js"; +import { ChatCompletionMessageParam } from "openai/resources"; + +import + Command +,{ApplicationIntegrationType +, InteractionContextTypes +} from "../command"; +import { config } from "../index"; +import { executeChatCompletion, replyInMultiMessage } from "../execution"; +import { formatName } from "../toOpenAIMessages"; + +export default class Ask extends Command implements Command { + name = "ask"; + description = "Promts the bot to reply to a single message without any history context"; + type = ApplicationCommandType.ChatInput; + options: APIApplicationCommandOption[] = [ + { + name: "content", + description: "The content of the prompt", + type: ApplicationCommandOptionType.String, + required: true, + }, + { + name: "ephemeral", + description: "if true, only you can see the response (default true)", + type: ApplicationCommandOptionType.Boolean, + required: false, + } + ]; + integration_types = [ + ApplicationIntegrationType.Guild_Install, + ApplicationIntegrationType.User_Install, + ]; + contexts = [ + InteractionContextTypes.Guild, + InteractionContextTypes.BotDM, + InteractionContextTypes.PrivateChannel, + ]; + + async execute(interaction: ChatInputCommandInteraction) { + const content = interaction.options.getString("content", true); + const ephemeral = interaction.options.getBoolean("ephemeral", false) ?? true; + + if (!interaction.channel && !interaction.channelId) { + console.error("No channel found in interaction"); + console.error(interaction); + await interaction.reply({ + content: "No channel found in interaction???", + ephemeral: true + }); + return; + } + + // TODO: check content in moderation API + + const messages: ChatCompletionMessageParam[] = [ + ...config.systemPrompt(interaction), + { role: "user", name: formatName(interaction.user.displayName), content } + ]; + + const [answer] = await Promise.all([ + executeChatCompletion(messages, interaction), + interaction.deferReply({ ephemeral }), + ]); + + await replyInMultiMessage(answer.choices[0].message.content, interaction); + } +} diff --git a/src/execution.ts b/src/execution.ts index f715564..579a435 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -315,7 +315,7 @@ async function executeFromQueue(channel: string) { * @param answerContent - The content of the answer. * @param message - The request message to reply to. */ -async function replyInMultiMessage(answerContent: string | null, message: apiRequest) { +export async function replyInMultiMessage(answerContent: string | null, message: apiRequest) { if (answerContent === null || answerContent === "") { if (message instanceof DiscordApi.Message) message.react("😶").catch(() => { }); } @@ -345,7 +345,7 @@ async function replyInMultiMessage(answerContent: string | null, message: apiReq * @param message An optional RequestMessage object representing the request message, used for logging. * @returns A Promise that resolves to the answer from the chat completion process. */ -async function executeChatCompletion( +export async function executeChatCompletion( OpenAImessages: ChatCompletionMessageParam[], message: apiRequest | undefined, ) { diff --git a/src/toOpenAIMessages.ts b/src/toOpenAIMessages.ts index f93d235..54ad00f 100644 --- a/src/toOpenAIMessages.ts +++ b/src/toOpenAIMessages.ts @@ -63,7 +63,7 @@ export function formatMessage(message: DiscordMessage): string { * @param name the name to format * @returns formatted name */ -function formatName(name: string): string { +export function formatName(name: string): string { // replace all characters to ascii equivelant return FoldToAscii.foldReplacing(name) // White spaces are not allowed From b1e464fe509413443dfcd6ce890afd7c3f43b329 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 06:14:57 +0200 Subject: [PATCH 20/23] Dockerfile: create db directory --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dockerfile b/Dockerfile index 52b9fb1..6386189 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,12 @@ RUN npx tsc # regardless of the user of the container RUN chmod 777 dist +# Create a db directory for sqlite database file +# it is required because in order to write to sqlite file +# the directory must be writable +RUN mkdir /db +RUN chmod 777 /db + # Run the app CMD ["node", "dist/src/index.js"] STOPSIGNAL SIGINT From 6f5f4251663152990d966c58755bbf97f6525dca Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 07:02:21 +0200 Subject: [PATCH 21/23] configDefault: add status options --- src/configDefault.ts | 68 ++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + 2 files changed, 69 insertions(+) diff --git a/src/configDefault.ts b/src/configDefault.ts index dcd4dd9..130b1a2 100644 --- a/src/configDefault.ts +++ b/src/configDefault.ts @@ -1,3 +1,8 @@ +import { + ActivityType +, type PresenceStatusData +, type PresenceData +} from "discord.js"; import { ChatCompletionMessageParam as OpenAIMessage, ChatCompletionCreateParamsNonStreaming as ChatCompletionRequestData, @@ -13,6 +18,8 @@ export interface IConfigRequired { readonly Discord: string; readonly OpenAI: string; }; + /** Discord bot status */ + readonly status: PresenceData /** Messages to append at the start of every chat history when sending to API */ systemPrompt(context: apiRequest): OpenAIMessage[]; /** OpenAI model config */ @@ -39,6 +46,10 @@ export default function newConfig(config?: IConfig): IConfigRequired { return { ...defaultConfig, ...config }; } +function isEnvDefined(key: string): boolean { + return process.env[key] !== undefined; +} + function envAsString(key: string): string | undefined { key = key.toLocaleUpperCase(); return process.env[key]; @@ -50,6 +61,53 @@ function envAsNumber(key: string): number | undefined { return !Number.isNaN(value) ? value : undefined; } +function envAsBoolean(key: string): boolean | undefined { + key = key.toUpperCase(); + const value = process.env[key]; + return !(value === "false" || value === "0"); +} + +function envAsActivityType(key: string): ActivityType | undefined { + key = key.toUpperCase(); + const value = process.env[key]?.toUpperCase(); + switch (value) { + case "0": + case "PLAYING": + return ActivityType.Playing; + case "1": + case "STREAMING": + return ActivityType.Streaming; + case "2": + case "LISTENING": + return ActivityType.Listening; + case "3": + case "WATCHING": + return ActivityType.Watching; + case "4": + case "CUSTOM": + return ActivityType.Custom; + case "5": + case "COMPETING": + return ActivityType.Competing; + default: + return undefined; + } +} + +function envAsPresenceStatusData(key: string): PresenceStatusData | undefined { + key = key.toUpperCase(); + const value = process.env[key]?.toLowerCase(); + switch (value) { + case "online": + case "idle": + case "dnd": + case "invisible": + return value; + default: + return undefined; + } +} + const defaultConfig: IConfigRequired = { tokens: { Discord: envAsString("TOKENS__DISCORD") ?? "", @@ -65,6 +123,16 @@ const defaultConfig: IConfigRequired = { }, ]; }, + status: { + activities: isEnvDefined("STATUS__NAME") ? [{ + name: envAsString("STATUS__NAME") as string, + type: envAsActivityType("STATUS__TYPE") ?? ActivityType.Custom, + state: envAsString("STATUS__STATE"), + url: envAsString("STATUS__URL"), + }] : undefined, + status: envAsPresenceStatusData("STATUS__STATUS"), + afk: envAsBoolean("STATUS__AFK"), + }, chatCompletionParams: { model: envAsString("CHAT_COMPLETION_PARAMS__MODEL") ?? "gpt-3.5-turbo", max_tokens: envAsNumber("CHAT_COMPLETION_PARAMS__MAX_TOKENS") ?? 384, diff --git a/src/index.ts b/src/index.ts index eb7a9f7..38468b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,6 +55,7 @@ interactionManager.bindClient(discord); discord.on("ready", event => { console.log(`Connected to Discord as ${event.user.tag} (${event.user.id})`); + event.user.setPresence(config.status); }); discord.on("messageCreate", message => { From c4edf55f656ce0bb8ffe928c3f15e99124bfc94b Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 08:07:46 +0200 Subject: [PATCH 22/23] commands/ask: enforce userLimit --- src/commands/ask.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/commands/ask.ts b/src/commands/ask.ts index ea39da7..d5a1d30 100644 --- a/src/commands/ask.ts +++ b/src/commands/ask.ts @@ -56,7 +56,23 @@ export default class Ask extends Command implements Command { }); return; } - + + const userLimit = await config.quota.checkUser(interaction.user, interaction); + + if (userLimit.used >= userLimit.quota) { + interaction.reply({ + + embeds: [{ + color: 0xff0000, + description: "You've used up your quota,\n" + userLimit.toString(), + }], + ephemeral: true, + }).catch(e => { + console.error("Failed to reply to user: ", e); + }); + return; + } + // TODO: check content in moderation API const messages: ChatCompletionMessageParam[] = [ From 1b402c791ce8f4963f10cb8d8ac6919893ca6b22 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Fri, 26 Apr 2024 08:13:29 +0200 Subject: [PATCH 23/23] execution: log channel ID to terminal if it's unknown channelId is set for commands executed outside of common guilds no changes for usage logging in database is required --- src/execution.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/execution.ts b/src/execution.ts index 579a435..8c550af 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -191,7 +191,8 @@ function logUsedTokens( // it doesn't make sense to store usage in database if we don't know where it came from return; } - const channelName: string = !message.channel ? "[No channel]" + const channelName: string = !message.channelId ? "[No channel]" + : !message.channel ? `[Unknown channel: ${message.channelId}]` : !message.channel.isDMBased() ? `#${message.channel.name} (${message.guild?.name})` : `#@${getAuthor(message).tag}` ;