diff --git a/src/chatgpt-api.ts b/src/chatgpt-api.ts index 7598960..32a39a1 100644 --- a/src/chatgpt-api.ts +++ b/src/chatgpt-api.ts @@ -10,6 +10,79 @@ const KEY_ACCESS_TOKEN = 'accessToken' const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' +/** + * A conversation wrapper around the ChatGPT API. This allows you to send + * multiple messages to ChatGPT and receive responses, without having to + * manually pass the conversation ID and parent message ID for each message. + */ +class Conversation { + api: ChatGPTAPI + conversationId: string = undefined + parentMessageId: string = undefined + + /** + * Creates a new conversation wrapper around the ChatGPT API. + * @param api - The ChatGPT API instance to use. + */ + constructor( + api: ChatGPTAPI, + opts: { conversationId?: string; parentMessageId?: string } = {} + ) { + this.api = api + this.conversationId = opts.conversationId + this.parentMessageId = opts.parentMessageId + } + + /** + * Sends a message to ChatGPT, waits for the response to resolve, and returns + * the response. + * If this is the first message in the conversation, the conversation ID and + * parent message ID will be automatically set. + * This allows you to send multiple messages to ChatGPT and receive responses, + * without having to manually pass the conversation ID and parent message ID + * for each message. + * If you want to manually pass the conversation ID and parent message ID, + * use `api.sendMessage` instead. + * + * @param message - The plaintext message to send. + * @param opts.onProgress - Optional listener which will be called every time the partial response is updated + * @param opts.onConversationResponse - Optional listener which will be called every time a conversation response is received + * @returns The plaintext response from ChatGPT. + */ + async sendMessage( + message: string, + opts: { + onProgress?: (partialResponse: string) => void + onConversationResponse?: ( + response: types.ConversationResponseEvent + ) => void + } = {} + ) { + const { onProgress, onConversationResponse } = opts + if (!this.conversationId) { + return this.api.sendMessage(message, { + onProgress, + onConversationResponse: (response) => { + this.conversationId = response.conversation_id + this.parentMessageId = response.message.id + onConversationResponse?.(response) + } + }) + } + + return this.api.sendMessage(message, { + conversationId: this.conversationId, + parentMessageId: this.parentMessageId, + onProgress, + onConversationResponse: (response) => { + this.conversationId = response.conversation_id + this.parentMessageId = response.message.id + onConversationResponse?.(response) + } + }) + } +} + export class ChatGPTAPI { protected _sessionToken: string protected _markdown: boolean @@ -82,15 +155,25 @@ export class ChatGPTAPI { * @param message - The plaintext message to send. * @param opts.conversationId - Optional ID of the previous message in a conversation * @param opts.onProgress - Optional listener which will be called every time the partial response is updated + * @param opts.onConversationResponse - Optional listener which will be called every time the partial response is updated with the full conversation response */ async sendMessage( message: string, opts: { conversationId?: string + parentMessageId?: string onProgress?: (partialResponse: string) => void + onConversationResponse?: ( + response: types.ConversationResponseEvent + ) => void } = {} ): Promise { - const { conversationId = uuidv4(), onProgress } = opts + const { + conversationId, + parentMessageId = uuidv4(), + onProgress, + onConversationResponse + } = opts const accessToken = await this.refreshAccessToken() @@ -107,7 +190,11 @@ export class ChatGPTAPI { } ], model: 'text-davinci-002-render', - parent_message_id: conversationId + parent_message_id: parentMessageId + } + + if (conversationId) { + body.conversation_id = conversationId } const url = `${this._backendApiBaseUrl}/conversation` @@ -134,6 +221,9 @@ export class ChatGPTAPI { try { const parsedData: types.ConversationResponseEvent = JSON.parse(data) + if (onConversationResponse) { + onConversationResponse(parsedData) + } const message = parsedData.message // console.log('event', JSON.stringify(parsedData, null, 2)) @@ -197,4 +287,17 @@ export class ChatGPTAPI { throw new Error(`ChatGPT failed to refresh auth token. ${err.toString()}`) } } + + /** + * Get a new Conversation instance, which can be used to send multiple messages as part of a single conversation. + * + * @param opts.conversationId - Optional Data of the previous message in a conversation + * @param opts.parentMessageId - Optional Data of the previous message in a conversation + * @returns a new Conversation instance + */ + getConversation( + opts: { conversationId?: string; parentMessageId?: string } = {} + ) { + return new Conversation(this, opts) + } } diff --git a/src/demo-conversation.ts b/src/demo-conversation.ts new file mode 100644 index 0000000..c0199b8 --- /dev/null +++ b/src/demo-conversation.ts @@ -0,0 +1,57 @@ +import dotenv from 'dotenv-safe' +import { oraPromise } from 'ora' + +import { ChatGPTAPI } from '.' + +dotenv.config() + +/** + * Example CLI for testing functionality. + * + * ``` + * npx tsx src/demo.ts + * ``` + */ +async function main() { + const api = new ChatGPTAPI({ sessionToken: process.env.SESSION_TOKEN }) + await api.ensureAuth() + + const conversation = api.getConversation() + + const prompt = 'What is OpenAI?' + + const response = await oraPromise(conversation.sendMessage(prompt), { + text: prompt + }) + + console.log(response) + + const prompt2 = 'Did they made OpenGPT?' + + console.log( + await oraPromise(conversation.sendMessage(prompt2), { + text: prompt2 + }) + ) + + const prompt3 = 'Who founded this institute?' + + console.log( + await oraPromise(conversation.sendMessage(prompt3), { + text: prompt3 + }) + ) + + const prompt4 = 'Who is that?' + + console.log( + await oraPromise(conversation.sendMessage(prompt4), { + text: prompt4 + }) + ) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +})