Skip to content

Commit

Permalink
Remove 'any' + Update OpenAI bot to allow system messages.
Browse files Browse the repository at this point in the history
  • Loading branch information
rasben committed Apr 14, 2023
1 parent c199aea commit ca8105b
Show file tree
Hide file tree
Showing 17 changed files with 172 additions and 76 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
87 changes: 64 additions & 23 deletions chatbot_engine/chatbot.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// @ts-ignore
import { Response } from "https://deno.land/[email protected]/http/server.ts";

import { supabase, commands } from "./shared.ts";

import {
Expand All @@ -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, "");
Expand Down Expand Up @@ -45,7 +81,7 @@ async function getChatSummary(summary_id: string): Promise<string> {
}

// Save a chat summary to Supabase DB.
async function setChatSummary(vars: any): Promise<void> {
async function setChatSummary(vars: ChatbotVariables): Promise<void> {
if (vars.bot_reply) {
vars.messages.push({
role: "assistant",
Expand Down Expand Up @@ -76,18 +112,25 @@ async function setChatSummary(vars: any): Promise<void> {
}
}

// Build the variables for processing the chatbot.
async function buildVariables(req: any, options: any): Promise<any> {
export async function getPayload(req: Request): Promise<ChatbotPayload> {
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"],
Expand All @@ -100,55 +143,53 @@ async function buildVariables(req: any, options: any): Promise<any> {
});

// 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<Response> {
const vars = await buildVariables(req, options);
export async function serveResponse(
payload: ChatbotPayload,
options: ChatbotOptions
): Promise<Response> {
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.
if (!(await basicConsentCheck(vars.user_id))) {
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({
Expand Down
8 changes: 6 additions & 2 deletions chatbot_engine/openai.ts
Original file line number Diff line number Diff line change
@@ -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<string | boolean> {
// eslint-disable-next-line no-undef
export async function callAPI(
messages: ChatbotMessage[]
): Promise<string | boolean> {
// @ts-ignore
const openAIAPIKey = Deno.env.get("OPENAI_API_KEY");
console.log(messages);

Expand Down
2 changes: 1 addition & 1 deletion chatbot_engine/response.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
return new Response(
Expand Down
5 changes: 3 additions & 2 deletions chatbot_engine/shared.ts
Original file line number Diff line number Diff line change
@@ -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") ?? ""
);

Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
},
Expand All @@ -25,5 +26,8 @@
"concurrently": "^8.0.1",
"eslint": "^8.37.0",
"prettier": "^2.8.7"
},
"dependencies": {
"@tsconfig/deno": "^1.0.7"
}
}
2 changes: 1 addition & 1 deletion supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 7 additions & 4 deletions supabase/functions/haddockbot/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { serveResponse } from "../../../chatbot_engine/chatbot.ts";
// @ts-ignore
import { serve } from "https://deno.land/[email protected]/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!"',
});
Expand Down
31 changes: 20 additions & 11 deletions supabase/functions/openaibot/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { serve } from "https://deno.land/[email protected]/http/server.ts";
// @ts-ignore
import { serve } from "https://deno.land/[email protected]/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.
Expand All @@ -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);
});
11 changes: 7 additions & 4 deletions supabase/functions/praisebot/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { serveResponse } from "../../../chatbot_engine/chatbot.ts";
// @ts-ignore
import { serve } from "https://deno.land/[email protected]/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",
Expand Down
11 changes: 7 additions & 4 deletions supabase/functions/ragebot/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { serveResponse } from "../../../chatbot_engine/chatbot.ts";
// @ts-ignore
import { serve } from "https://deno.land/[email protected]/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.',
});
Expand Down
Loading

0 comments on commit ca8105b

Please sign in to comment.