pull/3/head
Travis Fischer 2022-12-02 17:43:59 -06:00
commit 8ef2c77a10
19 zmienionych plików z 4094 dodań i 0 usunięć

14
.env.example 100644
Wyświetl plik

@ -0,0 +1,14 @@
# ------------------------------------------------------------------------------
# This is an example .env file.
#
# All of these environment vars must be defined either in your environment or in
# a local .env file in order to run this app.
#
# @see https://nextjs.org/docs/basic-features/environment-variables for details.
# ------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Replicate API
# -----------------------------------------------------------------------------
REPLICATE_API_TOKEN=

1
.github/funding.yml vendored 100644
Wyświetl plik

@ -0,0 +1 @@
github: [transitive-bullshit]

51
.github/workflows/test.yml vendored 100644
Wyświetl plik

@ -0,0 +1,51 @@
name: CI
on: [push, pull_request]
jobs:
test:
name: Test Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version:
- 18
- 16
- 14
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install pnpm
uses: pnpm/action-setup@v2
id: pnpm-install
with:
version: 7
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run test
run: pnpm run test

49
.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1,49 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
*.swp
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# local env files
.env
.env.local
.env.build
.env.development.local
.env.test.local
.env.production.local
# data dumps
out/

Wyświetl plik

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run pre-commit

1
.npmrc 100644
Wyświetl plik

@ -0,0 +1 @@
enable-pre-post-scripts=true

6
.prettierignore 100644
Wyświetl plik

@ -0,0 +1,6 @@
.snapshots/
build/
dist/
node_modules/
.next/
.vercel/

16
.prettierrc.cjs 100644
Wyświetl plik

@ -0,0 +1,16 @@
module.exports = {
plugins: [require('@trivago/prettier-plugin-sort-imports')],
singleQuote: true,
jsxSingleQuote: true,
semi: false,
useTabs: false,
tabWidth: 2,
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'always',
trailingComma: 'none',
importOrder: ['^node:.*', '<THIRD_PARTY_MODULES>', '^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderGroupNamespaceSpecifiers: true
}

21
license 100644
Wyświetl plik

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Travis Fischer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

78
package.json 100644
Wyświetl plik

@ -0,0 +1,78 @@
{
"name": "chatgpt",
"version": "0.0.1",
"description": "Node.js wrapper around ChatGPT. Uses headless Chrome as a temporary solution until the official API is released.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"repository": "transitive-bullshit/chatgpt-api",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
"exports": {
"import": "./build/index.js",
"default": "./build/index.js",
"types": "./build/index.d.ts"
},
"files": [
"build"
],
"engines": {
"node": ">=14"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"clean": "del build",
"prebuild": "run-s clean",
"predev": "run-s clean",
"pretest": "run-s build",
"docs": "typedoc",
"prepare": "husky install",
"pre-commit": "lint-staged",
"test": "run-p test:*",
"test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check"
},
"dependencies": {
"html-to-md": "npm:@fisch0920/html-to-md@^0.8.1",
"joplin-turndown": "^4.0.30",
"node-html-markdown": "^1.2.2",
"playwright": "^1.28.1",
"turndown": "^7.1.1"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@types/node": "^18.11.9",
"del-cli": "^5.0.0",
"delay": "^5.0.0",
"husky": "^8.0.2",
"lint-staged": "^13.0.3",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.0.2",
"ora": "^6.1.2",
"prettier": "^2.8.0",
"tsup": "^6.5.0",
"tsx": "^3.12.1",
"typedoc": "^0.23.21",
"typedoc-plugin-markdown": "^3.13.6",
"typescript": "^4.9.3"
},
"lint-staged": {
"*.{ts,tsx}": [
"prettier --write"
]
},
"keywords": [
"openai",
"chatgpt",
"gpt",
"gpt3",
"gpt4",
"chatbot",
"chat",
"machine learning",
"conversation",
"conversational ai",
"ai",
"ml",
"bot"
]
}

3548
pnpm-lock.yaml 100644

Plik diff jest za duży Load Diff

34
readme.md 100644
Wyświetl plik

@ -0,0 +1,34 @@
# ChatGPT API <!-- omit in toc -->
> Node.js wrapper around ChatGPT. Uses headless Chrome as a temporary solution until the official API is released.
[![NPM](https://img.shields.io/npm/v/chatgpt.svg)](https://www.npmjs.com/package/chatgpt) [![Build Status](https://github.com/transitive-bullshit/chatgpt-api/actions/workflows/test.yml/badge.svg)](https://github.com/transitive-bullshit/chatgpt-api/actions/workflows/test.yml) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/transitive-bullshit/chatgpt-api/blob/main/license) [![Prettier Code Formatting](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io)
- [Intro](#intro)
- [Docs](#docs)
- [Todo](#todo)
- [Related](#related)
- [License](#license)
## Intro
TODO
## Docs
See the [auto-generated docs](./docs/modules.md).
## Todo
- [ ] Add message and conversation IDs
- [ ] Add support for streaming responses
## Related
- Inspired by the [Go module](https://github.com/danielgross/whatsapp-gpt) by [Daniel Gross](https://github.com/danielgross)
## License
MIT © [Travis Fischer](https://transitivebullsh.it)
Support my open source work by <a href="https://twitter.com/transitive_bs">following me on twitter <img src="https://storage.googleapis.com/saasify-assets/twitter-logo.svg" alt="twitter" height="24px" align="center"></a>

170
src/chatgpt-api.ts 100644
Wyświetl plik

@ -0,0 +1,170 @@
import delay from 'delay'
import html2md from 'html-to-md'
import { type ChromiumBrowserContext, type Page, chromium } from 'playwright'
export class ChatGPTAPI {
protected _userDataDir: string
protected _headless: boolean
protected _markdown: boolean
protected _chatUrl: string
protected _browser: ChromiumBrowserContext
protected _page: Page
/**
* @param opts.userDataDir  Path to a directory for storing persistent chromium session data
* @param opts.chatUrl  OpenAI chat URL
* @param opts.headless - Whether or not to use headless mode
* @param opts.markdown  Whether or not to parse chat messages as markdown
*/
constructor(
opts: {
/** @defaultValue `'/tmp/chatgpt'` **/
userDataDir?: string
/** @defaultValue `'https://chat.openai.com/'` **/
chatUrl?: string
/** @defaultValue `false` **/
headless?: boolean
/** @defaultValue `true` **/
markdown?: boolean
} = {}
) {
const {
userDataDir = '/tmp/chatgpt',
chatUrl = 'https://chat.openai.com/',
headless = false,
markdown = true
} = opts
this._userDataDir = userDataDir
this._headless = !!headless
this._chatUrl = chatUrl
this._markdown = !!markdown
}
async init() {
this._browser = await chromium.launchPersistentContext(this._userDataDir, {
headless: this._headless
})
this._page = await this._browser.newPage()
await this._page.goto(this._chatUrl)
// dismiss welcome modal
do {
const modalSelector = '[data-headlessui-state="open"]'
if (!(await this._page.isVisible(modalSelector, { timeout: 500 }))) {
break
}
const modal = await this._page.locator(modalSelector)
if (modal) {
await modal.locator('button').last().click()
} else {
break
}
} while (true)
return this._page
}
async getIsSignedIn() {
const inputBox = await this._getInputBox()
return !!inputBox
}
async getLastMessage(): Promise<string | null> {
const messages = await this.getMessages()
if (messages) {
return messages[messages.length - 1]
} else {
return null
}
}
async getPrompts(): Promise<string[]> {
// Get all prompts
const messages = await this._page.$$(
'[class*="ConversationItem__Message"]:has([class*="ConversationItem__ActionButtons"]):has([class*="ConversationItem__Role"] [class*="Avatar__Wrapper"])'
)
// prompts are always plaintext
return Promise.all(messages.map((a) => a.innerText()))
}
async getMessages(): Promise<string[]> {
// Get all complete messages
// (in-progress messages that are being streamed back don't contain action buttons)
const messages = await this._page.$$(
'[class*="ConversationItem__Message"]:has([class*="ConversationItem__ActionButtons"]):not(:has([class*="ConversationItem__Role"] [class*="Avatar__Wrapper"]))'
)
if (this._markdown) {
const htmlMessages = await Promise.all(messages.map((a) => a.innerHTML()))
const markdownMessages = htmlMessages.map((messageHtml) => {
// parse markdown from message HTML
messageHtml = messageHtml.replace('Copy code</button>', '</button>')
return html2md(messageHtml, {
ignoreTags: [
'button',
'svg',
'style',
'form',
'noscript',
'script',
'meta',
'head'
],
skipTags: ['button', 'svg']
})
})
return markdownMessages
} else {
// plaintext
const plaintextMessages = await Promise.all(
messages.map((a) => a.innerText())
)
return plaintextMessages
}
}
async sendMessage(message: string): Promise<string> {
const inputBox = await this._getInputBox()
if (!inputBox) throw new Error('not signed in')
const lastMessage = await this.getLastMessage()
await inputBox.click()
await inputBox.fill(message)
await inputBox.press('Enter')
do {
await delay(1000)
// TODO: this logic needs some work because we can have repeat messages...
const newLastMessage = await this.getLastMessage()
if (
newLastMessage &&
lastMessage?.toLowerCase() !== newLastMessage?.toLowerCase()
) {
return newLastMessage
}
} while (true)
}
async close() {
return await this._browser.close()
}
protected async _getInputBox(): Promise<any> {
return this._page.$(
'div[class*="PromptTextarea__TextareaWrapper"] textarea'
)
}
}

52
src/example.ts 100644
Wyświetl plik

@ -0,0 +1,52 @@
import delay from 'delay'
import { oraPromise } from 'ora'
import { ChatGPTAPI } from './chatgpt-api'
/**
* Example CLI for testing functionality.
*/
async function main() {
const api = new ChatGPTAPI()
await api.init()
const isSignedIn = await api.getIsSignedIn()
if (!isSignedIn) {
// Wait until the user signs in via the chromium browser
await oraPromise(
new Promise<void>(async (resolve, reject) => {
try {
await delay(1000)
const isSignedIn = await api.getIsSignedIn()
if (isSignedIn) {
return resolve()
}
} catch (err) {
return reject(err)
}
}),
'Please sign in to ChatGPT'
)
}
const response = await api.sendMessage(
// 'Write a TypeScript function for conway sort.'
'Write a python version of bubble sort. Do not include example usage.'
)
// const prompts = await api.getPrompts()
// const messages = await api.getMessages()
// console.log('prompts', prompts)
// console.log('messages', messages)
// Wait forever; useful for debugging chromium session
// await new Promise(() => {})
await api.close()
return response
}
main().then((res) => {
console.log(res)
})

1
src/index.ts 100644
Wyświetl plik

@ -0,0 +1 @@
export * from './chatgpt-api'

0
src/types.ts 100644
Wyświetl plik

20
tsconfig.json 100644
Wyświetl plik

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"baseUrl": ".",
"outDir": "build",
"noEmit": true
},
"exclude": ["node_modules", "build"],
"include": ["**/*.ts"]
}

14
tsup.config.ts 100644
Wyświetl plik

@ -0,0 +1,14 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'build',
target: 'node14',
platform: 'node',
format: ['esm'],
splitting: false,
sourcemap: true,
minify: true,
shims: false,
dts: true
})

14
typedoc.json 100644
Wyświetl plik

@ -0,0 +1,14 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["./src/index.ts"],
"exclude": ["**/*.test.ts"],
"plugin": ["typedoc-plugin-markdown"],
"out": "docs",
"hideBreadcrumbs": false,
"hideInPageTOC": false,
"excludePrivate": true,
"excludeProtected": true,
"excludeExternals": true,
"excludeInternal": true,
"entryDocument": "readme.md"
}