chatgpt-optimized-official
Version:
ChatGPT Client using official OpenAI API
335 lines (289 loc) • 10.6 kB
text/typescript
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;