[
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu\n{\n    \"name\": \"TypeChat Development (Python and TypeScript)\",\n    // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile\n    \"image\": \"mcr.microsoft.com/devcontainers/python:1-3.12\",\n\n    // Features to add to the dev container. More info: https://containers.dev/features.\n    \"features\": {\n        \"ghcr.io/devcontainers/features/node:1\": {\n            \"nodeGypDependencies\": true,\n            \"version\": \"lts\",\n            \"nvmVersion\": \"latest\"\n        },\n        \"ghcr.io/devcontainers-contrib/features/hatch:2\": {\n            \"version\": \"latest\"\n        }\n    },\n\n    // Use 'forwardPorts' to make a list of ports inside the container available locally.\n    \"forwardPorts\": [7860, 7861, 7862, 7863, 7864, 7865, 7866, 7867, 7868, 7869, 7870],\n\n    // Configure tool-specific properties.\n    \"customizations\": {\n        \"vscode\": {\n            \"extensions\": [\n                \"ms-python.black-formatter\"\n            ],\n            \"settings\": {\n                // Force the editor to pick up on the right environment and interpreter.\n                \"python.defaultInterpreterPath\": \"/workspaces/TypeChat/.venv\",\n\n                // Respect the paths of the interpreter.\n                \"python.analysis.autoSearchPaths\": false\n            }\n        }\n    },\n\n    // Use 'postCreateCommand' to run commands after the container is created.\n    \"postCreateCommand\": {\n        \"site - npm\": \"cd site; npm ci\",\n        \"typescript - npm\": \"cd typescript; npm ci\",\n        \"python - hatch\": \"cd python; hatch env create\",\n        \"python - npm\": \"cd python; npm ci\"\n    }\n\n    // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.\n    // \"remoteUser\": \"root\"\n}\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for more information:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n# https://containers.dev/guide/dependabot\n\nversion: 2\nupdates:\n  - package-ecosystem: \"devcontainers\"\n    directory: \"/\"\n    schedule:\n      interval: weekly\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/ci.js.yml",
    "content": "# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n\npermissions:\n  contents: read\n\n# Ensure scripts are run with pipefail. See:\n# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference\ndefaults:\n  run:\n    shell: bash\n    working-directory: typescript\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [18.x, 20.x, 22.x]\n        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/\n\n    steps:\n    - uses: actions/checkout@v6\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v6\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: \"npm\"\n        cache-dependency-path: \"typescript/package-lock.json\"\n    - run: npm ci\n    - run: npm run build-all\n"
  },
  {
    "path": ".github/workflows/ci.python.yml",
    "content": "name: Python CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\ndefaults:\n  run:\n    shell: bash\n    working-directory: ./python\n\njobs:\n  pyright:\n    permissions:\n      contents: read\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - ubuntu-latest\n        python-version:\n          - '3.11'\n          - '3.12'\n          - '3.13'\n          - '3.14'\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install Hatch\n        run: |\n          python -m pip install --upgrade pip\n          pip install hatch \"virtualenv<20.29\"\n\n      - name: Set up Hatch Environment\n        run: |\n          hatch env create\n          HATCH_ENV=$(hatch env find)\n          echo $HATCH_ENV\n          echo \"$HATCH_ENV/bin\" >> $GITHUB_PATH\n\n      - name: Get Pyright Version\n        id: pyright-version\n        run: |\n          PYRIGHT_VERSION=$(jq -r '.devDependencies.pyright' < package.json)\n          echo $PYRIGHT_VERSION\n          echo \"version=$PYRIGHT_VERSION\" >> $GITHUB_OUTPUT\n\n      - name: Run pyright ${{ steps.pyright-version.outputs.version }}\n        uses: jakebailey/pyright-action@v3\n        with:\n          version: ${{ steps.pyright-version.outputs.version }}\n          python-version: ${{ matrix.python-version}}\n          annotate: ${{ matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest' }} # Only let one build post comments.\n          working-directory: ./python\n\n      - name: Test with Pytest\n        run: |\n          pytest -vv\n\n"
  },
  {
    "path": ".github/workflows/github-pages.yml",
    "content": "name: Deploy to GitHub Pages\non:\n  push:\n    branches:\n      - main\n      \npermissions:\n  contents: read\n  id-token: write\n  pages: write\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n    - name: Use Node.js\n      uses: actions/setup-node@v6\n      with:\n        node-version: 18.x\n        cache: 'npm'\n        cache-dependency-path: \"site/package-lock.json\"\n    - name: Build site\n      run: |\n        cd site\n        echo \"Building the site\"\n        npm ci\n        npm run build\n    - name : Upload artifact\n      uses: actions/upload-pages-artifact@v4\n      with:\n        name: github-pages\n        path: site/_site\n    - name: Deploy to GitHub Pages from artifacts\n      uses: actions/deploy-pages@v4 \n"
  },
  {
    "path": ".gitignore",
    "content": "build/\r\ndist/\r\nout/\r\nnode_modules/\r\n.venv/\r\n.env*\r\n*.map\r\n*.out.txt\r\n*.bat\r\n\r\n# Local development and debugging\r\n.scratch/\r\n**/.vscode/*\r\n**/tsconfig.debug.json\r\n!**/.vscode/launch.json\r\n**/build.bat\r\n\r\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Microsoft Open Source Code of Conduct\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\n\nResources:\n\n- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)\n- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)\n- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns\n"
  },
  {
    "path": "LICENSE",
    "content": "    MIT License\n\n    Copyright (c) Microsoft Corporation.\n\n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE\n"
  },
  {
    "path": "README.md",
    "content": "# TypeChat\n\nTypeChat is a library that makes it easy to build natural language interfaces using types.\n\nBuilding natural language interfaces has traditionally been difficult. These apps often relied on complex decision trees to determine intent and collect the required inputs to take action. Large language models (LLMs) have made this easier by enabling us to take natural language input from a user and match to intent. This has introduced its own challenges including the need to constrain the model's reply for safety, structure responses from the model for further processing, and ensuring that the reply from the model is valid. Prompt engineering aims to solve these problems, but comes with a steep learning curve and increased fragility as the prompt increases in size.\n\nTypeChat replaces _prompt engineering_ with _schema engineering_.\n\nSimply define types that represent the intents supported in your natural language application. That could be as simple as an interface for categorizing sentiment or more complex examples like types for a shopping cart or music application. For example, to add additional intents to a schema, a developer can add additional types into a discriminated union. To make schemas hierarchical, a developer can use a \"meta-schema\" to choose one or more sub-schemas based on user input.\n\nAfter defining your types, TypeChat takes care of the rest by:\n\n1. Constructing a prompt to the LLM using types.\n2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction.\n3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent.\n\nTypes are all you need!\n\n# Getting Started\n\nInstall TypeChat for TypeScript/JavaScript:\n\n```\nnpm install typechat\n```\n\n<!--\n\nOr from PyPI:\n\n...\n\nOr NuGet\n\n...\n\n-->\n\nYou can also work with TypeChat from source for:\n\n* [Python](./python/README.md)\n* [TypeScript](./typescript/README.md)\n* [C#/.NET](https://github.com/microsoft/TypeChat.net)\n\nTo see TypeChat in action, we recommend exploring the [TypeChat example projects](./typescript/examples). You can try them on your local machine or in a GitHub Codespace.\n\nTo learn more about TypeChat, visit the [documentation](https://microsoft.github.io/TypeChat) which includes more information on TypeChat and how to get started.\n\n## Contributing\n\nThis project welcomes contributions and suggestions.  Most contributions require you to agree to a\nContributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\nthe rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.\n\nWhen you submit a pull request, a CLA bot will automatically determine whether you need to provide\na CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\nprovided by the bot. You will only need to do this once across all repos using our CLA.\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\nFor more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\ncontact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n\n## Trademarks\n\nThis project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft \ntrademarks or logos is subject to and must follow \n[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).\nUse of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.\nAny use of third-party trademarks or logos are subject to those third-party's policies.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "<!-- BEGIN MICROSOFT SECURITY.MD V0.0.8 BLOCK -->\n\n## Security\n\nMicrosoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).\n\nIf you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.\n\n## Reporting Security Issues\n\n**Please do not report security vulnerabilities through public GitHub issues.**\n\nInstead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).\n\nIf you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com).  If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).\n\nYou should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). \n\nPlease include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:\n\n  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)\n  * Full paths of source file(s) related to the manifestation of the issue\n  * The location of the affected source code (tag/branch/commit or direct URL)\n  * Any special configuration required to reproduce the issue\n  * Step-by-step instructions to reproduce the issue\n  * Proof-of-concept or exploit code (if possible)\n  * Impact of the issue, including how an attacker might exploit the issue\n\nThis information will help us triage your report more quickly.\n\nIf you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.\n\n## Preferred Languages\n\nWe prefer all communications to be in English.\n\n## Policy\n\nMicrosoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).\n\n<!-- END MICROSOFT SECURITY.MD BLOCK -->\n"
  },
  {
    "path": "SUPPORT.md",
    "content": "# Support\r\n\r\n## How to file issues and get help  \r\n\r\nThis project uses GitHub Issues to track bugs and feature requests.\r\nPlease search the existing issues before filing new issues to avoid duplicates.\r\nFor new issues, file your bug or feature request as a new issue.\r\n\r\nFor help and questions about using this project, please either use the project's GitHub Discussions area or Stack Overflow.\r\n\r\n## Microsoft Support Policy  \r\n\r\nSupport for this project is limited to the resources listed above.\r\n"
  },
  {
    "path": "TypeChat.code-workspace",
    "content": "{\n\t\"folders\": [\n\t\t{\n\t\t\t\"name\": \"TypeChat Root\",\n\t\t\t\"path\": \"./\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"Python\",\n\t\t\t\"path\": \"./python\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"TypeScript\",\n\t\t\t\"path\": \"./typescript\"\n\t\t},\n\t],\n\t\"settings\": {\n\t}\n}"
  },
  {
    "path": "dotnet/README.md",
    "content": "# TypeChat for .NET\n\nTypeChat in .NET and C# is currently available on a separate [TypeChat.NET repository](https://github.com/microsoft/TypeChat.net).\n"
  },
  {
    "path": "python/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\nnode_modules\n"
  },
  {
    "path": "python/LICENSE",
    "content": "    MIT License\n\n    Copyright (c) Microsoft Corporation.\n\n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE\n"
  },
  {
    "path": "python/README.md",
    "content": "# TypeChat\n\nTypeChat is a library that makes it easy to build natural language interfaces using types.\n\nBuilding natural language interfaces has traditionally been difficult.\nThese apps often relied on complex decision trees to determine intent and collect the required inputs to take action.\nLarge language models (LLMs) have made this easier by enabling us to take natural language input from a user and match to intent.\nThis has introduced its own challenges, including the need to constrain the model's reply for safety,\nstructure responses from the model for further processing, and ensuring that the reply from the model is valid.\nPrompt engineering aims to solve these problems, but comes with a steep learning curve and increased fragility as the prompt increases in size.\n\nTypeChat replaces _prompt engineering_ with _schema engineering_.\n\nSimply define types that represent the intents supported in your natural language application.\nThat could be as simple as an interface for categorizing sentiment or more complex examples like types for a shopping cart or music application.\nFor example, to add additional intents to a schema, a developer can add additional types into a discriminated union.\nTo make schemas hierarchical, a developer can use a \"meta-schema\" to choose one or more sub-schemas based on user input.\n\nAfter defining your types, TypeChat takes care of the rest by:\n\n1. Constructing a prompt to the LLM using types.\n2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction.\n3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent.\n\nTypes are all you need!\n\n## Getting Started\n\nInstall TypeChat:\n\n```sh\npip install typechat\n```\n\nYou can also develop TypeChat from source, which needs [Python >=3.11](https://www.python.org/downloads/),\n[hatch](https://hatch.pypa.io/1.6/install/), and [Node.js >=20](https://nodejs.org/en/download):\n\n```sh\ngit clone https://github.com/microsoft/TypeChat\ncd TypeChat/python\nhatch shell\nnpm ci\n```\n\nTo see TypeChat in action, we recommend exploring the\n[TypeChat example projects](https://github.com/microsoft/TypeChat/tree/main/python/examples).\nYou can try them on your local machine or in a GitHub Codespace.\n\nTo learn more about TypeChat, visit the\n[documentation](https://microsoft.github.io/TypeChat/docs/python/basic-usage/)\nwhich includes more information on TypeChat and how to get started.\n\n## Contributing\n\nThis project welcomes contributions and suggestions.  Most contributions require you to agree to a\nContributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\nthe rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.\n\nWhen you submit a pull request, a CLA bot will automatically determine whether you need to provide\na CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\nprovided by the bot. You will only need to do this once across all repos using our CLA.\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\nFor more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\ncontact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n\n## Trademarks\n\nThis project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft \ntrademarks or logos is subject to and must follow \n[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).\nUse of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.\nAny use of third-party trademarks or logos are subject to those third-party's policies.\n"
  },
  {
    "path": "python/examples/README.md",
    "content": "\nTo see TypeChat in action, check out the examples found in this directory.\n\nEach example shows how TypeChat handles natural language input, and maps to validated JSON as output. Most example inputs run on both GPT 3.5 and GPT 4.\nWe are working to reproduce outputs with other models.\nGenerally, models trained on both code and natural language text have high accuracy.\n\nWe recommend reading each example in the following order.\n\n\n| Name | Description |\n| ---- | ----------- |\n| [Sentiment](https://github.com/microsoft/TypeChat/tree/main/python/examples/sentiment) | A sentiment classifier which categorizes user input as negative, neutral, or positive. This is TypeChat's \"hello world!\" |\n| [Coffee Shop](https://github.com/microsoft/TypeChat/tree/main/python/examples/coffeeShop) | An intelligent agent for a coffee shop. This sample translates user intent is translated to a list of coffee order items.\n| [Calendar](https://github.com/microsoft/TypeChat/tree/main/python/examples/calendar) | An intelligent scheduler. This sample translates user intent into a sequence of actions to modify a calendar. |\n| [HealthData](https://github.com/microsoft/TypeChat/tree/main/python/examples/healthData) | The Health Data Agent shows how strongly typed **agents with history** could interact with a user to collect information needed for one or more data types (\"form filling\"). |\n| [Restaurant](https://github.com/microsoft/TypeChat/tree/main/python/examples/restaurant) | An intelligent agent for taking orders at a restaurant. Similar to the coffee shop example, but uses a more complex schema to model more complex linguistic input. The prose files illustrate the line between simpler and more advanced language models in handling compound sentences, distractions, and corrections. This example also shows how we can use TypeScript to provide a user intent summary. |\n| [Math](https://github.com/microsoft/TypeChat/tree/main/python/examples/math) | Translate calculations into simple programs given an API that can perform the 4 basic mathematical operators. This example highlights TypeChat's program generation capabilities. |\n| [MultiSchema](https://github.com/microsoft/TypeChat/tree/main/python/examples/multiSchema) | This application demonstrates a simple way to write a **super-app** that automatically routes user requests to child apps. |\n| [Music](https://github.com/microsoft/TypeChat/tree/main/python/examples/music) | An app for playing music, creating playlists, etc. on Spotify through natural language. Each user intent is translated into a series of actions in JSON which correspond to a simple dataflow program, where each step can consume data produced from previous step. |\n\n## Step 1: Configure your development environment\n\n### Option 1: Local Machine\n\nYou can experiment with these TypeChat examples on your local machine.\n\nYou will need [Python >=3.11](https://www.python.org/downloads/) and [hatch](https://hatch.pypa.io/1.6/install/).\n\n```sh\ngit clone https://github.com/microsoft/TypeChat\ncd TypeChat/python\nhatch shell\npython examples/sentiment/demo.py\n```\n\nAlternatively, you can just use `venv` and `pip`:\n\n```sh\ngit clone https://github.com/microsoft/TypeChat\ncd TypeChat/python\npython -m venv ../.venv\n\n# Activate the virtual environment\n# Windows\n../.venv/Scripts/Activate.ps1\n# Unix/POSIX\nsource ../.venv/bin/activate\n\npip install .[examples]\n\npython examples/sentiment/demo.py\n```\n\n### Option 2: GitHub Codespaces\n\nGitHub Codespaces enables you to try TypeChat quickly in a development environment hosted in the cloud.\n\nOn the TypeChat repository page:\n\n1. Click the green button labeled `<> Code`\n2. Select the `Codespaces` tab.\n3. Click the green `Create codespace` button.\n\n<details>\n<summary>If this is your first time creating a codespace, read this.</summary>\n\nIf this is your first time creating a codespace on this repository, GitHub will take a moment to create a dev container image for your session.\nOnce the image has been created, the browser will load Visual Studio Code in a developer environment automatically configured with the necessary prerequisites, TypeChat cloned, and packages installed.\n\nRemember that you are running in the cloud, so all changes you make to the source tree must be committed and pushed before destroying the codespace. GitHub accounts are usually configured to automatically delete codespaces that have been inactive for 30 days.\n\nFor more information, see the [GitHub Codespaces Overview](https://docs.github.com/en/codespaces/overview)\n</details>\n\n## Step 2: Configure environment variables\n\nCurrently, the examples are running on OpenAI or Azure OpenAI endpoints.\nTo use an OpenAI endpoint, include the following environment variables:\n\n| Variable | Value |\n|----------|-------|\n| `OPENAI_MODEL`| The OpenAI model name (e.g. `gpt-3.5-turbo` or `gpt-4`) |\n| `OPENAI_API_KEY` | Your OpenAI API key  |\n| `OPENAI_ENDPOINT` | OpenAI API Endpoint - *optional*,  defaults to `\"https://api.openai.com/v1/chat/completions\"` |\n| `OPENAI_ORGANIZATION` | OpenAI Organization - *optional*, defaults to `\"\"` |\n\nTo use an Azure OpenAI endpoint, include the following environment variables:\n\n| Variable | Value |\n|----------|-------|\n| `AZURE_OPENAI_ENDPOINT` | The full URL of the Azure OpenAI REST API (e.g. `https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2023-05-15`) |\n| `AZURE_OPENAI_API_KEY` | Your Azure OpenAI API key |\n\nWe recommend setting environment variables by creating a `.env` file in the root directory of the project that looks like the following:\n\n```ini\n# For OpenAI\nOPENAI_MODEL=...\nOPENAI_API_KEY=...\n\n# For Azure OpenAI\nAZURE_OPENAI_ENDPOINT=...\nAZURE_OPENAI_API_KEY=...\n```\n\n## Step 3: Run the examples\n\nExamples can be found in the `examples` directory.\n\nTo run an example interactively, type `python examples/<example_name>/demo.py` from the example's directory and enter requests when prompted. Type `quit` or `exit` to end the session. You can also open in VS Code the selected example's directory and press <kbd>F5</kbd> to launch it in debug mode.\n\nNote that there are various sample \"prose\" files (e.g. `input.txt`) provided in each `src` directory that can give a sense of what you can run.\n\nTo run an example with one of these input files, run `python demo.py <input-file-path>`.\nFor example, in the `coffeeShop` directory, you can run:\n\n```\npython demo.py input.txt\n```"
  },
  {
    "path": "python/examples/calendar/README.md",
    "content": "# Calendar\n\nThe Calendar example shows how you can capture user intent as a sequence of actions, such as adding event to a calendar or searching for an event as defined by the [`CalendarActions`](./schema.py) type.\n\n# Try Calendar\n\nTo run the Calendar example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`input.txt`](./input.txt).\n\nFor example, we could use natural language to describe an event coming up soon:\n\n**Input**:\n\n```\n📅> I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024\n```\n\n**Output**:\n\n```json\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"Friday March 15, 2024\",\n        \"timeRange\": {\n          \"startTime\": \"12:00 pm\",\n          \"endTime\": \"2:00 pm\"\n        },\n        \"description\": \"get my tires changed\"\n      }\n    }\n  ]\n}\n```"
  },
  {
    "path": "python/examples/calendar/demo.py",
    "content": "import asyncio\nimport json\n\nimport sys\nfrom dotenv import dotenv_values\nimport schema as calendar\nfrom typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests\n\nasync def main():\n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatValidator(calendar.CalendarActions)\n    translator = TypeChatJsonTranslator(model, validator, calendar.CalendarActions)\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(json.dumps(result, indent=2))\n            if any(item[\"actionType\"] == \"Unknown\" for item in result[\"actions\"]):\n                print(\"I did not understand the following\")\n                for item in result[\"actions\"]:\n                    if item[\"actionType\"] == \"Unknown\":\n                        print(item[\"text\"])\n\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"📅> \", file_path, request_handler)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "python/examples/calendar/input.txt",
    "content": "I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024\nSearch for any meetings with Gavin this week\nSet up an event for friday named Jeffs pizza party at 6pm\nPlease add Jennifer to the scrum next Thursday\nWill you please add an appointment with Jerri Skinner at 9 am?  I need it to last 2 hours\nDo I have any plan with Rosy this month?\nI need to add a meeting with my boss on Monday at 10am. Also make sure to schedule and appointment with Sally, May, and Boris tomorrow at 3pm. Now just add to it Jesse and Abby and make it last ninety minutes\nAdd meeting with team today at 2\ncan you record lunch with Luis at 12pm on Friday and also add Isobel to the Wednesday ping pong game at 4pm\nI said I'd meet with Jenny this afternoon at 2pm and after that I need to go to the dry cleaner and then the soccer game.  Leave an hour for each of those starting at 3:30\n"
  },
  {
    "path": "python/examples/calendar/schema.py",
    "content": "from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc\n\nclass UnknownAction(TypedDict):\n    \"\"\"\n    if the user types text that can not easily be understood as a calendar action, this action is used\n    \"\"\"\n\n    actionType: Literal[\"Unknown\"]\n    text: Annotated[str, Doc(\"text typed by the user that the system did not understand\")]\n\n\nclass EventTimeRange(TypedDict, total=False):\n    startTime: str\n    endTime: str\n    duration: str\n\n\nclass Event(TypedDict):\n    day: Annotated[str, Doc(\"date (example: March 22, 2024) or relative date (example: after EventReference)\")]\n    timeRange: EventTimeRange\n    description: str\n    location: NotRequired[str]\n    participants: NotRequired[Annotated[list[str], Doc(\"a list of people or named groups like 'team'\")]]\n\n\nclass EventReference(TypedDict, total=False):\n    \"\"\"\n    properties used by the requester in referring to an event\n    these properties are only specified if given directly by the requester\n    \"\"\"\n\n    day: Annotated[str, Doc(\"date (example: March 22, 2024) or relative date (example: after EventReference)\")]\n    dayRange: Annotated[str, Doc(\"(examples: this month, this week, in the next two days)\")]\n    timeRange: EventTimeRange\n    description: str\n    location: str\n    participants: list[str]\n\n\nclass FindEventsAction(TypedDict):\n    actionType: Literal[\"find events\"]\n    eventReference: Annotated[EventReference, Doc(\"one or more event properties to use to search for matching events\")]\n\n\nclass ChangeDescriptionAction(TypedDict):\n    actionType: Literal[\"change description\"]\n    eventReference: NotRequired[Annotated[EventReference, Doc(\"event to be changed\")]]\n    description: Annotated[str, Doc(\"new description for the event\")]\n\n\nclass ChangeTimeRangeAction(TypedDict):\n    actionType: Literal[\"change time range\"]\n    eventReference: NotRequired[Annotated[EventReference, Doc(\"event to be changed\")]]\n    timeRange: Annotated[EventTimeRange, Doc(\"new time range for the event\")]\n\n\nclass AddParticipantsAction(TypedDict):\n    actionType: Literal[\"add participants\"]\n    eventReference: NotRequired[\n        Annotated[EventReference, Doc(\"event to be augmented; if not specified assume last event discussed\")]\n    ]\n    participants: NotRequired[Annotated[list[str], \"new participants (one or more)\"]]\n\n\nclass RemoveEventAction(TypedDict):\n    actionType: Literal[\"remove event\"]\n    eventReference: EventReference\n\n\nclass AddEventAction(TypedDict):\n    actionType: Literal[\"add event\"]\n    event: Event\n\n\nActions = (\n    AddEventAction\n    | RemoveEventAction\n    | AddParticipantsAction\n    | ChangeTimeRangeAction\n    | ChangeDescriptionAction\n    | FindEventsAction\n    | UnknownAction\n)\n\n\nclass CalendarActions(TypedDict):\n    actions: list[Actions]\n"
  },
  {
    "path": "python/examples/coffeeShop/README.md",
    "content": "# Coffee Shop\n\nThe Coffee Shop example shows how to capture user intent as a set of \"nouns\".\nIn this case, the nouns are items in a coffee order, where valid items are defined starting from the [`Cart`](./schema.py) type.\nThis example also uses the [`UnknownText`](./schema.py) type as a way to capture user input that doesn't match to an existing type in [`Cart`](./schema.py).\n\n# Try Coffee Shop\n\nTo run the Coffee Shop example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`src/input.txt`](./input.txt) and [`src/input2.txt`](./input2.txt).\n\nFor example, we could use natural language to describe our coffee shop order:\n\n**Input**:\n\n```\n☕> we'd like a cappuccino with a pack of sugar\n```\n\n**Output**:\n\n```json\n{\n  \"items\": [\n    {\n      \"type\": \"lineitem\",\n      \"product\": {\n        \"type\": \"LatteDrinks\",\n        \"name\": \"cappuccino\",\n        \"options\": [\n          {\n            \"type\": \"Sweeteners\",\n            \"name\": \"sugar\",\n            \"optionQuantity\": \"regular\"\n          }\n        ]\n      },\n      \"quantity\": 1\n    }\n  ]\n}\n```"
  },
  {
    "path": "python/examples/coffeeShop/demo.py",
    "content": "import asyncio\nimport json\nimport sys\n\nimport schema as coffeeshop\nfrom dotenv import dotenv_values\n\nfrom typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests\n\nasync def main():\n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatValidator(coffeeshop.Cart)\n    translator = TypeChatJsonTranslator(model, validator, coffeeshop.Cart)\n\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(json.dumps(result, indent=2))\n            if any(item[\"type\"] == \"Unknown\" for item in result[\"items\"]):\n                print(\"I did not understand the following\")\n                for item in result[\"items\"]:\n                    if item[\"type\"] == \"Unknown\":\n                        print(item[\"text\"])\n\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"☕> \", file_path, request_handler)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "python/examples/coffeeShop/input.txt",
    "content": "i'd like a latte that's it\ni'll have a dark roast coffee thank you\nget me a coffee please\ncould i please get two mochas that's all\nwe need twenty five flat whites and that'll do it\nhow about a tall cappuccino\ni'd like a venti iced latte\ni'd like a iced venti latte\ni'd like a venti latte iced\ni'd like a latte iced venti\nwe'll also have a short tall latte\ni wanna latte macchiato with vanilla\nhow about a peppermint latte\nmay i also get a decaf soy vanilla syrup caramel latte with sugar and foam\ni want a latte with peppermint syrup with peppermint syrup\ni'd like a decaf half caf latte\ncan I get a skim soy latte\ni'd like a light nutmeg espresso that's it\ncan i have an cappuccino no foam\ncan i have an espresso with no nutmeg\nwe want a light whipped no foam mocha with extra hazelnut and cinnamon\ni'd like a latte cut in half\ni'd like a strawberry latte\ni want a five pump caramel flat white\ni want a flat white with five pumps of caramel syrup\ni want a two pump peppermint three squirt raspberry skinny vanilla latte with a pump of caramel and two sugars\ni want a latte cappuccino espresso and an apple muffin\ni'd like a tall decaf latte iced a grande cappuccino double espresso and a warmed poppyseed muffin sliced in half\nwe'd like a latte with soy and a coffee with soy\ni want a latte latte macchiato and a chai latte\nwe'd like a cappuccino with two pumps of vanilla\nmake that cappuccino with three pumps of vanilla\nwe'd like a cappuccino with a pack of sugar\nmake that cappuccino with two packs of sugar\nwe'd like a cappuccino with a pack of sugar\nmake that with two packs of sugar\ni'd like a flat white with two equal\nadd three equal to the flat white\ni'd like a flat white with two equal\ntwo tall lattes. the first one with no foam. the second one with whole milk.\ntwo tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande.\nun petit cafe\nen lille kaffe\na raspberry latte\na strawberry latte\nroses are red\ntwo lawnmowers, a grande latte and a tall tree\n"
  },
  {
    "path": "python/examples/coffeeShop/input2.txt",
    "content": "two tall lattes. the first one with no foam. the second one with whole milk.\ntwo tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande.\nun petit cafe\nen lille kaffe\na raspberry latte\na strawberry latte\nroses are red\ntwo lawnmowers, a grande latte and a tall tree\n"
  },
  {
    "path": "python/examples/coffeeShop/schema.py",
    "content": "from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc\n\n\nclass UnknownText(TypedDict):\n    \"\"\"\n    Use this type for order items that match nothing else\n    \"\"\"\n\n    type: Literal[\"Unknown\"]\n    text: Annotated[str, Doc(\"The text that wasn't understood\")]\n\n\nclass Caffeine(TypedDict):\n    type: Literal[\"Caffeine\"]\n    name: Literal[\"regular\", \"two thirds caf\", \"half caf\", \"one third caf\", \"decaf\"]\n\n\nclass Milk(TypedDict):\n    type: Literal[\"Milk\"]\n    name: Literal[\n        \"whole milk\", \"two percent milk\", \"nonfat milk\", \"coconut milk\", \"soy milk\", \"almond milk\", \"oat milk\"\n    ]\n\n\nclass Creamer(TypedDict):\n    type: Literal[\"Creamer\"]\n    name: Literal[\n        \"whole milk creamer\",\n        \"two percent milk creamer\",\n        \"one percent milk creamer\",\n        \"nonfat milk creamer\",\n        \"coconut milk creamer\",\n        \"soy milk creamer\",\n        \"almond milk creamer\",\n        \"oat milk creamer\",\n        \"half and half\",\n        \"heavy cream\",\n    ]\n\n\nclass Topping(TypedDict):\n    type: Literal[\"Topping\"]\n    name: Literal[\"cinnamon\", \"foam\", \"ice\", \"nutmeg\", \"whipped cream\", \"water\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nclass LattePreparation(TypedDict):\n    type: Literal[\"LattePreparation\"]\n    name: Literal[\"for here cup\", \"lid\", \"with room\", \"to go\", \"dry\", \"wet\"]\n\n\nclass Sweetener(TypedDict):\n    type: Literal[\"Sweetener\"]\n    name: Literal[\"equal\", \"honey\", \"splenda\", \"sugar\", \"sugar in the raw\", \"sweet n low\", \"espresso shot\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nCaffeineOptions = Caffeine | Milk | Creamer\n\nLatteOptions = CaffeineOptions | Topping | LattePreparation | Sweetener\n\nCoffeeTemperature = Literal[\"hot\", \"extra hot\", \"warm\", \"iced\"]\n\nCoffeeSize = Literal[\"short\", \"tall\", \"grande\", \"venti\"]\n\nEspressoSize = Literal[\"solo\", \"doppio\", \"triple\", \"quad\"]\n\nOptionQuantity = Literal[\"no\", \"light\", \"regular\", \"extra\"] | int\n\n\nclass Syrup(TypedDict):\n    type: Literal[\"Syrup\"]\n    name: Literal[\n        \"almond syrup\",\n        \"buttered rum syrup\",\n        \"caramel syrup\",\n        \"cinnamon syrup\",\n        \"hazelnut syrup\",\n        \"orange syrup\",\n        \"peppermint syrup\",\n        \"raspberry syrup\",\n        \"toffee syrup\",\n        \"vanilla syrup\",\n    ]\n    optionQuantity: NotRequired[OptionQuantity]\n\n\nclass LatteDrink(TypedDict):\n    type: Literal[\"LatteDrink\"]\n    name: Literal[\"cappuccino\", \"flat white\", \"latte\", \"latte macchiato\", \"mocha\", \"chai latte\"]\n    temperature: NotRequired[\"CoffeeTemperature\"]\n    size: NotRequired[Annotated[CoffeeSize, Doc(\"The default is 'grande'\")]]\n    options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]]\n\n\nclass EspressoDrink(TypedDict):\n    type: Literal[\"EspressoDrink\"]\n    name: Literal[\"espresso\", \"lungo\", \"ristretto\", \"macchiato\"]\n    temperature: NotRequired[\"CoffeeTemperature\"]\n    size: NotRequired[Annotated[\"EspressoSize\", Doc(\"The default is 'doppio'\")]]\n    options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]]\n\n\nclass CoffeeDrink(TypedDict):\n    type: Literal[\"CoffeeDrink\"]\n    name: Literal[\"americano\", \"coffee\"]\n    temperature: NotRequired[CoffeeTemperature]\n    size: NotRequired[Annotated[CoffeeSize, Doc(\"The default is 'grande'\")]]\n    options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]]\n\n\nclass BakeryOption(TypedDict):\n    type: Literal[\"BakeryOption\"]\n    name: Literal[\"butter\", \"strawberry jam\", \"cream cheese\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nclass BakeryPreparation(TypedDict):\n    type: Literal[\"BakeryPreparation\"]\n    name: Literal[\"warmed\", \"cut in half\"]\n\n\nclass BakeryProduct(TypedDict):\n    type: Literal[\"BakeryProduct\"]\n    name: Literal[\"apple bran muffin\", \"blueberry muffin\", \"lemon poppyseed muffin\", \"bagel\"]\n    options: list[BakeryOption | BakeryPreparation]\n\n\nProduct = BakeryProduct | LatteDrink | EspressoDrink | CoffeeDrink | UnknownText\n\n\nclass LineItem(TypedDict):\n    type: Literal[\"LineItem\"]\n    product: Product\n    quantity: int\n\n\nclass Cart(TypedDict):\n    type: Literal[\"Cart\"]\n    items: list[LineItem | UnknownText]"
  },
  {
    "path": "python/examples/healthData/README.md",
    "content": "# Health Data Agent\n\nThis example requires GPT-4.\n\nDemonstrates a ***strongly typed*** chat: a natural language interface for entering health information. You work with a *health data agent* to interactively enter your medications or conditions.\n\nThe Health Data Agent shows how strongly typed **agents with history** could interact with a user to collect information needed for one or more data types (\"form filling\").\n\n## Target models\n\nFor best and consistent results, use **gpt-4**.\n\n## Try the Health Data Agent\n\nTo run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n## Usage\n\nExample prompts can be found in [`input.txt`](./input.txt).\n\nFor example, given the following input statement:\n\n**Input**:\n\n```console\n🤧> I am taking klaritin for my allergies\n\n```\n\n**Output**:\n"
  },
  {
    "path": "python/examples/healthData/demo.py",
    "content": "import asyncio\nimport json\nimport sys\nfrom dotenv import dotenv_values\nimport schema as health\nfrom typechat import Failure, TypeChatValidator, create_language_model, process_requests\nfrom translator import TranslatorWithHistory\n\nhealth_instructions = \"\"\"\nHelp me enter my health data step by step.\nAsk specific questions to gather required and optional fields I have not already providedStop asking if I don't know the answer\nAutomatically fix my spelling mistakes\nMy health data may be complex: always record and return ALL of it.\nAlways return a response:\n- If you don't understand what I say, ask a question.\n- At least respond with an OK message.\n\n\"\"\"\n\nasync def main():\n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatValidator(health.HealthDataResponse)\n    translator = TranslatorWithHistory(\n        model, validator, health.HealthDataResponse, additional_agent_instructions=health_instructions\n    )\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(json.dumps(result, indent=2))\n\n            agent_message = result.get(\"message\", \"None\")\n            not_translated = result.get(\"notTranslated\", None)\n\n            if agent_message:\n                print(f\"\\n📝: {agent_message}\")\n\n            if not_translated:\n                print(f\"\\n🤔: I did not understand\\n {not_translated}\")\n\n\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"💉💊🤧> \", file_path, request_handler)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "python/examples/healthData/input.txt",
    "content": "#\n# Conversations with a Health Data Agent\n# For each conversation: \n# You start with the first line \n# Then type the next line in response\n#\n\n# ================\n# USE GPT4\n# ================\n# Conversation:\ni want to record my shingles\nAugust 2016\nIt lasted 3 months\nI also broke my foot\nI broke it in high school\n2001\nThe foot took a year to be ok\n\n# Conversation: \nklaritin\n2 tablets 3 times a day\n300 mg\nactually that is 1 tablet\n@clear\n\n# Conversation: \nklaritin\n1 pill, morning and before bedtime\nCan't remember\nActually, that is 3 tablets\n500 mg\n@clear\n\n#Conversation\nI am taking binadryl now\nAs needed. Groceery store strength\nThat is all I have\nI also got allergies. Pollen\n@clear\n\n# Conversation: \nRobotussin\n1 cup\nDaily, as needed\nRobotussin with Codeine\nPut down strength as I don't know\n@clear\n\n# Conversation: \nHey\nMelatonin\n1 3mg tablet every night\n@clear\n\n# Conversation: \nI got the flu\nStarted 2 weeks ago\nIts gone now. Only lasted about a week\nI took some sudafed though\nI took 2 sudafed twice a day. Regular strength\n@clear\n\n"
  },
  {
    "path": "python/examples/healthData/schema.py",
    "content": "from typing_extensions import TypedDict, Annotated, NotRequired, Literal, Doc\n\n\nclass Quantity(TypedDict):\n    value: Annotated[float, Doc(\"Exact number\")]\n    units: Annotated[str, Doc(\"UNITS include mg, kg, cm, pounds, liter, ml, tablet, pill, cup, per-day, per-week..ETC\")]\n\n\nclass ApproxDatetime(TypedDict):\n    displayText: Annotated[str, Doc(\"Default: Unknown. Required\")]\n    timestamp: NotRequired[Annotated[str, Doc(\"If precise timestamp can be set\")]]\n\n\nclass ApproxQuantity(TypedDict):\n    displayText: Annotated[str, Doc(\"Default: Unknown. Required\")]\n    quantity: NotRequired[Annotated[Quantity, Doc(\"Optional: only if precise quantities are available\")]]\n\n\nclass OtherHealthData(TypedDict):\n    \"\"\"\n    Use for health data that match nothing else. E.g. immunization, blood prssure etc\n    \"\"\"\n\n    text: str\n    when: NotRequired[ApproxDatetime]\n\n\nclass Condition(TypedDict):\n    \"\"\"\n    Disease, Ailment, Injury, Sickness\n    \"\"\"\n\n    name: Annotated[str, Doc(\"Fix any spelling mistakes, especially phonetic spelling\")]\n    startDate: Annotated[ApproxDatetime, Doc(\"When the condition started? Required\")]\n    status: Annotated[\n        Literal[\"active\", \"recurrence\", \"relapse\", \"inactive\", \"remission\", \"resolved\", \"unknown\"],\n        Doc(\"Always ask for current status of the condition\"),\n    ]\n    endDate: NotRequired[Annotated[ApproxDatetime, Doc(\"If the condition was no longer active\")]]\n\n\nclass Medication(TypedDict):\n    \"\"\"\n    Meds, pills etc.\n    \"\"\"\n\n    name: Annotated[str, Doc(\"Fix any spelling mistakes, especially phonetic spelling\")]\n    dose: Annotated[ApproxQuantity, Doc(\"E.g. 2 tablets, 1 cup. Required\")]\n    frequency: Annotated[ApproxQuantity, Doc(\"E.g. twice a day. Required\")]\n    strength: Annotated[ApproxQuantity, Doc(\"E.g. 50 mg. Required\")]\n\n\nclass HealthData(TypedDict, total=False):\n    medication: list[Medication]\n    condition: list[Condition]\n    other: list[OtherHealthData]\n\n\nclass HealthDataResponse(TypedDict, total=False):\n    data: Annotated[HealthData, Doc(\"Return this if JSON has ALL required information. Else ask questions\")]\n    message: Annotated[str, Doc(\"Use this to ask questions and give pertinent responses\")]\n    notTranslated: Annotated[str, Doc(\"Use this parts of the user request not translateed, off topic, etc\")]\n"
  },
  {
    "path": "python/examples/healthData/translator.py",
    "content": "import json\nfrom typing_extensions import TypeVar, Any, override, TypedDict, Literal\n\nfrom typechat import TypeChatValidator, TypeChatLanguageModel, TypeChatJsonTranslator, Result, Failure, PromptSection\n\nfrom datetime import datetime\n\nT = TypeVar(\"T\", covariant=True)\n\n\nclass ChatMessage(TypedDict):\n    source: Literal[\"system\", \"user\", \"assistant\"]\n    body: Any\n\n\nclass TranslatorWithHistory(TypeChatJsonTranslator[T]):\n    _chat_history: list[ChatMessage]\n    _max_prompt_length: int\n    _additional_agent_instructions: str\n\n    def __init__(\n        self, model: TypeChatLanguageModel, validator: TypeChatValidator[T], target_type: type[T], additional_agent_instructions: str\n    ):\n        super().__init__(model=model, validator=validator, target_type=target_type)\n        self._chat_history = []\n        self._max_prompt_length = 2048\n        self._additional_agent_instructions = additional_agent_instructions\n\n    @override\n    async def translate(self, input: str, *, prompt_preamble: str | list[PromptSection] | None = None) -> Result[T]:\n        result = await super().translate(input=input, prompt_preamble=prompt_preamble)\n        if not isinstance(result, Failure):\n            self._chat_history.append(ChatMessage(source=\"assistant\", body=result.value))\n        return result\n\n    @override\n    def _create_request_prompt(self, intent: str) -> str:\n        # TODO: drop history entries if we exceed the max_prompt_length\n        history_str = json.dumps(self._chat_history, indent=2, default=lambda o: None, allow_nan=False)\n\n        now = datetime.now()\n\n        prompt = F\"\"\"\nuser: You are a service that translates user requests into JSON objects of type  \"{self.type_name}\" according to the following TypeScript definitions:\n'''\n{self.schema_str}\n'''\n\nuser:\nUse precise date and times RELATIVE TO CURRENT DATE: {now.strftime('%A, %m %d, %Y')} CURRENT TIME: {now.strftime(\"%H:%M:%S\")}\nAlso turn ranges like next week and next month into precise dates\n\nuser:\n{self._additional_agent_instructions}\n\nsystem:\nIMPORTANT CONTEXT for the user request:\n{history_str}\n\nuser:\nThe following is a user request:\n'''\n{intent}\n'''\n    The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n\"\"\"\n        return prompt\n"
  },
  {
    "path": "python/examples/math/README.md",
    "content": "# Math\n\nThe Math example shows how to use TypeChat for program generation based on an API schema with the `evaluateJsonProgram` function. This example translates calculations into simple programs given an [`API`](./schema.py) type that can perform the four basic mathematical operations.\n\n# Try Math\n\nTo run the Math example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`input.txt`](./input.txt).\n\nFor example, we could use natural language to describe mathematical operations, and TypeChat will generate a program that can execute the math API defined in the schema.\n\n**Input**:\n\n```\n🟰> multiply two by three, then multiply four by five, then sum the results\n```\n\n**Output**:\n\n```\nimport { API } from \"./schema\";\nfunction program(api: API) {\n  const step1 = api.mul(2, 3);\n  const step2 = api.mul(4, 5);\n  return api.add(step1, step2);\n}\nRunning program:\nmul(2, 3)\nmul(4, 5)\nadd(6, 20)\nResult: 26\n```\n"
  },
  {
    "path": "python/examples/math/demo.py",
    "content": "import asyncio\nfrom collections.abc import Sequence\nimport json\nimport sys\nfrom typing import cast\nfrom dotenv import dotenv_values\nimport schema as math\nfrom typechat import Failure, create_language_model, process_requests\nfrom program import TypeChatProgramTranslator, TypeChatProgramValidator, evaluate_json_program\n\nasync def main():\n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatProgramValidator()\n    translator = TypeChatProgramTranslator(model, validator, math.MathAPI)\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(json.dumps(result, indent=2))\n            math_result = await evaluate_json_program(result, apply_operations)\n            print(f\"Math Result: {math_result}\")\n\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"🧮> \", file_path, request_handler)\n\n\nasync def apply_operations(func: str, args: Sequence[object]) -> int | float:\n    print(f\"{func}({json.dumps(args)}) \")\n\n    for arg in args:\n        if not isinstance(arg, (int, float)):\n            raise ValueError(\"All arguments are expected to be numeric.\")\n\n    args = cast(Sequence[int | float], args)\n\n    match func:\n        case \"add\":\n            return args[0] + args[1]\n        case \"sub\":\n            return args[0] - args[1]\n        case \"mul\":\n            return args[0] * args[1]\n        case \"div\":\n            return args[0] / args[1]\n        case \"neg\":\n            return -1 * args[0]\n        case \"id\":\n            return args[0]\n        case _:\n            raise ValueError(f'Unexpected function name {func}')\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "python/examples/math/input.txt",
    "content": "1 + 2\r\n1 + 2 * 3\r\n2 * 3 + 4 * 5\r\n2 3 * 4 5 * +\r\nmultiply two by three, then multiply four by five, then sum the results\r\n"
  },
  {
    "path": "python/examples/math/program.py",
    "content": "from __future__ import annotations\nimport asyncio\nfrom collections.abc import Sequence\nfrom typing import Any, TypeAlias, TypedDict, cast\nfrom typing_extensions import (\n    TypeVar,\n    Callable,\n    Awaitable,\n    Annotated,\n    NotRequired,\n    override,\n    Doc,\n)\n\nfrom typechat import (\n    Failure,\n    Success,\n    TypeChatLanguageModel,\n    TypeChatValidator,\n    TypeChatJsonTranslator,\n    python_type_to_typescript_schema,\n)\n\nT = TypeVar(\"T\", covariant=True)\n\n\nExpression: TypeAlias = \"str | int | float | bool | None | dict[str, Expression] | list[Expression] | FunctionCall | ResultReference\"\n\nJsonProgram = TypedDict(\"JsonProgram\", {\"@steps\": list[\"FunctionCall\"]})\n\n\nResultReference = TypedDict(\n    \"ResultReference\", {\"@ref\": Annotated[int, Doc(\"Index of the previous expression in the 'steps' array\")]}\n)\n\nFunctionCall = TypedDict(\n    \"FunctionCall\",\n    {\n        \"@func\": Annotated[str, Doc(\"Name of the function\")],\n        \"@args\": NotRequired[Annotated[list[Expression], Doc(\"Arguments for the function, if any\")]],\n    },\n)\n\ntranslation_result = python_type_to_typescript_schema(JsonProgram)\nprogram_schema_text = translation_result.typescript_schema_str\n\n\nJsonValue = str | int | float | bool | None | dict[str, \"JsonValue\"] | list[\"JsonValue\"]\n\nasync def evaluate_json_program(\n    program: JsonProgram,\n    onCall: Callable[[str, Sequence[JsonValue]], Awaitable[JsonValue]]\n) -> JsonValue:\n    results: list[JsonValue] = []\n\n    def evaluate_array(array: Sequence[JsonValue]) -> Awaitable[list[JsonValue]]:\n        return asyncio.gather(*[evaluate_expression(e) for e in array])\n\n    async def evaluate_expression(expr: JsonValue) -> JsonValue:\n        match expr:\n            case bool() | int() | float() | str() | None:\n                return expr\n\n            case { \"@ref\": int(index) } if not isinstance(index, bool):\n                if 0 <= index < len(results):\n                    return results[index]\n                \n                raise ValueError(f\"Index {index} is out of range [0, {len(results)})\")\n\n            case { \"@ref\": ref_value }:\n                raise ValueError(f\"'ref' value must be an integer, but was ${ref_value}\")\n\n            case { \"@func\": str(function_name) }:\n                args: list[JsonValue]\n                match expr:\n                    case { \"@args\": None }:\n                        args = []\n                    case { \"@args\": list() }:\n                        args = cast(list[JsonValue], expr[\"@args\"]) # TODO\n                    case { \"@args\": _ }:\n                        raise ValueError(\"Given an invalid value for '@args'.\")\n                    case _:\n                        args = []\n\n                return await onCall(function_name, await evaluate_array(args))\n            \n            case list(array_expression_elements):\n                return await evaluate_array(array_expression_elements)    \n\n            case _:\n                raise ValueError(\"This condition should never hit\")\n\n    for step in program[\"@steps\"]:\n        results.append(await evaluate_expression(cast(JsonValue, step)))\n\n    if len(results) > 0:\n        return results[-1]\n    else:\n        return None\n\n\nclass TypeChatProgramValidator(TypeChatValidator[JsonProgram]):\n    def __init__(self):\n        # TODO: This example should eventually be updated to use Python 3.12 type aliases\n        # Passing in `JsonProgram` for `py_type` would cause issues because\n        # Pydantic's `TypeAdapter` ends up trying to eagerly construct an\n        # anonymous recursive type. Even a NewType does not work here.\n        # For now, we just pass in `Any` in place of `JsonProgram`.\n        super().__init__(py_type=cast(type[JsonProgram], Any))\n\n    @override\n    def validate_object(self, obj: Any) -> Success[JsonProgram] | Failure:\n        if \"@steps\" in obj and isinstance(obj[\"@steps\"], Sequence):\n            return Success(obj)\n        else:\n            return Failure(\"This is not a valid program. The program must have an array of @steps\")\n        \n\n\nclass TypeChatProgramTranslator(TypeChatJsonTranslator[JsonProgram]):\n    _api_declaration_str: str\n\n    def __init__(self, model: TypeChatLanguageModel, validator: TypeChatProgramValidator, api_type: type):\n        super().__init__(model=model, validator=validator, target_type=api_type, _raise_on_schema_errors = False)\n        # TODO: the conversion result here has errors!\n        conversion_result = python_type_to_typescript_schema(api_type)\n        self._api_declaration_str = conversion_result.typescript_schema_str\n\n    @override\n    def _create_request_prompt(self, intent: str) -> str:\n\n        prompt = F\"\"\"\nYou are a service that translates user requests into programs represented as JSON using the following TypeScript definitions:\n```\n{program_schema_text}\n```\nThe programs can call functions from the API defined in the following TypeScript definitions:\n```\n{self._api_declaration_str}\n```\nThe following is a user request:\n'''\n{intent}\n'''\nThe following is the user request translated into a JSON program object with 2 spaces of indentation and no properties with the value undefined:\n\"\"\"\n        return prompt\n\n    @override\n    def _create_repair_prompt(self, validation_error: str) -> str:\n        prompt = F\"\"\"\nThe JSON program object is invalid for the following reason:\n'''\n{validation_error}\n'''\nThe following is a revised JSON program object:\n\"\"\"\n        return prompt\n"
  },
  {
    "path": "python/examples/math/schema.py",
    "content": "from typing_extensions import TypedDict, Annotated, Callable, Doc\n\n\nclass MathAPI(TypedDict):\n    \"\"\"\n    This is API for a simple calculator\n    \"\"\"\n\n    add: Annotated[Callable[[float, float], float], Doc(\"Add two numbers\")]\n    sub: Annotated[Callable[[float, float], float], Doc(\"Subtract two numbers\")]\n    mul: Annotated[Callable[[float, float], float], Doc(\"Multiply two numbers\")]\n    div: Annotated[Callable[[float, float], float], Doc(\"Divide two numbers\")]\n    neg: Annotated[Callable[[float], float], Doc(\"Negate a number\")]\n    id: Annotated[Callable[[float], float], Doc(\"Identity function\")]\n    unknown: Annotated[Callable[[str], float], Doc(\"Unknown request\")]\n"
  },
  {
    "path": "python/examples/math/schemaV2.py",
    "content": "from typing_extensions import Protocol, runtime_checkable\n\n\n@runtime_checkable\nclass MathAPI(Protocol):\n    \"\"\"\n    This is API for a simple calculator\n    \"\"\"\n\n    def add(self, x: float, y: float) -> float:\n        \"\"\"\n        Add two numbers\n        \"\"\"\n        ...\n\n    def sub(self, x: float, y: float) -> float:\n        \"\"\"\n        Subtract two numbers\n        \"\"\"\n        ...\n\n    def mul(self, x: float, y: float) -> float:\n        \"\"\"\n        Multiply two numbers\n        \"\"\"\n        ...\n\n    def div(self, x: float, y: float) -> float:\n        \"\"\"\n        Divide two numbers\n        \"\"\"\n        ...\n\n    def neg(self, x: float) -> float:\n        \"\"\"\n        Negate a number\n        \"\"\"\n        ...\n\n    def id(self, x: float, y: float) -> float:\n        \"\"\"\n        Identity function\n        \"\"\"\n        ...\n\n    def unknown(self, text: str) -> float:\n        \"\"\"\n        unknown request\n        \"\"\"\n        ...\n"
  },
  {
    "path": "python/examples/multiSchema/README.md",
    "content": "# MultiSchema\n\nThis application demonstrates a simple way to write a **super-app** that automatically routes user requests to child apps. \n\nIn this example, the child apps are existing TypeChat chat examples:\n\n* CoffeeShop\n* Restaurant\n* Calendar\n* Sentiment\n* Math\n* Plugins\n* HealthData\n\n## Target Models\n\nWorks with GPT-3.5 Turbo and GPT-4.\n\nSub-apps like HealthData and Plugins work best with GPT-4.\n\n# Usage\n\nExample prompts can be found in [`input.txt`](input.txt).\n"
  },
  {
    "path": "python/examples/multiSchema/agents.py",
    "content": "from collections.abc import Sequence\r\nimport os\r\nimport sys\r\nfrom typing import cast\r\n\r\nexamples_path = os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"..\"))\r\nif examples_path not in sys.path:\r\n    sys.path.append(examples_path)\r\n\r\nimport json\r\n\r\nfrom typing_extensions import TypeVar, Generic\r\nfrom typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, TypeChatLanguageModel\r\n\r\nimport examples.math.schema as math_schema\r\nfrom examples.math.program import (\r\n    TypeChatProgramTranslator,\r\n    TypeChatProgramValidator,\r\n    evaluate_json_program,\r\n)\r\n\r\nimport examples.music.schema as music_schema\r\nfrom examples.music.client import ClientContext, handle_call, get_client_context\r\n\r\nT = TypeVar(\"T\", covariant=True)\r\n\r\n\r\nclass JsonPrintAgent(Generic[T]):\r\n    _validator: TypeChatValidator[T]\r\n    _translator: TypeChatJsonTranslator[T]\r\n\r\n    def __init__(self, model: TypeChatLanguageModel, target_type: type[T]):\r\n        super().__init__()\r\n        self._validator = TypeChatValidator(target_type)\r\n        self._translator = TypeChatJsonTranslator(model, self._validator, target_type)\r\n\r\n    async def handle_request(self, line: str):\r\n        result = await self._translator.translate(line)\r\n        if isinstance(result, Failure):\r\n            print(result.message)\r\n        else:\r\n            result = result.value\r\n            print(json.dumps(result, indent=2))\r\n\r\n\r\nclass MathAgent:\r\n    _validator: TypeChatProgramValidator\r\n    _translator: TypeChatProgramTranslator\r\n\r\n    def __init__(self, model: TypeChatLanguageModel):\r\n        super().__init__()\r\n        self._validator = TypeChatProgramValidator()\r\n        self._translator = TypeChatProgramTranslator(model, self._validator, math_schema.MathAPI)\r\n\r\n    async def _handle_json_program_call(self, func: str, args: Sequence[object]) -> int | float:\r\n        print(f\"{func}({json.dumps(args)}) \")\r\n        \r\n        for arg in args:\r\n            if not isinstance(arg, (int, float)):\r\n                raise ValueError(\"All arguments are expected to be numeric.\")\r\n\r\n        args = cast(Sequence[int | float], args)\r\n\r\n        match func:\r\n            case \"add\":\r\n                return args[0] + args[1]\r\n            case \"sub\":\r\n                return args[0] - args[1]\r\n            case \"mul\":\r\n                return args[0] * args[1]\r\n            case \"div\":\r\n                return args[0] / args[1]\r\n            case \"neg\":\r\n                return -1 * args[0]\r\n            case \"id\":\r\n                return args[0]\r\n            case _:\r\n                raise ValueError(f'Unexpected function name {func}')\r\n\r\n    async def handle_request(self, line: str):\r\n        result = await self._translator.translate(line)\r\n        if isinstance(result, Failure):\r\n            print(result.message)\r\n        else:\r\n            result = result.value\r\n            print(json.dumps(result, indent=2))\r\n\r\n            math_result = await evaluate_json_program(result, self._handle_json_program_call)\r\n            print(f\"Math Result: {math_result}\")\r\n\r\n\r\nclass MusicAgent:\r\n    _validator: TypeChatValidator[music_schema.PlayerActions]\r\n    _translator: TypeChatJsonTranslator[music_schema.PlayerActions]\r\n    _client_context: ClientContext | None\r\n    _authentication_vals: dict[str, str | None]\r\n\r\n    def __init__(self, model: TypeChatLanguageModel, authentication_vals: dict[str, str | None]):\r\n        super().__init__()\r\n        self._validator = TypeChatValidator(music_schema.PlayerActions)\r\n        self._translator = TypeChatJsonTranslator(model, self._validator, music_schema.PlayerActions)\r\n        self._authentication_vals = authentication_vals\r\n        self._client_context = None\r\n\r\n    async def authenticate(self):\r\n        self._client_context = await get_client_context(self._authentication_vals)\r\n\r\n    async def handle_request(self, line: str):\r\n        if not self._client_context:\r\n            await self.authenticate()\r\n\r\n        assert self._client_context\r\n        result = await self._translator.translate(line)\r\n        if isinstance(result, Failure):\r\n            print(result.message)\r\n        else:\r\n            result = result.value\r\n            print(json.dumps(result, indent=2))\r\n\r\n            try:\r\n                for action in result[\"actions\"]:\r\n                    await handle_call(action, self._client_context)\r\n            except Exception as error:\r\n                print(\"An exception occurred: \", error)\r\n"
  },
  {
    "path": "python/examples/multiSchema/demo.py",
    "content": "import os\r\nimport sys\r\n\r\nexamples_path = os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"..\"))\r\nif examples_path not in sys.path:\r\n    sys.path.append(examples_path)\r\n\r\nimport asyncio\r\nfrom dotenv import dotenv_values\r\nfrom typechat import create_language_model, process_requests\r\n\r\nfrom router import TextRequestRouter\r\nfrom agents import MathAgent, JsonPrintAgent, MusicAgent\r\nimport examples.restaurant.schema as restaurant\r\nimport examples.calendar.schema as calendar\r\nimport examples.coffeeShop.schema as coffeeShop\r\nimport examples.sentiment.schema as sentiment\r\n\r\n\r\nasync def handle_unknown(_line: str):\r\n    print(\"The input did not match any registered agents\")\r\n\r\nasync def main():\r\n    env_vals = dotenv_values()\r\n    model = create_language_model(env_vals)\r\n    router = TextRequestRouter(model=model)\r\n\r\n    # register agents\r\n    math_agent = MathAgent(model=model)\r\n    router.register_agent(\r\n        name=\"Math\", description=\"Calculations using the four basic math operations\", handler=math_agent.handle_request\r\n    )\r\n\r\n    music_agent = MusicAgent(model=model, authentication_vals=env_vals)\r\n    await music_agent.authenticate()\r\n    router.register_agent(\r\n        name=\"Music Player\",\r\n        description=\"Actions related to music, podcasts, artists, and managing music libraries\",\r\n        handler=music_agent.handle_request,\r\n    )\r\n\r\n    coffee_agent = JsonPrintAgent(model=model, target_type=coffeeShop.Cart)\r\n    router.register_agent(\r\n        name=\"CoffeeShop\",\r\n        description=\"Order Coffee Drinks (Italian names included) and Baked Goods\",\r\n        handler=coffee_agent.handle_request,\r\n    )\r\n\r\n    calendar_agent = JsonPrintAgent(model=model, target_type=calendar.CalendarActions)\r\n    router.register_agent(\r\n        name=\"Calendar\",\r\n        description=\"Actions related to calendars, appointments, meetings, schedules\",\r\n        handler=calendar_agent.handle_request,\r\n    )\r\n\r\n    restaurant_agent = JsonPrintAgent(model=model, target_type=restaurant.Order)\r\n    router.register_agent(\r\n        name=\"Restaurant\", description=\"Order pizza, beer and salads\", handler=restaurant_agent.handle_request\r\n    )\r\n\r\n    sentiment_agent = JsonPrintAgent(model=model, target_type=sentiment.Sentiment)\r\n    router.register_agent(\r\n        name=\"Sentiment\",\r\n        description=\"Statements with sentiments, emotions, feelings, impressions about places, things, the surroundings\",\r\n        handler=sentiment_agent.handle_request,\r\n    )\r\n\r\n    # register a handler for unknown results\r\n    router.register_agent(name=\"No Match\", description=\"Handles all unrecognized requests\", handler=handle_unknown)\r\n\r\n    async def request_handler(message: str):\r\n        await router.route_request(message)\r\n\r\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\r\n    await process_requests(\"🔀> \", file_path, request_handler)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())\r\n"
  },
  {
    "path": "python/examples/multiSchema/input.txt",
    "content": "I'd like two large, one with pepperoni and the other with extra sauce.  The pepperoni gets basil and the extra sauce gets Canadian bacon.  And add a whole salad. \nI also want an espresso with extra foam and a muffin with jam\nAnd book me a lunch with Claude Debussy next week at 12.30 at Le Petit Chien!\nI bought 4 shoes for 12.50 each. How much did I spend?\nIts cold!\nIts cold and I want hot cafe to warm me up\nThe coffee is cold\nThe coffee is awful\n(2*4)+(9*7)"
  },
  {
    "path": "python/examples/multiSchema/router.py",
    "content": "import json\r\nfrom typing_extensions import Any, Callable, Awaitable, TypedDict, Annotated\r\nfrom typechat import Failure, TypeChatValidator, TypeChatLanguageModel, TypeChatJsonTranslator\r\n\r\n\r\nclass AgentInfo(TypedDict):\r\n    name: str\r\n    description: str\r\n    handler: Callable[[str], Awaitable[Any]]\r\n\r\n\r\nclass TaskClassification(TypedDict):\r\n    task_kind: Annotated[str, \"Describe the kind of task to perform.\"]\r\n\r\n\r\nclass TextRequestRouter:\r\n    _current_agents: dict[str, AgentInfo]\r\n    _validator: TypeChatValidator[TaskClassification]\r\n    _translator: TypeChatJsonTranslator[TaskClassification]\r\n\r\n    def __init__(self, model: TypeChatLanguageModel):\r\n        super().__init__()\r\n        self._validator = TypeChatValidator(TaskClassification)\r\n        self._translator = TypeChatJsonTranslator(model, self._validator, TaskClassification)\r\n        self._current_agents = {}\r\n\r\n    def register_agent(self, name: str, description: str, handler: Callable[[str], Awaitable[Any]]):\r\n        agent = AgentInfo(name=name, description=description, handler=handler)\r\n        self._current_agents[name] = agent\r\n\r\n    async def route_request(self, line: str):\r\n        classes_str = json.dumps(self._current_agents, indent=2, default=lambda o: None, allow_nan=False)\r\n\r\n        prompt_fragment = F\"\"\"\r\nClassify \"\"{line}\"\" using the following classification table:\r\n'''\r\n{classes_str}\r\n'''\r\n\"\"\"\r\n\r\n        result = await self._translator.translate(prompt_fragment)\r\n        if isinstance(result, Failure):\r\n            print(\"Translation Failed ❌\")\r\n            print(f\"Context: {result.message}\")\r\n        else:\r\n            result = result.value\r\n            print(\"Translation Succeeded! ✅\\n\")\r\n            print(f\"The target class is {result['task_kind']}\")\r\n            target = self._current_agents[result[\"task_kind\"]]\r\n            await target.get(\"handler\")(line)\r\n"
  },
  {
    "path": "python/examples/music/README.md",
    "content": "# Music\r\n\r\nThe Music example shows how to capture user intent as actions in JSON which corresponds to a simple dataflow program over the API provided in the intent schema. This example shows this pattern using natural language to control the Spotify API to play music, create playlists, and perform other actions from the API. \r\n\r\n# Try Music\r\n\r\nA Spotify Premium account is required to run this example.\r\n\r\nTo run the Music example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\r\n\r\nThis example also requires additional setup to use the Spotify API:\r\n\r\n1. Go to https://developer.spotify.com/dashboard. \r\n2. Log into Spotify with your user account if you are not already logged in.\r\n3. Click the button in the upper right labeled \"Create App\".\r\n4. Fill in the form, making sure the Redirect URI is http://localhost:PORT/callback, where PORT is a four-digit port number you choose for the authorization redirect.\r\n5. Click the settings button and copy down the Client ID and Client Secret (the client secret requires you to click 'View client secret').\r\n6. In your `.env` file, set `SPOTIFY_APP_CLI` to your Client ID and `SPOTIFY_APP_CLISEC` to your Client Secret.  Also set `SPOTIFY_APP_PORT` to the PORT on your local machine that you chose in step 4.\r\n\r\n# Usage\r\nExample prompts can be found in [`input.txt`](./input.txt).\r\n\r\nFor example, use natural language to start playing a song with the Spotify player:\r\n\r\n**Input**:\r\n\r\n```\r\n🎵> play shake it off by taylor swift\r\n```\r\n\r\n**Output**:\r\n\r\n```\r\nJSON View\r\n{\r\n  \"actions\": [\r\n    {\r\n      \"actionName\": \"play\",\r\n      \"parameters\": {\r\n        \"artist\": \"taylor swift\",\r\n        \"trackName\": \"shake it off\",\r\n        \"quantity\": 0\r\n      }\r\n    }\r\n  ]\r\n}\r\nPlaying...\r\nShake It Off\r\n```"
  },
  {
    "path": "python/examples/music/client.py",
    "content": "import os\nimport sys\n\ncurrent_path = os.path.abspath(os.path.dirname(__file__))\nif current_path not in sys.path:\n    sys.path.append(current_path)\n\nimport math\nfrom typing import Any, Optional\n\nfrom pydantic.dataclasses import dataclass\nimport spotipy # type: ignore\n\nfrom schema import PlayerAction\nfrom spotipyWrapper import SimplifiedTrackInfo, SimplifiedPlaylistInfo, AsyncSpotipy\n\n\nclass Config:\n    arbitrary_types_allowed = True\n\n\n@dataclass(config=Config)\nclass ClientContext:\n    service: AsyncSpotipy\n    userId: str\n    deviceId: Optional[str] = None\n    currentTrackList: Optional[list[SimplifiedTrackInfo]] = None\n    lastTrackStartIndex: Optional[int] = 0\n    lastTrackEndIndex: Optional[int] = -1\n\n\nasync def get_client_context(vals: dict[str, str | None]) -> ClientContext:\n    scopes = [\n        \"user-read-private\",\n        \"playlist-read-collaborative\",\n        \"playlist-modify-private\",\n        \"playlist-read-private\",\n        \"playlist-modify-public\",\n        \"streaming\",\n        \"user-library-read\",\n        \"user-top-read\",\n        \"user-read-playback-state\",\n        \"user-modify-playback-state\",\n        \"user-read-recently-played\",\n        \"user-read-currently-playing\",\n        \"user-library-modify\",\n        \"ugc-image-upload\",\n    ]\n\n    scopes_str = \" \".join(scopes)\n\n    auth_manager = spotipy.SpotifyOAuth(\n        client_id=vals.get(\"SPOTIFY_APP_CLI\", None),\n        client_secret=vals.get(\"SPOTIFY_APP_CLISEC\", None),\n        redirect_uri=f\"http://localhost:{vals.get('SPOTIFY_APP_PORT', 80)}/callback\",\n        scope=scopes_str,\n    )\n\n    spotify = spotipy.Spotify(auth_manager=auth_manager)\n    devices = spotify.devices()\n    device_id: str = \"\"\n    if devices:\n        device_list = devices.get(\"devices\", [])\n        if device_list:\n            device_id = device_list[0].get(\"id\", \"\")\n    user: dict[str, Any] = spotify.current_user()  # type: ignore\n    result = ClientContext(deviceId=device_id, service=AsyncSpotipy(spotify), userId=user.get(\"id\", None))\n    return result\n\n\nasync def play_album(album_uri: str, context: ClientContext):\n    await context.service.start_playback(context_uri=album_uri, device_id=context.deviceId)\n\n\nasync def play_tracks_with_query(query: str, quantity: int, context: ClientContext):\n    # To do: paginate until we get to the requested number of items\n    results = await context.service.search(q=query, type='track', limit=quantity, offset=0)\n    item_uris = [t[\"uri\"] for t in results['tracks']['items']]\n    await context.service.start_playback(device_id=context.deviceId, uris=item_uris)\n\n\nasync def play_albums_with_query(query: str, quantity: int, context: ClientContext):\n    results = await context.service.search(q=query, type='album', limit=quantity, offset=0)\n    item_uris = [t[\"uri\"] for t in results['albums']['items']]\n    await context.service.start_playback(device_id=context.deviceId, uris=item_uris)\n\n\nasync def play_artist_with_query(query: str, context: ClientContext):\n    results = await context.service.search(q=query, type='artist')\n    items = results['artists']['items']\n    if len(items) > 0:\n        artist = items[0]\n        await context.service.start_playback(context_uri=artist[\"uri\"], device_id=context.deviceId)\n\n\ndef get_tracks_from_result_list(resultItems: list[Any]) -> list[SimplifiedTrackInfo]:\n    tracks = [\n        SimplifiedTrackInfo(\n            name=track[\"name\"],\n            artistNames=[a[\"name\"] for a in track[\"artists\"]],\n            artistUris=[a[\"uri\"] for a in track[\"artists\"]],\n            albumName=track[\"album\"][\"name\"],\n            uri=track[\"uri\"],\n        )\n        for track in resultItems\n    ]\n\n    return tracks\n\n\nasync def get_tracks_from_search(query: str, context: ClientContext) -> list[SimplifiedTrackInfo]:\n    results = await context.service.search(q=query, type='track', limit=50, offset=0)\n    tracks: list[SimplifiedTrackInfo] = []\n    while results:\n        tracks.extend(get_tracks_from_result_list(resultItems=results['tracks']['items']))\n\n        if results['next']:\n            results = await context.service.next(results)\n        else:\n            results = None\n\n    return tracks\n\n\nasync def get_tracks_with_genres(\n    tracks: list[SimplifiedTrackInfo], context: ClientContext\n) -> list[SimplifiedTrackInfo]:\n    unique_artist_ids: list[str] = list(set([a for a in track.artistUris]))  # type: ignore\n    genre_lookup: dict[str, list[str]] = {}\n    for artist_id in unique_artist_ids:\n        artist = await context.service.artist(artist_id)\n        genre_lookup[artist_id] = [g.casefold() for g in artist[\"genres\"]]\n    for track in tracks:\n        track_genres:set[str] = set()\n        for artist_id in track.artistUris:\n            track_genres.update(set(genre_lookup[artist_id]))\n        track.genres = list(track_genres)\n\n    return tracks\n\n\ndef print_tracks(tracks: list[SimplifiedTrackInfo]):\n    for track in tracks:\n        print(f\" {track.name}\")\n        print(f\"   Artists: {', '.join(track.artistNames)}\")\n        print(f\"   Album: {track.albumName}\")\n\n\ndef update_track_list_and_print(tracks: list[SimplifiedTrackInfo], context: ClientContext):\n    print_tracks(tracks)\n    context.currentTrackList = tracks\n\n\nasync def get_current_users_playlists(context: ClientContext) -> list[SimplifiedPlaylistInfo]:\n    results = await context.service.current_user_playlists(limit=50)\n    playlists: list[SimplifiedPlaylistInfo] = []\n    while results:\n        playlists.extend(\n            [SimplifiedPlaylistInfo(name=curr_list[\"name\"], id=curr_list[\"id\"]) for curr_list in results['items']]\n        )\n\n        if results['next']:\n            results = await context.service.next(results)\n        else:\n            results = None\n\n    return playlists\n\n\nasync def print_status(context: ClientContext):\n    state = await context.service.current_playback()\n    if not state:\n        print(\"Nothing playing according to Spotify\")\n    await list_available_devices(context)\n\n\nasync def list_available_devices(context: ClientContext):\n    devices = await context.service.devices()\n    for device in devices[\"devices\"]:\n        if device[\"is_active\"]:\n            print(F\"Active Device {device['name']} of type {device['type']}\")\n        else:\n            print(f\"Device {device['name']} of type {device['type']} is available\")\n\n\nasync def handle_call(action: PlayerAction, context: ClientContext):\n    match action[\"actionName\"]:\n        case \"play\":\n            start_index = action[\"parameters\"].get(\"trackNumber\", None)\n            end_index = 0\n\n            if start_index is None:\n                track_range = action[\"parameters\"].get(\"trackRange\", None)\n                if track_range:\n                    start_index = track_range[0]\n                    end_index = track_range[1]\n\n            if start_index is not None:\n                if not end_index:\n                    end_index = start_index + 1\n                if context.currentTrackList is None:\n                    queue = await context.service.queue()\n                    if queue[\"queue\"]:\n                        tracks = get_tracks_from_result_list(resultItems=queue[\"queue\"])\n                        context.currentTrackList = tracks\n\n                if context.currentTrackList:\n                    item_uris = [a.uri for a in context.currentTrackList]\n                    await context.service.start_playback(\n                        device_id=context.deviceId, uris=item_uris, offset={\"position\": start_index}\n                    )\n            else:\n                query = action[\"parameters\"].get(\"query\", None)\n                album = action[\"parameters\"].get(\"album\", None)\n                track = action[\"parameters\"].get(\"trackName\", None)\n                artist = action[\"parameters\"].get(\"artist\", None)\n                quantity = action[\"parameters\"].get(\"quantity\", 1)\n                if quantity < 9:\n                    quantity = 1\n\n                if query:\n                    actionType = action[\"parameters\"].get(\"itemType\", \"album\")\n                    if actionType == \"track\":\n                        await play_tracks_with_query(query, quantity, context)\n                    else:\n                        await play_albums_with_query(query, quantity, context)\n                elif track is not None:\n                    query = 'track:' + track\n                    await play_tracks_with_query(query, quantity, context)\n                elif album is not None:\n                    query = 'album:' + album\n                    await play_albums_with_query(query, quantity, context)\n                elif artist is not None:\n                    query = 'artist:' + artist\n                    await play_artist_with_query(query, context)\n                else:\n                    # Resume playback on default device\n                    await context.service.start_playback(device_id=context.deviceId)\n        case \"status\":\n            await print_status(context)\n        case \"getQueue\":\n            queue = await context.service.queue()\n            print(\"Current Queue: \")\n            for track in queue[\"queue\"]:\n                print(f\" {track['name']}\")\n                print(f\"   Artists: {', '.join([a['name'] for a in track['artists']])}\")\n                print(f\"   Album: {track['album']['name']}\")\n            await print_status(context)\n        case \"pause\":\n            await context.service.pause_playback(device_id=context.deviceId)\n            await print_status(context)\n        case \"next\":\n            await context.service.next_track(device_id=context.deviceId)\n            await print_status(context)\n        case \"previous\":\n            await context.service.previous_track(device_id=context.deviceId)\n            await print_status(context)\n        case \"shuffle\":\n            await context.service.shuffle(device_id=context.deviceId, state=action[\"parameters\"][\"on\"])\n            await print_status(context)\n        case \"resume\":\n            await context.service.start_playback(device_id=context.deviceId)\n            await print_status(context)\n        case \"listDevices\":\n            await list_available_devices(context)\n        case \"selectDevice\":\n            deviceKeyword = action[\"parameters\"][\"keyword\"].lower()\n            devices = await context.service.devices()\n            devices = devices[\"devices\"]\n            target_device = next(\n                (d for d in devices if d[\"name\"].lower() == deviceKeyword or d[\"type\"].lower() == deviceKeyword), None\n            )\n            if target_device:\n                await context.service.transfer_playback(device_id=target_device)\n                print(f\"Selected device {target_device}\")\n        case \"setVolume\":\n            new_volume = action[\"parameters\"].get(\"newVolumeLevel\", None)\n            new_volume = max(0, min(new_volume, 100))\n            print(f\"Setting volume to {new_volume} ...\")\n            await context.service.volume(device_id=context.deviceId, volume_percent=new_volume)\n        case \"changeVolume\":\n            playback_state = await context.service.current_playback()\n            if playback_state and playback_state[\"device\"]:\n                volume = int(playback_state[\"device\"][\"volume_percent\"])\n                volume_change = int(action[\"parameters\"].get(\"volumeChangePercentage\", 0))\n                new_volume = math.floor((1.0 + volume_change / 100) * volume)\n                new_volume = max(0, min(new_volume, 100))\n                print(f\"Setting volume to {new_volume} ...\")\n                await context.service.volume(device_id=context.deviceId, volume_percent=new_volume)\n        case \"searchTracks\":\n            query = \"track:\" + action[\"parameters\"].get(\"query\", None)\n            tracks = await get_tracks_from_search(query=query, context=context)\n            print(\"Search Results: \")\n            update_track_list_and_print(tracks, context)\n        case \"listPlaylists\":\n            playlists = await get_current_users_playlists(context)\n            for i, playlist in enumerate(playlists):\n                print(\"%4d %s\" % (i + 1, playlist.name))\n\n        case \"getPlaylist\":\n            playlists = await get_current_users_playlists(context)\n            name = action[\"parameters\"].get(\"name\", None)\n            target_playlist = next((p for p in playlists if p.name.casefold() == name.casefold()), None)\n            if target_playlist:\n                results = await context.service.playlist_items(\n                    playlist_id=target_playlist.id, additional_types=['track']\n                )\n                tracks = get_tracks_from_result_list(resultItems=results['items'])\n\n                print(\"PLaylist items: \")\n                update_track_list_and_print(tracks, context)\n        case \"getAlbum\":\n            name = action[\"parameters\"].get(\"name\", None)\n            if name:\n                results = await context.service.search(q='album:' + name, type='album', limit=50, offset=0)\n                if results:\n                    target_album_info = results[\"albums\"][\"items\"][0]\n                    if target_album_info:\n                        target_album = await context.service.album(target_album_info[\"uri\"])\n                        tracks = get_tracks_from_result_list(resultItems=target_album['tracks'])\n\n                        print(\"Album items: \")\n                        update_track_list_and_print(tracks, context)\n        case \"getFavorites\":\n            count = action[\"parameters\"].get(\"count\", 50)\n            results = await context.service.current_user_top_tracks(limit=count, offset=0)\n            if results:\n                tracks = get_tracks_from_result_list(resultItems=results['items'])\n\n                print(\"Favorite tracks: \")\n                update_track_list_and_print(tracks, context)\n        case \"filterTracks\":\n            trackCollection = context.currentTrackList\n            filter_type = action[\"parameters\"].get(\"filterType\", None)\n            filter_value = action[\"parameters\"].get(\"filterValue\", None)\n\n            if filter_type and filter_value and trackCollection:\n                matched_tracks: list[SimplifiedTrackInfo] = []\n                filter_value = filter_value.casefold()\n                match filter_type:\n                    case \"genre\":\n                        extended_collection = await get_tracks_with_genres(trackCollection, context)\n                        matched_tracks = [t for t in extended_collection if filter_value in t.genres]\n                    case \"artist\":\n                        matched_tracks = [\n                            t\n                            for t in trackCollection\n                            if any(filter_value in a for a in list(map(str.casefold, t.artistNames)))\n                        ]\n                    case \"name\":\n                        matched_tracks = [t for t in trackCollection if filter_value in t.name.casefold()]\n                if action[\"parameters\"].get(\"negate\", None):\n                    tracks = [t for t in trackCollection if t not in matched_tracks]\n                else:\n                    tracks = matched_tracks\n\n                print(\"Filtered tracks:\")\n                update_track_list_and_print(tracks, context)\n        case \"createPlaylist\":\n            name = action[\"parameters\"][\"name\"]\n            trackCollection = context.currentTrackList\n            if name and trackCollection:\n                uris = [t.uri for t in trackCollection]\n                playlist = await context.service.user_playlist_create(user=context.userId, name=name)\n                await context.service.playlist_add_items(playlist_id=playlist[\"id\"], items=uris)\n                print(f\"Playlist {name} created with tracks:\")\n                print_tracks(trackCollection)\n            else:\n                print(\"no input tracks for createPlaylist\")\n        case \"deletePlaylist\":\n            name = action[\"parameters\"].get(\"name\", None)\n            playlists = await get_current_users_playlists(context)\n            if name and playlists:\n                target_playlist = next((p for p in playlists if p.name.casefold() == name.casefold()), None)\n                if target_playlist:\n                    await context.service.current_user_unfollow_playlist(playlist_id=target_playlist.id)\n                    print(f\"Playlist {name} deleted\")\n        case \"Unknown\":\n            print(f\"Text not understood in this context: {action.get('text', None)}\")\n"
  },
  {
    "path": "python/examples/music/demo.py",
    "content": "import asyncio\nimport json\n\nimport sys\nfrom dotenv import dotenv_values\nimport schema as music\nfrom typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests\nfrom client import handle_call, get_client_context\n\nasync def main():\n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatValidator(music.PlayerActions)\n    translator = TypeChatJsonTranslator(model, validator, music.PlayerActions)\n    player_context = await get_client_context(env_vals)\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(json.dumps(result, indent=2))\n            try:\n                for action in result[\"actions\"]:\n                    await handle_call(action, player_context)\n            except Exception as error:\n                print(\"An exception occurred: \", error)\n\n            if any(item[\"actionName\"] == \"Unknown\" for item in result[\"actions\"]):\n                print(\"I did not understand the following\")\n                for item in result[\"actions\"]:\n                    if item[\"actionName\"] == \"Unknown\":\n                        print(item[\"text\"])\n\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"🎵> \", file_path, request_handler)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "python/examples/music/input.txt",
    "content": "play Taylor Swift Shake It Off\r\nget my top 20 favorites and make a playlist named animalTracks of the tracks that have animals in their names\r\nget my favorite 100 tracks from the last two months and show only the ones by Bach\r\nmake it loud\r\nget my favorite 80 tracks from the last 8 months and create one playlist named class8 containing the classical tracks and another playlist containing the blues tracks\r\ntoggle shuffle on and skip to the next track\r\ngo back to the last song\r\nplay my playlist class8\r\nplay the fourth one\r\nshow me my queue\r\n"
  },
  {
    "path": "python/examples/music/schema.py",
    "content": "from typing_extensions import Literal, Required, NotRequired, TypedDict, Annotated, Doc\n\n\nclass unknownActionParameters(TypedDict):\n    text: Annotated[str, \"text typed by the user that the system did not understand\"]\n\n\nclass UnknownAction(TypedDict):\n    \"\"\"\n    Use this action for requests that weren't understood\n    \"\"\"\n\n    actionName: Literal[\"Unknown\"]\n    text: unknownActionParameters\n\n\nclass EmptyParameters(TypedDict):\n    pass\n\n\nclass PlayParameters(TypedDict, total=False):\n    artist: Annotated[str, Doc(\"artist (performer, composer) to search for to play\")]\n    album: Annotated[str, Doc(\"album to search for to play\")]\n    trackName: Annotated[str, Doc(\"track to search for to play\")]\n    query: Annotated[str, Doc(\"other description to search for to play\")]\n    itemType: Annotated[Literal[\"track\", \"album\"], Doc(\"this property is only used when the user specifies the item type\")]\n    quantity: Required[Annotated[\n        int,\n        Doc(\"number of items to play, examples: three, a/an (=1), a few (=3), a couple of (=2), some (=5). Use -1 for all, 0 if unspecified.\"),\n    ]]\n    trackNumber: Annotated[int, Doc(\"play the track at this index in the current track list\")]\n    trackRange: Annotated[list[int], Doc(\"play this range of tracks example 1-3\")]\n\n\nclass PlayAction(TypedDict):\n    \"\"\"\n    play a track, album, or artist; this action is chosen over search if both could apply\n    with no parameters, play means resume playback\n    \"\"\"\n\n    actionName: Literal[\"play\"]\n    parameters: PlayParameters\n\n\nclass StatusAction(TypedDict):\n    \"\"\"\n    show now playing including track information, and playback status including playback device\n    \"\"\"\n\n    actionName: Literal[\"status\"]\n    parameters: EmptyParameters\n\n\nclass PauseAction(TypedDict):\n    \"\"\"\n    pause playback\n    \"\"\"\n\n    actionName: Literal[\"pause\"]\n    parameters: EmptyParameters\n\n\nclass ResumeAction(TypedDict):\n    \"\"\"\n    resume playback\n    \"\"\"\n\n    actionName: Literal[\"resume\"]\n    parameters: EmptyParameters\n\n\nclass NextAction(TypedDict):\n    \"\"\"\n    next track\n    \"\"\"\n\n    actionName: Literal[\"next\"]\n    parameters: EmptyParameters\n\n\nclass PreviousAction(TypedDict):\n    \"\"\"\n    previous track\n    \"\"\"\n\n    actionName: Literal[\"previous\"]\n    parameters: EmptyParameters\n\n\nclass ShuffleActionParameters(TypedDict):\n    on: bool\n\n\nclass ShuffleAction(TypedDict):\n    \"\"\"\n    turn shuffle on or off\n    \"\"\"\n\n    actionName: Literal[\"shuffle\"]\n    parameters: ShuffleActionParameters\n\n\nclass ListDevicesAction(TypedDict):\n    \"\"\"\n    list available playback devices\n    \"\"\"\n\n    actionName: Literal[\"listDevices\"]\n    parameters: EmptyParameters\n\n\nclass SelectDeviceActionParameters(TypedDict):\n    keyword: Annotated[str, Doc(\"keyword to match against device name\")]\n\n\nclass SelectDeviceAction(TypedDict):\n    \"\"\"\n    select playback device by keyword\n    \"\"\"\n\n    actionName: Literal[\"selectDevice\"]\n    parameters: SelectDeviceActionParameters\n\n\nclass SelectVolumeActionParameters(TypedDict):\n    newVolumeLevel: Annotated[int, Doc(\"new volume level\")]\n\n\nclass SetVolumeAction(TypedDict):\n    \"\"\"\n    set volume\n    \"\"\"\n\n    actionName: Literal[\"setVolume\"]\n    parameters: SelectVolumeActionParameters\n\n\nclass ChangeVolumeActionParameters(TypedDict):\n    volumeChangePercentage: Annotated[int, \"volume change percentage\"]\n\n\nclass ChangeVolumeAction(TypedDict):\n    \"\"\"\n    change volume plus or minus a specified percentage\n    \"\"\"\n\n    actionName: Literal[\"changeVolume\"]\n    parameters: ChangeVolumeActionParameters\n\n\nclass SearchTracksActionParameters(TypedDict):\n    query: Annotated[\n        str,\n        Doc(\n        \"\"\" \n        the part of the request specifying the the search keywords\n        examples: song name, album name, artist name\n        \"\"\"),\n    ]\n\n\nclass SearchTracksAction(TypedDict):\n    \"\"\"\n    this action is only used when the user asks for a search as in 'search', 'find', 'look for'\n    query is a Spotify search expression such as 'Rock Lobster' or 'te kanawa queen of night'\n    set the current track list to the result of the search\n    \"\"\"\n\n    actionName: Literal[\"searchTracks\"]\n    parameters: SearchTracksActionParameters\n\n\nclass ListPlaylistsAction(TypedDict):\n    \"\"\"\n    list all playlists\n    \"\"\"\n\n    actionName: Literal[\"listPlaylists\"]\n    parameters: EmptyParameters\n\n\nclass GetPlaylistActionParameters(TypedDict):\n    name: Annotated[str, \"name of playlist to get\"]\n\n\nclass GetPlaylistAction(TypedDict):\n    \"\"\"\n    get playlist by name\n    \"\"\"\n\n    actionName: Literal[\"getPlaylist\"]\n    parameters: GetPlaylistActionParameters\n\n\nclass GetAlbumActionParameters(TypedDict):\n    name: Annotated[str, \"name of album to get\"]\n\n\nclass GetAlbumAction(TypedDict):\n    \"\"\"\n    get album by name; if name is \"\", use the currently playing track\n    set the current track list the tracks in the album\n    \"\"\"\n\n    actionName: Literal[\"getAlbum\"]\n    parameters: GetPlaylistActionParameters\n\n\nclass GetFavoritesActionParameters(TypedDict):\n    count: NotRequired[Annotated[int, \"number of favorites to get\"]]\n\n\nclass GetFavoritesAction(TypedDict):\n    \"\"\"\n    Set the current track list to the user's favorite tracks\n    \"\"\"\n\n    actionName: Literal[\"getFavorites\"]\n    parameters: GetFavoritesActionParameters\n\n\nclass FilterTracksActionParameters(TypedDict):\n    filterType: Annotated[\n        Literal[\"genre\", \"artist\", \"name\"],\n        Doc(\"filter type is one of 'genre', 'artist', 'name'; name does a fuzzy match on the track name\"),\n    ]\n    filterValue: Annotated[str, Doc(\"filter value is the value to match against\")]\n    negate: NotRequired[Annotated[bool, Doc(\"if negate is true, keep the tracks that do not match the filter\")]]\n\n\nclass FilterTracksAction(TypedDict):\n    \"\"\"\n    apply a filter to match tracks in the current track list\n    set the current track list to the tracks that match the filter\n    \"\"\"\n\n    actionName: Literal[\"filterTracks\"]\n    parameters: FilterTracksActionParameters\n\n\nclass CreatePlaylistActionParameters(TypedDict):\n    name: Annotated[str, \"name of playlist to create\"]\n\n\nclass CreatePlaylistAction(TypedDict):\n    \"\"\"\n    create a new playlist from the current track list\n    \"\"\"\n\n    actionName: Literal[\"createPlaylist\"]\n    parameters: CreatePlaylistActionParameters\n\n\nclass DeletePlaylistActionParameters(TypedDict):\n    name: Annotated[str, Doc(\"name of playlist to delete\")]\n\n\nclass DeletePlaylistAction(TypedDict):\n    \"\"\"\n    delete a playlist\n    \"\"\"\n\n    actionName: Literal[\"deletePlaylist\"]\n    parameters: DeletePlaylistActionParameters\n\n\nclass GetQueueAction(TypedDict):\n    \"\"\"\n    set the current track list to the queue of upcoming tracks\n    \"\"\"\n\n    actionName: Literal[\"getQueue\"]\n    parameters: EmptyParameters\n\n\nPlayerAction = (\n    PlayAction\n    | StatusAction\n    | PauseAction\n    | ResumeAction\n    | NextAction\n    | PreviousAction\n    | ShuffleAction\n    | ListDevicesAction\n    | SelectDeviceAction\n    | SetVolumeAction\n    | ChangeVolumeAction\n    | SearchTracksAction\n    | ListPlaylistsAction\n    | GetPlaylistAction\n    | GetAlbumAction\n    | GetFavoritesAction\n    | FilterTracksAction\n    | CreatePlaylistAction\n    | DeletePlaylistAction\n    | GetQueueAction\n    | UnknownAction\n)\n\n\nclass PlayerActions(TypedDict):\n    actions: list[PlayerAction]\n"
  },
  {
    "path": "python/examples/music/spotipyWrapper.py",
    "content": "from typing_extensions import Any\nfrom dataclasses import dataclass, field\nimport spotipy # type: ignore\n\n# The spotipy library does not provide type hints or async methods. This file has some wrappers and stubs \n# to give just-enough typing for the demo\n\n# This class holds the Track info needed for our use\n@dataclass\nclass SimplifiedTrackInfo:\n    name: str\n    uri: str\n    artistNames: list[str]\n    artistUris: list[str]\n    albumName: str\n    genres: list[str] = field(default_factory=list)\n\n# This class holds the Playlist info needed for our use\n@dataclass\nclass SimplifiedPlaylistInfo:\n    name: str\n    id: str\n\n# This wrapper class allows the rest of the code to use type hints and async pattern\nclass AsyncSpotipy:\n    _service: spotipy.Spotify\n\n    def __init__(self, service: spotipy.Spotify):\n        super().__init__()\n        self._service = service\n\n    async def devices(self) -> dict[str, Any]:\n        return self._service.devices()  # type: ignore\n\n    async def search(\n        self, q: str, limit: int = 10, offset: int = 0, type: str = \"track\", market: str | None = None\n    ) -> dict[str, Any]:\n        return self._service.search(q=q, limit=limit, offset=offset, type=type, market=market)  # type: ignore\n\n    async def next(self, result: dict[str, Any]) -> dict[str, Any]:\n        return self._service.next(result=result)  # type: ignore\n\n    async def artist(self, artist_id: str) -> dict[str, Any]:\n        return self._service.artist(artist_id=artist_id)  # type: ignore\n\n    async def album(self, album_id: str, market: str | None = None) -> dict[str, Any]:\n        return self._service.album(album_id=album_id, market=market)  # type: ignore\n\n    async def queue(self) -> dict[str, Any]:\n        return self._service.queue()  # type: ignore\n\n    async def current_playback(self, market: str | None = None, additional_types: str | None = None) -> dict[str, Any]:\n        return self._service.current_playback(market=market, additional_types=additional_types)  # type: ignore\n\n    async def start_playback(\n        self,\n        device_id: str | None = None,\n        context_uri: str | None = None,\n        uris: list[str] | None = None,\n        offset: dict[str, int] | None = None,\n        position_ms: int | None = None,\n    ) -> None:\n        return self._service.start_playback(device_id=device_id, context_uri=context_uri, uris=uris, offset=offset, position_ms=position_ms)  # type: ignore\n\n    async def pause_playback(self, device_id: str | None = None) -> None:\n        return self._service.pause_playback(device_id=device_id)  # type: ignore\n\n    async def next_track(self, device_id: str | None = None) -> None:\n        return self._service.next_track(device_id=device_id)  # type: ignore\n\n    async def previous_track(self, device_id: str | None = None) -> None:\n        return self._service.previous_track(device_id=device_id)  # type: ignore\n\n    async def volume(self, volume_percent: int, device_id: str | None = None) -> None:\n        return self._service.volume(volume_percent=volume_percent, device_id=device_id)  # type: ignore\n\n    async def shuffle(self, state: bool, device_id: str | None = None) -> None:\n        return self._service.shuffle(state=state, device_id=device_id)  # type: ignore\n\n    async def transfer_playback(self, device_id: str, force_play: bool = True) -> None:\n        return self._service.transfer_playback(device_id=device_id, force_play=force_play)  # type: ignore\n\n    async def current_user_top_tracks(\n        self, limit: int = 20, offset: int = 0, time_range: str = \"medium_term\"\n    ) -> dict[str, Any]:\n        return self._service.current_user_top_tracks(limit=limit, offset=offset, time_range=time_range)  # type: ignore\n\n    async def current_user_playlists(self, limit: int = 50, offset: int = 0) -> dict[str, Any]:\n        return self._service.current_user_playlists(limit=limit, offset=offset)  # type: ignore\n\n    async def user_playlist_create(\n        self, user: str, name: str, public: bool = True, collaborative: bool = False, description: str = \"\"\n    ) -> dict[str, Any]:\n        return self._service.user_playlist_create(user=user, name=name, public=public, collaborative=collaborative, description=description)  # type: ignore\n\n    async def playlist_items(\n        self,\n        playlist_id: str,\n        fields: str | None = None,\n        limit: int = 100,\n        offset: int = 0,\n        market: str | None = None,\n        additional_types: list[str] | None = None,\n    ) -> dict[str, Any]:\n        return self._service.playlist_items(playlist_id=playlist_id, fields=fields, limit=limit, offset=offset, market=market, additional_types=additional_types)  # type: ignore\n\n    async def playlist_add_items(self, playlist_id: str, items: list[str], position: int | None = None) -> None:\n        return self._service.playlist_add_items(playlist_id=playlist_id, items=items, position=position)  # type: ignore\n\n    async def current_user_unfollow_playlist(self, playlist_id: str) -> None:\n        return self._service.current_user_unfollow_playlist(playlist_id=playlist_id)  # type: ignore\n\n"
  },
  {
    "path": "python/examples/restaurant/README.md",
    "content": "# Restaurant\n\nThe Restaurant example shows how to capture user intent as a set of \"nouns\", but with more complex linguistic input.\nThis example can act as a \"stress test\" for language models, illustrating the line between simpler and more advanced language models in handling compound sentences, distractions, and corrections.\nThis example also shows how we can create a \"user intent summary\" to display to a user.\nIt uses a natural language experience for placing an order with the [`Order`](./schema.py) type.\n\n# Try Restaurant\n\nTo run the Restaurant example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`input.txt`](./input.txt).\n\nFor example, given the following order:\n\n**Input**:\n\n```\n🍕> I want three pizzas, one with mushrooms and the other two with sausage. Make one sausage a small. And give me a whole Greek and a Pale Ale. And give me a Mack and Jacks.\n```\n\n**Output**:\n\n*This is GPT-4-0613 output; GPT-3.5-turbo and most other models miss this one.*\n\n```\n1 large pizza with mushrooms\n1 large pizza with sausage\n1 small pizza with sausage\n1 whole Greek salad\n1 Pale Ale\n1 Mack and Jacks\n```\n\n> **Note**\n>\n> Across different models, you may see that model responses may not correspond to the user intent.\n> In the above example, some models may not be able to capture the fact that the order is still only for 3 pizzas,\n> and that \"make one sausage a small\" is not a request for a new pizza.\n> \n> ```diff\n>   1 large pizza with mushrooms\n> - 1 large pizza with sausage\n> + 2 large pizza with sausage\n>   1 small pizza with sausage\n>   1 whole Greek salad\n>   1 Pale Ale\n>   1 Mack and Jacks\n> ```\n>\n> The output here from GPT 3.5-turbo incorrectly shows 1 mushroom pizza and 3 sausage pizzas.\n\nBecause all language models are probabilistic and therefore will sometimes output incorrect inferences, the TypeChat pattern includes asking the user for confirmation (or giving the user an easy way to undo actions).  It is important to ask for confirmation without use of the language model so that incorrect inference is guaranteed not to be part of the intent summary generated.\n\nIn this example, the function `printOrder` in the file `main.ts` summarizes the food order (as seen in the above output) without use of a language model.  The `printOrder` function can work with a strongly typed `Order object` because the TypeChat validation process has checked that the emitted JSON corresponds to the `Order` type:\n\n```typescript\nfunction printOrder(order: Order) {\n```\n\nHaving a validated, typed data structure simplifies the task of generating a succinct summary suitable for user confirmation."
  },
  {
    "path": "python/examples/restaurant/demo.py",
    "content": "import asyncio\nimport json\nimport sys\nfrom dotenv import dotenv_values\nimport schema as restaurant\nfrom typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests\n\nasync def main():\n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatValidator(restaurant.Order)\n    translator = TypeChatJsonTranslator(model, validator, restaurant.Order)\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(json.dumps(result, indent=2))\n            if any(item[\"itemType\"] == \"Unknown\" for item in result[\"items\"]):\n                print(\"I did not understand the following\")\n                for item in result[\"items\"]:\n                    if item[\"itemType\"] == \"Unknown\":\n                        print(item[\"text\"])\n\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"🍕> \", file_path, request_handler)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "python/examples/restaurant/input.txt",
    "content": "I'd like two large, one with pepperoni and the other with extra sauce.  The pepperoni gets basil and the extra sauce gets Canadian bacon.  And add a whole salad. Make the Canadian bacon a medium. Make the salad a Greek with no red onions.  And give me two Mack and Jacks and a Sierra Nevada.  Oh, and add another salad with no red onions.\nI'd like two large with olives and mushrooms.  And the first one gets extra sauce.  The second one gets basil.  Both get arugula.  And add a Pale Ale. Give me a two Greeks with no red onions, a half and a whole.  And a large with sausage and mushrooms.  Plus three Pale Ales and a Mack and Jacks.\nI'll take two large with pepperoni.  Put olives on one of them. Make the olive a small.  And give me whole Greek plus a Pale Ale and an M&J.\nI want three pizzas, one with mushrooms and the other two with sausage.  Make one sausage a small.  And give me a whole Greek and a Pale Ale.  And give me a Mack and Jacks.\nI would like to order one with basil and one with extra sauce.  Throw in a salad and an ale.\nI would love to have a pepperoni with extra sauce, basil and arugula. Lovely weather we're having. Throw in some pineapple.  And give me a whole Greek and a Pale Ale.  Boy, those Mariners are doggin it. And how about a Mack and Jacks.\nI'll have two pepperoni, the first with extra sauce and the second with basil.  Add pineapple to the first and add olives to the second.\nI sure am hungry for a pizza with pepperoni and a salad with no croutons.  And I'm thirsty for 3 Pale Ales\ngive me three regular salads and two Greeks and make the regular ones with no red onions\nI'll take four large pepperoni pizzas.  Put extra sauce on two of them.  plus an M&J and a Pale Ale\nI'll take a yeti, a pale ale and a large with olives and take the extra cheese off the yeti and add a Greek\nI'll take a medium Pig with no arugula\nI'll take a small Pig with no arugula and a Greek with croutons and no red onions\n\n"
  },
  {
    "path": "python/examples/restaurant/schema.py",
    "content": "from typing_extensions import Literal, Required, NotRequired, TypedDict, Annotated, Doc\n\n\nclass UnknownText(TypedDict):\n    \"\"\"\n    Use this type for order items that match nothing else\n    \"\"\"\n\n    itemType: Literal[\"Unknown\"]\n    text: Annotated[str, \"The text that wasn't understood\"]\n\n\nclass Pizza(TypedDict, total=False):\n    itemType: Required[Literal[\"Pizza\"]]\n    size: Annotated[Literal[\"small\", \"medium\", \"large\", \"extra large\"], \"default: large\"]\n    addedToppings: Annotated[list[str], Doc(\"toppings requested (examples: pepperoni, arugula)\")]\n    removedToppings: Annotated[list[str], Doc(\"toppings requested to be removed (examples: fresh garlic, anchovies)\")]\n    quantity: Annotated[int, \"default: 1\"]\n    name: Annotated[\n        Literal[\"Hawaiian\", \"Yeti\", \"Pig In a Forest\", \"Cherry Bomb\"],\n        Doc(\"used if the requester references a pizza by name\"),\n    ]\n\n\nclass Beer(TypedDict):\n    itemType: Literal[\"Beer\"]\n    kind: Annotated[str, Doc(\"examples: Mack and Jacks, Sierra Nevada Pale Ale, Miller Lite\")]\n    quantity: NotRequired[Annotated[int, \"default: 1\"]]\n\n\nSaladSize = Literal[\"half\", \"whole\"]\n\nSaladStyle = Literal[\"Garden\", \"Greek\"]\n\n\nclass Salad(TypedDict, total=False):\n    itemType: Required[Literal[\"Salad\"]]\n    portion: Annotated[str, \"default: half\"]\n    style: Annotated[str, \"default: Garden\"]\n    addedIngredients: Annotated[list[str], Doc(\"ingredients requested (examples: parmesan, croutons)\")]\n    removedIngredients: Annotated[list[str], Doc(\"ingredients requested to be removed (example: red onions)\")]\n    quantity: Annotated[int, \"default: 1\"]\n\n\nOrderItem = Pizza | Beer | Salad\n\n\nclass Order(TypedDict):\n    items: list[OrderItem | UnknownText]\n"
  },
  {
    "path": "python/examples/sentiment/README.md",
    "content": "# Sentiment\n\nThe Sentiment example shows how to match user intent to a set of nouns, in this case categorizing user sentiment of the input as negative, neutral, or positive with the [`SentimentResponse`](./schema.py) type.\n\n# Try Sentiment\nTo run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\nExample prompts can be found in [`input.txt`](./input.txt).\n\nFor example, given the following input statement:\n\n**Input**:\n```\n😀> TypeChat is awesome!\n```\n\n**Output**:\n```\nThe sentiment is positive\n```"
  },
  {
    "path": "python/examples/sentiment/demo.py",
    "content": "import asyncio\nimport sys\n\nfrom dotenv import dotenv_values\nfrom typechat import (Failure, TypeChatJsonTranslator, TypeChatValidator,\n                      create_language_model, process_requests)\n\nimport schema as sentiment\n\n\nasync def main():    \n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatValidator(sentiment.Sentiment)\n    translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(f\"The sentiment is {result.sentiment}\")\n\n    file_path = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"😀> \", file_path, request_handler)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "python/examples/sentiment/input.txt",
    "content": "hello, world\nTypeChat is awesome!\nI'm having a good day\nit's very rainy outside"
  },
  {
    "path": "python/examples/sentiment/schema.py",
    "content": "from dataclasses import dataclass\nfrom typing_extensions import Literal, Annotated, Doc\n\n@dataclass\nclass Sentiment:\n    \"\"\"\n    The following is a schema definition for determining the sentiment of a some user input.\n    \"\"\"\n\n    sentiment: Annotated[Literal[\"negative\", \"neutral\", \"positive\"], Doc(\"The sentiment for the text\")]\n"
  },
  {
    "path": "python/notebooks/calendar.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install --upgrade setuptools\\n\",\n    \"%pip install --upgrade gradio\\n\",\n    \"%pip install ipywidgets\\n\",\n    \"%pip install pandas\\n\",\n    \"%pip install tabulate\\n\",\n    \"%pip install python-dotenv\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install -e ../\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import setuptools\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"module_path = os.path.abspath(os.path.join('..'))\\n\",\n    \"if module_path not in sys.path:\\n\",\n    \"    sys.path.append(module_path)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from dotenv import dotenv_values\\n\",\n    \"from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\\n\",\n    \"from examples.calendar import schema as calendar\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"model = create_language_model(dotenv_values())\\n\",\n    \"validator = TypeChatValidator(calendar.CalendarActions)\\n\",\n    \"translator = TypeChatJsonTranslator(model, validator, calendar.CalendarActions)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas\\n\",\n    \"\\n\",\n    \"async def get_translation(message, history):\\n\",\n    \"    result = await translator.translate(message)\\n\",\n    \"    if isinstance(result, Failure):\\n\",\n    \"        return f\\\"Translation Failed ❌ \\\\n Context: {result.message}\\\"\\n\",\n    \"    else:\\n\",\n    \"        result = result.value\\n\",\n    \"        df = pandas.DataFrame.from_dict(result[\\\"actions\\\"])\\n\",\n    \"        return f\\\"Translation Succeeded! ✅\\\\n Table View \\\\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\\\n ```  \\\\n\\\"\\n\",\n    \"\\n\",\n    \"def get_examples():\\n\",\n    \"    example_prompts = []\\n\",\n    \"    with open('../examples/calendar/input.txt') as prompts_file:\\n\",\n    \"        for line in prompts_file:\\n\",\n    \"            example_prompts.append(line)\\n\",\n    \"    return example_prompts\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import gradio as gr\\n\",\n    \"\\n\",\n    \"gr.ChatInterface(get_translation, title=\\\"📅 Calendar\\\", examples=get_examples()).launch()\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.1\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "python/notebooks/coffeeShop.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install --upgrade setuptools\\n\",\n    \"%pip install --upgrade gradio\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"import sys\\n\",\n    \"from dotenv import dotenv_values\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"module_path = os.path.abspath(os.path.join('..'))\\n\",\n    \"if module_path not in sys.path:\\n\",\n    \"    sys.path.append(module_path)\\n\",\n    \"\\n\",\n    \"from examples.coffeeShop import schema as coffeeshop\\n\",\n    \"from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"env_vals = dotenv_values()\\n\",\n    \"model = create_language_model(env_vals)\\n\",\n    \"validator = TypeChatValidator(coffeeshop.Cart)\\n\",\n    \"translator = TypeChatJsonTranslator(model, validator, coffeeshop.Cart)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas\\n\",\n    \"async def get_translation(message, history):\\n\",\n    \"    result = await translator.translate(message)\\n\",\n    \"    if isinstance(result, Failure):\\n\",\n    \"        return f\\\"Translation Failed ❌ \\\\n Context: {result.message}\\\"\\n\",\n    \"    else:\\n\",\n    \"        result = result.value\\n\",\n    \"        df = pandas.DataFrame.from_dict(result[\\\"items\\\"])\\n\",\n    \"        #return f\\\"Translation Succeeded! ✅\\\\n JSON View \\\\n ``` {json.dumps(result, indent=2)} \\\\n ``` \\\\n\\\"\\n\",\n    \"        return f\\\"Translation Succeeded! ✅\\\\n Coffee Shop Items \\\\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\\\n ```  \\\\n\\\"\\n\",\n    \"\\n\",\n    \"def get_examples():\\n\",\n    \"    example_prompts = []\\n\",\n    \"    with open('../examples/coffeeShop/input.txt') as prompts_file:\\n\",\n    \"        for line in prompts_file:\\n\",\n    \"            example_prompts.append(line)\\n\",\n    \"    return example_prompts\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import setuptools\\n\",\n    \"import gradio as gr\\n\",\n    \"\\n\",\n    \"gr.ChatInterface(get_translation, title=\\\"☕ Coffee\\\", examples=get_examples()).launch()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.1\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "python/notebooks/healthData.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install --upgrade setuptools\\n\",\n    \"%pip install --upgrade gradio\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import setuptools\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"module_path = os.path.abspath(os.path.join('..'))\\n\",\n    \"if module_path not in sys.path:\\n\",\n    \"    sys.path.append(module_path)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from dotenv import dotenv_values\\n\",\n    \"from typechat import Failure, TypeChatValidator, create_language_model\\n\",\n    \"from examples.healthData import schema as health\\n\",\n    \"from examples.healthData.translator import TranslatorWithHistory\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"health_instructions = \\\"\\\"\\\"\\n\",\n    \"Help me enter my health data step by step.\\n\",\n    \"Ask specific questions to gather required and optional fields I have not already providedStop asking if I don't know the answer\\n\",\n    \"Automatically fix my spelling mistakes\\n\",\n    \"My health data may be complex: always record and return ALL of it.\\n\",\n    \"Always return a response:\\n\",\n    \"- If you don't understand what I say, ask a question.\\n\",\n    \"- At least respond with an OK message.\\n\",\n    \"\\n\",\n    \"\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"env_vals = dotenv_values()\\n\",\n    \"model = create_language_model(env_vals)\\n\",\n    \"validator = TypeChatValidator(health.HealthDataResponse)\\n\",\n    \"translator = TranslatorWithHistory(model, validator, health.HealthDataResponse, additional_agent_instructions=health_instructions)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas\\n\",\n    \"\\n\",\n    \"async def get_translation(message, history):\\n\",\n    \"    result = await translator.translate(message)\\n\",\n    \"    if isinstance(result, Failure):\\n\",\n    \"        return f\\\"Translation Failed ❌ \\\\n Context: {result.message}\\\"\\n\",\n    \"    else:\\n\",\n    \"        result = result.value\\n\",\n    \"        output = f\\\"Translation Succeeded! ✅\\\\n\\\"\\n\",\n    \"        \\n\",\n    \"        data = result.get(\\\"data\\\", None)\\n\",\n    \"        if data:\\n\",\n    \"            df = pandas.DataFrame.from_dict(data)\\n\",\n    \"            output += f\\\"HealthData \\\\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\\\n ```  \\\\n\\\"\\n\",\n    \"\\n\",\n    \"        message = result.get(\\\"message\\\", None)\\n\",\n    \"        not_translated = result.get(\\\"notTranslated\\\", None)\\n\",\n    \"\\n\",\n    \"        if message:\\n\",\n    \"            output += f\\\"\\\\n📝: {message}\\\"\\n\",\n    \"            \\n\",\n    \"        if not_translated:\\n\",\n    \"            output += f\\\"\\\\n🤔: I did not understand\\\\n {not_translated}\\\" \\n\",\n    \"            \\n\",\n    \"        return output\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_examples():\\n\",\n    \"    example_prompts = []\\n\",\n    \"    with open('../examples/healthData/input.txt') as prompts_file:\\n\",\n    \"        for line in prompts_file:\\n\",\n    \"            example_prompts.append(line)\\n\",\n    \"    return example_prompts\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import gradio as gr\\n\",\n    \"\\n\",\n    \"gr.ChatInterface(get_translation, title=\\\"💉💊🤧 Health Data\\\", examples=get_examples()).launch(debug=False)\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.0\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "python/notebooks/math.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install --upgrade setuptools\\n\",\n    \"%pip install --upgrade gradio\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import setuptools\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"module_path = os.path.abspath(os.path.join('..'))\\n\",\n    \"if module_path not in sys.path:\\n\",\n    \"    sys.path.append(module_path)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from dotenv import dotenv_values\\n\",\n    \"from typechat import Failure, create_language_model\\n\",\n    \"from examples.math.program import TypeChatProgramTranslator, TypeChatProgramValidator, evaluate_json_program\\n\",\n    \"from examples.math import schema as math\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"env_vals = dotenv_values()\\n\",\n    \"model = create_language_model(env_vals)\\n\",\n    \"validator = TypeChatProgramValidator()\\n\",\n    \"translator = TypeChatProgramTranslator(model, validator, math.MathAPI)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas\\n\",\n    \"async def handleCall(func:str, args: list[int|float]) -> int|float:\\n\",\n    \"    print(f\\\"{func}({json.dumps(args)}) \\\")\\n\",\n    \"    match func:\\n\",\n    \"        case \\\"add\\\":\\n\",\n    \"            return args[0] + args[1]\\n\",\n    \"        case \\\"sub\\\":\\n\",\n    \"            return args[0] - args[1]\\n\",\n    \"        case \\\"mul\\\":\\n\",\n    \"            return args[0] * args[1]\\n\",\n    \"        case \\\"div\\\":\\n\",\n    \"            return args[0] / args[1]\\n\",\n    \"        case \\\"neg\\\":\\n\",\n    \"            return -1 * args[0]\\n\",\n    \"        case \\\"id\\\":\\n\",\n    \"            return args[0]\\n\",\n    \"        case _:\\n\",\n    \"            raise ValueError(f'Unexpected function name {func}')\\n\",\n    \"        \\n\",\n    \"async def get_translation(message, history):\\n\",\n    \"    result = await translator.translate(message)\\n\",\n    \"    if isinstance(result, Failure):\\n\",\n    \"        return f\\\"Translation Failed ❌ \\\\n Context: {result.message}\\\"\\n\",\n    \"    else:\\n\",\n    \"        result = result.value\\n\",\n    \"        math_result = await evaluate_json_program(result, handleCall)\\n\",\n    \"        df = pandas.DataFrame.from_dict(result[\\\"@steps\\\"])\\n\",\n    \"        return f\\\"Translation Succeeded! ✅\\\\n Here is a table of operations needed to get the answer \\\\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\\\n ```  \\\\n Math Result: {math_result}\\\"\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_examples():\\n\",\n    \"    example_prompts = []\\n\",\n    \"    with open('../examples/math/input.txt') as prompts_file:\\n\",\n    \"        for line in prompts_file:\\n\",\n    \"            example_prompts.append(line)\\n\",\n    \"    return example_prompts\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import gradio as gr\\n\",\n    \"\\n\",\n    \"gr.ChatInterface(get_translation, title=\\\"🧮 Math\\\", examples=get_examples()).launch()\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.0\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "python/notebooks/music.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install --upgrade setuptools\\n\",\n    \"%pip install --upgrade gradio\\n\",\n    \"%pip install ipywidgets\\n\",\n    \"%pip install openai\\n\",\n    \"%pip install pandas\\n\",\n    \"%pip install tabulate\\n\",\n    \"%pip install python-dotenv\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install -e ../\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import setuptools\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"module_path = os.path.abspath(os.path.join('..'))\\n\",\n    \"if module_path not in sys.path:\\n\",\n    \"    sys.path.append(module_path)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from dotenv import dotenv_values\\n\",\n    \"from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\\n\",\n    \"from examples.music import schema as music\\n\",\n    \"from examples.music.client import handle_call, get_client_context\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"env_vals = dotenv_values()\\n\",\n    \"model = create_language_model(env_vals)\\n\",\n    \"validator = TypeChatValidator(music.PlayerActions)\\n\",\n    \"translator = TypeChatJsonTranslator(model, validator, music.PlayerActions)\\n\",\n    \"player_context = await get_client_context(env_vals)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas\\n\",\n    \"\\n\",\n    \"async def get_translation(message, history):\\n\",\n    \"    result = await translator.translate(message)\\n\",\n    \"    if isinstance(result, Failure):\\n\",\n    \"        return f\\\"Translation Failed ❌ \\\\n Context: {result.message}\\\"\\n\",\n    \"    else:\\n\",\n    \"        result = result.value\\n\",\n    \"        df = pandas.DataFrame.from_dict(result[\\\"actions\\\"])\\n\",\n    \"        try:\\n\",\n    \"            for action in result[\\\"actions\\\"]:\\n\",\n    \"                await handle_call(action, player_context)\\n\",\n    \"            return f\\\"Translation Succeeded! ✅\\\\n Table View \\\\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\\\n ```  \\\\n\\\"\\n\",\n    \"        except Exception as error:\\n\",\n    \"            return f\\\"An exception occurred: {error}\\\"\\n\",\n    \"        \\n\",\n    \"\\n\",\n    \"def get_examples():\\n\",\n    \"    example_prompts = []\\n\",\n    \"    with open('../examples/music/input.txt') as prompts_file:\\n\",\n    \"        for line in prompts_file:\\n\",\n    \"            example_prompts.append(line)\\n\",\n    \"    return example_prompts\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import gradio as gr\\n\",\n    \"\\n\",\n    \"gr.ChatInterface(get_translation, title=\\\"🎵 Music\\\", examples=get_examples()).launch()\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.0\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "python/notebooks/restaurant.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install --upgrade setuptools\\n\",\n    \"%pip install --upgrade gradio\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import setuptools\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"module_path = os.path.abspath(os.path.join('..'))\\n\",\n    \"if module_path not in sys.path:\\n\",\n    \"    sys.path.append(module_path)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from dotenv import dotenv_values\\n\",\n    \"from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\\n\",\n    \"from examples.restaurant import schema as restaurant\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"env_vals = dotenv_values()\\n\",\n    \"model = create_language_model(env_vals)\\n\",\n    \"validator = TypeChatValidator(restaurant.Order)\\n\",\n    \"translator = TypeChatJsonTranslator(model, validator, restaurant.Order)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas\\n\",\n    \"async def get_translation(message, history):\\n\",\n    \"    result = await translator.translate(message)\\n\",\n    \"    if isinstance(result, Failure):\\n\",\n    \"        return f\\\"Translation Failed ❌ \\\\n Context: {result.message}\\\"\\n\",\n    \"    else:\\n\",\n    \"        result = result.value\\n\",\n    \"        df = pandas.DataFrame.from_dict(result[\\\"items\\\"])\\n\",\n    \"        return f\\\"Translation Succeeded! ✅\\\\n Restaurant orders \\\\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\\\n ```  \\\\n\\\"\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_examples():\\n\",\n    \"    example_prompts = []\\n\",\n    \"    with open('../examples/restaurant/input.txt') as prompts_file:\\n\",\n    \"        for line in prompts_file:\\n\",\n    \"            example_prompts.append(line)\\n\",\n    \"    return example_prompts\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import gradio as gr\\n\",\n    \"\\n\",\n    \"gr.ChatInterface(get_translation, title=\\\"🍕 Restaurant\\\", examples=get_examples()).launch()\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.0\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "python/notebooks/sentiment.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install --upgrade setuptools\\n\",\n    \"%pip install --upgrade gradio\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"import sys\\n\",\n    \"from dotenv import dotenv_values\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"module_path = os.path.abspath(os.path.join('..'))\\n\",\n    \"if module_path not in sys.path:\\n\",\n    \"    sys.path.append(module_path)\\n\",\n    \"\\n\",\n    \"from examples.sentiment import schema as sentiment\\n\",\n    \"from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"env_vals = dotenv_values()\\n\",\n    \"model = create_language_model(env_vals)\\n\",\n    \"validator = TypeChatValidator(sentiment.Sentiment)\\n\",\n    \"translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"async def get_translation(message, history):\\n\",\n    \"    result = await translator.translate(message)\\n\",\n    \"    if isinstance(result, Failure):\\n\",\n    \"        return f\\\"Translation Failed ❌ \\\\n Context: {result.message}\\\"\\n\",\n    \"    else:\\n\",\n    \"        result = result.value\\n\",\n    \"        return f\\\"Translation Succeeded! ✅\\\\n The sentiment is {result['sentiment']}\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import setuptools\\n\",\n    \"import gradio as gr\\n\",\n    \"\\n\",\n    \"gr.ChatInterface(get_translation, title=\\\"😀 Sentiment\\\").launch()\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.0\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "python/package.json",
    "content": "{\n  \"name\": \"typechat-py\",\n  \"private\": true,\n  \"version\": \"0.0.4\",\n  \"description\": \"TypeChat is a library that makes it easy to build natural language interfaces using types.\",\n  \"scripts\": {\n    \"check\": \"pyright\"\n  },\n  \"author\": \"Microsoft\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"pyright\": \"1.1.358\"\n  }\n}\n"
  },
  {
    "path": "python/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"typechat\"\ndynamic = [\"version\"]\ndescription = 'TypeChat is a library that makes it easy to build natural language interfaces using types.'\nreadme = \"README.md\"\nrequires-python = \">=3.11\"\nlicense = \"MIT\"\nkeywords = []\nauthors = [\n  { name = \"Microsoft Corporation\" },\n]\nclassifiers = [\n  \"Development Status :: 4 - Beta\",\n  \"Programming Language :: Python\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: Implementation :: CPython\",\n  \"Programming Language :: Python :: Implementation :: PyPy\",\n]\ndependencies = [\n  \"pydantic>=2.5.2\",\n  \"pydantic_core>=2.16.3\",\n  \"httpx>=0.27.0\",\n  \"typing_extensions>=4.10.0\",\n]\n\n[project.optional-dependencies]\n# Development-time dependencies.\ndev = [\n  \"coverage[toml]>=6.5\",\n  \"pytest>=8.0.2\",\n  \"syrupy>=5.0.0\",\n]\n\n# Dependencies for examples.\nexamples = [\n  \"python-dotenv>=1.0.0\",\n  \"spotipy\",\n]\n\n[project.urls]\nDocumentation = \"https://github.com/microsoft/TypeChat#readme\"\nIssues = \"https://github.com/microsoft/TypeChat/issues\"\nSource = \"https://github.com/microsoft/TypeChat\"\n\n[tool.hatch.version]\npath = \"src/typechat/__about__.py\"\n\n[tool.hatch.envs.default]\n# While users can always look up the virtual environment\n# to select the right interpreter for their editor, often editors can\n# automatically pick up on a local `.venv` or at least hint towards using it.\n# The only catch is that this tends to only kick in at the workspace root.\ntype = \"virtual\"\npath = \"../.venv\"\n\n# Include dependencies from optional-dependencies for\n# development of the core package along with examples.\nfeatures = [\n  \"dev\",\n  \"examples\"\n]\n\n[tool.hatch.envs.default.scripts]\ntest = \"pytest {args:tests}\"\ntest-cov = \"coverage run -m pytest {args:tests}\"\ncov-report = [\n  \"- coverage combine\",\n  \"coverage report\",\n]\ncov = [\n  \"test-cov\",\n  \"cov-report\",\n]\n\n[[tool.hatch.envs.all.matrix]]\npython = [\"3.11\", \"3.12\"]\n\n[tool.hatch.envs.lint]\ndetached = true\ndependencies = [\n  \"black>=23.1.0\",\n  \"mypy>=1.0.0\",\n  \"ruff>=0.0.243\",\n]\n[tool.hatch.envs.lint.scripts]\ntyping = [\n  \"npx pyright\",\n  # mypy should not include tests, as it does not fully support\n  # PEP 695 (type aliases, type parameters, etc.)\n  # https://github.com/python/mypy/issues/1523895\n  \"mypy --install-types --non-interactive {args:src/typechat}\"\n]\nstyle = [\n  \"ruff {args:.}\",\n  \"black --check --diff {args:.}\",\n]\nfmt = [\n  \"black {args:.}\",\n  \"ruff --fix {args:.}\",\n  \"style\",\n]\nall = [\n  \"style\",\n  \"typing\",\n]\n\n[tool.mypy]\npython_version = \"3.11\"\nuntyped_calls_exclude  = [\"spotipy\"]\n\n[tool.black]\ntarget-version = [\"py311\"]\nline-length = 120\nskip-string-normalization = true\n\n[tool.ruff]\ntarget-version = \"py311\"\nline-length = 120\nselect = [\n  \"A\",\n  \"ARG\",\n  \"B\",\n  \"C\",\n  \"DTZ\",\n  \"E\",\n  \"EM\",\n  \"F\",\n  \"FBT\",\n  \"I\",\n  \"ICN\",\n  \"ISC\",\n  \"N\",\n  \"PLC\",\n  \"PLE\",\n  \"PLR\",\n  \"PLW\",\n  \"Q\",\n  \"RUF\",\n  \"S\",\n  \"T\",\n  \"TID\",\n  \"UP\",\n  \"W\",\n  \"YTT\",\n]\nignore = [\n  # # Allow non-abstract empty methods in abstract base classes\n  # \"B027\",\n  # # Allow boolean positional values in function calls, like `dict.get(... True)`\n  # \"FBT003\",\n  # # Ignore checks for possible passwords\n  # \"S105\", \"S106\", \"S107\",\n  # # Ignore complexity\n  # \"C901\", \"PLR0911\", \"PLR0912\", \"PLR0913\", \"PLR0915\",\n]\nunfixable = [\n  # # Don't touch unused imports\n  # \"F401\",\n]\n\n[tool.ruff.isort]\nknown-first-party = [\"typechat\"]\n\n[tool.ruff.flake8-tidy-imports]\nban-relative-imports = \"all\"\n\n[tool.ruff.per-file-ignores]\n# Tests can use magic values, assertions, and relative imports\n\"tests/**/*\" = [\"PLR2004\", \"S101\", \"TID252\"]\n\n[tool.coverage.run]\nsource_pkgs = [\"typechat\", \"tests\"]\nbranch = true\nparallel = true\nomit = [\n  \"src/typechat/__about__.py\",\n]\n\n[tool.coverage.paths]\ntypechat = [\"src/typechat\", \"*/typechat/src/typechat\"]\ntests = [\"tests\", \"*/typechat/tests\"]\n\n[tool.coverage.report]\nexclude_lines = [\n  \"no cov\",\n  \"if __name__ == .__main__.:\",\n  \"if TYPE_CHECKING:\",\n]\n"
  },
  {
    "path": "python/pyrightconfig.json",
    "content": "{\n    \"typeCheckingMode\": \"strict\",\n    \"reportCallInDefaultInitializer\": \"error\",\n    \"reportImplicitOverride\": \"error\",\n    \"reportImplicitStringConcatenation\": \"error\",\n    \"reportImportCycles\": \"error\",\n    \"reportMissingSuperCall\": \"error\",\n    \"reportPropertyTypeMismatch\": \"error\",\n    \"reportShadowedImports\": \"error\",\n    \"reportUninitializedInstanceVariable\": \"error\",\n    \"reportUnnecessaryTypeIgnoreComment\": \"error\",\n    \"reportUnusedCallResult\": \"none\",\n    \"pythonVersion\": \"3.11\",\n    \"include\": [\n        \"**/*\",\n    ],\n}"
  },
  {
    "path": "python/src/typechat/__about__.py",
    "content": "# SPDX-FileCopyrightText: Microsoft Corporation\n#\n# SPDX-License-Identifier: MIT\n__version__ = \"0.0.4\"\n"
  },
  {
    "path": "python/src/typechat/__init__.py",
    "content": "# SPDX-FileCopyrightText: Microsoft Corporation\n#\n# SPDX-License-Identifier: MIT\n\nfrom typechat._internal.model import PromptSection, TypeChatLanguageModel, create_language_model, create_openai_language_model, create_azure_openai_language_model\nfrom typechat._internal.result import Failure, Result, Success\nfrom typechat._internal.translator import TypeChatJsonTranslator\nfrom typechat._internal.ts_conversion import python_type_to_typescript_schema\nfrom typechat._internal.validator import TypeChatValidator\nfrom typechat._internal.interactive import process_requests\n\n__all__ = [\n    \"TypeChatLanguageModel\",\n    \"TypeChatJsonTranslator\",\n    \"TypeChatValidator\",\n    \"Success\",\n    \"Failure\",\n    \"Result\",\n    \"python_type_to_typescript_schema\",\n    \"PromptSection\",\n    \"create_language_model\",\n    \"create_openai_language_model\",\n    \"create_azure_openai_language_model\",\n    \"process_requests\",\n]\n"
  },
  {
    "path": "python/src/typechat/_internal/__init__.py",
    "content": ""
  },
  {
    "path": "python/src/typechat/_internal/interactive.py",
    "content": "from typing import Callable, Awaitable\n\nasync def process_requests(interactive_prompt: str, input_file_name: str | None, process_request: Callable[[str], Awaitable[None]]):\n    \"\"\"\n    A request processor for interactive input or input from a text file. If an input file name is specified,\n    the callback function is invoked for each line in file. Otherwise, the callback function is invoked for\n    each line of interactive input until the user types \"quit\" or \"exit\".\n    \n    Args:\n        interactive_prompt: Prompt to present to user.\n        input_file_name: Input text file name, if any.\n        process_request: Async callback function that is invoked for each interactive input or each line in text file.\n    \"\"\"\n    if input_file_name is not None:\n        with open(input_file_name, \"r\") as file:\n            lines = filter(str.rstrip, file)\n            for line in lines:\n                if line.startswith(\"# \"):\n                    continue\n                print(interactive_prompt + line)\n                await process_request(line)\n    else:\n        try:\n            # Use readline to enable input editing and history\n            import readline  # type: ignore\n        except ImportError:\n            pass\n        while True:\n            try:\n                line = input(interactive_prompt)\n            except EOFError:\n                print(\"\\n\")\n                break\n            if line.lower().strip() in (\"quit\", \"exit\"):\n                break\n            else:\n                await process_request(line)\n"
  },
  {
    "path": "python/src/typechat/_internal/model.py",
    "content": "import asyncio\nfrom types import TracebackType\nfrom typing_extensions import AsyncContextManager, Literal, Protocol, Self, TypedDict, cast, override\n\nfrom typechat._internal.result import Failure, Result, Success\n\nimport httpx\n\nclass PromptSection(TypedDict):\n    \"\"\"\n    Represents a section of an LLM prompt with an associated role. TypeChat uses the \"user\" role for\n    prompts it generates and the \"assistant\" role for previous LLM responses (which will be part of\n    the prompt in repair attempts). TypeChat currently doesn't use the \"system\" role.\n    \"\"\"\n    role: Literal[\"system\", \"user\", \"assistant\"]\n    content: str\n\nclass TypeChatLanguageModel(Protocol):\n    async def complete(self, prompt: str | list[PromptSection]) -> Result[str]:\n        \"\"\"\n        Represents a AI language model that can complete prompts.\n        \n        TypeChat uses an implementation of this protocol to communicate\n        with an AI service that can translate natural language requests to JSON\n        instances according to a provided schema.\n        The `create_language_model` function can create an instance.\n        \"\"\"\n        ...\n\n_TRANSIENT_ERROR_CODES = [\n    429,\n    500,\n    502,\n    503,\n    504,\n]\n\nclass HttpxLanguageModel(TypeChatLanguageModel, AsyncContextManager):\n    url: str\n    headers: dict[str, str]\n    default_params: dict[str, str]\n    # Specifies the maximum number of retry attempts.\n    max_retry_attempts: int = 3\n    # Specifies the delay before retrying in milliseconds.\n    retry_pause_seconds: float = 1.0\n    # Specifies how long a request should wait in seconds\n    # before timing out with a Failure.\n    timeout_seconds = 10\n    _async_client: httpx.AsyncClient\n\n    def __init__(self, url: str, headers: dict[str, str], default_params: dict[str, str]):\n        super().__init__()\n        self.url = url\n        self.headers = headers\n        self.default_params = default_params\n        self._async_client = httpx.AsyncClient()\n\n    @override\n    async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Failure:\n        headers = {\n            \"Content-Type\": \"application/json\",\n            **self.headers,\n        }\n\n        if isinstance(prompt, str):\n            prompt = [{\"role\": \"user\", \"content\": prompt}]\n\n        body = {\n            **self.default_params,\n            \"messages\": prompt,\n            \"temperature\": 0.0,\n            \"n\": 1,\n        }\n        retry_count = 0\n        while True:\n            try:\n                response = await self._async_client.post(\n                    self.url,\n                    headers=headers,\n                    json=body,\n                    timeout=self.timeout_seconds\n                )\n                if response.is_success:\n                    json_result = cast(\n                        dict[Literal[\"choices\"], list[dict[Literal[\"message\"], PromptSection]]],\n                        response.json()\n                    )\n                    return Success(json_result[\"choices\"][0][\"message\"][\"content\"] or \"\")\n\n                if response.status_code not in _TRANSIENT_ERROR_CODES or retry_count >= self.max_retry_attempts:\n                    return Failure(f\"REST API error {response.status_code}: {response.reason_phrase}\")\n            except Exception as e:\n                if retry_count >= self.max_retry_attempts:\n                    return Failure(str(e) or f\"{repr(e)} raised from within internal TypeChat language model.\")\n\n            await asyncio.sleep(self.retry_pause_seconds)\n            retry_count += 1\n\n    @override\n    async def __aenter__(self) -> Self:\n        return self\n\n    @override\n    async def __aexit__(self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None) -> bool | None:\n        await self._async_client.aclose()\n\n    def __del__(self):\n        try:\n            asyncio.get_running_loop().create_task(self._async_client.aclose())\n        except Exception:\n            pass\n\ndef create_language_model(vals: dict[str, str | None]) -> HttpxLanguageModel:\n    \"\"\"\n    Creates a language model encapsulation of an OpenAI or Azure OpenAI REST API endpoint\n    chosen by a dictionary of variables (typically just `os.environ`).\n\n    If an `OPENAI_API_KEY` environment variable exists, an OpenAI model is constructed.\n    The `OPENAI_ENDPOINT` and `OPENAI_MODEL` environment variables must also be defined or an error will be raised.\n\n    If an `AZURE_OPENAI_API_KEY` environment variable exists, an Azure OpenAI model is constructed.\n    The `AZURE_OPENAI_ENDPOINT` environment variable must also be defined or an exception will be thrown.\n\n    If none of these key variables are defined, an exception is thrown.\n    @returns An instance of `TypeChatLanguageModel`.\n\n    Args:\n        vals: A dictionary of variables. Typically just `os.environ`.\n    \"\"\"\n    \n    def required_var(name: str) -> str:\n        val = vals.get(name, None)\n        if val is None:\n            raise ValueError(f\"Missing environment variable {name}.\")\n        return val\n\n    if \"OPENAI_API_KEY\" in vals:\n        api_key = required_var(\"OPENAI_API_KEY\")\n        model = required_var(\"OPENAI_MODEL\")\n        endpoint = vals.get(\"OPENAI_ENDPOINT\", None) or \"https://api.openai.com/v1/chat/completions\"\n        org = vals.get(\"OPENAI_ORG\", None) or \"\"\n        return create_openai_language_model(api_key, model, endpoint, org)\n\n    elif \"AZURE_OPENAI_API_KEY\" in vals:\n        api_key=required_var(\"AZURE_OPENAI_API_KEY\")\n        endpoint=required_var(\"AZURE_OPENAI_ENDPOINT\")\n        return create_azure_openai_language_model(api_key, endpoint)\n    else:\n        raise ValueError(\"Missing environment variables for OPENAI_API_KEY or AZURE_OPENAI_API_KEY.\")\n\ndef create_openai_language_model(api_key: str, model: str, endpoint: str = \"https://api.openai.com/v1/chat/completions\", org: str = \"\") -> HttpxLanguageModel:\n    \"\"\"\n    Creates a language model encapsulation of an OpenAI REST API endpoint.\n\n    Args:\n        api_key: The OpenAI API key.\n        model: The OpenAI model name.\n        endpoint: The OpenAI REST API endpoint.\n        org: The OpenAI organization.\n    \"\"\"\n    headers = {\n        \"Authorization\": f\"Bearer {api_key}\",\n        \"OpenAI-Organization\": org,\n    }\n    default_params = {\n        \"model\": model,\n    }\n    return HttpxLanguageModel(url=endpoint, headers=headers, default_params=default_params)\n\ndef create_azure_openai_language_model(api_key: str, endpoint: str) -> HttpxLanguageModel:\n    \"\"\"\n    Creates a language model encapsulation of an Azure OpenAI REST API endpoint.\n\n    Args:\n        api_key: The Azure OpenAI API key.\n        endpoint: The Azure OpenAI REST API endpoint.\n    \"\"\"\n    headers = {\n        # Needed when using managed identity\n        \"Authorization\": f\"Bearer {api_key}\",\n        # Needed when using regular API key\n        \"api-key\": api_key,\n    }\n    return HttpxLanguageModel(url=endpoint, headers=headers, default_params={})\n"
  },
  {
    "path": "python/src/typechat/_internal/result.py",
    "content": "from dataclasses import dataclass\nfrom typing_extensions import Generic, TypeAlias, TypeVar\n\nT = TypeVar(\"T\", covariant=True)\n\n@dataclass\nclass Success(Generic[T]):\n    \"An object representing a successful operation with a result of type `T`.\"\n    value: T\n\n\n@dataclass\nclass Failure:\n    \"An object representing an operation that failed for the reason given in `message`.\"\n    message: str\n\n\n\"\"\"\nAn object representing a successful or failed operation of type `T`.\n\"\"\"\nResult: TypeAlias = Success[T] | Failure\n"
  },
  {
    "path": "python/src/typechat/_internal/translator.py",
    "content": "from typing_extensions import Generic, TypeVar\n\nimport pydantic_core\n\nfrom typechat._internal.model import PromptSection, TypeChatLanguageModel\nfrom typechat._internal.result import Failure, Result, Success\nfrom typechat._internal.ts_conversion import python_type_to_typescript_schema\nfrom typechat._internal.validator import TypeChatValidator\n\nT = TypeVar(\"T\", covariant=True)\n\nclass TypeChatJsonTranslator(Generic[T]):\n    \"\"\"\n    Represents an object that can translate natural language requests in JSON objects of the given type.\n    \"\"\"\n\n    model: TypeChatLanguageModel\n    validator: TypeChatValidator[T]\n    target_type: type[T]\n    type_name: str\n    schema_str: str\n    _max_repair_attempts = 1\n\n    def __init__(\n        self,\n        model: TypeChatLanguageModel,\n        validator: TypeChatValidator[T],\n        target_type: type[T],\n        *, # keyword-only parameters follow\n        _raise_on_schema_errors: bool = True,\n    ):\n        \"\"\"\n        Args:\n            model: The associated `TypeChatLanguageModel`.\n            validator: The associated `TypeChatValidator[T]`.\n            target_type: A runtime type object describing `T` - the expected shape of JSON data.\n        \"\"\"\n        super().__init__()\n        self.model = model\n        self.validator = validator\n        self.target_type = target_type\n\n        conversion_result = python_type_to_typescript_schema(target_type)\n\n        if _raise_on_schema_errors and conversion_result.errors:\n            error_text = \"\".join(f\"\\n- {error}\" for error in conversion_result.errors)\n            raise ValueError(f\"Could not convert Python type to TypeScript schema: \\n{error_text}\")\n\n        self.type_name = conversion_result.typescript_type_reference\n        self.schema_str = conversion_result.typescript_schema_str\n\n    async def translate(self, input: str, *, prompt_preamble: str | list[PromptSection] | None = None) -> Result[T]:\n        \"\"\"\n        Translates a natural language request into an object of type `T`. If the JSON object returned by\n        the language model fails to validate, repair attempts will be made up until `_max_repair_attempts`.\n        The prompt for the subsequent attempts will include the diagnostics produced for the prior attempt.\n        This often helps produce a valid instance.\n\n        Args:\n            input: A natural language request.\n            prompt_preamble: An optional string or list of prompt sections to prepend to the generated prompt.\\\n                             If a string is given, it is converted to a single \"user\" role prompt section.\n        \"\"\"\n\n        messages: list[PromptSection] = []\n\n        if prompt_preamble:\n            if isinstance(prompt_preamble, str):\n                prompt_preamble = [{\"role\": \"user\", \"content\": prompt_preamble}]\n            messages.extend(prompt_preamble)\n\n        messages.append({\"role\": \"user\", \"content\": self._create_request_prompt(input)})\n\n        num_repairs_attempted = 0\n        while True:\n            completion_response = await self.model.complete(messages)\n            if isinstance(completion_response, Failure):\n                return completion_response\n\n            text_response = completion_response.value\n            first_curly = text_response.find(\"{\")\n            last_curly = text_response.rfind(\"}\") + 1\n            error_message: str\n            if 0 <= first_curly < last_curly:\n                trimmed_response = text_response[first_curly:last_curly]\n                try:\n                    parsed_response = pydantic_core.from_json(trimmed_response, allow_inf_nan=False, cache_strings=False)\n                except ValueError as e:\n                    error_message = f\"Error: {e}\\n\\nAttempted to parse:\\n\\n{trimmed_response}\"\n                else:\n                    result = self.validator.validate_object(parsed_response)\n                    if isinstance(result, Success):\n                        return result\n                    error_message = result.message\n            else:\n                error_message = f\"Response did not contain any text resembling JSON.\\nResponse was\\n\\n{text_response}\"\n            if num_repairs_attempted >= self._max_repair_attempts:\n                return Failure(error_message)\n            num_repairs_attempted += 1\n            messages.append({\"role\": \"assistant\", \"content\": text_response})\n            messages.append({\"role\": \"user\", \"content\": self._create_repair_prompt(error_message)})\n\n    def _create_request_prompt(self, intent: str) -> str:\n        prompt = f\"\"\"\nYou are a service that translates user requests into JSON objects of type \"{self.type_name}\" according to the following TypeScript definitions:\n```\n{self.schema_str}\n```\nThe following is a user request:\n'''\n{intent}\n'''\nThe following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n\"\"\"\n        return prompt\n\n    def _create_repair_prompt(self, validation_error: str) -> str:\n        prompt = f\"\"\"\nThe above JSON object is invalid for the following reason:\n'''\n{validation_error}\n'''\nThe following is a revised JSON object:\n\"\"\"\n        return prompt\n"
  },
  {
    "path": "python/src/typechat/_internal/ts_conversion/__init__.py",
    "content": "from dataclasses import dataclass\nfrom typing_extensions import TypeAliasType\n\nfrom typechat._internal.ts_conversion.python_type_to_ts_nodes import python_type_to_typescript_nodes\nfrom typechat._internal.ts_conversion.ts_node_to_string import ts_declaration_to_str\n\n__all__ = [\n    \"python_type_to_typescript_schema\",\n    \"TypeScriptSchemaConversionResult\",\n]\n\n@dataclass\nclass TypeScriptSchemaConversionResult:\n    typescript_schema_str: str\n    \"\"\"The TypeScript declarations generated from the Python declarations.\"\"\"\n\n    typescript_type_reference: str\n    \"\"\"The TypeScript string representation of a given Python type.\"\"\"\n\n    errors: list[str]\n    \"\"\"Any errors that occurred during conversion.\"\"\"\n\ndef python_type_to_typescript_schema(py_type: type | TypeAliasType) -> TypeScriptSchemaConversionResult:\n    \"\"\"Converts a Python type to a TypeScript schema.\"\"\"\n\n    node_conversion_result = python_type_to_typescript_nodes(py_type)\n\n    decl_strs = map(ts_declaration_to_str, node_conversion_result.type_declarations)\n    schema_str = \"\\n\".join(decl_strs)\n\n    return TypeScriptSchemaConversionResult(\n        typescript_schema_str=schema_str,\n        typescript_type_reference=py_type.__name__,\n        errors=node_conversion_result.errors,\n    )\n"
  },
  {
    "path": "python/src/typechat/_internal/ts_conversion/python_type_to_ts_nodes.py",
    "content": "from __future__ import annotations\n\nfrom collections import OrderedDict\nimport inspect\nimport sys\nimport typing\nimport typing_extensions\nfrom dataclasses import MISSING, Field, dataclass\nfrom types import NoneType, UnionType\nfrom typing_extensions import (\n    Annotated,\n    Any,\n    ClassVar,\n    Doc,\n    Final,\n    Generic,\n    Literal,\n    LiteralString,\n    Never,\n    NoReturn,\n    NotRequired,\n    Protocol,\n    Required,\n    TypeAlias,\n    TypeAliasType,    \n    TypeGuard,\n    TypeVar,\n    Union,\n    cast,\n    get_args,\n    get_origin,\n    get_original_bases,\n    get_type_hints,\n    is_typeddict,\n)\n\nfrom typechat._internal.ts_conversion.ts_type_nodes import (\n    AnyTypeReferenceNode,\n    ArrayTypeNode,\n    BooleanTypeReferenceNode,\n    IdentifierNode,\n    IndexSignatureDeclarationNode,\n    InterfaceDeclarationNode,\n    LiteralTypeNode,\n    NeverTypeReferenceNode,\n    NullTypeReferenceNode,\n    NumberTypeReferenceNode,\n    PropertyDeclarationNode,\n    StringTypeReferenceNode,\n    ThisTypeReferenceNode,\n    TopLevelDeclarationNode,\n    TupleTypeNode,\n    TypeAliasDeclarationNode,\n    TypeNode,\n    TypeParameterDeclarationNode,\n    TypeReferenceNode,\n    UnionTypeNode,\n)\n\nclass GenericDeclarationish(Protocol):\n    __parameters__: list[TypeVar]\n    __type_params__: list[TypeVar] # NOTE: may not be present unless running in 3.12\n\nclass GenericAliasish(Protocol):\n    __origin__: object\n    __args__: tuple[object, ...]\n    __name__: str\n\n\nclass Annotatedish(Protocol):\n    # NOTE: `__origin__` here refers to `SomeType` in `Annnotated[SomeType, ...]`\n    __origin__: object\n    __metadata__: tuple[object, ...]\n\nclass Dataclassish(Protocol):\n    __dataclass_fields__: dict[str, Field[Any]]\n\n# type[TypedDict]\n# https://github.com/microsoft/pyright/pull/6505#issuecomment-1834431725\nclass TypeOfTypedDict(Protocol):\n    __total__: bool\n\nif sys.version_info >= (3, 12) and typing.TypeAliasType is not typing_extensions.TypeAliasType:\n    # Sometimes typing_extensions aliases TypeAliasType,\n    # sometimes it's its own declaration.\n    def is_type_alias_type(py_type: object) -> TypeGuard[TypeAliasType]:\n        return isinstance(py_type, typing.TypeAliasType | typing_extensions.TypeAliasType)\nelse:\n    def is_type_alias_type(py_type: object) -> TypeGuard[TypeAliasType]:\n        return isinstance(py_type, typing_extensions.TypeAliasType)\n\n\ndef is_generic(py_type: object) -> TypeGuard[GenericAliasish]:\n    return hasattr(py_type, \"__origin__\") and hasattr(py_type, \"__args__\")\n\ndef is_dataclass(py_type: object) -> TypeGuard[Dataclassish]:\n    return hasattr(py_type, \"__dataclass_fields__\") and isinstance(cast(Any, py_type).__dataclass_fields__, dict)\n\nTypeReferenceTarget: TypeAlias = type | TypeAliasType | TypeVar | GenericAliasish\n\ndef is_python_type_or_alias(origin: object) -> TypeGuard[type | TypeAliasType]:\n    return isinstance(origin, type) or is_type_alias_type(origin)\n\n\n_KNOWN_GENERIC_SPECIAL_FORMS: frozenset[Any] = frozenset(\n    [\n        Required,\n        NotRequired,\n        ClassVar,\n        Final,\n        Annotated,\n        Generic,\n    ]\n)\n\n_KNOWN_SPECIAL_BASES: frozenset[Any] = frozenset([\n    typing.TypedDict,\n    typing_extensions.TypedDict,\n    Protocol,\n\n    # In older versions of Python, `__orig_bases__` will not be defined on `TypedDict`s\n    # derived from the built-in `typing` module (but they will from `typing_extensions`!).\n    # So `get_original_bases` will fetch `__bases__` which will map `TypedDict` to a plain `dict`.\n    dict,\n])\n\n\n@dataclass\nclass TypeScriptNodeTranslationResult:\n    type_declarations: list[TopLevelDeclarationNode]\n    errors: list[str]\n\n\n# TODO: https://github.com/microsoft/pyright/issues/6587\n_SELF_TYPE = getattr(typing_extensions, \"Self\")\n\n_LIST_TYPES: set[object] = {\n    list,\n    set,\n    frozenset,\n    # TODO: https://github.com/microsoft/pyright/issues/6582\n    # collections.abc.MutableSequence,\n    # collections.abc.Sequence,\n    # collections.abc.Set\n}\n\n# TODO: https://github.com/microsoft/pyright/issues/6582\n# _DICT_TYPES: set[type] = {\n#     dict,\n#     collections.abc.MutableMapping,\n#     collections.abc.Mapping\n# }\n\n\ndef python_type_to_typescript_nodes(root_py_type: object) -> TypeScriptNodeTranslationResult:\n    # TODO: handle conflicting names\n\n    declared_types: OrderedDict[object, TopLevelDeclarationNode | None] = OrderedDict()\n    undeclared_types: OrderedDict[object, object] = OrderedDict({root_py_type: root_py_type}) # just a set, really\n    used_names: dict[str, type | TypeAliasType] = {}\n    errors: list[str] = []\n\n    def skip_annotations(py_type: object) -> object:\n        origin = py_type\n        while (origin := get_origin(py_type)) and origin in _KNOWN_GENERIC_SPECIAL_FORMS:\n            type_arguments = get_args(py_type)\n            if not type_arguments:\n                errors.append(f\"'{origin}' has been used without any type arguments.\")\n                return Any\n            py_type = type_arguments[0]\n            continue\n        return py_type\n\n    def convert_to_type_reference_node(py_type: TypeReferenceTarget) -> TypeNode:\n        py_type_to_declare = py_type\n\n        if is_generic(py_type):\n            py_type_to_declare = get_origin(py_type)\n\n        if py_type_to_declare not in declared_types:\n            if is_python_type_or_alias(py_type_to_declare):\n                undeclared_types[py_type_to_declare] = py_type_to_declare\n            elif not isinstance(py_type, TypeVar):\n                errors.append(f\"Invalid usage of '{py_type}' as a type annotation.\")\n                return AnyTypeReferenceNode\n\n        if is_generic(py_type):\n            return generic_alias_to_type_reference(py_type)\n\n        return TypeReferenceNode(IdentifierNode(py_type.__name__))\n\n    def generic_alias_to_type_reference(py_type: GenericAliasish) -> TypeReferenceNode:\n        origin = get_origin(py_type)\n        assert origin is not None\n        name = origin.__name__\n        type_arguments = list(map(convert_to_type_node, get_args(py_type)))\n        return TypeReferenceNode(IdentifierNode(name), type_arguments)\n\n    def convert_literal_type_arg_to_type_node(py_type: object) -> TypeNode:\n        py_type = skip_annotations(py_type)\n        match py_type:\n            case str() | int() | float():  # no need to match bool, it's a subclass of int\n                return LiteralTypeNode(py_type)\n            case None:\n                return NullTypeReferenceNode\n            case _:\n                errors.append(f\"'{py_type}' cannot be used as a literal type.\")\n                return AnyTypeReferenceNode\n\n    def convert_to_type_node(py_type: object) -> TypeNode:\n        py_type = skip_annotations(py_type)\n\n        if py_type is str or py_type is LiteralString:\n            return StringTypeReferenceNode\n        if py_type is int or py_type is float:\n            return NumberTypeReferenceNode\n        if py_type is bool:\n            return BooleanTypeReferenceNode\n        if py_type is Any or py_type is object:\n            return AnyTypeReferenceNode\n        if py_type is None or py_type is NoneType:\n            return NullTypeReferenceNode\n        if py_type is Never or py_type is NoReturn:\n            return NeverTypeReferenceNode\n        if py_type is _SELF_TYPE:\n            return ThisTypeReferenceNode\n\n        # TODO: consider handling bare 'tuple' (and list, etc.)\n        # https://docs.python.org/3/library/typing.html#annotating-tuples\n        # Using plain tuple as an annotation is equivalent to using tuple[Any, ...]:\n\n        origin = get_origin(py_type)\n        if origin is not None:\n            if origin in _LIST_TYPES:\n                (type_arg,) = get_type_argument_nodes(py_type, 1, AnyTypeReferenceNode)\n                if isinstance(type_arg, UnionTypeNode):\n                    return TypeReferenceNode(IdentifierNode(\"Array\"), [type_arg])\n                return ArrayTypeNode(type_arg)\n\n            if origin is dict:\n                # TODO\n                # Currently, we naively assume all dicts are string-keyed\n                # unless they're annotated with `int` or `float` (note: not `int | float`).\n                key_type_arg, value_type_arg = get_type_argument_nodes(py_type, 2, AnyTypeReferenceNode)\n                if key_type_arg is not NumberTypeReferenceNode:\n                    key_type_arg = StringTypeReferenceNode\n                return TypeReferenceNode(IdentifierNode(\"Record\"), [key_type_arg, value_type_arg])\n\n            if origin is tuple:\n                # Note that when the type is `tuple[()]`,\n                # `type_args` will be an empty tuple.\n                # Which is nice, because we don't have to special-case anything!\n                type_args = get_args(py_type)\n\n                if Ellipsis in type_args:\n                    if len(type_args) != 2:\n                        errors.append(\n                            f\"The tuple type '{py_type}' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'.\"\n                        )\n                        return ArrayTypeNode(AnyTypeReferenceNode)\n\n                    ellipsis_index = type_args.index(Ellipsis)\n                    if ellipsis_index != 1:\n                        errors.append(\n                            f\"The tuple type '{py_type}' is ill-formed because the ellipsis (...) cannot be the first element.\"\n                        )\n                        return ArrayTypeNode(AnyTypeReferenceNode)\n\n                    return ArrayTypeNode(convert_to_type_node(type_args[0]))\n\n                return TupleTypeNode([convert_to_type_node(py_type_arg) for py_type_arg in type_args])\n\n            if origin is Union or origin is UnionType:\n                type_node = [convert_to_type_node(py_type_arg) for py_type_arg in get_args(py_type)]\n                assert len(type_node) > 1\n                return UnionTypeNode(type_node)\n\n            if origin is Literal:\n                type_node = [convert_literal_type_arg_to_type_node(py_type_arg) for py_type_arg in get_args(py_type)]\n                assert len(type_node) >= 1\n                return UnionTypeNode(type_node)\n\n            assert is_generic(py_type)\n            return convert_to_type_reference_node(py_type)\n\n        if is_python_type_or_alias(py_type):\n            return convert_to_type_reference_node(py_type)\n\n        if isinstance(py_type, TypeVar):\n            return convert_to_type_reference_node(py_type)\n\n        errors.append(f\"'{py_type}' cannot be used as a type annotation.\")\n        return AnyTypeReferenceNode\n\n    def declare_property(name: str, py_annotation: type | TypeAliasType, is_typeddict_attribute: bool, optionality_default: bool):\n        \"\"\"\n        Declare a property for a given type.\n        If 'optionality_default' is \n        \"\"\"\n        current_annotation: object = py_annotation\n        origin: object\n        optional: bool | None = None\n        comment: str | None = None\n        while origin := get_origin(current_annotation):\n            if origin is Annotated and comment is None:\n                current_annotation = cast(Annotatedish, current_annotation)\n\n                for metadata in current_annotation.__metadata__:\n                    if isinstance(metadata, Doc):\n                        comment = metadata.documentation\n                        break\n                    if isinstance(metadata, str):\n                        comment = metadata\n                        break\n\n                current_annotation = current_annotation.__origin__\n\n            elif origin is Required or origin is NotRequired:\n                if not is_typeddict_attribute:\n                    errors.append(f\"Optionality cannot be specified with {origin} outside of TypedDicts.\")\n\n                if optional is None:\n                    optional = origin is NotRequired\n                else:\n                    errors.append(f\"{origin} cannot be used within another optionality annotation.\")\n\n                current_annotation = get_args(current_annotation)[0]\n            else:\n                break\n\n        if optional is None:\n            optional = optionality_default\n\n        type_annotation = convert_to_type_node(skip_annotations(current_annotation))\n        return PropertyDeclarationNode(name, optional, comment or \"\", type_annotation)\n\n    def reserve_name(val: type | TypeAliasType):\n        type_name = val.__name__\n        if type_name in used_names:\n            errors.append(f\"Cannot create a schema using two types with the same name. {type_name} conflicts between {val} and {used_names[type_name]}\")\n        else:\n            used_names[type_name] = val\n\n    def declare_type(py_type: object):\n        if (is_typeddict(py_type) or is_dataclass(py_type)) and isinstance(py_type, type):\n            comment = py_type.__doc__ or \"\"\n\n            if hasattr(py_type, \"__type_params__\") and cast(GenericDeclarationish, py_type).__type_params__:\n                type_params = [\n                    TypeParameterDeclarationNode(type_param.__name__)\n                    for type_param in cast(GenericDeclarationish, py_type).__type_params__\n                ]\n            elif hasattr(py_type, \"__parameters__\") and cast(GenericDeclarationish, py_type).__parameters__:\n                type_params = [\n                    TypeParameterDeclarationNode(type_param.__name__)\n                    for type_param in cast(GenericDeclarationish, py_type).__parameters__\n                ]\n            else:\n                type_params = None\n\n            annotated_members = get_type_hints(py_type, include_extras=True)\n\n            raw_but_filtered_bases: list[type] = [\n                base\n                for base in get_original_bases(py_type)\n                if not(base is object or base in _KNOWN_SPECIAL_BASES or get_origin(base) in _KNOWN_GENERIC_SPECIAL_FORMS)\n            ]\n            base_attributes: OrderedDict[str, set[object]] = OrderedDict()\n            for base in raw_but_filtered_bases:\n                for prop, type_hint in get_type_hints(get_origin(base) or base, include_extras=True).items():\n                    base_attributes.setdefault(prop, set()).add(type_hint)\n            bases = [convert_to_type_node(base) for base in raw_but_filtered_bases]\n\n            properties: list[PropertyDeclarationNode | IndexSignatureDeclarationNode] = []\n            if is_typeddict(py_type):\n                for attr_name, type_hint in annotated_members.items():\n                    if attribute_identical_in_all_bases(attr_name, type_hint, base_attributes):\n                        continue\n\n                    assume_optional = cast(TypeOfTypedDict, py_type).__total__ is False\n                    prop = declare_property(attr_name, type_hint, is_typeddict_attribute=True, optionality_default=assume_optional)\n                    properties.append(prop)\n            else:\n                # When a dataclass is created with no explicit docstring, @dataclass will\n                # generate one for us; however, we don't want these in the default output.\n                cleaned_signature = str(inspect.signature(py_type)).replace(\" -> None\", \"\")\n                dataclass_doc = f\"{py_type.__name__}{cleaned_signature}\"\n                if comment == dataclass_doc:\n                    comment = \"\"\n\n                for attr_name, field in cast(Dataclassish, py_type).__dataclass_fields__.items():\n                    type_hint = annotated_members[attr_name]\n                    optional = not(field.default is MISSING and field.default_factory is MISSING)\n                    prop = declare_property(attr_name, type_hint, is_typeddict_attribute=False, optionality_default=optional)\n                    properties.append(prop)\n\n            reserve_name(py_type)\n            return InterfaceDeclarationNode(py_type.__name__, type_params, comment, bases, properties)\n        if isinstance(py_type, type):\n            errors.append(f\"{py_type.__name__} was not a TypedDict, dataclass, or type alias, and cannot be translated.\")\n\n            reserve_name(py_type)\n\n            return InterfaceDeclarationNode(py_type.__name__, None, \"\", None, [])\n        if is_type_alias_type(py_type):\n            type_params = [TypeParameterDeclarationNode(type_param.__name__) for type_param in py_type.__type_params__]\n\n            reserve_name(py_type)\n\n            return TypeAliasDeclarationNode(\n                py_type.__name__,\n                type_params,\n                f\"Comment for {py_type.__name__}.\",\n                convert_to_type_node(py_type.__value__),\n            )\n\n        raise RuntimeError(f\"Cannot declare type {py_type}.\")\n\n    def attribute_identical_in_all_bases(attr_name: str, type_hint: object, base_attributes: dict[str, set[object]]) -> bool:\n        \"\"\"\n        We typically want to omit attributes with type hints that are\n        identical to those declared in all base types.\n        \"\"\"\n        return attr_name in base_attributes and len(base_attributes[attr_name]) == 1 and type_hint in base_attributes[attr_name]\n\n    def get_type_argument_nodes(py_type: object, count: int, default: TypeNode) -> list[TypeNode]:\n        py_type_args = get_args(py_type)\n        result: list[TypeNode] = []\n        if len(py_type_args) != count:\n            errors.append(f\"Expected '{count}' type arguments for '{py_type}'.\")\n        for i in range(count):\n            if i < len(py_type_args):\n                type_node = convert_to_type_node(py_type_args[i])\n            else:\n                type_node = default\n            result.append(type_node)\n        return result\n\n    while undeclared_types:\n        py_type = undeclared_types.popitem()[0]\n        declared_types[py_type] = None\n        declared_types[py_type] = declare_type(py_type)\n\n    type_declarations = cast(list[TopLevelDeclarationNode], list(declared_types.values()))\n    assert None not in type_declarations\n\n    return TypeScriptNodeTranslationResult(type_declarations, errors)\n"
  },
  {
    "path": "python/src/typechat/_internal/ts_conversion/ts_node_to_string.py",
    "content": "import json\nfrom typing_extensions import assert_never\n\nfrom typechat._internal.ts_conversion.ts_type_nodes import (\n    ArrayTypeNode,\n    IdentifierNode,\n    IndexSignatureDeclarationNode,\n    InterfaceDeclarationNode,\n    LiteralTypeNode,\n    NullTypeReferenceNode,\n    PropertyDeclarationNode,\n    TopLevelDeclarationNode,\n    TupleTypeNode,\n    TypeAliasDeclarationNode,\n    TypeNode,\n    TypeReferenceNode,\n    UnionTypeNode,\n)\n\n\ndef comment_to_str(comment_text: str, indentation: str) -> str:\n    comment_text = comment_text.strip()\n    if not comment_text:\n        return \"\"\n    lines = [line.strip() for line in comment_text.splitlines()]\n\n    return \"\\n\".join([f\"{indentation}// {line}\" for line in lines]) + \"\\n\"\n\n\ndef ts_type_to_str(type_node: TypeNode) -> str:\n    match type_node:\n        case TypeReferenceNode(name, type_arguments):\n            assert isinstance(name, IdentifierNode)\n            if type_arguments is None:\n                return name.text\n            return f\"{name.text}<{', '.join([ts_type_to_str(arg) for arg in type_arguments])}>\"\n        case ArrayTypeNode(element_type):\n            assert type(element_type) is not UnionTypeNode\n            # if type(element_type) is UnionTypeNode:\n            #     return f\"Array<{ts_type_to_str(element_type)}>\"\n            return f\"{ts_type_to_str(element_type)}[]\"\n        case TupleTypeNode(element_types):\n            return f\"[{', '.join([ts_type_to_str(element_type) for element_type in element_types])}]\"\n        case UnionTypeNode(types):\n            # Remove duplicates, but try to preserve order of types,\n            # and put null at the end if it's present.\n            str_set: set[str] = set()\n            type_strs: list[str] = []\n            nullable = False\n            for type_node in types:\n                if type_node is NullTypeReferenceNode:\n                    nullable = True\n                    continue\n                type_str = ts_type_to_str(type_node)\n                if type_str not in str_set:\n                    str_set.add(type_str)\n                    type_strs.append(type_str)\n            if nullable:\n                type_strs.append(\"null\")\n            return \" | \".join(type_strs)\n        case LiteralTypeNode(value):\n            return json.dumps(value)\n        # case _:\n        #     raise NotImplementedError(f\"Unhandled type {type(type_node)}\")\n    assert_never(type_node)\n\ndef object_member_to_str(member: PropertyDeclarationNode | IndexSignatureDeclarationNode) -> str:\n    match member:\n        case PropertyDeclarationNode(name, is_optional, comment, annotation):\n            comment = comment_to_str(comment, \"    \")\n            if not name.isidentifier():\n                name = json.dumps(name)\n            return f\"{comment}    {name}{'?' if is_optional else ''}: {ts_type_to_str(annotation)};\"\n        case IndexSignatureDeclarationNode(key_type, value_type):\n            return f\"[key: {ts_type_to_str(key_type)}]: {ts_type_to_str(value_type)};\"\n        # case _:\n        #     raise NotImplementedError(f\"Unhandled member type {type(member)}\")\n    assert_never(member)\n\n\ndef ts_declaration_to_str(declaration: TopLevelDeclarationNode) -> str:\n    match declaration:\n        case InterfaceDeclarationNode(name, type_parameters, comment, base_types, members):\n            comment = comment_to_str(comment, \"\")\n            type_param_str = f\"<{', '.join([param.name for param in type_parameters])}>\" if type_parameters else \"\"\n            base_type_str = (\n                f\" extends {', '.join([ts_type_to_str(base_type) for base_type in base_types])}\" if base_types else \"\"\n            )\n            members_str = \"\\n\".join([f\"{object_member_to_str(member)}\" for member in members]) + \"\\n\" if members else \"\"\n            return f\"{comment}interface {name}{type_param_str}{base_type_str} {{\\n{members_str}}}\\n\"\n        case TypeAliasDeclarationNode(name, type_parameters, comment, target):\n            type_param_str = f\"<{', '.join([param.name for param in type_parameters])}>\" if type_parameters else \"\"\n            return f\"type {name}{type_param_str} = {ts_type_to_str(target)}\\n\"\n        # case _:\n        #     raise NotImplementedError(f\"Unhandled declaration type {type(declaration)}\")\n    assert_never(declaration)\n"
  },
  {
    "path": "python/src/typechat/_internal/ts_conversion/ts_type_nodes.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing_extensions import TypeAlias\n\nTypeNode: TypeAlias = \"TypeReferenceNode | UnionTypeNode | LiteralTypeNode | ArrayTypeNode | TupleTypeNode\"\n\n@dataclass\nclass IdentifierNode:\n    text: str\n\n@dataclass\nclass QualifiedNameNode:\n    left: QualifiedNameNode | IdentifierNode\n    right: IdentifierNode\n\n@dataclass\nclass TypeReferenceNode:\n    name: QualifiedNameNode | IdentifierNode\n    type_arguments: list[TypeNode] | None = None\n\n@dataclass\nclass UnionTypeNode:\n    types: list[TypeNode]\n\n@dataclass\nclass LiteralTypeNode:\n    value: str | int | float | bool\n\n@dataclass\nclass ArrayTypeNode:\n    element_type: TypeNode\n\n@dataclass\nclass TupleTypeNode:\n    element_types: list[TypeNode]\n\n@dataclass\nclass InterfaceDeclarationNode:\n    name: str\n    type_parameters: list[TypeParameterDeclarationNode] | None\n    comment: str\n    base_types: list[TypeNode] | None\n    members: list[PropertyDeclarationNode | IndexSignatureDeclarationNode]\n\n@dataclass\nclass TypeParameterDeclarationNode:\n    name: str\n    constraint: TypeNode | None = None\n\n@dataclass\nclass PropertyDeclarationNode:\n    name: str\n    is_optional: bool\n    comment: str\n    type: TypeNode\n\n@dataclass\nclass IndexSignatureDeclarationNode:\n    key_type: TypeNode\n    value_type: TypeNode\n\n@dataclass\nclass TypeAliasDeclarationNode:\n    name: str\n    type_parameters: list[TypeParameterDeclarationNode] | None\n    comment: str\n    type: TypeNode\n\nTopLevelDeclarationNode: TypeAlias = \"InterfaceDeclarationNode | TypeAliasDeclarationNode\"\n\nStringTypeReferenceNode = TypeReferenceNode(IdentifierNode(\"string\"))\nNumberTypeReferenceNode = TypeReferenceNode(IdentifierNode(\"number\"))\nBooleanTypeReferenceNode = TypeReferenceNode(IdentifierNode(\"boolean\"))\nAnyTypeReferenceNode = TypeReferenceNode(IdentifierNode(\"any\"))\nNullTypeReferenceNode = TypeReferenceNode(IdentifierNode(\"null\"))\nNeverTypeReferenceNode = TypeReferenceNode(IdentifierNode(\"never\"))\nThisTypeReferenceNode = TypeReferenceNode(IdentifierNode(\"this\"))\n"
  },
  {
    "path": "python/src/typechat/_internal/validator.py",
    "content": "import json\nfrom typing_extensions import Generic, TypeVar\n\nimport pydantic\nimport pydantic_core\n\nfrom typechat._internal.result import Failure, Result, Success\n\nT = TypeVar(\"T\", covariant=True)\n\nclass TypeChatValidator(Generic[T]):\n    \"\"\"\n    Validates an object against a given Python type.\n    \"\"\"\n\n    _adapted_type: pydantic.TypeAdapter[T]\n\n    def __init__(self, py_type: type[T]):\n        \"\"\"\n        Args:\n\n            py_type: The schema type to validate against.\n        \"\"\"\n        super().__init__()\n        self._adapted_type = pydantic.TypeAdapter(py_type)\n\n    def validate_object(self, obj: object) -> Result[T]:\n        \"\"\"\n        Validates the given Python object according to the associated schema type.\n\n        Returns a `Success[T]` object containing the object if validation was successful.\n        Otherwise, returns a `Failure` object with a `message` property describing the error.\n        \"\"\"\n        try:\n            # TODO: Switch to `validate_python` when validation modes are exposed.\n            # https://github.com/pydantic/pydantic-core/issues/712\n            # We'd prefer to keep `validate_object` as the core method and\n            # allow translators to concern themselves with the JSON instead.\n            # However, under Pydantic's `strict` mode, a `dict` isn't considered compatible\n            # with a dataclass. So for now, jump back to JSON and validate the string.\n            json_str = pydantic_core.to_json(obj)\n            typed_dict = self._adapted_type.validate_json(json_str, strict=True)\n            return Success(typed_dict)\n        except pydantic.ValidationError as validation_error:\n            return _handle_error(validation_error)\n\n\ndef _handle_error(validation_error: pydantic.ValidationError) -> Failure:\n    error_strings: list[str] = []\n    for error in validation_error.errors(include_url=False):\n        error_string = \"\"\n        loc_path = error[\"loc\"]\n        if loc_path:\n            error_string += f\"Validation path `{'.'.join(map(str, loc_path))}` \"\n        else:\n            error_string += \"Root validation \"\n        input = error[\"input\"]\n        error_string += f\"failed for value `{json.dumps(input)}` because:\\n  {error['msg']}\"\n        error_strings.append(error_string)\n\n    if len(error_strings) > 1:\n        failure_message = \"Several possible issues may have occurred with the given data.\\n\\n\"\n    else:\n        failure_message = \"\"\n    failure_message += \"\\n\".join(error_strings)\n\n    return Failure(failure_message)\n"
  },
  {
    "path": "python/src/typechat/py.typed",
    "content": ""
  },
  {
    "path": "python/tests/__init__.py",
    "content": "# SPDX-FileCopyrightText: Microsoft Corporation\n#\n# SPDX-License-Identifier: MIT\n"
  },
  {
    "path": "python/tests/__py3.11_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts",
    "content": "// Entry point is: 'Derived'\n\ninterface Derived {\n    my_attr_1: string;\n    my_attr_2: number;\n}\n"
  },
  {
    "path": "python/tests/__py3.11_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts",
    "content": "// Entry point is: 'D_or_E'\n\ntype D_or_E = D | E\n\n// This is the definition of the class E.\ninterface E extends C<string> {\n    tag: \"E\";\n    next: this | null;\n}\n\n// This is a generic class named C.\ninterface C<T> {\n    x?: T;\n    c: C<number | null>;\n}\n\n// This is the definition of the class D.\ninterface D extends C<string> {\n    tag?: \"D\";\n    // This comes from string metadata\n    // within an Annotated hint.\n    y: boolean | null;\n    z?: number[] | null;\n    other?: IndirectC;\n    non_class?: NonClass;\n    // This comes from later metadata.\n    multiple_metadata?: string;\n}\n\ninterface NonClass {\n    a: number;\n    \"my-dict\": Record<string, number>;\n}\n\ntype IndirectC = C<number>\n"
  },
  {
    "path": "python/tests/__py3.12+_snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts",
    "content": "// Entry point is: 'FirstOrSecond'\n\ntype FirstOrSecond<T> = First<T> | Second<T>\n\ninterface Second<T> {\n    kind: \"second\";\n    second_attr: T;\n}\n\ninterface First<T> {\n    kind: \"first\";\n    first_attr: T;\n}\n"
  },
  {
    "path": "python/tests/__py3.12+_snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts",
    "content": "// Entry point is: 'Nested'\n\ninterface Nested {\n    item: FirstOrSecond<string>;\n}\n\ntype FirstOrSecond<T> = First<T> | Second<T>\n\ninterface Second<T> {\n    kind: \"second\";\n    second_attr: T;\n}\n\ninterface First<T> {\n    kind: \"first\";\n    first_attr: T;\n}\n"
  },
  {
    "path": "python/tests/__py3.12+_snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts",
    "content": "// Entry point is: 'StrOrInt'\n\ntype StrOrInt = string | number\n"
  },
  {
    "path": "python/tests/__py3.12_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts",
    "content": "// Entry point is: 'Derived'\n\n// ERRORS:\n// !!! Cannot create a schema using two types with the same name. C conflicts between <class 'tests.test_conflicting_names_1.a.<locals>.C'> and <class 'tests.test_conflicting_names_1.b.<locals>.C'>\n\ninterface Derived extends C, C {\n}\n\ninterface C {\n    my_attr_2: number;\n}\n\ninterface C {\n    my_attr_1: string;\n}\n"
  },
  {
    "path": "python/tests/__py3.12_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts",
    "content": "// Entry point is: 'D_or_E'\n\ntype D_or_E = D | E\n\n// This is the definition of the class E.\ninterface E extends C<string> {\n    tag: \"E\";\n    next: this | null;\n}\n\n// This is a generic class named C.\ninterface C<T> {\n    x?: T;\n    c: C<number | null>;\n}\n\n// This is the definition of the class D.\ninterface D extends C<string> {\n    tag?: \"D\";\n    // This comes from string metadata\n    // within an Annotated hint.\n    y: boolean | null;\n    z?: number[] | null;\n    other?: IndirectC;\n    non_class?: NonClass;\n    // This comes from later metadata.\n    multiple_metadata?: string;\n}\n\ninterface NonClass {\n    a: number;\n    \"my-dict\": Record<string, number>;\n}\n\ntype IndirectC = C<number>\n"
  },
  {
    "path": "python/tests/__py3.13_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts",
    "content": "// Entry point is: 'Derived'\n\n// ERRORS:\n// !!! Cannot create a schema using two types with the same name. C conflicts between <class 'tests.test_conflicting_names_1.a.<locals>.C'> and <class 'tests.test_conflicting_names_1.b.<locals>.C'>\n\ninterface Derived extends C, C {\n}\n\ninterface C {\n    my_attr_2: number;\n}\n\ninterface C {\n    my_attr_1: string;\n}\n"
  },
  {
    "path": "python/tests/__py3.13_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts",
    "content": "// Entry point is: 'D_or_E'\n\ntype D_or_E = D | E\n\n// This is the definition of the class E.\ninterface E extends C<string> {\n    tag: \"E\";\n    next: this | null;\n}\n\n// This is a generic class named C.\ninterface C<T> {\n    x?: T;\n    c: C<number | null>;\n}\n\n// This is the definition of the class D.\ninterface D extends C<string> {\n    tag?: \"D\";\n    // This comes from string metadata\n    // within an Annotated hint.\n    y: boolean | null;\n    z?: number[] | null;\n    other?: IndirectC;\n    non_class?: NonClass;\n    // This comes from later metadata.\n    multiple_metadata?: string;\n}\n\ninterface NonClass {\n    a: number;\n    \"my-dict\": Record<string, number>;\n}\n\ntype IndirectC = C<number>\n"
  },
  {
    "path": "python/tests/__py3.14_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts",
    "content": "// Entry point is: 'Derived'\n\n// ERRORS:\n// !!! Cannot create a schema using two types with the same name. C conflicts between <class 'tests.test_conflicting_names_1.a.<locals>.C'> and <class 'tests.test_conflicting_names_1.b.<locals>.C'>\n\ninterface Derived extends C, C {\n}\n\ninterface C {\n    my_attr_2: number;\n}\n\ninterface C {\n    my_attr_1: string;\n}\n"
  },
  {
    "path": "python/tests/__py3.14_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts",
    "content": "// Entry point is: 'D_or_E'\n\ntype D_or_E = D | E\n\n// This is the definition of the class E.\ninterface E extends C<string> {\n    tag: \"E\";\n    next: this | null;\n}\n\n// This is a generic class named C.\ninterface C<T> {\n    x?: T;\n    c: C<number | null>;\n}\n\n// This is the definition of the class D.\ninterface D extends C<string> {\n    tag?: \"D\";\n    // This comes from string metadata\n    // within an Annotated hint.\n    y: boolean | null;\n    z?: number[] | null;\n    other?: IndirectC;\n    non_class?: NonClass;\n    // This comes from later metadata.\n    multiple_metadata?: string;\n}\n\ninterface NonClass {\n    a: number;\n    \"my-dict\": Record<string, number>;\n}\n\ntype IndirectC = C<number>\n"
  },
  {
    "path": "python/tests/__snapshots__/test_coffeeshop/test_coffeeshop_schema.schema.d.ts",
    "content": "// Entry point is: 'Cart'\n\ninterface Cart {\n    type: \"Cart\";\n    items: Array<LineItem | UnknownText>;\n}\n\n// Represents any text that could not be understood.\ninterface UnknownText {\n    type: \"UnknownText\";\n    // The text that wasn't understood\n    text: string;\n}\n\ninterface LineItem {\n    type: \"LineItem\";\n    product: BakeryProduct | LatteDrink | CoffeeDrink | EspressoDrink | UnknownText;\n    quantity: number;\n}\n\ninterface EspressoDrink {\n    type: \"EspressoDrink\";\n    name: \"espresso\" | \"lungo\" | \"ristretto\" | \"macchiato\";\n    temperature?: \"hot\" | \"extra hot\" | \"warm\" | \"iced\";\n    // The default is 'doppio'\n    size?: \"solo\" | \"doppio\" | \"triple\" | \"quad\";\n    options?: Array<Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation>;\n}\n\ninterface LattePreparation {\n    type: \"LattePreparation\";\n    name: \"for here cup\" | \"lid\" | \"with room\" | \"to go\" | \"dry\" | \"wet\";\n}\n\ninterface Caffeine {\n    type: \"Caffeine\";\n    name: \"regular\" | \"two thirds caf\" | \"half caf\" | \"one third caf\" | \"decaf\";\n}\n\ninterface Topping {\n    type: \"Topping\";\n    name: \"cinnamon\" | \"foam\" | \"ice\" | \"nutmeg\" | \"whipped cream\" | \"water\";\n    optionQuantity?: \"no\" | \"light\" | \"regular\" | \"extra\";\n}\n\ninterface Syrup {\n    type: \"Syrup\";\n    name: \"almond syrup\" | \"buttered rum syrup\" | \"caramel syrup\" | \"cinnamon syrup\" | \"hazelnut syrup\" | \"orange syrup\" | \"peppermint syrup\" | \"raspberry syrup\" | \"toffee syrup\" | \"vanilla syrup\";\n    optionQuantity?: \"no\" | \"light\" | \"regular\" | \"extra\";\n}\n\ninterface Sweetener {\n    type: \"Sweetener\";\n    name: \"equal\" | \"honey\" | \"splenda\" | \"sugar\" | \"sugar in the raw\" | \"sweet n low\" | \"espresso shot\";\n    optionQuantity?: \"no\" | \"light\" | \"regular\" | \"extra\";\n}\n\ninterface Creamer {\n    type: \"Creamer\";\n    name: \"whole milk creamer\" | \"two percent milk creamer\" | \"one percent milk creamer\" | \"nonfat milk creamer\" | \"coconut milk creamer\" | \"soy milk creamer\" | \"almond milk creamer\" | \"oat milk creamer\" | \"half and half\" | \"heavy cream\";\n}\n\ninterface CoffeeDrink {\n    type: \"CoffeeDrink\";\n    name: \"americano\" | \"coffee\";\n    temperature?: \"hot\" | \"extra hot\" | \"warm\" | \"iced\";\n    // The default is 'grande'\n    size?: \"short\" | \"tall\" | \"grande\" | \"venti\";\n    options?: Array<Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation>;\n}\n\ninterface LatteDrink {\n    type: \"LatteDrink\";\n    name: \"cappuccino\" | \"flat white\" | \"latte\" | \"latte macchiato\" | \"mocha\" | \"chai latte\";\n    temperature?: \"hot\" | \"extra hot\" | \"warm\" | \"iced\";\n    // The default is 'grande'\n    size?: \"short\" | \"tall\" | \"grande\" | \"venti\";\n    options?: Array<Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation>;\n}\n\ninterface BakeryProduct {\n    type: \"BakeryProduct\";\n    name: \"apple bran muffin\" | \"blueberry muffin\" | \"lemon poppyseed muffin\" | \"bagel\";\n    options?: Array<BakeryOption | BakeryPreparation>;\n}\n\ninterface BakeryPreparation {\n    type: \"BakeryPreparation\";\n    name: \"warmed\" | \"cut in half\";\n}\n\ninterface BakeryOption {\n    type: \"BakeryOption\";\n    name: \"butter\" | \"strawberry jam\" | \"cream cheese\";\n    optionQuantity?: \"no\" | \"light\" | \"regular\" | \"extra\";\n}\n"
  },
  {
    "path": "python/tests/__snapshots__/test_dataclasses/test_data_classes.schema.d.ts",
    "content": "// Entry point is: 'Response'\n\ninterface Response {\n    attr_1: string;\n    // Hello!\n    attr_2: number;\n    attr_3: string | null;\n    attr_4?: string;\n    attr_5?: string | null;\n    attr_6?: string[];\n    attr_7?: Options;\n    _underscore_attr_1?: number;\n}\n\n// TODO: someone add something here.\ninterface Options {\n}\n"
  },
  {
    "path": "python/tests/__snapshots__/test_generic_alias_1/test_generic_alias1.schema.d.ts",
    "content": "// Entry point is: 'FirstOrSecond'\n\ntype FirstOrSecond<T> = First<T> | Second<T>\n\ninterface Second<T> {\n    kind: \"second\";\n    second_attr: T;\n}\n\ninterface First<T> {\n    kind: \"first\";\n    first_attr: T;\n}\n"
  },
  {
    "path": "python/tests/__snapshots__/test_generic_alias_2/test_generic_alias2.schema.d.ts",
    "content": "// Entry point is: 'Nested'\n\ninterface Nested {\n    item: FirstOrSecond<string>;\n}\n\ntype FirstOrSecond<T> = First<T> | Second<T>\n\ninterface Second<T> {\n    kind: \"second\";\n    second_attr: T;\n}\n\ninterface First<T> {\n    kind: \"first\";\n    first_attr: T;\n}\n"
  },
  {
    "path": "python/tests/__snapshots__/test_translator.ambr",
    "content": "# serializer version: 1\n# name: test_translator_with_immediate_pass\n  list([\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    }),\n  ])\n# ---\n# name: test_translator_with_invalid_json\n  list([\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\" \"b\": true }',\n    }),\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n        dict({\n          'content': '{ \"a\": \"hello\" \"b\": true }',\n          'role': 'assistant',\n        }),\n        dict({\n          'content': '''\n            \n            The above JSON object is invalid for the following reason:\n            '''\n            Error: expected `,` or `}` at line 1 column 16\n            \n            Attempted to parse:\n            \n            { \"a\": \"hello\" \"b\": true }\n            '''\n            The following is a revised JSON object:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\" \"b\": true, \"c\": 1234 }',\n    }),\n  ])\n# ---\n# name: test_translator_with_single_failure\n  list([\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\", \"b\": true }',\n    }),\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n        dict({\n          'content': '{ \"a\": \"hello\", \"b\": true }',\n          'role': 'assistant',\n        }),\n        dict({\n          'content': '''\n            \n            The above JSON object is invalid for the following reason:\n            '''\n            Validation path `c` failed for value `{\"a\": \"hello\", \"b\": true}` because:\n              Field required\n            '''\n            The following is a revised JSON object:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    }),\n  ])\n# ---\n# name: test_translator_with_single_failure_and_list_preamble_1\n  list([\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': 'Hey, I need some stuff.',\n          'role': 'user',\n        }),\n        dict({\n          'content': 'Okay, what kind of stuff?',\n          'role': 'assistant',\n        }),\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\", \"b\": true }',\n    }),\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': 'Hey, I need some stuff.',\n          'role': 'user',\n        }),\n        dict({\n          'content': 'Okay, what kind of stuff?',\n          'role': 'assistant',\n        }),\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n        dict({\n          'content': '{ \"a\": \"hello\", \"b\": true }',\n          'role': 'assistant',\n        }),\n        dict({\n          'content': '''\n            \n            The above JSON object is invalid for the following reason:\n            '''\n            Validation path `c` failed for value `{\"a\": \"hello\", \"b\": true}` because:\n              Field required\n            '''\n            The following is a revised JSON object:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    }),\n  ])\n# ---\n# name: test_translator_with_single_failure_and_str_preamble\n  list([\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': 'Just so you know, I need some stuff.',\n          'role': 'user',\n        }),\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\", \"b\": true }',\n    }),\n    dict({\n      'kind': 'CLIENT REQUEST',\n      'payload': list([\n        dict({\n          'content': 'Just so you know, I need some stuff.',\n          'role': 'user',\n        }),\n        dict({\n          'content': '''\n            \n            You are a service that translates user requests into JSON objects of type \"ExampleABC\" according to the following TypeScript definitions:\n            ```\n            interface ExampleABC {\n                a: string;\n                b: boolean;\n                c: number;\n            }\n            \n            ```\n            The following is a user request:\n            '''\n            Get me stuff.\n            '''\n            The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n  \n          ''',\n          'role': 'user',\n        }),\n        dict({\n          'content': '{ \"a\": \"hello\", \"b\": true }',\n          'role': 'assistant',\n        }),\n        dict({\n          'content': '''\n            \n            The above JSON object is invalid for the following reason:\n            '''\n            Validation path `c` failed for value `{\"a\": \"hello\", \"b\": true}` because:\n              Field required\n            '''\n            The following is a revised JSON object:\n  \n          ''',\n          'role': 'user',\n        }),\n      ]),\n    }),\n    dict({\n      'kind': 'MODEL RESPONSE',\n      'payload': '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    }),\n  ])\n# ---\n"
  },
  {
    "path": "python/tests/__snapshots__/test_tuple_errors_1/test_tuples_2.schema.d.ts",
    "content": "// Entry point is: 'TupleContainer'\n\n// ERRORS:\n// !!! '()' cannot be used as a type annotation.\n// !!! '()' cannot be used as a type annotation.\n// !!! '()' cannot be used as a type annotation.\n// !!! The tuple type 'tuple[...]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'.\n// !!! The tuple type 'tuple[int, int, ...]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'.\n// !!! The tuple type 'tuple[..., int]' is ill-formed because the ellipsis (...) cannot be the first element.\n// !!! The tuple type 'tuple[..., ...]' is ill-formed because the ellipsis (...) cannot be the first element.\n// !!! The tuple type 'tuple[int, ..., int]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'.\n// !!! The tuple type 'tuple[int, ..., int, ...]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'.\n\ninterface TupleContainer {\n    empty_tuples_args_1: [any, any];\n    empty_tuples_args_2: any[];\n    arbitrary_length_1: any[];\n    arbitrary_length_2: any[];\n    arbitrary_length_3: any[];\n    arbitrary_length_4: any[];\n    arbitrary_length_5: any[];\n    arbitrary_length_6: any[];\n}\n"
  },
  {
    "path": "python/tests/__snapshots__/test_tuples_1/test_tuples_1.schema.d.ts",
    "content": "// Entry point is: 'TupleContainer'\n\ninterface TupleContainer {\n    empty_tuple: [];\n    tuple_1: [number];\n    tuple_2: [number, string];\n    tuple_3: [number, string];\n    arbitrary_length_1: number[];\n    arbitrary_length_2: number[];\n    arbitrary_length_3: number[];\n    arbitrary_length_4: number[];\n    arbitrary_length_5: number[] | [number];\n    arbitrary_length_6: number[] | [number] | [number, number];\n}\n"
  },
  {
    "path": "python/tests/coffeeshop_deprecated.py",
    "content": "from typing import List, Literal, NotRequired, TypeAlias, TypedDict, Union\n\nfrom typechat import python_type_to_typescript_schema\n\n# This version of coffeeshop uses older constructs for\n# types like List and Union. It is included here for\n# testing purposes.\n\nclass UnknownText(TypedDict):\n    \"\"\"\n    Represents any text that could not be understood.\n    \"\"\"\n\n    type: Literal[\"UnknownText\"]\n    text: str\n\n\nclass Caffeine(TypedDict):\n    type: Literal[\"Caffeine\"]\n    name: Literal[\"regular\", \"two thirds caf\", \"half caf\", \"one third caf\", \"decaf\"]\n\n\nclass Milk(TypedDict):\n    type: Literal[\"Milk\"]\n    name: Literal[\n        \"whole milk\", \"two percent milk\", \"nonfat milk\", \"coconut milk\", \"soy milk\", \"almond milk\", \"oat milk\"\n    ]\n\n\nclass Creamer(TypedDict):\n    type: Literal[\"Creamer\"]\n    name: Literal[\n        \"whole milk creamer\",\n        \"two percent milk creamer\",\n        \"one percent milk creamer\",\n        \"nonfat milk creamer\",\n        \"coconut milk creamer\",\n        \"soy milk creamer\",\n        \"almond milk creamer\",\n        \"oat milk creamer\",\n        \"half and half\",\n        \"heavy cream\",\n    ]\n\n\nclass Topping(TypedDict):\n    type: Literal[\"Topping\"]\n    name: Literal[\"cinnamon\", \"foam\", \"ice\", \"nutmeg\", \"whipped cream\", \"water\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nclass LattePreparation(TypedDict):\n    type: Literal[\"LattePreparation\"]\n    name: Literal[\"for here cup\", \"lid\", \"with room\", \"to go\", \"dry\", \"wet\"]\n\n\nclass Sweetener(TypedDict):\n    type: Literal[\"Sweetener\"]\n    name: Literal[\"equal\", \"honey\", \"splenda\", \"sugar\", \"sugar in the raw\", \"sweet n low\", \"espresso shot\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nCaffeineOptions = Union[Caffeine, Milk, Creamer]\n\nLatteOptions = Union[CaffeineOptions, Topping, LattePreparation, Sweetener]\n\nCoffeeTemperature: TypeAlias = Literal[\"hot\", \"extra hot\", \"warm\", \"iced\"]\n\nCoffeeSize: TypeAlias = Literal[\"short\", \"tall\", \"grande\", \"venti\"]\n\nEspressoSize: TypeAlias = Literal[\"solo\", \"doppio\", \"triple\", \"quad\"]\n\nOptionQuantity: TypeAlias = Literal[\"no\", \"light\", \"regular\", \"extra\"]\n\n\nclass Syrup(TypedDict):\n    type: Literal[\"Syrup\"]\n    name: Literal[\n        \"almond syrup\",\n        \"buttered rum syrup\",\n        \"caramel syrup\",\n        \"cinnamon syrup\",\n        \"hazelnut syrup\",\n        \"orange syrup\",\n        \"peppermint syrup\",\n        \"raspberry syrup\",\n        \"toffee syrup\",\n        \"vanilla syrup\",\n    ]\n    optionQuantity: NotRequired[OptionQuantity]\n\n\nclass LatteDrink(TypedDict):\n    type: Literal[\"LatteDrink\"]\n    name: Literal[\"cappuccino\", \"flat white\", \"latte\", \"latte macchiato\", \"mocha\", \"chai latte\"]\n    temperature: NotRequired[\"CoffeeTemperature\"]\n    size: NotRequired[\"CoffeeSize\"]  # The default is 'grande'\n    options: NotRequired[List[Union[Creamer, Sweetener, Syrup, Topping, Caffeine, LattePreparation]]]\n\n\nclass EspressoDrink(TypedDict):\n    type: Literal[\"EspressoDrink\"]\n    name: Literal[\"espresso\", \"lungo\", \"ristretto\", \"macchiato\"]\n    temperature: NotRequired[\"CoffeeTemperature\"]\n    size: NotRequired[\"EspressoSize\"]  # The default is 'doppio'\n    options: NotRequired[List[Union[Creamer, Sweetener, Syrup, Topping, Caffeine, LattePreparation]]]\n\n\nclass CoffeeDrink(TypedDict):\n    type: Literal[\"CoffeeDrink\"]\n    name: Literal[\"americano\", \"coffee\"]\n    temperature: NotRequired[CoffeeTemperature]\n    size: NotRequired[CoffeeSize]  # The default is \"grande\"\n    options: NotRequired[List[Union[Creamer, Sweetener, Syrup, Topping, Caffeine, LattePreparation]]]\n\n\nclass BakeryOption(TypedDict):\n    type: Literal[\"BakeryOption\"]\n    name: Literal[\"butter\", \"strawberry jam\", \"cream cheese\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nclass BakeryPreparation(TypedDict):\n    type: Literal[\"BakeryPreparation\"]\n    name: Literal[\"warmed\", \"cut in half\"]\n\n\nclass BakeryProduct(TypedDict):\n    type: Literal[\"BakeryProduct\"]\n    name: Literal[\"apple bran muffin\", \"blueberry muffin\", \"lemon poppyseed muffin\", \"bagel\"]\n    options: NotRequired[List[BakeryOption | BakeryPreparation]]\n\n\nProduct = Union[BakeryProduct, LatteDrink, CoffeeDrink, UnknownText]\n\n\nclass LineItem(TypedDict):\n    type: Literal[\"LineItem\"]\n    product: Product\n    quantity: int\n\n\nclass Cart(TypedDict):\n    type: Literal[\"Cart\"]\n    items: List[LineItem | UnknownText]\n\nresult = python_type_to_typescript_schema(Cart)\n\nprint(f\"// Entry point is: '{result.typescript_type_reference}'\")\nprint(\"// TypeScript Schema:\\n\")\nprint(result.typescript_schema_str)\nif result.errors:\n    print(\"// Errors:\")\n    for err in result.errors:\n        print(f\"// - {err}\\n\")\n"
  },
  {
    "path": "python/tests/test_coffeeshop.py",
    "content": "from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc, Any\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import TypeScriptSchemaSnapshotExtension\n\nclass UnknownText(TypedDict):\n    \"\"\"\n    Represents any text that could not be understood.\n    \"\"\"\n\n    type: Literal[\"UnknownText\"]\n    text: Annotated[str, Doc(\"The text that wasn't understood\")]\n\n\nclass Caffeine(TypedDict):\n    type: Literal[\"Caffeine\"]\n    name: Literal[\"regular\", \"two thirds caf\", \"half caf\", \"one third caf\", \"decaf\"]\n\n\nclass Milk(TypedDict):\n    type: Literal[\"Milk\"]\n    name: Literal[\n        \"whole milk\", \"two percent milk\", \"nonfat milk\", \"coconut milk\", \"soy milk\", \"almond milk\", \"oat milk\"\n    ]\n\n\nclass Creamer(TypedDict):\n    type: Literal[\"Creamer\"]\n    name: Literal[\n        \"whole milk creamer\",\n        \"two percent milk creamer\",\n        \"one percent milk creamer\",\n        \"nonfat milk creamer\",\n        \"coconut milk creamer\",\n        \"soy milk creamer\",\n        \"almond milk creamer\",\n        \"oat milk creamer\",\n        \"half and half\",\n        \"heavy cream\",\n    ]\n\n\nclass Topping(TypedDict):\n    type: Literal[\"Topping\"]\n    name: Literal[\"cinnamon\", \"foam\", \"ice\", \"nutmeg\", \"whipped cream\", \"water\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nclass LattePreparation(TypedDict):\n    type: Literal[\"LattePreparation\"]\n    name: Literal[\"for here cup\", \"lid\", \"with room\", \"to go\", \"dry\", \"wet\"]\n\n\nclass Sweetener(TypedDict):\n    type: Literal[\"Sweetener\"]\n    name: Literal[\"equal\", \"honey\", \"splenda\", \"sugar\", \"sugar in the raw\", \"sweet n low\", \"espresso shot\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nCaffeineOptions = Caffeine | Milk | Creamer\n\nLatteOptions = CaffeineOptions | Topping | LattePreparation | Sweetener\n\nCoffeeTemperature = Literal[\"hot\", \"extra hot\", \"warm\", \"iced\"]\n\nCoffeeSize = Literal[\"short\", \"tall\", \"grande\", \"venti\"]\n\nEspressoSize = Literal[\"solo\", \"doppio\", \"triple\", \"quad\"]\n\nOptionQuantity = Literal[\"no\", \"light\", \"regular\", \"extra\"]\n\n\nclass Syrup(TypedDict):\n    type: Literal[\"Syrup\"]\n    name: Literal[\n        \"almond syrup\",\n        \"buttered rum syrup\",\n        \"caramel syrup\",\n        \"cinnamon syrup\",\n        \"hazelnut syrup\",\n        \"orange syrup\",\n        \"peppermint syrup\",\n        \"raspberry syrup\",\n        \"toffee syrup\",\n        \"vanilla syrup\",\n    ]\n    optionQuantity: NotRequired[OptionQuantity]\n\n\nclass LatteDrink(TypedDict):\n    type: Literal[\"LatteDrink\"]\n    name: Literal[\"cappuccino\", \"flat white\", \"latte\", \"latte macchiato\", \"mocha\", \"chai latte\"]\n    temperature: NotRequired[\"CoffeeTemperature\"]\n    size: NotRequired[Annotated[\"CoffeeSize\", Doc(\"The default is 'grande'\")]]\n    options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]]\n\n\nclass EspressoDrink(TypedDict):\n    type: Literal[\"EspressoDrink\"]\n    name: Literal[\"espresso\", \"lungo\", \"ristretto\", \"macchiato\"]\n    temperature: NotRequired[\"CoffeeTemperature\"]\n    size: NotRequired[Annotated[\"EspressoSize\", Doc(\"The default is 'doppio'\")]]\n    options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]]\n\n\nclass CoffeeDrink(TypedDict):\n    type: Literal[\"CoffeeDrink\"]\n    name: Literal[\"americano\", \"coffee\"]\n    temperature: NotRequired[CoffeeTemperature]\n    size: NotRequired[Annotated[CoffeeSize, Doc(\"The default is 'grande'\")]]\n    options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]]\n\n\nclass BakeryOption(TypedDict):\n    type: Literal[\"BakeryOption\"]\n    name: Literal[\"butter\", \"strawberry jam\", \"cream cheese\"]\n    optionQuantity: NotRequired[\"OptionQuantity\"]\n\n\nclass BakeryPreparation(TypedDict):\n    type: Literal[\"BakeryPreparation\"]\n    name: Literal[\"warmed\", \"cut in half\"]\n\n\nclass BakeryProduct(TypedDict):\n    type: Literal[\"BakeryProduct\"]\n    name: Literal[\"apple bran muffin\", \"blueberry muffin\", \"lemon poppyseed muffin\", \"bagel\"]\n    options: NotRequired[list[BakeryOption | BakeryPreparation]]\n\n\nProduct = BakeryProduct | LatteDrink | CoffeeDrink | EspressoDrink | UnknownText\n\n\nclass LineItem(TypedDict):\n    type: Literal[\"LineItem\"]\n    product: Product\n    quantity: int\n\n\nclass Cart(TypedDict):\n    type: Literal[\"Cart\"]\n    items: list[LineItem | UnknownText]\n\n\ndef test_coffeeshop_schema(snapshot: Any):\n    assert(python_type_to_typescript_schema(Cart) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension))\n"
  },
  {
    "path": "python/tests/test_conflicting_names_1.py",
    "content": "from typing import Any, TypedDict, cast\n\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import PyVersionedTypeScriptSchemaSnapshotExtension\n\n\ndef a():\n    class C(TypedDict):\n        my_attr_1: str\n    return C\n\n\ndef b():\n    class C(TypedDict):\n        my_attr_2: int\n    return C\n\nA = a()\nB = b()\n\nclass Derived(A, B): # type: ignore\n    pass\n\ndef test_conflicting_names_1(snapshot: Any):\n    assert python_type_to_typescript_schema(cast(type, Derived)) == snapshot(extension_class=PyVersionedTypeScriptSchemaSnapshotExtension)\n"
  },
  {
    "path": "python/tests/test_dataclasses.py",
    "content": "from typing_extensions import Any\nfrom typing import Annotated\nfrom dataclasses import dataclass, field\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import TypeScriptSchemaSnapshotExtension\n\n@dataclass\nclass Options:\n    \"\"\"\n    TODO: someone add something here.\n    \"\"\"\n    ...\n\n@dataclass\nclass Response:\n    attr_1: str\n    attr_2: Annotated[int, \"Hello!\"]\n    attr_3: str | None\n    attr_4: str = \"hello!\"\n    attr_5: str | None = None\n    attr_6: list[str] = field(default_factory=list)\n    attr_7: Options = field(default_factory=Options)\n    _underscore_attr_1: int = 123\n\n    def do_something(self):\n        print(f\"{self.attr_1=}\")\n\n\ndef test_data_classes(snapshot: Any):\n    assert(python_type_to_typescript_schema(Response) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension))\n"
  },
  {
    "path": "python/tests/test_generic_alias_1.py",
    "content": "from typing_extensions import TypeAliasType, Any\nfrom typing import Literal, TypedDict, TypeVar, Generic\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import TypeScriptSchemaSnapshotExtension\n\nT = TypeVar(\"T\", covariant=True)\n\nclass First(Generic[T], TypedDict):\n    kind: Literal[\"first\"]\n    first_attr: T\n\n\nclass Second(Generic[T], TypedDict):\n    kind: Literal[\"second\"]\n    second_attr: T\n\n\nFirstOrSecond = TypeAliasType(\"FirstOrSecond\", First[T] | Second[T], type_params=(T,))\n\n\ndef test_generic_alias1(snapshot: Any):\n    assert(python_type_to_typescript_schema(FirstOrSecond) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension))\n"
  },
  {
    "path": "python/tests/test_generic_alias_2.py",
    "content": "from typing_extensions import TypeAliasType, Any\nfrom typing import Literal, TypedDict, Generic, TypeVar\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import TypeScriptSchemaSnapshotExtension\n\nT = TypeVar(\"T\", covariant=True)\n\nclass First(Generic[T], TypedDict):\n    kind: Literal[\"first\"]\n    first_attr: T\n\n\nclass Second(Generic[T], TypedDict):\n    kind: Literal[\"second\"]\n    second_attr: T\n\n\nFirstOrSecond = TypeAliasType(\"FirstOrSecond\", First[T] | Second[T], type_params=(T,))\n\n\nclass Nested(TypedDict):\n    item: FirstOrSecond[str]\n\n\ndef test_generic_alias2(snapshot: Any):\n    assert(python_type_to_typescript_schema(Nested) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension))\n"
  },
  {
    "path": "python/tests/test_generic_alias_3.py",
    "content": "from typing import Any\nfrom .utilities import check_snapshot_for_module_string_if_3_12_plus\n\nmodule_str = \"\"\"\nfrom typing import Literal, TypedDict\nclass First[T](TypedDict):\n    kind: Literal[\"first\"]\n    first_attr: T\n\n\nclass Second[T](TypedDict):\n    kind: Literal[\"second\"]\n    second_attr: T\n\n\ntype FirstOrSecond[T] = First[T] | Second[T]\n\"\"\"\n\ndef test_generic_alias3(snapshot: Any):\n    check_snapshot_for_module_string_if_3_12_plus(snapshot, input_type_str=\"FirstOrSecond\", module_str=module_str)\n"
  },
  {
    "path": "python/tests/test_generic_alias_4.py",
    "content": "from typing import Any\nfrom .utilities import check_snapshot_for_module_string_if_3_12_plus\n\nmodule_str = \"\"\"\nfrom typing import Literal, TypedDict\nclass First[T](TypedDict):\n    kind: Literal[\"first\"]\n    first_attr: T\n\n\nclass Second[T](TypedDict):\n    kind: Literal[\"second\"]\n    second_attr: T\n\n\ntype FirstOrSecond[T] = First[T] | Second[T]\n\nclass Nested(TypedDict):\n    item: FirstOrSecond[str]\n\"\"\"\n\ndef test_generic_alias4(snapshot: Any):\n    check_snapshot_for_module_string_if_3_12_plus(snapshot, input_type_str=\"Nested\", module_str=module_str)\n"
  },
  {
    "path": "python/tests/test_hello_world.py",
    "content": "from typing import Annotated, Literal, NotRequired, Optional, Required, Self, TypedDict, TypeVar, Generic, Any\nfrom typing_extensions import TypeAliasType\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import PyVersionedTypeScriptSchemaSnapshotExtension\n\nT = TypeVar(\"T\", covariant=True)\n\nclass C(Generic[T], TypedDict):\n    \"This is a generic class named C.\"\n    x: NotRequired[T]\n    c: \"C[int | float | None]\"\n\nIndirectC = TypeAliasType(\"IndirectC\", C[int])\n\n\nclass D(C[str], total=False):\n    \"This is the definition of the class D.\"\n    tag: Literal[\"D\"]\n    y: Required[Annotated[bool | None, \"This comes from string metadata\\nwithin an Annotated hint.\"]]\n    z: Optional[list[int]]\n    other: IndirectC\n    non_class: \"NonClass\"\n\n    multiple_metadata: Annotated[str, None, str, \"This comes from later metadata.\", int]\n\n\nNonClass = TypedDict(\"NonClass\", {\"a\": int, \"my-dict\": dict[str, int]})\n\n\nclass E(C[str]):\n    \"This is the definition of the class E.\"\n    tag: Literal[\"E\"]\n    next: Self | None\n\n\nD_or_E = TypeAliasType(\"D_or_E\", D | E)\n\n\ndef test_generic_alias1(snapshot: Any):\n    assert(python_type_to_typescript_schema(D_or_E) == snapshot(extension_class=PyVersionedTypeScriptSchemaSnapshotExtension))\n"
  },
  {
    "path": "python/tests/test_translator.py",
    "content": "\nimport asyncio\nfrom dataclasses import dataclass\nfrom typing_extensions import Any, Iterator, Literal, TypedDict, override\nimport typechat\n\nclass ConvoRecord(TypedDict):\n    kind: Literal[\"CLIENT REQUEST\", \"MODEL RESPONSE\"]\n    payload: str | list[typechat.PromptSection]\n\nclass FixedModel(typechat.TypeChatLanguageModel):\n    responses: Iterator[str]\n    conversation: list[ConvoRecord]\n\n    \"A model which responds with one of a series of responses.\"\n    def __init__(self, responses: list[str]) -> None:\n        super().__init__()\n        self.responses = iter(responses)\n        self.conversation = []\n\n    @override\n    async def complete(self, prompt: str | list[typechat.PromptSection]) -> typechat.Result[str]:\n        # Capture a snapshot because the translator\n        # can choose to pass in the same underlying list.\n        if isinstance(prompt, list):\n            prompt = prompt.copy()\n\n        self.conversation.append({ \"kind\": \"CLIENT REQUEST\", \"payload\": prompt })\n        response = next(self.responses)\n        self.conversation.append({ \"kind\": \"MODEL RESPONSE\", \"payload\": response })\n        return typechat.Success(response)\n\n@dataclass\nclass ExampleABC:\n    a: str\n    b: bool\n    c: int\n\nv = typechat.TypeChatValidator(ExampleABC)\n\ndef test_translator_with_immediate_pass(snapshot: Any):\n    m = FixedModel([\n        '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    ])\n    t = typechat.TypeChatJsonTranslator(m, v, ExampleABC)\n    asyncio.run(t.translate(\"Get me stuff.\"))\n    \n    assert m.conversation == snapshot\n\ndef test_translator_with_single_failure(snapshot: Any):\n    m = FixedModel([\n        '{ \"a\": \"hello\", \"b\": true }',\n        '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    ])\n    t = typechat.TypeChatJsonTranslator(m, v, ExampleABC)\n    asyncio.run(t.translate(\"Get me stuff.\"))\n    \n    assert m.conversation == snapshot\n\ndef test_translator_with_invalid_json(snapshot: Any):\n    m = FixedModel([\n        '{ \"a\": \"hello\" \"b\": true }',\n        '{ \"a\": \"hello\" \"b\": true, \"c\": 1234 }',\n    ])\n    t = typechat.TypeChatJsonTranslator(m, v, ExampleABC)\n    asyncio.run(t.translate(\"Get me stuff.\"))\n    \n    assert m.conversation == snapshot\n\ndef test_translator_with_single_failure_and_str_preamble(snapshot: Any):\n    m = FixedModel([\n        '{ \"a\": \"hello\", \"b\": true }',\n        '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    ])\n    t = typechat.TypeChatJsonTranslator(m, v, ExampleABC)\n    asyncio.run(t.translate(\n        \"Get me stuff.\",\n        prompt_preamble=\"Just so you know, I need some stuff.\",\n    ))\n    \n    assert m.conversation == snapshot\n\ndef test_translator_with_single_failure_and_list_preamble_1(snapshot: Any):\n    m = FixedModel([\n        '{ \"a\": \"hello\", \"b\": true }',\n        '{ \"a\": \"hello\", \"b\": true, \"c\": 1234 }',\n    ])\n    t = typechat.TypeChatJsonTranslator(m, v, ExampleABC)\n    asyncio.run(t.translate(\"Get me stuff.\", prompt_preamble=[\n        {\"role\": \"user\", \"content\": \"Hey, I need some stuff.\"},\n        {\"role\": \"assistant\", \"content\": \"Okay, what kind of stuff?\"},\n    ]))\n    \n    assert m.conversation == snapshot\n\n"
  },
  {
    "path": "python/tests/test_tuple_errors_1.py",
    "content": "\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import TypeScriptSchemaSnapshotExtension\n\n@dataclass\nclass TupleContainer:\n    empty_tuples_args_1: tuple[(), ()] # type: ignore\n    empty_tuples_args_2: tuple[(), ...] # type: ignore\n\n    # Arbitrary-length tuples have exactly two type arguments – the type and an ellipsis.\n    # Any other tuple form that uses an ellipsis is invalid.\n    arbitrary_length_1: tuple[...] # type: ignore\n    arbitrary_length_2: tuple[int, int, ...] # type: ignore\n    arbitrary_length_3: tuple[..., int] # type: ignore\n    arbitrary_length_4: tuple[..., ...] # type: ignore\n    arbitrary_length_5: tuple[int, ..., int] # type: ignore\n    arbitrary_length_6: tuple[int, ..., int, ...] # type: ignore\n\ndef test_tuples_2(snapshot: Any):\n    assert python_type_to_typescript_schema(TupleContainer) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)"
  },
  {
    "path": "python/tests/test_tuples_1.py",
    "content": "\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom typechat import python_type_to_typescript_schema\nfrom .utilities import TypeScriptSchemaSnapshotExtension\n\n@dataclass\nclass TupleContainer:\n    # The empty tuple can be annotated as tuple[()].\n    empty_tuple: tuple[()]\n\n    tuple_1: tuple[int]\n    tuple_2: tuple[int, str]\n    tuple_3: tuple[int, str] | tuple[float, str]\n\n\n    # Arbitrary-length homogeneous tuples can be expressed using one type and an ellipsis, for example tuple[int, ...].\n    arbitrary_length_1: tuple[int, ...]\n    arbitrary_length_2: tuple[int, ...] | list[int]\n    arbitrary_length_3: tuple[int, ...] | tuple[int, ...]\n    arbitrary_length_4: tuple[int, ...] | tuple[float, ...]\n    arbitrary_length_5: tuple[int, ...] | tuple[int]\n    arbitrary_length_6: tuple[int, ...] | tuple[int] | tuple[int, int]\n\ndef test_tuples_1(snapshot: Any):\n    assert python_type_to_typescript_schema(TupleContainer) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)"
  },
  {
    "path": "python/tests/test_type_alias_syntax.py",
    "content": "from typing import Any\nfrom .utilities import check_snapshot_for_module_string_if_3_12_plus\n\nmodule_str = \"type StrOrInt = str | int\"\n\ndef test_type_alias_union1(snapshot: Any):\n    check_snapshot_for_module_string_if_3_12_plus(snapshot, \"StrOrInt\", module_str)\n"
  },
  {
    "path": "python/tests/test_validator.py",
    "content": "\nfrom dataclasses import dataclass\nimport typechat\n\n@dataclass\nclass Example:\n    a: str\n    b: int\n    c: bool\n\nv = typechat.TypeChatValidator(Example)\n\ndef test_dict_valid_as_dataclass():\n    r = v.validate_object({\"a\": \"hello!\", \"b\": 42, \"c\": True})\n    assert r == typechat.Success(Example(a=\"hello!\", b=42, c=True))\n    "
  },
  {
    "path": "python/tests/utilities.py",
    "content": "from pathlib import Path\nimport sys\nimport types\n\nfrom typing_extensions import Any, override\nimport pytest\n\nfrom syrupy.extensions.single_file import SingleFileSnapshotExtension, WriteMode\nfrom syrupy.location import PyTestLocation\n\nfrom typechat._internal.ts_conversion import TypeScriptSchemaConversionResult, python_type_to_typescript_schema\n\nclass TypeScriptSchemaSnapshotExtension(SingleFileSnapshotExtension):\n    _write_mode = WriteMode.TEXT\n    file_extension = \"schema.d.ts\"\n\n    @override\n    def serialize(self, data: TypeScriptSchemaConversionResult, *,\n        exclude: Any = None,\n        include: Any = None,\n        matcher: Any = None,\n    ) -> str:\n        result_str = f\"// Entry point is: '{data.typescript_type_reference}'\\n\\n\"\n        if data.errors:\n            result_str += \"// ERRORS:\\n\"\n            for err in data.errors:\n                result_str += f\"// !!! {err}\\n\"\n            result_str += \"\\n\"\n\n        result_str += data.typescript_schema_str\n        return result_str\n\nclass PyVersionedTypeScriptSchemaSnapshotExtension(TypeScriptSchemaSnapshotExtension):\n    py_ver_dir: str = f\"__py{sys.version_info.major}.{sys.version_info.minor}_snapshots__\"\n\n    @override\n    @classmethod\n    def dirname(cls, *, test_location: PyTestLocation) -> str:\n        result = Path(test_location.filepath).parent.joinpath(\n            f\"{cls.py_ver_dir}\",\n            test_location.basename,\n        )\n        return str(result)\n\nclass PyVersioned3_12_PlusSnapshotExtension(PyVersionedTypeScriptSchemaSnapshotExtension):\n    py_ver_dir: str = f\"__py3.12+_snapshots__\"\n\ndef check_snapshot_for_module_string_if_3_12_plus(snapshot: Any, input_type_str: str, module_str: str):\n    if sys.version_info < (3, 12):\n        pytest.skip(\"requires python 3.12 or higher\")\n\n    module = types.ModuleType(\"test_module\")\n    exec(module_str, module.__dict__)\n    type_obj = eval(input_type_str, globals(), module.__dict__)\n\n    assert(python_type_to_typescript_schema(type_obj) == snapshot(extension_class=PyVersioned3_12_PlusSnapshotExtension))\n\n@pytest.fixture\ndef snapshot_schema(snapshot: Any):\n    return snapshot.with_defaults(extension_class=TypeScriptSchemaSnapshotExtension)\n"
  },
  {
    "path": "site/.eleventy.js",
    "content": "\nconst dateFormatter = new Intl.DateTimeFormat(\"en-US\", { year: \"numeric\", month: \"long\", day: \"numeric\" });\nconst listFormatter = new Intl.ListFormat(\"en-US\", { style: \"long\", type: \"conjunction\" });\n\n/**\n * \n * @param {import(\"@11ty/eleventy\").UserConfig} eleventyConfig \n */\nmodule.exports = async function (eleventyConfig) {\n    const shiki = await import(\"shiki\");\n    const { EleventyHtmlBasePlugin } = await import(\"@11ty/eleventy\");\n\n    eleventyConfig.addPlugin(EleventyHtmlBasePlugin);\n\n    eleventyConfig.addPassthroughCopy(\"./src/css\");\n    eleventyConfig.addPassthroughCopy(\"./src/js\");\n\n    eleventyConfig.addFilter(\"formatDate\", value => dateFormatter.format(value));\n    eleventyConfig.addFilter(\"formatList\", value => listFormatter.format(value));\n\n    eleventyConfig.setNunjucksEnvironmentOptions({\n        throwOnUndefined: true,\n    });\n\n    eleventyConfig.amendLibrary(\"md\", () => { });\n    eleventyConfig.on(\"eleventy.before\", async () => {\n        const highlighter = await shiki.getHighlighter({\n            langs: [\n                \"typescript\", \"javascript\", \"tsx\", \"jsx\",\n                \"jsonc\", \"json\",\n                \"html\", \"diff\",\n                \"bat\", \"sh\",\n                \"python\", \"py\",\n            ],\n            theme: \"dark-plus\"\n        });\n        eleventyConfig.amendLibrary(\"md\", (mdLib) =>\n            mdLib.set({\n                highlight: (code, lang) => highlighter.codeToHtml(code, { lang }),\n            })\n        );\n    });\n\n    return {\n        dir: {\n            input: \"src\",\n            output: \"_site\"\n        },\n        pathPrefix: \"TypeChat\",\n    };\n}\n"
  },
  {
    "path": "site/.gitignore",
    "content": "_site"
  },
  {
    "path": "site/jsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"es2021\",\n        \"module\": \"nodenext\",\n        \"lib\": [\"esnext\", \"es2021.intl\"],\n        \"noEmit\": true,\n        \"checkJs\": true\n    },\n    \"include\": [\n        \"./.eleventy.js\",\n    ],\n    \"exclude\": []\n}\n"
  },
  {
    "path": "site/package.json",
    "content": "{\n  \"name\": \"typechat-site\",\n  \"private\": true,\n  \"version\": \"0.0.1\",\n  \"description\": \"Website for TypeChat\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"eleventy\",\n    \"serve\": \"eleventy --serve\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/microsoft/TypeChat.git\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/microsoft/TypeChat/issues\"\n  },\n  \"homepage\": \"https://github.com/microsoft/TypeChat#readme\",\n  \"devDependencies\": {\n    \"@11ty/eleventy\": \"^3.1.2\",\n    \"shiki\": \"^0.14.3\"\n  }\n}\n"
  },
  {
    "path": "site/src/_data/docsTOC.json",
    "content": "[\n    {\n        \"groupName\": \"Home\",\n        \"pages\": [\n            { \"title\": \"Introduction\", \"url\": \"/docs/introduction/\"},\n            { \"title\": \"Examples\", \"url\": \"/docs/examples/\"},\n            { \"title\": \"Techniques\", \"url\": \"/docs/techniques/\"},\n            { \"title\": \"FAQ\", \"url\": \"/docs/faq/\"},\n            { \"title\": \"Basic Usage for TypeScript\", \"url\": \"/docs/typescript/basic-usage/\"}\n        ]\n    }\n]\n"
  },
  {
    "path": "site/src/_data/headernav.json",
    "content": "[\n    {\n        \"title\": \"Home\",\n        \"dest\": \"/\"\n    },\n    {\n        \"title\": \"Docs\",\n        \"dest\": \"/docs/\"\n    },\n    {\n        \"title\": \"Blog\",\n        \"dest\": \"/blog/\"\n    },\n    {\n        \"title\": \"GitHub\",\n        \"dest\": \"https://github.com/microsoft/TypeChat\",\n        \"isExternal\": true\n    }\n]"
  },
  {
    "path": "site/src/_includes/base.njk",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" class=\"h-100\">\n\n<head>\n    <meta charset='utf-8'>\n    <meta http-equiv='X-UA-Compatible' content='IE=edge'>\n    <title>{% if title %}{{title}} - TypeChat{% else %}TypeChat{% endif %}</title>\n    <meta name='viewport' content='width=device-width, initial-scale=1'>\n    <!-- <link rel='stylesheet' type='text/css' media='screen' href='main.css'> -->\n    <link href=\"/css/5.3.0_dist_css_bootstrap.min.css\" rel=\"stylesheet\">\n    <link href=\"/css/styles.css\" rel=\"stylesheet\">\n    <script src=\"/js/interactivity.js\" defer></script>\n    <noscript><link href=\"/css/noscript-styles.css\" rel=\"stylesheet\"></noscript>\n</head>\n\n<body class=\"h-100\">\n{% include \"header-prologue.njk\" %}\n\n{{ content | safe }}\n\n{% include \"footer.njk\" %}\n</div>\n</body>\n\n</html>"
  },
  {
    "path": "site/src/_includes/blog.njk",
    "content": "---\nlayout: base\n---\n\n<main>\n<nav class=\"typechat-docs-smol-nav d-md-none mx-auto text-center\">\n    <label class=\"mb-3\">\n    Blog Posts\n    <select class=\"nav-on-change\">\n    {% for post in collections.post %}\n        {%- if post.url == page.url -%}\n        <option value=\"{{post.url | url}}\" selected>{{post.data.title}}</a></li>\n        {% else %}\n        <option value=\"{{post.url | url}}\">{{post.data.title}}</a></li>\n        {% endif %}\n    </li>\n    {% endfor %}\n    </select>\n    </label>\n    <hr />\n</nav>\n<div class=\"container d-flex flex-nowrap with-sidebar typechat-cap-content-width\">\n<aside class=\"flex-shrink-0 p-3 d-none d-md-block\">\n<a href=\"/blog/\" class=\"d-flex align-items-center pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom\">\n    <span class=\"fs-5 fw-semibold\">Blog Posts</span>\n</a>\n<ul class=\"list-unstyled ps-0\">\n{% for post in collections.post %}\n    <li class=\"mb-2\">\n        <ul class=\"btn-toggle-nav list-unstyled fw-normal pb-1 ms-2 small\">\n            <li class=\"mb-1\">\n            {%- if post.url == page.url -%}\n            <a href=\"{{post.url}}\" class=\"link-body-emphasis d-inline-flex text-decoration-none rounded active\" aria-current=\"page\">{{post.data.title}}</a>\n            {% else %}\n            <a href=\"{{post.url}}\" class=\"link-body-emphasis d-inline-flex text-decoration-none rounded\">{{post.data.title}}</a>\n            {% endif %}\n            </li>\n        </ul>\n    </li>\n{% endfor %}\n</ul>\n</aside>\n<article class=\"typechat-prose-content\">\n{{ content | safe }}\n</article>\n</div>\n</main>"
  },
  {
    "path": "site/src/_includes/doc-page.njk",
    "content": "---\nlayout: docs\n---\n<h1>{{title}}</h1>\n{{content | safe}}"
  },
  {
    "path": "site/src/_includes/docs.njk",
    "content": "---\nlayout: base\ntitle: Docs\n---\n<main>\n<nav class=\"typechat-docs-smol-nav d-md-none mx-auto text-center\">\n    <label class=\"mb-3\">\n    Doc Pages\n    <select class=\"nav-on-change\">\n    {% for docsGroup in docsTOC %}\n        <li class=\"mb-2\">\n            <optgroup label=\"{{docsGroup.groupName}}\">\n                {% for docPage in docsGroup.pages %}\n                {%- if docPage.url == page.url -%}\n                <option value=\"{{docPage.url | url}}\" selected>{{docPage.title}}</option>\n                {% else %}\n                <option value=\"{{docPage.url | url}}\">{{docPage.title}}</option>\n                {% endif %}\n                {% endfor %}\n            </optgroup>\n        </li>\n    {% endfor %}\n    </select>\n    </label>\n    <hr />\n</nav>\n<div class=\"container d-flex flex-nowrap with-sidebar typechat-cap-content-width\">\n<nav class=\"flex-shrink-0 p-3 d-none d-md-block\">\n    <a href=\"/docs/\" class=\"d-flex align-items-center pb-3 mb-3 link-body-emphasis text-decoration-none border-bottom\">\n        <span class=\"fs-5 fw-semibold\">Docs</span>\n    </a>\n    <ul class=\"list-unstyled ps-0\">\n    {% for docsGroup in docsTOC %}\n        <li class=\"mb-2\">\n            <div class=\"mb-2\">{{docsGroup.groupName}}</div>\n            <ul class=\"btn-toggle-nav list-unstyled fw-normal pb-1 ms-2 small\">\n                {% for docPage in docsGroup.pages %}\n                <li class=\"mb-1\">\n                {%- if navItem.dest == page.url -%}\n                <a href=\"{{docPage.url}}\" class=\"link-body-emphasis d-inline-flex text-decoration-none rounded active\" aria-current=\"page\">{{docPage.title}}</a>\n                {% else %}\n                <a href=\"{{docPage.url}}\" class=\"link-body-emphasis d-inline-flex text-decoration-none rounded\">{{docPage.title}}</a>\n                {% endif %}\n                </li>\n                {% endfor %}\n            </ul>\n        </li>\n    {% endfor %}\n    </ul>\n</nav>\n<article class=\"container typechat-prose-content\">\n{{ content | safe }}\n</article>\n</div>\n</main>"
  },
  {
    "path": "site/src/_includes/footer.njk",
    "content": "<footer class=\"container mt-auto d-flex flex-wrap justify-content-between align-items-center py-3 border-top\">\n    <ul class=\"nav mx-auto ml-md-0\">\n        <li class=\"nav-item nav-item mx-1\">\n            <a class=\"nav-link text-body-secondary\" target=\"_blank\" href=\"https://microsoft.github.io/\">© Microsoft</span>\n        </li>\n        <li class=\"nav-item mx-1\">\n            <a class=\"nav-link text-body-secondary\" target=\"_blank\" href=\"https://go.microsoft.com/fwlink/?LinkId=521839\">Privacy</a>\n        </li>\n        <li class=\"nav-item mx-1\">\n            <a class=\"nav-link text-body-secondary\" target=\"_blank\" href=\"https://go.microsoft.com/fwlink/?LinkID=20697\">Terms of Use</a>\n        </li>\n    </ul>\n\n    <ul class=\"nav mx-auto mr-md-0\">\n        <li class=\"nav-item\">\n            <a class=\"nav-link text-body-secondary\" href=\"https://twitter.com/typechat\">\n                Twitter\n            </a>\n        </li>\n        <li class=\"nav-item\">\n            <a class=\"nav-link text-body-secondary\" href=\"https://bsky.app/profile/typechat.bsky.social\">\n                Bluesky\n            </a>\n        </li>\n    </ul>\n</footer>\n"
  },
  {
    "path": "site/src/_includes/header-prologue.njk",
    "content": "<a class=\"skip-to-main\" href=\"#site-content\" tabindex=\"0\">Skip to main content</a>\n<div class=\"d-flex w-100 h-100 p-3 mx-auto flex-column\">\n<header class=\"container d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom\">\n    <a href=\"/\" class=\"d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none\">\n        <!-- <svg class=\"bi me-2\" width=\"40\" height=\"32\">\n            <use xlink:href=\"#bootstrap\"></use>\n        </svg> -->\n        <span class=\"fs-3\" style=\"margin-right: 2rem;\">TypeChat</span>\n    </a>\n\n    <ul class=\"nav nav-underline\">\n        {%- for navItem in headernav -%}\n            {%- if page.url === navItem.dest or (navItem.dest !== \"/\" and page.url.startsWith(navItem.dest)) -%}\n                <li class=\"nav-item\"><a href=\"{{navItem.dest}}\" class=\"nav-link active\" aria-current=\"page\">{{navItem.title}}</a></li>\n            {%- elif navItem.isExternal -%}\n                <li class=\"nav-item\"><a href=\"{{navItem.dest}}\" target=\"_blank\" rel=\"noopener\" class=\"nav-link\">{{navItem.title}}</a></li>\n            {%- else -%}\n                <li class=\"nav-item\"><a href=\"{{navItem.dest}}\" class=\"nav-link\">{{navItem.title}}</a></li>\n            {%- endif -%}\n        {%- endfor -%}\n    </ul>\n</header>\n<div class=\"d-none\" id=\"site-content\"></div>\n"
  },
  {
    "path": "site/src/blog/announcing-typechat-0-1-0.md",
    "content": "---\ntitle: Announcing TypeChat 0.1.0\nlayout: blog\ntags: post\ndate: 2024-03-25\nauthors: [\"Daniel Rosenwasser\"]\n---\n\n# {{title}}\n\n*{{date | formatDate}}{% if authors %} by {{authors | formatList}}{% endif %}*\n\nToday we've released a new version of TypeChat for TypeScript and JavaScript. To get it, you can run\n\n```sh\nnpm install typechat\n```\n\nAs a refresher, TypeChat is an experimental library for getting structured output (like JSON) from AI language models.\nThe way it works is by using types in your programs to guide language models, and then using those same types to ensure that the responses match up with your types.\nWhen they don't, TypeChat can use validation errors to guide language models to repair their responses.\nYou can [read our original announcement blog post](/blog/introducing-typechat/) for more details, but we should be able to catch you up to speed here too.\n\nHere's a few things that are new to TypeChat for TypeScript.\n\n## Pluggable Validators\n\nThe original version of TypeChat actually leveraged the raw contents of a TypeScript schema file.\nIt looked something like this:\n\n```ts\n// Load up the contents of our \"Response\" schema.\nconst schema = fs.readFileSync(path.join(__dirname, \"sentimentSchema.ts\"), \"utf8\");\nconst translator = typechat.createJsonTranslator<SomeType>(model, schema, \"SomeType\");\n\n// Process requests interactively.\ntypechat.processRequests(\"> \", /*inputFile*/ undefined, async (request) => {\n    const response = await translator.translate(request);\n\n    if (response.success) {\n        console.log(`❌ ${response.message}`);\n        return;\n    }\n\n    console.log(\"The request was translated into the following value:\")\n    console.log(response.data);\n});\n```\n\nThis worked, but had a few issues:\n\n1. The schema file had to be self-contained. Everything had to be in the same file for TypeChat.\n1. The schema file also had to be present if you weren't running in-place.\n\n   This often meant copying the schema file along to the output directory if you weren't using something like ts-node, tsx, or tsimp.\n\n1. The schema was fixed. While possible to generate a text schema on the fly, it's an error-prone task.\n\nWhile there are a lot of ergonomic benefits to using a textual TypeScript schema, we explored whether there we could add a bit more flexibility and made a few changes to TypeChat.\n\nThe first is that we've broken out a piece of `TypeChatJsonTranslator` into a more granular concept: a `TypeChatJsonValidator`.\nA `TypeChatJsonValidator` is responsible for generating a string schema representation to guide language models, and to actually make sure the data that comes back matches some type.\nThis means that to construct a `TypeChatJsonTranslator`, you need to make a `TypeChatJsonValidator` first;\nbut it also means that validators are swappable.\nHere's what using that looks like now:\n\n```ts\nimport fs from \"fs\";\nimport path from \"path\";\n\nimport { createLanguageModel, createJsonTranslator } from \"typechat\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\n\nimport { SentimentResponse } from \"./sentimentSchema\";\n\nconst model = createLanguageModel(process.env);\nconst schema = fs.readFileSync(path.join(__dirname, \"sentimentSchema.ts\"), \"utf8\");\nconst validator = createTypeScriptJsonValidator<SentimentResponse>(schema, \"SentimentResponse\");\nconst translator = createJsonTranslator(model, validator);\n\ntranslator.translate(\"hello world!\").then(response => {\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    console.log(`The sentiment is ${response.data.sentiment}`);\n});\n```\n\nNotice that instead of passing the schema into `createJsonTranslator`, we're passing it into `createTypeScriptJsonValidator` which we need to import from `typechat/ts`.\nThe created validator the needs to be passed into `createJsonTranslator`.\n\nFor existing calls to `createJsonTranslator`, you'll probably see a message like:\n\n> TS2554: Expected 2 arguments, but got 3.\n\nyou'll need to drop the name of the type, and substitute the argument schema with a validator.\nHere's the effective diff:\n\n```diff\n  import { createJsonTranslator, createLanguageModel, processRequests } from \"typechat\";\n+ import { createTypeScriptJsonValidator } from \"typechat/ts\";\n  import { SentimentResponse } from \"./sentimentSchema\";\n\n  // ...\n\n- const translator = createJsonTranslator<SentimentResponse>(model, schema, \"Sentiment\")\n+ const validator = createTypeScriptJsonValidator<SentimentResponse>(schema, \"SentimentResponse\");\n+ const translator = createJsonTranslator(model, validator);\n\n  // ...\n```\n\n## Zod Validators\n\nThe second change builds on pluggable validators: TypeChat makes it possible to create validators from Zod schemas.\n[If you're not familiar with Zod](https://zod.dev/), it's a popular library in the TypeScript/JavaScript ecosystem for validating data.\nOne strength of this library is that as Zod type validator objects are constructed, static types can be derived from them.\nBut for TypeChat, its more notable strength is the ability to construct schemas *dynamically*.\n\nTo use a Zod-based schema, we first need to create a few Zod type validator objects and create an object defining all the ones we intend to use.\n\n```ts\n// sentimentSchema.ts\n\nimport { z } from \"zod\";\n\nexport const SentimentResponse = z.object({\n    sentiment: z.enum([\"negative\", \"neutral\", \"positive\"])\n        .describe(\"The sentiment of the text\")\n});\n\n// Maps the property \"SentimentResponse\" to the above Zod validator.\nexport const SentimentSchema = {\n    SentimentResponse\n};\n```\n\nNote that while TypeScript schema files can use raw JavaScript/TypeScript `// comment` syntax, TypeChat generates comments from Zod based on [whatever we pass in to `.describe()` calls](https://zod.dev/?id=describe).\n\nNext, we have to construct a TypeChat Zod validator.\nWe pass in the object map of types, and specify which type we want the model to conform to:\n\n```ts\n// main.ts\n\nimport { createJsonTranslator, createLanguageModel } from \"typechat\";\nimport { createZodJsonValidator } from \"typechat/zod\";\n\nimport { SentimentSchema } from \"./sentimentSchema\";\n\nconst model = createLanguageModel(process.env);\nconst validator = createZodJsonValidator(SentimentSchema, \"SentimentResponse\");\nconst translator = createJsonTranslator(model, validator);\n\ntranslator.translate(\"hello world!\").then(response => {\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    console.log(`The sentiment is ${response.data.sentiment}`);\n});\n```\n\nThat's it!\n\nWhile using a Zod schema has lots of advantages, you may still prefer the ergonomics of writing a plain TypeScript schema.\nEither option works.\n\nFor more information, [see the changes on GitHub](https://github.com/microsoft/TypeChat/pull/147).\n\n## A `validateInstance` Hook\n\nAnother new addition to TypeChat is the `validateInstance` hook on `TypeChatJsonTranslator`s.\nIt allows you to tack on an extra level of validation beyond what the internal validator will perform.\n\n```ts\nimport { createJsonTranslator, error, success } from \"typechat\";\n\n// ...\n\nconst translator = createJsonTranslator(model, validator);\ntranslator.validateInstance = summary => {\n    for (const person of summary.people) {\n        if (person.age < 0) {\n            return error(\n                `'{person.name}' has a negative age, that doesn't make sense.`\n            )\n        }\n    }\n    return success(summary)\n}\n```\n\nIf `validateInstance` returns a TypeChat `Error`, then the translator will use the message to repair the AI response.\n\nYou can [see specifics of this change on GitHub](https://github.com/microsoft/TypeChat/pull/115).\n\n## Other Changes\n\nOther changes to be aware of are:\n\n* `TypeChatJsonProgram` and related functions, such as `createModuleTextFromProgram`, `evaluateJsonProgram`, and `createProgramTranslator` all live in `typechat/ts` ([see the pull request for these `TypeChatJsonProgram` changes](https://github.com/microsoft/TypeChat/pull/147)).\n* The `processRequests` function for creating a REPL-like prompt now lives in `typechat/interactive` ([see the pull request for these `processRequests` changes](https://github.com/microsoft/TypeChat/pull/221)).\n\n## What's Next?\n\nWe'll be trying to improve TypeChat based on the feedback we receive.\nWe're also working to bring TypeChat to other language ecosystems, like Python and .NET, so keep an eye out for that in the near future.\n\nGive TypeChat a try and let us know what you think over [on GitHub](https://github.com/microsoft/TypeChat/), where you can file an issue or post a topic in our discussion forum!\n\n"
  },
  {
    "path": "site/src/blog/index.njk",
    "content": "---\ntitle: Blog\n---\n\n<!DOCTYPE html>\n<html lang=\"en\" class=\"h-100\">\n\n<head>\n    <meta charset='utf-8'>\n    <meta http-equiv='X-UA-Compatible' content='IE=edge'>\n    {% set latestPost = collections.post[0] %}\n    {% if latestPost %}\n    <meta charset=\"utf-8\">\n    <title>Redirecting to {{latestPost.data.title}}</title>\n    <meta http-equiv=\"refresh\" content=\"0; URL={{latestPost.url}}\">\n    <link rel=\"canonical\" href=\"{{latestPost.url}}\">\n    {%endif%}\n    <meta name='viewport' content='width=device-width, initial-scale=1'>\n    <!-- <link rel='stylesheet' type='text/css' media='screen' href='main.css'> -->\n    <link href=\"/css/5.3.0_dist_css_bootstrap.min.css\" rel=\"stylesheet\">\n    <link href=\"/css/styles.css\" rel=\"stylesheet\">\n</head>\n\n<body class=\"h-100\">\n{% include \"header-prologue.njk\" %}\n\n{% include \"footer.njk\" %}\n</div>\n</body>\n\n</html>\n{#-\n<main class=\"container text-center\">\n<h1 class=\"pb-2\">Blog Posts</h1>\n<ul class=\"list-unstyled ps-0\">\n{% for post in collections.post %}\n<li class=\"mb-2\">\n    <li class=\"mb-1\">\n    <a href=\"{{post.url}}\" class=\"link-dark link-underline-opacity-25 link-underline-opacity-50-hover\">\n        <div class=\"fs-5\">{{post.data.title}}</div>\n        <div>({{post.date | formatDate }}{% if post.data.authors %} by {{post.data.authors | formatList}} {% endif %})</div>\n    </a>\n    </li>\n    </ul>\n</li>\n{% endfor %}\n</ul>\n</main>\n-#}\n"
  },
  {
    "path": "site/src/blog/introducing-typechat.md",
    "content": "---\ntitle: Introducing TypeChat\nlayout: blog\ntags: post\ndate: 2023-07-20\nauthors: [\"Anders Hejlsberg\", \"Steve Lucco\", \"Daniel Rosenwasser\", \"Pierce Boggan\", \"Umesh Madan\", \"Mike Hopcroft\", \"Gayathri Chandrasekaran\"]\n---\n\n# {{title}}\n\n*{{date | formatDate}}{% if authors %} by {{authors | formatList}}{% endif %}*\n\nIn the last few months, we've seen a rush of excitement around the newest wave of large language models.\nWhile chat assistants have been the most direct application, there's a big question around how to best integrate these models into existing app interfaces.\n\nIn other words, how do we *augment* traditional UI with natural language interfaces?\nHow do we use AI to take a user request and turn it into something our apps can operate on?\nAnd how do we make sure our apps are safe, and doing work that developers and users alike can trust?\n\nToday we're releasing **TypeChat**, an experimental library that aims to answer these questions.\nIt uses the type definitions in your codebase to retrieve structured AI responses that are type-safe.\n\nYou can get up and running with TypeChat today by running\n\n```\nnpm install typechat\n```\n\nand hooking it up with any language model to work with your app.\n\nBut let's first quickly explore why TypeChat exists.\n\n## Pampering and Parsing\n\nThe current wave of LLMs default to conversational *natural* language — languages that humans communicate in like English.\nParsing natural language is an extremely difficult task, no matter how much you pamper a prompt with rules like \"respond in the form a bulleted list\".\nNatural language might have structure, but it's hard for typical software to reconstruct it from raw text.\n\nSurprisingly, we can ask LLMs to respond in the form of JSON, and they generally respond with something sensible!\n\n> **User:**\n> \n> Translate the following request into JSON.\n> \n> > Could I get a blueberry muffin and a grande latte?\n> \n> Respond only in JSON like the following:\n> \n> ```json\n> {\n>     \"items\": [\n>         { \"name\": \"croissant\", \"quantity\": 2  },\n>         { \"name\": \"latte\", \"quantity\": 1, \"size\": \"tall\" }\n>     ]\n> }\n> ```\n> \n> **ChatBot:**\n> \n> ```json\n> {\n>     \"items\": [\n>         {\n>             \"name\": \"blueberry muffin\",\n>             \"quantity\": 1\n>         },\n>         {\n>             \"name\": \"latte\",\n>             \"quantity\": 1,\n>             \"size\": \"grande\"\n>         }\n>     ]\n> }\n> ```\n\nThis is good — though this example shows the best-case response.\nWhile examples can help guide structure, they don't define what an AI should return extensively, and they don't provide anything we can validate against.\n\n## Just Add Types!\n\nLuckily **types** do precisely that.\nWhat we've found is that because LLMs have seen so many type definitions in the wild, types also act as a great guide for how an AI should respond.\nBecause we're typically working with JSON — *JavaScript* Object Notation — and because it's is very near and dear to our hearts, we've been using TypeScript types in our prompts.\n\n> **User:**\n> \n> Translate the following request into JSON.\n> \n> > Could I get a blueberry muffin and a grande latte?\n> \n> Respond only in JSON that satisfies the `Response` type:\n> \n> ```ts\n> type Response = {\n>     items: Item[];\n> };\n> \n> type Item = {\n>     name: string;\n>     quantity: number;\n>     size?: string;\n>     notes?: string;\n> }\n> ```\n>\n> **ChatBot:**\n>\n> ```json\n> {\n>   \"items\": [\n>     {\n>       \"name\": \"blueberry muffin\",\n>       \"quantity\": 1\n>     },\n>     {\n>       \"name\": \"latte\",\n>       \"quantity\": 1,\n>       \"size\": \"grande\"\n>     }\n>   ]\n> }\n> ```\n\nThis is pretty great!\nTypeScript has shown that it's well-suited to precisely describe JSON.\nBut what happens when a language model stumbles and makes up a response that doesn't conform to our types?\n\nWell because these types are valid TypeScript code, we can validate the response against them using the TypeScript compiler itself!\nIn fact, the error feedback from the compiler can even be used to guide repairs.\nWhen put together, we can get a robust process for getting well-typed responses that our apps can further massage, validate with a user, etc.\n\nIn other words, **types are all you need**.\n\n## Enter TypeChat\n\nThe technique of combining a human prompt and a \"response schema\" is not necessarily unique — but it is promising.\nAnd as we've focused on translating user intent to structured data, we've found that TypeScript is very well-suited for the task.\nWe've grown more confident with this approach, and in order to prove it out, we're releasing a library called TypeChat to help make it easier to use in your apps.\n[TypeChat is already on npm](https://npmjs.com/package/typechat) if you want to try it now, and provides tools for prompt prototyping, schema validation, repair, and more.\n\nHere's the basic code to hook TypeChat up to an LLM and decide if a sentence is negative, neutral, or positive.\n\n```ts\n// ./src/sentimentSchema.ts\n\n// The following is a schema definition for determining the sentiment of a some user input.\n\nexport interface SentimentResponse {\n    /** The sentiment of the text. */\n    sentiment: \"negative\" | \"neutral\" | \"positive\";\n}\n```\n\n```ts\n// ./src/main.ts\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport dotenv from \"dotenv\";\nimport * as typechat from \"typechat\";\nimport { SentimentResponse } from \"./sentimentSchema\";\n\n// Load environment variables.\ndotenv.config({ path: path.join(__dirname, \"../.env\") });\n\n// Create a language model based on the environment variables.\nconst model = typechat.createLanguageModel(process.env);\n\n// Load up the contents of our \"Response\" schema.\nconst schema = fs.readFileSync(path.join(__dirname, \"sentimentSchema.ts\"), \"utf8\");\nconst translator = typechat.createJsonTranslator<SentimentResponse>(model, schema, \"SentimentResponse\");\n\n// Process requests interactively.\ntypechat.processRequests(\"😀> \", /*inputFile*/ undefined, async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    console.log(`The sentiment is ${response.data.sentiment}`);\n});\n```\n\nTypeChat can be used in a number of different ways.\nThe way we've discussed here so far is all about using a \"data schema\" to turn some user intent into a structured response;\nhowever, TypeChat also makes it possible to use an \"API schema\" to construct basic programs.\nWe have some [docs](/docs/) and [examples](/docs/examples/) to get a sense of the different ways you can use TypeChat.\n\n## Open and Pluggable\n\nFirst of all, TypeChat is open-source.\nWe're MIT-licensed and you can [find us on GitHub](https://github.com/Microsoft/TypeChat) where we're eager to hear your thoughts, share our ideas, and build with you.\n\nSecond, TypeChat is built in a way that is meant to be model-neutral.\nWhile we have some very basic integration with the OpenAI API and the Azure OpenAI service for convenience, this approach should work for any chat completion-style API that you want to use — though note that at the moment, TypeChat works best with models that have been trained on both prose and code.\n\n## Try It Today!\n\nWe'd love to know if TypeChat is something that's useful and interests you!\nAs we mentioned, we'll be welcoming you on [GitHub](https://github.com/Microsoft/TypeChat) if you have any question, suggestions, and more.\n\nHappy Hacking!\n"
  },
  {
    "path": "site/src/css/noscript-styles.css",
    "content": ".typechat-hero button {\n    display: none !important;\n}\n\n.typechat-docs-smol-nav {\n    display: none !important;\n}\n"
  },
  {
    "path": "site/src/css/styles.css",
    "content": ".skip-to-main {\n    position: absolute;\n    opacity: 0;\n    z-index: -999999999;\n    margin: 0 auto;\n    padding: 2rem 0;\n    background-color: #000;\n    color: #fff;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 60px;\n    text-align: center;\n}\n\n.skip-to-main:focus {\n    opacity: 1;\n    z-index: 999999999;\n}\n\n.with-sidebar {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 2rem;\n}\n\n.with-sidebar > :first-child {\n    flex-basis: 140px;\n    flex-grow: 1;\n}\n\n.with-sidebar > :last-child {\n    flex-basis: 0;\n    flex-grow: 999;\n    min-inline-size: 50%;\n}\n\n:root {\n    --typechat-monospace: Consolas, Menlo, Monaco, Roboto, monospace;\n    --typechat-inline-code-color: #a10615;\n    --typechat-rounding-radius: 0.5rem;\n}\n\n.typechat-cap-content-width {\n    max-width: 1000px;\n}\n\n.typechat-hero .typechat-code-copy {\n    background-color: #212529;\n    color: #fff;\n    font-style: var(--typechat-monospace);\n    border-radius: var(--typechat-rounding-radius);\n    padding: 0.75rem 1rem;\n    text-align: center;\n}\n\n.typechat-hero .typechat-code-copy code {\n    background-color: inherit;\n    color: inherit;\n}\n\n.typechat-hero .typechat-code-copy button {\n    height: 100%;\n    width: fit-content;\n    right: 0;\n    top: 0;\n    border: none;\n    /* border-radius: var(--typechat-rounding-radius) 0 0 var(--typechat-rounding-radius); */\n    border-radius: 0 var(--typechat-rounding-radius) var(--typechat-rounding-radius) 0;\n}\n\n.typechat-prose-content :is(pre, blockquote) {\n    padding: 1rem;\n    border-radius: 0.5rem;\n    box-shadow: 0px 2px 5px #666;\n}\n\n.typechat-prose-content blockquote {\n    background-color: #f5f8fa;\n}\n\n.typechat-prose-content pre:focus {\n    outline: 3px solid #0078d4;\n    outline-offset: 2px;\n}\n\n.typechat-prose-content code {\n    font-family: var(--typechat-monospace);\n    color: var(--typechat-inline-code-color);\n    font-size: inherit;\n}\n\n.typechat-prose-content :not(pre) code {\n    word-break: break-all;\n}\n\n.typechat-prose-content blockquote > *:last-child {\n    margin-bottom: 0;\n}\n\n.typechat-prose-content :not(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {\n    margin-top: 1rem;\n}\n\n.typechat-prose-content table {\n    margin-bottom: 1rem;\n    border-collapse: collapse;\n}\n\n.typechat-prose-content td, th {\n    border: 1px solid #666;\n    border-left: 0;\n    border-right: 0;\n    padding: 0.5rem;\n}\n\n.typechat-prose-content th {\n    border-top: 0;\n}\n"
  },
  {
    "path": "site/src/docs/examples.md",
    "content": "---\nlayout: doc-page\ntitle: Examples\n---\n\nTo see TypeChat in action, check out the examples found in [`/typescript/examples`](https://github.com/microsoft/TypeChat/tree/main/typescript/examples).\n\nEach example shows how TypeChat handles natural language input, and maps to validated JSON as output. Most example inputs run on both GPT 3.5 and GPT 4.\nWe are working to reproduce outputs with other models.\nGenerally, models trained on both code and natural language text have high accuracy.\n\nWe recommend reading each example in the following order.\n\n| Name | Description |\n| ---- | ----------- |\n| [Sentiment](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/sentiment) | A sentiment classifier which categorizes user input as negative, neutral, or positive. This is TypeChat's \"hello world!\" |\n| [Coffee Shop](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/coffeeShop) | An intelligent agent for a coffee shop. This sample translates user intent into a list of coffee order items.\n| [Calendar](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/calendar) | An intelligent scheduler. This sample translates user intent into a sequence of actions to modify a calendar. |\n| [Restaurant](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/restaurant) | An intelligent agent for taking orders at a restaurant. Similar to the coffee shop example, but uses a more complex schema to model more complex linguistic input. The prose files illustrate the line between simpler and more advanced language models in handling compound sentences, distractions, and corrections. This example also shows how we can use TypeScript to provide a user intent summary. |\n| [Math](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/math) | Translate calculations into simple programs given an API that can perform the 4 basic mathematical operators. This example highlights TypeChat's program generation capabilities. |\n| [Music](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/music) | An app for playing music, creating playlists, etc. on Spotify through natural language. Each user intent is translated into a series of actions in JSON which correspond to a simple dataflow program, where each step can consume data produced from previous step. |\n\n## Step 1: Configure development environment\n\n### Option 1: Local Machine\n\nYou can experiment with these TypeChat examples on your local machine with just Node.js.\n\nEnsure [Node.js (18.16.0 LTS or newer)](https://nodejs.org/en) or newer is installed.\n\n```\ngit clone https://github.com/microsoft/TypeChat\ncd TypeChat/typescript\nnpm install\n```\n\n### Option 2: GitHub Codespaces\n\nGitHub Codespaces enables you to try TypeChat quickly in a development environment hosted in the cloud.\n\nOn the TypeChat repository page:\n\n1. Click the green button labeled `<> Code`\n2. Select the `Codespaces` tab.\n3. Click the green `Create codespace` button.\n\n<details>\n<summary>If this is your first time creating a codespace, read this.</summary>\n\nIf this is your first time creating a codespace on this repository, GitHub will take a moment to create a dev container image for your session.\nOnce the image has been created, the browser will load Visual Studio Code in a developer environment automatically configured with the necessary prerequisites, TypeChat cloned, and packages installed.\n\nRemember that you are running in the cloud, so all changes you make to the source tree must be committed and pushed before destroying the codespace. GitHub accounts are usually configured to automatically delete codespaces that have been inactive for 30 days.\n\nFor more information, see the [GitHub Codespaces Overview](https://docs.github.com/en/codespaces/overview)\n</details>\n\n## Step 2: Build TypeChat Examples\n\nBuild TypeChat and the examples by running the following command in the repository root:\n\n```\nnpm run build-all\n```\n\n## Step 3: Configure environment variables\n\nCurrently, the examples are running on OpenAI or Azure OpenAI endpoints.\nTo use an OpenAI endpoint, include the following environment variables:\n\n| Variable | Value |\n|----------|-------|\n| `OPENAI_MODEL`| The OpenAI model name (e.g. `gpt-3.5-turbo` or `gpt-4`) |\n| `OPENAI_API_KEY` | Your OpenAI API key |\n\nTo use an Azure OpenAI endpoint, include the following environment variables:\n\n| Variable | Value |\n|----------|-------|\n| `AZURE_OPENAI_ENDPOINT` | The full URL of the Azure OpenAI REST API (e.g. `https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2023-05-15`) |\n| `AZURE_OPENAI_API_KEY` | Your Azure OpenAI API key |\n\nWe recommend setting environment variables by creating a `.env` file in the root directory of the project that looks like the following:\n\n```\n# For OpenAI\nOPENAI_MODEL=...\nOPENAI_API_KEY=...\n\n# For Azure OpenAI\nAZURE_OPENAI_ENDPOINT=...\nAZURE_OPENAI_API_KEY=...\n```\n\n## Step 4: Run the examples\n\nExamples can be found in the `typescript/examples` directory.\n\nTo run an example interactively, type `node ./dist/main.js` from the example's directory and enter requests when prompted. Type `quit` or `exit` to end the session. You can also open in VS Code the selected example's directory and press <kbd>F5</kbd> to launch it in debug mode.\n\nNote that there are various sample \"prose\" files (e.g. `input.txt`) provided in each `src` directory that can give a sense of what you can run.\n\nTo run an example with one of these input files, run `node ./dist/main.js <input-file-path>`.\nFor example, in the `coffeeShop` directory, you can run:\n\n```\nnode ./dist/main.js ./dist/input.txt\n```\n"
  },
  {
    "path": "site/src/docs/faq.md",
    "content": "---\nlayout: doc-page\ntitle: Frequently Asked Questions (FAQ)\n---\n\n### What is TypeChat?\n\nTypeChat makes it easy to build natural language interfaces using types. These types represent your application's domain, such as an interface for representing user sentiment or types for actions a user could take in a music app.\n\nAfter defining your types, TypeChat takes care of the rest by:\n\n1. Constructing a prompt to the LLM using types.\n2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction.\n3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent.\n\nTypes are all you need!\n\n### Why is TypeChat useful?\n\nIf you want to add a natural language interface to an app – for example, let’s assume a coffee ordering app that let’s you speak out your order – then you eventually need to translate a request into something precise and concrete that your app can process for tasks like billing, ordering, etc. \n\nTypeChat lets you push on large language models to do this work without having to worry about how to parse out its response or dealing with “imaginary” items and tasks. This is because everything must be structured JSON that is validated against your types. \n\n### What are the benefits of using TypeChat?\n\nTypeChat was created with the purpose of increasing safety in natural language interfaces. \n\nWe believe TypeChat has three key primary benefits when working with large language models:\n\n1. Accurate: Large language models do a great job matching user intent to scoped types. TypeChat's validation and repair cleans up the rest!\n2. Approachable: No more prompt engineering! Types are all you need. You probably have them already lying around.\n3. Safety: Types constrain domain and model uncertainty. Repeating back the instance confirms that it aligns with user intent before taking action.\n\n### How does TypeChat work? How does TypeChat relate to TypeScript?\n\nTypeChat uses TypeScript types as the “specification language” for responses from language models. The approach for sending a request is minimal that includes the user's inputs, your types, and text requesting the model to translate the user input into a JSON object in alignment with the TypeScript types.\n\nOnce receiving an AI response, TypeChat uses the TypeScript compiler API under the hood to validate the data based on the types you provided. If validation fails, TypeChat sends a repair prompt back to the model that includes diagnostics from the TypeScript compiler. That’s how TypeChat can guarantee that your response is correctly typed.\n\n### How reliable is TypeChat?\n\nTypeChat is _very_ reliable. Large language models have proven they do well when constrained with unambiguous, formal descriptions of possible outputs. They also perform better the more training they have received. TypeScript is the type system for the world's most popular programming language, and JSON is the interchange format for the most popular programming language. As a result, the model has extreme familiarity with both, increasing accuracy. TypeChat purposely creates the prompt compact, and TypeScript can be as much as 5x more concise than a JSON Schema equivalent. Most of the time, the model responds well to the prompt from TypeChat, and sends back a valid instance. TypeChat adds validation, and (if that fails) self-repairing logic to obtain a valid response from the model using diagnostics from the TypeScript compiler. Finally, TypeChat keeps the user in the loop for final confirmation of intent, serving as a final safety mechanism.\n\n### What languages does TypeChat support?\n\nCurrently TypeChat is being developed just for TypeScript and JavaScript. Developers interested in support for additional languages can engage in discussion on TypeChat's repo in GitHub Discussions."
  },
  {
    "path": "site/src/docs/index.njk",
    "content": "---\ntitle: Docs\n---\n\n<!DOCTYPE html>\n<html lang=\"en\" class=\"h-100\">\n<head>\n    <meta charset='utf-8'>\n    <meta http-equiv='X-UA-Compatible' content='IE=edge'>\n    {% set firstDoc = docsTOC[0].pages[0] %}\n    {% if firstDoc %}\n    <meta charset=\"utf-8\">\n    <title>Redirecting to {{firstDoc.title}}</title>\n    <meta http-equiv=\"refresh\" content=\"0; URL={{firstDoc.url}}\">\n    <link rel=\"canonical\" href=\"{{firstDoc.url}}\">\n    {%endif%}\n    <meta name='viewport' content='width=device-width, initial-scale=1'>\n    <!-- <link rel='stylesheet' type='text/css' media='screen' href='main.css'> -->\n    <link href=\"/css/5.3.0_dist_css_bootstrap.min.css\" rel=\"stylesheet\">\n    <link href=\"/css/styles.css\" rel=\"stylesheet\">\n</head>\n\n<body class=\"h-100\">\n{% include \"header-prologue.njk\" %}\n\n{% include \"footer.njk\" %}\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "site/src/docs/introduction.md",
    "content": "---\nlayout: doc-page\ntitle: Introduction\n---\n\nLarge language models (or LLMs) can give us surprisingly rich answers to requests.\nMaybe you've tried chatting with a language model and been delighted at how effective it is to brainstorm with it.\n\n> **User:**\n>\n> It's rainy in Seattle (surprise!). Got 3 quick suggestions for what I should do this weekend? Keep it short and direct.\n> \n> **ChatGPT:**\n>\n> 1. Visit the Seattle Art Museum (SAM) for a day of artistic inspiration and exploration.\n> 2. Enjoy a cozy afternoon at a local coffee shop, sipping your favorite brew and diving into a good book.\n> 3. Embrace the rainy weather by taking a scenic drive to Snoqualmie Falls and witness the majestic waterfall in all its splendor.\n\nThese models are great chat assistants on their own, but can they be used to add natural language interfaces to an application?\nThe answer as many of us have seen is \"yes - sort of\", and it really depends on how we ask the model.\n\n## Adding Structure\n\nRemember, by default these models respond in *natural language* which is how we read and write today;\nbut traditional programs are kind of bad at working with these answers.\nMost software expects to work off of a level of structure over bytes, characters, JSON blobs, etc. &mdash; not natural languages like English.\nWhen we don't have structure, we have to parse out what we're interested in, and it might not always be practical.\n\nOne way to add some structure and make parsing easier might be to ask for a list of answers:\n\n> **User:**\n>\n> Provide 3 suggestions for specific places to go to in Seattle on a rainy day.\n> Respond in the form of a bulleted list.\n> Be succinct.\n> \n> **ChatGPT:**\n>\n> - Seattle Art Museum: Explore the extensive art collection and enjoy the indoor exhibits.\n> - Chihuly Garden and Glass: Admire the stunning glass artwork of Dale Chihuly in a covered garden and gallery.\n> - Pacific Science Center: Engage in interactive exhibits, planetarium shows, and IMAX films for a day of educational fun.\n\nMaybe a bulleted list is enough to go off of, but how would we break this data up?\nStrip the leading bullet points and split on `\": \"` to separate the venue from its description?\nWhat if a venue has a `\": \"` in its name?\nIt turns out that we're still left with problems parsing out natural language.\n\nNow what if we asked the model for a much more structured format like JSON?\nAs a matter of fact, many language models are great at generating JSON!\n\n> **User:**\n> Provide 3 suggestions for specific places to go to in Seattle on a rainy day. Respond in the form of JSON. The JSON should have the following format\n> \n> ```json\n> [\n>     { \"venue\": \"...\", \"description\": \"...\" },\n>     { \"venue\": \"...\", \"description\": \"...\" }\n> ]\n> ```\n\n> **ChatGPT:**\n> ```json\n> [\n>     {\n>         \"venue\": \"Seattle Art Museum\",\n>         \"description\": \"The Seattle Art Museum (SAM) is a world-class museum with a diverse collection of art from around the globe. It offers a great indoor activity for a rainy day, allowing you to explore various art exhibitions and displays, including contemporary art, African and Oceanic art, and European painting and sculpture.\"\n>     },\n>     {\n>         \"venue\": \"Chihuly Garden and Glass\",\n>         \"description\": \"Chihuly Garden and Glass is an extraordinary exhibition showcasing the breathtaking glass artwork of Dale Chihuly. Located at the Seattle Center, this indoor attraction offers a mesmerizing experience as you wander through vibrant glass sculptures and installations. The beautiful interplay of light and color will captivate your senses, making it a perfect rainy day destination.\"\n>     },\n>     {\n>         \"venue\": \"Museum of Pop Culture\",\n>         \"description\": \"The Museum of Pop Culture (MoPOP) is a vibrant museum dedicated to contemporary pop culture, music, science fiction, and more. It features engaging exhibits that celebrate music icons, explore science fiction themes, and delve into the world of video games. Whether you're a fan of music, movies, or pop culture in general, MoPOP offers an immersive and entertaining experience, making it an ideal choice on a rainy day.\"\n>     }\n> ]\n> ```\n\nHere we asked a model to respond directly with JSON code and provided a sort of \"template\" or example response to guide its answer.\nAs you can see, it worked pretty well!\n\nExample responses are great, but we wondered if there was another approach.\nIn our case, we asked whether we could simply guide a model with TypeScript types.\n\n> **User:**\n> Provide 3 suggestions for specific places to go to in Seattle on a rainy day. Respond strictly with JSON. The JSON should be compatible with the TypeScript type `Response` from the following:\n>\n> ```ts\n> interface Response {\n>      data: Array<{ venue: string, description: string }>;\n> }\n> ```\n>\n> **ChatGPT:**\n> ```json\n> {\n>   \"data\": [\n>     {\n>       \"venue\": \"Museum of Pop Culture\",\n>       \"description\": \"Explore the fascinating world of popular culture through interactive exhibits, music memorabilia, and multimedia displays at the Museum of Pop Culture.\"\n>     },\n>     {\n>       \"venue\": \"Seattle Art Museum\",\n>       \"description\": \"Indulge in the diverse collection of artworks from around the world at the Seattle Art Museum, featuring both contemporary and classical pieces.\"\n>     },\n>     {\n>       \"venue\": \"Pike Place Market\",\n>       \"description\": \"Enjoy the vibrant atmosphere of Pike Place Market, where you can wander through covered stalls offering fresh produce, crafts, and delicious food from local vendors.\"\n>     }\n>   ]\n> }\n> ```\n\nThis works fairly well!\nAnd despite being conceptually simple, it's a very powerful way to bridge the gap between natural language and something our applications can work with.\nBut we wanted to make it as easy as possible to use this pattern, and brush away some of the sharp edges of language model responses.\n\nThis is the key idea behind TypeChat.\nBy just giving a model with a schema (some types) and a request, we can integrate natural language into an application and work through well-typed structured data.\n\n## Introducing TypeChat\n\nTypeChat makes it easy to build natural language interfaces using types.\n\nSimply define types that represent the intents supported in your NL application. That could be as simple as an interface for categorizing sentiment or more complex examples like types for a shopping cart or music application. For example, to add additional intents to a schema, a developer can add the intents using type composition, such as adding additional types into a discriminated union. To make schemas hierarchical, a developer can use a \"meta-schema\" to choose one or more sub-schemas based on user input.\n\nAfter defining your types, TypeChat takes care of the rest by:\n\n1. Constructing a prompt to the LLM using types.\n2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction.\n3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent.\n\nTypes are all you need!"
  },
  {
    "path": "site/src/docs/python/basic-usage.md",
    "content": "---\nlayout: doc-page\ntitle: Basic Python Usage\n---\n\nTypeChat is currently a small library, so we can get a solid understanding\njust by going through the following example:\n\n```py\nimport asyncio\nimport sys\n\nfrom dotenv import dotenv_values\nfrom typechat import (Failure, TypeChatJsonTranslator, TypeChatValidator,\n                      create_language_model, process_requests)\n\nimport schema as sentiment  # See below for what's in schema.py.\n\nasync def main():\n    env_vals = dotenv_values()\n    model = create_language_model(env_vals)\n    validator = TypeChatValidator(sentiment.Sentiment)\n    translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)\n\n    async def request_handler(message: str):\n        result = await translator.translate(message)\n        if isinstance(result, Failure):\n            print(result.message)\n        else:\n            result = result.value\n            print(f\"The sentiment is {result['sentiment']}\")\n\n    filename = sys.argv[1] if len(sys.argv) == 2 else None\n    await process_requests(\"😀> \", filename, request_handler)\n\nasyncio.run(main())\n```\n\nLet's break it down step-by-step.\n\n## Providing a Model\n\nTypeChat can be used with any language model.\nAs long as you have a class with the following shape...\n\n```py\nclass TypeChatLanguageModel(Protocol):\n\n    async def complete(self, prompt: str | list[PromptSection]) -> Result[str]:\n        \"\"\"\n        Represents a AI language model that can complete prompts.\n\n        TypeChat uses an implementation of this protocol to communicate\n        with an AI service that can translate natural language requests to JSON\n        instances according to a provided schema.\n        The `create_language_model` function can create an instance.\n        \"\"\"\n        ...\n```\n\nthen you should be able to try TypeChat out with such a model.\n\nThe key thing here is providing a `complete` method.\n`complete` is just a function that takes a `string` and eventually returns a\nstring (wrapped in a `Result`) if all goes well.\n\nFor convenience, TypeChat provides two functions out of the box to connect to\nthe OpenAI API and Azure's OpenAI Services.\nYou can call these directly.\n\n```py\ndef create_openai_language_model(\n    api_key: str,\n    model: str,\n    endpoint: str = \"https://api.openai.com/v1/chat/completions\",\n    org: str = \"\"\n):\n    ...\n\ndef create_azure_openai_language_model(api_key: str, endpoint: str): ...\n```\n\nFor even more convenience, TypeChat also provides a function to infer whether\nyou're using OpenAI or Azure OpenAI.\n\n```ts\ndef create_language_model(\n    vals: dict[str, str | None]\n) -> TypeChatLanguageModel: ...\n```\n\nWith `create_language_model`, you can populate your environment variables and\npass them in.\nBased on whether `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` is set, you'll get\na model of the appropriate type.\n\nThe `TypeChatLanguageModel` returned by these functions has a few writable\nattributes you might find useful:\n\n- `max_retry_attempts`\n- `retry_pause_seconds`\n- `timeout_seconds`\n\nThough note that these are unstable.\n\nRegardless of how you decide to construct your model, it is important to avoid committing credentials directly in source.\nOne way to make this work between production and development environments is to use a `.env` file in development, and specify that `.env` in your `.gitignore`.\nYou can use a library like [`python-dotenv`](https://pypi.org/project/python-dotenv/) to help load these up.\n\n```py\nfrom dotenv import load_dotenv\nload_dotenv()\n\n// ...\n\nimport typechat\nmodel = typechat.create_language_model(os.environ)\n```\n\n## Defining and Loading the Schema\n\nTypeChat describes types to language models to help guide their responses.\nTo do so, all we have to do is define either a [`@dataclass`](https://docs.python.org/3/library/dataclasses.html) or a [`TypedDict`](https://typing.readthedocs.io/en/latest/spec/typeddict.html) class to describe the response we're expecting.\nHere's what our schema file `schema.py` look like:\n\n```py\nfrom dataclasses import dataclass\nfrom typing import Literal\n\n@dataclass\nclass Sentiment:\n    \"\"\"\n    The following is a schema definition for determining the sentiment of a some user input.\n    \"\"\"\n\n    sentiment: Literal[\"negative\", \"neutral\", \"positive\"]\n```\n\nHere, we're saying that the `sentiment` attribute has to be one of three possible strings: `negative`, `neutral`, or `positive`.\nWe did this with [the `typing.Literal` hint](https://docs.python.org/3/library/typing.html#typing.Literal).\n\nWe defined `Sentiment` as a `@dataclass` so we could have all of the conveniences of standard Python objects - for example, to access the `sentiment` attribute, we can just write `value.sentiment`.\nIf we declared `Sentiment` as a `TypedDict`, TypeChat would provide us with a `dict`.\nThat would mean that to access the value of `sentiment`, we would have to write `value[\"sentiment\"]`.\n\nNote that while we used [the built-in `typing` module](https://docs.python.org/3/library/typing.html), [`typing_extensions`](https://pypi.org/project/typing-extensions/) is supported as well.\nTypeChat also understands constructs like `Annotated` and `Doc` to add comments to individual attributes.\n\n## Creating a Validator\n\nA validator really has two jobs generating a textual schema for language models, and making sure any data fits a given shape.\nThe built-in validator looks roughly like this:\n\n```py\nclass TypeChatValidator(Generic[T]):\n    \"\"\"\n    Validates an object against a given Python type.\n    \"\"\"\n\n    def __init__(self, py_type: type[T]):\n        \"\"\"\n        Args:\n\n            py_type: The schema type to validate against.\n        \"\"\"\n        ...\n\n    def validate_object(self, obj: object) -> Result[T]:\n        \"\"\"\n        Validates the given Python object according to the associated schema type.\n\n        Returns a `Success[T]` object containing the object if validation was successful.\n        Otherwise, returns a `Failure` object with a `message` property describing the error.\n        \"\"\"\n        ...\n```\n\nTo construct a validator, we just have to pass in the type we defined:\n\n```py\nimport schema as sentiment\nvalidator = TypeChatValidator(sentiment.Sentiment)\n```\n\n## Creating a JSON Translator\n\nA `TypeChatJsonTranslator` brings all these concepts together.\nA translator takes a language model, a validator, and our expected type, and\nprovides a way to translate some user input into objects following our schema.\nTo do so, it crafts a prompt based on the schema, reaches out to the model,\nparses out JSON data, and attempts validation.\nOptionally, it will craft repair prompts and retry if validation fails.\n\n```py\ntranslator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)\n```\n\nWhen we are ready to translate a user request, we can call the `translate`\nmethod.\n\n```ts\ntranslator.translate(\"Hello world! 🙂\");\n```\n\nWe'll come back to this.\n\n## Creating a \"REPL\"\n\nTypeChat exports a `process_requests` function that makes it easy to\nexperiment with TypeChat.\nDepending on its second argument, it either creates an interactive command\nline (if given `None`), or reads lines from the given a file path.\n\n```ts\nasync def request_handler(message: str):\n    ...\n\nfilename = sys.argv[1] if len(sys.argv) == 2 else None\nawait process_requests(\"😀> \", filename, request_handler)\n```\n\n`process_requests` takes 3 things.\nFirst, there's the prompt string - this is what a user will see before their\nown input in interactive scenarios.\nYou can make this playful.\nWe like to use emoji here. 😄\n\nNext, we take a text file name.\nInput strings will be read from this file one line at a time.\nIf the file name was `None`, `process_requests` will work on standard input\nand provide an interactive prompt (assuming `sys.stdin.isatty()` is true).\nBy checking `sys.argv`, our script makes our program interactive unless the\nperson running the program provided an input file as a command line argument\n(e.g. `python ./example.py inputFile.txt`).\n\nFinally, there's the request handler.\nWe'll fill that in next.\n\n## Translating Requests\n\nOur handler receives some user input (the `message` string) each time it's\ncalled.\nIt's time to pass that string into over to our `translator` object.\n\n```ts\nasync def request_handler(message: str):\n    result = await translator.translate(message)\n    if isinstance(result, Failure):\n        print(result.message)\n    else:\n        print(f\"The sentiment is {result.value.sentiment}\")\n```\n\nWe're calling the `translate` method on each string and getting a response.\nIf something goes wrong, TypeChat will retry requests up to a maximum\nspecified by `retry_max_attempts` on our `model`.\nHowever, if the initial request as well as all retries fail, `result` will be\na `typechat.Failure` and we'll be able to grab a `message` explaining what\nwent wrong.\n\nIn the ideal case, `result` will be a `typechat.Success` and we'll be able to\naccess our well-typed `value` property!\nThis will correspond to the type that we passed in when we created our\ntranslator object (i.e. `Sentiment`).\n\nThat's it!\nYou should now have a basic idea of TypeChat's APIs and how to get started\nwith a new project. 🎉\n"
  },
  {
    "path": "site/src/docs/techniques.md",
    "content": "---\nlayout: doc-page\ntitle: Techniques\n---\n\nThis document defines techniques for working with TypeChat.\n\n### Schema Engineering\n\nTypeChat replaces _prompt engineering_ with _schema engineering_: Instead of writing unstructured natural language prompts to describe the format of your desired output, you write TypeScript type definitions. These TypeScript schema aren't necessarily the exact types your application uses to process and store your data. Rather, they're types that bridge between natural language and your application logic by _controlling and constraining_ LLM responses in ways that are meaningful to your application.\n\nTo use an analogy, in the Model-View-ViewModel (MVVM) user interface design pattern, the ViewModel bridges between the user interface and the application logic, but it isn't the model the application uses to process and store information. The schema you design for TypeChat are like the ViewModel, but are perhaps more meaningfully called _Response Models_.\n\nTo maximize success with TypeChat, we recommend the following best practices when defining Response Model types:\n\n* Keep it simple (primitives, arrays, and objects).\n* Only use types that are representable as JSON (i.e. no classes).\n* Make data structures as flat and regular as possible.\n* Include comments on types and properties that describe intent in natural language.\n* Restrict use of generics.\n* Avoid deep inheritance hierarchies.\n* Don't use conditional, mapped, and indexed access types.\n* Allow room for LLMs to color slightly outside the lines (e.g. use `string` instead of literal types).\n* Include an escape hatch to suppress hallucinations.\n\nThe last point merits further elaboration. We've found that when Response Models attempt to fit user requests into narrow schema with no wiggle room, the LLMs are likely to hallucinate answers for user requests that are outside the domain. For example, if you ask your coffee shop bot for \"two tall trees\", given no other option it may well turn that into two tall lattes (without letting you know it did so).\n\nHowever, when you include an _escape hatch_ in the form of an \"unknown\" category in your schema, the LLMs happily route non-domain requests into that bucket. Not only does this greatly suppress hallucinations, it also gives you a convenient way of letting the user know which parts of a request weren't understood. The examples in the TypeChat repo all use this technique."
  },
  {
    "path": "site/src/docs/typescript/basic-usage.md",
    "content": "---\nlayout: doc-page\ntitle: Basic TypeScript Usage\n---\n\nTypeChat is currently a small library, so we can get a solid understanding just by understanding the following example:\n\n```ts\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createJsonTranslator, createLanguageModel } from \"typechat\";\nimport { processRequests } from \"typechat/interactive\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\nimport { SentimentResponse } from \"./sentimentSchema\";\n\n// Create a model.\nconst model = createLanguageModel(process.env);\n\n// Create a validator.\nconst schema = fs.readFileSync(path.join(__dirname, \"sentimentSchema.ts\"), \"utf8\");\nconst validator = createTypeScriptJsonValidator<SentimentResponse>(schema, \"SentimentResponse\");\n\n// Create a translator.\nconst translator = createJsonTranslator(model, validator);\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"😀> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    console.log(`The sentiment is ${response.data.sentiment}`);\n});\n```\n\nLet's break it down step-by-step.\n\n## Providing a Model\n\nTypeChat can be used with any language model.\nAs long as you can construct an object with the following properties:\n\n```ts\nexport interface TypeChatLanguageModel {\n    /**\n     * Optional property that specifies the maximum number of retry attempts (the default is 3).\n     */\n    retryMaxAttempts?: number;\n    /**\n     * Optional property that specifies the delay before retrying in milliseconds (the default is 1000ms).\n     */\n    retryPauseMs?: number;\n    /**\n     * Obtains a completion from the language model for the given prompt.\n     * @param prompt The prompt string.\n     */\n    complete(prompt: string): Promise<Result<string>>;\n}\n```\n\nthen you should be able to try TypeChat out with such a model.\n\nThe key thing here is that only `complete` is required.\n`complete` is just a function that takes a `string` and eventually returns a `string` if all goes well.\n\nFor convenience, TypeChat provides two functions out of the box to connect to the OpenAI API and Azure's OpenAI Services.\nYou can call these directly.\n\n```ts\nexport function createOpenAILanguageModel(apiKey: string, model: string, endPoint? string): TypeChatLanguageModel;\n\nexport function createAzureOpenAILanguageModel(apiKey: string, endPoint: string): TypeChatLanguageModel;\n```\n\nFor even more convenience, TypeChat also provides a function to infer whether you're using OpenAI or Azure OpenAI.\n\n```ts\nexport function createLanguageModel(env: Record<string, string | undefined>): TypeChatLanguageModel\n```\n\nWith `createLanguageModel`, you can populate your environment variables and pass them in.\nBased on whether `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` is set, you'll get a model of the appropriate type.\n\nRegardless, of how you decide to construct your model, it is important to avoid committing credentials directly in source.\nOne way to make this work between production and development environments is to use a `.env` file in development, and specify that `.env` in your `.gitignore`.\nYou can use a library like [`dotenv`](https://www.npmjs.com/package/dotenv) to help load these up.\n\n```ts\nimport dotenv from \"dotenv\";\ndotenv.config(/*...*/);\n\n// ...\n\nimport * as typechat from \"typechat\";\nconst model = typechat.createLanguageModel(process.env);\n```\n\n\n## Defining and Loading the Schema\n\nTypeChat describes types to language models to help guide their responses.\nIn this case, we are using a `TypeScriptJsonValidator` which uses the TypeScript compiler to validate data against a set of types.\nThat means that we'll be writing out the types of the data we expect to get back in a `.ts` file.\nHere's what our schema file `sentimentSchema.ts` look like:\n\n```ts\n// The following is a schema definition for determining the sentiment of a some user input.\n\nexport interface SentimentResponse {\n    sentiment: \"negative\" | \"neutral\" | \"positive\";  // The sentiment of the text\n}\n```\n\nIt also means we will need to manually load up an input `.ts` file verbatim.\n\n```ts\n// Load up the type from our schema.\nimport type { SentimentResponse } from \"./sentimentSchema\";\n\n// Load up the schema file contents.\nconst schema = fs.readFileSync(path.join(__dirname, \"sentimentSchema.ts\"), \"utf8\");\n```\n\nNote: this code assumes a CommonJS module. If you're using ECMAScript modules, you can use [`import.meta.url`](https://nodejs.org/docs/latest-v19.x/api/esm.html#importmetaurl) or via [`import.meta.dirname`](https://nodejs.org/docs/latest-v21.x/api/esm.html#importmetadirname) depending on the version of your runtime.\n\nThis introduces some complications to certain kinds of builds, since our input files need to be treated as local assets.\nOne way to achieve this is to use a runtime or tool like [`ts-node`](https://www.npmjs.com/package/ts-node) to both import the file for its types, as well as read the file contents.\nAnother is to use a utility like [`copyfiles`](https://www.npmjs.com/package/copyfiles) to move specific schema files to the output directory.\nIf you're using a bundler, there might be custom way to import a file as a raw string as well.\nRegardless, [our examples](https://github.com/microsoft/TypeChat/tree/main/typescript/examples) should work with either of the first two options.\n\nAlternatively, if we want, we can build our schema with objects entirely in memory using Zod and a `ZodValidator` which we'll touch on in a moment.\nHere's what our schema would look like if we went down that path.\n\n```ts\nimport { z } from \"zod\";\n\nexport const SentimentResponse = z.object({\n    sentiment: z.enum([\"negative\", \"neutral\", \"positive\"]).describe(\"The sentiment of the text\")\n});\n\nexport const SentimentSchema = {\n    SentimentResponse\n};\n```\n\n## Creating a Validator\n\nA validator really has two jobs generating a textual schema for language models, and making sure any data fits a given shape.\nThe interface looks roughly like this:\n\n```ts\n/**\n * An object that represents a TypeScript schema for JSON objects.\n */\nexport interface TypeChatJsonValidator<T extends object> {\n    /**\n     * Return a string containing TypeScript source code for the validation schema.\n     */\n    getSchemaText(): string;\n    /**\n     * Return the name of the JSON object target type in the schema.\n     */\n    getTypeName(): string;\n    /**\n     * Validates the given JSON object according to the associated TypeScript schema. Returns a\n     * `Success<T>` object containing the JSON object if validation was successful. Otherwise, returns\n     * an `Error` object with a `message` property describing the error.\n     * @param jsonText The JSON object to validate.\n     * @returns The JSON object or an error message.\n     */\n    validate(jsonObject: object): Result<T>;\n}\n```\n\nIn other words, this is just the text of all types, the name of the top-level type to respond with, and a validation function that returns a strongly-typed view of the input if it succeeds.\n\nTypeChat ships with two validators.\n\n### `TypeScriptJsonValidator`\n\nA `TypeScriptJsonValidator` operates off of TypeScript text files.\nTo create one, we have to import `createTypeScriptJsonValidator` out of `typechat/ts`:\n\n```ts\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\n```\n\nWe'll also need to actually import the type from our schema.\n\n```ts\nimport { SentimentResponse } from \"./sentimentSchema\";\n```\n\nWith our schema text and this type, we have enough to create a validator:\n\n```ts\nconst validator = createTypeScriptJsonValidator<SentimentResponse>(schema, \"SentimentResponse\");\n```\n\nWe provided the text of the schema and the name of the type we want returned data to satisfy.\nWe also have to provide the type argument `SentimentResponse` to explain what data shape we expect (though note that this is a bit like a type cast and isn't guaranteed).\n\n### Zod Validators\n\nIf you chose to define your schema with Zod, you can use the `createZodJsonValidator` function:\n\n```ts\nimport { createZodJsonValidator } from \"typechat/zod\";\n```\n\nInstead of a source file, a Zod validator needs a JavaScript object mapping from type names to Zod type objects like `myObj` in the following example:\n\n```ts\nexport const MyType = z.object(/*...*/);\n\nexport const MyOtherType = z.object(/*...*/);\n\nexport let myObj = {\n    MyType,\n    MyOtherType,\n}\n```\n\nFrom above, that was just `SentimentSchema`:\n\n```ts\nexport const SentimentSchema = {\n    SentimentResponse\n};\n```\n\nSo we'll need to import that object...\n\n```ts\nimport { SentimentSchema } from \"./sentimentSchema\";\n```\n\nand provide it, along with our expected type name, to `createZodJsonValidator`:\n\n```ts\nconst validator = createZodJsonValidator(SentimentSchema, \"SentimentResponse\");\n```\n\n## Creating a JSON Translator\n\nA `TypeChatJsonTranslator` brings these together.\n\n```ts\nimport { createJsonTranslator } from \"typechat\";\n```\n\nA translator takes both a model and a validator, and provides a way to translate some user input into objects following our schema.\nTo do so, it crafts a prompt based on the schema, reaches out to the model, parses out JSON data, and attempts validation.\nOptionally, it will craft repair prompts and retry if validation fails.\n\n```ts\nconst translator = createJsonTranslator(model, validator);\n```\n\nWhen we are ready to translate a user request, we can call the `translate` method.\n\n```ts\ntranslator.translate(\"Hello world! 🙂\");\n```\n\nWe'll come back to this.\n\n## Creating the Prompt\n\nTypeChat exports a `processRequests` function that makes it easy to experiment with TypeChat.\nWe need to import it from `typechat/interactive`.\n\n```ts\nimport { processRequests } from \"typechat/interactive\";\n```\n\nIt either creates an interactive command line prompt, or reads lines in from a file.\n\n```ts\ntypechat.processRequests(\"😀> \", process.argv[2], async (request) => {\n    // ...\n});\n```\n\n`processRequests` takes 3 things.\nFirst, there's the prompt prefix - this is what a user will see before their own text in interactive scenarios.\nYou can make this playful.\nWe like to use emoji here. 😄\n\nNext, we take a text file name.\nInput strings will be read from this file on a per-line basis.\nIf the file name was `undefined`, `processRequests` will work on standard input and provide an interactive prompt.\nUsing `process.argv[2]` makes our program interactive by default unless the person running the program provided an input file as a command line argument (e.g. `node ./dist/main.js inputFile.txt`).\n\nFinally, there's the request handler.\nWe'll fill that in next.\n\n## Translating Requests\n\nOur handler receives some user input (the `request` string) each time it's called.\nIt's time to pass that string into over to our `translator` object.\n\n```ts\ntypechat.processRequests(\"😀> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    console.log(`The sentiment is ${response.data.sentiment}`);\n});\n```\n\nWe're calling the `translate` method on each string and getting a response.\nIf something goes wrong, TypeChat will retry requests up to a maximum specified by `retryMaxAttempts` on our `model`.\nHowever, if the initial request as well as all retries fail, `response.success` will be `false` and we'll be able to grab a `message` explaining what went wrong.\n\nIn the ideal case, `response.success` will be `true` and we'll be able to access our well-typed `data` property!\nThis will correspond to the type that we passed in when we created our translator object (i.e. `SentimentResponse`).\n\nThat's it!\nYou should now have a basic idea of TypeChat's APIs and how to get started with a new project. 🎉\n"
  },
  {
    "path": "site/src/index.njk",
    "content": "---\nlayout: base\n---\n\n<main class=\"container d-flex flex-nowrap col-xl-10 col-xxl-8 px-4 py-5 typechat-hero typechat-cap-content-width\">\n    <div class=\"row align-items-center g-lg-6 py-5\">\n        <div class=\"col-lg-6 col-xl-7 text-center text-lg-start\">\n            <h1 class=\"display-4 fw-bold lh-1 text-body-emphasis mb-3\">TypeChat</h1>\n            <p class=\"col-lg-10 fs-4\">TypeChat helps get well-typed responses from language models to build\n                pragmatic natural language interfaces.</p>\n            <p class=\"col-lg-10 fs-4\">All powered through your types.</p>\n        </div>\n        <div class=\"col-md-10 mx-auto col-lg-6 col-xl-5\">\n            <div class=\"p-4 p-md-4 border rounded-3 bg-body-tertiary\">\n                <a href=\"/docs/\" class=\"mb-3 w-100 btn btn-lg btn-primary\">Docs</a>\n                <a href=\"https://github.com/microsoft/TypeChat\" target=\"_blank\" class=\"mb-2 w-100 btn btn-lg btn-primary\">GitHub</a>\n                <hr class=\"mb-4\" />\n                <div class=\"w-100 typechat-code-copy position-relative\">\n                    <button class=\"position-absolute d-none d-sm-block btn btn-dark\" type=\"button\" title=\"Copy 'npm install' command.\" aria-label=title=\"Copy 'npm install' command.\">📋</button>\n                    <code class=\"fs-5\">npm install typechat</code>\n                </div>\n            </div>\n        </div>\n    </div>\n</main>\n"
  },
  {
    "path": "site/src/js/interactivity.js",
    "content": "// @ts-check\n\n{\n    /** @type {any} */\n    let lastTimeout;\n\n    /** @type {HTMLButtonElement | null} */\n    const copyButton = document.querySelector(\".typechat-code-copy button\");\n    copyButton?.addEventListener(\"click\", async () => {\n        clearTimeout(lastTimeout);\n        try {\n            await navigator.clipboard?.writeText(\"npm install typechat\");\n            copyButton.textContent = \"✅\";\n            copyButton.title = copyButton.ariaLabel = \"Command copied.\"\n        }\n        catch {\n            copyButton.textContent = \"❌\";\n            copyButton.title = copyButton.ariaLabel = \"Error copying.\"\n        }\n        lastTimeout = setTimeout(() => {\n            copyButton.textContent = \"📋\";\n            copyButton.title = copyButton.ariaLabel = \"Copy 'npm install' command.\"\n        }, 1500);\n    });\n}\n\n{\n    const selectElements = /** @type {HTMLCollectionOf<HTMLSelectElement>} */ (document.getElementsByClassName(\"nav-on-change\"));\n    for (const select of selectElements) {\n        const change = () => {\n            window.location.pathname = select.value;\n        };\n        select.onchange = change;\n        // if (select.options.length === 1 && window.location.pathname !== select.value) {\n        //     change();\n        // }\n    }\n}\n"
  },
  {
    "path": "typescript/.gitignore",
    "content": "build/\ndist/\nout/\nnode_modules/\n.env\n*.map\n*.out.txt\n*.bat\n\n# Copied at publish time from repo root\nSECURITY.md\n\n# Local development and debugging\n.scratch/\n**/.vscode/*\n**/tsconfig.debug.json\n!**/.vscode/launch.json\n**/build.bat\n\n"
  },
  {
    "path": "typescript/.npmignore",
    "content": "src/\nexamples/\nnode_modules/\n*.map\n*.ts\n!*.d.ts\ntsconfig.json\n.gitignore\n"
  },
  {
    "path": "typescript/LICENSE",
    "content": "    MIT License\n\n    Copyright (c) Microsoft Corporation.\n\n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE\n"
  },
  {
    "path": "typescript/README.md",
    "content": "# TypeChat\n\nTypeChat is a library that makes it easy to build natural language interfaces using types.\n\nBuilding natural language interfaces has traditionally been difficult. These apps often relied on complex decision trees to determine intent and collect the required inputs to take action. Large language models (LLMs) have made this easier by enabling us to take natural language input from a user and match to intent. This has introduced its own challenges including the need to constrain the model's reply for safety, structure responses from the model for further processing, and ensuring that the reply from the model is valid. Prompt engineering aims to solve these problems, but comes with a steep learning curve and increased fragility as the prompt increases in size.\n\nTypeChat replaces _prompt engineering_ with _schema engineering_.\n\nSimply define types that represent the intents supported in your natural language application. That could be as simple as an interface for categorizing sentiment or more complex examples like types for a shopping cart or music application. For example, to add additional intents to a schema, a developer can add additional types into a discriminated union. To make schemas hierarchical, a developer can use a \"meta-schema\" to choose one or more sub-schemas based on user input.\n\nAfter defining your types, TypeChat takes care of the rest by:\n\n1. Constructing a prompt to the LLM using types.\n2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction.\n3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent.\n\nTypes are all you need!\n\n# Getting Started\n\nInstall TypeChat:\n\n```sh\nnpm install typechat\n```\n\nYou can also build TypeChat from source:\n\n```sh\ngit clone https://github.com/microsoft/TypeChat\ncd TypeChat/typescript\nnpm install\nnpm run build\n```\n\nTo see TypeChat in action, we recommend exploring the [TypeChat example projects](https://github.com/microsoft/TypeChat/tree/main/typescript/examples). You can try them on your local machine or in a GitHub Codespace.\n\nTo learn more about TypeChat, visit the [documentation](https://microsoft.github.io/TypeChat) which includes more information on TypeChat and how to get started.\n\n## Contributing\n\nThis project welcomes contributions and suggestions.  Most contributions require you to agree to a\nContributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\nthe rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.\n\nWhen you submit a pull request, a CLA bot will automatically determine whether you need to provide\na CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\nprovided by the bot. You will only need to do this once across all repos using our CLA.\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\nFor more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\ncontact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n\n## Trademarks\n\nThis project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft \ntrademarks or logos is subject to and must follow \n[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).\nUse of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.\nAny use of third-party trademarks or logos are subject to those third-party's policies.\n"
  },
  {
    "path": "typescript/examples/README.md",
    "content": "\nTo see TypeChat in action, check out the examples found in this directory.\n\nEach example shows how TypeChat handles natural language input, and maps to validated JSON as output. Most example inputs run on both GPT 3.5 and GPT 4.\nWe are working to reproduce outputs with other models.\nGenerally, models trained on both code and natural language text have high accuracy.\n\nWe recommend reading each example in the following order.\n\n\n| Name | Description |\n| ---- | ----------- |\n| [Sentiment](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/sentiment) | A sentiment classifier which categorizes user input as negative, neutral, or positive. This is TypeChat's \"hello world!\" |\n| [Coffee Shop](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/coffeeShop) | An intelligent agent for a coffee shop. This sample translates user intent is translated to a list of coffee order items.\n| [Calendar](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/calendar) | An intelligent scheduler. This sample translates user intent into a sequence of actions to modify a calendar. |\n| [Restaurant](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/restaurant) | An intelligent agent for taking orders at a restaurant. Similar to the coffee shop example, but uses a more complex schema to model more complex linguistic input. The prose files illustrate the line between simpler and more advanced language models in handling compound sentences, distractions, and corrections. This example also shows how we can use TypeScript to provide a user intent summary. |\n| [Math](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/math) | Translate calculations into simple programs given an API that can perform the 4 basic mathematical operators. This example highlights TypeChat's program generation capabilities. |\n| [Music](https://github.com/microsoft/TypeChat/tree/main/typescript/examples/music) | An app for playing music, creating playlists, etc. on Spotify through natural language. Each user intent is translated into a series of actions in JSON which correspond to a simple dataflow program, where each step can consume data produced from previous step. |\n\n## Step 1: Configure your development environment\n\n### Option 1: Local Machine\n\nYou can experiment with these TypeChat examples on your local machine with just Node.js.\n\nEnsure [Node.js (18.16.0 LTS or newer)](https://nodejs.org/en) or newer is installed.\n\n```\ngit clone https://github.com/microsoft/TypeChat\ncd TypeChat/typescript\nnpm install\n```\n\n### Option 2: GitHub Codespaces\n\nGitHub Codespaces enables you to try TypeChat quickly in a development environment hosted in the cloud.\n\nOn the TypeChat repository page:\n\n1. Click the green button labeled `<> Code`\n2. Select the `Codespaces` tab.\n3. Click the green `Create codespace` button.\n\n<details>\n<summary>If this is your first time creating a codespace, read this.</summary>\n\nIf this is your first time creating a codespace on this repository, GitHub will take a moment to create a dev container image for your session.\nOnce the image has been created, the browser will load Visual Studio Code in a developer environment automatically configured with the necessary prerequisites, TypeChat cloned, and packages installed.\n\nRemember that you are running in the cloud, so all changes you make to the source tree must be committed and pushed before destroying the codespace. GitHub accounts are usually configured to automatically delete codespaces that have been inactive for 30 days.\n\nFor more information, see the [GitHub Codespaces Overview](https://docs.github.com/en/codespaces/overview)\n</details>\n\n## Step 2: Build TypeChat Samples\n\nBuild TypeChat and the examples by running the following command in the repository root:\n\n```sh\nnpm run build-all\n```\n\n## Step 3: Configure environment variables\n\nCurrently, the examples are running on OpenAI or Azure OpenAI endpoints.\nTo use an OpenAI endpoint, include the following environment variables:\n\n| Variable | Value |\n|----------|-------|\n| `OPENAI_MODEL`| The OpenAI model name (e.g. `gpt-3.5-turbo` or `gpt-4`) |\n| `OPENAI_API_KEY` | Your OpenAI API key  |\n| `OPENAI_ENDPOINT` | OpenAI API Endpoint - *optional*, defaults to `\"https://api.openai.com/v1/chat/completions\"` |\n| `OPENAI_ORGANIZATION` | OpenAI Organization - *optional*, defaults to `\"\"` |\n\nTo use an Azure OpenAI endpoint, include the following environment variables:\n\n| Variable | Value |\n|----------|-------|\n| `AZURE_OPENAI_ENDPOINT` | The full URL of the Azure OpenAI REST API (e.g. `https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2023-05-15`) |\n| `AZURE_OPENAI_API_KEY` | Your Azure OpenAI API key |\n\nWe recommend setting environment variables by creating a `.env` file in the root directory of the project that looks like the following:\n\n```ini\n# For OpenAI\nOPENAI_MODEL=...\nOPENAI_API_KEY=...\n\n# For Azure OpenAI\nAZURE_OPENAI_ENDPOINT=...\nAZURE_OPENAI_API_KEY=...\n```\n\n## Step 4: Run the examples\n\nExamples can be found in the `examples` directory.\n\nTo run an example interactively, type `node ./dist/main.js` from the example's directory and enter requests when prompted. Type `quit` or `exit` to end the session. You can also open in VS Code the selected example's directory and press <kbd>F5</kbd> to launch it in debug mode.\n\nNote that there are various sample \"prose\" files (e.g. `input.txt`) provided in each `src` directory that can give a sense of what you can run.\n\nTo run an example with one of these input files, run `node ./dist/main.js <input-file-path>`.\nFor example, in the `coffeeShop` directory, you can run:\n\n```sh\nnode ./dist/main.js ./dist/input.txt\n```\n"
  },
  {
    "path": "typescript/examples/calendar/.vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"skipFiles\": [\n                \"<node_internals>/**\"\n            ],\n            \"program\": \"${workspaceFolder}/dist/main.js\",\n            \"console\": \"externalTerminal\"\n        }\n    ]\n}"
  },
  {
    "path": "typescript/examples/calendar/README.md",
    "content": "# Calendar\n\nThe Calendar example shows how you can capture user intent as a sequence of actions, such as adding event to a calendar or searching for an event as defined by the [`CalendarActions`](./src/calendarActionsSchema.ts) type.\n\n# Try Calendar\n\nTo run the Calendar example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\n\nFor example, we could use natural language to describe an event coming up soon:\n\n**Input**:\n\n```\n📅> I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024\n```\n\n**Output**:\n\n```json\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"Friday March 15, 2024\",\n        \"timeRange\": {\n          \"startTime\": \"12:00 pm\",\n          \"endTime\": \"2:00 pm\"\n        },\n        \"description\": \"get my tires changed\"\n      }\n    }\n  ]\n}\n```"
  },
  {
    "path": "typescript/examples/calendar/package.json",
    "content": "{\n  \"name\": \"calendar\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/calendar/src/calendarActionsSchema.ts",
    "content": "// The following types define the structure of an object of type CalendarActions that represents a list of requested calendar actions\n\nexport type CalendarActions = {\n    actions: Action[];\n};\n\nexport type Action =\n    | AddEventAction\n    | RemoveEventAction\n    | AddParticipantsAction\n    | ChangeTimeRangeAction\n    | ChangeDescriptionAction\n    | FindEventsAction\n    | UnknownAction;\n\nexport type AddEventAction = {\n    actionType: 'add event';\n    event: Event;\n};\n\nexport type RemoveEventAction = {\n    actionType: 'remove event';\n    eventReference: EventReference;\n};\n\nexport type AddParticipantsAction = {\n    actionType: 'add participants';\n    // event to be augmented; if not specified assume last event discussed\n    eventReference?: EventReference;\n    // new participants (one or more)\n    participants: string[];\n};\n\nexport type ChangeTimeRangeAction = {\n    actionType: 'change time range';\n    // event to be changed\n    eventReference?: EventReference;\n    // new time range for the event\n    timeRange: EventTimeRange;\n};\n\nexport type ChangeDescriptionAction = {\n    actionType: 'change description';\n    // event to be changed\n    eventReference?: EventReference;\n    // new description for the event\n    description: string;\n};\n\nexport type FindEventsAction = {\n    actionType: 'find events';\n    // one or more event properties to use to search for matching events\n    eventReference: EventReference;\n};\n\n// if the user types text that can not easily be understood as a calendar action, this action is used\nexport interface UnknownAction {\n    actionType: 'unknown';\n    // text typed by the user that the system did not understand\n    text: string;\n}\n\nexport type EventTimeRange = {\n    startTime?: string;\n    endTime?: string;\n    duration?: string;\n};\n\nexport type Event = {\n    // date (example: March 22, 2024) or relative date (example: after EventReference) \n    day: string;\n    timeRange: EventTimeRange;\n    description: string;\n    location?: string;\n    // a list of people or named groups like 'team'\n    participants?: string[];\n};\n\n// properties used by the requester in referring to an event\n// these properties are only specified if given directly by the requester\nexport type EventReference = {\n    // date (example: March 22, 2024) or relative date (example: after EventReference)\n    day?: string;\n    // (examples: this month, this week, in the next two days)\n    dayRange?: string;\n    timeRange?: EventTimeRange;\n    description?: string;\n    location?: string;\n    participants?: string[];\n};\n"
  },
  {
    "path": "typescript/examples/calendar/src/expectedOutput.txt",
    "content": "I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"March 15, 2024\",\n        \"timeRange\": {\n          \"startTime\": \"12:00\",\n          \"endTime\": \"2:00\"\n        },\n        \"description\": \"get tires changed\"\n      }\n    }\n  ]\n}\nSearch for any meetings with Gavin this week\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"find events\",\n      \"eventReference\": {\n        \"dayRange\": \"this week\",\n        \"participants\": [\n          \"Gavin\"\n        ]\n      }\n    }\n  ]\n}\nSet up an event for friday named Jeffs pizza party at 6pm\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"friday\",\n        \"timeRange\": {\n          \"startTime\": \"6pm\"\n        },\n        \"description\": \"Jeffs pizza party\"\n      }\n    }\n  ]\n}\nPlease add Jennifer to the scrum next Thursday\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add participants\",\n      \"eventReference\": {\n        \"day\": \"next Thursday\",\n        \"description\": \"scrum\"\n      },\n      \"participants\": [\n        \"Jennifer\"\n      ]\n    }\n  ]\n}\nWill you please add an appointment with Jerri Skinner at 9 am?  I need it to last 2 hours\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"today\",\n        \"timeRange\": {\n          \"startTime\": \"9 am\",\n          \"duration\": \"2 hours\"\n        },\n        \"description\": \"appointment with Jerri Skinner\"\n      }\n    }\n  ]\n}\nDo I have any plan with Rosy this month?\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"find events\",\n      \"eventReference\": {\n        \"dayRange\": \"this month\",\n        \"participants\": [\n          \"Rosy\"\n        ]\n      }\n    }\n  ]\n}\nI need to add a meeting with my boss on Monday at 10am. Also make sure to schedule and appointment with Sally, May, and Boris tomorrow at 3pm. Now just add to it Jesse and Abby and make it last ninety minutes\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"Monday\",\n        \"timeRange\": {\n          \"startTime\": \"10am\"\n        },\n        \"description\": \"meeting with boss\"\n      }\n    },\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"tomorrow\",\n        \"timeRange\": {\n          \"startTime\": \"3pm\"\n        },\n        \"description\": \"appointment with Sally, May, and Boris\",\n        \"participants\": [\n          \"Sally\",\n          \"May\",\n          \"Boris\"\n        ]\n      }\n    },\n    {\n      \"actionType\": \"add participants\",\n      \"participants\": [\n        \"Jesse\",\n        \"Abby\"\n      ]\n    },\n    {\n      \"actionType\": \"change time range\",\n      \"timeRange\": {\n        \"duration\": \"90 minutes\"\n      }\n    }\n  ]\n}\nAdd meeting with team today at 2\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"today\",\n        \"timeRange\": {\n          \"startTime\": \"2:00\",\n          \"duration\": \"1 hour\"\n        },\n        \"description\": \"meeting with team\",\n        \"participants\": [\n          \"team\"\n        ]\n      }\n    }\n  ]\n}\ncan you record lunch with Luis at 12pm on Friday and also add Isobel to the Wednesday ping pong game at 4pm\nError: JSON instance does not match schema\nType 'null' is not assignable to type 'string | undefined'.\nType 'null' is not assignable to type 'string | undefined'.\nType 'null' is not assignable to type 'string[] | undefined'.\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"Friday\",\n        \"timeRange\": {\n          \"startTime\": \"12pm\",\n          \"duration\": \"1h\"\n        },\n        \"description\": \"lunch with Luis\",\n        \"participants\": [\n          \"Luis\"\n        ]\n      }\n    },\n    {\n      \"actionType\": \"add participants\",\n      \"eventReference\": {\n        \"day\": \"Wednesday\",\n        \"timeRange\": {\n          \"startTime\": \"4pm\",\n          \"duration\": \"1h\"\n        },\n        \"description\": \"ping pong game\"\n      },\n      \"participants\": [\n        \"Isobel\"\n      ]\n    }\n  ]\n}\nI said I'd meet with Jenny this afternoon at 2pm and after that I need to go to the dry cleaner and then the soccer game.  Leave an hour for each of those starting at 3:30\nValid instance:\n{\n  \"actions\": [\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"today\",\n        \"timeRange\": {\n          \"startTime\": \"2:00 pm\",\n          \"duration\": \"1 hour\"\n        },\n        \"description\": \"meeting with Jenny\",\n        \"participants\": [\n          \"Jenny\"\n        ]\n      }\n    },\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"today\",\n        \"timeRange\": {\n          \"startTime\": \"3:30 pm\",\n          \"duration\": \"1 hour\"\n        },\n        \"description\": \"dry cleaner\"\n      }\n    },\n    {\n      \"actionType\": \"add event\",\n      \"event\": {\n        \"day\": \"today\",\n        \"timeRange\": {\n          \"startTime\": \"4:30 pm\",\n          \"duration\": \"1 hour\"\n        },\n        \"description\": \"soccer game\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "typescript/examples/calendar/src/input.txt",
    "content": "I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024\nSearch for any meetings with Gavin this week\nSet up an event for friday named Jeffs pizza party at 6pm\nPlease add Jennifer to the scrum next Thursday\nWill you please add an appointment with Jerri Skinner at 9 am?  I need it to last 2 hours\nDo I have any plan with Rosy this month?\nI need to add a meeting with my boss on Monday at 10am. Also make sure to schedule and appointment with Sally, May, and Boris tomorrow at 3pm. Now just add to it Jesse and Abby and make it last ninety minutes\nAdd meeting with team today at 2\ncan you record lunch with Luis at 12pm on Friday and also add Isobel to the Wednesday ping pong game at 4pm\nI said I'd meet with Jenny this afternoon at 2pm and after that I need to go to the dry cleaner and then the soccer game.  Leave an hour for each of those starting at 3:30\n"
  },
  {
    "path": "typescript/examples/calendar/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createJsonTranslator, createLanguageModel } from \"typechat\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\nimport { processRequests } from \"typechat/interactive\";\nimport { CalendarActions } from './calendarActionsSchema';\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst schema = fs.readFileSync(path.join(__dirname, \"calendarActionsSchema.ts\"), \"utf8\");\nconst validator = createTypeScriptJsonValidator<CalendarActions>(schema, \"CalendarActions\");\nconst translator = createJsonTranslator(model, validator);\n//translator.stripNulls = true;\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"📅> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    const calendarActions = response.data;\n    console.log(JSON.stringify(calendarActions, undefined, 2));\n    if (calendarActions.actions.some(item => item.actionType === \"unknown\")) {\n        console.log(\"I didn't understand the following:\");\n        for (const action of calendarActions.actions) {\n            if (action.actionType === \"unknown\") console.log(action.text);\n        }\n        return;\n    }\n});\n"
  },
  {
    "path": "typescript/examples/calendar/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/coffeeShop/.vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"skipFiles\": [\n                \"<node_internals>/**\"\n            ],\n            \"program\": \"${workspaceFolder}/dist/main.js\",\n            \"console\": \"externalTerminal\"\n        }\n    ]\n}"
  },
  {
    "path": "typescript/examples/coffeeShop/README.md",
    "content": "# Coffee Shop\n\nThe Coffee Shop example shows how to capture user intent as a set of \"nouns\".\nIn this case, the nouns are items in a coffee order, where valid items are defined starting from the [`Cart`](./src/coffeeShopSchema.ts) type.\nThis example also uses the [`UnknownText`](./src/coffeeShopSchema.ts) type as a way to capture user input that doesn't match to an existing type in [`Cart`](./src/coffeeShopSchema.ts).\n\n# Try Coffee Shop\n\nTo run the Coffee Shop example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`src/input.txt`](./src/input.txt) and [`src/input2.txt`](./src/input2.txt).\n\nFor example, we could use natural language to describe our coffee shop order:\n\n**Input**:\n\n```\n☕> we'd like a cappuccino with a pack of sugar\n```\n\n**Output**:\n\n```json\n{\n  \"items\": [\n    {\n      \"type\": \"lineitem\",\n      \"product\": {\n        \"type\": \"LatteDrinks\",\n        \"name\": \"cappuccino\",\n        \"options\": [\n          {\n            \"type\": \"Sweeteners\",\n            \"name\": \"sugar\",\n            \"optionQuantity\": \"regular\"\n          }\n        ]\n      },\n      \"quantity\": 1\n    }\n  ]\n}\n```"
  },
  {
    "path": "typescript/examples/coffeeShop/package.json",
    "content": "{\n  \"name\": \"coffeeshop\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/coffeeShop/src/coffeeShopSchema.ts",
    "content": "// The following is a schema definition for ordering lattes.\n\nexport interface Cart {\n    items: (LineItem | UnknownText)[];\n}\n\n// Use this type for order items that match nothing else\nexport interface UnknownText {\n    type: \"unknown\",\n    text: string; // The text that wasn't understood\n}\n\nexport interface LineItem {\n    type: \"lineitem\",\n    product: Product;\n    quantity: number;\n}\n\nexport type Product = BakeryProducts | LatteDrinks | EspressoDrinks | CoffeeDrinks;\n\nexport interface BakeryProducts {\n    type: \"BakeryProducts\";\n    name: \"apple bran muffin\" | \"blueberry muffin\" | \"lemon poppyseed muffin\" | \"bagel\";\n    options: (BakeryOptions | BakeryPreparations)[];\n}\n\nexport interface BakeryOptions {\n    type: \"BakeryOptions\";\n    name: \"butter\" | \"strawberry jam\" | \"cream cheese\";\n    optionQuantity?: OptionQuantity;\n}\n\nexport interface BakeryPreparations {\n    type: \"BakeryPreparations\";\n    name: \"warmed\" | \"cut in half\";\n}\n\nexport interface LatteDrinks {\n    type: \"LatteDrinks\";\n    name: \"cappuccino\" | \"flat white\" | \"latte\" | \"latte macchiato\" | \"mocha\" | \"chai latte\";\n    temperature?: CoffeeTemperature;\n    size?: CoffeeSize;  // The default is \"grande\"\n    options?: (Milks | Sweeteners | Syrups | Toppings | Caffeines | LattePreparations)[];\n}\n\nexport interface EspressoDrinks {\n    type: \"EspressoDrinks\";\n    name: \"espresso\" | \"lungo\" | \"ristretto\" | \"macchiato\";\n    temperature?: CoffeeTemperature;\n    size?: EspressoSize;  // The default is \"doppio\"\n    options?: (Creamers | Sweeteners | Syrups | Toppings | Caffeines | LattePreparations)[];\n}\n\nexport interface CoffeeDrinks {\n    type: \"CoffeeDrinks\";\n    name: \"americano\" | \"coffee\";\n    temperature?: CoffeeTemperature;\n    size?: CoffeeSize;  // The default is \"grande\"\n    options?: (Creamers | Sweeteners | Syrups | Toppings | Caffeines | LattePreparations)[];\n}\n\nexport interface Syrups {\n    type: \"Syrups\";\n    name: \"almond syrup\" | \"buttered rum syrup\" | \"caramel syrup\" | \"cinnamon syrup\" | \"hazelnut syrup\" |\n        \"orange syrup\" | \"peppermint syrup\" | \"raspberry syrup\" | \"toffee syrup\" | \"vanilla syrup\";\n    optionQuantity?: OptionQuantity;\n}\n\nexport interface Caffeines {\n    type: \"Caffeines\";\n    name: \"regular\" | \"two thirds caf\" | \"half caf\" | \"one third caf\" | \"decaf\";\n}\n\nexport interface Milks {\n    type: \"Milks\";\n    name: \"whole milk\" | \"two percent milk\" | \"nonfat milk\" | \"coconut milk\" | \"soy milk\" | \"almond milk\" | \"oat milk\";\n}\n\nexport interface Creamers {\n    type: \"Creamers\";\n    name: \"whole milk creamer\" | \"two percent milk creamer\" | \"one percent milk creamer\" | \"nonfat milk creamer\" |\n        \"coconut milk creamer\" | \"soy milk creamer\" | \"almond milk creamer\" | \"oat milk creamer\" | \"half and half\" |\n        \"heavy cream\";\n}\n\nexport interface Toppings {\n    type: \"Toppings\";\n    name: \"cinnamon\" | \"foam\" | \"ice\" | \"nutmeg\" | \"whipped cream\" | \"water\";\n    optionQuantity?: OptionQuantity;\n}\n\nexport interface LattePreparations {\n    type: \"LattePreparations\";\n    name: \"for here cup\" | \"lid\" | \"with room\" | \"to go\" | \"dry\" | \"wet\";\n}\n\nexport interface Sweeteners {\n    type: \"Sweeteners\";\n    name: \"equal\" | \"honey\" | \"splenda\" | \"sugar\" | \"sugar in the raw\" | \"sweet n low\" | \"espresso shot\";\n    optionQuantity?: OptionQuantity;\n}\n\nexport type CoffeeTemperature = \"hot\" | \"extra hot\" | \"warm\" | \"iced\";\n\nexport type CoffeeSize = \"short\" | \"tall\" | \"grande\" | \"venti\";\n\nexport type EspressoSize = \"solo\" | \"doppio\" | \"triple\" | \"quad\";\n\nexport type OptionQuantity = \"no\" | \"light\" | \"regular\" | \"extra\" | number;\n"
  },
  {
    "path": "typescript/examples/coffeeShop/src/input.txt",
    "content": "i'd like a latte that's it\ni'll have a dark roast coffee thank you\nget me a coffee please\ncould i please get two mochas that's all\nwe need twenty five flat whites and that'll do it\nhow about a tall cappuccino\ni'd like a venti iced latte\ni'd like a iced venti latte\ni'd like a venti latte iced\ni'd like a latte iced venti\nwe'll also have a short tall latte\ni wanna latte macchiato with vanilla\nhow about a peppermint latte\nmay i also get a decaf soy vanilla syrup caramel latte with sugar and foam\ni want a latte with peppermint syrup with peppermint syrup\ni'd like a decaf half caf latte\ncan I get a skim soy latte\ni'd like a light nutmeg espresso that's it\ncan i have an cappuccino no foam\ncan i have an espresso with no nutmeg\nwe want a light whipped no foam mocha with extra hazelnut and cinnamon\ni'd like a latte cut in half\ni'd like a strawberry latte\ni want a five pump caramel flat white\ni want a flat white with five pumps of caramel syrup\ni want a two pump peppermint three squirt raspberry skinny vanilla latte with a pump of caramel and two sugars\ni want a latte cappuccino espresso and an apple muffin\ni'd like a tall decaf latte iced a grande cappuccino double espresso and a warmed poppyseed muffin sliced in half\nwe'd like a latte with soy and a coffee with soy\ni want a latte latte macchiato and a chai latte\nwe'd like a cappuccino with two pumps of vanilla\nmake that cappuccino with three pumps of vanilla\nwe'd like a cappuccino with a pack of sugar\nmake that cappuccino with two packs of sugar\nwe'd like a cappuccino with a pack of sugar\nmake that with two packs of sugar\ni'd like a flat white with two equal\nadd three equal to the flat white\ni'd like a flat white with two equal\ntwo tall lattes. the first one with no foam. the second one with whole milk.\ntwo tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande.\nun petit cafe\nen lille kaffe\na raspberry latte\na strawberry latte\nroses are red\ntwo lawnmowers, a grande latte and a tall tree\n"
  },
  {
    "path": "typescript/examples/coffeeShop/src/input2.txt",
    "content": "two tall lattes. the first one with no foam. the second one with whole milk.\ntwo tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande.\nun petit cafe\nen lille kaffe\na raspberry latte\na strawberry latte\nroses are red\ntwo lawnmowers, a grande latte and a tall tree\n"
  },
  {
    "path": "typescript/examples/coffeeShop/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createJsonTranslator, createLanguageModel } from \"typechat\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\nimport { processRequests } from \"typechat/interactive\";\nimport { Cart } from \"./coffeeShopSchema\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst schema = fs.readFileSync(path.join(__dirname, \"coffeeShopSchema.ts\"), \"utf8\");\nconst validator = createTypeScriptJsonValidator<Cart>(schema, \"Cart\");\nconst translator = createJsonTranslator(model, validator);\n\nfunction processOrder(cart: Cart) {\n    // Process the items in the cart\n    void cart;\n}\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"☕> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    const cart = response.data;\n    console.log(JSON.stringify(cart, undefined, 2));\n    if (cart.items.some(item => item.type === \"unknown\")) {\n        console.log(\"I didn't understand the following:\");\n        for (const item of cart.items) {\n            if (item.type === \"unknown\") console.log(item.text);\n        }\n        return;\n    }\n    processOrder(cart);\n    console.log(\"Success!\");\n});\n"
  },
  {
    "path": "typescript/examples/coffeeShop/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/coffeeShop-zod/README.md",
    "content": "# Coffee Shop\n\nThe Coffee Shop example shows how to capture user intent as a set of \"nouns\".\nIn this case, the nouns are items in a coffee order, where valid items are defined starting from the [`Cart`](./src/coffeeShopSchema.ts) type.\nThis example also uses the [`UnknownText`](./src/coffeeShopSchema.ts) type as a way to capture user input that doesn't match to an existing type in [`Cart`](./src/coffeeShopSchema.ts).\n\n# Try Coffee Shop\n\nTo run the Coffee Shop example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`src/input.txt`](./src/input.txt) and [`src/input2.txt`](./src/input2.txt).\n\nFor example, we could use natural language to describe our coffee shop order:\n\n**Input**:\n\n```\n☕> we'd like a cappuccino with a pack of sugar\n```\n\n**Output**:\n\n```json\n{\n  \"items\": [\n    {\n      \"type\": \"lineitem\",\n      \"product\": {\n        \"type\": \"LatteDrinks\",\n        \"name\": \"cappuccino\",\n        \"options\": [\n          {\n            \"type\": \"Sweeteners\",\n            \"name\": \"sugar\",\n            \"optionQuantity\": \"regular\"\n          }\n        ]\n      },\n      \"quantity\": 1\n    }\n  ]\n}\n```"
  },
  {
    "path": "typescript/examples/coffeeShop-zod/package.json",
    "content": "{\n  \"name\": \"coffeeshop-zod\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"zod\": \"^3.22.4\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\",\n    \"typescript\": \"^5.3.3\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/coffeeShop-zod/src/coffeeShopSchema.ts",
    "content": "import { z } from \"zod\";\n\nexport const OptionQuantity = z.union([z.literal('no'), z.literal('light'), z.literal('regular'), z.literal('extra'), z.number()]);\n\nexport const BakeryOptions = z.object({\n    type: z.literal('BakeryOptions'),\n    name: z.enum(['butter', 'strawberry jam', 'cream cheese']),\n    optionQuantity: OptionQuantity.optional()\n});\n\nexport const BakeryPreparations = z.object({\n    type: z.literal('BakeryPreparations'),\n    name: z.enum(['warmed', 'cut in half'])\n});\n\nexport const BakeryProducts = z.object({\n    type: z.literal('BakeryProducts'),\n    name: z.enum(['apple bran muffin', 'blueberry muffin', 'lemon poppyseed muffin', 'bagel']),\n    options: z.discriminatedUnion(\"type\", [BakeryOptions, BakeryPreparations]).array()\n})\n\nexport const CoffeeTemperature = z.enum(['hot', 'extra hot', 'warm', 'iced']);\n\nexport const CoffeeSize = z.enum(['short', 'tall', 'grande', 'venti']);\n\nexport const Milks = z.object({\n    type: z.literal('Milks'),\n    name: z.enum(['whole milk', 'two percent milk', 'nonfat milk', 'coconut milk', 'soy milk', 'almond milk', 'oat milk'])\n})\n\nexport const Sweeteners = z.object({\n    type: z.literal('Sweeteners'),\n    name: z.enum(['equal', 'honey', 'splenda', 'sugar', 'sugar in the raw', 'sweet n low', 'espresso shot']),\n    optionQuantity: OptionQuantity.optional()\n});\n\nexport const Syrups = z.object({\n    type: z.literal('Syrups'),\n    name: z.enum(['almond syrup', 'buttered rum syrup', 'caramel syrup', 'cinnamon syrup', 'hazelnut syrup',\n        'orange syrup', 'peppermint syrup', 'raspberry syrup', 'toffee syrup', 'vanilla syrup']),\n    optionQuantity: OptionQuantity.optional()\n});\n\nexport const Toppings = z.object({\n    type: z.literal('Toppings'),\n    name: z.enum(['cinnamon', 'foam', 'ice', 'nutmeg', 'whipped cream', 'water']),\n    optionQuantity: OptionQuantity.optional()\n});\n\nexport const Caffeines = z.object({\n    type: z.literal('Caffeines'),\n    name: z.enum(['regular', 'two thirds caf', 'half caf', 'one third caf', 'decaf'])\n});\n\nexport const LattePreparations = z.object({\n    type: z.literal('LattePreparations'),\n    name: z.enum(['for here cup', 'lid', 'with room', 'to go', 'dry', 'wet'])\n});\n\nexport const LatteDrinks = z.object({\n    type: z.literal('LatteDrinks'),\n    name: z.enum(['cappuccino', 'flat white', 'latte', 'latte macchiato', 'mocha', 'chai latte']),\n    temperature: CoffeeTemperature.optional(),\n    size: CoffeeSize.describe(\"The default is 'grande'\"),\n    options: z.discriminatedUnion(\"type\", [Milks, Sweeteners, Syrups, Toppings, Caffeines, LattePreparations]).array().optional(),\n});\n\nexport const EspressoSize = z.enum(['solo', 'doppio', 'triple', 'quad']);\n\nexport const Creamers = z.object({\n    type: z.literal('Creamers'),\n    name: z.enum(['whole milk creamer', 'two percent milk creamer', 'one percent milk creamer', 'nonfat milk creamer',\n        'coconut milk creamer', 'soy milk creamer', 'almond milk creamer', 'oat milk creamer', 'half and half', 'heavy cream'])\n});\n\nexport const EspressoDrinks = z.object({\n    type: z.literal('EspressoDrinks'),\n    name: z.enum(['espresso', 'lungo', 'ristretto', 'macchiato']),\n    temperature: CoffeeTemperature.optional(),\n    size: EspressoSize.optional().describe(\"The default is 'doppio'\"),\n    options: z.discriminatedUnion(\"type\", [Creamers, Sweeteners, Syrups, Toppings, Caffeines, LattePreparations]).array().optional()\n});\n\nexport const CoffeeDrinks = z.object({\n    type: z.literal('CoffeeDrinks'),\n    name: z.enum(['americano', 'coffee']),\n    temperature: CoffeeTemperature.optional(),\n    size: CoffeeSize.optional().describe(\"The default is 'grande'\"),\n    options: z.discriminatedUnion(\"type\", [Creamers, Sweeteners, Syrups, Toppings, Caffeines, LattePreparations]).array().optional()\n});\n\nexport const Product = z.discriminatedUnion(\"type\", [BakeryProducts, LatteDrinks, EspressoDrinks, CoffeeDrinks]);\n\nexport const LineItem = z.object({\n    type: z.literal('lineitem'),\n    product: Product,\n    quantity: z.number()\n});\n\nexport const UnknownText = z.object({\n    type: z.literal('unknown'),\n    text: z.string().describe(\"The text that wasn't understood\")\n});\n\nexport const Cart = z.object({\n    items: z.discriminatedUnion(\"type\", [LineItem, UnknownText]).array()\n});\n\nexport const CoffeeShopSchema = {\n    Cart: Cart.describe(\"A schema definition for ordering coffee and bakery products\"),\n    UnknownText: UnknownText.describe(\"Use this type for order items that match nothing else\"),\n    LineItem,\n    Product,\n    BakeryProducts,\n    BakeryOptions,\n    BakeryPreparations,\n    LatteDrinks,\n    EspressoDrinks,\n    CoffeeDrinks,\n    Syrups,\n    Caffeines,\n    Milks,\n    Creamers,\n    Toppings,\n    LattePreparations,\n    Sweeteners,\n    CoffeeTemperature,\n    CoffeeSize,\n    EspressoSize,\n    OptionQuantity\n};\n"
  },
  {
    "path": "typescript/examples/coffeeShop-zod/src/input.txt",
    "content": "i'd like a latte that's it\ni'll have a dark roast coffee thank you\nget me a coffee please\ncould i please get two mochas that's all\nwe need twenty five flat whites and that'll do it\nhow about a tall cappuccino\ni'd like a venti iced latte\ni'd like a iced venti latte\ni'd like a venti latte iced\ni'd like a latte iced venti\nwe'll also have a short tall latte\ni wanna latte macchiato with vanilla\nhow about a peppermint latte\nmay i also get a decaf soy vanilla syrup caramel latte with sugar and foam\ni want a latte with peppermint syrup with peppermint syrup\ni'd like a decaf half caf latte\ncan I get a skim soy latte\ni'd like a light nutmeg espresso that's it\ncan i have an cappuccino no foam\ncan i have an espresso with no nutmeg\nwe want a light whipped no foam mocha with extra hazelnut and cinnamon\ni'd like a latte cut in half\ni'd like a strawberry latte\ni want a five pump caramel flat white\ni want a flat white with five pumps of caramel syrup\ni want a two pump peppermint three squirt raspberry skinny vanilla latte with a pump of caramel and two sugars\ni want a latte cappuccino espresso and an apple muffin\ni'd like a tall decaf latte iced a grande cappuccino double espresso and a warmed poppyseed muffin sliced in half\nwe'd like a latte with soy and a coffee with soy\ni want a latte latte macchiato and a chai latte\nwe'd like a cappuccino with two pumps of vanilla\nmake that cappuccino with three pumps of vanilla\nwe'd like a cappuccino with a pack of sugar\nmake that cappuccino with two packs of sugar\nwe'd like a cappuccino with a pack of sugar\nmake that with two packs of sugar\ni'd like a flat white with two equal\nadd three equal to the flat white\ni'd like a flat white with two equal\ntwo tall lattes. the first one with no foam. the second one with whole milk.\ntwo tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande.\nun petit cafe\nen lille kaffe\na raspberry latte\na strawberry latte\nroses are red\ntwo lawnmowers, a grande latte and a tall tree\n"
  },
  {
    "path": "typescript/examples/coffeeShop-zod/src/input2.txt",
    "content": "two tall lattes. the first one with no foam. the second one with whole milk.\ntwo tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande.\nun petit cafe\nen lille kaffe\na raspberry latte\na strawberry latte\nroses are red\ntwo lawnmowers, a grande latte and a tall tree\n"
  },
  {
    "path": "typescript/examples/coffeeShop-zod/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport { createJsonTranslator, createLanguageModel } from \"typechat\";\nimport { createZodJsonValidator } from \"typechat/zod\";\nimport { processRequests } from \"typechat/interactive\";\nimport { z } from \"zod\";\nimport { CoffeeShopSchema } from \"./coffeeShopSchema\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst validator = createZodJsonValidator(CoffeeShopSchema, \"Cart\");\nconst translator = createJsonTranslator(model, validator);\n\nfunction processOrder(cart: z.TypeOf<typeof CoffeeShopSchema.Cart>) {\n    // Process the items in the cart\n    void cart;\n}\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"☕> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    const cart = response.data;\n    console.log(JSON.stringify(cart, undefined, 2));\n    if (cart.items.some(item => item.type === \"unknown\")) {\n        console.log(\"I didn't understand the following:\");\n        for (const item of cart.items) {\n            if (item.type === \"unknown\") console.log(item.text);\n        }\n        return;\n    }\n    processOrder(cart);\n    console.log(\"Success!\");\n});\n"
  },
  {
    "path": "typescript/examples/coffeeShop-zod/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/crossword/README.md",
    "content": "# Crossword\n\nThe Crossword example shows how to include an image in a multimodal prompt and use the image to answer a user's question. The responses follow the  [`CrosswordActions`](./src/crosswordSchema.ts) type.\n\n## Target models\n\nThis example explores multi-modal input. Torun this, you will need a model that accepts images as input. The example has beeentested with **gpt-4-vision** and **gpt-4-omni** models.\n\n# Try Crossword\nTo run the Crossword example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\n\nFor example, given the following input statement:\n\n**Input**:\n```\n🏁> What is the clue for 61 across\n```\n\n**Output**:\n```\n\"Monogram in French fashion\"\n```"
  },
  {
    "path": "typescript/examples/crossword/package.json",
    "content": "{\n  \"name\": \"crossword\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt src/**/*.jpeg dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/crossword/src/crosswordSchema.ts",
    "content": "// The following is a schema definition for determining the sentiment of a some user input.\n\nexport type GetClueText = {\n    actionName: \"getClueText\";\n    parameters: {\n        clueNumber: number;\n        clueDirection: \"across\" | \"down\";\n        value: string;\n    };\n};\n\n// This gives the answer for the requested crossword clue\nexport type GetAnswerValue = {\n    actionName: \"getAnswerValue\";\n    parameters: {\n        proposedAnswer: string;\n        clueNumber: number;\n        clueDirection: \"across\" | \"down\";\n    };\n};\n\nexport type UnknownAction = {\n    actionName: \"unknown\";\n    parameters: {\n        // text typed by the user that the system did not understand\n        text: string;\n    };\n};\n\nexport type CrosswordActions =\n    | GetClueText\n    | GetAnswerValue\n    | UnknownAction;"
  },
  {
    "path": "typescript/examples/crossword/src/input.txt",
    "content": "What is the clue for 1 down\nGive me a hint for solving 4 down"
  },
  {
    "path": "typescript/examples/crossword/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createLanguageModel } from \"typechat\";\nimport { processRequests } from \"typechat/interactive\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\nimport { CrosswordActions } from \"./crosswordSchema\";\nimport { createCrosswordActionTranslator } from \"./translator\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst schema = fs.readFileSync(path.join(__dirname, \"crosswordSchema.ts\"), \"utf8\");\n\nconst rawImage = fs.readFileSync(path.join(__dirname, \"puzzleScreenshot.jpeg\"),\"base64\");\nconst screenshot = `data:image/jpeg;base64,${rawImage}`;\n\nconst validator = createTypeScriptJsonValidator<CrosswordActions>(schema, \"CrosswordActions\");\nconst translator = createCrosswordActionTranslator(model, validator, screenshot);\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"🏁> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n\n    console.log(JSON.stringify(response.data));\n});\n"
  },
  {
    "path": "typescript/examples/crossword/src/translator.ts",
    "content": "import {\n  TypeChatLanguageModel,\n  createJsonTranslator,\n  TypeChatJsonTranslator,\n  MultimodalPromptContent,\n  PromptContent,\n} from \"typechat\";\nimport { TypeScriptJsonValidator } from \"typechat/ts\";\n\nexport function createCrosswordActionTranslator<T extends object>(\n  model: TypeChatLanguageModel,\n  validator: TypeScriptJsonValidator<T>,\n  crosswordImage: string\n): TypeChatJsonTranslator<T> {\n  const _imageContent = crosswordImage;\n\n  const _translator = createJsonTranslator(model, validator);\n  _translator.createRequestPrompt = createRequestPrompt\n\n  return _translator;\n\n  function createRequestPrompt(request: string): PromptContent {\n    const screenshotSection = getScreenshotPromptSection(_imageContent);\n    const contentSections = [\n      {\n        type: \"text\",\n        text: \"You are a virtual assistant that can help users to complete requests by interacting with the UI of a webpage.\",\n      },\n      ...screenshotSection,\n      {\n        type: \"text\",\n        text: `\n                Use the layout information provided to answer user queries. \n                The responses should be translated into JSON objects of type ${_translator.validator.getTypeName()} using the typescript schema below:\n                \n                '''\n                ${_translator.validator.getSchemaText()}\n                '''\n            `,\n      },\n      {\n        type: \"text\",\n        text: `\n                The following is a user request:\n                '''\n                ${request}\n                '''\n                The following is the assistant's response translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:   \n            `,\n      },\n    ] as MultimodalPromptContent[];\n\n    return contentSections;\n  }\n\n  function getScreenshotPromptSection(screenshot: string | undefined) {\n    let screenshotSection = [];\n    if (screenshot) {\n      screenshotSection.push({\n        type: \"text\",\n        text: \"Here is a screenshot of the currently visible webpage\",\n      });\n\n      screenshotSection.push({\n        type: \"image_url\",\n        image_url: {\n          url: screenshot,\n          detail: \"high\"\n        },\n      });\n\n      screenshotSection.push({\n        type: \"text\",\n        text: `Use the top left corner as coordinate 0,0 and draw a virtual grid of 1x1 pixels, \n               where x values increase for each pixel as you go from left to right, and y values increase \n               as you go from top to bottom. \n            `,\n      });\n    }\n    return screenshotSection;\n  }\n}\n"
  },
  {
    "path": "typescript/examples/crossword/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/healthData/README.md",
    "content": "# Health Data Agent\n\nThis example requires GPT-4.\n\nDemonstrates a ***strongly typed*** chat: a natural language interface for entering health information. You work with a *health data agent* to interactively enter your medications or conditions.\n\nThe Health Data Agent shows how strongly typed **agents with history** could interact with a user to collect information needed for one or more data types (\"form filling\").\n\n## Target models\n\nFor best and consistent results, use **gpt-4**.\n\n## Try the Health Data Agent\n\nTo run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n## Usage\n\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\n\nFor example, given the following input statement:\n\n**Input**:\n\n```console\n🤧> I am taking klaritin for my allergies\n\n```\n\n**Output**:\n"
  },
  {
    "path": "typescript/examples/healthData/package.json",
    "content": "{\n  \"name\": \"health-data\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.1.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.3.1\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/healthData/src/healthDataSchema.ts",
    "content": "// The following is a schema definition for enetring health data.\n\nexport interface HealthDataResponse {\n    // ONLY present when ALL required information is known.\n    // Otherwise, use 'message' to keep asking questions.\n    data?: HealthData;\n    // Use this to ask questions and give pertinent responses\n    message?: string;\n    // Use this parts of the user request not translated, off topic, etc.\n    notTranslated?: string;\n}\n\nexport interface HealthData {\n    medication?: Medication[];\n    condition?: Condition[];\n    other?: OtherHealthData[];\n}\n\n// Meds, pills etc.\nexport interface Medication {\n    // Fix any spelling mistakes, especially phonetic spelling\n    name: string;\n    // E.g. 2 tablets, 1 cup. Required\n    dose: ApproxQuantity;\n    // E.g. twice a day. Required\n    frequency: ApproxQuantity;\n    // E.g. 50 mg. Required\n    strength: ApproxQuantity;\n}\n\n// Disease, Ailment, Injury, Sickness\nexport interface Condition {\n    // Fix any spelling mistakes, especially phonetic spelling\n    name: string;\n    // When the condition started.\n    startDate: ApproxDatetime;\n    // Always ask for current status of the condition\n    status: \"active\" | \"recurrence\" | \"relapse\" | \"inactive\" | \"remission\" | \"resolved\" | \"unknown\";\n    // If the condition was no longer active\n    endDate?: ApproxDatetime;\n}\n\n// Use for health data that match nothing else. E.g. immunization, blood prssure etc\nexport interface OtherHealthData {\n    text: string;\n    when?: ApproxDatetime;\n}\n\nexport interface ApproxQuantity {\n    // Default: \"unknown\"\n    displayText: string;\n    // Only specify if precise quantities are available\n    quantity?: Quantity;\n}\n\nexport interface ApproxDatetime {\n    // Default: \"unknown\"\n    displayText: string;\n    // If precise timestamp can be set\n    timestamp?: string;\n}\n\nexport interface Quantity {\n    // Exact number\n    value: number;\n    // Units like mg, kg, cm, pounds, liter, ml, tablet, pill, cup, per-day, per-week, etc.\n    units: string;\n}"
  },
  {
    "path": "typescript/examples/healthData/src/input.txt",
    "content": "#\n# Conversations with a Health Data Agent\n# For each conversation: \n# You start with the first line \n# Then type the next line in response\n#\n\n# ================\n# USE GPT4\n# ================\n# Conversation:\ni want to record my shingles\nAugust 2016\nIt lasted 3 months\nI also broke my foot\nI broke it in high school\n2001\nThe foot took a year to be ok\n\n# Conversation: \nklaritin\n2 tablets 3 times a day\n300 mg\nactually that is 1 tablet\n@clear\n\n# Conversation: \nklaritin\n1 pill, morning and before bedtime\nCan't remember\nActually, that is 3 tablets\n500 mg\n@clear\n\n#Conversation\nI am taking binadryl now\nAs needed. Groceery store strength\nThat is all I have\nI also got allergies. Pollen\n@clear\n\n# Conversation: \nRobotussin\n1 cup\nDaily, as needed\nRobotussin with Codeine\nPut down strength as I don't know\n@clear\n\n# Conversation: \nHey\nMelatonin\n1 3mg tablet every night\n@clear\n\n# Conversation: \nI got the flu\nStarted 2 weeks ago\nIts gone now. Only lasted about a week\nI took some sudafed though\nI took 2 sudafed twice a day. Regular strength\n@clear\n\n"
  },
  {
    "path": "typescript/examples/healthData/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createLanguageModel } from \"typechat\";\nimport { processRequests } from \"typechat/interactive\";\nimport { HealthDataResponse } from \"./healthDataSchema\";\nimport { createHealthDataTranslator } from \"./translator\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst healthInstructions = `\nHelp me enter my health data step by step.\nAsk specific questions to gather required and optional fields\nI have not already providedStop asking if I don't know the answer\nAutomatically fix my spelling mistakes\nMy health data may be complex: always record and return ALL of it.\nAlways return a response:\n- If you don't understand what I say, ask a question.\n- At least respond with an OK message.\n`;\n\nconst model = createLanguageModel(process.env);\nconst schema = fs.readFileSync(path.join(__dirname, \"healthDataSchema.ts\"), \"utf8\");\nconst translator = createHealthDataTranslator<HealthDataResponse>(model, schema, \"HealthDataResponse\",\n                        healthInstructions);\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"🤧> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(\"Translation Failed ❌\");\n        console.log(`Context: ${response.message}`);\n    }\n    else {\n        const healthData = response.data;\n        console.log(\"Translation Succeeded! ✅\\n\");\n        console.log(\"JSON View\");\n        console.log(JSON.stringify(healthData, undefined, 2));\n\n        const message = healthData.message;\n        const notTranslated = healthData.notTranslated;\n\n        if (message) {\n            console.log(`\\n📝: ${message}`);\n        }\n            \n        if (notTranslated) {\n            console.log(`\\n🤔: I did not understand\\n ${notTranslated}`)\n        }\n    }\n});\n"
  },
  {
    "path": "typescript/examples/healthData/src/translator.ts",
    "content": "import {Result, TypeChatLanguageModel, createJsonTranslator, TypeChatJsonTranslator} from \"typechat\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\n\ntype ChatMessage = {\n    source: \"system\" | \"user\" | \"assistant\";\n    body: object;\n};\n\nexport interface TranslatorWithHistory<T extends object> {\n    _chatHistory: ChatMessage[];\n    _maxPromptLength: number;\n    _additionalAgentInstructions: string;\n    _translator: TypeChatJsonTranslator<T>;\n    translate(request: string): Promise<Result<T>>;\n}\n\nexport function createHealthDataTranslator<T extends object>(model: TypeChatLanguageModel, schema: string, typename: string, additionalAgentInstructions: string): TranslatorWithHistory<T> {\n    const _chatHistory: ChatMessage[] = [];\n    const _maxPromptLength = 2048;\n    const _additionalAgentInstructions = additionalAgentInstructions;\n    \n    const validator = createTypeScriptJsonValidator<T>(schema, typename);\n    const _translator = createJsonTranslator(model, validator);\n    _translator.createRequestPrompt = createRequestPrompt;\n    \n    const customtranslator: TranslatorWithHistory<T> = {\n        _chatHistory,\n        _maxPromptLength,\n        _additionalAgentInstructions,\n        _translator,\n        translate,\n    };\n\n    return customtranslator;\n\n    async function translate(request: string): Promise<Result<T>> {\n        const response = await _translator.translate(request);\n        if (response.success) {\n            _chatHistory.push({ source: \"assistant\", body: response.data });\n        }\n        return response;\n\n    }\n\n    function createRequestPrompt(intent: string): string {\n        // TODO: drop history entries if we exceed the max_prompt_length\n        const historyStr = JSON.stringify(_chatHistory, undefined, 2);\n        \n        const now = new Date();\n\n        const prompt = `\nuser: You are a service that translates user requests into JSON objects of type \"${typename}\" according to the following TypeScript definitions:\n'''\n${schema}\n'''\n\nuser:\nUse precise date and times RELATIVE TO CURRENT DATE: ${now.toLocaleDateString()} CURRENT TIME: ${now.toTimeString().split(' ')[0]}\nAlso turn ranges like next week and next month into precise dates\n\nuser:\n${_additionalAgentInstructions}\n\nsystem:\nIMPORTANT CONTEXT for the user request:\n${historyStr}\n\nuser:\nThe following is a user request:\n'''\n${intent}\n'''\nThe following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\n\"\"\"\n`;\n        return prompt;\n    }\n}\n"
  },
  {
    "path": "typescript/examples/healthData/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/math/.vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"skipFiles\": [\n                \"<node_internals>/**\"\n            ],\n            \"program\": \"${workspaceFolder}/dist/main.js\",\n            \"console\": \"externalTerminal\"\n        }\n    ]\n}"
  },
  {
    "path": "typescript/examples/math/README.md",
    "content": "# Math\n\nThe Math example shows how to use TypeChat for program generation based on an API schema with the `evaluateJsonProgram` function. This example translates calculations into simple programs given an [`API`](./src/mathSchema.ts) type that can perform the four basic mathematical operations.\n\n# Try Math\n\nTo run the Math example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\n\nFor example, we could use natural language to describe mathematical operations, and TypeChat will generate a program that can execute the math API defined in the schema.\n\n**Input**:\n\n```\n🟰> multiply two by three, then multiply four by five, then sum the results\n```\n\n**Output**:\n\n```\nimport { API } from \"./schema\";\nfunction program(api: API) {\n  const step1 = api.mul(2, 3);\n  const step2 = api.mul(4, 5);\n  return api.add(step1, step2);\n}\nRunning program:\nmul(2, 3)\nmul(4, 5)\nadd(6, 20)\nResult: 26\n```\n"
  },
  {
    "path": "typescript/examples/math/package.json",
    "content": "{\n  \"name\": \"math\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/math/src/input.txt",
    "content": "1 + 2\r\n1 + 2 * 3\r\n2 * 3 + 4 * 5\r\n2 3 * 4 5 * +\r\nmultiply two by three, then multiply four by five, then sum the results\r\n"
  },
  {
    "path": "typescript/examples/math/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createLanguageModel, getData } from \"typechat\";\nimport { processRequests } from \"typechat/interactive\";\nimport { createModuleTextFromProgram, createProgramTranslator, evaluateJsonProgram } from \"typechat/ts\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst schema = fs.readFileSync(path.join(__dirname, \"mathSchema.ts\"), \"utf8\");\nconst translator = createProgramTranslator(model, schema);\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"🧮 > \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    const program = response.data;\n    console.log(getData(createModuleTextFromProgram(program)));\n    console.log(\"Running program:\");\n    const result = await evaluateJsonProgram(program, handleCall);\n    console.log(`Result: ${typeof result === \"number\" ? result : \"Error\"}`);\n});\n\nasync function handleCall(func: string, args: any[]): Promise<unknown> {\n    console.log(`${func}(${args.map(arg => typeof arg === \"number\" ? arg : JSON.stringify(arg, undefined, 2)).join(\", \")})`);\n    switch (func) {\n        case \"add\":\n            return args[0] + args[1];\n        case \"sub\":\n            return args[0] - args[1];\n        case \"mul\":\n            return args[0] * args[1];\n        case \"div\":\n            return args[0] / args[1];\n        case \"neg\":\n            return -args[0];\n        case \"id\":\n            return args[0];\n    }\n    return NaN;\n}\n"
  },
  {
    "path": "typescript/examples/math/src/mathSchema.ts",
    "content": "// This is a schema for writing programs that evaluate expressions.\n\nexport type API = {\n    // Add two numbers\n    add(x: number, y: number): number;\n    // Subtract two numbers\n    sub(x: number, y: number): number;\n    // Multiply two numbers\n    mul(x: number, y: number): number;\n    // Divide two numbers\n    div(x: number, y: number): number;\n    // Negate a number\n    neg(x: number): number;\n    // Identity function\n    id(x: number): number;\n    // Unknown request\n    unknown(text: string): number;\n}\n"
  },
  {
    "path": "typescript/examples/math/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true,\n  }\n}\n"
  },
  {
    "path": "typescript/examples/multiSchema/README.md",
    "content": "# MultiSchema\n\nThis application demonstrates a simple way to write a **super-app** that automatically routes user requests to child apps. \n\nIn this example, the child apps are existing TypeChat chat examples:\n\n* CoffeeShop\n* Restaurant\n* Calendar\n* Sentiment\n* Math\n* Plugins\n* HealthData\n\n## Target Models\n\nWorks with GPT-3.5 Turbo and GPT-4.\n\nSub-apps like HealthData and Plugins work best with GPT-4.\n\n# Usage\n\nExample prompts can be found in [`src/input.txt`](src/input.txt).\n"
  },
  {
    "path": "typescript/examples/multiSchema/package.json",
    "content": "{\n  \"name\": \"multi-schema\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 -f ../../examples/**/src/**/*Schema.ts src/**/*.txt dist\"\n\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"typechat\": \"^0.1.0\",\n    \"find-config\": \"^1.0.0\",\n    \"music\": \"^0.0.1\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.3.1\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/multiSchema/src/agent.ts",
    "content": "// TypeScript file for TypeChat agents.\nimport { Result, TypeChatJsonTranslator, TypeChatLanguageModel, createJsonTranslator, getData, success } from \"typechat\";\nimport { Program, createModuleTextFromProgram, createProgramTranslator, createTypeScriptJsonValidator, evaluateJsonProgram } from \"typechat/ts\";\n\nexport type AgentInfo = {\n    name: string;\n    description: string;\n};\n\nexport interface AgentClassificationResponse {\n    agenInfo : AgentInfo;\n}\n\nexport type MessageHandler<T extends object> = (message: string) => Promise<Result<T>>;\n\nexport interface Agent<T extends object> extends AgentInfo {\n    handleMessage(message: string): Promise<Result<T>>;\n};\n\ninterface JsonPrintAgent<T extends object> extends Agent<T> {\n    _translator: TypeChatJsonTranslator<T>;\n}\n\nexport function createJsonPrintAgent<T extends object>(\n    name: string,\n    description: string,\n    model: TypeChatLanguageModel,\n    schema: string,\n    typeName: string\n): JsonPrintAgent<T> {\n    const validator = createTypeScriptJsonValidator<T>(schema, typeName)\n    const _translator = createJsonTranslator<T>(model, validator);\n    const jsonPrintAgent: JsonPrintAgent<T> = {\n        _translator,\n        name: name,\n        description: description,\n        handleMessage: _handleMessage,\n    };\n\n    return jsonPrintAgent;\n\n    async function _handleMessage(request: string): Promise<Result<T>> {\n        const response = await _translator.translate(request);\n        if (response.success) {\n            console.log(\"Translation Succeeded! ✅\\n\")\n            console.log(\"JSON View\")\n            console.log(JSON.stringify(response.data, undefined, 2))\n        }\n        else {\n            console.log(\"Translation Failed ❌\")\n            console.log(`Context: ${response.message}`)\n        }\n        return response;\n    }\n}\n\ninterface MathAgent<T extends object> extends Agent<T> {\n    _translator: TypeChatJsonTranslator<Program>;\n    //_handleCall(func: string, args: any[]): Promise<unknown>;\n}\n\nexport function createJsonMathAgent<T extends object>\n    (name: string, description: string,\n     model: TypeChatLanguageModel,\n     schema: string): MathAgent<T>\n{\n    async function _handleCall(func: string, args: any[]): Promise<unknown> {\n        // implementation goes here\n        console.log(`${func}(${args.map(arg => typeof arg === \"number\" ? arg : JSON.stringify(arg, undefined, 2)).join(\", \")})`);\n        switch (func) {\n            case \"add\":\n                return args[0] + args[1];\n            case \"sub\":\n                return args[0] - args[1];\n            case \"mul\":\n                return args[0] * args[1];\n            case \"div\":\n                return args[0] / args[1];\n            case \"neg\":\n                return -args[0];\n            case \"id\":\n                return args[0];\n        }\n        return NaN;\n    }\n\n    const _translator = createProgramTranslator(model, schema);\n    const mathAgent : MathAgent<T> = {\n        _translator,\n        name: name,\n        description: description,\n        handleMessage: _handleMessage,\n    };\n\n    return mathAgent;\n\n    async function _handleMessage(request: string): Promise<Result<T>> {\n        const response = await _translator.translate(request);\n        if (!response.success) {\n            console.log(response.message);\n            return response;\n        }\n\n        const program = response.data;\n        console.log(getData(createModuleTextFromProgram(program)));\n        console.log(\"Running program:\");\n        const result = await evaluateJsonProgram(program, _handleCall);\n        console.log(`Result: ${typeof result === \"number\" ? result : \"Error\"}`);\n        return success(\"Successful evaluation\" as any);\n    }\n}"
  },
  {
    "path": "typescript/examples/multiSchema/src/classificationSchema.ts",
    "content": "\nexport interface TaskClassification {\n    name: string;\n    description: string;\n}\n\n/**\n * Represents the response of a task classification.\n */\nexport interface TaskClassificationResponse {\n    // Describe the kind of task to perform.\n    taskType: string;\n}\n"
  },
  {
    "path": "typescript/examples/multiSchema/src/input.txt",
    "content": "I'd like two large, one with pepperoni and the other with extra sauce.  The pepperoni gets basil and the extra sauce gets Canadian bacon.  And add a whole salad. \nI also want an espresso with extra foam and a muffin with jam\nAnd book me a lunch with Claude Debussy next week at 12.30 at Le Petit Chien!\nI bought 4 shoes for 12.50 each. How much did I spend?\nIts cold!\nIts cold and I want hot cafe to warm me up\nThe coffee is cold\nThe coffee is awful\n(2*4)+(9*7)"
  },
  {
    "path": "typescript/examples/multiSchema/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createLanguageModel } from \"typechat\";\nimport { processRequests } from \"typechat/interactive\";\nimport { createJsonMathAgent, createJsonPrintAgent } from \"./agent\";\nimport { createAgentRouter } from \"./router\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst taskClassificationSchema = fs.readFileSync(path.join(__dirname, \"classificationSchema.ts\"), \"utf8\");\nconst router = createAgentRouter(model, taskClassificationSchema, \"TaskClassificationResponse\")\n\nconst sentimentSchema = fs.readFileSync(path.join(__dirname, \"sentimentSchema.ts\"), \"utf8\");\nconst sentimentAgent = createJsonPrintAgent\n    (\"Sentiment\",\n    \"Statements with sentiments, emotions, feelings, impressions about places, things, the surroundings\",\n    model, sentimentSchema, \"SentimentResponse\"\n);\nrouter.registerAgent(\"Sentiment\", sentimentAgent);\n\nconst coffeeShopSchema  = fs.readFileSync(path.join(__dirname, \"coffeeShopSchema.ts\"), \"utf8\");\nconst coffeeShopAgent = createJsonPrintAgent(\n    \"CoffeeShop\",\n    \"Order Coffee Drinks (Italian names included) and Baked Goods\",\n    model, coffeeShopSchema, \"Cart\"\n);\nrouter.registerAgent(\"CoffeeShop\", coffeeShopAgent);\n\nconst calendarSchema  = fs.readFileSync(path.join(__dirname, \"calendarActionsSchema.ts\"), \"utf8\");\nconst calendarAgent = createJsonPrintAgent(\n    \"Calendar\",\n    \"Actions related to calendars, appointments, meetings, schedules\",\n    model, calendarSchema, \"CalendarActions\"\n);\nrouter.registerAgent(\"Calendar\", calendarAgent);\n\nconst orderSchema  = fs.readFileSync(path.join(__dirname, \"foodOrderViewSchema.ts\"), \"utf8\");\nconst restaurantOrderAgent = createJsonPrintAgent(\n    \"Restaurant\",\n    \"Order pizza, beer and salads\",\n    model, orderSchema, \"Order\"\n);\nrouter.registerAgent(\"Restaurant\", restaurantOrderAgent);\n\nconst mathSchema  = fs.readFileSync(path.join(__dirname, \"mathSchema.ts\"), \"utf8\");\nconst mathAgent = createJsonMathAgent(\n    \"Math\",\n    \"Calculations using the four basic math operations\",\n    model, mathSchema\n);\nrouter.registerAgent(\"Math\", mathAgent);\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"🔀> \", process.argv[2], async (request) => {\n    await router.routeRequest(request);\n});"
  },
  {
    "path": "typescript/examples/multiSchema/src/router.ts",
    "content": "import { Result, TypeChatJsonTranslator, TypeChatLanguageModel, createJsonTranslator } from \"typechat\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\nimport { Agent, MessageHandler } from \"./agent\";\nimport { TaskClassification, TaskClassificationResponse } from \"./classificationSchema\";\n\nexport interface AgentRouter<T extends object> {\n    _taskTypes: TaskClassification[];\n    _agentMap: { [name: string]: Agent<T> };\n    _taskClassifier: TypeChatJsonTranslator<TaskClassificationResponse>\n    _handlerUnknownTask: MessageHandler<T>;\n    registerAgent(name: string, agent: Agent<T>): Promise<void>\n    routeRequest(request: string): Promise<void>\n}\n\nexport function createAgentRouter<T extends object>(model: TypeChatLanguageModel, schema: string, typeName: string): AgentRouter<T> {\n    const validator = createTypeScriptJsonValidator<TaskClassificationResponse>(schema, typeName)\n    const taskClassifier = createJsonTranslator<TaskClassificationResponse>(model, validator);\n    const router: AgentRouter<T> = {\n        _taskTypes: [],\n        _agentMap: {},\n        _taskClassifier: taskClassifier,\n        _handlerUnknownTask: handlerUnknownTask,\n        registerAgent,\n        routeRequest: routeRequest,\n    };\n\n    router._taskTypes.push({\n        name: \"No Match\",\n        description: \"Handles all unrecognized requests\"\n    });\n\n    return router;\n\n    async function handlerUnknownTask(request: string): Promise<Result<T>> {\n        console.log(`🤖The request \"${request}\" was not recognized by any agent.`);\n        return { success: false, message: `The request \"${request}\" was not recognized by any agent.` };\n    }\n\n    async function registerAgent(name: string, agent: Agent<T>): Promise<void> {\n        if (!router._agentMap[name]) {\n            router._agentMap[name] = agent;\n\n            // Add the agent's task type to the list of task types\n            router._taskTypes.push({name: name, description: agent.description});\n        }\n        return;\n    }\n\n    async function routeRequest(request:string): Promise<void> {\n        const initClasses = JSON.stringify(router._taskTypes, undefined, 2);\n        const fullRequest = `\nClassify \"${request}\" using the following classification table:\\n\n${initClasses}\\n`;\n        const response = await router._taskClassifier.translate(request, [{\n            role: \"assistant\", content: `${fullRequest}`\n        }]);\n\n        if (response.success) {\n            if (response.data.taskType != \"No Match\") {\n                const agentName = response.data.taskType;\n                console.log(`🤖 The task will be handled by the ${agentName} Agent.`);\n                const agent = router._agentMap[agentName];\n                await agent.handleMessage(request);\n            }\n            else {\n                router._handlerUnknownTask(request);\n            }\n        }\n        else {\n            console.log(\"🙈 Sorry, we could not find an agent to handle your request.\\n\")\n            console.log(`Context: ${response.message}`)\n        }   \n        return\n    }\n}"
  },
  {
    "path": "typescript/examples/multiSchema/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  },\n  \"references\": [\n    { \"path\": \"../../music/src\" }\n  ]\n}"
  },
  {
    "path": "typescript/examples/music/.vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"skipFiles\": [\n                \"<node_internals>/**\"\n            ],\n            \"program\": \"${workspaceFolder}/dist/main.js\",\n            \"console\": \"externalTerminal\"\n        }\n    ]\n}"
  },
  {
    "path": "typescript/examples/music/README.md",
    "content": "# Music\r\n\r\nThe Music example shows how to capture user intent as actions in JSON which corresponds to a simple dataflow program over the API provided in the intent schema. This example shows this pattern using natural language to control the Spotify API to play music, create playlists, and perform other actions from the API. \r\n\r\n# Try Music\r\n\r\nA Spotify Premium account is required to run this example.\r\n\r\nTo run the Music example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\r\n\r\nThis example also requires additional setup to use the Spotify API:\r\n\r\n1. Go to https://developer.spotify.com/dashboard. \r\n2. Log into Spotify with your user account if you are not already logged in.\r\n3. Click the button in the upper right labeled \"Create App\".\r\n4. Fill in the form, making sure the Redirect URI is http://localhost:PORT/callback, where PORT is a four-digit port number you choose for the authorization redirect.\r\n5. Click the settings button and copy down the Client ID and Client Secret (the client secret requires you to click 'View client secret').\r\n6. In your `.env` file, set `SPOTIFY_APP_CLI` to your Client ID and `SPOTIFY_APP_CLISEC` to your Client Secret.  Also set `SPOTIFY_APP_PORT` to the PORT on your local machine that you chose in step 4.\r\n\r\n# Usage\r\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\r\n\r\nFor example, use natural language to start playing a song with the Spotify player:\r\n\r\n**Input**:\r\n\r\n```\r\n🎵> play shake it off by taylor swift\r\n```\r\n\r\n**Output**:\r\n\r\n```\r\nPlan Validated:\r\n{\r\n    \"@steps\": [\r\n        {\r\n            \"@func\": \"searchTracks\",\r\n            \"@args\": [\r\n                \"shake it off taylor swift\"\r\n            ]\r\n        },\r\n        {\r\n            \"@func\": \"play\",\r\n            \"@args\": [\r\n                {\r\n                    \"@ref\": 0\r\n                }\r\n            ]\r\n        }\r\n    ]\r\n}\r\nimport { API } from \"./schema\";\r\nfunction program(api: API) {\r\n  const step1 = api.searchTracks(\"shake it off taylor swift\");\r\n  return api.play(step1);\r\n}\r\nPlaying...\r\nShake It Off\r\n```"
  },
  {
    "path": "typescript/examples/music/migrations.md",
    "content": "# Local Music DB Migrations\n\nTracks table\n```[SQL]\nCREATE TABLE tracks (\n    id INTEGER PRIMARY KEY,\n    title TEXT NOT NULL,\n    artist_id INTEGER NOT NULL,\n    album_id INTEGER,\n    duration INTEGER,\n    release_date TEXT,\n    genre TEXT,\n);\n```\nAlbums table\n```[SQL]\nCREATE TABLE albums (\n    id INTEGER PRIMARY KEY,\n    title TEXT NOT NULL,\n    artist_id INTEGER NOT NULL,\n    release_date TEXT,\n    genre TEXT,\n);\n```\nPlaylists table\n```[SQL]\nCREATE TABLE playlists (\n    id INTEGER PRIMARY KEY,\n    title TEXT NOT NULL,\n    user_id INTEGER NOT NULL,\n    creation_date TEXT,\n    description TEXT,\n);\n```\nArtists table\n```[SQL]\nCREATE TABLE artists (\n    id INTEGER PRIMARY KEY,\n    name TEXT NOT NULL,\n    country TEXT,\n    genre TEXT\n);\n```\n"
  },
  {
    "path": "typescript/examples/music/package.json",
    "content": "{\n  \"name\": \"music\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt src/**/*.html dist\"\n  },\n  \"exports\": {\n    \"./*\": [\"./dist/*.js\"]\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"axios\": \"^1.6.2\",\n    \"chalk\": \"^2.3.1\",\n    \"dotenv\": \"^16.3.1\",\n    \"express\": \"^4.18.2\",\n    \"find-config\": \"^1.0.0\",\n    \"open\": \"^7.0.4\",\n    \"sqlite3\": \"^5.1.6\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.17\",\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"@types/spotify-api\": \"^0.0.22\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/music/src/authz.ts",
    "content": "import open from \"open\";\r\nimport chalk from \"chalk\";\r\nimport express from \"express\";\r\nimport dotenv from \"dotenv\";\r\nimport path from \"path\";\r\n\r\ndotenv.config({ path: path.join(__dirname, \"../../../.env\") });\r\n\r\nimport { Server } from \"http\";\r\ntype AuthzHandlerFn = (token: string | undefined) => void;\r\ntype AuthzHandler = AuthzHandlerFn | undefined;\r\ntype AuthzServer = Server | undefined;\r\n\r\nconst scope = [\r\n    \"user-read-private\",\r\n    \"playlist-read-collaborative\",\r\n    \"playlist-modify-private\",\r\n    \"playlist-read-private\",\r\n    \"playlist-modify-public\",\r\n    \"streaming\",\r\n    \"user-library-read\",\r\n    \"user-top-read\",\r\n    \"user-read-playback-state\",\r\n    \"user-modify-playback-state\",\r\n    \"user-read-recently-played\",\r\n    \"user-read-currently-playing\",\r\n    \"user-library-modify\",\r\n    \"ugc-image-upload\",\r\n].join(\"%20\");\r\n\r\nconst baseClientId = process.env.SPOTIFY_APP_CLI;\r\nconst defaultPort = process.env.SPOTIFY_APP_PORT;\r\nexport class Authzor {\r\n    url: string;\r\n    app: express.Express;\r\n    handler: AuthzHandler;\r\n    server: AuthzServer;\r\n    private redirectCount = 0;\r\n\r\n    constructor(\r\n        public port = defaultPort,\r\n        public showDialog = false,\r\n        public clientId = baseClientId\r\n    ) {\r\n        const redirectUri = \"http://localhost:\" + port + \"/callback\";\r\n\r\n        this.url =\r\n            \"https://accounts.spotify.com/authorize\" +\r\n            \"?client_id=\" +\r\n            clientId +\r\n            \"&response_type=token\" +\r\n            \"&scope=\" +\r\n            scope +\r\n            \"&show_dialog=\" +\r\n            showDialog +\r\n            \"&redirect_uri=\" +\r\n            redirectUri;\r\n\r\n        this.app = express();\r\n\r\n        this.app.get(\"/callback\", (req, res) => {\r\n            if (req.query.error) {\r\n                console.log(\r\n                    chalk.red(\"Something went wrong. Error: \"),\r\n                    req.query.error\r\n                );\r\n            } else {\r\n                // update this when implementing re-auth on token expire\r\n                if (this.redirectCount === 0) {\r\n                    res.sendFile(__dirname + \"/callback.html\");\r\n                    this.redirectCount++;\r\n                }\r\n            }\r\n        });\r\n\r\n        this.app.get(\"/token\", (req, res) => {\r\n            res.sendStatus(200);\r\n            const token = req.query.access_token as string;\r\n            if (token) {\r\n                if (this.handler) {\r\n                    this.handler(token);\r\n                }\r\n            }\r\n            this.close();\r\n        });\r\n    }\r\n\r\n    authorize(connect: boolean, handler: AuthzHandlerFn) {\r\n        if (baseClientId && connect) {\r\n            this.handler = handler;\r\n            this.server = this.app.listen(this.port, () => {\r\n                if (this.showDialog) {\r\n                    console.log(\r\n                        chalk.blue(\r\n                            \"Opening the Spotify Login Dialog in your browser...\"\r\n                        )\r\n                    );\r\n                }\r\n                open(this.url, { wait: false });\r\n            });\r\n        } else {\r\n            handler(undefined);\r\n        }\r\n    }\r\n\r\n    close() {\r\n        if (this.server) {\r\n            this.server.close();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "typescript/examples/music/src/callback.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title></title>\n</head>\n<body>\n  <script>\n    const hash = window.location.hash\n      .substring(1)\n\n      fetch('/token?' + hash)\n      .then(() => {\n        window.close()\n      })\n  </script>\n</body>\n</html>"
  },
  {
    "path": "typescript/examples/music/src/chatifyActionsSchema.ts",
    "content": "// This is a schema for writing programs that control a Spotify music player\n\ntype Track = { name: string };\ntype TrackList = Track[];\ntype Playlist = TrackList; \n\nexport type API = {\n    // play track list\n    play(\n        // track list to play\n        trackList: TrackList,\n        // start playing at this track index\n        startIndex?: number,\n        // play this many tracks\n        count?: number\n    ): void;\n    // print a list of tracks\n    printTracks(trackList: TrackList): void;\n    // see what is up next\n    getQueue(): void;\n    // show now playing\n    status(): void;\n    // control playback\n    // pause playback\n    pause(): void;\n    // next track\n    next(): void;\n    // previous track\n    previous(): void;\n    // turn shuffle on\n    shuffleOn(): void;\n    // turn shuffle off\n    shuffleOff(): void;\n    // resume playing\n    resume(): void;\n    // list available playback devices\n    listDevices(): void;\n    // select playback device by keyword\n    selectDevice(keyword: string): void;\n    // set volume\n    setVolume(newVolumeLevel: number): void;\n    // change volume\n    changeVolume(volumeChangeAmount: number): void;\n    // query is a Spotify search expression such as 'Rock Lobster' or 'te kanawa queen of night'\n    searchTracks(query: string): TrackList;\n    // return the last track list shown to the user\n    // for example, if the user types \"play the third one\" the player plays the third track\n    // from the last track list shown\n    getLastTrackList(): TrackList;\n    // list all playlists\n    listPlaylists(): void;\n    // get playlist by name\n    getPlaylist(name: string): Playlist;\n    // get album by name; if name is \"\", use the currently playing track\n    getAlbum(name: string): TrackList;\n    // Return a list of the user's favorite tracks\n    getFavorites(count?: number): TrackList;\n    // apply a filter to match tracks\n    filterTracks(\n        // track list to filter\n        trackList: TrackList,\n        // filter type is one of \"genre\", \"artist\", \"name\"; name does a fuzzy match on the track name\n        // for example, filterType: \"name\", filter: \"color\" matches \"Red Red Wine\"\n        filterType: \"genre\" | \"artist\" | \"name\",\n        filter: string,\n        negate?: boolean\n    ): TrackList;\n    // create a Spotify playlist from a list of tracks\n    createPlaylist(trackList: TrackList, name: string): void;\n    // Delete playlist given by playlist\n    deletePlaylist(playlist: Playlist): void;\n    // call this function for requests that weren't understood\n    unknownAction(text: string): void;\n    // call this function if the user asks a non-music question; non-music non-questions use UnknownAction\n    nonMusicQuestion(text: string): void;\n};\n"
  },
  {
    "path": "typescript/examples/music/src/dbInterface.ts",
    "content": "import sqlite3 from \"sqlite3\";\n\nconst dbPath = \"../musicLibrary.db\"\n\ntype Row = { [key:string] : unknown }\n\nfunction executeQuery(query: string, params: any[] = []): Row[] | void {\n    const db = new sqlite3.Database(dbPath, (error) => {\n        if (error) {\n            console.log(`Error executing query: ${query} against ${dbPath}`);\n            return;\n        }\n        \n        db.all(query, params, (error: Error, rows: Row[]) => {\n            db.close();\n\n            if (error) {\n                console.log(`Error executing query: ${query} against ${dbPath}`);\n                return;\n            }\n\n            return rows;\n        });\n    });\n}\n\nexport function insertTracks(tracks: SpotifyApi.TrackObjectFull[]) {\n    let insertQuery = 'INSERT INTO tracks (id, title, artist_id, album_id, duration, release_data, genre)\\nVALUES\\n';\n    for (const track of tracks) {\n        // TODO: genre\n        insertQuery += `    (${track.id},${track.name},${track.artists[0].id},${track.album.id},${track.duration_ms},${track.album.release_date})`;\n    }    \n\n\n}\nexport function getArtists() {\n    const query = \"SELECT * FROM artists\";\n    const artists = executeQuery(query);\n    return artists;\n}\n"
  },
  {
    "path": "typescript/examples/music/src/endpoints.ts",
    "content": "import axios from \"axios\";\nimport { SpotifyService } from \"./service\";\n\nexport const limitMax = 50;\n\nexport async function search(\n    query: SpotifyApi.SearchForItemParameterObject,\n    service: SpotifyService\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveToken()}`,\n        },\n    };\n\n    const searchUrl = getUrlWithParams(\n        \"https://api.spotify.com/v1/search\",\n        query\n    );\n    try {\n        const spotifyResult = await axios.get(searchUrl, config);\n        return spotifyResult.data as SpotifyApi.SearchResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getTop(\n    service: SpotifyService,\n    limit = limitMax,\n    offset = 0\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n\n    const tracksUrl = getUrlWithParams(\"https://api.spotify.com/v1/me/tracks\", {\n        limit,\n        offset,\n    });\n    try {\n        const spotifyResult = await axios.get(tracksUrl, config);\n\n        return spotifyResult.data as SpotifyApi.PagingObject<SpotifyApi.PlaylistTrackObject>;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getTopK(service: SpotifyService, k = limitMax) {\n    if (k > limitMax) {\n        const topTracks = [] as SpotifyApi.PlaylistTrackObject[];\n        let offset = 0;\n        while (k > 0) {\n            let count = limitMax;\n            if (k < count) {\n                count = k;\n            }\n            const hist = await getTop(service, count, offset);\n            if (hist && hist.items) {\n                topTracks.push(...hist.items);\n            }\n            k -= limitMax;\n            offset += limitMax;\n        }\n        return topTracks;\n    } else {\n        const hist = await getTop(service, k);\n        if (hist && hist.items) {\n            return hist.items;\n        }\n    }\n    return undefined;\n}\n\nexport async function getArtist(service: SpotifyService, id: string) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n\n    const artistsUrl = getUrlWithParams(\"https://api.spotify.com/v1/artists\", {\n        ids: id,\n    });\n    try {\n        const spotifyResult = await axios.get(artistsUrl, config);\n\n        return spotifyResult.data as SpotifyApi.MultipleArtistsResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getHistoryURL(service: SpotifyService, url: string) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n\n    console.log(url);\n    try {\n        const spotifyResult = await axios.get(url, config);\n\n        const spotData =\n            spotifyResult.data as SpotifyApi.UsersRecentlyPlayedTracksResponse;\n        return spotData;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getRecent(\n    service: SpotifyService,\n    after = Date.parse(\"2023-01-01T00:00:00.000Z\")\n) {\n    const playHistory = [] as SpotifyApi.PlayHistoryObject[];\n    console.log(new Date(after).toLocaleString());\n    const params = {\n        limit: 50,\n        after,\n    };\n    let nextURL: string | null | undefined = getUrlWithParams(\n        \"https://api.spotify.com/v1/me/player/recently-played\",\n        params\n    );\n    while (nextURL) {\n        const hist = await getHistoryURL(service, nextURL);\n        if (hist && hist.items) {\n            console.log(hist.items.length);\n            playHistory.push(...hist.items);\n        }\n        nextURL = hist?.next;\n        console.log(nextURL);\n    }\n    return playHistory;\n}\n\nexport async function getUserProfile(service: SpotifyService) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n\n    try {\n        const spotifyResult = await axios.get(\n            \"https://api.spotify.com/v1/me\",\n            config\n        );\n\n        return spotifyResult.data as SpotifyApi.UserProfileResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getPlaybackState(service: SpotifyService) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const spotifyResult = await axios.get(\n            \"https://api.spotify.com/v1/me/player\",\n            config\n        );\n\n        return spotifyResult.data as SpotifyApi.CurrentPlaybackResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function transferPlayback(\n    service: SpotifyService,\n    deviceId: string,\n    play = false\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    const xferUrl = \"https://api.spotify.com/v1/me/player/\";\n\n    const params = { device_ids: [deviceId], play };\n    try {\n        const spotifyResult = await axios.put(xferUrl, params, config);\n\n        return spotifyResult.data;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function play(\n    service: SpotifyService,\n    deviceId: string,\n    uris?: string[],\n    contextUri?: string,\n    trackNumber?: number,\n    seekms?: number\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    const smallTrack: SpotifyApi.PlayParameterObject = {};\n    if (contextUri) {\n        smallTrack.context_uri = contextUri;\n        if (trackNumber) {\n            smallTrack.offset = { position: trackNumber };\n            if (seekms) {\n                smallTrack.position_ms = seekms;\n            }\n        }\n    } else if (uris) {\n        smallTrack.uris = uris;\n    }\n    const playUrl = getUrlWithParams(\n        \"https://api.spotify.com/v1/me/player/play\",\n        { device_id: deviceId }\n    );\n    try {\n        const spotifyResult = await axios.put(playUrl, smallTrack, config);\n\n        return spotifyResult.data;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getDevices(service: SpotifyService) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n\n    try {\n        const spotifyResult = await axios.get(\n            \"https://api.spotify.com/v1/me/player/devices\",\n            config\n        );\n\n        return spotifyResult.data as SpotifyApi.UserDevicesResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function pause(service: SpotifyService, deviceId: string) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n\n    const pauseUrl = getUrlWithParams(\n        \"https://api.spotify.com/v1/me/player/pause\",\n        { device_id: deviceId }\n    );\n    try {\n        const spotifyResult = await axios.put(pauseUrl, {}, config);\n\n        return spotifyResult.data;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n}\n\nexport async function getQueue(service: SpotifyService) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const spotifyResult = await axios.get(\n            `https://api.spotify.com/v1/me/player/queue?limit=50`,\n            config\n        );\n\n        return spotifyResult.data as SpotifyApi.UsersQueueResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function previous(service: SpotifyService, deviceId: string) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const spotifyResult = await axios.post(\n            `https://api.spotify.com/v1/me/player/previous?device_id=${deviceId}`,\n            {},\n            config\n        );\n\n        return spotifyResult.data;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function shuffle(\n    service: SpotifyService,\n    deviceId: string,\n    newShuffleState: boolean\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const spotifyResult = await axios.put(\n            `https://api.spotify.com/v1/me/player/shuffle?state=${newShuffleState}&device_id=${deviceId}`,\n            {},\n            config\n        );\n\n        return spotifyResult.data;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function next(service: SpotifyService, deviceId: string) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const spotifyResult = await axios.post(\n            `https://api.spotify.com/v1/me/player/next?device_id=${deviceId}`,\n            {},\n            config\n        );\n\n        return spotifyResult.data;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getPlaylists(service: SpotifyService) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const getUri = \"https://api.spotify.com/v1/me/playlists\";\n        const spotifyResult = await axios.get(getUri, config);\n\n        return spotifyResult.data as SpotifyApi.ListOfCurrentUsersPlaylistsResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getAlbumTracks(service: SpotifyService, albumId: string) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const getUri = `https://api.spotify.com/v1/albums/${encodeURIComponent(\n            albumId\n        )}/tracks`;\n        const spotifyResult = await axios.get(getUri, config);\n\n        return spotifyResult.data as SpotifyApi.AlbumTracksResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function getPlaylistTracks(\n    service: SpotifyService,\n    playlistId: string\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const getUri = `https://api.spotify.com/v1/playlists/${encodeURIComponent(\n            playlistId\n        )}/tracks`;\n        const spotifyResult = await axios.get(getUri, config);\n\n        return spotifyResult.data as SpotifyApi.PlaylistTrackResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function deletePlaylist(\n    service: SpotifyService,\n    playlistId: string\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const deleteUri = `https://api.spotify.com/v1/playlists/${encodeURIComponent(\n            playlistId\n        )}/followers`;\n        const spotifyResult = await axios.delete(deleteUri, config);\n\n        return spotifyResult.data as SpotifyApi.UnfollowPlaylistResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function createPlaylist(\n    service: SpotifyService,\n    name: string,\n    userId: string,\n    uris: string[],\n    description = \"\"\n) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n    try {\n        const createUri = `https://api.spotify.com/v1/users/${userId}/playlists`;\n        const spotifyResult = await axios.post(\n            createUri,\n            { name, public: false, description },\n            config\n        );\n        const playlistResponse =\n            spotifyResult.data as SpotifyApi.CreatePlaylistResponse;\n        const addTracksResult = await axios.post(\n            `https://api.spotify.com/v1/playlists/${playlistResponse.id}/tracks`,\n            { uris },\n            config\n        );\n        return addTracksResult.data as SpotifyApi.AddTracksToPlaylistResponse;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n    return undefined;\n}\n\nexport async function setVolume(service: SpotifyService, amt = limitMax) {\n    const config = {\n        headers: {\n            Authorization: `Bearer ${service.retrieveUser().token}`,\n        },\n    };\n\n    const volumeUrl = getUrlWithParams(\n        \"https://api.spotify.com/v1/me/player/volume?volume_percent\",\n        {\n            volume_percent: amt,\n        }\n    );\n    try {\n        const spotifyResult = await axios.put(volumeUrl, {}, config);\n\n        return spotifyResult.data;\n    } catch (e) {\n        if (e instanceof axios.AxiosError) {\n            console.log(e.message);\n        } else {\n            throw e;\n        }\n    }\n}\n\nfunction getUrlWithParams(urlString: string, queryParams: Record<string, any>) {\n    const params = new URLSearchParams(queryParams);\n    const url = new URL(urlString);\n    url.search = params.toString();\n    return url.toString();\n}\n"
  },
  {
    "path": "typescript/examples/music/src/input.txt",
    "content": "play Taylor Swift Shake It Off\r\nget my top 20 favorites and make a playlist named animalTracks of the tracks that have animals in their names\r\nget my favorite 100 tracks from the last two months and show only the ones by Bach\r\nmake it loud\r\nget my favorite 80 tracks from the last 8 months and create one playlist named class8 containing the classical tracks and another playlist containing the blues tracks\r\ntoggle shuffle on and skip to the next track\r\ngo back to the last song\r\nplay my playlist class8\r\nplay the fourth one\r\nshow me my queue\r\n"
  },
  {
    "path": "typescript/examples/music/src/localParser.ts",
    "content": "import chalk from \"chalk\";\r\nimport axios from \"axios\";\r\nimport path from \"path\";\r\nimport dotenv from \"dotenv\";\r\n\r\ndotenv.config({ path: path.join(__dirname, \"../../../.env\") });\r\n\r\nexport async function parseOut(request: string, surl: string) {\r\n    try {\r\n        const result = await axios.post(surl, {\r\n            Text: request,\r\n        });\r\n        console.log(result.data);\r\n    } catch (e) {\r\n        if (e instanceof axios.AxiosError) {\r\n            console.log(e.message);\r\n        } else {\r\n            throw e;\r\n        }\r\n    }\r\n}\r\n\r\nexport function localParser(userPrompt: string) {\r\n    userPrompt = userPrompt.trim();\r\n    const surl = process.env.PARSER_SERVICE_ENDPOINT;\r\n    if (surl) {\r\n       parseOut(userPrompt, surl);\r\n    }\r\n    if (\r\n        userPrompt === \"play\" ||\r\n        userPrompt === \"resume\" ||\r\n        userPrompt === \"pause\" ||\r\n        userPrompt === \"next\" ||\r\n        userPrompt === \"previous\"\r\n    ) {\r\n        console.log(chalk.green(\"Instance parsed locally:\"));\r\n        return JSON.stringify({\r\n            \"@steps\": [\r\n                {\r\n                    \"@func\": userPrompt === \"play\" ? \"resume\" : userPrompt,\r\n                    \"@args\": [],\r\n                },\r\n            ],\r\n        });\r\n    } else if (userPrompt.startsWith(\"play\")) {\r\n        const matchedPlaySelect = userPrompt.match(\r\n            /play (T|t|track|Track|#|number|Number|no.|No.)?\\s?([0-9]+)/\r\n        );\r\n        if (matchedPlaySelect) {\r\n            const trackOffset = +matchedPlaySelect[2];\r\n            console.log(chalk.green(\"Instance parsed locally:\"));\r\n            return JSON.stringify({\r\n                \"@steps\": [\r\n                    {\r\n                        \"@func\": \"getLastTrackList\",\r\n                        \"@args\": [],\r\n                    },\r\n                    {\r\n                        \"@func\": \"play\",\r\n                        \"@args\": [{ \"@ref\": 0 }, trackOffset - 1],\r\n                    },\r\n                ],\r\n            });\r\n        }\r\n    } else if (userPrompt.startsWith(\"shuffle\")) {\r\n        const matchedShuffleSet = userPrompt.match(\r\n            /shuffle (on|off|true|false|yes|no)/\r\n        );\r\n        if (matchedShuffleSet) {\r\n            const shuffleArg = matchedShuffleSet[1];\r\n            let shuffleFunc = \"\";\r\n            if ([\"on\", \"true\", \"yes\"].includes(shuffleArg)) {\r\n                shuffleFunc = \"shuffleOn\";\r\n            } else if ([\"off\", \"false\", \"no\"].includes(shuffleArg)) {\r\n                shuffleFunc = \"shuffleOff\";\r\n            }\r\n            if (shuffleFunc.length > 0) {\r\n                return JSON.stringify({\r\n                    \"@steps\": [\r\n                        {\r\n                            \"@func\": shuffleFunc,\r\n                            \"@args\": [],\r\n                        },\r\n                    ],\r\n                });\r\n            }\r\n        }\r\n    }\r\n    return undefined;\r\n}\r\n"
  },
  {
    "path": "typescript/examples/music/src/main.ts",
    "content": "import fs from \"fs\";\nimport path from \"path\";\nimport readline from \"readline/promises\";\nimport { Authzor } from \"./authz\";\nimport chalk from \"chalk\";\nimport dotenv from \"dotenv\";\nimport * as Filter from \"./trackFilter\";\nimport {\n    createLanguageModel,\n    getData,\n} from \"typechat\";\nimport {\n    createProgramTranslator,\n    Program,\n    createModuleTextFromProgram,\n    evaluateJsonProgram,\n} from \"typechat/ts\";\nimport {\n    AlbumTrackCollection,\n    ITrackCollection,\n    PlaylistTrackCollection,\n    TrackCollection,\n} from \"./trackCollections\";\nimport { applyFilterExpr } from \"./trackFilter\";\nimport {\n    play,\n    getUserProfile,\n    getDevices,\n    search,\n    setVolume,\n    limitMax,\n    getTopK,\n    createPlaylist,\n    deletePlaylist,\n    getPlaylists,\n    getPlaybackState,\n    getPlaylistTracks,\n    pause,\n    next,\n    previous,\n    shuffle,\n    getAlbumTracks,\n    getQueue,\n    getRecent,\n} from \"./endpoints\";\nimport { listAvailableDevices, printStatus, selectDevice } from \"./playback\";\nimport { SpotifyService, User } from \"./service\";\nimport { localParser } from \"./localParser\";\n\ndotenv.config({ path: path.join(__dirname, \"../../../.env\") });\nconst schemaFilename = \"chatifyActionsSchema.ts\";\nconst model = createLanguageModel(process.env);\n\n// open schema file containing ts definitions\nconst schemaText = fs.readFileSync(\n    path.join(__dirname, schemaFilename),\n    \"utf8\"\n);\n\nconst keys = {\n    clientId: process.env.SPOTIFY_APP_CLI,\n    clientSecret: process.env.SPOTIFY_APP_CLISEC,\n};\n\nexport interface IClientContext {\n    service: SpotifyService;\n    deviceId?: string;\n    user: User;\n    lastTrackList?: SpotifyApi.TrackObjectFull[];\n    lastTrackOffset: number;\n    lastTrackCount: number;\n}\n\nasync function printTrackNames(\n    tracks: SpotifyApi.TrackObjectFull[],\n    context: IClientContext\n) {\n    let count = 1;\n    for (const track of tracks) {\n        let prefix = \"\";\n        if (context && tracks.length > 1) {\n            prefix = `T${count}: `;\n        }\n        console.log(chalk.cyanBright(`${prefix}${track.name}`));\n        const artists =\n            \"   Artists: \" +\n            track.artists.map((artist) => chalk.green(artist.name)).join(\", \");\n        console.log(artists);\n        console.log(\"   Album: \" + chalk.rgb(181, 101, 29)(track.album.name));\n\n        count++;\n    }\n    if (tracks.length > 1) {\n        context.lastTrackList = tracks;\n        context.lastTrackOffset = 0;\n        context.lastTrackCount = count;\n    }\n}\n\nasync function printPlaylist(\n    playlist: SpotifyApi.PlaylistObjectSimplified,\n    fetchedTracks: SpotifyApi.TrackObjectFull[],\n    context: IClientContext\n) {\n    console.log(chalk.cyanBright(`Starting playlist --> ${playlist.name}`));\n    console.log(\n        chalk.cyanBright(`--------------------------------------------`)\n    );\n    const playlistTotalTracks = playlist.tracks.total;\n    console.log(\n        chalk.cyan(\n            `First ${fetchedTracks.length} out of ${playlistTotalTracks} songs in list`\n        )\n    );\n    fetchedTracks.forEach((track, i) => {\n        console.log(\n            chalk.cyan(\n                ` ${i < 99 ? (i < 9 ? \"  \" : \" \") : \"\"}${i + 1} - ${track.name}`\n            )\n        );\n    });\n    console.log(\n        chalk.cyanBright(`--------------------------------------------`)\n    );\n}\n\nfunction chalkPlan(plan: Program) {\n    console.log(chalk.green(\"Plan Validated:\"));\n    const lines = JSON.stringify(plan, null, 4).split(\"\\n\");\n    for (let i = 0; i < lines.length; i++) {\n        lines[i] = lines[i].replace(\n            /\"([^\"]+)\"(:?)|([0-9]+)/g,\n            (match, word, colon, integer) => {\n                if (integer) {\n                    return chalk.hex(\"#B5CEA8\")(integer);\n                } else if (colon) {\n                    return `\"${chalk.cyan(word)}\":`;\n                } else {\n                    return `\"${chalk.rgb(181, 101, 29)(word)}\"`;\n                }\n            }\n        );\n        console.log(lines[i]);\n    }\n}\n\nasync function getClientContext(token: string) {\n    const clientData = {\n        clientId: keys.clientId ? keys.clientId : \"\",\n        clientSecret: keys.clientSecret ? keys.clientSecret : \"\",\n    };\n\n    const service = new SpotifyService(clientData);\n    service.storeUser({\n        username: \"musicLover\",\n        token,\n    });\n    await service.init();\n    const userdata = await getUserProfile(service);\n    const user = service.retrieveUser();\n    user.id = userdata?.id;\n    user.username = userdata?.display_name;\n    const devices = await getDevices(service);\n    let deviceId;\n    if (devices && devices.devices.length > 0) {\n        const activeDevice =\n            devices.devices.find((device) => device.is_active) ??\n            devices.devices[0];\n        deviceId = activeDevice.id;\n    }\n\n    return {\n        deviceId,\n        service,\n    } as IClientContext;\n}\n\nconst translator = createProgramTranslator(model, schemaText);\n\nasync function handleCall(\n    func: string,\n    args: unknown[],\n    clientContext: IClientContext\n): Promise<unknown> {\n    let result: ITrackCollection | undefined = undefined;\n    switch (func) {\n        case \"play\": {\n            const input = args[0] as ITrackCollection;\n            if (input && input.getTrackCount() > 0) {\n                let startIndex = args[1] ? +args[1] : 0;\n                const count = args[2] ? +args[2] : 1;\n                if (startIndex < 0) {\n                    startIndex = input.getTrackCount() + startIndex;\n                }\n                const fetchedTracks = await input.getTracks(\n                    clientContext.service\n                );\n                const contextUri = input.getContext();\n                const tracks = fetchedTracks!.slice(\n                    startIndex,\n                    startIndex + count\n                );\n                const uris = tracks.map((track) => track.uri);\n                console.log(chalk.cyanBright(\"Playing...\"));\n                printTrackNames(tracks, clientContext);\n                if (clientContext.deviceId) {\n                    await play(\n                        clientContext.service,\n                        clientContext.deviceId,\n                        uris,\n                        contextUri\n                    );\n                }\n            } else if (clientContext.deviceId) {\n                await play(clientContext.service, clientContext.deviceId);\n            }\n            break;\n        }\n        case \"printTracks\": {\n            const input = args[0] as ITrackCollection;\n            if (input) {\n                const fetchedTracks = await input.getTracks(\n                    clientContext.service\n                );\n                const playlist = input.getPlaylist();\n                if (playlist) {\n                    printPlaylist(playlist, fetchedTracks, clientContext);\n                } else {\n                    printTrackNames(fetchedTracks, clientContext);\n                }\n            }\n            break;\n        }\n        case \"status\": {\n            await printStatus(clientContext);\n            break;\n        }\n        case \"getQueue\": {\n            const currentQueue = await getQueue(clientContext.service);\n            if (currentQueue) {\n                // not yet supporting episidoes\n                const filtered = currentQueue.queue.filter(\n                    (item) => item.type === \"track\"\n                ) as SpotifyApi.TrackObjectFull[];\n                console.log(chalk.magentaBright(\"Current Queue:\"));\n                console.log(\n                    chalk.cyanBright(\n                        `--------------------------------------------`\n                    )\n                );\n                await printTrackNames(filtered, clientContext);\n                console.log(\n                    chalk.cyanBright(\n                        `--------------------------------------------`\n                    )\n                );\n                await printStatus(clientContext);\n            }\n            break;\n        }\n        case \"pause\": {\n            if (clientContext.deviceId) {\n                await pause(clientContext.service, clientContext.deviceId);\n                await printStatus(clientContext);\n            }\n            break;\n        }\n        case \"next\": {\n            if (clientContext.deviceId) {\n                await next(clientContext.service, clientContext.deviceId);\n                await printStatus(clientContext);\n            }\n            break;\n        }\n        case \"previous\": {\n            if (clientContext.deviceId) {\n                await previous(clientContext.service, clientContext.deviceId);\n                await printStatus(clientContext);\n            }\n            break;\n        }\n        case \"shuffleOn\": {\n            if (clientContext.deviceId) {\n                await shuffle(\n                    clientContext.service,\n                    clientContext.deviceId,\n                    true\n                );\n                await printStatus(clientContext);\n            }\n            break;\n        }\n        case \"shuffleOff\": {\n            if (clientContext.deviceId) {\n                await shuffle(\n                    clientContext.service,\n                    clientContext.deviceId,\n                    false\n                );\n                await printStatus(clientContext);\n            }\n            break;\n        }\n        case \"resume\": {\n            if (clientContext.deviceId) {\n                await play(clientContext.service, clientContext.deviceId);\n                await printStatus(clientContext);\n            }\n            break;\n        }\n        case \"listDevices\": {\n            await listAvailableDevices(clientContext);\n            break;\n        }\n        case \"selectDevice\": {\n            if (clientContext.deviceId) {\n                const keyword = args[0] as string;\n                await selectDevice(keyword, clientContext);\n            }\n            break;\n        }\n        case \"setVolume\": {\n            let newVolumeLevel = args[0] as number;\n            if (newVolumeLevel > 50) {\n                newVolumeLevel = 50;\n            }\n            console.log(\n                chalk.yellowBright(`setting volume to ${newVolumeLevel} ...`)\n            );\n            await setVolume(clientContext.service, newVolumeLevel);\n            break;\n        }\n        case \"changeVolume\": {\n            const volumeChangeAmount = args[0] as number;\n            const playback = await getPlaybackState(clientContext.service);\n            if (playback && playback.device) {\n                const volpct = playback.device.volume_percent || 50;\n                let nv = Math.floor(\n                    (1.0 + volumeChangeAmount / 100.0) * volpct\n                );\n                if (nv > 50) {\n                    nv = 50;\n                }\n                console.log(chalk.yellowBright(`setting volume to ${nv} ...`));\n                await setVolume(clientContext.service, nv);\n            }\n            break;\n        }\n        case \"searchTracks\": {\n            const queryString = args[0] as string;\n            const query: SpotifyApi.SearchForItemParameterObject = {\n                q: queryString,\n                type: \"track\",\n                limit: 50,\n                offset: 0,\n            };\n            const data = await search(query, clientContext.service);\n            if (data && data.tracks) {\n                result = new TrackCollection(\n                    data.tracks.items,\n                    data.tracks.items.length\n                );\n                1;\n            }\n            break;\n        }\n        case \"getLastTrackList\": {\n            if (clientContext && clientContext.lastTrackList) {\n                result = new TrackCollection(\n                    clientContext.lastTrackList,\n                    clientContext.lastTrackCount\n                );\n            }\n            break;\n        }\n        case \"listPlaylists\": {\n            const playlists = await getPlaylists(clientContext.service);\n            if (playlists) {\n                for (const playlist of playlists.items) {\n                    console.log(chalk.magentaBright(`${playlist.name}`));\n                }\n            }\n            break;\n        }\n        case \"getPlaylist\": {\n            const playlistName = args[0] as string;\n            const playlists = await getPlaylists(clientContext.service);\n            const playlist = playlists?.items.find((playlist) => {\n                return playlist.name\n                    .toLowerCase()\n                    .includes(playlistName.toLowerCase());\n            });\n            if (playlist) {\n                const playlistResponse = await getPlaylistTracks(\n                    clientContext.service,\n                    playlist.id\n                );\n                // TODO: add paging\n                if (playlistResponse) {\n                    result = new PlaylistTrackCollection(\n                        playlist,\n                        playlistResponse.items.map((item) => item.track!)\n                    );\n                }\n            }\n            break;\n        }\n        case \"getAlbum\": {\n            const name = args[0] as string;\n            if (name.length > 0) {\n                // search for album by name and load it as track collection\n            } else {\n                // get album of current playing track and load it as track collection\n                const status = await getPlaybackState(clientContext.service);\n                if (status && status.item && status.item.type === \"track\") {\n                    const track = status.item as SpotifyApi.TrackObjectFull;\n                    const album = track.album;\n                    // TODO: add paging\n                    const getTracksResponse = await getAlbumTracks(\n                        clientContext.service,\n                        album.id\n                    );\n                    if (status.is_playing) {\n                        await play(\n                            clientContext.service,\n                            clientContext.deviceId!,\n                            [],\n                            album.uri,\n                            status.item.track_number - 1,\n                            status.progress_ms ? status.progress_ms : 0\n                        );\n                    }\n                    if (getTracksResponse) {\n                        result = new AlbumTrackCollection(\n                            album,\n                            getTracksResponse.items\n                        );\n                    }\n                }\n            }\n            break;\n        }\n        case \"getFavorites\": {\n            const countOption = args[0] as number;\n            let count = limitMax;\n            if (countOption !== undefined) {\n                count = countOption;\n            }\n            const tops = await getTopK(clientContext.service, count);\n            if (tops) {\n                const tracks = tops.map((pto) => pto.track!);\n                result = new TrackCollection(tracks, tracks.length);\n            }\n            break;\n        }\n        case \"filterTracks\": {\n            const trackCollection = args[0] as ITrackCollection;\n            let filterType = args[1] as string;\n            const filterText = args[2] as string;\n            const negate = args[3] as boolean;\n            // TODO: add filter validation to overall instance validation\n            if (filterType === \"name\") {\n                filterType = \"description\";\n            }\n            const filter = filterType + \":\" + filterText;\n            const parseResult = Filter.parseFilter(filter);\n            if (parseResult.ast) {\n                const trackList = await trackCollection.getTracks(\n                    clientContext.service\n                );\n                if (trackList) {\n                    const tracks = await applyFilterExpr(\n                        clientContext,\n                        model,\n                        parseResult.ast,\n                        trackList,\n                        negate\n                    );\n                    result = new TrackCollection(tracks, tracks.length);\n                }\n            } else {\n                console.log(parseResult.diagnostics);\n            }\n            break;\n        }\n        case \"createPlaylist\": {\n            const input = args[0] as ITrackCollection;\n            const name = args[1] as string;\n            const trackList = await input.getTracks(clientContext.service);\n            if (input && trackList.length > 0) {\n                const uris = trackList.map((track) => (track ? track.uri : \"\"));\n                await createPlaylist(\n                    clientContext.service,\n                    name,\n                    clientContext.service.retrieveUser().id!,\n                    uris,\n                    name\n                );\n                console.log(`playlist ${name} created with tracks:`);\n                printTrackNames(trackList, clientContext);\n            } else {\n                console.log(chalk.red(\"no input tracks for createPlaylist\"));\n            }\n            break;\n        }\n        case \"deletePlaylist\": {\n            const playlistCollection = args[0] as PlaylistTrackCollection;\n            if (playlistCollection) {\n                const playlist = playlistCollection.getPlaylist();\n                await deletePlaylist(clientContext.service, playlist.id);\n                console.log(\n                    chalk.magentaBright(`playlist ${playlist.name} deleted`)\n                );\n                break;\n            }\n\n            break;\n        }\n        case \"unknownAction\": {\n            const text = args[0] as string;\n            console.log(`Text not understood in this context: ${text}`);\n            break;\n        }\n        case \"nonMusicQuestion\": {\n            const text = args[0] as string;\n            const ret = await model.complete(text);\n            if (ret.success) {\n                console.log(ret.data);\n            }\n            break;\n        }\n    }\n    return result;\n}\n\n// set this to false to just look at llm generation without Spotify connection\nconst spotifyConnect = true;\n\nexport async function index(context: IClientContext) {\n    let playHistory = await getRecent(\n        context.service,\n        Date.parse(\"2018-01-01T00:00:00.00Z\")\n    );\n    if (playHistory) {\n        console.log(playHistory?.length);\n        let trackNames = '';\n        playHistory.map((item) => {\n            trackNames += item.track.name + '\\n';\n        });\n        fs.writeFileSync(\"bigFetch.txt\", trackNames);\n    }\n}\n\nfunction checkAck(input: string, program: Program): Program | undefined {\n    const linput = input.toLocaleLowerCase();\n    if ([\"y\",\"yes\",\"ok\"].includes(linput)) {\n        return program;\n    } else {\n        return undefined;\n    }\n}\n\n// whether to confirm each action with the user\nconst confirmMode = true;\n\n// Process requests interactively (no batch mode for now)\nasync function musicApp() {\n    const authz = new Authzor();\n    authz.authorize(spotifyConnect, async (token) => {\n        let context: IClientContext | undefined = undefined;\n        if (token) {\n            context = await getClientContext(token);\n        } else {\n            console.log(\n                chalk.yellow(\n                    \"Spotify connection not active: showing plans only\"\n                )\n            );\n        }\n        const musicPrompt = \"🎵> \";\n        const confirmPrompt = \"👍👎 (answer y/n)> \";\n        const stdio = readline.createInterface({ input: process.stdin, output: process.stdout });\n        while (true) {\n            const request = await stdio.question(musicPrompt);\n            if (request.toLowerCase() === \"quit\" || request.toLowerCase() === \"exit\") {\n                stdio.close();\n                return;\n            }\n            const localResult = localParser(request);\n            let program: Program | undefined = undefined;\n            if (localResult) {\n                program = JSON.parse(localResult) as Program;\n            } else {\n                const response = await translator.translate(request);\n                if (!response.success) {\n                    console.log(response.message);\n                    continue;\n                }\n                program = response.data;\n            }\n            if (program !== undefined) {\n                chalkPlan(program);\n                console.log(getData(createModuleTextFromProgram(program)));\n                if (confirmMode && (!localResult)) {\n                     const input = await stdio.question(confirmPrompt);\n                     program = checkAck(input, program);\n                     if (program === undefined) {\n                        console.log(\"Thanks for the feedback. Canceling execution...\")\n                        continue;\n                     }                       \n                }\n                if (context !== undefined) {\n                    const result = await evaluateJsonProgram(\n                        program,\n                        async (func, args) => {\n                            return await handleCall(func, args, context!);\n                        }\n                    );\n                    if (result !== undefined) {\n                        const collection = result as ITrackCollection;\n                        const trackList = await collection.getTracks(\n                            context.service\n                        );\n                        if (trackList) {\n                            printTrackNames(trackList, context);\n                            context.lastTrackList = trackList;\n                        }\n                    }\n                }\n            }\n        }\n    });\n}\n\nmusicApp();\n"
  },
  {
    "path": "typescript/examples/music/src/playback.ts",
    "content": "import { getDevices, getPlaybackState, transferPlayback } from \"./endpoints\";\nimport { IClientContext } from \"./main\";\nimport chalk from \"chalk\";\n\n// convert milliseconds to elapsed minutes and seconds as a string\nfunction msToElapsedMinSec(ms: number) {\n  const seconds = Math.floor(ms / 1000);\n  const minutes = Math.floor(seconds / 60);\n  const remainingSeconds = seconds % 60;\n  // add leading zero if needed\n  if (remainingSeconds < 10) {\n      return `${minutes}:0${remainingSeconds}`;\n  } else {\n      return `${minutes}:${remainingSeconds}`;\n  }\n}\n\nconst pauseSymbol = \"⏸️\";\nconst playSymbol = \"▶️\";\n\nexport function chalkStatus(status: SpotifyApi.CurrentPlaybackResponse) {\n  if (status.item) {\n      let timePart = msToElapsedMinSec(status.item.duration_ms);\n      if (status.progress_ms) {\n          timePart = `${msToElapsedMinSec(status.progress_ms)}/${timePart}`;\n      }\n      let symbol = status.is_playing ? playSymbol : pauseSymbol;\n      console.log(\n          `${symbol}  ${timePart}  ${chalk.cyanBright(status.item.name)}`\n      );\n      if (status.item.type === \"track\") {\n          const artists =\n              \"   Artists: \" +\n              status.item.artists\n                  .map((artist) => chalk.green(artist.name))\n                  .join(\", \");\n          console.log(artists);\n      }\n  }\n}\n\nexport async function printStatus(context: IClientContext) {\n  const status = await getPlaybackState(context.service);\n  if (status) {\n    chalkStatus(status);\n  } else {\n    console.log(\"Nothing playing according to Spotify.\");\n  }\n  const devices = await getDevices(context.service);\n  if (devices && devices.devices.length > 0) {\n    const activeDevice =\n      devices.devices.find((device) => device.is_active) ?? devices.devices[0];\n    if (activeDevice) {\n      console.log(\n        \"   Active device: \" +\n          chalk.magenta(`${activeDevice.name} of type ${activeDevice.type}`)\n      );\n    } else {\n      for (const device of devices.devices) {\n        console.log(\n          chalk.magenta(\n            `   Device ${device.name} of type ${device.type} is available`\n          )\n        );\n      }\n    }\n  }\n}\n\nexport async function selectDevice(keyword: string, context: IClientContext) {\n  const devices = await getDevices(context.service);\n  if (devices && devices.devices.length > 0) {\n    for (const device of devices.devices) {\n      if (\n        device.name.toLowerCase().includes(keyword.toLowerCase()) ||\n        device.type.toLowerCase().includes(keyword.toLowerCase())\n      ) {\n        const status = await getPlaybackState(context.service);\n        if (status) {\n          if (status.device.id === device.id) {\n            console.log(\n              chalk.green(`Device ${device.name} is already selected`)\n            );\n            return;\n          }\n          await transferPlayback(\n            context.service,\n            device.id!,\n            status.is_playing\n          );\n        }\n        context.deviceId = device.id!;\n        console.log(\n          chalk.green(`Selected device ${device.name} of type ${device.type}`)\n        );\n      }\n    }\n  } else {\n    console.log(chalk.red(\"No devices matched keyword\"));\n  }\n}\n\nexport async function listAvailableDevices(context: IClientContext) {\n  const devices = await getDevices(context.service);\n  if (devices && devices.devices.length > 0) {\n    let count = 0;\n    for (const device of devices.devices) {\n      console.log(\n        chalk.magenta(\n          `Device ${device.name} of type ${device.type} is available`\n        )\n      );\n      count++;\n    }\n  }\n}\n\n"
  },
  {
    "path": "typescript/examples/music/src/service.ts",
    "content": "import axios from 'axios';\n\nexport type ClientData = {\n    clientId: string;\n    clientSecret: string;\n};\n\nexport type User = {\n    username?: string;\n    token: string;\n    id?: string;\n};\n\nexport class SpotifyService {\n    private accessToken?: string;\n    private clientId: string;\n    private clientSecret: string;\n    private loggedIn: boolean;\n    private loggedInUser: User | null;\n\n    constructor(clientData: ClientData) {\n        this.clientId = clientData.clientId;\n        this.clientSecret = clientData.clientSecret;\n        this.loggedIn = false;\n        this.loggedInUser = null;\n    }\n\n    storeToken(token: string): string {\n        this.accessToken = token;\n        return this.accessToken;\n    }\n\n    retrieveToken(): string {\n        if (!this.accessToken) {\n            throw new Error('SpotifyService: no accessToken');\n        }\n        return this.accessToken;\n    }\n\n    isLoggedIn(): boolean {\n        return this.loggedIn;\n    }\n\n    retrieveUser(): User {\n        if (this.loggedInUser === null) {\n            throw new Error('SpotifyService: no loggedInUser');\n        }\n        return this.loggedInUser;\n    }\n\n    storeUser(user: User) {\n        this.loggedInUser = user;\n    }\n\n    async init(): Promise<Object> {\n        const authConfig = {\n            headers: {\n                Authorization: `Basic ${Buffer.from(\n                    `${this.clientId}:${this.clientSecret}`\n                ).toString('base64')}`,\n            },\n        };\n\n        try {\n            const authData = await axios.post(\n                'https://accounts.spotify.com/api/token',\n                'grant_type=client_credentials',\n                authConfig\n            );\n            this.storeToken(authData.data.access_token);\n\n            return authData.data;\n        } catch (e) {\n            // TODO: REVIEW: should we really be returning the response\n            // data in an error condition?\n            if (e instanceof axios.AxiosError) {\n                // TODO: REVIEW: the type returned here may not be Promise<Object>\n                return e.response?.data;\n            } else {\n                throw e;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "typescript/examples/music/src/trackCollections.ts",
    "content": "import { SpotifyService } from \"./service\";\r\n\r\n// for now, no paging of track lists; later add offset and count\r\nexport interface ITrackCollection {\r\n    getTrackCount(): number;\r\n    getTracks(service: SpotifyService): Promise<SpotifyApi.TrackObjectFull[]>;\r\n    getContext(): string | undefined;\r\n    getPlaylist(): SpotifyApi.PlaylistObjectSimplified | undefined;\r\n}\r\n\r\nexport class TrackCollection implements ITrackCollection {\r\n    contextUri: string | undefined = undefined;\r\n\r\n    constructor(public tracks: SpotifyApi.TrackObjectFull[], public trackCount:number) {\r\n\r\n    }\r\n    getContext() {\r\n        return this.contextUri;\r\n    }\r\n\r\n    async getTracks(\r\n        service: SpotifyService\r\n    ) {\r\n        return this.tracks;\r\n    }\r\n\r\n    getTrackCount(): number {\r\n        return this.trackCount;\r\n    }\r\n\r\n    getPlaylist(): SpotifyApi.PlaylistObjectSimplified | undefined {\r\n        return undefined;\r\n    }\r\n}\r\n\r\nexport class PlaylistTrackCollection extends TrackCollection {\r\n    constructor(public playlist: SpotifyApi.PlaylistObjectSimplified, \r\n        tracks: SpotifyApi.TrackObjectFull[]) {\r\n        super(tracks,0);\r\n        this.contextUri = playlist.uri;\r\n        this.trackCount = tracks.length;\r\n    }\r\n\r\n    getPlaylist() {\r\n        return this.playlist;\r\n    }\r\n}\r\n\r\nexport class AlbumTrackCollection extends TrackCollection  {\r\n    constructor(public album: SpotifyApi.AlbumObjectSimplified,\r\n        tracks: SpotifyApi.TrackObjectSimplified[] \r\n    ) {\r\n        super([], 0);\r\n        this.contextUri = album.uri;\r\n        this.trackCount = tracks.length;\r\n        this.tracks = tracks.map((albumItem) => {\r\n            const fullTrack = albumItem as SpotifyApi.TrackObjectFull;\r\n            fullTrack.album = album;\r\n            return fullTrack;\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "typescript/examples/music/src/trackFilter.ts",
    "content": "import { TypeChatLanguageModel } from \"typechat\";\nimport { getArtist } from \"./endpoints\";\nimport { IClientContext } from \"./main\";\n\nexport enum FilterTokenType {\n    Genre,\n    Artist,\n    Year,\n    Description,\n    Colon,\n    AND,\n    OR,\n    LParen,\n    RParen,\n    Value,\n}\n\n// split a string into an array of non-whitespace strings\nfunction splitNonWhitespace(str: string): string[] {\n    const nested = str.split(/\\s+/).map((w) => w.split(/\\b/));\n    return nested.flat().filter((w) => w.length > 0);\n}\n\ninterface FilterToken {\n    type: FilterTokenType;\n    rawValue?: string;\n}\n\nfunction tokenize(filter: string) {\n    const nonws = splitNonWhitespace(filter);\n    const tokens: FilterToken[] = [];\n    for (const rawtok of nonws) {\n        const tok = rawtok.toLowerCase();\n        if (tok === 'and') {\n            tokens.push({ type: FilterTokenType.AND });\n        } else if (tok === 'or') {\n            tokens.push({ type: FilterTokenType.OR });\n        } else if (tok === '(') {\n            tokens.push({ type: FilterTokenType.LParen });\n        } else if (tok === ')') {\n            tokens.push({ type: FilterTokenType.RParen });\n        } else if (tok === ':') {\n            tokens.push({ type: FilterTokenType.Colon });\n        } else if (tok === 'genre') {\n            tokens.push({ type: FilterTokenType.Genre });\n        } else if (tok === 'artist') {\n            tokens.push({ type: FilterTokenType.Artist });\n        } else if (tok === 'year') {\n            tokens.push({ type: FilterTokenType.Year });\n        } else if (tok === 'description') {\n            tokens.push({ type: FilterTokenType.Description });\n        } else {\n            tokens.push({ type: FilterTokenType.Value, rawValue: rawtok });\n        }\n    }\n    return tokens;\n}\n\nexport enum FilterConstraintType {\n    Genre = 'genre',\n    Artist = 'artist',\n    Year = 'year',\n    Description = 'description',\n}\n\nexport enum FilterCombinerType {\n    AND = 'AND',\n    OR = 'OR',\n}\n\nexport interface FilterCombiner {\n    type: 'combiner';\n    combinerType: FilterCombinerType;\n    operands: FilterNode[];\n}\n\nexport interface FilterConstraint {\n    type: 'constraint';\n    constraintType: FilterConstraintType;\n    constraintValue: string;\n}\n\nexport type FilterNode = FilterConstraint | FilterCombiner;\n\nexport interface IFilterResult {\n    diagnostics?: string[];\n    ast?: FilterNode;\n}\n\nfunction makeFilterCombiner(combinerType = FilterCombinerType.AND) {\n    return { type: 'combiner', combinerType, operands: [] } as FilterCombiner;\n}\n\n// map filter token type to filter constraint type\nconst filterConstraintTypeMap = new Map<FilterTokenType, FilterConstraintType>([\n    [FilterTokenType.Genre, FilterConstraintType.Genre],\n    [FilterTokenType.Artist, FilterConstraintType.Artist],\n    [FilterTokenType.Year, FilterConstraintType.Year],\n    [FilterTokenType.Description, FilterConstraintType.Description],\n]);\n\nfunction makeFilterConstraint(\n    constraintType: FilterConstraintType,\n    constraintValue?: string\n) {\n    return {\n        type: 'constraint',\n        constraintType,\n        constraintValue,\n    } as FilterConstraint;\n}\n\nfunction isValueBoundary(tokenType: FilterTokenType) {\n    return (\n        tokenType !== FilterTokenType.Colon &&\n        tokenType !== FilterTokenType.Value\n    );\n}\n\nexport function filterNodeToString(node: FilterNode, depth = 0): string {\n    if (node.type === 'combiner') {\n        return (\n            '(' +\n            node.combinerType +\n            ' ' +\n            node.operands\n                .map((op) => filterNodeToString(op, depth + 1))\n                .join(' ') +\n            ')'\n        );\n    } else {\n        return node.constraintType + ':' + node.constraintValue;\n    }\n}\n\nfunction simplifyFilterNode(ast: FilterNode): FilterNode {\n    if (ast.type === 'combiner') {\n        if (ast.operands.length === 1) {\n            return simplifyFilterNode(ast.operands[0]);\n        } else {\n            for (let i = 0; i < ast.operands.length; i++) {\n                ast.operands[i] = simplifyFilterNode(ast.operands[i]);\n            }\n            return ast;\n        }\n    } else {\n        return ast;\n    }\n}\n\ninterface FilterStackFrame {\n    pendingOr?: FilterCombiner;\n    andExpr: FilterCombiner;\n}\n\nexport function parseFilter(filter: string): IFilterResult {\n    const tokens = tokenize(filter);\n    let pendingConstraint: FilterConstraint | undefined = undefined;\n    const stack: FilterStackFrame[] = [{ andExpr: makeFilterCombiner() }];\n    for (const token of tokens) {\n        if (isValueBoundary(token.type)) {\n            if (pendingConstraint) {\n                stack[stack.length - 1].andExpr.operands.push(\n                    pendingConstraint\n                );\n                pendingConstraint = undefined;\n            }\n        }\n        if (\n            token.type === FilterTokenType.Genre ||\n            token.type === FilterTokenType.Artist ||\n            token.type === FilterTokenType.Year ||\n            token.type === FilterTokenType.Description\n        ) {\n            if (pendingConstraint !== undefined) {\n                return { diagnostics: ['Nested constraint prefix'] };\n            } else {\n                pendingConstraint = makeFilterConstraint(\n                    filterConstraintTypeMap.get(token.type)!,\n                    ''\n                );\n            }\n        } else if (token.type === FilterTokenType.Colon) {\n            if (!pendingConstraint) {\n                return { diagnostics: [\"Expected constraint type before ':'\"] };\n            }\n        } else if (token.type === FilterTokenType.AND) {\n            // do nothing; always in an AND\n        } else if (token.type === FilterTokenType.OR) {\n            const orNode = makeFilterCombiner(FilterCombinerType.OR);\n            const top = stack[stack.length - 1];\n            if (top.pendingOr) {\n                top.pendingOr.operands.push(top.andExpr);\n                orNode.operands.push(top.pendingOr);\n            } else {\n                orNode.operands.push(top.andExpr);\n            }\n            top.pendingOr = orNode;\n            top.andExpr = makeFilterCombiner();\n        } else if (token.type === FilterTokenType.LParen) {\n            stack.push({ andExpr: makeFilterCombiner() });\n        } else if (token.type === FilterTokenType.RParen) {\n            if (stack.length === 1) {\n                return { diagnostics: ['Mismatched )'] };\n            }\n            const prevTop = stack.pop()!;\n            if (prevTop.pendingOr) {\n                prevTop.pendingOr.operands.push(prevTop.andExpr);\n                stack[stack.length - 1].andExpr.operands.push(\n                    prevTop.pendingOr\n                );\n            } else {\n                stack[stack.length - 1].andExpr.operands.push(prevTop.andExpr);\n            }\n        } else if (token.type === FilterTokenType.Value) {\n            if (!pendingConstraint) {\n                console.log(token.rawValue);\n                return {\n                    diagnostics: [\n                        'Unexpected: value without constraint prefix',\n                    ],\n                };\n            } else {\n                if (pendingConstraint.constraintValue.length > 0) {\n                    pendingConstraint.constraintValue += ' ';\n                }\n                pendingConstraint.constraintValue += token.rawValue!;\n            }\n        }\n    }\n    if (pendingConstraint) {\n        stack[stack.length - 1].andExpr.operands.push(pendingConstraint);\n    }\n    if (stack.length !== 1) {\n        return { diagnostics: ['Mismatched ('] };\n    }\n    const top = stack[0];\n    if (top.pendingOr) {\n        top.pendingOr.operands.push(top.andExpr);\n        return { ast: simplifyFilterNode(top.pendingOr) };\n    } else {\n        return { ast: simplifyFilterNode(top.andExpr) };\n    }\n}\n\n\nconst filterDiag = false;\n\nexport async function applyFilterExpr(\n    clientContext: IClientContext,\n    model: TypeChatLanguageModel,\n    filterExpr: FilterNode,\n    tracks: SpotifyApi.TrackObjectFull[],\n    negate = false\n): Promise<SpotifyApi.TrackObjectFull[]> {\n    if (tracks.length === 0) {\n        return tracks;\n    }\n    switch (filterExpr.type) {\n        case \"constraint\":\n            switch (filterExpr.constraintType) {\n                case FilterConstraintType.Genre: {\n                    process.stdout.write(\n                        `fetching genre for ${tracks.length} tracks`\n                    );\n                    const genre = filterExpr.constraintValue;\n                    const results = [] as SpotifyApi.TrackObjectFull[];\n                    for (const track of tracks) {\n                        process.stdout.write(\".\");\n                        const wrapper = await getArtist(\n                            clientContext.service,\n                            track.album.artists[0].id\n                        );\n                        if (wrapper) {\n                            let hit = wrapper.artists[0].genres.includes(genre);\n                            if (negate) {\n                                hit = !hit;\n                            }\n                            if (hit) {\n                                results.push(track);\n                            }\n                        }\n                    }\n                    process.stdout.write(\"\\n\");\n                    tracks = results;\n                    break;\n                }\n                case FilterConstraintType.Artist: {\n                    const results = [] as SpotifyApi.TrackObjectFull[];\n                    for (const track of tracks) {\n                        let hit = false;\n                        for (const artist of track.artists) {\n                            if (filterDiag) {\n                                console.log(\n                                    `${artist.name.toLowerCase()} vs ${filterExpr.constraintValue.toLowerCase()}`\n                                );\n                            }\n                            if (\n                                artist.name\n                                    .toLowerCase()\n                                    .includes(\n                                        filterExpr.constraintValue.toLowerCase()\n                                    )\n                            ) {\n                                hit = true;\n                            }\n                            if (negate) {\n                                hit = !hit;\n                            }\n                            if (hit) {\n                                results.push(track);\n                            }\n                            if (hit) {\n                                break;\n                            }\n                        }\n                    }\n                    process.stdout.write(\"\\n\");\n                    tracks = results;\n                    break;\n                }\n                case FilterConstraintType.Year: {\n                    const results = [] as SpotifyApi.TrackObjectFull[];\n                    for (const track of tracks) {\n                        // TODO year ranges\n                        if (filterDiag) {\n                            console.log(\n                                `${track.album.release_date} vs ${filterExpr.constraintValue}`\n                            );\n                        }\n                        if (\n                            track.album.release_date.includes(\n                                filterExpr.constraintValue\n                            )\n                        ) {\n                            results.push(track);\n                        }\n                    }\n                    tracks = results;\n                    break;\n                }\n                case FilterConstraintType.Description: {\n                    const results = [] as SpotifyApi.TrackObjectFull[];\n\n                    const indicesResult = await llmFilter(\n                        model,\n                        filterExpr.constraintValue,\n                        tracks\n                    );\n                    if (indicesResult.success) {\n                        if (indicesResult.data) {\n                            const indices = JSON.parse(indicesResult.data) as {\n                                trackNumbers: number[];\n                            };\n                            for (const j of indices.trackNumbers) {\n                                results.push(tracks[j]);\n                            }\n                        }\n                    }\n                    tracks = results;\n                    break;\n                }\n            }\n            break;\n        case \"combiner\":\n            if (filterExpr.combinerType === FilterCombinerType.AND) {\n                for (const childExpr of filterExpr.operands) {\n                    tracks = await applyFilterExpr(\n                        clientContext,\n                        model,\n                        childExpr,\n                        tracks,\n                        negate\n                    );\n                }\n            } else if (\n                filterExpr.combinerType === FilterCombinerType.OR\n            ) {\n                let subTracks = [] as SpotifyApi.TrackObjectFull[];\n                for (const childExpr of filterExpr.operands) {\n                    subTracks = subTracks.concat(\n                        await applyFilterExpr(\n                            clientContext,\n                            model,\n                            childExpr,\n                            tracks,\n                            negate\n                        )\n                    );\n                }\n                tracks = uniqueTracks(subTracks);\n            }\n            break;\n    }\n    return tracks;\n}\n\nfunction uniqueTracks(tracks: SpotifyApi.TrackObjectFull[]) {\n    const map = new Map<string, SpotifyApi.TrackObjectFull>();\n    for (const track of tracks) {\n        map.set(track.id, track);\n    }\n    return [...map.values()];\n}\n\nasync function llmFilter(\n    model: TypeChatLanguageModel,\n    description: string,\n    tracks: SpotifyApi.TrackObjectFull[]\n) {\n    let prompt =\n        \"The following is a numbered list of music tracks, one track per line\\n\";\n    for (let i = 0; i < tracks.length; i++) {\n        const track = tracks[i];\n        prompt += `${i}: ${track.name}\\n`;\n    }\n    prompt += `Use the following TypeScript type to output the track names that match the description ${description}:\n    type Matches = {\n        trackNumbers: number[];\n    };\\n`;\n    prompt += `Here is a JSON object of type Matches containing the track numbers of the tracks that match ${description}:\\n`;\n    const ret = await model.complete(prompt);\n    return ret;\n}\n\n\n// the remainder is for testing\nconst testFilters = [\n    'artist:elton john OR artist: bach',\n    'genre:baroque AND description:animals',\n    'genre:baroque OR description:animals',\n    'genre:baroque OR description:animals OR artist:bach',\n    'genre:baroque OR (description:animals OR artist:bach)',\n    'genre:baroque (description :   animals OR artist: bach)',\n    'genre:baroque artist:toscanini (description:animals OR artist:bach AND artist:swift)',\n    'genre:baroque artist:toscanini year: 1941 (description:animals OR artist:bach AND artist:swift)',\n    'genre:grunge artist:cobain year: 1992-1997 OR (description:animals AND artist:swift)',\n    'genre:grunge artist:cobain year: 1992-1997 OR (description:animals AND artist:swift) OR (genre:baroque AND artist:bach)',\n];\n\n// if this is the main module, run some tests\nif (require.main === module) {\n    for (const filter of testFilters) {\n        const result = parseFilter(filter);\n        console.log(filter);\n        if (result.diagnostics) {\n            console.log(result.diagnostics);\n        } else if (result.ast) {\n            console.log(filterNodeToString(result.ast));\n        }\n    }\n}\n"
  },
  {
    "path": "typescript/examples/music/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\", \"spotify-api\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"inlineSourceMap\": true,\n    \"declaration\": true,\n    \"composite\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/restaurant/.vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"skipFiles\": [\n                \"<node_internals>/**\"\n            ],\n            \"program\": \"${workspaceFolder}/dist/main.js\",\n            \"console\": \"externalTerminal\"\n        }\n    ]\n}"
  },
  {
    "path": "typescript/examples/restaurant/README.md",
    "content": "# Restaurant\n\nThe Restaurant example shows how to capture user intent as a set of \"nouns\", but with more complex linguistic input.\nThis example can act as a \"stress test\" for language models, illustrating the line between simpler and more advanced language models in handling compound sentences, distractions, and corrections.\nThis example also shows how we can create a \"user intent summary\" to display to a user.\nIt uses a natural language experience for placing an order with the [`Order`](./src/foodOrderViewSchema.ts) type.\n\n# Try Restaurant\n\nTo run the Restaurant example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\n\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\n\nFor example, given the following order:\n\n**Input**:\n\n```\n🍕> I want three pizzas, one with mushrooms and the other two with sausage. Make one sausage a small. And give me a whole Greek and a Pale Ale. And give me a Mack and Jacks.\n```\n\n**Output**:\n\n*This is GPT-4-0613 output; GPT-3.5-turbo and most other models miss this one.*\n\n```\n1 large pizza with mushrooms\n1 large pizza with sausage\n1 small pizza with sausage\n1 whole Greek salad\n1 Pale Ale\n1 Mack and Jacks\n```\n\n> **Note**\n>\n> Across different models, you may see that model responses may not correspond to the user intent.\n> In the above example, some models may not be able to capture the fact that the order is still only for 3 pizzas,\n> and that \"make one sausage a small\" is not a request for a new pizza.\n> \n> ```diff\n>   1 large pizza with mushrooms\n> - 1 large pizza with sausage\n> + 2 large pizza with sausage\n>   1 small pizza with sausage\n>   1 whole Greek salad\n>   1 Pale Ale\n>   1 Mack and Jacks\n> ```\n>\n> The output here from GPT 3.5-turbo incorrectly shows 1 mushroom pizza and 3 sausage pizzas.\n\nBecause all language models are probabilistic and therefore will sometimes output incorrect inferences, the TypeChat pattern includes asking the user for confirmation (or giving the user an easy way to undo actions).  It is important to ask for confirmation without use of the language model so that incorrect inference is guaranteed not to be part of the intent summary generated.\n\nIn this example, the function `printOrder` in the file `main.ts` summarizes the food order (as seen in the above output) without use of a language model.  The `printOrder` function can work with a strongly typed `Order object` because the TypeChat validation process has checked that the emitted JSON corresponds to the `Order` type:\n\n```typescript\nfunction printOrder(order: Order) {\n```\n\nHaving a validated, typed data structure simplifies the task of generating a succinct summary suitable for user confirmation."
  },
  {
    "path": "typescript/examples/restaurant/package.json",
    "content": "{\n  \"name\": \"restaurant\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/restaurant/src/foodOrderViewSchema.ts",
    "content": "// an order from a restaurant that serves pizza, beer, and salad\nexport type Order = {\n    items: (OrderItem | UnknownText)[];\n};\n\nexport type OrderItem = Pizza | Beer | Salad;\n\n// Use this type for order items that match nothing else\nexport interface UnknownText {\n    itemType: \"unknown\",\n    text: string; // The text that wasn't understood\n}\n\nexport type Pizza = {\n    itemType: \"pizza\";\n    // default: large\n    size?: \"small\" | \"medium\" | \"large\" | \"extra large\";\n    // toppings requested (examples: pepperoni, arugula)\n    addedToppings?: string[];\n    // toppings requested to be removed (examples: fresh garlic, anchovies)\n    removedToppings?: string[];\n    // default: 1\n    quantity?: number;\n    // used if the requester references a pizza by name\n    name?: \"Hawaiian\" | \"Yeti\" | \"Pig In a Forest\" | \"Cherry Bomb\";\n};\n\nexport type Beer = {\n    itemType: \"beer\";\n    // examples: Mack and Jacks, Sierra Nevada Pale Ale, Miller Lite\n    kind: string;\n    // default: 1\n    quantity?: number;\n};\n\nexport const saladSize = [\"half\", \"whole\"];\n\nexport const saladStyle = [\"Garden\", \"Greek\"];\n\nexport type Salad = {\n    itemType: \"salad\";\n    // default: half\n    portion?: string;\n    // default: Garden\n    style?: string;\n    // ingredients requested (examples: parmesan, croutons)\n    addedIngredients?: string[];\n    // ingredients requested to be removed (example: red onions)\n    removedIngredients?: string[];\n    // default: 1\n    quantity?: number;\n};\n\n"
  },
  {
    "path": "typescript/examples/restaurant/src/input.txt",
    "content": "I'd like two large, one with pepperoni and the other with extra sauce.  The pepperoni gets basil and the extra sauce gets Canadian bacon.  And add a whole salad. Make the Canadian bacon a medium. Make the salad a Greek with no red onions.  And give me two Mack and Jacks and a Sierra Nevada.  Oh, and add another salad with no red onions.\nI'd like two large with olives and mushrooms.  And the first one gets extra sauce.  The second one gets basil.  Both get arugula.  And add a Pale Ale. Give me a two Greeks with no red onions, a half and a whole.  And a large with sausage and mushrooms.  Plus three Pale Ales and a Mack and Jacks.\nI'll take two large with pepperoni.  Put olives on one of them. Make the olive a small.  And give me whole Greek plus a Pale Ale and an M&J.\nI want three pizzas, one with mushrooms and the other two with sausage.  Make one sausage a small.  And give me a whole Greek and a Pale Ale.  And give me a Mack and Jacks.\nI would like to order one with basil and one with extra sauce.  Throw in a salad and an ale.\nI would love to have a pepperoni with extra sauce, basil and arugula. Lovely weather we're having. Throw in some pineapple.  And give me a whole Greek and a Pale Ale.  Boy, those Mariners are doggin it. And how about a Mack and Jacks.\nI'll have two pepperoni, the first with extra sauce and the second with basil.  Add pineapple to the first and add olives to the second.\nI sure am hungry for a pizza with pepperoni and a salad with no croutons.  And I'm thirsty for 3 Pale Ales\ngive me three regular salads and two Greeks and make the regular ones with no red onions\nI'll take four large pepperoni pizzas.  Put extra sauce on two of them.  plus an M&J and a Pale Ale\nI'll take a yeti, a pale ale and a large with olives and take the extra cheese off the yeti and add a Greek\nI'll take a medium Pig with no arugula\nI'll take a small Pig with no arugula and a Greek with croutons and no red onions\n\n"
  },
  {
    "path": "typescript/examples/restaurant/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport {\n  createJsonTranslator,\n  createLanguageModel,\n} from \"typechat\";\nimport {\n  processRequests,\n} from \"typechat/interactive\";\nimport {\n  createTypeScriptJsonValidator,\n} from \"typechat/ts\";\nimport { Order } from \"./foodOrderViewSchema\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst viewSchema = fs.readFileSync(\n  path.join(__dirname, \"foodOrderViewSchema.ts\"),\n  \"utf8\"\n);\nconst validator = createTypeScriptJsonValidator<Order>(viewSchema, \"Order\");\nconst translator = createJsonTranslator(model, validator);\n\nconst saladIngredients = [\n  \"lettuce\",\n  \"tomatoes\",\n  \"red onions\",\n  \"olives\",\n  \"peppers\",\n  \"parmesan\",\n  \"croutons\",\n];\n\nconst pizzaToppings = [\n  \"pepperoni\",\n  \"sausage\",\n  \"mushrooms\",\n  \"basil\",\n  \"extra cheese\",\n  \"extra sauce\",\n  \"anchovies\",\n  \"pineapple\",\n  \"olives\",\n  \"arugula\",\n  \"Canadian bacon\",\n  \"Mama Lil's Peppers\",\n];\n\n// a function that takes two arrays of strings a and b and removes from a and b\n// all strings that are in both a and b\nfunction removeCommonStrings(a: string[], b: string[]) {\n  const aSet = new Set(a);\n  const bSet = new Set(b);\n  for (const item of aSet) {\n    if (bSet.has(item)) {\n      aSet.delete(item);\n      bSet.delete(item);\n    }\n  }\n  return [Array.from(aSet), Array.from(bSet)];\n}\n\nconst namedPizzas = new Map([\n  [\"Hawaiian\", [\"pineapple\", \"Canadian bacon\"]],\n  [\"Yeti\", [\"extra cheese\", \"extra sauce\"]],\n  [\"Pig In a Forest\", [\"mushrooms\", \"basil\", \"Canadian bacon\", \"arugula\"]],\n  [\"Cherry Bomb\", [\"pepperoni\", \"sausage\", \"Mama Lil's Peppers\"]],\n]);\n\nfunction printOrder(order: Order) {\n  if (order.items && order.items.length > 0) {\n    for (const item of order.items) {\n      if (item.itemType === \"unknown\") {\n        break;\n      }\n      switch (item.itemType) {\n        case \"pizza\": {\n          if (item.name) {\n            const addedToppings = namedPizzas.get(item.name);\n            if (addedToppings) {\n              if (item.addedToppings) {\n                item.addedToppings = item.addedToppings.concat(addedToppings);\n              } else {\n                item.addedToppings = addedToppings;\n              }\n            }\n          }\n          if (!item.size) {\n            item.size = \"large\";\n          }\n          let quantity = 1;\n          if (item.quantity) {\n            quantity = item.quantity;\n          }\n          let pizzaStr = `    ${quantity} ${item.size} pizza`;\n          if (item.addedToppings && item.removedToppings) {\n            [item.addedToppings, item.removedToppings] =\n              removeCommonStrings(item.addedToppings, item.removedToppings);\n          }\n          if (item.addedToppings && item.addedToppings.length > 0) {\n            pizzaStr += \" with\";\n            for (const [index, addedTopping] of item.addedToppings.entries()) {\n              if (pizzaToppings.includes(addedTopping)) {\n                pizzaStr += `${index === 0 ? \" \" : \", \"}${addedTopping}`;\n              } else {\n                console.log(`We are out of ${addedTopping}`);\n              }\n            }\n          }\n          if (item.removedToppings && item.removedToppings.length > 0) {\n            pizzaStr += \" and without\";\n            for (const [\n              index,\n              removedTopping,\n            ] of item.removedToppings.entries()) {\n              pizzaStr += `${index === 0 ? \" \" : \", \"}${removedTopping}`;\n            }\n          }\n          console.log(pizzaStr);\n          break;\n        }\n        case \"beer\": {\n          let quantity = 1;\n          if (item.quantity) {\n            quantity = item.quantity;\n          }\n          const beerStr = `    ${quantity} ${item.kind}`;\n          console.log(beerStr);\n          break;\n        }\n        case \"salad\": {\n          let quantity = 1;\n          if (item.quantity) {\n            quantity = item.quantity;\n          }\n          if (!item.portion) {\n            item.portion = \"half\";\n          }\n          if (!item.style) {\n            item.style = \"Garden\";\n          }\n          let saladStr = `    ${quantity} ${item.portion} ${item.style} salad`;\n          if (item.addedIngredients && item.removedIngredients) {\n            [item.addedIngredients, item.removedIngredients] =\n              removeCommonStrings(item.addedIngredients, item.removedIngredients);\n          }\n          if (item.addedIngredients && item.addedIngredients.length > 0) {\n            saladStr += \" with\";\n            for (const [\n              index,\n              addedIngredient,\n            ] of item.addedIngredients.entries()) {\n              if (saladIngredients.includes(addedIngredient)) {\n                saladStr += `${index === 0 ? \" \" : \", \"}${addedIngredient}`;\n              } else {\n                console.log(`We are out of ${addedIngredient}`);\n              }\n            }\n          }\n          if (item.removedIngredients && item.removedIngredients.length > 0) {\n            saladStr += \" without\";\n            for (const [\n              index,\n              removedIngredient,\n            ] of item.removedIngredients.entries()) {\n              saladStr += `${index === 0 ? \" \" : \", \"}${removedIngredient}`;\n            }\n          }\n          console.log(saladStr);\n          break;\n        }\n      }\n    }\n  }\n}\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"🍕> \", process.argv[2], async (request) => {\n  const response = await translator.translate(request);\n  if (!response.success) {\n    console.log(response.message);\n    return;\n  }\n  const order = response.data;\n  if (order.items.some((item) => item.itemType === \"unknown\")) {\n    console.log(\"I didn't understand the following:\");\n    for (const item of order.items) {\n      if (item.itemType === \"unknown\") console.log(item.text);\n    }\n  }\n  printOrder(order);\n});\n"
  },
  {
    "path": "typescript/examples/restaurant/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/sentiment/.vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"skipFiles\": [\n                \"<node_internals>/**\"\n            ],\n            \"program\": \"${workspaceFolder}/dist/main.js\",\n            \"console\": \"externalTerminal\"\n        }\n    ]\n}"
  },
  {
    "path": "typescript/examples/sentiment/README.md",
    "content": "# Sentiment\n\nThe Sentiment example shows how to match user intent to a set of nouns, in this case categorizing user sentiment of the input as negative, neutral, or positive with the [`SentimentResponse`](./src/sentimentSchema.ts) type.\n\n# Try Sentiment\nTo run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\n\nFor example, given the following input statement:\n\n**Input**:\n```\n😀> TypeChat is awesome!\n```\n\n**Output**:\n```\nThe sentiment is positive\n```"
  },
  {
    "path": "typescript/examples/sentiment/package.json",
    "content": "{\n  \"name\": \"sentiment\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\",\n    \"typescript\": \"^5.3.3\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/sentiment/src/input.txt",
    "content": "hello, world\nTypeChat is awesome!\nI'm having a good day\nit's very rainy outside"
  },
  {
    "path": "typescript/examples/sentiment/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createJsonTranslator, createLanguageModel } from \"typechat\";\nimport { processRequests } from \"typechat/interactive\";\nimport { createTypeScriptJsonValidator } from \"typechat/ts\";\nimport { SentimentResponse } from \"./sentimentSchema\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst schema = fs.readFileSync(path.join(__dirname, \"sentimentSchema.ts\"), \"utf8\");\nconst validator = createTypeScriptJsonValidator<SentimentResponse>(schema, \"SentimentResponse\");\nconst translator = createJsonTranslator(model, validator);\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"😀> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    console.log(`The sentiment is ${response.data.sentiment}`);\n});\n"
  },
  {
    "path": "typescript/examples/sentiment/src/sentimentSchema.ts",
    "content": "// The following is a schema definition for determining the sentiment of a some user input.\n\nexport interface SentimentResponse {\n    sentiment: \"negative\" | \"neutral\" | \"positive\";  // The sentiment of the text\n}\n"
  },
  {
    "path": "typescript/examples/sentiment/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/examples/sentiment-zod/README.md",
    "content": "# Sentiment\n\nThe Sentiment example shows how to match user intent to a set of nouns, in this case categorizing user sentiment of the input as negative, neutral, or positive with the [`SentimentResponse`](./src/sentimentSchema.ts) type.\n\n# Try Sentiment\nTo run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment).\n\n# Usage\nExample prompts can be found in [`src/input.txt`](./src/input.txt).\n\nFor example, given the following input statement:\n\n**Input**:\n```\n😀> TypeChat is awesome!\n```\n\n**Output**:\n```\nThe sentiment is positive\n```"
  },
  {
    "path": "typescript/examples/sentiment-zod/package.json",
    "content": "{\n  \"name\": \"sentiment-zod\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"description\": \"\",\n  \"main\": \"dist/main.js\",\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"postbuild\": \"copyfiles -u 1 src/**/*.txt dist\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"dotenv\": \"^16.3.1\",\n    \"find-config\": \"^1.0.0\",\n    \"typechat\": \"^0.1.0\"\n  },\n  \"devDependencies\": {\n    \"@types/find-config\": \"1.0.4\",\n    \"@types/node\": \"^20.10.4\",\n    \"copyfiles\": \"^2.4.1\",\n    \"typescript\": \"^5.3.3\"\n  }\n}\n"
  },
  {
    "path": "typescript/examples/sentiment-zod/src/input.txt",
    "content": "hello, world\nTypeChat is awesome!\nI'm having a good day\nit's very rainy outside"
  },
  {
    "path": "typescript/examples/sentiment-zod/src/main.ts",
    "content": "import assert from \"assert\";\nimport dotenv from \"dotenv\";\nimport findConfig from \"find-config\";\nimport { createJsonTranslator, createLanguageModel } from \"typechat\";\nimport { processRequests } from \"typechat/interactive\";\nimport { createZodJsonValidator } from \"typechat/zod\";\nimport { SentimentSchema } from \"./sentimentSchema\";\n\nconst dotEnvPath = findConfig(\".env\");\nassert(dotEnvPath, \".env file not found!\");\ndotenv.config({ path: dotEnvPath });\n\nconst model = createLanguageModel(process.env);\nconst validator = createZodJsonValidator(SentimentSchema, \"SentimentResponse\");\nconst translator = createJsonTranslator(model, validator);\n\n// Process requests interactively or from the input file specified on the command line\nprocessRequests(\"😀> \", process.argv[2], async (request) => {\n    const response = await translator.translate(request);\n    if (!response.success) {\n        console.log(response.message);\n        return;\n    }\n    console.log(`The sentiment is ${response.data.sentiment}`);\n});\n"
  },
  {
    "path": "typescript/examples/sentiment-zod/src/sentimentSchema.ts",
    "content": "import { z } from \"zod\";\n\nexport const SentimentResponse = z.object({\n    sentiment: z.enum([\"negative\", \"neutral\", \"positive\"]).describe(\"The sentiment of the text\")\n});\n\nexport const SentimentSchema = {\n    SentimentResponse\n};\n"
  },
  {
    "path": "typescript/examples/sentiment-zod/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"outDir\": \"../dist\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"inlineSourceMap\": true\n  }\n}\n"
  },
  {
    "path": "typescript/package.json",
    "content": "{\n  \"name\": \"typechat\",\n  \"author\": \"Microsoft\",\n  \"version\": \"0.1.2\",\n  \"license\": \"MIT\",\n  \"description\": \"TypeChat is an experimental library that makes it easy to build natural language interfaces using types.\",\n  \"keywords\": [\n    \"schema\",\n    \"LLM\",\n    \"prompt\",\n    \"TypeScript\",\n    \"validation\"\n  ],\n  \"homepage\": \"https://github.com/microsoft/TypeChat#readme\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/microsoft/TypeChat.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/microsoft/TypeChat/issues\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p src\",\n    \"build-all\": \"npm run build --workspaces\",\n    \"prepare\": \"npm run build-all\",\n    \"prepublishOnly\": \"node -e \\\"require('fs').copyFileSync('../SECURITY.md','SECURITY.md')\\\"\",\n    \"postpublish\": \"node -e \\\"require('fs').unlinkSync('SECURITY.md')\\\"\",\n    \"publish-package\": \"npm publish\",\n    \"publish-package:dry-run\": \"npm publish --dry-run\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": \"./dist/index.js\",\n    \"./ts\": \"./dist/ts/index.js\",\n    \"./zod\": \"./dist/zod/index.js\",\n    \"./interactive\": \"./dist/interactive/index.js\"\n  },\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"files\": [\n    \"dist\",\n    \"LICENSE\",\n    \"README.md\",\n    \"SECURITY.md\"\n  ],\n  \"dependencies\": {},\n  \"peerDependencies\": {\n    \"typescript\": \"^5.3.3\",\n    \"zod\": \"^3.22.4\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    },\n    \"zod\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.10.4\"\n  },\n  \"workspaces\": [\n    \"./\",\n    \"./examples/*\"\n  ]\n}\n"
  },
  {
    "path": "typescript/src/index.ts",
    "content": "export * from './result';\nexport * from './model';\nexport * from './typechat';\n"
  },
  {
    "path": "typescript/src/interactive/index.ts",
    "content": "export * from './interactive';\r\n"
  },
  {
    "path": "typescript/src/interactive/interactive.ts",
    "content": "import fs from \"fs\";\nimport readline from \"readline/promises\";\n\n/**\n * A request processor for interactive input or input from a text file. If an input file name is specified,\n * the callback function is invoked for each line in file. Otherwise, the callback function is invoked for\n * each line of interactive input until the user types \"quit\" or \"exit\".\n * @param interactivePrompt Prompt to present to user.\n * @param inputFileName Input text file name, if any.\n * @param processRequest Async callback function that is invoked for each interactive input or each line in text file.\n */\nexport async function processRequests(interactivePrompt: string, inputFileName: string | undefined, processRequest: (request: string) => Promise<void>) {\n    if (inputFileName) {\n        const lines = fs.readFileSync(inputFileName).toString().split(/\\r?\\n/);\n        for (const line of lines) {\n            if (line.length) {\n                console.log(interactivePrompt + line);\n                await processRequest(line);\n            }\n        }\n    }\n    else {\n        const stdio = readline.createInterface({ input: process.stdin, output: process.stdout });\n        while (true) {\n            const input = await stdio.question(interactivePrompt);\n            if (input.toLowerCase() === \"quit\" || input.toLowerCase() === \"exit\") {\n                break;\n            }\n            else if (input.length) {\n                await processRequest(input);\n            }\n        }\n        stdio.close();\n    }\n}\n"
  },
  {
    "path": "typescript/src/model.ts",
    "content": "import { Result, success, error } from \"./result\";\n\n/**\n * Represents a section of an LLM prompt with an associated role. TypeChat uses the \"user\" role for\n * prompts it generates and the \"assistant\" role for previous LLM responses (which will be part of\n * the prompt in repair attempts). TypeChat currently doesn't use the \"system\" role.\n */\nexport interface PromptSection {\n    /**\n     * Specifies the role of this section.\n     */\n    role: \"system\" | \"user\" | \"assistant\";\n    /**\n     * Specifies the content of this section.\n     */\n    content: PromptContent;\n}\n\nexport type PromptContent =\n    | string\n    | MultimodalPromptContent[];\n\n/**\n * GPT-4-vision, GPT-4-omni and GPT-4-turbo allow multi-modal input, where images and text can\n * be part of the prompt. To support this, the content section of the prompt has an array of objects.\n */\nexport type MultimodalPromptContent =\n    | string\n    | TextPromptContent\n    | ImagePromptContent;\n\nexport type TextPromptContent = {\n    type: \"text\";\n    text: string;\n};\n\nexport type ImagePromptContent = {\n    type: \"image_url\";\n    image_url: ImageUrl;\n};\n\nexport type ImageUrl = {\n    /*\n     * This could be a URL to a hosted image, or the base64-encoded image content.\n     */\n    url: string;\n    \n    /*\n     * Controls how the model processes the image and generates its textual understanding.\n     * In \"low\" mode, the model treats the image as 512x512px, while \"high\" mode considers\n     * the image at full size.\n     */\n    detail?: \"auto\" | \"low\" | \"high\";\n};\n\n/**\n * Represents a AI language model that can complete prompts. TypeChat uses an implementation of this\n * interface to communicate with an AI service that can translate natural language requests to JSON\n * instances according to a provided schema. The `createLanguageModel`, `createOpenAILanguageModel`,\n * and `createAzureOpenAILanguageModel` functions create instances of this interface.\n */\nexport interface TypeChatLanguageModel {\n    /**\n     * Optional property that specifies the maximum number of retry attempts (the default is 3).\n     */\n    retryMaxAttempts?: number;\n    /**\n     * Optional property that specifies the delay before retrying in milliseconds (the default is 1000ms).\n     */\n    retryPauseMs?: number;\n    /**\n     * Obtains a completion from the language model for the given prompt.\n     * @param prompt A prompt string or an array of prompt sections. If a string is specified,\n     *   it is converted into a single \"user\" role prompt section.\n     */\n    complete(prompt: string | PromptSection[]): Promise<Result<string>>;\n}\n\n/**\n * Creates a language model encapsulation of an OpenAI or Azure OpenAI REST API endpoint\n * chosen by environment variables.\n *\n * If an `OPENAI_API_KEY` environment variable exists, the `createOpenAILanguageModel` function\n * is used to create the instance. The `OPENAI_ENDPOINT` and `OPENAI_MODEL` environment variables\n * must also be defined or an exception will be thrown.\n *\n * If an `AZURE_OPENAI_API_KEY` environment variable exists, the `createAzureOpenAILanguageModel` function\n * is used to create the instance. The `AZURE_OPENAI_ENDPOINT` environment variable must also be defined\n * or an exception will be thrown.\n *\n * If none of these key variables are defined, an exception is thrown.\n * @returns An instance of `TypeChatLanguageModel`.\n */\nexport function createLanguageModel(env: Record<string, string | undefined>): TypeChatLanguageModel {\n    if (env.OPENAI_API_KEY) {\n        const apiKey = env.OPENAI_API_KEY ?? missingEnvironmentVariable(\"OPENAI_API_KEY\");\n        const model = env.OPENAI_MODEL ?? missingEnvironmentVariable(\"OPENAI_MODEL\");\n        const endPoint = env.OPENAI_ENDPOINT ?? \"https://api.openai.com/v1/chat/completions\";\n        const org = env.OPENAI_ORGANIZATION ?? \"\";\n        return createOpenAILanguageModel(apiKey, model, endPoint, org);\n    }\n    if (env.AZURE_OPENAI_API_KEY) {\n        const apiKey = env.AZURE_OPENAI_API_KEY ?? missingEnvironmentVariable(\"AZURE_OPENAI_API_KEY\");\n        const endPoint = env.AZURE_OPENAI_ENDPOINT ?? missingEnvironmentVariable(\"AZURE_OPENAI_ENDPOINT\");\n        return createAzureOpenAILanguageModel(apiKey, endPoint);\n    }\n    missingEnvironmentVariable(\"OPENAI_API_KEY or AZURE_OPENAI_API_KEY\");\n}\n\n/**\n * Creates a language model encapsulation of an OpenAI REST API endpoint.\n * @param apiKey The OpenAI API key.\n * @param model The model name.\n * @param endPoint The URL of the OpenAI REST API endpoint. Defaults to \"https://api.openai.com/v1/chat/completions\".\n * @param org The OpenAI organization id.\n * @returns An instance of `TypeChatLanguageModel`.\n */\nexport function createOpenAILanguageModel(apiKey: string, model: string, endPoint = \"https://api.openai.com/v1/chat/completions\", org = \"\"): TypeChatLanguageModel {\n    const headers = {\n        \"Authorization\": `Bearer ${apiKey}`,\n        \"OpenAI-Organization\": org\n    };\n    return createFetchLanguageModel(endPoint, headers, { model });\n}\n\n/**\n * Creates a language model encapsulation of an Azure OpenAI REST API endpoint.\n * @param endPoint The URL of the OpenAI REST API endpoint. The URL must be in the format\n *   \"https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version={API-version}\".\n *   Example deployment names are \"gpt-35-turbo\" and \"gpt-4\". An example API versions is \"2023-05-15\".\n * @param apiKey The Azure OpenAI API key.\n * @returns An instance of `TypeChatLanguageModel`.\n */\nexport function createAzureOpenAILanguageModel(apiKey: string, endPoint: string): TypeChatLanguageModel {\n    const headers = {\n        // Needed when using managed identity\n        \"Authorization\": `Bearer ${apiKey}`,\n        // Needed when using regular API key\n        \"api-key\": apiKey\n    };\n    return createFetchLanguageModel(endPoint, headers, {});\n}\n\n/**\n * Common OpenAI REST API endpoint encapsulation using the fetch API.\n */\nfunction createFetchLanguageModel(url: string, headers: object, defaultParams: object) {\n    const model: TypeChatLanguageModel = {\n        complete\n    };\n    return model;\n\n    async function complete(prompt: string | PromptSection[]) {\n        let retryCount = 0;\n        const retryMaxAttempts = model.retryMaxAttempts ?? 3;\n        const retryPauseMs = model.retryPauseMs ?? 1000;\n        const messages = typeof prompt === \"string\" ? [{ role: \"user\", content: prompt }] : prompt;\n        while (true) {\n            const options = {\n                method: \"POST\",\n                body: JSON.stringify({\n                    ...defaultParams,\n                    messages,\n                    temperature: 0,\n                    n: 1\n                }),\n                headers: {\n                    \"content-type\": \"application/json\",\n                    ...headers\n                }\n            }\n            const response = await fetch(url, options);\n            if (response.ok) {\n                const json = await response.json() as { choices: { message: PromptSection }[] };\n                if (typeof json.choices[0].message.content === \"string\") {\n                    return success(json.choices[0].message.content ?? \"\");\n                } else {\n                    return error(`REST API unexpected response format: ${JSON.stringify(json.choices[0].message.content)}`);\n                }\n            }\n            if (!isTransientHttpError(response.status) || retryCount >= retryMaxAttempts) {\n                return error(`REST API error ${response.status}: ${response.statusText}`);\n            }\n            await sleep(retryPauseMs);\n            retryCount++;\n        }\n    }\n}\n\n/**\n * Returns true of the given HTTP status code represents a transient error.\n */\nfunction isTransientHttpError(code: number): boolean {\n    switch (code) {\n        case 429: // TooManyRequests\n        case 500: // InternalServerError\n        case 502: // BadGateway\n        case 503: // ServiceUnavailable\n        case 504: // GatewayTimeout\n            return true;\n    }\n    return false;\n}\n\n/**\n * Sleeps for the given number of milliseconds.\n */\nfunction sleep(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Throws an exception for a missing environment variable.\n */\nfunction missingEnvironmentVariable(name: string): never {\n    throw new Error(`Missing environment variable: ${name}`);\n}\n"
  },
  {
    "path": "typescript/src/result.ts",
    "content": "/**\n * An object representing a successful operation with a result of type `T`.\n */\nexport type Success<T> = { success: true, data: T };\n\n/**\n * An object representing an operation that failed for the reason given in `message`.\n */\nexport type Error = { success: false, message: string };\n\n/**\n * An object representing a successful or failed operation of type `T`.\n */\nexport type Result<T> = Success<T> | Error;\n\n/**\n * Returns a `Success<T>` object.\n * @param data The value for the `data` property of the result.\n * @returns A `Success<T>` object.\n */\nexport function success<T>(data: T): Success<T> {\n    return { success: true, data };\n}\n\n/**\n * Returns an `Error` object.\n * @param message The value for the `message` property of the result.\n * @returns An `Error` object.\n */\nexport function error(message: string): Error {\n    return { success: false, message };\n}\n\n/**\n * Obtains the value associated with a successful `Result<T>` or throws an exception if\n * the result is an error.\n * @param result The `Result<T>` from which to obtain the `data` property.\n * @returns The value of the `data` property.\n */\nexport function getData<T>(result: Result<T>) {\n    if (result.success) {\n        return result.data;\n    }\n    throw new Error(result.message);\n}\n"
  },
  {
    "path": "typescript/src/ts/index.ts",
    "content": "export * from './validate';\r\nexport * from './program';\r\n"
  },
  {
    "path": "typescript/src/ts/program.ts",
    "content": "import { Result, error, success } from \"../result\";\r\nimport { TypeChatLanguageModel } from \"../model\";\r\nimport { createTypeScriptJsonValidator } from \"./validate\";\r\nimport { TypeChatJsonTranslator, createJsonTranslator } from \"../typechat\";\r\n\r\nconst programSchemaText = `// A program consists of a sequence of function calls that are evaluated in order.\r\nexport type Program = {\r\n    \"@steps\": FunctionCall[];\r\n}\r\n\r\n// A function call specifies a function name and a list of argument expressions. Arguments may contain\r\n// nested function calls and result references.\r\nexport type FunctionCall = {\r\n    // Name of the function\r\n    \"@func\": string;\r\n    // Arguments for the function, if any\r\n    \"@args\"?: Expression[];\r\n};\r\n\r\n// An expression is a JSON value, a function call, or a reference to the result of a preceding expression.\r\nexport type Expression = JsonValue | FunctionCall | ResultReference;\r\n\r\n// A JSON value is a string, a number, a boolean, null, an object, or an array. Function calls and result\r\n// references can be nested in objects and arrays.\r\nexport type JsonValue = string | number | boolean | null | { [x: string]: Expression } | Expression[];\r\n\r\n// A result reference represents the value of an expression from a preceding step.\r\nexport type ResultReference = {\r\n    // Index of the previous expression in the \"@steps\" array\r\n    \"@ref\": number;\r\n};\r\n`;\r\n\r\n/**\r\n * A program consists of a sequence of function calls that are evaluated in order.\r\n */\r\nexport type Program = {\r\n    \"@steps\": FunctionCall[];\r\n}\r\n\r\n/**\r\n * A function call specifies a function name and a list of argument expressions. Arguments may contain\r\n * nested function calls and result references.\r\n */\r\nexport type FunctionCall = {\r\n    // Name of the function\r\n    \"@func\": string;\r\n    // Arguments for the function, if any\r\n    \"@args\"?: Expression[];\r\n};\r\n\r\n/**\r\n * An expression is a JSON value, a function call, or a reference to the result of a preceding expression.\r\n */\r\nexport type Expression = JsonValue | FunctionCall | ResultReference;\r\n\r\n/**\r\n * A JSON value is a string, a number, a boolean, null, an object, or an array. Function calls and result\r\n * references can be nested in objects and arrays.\r\n */\r\nexport type JsonValue = string | number | boolean | null | { [x: string]: Expression } | Expression[];\r\n\r\n/**\r\n * A result reference represents the value of an expression from a preceding step.\r\n */\r\nexport type ResultReference = {\r\n    // Index of the previous expression in the \"@steps\" array\r\n    \"@ref\": number;\r\n};\r\n\r\n/**\r\n * Transforms a JSON program object into an equivalent TypeScript module suitable for type checking.\r\n * The generated module takes the form:\r\n * \r\n *   import { API } from \"./schema\";\r\n *   function program(api: API) {\r\n *     const step1 = api.someFunction1(...);\r\n *     const step2 = api.someFunction2(...);\r\n *     return api.someFunction3(...);\r\n *   }\r\n * \r\n * @param jsonObject A JSON program object.\r\n * @returns A `Success<string>` with the module source code or an `Error` explaining why the JSON object\r\n * couldn't be transformed.\r\n */\r\nexport function createModuleTextFromProgram(jsonObject: object): Result<string> {\r\n    const steps = (jsonObject as Program)[\"@steps\"];\r\n    if (!(Array.isArray(steps) && steps.every(step => typeof step === \"object\" && step !== null && step.hasOwnProperty(\"@func\")))) {\r\n        return error(\"JSON object is not a valid program\");\r\n    }\r\n    let hasError = false;\r\n    let functionBody = \"\";\r\n    let currentStep = 0;\r\n    while (currentStep < steps.length) {\r\n        functionBody += `  ${currentStep === steps.length - 1 ? `return` : `const step${currentStep + 1} =`} ${exprToString(steps[currentStep])};\\n`;\r\n        currentStep++;\r\n    }\r\n    return hasError ?\r\n        error(\"JSON program contains an invalid expression\") :\r\n        success(`import { API } from \"./schema\";\\nfunction program(api: API) {\\n${functionBody}}`);\r\n\r\n    function exprToString(expr: unknown): string {\r\n        return typeof expr === \"object\" && expr !== null ?\r\n            objectToString(expr as Record<string, unknown>) :\r\n            JSON.stringify(expr);\r\n    }\r\n\r\n    function objectToString(obj: Record<string, unknown>) {\r\n        if (obj.hasOwnProperty(\"@ref\")) {\r\n            const index = obj[\"@ref\"];\r\n            if (typeof index === \"number\" && index < currentStep && Object.keys(obj).length === 1) {\r\n                return `step${index + 1}`;\r\n            }\r\n        }\r\n        else if (obj.hasOwnProperty(\"@func\")) {\r\n            const func = obj[\"@func\"];\r\n            const hasArgs = obj.hasOwnProperty(\"@args\");\r\n            const args = hasArgs ? obj[\"@args\"] : [];\r\n            if (typeof func === \"string\" && (Array.isArray(args)) && Object.keys(obj).length === (hasArgs ? 2 : 1)) {\r\n                return `api.${func}(${arrayToString(args)})`;\r\n            }\r\n        }\r\n        else if (Array.isArray(obj)) {\r\n            return `[${arrayToString(obj)}]`;\r\n        }\r\n        else {\r\n            return `{ ${Object.keys(obj).map(key => `${JSON.stringify(key)}: ${exprToString(obj[key])}`).join(\", \")} }`;\r\n        }\r\n        hasError = true;\r\n        return \"\";\r\n    }\r\n\r\n    function arrayToString(array: unknown[]) {\r\n        return array.map(exprToString).join(\", \");\r\n    }\r\n}\r\n\r\n/**\r\n * Evaluates a JSON program using a simple interpreter. Function calls in the program are passed to the `onCall`\r\n * callback function for validation and dispatch. Thus, unlike JavaScript's `eval`, access to external functionality\r\n * and resources is entirely controlled by the host application. Note that `onCall` is expected to return a `Promise`\r\n * such that function dispatch can be implemented asynchronously if desired.\r\n * @param program The JSON program to evaluate.\r\n * @param onCall A callback function for handling function calls in the program.\r\n * @returns A `Promise` with the value of the last expression in the program.\r\n */\r\nexport async function evaluateJsonProgram(program: Program, onCall: (func: string, args: unknown[]) => Promise<unknown>) {\r\n    const results: unknown[] = [];\r\n    for (const expr of program[\"@steps\"]) {\r\n        results.push(await evaluate(expr));\r\n    }\r\n    return results.length > 0 ? results[results.length - 1] : undefined;\r\n\r\n    async function evaluate(expr: unknown): Promise<unknown> {\r\n        return typeof expr === \"object\" && expr !== null ?\r\n            await evaluateObject(expr as Record<string, unknown>) :\r\n            expr;\r\n    }\r\n\r\n    async function evaluateObject(obj: Record<string, unknown>) {\r\n        if (obj.hasOwnProperty(\"@ref\")) {\r\n            const index = obj[\"@ref\"];\r\n            if (typeof index === \"number\" && index < results.length) {\r\n                return results[index];\r\n            }\r\n        }\r\n        else if (obj.hasOwnProperty(\"@func\")) {\r\n            const func = obj[\"@func\"];\r\n            const args = obj.hasOwnProperty(\"@args\") ? obj[\"@args\"] : [];\r\n            if (typeof func === \"string\" && Array.isArray(args)) {\r\n                return await onCall(func, await evaluateArray(args));\r\n            }\r\n        }\r\n        else if (Array.isArray(obj)) {\r\n            return evaluateArray(obj);\r\n        }\r\n        else {\r\n            const values = await Promise.all(Object.values(obj).map(evaluate));\r\n            return Object.fromEntries(Object.keys(obj).map((k, i) => [k, values[i]]));\r\n        }\r\n    }\r\n\r\n    function evaluateArray(array: unknown[]) {\r\n        return Promise.all(array.map(evaluate));\r\n    }\r\n}\r\n\r\n/**\r\n * Creates an object that can translate natural language requests into simple programs, represented as JSON, that compose\r\n * functions from a specified API. The resulting programs can be safely evaluated using the `evaluateJsonProgram`\r\n * function.\r\n * @param model The language model to use for translating requests into programs.\r\n * @param schema The TypeScript source code for the target API. The source code must export a type named `API`.\r\n * @returns A `TypeChatJsonTranslator<Program>` instance.\r\n */\r\nexport function createProgramTranslator(model: TypeChatLanguageModel, schema: string): TypeChatJsonTranslator<Program> {\r\n    const validator = createTypeScriptJsonValidator<Program>(schema, \"Program\");\r\n    validator.createModuleTextFromJson = createModuleTextFromProgram;\r\n    const translator = createJsonTranslator<Program>(model, validator);\r\n    translator.createRequestPrompt = createRequestPrompt;\r\n    translator.createRepairPrompt = createRepairPrompt;\r\n    return translator;\r\n\r\n    function createRequestPrompt(request: string) {\r\n        return `You are a service that translates user requests into programs represented as JSON using the following TypeScript definitions:\\n` +\r\n            `\\`\\`\\`\\n${programSchemaText}\\`\\`\\`\\n` +\r\n            `The programs can call functions from the API defined in the following TypeScript definitions:\\n` +\r\n            `\\`\\`\\`\\n${validator.getSchemaText()}\\`\\`\\`\\n` +\r\n            `The following is a user request:\\n` +\r\n            `\"\"\"\\n${request}\\n\"\"\"\\n` +\r\n            `The following is the user request translated into a JSON program object with 2 spaces of indentation and no properties with the value undefined:\\n`;\r\n    }\r\n\r\n    function createRepairPrompt(validationError: string) {\r\n        return `The JSON program object is invalid for the following reason:\\n` +\r\n            `\"\"\"\\n${validationError}\\n\"\"\"\\n` +\r\n            `The following is a revised JSON program object:\\n`;\r\n    }\r\n}\r\n"
  },
  {
    "path": "typescript/src/ts/validate.ts",
    "content": "import ts from 'typescript';\nimport { Result, success, error } from '../result';\nimport { TypeChatJsonValidator } from \"../typechat\";\n\nconst libText = `interface Array<T> { length: number, [n: number]: T }\ninterface Object { toString(): string }\ninterface Function { prototype: unknown }\ninterface CallableFunction extends Function {}\ninterface NewableFunction extends Function {}\ninterface String { readonly length: number }\ninterface Boolean { valueOf(): boolean }\ninterface Number { valueOf(): number }\ninterface RegExp { test(string: string): boolean }`;\n\n/**\n * Represents an object that can validate JSON strings according to a given TypeScript schema.\n */\nexport interface TypeScriptJsonValidator<T extends object> extends TypeChatJsonValidator<T> {\n    /**\n     * Transform JSON into TypeScript code for validation. Returns a `Success<string>` object if the conversion is\n     * successful, or an `Error` object if the JSON can't be transformed. The returned TypeScript source code is\n     * expected to be an ECMAScript module that imports one or more types from `\"./schema\"` and combines those\n     * types and a representation of the JSON object in a manner suitable for type-checking by the TypeScript compiler.\n     */\n    createModuleTextFromJson(jsonObject: object): Result<string>;\n}\n\n/**\n * Returns a JSON validator for a given TypeScript schema. Validation is performed by an in-memory instance of\n * the TypeScript compiler. The specified type argument `T` must be the same type as `typeName` in the given `schema`.\n * @param schema A string containing the TypeScript source code for the JSON schema.\n * @param typeName The name of the JSON target type in the schema.\n * @returns A `TypeChatJsonValidator<T>` instance.\n */\nexport function createTypeScriptJsonValidator<T extends object = object>(schema: string, typeName: string): TypeScriptJsonValidator<T> {\n    const options = {\n        ...ts.getDefaultCompilerOptions(),\n        strict: true,\n        skipLibCheck: true,\n        noLib: true,\n        types: []\n    };\n    const rootProgram = createProgramFromModuleText(\"\");\n    const validator: TypeScriptJsonValidator<T> = {\n        getSchemaText: () => schema,\n        getTypeName: () => typeName,\n        createModuleTextFromJson,\n        validate\n    };\n    return validator;\n\n    function validate(jsonObject: object) {\n        const moduleResult = validator.createModuleTextFromJson(jsonObject);\n        if (!moduleResult.success) {\n            return moduleResult;\n        }\n        const program = createProgramFromModuleText(moduleResult.data, rootProgram);\n        const syntacticDiagnostics = program.getSyntacticDiagnostics();\n        const programDiagnostics = syntacticDiagnostics.length ? syntacticDiagnostics : program.getSemanticDiagnostics();\n        if (programDiagnostics.length) {\n            const diagnostics = programDiagnostics.map(d => typeof d.messageText === \"string\" ? d.messageText : d.messageText.messageText).join(\"\\n\");\n            return error(diagnostics);\n        }\n        return success(jsonObject as T);\n    }\n\n    function createModuleTextFromJson(jsonObject: object) {\n        return success(`import { ${typeName} } from './schema';\\nconst json: ${typeName} = ${JSON.stringify(jsonObject, undefined, 2)};\\n`);\n    }\n\n    function createProgramFromModuleText(moduleText: string, oldProgram?: ts.Program) {\n        const fileMap = new Map([\n            createFileMapEntry(\"/lib.d.ts\", libText),\n            createFileMapEntry(\"/schema.ts\", schema),\n            createFileMapEntry(\"/json.ts\", moduleText)\n        ]);\n        const host: ts.CompilerHost = {\n            getSourceFile: fileName => fileMap.get(fileName),\n            getDefaultLibFileName: () => \"lib.d.ts\",\n            writeFile: () => {},\n            getCurrentDirectory: () => \"/\",\n            getCanonicalFileName: fileName => fileName,\n            useCaseSensitiveFileNames: () => true,\n            getNewLine: () => \"\\n\",\n            fileExists: fileName => fileMap.has(fileName),\n            readFile: fileName => \"\",\n        };\n        return ts.createProgram(Array.from(fileMap.keys()), options, host, oldProgram);\n    }\n\n    function createFileMapEntry(filePath: string, fileText: string): [string, ts.SourceFile] {\n        return [filePath, ts.createSourceFile(filePath, fileText, ts.ScriptTarget.Latest)];\n    }\n}\n"
  },
  {
    "path": "typescript/src/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"lib\": [\"es2021\"],\n    \"module\": \"node16\",\n    \"types\": [\"node\"],\n    \"esModuleInterop\": true,\n    \"outDir\": \"../dist\",\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"declaration\": true\n  }\n}\n"
  },
  {
    "path": "typescript/src/typechat.ts",
    "content": "import { Result, success, error } from \"./result\";\nimport { TypeChatLanguageModel, PromptSection, PromptContent } from \"./model\";\n\n/**\n * Represents an object that can translate natural language requests in JSON objects of the given type.\n */\nexport interface TypeChatJsonTranslator<T extends object> {\n    /**\n     * The associated `TypeChatLanguageModel`.\n     */\n    model: TypeChatLanguageModel;\n    /**\n     * The associated `TypeChatJsonValidator<T>`.\n     */\n    validator: TypeChatJsonValidator<T>;\n    /**\n     * A boolean indicating whether to attempt repairing JSON objects that fail to validate. The default is `true`,\n     * but an application can set the property to `false` to disable repair attempts.\n     */\n    attemptRepair:  boolean;\n    /**\n     * A boolean indicating whether to delete properties with null values from parsed JSON objects. Some language\n     * models (e.g. gpt-3.5-turbo) have a tendency to assign null values to optional properties instead of omitting\n     * them. The default for this property is `false`, but an application can set the property to `true` for schemas\n     * that don't permit null values.\n     */\n    stripNulls:  boolean;\n    /**\n     * Creates an AI language model prompt from the given request. This function is called by `completeAndValidate`\n     * to obtain the prompt. An application can assign a new function to provide a different prompt.\n     * @param request The natural language request.\n     * @returns A prompt that combines the request with the schema and type name of the underlying validator.\n     */\n    createRequestPrompt(request: string): PromptContent;\n    /**\n     * Creates a repair prompt to append to an original prompt/response in order to repair a JSON object that\n     * failed to validate. This function is called by `completeAndValidate` when `attemptRepair` is true and the\n     * JSON object produced by the original prompt failed to validate. An application can assign a new function\n     * to provide a different repair prompt.\n     * @param validationError The error message returned by the validator.\n     * @returns A repair prompt constructed from the error message.\n     */\n    createRepairPrompt(validationError: string): string;\n    /**\n     * Optionally implements additional validation logic beyond what is expressed in the schema. This function is\n     * called following successful schema validation of an instance. By default the function just returns a\n     * `Success<T>` for the given instance, but an application can assign a new function that implements any\n     * additional validation.\n     * @param instance The instance to validate.\n     * @returns A `Success<T>` with the final validated instance, or an `Error` explaining the validation failure.\n     */\n    validateInstance(instance: T): Result<T>;\n    /**\n     * Translates a natural language request into an object of type `T`. If the JSON object returned by\n     * the language model fails to validate and the `attemptRepair` property is `true`, a second\n     * attempt to translate the request will be made. The prompt for the second attempt will include the\n     * diagnostics produced for the first attempt. This often helps produce a valid instance.\n     * @param request The natural language request.\n     * @param promptPreamble An optional string or array of prompt sections to prepend to the generated\n     *   prompt. If a string is specified, it is converted into a single \"user\" role prompt section.\n     * @returns A promise for the resulting object.\n     */\n    translate(request: string, promptPreamble?: string | PromptSection[]): Promise<Result<T>>;\n}\n\n/**\n * An object that represents a TypeScript schema for JSON objects.\n */\nexport interface TypeChatJsonValidator<T extends object> {\n    /**\n     * Return a string containing TypeScript source code for the validation schema.\n     */\n    getSchemaText(): string;\n    /**\n     * Return the name of the JSON object target type in the schema.\n     */\n    getTypeName(): string;\n    /**\n     * Validates the given JSON object according to the associated TypeScript schema. Returns a\n     * `Success<T>` object containing the JSON object if validation was successful. Otherwise, returns\n     * an `Error` object with a `message` property describing the error.\n     * @param jsonText The JSON object to validate.\n     * @returns The JSON object or an error message.\n     */\n    validate(jsonObject: object): Result<T>;\n}\n\n/**\n * Creates an object that can translate natural language requests into JSON objects of the given type.\n * The specified type argument `T` must be the same type as `typeName` in the given `schema`. The function\n * creates a `TypeChatJsonValidator<T>` and stores it in the `validator` property of the returned instance.\n * @param model The language model to use for translating requests into JSON.\n * @param validator A string containing the TypeScript source code for the JSON schema.\n * @param typeName The name of the JSON target type in the schema.\n * @returns A `TypeChatJsonTranslator<T>` instance.\n */\nexport function createJsonTranslator<T extends object>(model: TypeChatLanguageModel, validator: TypeChatJsonValidator<T>): TypeChatJsonTranslator<T> {\n    const typeChat: TypeChatJsonTranslator<T> = {\n        model,\n        validator,\n        attemptRepair: true,\n        stripNulls: false,\n        createRequestPrompt,\n        createRepairPrompt,\n        validateInstance: success,\n        translate\n    };\n    return typeChat;\n\n    function createRequestPrompt(request: string) {\n        return `You are a service that translates user requests into JSON objects of type \"${validator.getTypeName()}\" according to the following TypeScript definitions:\\n` +\n            `\\`\\`\\`\\n${validator.getSchemaText()}\\`\\`\\`\\n` +\n            `The following is a user request:\\n` +\n            `\"\"\"\\n${request}\\n\"\"\"\\n` +\n            `The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:\\n`;\n    }\n\n    function createRepairPrompt(validationError: string) {\n        return `The JSON object is invalid for the following reason:\\n` +\n            `\"\"\"\\n${validationError}\\n\"\"\"\\n` +\n            `The following is a revised JSON object:\\n`;\n    }\n\n    async function translate(request: string, promptPreamble?: string | PromptSection[]) {\n        const preamble: PromptSection[] = typeof promptPreamble === \"string\" ? [{ role: \"user\", content: promptPreamble }] : promptPreamble ?? [];\n        let prompt: PromptSection[] = [...preamble, { role: \"user\", content: typeChat.createRequestPrompt(request) }];\n        let attemptRepair = typeChat.attemptRepair;\n        while (true) {\n            const response = await model.complete(prompt);\n            if (!response.success) {\n                return response;\n            }\n            const responseText = response.data;\n            const startIndex = responseText.indexOf(\"{\");\n            const endIndex = responseText.lastIndexOf(\"}\");\n            if (!(startIndex >= 0 && endIndex > startIndex)) {\n                return error(`Response is not JSON:\\n${responseText}`);\n            }\n            const jsonText = responseText.slice(startIndex, endIndex + 1);\n            let jsonObject;\n            try {\n                jsonObject = JSON.parse(jsonText) as object;\n            }\n            catch (e) {\n                return error(e instanceof SyntaxError ? e.message : \"JSON parse error\");\n            }\n            if (typeChat.stripNulls) {\n                stripNulls(jsonObject);\n            }\n            const schemaValidation = validator.validate(jsonObject);\n            const validation = schemaValidation.success ? typeChat.validateInstance(schemaValidation.data) : schemaValidation;\n            if (validation.success) {\n                return validation;\n            }\n            if (!attemptRepair) {\n                return error(`JSON validation failed: ${validation.message}\\n${jsonText}`);\n            }\n            prompt.push({ role: \"assistant\", content: responseText });\n            prompt.push({ role: \"user\", content: typeChat.createRepairPrompt(validation.message) });\n            attemptRepair = false;\n        }\n    }\n}\n\n/**\n * Recursively delete properties with null values from the given object. This function assumes there are no\n * circular references in the object.\n * @param obj The object in which to strip null valued properties.\n */\nfunction stripNulls(obj: any) {\n    let keysToDelete: string[] | undefined;\n    for (const k in obj) {\n        const value = obj[k];\n        if (value === null) {\n            (keysToDelete ??= []).push(k);\n        }\n        else {\n            if (Array.isArray(value)) {\n                if (value.some(x => x === null)) {\n                    obj[k] = value.filter(x => x !== null);\n                }\n            }\n            if (typeof value === \"object\") {\n                stripNulls(value);\n            }\n        }\n    }\n    if (keysToDelete) {\n        for (const k of keysToDelete) {\n            delete obj[k];\n        }\n    }\n}\n"
  },
  {
    "path": "typescript/src/zod/index.ts",
    "content": "export * from './validate';\r\n"
  },
  {
    "path": "typescript/src/zod/validate.ts",
    "content": "import { z } from \"zod\";\nimport { success, error } from '../result';\nimport { TypeChatJsonValidator } from '../typechat';\n\n/**\n * Returns a JSON validator for a given Zod schema. The schema is supplied as an object where each property provides\n * a name for an associated Zod type. The `validate` method of the returned object validates a JSON object against the\n * supplied schema, the `getSchemaText` method obtains the TypeScript source text representation of the schema, and\n * the `getTypeName` method obtains the name of the given target type in the schema.\n * @param schema A schema object where each property provides a name for an associated Zod type.\n * @param targetType The name in the schema of the target type for JSON validation.\n * @returns A `TypeChatJsonValidator<z.TypeOf<T[K]>>`, where T is the schema and K is the target type name.\n */\nexport function createZodJsonValidator<T extends Record<string, z.ZodType>, K extends keyof T & string>(schema: T, typeName: K): TypeChatJsonValidator<z.TypeOf<T[K]>> {\n    let schemaText: string;\n    const validator: TypeChatJsonValidator<z.TypeOf<T[K]>> = {\n        getSchemaText: () => schemaText ??= getZodSchemaAsTypeScript(schema),\n        getTypeName: () => typeName,\n        validate\n    };\n    return validator;\n\n    function validate(jsonObject: object) {\n        const result = schema[typeName].safeParse(jsonObject);\n        if (!result.success) {\n            return error(result.error.issues.map(({ path, message }) => `${path.map(key => `[${JSON.stringify(key)}]`).join(\"\")}: ${message}`).join(\"\\\"\"));\n        }\n        return success(result.data as z.TypeOf<T[K]>);\n    }\n}\n\nfunction getTypeKind(type: z.ZodType) {\n    return (type._def as z.ZodTypeDef & { typeName: z.ZodFirstPartyTypeKind }).typeName;\n}\n\nfunction getTypeIdentity(type: z.ZodType): object {\n    switch (getTypeKind(type)) {\n        case z.ZodFirstPartyTypeKind.ZodObject:\n            return (type._def as z.ZodObjectDef).shape();\n        case z.ZodFirstPartyTypeKind.ZodEnum:\n            return (type._def as z.ZodEnumDef).values;\n        case z.ZodFirstPartyTypeKind.ZodUnion:\n            return (type._def as z.ZodUnionDef).options;\n    }\n    return type;\n}\n\nconst enum TypePrecedence {\n    Union = 0,\n    Intersection = 1,\n    Object = 2\n}\n\nfunction getTypePrecedence(type: z.ZodType): TypePrecedence {\n    switch (getTypeKind(type)) {\n        case z.ZodFirstPartyTypeKind.ZodEnum:\n        case z.ZodFirstPartyTypeKind.ZodUnion:\n        case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion:\n            return TypePrecedence.Union;\n        case z.ZodFirstPartyTypeKind.ZodIntersection:\n            return TypePrecedence.Intersection;\n    }\n    return TypePrecedence.Object;\n}\n\n/**\n * Returns the TypeScript source code corresponding to a Zod schema. The schema is supplied as an object where each\n * property provides a name for an associated Zod type. The return value is a string containing the TypeScript source\n * code corresponding to the schema. Each property of the schema object is emitted as a named `interface` or `type`\n * declaration for the associated type and is referenced by that name in the emitted type declarations. Other types\n * referenced in the schema are emitted in their structural form.\n * @param schema A schema object where each property provides a name for an associated Zod type.\n * @returns The TypeScript source code corresponding to the schema.\n */\nexport function getZodSchemaAsTypeScript(schema: Record<string, z.ZodType>): string {\n    let result = \"\";\n    let startOfLine = true;\n    let indent = 0;\n    const entries = Array.from(Object.entries(schema));\n    let namedTypes = new Map<object, string>(entries.map(([name, type]) => [getTypeIdentity(type), name]));\n    for (const [name, type] of entries) {\n        if (result) {\n            appendNewLine();\n        }\n        const description = type._def.description;\n        if (description) {\n            for (const comment of description.split(\"\\n\")) {\n                append(`// ${comment}`);\n                appendNewLine();\n            }\n        }\n        if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodObject) {\n            append(`interface ${name} `);\n            appendObjectType(type as z.ZodObject<z.ZodRawShape>);\n        }\n        else {\n            append(`type ${name} = `);\n            appendTypeDefinition(type);\n            append(\";\");\n        }\n        appendNewLine();\n    }\n    return result;\n\n    function append(s: string) {\n        if (startOfLine) {\n            result += \"    \".repeat(indent);\n            startOfLine = false;\n        }\n        result += s;\n    }\n\n    function appendNewLine() {\n        append(\"\\n\");\n        startOfLine = true;\n    }\n\n    function appendType(type: z.ZodType, minPrecedence = 0) {\n        const name = namedTypes.get(getTypeIdentity(type));\n        if (name) {\n            append(name);\n        }\n        else {\n            const parenthesize = getTypePrecedence(type) < minPrecedence;\n            if (parenthesize) append(\"(\");\n            appendTypeDefinition(type);\n            if (parenthesize) append(\")\");\n        }\n    }\n\n    function appendTypeDefinition(type: z.ZodType) {\n        switch (getTypeKind(type)) {\n            case z.ZodFirstPartyTypeKind.ZodString:\n                return append(\"string\");\n            case z.ZodFirstPartyTypeKind.ZodNumber:\n                return append(\"number\");\n            case z.ZodFirstPartyTypeKind.ZodBoolean:\n                return append(\"boolean\");\n            case z.ZodFirstPartyTypeKind.ZodDate:\n                return append(\"Date\");\n            case z.ZodFirstPartyTypeKind.ZodUndefined:\n                return append(\"undefined\");\n            case z.ZodFirstPartyTypeKind.ZodNull:\n                return append(\"null\");\n            case z.ZodFirstPartyTypeKind.ZodUnknown:\n                return append(\"unknown\");\n            case z.ZodFirstPartyTypeKind.ZodArray:\n                return appendArrayType(type);\n            case z.ZodFirstPartyTypeKind.ZodObject:\n                return appendObjectType(type);\n            case z.ZodFirstPartyTypeKind.ZodUnion:\n                return appendUnionOrIntersectionTypes((type._def as z.ZodUnionDef).options, TypePrecedence.Union);\n            case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion:\n                return appendUnionOrIntersectionTypes([...(type._def as z.ZodDiscriminatedUnionDef<string>).options.values()], TypePrecedence.Union);\n            case z.ZodFirstPartyTypeKind.ZodIntersection:\n                return appendUnionOrIntersectionTypes((type._def as z.ZodUnionDef).options, TypePrecedence.Intersection);\n            case z.ZodFirstPartyTypeKind.ZodTuple:\n                return appendTupleType(type);\n            case z.ZodFirstPartyTypeKind.ZodRecord:\n                return appendRecordType(type);\n            case z.ZodFirstPartyTypeKind.ZodLiteral:\n                return appendLiteral((type._def as z.ZodLiteralDef).value);\n            case z.ZodFirstPartyTypeKind.ZodEnum:\n                return append((type._def as z.ZodEnumDef).values.map(value => JSON.stringify(value)).join(\" | \"));\n            case z.ZodFirstPartyTypeKind.ZodOptional:\n                return appendUnionOrIntersectionTypes([(type._def as z.ZodOptionalDef).innerType, z.undefined()], TypePrecedence.Union);\n            case z.ZodFirstPartyTypeKind.ZodReadonly:\n                return appendReadonlyType(type);\n        }\n        append(\"any\");\n    }\n\n    function appendArrayType(arrayType: z.ZodType) {\n        appendType((arrayType._def as z.ZodArrayDef).type, TypePrecedence.Object);\n        append(\"[]\");\n    }\n\n    function appendObjectType(objectType: z.ZodType) {\n        append(\"{\");\n        appendNewLine();\n        indent++;\n        for (let [name, type] of Object.entries((objectType._def as z.ZodObjectDef).shape())) {\n            const comment = type.description;\n            append(name);\n            if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodOptional) {\n                append(\"?\");\n                type = (type._def as z.ZodOptionalDef).innerType;\n            }\n            append(\": \");\n            appendType(type);\n            append(\";\");\n            if (comment) append(` // ${comment}`);\n            appendNewLine();\n        }\n        indent--;\n        append(\"}\");\n    }\n\n    function appendUnionOrIntersectionTypes(types: readonly z.ZodType[], minPrecedence: TypePrecedence) {\n        let first = true;\n        for (const type of types) {\n            if (!first) append(minPrecedence === TypePrecedence.Intersection ? \" & \" : \" | \");\n            appendType(type, minPrecedence);\n            first = false;\n        }\n    }\n\n    function appendTupleType(tupleType: z.ZodType) {\n        append(\"[\");\n        let first = true;\n        for (let type of (tupleType._def as z.ZodTupleDef<z.ZodTupleItems, z.ZodType>).items) {\n            if (!first) append(\", \");\n            if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodOptional) {\n                appendType((type._def as z.ZodOptionalDef).innerType, TypePrecedence.Object);\n                append(\"?\");\n            }\n            else {\n                appendType(type);\n            }\n            first = false;\n        }\n        const rest = (tupleType._def as z.ZodTupleDef<z.ZodTupleItems, z.ZodType>).rest;\n        if (rest) {\n            if (!first) append(\", \");\n            append(\"...\");\n            appendType(rest, TypePrecedence.Object);\n            append(\"[]\");\n        }\n        append(\"]\");\n    }\n\n    function appendRecordType(recordType: z.ZodType) {\n        append(\"Record<\");\n        appendType((recordType._def as z.ZodRecordDef).keyType);\n        append(\", \");\n        appendType((recordType._def as z.ZodRecordDef).valueType);\n        append(\">\");\n    }\n\n    function appendLiteral(value: unknown) {\n        append(typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\" ? JSON.stringify(value) : \"any\");\n    }\n\n    function appendReadonlyType(readonlyType: z.ZodType) {\n        append(\"Readonly<\");\n        appendType((readonlyType._def as z.ZodReadonlyDef).innerType);\n        append(\">\");\n    }\n}\n"
  }
]