Commit 1af5db24 authored by Travis Fischer's avatar Travis Fischer

feat: redesign main sendMessage, initSession, closeSession API

parent 24f51ac3
...@@ -16,40 +16,63 @@ async function main() { ...@@ -16,40 +16,63 @@ async function main() {
const email = process.env.OPENAI_EMAIL const email = process.env.OPENAI_EMAIL
const password = process.env.OPENAI_PASSWORD const password = process.env.OPENAI_PASSWORD
const api = new ChatGPTAPIBrowser({ email, password, debug: true }) const api = new ChatGPTAPIBrowser({
await api.init() email,
password,
debug: false,
minimize: true
})
await api.initSession()
const prompt = 'What is OpenAI?' const prompt = 'Write a poem about cats.'
const response = await oraPromise(api.sendMessage(prompt), { let res = await oraPromise(api.sendMessage(prompt), {
text: prompt text: prompt
}) })
console.log(response) console.log('\n' + res.response + '\n')
const prompt2 = 'Did they made OpenGPT?' const prompt2 = 'Can you make it cuter and shorter?'
console.log( res = await oraPromise(
await oraPromise(api.sendMessage(prompt2), { api.sendMessage(prompt2, {
conversationId: res.conversationId,
parentMessageId: res.messageId
}),
{
text: prompt2 text: prompt2
}) }
) )
console.log('\n' + res.response + '\n')
const prompt3 = 'Who founded this institute?' const prompt3 = 'Now write it in French.'
console.log( res = await oraPromise(
await oraPromise(api.sendMessage(prompt3), { api.sendMessage(prompt3, {
conversationId: res.conversationId,
parentMessageId: res.messageId
}),
{
text: prompt3 text: prompt3
}) }
) )
console.log('\n' + res.response + '\n')
const prompt4 = 'Who is that?' const prompt4 = 'What were we talking about again?'
console.log( res = await oraPromise(
await oraPromise(api.sendMessage(prompt4), { api.sendMessage(prompt4, {
conversationId: res.conversationId,
parentMessageId: res.messageId
}),
{
text: prompt4 text: prompt4
}) }
) )
console.log('\n' + res.response + '\n')
// close the browser at the end
await api.closeSession()
} }
main().catch((err) => { main().catch((err) => {
......
...@@ -22,41 +22,56 @@ async function main() { ...@@ -22,41 +22,56 @@ async function main() {
}) })
const api = new ChatGPTAPI({ ...authInfo }) const api = new ChatGPTAPI({ ...authInfo })
await api.ensureAuth() await api.initSession()
const conversation = api.getConversation() const prompt = 'Write a poem about cats.'
const prompt = 'What is OpenAI?' let res = await oraPromise(api.sendMessage(prompt), {
const response = await oraPromise(conversation.sendMessage(prompt), {
text: prompt text: prompt
}) })
console.log(response) console.log('\n' + res.response + '\n')
const prompt2 = 'Did they made OpenGPT?' const prompt2 = 'Can you make it cuter and shorter?'
console.log( res = await oraPromise(
await oraPromise(conversation.sendMessage(prompt2), { api.sendMessage(prompt2, {
conversationId: res.conversationId,
parentMessageId: res.messageId
}),
{
text: prompt2 text: prompt2
}) }
) )
console.log('\n' + res.response + '\n')
const prompt3 = 'Who founded this institute?' const prompt3 = 'Now write it in French.'
console.log( res = await oraPromise(
await oraPromise(conversation.sendMessage(prompt3), { api.sendMessage(prompt3, {
conversationId: res.conversationId,
parentMessageId: res.messageId
}),
{
text: prompt3 text: prompt3
}) }
) )
console.log('\n' + res.response + '\n')
const prompt4 = 'Who is that?' const prompt4 = 'What were we talking about again?'
console.log( res = await oraPromise(
await oraPromise(conversation.sendMessage(prompt4), { api.sendMessage(prompt4, {
conversationId: res.conversationId,
parentMessageId: res.messageId
}),
{
text: prompt4 text: prompt4
}) }
) )
console.log('\n' + res.response + '\n')
await api.closeSession()
} }
main().catch((err) => { main().catch((err) => {
......
...@@ -23,24 +23,21 @@ async function main() { ...@@ -23,24 +23,21 @@ async function main() {
debug: false, debug: false,
minimize: true minimize: true
}) })
await api.init() await api.initSession()
const prompt = const prompt =
'Write a python version of bubble sort. Do not include example usage.' 'Write a python version of bubble sort. Do not include example usage.'
const response = await oraPromise(api.sendMessage(prompt), { const res = await oraPromise(api.sendMessage(prompt), {
text: prompt text: prompt
}) })
console.log(res.response)
await api.close() // close the browser at the end
return response await api.closeSession()
} }
main() main().catch((err) => {
.then((res) => { console.error(err)
console.log(res) process.exit(1)
}) })
.catch((err) => {
console.error(err)
process.exit(1)
})
import * as types from './types'
export abstract class AChatGPTAPI {
/**
* Performs any async initialization work required to ensure that this API is
* properly authenticated.
*
* @throws An error if the session failed to initialize properly.
*/
abstract initSession(): Promise<void>
/**
* Sends a message to ChatGPT, waits for the response to resolve, and returns
* the response.
*
* If you want to receive a stream of partial responses, use `opts.onProgress`.
*
* @param message - The prompt message to send
* @param opts.conversationId - Optional ID of a conversation to continue
* @param opts.parentMessageId - Optional ID of the previous message in the conversation
* @param opts.messageId - Optional ID of the message to send (defaults to a random UUID)
* @param opts.action - Optional ChatGPT `action` (either `next` or `variant`)
* @param opts.timeoutMs - Optional timeout in milliseconds (defaults to no timeout)
* @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated
* @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
*
* @returns The response from ChatGPT, including `conversationId`, `messageId`, and
* the `response` text.
*/
abstract sendMessage(
message: string,
opts?: types.SendMessageOptions
): Promise<types.ChatResponse>
/**
* @returns `true` if the client is authenticated with a valid session or `false`
* otherwise.
*/
abstract getIsAuthenticated(): Promise<boolean>
/**
* Refreshes the current ChatGPT session.
*
* @returns Access credentials for the new session.
* @throws An error if it fails.
*/
abstract refreshSession(): Promise<any>
/**
* Closes the current ChatGPT session and starts a new one.
*
* @returns Access credentials for the new session.
* @throws An error if it fails.
*/
async resetSession(): Promise<any> {
await this.closeSession()
return this.initSession()
}
/**
* Closes the active session.
*
* @throws An error if it fails.
*/
abstract closeSession(): Promise<void>
}
...@@ -3,6 +3,7 @@ import type { Browser, HTTPRequest, HTTPResponse, Page } from 'puppeteer' ...@@ -3,6 +3,7 @@ import type { Browser, HTTPRequest, HTTPResponse, Page } from 'puppeteer'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as types from './types' import * as types from './types'
import { AChatGPTAPI } from './abstract-chatgpt-api'
import { getBrowser, getOpenAIAuth } from './openai-auth' import { getBrowser, getOpenAIAuth } from './openai-auth'
import { import {
browserPostEventStream, browserPostEventStream,
...@@ -11,7 +12,7 @@ import { ...@@ -11,7 +12,7 @@ import {
minimizePage minimizePage
} from './utils' } from './utils'
export class ChatGPTAPIBrowser { export class ChatGPTAPIBrowser extends AChatGPTAPI {
protected _markdown: boolean protected _markdown: boolean
protected _debug: boolean protected _debug: boolean
protected _minimize: boolean protected _minimize: boolean
...@@ -27,7 +28,7 @@ export class ChatGPTAPIBrowser { ...@@ -27,7 +28,7 @@ export class ChatGPTAPIBrowser {
protected _page: Page protected _page: Page
/** /**
* Creates a new client wrapper for automating the ChatGPT webapp. * Creates a new client for automating the ChatGPT webapp.
*/ */
constructor(opts: { constructor(opts: {
email: string email: string
...@@ -51,6 +52,8 @@ export class ChatGPTAPIBrowser { ...@@ -51,6 +52,8 @@ export class ChatGPTAPIBrowser {
/** @defaultValue `undefined` **/ /** @defaultValue `undefined` **/
executablePath?: string executablePath?: string
}) { }) {
super()
const { const {
email, email,
password, password,
...@@ -71,14 +74,23 @@ export class ChatGPTAPIBrowser { ...@@ -71,14 +74,23 @@ export class ChatGPTAPIBrowser {
this._minimize = !!minimize this._minimize = !!minimize
this._captchaToken = captchaToken this._captchaToken = captchaToken
this._executablePath = executablePath this._executablePath = executablePath
if (!this._email) {
const error = new types.ChatGPTError('ChatGPT invalid email')
error.statusCode = 401
throw error
}
if (!this._password) {
const error = new types.ChatGPTError('ChatGPT invalid password')
error.statusCode = 401
throw error
}
} }
async init() { override async initSession() {
if (this._browser) { if (this._browser) {
await this._browser.close() await this.closeSession()
this._page = null
this._browser = null
this._accessToken = null
} }
try { try {
...@@ -89,6 +101,15 @@ export class ChatGPTAPIBrowser { ...@@ -89,6 +101,15 @@ export class ChatGPTAPIBrowser {
this._page = this._page =
(await this._browser.pages())[0] || (await this._browser.newPage()) (await this._browser.pages())[0] || (await this._browser.newPage())
// bypass annoying popup modals
this._page.evaluateOnNewDocument(() => {
window.localStorage.setItem('oai/apps/hasSeenOnboarding/chat', 'true')
window.localStorage.setItem(
'oai/apps/hasSeenReleaseAnnouncement/2022-12-15',
'true'
)
})
await maximizePage(this._page) await maximizePage(this._page)
this._page.on('request', this._onRequest.bind(this)) this._page.on('request', this._onRequest.bind(this))
...@@ -140,15 +161,13 @@ export class ChatGPTAPIBrowser { ...@@ -140,15 +161,13 @@ export class ChatGPTAPIBrowser {
await delay(300) await delay(300)
} while (true) } while (true)
if (!this.getIsAuthenticated()) { if (!(await this.getIsAuthenticated())) {
return false throw new types.ChatGPTError('Failed to authenticate session')
} }
if (this._minimize) { if (this._minimize) {
await minimizePage(this._page) return minimizePage(this._page)
} }
return true
} }
_onRequest = (request: HTTPRequest) => { _onRequest = (request: HTTPRequest) => {
...@@ -221,11 +240,13 @@ export class ChatGPTAPIBrowser { ...@@ -221,11 +240,13 @@ export class ChatGPTAPIBrowser {
if (url.endsWith('/conversation')) { if (url.endsWith('/conversation')) {
if (status === 403) { if (status === 403) {
await this.handle403Error() await this.refreshSession()
} }
} else if (url.endsWith('api/auth/session')) { } else if (url.endsWith('api/auth/session')) {
if (status === 403) { if (status === 401) {
await this.handle403Error() await this.resetSession()
} else if (status === 403) {
await this.refreshSession()
} else { } else {
const session: types.SessionResult = body const session: types.SessionResult = body
...@@ -236,8 +257,30 @@ export class ChatGPTAPIBrowser { ...@@ -236,8 +257,30 @@ export class ChatGPTAPIBrowser {
} }
} }
async handle403Error() { /**
console.log(`ChatGPT "${this._email}" session expired; refreshing...`) * Attempts to handle 401 errors by re-authenticating.
*/
async resetSession() {
console.log(
`ChatGPT "${this._email}" session expired; re-authenticating...`
)
try {
await this.closeSession()
await this.initSession()
console.log(`ChatGPT "${this._email}" re-authenticated successfully`)
} catch (err) {
console.error(
`ChatGPT "${this._email}" error re-authenticating`,
err.toString()
)
}
}
/**
* Attempts to handle 403 errors by refreshing the page.
*/
async refreshSession() {
console.log(`ChatGPT "${this._email}" session expired (403); refreshing...`)
try { try {
await maximizePage(this._page) await maximizePage(this._page)
await this._page.reload({ await this._page.reload({
...@@ -247,6 +290,7 @@ export class ChatGPTAPIBrowser { ...@@ -247,6 +290,7 @@ export class ChatGPTAPIBrowser {
if (this._minimize) { if (this._minimize) {
await minimizePage(this._page) await minimizePage(this._page)
} }
console.log(`ChatGPT "${this._email}" refreshed session successfully`)
} catch (err) { } catch (err) {
console.error( console.error(
`ChatGPT "${this._email}" error refreshing session`, `ChatGPT "${this._email}" error refreshing session`,
...@@ -257,6 +301,10 @@ export class ChatGPTAPIBrowser { ...@@ -257,6 +301,10 @@ export class ChatGPTAPIBrowser {
async getIsAuthenticated() { async getIsAuthenticated() {
try { try {
if (!this._accessToken) {
return false
}
const inputBox = await this._getInputBox() const inputBox = await this._getInputBox()
return !!inputBox return !!inputBox
} catch (err) { } catch (err) {
...@@ -328,28 +376,25 @@ export class ChatGPTAPIBrowser { ...@@ -328,28 +376,25 @@ export class ChatGPTAPIBrowser {
// } // }
// } // }
async sendMessage( override async sendMessage(
message: string, message: string,
opts: types.SendMessageOptions = {} opts: types.SendMessageOptions = {}
): Promise<string> { ): Promise<types.ChatResponse> {
const { const {
conversationId, conversationId,
parentMessageId = uuidv4(), parentMessageId = uuidv4(),
messageId = uuidv4(), messageId = uuidv4(),
action = 'next', action = 'next',
timeoutMs
// TODO // TODO
timeoutMs, // onProgress
// onProgress,
onConversationResponse
} = opts } = opts
const inputBox = await this._getInputBox() if (!(await this.getIsAuthenticated())) {
if (!inputBox || !this._accessToken) {
console.log(`chatgpt re-authenticating ${this._email}`) console.log(`chatgpt re-authenticating ${this._email}`)
let isAuthenticated = false
try { try {
isAuthenticated = await this.init() await this.resetSession()
} catch (err) { } catch (err) {
console.warn( console.warn(
`chatgpt error re-authenticating ${this._email}`, `chatgpt error re-authenticating ${this._email}`,
...@@ -357,7 +402,7 @@ export class ChatGPTAPIBrowser { ...@@ -357,7 +402,7 @@ export class ChatGPTAPIBrowser {
) )
} }
if (!isAuthenticated || !this._accessToken) { if (!(await this.getIsAuthenticated())) {
const error = new types.ChatGPTError('Not signed in') const error = new types.ChatGPTError('Not signed in')
error.statusCode = 401 error.statusCode = 401
throw error throw error
...@@ -395,25 +440,20 @@ export class ChatGPTAPIBrowser { ...@@ -395,25 +440,20 @@ export class ChatGPTAPIBrowser {
) )
// console.log('<<< EVALUATE', result) // console.log('<<< EVALUATE', result)
if (result.error) { if ('error' in result) {
const error = new types.ChatGPTError(result.error.message) const error = new types.ChatGPTError(result.error.message)
error.statusCode = result.error.statusCode error.statusCode = result.error.statusCode
error.statusText = result.error.statusText error.statusText = result.error.statusText
if (error.statusCode === 403) { if (error.statusCode === 403) {
await this.handle403Error() await this.refreshSession()
} }
throw error throw error
} else {
return result
} }
// TODO: support sending partial response events
if (onConversationResponse) {
onConversationResponse(result.conversationResponse)
}
return result.response
// const lastMessage = await this.getLastMessage() // const lastMessage = await this.getLastMessage()
// await inputBox.focus() // await inputBox.focus()
...@@ -465,10 +505,11 @@ export class ChatGPTAPIBrowser { ...@@ -465,10 +505,11 @@ export class ChatGPTAPIBrowser {
} }
} }
async close() { override async closeSession() {
await this._browser.close() await this._browser.close()
this._page = null this._page = null
this._browser = null this._browser = null
this._accessToken = null
} }
protected async _getInputBox() { protected async _getInputBox() {
......
...@@ -3,7 +3,7 @@ import pTimeout from 'p-timeout' ...@@ -3,7 +3,7 @@ import pTimeout from 'p-timeout'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as types from './types' import * as types from './types'
import { ChatGPTConversation } from './chatgpt-conversation' import { AChatGPTAPI } from './abstract-chatgpt-api'
import { fetch } from './fetch' import { fetch } from './fetch'
import { fetchSSE } from './fetch-sse' import { fetchSSE } from './fetch-sse'
import { markdownToText } from './utils' import { markdownToText } from './utils'
...@@ -12,7 +12,7 @@ const KEY_ACCESS_TOKEN = 'accessToken' ...@@ -12,7 +12,7 @@ const KEY_ACCESS_TOKEN = 'accessToken'
const USER_AGENT = const USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
export class ChatGPTAPI { export class ChatGPTAPI extends AChatGPTAPI {
protected _sessionToken: string protected _sessionToken: string
protected _clearanceToken: string protected _clearanceToken: string
protected _markdown: boolean protected _markdown: boolean
...@@ -71,6 +71,8 @@ export class ChatGPTAPI { ...@@ -71,6 +71,8 @@ export class ChatGPTAPI {
/** @defaultValue `false` **/ /** @defaultValue `false` **/
debug?: boolean debug?: boolean
}) { }) {
super()
const { const {
sessionToken, sessionToken,
clearanceToken, clearanceToken,
...@@ -113,11 +115,15 @@ export class ChatGPTAPI { ...@@ -113,11 +115,15 @@ export class ChatGPTAPI {
} }
if (!this._sessionToken) { if (!this._sessionToken) {
throw new types.ChatGPTError('ChatGPT invalid session token') const error = new types.ChatGPTError('ChatGPT invalid session token')
error.statusCode = 401
throw error
} }
if (!this._clearanceToken) { if (!this._clearanceToken) {
throw new types.ChatGPTError('ChatGPT invalid clearance token') const error = new types.ChatGPTError('ChatGPT invalid clearance token')
error.statusCode = 401
throw error
} }
} }
...@@ -143,6 +149,14 @@ export class ChatGPTAPI { ...@@ -143,6 +149,14 @@ export class ChatGPTAPI {
return this._userAgent return this._userAgent
} }
/**
* Refreshes the client's access token which will succeed only if the session
* is valid.
*/
override async initSession() {
await this.refreshSession()
}
/** /**
* Sends a message to ChatGPT, waits for the response to resolve, and returns * Sends a message to ChatGPT, waits for the response to resolve, and returns
* the response. * the response.
...@@ -159,23 +173,21 @@ export class ChatGPTAPI { ...@@ -159,23 +173,21 @@ export class ChatGPTAPI {
* @param opts.action - Optional ChatGPT `action` (either `next` or `variant`) * @param opts.action - Optional ChatGPT `action` (either `next` or `variant`)
* @param opts.timeoutMs - Optional timeout in milliseconds (defaults to no timeout) * @param opts.timeoutMs - Optional timeout in milliseconds (defaults to no timeout)
* @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated * @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated
* @param opts.onConversationResponse - Optional callback which will be invoked every time the partial response is updated with the full conversation response
* @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) * @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
* *
* @returns The response from ChatGPT * @returns The response from ChatGPT
*/ */
async sendMessage( override async sendMessage(
message: string, message: string,
opts: types.SendMessageOptions = {} opts: types.SendMessageOptions = {}
): Promise<string> { ): Promise<types.ChatResponse> {
const { const {
conversationId, conversationId,
parentMessageId = uuidv4(), parentMessageId = uuidv4(),
messageId = uuidv4(), messageId = uuidv4(),
action = 'next', action = 'next',
timeoutMs, timeoutMs,
onProgress, onProgress
onConversationResponse
} = opts } = opts
let { abortSignal } = opts let { abortSignal } = opts
...@@ -186,7 +198,7 @@ export class ChatGPTAPI { ...@@ -186,7 +198,7 @@ export class ChatGPTAPI {
abortSignal = abortController.signal abortSignal = abortController.signal
} }
const accessToken = await this.refreshAccessToken() const accessToken = await this.refreshSession()
const body: types.ConversationJSONBody = { const body: types.ConversationJSONBody = {
action, action,
...@@ -208,9 +220,13 @@ export class ChatGPTAPI { ...@@ -208,9 +220,13 @@ export class ChatGPTAPI {
body.conversation_id = conversationId body.conversation_id = conversationId
} }
let response = '' const result: types.ChatResponse = {
conversationId,
messageId,
response: ''
}
const responseP = new Promise<string>((resolve, reject) => { const responseP = new Promise<types.ChatResponse>((resolve, reject) => {
const url = `${this._backendApiBaseUrl}/conversation` const url = `${this._backendApiBaseUrl}/conversation`
const headers = { const headers = {
...this._headers, ...this._headers,
...@@ -231,17 +247,22 @@ export class ChatGPTAPI { ...@@ -231,17 +247,22 @@ export class ChatGPTAPI {
signal: abortSignal, signal: abortSignal,
onMessage: (data: string) => { onMessage: (data: string) => {
if (data === '[DONE]') { if (data === '[DONE]') {
return resolve(response) return resolve(result)
} }
try { try {
const parsedData: types.ConversationResponseEvent = JSON.parse(data) const convoResponseEvent: types.ConversationResponseEvent =
if (onConversationResponse) { JSON.parse(data)
onConversationResponse(parsedData) if (convoResponseEvent.conversation_id) {
result.conversationId = convoResponseEvent.conversation_id
} }
const message = parsedData.message if (convoResponseEvent.message?.id) {
// console.log('event', JSON.stringify(parsedData, null, 2)) result.messageId = convoResponseEvent.message.id
}
const message = convoResponseEvent.message
// console.log('event', JSON.stringify(convoResponseEvent, null, 2))
if (message) { if (message) {
let text = message?.content?.parts?.[0] let text = message?.content?.parts?.[0]
...@@ -251,10 +272,10 @@ export class ChatGPTAPI { ...@@ -251,10 +272,10 @@ export class ChatGPTAPI {
text = markdownToText(text) text = markdownToText(text)
} }
response = text result.response = text
if (onProgress) { if (onProgress) {
onProgress(text) onProgress(result)
} }
} }
} }
...@@ -267,7 +288,7 @@ export class ChatGPTAPI { ...@@ -267,7 +288,7 @@ export class ChatGPTAPI {
const errMessageL = err.toString().toLowerCase() const errMessageL = err.toString().toLowerCase()
if ( if (
response && result.response &&
(errMessageL === 'error: typeerror: terminated' || (errMessageL === 'error: typeerror: terminated' ||
errMessageL === 'typeerror: terminated') errMessageL === 'typeerror: terminated')
) { ) {
...@@ -275,7 +296,7 @@ export class ChatGPTAPI { ...@@ -275,7 +296,7 @@ export class ChatGPTAPI {
// the HTTP request has resolved cleanly. In my testing, these cases tend to // the HTTP request has resolved cleanly. In my testing, these cases tend to
// happen when OpenAI has already send the last `response`, so we can ignore // happen when OpenAI has already send the last `response`, so we can ignore
// the `fetch` error in this case. // the `fetch` error in this case.
return resolve(response) return resolve(result)
} else { } else {
return reject(err) return reject(err)
} }
...@@ -301,7 +322,7 @@ export class ChatGPTAPI { ...@@ -301,7 +322,7 @@ export class ChatGPTAPI {
} }
async sendModeration(input: string) { async sendModeration(input: string) {
const accessToken = await this.refreshAccessToken() const accessToken = await this.refreshSession()
const url = `${this._backendApiBaseUrl}/moderations` const url = `${this._backendApiBaseUrl}/moderations`
const headers = { const headers = {
...this._headers, ...this._headers,
...@@ -343,23 +364,15 @@ export class ChatGPTAPI { ...@@ -343,23 +364,15 @@ export class ChatGPTAPI {
* @returns `true` if the client has a valid acces token or `false` if refreshing * @returns `true` if the client has a valid acces token or `false` if refreshing
* the token fails. * the token fails.
*/ */
async getIsAuthenticated() { override async getIsAuthenticated() {
try { try {
void (await this.refreshAccessToken()) void (await this.refreshSession())
return true return true
} catch (err) { } catch (err) {
return false return false
} }
} }
/**
* Refreshes the client's access token which will succeed only if the session
* is still valid.
*/
async ensureAuth() {
return await this.refreshAccessToken()
}
/** /**
* Attempts to refresh the current access token using the ChatGPT * Attempts to refresh the current access token using the ChatGPT
* `sessionToken` cookie. * `sessionToken` cookie.
...@@ -370,7 +383,7 @@ export class ChatGPTAPI { ...@@ -370,7 +383,7 @@ export class ChatGPTAPI {
* @returns A valid access token * @returns A valid access token
* @throws An error if refreshing the access token fails. * @throws An error if refreshing the access token fails.
*/ */
async refreshAccessToken(): Promise<string> { override async refreshSession(): Promise<string> {
const cachedAccessToken = this._accessTokenCache.get(KEY_ACCESS_TOKEN) const cachedAccessToken = this._accessTokenCache.get(KEY_ACCESS_TOKEN)
if (cachedAccessToken) { if (cachedAccessToken) {
return cachedAccessToken return cachedAccessToken
...@@ -454,17 +467,7 @@ export class ChatGPTAPI { ...@@ -454,17 +467,7 @@ export class ChatGPTAPI {
} }
} }
/** override async closeSession(): Promise<void> {
* Gets a new ChatGPTConversation instance, which can be used to send multiple this._accessTokenCache.delete(KEY_ACCESS_TOKEN)
* messages as part of a single conversation.
*
* @param opts.conversationId - Optional ID of the previous message in a conversation
* @param opts.parentMessageId - Optional ID of the previous message in a conversation
* @returns The new conversation instance
*/
getConversation(
opts: { conversationId?: string; parentMessageId?: string } = {}
) {
return new ChatGPTConversation(this, opts)
} }
} }
import * as types from './types'
import { type ChatGPTAPI } from './chatgpt-api'
/**
* A conversation wrapper around the ChatGPTAPI. 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.
*/
export class ChatGPTConversation {
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
* @param opts.conversationId - Optional ID of a conversation to continue
* @param opts.parentMessageId - Optional ID of the previous message in the conversation
*/
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.
*
* @param message - The prompt message to send
* @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated
* @param opts.onConversationResponse - Optional callback which will be invoked every time the partial response is updated with the full conversation response
* @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
*
* @returns The response from ChatGPT
*/
async sendMessage(
message: string,
opts: types.SendConversationMessageOptions = {}
): Promise<string> {
const { onConversationResponse, ...rest } = opts
return this.api.sendMessage(message, {
...rest,
conversationId: this.conversationId,
parentMessageId: this.parentMessageId,
onConversationResponse: (response) => {
if (response.conversation_id) {
this.conversationId = response.conversation_id
}
if (response.message?.id) {
this.parentMessageId = response.message.id
}
if (onConversationResponse) {
return onConversationResponse(response)
}
}
})
}
}
export * from './chatgpt-api' export * from './chatgpt-api'
export * from './chatgpt-api-browser' export * from './chatgpt-api-browser'
export * from './chatgpt-conversation' export * from './abstract-chatgpt-api'
export * from './types' export * from './types'
export * from './utils' export * from './utils'
export * from './openai-auth' export * from './openai-auth'
...@@ -281,8 +281,7 @@ export type SendMessageOptions = { ...@@ -281,8 +281,7 @@ export type SendMessageOptions = {
messageId?: string messageId?: string
action?: MessageActionType action?: MessageActionType
timeoutMs?: number timeoutMs?: number
onProgress?: (partialResponse: string) => void onProgress?: (partialResponse: ChatResponse) => void
onConversationResponse?: (response: ConversationResponseEvent) => void
abortSignal?: AbortSignal abortSignal?: AbortSignal
} }
...@@ -300,16 +299,12 @@ export class ChatGPTError extends Error { ...@@ -300,16 +299,12 @@ export class ChatGPTError extends Error {
export type ChatError = { export type ChatError = {
error: { message: string; statusCode?: number; statusText?: string } error: { message: string; statusCode?: number; statusText?: string }
response: null
conversationId?: string conversationId?: string
messageId?: string messageId?: string
conversationResponse?: ConversationResponseEvent
} }
export type ChatResponse = { export type ChatResponse = {
error: null
response: string response: string
conversationId: string conversationId: string
messageId: string messageId: string
conversationResponse?: ConversationResponseEvent
} }
...@@ -37,7 +37,7 @@ export async function maximizePage(page: Page) { ...@@ -37,7 +37,7 @@ export async function maximizePage(page: Page) {
} }
export function isRelevantRequest(url: string): boolean { export function isRelevantRequest(url: string): boolean {
let pathname let pathname: string
try { try {
const parsedUrl = new URL(url) const parsedUrl = new URL(url)
...@@ -102,7 +102,6 @@ export async function browserPostEventStream( ...@@ -102,7 +102,6 @@ export async function browserPostEventStream(
const BOM = [239, 187, 191] const BOM = [239, 187, 191]
let conversationResponse: types.ConversationResponseEvent
let conversationId: string = body?.conversation_id let conversationId: string = body?.conversation_id
let messageId: string = body?.messages?.[0]?.id let messageId: string = body?.messages?.[0]?.id
let response = '' let response = ''
...@@ -136,7 +135,6 @@ export async function browserPostEventStream( ...@@ -136,7 +135,6 @@ export async function browserPostEventStream(
statusCode: res.status, statusCode: res.status,
statusText: res.statusText statusText: res.statusText
}, },
response: null,
conversationId, conversationId,
messageId messageId
} }
...@@ -147,18 +145,15 @@ export async function browserPostEventStream( ...@@ -147,18 +145,15 @@ export async function browserPostEventStream(
function onMessage(data: string) { function onMessage(data: string) {
if (data === '[DONE]') { if (data === '[DONE]') {
return resolve({ return resolve({
error: null,
response, response,
conversationId, conversationId,
messageId, messageId
conversationResponse
}) })
} }
try { try {
const convoResponseEvent: types.ConversationResponseEvent = const convoResponseEvent: types.ConversationResponseEvent =
JSON.parse(data) JSON.parse(data)
conversationResponse = convoResponseEvent
if (convoResponseEvent.conversation_id) { if (convoResponseEvent.conversation_id) {
conversationId = convoResponseEvent.conversation_id conversationId = convoResponseEvent.conversation_id
} }
...@@ -220,11 +215,9 @@ export async function browserPostEventStream( ...@@ -220,11 +215,9 @@ export async function browserPostEventStream(
// happen when OpenAI has already send the last `response`, so we can ignore // happen when OpenAI has already send the last `response`, so we can ignore
// the `fetch` error in this case. // the `fetch` error in this case.
return { return {
error: null,
response, response,
conversationId, conversationId,
messageId, messageId
conversationResponse
} }
} }
...@@ -234,10 +227,8 @@ export async function browserPostEventStream( ...@@ -234,10 +227,8 @@ export async function browserPostEventStream(
statusCode: err.statusCode || err.status || err.response?.statusCode, statusCode: err.statusCode || err.status || err.response?.statusCode,
statusText: err.statusText || err.response?.statusText statusText: err.statusText || err.response?.statusText
}, },
response: null,
conversationId, conversationId,
messageId, messageId
conversationResponse
} }
} }
...@@ -456,7 +447,7 @@ export async function browserPostEventStream( ...@@ -456,7 +447,7 @@ export async function browserPostEventStream(
customTimers = { setTimeout, clearTimeout } customTimers = { setTimeout, clearTimeout }
} = options } = options
let timer let timer: number
const cancelablePromise = new Promise((resolve, reject) => { const cancelablePromise = new Promise((resolve, reject) => {
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) { if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment