UNPKG

chatgpt-optimized-official

Version:
335 lines (289 loc) 10.6 kB
import { encode } from "gpt-3-encoder"; //import { Configuration, OpenAIApi } from "openai"; import axios from "axios"; import Options from "../models/options.js"; import Usage from "../models/chatgpt-usage.js"; import Conversation from "../models/conversation.js"; import MessageType from "../enums/message-type.js"; import AppDbContext from "./app-dbcontext.js"; import OpenAIKey from "../models/openai-key.js"; import { randomUUID } from "crypto"; class OpenAI { private db: AppDbContext; public options: Options; public onUsage: (usage: Usage) => void; constructor(key: string | string[], options?: Options) { this.db = new AppDbContext(); this.db.WaitForLoad().then(() => { if (typeof key === "string") { if (this.db.keys.Any((x) => x.key === key)) return; this.db.keys.Add({ key: key, queries: 0, balance: 0, tokens: 0, }); } else if (Array.isArray(key)) { key.forEach((k) => { if (this.db.keys.Any((x) => x.key === k)) return; this.db.keys.Add({ key: k, queries: 0, balance: 0, tokens: 0, }); }); } }); this.options = { model: options?.model || "text-davinci-003", // default model temperature: options?.temperature || 0.7, max_tokens: options?.max_tokens || 512, top_p: options?.top_p || 0.9, frequency_penalty: options?.frequency_penalty || 0, presence_penalty: options?.presence_penalty || 0, instructions: options?.instructions || `You are ChatGPT, a language model developed by OpenAI. You are designed to respond to user input in a conversational manner, Answer as concisely as possible. Your training data comes from a diverse range of internet text and You have been trained to generate human-like responses to various questions and prompts. You can provide information on a wide range of topics, but your knowledge is limited to what was present in your training data, which has a cutoff date of 2021. You strive to provide accurate and helpful information to the best of your ability.\nKnowledge cutoff: 2021-09`, stop: options?.stop || "<|im_end|>", aiName: options?.aiName || "ChatGPT", moderation: options?.moderation || false, endpoint: options?.endpoint || "https://api.openai.com/v1/completions", price: options?.price || 0.02, max_conversation_tokens: options?.max_conversation_tokens || 4097, }; } private getOpenAIKey(): OpenAIKey { let key = this.db.keys.OrderBy((x) => x.balance).FirstOrDefault(); if (key == null) { key = this.db.keys.FirstOrDefault(); } if (key == null) { throw new Error("No keys available."); } return key; } private async *chunksToLines(chunksAsync: any) { let previous = ""; for await (const chunk of chunksAsync) { const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); previous += bufferChunk; let eolIndex: number; while ((eolIndex = previous.indexOf("\n")) >= 0) { // line includes the EOL const line = previous.slice(0, eolIndex + 1).trimEnd(); if (line === "data: [DONE]") break; if (line.startsWith("data: ")) yield line; previous = previous.slice(eolIndex + 1); } } } private async *linesToMessages(linesAsync: any) { for await (const line of linesAsync) { const message = line.substring("data :".length); yield message; } } private async *streamCompletion(data: any) { yield* this.linesToMessages(this.chunksToLines(data)); } private getInstructions(username: string): string { return `[START_INSTRUCTIONS] ${this.options.instructions} Current date: ${this.getToday()} Current time: ${this.getTime()}${username !== "User" ? `\nName of the user talking to: ${username}` : ""} [END_INSTRUCTIONS]${this.options.stop}\n`; } public addConversation(conversationId: string, userName: string = "User") { let conversation: Conversation = { id: conversationId, userName: userName, messages: [], }; this.db.conversations.Add(conversation); return conversation; } public getConversation(conversationId: string, userName: string = "User") { let conversation = this.db.conversations.Where((conversation) => conversation.id === conversationId).FirstOrDefault(); if (!conversation) { conversation = this.addConversation(conversationId, userName); } else { conversation.lastActive = Date.now(); } conversation.userName = userName; return conversation; } public resetConversation(conversationId: string) { let conversation = this.db.conversations.Where((conversation) => conversation.id === conversationId).FirstOrDefault(); if (conversation) { conversation.messages = []; conversation.lastActive = Date.now(); } return conversation; } public async ask(prompt: string, conversationId: string = "default", userName: string = "User") { return await this.askStream( (data) => {}, (data) => {}, prompt, conversationId, userName, ); } public async askStream(data: (arg0: string) => void, usage: (usage: Usage) => void, prompt: string, conversationId: string = "default", userName: string = "User") { let oAIKey = this.getOpenAIKey(); let conversation = this.getConversation(conversationId, userName); if (this.options.moderation) { let flagged = await this.moderate(prompt, oAIKey.key); if (flagged) { for (let chunk in "Your message was flagged as inappropriate and was not sent.".split("")) { data(chunk); await this.wait(100); } return "Your message was flagged as inappropriate and was not sent."; } } let promptStr = this.generatePrompt(conversation, prompt); let prompt_tokens = encode(promptStr).length; try { const response = await axios.post( this.options.endpoint, { model: this.options.model, prompt: promptStr, temperature: this.options.temperature, max_tokens: this.options.max_tokens, top_p: this.options.top_p, frequency_penalty: this.options.frequency_penalty, presence_penalty: this.options.presence_penalty, stop: [this.options.stop], stream: true, }, { responseType: "stream", headers: { Accept: "text/event-stream", "Content-Type": "application/json", Authorization: `Bearer ${oAIKey.key}`, }, }, ); let responseStr = ""; for await (const message of this.streamCompletion(response.data)) { try { const parsed = JSON.parse(message); const { text } = parsed.choices[0]; responseStr += text; data(text); } catch (error) { console.error("Could not JSON parse stream message", message, error); } } let completion_tokens = encode(responseStr).length; responseStr = responseStr .replace(new RegExp(`\n${conversation.userName}:.*`, "gs"), "") .replace(new RegExp(`${conversation.userName}:.*`, "gs"), "") .replace(/<\|im_end\|>/g, "") .replace(this.options.stop, "") .replace(`${this.options.aiName}: `, "") .trim(); let usageData = { key: oAIKey.key, prompt_tokens: prompt_tokens, completion_tokens: completion_tokens, total_tokens: prompt_tokens + completion_tokens, }; usage(usageData); if (this.onUsage) this.onUsage(usageData); oAIKey.tokens += usageData.total_tokens; oAIKey.balance = (oAIKey.tokens / 1000) * this.options.price; oAIKey.queries++; conversation.messages.push({ id: randomUUID(), content: responseStr, type: MessageType.Assistant, date: Date.now(), }); return responseStr; } catch (error: any) { try { if (error.response && error.response.data) { let errorResponseStr = ""; for await (const message of error.response.data) { errorResponseStr += message; } const errorResponseJson = JSON.parse(errorResponseStr); throw new Error(errorResponseJson.error.message); } else { throw new Error(error.message); } } catch (e) { throw new Error(error.message); } } } private generatePrompt(conversation: Conversation, prompt: string) { prompt = [",", "!", "?", "."].includes(prompt[prompt.length - 1]) ? prompt : `${prompt}.`; // Thanks to https://github.com/optionsx conversation.messages.push({ id: randomUUID(), content: prompt, type: MessageType.User, date: Date.now(), }); let promptStr = this.convToString(conversation); let promptEncodedLength = encode(promptStr).length; let totalLength = promptEncodedLength + this.options.max_tokens; while (totalLength > this.options.max_conversation_tokens) { conversation.messages.shift(); promptStr = this.convToString(conversation); promptEncodedLength = encode(promptStr).length; totalLength = promptEncodedLength + this.options.max_tokens; } conversation.lastActive = Date.now(); return promptStr; } public async moderate(prompt: string, key: string) { // try { // let openAi = new OpenAIApi(new Configuration({ apiKey: key })); // let response = await openAi.createModeration({ // input: prompt, // }); // return response.data.results[0].flagged; // } catch (error) { // return false; // } return false; } private convToString(conversation: Conversation) { let messages: string[] = []; for (let i = 0; i < conversation.messages.length; i++) { let message = conversation.messages[i]; if (i === 0) { messages.push(this.getInstructions(conversation.userName)); } messages.push(`${message.type === MessageType.User ? conversation.userName : this.options.aiName}: ${conversation.messages[i].content}${this.options.stop}`); } messages.push(`${this.options.aiName}: `); let result = messages.join("\n"); return result; } private getToday() { let today = new Date(); let dd = String(today.getDate()).padStart(2, "0"); let mm = String(today.getMonth() + 1).padStart(2, "0"); let yyyy = today.getFullYear(); return `${yyyy}-${mm}-${dd}`; } private getTime() { let today = new Date(); let hours: any = today.getHours(); let minutes: any = today.getMinutes(); let ampm = hours >= 12 ? "PM" : "AM"; hours = hours % 12; hours = hours ? hours : 12; minutes = minutes < 10 ? `0${minutes}` : minutes; return `${hours}:${minutes} ${ampm}`; } private wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } } export default OpenAI;