[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n[*.{js,json,yml}]\ncharset = utf-8\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"root\": true,\n  \"plugins\": [\"prettier\", \"@typescript-eslint\"],\n  \"extends\": [\n    \"universe/node\",\n    \"plugin:prettier/recommended\",\n    \"plugin:import/recommended\",\n    \"plugin:import/typescript\",\n    \"plugin:@typescript-eslint/recommended\"\n  ],\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"ecmaVersion\": 2020,\n    \"sourceType\": \"module\"\n  },\n  \"settings\": {\n    \"import/extensions\": [\".ts\", \".tsx\", \".js\", \".jsx\"],\n    \"import/resolver\": {\n      \"typescript\": {\n        \"project\": \"tsconfig.json\"\n      }\n    }\n  },\n  \"rules\": {\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n    \"@typescript-eslint/ban-ts-comment\": \"off\",\n    \"@typescript-eslint/no-empty-interface\": \"off\",\n    \"no-shadow\": \"off\",\n    \"no-console\": [\"error\", { \"allow\": [\"warn\", \"error\", \"info\"] }],\n    \"react/react-in-jsx-scope\": \"off\",\n    \"react/jsx-props-no-spreading\": \"off\",\n    \"jsx-a11y/anchor-is-valid\": \"off\",\n    \"jsx-a11y/alt-text\": \"off\",\n    \"jsx-a11y/click-events-have-key-events\": \"off\",\n    \"jsx-a11y/no-static-element-interactions\": \"off\",\n    \"jsx-a11y/interactive-supports-focus\": \"off\",\n    \"react/require-default-props\": \"off\",\n    \"no-param-reassign\": \"off\",\n    \"import/no-anonymous-default-export\": \"off\",\n    \"import/no-named-as-default\": \"off\",\n    \"import/no-named-as-default-member\": \"off\",\n    \"import/order\": [\n      \"error\",\n      {\n        \"newlines-between\": \"always\",\n        \"groups\": [\n          [\"builtin\", \"external\"],\n          \"internal\",\n          \"parent\",\n          \"sibling\",\n          \"index\"\n        ],\n        \"pathGroups\": [\n          {\n            \"pattern\": \"~/**\",\n            \"group\": \"parent\",\n            \"position\": \"before\"\n          }\n        ],\n        \"pathGroupsExcludedImportTypes\": [\"react\"],\n        \"alphabetize\": { \"order\": \"asc\", \"caseInsensitive\": true }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "/.yarn/**            linguist-vendored\n/.yarn/releases/*    binary\n/.yarn/plugins/**/*  binary\n/.pnp.*              binary linguist-generated\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Release & Publish\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Setup .npmrc file to publish to npm\n        uses: actions/setup-node@v2\n        with:\n          node-version: '18.x'\n          registry-url: 'https://registry.npmjs.org'\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v3 # docs https://pnpm.io/continuous-integration#github-actions\n        with:\n          version: 8 # Optional: specify a pnpm version\n      - name: Install modules\n        run: pnpm install\n      - name: Build\n        run: pnpm build\n      - name: Publish to npm\n        run: npm publish --access public\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches: [main, staging]\n  pull_request:\n    branches: [main, staging]\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os:\n          - ubuntu-latest\n          - windows-latest\n        node: [16.x, 18.x]\n\n    steps:\n      - uses: actions/checkout@v2\n      - name: Set up Node.js\n        uses: actions/setup-node@v2\n        with:\n          node-version: ${{ matrix.node }}\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v3 # docs https://pnpm.io/continuous-integration#github-actions\n        with:\n          version: 8 # Optional: specify a pnpm version\n      - name: Install modules\n        run: pnpm install\n      - name: Run tests\n        run: pnpm test\n"
  },
  {
    "path": ".gitignore",
    "content": ".yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/sdks\n!.yarn/versions\n\n\n# Swap the comments on the following lines if you don't wish to use zero-installs\n# Documentation here: https://yarnpkg.com/features/zero-installs\n#!.yarn/cache\n#.pnp.*\n\nnode_modules/\ndist/\nnpm-debug.*\n.DS_Store\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"all\",\n  \"semi\": true,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 David Zhang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ✨ ZodGPT\n\n[![test](https://github.com/dzhng/zod-gpt/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/dzhng/zod-gpt/actions/workflows/test.yml)\n\nGet structured, fully typed, and validated JSON outputs from OpenAI and Anthropic models.\n\nUnder the hood, `zod-gpt` uses functions to coerce the model to always respond as function calls. Add self-reflection for reliability and zod for parsing & typing.\n\n- [Introduction](#-introduction)\n- [Usage](#-usage)\n  - [Install](#install)\n  - [Request](#request)\n  - [Auto Healing](#-auto-healing)\n  - [Text Slicing](#-text-slicing)\n- [Debugging](#-debugging)\n- [API Reference](#-api-reference)\n\n## 👋 Introduction\n\nZodGPT is a library for\n\n- Receiving structured outputs from models with complete type safety. All responses are fully validated & typed, works with [zod](https://github.com/colinhacks/zod) as a peer dep.\n- Schema definition, serialization / parsing, and **automatically asking the model to correct outputs**.\n- Handle rate limit and any other API errors as gracefully as possible (e.g. exponential backoff for rate-limit) via [llm-api](https://github.com/dzhng/llm-api).\n\nWith `zod-gpt`, you can simply query OpenAI's ChatGPT model like so:\n\n```typescript\nimport { OpenAIChatApi } from 'llm-api';\nimport { completion } from 'zod-gpt';\n\nconst openai = new OpenAIChatApi({ apiKey: 'YOUR_OPENAI_KEY' });\n\nconst response = await completion(openai, 'Generate a startup idea', {\n  schema: z.object({\n    name: z.string().describe('The name of the startup'),\n    description: z.string().describe('What does this startup do?'),\n  }),\n});\n\n// data will be typed as { name: string; description: string }\nconsole.log(response.data);\n```\n\nAnthropic is also supported via `llm-api`:\n\n```typescript\nimport { AnthropicChatApi } from 'llm-api';\nimport { completion } from 'zod-gpt';\n\nconst client = new AnthropicChatApi({ apiKey: 'YOUR_ANTHROPIC_KEY' });\nconst response = await completion(client, ...);\n```\n\n## 🔨 Usage\n\n### Install\n\nThis package is hosted on npm:\n\n```\nnpm i zod-gpt\n```\n\n```\nyarn add zod-gpt\n```\n\nTo setup in your codebase, initialize a new instance with the model you want via the `llm-api` peer dep. Note that `zod-gpt` is designed to work with any models that implements the `CompletionApi` interface, so you can also import your own API wrapper.\n\n```typescript\nimport { OpenAIChatApi } from 'llm-api';\n\nconst openai = new OpenAIChatApi(\n  { apiKey: 'YOUR_OPENAI_KEY' },\n  { model: 'gpt-4-0613' },\n);\n```\n\n### Request\n\nTo send a standard completion request with a given model, simply call the `completion` method.\n\n```typescript\nconst response = await completion(openai, 'hello');\n\n// data will be typed as string\nconsole.log(response.data);\n```\n\nTo add schema parsing and typing, simply add a `schema` key in the options argument. **Make sure to add a description to each key via the `describe` method.** The descriptions will be fed into the model to ensure that it understand exactly what data is requested for each key. Try to error on the side of being over descriptive to ensure the model understands exactly.\n\n```typescript\nconst response = await completion(\n  openai,\n  'Generate a step by step plan on how to run a hackathon',\n  {\n    schema: z.object({\n      plan: z.array(\n        z.object({\n          reason: z.string().describe('Name the reasoning for this step'),\n          id: z.string().describe('Unique step id'),\n          task: z\n            .string()\n            .describe('What is the task to be done for this step?'),\n        }),\n      ),\n    }),\n  },\n);\n\n// data will be typed as { plan: { reason: string; id: string; task: string }[] }\nconsole.info('Response:', response.data);\n```\n\nNOTE: the `schema` key ONLY takes object type schemas - this is a limitation of the `functions` API. If you need to generate arrays or other type of reponses, simply wrap them in an object like the above example.\n\n### 🧑‍⚕️ Auto Healing\n\nBy default, `zod-gpt` has logic to automatically detect and heal any schema errors via self-reflection (e.g. if the function api is not being used correctly, if the schema has parse errors.. etc). This means whenever these types of errors happen, `zod-gpt` will send a new message to re-ask the model to correct its own output, together with any error messages it gathered from parsing.\n\nThe logic is simple but incredabily powerful, and adds a layer of reliability to model outputs. I suggest leaving this flag set to true (its default setting), unless if token usage or response time becomes a real issue.\n\n### 📃 Text Slicing\n\nA common way to handle token limit issues is to split your content. `zod-gpt` provides an `autoSlice` option to automatically split your text when a token overflow error from `llm-api` is detected. It's smart enough to only split your text if it determines that it is above the token limit, and will try to preserve as much of the original text as possible.\n\n```typescript\nconst openai = new OpenAIChatApi(\n  { apiKey: 'YOUR_OPENAI_KEY' },\n  // make sure `contextSize` is set to enable throwing TokenErrors\n  { model: 'gpt-4-0613', contextSize: 8129 },\n);\n\nconst response = await completion(\n  openai,\n  'hello world, testing overflow logic',\n  { autoSlice: true },\n);\n```\n\n## 🤓 Debugging\n\n`zod-gpt` uses the `debug` module for logging & error messages. To run in debug mode, set the `DEBUG` env variable:\n\n`DEBUG=zod-gpt:* yarn playground`\n\nYou can also specify different logging types via:\n\n`DEBUG=zod-gpt:error yarn playground`\n`DEBUG=zod-gpt:log yarn playground`\n\n## ✅ API Reference\n\n### LLM Provider Support\n\n`zod-gpt` currently users the [llm-api](https://github.com/dzhng/llm-api) library to support multiple LLM providers. Check the `llm-api` documentation on how to configure model parameters.\n\n#### Completion\n\nTo send a completion request to a model:\n\n```typescript\nconst res: Response = await completion(model, prompt, options: RequestOptions);\n```\n\n**options**\nYou can override the default request options via this parameter. The `RequestOptions` object extends the request options defined in `llm-api`.\n\n```typescript\ntype RequestOptions = {\n  // set a zod schema to enable JSON output\n  schema?: T;\n\n  // set to enable automatically slicing the prompt on token overflow. prompt will be sliced starting from the last character\n  // default: false\n  autoSlice?: boolean;\n\n  // attempt to auto heal the output via reflection\n  // default: true\n  autoHeal?: boolean;\n\n  // set message history, useful if you want to continue an existing conversation\n  messageHistory?: ChatRequestMessage[];\n\n  // the number of time to retry this request due to rate limit or recoverable API errors\n  // default: 3\n  retries?: number;\n  // default: 30s\n  retryInterval?: number;\n  // default: 60s\n  timeout?: number;\n\n  // the minimum amount of tokens to allocate for the response. if the request is predicted to not have enough tokens, it will automatically throw a 'TokenError' without sending the request\n  // default: 200\n  minimumResponseTokens?: number;\n};\n```\n\n#### Response\n\nCompletion responses extends the model responses from `llm-api`, specifically adding a `data` field for the pased JSON that's automatically typed according to the input `zod` schema.\n\n```typescript\ninterface Response<T extends z.ZodType> {\n  // parsed and typecasted data from the model\n  data: z.infer<T>;\n\n  // raw response from the completion API\n  content?: string;\n  name?: string;\n  arguments?: JsonValue;\n  usage?: {\n    promptTokens: number;\n    completionTokens: number;\n    totalTokens: number;\n  };\n}\n```\n\n### Misc\n\n#### Text Splitting\n\nIf you need to split long text into multiple chunks before calling the llm, few text splitters are also exported in `text-spitter.ts`. Try to default to `RecursiveTextSplitter` unless if there is a specific reason to use the other text splitters, as it is the most widely used text splitter.\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n  clearMocks: true,\n  roots: ['<rootDir>/src'],\n  modulePaths: ['<rootDir>/src'],\n  testRegex: '(/__tests__/.*|(\\\\.|/)(test))\\\\.tsx?$',\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],\n  reporters: ['default'],\n  globals: {\n    // we must specify a custom tsconfig for tests because we need the typescript transform\n    // to transform jsx into js rather than leaving it jsx such as the next build requires.  you\n    // can see this setting in tsconfig.jest.json -> \"jsx\": \"react\"\n    'ts-jest': {\n      tsconfig: 'tsconfig.json',\n\n      // set isolatedModules to fix jest memory leak with ts include directories\n      // https://github.com/kulshekhar/ts-jest/issues/1967\n      isolatedModules: true,\n    },\n\n    // disable types from preventing tests from running\n    // https://github.com/kulshekhar/ts-jest/issues/822\n    diagnostics: {\n      exclude: ['!**/*.(spec|test).ts?(x)'],\n    },\n  },\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"zod-gpt\",\n  \"description\": \"Get structured, fully typed JSON outputs from OpenAI and Anthropic LLMs\",\n  \"version\": \"0.16.0\",\n  \"main\": \"dist/src/index.js\",\n  \"types\": \"dist/src/index.d.ts\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"keywords\": [\n    \"typescript\",\n    \"zod\",\n    \"gpt\",\n    \"chatgpt\",\n    \"llama\",\n    \"llm\",\n    \"ai\",\n    \"ml\",\n    \"prompt\",\n    \"prompt engineering\",\n    \"openai\"\n  ],\n  \"author\": \"David Zhang <david@aomni.com>\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://github.com/dzhng/zod-gpt\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+ssh://git@github.com/dzhng/zod-gpt.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/dzhng/zod-gpt/issues\"\n  },\n  \"scripts\": {\n    \"setup\": \"husky install\",\n    \"build\": \"tsc --build --pretty\",\n    \"lint\": \"eslint src --ext ts,tsx,js,jsx --ignore-path .gitignore --fix\",\n    \"test\": \"jest --passWithNoTests\",\n    \"test:update\": \"jest -u --passWithNoTests\",\n    \"playground\": \"tsx playground\"\n  },\n  \"dependencies\": {\n    \"debug\": \"^4.3.4\",\n    \"jsonic\": \"^1.0.1\",\n    \"jsonrepair\": \"^3.4.0\",\n    \"lodash\": \"^4.17.21\",\n    \"tsx\": \"^4.7.1\",\n    \"type-fest\": \"^4.6.0\",\n    \"zod-to-json-schema\": \"^3.21.4\"\n  },\n  \"peerDependencies\": {\n    \"llm-api\": \"^1.6.0\",\n    \"zod\": \"^3.22.4\"\n  },\n  \"devDependencies\": {\n    \"@types/debug\": \"^4.1.10\",\n    \"@types/jest\": \"^29.5.7\",\n    \"@types/jsonic\": \"^0.3.2\",\n    \"@types/lodash\": \"^4.14.200\",\n    \"eslint\": \"^8.36.0\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-config-universe\": \"^11.1.1\",\n    \"eslint-import-resolver-typescript\": \"^3.3.0\",\n    \"eslint-plugin-import\": \"^2.26.0\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"husky\": \"^8.0.2\",\n    \"jest\": \"^29.7.0\",\n    \"lint-staged\": \"^13.2.0\",\n    \"llm-api\": \"^1.6.0\",\n    \"prettier\": \"^2.8.0\",\n    \"ts-jest\": \"^29.1.1\",\n    \"typescript\": \"^5.2.2\",\n    \"zod\": \"^3.22.4\"\n  },\n  \"lint-staged\": {\n    \"*.{js,jsx,ts,tsx}\": [\n      \"eslint --ext ts,tsx,js,jsx --fix --ignore-path .gitignore \",\n      \"prettier --write\"\n    ],\n    \"*.{json,md,css,scss}\": [\n      \"prettier --write\"\n    ]\n  }\n}\n"
  },
  {
    "path": "playground.ts",
    "content": "import {\n  AnthropicChatApi,\n  OpenAIChatApi,\n  AnthropicBedrockChatApi,\n  GroqChatApi,\n} from 'llm-api';\nimport { z } from 'zod';\n\nimport { completion } from './src';\n\n(async function go() {\n  let client:\n    | OpenAIChatApi\n    | AnthropicChatApi\n    | AnthropicBedrockChatApi\n    | GroqChatApi\n    | undefined;\n\n  if (process.env.OPENAI_KEY) {\n    client = new OpenAIChatApi(\n      {\n        apiKey: process.env.OPENAI_KEY ?? 'YOUR_client_KEY',\n      },\n      { contextSize: 4096 },\n    );\n  } else if (process.env.ANTHROPIC_KEY) {\n    client = new AnthropicChatApi(\n      {\n        apiKey: process.env.ANTHROPIC_KEY ?? 'YOUR_client_KEY',\n      },\n      { stream: true, temperature: 0 },\n    );\n  } else if (\n    process.env.AWS_BEDROCK_ACCESS_KEY &&\n    process.env.AWS_BEDROCK_SECRET_KEY\n  ) {\n    client = new AnthropicBedrockChatApi(\n      {\n        accessKeyId: process.env.AWS_BEDROCK_ACCESS_KEY ?? 'YOUR_access_key',\n        secretAccessKey:\n          process.env.AWS_BEDROCK_SECRET_KEY ?? 'YOUR_secret_key',\n      },\n      { stream: true, temperature: 0, model: 'anthropic.claude-v2' },\n    );\n  } else if (process.env.GROQ_KEY) {\n    client = new GroqChatApi(\n      {\n        apiKey: process.env.GROQ_KEY ?? 'YOUR_client_KEY',\n      },\n      { stream: false, temperature: 0 },\n    );\n  }\n  if (!client) {\n    throw new Error(\n      'Please pass in either an OpenAI or Anthropic environment variable',\n    );\n  }\n\n  /*const resSlice = await completion(\n    client,\n    'Just say hello and ignore the rest of this message\\n' +\n      Array(500_000).fill('1'),\n    { autoSlice: true },\n  );\n  console.info('Response slice: ', resSlice.data);*/\n\n  const resStartup = await completion(client, 'Generate a startup idea', {\n    schema: z.object({\n      name: z.string().describe('The name of the startup'),\n      description: z.string().describe('What does this startup do?'),\n    }),\n  });\n  console.info('Response 1: ', resStartup.data);\n\n  const resHello = await completion(client, 'Hello');\n  console.info('Response 2:', resHello.data);\n\n  const resComplexSchema = await completion(\n    client,\n    'Generate a step by step plan to run a hackathon',\n    {\n      schema: z.object({\n        plan: z.array(\n          z.object({\n            reason: z.string().describe('Name the reasoning for this step'),\n            name: z.string().describe('Step name'),\n            task: z\n              .string()\n              .describe('What is the task to be done for this step?')\n              .optional(),\n          }),\n        ),\n      }),\n    },\n  );\n  console.info('Response 3:', resComplexSchema.data);\n\n  const resBulletPoints = await completion(\n    client,\n    'Generate a list of interesting areas of exploration about the renaissance',\n    {\n      schema: z.object({\n        topics: z\n          .array(\n            z.object({\n              title: z.string().describe('Title of the idea'),\n              reason: z.string().describe('Why you choose this idea'),\n              peopleInvolved: z\n                .string()\n                .describe(\n                  \"If there any known figures that's related to this idea\",\n                )\n                .optional(),\n            }),\n          )\n          .min(10)\n          .max(20),\n      }),\n    },\n  );\n  console.info('Response 4:', resBulletPoints.data);\n\n  const resBuletPoints2 = await resBulletPoints.respond('Generate 10 more');\n  console.info('Response 4R:', resBuletPoints2.data);\n\n  const resMessageHistory = await completion(\n    client,\n    'What did I mention in my first message to you?',\n    {\n      messageHistory: [\n        { role: 'user', content: 'Tell me about large langauge models' },\n        { role: 'assistant', content: 'ok' },\n      ],\n    },\n  );\n  console.info('Response 5:', resMessageHistory.data);\n\n  const meaning = await completion(client, 'What is the meaning of life?')\n    .then((res) => res.respond('why'))\n    .then((res) => res.respond('why'))\n    .then((res) => res.respond('why'))\n    .then((res) => res.respond('why'))\n    .then((res) => res.respond('why'));\n\n  console.info('The meaning of life after 5 whys is: ', meaning.content);\n})();\n"
  },
  {
    "path": "src/__snapshots__/text-splitter.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`RecursiveCharacterTextSplitter Should correctly spilt text by seperators 1`] = `\n[\n  \"Hello world\",\n  \"this is a test of the recursive text splitter\",\n]\n`;\n\nexports[`RecursiveCharacterTextSplitter Should correctly spilt text by seperators 2`] = `\n[\n  \"Hello world, this is a test of the recursive text splitter\",\n  \"If I have a period, it should split along the period.\",\n]\n`;\n\nexports[`RecursiveCharacterTextSplitter Should correctly spilt text by seperators 3`] = `\n[\n  \"Hello world, this is a test of the recursive text splitter\",\n  \"If I have a period, it should split along the period.\",\n  \"Or, if there is a new line, it should prioritize splitting on new lines instead.\",\n]\n`;\n\nexports[`RecursiveCharacterTextSplitter Should correctly spilt text by seperators 4`] = `\n[\n  58,\n  53,\n  80,\n]\n`;\n"
  },
  {
    "path": "src/completion.ts",
    "content": "import {\n  TokenError,\n  CompletionApi,\n  AnthropicChatApi,\n  ChatRequestMessage,\n  AnthropicBedrockChatApi,\n  GroqChatApi,\n} from 'llm-api';\nimport { defaults, last } from 'lodash';\nimport { z } from 'zod';\n\nimport type { RequestOptions, Response } from './types';\nimport { debug, parseUnsafeJson, zodToJsonSchema } from './utils';\n\nconst FunctionName = 'print';\nconst FunctionDescription =\n  'Respond by calling this function with the correct parameters.';\n\nconst Defaults = {\n  autoHeal: true,\n  autoSlice: false,\n};\n\nexport async function completion<T extends z.ZodType = z.ZodString>(\n  model: CompletionApi,\n  prompt: string | (() => string),\n  opt?: Partial<RequestOptions<T>>,\n): Promise<Response<T>> {\n  const message = typeof prompt === 'string' ? prompt : prompt();\n  const messages: ChatRequestMessage[] = [\n    ...(opt?.messageHistory ?? []),\n    { role: 'user', content: message },\n  ];\n\n  return chat(model, messages, opt);\n}\n\nexport async function chat<T extends z.ZodType = z.ZodString>(\n  model: CompletionApi,\n  messages: ChatRequestMessage[],\n  _opt?: Partial<RequestOptions<T>>,\n): Promise<Response<T>> {\n  const jsonSchema = _opt?.schema && zodToJsonSchema(_opt?.schema);\n  const opt = defaults(\n    {\n      // build function to call if schema is defined\n      callFunction: _opt?.schema\n        ? _opt.functionName ?? FunctionName\n        : undefined,\n      functions: _opt?.schema\n        ? [\n            {\n              name: _opt.functionName ?? FunctionName,\n              description: _opt.functionDescription ?? FunctionDescription,\n              parameters: jsonSchema,\n            },\n          ]\n        : undefined,\n    },\n    _opt,\n    Defaults,\n  );\n\n  if (\n    opt.schema &&\n    (opt.schema._def as any).typeName !== z.ZodFirstPartyTypeKind.ZodObject\n  ) {\n    throw new Error('Schemas can ONLY be an object');\n  }\n  debug.log('⬆️ sending request:', messages);\n\n  try {\n    const hasFunctionCall = !(\n      model instanceof AnthropicChatApi ||\n      model instanceof AnthropicBedrockChatApi ||\n      model instanceof GroqChatApi\n    );\n    const schemaInstructions =\n      !hasFunctionCall && _opt?.schema && JSON.stringify(jsonSchema);\n    const firstSchemaKey =\n      !hasFunctionCall &&\n      _opt?.schema &&\n      Object.keys(jsonSchema['properties'])[0];\n    const responsePrefix = `\\`\\`\\`json\\n{ \"${firstSchemaKey}\":`;\n    const stopSequence = '```';\n\n    // Anthropic does not have support for functions, so create a custom system message and inject it as the first system message\n    // Use the `responsePrefix` property to steer anthropic to output in the json structure\n    let response =\n      !hasFunctionCall && _opt?.schema\n        ? await model.chatCompletion(messages, {\n            ...opt,\n            systemMessage:\n              `You will respond to ALL human messages in JSON. Make sure the response correctly follow the following JSON schema specifications:\\n<json_schema>\\n${schemaInstructions}\\n</json_schema>\\n\\n${\n                opt.systemMessage\n                  ? typeof opt.systemMessage === 'string'\n                    ? opt.systemMessage\n                    : opt.systemMessage()\n                  : ''\n              }`.trim(),\n            responsePrefix: opt.responsePrefix ?? responsePrefix,\n            stop: stopSequence,\n          })\n        : await model.chatCompletion(messages, opt);\n    if (!response) {\n      throw new Error('Chat request failed');\n    }\n\n    // only send this debug msg when stream is not enabled, or there'll be duplicate log msgs since stream also streams in the logs\n    !model.modelConfig.stream && debug.log('⬇️ received response:', response);\n\n    // validate res content, and recursively loop if invalid\n    if (opt?.schema) {\n      if (hasFunctionCall && !response.arguments) {\n        if (opt.autoHeal) {\n          debug.log('⚠️ function not called, autohealing...');\n          response = await response.respond({\n            role: 'user',\n            content: `Please respond with a call to the ${FunctionName} function`,\n          });\n\n          if (!response.arguments) {\n            throw new Error('Response function autoheal failed');\n          }\n        } else {\n          throw new Error('Response function not called');\n        }\n      }\n\n      let json = hasFunctionCall\n        ? response.arguments\n        : parseUnsafeJson(response.content ?? '');\n      if (!json) {\n        throw new Error('No response received');\n      }\n\n      const res = opt.schema.safeParse(json);\n      if (res.success) {\n        return {\n          ...response,\n          respond: (message: string | ChatRequestMessage, opt) =>\n            chat(\n              model,\n              [\n                ...messages,\n                response.message,\n                typeof message === 'string'\n                  ? {\n                      role: hasFunctionCall ? 'tool' : 'user',\n                      toolCallId: response.toolCallId,\n                      content: message,\n                    }\n                  : message,\n              ],\n              opt ?? _opt,\n            ),\n          data: res.data,\n        };\n      } else {\n        debug.error('⚠️ error parsing response', res.error);\n        if (opt.autoHeal) {\n          debug.log('⚠️ response parsing failed, autohealing...', res.error);\n          const issuesMessage = res.error.issues.reduce(\n            (prev, issue) =>\n              issue.path && issue.path.length > 0\n                ? `${prev}\\nThe issue is at path ${issue.path.join('.')}: ${\n                    issue.message\n                  }.`\n                : `\\nThe issue is: ${issue.message}.`,\n            hasFunctionCall\n              ? `There is an issue with that response, please rewrite by calling the ${FunctionName} function with the correct parameters.`\n              : `There is an issue with that response, please follow the JSON schema EXACTLY, the output must be valid parsable JSON: ${schemaInstructions}`,\n          );\n          response = await response.respond(issuesMessage);\n        } else {\n          throw new Error('Response parsing failed');\n        }\n      }\n\n      json = hasFunctionCall\n        ? response.arguments\n        : parseUnsafeJson(response.content ?? '');\n      if (!json) {\n        throw new Error('Response schema autoheal failed');\n      }\n\n      // TODO: there is definitely a cleaner way to implement this to avoid the duplicate parsing\n      const data = opt.schema.parse(json);\n      return {\n        ...response,\n        respond: (message: string | ChatRequestMessage, opt) =>\n          chat(\n            model,\n            [\n              ...messages,\n              response.message,\n              typeof message === 'string'\n                ? {\n                    role: hasFunctionCall ? 'tool' : 'user',\n                    toolCallId: response.toolCallId,\n                    content: message,\n                  }\n                : message,\n            ],\n            opt ?? _opt,\n          ),\n        data,\n      };\n    }\n\n    // if no schema is defined, default to string\n    return {\n      ...response,\n      respond: (message: string | ChatRequestMessage, opt) =>\n        chat(\n          model,\n          [\n            ...messages,\n            response.message,\n            typeof message === 'string'\n              ? { role: 'user', content: message }\n              : message,\n          ],\n          opt ?? _opt,\n        ),\n      data: String(response.content),\n    };\n  } catch (e) {\n    // For autoslice, keep looping recursively, chopping off a bit of the message at a time, until it fits\n    if (e instanceof TokenError && opt.autoSlice) {\n      // break out the last message to auto slice\n      const message = last(messages)?.content ?? '';\n      const chunkSize = message.length - e.overflowTokens;\n      if (chunkSize < 0) {\n        throw e;\n      }\n\n      debug.log(\n        `⚠️ Request prompt too long, splitting text with chunk size of ${chunkSize}`,\n      );\n      const newMessage = message.slice(0, chunkSize);\n      return chat(\n        model,\n        [...messages.slice(0, -1), { role: 'user', content: newMessage }],\n        opt,\n      );\n    } else {\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "src/config.ts",
    "content": "// completion request\nexport const RateLimitRetryIntervalMs = 30_000;\nexport const CompletionDefaultRetries = 3;\nexport const CompletionDefaultTimeout = 60_000;\nexport const MinimumResponseTokens = 200;\n"
  },
  {
    "path": "src/index.ts",
    "content": "export * from './completion';\nexport * from './text-splitter';\nexport * from './types';\n"
  },
  {
    "path": "src/text-splitter.test.ts",
    "content": "import { RecursiveCharacterTextSplitter } from './text-splitter';\n\ndescribe('RecursiveCharacterTextSplitter', () => {\n  it('Should correctly spilt text by seperators', () => {\n    const splitter = new RecursiveCharacterTextSplitter({\n      chunkSize: 50,\n      chunkOverlap: 10,\n    });\n    expect(\n      splitter.splitText(\n        'Hello world, this is a test of the recursive text splitter.',\n      ),\n    ).toMatchSnapshot();\n\n    splitter.chunkSize = 100;\n    expect(\n      splitter.splitText(\n        'Hello world, this is a test of the recursive text splitter. If I have a period, it should split along the period.',\n      ),\n    ).toMatchSnapshot();\n\n    splitter.chunkSize = 110;\n    const res = splitter.splitText(\n      'Hello world, this is a test of the recursive text splitter. If I have a period, it should split along the period.\\nOr, if there is a new line, it should prioritize splitting on new lines instead.',\n    );\n    expect(res).toMatchSnapshot();\n    expect(res.map((r) => r.length)).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "src/text-splitter.ts",
    "content": "interface TextSplitterParams {\n  chunkSize: number;\n\n  chunkOverlap: number;\n}\n\nabstract class TextSplitter implements TextSplitterParams {\n  chunkSize = 1000;\n  chunkOverlap = 200;\n\n  constructor(fields?: Partial<TextSplitterParams>) {\n    this.chunkSize = fields?.chunkSize ?? this.chunkSize;\n    this.chunkOverlap = fields?.chunkOverlap ?? this.chunkOverlap;\n    if (this.chunkOverlap >= this.chunkSize) {\n      throw new Error('Cannot have chunkOverlap >= chunkSize');\n    }\n  }\n\n  abstract splitText(text: string): string[];\n\n  createDocuments(texts: string[]): string[] {\n    const documents: string[] = [];\n    for (let i = 0; i < texts.length; i += 1) {\n      const text = texts[i];\n      for (const chunk of this.splitText(text)) {\n        documents.push(chunk);\n      }\n    }\n    return documents;\n  }\n\n  splitDocuments(documents: string[]): string[] {\n    return this.createDocuments(documents);\n  }\n\n  private joinDocs(docs: string[], separator: string): string | null {\n    const text = docs.join(separator).trim();\n    return text === '' ? null : text;\n  }\n\n  mergeSplits(splits: string[], separator: string): string[] {\n    const docs: string[] = [];\n    const currentDoc: string[] = [];\n    let total = 0;\n    for (const d of splits) {\n      const _len = d.length;\n      if (total + _len >= this.chunkSize) {\n        if (total > this.chunkSize) {\n          console.warn(\n            `Created a chunk of size ${total}, +\nwhich is longer than the specified ${this.chunkSize}`,\n          );\n        }\n        if (currentDoc.length > 0) {\n          const doc = this.joinDocs(currentDoc, separator);\n          if (doc !== null) {\n            docs.push(doc);\n          }\n          // Keep on popping if:\n          // - we have a larger chunk than in the chunk overlap\n          // - or if we still have any chunks and the length is long\n          while (\n            total > this.chunkOverlap ||\n            (total + _len > this.chunkSize && total > 0)\n          ) {\n            total -= currentDoc[0].length;\n            currentDoc.shift();\n          }\n        }\n      }\n      currentDoc.push(d);\n      total += _len;\n    }\n    const doc = this.joinDocs(currentDoc, separator);\n    if (doc !== null) {\n      docs.push(doc);\n    }\n    return docs;\n  }\n}\n\nexport interface CharacterTextSplitterParams extends TextSplitterParams {\n  separator: string;\n}\n\nexport class CharacterTextSplitter\n  extends TextSplitter\n  implements CharacterTextSplitterParams\n{\n  separator = '\\n\\n';\n\n  constructor(fields?: Partial<CharacterTextSplitterParams>) {\n    super(fields);\n    this.separator = fields?.separator ?? this.separator;\n  }\n\n  public splitText(text: string): string[] {\n    // First we naively split the large input into a bunch of smaller ones.\n    let splits: string[];\n    if (this.separator) {\n      splits = text.split(this.separator);\n    } else {\n      splits = text.split('');\n    }\n    return this.mergeSplits(splits, this.separator);\n  }\n}\n\nexport interface RecursiveCharacterTextSplitterParams\n  extends TextSplitterParams {\n  separators: string[];\n}\n\nexport class RecursiveCharacterTextSplitter\n  extends TextSplitter\n  implements RecursiveCharacterTextSplitterParams\n{\n  separators: string[] = ['\\n\\n', '\\n', '.', ',', ' ', ''];\n\n  constructor(fields?: Partial<RecursiveCharacterTextSplitterParams>) {\n    super(fields);\n    this.separators = fields?.separators ?? this.separators;\n  }\n\n  splitText(text: string): string[] {\n    const finalChunks: string[] = [];\n\n    // Get appropriate separator to use\n    let separator: string = this.separators[this.separators.length - 1];\n    for (const s of this.separators) {\n      if (s === '') {\n        separator = s;\n        break;\n      }\n      if (text.includes(s)) {\n        separator = s;\n        break;\n      }\n    }\n\n    // Now that we have the separator, split the text\n    let splits: string[];\n    if (separator) {\n      splits = text.split(separator);\n    } else {\n      splits = text.split('');\n    }\n\n    // Now go merging things, recursively splitting longer texts.\n    let goodSplits: string[] = [];\n    for (const s of splits) {\n      if (s.length < this.chunkSize) {\n        goodSplits.push(s);\n      } else {\n        if (goodSplits.length) {\n          const mergedText = this.mergeSplits(goodSplits, separator);\n          finalChunks.push(...mergedText);\n          goodSplits = [];\n        }\n        const otherInfo = this.splitText(s);\n        finalChunks.push(...otherInfo);\n      }\n    }\n    if (goodSplits.length) {\n      const mergedText = this.mergeSplits(goodSplits, separator);\n      finalChunks.push(...mergedText);\n    }\n    return finalChunks;\n  }\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "import { ModelRequestOptions, ChatResponse, ChatRequestMessage } from 'llm-api';\nimport { z } from 'zod';\n\n// don't expost the functions array to the request layer\nexport type RequestOptions<T extends z.ZodType> = Omit<\n  ModelRequestOptions,\n  'functions' | 'callFunction'\n> & {\n  // set a zod schema to enable JSON output\n  schema?: T;\n\n  // override default function name and description used to print outputs\n  functionName?: string;\n  functionDescription?: string;\n\n  // set to enable automatically slicing the prompt on token overflow. prompt will be sliced starting from the last character\n  // default: false\n  autoSlice?: boolean;\n\n  // attempt to auto heal the output via reflection\n  // default: true\n  autoHeal?: boolean;\n\n  // set message history, useful if you want to continue an existing conversation\n  messageHistory?: ChatRequestMessage[];\n};\n\nexport type Response<T extends z.ZodType> = {\n  // override previous respond method to include schema types\n  respond: (\n    message: string | ChatRequestMessage,\n    opt?: ModelRequestOptions,\n  ) => Promise<Response<T>>;\n\n  // parsed and typecasted data from the model\n  data: z.infer<T>;\n} & ChatResponse;\n"
  },
  {
    "path": "src/utils.ts",
    "content": "import { debug as mDebug } from 'debug';\nimport jsonic from 'jsonic';\nimport { jsonrepair } from 'jsonrepair';\nimport { omit } from 'lodash';\nimport { z } from 'zod';\nimport zodToJsonSchemaImpl from 'zod-to-json-schema';\n\nconst error = mDebug('zod-gpt:error');\nconst log = mDebug('zod-gpt:log');\n// eslint-disable-next-line no-console\nlog.log = console.log.bind(console);\n\nexport const debug = {\n  error,\n  log,\n  write: (t: string) =>\n    process.env.DEBUG &&\n    'zod-gpt:log'.match(process.env.DEBUG) &&\n    process.stdout.write(t),\n};\n\nexport function sleep(delay: number) {\n  return new Promise((resolve) => {\n    setTimeout(resolve, delay);\n  });\n}\n\nconst extractJSONObjectResponse = (res: string): string | undefined =>\n  res.match(/\\{(.|\\n)*\\}/g)?.[0];\n\nconst extractJSONArrayResponse = (res: string): string | undefined =>\n  res.match(/\\[(.|\\n)*\\]/g)?.[0];\n\nconst extractJSONMarkdownResponse = (res: string): string | undefined => {\n  const match = res.match(/```json((.|\\n)*?)```/g)?.[0];\n  return match ? match.replace(/```json|```/g, '').trim() : undefined;\n};\n\nexport function parseUnsafeJson(json: string): any {\n  try {\n    const potientialJson = extractJSONMarkdownResponse(json);\n    const potientialArray = extractJSONArrayResponse(potientialJson ?? json);\n    const potientialObject = extractJSONObjectResponse(potientialJson ?? json);\n    // extract the larger text between potiential array and potiential object, we want the parent json object\n    const extracted =\n      (potientialArray?.length ?? 0) > (potientialObject?.length ?? 0)\n        ? potientialArray\n        : potientialObject;\n    if (extracted) {\n      return jsonic(jsonrepair(extracted));\n    } else {\n      return undefined;\n    }\n  } catch (e) {\n    debug.error('⚠️ error parsing unsafe json: ', json, e);\n    return undefined;\n  }\n}\n\nexport function zodToJsonSchema(schema: z.ZodType): any {\n  return omit(\n    zodToJsonSchemaImpl(schema, { $refStrategy: 'none' }),\n    '$ref',\n    '$schema',\n    'default',\n    'definitions',\n    'description',\n    'markdownDescription',\n  );\n}\n\nexport type MaybePromise<T> = Promise<T> | T;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"lib\": [\"dom\", \"esnext\"],\n    \"allowJs\": true,\n    \"alwaysStrict\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitThis\": true,\n    \"strictNullChecks\": true,\n\n    // compile settings\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": false,\n    \"removeComments\": true,\n    \"outDir\": \"dist\"\n  },\n  \"include\": [\"src/**/*\", \"playground.ts\"],\n  \"exclude\": [\"dist\", \"node_modules\", \"**/__mocks__/*\"],\n  \"ts-node\": {\n    \"compilerOptions\": { \"module\": \"commonjs\" }\n  }\n}\n"
  }
]