diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..e5b6d8d6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..15320837 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "jxnl/instructor-js" }], + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "ignore": [], + "privatePackages": { + "version": true, + "tag": false + } +} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..9737c557 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,23 @@ +module.exports = { + $schema: "https://json.schemastore.org/eslintrc", + root: true, + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint", "prettier"], + extends: ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], + rules: { + "prettier/prettier": "error", + "linebreak-style": "off", + "semi": "off", + "indent": "off", + "@typescript-eslint/semi": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_" + } + ] + } +} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..99787459 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish changesets + +on: + pull_request: + types: + - closed + +concurrency: ${{ github.workflow }}-${{ github.ref }} +permissions: {} #reset +jobs: + publish: + if: github.repository == 'jxnl/instructor-js' && github.event.pull_request.merged == true && (startsWith(github.head_ref, 'changeset-release/main') || startsWith(github.head_ref, '_publish-trigger')) + permissions: + contents: write + pull-requests: write + + name: Publish packages + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.9.0 + + - name: Install Dependencies + run: bun i + + - name: Creating .npmrc + run: | + cat << EOF > "$HOME/.npmrc" + //registry.npmjs.org/:_authToken=$NPM_TOKEN + EOF + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish packages & create release + id: changesets + uses: changesets/action@v1 + with: + publish: bun publish-packages + createGithubReleases: true + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 00000000..991fb138 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,53 @@ +name: Create changeset release PR + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} +permissions: {} #reset +env: + CI: true + +jobs: + release: + if: github.repository == 'jxnl/instructor-js' + permissions: + contents: write + pull-requests: write + + name: Changeset Release + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.9.0 + + - name: Setup bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Setup npmrc + run: echo "//npm.pkg.github.com/:_authToken=$NPM_TOKEN" > .npmrc + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Install Dependencies + run: bun i + + - name: Create version PR + uses: changesets/action@v1 + with: + version: bun run version-packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..c8ad6f48 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test-wf +on: [push] + +jobs: + + test: + name: run-tests + runs-on: ubuntu-latest + environment: OPENAI + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.9.0 + + - name: Install Dependencies + run: bun i + + - run: bun run type-check + - run: bun run lint + + - run: bun test diff --git a/.gitignore b/.gitignore index b3eb616a..b8449cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,91 @@ -node_modules/ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# 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* + +# local env files .env -dist/ \ No newline at end of file +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel +.turbo + +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +# vercel +.vercel + +dist + +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# vim +*.sw* + +# env +.env*.local +.envrc + + +tsconfig.tsbuildinfo + diff --git a/.prettierignore b/.prettierignore index d84261f1..2e7676f2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,27 @@ -/node_modules +node_modules +.next +build +dist +*.tsbuildinfo +*.gitignore +*.svg +*.lock +*.npmignore +*.sql +*.png +*.jpg +*.jpeg +*.gif +*.ico +*.sh +Dockerfile +Dockerfile.* +.env +.env.* +LICENSE +*.log +.DS_Store +.dockerignore +*.patch +*.toml -# don't format tsc output, will break source maps -/dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..ce6c89e9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,28 @@ +{ + "endOfLine": "lf", + "semi": false, + "trailingComma": "none", + "arrowParens": "avoid", + "singleQuote": false, + "tabWidth": 2, + "importOrder": [ + "^(react/(.*)$)|^(react$)", + "^(next/(.*)$)|^(next$)", + "", + "", + "^types$", + "^@/types/(.*)$", + "^@/config/(.*)$", + "^@/constants/(.*)$", + "^@/lib/(.*)$", + "", + "^[./]" + ], + "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], + "plugins": [ + "@ianvs/prettier-plugin-sort-imports" + ], + "printWidth": 100, + "proseWrap": "never", + "quoteProps": "consistent" +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..cfe83282 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +nodejs 20.9.0 +bun 1.0.15 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9b64678f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,51 @@ +{ + "editor.defaultFormatter": "GitHub.copilot", + "editor.tabSize": 2, + "editor.formatOnSave": false, + "editor.formatOnType": false, + "editor.formatOnPaste": false, + "editor.trimAutoWhitespace": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + }, + "[html]": { + "editor.defaultFormatter": "GitHub.copilot" + }, + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features", + "editor.autoIndent": "advanced" + }, + "[yaml]": { + "editor.autoIndent": "advanced", + "editor.tabSize": 2, + "editor.formatOnSave": true, + "editor.formatOnType": false, + "editor.formatOnPaste": true, + }, + "eslint.validate": [ + "javascript", + "typescript", + "typescriptreact", + ], + "eslint.debug": true, + "typescript.tsdk": "./node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.validate.enable": true, + "editor.quickSuggestions": { + "strings": true + }, + "css.validate": false, + "editor.inlineSuggest.enabled": true, + "[typescriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features", + } +} diff --git a/bun.d.ts b/bun.d.ts new file mode 100644 index 00000000..22f6a666 --- /dev/null +++ b/bun.d.ts @@ -0,0 +1 @@ +/// diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 00000000..f15e43b9 Binary files /dev/null and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..dcd8084d --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install] +peer = true \ No newline at end of file diff --git a/examples/extract_user/index.ts b/examples/extract_user/index.ts new file mode 100644 index 00000000..710df601 --- /dev/null +++ b/examples/extract_user/index.ts @@ -0,0 +1,33 @@ +import Instructor from "@/instructor" +import OpenAI from "openai" +import { z } from "zod" + +const UserSchema = z.object({ + age: z.number(), + name: z.string().refine(name => name.includes(" "), { + message: "Name must contain a space" + }) +}) + +type User = z.infer + +const oai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY ?? undefined, + organization: process.env.OPENAI_ORG_ID ?? undefined +}) + +const client = Instructor({ + client: oai, + mode: "FUNCTIONS" +}) + +//@ts-expect-error these types wont work since were using a proxy and just returning the OAI instance type +const user: User = await client.chat.completions.create({ + messages: [{ role: "user", content: "Jason Liu is 30 years old" }], + model: "gpt-3.5-turbo", + //@ts-expect-error same as above + response_model: UserSchema, + max_retries: 3 +}) + +console.log(user) diff --git a/examples/extract_user/run.ts b/examples/extract_user/run.ts deleted file mode 100644 index e9aecced..00000000 --- a/examples/extract_user/run.ts +++ /dev/null @@ -1,24 +0,0 @@ -import OpenAI from "openai"; -import { instructor } from "../../src"; -import { z } from "zod"; - -const client = instructor.patch({ - client: new OpenAI(), - mode: instructor.MODE.TOOLS, -}); - -client.chat.completions - .create({ - messages: [{ role: "user", content: "Jason Liu is 30 years old" }], - model: "gpt-3.5-turbo", - // @ts-ignore TODO fix type issue - response_model: z.object({ - age: z.number(), - // name should be uppercase, or - name: z.string().refine((name) => name === name.toUpperCase(), { - message: "name should have uppercase", - }), - }), - max_retries: 3, - }) - .then(console.log); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1a756cc3..00000000 --- a/package-lock.json +++ /dev/null @@ -1,333 +0,0 @@ -{ - "name": "instructor", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "instructor", - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "openai": "^4.24.1", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "devDependencies": { - "@types/node": "^20.10.6", - "typescript": "^5.3.3" - } - }, - "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.10.tgz", - "integrity": "sha512-PPpPK6F9ALFTn59Ka3BaL+qGuipRfxNE8qVgkp0bVixeiR2c2/L+IVOiBdu9JhhT22sWnQEp6YyHGI2b2+CMcA==", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/digest-fetch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", - "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", - "dependencies": { - "base-64": "^0.1.0", - "md5": "^2.3.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/formdata-node/node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "engines": { - "node": ">= 14" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/openai": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.24.1.tgz", - "integrity": "sha512-ezm/O3eiZMnyBqirUnWm9N6INJU1WhNtz+nK/Zj/2oyKvRz9pgpViDxa5wYOtyGYXPn1sIKBV0I/S4BDhtydqw==", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "digest-fetch": "^1.3.0", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" - }, - "bin": { - "openai": "bin/cli" - } - }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.4.tgz", - "integrity": "sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.3.tgz", - "integrity": "sha512-9isG8SqRe07p+Aio2ruBZmLm2Q6Sq4EqmXOiNpDxp+7f0LV6Q/LX65fs5Nn+FV/CzfF3NLBoksXbS2jNYIfpKw==", - "peerDependencies": { - "zod": "^3.22.4" - } - } - } -} diff --git a/package.json b/package.json index 63500287..7b10ff1f 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,27 @@ "name": "instructor-js", "version": "0.0.1", "description": "structured outputs for llms", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/instructor.js", + "module": "./dist/instructor.mjs", + "types": "./dist/instructor.d.ts", "exports": { - ".": "./dist/index.js" + ".": { + "import": "./dist/instructor.mjs", + "require": "./dist/instructor.js" + } }, "publishConfig": { "access": "public" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "prepublish": "tsc", - "build": "tsc" + "build": "tsup", + "lint": "eslint src/", + "type-check": "tsc --noEmit", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", + "changeset": "changeset", + "version-packages": "changeset version", + "publish-packages": "tsup && changeset publish" }, "repository": { "type": "git", @@ -32,12 +41,28 @@ }, "homepage": "https://github.com/jxnl/instructor-js#readme", "dependencies": { - "openai": "^4.24.1", - "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" }, + "peerDependencies": { + "openai": ">=4.24.1", + "zod": ">=3.22.4" + }, "devDependencies": { + "@changesets/changelog-github": "^0.5.0", + "@changesets/cli": "^2.27.1", + "@ianvs/prettier-plugin-sort-imports": "4.1.0", + "@types/bun": "^1.0.0", "@types/node": "^20.10.6", + "@typescript-eslint/eslint-plugin": "latest", + "@typescript-eslint/parser": "latest", + "eslint": "^8.40.0", + "eslint-config-prettier": "9.0.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^5.0.0", + "husky": "^8.0.3", + "prettier": "3.0.0", + "tsup": "^8.0.1", "typescript": "^5.3.3" } } diff --git a/src/constants/modes.ts b/src/constants/modes.ts new file mode 100644 index 00000000..b3970b32 --- /dev/null +++ b/src/constants/modes.ts @@ -0,0 +1,9 @@ +export const MODE = { + FUNCTIONS: "FUNCTIONS", + TOOLS: "TOOLS", + JSON: "JSON", + MD_JSON: "MD_JSON", + JSON_SCHEMA: "JSON_SCHEMA" +} + +export type MODE = keyof typeof MODE diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index f6a68516..00000000 --- a/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as instructor from "./patch"; -export * from "./patch"; diff --git a/src/patch.ts b/src/instructor.ts similarity index 57% rename from src/patch.ts rename to src/instructor.ts index 6144a4ed..5fe3e214 100644 --- a/src/patch.ts +++ b/src/instructor.ts @@ -1,34 +1,27 @@ -import OpenAI from "openai"; -import { ZodSchema } from "zod"; -import { JsonSchema7Type, zodToJsonSchema } from "zod-to-json-schema"; - +import assert from "assert" +import OpenAI from "openai" import { ChatCompletion, ChatCompletionCreateParams, - ChatCompletionMessage, -} from "openai/resources"; -import assert = require("assert"); + ChatCompletionMessage +} from "openai/resources/index.mjs" +import { ZodSchema } from "zod" +import { JsonSchema7Type, zodToJsonSchema } from "zod-to-json-schema" -export enum MODE { - FUNCTIONS, - TOOLS, - JSON, - MD_JSON, - JSON_SCHEMA, -} +import { MODE } from "@/constants/modes" export class OpenAISchema { - private response_model: ReturnType | any; + private response_model: ReturnType constructor(public zod_schema: ZodSchema) { - this.response_model = zodToJsonSchema(zod_schema); + this.response_model = zodToJsonSchema(zod_schema) } get definitions() { - return this.response_model["definitions"]; + return this.response_model["definitions"] } get properties() { - return this.response_model["properties"]; + return this.response_model["properties"] } get openai_schema() { @@ -45,176 +38,165 @@ export class OpenAISchema { curr.startsWith("$") || ["title", "description", "additionalProperties"].includes(curr) ) - return acc; - acc[curr] = this.response_model[curr]; - return acc; + return acc + acc[curr] = this.response_model[curr] + return acc }, {} as { - [key: string]: object | JsonSchema7Type; + [key: string]: object | JsonSchema7Type } - ), - }; + ) + } } } type PatchedChatCompletionCreateParams = ChatCompletionCreateParams & { - response_model?: ZodSchema | OpenAISchema; - max_retries?: number; -}; + response_model?: ZodSchema | OpenAISchema + max_retries?: number +} function handleResponseModel( response_model: ZodSchema | OpenAISchema, args: PatchedChatCompletionCreateParams[], - mode: MODE = MODE.FUNCTIONS + mode: MODE = "FUNCTIONS" ): [OpenAISchema, PatchedChatCompletionCreateParams[], MODE] { if (!(response_model instanceof OpenAISchema)) { - response_model = new OpenAISchema(response_model); + response_model = new OpenAISchema(response_model) } if (mode === MODE.FUNCTIONS) { - args[0].functions = [response_model.openai_schema]; - args[0].function_call = { name: response_model.openai_schema.name }; + args[0].functions = [response_model.openai_schema] + args[0].function_call = { name: response_model.openai_schema.name } } else if (mode === MODE.TOOLS) { - args[0].tools = [ - { type: "function", function: response_model.openai_schema }, - ]; + args[0].tools = [{ type: "function", function: response_model.openai_schema }] args[0].tool_choice = { type: "function", - function: { name: response_model.openai_schema.name }, - }; + function: { name: response_model.openai_schema.name } + } } else if ([MODE.JSON, MODE.MD_JSON, MODE.JSON_SCHEMA].includes(mode)) { let message: string = `As a genius expert, your task is to understand the content and provide the parsed objects in json that match the following json_schema: \n${JSON.stringify( response_model.properties - )}`; + )}` if (response_model["definitions"]) { message += `Here are some more definitions to adhere to: \n${JSON.stringify( response_model.definitions - )}`; + )}` } if (mode === MODE.JSON) { - args[0].response_format = { type: "json_object" }; + args[0].response_format = { type: "json_object" } } else if (mode == MODE.JSON_SCHEMA) { - args[0].response_format = { type: "json_object" }; + args[0].response_format = { type: "json_object" } } else if (mode === MODE.MD_JSON) { args[0].messages.push({ role: "assistant", - content: "```json", - }); - args[0].stop = "```"; + content: "```json" + }) + args[0].stop = "```" } if (args[0].messages[0].role != "system") { - args[0].messages.unshift({ role: "system", content: message }); + args[0].messages.unshift({ role: "system", content: message }) } else { - args[0].messages[0].content += `\n${message}`; + args[0].messages[0].content += `\n${message}` } } else { - console.error("unknown mode", mode); + console.error("unknown mode", mode) } - return [response_model, args, mode]; + return [response_model, args, mode] } function processResponse( response: OpenAI.Chat.Completions.ChatCompletion, response_model: OpenAISchema, - mode: MODE = MODE.FUNCTIONS + mode: MODE = "FUNCTIONS" ) { - const message = response.choices[0].message; + const message = response.choices[0].message if (mode === MODE.FUNCTIONS) { assert.equal( message.function_call!.name, response_model.openai_schema.name, "Function name does not match" - ); - return response_model.zod_schema.parse( - JSON.parse(message.function_call!.arguments) - ); + ) + return response_model.zod_schema.parse(JSON.parse(message.function_call!.arguments)) } else if (mode === MODE.TOOLS) { - const tool_call = message.tool_calls![0]; + const tool_call = message.tool_calls![0] assert.equal( tool_call.function.name, response_model.openai_schema.name, "Tool name does not match" - ); - return response_model.zod_schema.parse( - JSON.parse(tool_call.function.arguments) - ); + ) + return response_model.zod_schema.parse(JSON.parse(tool_call.function.arguments)) } else if ([MODE.JSON, MODE.MD_JSON, MODE.JSON_SCHEMA].includes(mode)) { - return response_model.zod_schema.parse(JSON.parse(message.content!)); + return response_model.zod_schema.parse(JSON.parse(message.content!)) } else { - console.error("unknown mode", mode); + console.error("unknown mode", mode) } } function dumpMessage(message: ChatCompletionMessage) { const ret: ChatCompletionMessage = { role: message.role, - content: message.content || "", - }; + content: message.content || "" + } if (message.tool_calls) { - ret["content"] += JSON.stringify(message.tool_calls); + ret["content"] += JSON.stringify(message.tool_calls) } if (message.function_call) { - ret["content"] += JSON.stringify(message.function_call); + ret["content"] += JSON.stringify(message.function_call) } - return ret; + return ret } -export const patch = ({ +const patch = ({ client, - mode, + mode }: { - client: OpenAI; - response_model?: ZodSchema | OpenAISchema; - max_retries?: number; - mode?: MODE; + client: OpenAI + response_model?: ZodSchema | OpenAISchema + max_retries?: number + mode?: MODE }): OpenAI => { client.chat.completions.create = new Proxy(client.chat.completions.create, { async apply(target, ctx, args: PatchedChatCompletionCreateParams[]) { + const max_retries = args[0].max_retries || 1 let retries = 0, - max_retries = args[0].max_retries || 1, response: ChatCompletion | undefined = undefined, - response_model = args[0].response_model; - [response_model, args, mode] = handleResponseModel( - response_model!, - args, - mode - ); - delete args[0].response_model; - delete args[0].max_retries; + response_model = args[0].response_model + ;[response_model, args, mode] = handleResponseModel(response_model!, args, mode) + + delete args[0].response_model + delete args[0].max_retries while (retries < max_retries) { try { response = (await target.apply( ctx, args as [PatchedChatCompletionCreateParams] - )) as ChatCompletion; - return processResponse( - response, - response_model as OpenAISchema, - mode - ); - } catch (error: any) { - console.error(error.errors || error); + )) as ChatCompletion + return processResponse(response, response_model as OpenAISchema, mode) + } catch (error) { + console.error(error.errors || error) if (!response) { - break; + break } - args[0].messages.push(dumpMessage(response.choices[0].message)); + args[0].messages.push(dumpMessage(response.choices[0].message)) args[0].messages.push({ role: "user", - content: `Recall the function correctly, fix the errors, exceptions found\n${error}`, - }); + content: `Recall the function correctly, fix the errors, exceptions found\n${error}` + }) if (mode == MODE.MD_JSON) { - args[0].messages.push({ role: "assistant", content: "```json" }); + args[0].messages.push({ role: "assistant", content: "```json" }) } - retries++; + retries++ if (retries > max_retries) { - throw error; + throw error } } finally { - response = undefined; + response = undefined } } - }, - }); - return client; -}; + } + }) + return client +} + +export default patch diff --git a/tests/functions.test.ts b/tests/functions.test.ts new file mode 100644 index 00000000..fa7044b9 --- /dev/null +++ b/tests/functions.test.ts @@ -0,0 +1,45 @@ +import Instructor from "@/instructor" +import { describe, expect, test } from "bun:test" +import OpenAI from "openai" +import { z } from "zod" + +async function extractUser() { + const UserSchema = z.object({ + age: z.number(), + name: z.string().refine(name => name.includes(" "), { + message: "Name must contain a space" + }) + }) + + type User = z.infer + + const oai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY ?? undefined, + organization: process.env.OPENAI_ORG_ID ?? undefined + }) + + const client = Instructor({ + client: oai, + mode: "FUNCTIONS" + }) + + //@ts-expect-error these types wont work since were using a proxy and just returning the OAI instance type + const user: User = await client.chat.completions.create({ + messages: [{ role: "user", content: "Jason Liu is 30 years old" }], + model: "gpt-3.5-turbo", + //@ts-expect-error same as above + response_model: UserSchema, + max_retries: 3 + }) + + return user +} + +describe("FunctionCall", () => { + test("Should return extracted name and age based on schema", async () => { + const user = await extractUser() + + expect(user.name).toEqual("Jason Liu") + expect(user.age).toEqual(30) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 7ac961f2..a6bf5f6a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,26 @@ { "compilerOptions": { - "target": "es6", - "module": "commonjs", - "outDir": "./dist", + "strict": false, + "noEmit": true, + "allowJs": true, + "jsx": "preserve", + "module": "esnext", + "target": "ESNext", + "skipLibCheck": true, "esModuleInterop": true, - "strict": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "strictPropertyInitialization": false, - "declaration": true + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "moduleDetection": "force", + "baseUrl": ".", + "lib": ["ESNext", "dom", "dom.iterable", "esnext"], + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "paths": { + "@/*": ["./src/*"] + } }, - "include": ["src/**/*"], - "exclude": ["examples", "node_modules", "dist"] -} \ No newline at end of file + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 00000000..cd612b0c --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup" + +export default defineConfig(options => { + return { + splitting: true, + entry: ["src/**/*.{ts,tsx}"], + format: ["esm", "cjs"], + dts: true, + minify: true, + clean: true, + external: ["openai", "zod"], + ...options + } +})