UNPKG

chatgpt-optimized-official

Version:
453 lines (404 loc) 18.9 kB
import axios from "axios"; import { randomUUID } from "crypto"; import { encode } from "gpt-3-encoder"; import Usage from "../models/chatgpt-usage.js"; import Options from "../models/chatgpt-options.js"; import Conversation from "../models/conversation.js"; import Message from "../models/chatgpt-message.js"; import MessageType from "../enums/message-type.js"; import AppDbContext from "./app-dbcontext.js"; import OpenAIKey from "../models/openai-key.js"; import { OpenAI, ClientOptions } from "openai"; //import { OpenAIApi, Configuration } from "@openai/api"; import { type } from "os"; class Assistant { public options: Options; private db: AppDbContext; public onUsage: (usage: Usage) => void; private openai: OpenAI; private assistantId: string; private theadId: string; private runId: string; // Este objeto mantiene un mapeo de conversationId a threadId private conversationThreads: { [conversationId: string]: string } = {}; 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, }); }); } }); // Default options if none are provided this.options = { model: options?.model || "gpt-4-1106-preview", // default model temperature: options?.temperature || 0.7, max_tokens: options?.max_tokens || 100, top_p: options?.top_p || 0.9, frequency_penalty: options?.frequency_penalty || 0, presence_penalty: options?.presence_penalty || 0, instructions: options?.instructions || `You are Assistant, 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`, price: options?.price || 0.002, max_conversation_tokens: options?.max_conversation_tokens || 4097, endpoint: options?.endpoint || "https://api.openai.com/v1/chat/completions", moderation: options?.moderation || false, functions: options?.functions || null, function_call: options?.function_call || null, name_assistant: options?.name_assistant || "My Assistant", tools: options?.tools || null, tool_choice: options?.tool_choice || null, }; this.openai = new OpenAI({ apiKey: String(key) }); this.createAssistant(this.options) } // Paso 1: Crear un Asistente public async createAssistant(options: Options) { try { const response = await this.openai.beta.assistants.create({ name: options.name_assistant, // Name of the assistant instructions: options.instructions, // Instructions for the assistant tools: options.tools, // List of tools to be used by the assistant model: options.model, // ID of the GPT-3 model to be used by the assistant }); console.log(response); this.assistantId = response.id; // return response; } catch (error) { console.log(error) } } // Paso 2: Crear un Hilo public async createThread(): Promise<string> { const response = await this.openai.beta.threads.create(); this.theadId = response.id; return response.id; } // Paso 3: Agregar un Mensaje a un Hilo public async addMessageToThread(threadId: string, content: string) { return await this.openai.beta.threads.messages.create(threadId, { role: 'user', content: content }); } // Paso 4: Ejecutar el Asistente public async runAssistant(threadId: string, instructions?: string) { const response = await this.openai.beta.threads.runs.create(threadId, { assistant_id: this.assistantId, instructions: instructions || '' }); return response.id; } // Paso 5: Verificar el Estado de Ejecución public async checkRunStatus(threadId: string, runId: string) { return await this.openai.beta.threads.runs.retrieve(threadId, runId); } // Paso 6: Obtener Respuestas del Asistente public async getAssistantResponses(threadId: string) { const messages = await this.openai.beta.threads.messages.list(threadId); return messages.data.filter(message => message.role === "assistant"); } // Paso 7: Crear y Ejecutar un Hilo public async createAndRunThread(prompt: string, userName: string = "User", conversationId: string | null = null): Promise<any> { let threadId: string; // Si no se proporciona un conversationId, crea un nuevo hilo y usa su ID como conversationId if (!conversationId) { threadId = await this.createThread(); // Aquí asumimos que createThread retorna el ID del nuevo hilo } else { // Si se proporciona un conversationId, úsalo como el threadId para continuar la conversación threadId = conversationId; } // Crear un nuevo hilo // const threadId = await this.createThread(); // Agregar el mensaje del usuario al hilo await this.addMessageToThread(threadId, prompt); // Ejecutar el asistente en el hilo y obtener la ID de la ejecución const runId = await this.runAssistant(threadId); // Esperar a que la ejecución termine y obtener la respuesta await this.waitForRunToComplete(threadId, runId); // Obtener las respuestas del asistente const responses = await this.getAssistantResponses(threadId); // Retornar la respuesta formateada const formattedResponse = this.formatResponse(responses); return { conversationId: threadId, ...formattedResponse }; } public async waitForRunToComplete(threadId: string, runId: string): Promise<void> { let status = await this.checkRunStatus(threadId, runId); while (status.status !== 'completed') { await new Promise(resolve => setTimeout(resolve, 1000)); // Esperar 1 segundo antes de verificar nuevamente status = await this.checkRunStatus(threadId, runId); } } public formatResponse(responses: any[]): any { // Asumiendo que las respuestas están en el formato deseado // y que simplemente necesitamos devolver la primera respuesta console.log(responses[0].content); return responses.length > 0 ? responses[0].content : null; } 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; 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 `${this.options.instructions} Current date: ${this.getToday()} Current time: ${this.getTime()}${username !== "User" ? `\nName of the user talking to: ${username}` : ""}`; } 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(); //console.log(conversation); if (conversation) { conversation.messages = []; conversation.lastActive = Date.now(); } return conversation; } public async ask(prompt: string, conversationId: string = "default", type: number = 1, function_name?: string, userName: string = "User") { return await this.askPost( (data) => { }, (data) => { }, prompt, conversationId, function_name, userName, type ); } public async askPost(data: (arg0: string) => void, usage: (usage: Usage) => void, prompt: string, conversationId: string = "default", function_name?: string, userName: string = "User", type: number = MessageType.User) { return await this.createAndRunThread(prompt, userName, conversationId); // let oAIKey = this.getOpenAIKey(); // let conversation = this.getConversation(conversationId, userName); // // if (this.options.moderation) { // // let flagged = await this.moderate(prompt, oAIKey.key); // // if (flagged) { // // return { message: "Your message was flagged as inappropriate and was not sent." }; // // } // // } // let promptStr = this.generatePrompt(conversation, prompt, type, function_name); // let prompt_tokens = this.countTokens(promptStr); // try { // let auxOptions = { // model: this.options.model, // messages: 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, // stream: false, // Note this // } // if (this.options.functions) { // auxOptions["functions"] = this.options.functions; // auxOptions["function_call"] = this.options.function_call ? this.options.function_call : "auto"; // } // const response = await axios.post( // this.options.endpoint, // auxOptions, // { // responseType: "json", // Note this // headers: { // Accept: "application/json", // Note this // "Content-Type": "application/json", // Authorization: `Bearer ${oAIKey.key}`, // }, // }, // ); // //console.log("Stream message:", response.data.choices[0]) // let completion_tokens = response.data.usage['completion_tokens']; // let usageData = { // key: oAIKey.key, // prompt_tokens: prompt_tokens, // completion_tokens: completion_tokens, // total_tokens: prompt_tokens + completion_tokens, // }; // if (this.onUsage) this.onUsage(usageData); // oAIKey.tokens += usageData.total_tokens; // oAIKey.balance = (oAIKey.tokens / 1000) * this.options.price; // oAIKey.queries++; // if (response.data.choices[0]['message']['content']) { // conversation.messages.push({ // id: randomUUID(), // content: response.data.choices[0]['message']['content'] ? response.data.choices[0]['message']['content'] : "", // type: MessageType.Assistant, // date: Date.now(), // }); // } // data(JSON.stringify(response.data.choices[0])) // return response.data.choices[0]; // return the full response // } catch (error: any) { // if (error.response && error.response.data && error.response.headers["content-type"] === "application/json") { // throw new Error(error.response.data.error.message); // } else { // throw new Error(error.message); // } // } } 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 generatePrompt(conversation: Conversation, prompt: string, type: number = MessageType.User, function_name?: string): Message[] { let message = { id: randomUUID(), content: prompt, type: type, date: Date.now(), }; if (type === MessageType.Function && function_name) { message["name"] = function_name; } conversation.messages.push(message); let messages = this.generateMessages(conversation); let promptEncodedLength = this.countTokens(messages); let totalLength = promptEncodedLength + this.options.max_tokens; while (totalLength > this.options.max_conversation_tokens) { conversation.messages.shift(); messages = this.generateMessages(conversation); promptEncodedLength = this.countTokens(messages); totalLength = promptEncodedLength + this.options.max_tokens; } conversation.lastActive = Date.now(); return messages; } private generateMessages(conversation: Conversation): Message[] { let messages: Message[] = []; messages.push({ role: "system", content: this.getInstructions(conversation.userName), }); for (let i = 0; i < conversation.messages.length; i++) { let message = conversation.messages[i]; if (message.type === MessageType.Function) { messages.push({ role: "function", name: message.name || "unknownFunction", // Default to "unknownFunction" if function_name is not provided content: message.content, }); } else { let role = message.type === MessageType.User ? "user" : "assistant"; messages.push({ role: role, content: message.content, }); } } return messages; } private countTokens(messages: Message[]): number { let tokens: number = 0; for (let message of messages) { if (message.content) { if (typeof message.content === "string") { tokens += encode(message.content).length; } else if (Array.isArray(message.content)) { for (let contentBlock of message.content) { if (contentBlock.type === "text") { tokens += encode(contentBlock.text).length; } else if (contentBlock.type === "image_url") { // Estimar tokens para imágenes if (contentBlock.image_url.detail === "low" || !contentBlock.image_url.detail) { tokens += 85; } else if (contentBlock.image_url.detail === "high") { tokens += 765; // Ajusta según el tamaño real de la imagen si es posible } else { tokens += 85; // Estimación por defecto } } } } } } return tokens; } 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 Assistant;