diff --git a/.eslintrc b/.eslintrc index 7498105..ec8f2db 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,7 @@ "plugin:@typescript-eslint/recommended" ], "rules": { - "@typescript-eslint/no-explicit-any": 0 + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-namespace": 0 } } diff --git a/README.md b/README.md index 1f9ef74..7243f41 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,3 @@ These summaries are generated by OpenAI, and passed along to future bot prompts. - (At the moment, temporary data may be stored) - Add more documentation for others to fork this repo and set up their own chatbot. - -- Enable typescript-eslint/no-explicit-any and fix my embarrasing `any`'s. diff --git a/chatbot_engine/chatbot.ts b/chatbot_engine/chatbot.ts index ecc14b5..77244a6 100644 --- a/chatbot_engine/chatbot.ts +++ b/chatbot_engine/chatbot.ts @@ -1,3 +1,6 @@ +// @ts-ignore +import { Response } from "https://deno.land/std@0.183.0/http/server.ts"; + import { supabase, commands } from "./shared.ts"; import { @@ -11,11 +14,44 @@ import { import { callAPI as openAICallAPI } from "./openai.ts"; import { returnResponse, returnError } from "./response.ts"; +export interface ChatbotMessage { + role: string; + content: string; +} + +export interface ChatbotPayload { + bot_full_name: string; + data: string; + message?: { + sender_full_name?: string; + sender_id?: number; + stream_id?: number; + }; +} + +export interface ChatbotVariables { + user_name?: string; + user_id: number; + bot_id: string; + prompt?: string; + messages: ChatbotMessage[]; + summary_id?: string; + user_message?: ChatbotMessage; + bot_reply?: string; +} + +interface ChatbotOptions { + messages?: ChatbotMessage[]; + personality?: string; +} + // Clean up the prompt string, such as removing the bot call name. -export function getCleanPrompt(prompt: string, bot_name: string) { +export function getCleanPrompt(payload: ChatbotPayload) { + let prompt = payload?.data.toString(); + // If the bot has been initialized by calling it's name, // we'll remove it from the prompt. - const bot_user_name = `@**${bot_name}**`; + const bot_user_name = `@**${payload?.bot_full_name}**`; if (prompt.startsWith(bot_user_name)) { prompt = prompt.replace(bot_user_name, ""); @@ -45,7 +81,7 @@ async function getChatSummary(summary_id: string): Promise { } // Save a chat summary to Supabase DB. -async function setChatSummary(vars: any): Promise { +async function setChatSummary(vars: ChatbotVariables): Promise { if (vars.bot_reply) { vars.messages.push({ role: "assistant", @@ -76,18 +112,25 @@ async function setChatSummary(vars: any): Promise { } } -// Build the variables for processing the chatbot. -async function buildVariables(req: any, options: any): Promise { +export async function getPayload(req: Request): Promise { const { bot_full_name, data, message } = await req.json(); - const user_name = message?.sender_full_name; + return { bot_full_name, data, message }; +} + +// Build the variables for processing the chatbot. +function buildVariables( + payload: ChatbotPayload, + options: ChatbotOptions +): ChatbotVariables { + const user_name = payload?.message?.sender_full_name; // Removing any whitespaces, symbols etc. from the bot name. - const bot_id = bot_full_name.replace(/\W/g, ""); + const bot_id = payload?.bot_full_name.replace(/\W/g, ""); const messages = options?.messages ?? []; - if (Object.hasOwn(options, "personality")) { + if ("personality" in options) { messages.push({ role: "system", content: "You are taking the personality of " + options["personality"], @@ -100,43 +143,41 @@ async function buildVariables(req: any, options: any): Promise { }); // If the user has not given GDPR consent, break out with a prompt for consent. - const user_id = message?.sender_id; - - let prompt = data.toString(); + const user_id = payload?.message?.sender_id; - prompt = getCleanPrompt(prompt, bot_full_name); + const prompt = getCleanPrompt(payload); // We want to save summaries pr user, pr stream: // AKA: If the user says they like hamburgers in #lounge, the bot wont // remember it in #random. - const stream_id = message?.stream_id; + const stream_id = payload?.message?.stream_id; const summary_id = `${bot_id}-${stream_id}-${user_id}`; return { user_name: user_name, - user_id: user_id, + user_id: user_id ?? 0, bot_id: bot_id, prompt: prompt, messages: messages, summary_id: summary_id, - // These options will be set further down the code. - user_message: {}, - bot_reply: "", }; } // Endpoint used by the bots - serving the response that Zulip understands. -export async function serveResponse(req: any, options: any): Promise { - const vars = await buildVariables(req, options); +export async function serveResponse( + payload: ChatbotPayload, + options: ChatbotOptions +): Promise { + const vars = buildVariables(payload, options); // Checking if the users prompt is one of the consent-change keywords. - if (await detectAndHandleConsentChange(vars.user_id, vars.prompt)) { + if (await detectAndHandleConsentChange(vars.user_id, vars.prompt ?? "")) { return await returnResponse(updateConsentInfo()); } // If the user wants to see the chat history, we'll return that. if (vars.prompt === commands.show_history) { - return await returnResponse(await getChatSummary(vars.summary_id)); + return await returnResponse(await getChatSummary(vars?.summary_id ?? "")); } // If the user has not given consent, we'll return a prompt for consent. @@ -144,11 +185,11 @@ export async function serveResponse(req: any, options: any): Promise { return returnResponse(consentInfo()); } - const hasFullConsent = await fullConsentCheck(vars.user_id); + const hasFullConsent = await fullConsentCheck(vars?.user_id ?? 0); // If the user has given full consent, we'll load any previous chat history. if (hasFullConsent) { - const summary = await getChatSummary(vars.summary_id); + const summary = await getChatSummary(vars?.summary_id ?? ""); if (summary) { vars.messages.push({ diff --git a/chatbot_engine/openai.ts b/chatbot_engine/openai.ts index f90c559..ad52368 100644 --- a/chatbot_engine/openai.ts +++ b/chatbot_engine/openai.ts @@ -1,10 +1,14 @@ +// @ts-ignore import { OpenAI } from "https://deno.land/x/openai/mod.ts"; +import { ChatbotMessage } from "./chatbot.ts"; const open_ai_model = "gpt-3.5-turbo"; // Send messages to OpenAI, and get a text response. -export async function callAPI(messages: any): Promise { - // eslint-disable-next-line no-undef +export async function callAPI( + messages: ChatbotMessage[] +): Promise { + // @ts-ignore const openAIAPIKey = Deno.env.get("OPENAI_API_KEY"); console.log(messages); diff --git a/chatbot_engine/response.ts b/chatbot_engine/response.ts index e816539..91ea006 100644 --- a/chatbot_engine/response.ts +++ b/chatbot_engine/response.ts @@ -1,6 +1,6 @@ // Helper, for returning a Response in a format that Zulip understands. export async function returnResponse( - message: any, + message: string, status = 200 ): Promise { return new Response( diff --git a/chatbot_engine/shared.ts b/chatbot_engine/shared.ts index d20cf12..bfa00f0 100644 --- a/chatbot_engine/shared.ts +++ b/chatbot_engine/shared.ts @@ -1,10 +1,11 @@ +// @ts-ignore import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; // Init'ing the supabase CLI. export const supabase = createClient( - // eslint-disable-next-line no-undef + // @ts-ignore Deno.env.get("SUPABASE_URL") ?? "", - // eslint-disable-next-line no-undef + // @ts-ignore Deno.env.get("SUPABASE_ANON_KEY") ?? "" ); diff --git a/package-lock.json b/package-lock.json index 1f78449..136bec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "zulip-openai", "version": "1.0.0", "license": "MIT", + "dependencies": { + "@tsconfig/deno": "^1.0.7" + }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", @@ -140,6 +143,11 @@ "node": ">= 8" } }, + "node_modules/@tsconfig/deno": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@tsconfig/deno/-/deno-1.0.7.tgz", + "integrity": "sha512-AqIaGDRyklUuxaYzSQeCtj6+/OXTOcTLpm04RW8a7T1tTTr6kd/zNq/7Ex38bNJG/60GQUuIdZwcd8MSz9/cuw==" + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -1929,6 +1937,11 @@ "fastq": "^1.6.0" } }, + "@tsconfig/deno": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@tsconfig/deno/-/deno-1.0.7.tgz", + "integrity": "sha512-AqIaGDRyklUuxaYzSQeCtj6+/OXTOcTLpm04RW8a7T1tTTr6kd/zNq/7Ex38bNJG/60GQUuIdZwcd8MSz9/cuw==" + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", diff --git a/package.json b/package.json index 89bb91c..f6a49ca 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "1.0.0", "description": "Zulip/OpenAI chatbots, powered by Supabase.", "scripts": { - "js:eslint": "eslint './**/*.ts'", + "typecheck": "tsc --noEmit", + "js:eslint": "eslint ./ --ext .ts", "js:prettier": "prettier './'", - "lint": "concurrently 'npm:js:eslint' 'npm:js:prettier -- --check' --raw", + "lint": "concurrently 'npm:typecheck' 'npm:js:eslint' 'npm:js:prettier -- --check' --raw", "format": "concurrently 'npm:js:eslint -- --fix' 'npm:js:prettier -- --write' --max-processes 1 --raw", "test": "concurrently 'npm:js:lint' 'npm:js:format'" }, @@ -25,5 +26,8 @@ "concurrently": "^8.0.1", "eslint": "^8.37.0", "prettier": "^2.8.7" + }, + "dependencies": { + "@tsconfig/deno": "^1.0.7" } } diff --git a/supabase/config.toml b/supabase/config.toml index d315953..2252431 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -5,7 +5,7 @@ project_id = "zulip-openai" [api] # Port to use for the API URL. port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# Schemas to expose in your API. Tablesreq: Request views and stored procedures in this schema will get API # endpoints. public and storage are always included. schemas = ["public", "storage", "graphql_public"] # Extra schemas to add to the search_path of every request. public is always included. diff --git a/supabase/functions/haddockbot/index.ts b/supabase/functions/haddockbot/index.ts index 73c7b01..fee7af1 100644 --- a/supabase/functions/haddockbot/index.ts +++ b/supabase/functions/haddockbot/index.ts @@ -1,8 +1,11 @@ -import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; -import { serveResponse } from "../../../chatbot_engine/chatbot.ts"; +// @ts-ignore +import { serve } from "https://deno.land/std@0.183.0/http/server.ts"; +import { getPayload, serveResponse } from "../../../chatbot_engine/chatbot.ts"; -serve(async (req) => { - return await serveResponse(req, { +serve(async (req: Request) => { + const payload = await getPayload(req); + + return await serveResponse(payload, { personality: 'Captain Haddock from TinTin - a character who uses a lot of sailor language and who is quick to anger. You use "insults" and "curses" when you get angry, which are very expressive and tend to use exclamations such as "dogs!" "vegetarian!", "swine!"', }); diff --git a/supabase/functions/openaibot/index.ts b/supabase/functions/openaibot/index.ts index 0dda0d9..9be3ade 100644 --- a/supabase/functions/openaibot/index.ts +++ b/supabase/functions/openaibot/index.ts @@ -1,17 +1,22 @@ -import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; +// @ts-ignore +import { serve } from "https://deno.land/std@0.183.0/http/server.ts"; import { - serveResponse, getCleanPrompt, + getPayload, + ChatbotMessage, } from "../../../chatbot_engine/chatbot.ts"; +import { callAPI as openAICallAPI } from "../../../chatbot_engine/openai.ts"; +import { + returnError, + returnResponse, +} from "../../../chatbot_engine/response.ts"; -serve(async (req) => { - const { bot_full_name, data } = await req.json(); - - let prompt = data.toString(); +serve(async (req: Request) => { + const payload = await getPayload(req); - prompt = getCleanPrompt(prompt, bot_full_name); + const prompt = getCleanPrompt(payload); - const messages = [] as any[]; + const messages = [] as ChatbotMessage[]; // Treat each line of the prompt as a seperate message. // - If the line is empty, skip it. @@ -34,7 +39,11 @@ serve(async (req) => { }); }); - return await serveResponse(req, { - messages: messages, - }); + const bot_reply = await openAICallAPI(messages); + + if (typeof bot_reply !== "string") { + return returnError("Could not get bot response."); + } + + return await returnResponse(bot_reply); }); diff --git a/supabase/functions/praisebot/index.ts b/supabase/functions/praisebot/index.ts index 36382e1..8077f95 100644 --- a/supabase/functions/praisebot/index.ts +++ b/supabase/functions/praisebot/index.ts @@ -1,8 +1,11 @@ -import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; -import { serveResponse } from "../../../chatbot_engine/chatbot.ts"; +// @ts-ignore +import { serve } from "https://deno.land/std@0.183.0/http/server.ts"; +import { getPayload, serveResponse } from "../../../chatbot_engine/chatbot.ts"; -serve(async (req) => { - return await serveResponse(req, { +serve(async (req: Request) => { + const payload = await getPayload(req); + + return await serveResponse(payload, { messages: [ { role: "system", diff --git a/supabase/functions/ragebot/index.ts b/supabase/functions/ragebot/index.ts index d7627c2..4e13870 100644 --- a/supabase/functions/ragebot/index.ts +++ b/supabase/functions/ragebot/index.ts @@ -1,8 +1,11 @@ -import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; -import { serveResponse } from "../../../chatbot_engine/chatbot.ts"; +// @ts-ignore +import { serve } from "https://deno.land/std@0.183.0/http/server.ts"; +import { getPayload, serveResponse } from "../../../chatbot_engine/chatbot.ts"; -serve(async (req) => { - return await serveResponse(req, { +serve(async (req: Request) => { + const payload = await getPayload(req); + + return await serveResponse(payload, { personality: 'a rage machine: The user will give you a topic that infuriates him, and you will assist in raging about it. You will not shy away from using swearing and CAPITAL LETTERS. The raging should start out simple and get more and more angry, usually ending up with simulating you smashing the keyboard - kinda like "GOD DAMN IT!!!!!1111!11oneonone!!!!!111111111111". As the rant goes on, the spelling and coherency falls more and more apart as it nears the end of the rant.', }); diff --git a/supabase/functions/readerbot/index.ts b/supabase/functions/readerbot/index.ts index a9c307a..85e4779 100644 --- a/supabase/functions/readerbot/index.ts +++ b/supabase/functions/readerbot/index.ts @@ -1,12 +1,18 @@ -import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; +// @ts-ignore +import { serve } from "https://deno.land/std@0.183.0/http/server.ts"; + +// @ts-ignore +import { Readability } from "https://cdn.skypack.dev/@mozilla/readability?dts"; +// @ts-ignore +import { NodeHtmlMarkdown } from "https://cdn.skypack.dev/node-html-markdown?dts"; +// @ts-ignore +import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts"; + +import { getPayload, getCleanPrompt } from "../../../chatbot_engine/chatbot.ts"; import { returnResponse, returnError, } from "../../../chatbot_engine/response.ts"; -import { getCleanPrompt } from "../../../chatbot_engine/chatbot.ts"; -import { Readability } from "https://cdn.skypack.dev/@mozilla/readability?dts"; -import { NodeHtmlMarkdown } from "https://cdn.skypack.dev/node-html-markdown?dts"; -import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts"; import { callAPI as openAICallAPI } from "../../../chatbot_engine/openai.ts"; function isValidHttpUrl(string: string): boolean { @@ -20,12 +26,10 @@ function isValidHttpUrl(string: string): boolean { return url.protocol === "http:" || url.protocol === "https:"; } -serve(async (req) => { - const { bot_full_name, data } = await req.json(); - - let prompt = data.toString(); +serve(async (req: Request) => { + const payload = await getPayload(req); - prompt = getCleanPrompt(prompt, bot_full_name); + const prompt = getCleanPrompt(payload); const tldr_trigger = "tldr "; const tldr = prompt.startsWith(tldr_trigger); diff --git a/supabase/functions/testbot/index.ts b/supabase/functions/testbot/index.ts index a12b924..6823530 100644 --- a/supabase/functions/testbot/index.ts +++ b/supabase/functions/testbot/index.ts @@ -1,10 +1,13 @@ -import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; -import { serveResponse } from "../../../chatbot_engine/chatbot.ts"; +// @ts-ignore +import { serve } from "https://deno.land/std@0.183.0/http/server.ts"; +import { getPayload, serveResponse } from "../../../chatbot_engine/chatbot.ts"; // A simple bot that only replies with "ok" // Used by the test suite to verify that the chatbot engine is working. -serve(async (req) => { - return await serveResponse(req, { +serve(async (req: Request) => { + const payload = await getPayload(req); + + return await serveResponse(payload, { personality: 'a machine that can ONLY reply with "ok" - no answers, no questions, no punctuation, no whitespaces, no nothing. You will only be able to reply with "ok" to any message.', }); diff --git a/supabase/functions/thedudebot/index.ts b/supabase/functions/thedudebot/index.ts index 13db31a..70dc1ab 100644 --- a/supabase/functions/thedudebot/index.ts +++ b/supabase/functions/thedudebot/index.ts @@ -1,8 +1,11 @@ -import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; -import { serveResponse } from "../../../chatbot_engine/chatbot.ts"; +// @ts-ignore +import { serve } from "https://deno.land/std@0.183.0/http/server.ts"; +import { getPayload, serveResponse } from "../../../chatbot_engine/chatbot.ts"; -serve(async (req) => { - return await serveResponse(req, { +serve(async (req: Request) => { + const payload = await getPayload(req); + + return await serveResponse(payload, { personality: 'The Dude from the movie The Big Lebowski. You are easy-going and very quick to get distracted from actually answering any questions, rather focusing on the "Dudeism philosophy" of taking it easy and going with the flow.', }); diff --git a/tsconfig.json b/tsconfig.json index 0967ef4..5662eb2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1 +1,7 @@ -{} +{ + "compilerOptions": { + "lib": ["es6", "dom"], + "allowImportingTsExtensions": true, + "skipLibCheck": true + } +}