Commit e0fd5f46 authored by Travis Fischer's avatar Travis Fischer

feat: add onProgress to ChatGPTAPIBrowser.sendMessage

parent 525524b8
import dotenv from 'dotenv-safe'
import { oraPromise } from 'ora'
import { ChatGPTAPIBrowser } from '../src'
dotenv.config()
/**
* Demo CLI for testing the `onProgress` handler.
*
* ```
* npx tsx demos/demo-on-progress.ts
* ```
*/
async function main() {
const email = process.env.OPENAI_EMAIL
const password = process.env.OPENAI_PASSWORD
const api = new ChatGPTAPIBrowser({
email,
password,
debug: false,
minimize: true
})
await api.initSession()
const prompt =
'Write a python version of bubble sort. Do not include example usage.'
console.log(prompt)
const res = await api.sendMessage(prompt, {
onProgress: (partialResponse) => {
console.log('p')
console.log('progress', partialResponse?.response)
}
})
console.log(res.response)
// close the browser at the end
await api.closeSession()
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
...@@ -197,7 +197,19 @@ A [basic demo](./demos/demo.ts) is included for testing purposes: ...@@ -197,7 +197,19 @@ A [basic demo](./demos/demo.ts) is included for testing purposes:
npx tsx demos/demo.ts npx tsx demos/demo.ts
``` ```
A [conversation demo](./demos/demo-conversation.ts) is also included: A [google auth demo](./demos/demo-google-auth.ts):
```bash
npx tsx demos/demo-google-auth.ts
```
A [demo showing on progress handler](./demos/demo-on-progress.ts):
```bash
npx tsx demos/demo-on-progress.ts
```
A [conversation demo](./demos/demo-conversation.ts):
```bash ```bash
npx tsx demos/demo-conversation.ts npx tsx demos/demo-conversation.ts
......
...@@ -33,6 +33,10 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -33,6 +33,10 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
protected _page: Page protected _page: Page
protected _proxyServer: string protected _proxyServer: string
protected _isRefreshing: boolean protected _isRefreshing: boolean
protected _messageOnProgressHandlers: Record<
string,
(partialResponse: types.ChatResponse) => void
>
/** /**
* Creates a new client for automating the ChatGPT webapp. * Creates a new client for automating the ChatGPT webapp.
...@@ -97,6 +101,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -97,6 +101,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
this._executablePath = executablePath this._executablePath = executablePath
this._proxyServer = proxyServer this._proxyServer = proxyServer
this._isRefreshing = false this._isRefreshing = false
this._messageOnProgressHandlers = {}
if (!this._email) { if (!this._email) {
const error = new types.ChatGPTError('ChatGPT invalid email') const error = new types.ChatGPTError('ChatGPT invalid email')
...@@ -196,6 +201,24 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -196,6 +201,24 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
}) })
} }
// TODO: will this exist after page reload and navigation?
await this._page.exposeFunction(
'ChatGPTAPIBrowserOnProgress',
(partialResponse: types.ChatResponse) => {
if ((partialResponse as any)?.origMessageId) {
const onProgress =
this._messageOnProgressHandlers[
(partialResponse as any).origMessageId
]
if (onProgress) {
onProgress(partialResponse)
return
}
}
}
)
// dismiss welcome modal (and other modals) // dismiss welcome modal (and other modals)
do { do {
const modalSelector = '[data-headlessui-state="open"]' const modalSelector = '[data-headlessui-state="open"]'
...@@ -482,9 +505,8 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -482,9 +505,8 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
parentMessageId = uuidv4(), parentMessageId = uuidv4(),
messageId = uuidv4(), messageId = uuidv4(),
action = 'next', action = 'next',
timeoutMs timeoutMs,
// TODO onProgress
// onProgress
} = opts } = opts
const url = `https://chat.openai.com/backend-api/conversation` const url = `https://chat.openai.com/backend-api/conversation`
...@@ -508,6 +530,16 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -508,6 +530,16 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
body.conversation_id = conversationId body.conversation_id = conversationId
} }
if (onProgress) {
this._messageOnProgressHandlers[messageId] = onProgress
}
const cleanup = () => {
if (this._messageOnProgressHandlers[messageId]) {
delete this._messageOnProgressHandlers[messageId]
}
}
let result: types.ChatResponse | types.ChatError let result: types.ChatResponse | types.ChatError
let numTries = 0 let numTries = 0
let is401 = false let is401 = false
...@@ -528,6 +560,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -528,6 +560,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
if (!(await this.getIsAuthenticated())) { 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
cleanup()
throw error throw error
} }
} }
...@@ -551,6 +584,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -551,6 +584,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
const error = new types.ChatGPTError(err.toString()) const error = new types.ChatGPTError(err.toString())
error.statusCode = err.response?.statusCode error.statusCode = err.response?.statusCode
error.statusText = err.response?.statusText error.statusText = err.response?.statusText
cleanup()
throw error throw error
} }
...@@ -570,6 +604,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -570,6 +604,7 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
is401 = true is401 = true
if (numTries >= 2) { if (numTries >= 2) {
cleanup()
throw error throw error
} else { } else {
continue continue
...@@ -590,10 +625,12 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI { ...@@ -590,10 +625,12 @@ export class ChatGPTAPIBrowser extends AChatGPTAPI {
result.response = markdownToText(result.response) result.response = markdownToText(result.response)
} }
cleanup()
return result return result
} }
} while (!result) } while (!result)
cleanup()
// console.log('<<< EVALUATE', result) // console.log('<<< EVALUATE', result)
// const lastMessage = await this.getLastMessage() // const lastMessage = await this.getLastMessage()
......
...@@ -272,6 +272,7 @@ export async function getBrowser( ...@@ -272,6 +272,7 @@ export async function getBrowser(
nopechaKey?: string nopechaKey?: string
proxyServer?: string proxyServer?: string
minimize?: boolean minimize?: boolean
debug?: boolean
timeoutMs?: number timeoutMs?: number
} = {} } = {}
) { ) {
...@@ -281,6 +282,7 @@ export async function getBrowser( ...@@ -281,6 +282,7 @@ export async function getBrowser(
executablePath = defaultChromeExecutablePath(), executablePath = defaultChromeExecutablePath(),
proxyServer = process.env.PROXY_SERVER, proxyServer = process.env.PROXY_SERVER,
minimize = false, minimize = false,
debug = false,
timeoutMs = DEFAULT_TIMEOUT_MS, timeoutMs = DEFAULT_TIMEOUT_MS,
...launchOptions ...launchOptions
} = opts } = opts
...@@ -387,8 +389,9 @@ export async function getBrowser( ...@@ -387,8 +389,9 @@ export async function getBrowser(
} }
await initializeNopechaExtension(browser, { await initializeNopechaExtension(browser, {
minimize,
nopechaKey, nopechaKey,
minimize,
debug,
timeoutMs timeoutMs
}) })
...@@ -398,12 +401,13 @@ export async function getBrowser( ...@@ -398,12 +401,13 @@ export async function getBrowser(
export async function initializeNopechaExtension( export async function initializeNopechaExtension(
browser: Browser, browser: Browser,
opts: { opts: {
minimize?: boolean
nopechaKey?: string nopechaKey?: string
minimize?: boolean
debug?: boolean
timeoutMs?: number timeoutMs?: number
} }
) { ) {
const { minimize = false, nopechaKey } = opts const { minimize = false, debug = false, nopechaKey } = opts
if (hasNopechaExtension) { if (hasNopechaExtension) {
const page = (await browser.pages())[0] || (await browser.newPage()) const page = (await browser.pages())[0] || (await browser.newPage())
...@@ -411,7 +415,9 @@ export async function initializeNopechaExtension( ...@@ -411,7 +415,9 @@ export async function initializeNopechaExtension(
await minimizePage(page) await minimizePage(page)
} }
console.log('initializing nopecha extension with key', nopechaKey, '...') if (debug) {
console.log('initializing nopecha extension with key', nopechaKey, '...')
}
// TODO: setting the nopecha extension key is really, really error prone... // TODO: setting the nopecha extension key is really, really error prone...
for (let i = 0; i < 5; ++i) { for (let i = 0; i < 5; ++i) {
......
...@@ -9,6 +9,12 @@ import stripMarkdown from 'strip-markdown' ...@@ -9,6 +9,12 @@ import stripMarkdown from 'strip-markdown'
import * as types from './types' import * as types from './types'
declare global {
function ChatGPTAPIBrowserOnProgress(
partialChatResponse: types.ChatResponse
): Promise<void>
}
export function markdownToText(markdown?: string): string { export function markdownToText(markdown?: string): string {
return remark() return remark()
.use(stripMarkdown) .use(stripMarkdown)
...@@ -103,6 +109,7 @@ export async function browserPostEventStream( ...@@ -103,6 +109,7 @@ export async function browserPostEventStream(
const BOM = [239, 187, 191] const BOM = [239, 187, 191]
let conversationId: string = body?.conversation_id let conversationId: string = body?.conversation_id
const origMessageId = body?.messages?.[0]?.id
let messageId: string = body?.messages?.[0]?.id let messageId: string = body?.messages?.[0]?.id
let response = '' let response = ''
...@@ -142,7 +149,7 @@ export async function browserPostEventStream( ...@@ -142,7 +149,7 @@ export async function browserPostEventStream(
const responseP = new Promise<types.ChatResponse>( const responseP = new Promise<types.ChatResponse>(
async (resolve, reject) => { async (resolve, reject) => {
function onMessage(data: string) { async function onMessage(data: string) {
if (data === '[DONE]') { if (data === '[DONE]') {
return resolve({ return resolve({
response, response,
...@@ -150,16 +157,24 @@ export async function browserPostEventStream( ...@@ -150,16 +157,24 @@ export async function browserPostEventStream(
messageId messageId
}) })
} }
let convoResponseEvent: types.ConversationResponseEvent
try { try {
const checkJson = JSON.parse(data) convoResponseEvent = JSON.parse(data)
} catch (error) { } catch (err) {
console.log('warning: parse error.') console.warn(
'warning: chatgpt even stream parse error',
err.toString(),
data
)
return
}
if (!convoResponseEvent) {
return return
} }
try { try {
const convoResponseEvent: types.ConversationResponseEvent =
JSON.parse(data)
if (convoResponseEvent.conversation_id) { if (convoResponseEvent.conversation_id) {
conversationId = convoResponseEvent.conversation_id conversationId = convoResponseEvent.conversation_id
} }
...@@ -172,6 +187,17 @@ export async function browserPostEventStream( ...@@ -172,6 +187,17 @@ export async function browserPostEventStream(
convoResponseEvent.message?.content?.parts?.[0] convoResponseEvent.message?.content?.parts?.[0]
if (partialResponse) { if (partialResponse) {
response = partialResponse response = partialResponse
if (window.ChatGPTAPIBrowserOnProgress) {
const partialChatResponse = {
origMessageId,
response,
conversationId,
messageId
}
await window.ChatGPTAPIBrowserOnProgress(partialChatResponse)
}
} }
} catch (err) { } catch (err) {
console.warn('fetchSSE onMessage unexpected error', err) console.warn('fetchSSE onMessage unexpected error', err)
......
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