Repository: microsoft/TypeChat Branch: main Commit: 7ee5629065a0 Files: 246 Total size: 509.3 KB Directory structure: gitextract_lrpyygt1/ ├── .devcontainer/ │ └── devcontainer.json ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.js.yml │ ├── ci.python.yml │ └── github-pages.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── TypeChat.code-workspace ├── dotnet/ │ └── README.md ├── python/ │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── examples/ │ │ ├── README.md │ │ ├── calendar/ │ │ │ ├── README.md │ │ │ ├── demo.py │ │ │ ├── input.txt │ │ │ └── schema.py │ │ ├── coffeeShop/ │ │ │ ├── README.md │ │ │ ├── demo.py │ │ │ ├── input.txt │ │ │ ├── input2.txt │ │ │ └── schema.py │ │ ├── healthData/ │ │ │ ├── README.md │ │ │ ├── demo.py │ │ │ ├── input.txt │ │ │ ├── schema.py │ │ │ └── translator.py │ │ ├── math/ │ │ │ ├── README.md │ │ │ ├── demo.py │ │ │ ├── input.txt │ │ │ ├── program.py │ │ │ ├── schema.py │ │ │ └── schemaV2.py │ │ ├── multiSchema/ │ │ │ ├── README.md │ │ │ ├── agents.py │ │ │ ├── demo.py │ │ │ ├── input.txt │ │ │ └── router.py │ │ ├── music/ │ │ │ ├── README.md │ │ │ ├── client.py │ │ │ ├── demo.py │ │ │ ├── input.txt │ │ │ ├── schema.py │ │ │ └── spotipyWrapper.py │ │ ├── restaurant/ │ │ │ ├── README.md │ │ │ ├── demo.py │ │ │ ├── input.txt │ │ │ └── schema.py │ │ └── sentiment/ │ │ ├── README.md │ │ ├── demo.py │ │ ├── input.txt │ │ └── schema.py │ ├── notebooks/ │ │ ├── calendar.ipynb │ │ ├── coffeeShop.ipynb │ │ ├── healthData.ipynb │ │ ├── math.ipynb │ │ ├── music.ipynb │ │ ├── restaurant.ipynb │ │ └── sentiment.ipynb │ ├── package.json │ ├── pyproject.toml │ ├── pyrightconfig.json │ ├── src/ │ │ └── typechat/ │ │ ├── __about__.py │ │ ├── __init__.py │ │ ├── _internal/ │ │ │ ├── __init__.py │ │ │ ├── interactive.py │ │ │ ├── model.py │ │ │ ├── result.py │ │ │ ├── translator.py │ │ │ ├── ts_conversion/ │ │ │ │ ├── __init__.py │ │ │ │ ├── python_type_to_ts_nodes.py │ │ │ │ ├── ts_node_to_string.py │ │ │ │ └── ts_type_nodes.py │ │ │ └── validator.py │ │ └── py.typed │ └── tests/ │ ├── __init__.py │ ├── __py3.11_snapshots__/ │ │ ├── test_conflicting_names_1/ │ │ │ └── test_conflicting_names_1.schema.d.ts │ │ └── test_hello_world/ │ │ └── test_generic_alias1.schema.d.ts │ ├── __py3.12+_snapshots__/ │ │ ├── test_generic_alias_3/ │ │ │ └── test_generic_alias3.schema.d.ts │ │ ├── test_generic_alias_4/ │ │ │ └── test_generic_alias4.schema.d.ts │ │ └── test_type_alias_syntax/ │ │ └── test_type_alias_union1.schema.d.ts │ ├── __py3.12_snapshots__/ │ │ ├── test_conflicting_names_1/ │ │ │ └── test_conflicting_names_1.schema.d.ts │ │ └── test_hello_world/ │ │ └── test_generic_alias1.schema.d.ts │ ├── __py3.13_snapshots__/ │ │ ├── test_conflicting_names_1/ │ │ │ └── test_conflicting_names_1.schema.d.ts │ │ └── test_hello_world/ │ │ └── test_generic_alias1.schema.d.ts │ ├── __py3.14_snapshots__/ │ │ ├── test_conflicting_names_1/ │ │ │ └── test_conflicting_names_1.schema.d.ts │ │ └── test_hello_world/ │ │ └── test_generic_alias1.schema.d.ts │ ├── __snapshots__/ │ │ ├── test_coffeeshop/ │ │ │ └── test_coffeeshop_schema.schema.d.ts │ │ ├── test_dataclasses/ │ │ │ └── test_data_classes.schema.d.ts │ │ ├── test_generic_alias_1/ │ │ │ └── test_generic_alias1.schema.d.ts │ │ ├── test_generic_alias_2/ │ │ │ └── test_generic_alias2.schema.d.ts │ │ ├── test_translator.ambr │ │ ├── test_tuple_errors_1/ │ │ │ └── test_tuples_2.schema.d.ts │ │ └── test_tuples_1/ │ │ └── test_tuples_1.schema.d.ts │ ├── coffeeshop_deprecated.py │ ├── test_coffeeshop.py │ ├── test_conflicting_names_1.py │ ├── test_dataclasses.py │ ├── test_generic_alias_1.py │ ├── test_generic_alias_2.py │ ├── test_generic_alias_3.py │ ├── test_generic_alias_4.py │ ├── test_hello_world.py │ ├── test_translator.py │ ├── test_tuple_errors_1.py │ ├── test_tuples_1.py │ ├── test_type_alias_syntax.py │ ├── test_validator.py │ └── utilities.py ├── site/ │ ├── .eleventy.js │ ├── .gitignore │ ├── jsconfig.json │ ├── package.json │ └── src/ │ ├── _data/ │ │ ├── docsTOC.json │ │ └── headernav.json │ ├── _includes/ │ │ ├── base.njk │ │ ├── blog.njk │ │ ├── doc-page.njk │ │ ├── docs.njk │ │ ├── footer.njk │ │ └── header-prologue.njk │ ├── blog/ │ │ ├── announcing-typechat-0-1-0.md │ │ ├── index.njk │ │ └── introducing-typechat.md │ ├── css/ │ │ ├── noscript-styles.css │ │ └── styles.css │ ├── docs/ │ │ ├── examples.md │ │ ├── faq.md │ │ ├── index.njk │ │ ├── introduction.md │ │ ├── python/ │ │ │ └── basic-usage.md │ │ ├── techniques.md │ │ └── typescript/ │ │ └── basic-usage.md │ ├── index.njk │ └── js/ │ └── interactivity.js └── typescript/ ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples/ │ ├── README.md │ ├── calendar/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── calendarActionsSchema.ts │ │ ├── expectedOutput.txt │ │ ├── input.txt │ │ ├── main.ts │ │ └── tsconfig.json │ ├── coffeeShop/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── coffeeShopSchema.ts │ │ ├── input.txt │ │ ├── input2.txt │ │ ├── main.ts │ │ └── tsconfig.json │ ├── coffeeShop-zod/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── coffeeShopSchema.ts │ │ ├── input.txt │ │ ├── input2.txt │ │ ├── main.ts │ │ └── tsconfig.json │ ├── crossword/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── crosswordSchema.ts │ │ ├── input.txt │ │ ├── main.ts │ │ ├── translator.ts │ │ └── tsconfig.json │ ├── healthData/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── healthDataSchema.ts │ │ ├── input.txt │ │ ├── main.ts │ │ ├── translator.ts │ │ └── tsconfig.json │ ├── math/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── input.txt │ │ ├── main.ts │ │ ├── mathSchema.ts │ │ └── tsconfig.json │ ├── multiSchema/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── agent.ts │ │ ├── classificationSchema.ts │ │ ├── input.txt │ │ ├── main.ts │ │ ├── router.ts │ │ └── tsconfig.json │ ├── music/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── migrations.md │ │ ├── package.json │ │ └── src/ │ │ ├── authz.ts │ │ ├── callback.html │ │ ├── chatifyActionsSchema.ts │ │ ├── dbInterface.ts │ │ ├── endpoints.ts │ │ ├── input.txt │ │ ├── localParser.ts │ │ ├── main.ts │ │ ├── playback.ts │ │ ├── service.ts │ │ ├── trackCollections.ts │ │ ├── trackFilter.ts │ │ └── tsconfig.json │ ├── restaurant/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── foodOrderViewSchema.ts │ │ ├── input.txt │ │ ├── main.ts │ │ └── tsconfig.json │ ├── sentiment/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── input.txt │ │ ├── main.ts │ │ ├── sentimentSchema.ts │ │ └── tsconfig.json │ └── sentiment-zod/ │ ├── README.md │ ├── package.json │ └── src/ │ ├── input.txt │ ├── main.ts │ ├── sentimentSchema.ts │ └── tsconfig.json ├── package.json └── src/ ├── index.ts ├── interactive/ │ ├── index.ts │ └── interactive.ts ├── model.ts ├── result.ts ├── ts/ │ ├── index.ts │ ├── program.ts │ └── validate.ts ├── tsconfig.json ├── typechat.ts └── zod/ ├── index.ts └── validate.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu { "name": "TypeChat Development (Python and TypeScript)", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/python:1-3.12", // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/node:1": { "nodeGypDependencies": true, "version": "lts", "nvmVersion": "latest" }, "ghcr.io/devcontainers-contrib/features/hatch:2": { "version": "latest" } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [7860, 7861, 7862, 7863, 7864, 7865, 7866, 7867, 7868, 7869, 7870], // Configure tool-specific properties. "customizations": { "vscode": { "extensions": [ "ms-python.black-formatter" ], "settings": { // Force the editor to pick up on the right environment and interpreter. "python.defaultInterpreterPath": "/workspaces/TypeChat/.venv", // Respect the paths of the interpreter. "python.analysis.autoSearchPaths": false } } }, // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": { "site - npm": "cd site; npm ci", "typescript - npm": "cd typescript; npm ci", "python - hatch": "cd python; hatch env create", "python - npm": "cd python; npm ci" } // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for more information: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates # https://containers.dev/guide/dependabot version: 2 updates: - package-ecosystem: "devcontainers" directory: "/" schedule: interval: weekly - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly ================================================ FILE: .github/workflows/ci.js.yml ================================================ # 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 # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs name: Node.js CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] permissions: contents: read # Ensure scripts are run with pipefail. See: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference defaults: run: shell: bash working-directory: typescript jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x, 22.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: "npm" cache-dependency-path: "typescript/package-lock.json" - run: npm ci - run: npm run build-all ================================================ FILE: .github/workflows/ci.python.yml ================================================ name: Python CI on: push: branches: - main pull_request: branches: - main defaults: run: shell: bash working-directory: ./python jobs: pyright: permissions: contents: read strategy: fail-fast: false matrix: os: - ubuntu-latest python-version: - '3.11' - '3.12' - '3.13' - '3.14' runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Hatch run: | python -m pip install --upgrade pip pip install hatch "virtualenv<20.29" - name: Set up Hatch Environment run: | hatch env create HATCH_ENV=$(hatch env find) echo $HATCH_ENV echo "$HATCH_ENV/bin" >> $GITHUB_PATH - name: Get Pyright Version id: pyright-version run: | PYRIGHT_VERSION=$(jq -r '.devDependencies.pyright' < package.json) echo $PYRIGHT_VERSION echo "version=$PYRIGHT_VERSION" >> $GITHUB_OUTPUT - name: Run pyright ${{ steps.pyright-version.outputs.version }} uses: jakebailey/pyright-action@v3 with: version: ${{ steps.pyright-version.outputs.version }} python-version: ${{ matrix.python-version}} annotate: ${{ matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest' }} # Only let one build post comments. working-directory: ./python - name: Test with Pytest run: | pytest -vv ================================================ FILE: .github/workflows/github-pages.yml ================================================ name: Deploy to GitHub Pages on: push: branches: - main permissions: contents: read id-token: write pages: write jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: 18.x cache: 'npm' cache-dependency-path: "site/package-lock.json" - name: Build site run: | cd site echo "Building the site" npm ci npm run build - name : Upload artifact uses: actions/upload-pages-artifact@v4 with: name: github-pages path: site/_site - name: Deploy to GitHub Pages from artifacts uses: actions/deploy-pages@v4 ================================================ FILE: .gitignore ================================================ build/ dist/ out/ node_modules/ .venv/ .env* *.map *.out.txt *.bat # Local development and debugging .scratch/ **/.vscode/* **/tsconfig.debug.json !**/.vscode/launch.json **/build.bat ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: README.md ================================================ # TypeChat TypeChat is a library that makes it easy to build natural language interfaces using types. Building 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. TypeChat replaces _prompt engineering_ with _schema engineering_. Simply 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. After defining your types, TypeChat takes care of the rest by: 1. Constructing a prompt to the LLM using types. 2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction. 3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent. Types are all you need! # Getting Started Install TypeChat for TypeScript/JavaScript: ``` npm install typechat ``` You can also work with TypeChat from source for: * [Python](./python/README.md) * [TypeScript](./typescript/README.md) * [C#/.NET](https://github.com/microsoft/TypeChat.net) To 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. To learn more about TypeChat, visit the [documentation](https://microsoft.github.io/TypeChat) which includes more information on TypeChat and how to get started. ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ================================================ FILE: SECURITY.md ================================================ ## Security Microsoft 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/). If 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. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). If 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). You 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). Please 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: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If 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. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). ================================================ FILE: SUPPORT.md ================================================ # Support ## How to file issues and get help This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue. For help and questions about using this project, please either use the project's GitHub Discussions area or Stack Overflow. ## Microsoft Support Policy Support for this project is limited to the resources listed above. ================================================ FILE: TypeChat.code-workspace ================================================ { "folders": [ { "name": "TypeChat Root", "path": "./" }, { "name": "Python", "path": "./python" }, { "name": "TypeScript", "path": "./typescript" }, ], "settings": { } } ================================================ FILE: dotnet/README.md ================================================ # TypeChat for .NET TypeChat in .NET and C# is currently available on a separate [TypeChat.NET repository](https://github.com/microsoft/TypeChat.net). ================================================ FILE: python/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ node_modules ================================================ FILE: python/LICENSE ================================================ MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: python/README.md ================================================ # TypeChat TypeChat is a library that makes it easy to build natural language interfaces using types. Building 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. TypeChat replaces _prompt engineering_ with _schema engineering_. Simply 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. After defining your types, TypeChat takes care of the rest by: 1. Constructing a prompt to the LLM using types. 2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction. 3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent. Types are all you need! ## Getting Started Install TypeChat: ```sh pip install typechat ``` You can also develop TypeChat from source, which needs [Python >=3.11](https://www.python.org/downloads/), [hatch](https://hatch.pypa.io/1.6/install/), and [Node.js >=20](https://nodejs.org/en/download): ```sh git clone https://github.com/microsoft/TypeChat cd TypeChat/python hatch shell npm ci ``` To see TypeChat in action, we recommend exploring the [TypeChat example projects](https://github.com/microsoft/TypeChat/tree/main/python/examples). You can try them on your local machine or in a GitHub Codespace. To learn more about TypeChat, visit the [documentation](https://microsoft.github.io/TypeChat/docs/python/basic-usage/) which includes more information on TypeChat and how to get started. ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ================================================ FILE: python/examples/README.md ================================================ To see TypeChat in action, check out the examples found in this directory. Each 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. We are working to reproduce outputs with other models. Generally, models trained on both code and natural language text have high accuracy. We recommend reading each example in the following order. | Name | Description | | ---- | ----------- | | [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!" | | [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. | [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. | | [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"). | | [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. | | [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. | | [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. | | [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. | ## Step 1: Configure your development environment ### Option 1: Local Machine You can experiment with these TypeChat examples on your local machine. You will need [Python >=3.11](https://www.python.org/downloads/) and [hatch](https://hatch.pypa.io/1.6/install/). ```sh git clone https://github.com/microsoft/TypeChat cd TypeChat/python hatch shell python examples/sentiment/demo.py ``` Alternatively, you can just use `venv` and `pip`: ```sh git clone https://github.com/microsoft/TypeChat cd TypeChat/python python -m venv ../.venv # Activate the virtual environment # Windows ../.venv/Scripts/Activate.ps1 # Unix/POSIX source ../.venv/bin/activate pip install .[examples] python examples/sentiment/demo.py ``` ### Option 2: GitHub Codespaces GitHub Codespaces enables you to try TypeChat quickly in a development environment hosted in the cloud. On the TypeChat repository page: 1. Click the green button labeled `<> Code` 2. Select the `Codespaces` tab. 3. Click the green `Create codespace` button.
If this is your first time creating a codespace, read this. If 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. Once 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. Remember 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. For more information, see the [GitHub Codespaces Overview](https://docs.github.com/en/codespaces/overview)
## Step 2: Configure environment variables Currently, the examples are running on OpenAI or Azure OpenAI endpoints. To use an OpenAI endpoint, include the following environment variables: | Variable | Value | |----------|-------| | `OPENAI_MODEL`| The OpenAI model name (e.g. `gpt-3.5-turbo` or `gpt-4`) | | `OPENAI_API_KEY` | Your OpenAI API key | | `OPENAI_ENDPOINT` | OpenAI API Endpoint - *optional*, defaults to `"https://api.openai.com/v1/chat/completions"` | | `OPENAI_ORGANIZATION` | OpenAI Organization - *optional*, defaults to `""` | To use an Azure OpenAI endpoint, include the following environment variables: | Variable | Value | |----------|-------| | `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`) | | `AZURE_OPENAI_API_KEY` | Your Azure OpenAI API key | We recommend setting environment variables by creating a `.env` file in the root directory of the project that looks like the following: ```ini # For OpenAI OPENAI_MODEL=... OPENAI_API_KEY=... # For Azure OpenAI AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=... ``` ## Step 3: Run the examples Examples can be found in the `examples` directory. To run an example interactively, type `python examples//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 F5 to launch it in debug mode. Note 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. To run an example with one of these input files, run `python demo.py `. For example, in the `coffeeShop` directory, you can run: ``` python demo.py input.txt ``` ================================================ FILE: python/examples/calendar/README.md ================================================ # Calendar The 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. # Try Calendar To run the Calendar example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`input.txt`](./input.txt). For example, we could use natural language to describe an event coming up soon: **Input**: ``` 📅> I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024 ``` **Output**: ```json { "actions": [ { "actionType": "add event", "event": { "day": "Friday March 15, 2024", "timeRange": { "startTime": "12:00 pm", "endTime": "2:00 pm" }, "description": "get my tires changed" } } ] } ``` ================================================ FILE: python/examples/calendar/demo.py ================================================ import asyncio import json import sys from dotenv import dotenv_values import schema as calendar from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatValidator(calendar.CalendarActions) translator = TypeChatJsonTranslator(model, validator, calendar.CalendarActions) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) if any(item["actionType"] == "Unknown" for item in result["actions"]): print("I did not understand the following") for item in result["actions"]: if item["actionType"] == "Unknown": print(item["text"]) file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("📅> ", file_path, request_handler) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/calendar/input.txt ================================================ I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024 Search for any meetings with Gavin this week Set up an event for friday named Jeffs pizza party at 6pm Please add Jennifer to the scrum next Thursday Will you please add an appointment with Jerri Skinner at 9 am? I need it to last 2 hours Do I have any plan with Rosy this month? I 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 Add meeting with team today at 2 can you record lunch with Luis at 12pm on Friday and also add Isobel to the Wednesday ping pong game at 4pm I 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 ================================================ FILE: python/examples/calendar/schema.py ================================================ from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc class UnknownAction(TypedDict): """ if the user types text that can not easily be understood as a calendar action, this action is used """ actionType: Literal["Unknown"] text: Annotated[str, Doc("text typed by the user that the system did not understand")] class EventTimeRange(TypedDict, total=False): startTime: str endTime: str duration: str class Event(TypedDict): day: Annotated[str, Doc("date (example: March 22, 2024) or relative date (example: after EventReference)")] timeRange: EventTimeRange description: str location: NotRequired[str] participants: NotRequired[Annotated[list[str], Doc("a list of people or named groups like 'team'")]] class EventReference(TypedDict, total=False): """ properties used by the requester in referring to an event these properties are only specified if given directly by the requester """ day: Annotated[str, Doc("date (example: March 22, 2024) or relative date (example: after EventReference)")] dayRange: Annotated[str, Doc("(examples: this month, this week, in the next two days)")] timeRange: EventTimeRange description: str location: str participants: list[str] class FindEventsAction(TypedDict): actionType: Literal["find events"] eventReference: Annotated[EventReference, Doc("one or more event properties to use to search for matching events")] class ChangeDescriptionAction(TypedDict): actionType: Literal["change description"] eventReference: NotRequired[Annotated[EventReference, Doc("event to be changed")]] description: Annotated[str, Doc("new description for the event")] class ChangeTimeRangeAction(TypedDict): actionType: Literal["change time range"] eventReference: NotRequired[Annotated[EventReference, Doc("event to be changed")]] timeRange: Annotated[EventTimeRange, Doc("new time range for the event")] class AddParticipantsAction(TypedDict): actionType: Literal["add participants"] eventReference: NotRequired[ Annotated[EventReference, Doc("event to be augmented; if not specified assume last event discussed")] ] participants: NotRequired[Annotated[list[str], "new participants (one or more)"]] class RemoveEventAction(TypedDict): actionType: Literal["remove event"] eventReference: EventReference class AddEventAction(TypedDict): actionType: Literal["add event"] event: Event Actions = ( AddEventAction | RemoveEventAction | AddParticipantsAction | ChangeTimeRangeAction | ChangeDescriptionAction | FindEventsAction | UnknownAction ) class CalendarActions(TypedDict): actions: list[Actions] ================================================ FILE: python/examples/coffeeShop/README.md ================================================ # Coffee Shop The Coffee Shop example shows how to capture user intent as a set of "nouns". In this case, the nouns are items in a coffee order, where valid items are defined starting from the [`Cart`](./schema.py) type. This 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). # Try Coffee Shop To run the Coffee Shop example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./input.txt) and [`src/input2.txt`](./input2.txt). For example, we could use natural language to describe our coffee shop order: **Input**: ``` ☕> we'd like a cappuccino with a pack of sugar ``` **Output**: ```json { "items": [ { "type": "lineitem", "product": { "type": "LatteDrinks", "name": "cappuccino", "options": [ { "type": "Sweeteners", "name": "sugar", "optionQuantity": "regular" } ] }, "quantity": 1 } ] } ``` ================================================ FILE: python/examples/coffeeShop/demo.py ================================================ import asyncio import json import sys import schema as coffeeshop from dotenv import dotenv_values from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatValidator(coffeeshop.Cart) translator = TypeChatJsonTranslator(model, validator, coffeeshop.Cart) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) if any(item["type"] == "Unknown" for item in result["items"]): print("I did not understand the following") for item in result["items"]: if item["type"] == "Unknown": print(item["text"]) file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("☕> ", file_path, request_handler) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/coffeeShop/input.txt ================================================ i'd like a latte that's it i'll have a dark roast coffee thank you get me a coffee please could i please get two mochas that's all we need twenty five flat whites and that'll do it how about a tall cappuccino i'd like a venti iced latte i'd like a iced venti latte i'd like a venti latte iced i'd like a latte iced venti we'll also have a short tall latte i wanna latte macchiato with vanilla how about a peppermint latte may i also get a decaf soy vanilla syrup caramel latte with sugar and foam i want a latte with peppermint syrup with peppermint syrup i'd like a decaf half caf latte can I get a skim soy latte i'd like a light nutmeg espresso that's it can i have an cappuccino no foam can i have an espresso with no nutmeg we want a light whipped no foam mocha with extra hazelnut and cinnamon i'd like a latte cut in half i'd like a strawberry latte i want a five pump caramel flat white i want a flat white with five pumps of caramel syrup i want a two pump peppermint three squirt raspberry skinny vanilla latte with a pump of caramel and two sugars i want a latte cappuccino espresso and an apple muffin i'd like a tall decaf latte iced a grande cappuccino double espresso and a warmed poppyseed muffin sliced in half we'd like a latte with soy and a coffee with soy i want a latte latte macchiato and a chai latte we'd like a cappuccino with two pumps of vanilla make that cappuccino with three pumps of vanilla we'd like a cappuccino with a pack of sugar make that cappuccino with two packs of sugar we'd like a cappuccino with a pack of sugar make that with two packs of sugar i'd like a flat white with two equal add three equal to the flat white i'd like a flat white with two equal two tall lattes. the first one with no foam. the second one with whole milk. two tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande. un petit cafe en lille kaffe a raspberry latte a strawberry latte roses are red two lawnmowers, a grande latte and a tall tree ================================================ FILE: python/examples/coffeeShop/input2.txt ================================================ two tall lattes. the first one with no foam. the second one with whole milk. two tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande. un petit cafe en lille kaffe a raspberry latte a strawberry latte roses are red two lawnmowers, a grande latte and a tall tree ================================================ FILE: python/examples/coffeeShop/schema.py ================================================ from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc class UnknownText(TypedDict): """ Use this type for order items that match nothing else """ type: Literal["Unknown"] text: Annotated[str, Doc("The text that wasn't understood")] class Caffeine(TypedDict): type: Literal["Caffeine"] name: Literal["regular", "two thirds caf", "half caf", "one third caf", "decaf"] class Milk(TypedDict): type: Literal["Milk"] name: Literal[ "whole milk", "two percent milk", "nonfat milk", "coconut milk", "soy milk", "almond milk", "oat milk" ] class Creamer(TypedDict): type: Literal["Creamer"] name: Literal[ "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", ] class Topping(TypedDict): type: Literal["Topping"] name: Literal["cinnamon", "foam", "ice", "nutmeg", "whipped cream", "water"] optionQuantity: NotRequired["OptionQuantity"] class LattePreparation(TypedDict): type: Literal["LattePreparation"] name: Literal["for here cup", "lid", "with room", "to go", "dry", "wet"] class Sweetener(TypedDict): type: Literal["Sweetener"] name: Literal["equal", "honey", "splenda", "sugar", "sugar in the raw", "sweet n low", "espresso shot"] optionQuantity: NotRequired["OptionQuantity"] CaffeineOptions = Caffeine | Milk | Creamer LatteOptions = CaffeineOptions | Topping | LattePreparation | Sweetener CoffeeTemperature = Literal["hot", "extra hot", "warm", "iced"] CoffeeSize = Literal["short", "tall", "grande", "venti"] EspressoSize = Literal["solo", "doppio", "triple", "quad"] OptionQuantity = Literal["no", "light", "regular", "extra"] | int class Syrup(TypedDict): type: Literal["Syrup"] name: Literal[ "almond syrup", "buttered rum syrup", "caramel syrup", "cinnamon syrup", "hazelnut syrup", "orange syrup", "peppermint syrup", "raspberry syrup", "toffee syrup", "vanilla syrup", ] optionQuantity: NotRequired[OptionQuantity] class LatteDrink(TypedDict): type: Literal["LatteDrink"] name: Literal["cappuccino", "flat white", "latte", "latte macchiato", "mocha", "chai latte"] temperature: NotRequired["CoffeeTemperature"] size: NotRequired[Annotated[CoffeeSize, Doc("The default is 'grande'")]] options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]] class EspressoDrink(TypedDict): type: Literal["EspressoDrink"] name: Literal["espresso", "lungo", "ristretto", "macchiato"] temperature: NotRequired["CoffeeTemperature"] size: NotRequired[Annotated["EspressoSize", Doc("The default is 'doppio'")]] options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]] class CoffeeDrink(TypedDict): type: Literal["CoffeeDrink"] name: Literal["americano", "coffee"] temperature: NotRequired[CoffeeTemperature] size: NotRequired[Annotated[CoffeeSize, Doc("The default is 'grande'")]] options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]] class BakeryOption(TypedDict): type: Literal["BakeryOption"] name: Literal["butter", "strawberry jam", "cream cheese"] optionQuantity: NotRequired["OptionQuantity"] class BakeryPreparation(TypedDict): type: Literal["BakeryPreparation"] name: Literal["warmed", "cut in half"] class BakeryProduct(TypedDict): type: Literal["BakeryProduct"] name: Literal["apple bran muffin", "blueberry muffin", "lemon poppyseed muffin", "bagel"] options: list[BakeryOption | BakeryPreparation] Product = BakeryProduct | LatteDrink | EspressoDrink | CoffeeDrink | UnknownText class LineItem(TypedDict): type: Literal["LineItem"] product: Product quantity: int class Cart(TypedDict): type: Literal["Cart"] items: list[LineItem | UnknownText] ================================================ FILE: python/examples/healthData/README.md ================================================ # Health Data Agent This example requires GPT-4. Demonstrates 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. 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"). ## Target models For best and consistent results, use **gpt-4**. ## Try the Health Data Agent To run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). ## Usage Example prompts can be found in [`input.txt`](./input.txt). For example, given the following input statement: **Input**: ```console 🤧> I am taking klaritin for my allergies ``` **Output**: ================================================ FILE: python/examples/healthData/demo.py ================================================ import asyncio import json import sys from dotenv import dotenv_values import schema as health from typechat import Failure, TypeChatValidator, create_language_model, process_requests from translator import TranslatorWithHistory health_instructions = """ Help me enter my health data step by step. Ask specific questions to gather required and optional fields I have not already providedStop asking if I don't know the answer Automatically fix my spelling mistakes My health data may be complex: always record and return ALL of it. Always return a response: - If you don't understand what I say, ask a question. - At least respond with an OK message. """ async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatValidator(health.HealthDataResponse) translator = TranslatorWithHistory( model, validator, health.HealthDataResponse, additional_agent_instructions=health_instructions ) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) agent_message = result.get("message", "None") not_translated = result.get("notTranslated", None) if agent_message: print(f"\n📝: {agent_message}") if not_translated: print(f"\n🤔: I did not understand\n {not_translated}") file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("💉💊🤧> ", file_path, request_handler) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/healthData/input.txt ================================================ # # Conversations with a Health Data Agent # For each conversation: # You start with the first line # Then type the next line in response # # ================ # USE GPT4 # ================ # Conversation: i want to record my shingles August 2016 It lasted 3 months I also broke my foot I broke it in high school 2001 The foot took a year to be ok # Conversation: klaritin 2 tablets 3 times a day 300 mg actually that is 1 tablet @clear # Conversation: klaritin 1 pill, morning and before bedtime Can't remember Actually, that is 3 tablets 500 mg @clear #Conversation I am taking binadryl now As needed. Groceery store strength That is all I have I also got allergies. Pollen @clear # Conversation: Robotussin 1 cup Daily, as needed Robotussin with Codeine Put down strength as I don't know @clear # Conversation: Hey Melatonin 1 3mg tablet every night @clear # Conversation: I got the flu Started 2 weeks ago Its gone now. Only lasted about a week I took some sudafed though I took 2 sudafed twice a day. Regular strength @clear ================================================ FILE: python/examples/healthData/schema.py ================================================ from typing_extensions import TypedDict, Annotated, NotRequired, Literal, Doc class Quantity(TypedDict): value: Annotated[float, Doc("Exact number")] units: Annotated[str, Doc("UNITS include mg, kg, cm, pounds, liter, ml, tablet, pill, cup, per-day, per-week..ETC")] class ApproxDatetime(TypedDict): displayText: Annotated[str, Doc("Default: Unknown. Required")] timestamp: NotRequired[Annotated[str, Doc("If precise timestamp can be set")]] class ApproxQuantity(TypedDict): displayText: Annotated[str, Doc("Default: Unknown. Required")] quantity: NotRequired[Annotated[Quantity, Doc("Optional: only if precise quantities are available")]] class OtherHealthData(TypedDict): """ Use for health data that match nothing else. E.g. immunization, blood prssure etc """ text: str when: NotRequired[ApproxDatetime] class Condition(TypedDict): """ Disease, Ailment, Injury, Sickness """ name: Annotated[str, Doc("Fix any spelling mistakes, especially phonetic spelling")] startDate: Annotated[ApproxDatetime, Doc("When the condition started? Required")] status: Annotated[ Literal["active", "recurrence", "relapse", "inactive", "remission", "resolved", "unknown"], Doc("Always ask for current status of the condition"), ] endDate: NotRequired[Annotated[ApproxDatetime, Doc("If the condition was no longer active")]] class Medication(TypedDict): """ Meds, pills etc. """ name: Annotated[str, Doc("Fix any spelling mistakes, especially phonetic spelling")] dose: Annotated[ApproxQuantity, Doc("E.g. 2 tablets, 1 cup. Required")] frequency: Annotated[ApproxQuantity, Doc("E.g. twice a day. Required")] strength: Annotated[ApproxQuantity, Doc("E.g. 50 mg. Required")] class HealthData(TypedDict, total=False): medication: list[Medication] condition: list[Condition] other: list[OtherHealthData] class HealthDataResponse(TypedDict, total=False): data: Annotated[HealthData, Doc("Return this if JSON has ALL required information. Else ask questions")] message: Annotated[str, Doc("Use this to ask questions and give pertinent responses")] notTranslated: Annotated[str, Doc("Use this parts of the user request not translateed, off topic, etc")] ================================================ FILE: python/examples/healthData/translator.py ================================================ import json from typing_extensions import TypeVar, Any, override, TypedDict, Literal from typechat import TypeChatValidator, TypeChatLanguageModel, TypeChatJsonTranslator, Result, Failure, PromptSection from datetime import datetime T = TypeVar("T", covariant=True) class ChatMessage(TypedDict): source: Literal["system", "user", "assistant"] body: Any class TranslatorWithHistory(TypeChatJsonTranslator[T]): _chat_history: list[ChatMessage] _max_prompt_length: int _additional_agent_instructions: str def __init__( self, model: TypeChatLanguageModel, validator: TypeChatValidator[T], target_type: type[T], additional_agent_instructions: str ): super().__init__(model=model, validator=validator, target_type=target_type) self._chat_history = [] self._max_prompt_length = 2048 self._additional_agent_instructions = additional_agent_instructions @override async def translate(self, input: str, *, prompt_preamble: str | list[PromptSection] | None = None) -> Result[T]: result = await super().translate(input=input, prompt_preamble=prompt_preamble) if not isinstance(result, Failure): self._chat_history.append(ChatMessage(source="assistant", body=result.value)) return result @override def _create_request_prompt(self, intent: str) -> str: # TODO: drop history entries if we exceed the max_prompt_length history_str = json.dumps(self._chat_history, indent=2, default=lambda o: None, allow_nan=False) now = datetime.now() prompt = F""" user: You are a service that translates user requests into JSON objects of type "{self.type_name}" according to the following TypeScript definitions: ''' {self.schema_str} ''' user: Use precise date and times RELATIVE TO CURRENT DATE: {now.strftime('%A, %m %d, %Y')} CURRENT TIME: {now.strftime("%H:%M:%S")} Also turn ranges like next week and next month into precise dates user: {self._additional_agent_instructions} system: IMPORTANT CONTEXT for the user request: {history_str} user: The following is a user request: ''' {intent} ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: """ return prompt ================================================ FILE: python/examples/math/README.md ================================================ # Math The 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. # Try Math To run the Math example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`input.txt`](./input.txt). For 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. **Input**: ``` 🟰> multiply two by three, then multiply four by five, then sum the results ``` **Output**: ``` import { API } from "./schema"; function program(api: API) { const step1 = api.mul(2, 3); const step2 = api.mul(4, 5); return api.add(step1, step2); } Running program: mul(2, 3) mul(4, 5) add(6, 20) Result: 26 ``` ================================================ FILE: python/examples/math/demo.py ================================================ import asyncio from collections.abc import Sequence import json import sys from typing import cast from dotenv import dotenv_values import schema as math from typechat import Failure, create_language_model, process_requests from program import TypeChatProgramTranslator, TypeChatProgramValidator, evaluate_json_program async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatProgramValidator() translator = TypeChatProgramTranslator(model, validator, math.MathAPI) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) math_result = await evaluate_json_program(result, apply_operations) print(f"Math Result: {math_result}") file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("🧮> ", file_path, request_handler) async def apply_operations(func: str, args: Sequence[object]) -> int | float: print(f"{func}({json.dumps(args)}) ") for arg in args: if not isinstance(arg, (int, float)): raise ValueError("All arguments are expected to be numeric.") args = cast(Sequence[int | float], args) match func: case "add": return args[0] + args[1] case "sub": return args[0] - args[1] case "mul": return args[0] * args[1] case "div": return args[0] / args[1] case "neg": return -1 * args[0] case "id": return args[0] case _: raise ValueError(f'Unexpected function name {func}') if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/math/input.txt ================================================ 1 + 2 1 + 2 * 3 2 * 3 + 4 * 5 2 3 * 4 5 * + multiply two by three, then multiply four by five, then sum the results ================================================ FILE: python/examples/math/program.py ================================================ from __future__ import annotations import asyncio from collections.abc import Sequence from typing import Any, TypeAlias, TypedDict, cast from typing_extensions import ( TypeVar, Callable, Awaitable, Annotated, NotRequired, override, Doc, ) from typechat import ( Failure, Success, TypeChatLanguageModel, TypeChatValidator, TypeChatJsonTranslator, python_type_to_typescript_schema, ) T = TypeVar("T", covariant=True) Expression: TypeAlias = "str | int | float | bool | None | dict[str, Expression] | list[Expression] | FunctionCall | ResultReference" JsonProgram = TypedDict("JsonProgram", {"@steps": list["FunctionCall"]}) ResultReference = TypedDict( "ResultReference", {"@ref": Annotated[int, Doc("Index of the previous expression in the 'steps' array")]} ) FunctionCall = TypedDict( "FunctionCall", { "@func": Annotated[str, Doc("Name of the function")], "@args": NotRequired[Annotated[list[Expression], Doc("Arguments for the function, if any")]], }, ) translation_result = python_type_to_typescript_schema(JsonProgram) program_schema_text = translation_result.typescript_schema_str JsonValue = str | int | float | bool | None | dict[str, "JsonValue"] | list["JsonValue"] async def evaluate_json_program( program: JsonProgram, onCall: Callable[[str, Sequence[JsonValue]], Awaitable[JsonValue]] ) -> JsonValue: results: list[JsonValue] = [] def evaluate_array(array: Sequence[JsonValue]) -> Awaitable[list[JsonValue]]: return asyncio.gather(*[evaluate_expression(e) for e in array]) async def evaluate_expression(expr: JsonValue) -> JsonValue: match expr: case bool() | int() | float() | str() | None: return expr case { "@ref": int(index) } if not isinstance(index, bool): if 0 <= index < len(results): return results[index] raise ValueError(f"Index {index} is out of range [0, {len(results)})") case { "@ref": ref_value }: raise ValueError(f"'ref' value must be an integer, but was ${ref_value}") case { "@func": str(function_name) }: args: list[JsonValue] match expr: case { "@args": None }: args = [] case { "@args": list() }: args = cast(list[JsonValue], expr["@args"]) # TODO case { "@args": _ }: raise ValueError("Given an invalid value for '@args'.") case _: args = [] return await onCall(function_name, await evaluate_array(args)) case list(array_expression_elements): return await evaluate_array(array_expression_elements) case _: raise ValueError("This condition should never hit") for step in program["@steps"]: results.append(await evaluate_expression(cast(JsonValue, step))) if len(results) > 0: return results[-1] else: return None class TypeChatProgramValidator(TypeChatValidator[JsonProgram]): def __init__(self): # TODO: This example should eventually be updated to use Python 3.12 type aliases # Passing in `JsonProgram` for `py_type` would cause issues because # Pydantic's `TypeAdapter` ends up trying to eagerly construct an # anonymous recursive type. Even a NewType does not work here. # For now, we just pass in `Any` in place of `JsonProgram`. super().__init__(py_type=cast(type[JsonProgram], Any)) @override def validate_object(self, obj: Any) -> Success[JsonProgram] | Failure: if "@steps" in obj and isinstance(obj["@steps"], Sequence): return Success(obj) else: return Failure("This is not a valid program. The program must have an array of @steps") class TypeChatProgramTranslator(TypeChatJsonTranslator[JsonProgram]): _api_declaration_str: str def __init__(self, model: TypeChatLanguageModel, validator: TypeChatProgramValidator, api_type: type): super().__init__(model=model, validator=validator, target_type=api_type, _raise_on_schema_errors = False) # TODO: the conversion result here has errors! conversion_result = python_type_to_typescript_schema(api_type) self._api_declaration_str = conversion_result.typescript_schema_str @override def _create_request_prompt(self, intent: str) -> str: prompt = F""" You are a service that translates user requests into programs represented as JSON using the following TypeScript definitions: ``` {program_schema_text} ``` The programs can call functions from the API defined in the following TypeScript definitions: ``` {self._api_declaration_str} ``` The following is a user request: ''' {intent} ''' The following is the user request translated into a JSON program object with 2 spaces of indentation and no properties with the value undefined: """ return prompt @override def _create_repair_prompt(self, validation_error: str) -> str: prompt = F""" The JSON program object is invalid for the following reason: ''' {validation_error} ''' The following is a revised JSON program object: """ return prompt ================================================ FILE: python/examples/math/schema.py ================================================ from typing_extensions import TypedDict, Annotated, Callable, Doc class MathAPI(TypedDict): """ This is API for a simple calculator """ add: Annotated[Callable[[float, float], float], Doc("Add two numbers")] sub: Annotated[Callable[[float, float], float], Doc("Subtract two numbers")] mul: Annotated[Callable[[float, float], float], Doc("Multiply two numbers")] div: Annotated[Callable[[float, float], float], Doc("Divide two numbers")] neg: Annotated[Callable[[float], float], Doc("Negate a number")] id: Annotated[Callable[[float], float], Doc("Identity function")] unknown: Annotated[Callable[[str], float], Doc("Unknown request")] ================================================ FILE: python/examples/math/schemaV2.py ================================================ from typing_extensions import Protocol, runtime_checkable @runtime_checkable class MathAPI(Protocol): """ This is API for a simple calculator """ def add(self, x: float, y: float) -> float: """ Add two numbers """ ... def sub(self, x: float, y: float) -> float: """ Subtract two numbers """ ... def mul(self, x: float, y: float) -> float: """ Multiply two numbers """ ... def div(self, x: float, y: float) -> float: """ Divide two numbers """ ... def neg(self, x: float) -> float: """ Negate a number """ ... def id(self, x: float, y: float) -> float: """ Identity function """ ... def unknown(self, text: str) -> float: """ unknown request """ ... ================================================ FILE: python/examples/multiSchema/README.md ================================================ # MultiSchema This application demonstrates a simple way to write a **super-app** that automatically routes user requests to child apps. In this example, the child apps are existing TypeChat chat examples: * CoffeeShop * Restaurant * Calendar * Sentiment * Math * Plugins * HealthData ## Target Models Works with GPT-3.5 Turbo and GPT-4. Sub-apps like HealthData and Plugins work best with GPT-4. # Usage Example prompts can be found in [`input.txt`](input.txt). ================================================ FILE: python/examples/multiSchema/agents.py ================================================ from collections.abc import Sequence import os import sys from typing import cast examples_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) if examples_path not in sys.path: sys.path.append(examples_path) import json from typing_extensions import TypeVar, Generic from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, TypeChatLanguageModel import examples.math.schema as math_schema from examples.math.program import ( TypeChatProgramTranslator, TypeChatProgramValidator, evaluate_json_program, ) import examples.music.schema as music_schema from examples.music.client import ClientContext, handle_call, get_client_context T = TypeVar("T", covariant=True) class JsonPrintAgent(Generic[T]): _validator: TypeChatValidator[T] _translator: TypeChatJsonTranslator[T] def __init__(self, model: TypeChatLanguageModel, target_type: type[T]): super().__init__() self._validator = TypeChatValidator(target_type) self._translator = TypeChatJsonTranslator(model, self._validator, target_type) async def handle_request(self, line: str): result = await self._translator.translate(line) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) class MathAgent: _validator: TypeChatProgramValidator _translator: TypeChatProgramTranslator def __init__(self, model: TypeChatLanguageModel): super().__init__() self._validator = TypeChatProgramValidator() self._translator = TypeChatProgramTranslator(model, self._validator, math_schema.MathAPI) async def _handle_json_program_call(self, func: str, args: Sequence[object]) -> int | float: print(f"{func}({json.dumps(args)}) ") for arg in args: if not isinstance(arg, (int, float)): raise ValueError("All arguments are expected to be numeric.") args = cast(Sequence[int | float], args) match func: case "add": return args[0] + args[1] case "sub": return args[0] - args[1] case "mul": return args[0] * args[1] case "div": return args[0] / args[1] case "neg": return -1 * args[0] case "id": return args[0] case _: raise ValueError(f'Unexpected function name {func}') async def handle_request(self, line: str): result = await self._translator.translate(line) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) math_result = await evaluate_json_program(result, self._handle_json_program_call) print(f"Math Result: {math_result}") class MusicAgent: _validator: TypeChatValidator[music_schema.PlayerActions] _translator: TypeChatJsonTranslator[music_schema.PlayerActions] _client_context: ClientContext | None _authentication_vals: dict[str, str | None] def __init__(self, model: TypeChatLanguageModel, authentication_vals: dict[str, str | None]): super().__init__() self._validator = TypeChatValidator(music_schema.PlayerActions) self._translator = TypeChatJsonTranslator(model, self._validator, music_schema.PlayerActions) self._authentication_vals = authentication_vals self._client_context = None async def authenticate(self): self._client_context = await get_client_context(self._authentication_vals) async def handle_request(self, line: str): if not self._client_context: await self.authenticate() assert self._client_context result = await self._translator.translate(line) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) try: for action in result["actions"]: await handle_call(action, self._client_context) except Exception as error: print("An exception occurred: ", error) ================================================ FILE: python/examples/multiSchema/demo.py ================================================ import os import sys examples_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) if examples_path not in sys.path: sys.path.append(examples_path) import asyncio from dotenv import dotenv_values from typechat import create_language_model, process_requests from router import TextRequestRouter from agents import MathAgent, JsonPrintAgent, MusicAgent import examples.restaurant.schema as restaurant import examples.calendar.schema as calendar import examples.coffeeShop.schema as coffeeShop import examples.sentiment.schema as sentiment async def handle_unknown(_line: str): print("The input did not match any registered agents") async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) router = TextRequestRouter(model=model) # register agents math_agent = MathAgent(model=model) router.register_agent( name="Math", description="Calculations using the four basic math operations", handler=math_agent.handle_request ) music_agent = MusicAgent(model=model, authentication_vals=env_vals) await music_agent.authenticate() router.register_agent( name="Music Player", description="Actions related to music, podcasts, artists, and managing music libraries", handler=music_agent.handle_request, ) coffee_agent = JsonPrintAgent(model=model, target_type=coffeeShop.Cart) router.register_agent( name="CoffeeShop", description="Order Coffee Drinks (Italian names included) and Baked Goods", handler=coffee_agent.handle_request, ) calendar_agent = JsonPrintAgent(model=model, target_type=calendar.CalendarActions) router.register_agent( name="Calendar", description="Actions related to calendars, appointments, meetings, schedules", handler=calendar_agent.handle_request, ) restaurant_agent = JsonPrintAgent(model=model, target_type=restaurant.Order) router.register_agent( name="Restaurant", description="Order pizza, beer and salads", handler=restaurant_agent.handle_request ) sentiment_agent = JsonPrintAgent(model=model, target_type=sentiment.Sentiment) router.register_agent( name="Sentiment", description="Statements with sentiments, emotions, feelings, impressions about places, things, the surroundings", handler=sentiment_agent.handle_request, ) # register a handler for unknown results router.register_agent(name="No Match", description="Handles all unrecognized requests", handler=handle_unknown) async def request_handler(message: str): await router.route_request(message) file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("🔀> ", file_path, request_handler) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/multiSchema/input.txt ================================================ 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. I also want an espresso with extra foam and a muffin with jam And book me a lunch with Claude Debussy next week at 12.30 at Le Petit Chien! I bought 4 shoes for 12.50 each. How much did I spend? Its cold! Its cold and I want hot cafe to warm me up The coffee is cold The coffee is awful (2*4)+(9*7) ================================================ FILE: python/examples/multiSchema/router.py ================================================ import json from typing_extensions import Any, Callable, Awaitable, TypedDict, Annotated from typechat import Failure, TypeChatValidator, TypeChatLanguageModel, TypeChatJsonTranslator class AgentInfo(TypedDict): name: str description: str handler: Callable[[str], Awaitable[Any]] class TaskClassification(TypedDict): task_kind: Annotated[str, "Describe the kind of task to perform."] class TextRequestRouter: _current_agents: dict[str, AgentInfo] _validator: TypeChatValidator[TaskClassification] _translator: TypeChatJsonTranslator[TaskClassification] def __init__(self, model: TypeChatLanguageModel): super().__init__() self._validator = TypeChatValidator(TaskClassification) self._translator = TypeChatJsonTranslator(model, self._validator, TaskClassification) self._current_agents = {} def register_agent(self, name: str, description: str, handler: Callable[[str], Awaitable[Any]]): agent = AgentInfo(name=name, description=description, handler=handler) self._current_agents[name] = agent async def route_request(self, line: str): classes_str = json.dumps(self._current_agents, indent=2, default=lambda o: None, allow_nan=False) prompt_fragment = F""" Classify ""{line}"" using the following classification table: ''' {classes_str} ''' """ result = await self._translator.translate(prompt_fragment) if isinstance(result, Failure): print("Translation Failed ❌") print(f"Context: {result.message}") else: result = result.value print("Translation Succeeded! ✅\n") print(f"The target class is {result['task_kind']}") target = self._current_agents[result["task_kind"]] await target.get("handler")(line) ================================================ FILE: python/examples/music/README.md ================================================ # Music The 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. # Try Music A Spotify Premium account is required to run this example. To run the Music example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). This example also requires additional setup to use the Spotify API: 1. Go to https://developer.spotify.com/dashboard. 2. Log into Spotify with your user account if you are not already logged in. 3. Click the button in the upper right labeled "Create App". 4. 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. 5. Click the settings button and copy down the Client ID and Client Secret (the client secret requires you to click 'View client secret'). 6. 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. # Usage Example prompts can be found in [`input.txt`](./input.txt). For example, use natural language to start playing a song with the Spotify player: **Input**: ``` 🎵> play shake it off by taylor swift ``` **Output**: ``` JSON View { "actions": [ { "actionName": "play", "parameters": { "artist": "taylor swift", "trackName": "shake it off", "quantity": 0 } } ] } Playing... Shake It Off ``` ================================================ FILE: python/examples/music/client.py ================================================ import os import sys current_path = os.path.abspath(os.path.dirname(__file__)) if current_path not in sys.path: sys.path.append(current_path) import math from typing import Any, Optional from pydantic.dataclasses import dataclass import spotipy # type: ignore from schema import PlayerAction from spotipyWrapper import SimplifiedTrackInfo, SimplifiedPlaylistInfo, AsyncSpotipy class Config: arbitrary_types_allowed = True @dataclass(config=Config) class ClientContext: service: AsyncSpotipy userId: str deviceId: Optional[str] = None currentTrackList: Optional[list[SimplifiedTrackInfo]] = None lastTrackStartIndex: Optional[int] = 0 lastTrackEndIndex: Optional[int] = -1 async def get_client_context(vals: dict[str, str | None]) -> ClientContext: scopes = [ "user-read-private", "playlist-read-collaborative", "playlist-modify-private", "playlist-read-private", "playlist-modify-public", "streaming", "user-library-read", "user-top-read", "user-read-playback-state", "user-modify-playback-state", "user-read-recently-played", "user-read-currently-playing", "user-library-modify", "ugc-image-upload", ] scopes_str = " ".join(scopes) auth_manager = spotipy.SpotifyOAuth( client_id=vals.get("SPOTIFY_APP_CLI", None), client_secret=vals.get("SPOTIFY_APP_CLISEC", None), redirect_uri=f"http://localhost:{vals.get('SPOTIFY_APP_PORT', 80)}/callback", scope=scopes_str, ) spotify = spotipy.Spotify(auth_manager=auth_manager) devices = spotify.devices() device_id: str = "" if devices: device_list = devices.get("devices", []) if device_list: device_id = device_list[0].get("id", "") user: dict[str, Any] = spotify.current_user() # type: ignore result = ClientContext(deviceId=device_id, service=AsyncSpotipy(spotify), userId=user.get("id", None)) return result async def play_album(album_uri: str, context: ClientContext): await context.service.start_playback(context_uri=album_uri, device_id=context.deviceId) async def play_tracks_with_query(query: str, quantity: int, context: ClientContext): # To do: paginate until we get to the requested number of items results = await context.service.search(q=query, type='track', limit=quantity, offset=0) item_uris = [t["uri"] for t in results['tracks']['items']] await context.service.start_playback(device_id=context.deviceId, uris=item_uris) async def play_albums_with_query(query: str, quantity: int, context: ClientContext): results = await context.service.search(q=query, type='album', limit=quantity, offset=0) item_uris = [t["uri"] for t in results['albums']['items']] await context.service.start_playback(device_id=context.deviceId, uris=item_uris) async def play_artist_with_query(query: str, context: ClientContext): results = await context.service.search(q=query, type='artist') items = results['artists']['items'] if len(items) > 0: artist = items[0] await context.service.start_playback(context_uri=artist["uri"], device_id=context.deviceId) def get_tracks_from_result_list(resultItems: list[Any]) -> list[SimplifiedTrackInfo]: tracks = [ SimplifiedTrackInfo( name=track["name"], artistNames=[a["name"] for a in track["artists"]], artistUris=[a["uri"] for a in track["artists"]], albumName=track["album"]["name"], uri=track["uri"], ) for track in resultItems ] return tracks async def get_tracks_from_search(query: str, context: ClientContext) -> list[SimplifiedTrackInfo]: results = await context.service.search(q=query, type='track', limit=50, offset=0) tracks: list[SimplifiedTrackInfo] = [] while results: tracks.extend(get_tracks_from_result_list(resultItems=results['tracks']['items'])) if results['next']: results = await context.service.next(results) else: results = None return tracks async def get_tracks_with_genres( tracks: list[SimplifiedTrackInfo], context: ClientContext ) -> list[SimplifiedTrackInfo]: unique_artist_ids: list[str] = list(set([a for a in track.artistUris])) # type: ignore genre_lookup: dict[str, list[str]] = {} for artist_id in unique_artist_ids: artist = await context.service.artist(artist_id) genre_lookup[artist_id] = [g.casefold() for g in artist["genres"]] for track in tracks: track_genres:set[str] = set() for artist_id in track.artistUris: track_genres.update(set(genre_lookup[artist_id])) track.genres = list(track_genres) return tracks def print_tracks(tracks: list[SimplifiedTrackInfo]): for track in tracks: print(f" {track.name}") print(f" Artists: {', '.join(track.artistNames)}") print(f" Album: {track.albumName}") def update_track_list_and_print(tracks: list[SimplifiedTrackInfo], context: ClientContext): print_tracks(tracks) context.currentTrackList = tracks async def get_current_users_playlists(context: ClientContext) -> list[SimplifiedPlaylistInfo]: results = await context.service.current_user_playlists(limit=50) playlists: list[SimplifiedPlaylistInfo] = [] while results: playlists.extend( [SimplifiedPlaylistInfo(name=curr_list["name"], id=curr_list["id"]) for curr_list in results['items']] ) if results['next']: results = await context.service.next(results) else: results = None return playlists async def print_status(context: ClientContext): state = await context.service.current_playback() if not state: print("Nothing playing according to Spotify") await list_available_devices(context) async def list_available_devices(context: ClientContext): devices = await context.service.devices() for device in devices["devices"]: if device["is_active"]: print(F"Active Device {device['name']} of type {device['type']}") else: print(f"Device {device['name']} of type {device['type']} is available") async def handle_call(action: PlayerAction, context: ClientContext): match action["actionName"]: case "play": start_index = action["parameters"].get("trackNumber", None) end_index = 0 if start_index is None: track_range = action["parameters"].get("trackRange", None) if track_range: start_index = track_range[0] end_index = track_range[1] if start_index is not None: if not end_index: end_index = start_index + 1 if context.currentTrackList is None: queue = await context.service.queue() if queue["queue"]: tracks = get_tracks_from_result_list(resultItems=queue["queue"]) context.currentTrackList = tracks if context.currentTrackList: item_uris = [a.uri for a in context.currentTrackList] await context.service.start_playback( device_id=context.deviceId, uris=item_uris, offset={"position": start_index} ) else: query = action["parameters"].get("query", None) album = action["parameters"].get("album", None) track = action["parameters"].get("trackName", None) artist = action["parameters"].get("artist", None) quantity = action["parameters"].get("quantity", 1) if quantity < 9: quantity = 1 if query: actionType = action["parameters"].get("itemType", "album") if actionType == "track": await play_tracks_with_query(query, quantity, context) else: await play_albums_with_query(query, quantity, context) elif track is not None: query = 'track:' + track await play_tracks_with_query(query, quantity, context) elif album is not None: query = 'album:' + album await play_albums_with_query(query, quantity, context) elif artist is not None: query = 'artist:' + artist await play_artist_with_query(query, context) else: # Resume playback on default device await context.service.start_playback(device_id=context.deviceId) case "status": await print_status(context) case "getQueue": queue = await context.service.queue() print("Current Queue: ") for track in queue["queue"]: print(f" {track['name']}") print(f" Artists: {', '.join([a['name'] for a in track['artists']])}") print(f" Album: {track['album']['name']}") await print_status(context) case "pause": await context.service.pause_playback(device_id=context.deviceId) await print_status(context) case "next": await context.service.next_track(device_id=context.deviceId) await print_status(context) case "previous": await context.service.previous_track(device_id=context.deviceId) await print_status(context) case "shuffle": await context.service.shuffle(device_id=context.deviceId, state=action["parameters"]["on"]) await print_status(context) case "resume": await context.service.start_playback(device_id=context.deviceId) await print_status(context) case "listDevices": await list_available_devices(context) case "selectDevice": deviceKeyword = action["parameters"]["keyword"].lower() devices = await context.service.devices() devices = devices["devices"] target_device = next( (d for d in devices if d["name"].lower() == deviceKeyword or d["type"].lower() == deviceKeyword), None ) if target_device: await context.service.transfer_playback(device_id=target_device) print(f"Selected device {target_device}") case "setVolume": new_volume = action["parameters"].get("newVolumeLevel", None) new_volume = max(0, min(new_volume, 100)) print(f"Setting volume to {new_volume} ...") await context.service.volume(device_id=context.deviceId, volume_percent=new_volume) case "changeVolume": playback_state = await context.service.current_playback() if playback_state and playback_state["device"]: volume = int(playback_state["device"]["volume_percent"]) volume_change = int(action["parameters"].get("volumeChangePercentage", 0)) new_volume = math.floor((1.0 + volume_change / 100) * volume) new_volume = max(0, min(new_volume, 100)) print(f"Setting volume to {new_volume} ...") await context.service.volume(device_id=context.deviceId, volume_percent=new_volume) case "searchTracks": query = "track:" + action["parameters"].get("query", None) tracks = await get_tracks_from_search(query=query, context=context) print("Search Results: ") update_track_list_and_print(tracks, context) case "listPlaylists": playlists = await get_current_users_playlists(context) for i, playlist in enumerate(playlists): print("%4d %s" % (i + 1, playlist.name)) case "getPlaylist": playlists = await get_current_users_playlists(context) name = action["parameters"].get("name", None) target_playlist = next((p for p in playlists if p.name.casefold() == name.casefold()), None) if target_playlist: results = await context.service.playlist_items( playlist_id=target_playlist.id, additional_types=['track'] ) tracks = get_tracks_from_result_list(resultItems=results['items']) print("PLaylist items: ") update_track_list_and_print(tracks, context) case "getAlbum": name = action["parameters"].get("name", None) if name: results = await context.service.search(q='album:' + name, type='album', limit=50, offset=0) if results: target_album_info = results["albums"]["items"][0] if target_album_info: target_album = await context.service.album(target_album_info["uri"]) tracks = get_tracks_from_result_list(resultItems=target_album['tracks']) print("Album items: ") update_track_list_and_print(tracks, context) case "getFavorites": count = action["parameters"].get("count", 50) results = await context.service.current_user_top_tracks(limit=count, offset=0) if results: tracks = get_tracks_from_result_list(resultItems=results['items']) print("Favorite tracks: ") update_track_list_and_print(tracks, context) case "filterTracks": trackCollection = context.currentTrackList filter_type = action["parameters"].get("filterType", None) filter_value = action["parameters"].get("filterValue", None) if filter_type and filter_value and trackCollection: matched_tracks: list[SimplifiedTrackInfo] = [] filter_value = filter_value.casefold() match filter_type: case "genre": extended_collection = await get_tracks_with_genres(trackCollection, context) matched_tracks = [t for t in extended_collection if filter_value in t.genres] case "artist": matched_tracks = [ t for t in trackCollection if any(filter_value in a for a in list(map(str.casefold, t.artistNames))) ] case "name": matched_tracks = [t for t in trackCollection if filter_value in t.name.casefold()] if action["parameters"].get("negate", None): tracks = [t for t in trackCollection if t not in matched_tracks] else: tracks = matched_tracks print("Filtered tracks:") update_track_list_and_print(tracks, context) case "createPlaylist": name = action["parameters"]["name"] trackCollection = context.currentTrackList if name and trackCollection: uris = [t.uri for t in trackCollection] playlist = await context.service.user_playlist_create(user=context.userId, name=name) await context.service.playlist_add_items(playlist_id=playlist["id"], items=uris) print(f"Playlist {name} created with tracks:") print_tracks(trackCollection) else: print("no input tracks for createPlaylist") case "deletePlaylist": name = action["parameters"].get("name", None) playlists = await get_current_users_playlists(context) if name and playlists: target_playlist = next((p for p in playlists if p.name.casefold() == name.casefold()), None) if target_playlist: await context.service.current_user_unfollow_playlist(playlist_id=target_playlist.id) print(f"Playlist {name} deleted") case "Unknown": print(f"Text not understood in this context: {action.get('text', None)}") ================================================ FILE: python/examples/music/demo.py ================================================ import asyncio import json import sys from dotenv import dotenv_values import schema as music from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests from client import handle_call, get_client_context async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatValidator(music.PlayerActions) translator = TypeChatJsonTranslator(model, validator, music.PlayerActions) player_context = await get_client_context(env_vals) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) try: for action in result["actions"]: await handle_call(action, player_context) except Exception as error: print("An exception occurred: ", error) if any(item["actionName"] == "Unknown" for item in result["actions"]): print("I did not understand the following") for item in result["actions"]: if item["actionName"] == "Unknown": print(item["text"]) file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("🎵> ", file_path, request_handler) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/music/input.txt ================================================ play Taylor Swift Shake It Off get my top 20 favorites and make a playlist named animalTracks of the tracks that have animals in their names get my favorite 100 tracks from the last two months and show only the ones by Bach make it loud get 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 toggle shuffle on and skip to the next track go back to the last song play my playlist class8 play the fourth one show me my queue ================================================ FILE: python/examples/music/schema.py ================================================ from typing_extensions import Literal, Required, NotRequired, TypedDict, Annotated, Doc class unknownActionParameters(TypedDict): text: Annotated[str, "text typed by the user that the system did not understand"] class UnknownAction(TypedDict): """ Use this action for requests that weren't understood """ actionName: Literal["Unknown"] text: unknownActionParameters class EmptyParameters(TypedDict): pass class PlayParameters(TypedDict, total=False): artist: Annotated[str, Doc("artist (performer, composer) to search for to play")] album: Annotated[str, Doc("album to search for to play")] trackName: Annotated[str, Doc("track to search for to play")] query: Annotated[str, Doc("other description to search for to play")] itemType: Annotated[Literal["track", "album"], Doc("this property is only used when the user specifies the item type")] quantity: Required[Annotated[ int, 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."), ]] trackNumber: Annotated[int, Doc("play the track at this index in the current track list")] trackRange: Annotated[list[int], Doc("play this range of tracks example 1-3")] class PlayAction(TypedDict): """ play a track, album, or artist; this action is chosen over search if both could apply with no parameters, play means resume playback """ actionName: Literal["play"] parameters: PlayParameters class StatusAction(TypedDict): """ show now playing including track information, and playback status including playback device """ actionName: Literal["status"] parameters: EmptyParameters class PauseAction(TypedDict): """ pause playback """ actionName: Literal["pause"] parameters: EmptyParameters class ResumeAction(TypedDict): """ resume playback """ actionName: Literal["resume"] parameters: EmptyParameters class NextAction(TypedDict): """ next track """ actionName: Literal["next"] parameters: EmptyParameters class PreviousAction(TypedDict): """ previous track """ actionName: Literal["previous"] parameters: EmptyParameters class ShuffleActionParameters(TypedDict): on: bool class ShuffleAction(TypedDict): """ turn shuffle on or off """ actionName: Literal["shuffle"] parameters: ShuffleActionParameters class ListDevicesAction(TypedDict): """ list available playback devices """ actionName: Literal["listDevices"] parameters: EmptyParameters class SelectDeviceActionParameters(TypedDict): keyword: Annotated[str, Doc("keyword to match against device name")] class SelectDeviceAction(TypedDict): """ select playback device by keyword """ actionName: Literal["selectDevice"] parameters: SelectDeviceActionParameters class SelectVolumeActionParameters(TypedDict): newVolumeLevel: Annotated[int, Doc("new volume level")] class SetVolumeAction(TypedDict): """ set volume """ actionName: Literal["setVolume"] parameters: SelectVolumeActionParameters class ChangeVolumeActionParameters(TypedDict): volumeChangePercentage: Annotated[int, "volume change percentage"] class ChangeVolumeAction(TypedDict): """ change volume plus or minus a specified percentage """ actionName: Literal["changeVolume"] parameters: ChangeVolumeActionParameters class SearchTracksActionParameters(TypedDict): query: Annotated[ str, Doc( """ the part of the request specifying the the search keywords examples: song name, album name, artist name """), ] class SearchTracksAction(TypedDict): """ this action is only used when the user asks for a search as in 'search', 'find', 'look for' query is a Spotify search expression such as 'Rock Lobster' or 'te kanawa queen of night' set the current track list to the result of the search """ actionName: Literal["searchTracks"] parameters: SearchTracksActionParameters class ListPlaylistsAction(TypedDict): """ list all playlists """ actionName: Literal["listPlaylists"] parameters: EmptyParameters class GetPlaylistActionParameters(TypedDict): name: Annotated[str, "name of playlist to get"] class GetPlaylistAction(TypedDict): """ get playlist by name """ actionName: Literal["getPlaylist"] parameters: GetPlaylistActionParameters class GetAlbumActionParameters(TypedDict): name: Annotated[str, "name of album to get"] class GetAlbumAction(TypedDict): """ get album by name; if name is "", use the currently playing track set the current track list the tracks in the album """ actionName: Literal["getAlbum"] parameters: GetPlaylistActionParameters class GetFavoritesActionParameters(TypedDict): count: NotRequired[Annotated[int, "number of favorites to get"]] class GetFavoritesAction(TypedDict): """ Set the current track list to the user's favorite tracks """ actionName: Literal["getFavorites"] parameters: GetFavoritesActionParameters class FilterTracksActionParameters(TypedDict): filterType: Annotated[ Literal["genre", "artist", "name"], Doc("filter type is one of 'genre', 'artist', 'name'; name does a fuzzy match on the track name"), ] filterValue: Annotated[str, Doc("filter value is the value to match against")] negate: NotRequired[Annotated[bool, Doc("if negate is true, keep the tracks that do not match the filter")]] class FilterTracksAction(TypedDict): """ apply a filter to match tracks in the current track list set the current track list to the tracks that match the filter """ actionName: Literal["filterTracks"] parameters: FilterTracksActionParameters class CreatePlaylistActionParameters(TypedDict): name: Annotated[str, "name of playlist to create"] class CreatePlaylistAction(TypedDict): """ create a new playlist from the current track list """ actionName: Literal["createPlaylist"] parameters: CreatePlaylistActionParameters class DeletePlaylistActionParameters(TypedDict): name: Annotated[str, Doc("name of playlist to delete")] class DeletePlaylistAction(TypedDict): """ delete a playlist """ actionName: Literal["deletePlaylist"] parameters: DeletePlaylistActionParameters class GetQueueAction(TypedDict): """ set the current track list to the queue of upcoming tracks """ actionName: Literal["getQueue"] parameters: EmptyParameters PlayerAction = ( PlayAction | StatusAction | PauseAction | ResumeAction | NextAction | PreviousAction | ShuffleAction | ListDevicesAction | SelectDeviceAction | SetVolumeAction | ChangeVolumeAction | SearchTracksAction | ListPlaylistsAction | GetPlaylistAction | GetAlbumAction | GetFavoritesAction | FilterTracksAction | CreatePlaylistAction | DeletePlaylistAction | GetQueueAction | UnknownAction ) class PlayerActions(TypedDict): actions: list[PlayerAction] ================================================ FILE: python/examples/music/spotipyWrapper.py ================================================ from typing_extensions import Any from dataclasses import dataclass, field import spotipy # type: ignore # The spotipy library does not provide type hints or async methods. This file has some wrappers and stubs # to give just-enough typing for the demo # This class holds the Track info needed for our use @dataclass class SimplifiedTrackInfo: name: str uri: str artistNames: list[str] artistUris: list[str] albumName: str genres: list[str] = field(default_factory=list) # This class holds the Playlist info needed for our use @dataclass class SimplifiedPlaylistInfo: name: str id: str # This wrapper class allows the rest of the code to use type hints and async pattern class AsyncSpotipy: _service: spotipy.Spotify def __init__(self, service: spotipy.Spotify): super().__init__() self._service = service async def devices(self) -> dict[str, Any]: return self._service.devices() # type: ignore async def search( self, q: str, limit: int = 10, offset: int = 0, type: str = "track", market: str | None = None ) -> dict[str, Any]: return self._service.search(q=q, limit=limit, offset=offset, type=type, market=market) # type: ignore async def next(self, result: dict[str, Any]) -> dict[str, Any]: return self._service.next(result=result) # type: ignore async def artist(self, artist_id: str) -> dict[str, Any]: return self._service.artist(artist_id=artist_id) # type: ignore async def album(self, album_id: str, market: str | None = None) -> dict[str, Any]: return self._service.album(album_id=album_id, market=market) # type: ignore async def queue(self) -> dict[str, Any]: return self._service.queue() # type: ignore async def current_playback(self, market: str | None = None, additional_types: str | None = None) -> dict[str, Any]: return self._service.current_playback(market=market, additional_types=additional_types) # type: ignore async def start_playback( self, device_id: str | None = None, context_uri: str | None = None, uris: list[str] | None = None, offset: dict[str, int] | None = None, position_ms: int | None = None, ) -> None: return self._service.start_playback(device_id=device_id, context_uri=context_uri, uris=uris, offset=offset, position_ms=position_ms) # type: ignore async def pause_playback(self, device_id: str | None = None) -> None: return self._service.pause_playback(device_id=device_id) # type: ignore async def next_track(self, device_id: str | None = None) -> None: return self._service.next_track(device_id=device_id) # type: ignore async def previous_track(self, device_id: str | None = None) -> None: return self._service.previous_track(device_id=device_id) # type: ignore async def volume(self, volume_percent: int, device_id: str | None = None) -> None: return self._service.volume(volume_percent=volume_percent, device_id=device_id) # type: ignore async def shuffle(self, state: bool, device_id: str | None = None) -> None: return self._service.shuffle(state=state, device_id=device_id) # type: ignore async def transfer_playback(self, device_id: str, force_play: bool = True) -> None: return self._service.transfer_playback(device_id=device_id, force_play=force_play) # type: ignore async def current_user_top_tracks( self, limit: int = 20, offset: int = 0, time_range: str = "medium_term" ) -> dict[str, Any]: return self._service.current_user_top_tracks(limit=limit, offset=offset, time_range=time_range) # type: ignore async def current_user_playlists(self, limit: int = 50, offset: int = 0) -> dict[str, Any]: return self._service.current_user_playlists(limit=limit, offset=offset) # type: ignore async def user_playlist_create( self, user: str, name: str, public: bool = True, collaborative: bool = False, description: str = "" ) -> dict[str, Any]: return self._service.user_playlist_create(user=user, name=name, public=public, collaborative=collaborative, description=description) # type: ignore async def playlist_items( self, playlist_id: str, fields: str | None = None, limit: int = 100, offset: int = 0, market: str | None = None, additional_types: list[str] | None = None, ) -> dict[str, Any]: return self._service.playlist_items(playlist_id=playlist_id, fields=fields, limit=limit, offset=offset, market=market, additional_types=additional_types) # type: ignore async def playlist_add_items(self, playlist_id: str, items: list[str], position: int | None = None) -> None: return self._service.playlist_add_items(playlist_id=playlist_id, items=items, position=position) # type: ignore async def current_user_unfollow_playlist(self, playlist_id: str) -> None: return self._service.current_user_unfollow_playlist(playlist_id=playlist_id) # type: ignore ================================================ FILE: python/examples/restaurant/README.md ================================================ # Restaurant The Restaurant example shows how to capture user intent as a set of "nouns", but with more complex linguistic input. This 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. This example also shows how we can create a "user intent summary" to display to a user. It uses a natural language experience for placing an order with the [`Order`](./schema.py) type. # Try Restaurant To run the Restaurant example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`input.txt`](./input.txt). For example, given the following order: **Input**: ``` 🍕> 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. ``` **Output**: *This is GPT-4-0613 output; GPT-3.5-turbo and most other models miss this one.* ``` 1 large pizza with mushrooms 1 large pizza with sausage 1 small pizza with sausage 1 whole Greek salad 1 Pale Ale 1 Mack and Jacks ``` > **Note** > > Across different models, you may see that model responses may not correspond to the user intent. > In the above example, some models may not be able to capture the fact that the order is still only for 3 pizzas, > and that "make one sausage a small" is not a request for a new pizza. > > ```diff > 1 large pizza with mushrooms > - 1 large pizza with sausage > + 2 large pizza with sausage > 1 small pizza with sausage > 1 whole Greek salad > 1 Pale Ale > 1 Mack and Jacks > ``` > > The output here from GPT 3.5-turbo incorrectly shows 1 mushroom pizza and 3 sausage pizzas. Because 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. In 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: ```typescript function printOrder(order: Order) { ``` Having a validated, typed data structure simplifies the task of generating a succinct summary suitable for user confirmation. ================================================ FILE: python/examples/restaurant/demo.py ================================================ import asyncio import json import sys from dotenv import dotenv_values import schema as restaurant from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatValidator(restaurant.Order) translator = TypeChatJsonTranslator(model, validator, restaurant.Order) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(json.dumps(result, indent=2)) if any(item["itemType"] == "Unknown" for item in result["items"]): print("I did not understand the following") for item in result["items"]: if item["itemType"] == "Unknown": print(item["text"]) file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("🍕> ", file_path, request_handler) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/restaurant/input.txt ================================================ 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. I'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. I'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. 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. I would like to order one with basil and one with extra sauce. Throw in a salad and an ale. I 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. I'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. I sure am hungry for a pizza with pepperoni and a salad with no croutons. And I'm thirsty for 3 Pale Ales give me three regular salads and two Greeks and make the regular ones with no red onions I'll take four large pepperoni pizzas. Put extra sauce on two of them. plus an M&J and a Pale Ale I'll take a yeti, a pale ale and a large with olives and take the extra cheese off the yeti and add a Greek I'll take a medium Pig with no arugula I'll take a small Pig with no arugula and a Greek with croutons and no red onions ================================================ FILE: python/examples/restaurant/schema.py ================================================ from typing_extensions import Literal, Required, NotRequired, TypedDict, Annotated, Doc class UnknownText(TypedDict): """ Use this type for order items that match nothing else """ itemType: Literal["Unknown"] text: Annotated[str, "The text that wasn't understood"] class Pizza(TypedDict, total=False): itemType: Required[Literal["Pizza"]] size: Annotated[Literal["small", "medium", "large", "extra large"], "default: large"] addedToppings: Annotated[list[str], Doc("toppings requested (examples: pepperoni, arugula)")] removedToppings: Annotated[list[str], Doc("toppings requested to be removed (examples: fresh garlic, anchovies)")] quantity: Annotated[int, "default: 1"] name: Annotated[ Literal["Hawaiian", "Yeti", "Pig In a Forest", "Cherry Bomb"], Doc("used if the requester references a pizza by name"), ] class Beer(TypedDict): itemType: Literal["Beer"] kind: Annotated[str, Doc("examples: Mack and Jacks, Sierra Nevada Pale Ale, Miller Lite")] quantity: NotRequired[Annotated[int, "default: 1"]] SaladSize = Literal["half", "whole"] SaladStyle = Literal["Garden", "Greek"] class Salad(TypedDict, total=False): itemType: Required[Literal["Salad"]] portion: Annotated[str, "default: half"] style: Annotated[str, "default: Garden"] addedIngredients: Annotated[list[str], Doc("ingredients requested (examples: parmesan, croutons)")] removedIngredients: Annotated[list[str], Doc("ingredients requested to be removed (example: red onions)")] quantity: Annotated[int, "default: 1"] OrderItem = Pizza | Beer | Salad class Order(TypedDict): items: list[OrderItem | UnknownText] ================================================ FILE: python/examples/sentiment/README.md ================================================ # Sentiment The 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. # Try Sentiment To run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`input.txt`](./input.txt). For example, given the following input statement: **Input**: ``` 😀> TypeChat is awesome! ``` **Output**: ``` The sentiment is positive ``` ================================================ FILE: python/examples/sentiment/demo.py ================================================ import asyncio import sys from dotenv import dotenv_values from typechat import (Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests) import schema as sentiment async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatValidator(sentiment.Sentiment) translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(f"The sentiment is {result.sentiment}") file_path = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("😀> ", file_path, request_handler) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: python/examples/sentiment/input.txt ================================================ hello, world TypeChat is awesome! I'm having a good day it's very rainy outside ================================================ FILE: python/examples/sentiment/schema.py ================================================ from dataclasses import dataclass from typing_extensions import Literal, Annotated, Doc @dataclass class Sentiment: """ The following is a schema definition for determining the sentiment of a some user input. """ sentiment: Annotated[Literal["negative", "neutral", "positive"], Doc("The sentiment for the text")] ================================================ FILE: python/notebooks/calendar.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade setuptools\n", "%pip install --upgrade gradio\n", "%pip install ipywidgets\n", "%pip install pandas\n", "%pip install tabulate\n", "%pip install python-dotenv" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install -e ../" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import setuptools\n", "\n", "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "if module_path not in sys.path:\n", " sys.path.append(module_path)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", "from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\n", "from examples.calendar import schema as calendar" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model = create_language_model(dotenv_values())\n", "validator = TypeChatValidator(calendar.CalendarActions)\n", "translator = TypeChatJsonTranslator(model, validator, calendar.CalendarActions)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas\n", "\n", "async def get_translation(message, history):\n", " result = await translator.translate(message)\n", " if isinstance(result, Failure):\n", " return f\"Translation Failed ❌ \\n Context: {result.message}\"\n", " else:\n", " result = result.value\n", " df = pandas.DataFrame.from_dict(result[\"actions\"])\n", " return f\"Translation Succeeded! ✅\\n Table View \\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\n ``` \\n\"\n", "\n", "def get_examples():\n", " example_prompts = []\n", " with open('../examples/calendar/input.txt') as prompts_file:\n", " for line in prompts_file:\n", " example_prompts.append(line)\n", " return example_prompts" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import gradio as gr\n", "\n", "gr.ChatInterface(get_translation, title=\"📅 Calendar\", examples=get_examples()).launch()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.1" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: python/notebooks/coffeeShop.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade setuptools\n", "%pip install --upgrade gradio" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", "\n", "import sys\n", "from dotenv import dotenv_values\n", "\n", "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "if module_path not in sys.path:\n", " sys.path.append(module_path)\n", "\n", "from examples.coffeeShop import schema as coffeeshop\n", "from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "env_vals = dotenv_values()\n", "model = create_language_model(env_vals)\n", "validator = TypeChatValidator(coffeeshop.Cart)\n", "translator = TypeChatJsonTranslator(model, validator, coffeeshop.Cart)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas\n", "async def get_translation(message, history):\n", " result = await translator.translate(message)\n", " if isinstance(result, Failure):\n", " return f\"Translation Failed ❌ \\n Context: {result.message}\"\n", " else:\n", " result = result.value\n", " df = pandas.DataFrame.from_dict(result[\"items\"])\n", " #return f\"Translation Succeeded! ✅\\n JSON View \\n ``` {json.dumps(result, indent=2)} \\n ``` \\n\"\n", " return f\"Translation Succeeded! ✅\\n Coffee Shop Items \\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\n ``` \\n\"\n", "\n", "def get_examples():\n", " example_prompts = []\n", " with open('../examples/coffeeShop/input.txt') as prompts_file:\n", " for line in prompts_file:\n", " example_prompts.append(line)\n", " return example_prompts\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import setuptools\n", "import gradio as gr\n", "\n", "gr.ChatInterface(get_translation, title=\"☕ Coffee\", examples=get_examples()).launch()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.1" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: python/notebooks/healthData.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade setuptools\n", "%pip install --upgrade gradio" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import setuptools\n", "\n", "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "if module_path not in sys.path:\n", " sys.path.append(module_path)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", "from typechat import Failure, TypeChatValidator, create_language_model\n", "from examples.healthData import schema as health\n", "from examples.healthData.translator import TranslatorWithHistory" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "health_instructions = \"\"\"\n", "Help me enter my health data step by step.\n", "Ask specific questions to gather required and optional fields I have not already providedStop asking if I don't know the answer\n", "Automatically fix my spelling mistakes\n", "My health data may be complex: always record and return ALL of it.\n", "Always 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", "\n", "env_vals = dotenv_values()\n", "model = create_language_model(env_vals)\n", "validator = TypeChatValidator(health.HealthDataResponse)\n", "translator = TranslatorWithHistory(model, validator, health.HealthDataResponse, additional_agent_instructions=health_instructions)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas\n", "\n", "async def get_translation(message, history):\n", " result = await translator.translate(message)\n", " if isinstance(result, Failure):\n", " return f\"Translation Failed ❌ \\n Context: {result.message}\"\n", " else:\n", " result = result.value\n", " output = f\"Translation Succeeded! ✅\\n\"\n", " \n", " data = result.get(\"data\", None)\n", " if data:\n", " df = pandas.DataFrame.from_dict(data)\n", " output += f\"HealthData \\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\n ``` \\n\"\n", "\n", " message = result.get(\"message\", None)\n", " not_translated = result.get(\"notTranslated\", None)\n", "\n", " if message:\n", " output += f\"\\n📝: {message}\"\n", " \n", " if not_translated:\n", " output += f\"\\n🤔: I did not understand\\n {not_translated}\" \n", " \n", " return output\n", "\n", "\n", "def get_examples():\n", " example_prompts = []\n", " with open('../examples/healthData/input.txt') as prompts_file:\n", " for line in prompts_file:\n", " example_prompts.append(line)\n", " return example_prompts\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import gradio as gr\n", "\n", "gr.ChatInterface(get_translation, title=\"💉💊🤧 Health Data\", examples=get_examples()).launch(debug=False)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: python/notebooks/math.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade setuptools\n", "%pip install --upgrade gradio" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import setuptools\n", "\n", "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "if module_path not in sys.path:\n", " sys.path.append(module_path)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", "from typechat import Failure, create_language_model\n", "from examples.math.program import TypeChatProgramTranslator, TypeChatProgramValidator, evaluate_json_program\n", "from examples.math import schema as math" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "env_vals = dotenv_values()\n", "model = create_language_model(env_vals)\n", "validator = TypeChatProgramValidator()\n", "translator = TypeChatProgramTranslator(model, validator, math.MathAPI)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas\n", "async def handleCall(func:str, args: list[int|float]) -> int|float:\n", " print(f\"{func}({json.dumps(args)}) \")\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", " \n", "async def get_translation(message, history):\n", " result = await translator.translate(message)\n", " if isinstance(result, Failure):\n", " return f\"Translation Failed ❌ \\n Context: {result.message}\"\n", " else:\n", " result = result.value\n", " math_result = await evaluate_json_program(result, handleCall)\n", " df = pandas.DataFrame.from_dict(result[\"@steps\"])\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", "def get_examples():\n", " example_prompts = []\n", " with open('../examples/math/input.txt') as prompts_file:\n", " for line in prompts_file:\n", " example_prompts.append(line)\n", " return example_prompts" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import gradio as gr\n", "\n", "gr.ChatInterface(get_translation, title=\"🧮 Math\", examples=get_examples()).launch()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: python/notebooks/music.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade setuptools\n", "%pip install --upgrade gradio\n", "%pip install ipywidgets\n", "%pip install openai\n", "%pip install pandas\n", "%pip install tabulate\n", "%pip install python-dotenv" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install -e ../" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import setuptools\n", "\n", "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "if module_path not in sys.path:\n", " sys.path.append(module_path)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", "from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\n", "from examples.music import schema as music\n", "from examples.music.client import handle_call, get_client_context" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "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)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas\n", "\n", "async def get_translation(message, history):\n", " result = await translator.translate(message)\n", " if isinstance(result, Failure):\n", " return f\"Translation Failed ❌ \\n Context: {result.message}\"\n", " else:\n", " result = result.value\n", " df = pandas.DataFrame.from_dict(result[\"actions\"])\n", " try:\n", " for action in result[\"actions\"]:\n", " await handle_call(action, player_context)\n", " return f\"Translation Succeeded! ✅\\n Table View \\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\n ``` \\n\"\n", " except Exception as error:\n", " return f\"An exception occurred: {error}\"\n", " \n", "\n", "def get_examples():\n", " example_prompts = []\n", " with open('../examples/music/input.txt') as prompts_file:\n", " for line in prompts_file:\n", " example_prompts.append(line)\n", " return example_prompts" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import gradio as gr\n", "\n", "gr.ChatInterface(get_translation, title=\"🎵 Music\", examples=get_examples()).launch()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: python/notebooks/restaurant.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade setuptools\n", "%pip install --upgrade gradio" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import setuptools\n", "\n", "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "if module_path not in sys.path:\n", " sys.path.append(module_path)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from dotenv import dotenv_values\n", "from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model\n", "from examples.restaurant import schema as restaurant" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "env_vals = dotenv_values()\n", "model = create_language_model(env_vals)\n", "validator = TypeChatValidator(restaurant.Order)\n", "translator = TypeChatJsonTranslator(model, validator, restaurant.Order)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas\n", "async def get_translation(message, history):\n", " result = await translator.translate(message)\n", " if isinstance(result, Failure):\n", " return f\"Translation Failed ❌ \\n Context: {result.message}\"\n", " else:\n", " result = result.value\n", " df = pandas.DataFrame.from_dict(result[\"items\"])\n", " return f\"Translation Succeeded! ✅\\n Restaurant orders \\n ``` {df.fillna('').to_markdown(tablefmt='grid')} \\n ``` \\n\"\n", "\n", "\n", "def get_examples():\n", " example_prompts = []\n", " with open('../examples/restaurant/input.txt') as prompts_file:\n", " for line in prompts_file:\n", " example_prompts.append(line)\n", " return example_prompts\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import gradio as gr\n", "\n", "gr.ChatInterface(get_translation, title=\"🍕 Restaurant\", examples=get_examples()).launch()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: python/notebooks/sentiment.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade setuptools\n", "%pip install --upgrade gradio" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", "\n", "import sys\n", "from dotenv import dotenv_values\n", "\n", "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "if module_path not in sys.path:\n", " sys.path.append(module_path)\n", "\n", "from examples.sentiment import schema as sentiment\n", "from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "env_vals = dotenv_values()\n", "model = create_language_model(env_vals)\n", "validator = TypeChatValidator(sentiment.Sentiment)\n", "translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "async def get_translation(message, history):\n", " result = await translator.translate(message)\n", " if isinstance(result, Failure):\n", " return f\"Translation Failed ❌ \\n Context: {result.message}\"\n", " else:\n", " result = result.value\n", " return f\"Translation Succeeded! ✅\\n The sentiment is {result['sentiment']}\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import setuptools\n", "import gradio as gr\n", "\n", "gr.ChatInterface(get_translation, title=\"😀 Sentiment\").launch()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: python/package.json ================================================ { "name": "typechat-py", "private": true, "version": "0.0.4", "description": "TypeChat is a library that makes it easy to build natural language interfaces using types.", "scripts": { "check": "pyright" }, "author": "Microsoft", "license": "MIT", "devDependencies": { "pyright": "1.1.358" } } ================================================ FILE: python/pyproject.toml ================================================ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "typechat" dynamic = ["version"] description = 'TypeChat is a library that makes it easy to build natural language interfaces using types.' readme = "README.md" requires-python = ">=3.11" license = "MIT" keywords = [] authors = [ { name = "Microsoft Corporation" }, ] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ "pydantic>=2.5.2", "pydantic_core>=2.16.3", "httpx>=0.27.0", "typing_extensions>=4.10.0", ] [project.optional-dependencies] # Development-time dependencies. dev = [ "coverage[toml]>=6.5", "pytest>=8.0.2", "syrupy>=5.0.0", ] # Dependencies for examples. examples = [ "python-dotenv>=1.0.0", "spotipy", ] [project.urls] Documentation = "https://github.com/microsoft/TypeChat#readme" Issues = "https://github.com/microsoft/TypeChat/issues" Source = "https://github.com/microsoft/TypeChat" [tool.hatch.version] path = "src/typechat/__about__.py" [tool.hatch.envs.default] # While users can always look up the virtual environment # to select the right interpreter for their editor, often editors can # automatically pick up on a local `.venv` or at least hint towards using it. # The only catch is that this tends to only kick in at the workspace root. type = "virtual" path = "../.venv" # Include dependencies from optional-dependencies for # development of the core package along with examples. features = [ "dev", "examples" ] [tool.hatch.envs.default.scripts] test = "pytest {args:tests}" test-cov = "coverage run -m pytest {args:tests}" cov-report = [ "- coverage combine", "coverage report", ] cov = [ "test-cov", "cov-report", ] [[tool.hatch.envs.all.matrix]] python = ["3.11", "3.12"] [tool.hatch.envs.lint] detached = true dependencies = [ "black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243", ] [tool.hatch.envs.lint.scripts] typing = [ "npx pyright", # mypy should not include tests, as it does not fully support # PEP 695 (type aliases, type parameters, etc.) # https://github.com/python/mypy/issues/1523895 "mypy --install-types --non-interactive {args:src/typechat}" ] style = [ "ruff {args:.}", "black --check --diff {args:.}", ] fmt = [ "black {args:.}", "ruff --fix {args:.}", "style", ] all = [ "style", "typing", ] [tool.mypy] python_version = "3.11" untyped_calls_exclude = ["spotipy"] [tool.black] target-version = ["py311"] line-length = 120 skip-string-normalization = true [tool.ruff] target-version = "py311" line-length = 120 select = [ "A", "ARG", "B", "C", "DTZ", "E", "EM", "F", "FBT", "I", "ICN", "ISC", "N", "PLC", "PLE", "PLR", "PLW", "Q", "RUF", "S", "T", "TID", "UP", "W", "YTT", ] ignore = [ # # Allow non-abstract empty methods in abstract base classes # "B027", # # Allow boolean positional values in function calls, like `dict.get(... True)` # "FBT003", # # Ignore checks for possible passwords # "S105", "S106", "S107", # # Ignore complexity # "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", ] unfixable = [ # # Don't touch unused imports # "F401", ] [tool.ruff.isort] known-first-party = ["typechat"] [tool.ruff.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.per-file-ignores] # Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252"] [tool.coverage.run] source_pkgs = ["typechat", "tests"] branch = true parallel = true omit = [ "src/typechat/__about__.py", ] [tool.coverage.paths] typechat = ["src/typechat", "*/typechat/src/typechat"] tests = ["tests", "*/typechat/tests"] [tool.coverage.report] exclude_lines = [ "no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] ================================================ FILE: python/pyrightconfig.json ================================================ { "typeCheckingMode": "strict", "reportCallInDefaultInitializer": "error", "reportImplicitOverride": "error", "reportImplicitStringConcatenation": "error", "reportImportCycles": "error", "reportMissingSuperCall": "error", "reportPropertyTypeMismatch": "error", "reportShadowedImports": "error", "reportUninitializedInstanceVariable": "error", "reportUnnecessaryTypeIgnoreComment": "error", "reportUnusedCallResult": "none", "pythonVersion": "3.11", "include": [ "**/*", ], } ================================================ FILE: python/src/typechat/__about__.py ================================================ # SPDX-FileCopyrightText: Microsoft Corporation # # SPDX-License-Identifier: MIT __version__ = "0.0.4" ================================================ FILE: python/src/typechat/__init__.py ================================================ # SPDX-FileCopyrightText: Microsoft Corporation # # SPDX-License-Identifier: MIT from typechat._internal.model import PromptSection, TypeChatLanguageModel, create_language_model, create_openai_language_model, create_azure_openai_language_model from typechat._internal.result import Failure, Result, Success from typechat._internal.translator import TypeChatJsonTranslator from typechat._internal.ts_conversion import python_type_to_typescript_schema from typechat._internal.validator import TypeChatValidator from typechat._internal.interactive import process_requests __all__ = [ "TypeChatLanguageModel", "TypeChatJsonTranslator", "TypeChatValidator", "Success", "Failure", "Result", "python_type_to_typescript_schema", "PromptSection", "create_language_model", "create_openai_language_model", "create_azure_openai_language_model", "process_requests", ] ================================================ FILE: python/src/typechat/_internal/__init__.py ================================================ ================================================ FILE: python/src/typechat/_internal/interactive.py ================================================ from typing import Callable, Awaitable async def process_requests(interactive_prompt: str, input_file_name: str | None, process_request: Callable[[str], Awaitable[None]]): """ A request processor for interactive input or input from a text file. If an input file name is specified, the callback function is invoked for each line in file. Otherwise, the callback function is invoked for each line of interactive input until the user types "quit" or "exit". Args: interactive_prompt: Prompt to present to user. input_file_name: Input text file name, if any. process_request: Async callback function that is invoked for each interactive input or each line in text file. """ if input_file_name is not None: with open(input_file_name, "r") as file: lines = filter(str.rstrip, file) for line in lines: if line.startswith("# "): continue print(interactive_prompt + line) await process_request(line) else: try: # Use readline to enable input editing and history import readline # type: ignore except ImportError: pass while True: try: line = input(interactive_prompt) except EOFError: print("\n") break if line.lower().strip() in ("quit", "exit"): break else: await process_request(line) ================================================ FILE: python/src/typechat/_internal/model.py ================================================ import asyncio from types import TracebackType from typing_extensions import AsyncContextManager, Literal, Protocol, Self, TypedDict, cast, override from typechat._internal.result import Failure, Result, Success import httpx class PromptSection(TypedDict): """ Represents a section of an LLM prompt with an associated role. TypeChat uses the "user" role for prompts it generates and the "assistant" role for previous LLM responses (which will be part of the prompt in repair attempts). TypeChat currently doesn't use the "system" role. """ role: Literal["system", "user", "assistant"] content: str class TypeChatLanguageModel(Protocol): async def complete(self, prompt: str | list[PromptSection]) -> Result[str]: """ Represents a AI language model that can complete prompts. TypeChat uses an implementation of this protocol to communicate with an AI service that can translate natural language requests to JSON instances according to a provided schema. The `create_language_model` function can create an instance. """ ... _TRANSIENT_ERROR_CODES = [ 429, 500, 502, 503, 504, ] class HttpxLanguageModel(TypeChatLanguageModel, AsyncContextManager): url: str headers: dict[str, str] default_params: dict[str, str] # Specifies the maximum number of retry attempts. max_retry_attempts: int = 3 # Specifies the delay before retrying in milliseconds. retry_pause_seconds: float = 1.0 # Specifies how long a request should wait in seconds # before timing out with a Failure. timeout_seconds = 10 _async_client: httpx.AsyncClient def __init__(self, url: str, headers: dict[str, str], default_params: dict[str, str]): super().__init__() self.url = url self.headers = headers self.default_params = default_params self._async_client = httpx.AsyncClient() @override async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Failure: headers = { "Content-Type": "application/json", **self.headers, } if isinstance(prompt, str): prompt = [{"role": "user", "content": prompt}] body = { **self.default_params, "messages": prompt, "temperature": 0.0, "n": 1, } retry_count = 0 while True: try: response = await self._async_client.post( self.url, headers=headers, json=body, timeout=self.timeout_seconds ) if response.is_success: json_result = cast( dict[Literal["choices"], list[dict[Literal["message"], PromptSection]]], response.json() ) return Success(json_result["choices"][0]["message"]["content"] or "") if response.status_code not in _TRANSIENT_ERROR_CODES or retry_count >= self.max_retry_attempts: return Failure(f"REST API error {response.status_code}: {response.reason_phrase}") except Exception as e: if retry_count >= self.max_retry_attempts: return Failure(str(e) or f"{repr(e)} raised from within internal TypeChat language model.") await asyncio.sleep(self.retry_pause_seconds) retry_count += 1 @override async def __aenter__(self) -> Self: return self @override async def __aexit__(self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None) -> bool | None: await self._async_client.aclose() def __del__(self): try: asyncio.get_running_loop().create_task(self._async_client.aclose()) except Exception: pass def create_language_model(vals: dict[str, str | None]) -> HttpxLanguageModel: """ Creates a language model encapsulation of an OpenAI or Azure OpenAI REST API endpoint chosen by a dictionary of variables (typically just `os.environ`). If an `OPENAI_API_KEY` environment variable exists, an OpenAI model is constructed. The `OPENAI_ENDPOINT` and `OPENAI_MODEL` environment variables must also be defined or an error will be raised. If an `AZURE_OPENAI_API_KEY` environment variable exists, an Azure OpenAI model is constructed. The `AZURE_OPENAI_ENDPOINT` environment variable must also be defined or an exception will be thrown. If none of these key variables are defined, an exception is thrown. @returns An instance of `TypeChatLanguageModel`. Args: vals: A dictionary of variables. Typically just `os.environ`. """ def required_var(name: str) -> str: val = vals.get(name, None) if val is None: raise ValueError(f"Missing environment variable {name}.") return val if "OPENAI_API_KEY" in vals: api_key = required_var("OPENAI_API_KEY") model = required_var("OPENAI_MODEL") endpoint = vals.get("OPENAI_ENDPOINT", None) or "https://api.openai.com/v1/chat/completions" org = vals.get("OPENAI_ORG", None) or "" return create_openai_language_model(api_key, model, endpoint, org) elif "AZURE_OPENAI_API_KEY" in vals: api_key=required_var("AZURE_OPENAI_API_KEY") endpoint=required_var("AZURE_OPENAI_ENDPOINT") return create_azure_openai_language_model(api_key, endpoint) else: raise ValueError("Missing environment variables for OPENAI_API_KEY or AZURE_OPENAI_API_KEY.") def create_openai_language_model(api_key: str, model: str, endpoint: str = "https://api.openai.com/v1/chat/completions", org: str = "") -> HttpxLanguageModel: """ Creates a language model encapsulation of an OpenAI REST API endpoint. Args: api_key: The OpenAI API key. model: The OpenAI model name. endpoint: The OpenAI REST API endpoint. org: The OpenAI organization. """ headers = { "Authorization": f"Bearer {api_key}", "OpenAI-Organization": org, } default_params = { "model": model, } return HttpxLanguageModel(url=endpoint, headers=headers, default_params=default_params) def create_azure_openai_language_model(api_key: str, endpoint: str) -> HttpxLanguageModel: """ Creates a language model encapsulation of an Azure OpenAI REST API endpoint. Args: api_key: The Azure OpenAI API key. endpoint: The Azure OpenAI REST API endpoint. """ headers = { # Needed when using managed identity "Authorization": f"Bearer {api_key}", # Needed when using regular API key "api-key": api_key, } return HttpxLanguageModel(url=endpoint, headers=headers, default_params={}) ================================================ FILE: python/src/typechat/_internal/result.py ================================================ from dataclasses import dataclass from typing_extensions import Generic, TypeAlias, TypeVar T = TypeVar("T", covariant=True) @dataclass class Success(Generic[T]): "An object representing a successful operation with a result of type `T`." value: T @dataclass class Failure: "An object representing an operation that failed for the reason given in `message`." message: str """ An object representing a successful or failed operation of type `T`. """ Result: TypeAlias = Success[T] | Failure ================================================ FILE: python/src/typechat/_internal/translator.py ================================================ from typing_extensions import Generic, TypeVar import pydantic_core from typechat._internal.model import PromptSection, TypeChatLanguageModel from typechat._internal.result import Failure, Result, Success from typechat._internal.ts_conversion import python_type_to_typescript_schema from typechat._internal.validator import TypeChatValidator T = TypeVar("T", covariant=True) class TypeChatJsonTranslator(Generic[T]): """ Represents an object that can translate natural language requests in JSON objects of the given type. """ model: TypeChatLanguageModel validator: TypeChatValidator[T] target_type: type[T] type_name: str schema_str: str _max_repair_attempts = 1 def __init__( self, model: TypeChatLanguageModel, validator: TypeChatValidator[T], target_type: type[T], *, # keyword-only parameters follow _raise_on_schema_errors: bool = True, ): """ Args: model: The associated `TypeChatLanguageModel`. validator: The associated `TypeChatValidator[T]`. target_type: A runtime type object describing `T` - the expected shape of JSON data. """ super().__init__() self.model = model self.validator = validator self.target_type = target_type conversion_result = python_type_to_typescript_schema(target_type) if _raise_on_schema_errors and conversion_result.errors: error_text = "".join(f"\n- {error}" for error in conversion_result.errors) raise ValueError(f"Could not convert Python type to TypeScript schema: \n{error_text}") self.type_name = conversion_result.typescript_type_reference self.schema_str = conversion_result.typescript_schema_str async def translate(self, input: str, *, prompt_preamble: str | list[PromptSection] | None = None) -> Result[T]: """ Translates a natural language request into an object of type `T`. If the JSON object returned by the language model fails to validate, repair attempts will be made up until `_max_repair_attempts`. The prompt for the subsequent attempts will include the diagnostics produced for the prior attempt. This often helps produce a valid instance. Args: input: A natural language request. prompt_preamble: An optional string or list of prompt sections to prepend to the generated prompt.\ If a string is given, it is converted to a single "user" role prompt section. """ messages: list[PromptSection] = [] if prompt_preamble: if isinstance(prompt_preamble, str): prompt_preamble = [{"role": "user", "content": prompt_preamble}] messages.extend(prompt_preamble) messages.append({"role": "user", "content": self._create_request_prompt(input)}) num_repairs_attempted = 0 while True: completion_response = await self.model.complete(messages) if isinstance(completion_response, Failure): return completion_response text_response = completion_response.value first_curly = text_response.find("{") last_curly = text_response.rfind("}") + 1 error_message: str if 0 <= first_curly < last_curly: trimmed_response = text_response[first_curly:last_curly] try: parsed_response = pydantic_core.from_json(trimmed_response, allow_inf_nan=False, cache_strings=False) except ValueError as e: error_message = f"Error: {e}\n\nAttempted to parse:\n\n{trimmed_response}" else: result = self.validator.validate_object(parsed_response) if isinstance(result, Success): return result error_message = result.message else: error_message = f"Response did not contain any text resembling JSON.\nResponse was\n\n{text_response}" if num_repairs_attempted >= self._max_repair_attempts: return Failure(error_message) num_repairs_attempted += 1 messages.append({"role": "assistant", "content": text_response}) messages.append({"role": "user", "content": self._create_repair_prompt(error_message)}) def _create_request_prompt(self, intent: str) -> str: prompt = f""" You are a service that translates user requests into JSON objects of type "{self.type_name}" according to the following TypeScript definitions: ``` {self.schema_str} ``` The following is a user request: ''' {intent} ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: """ return prompt def _create_repair_prompt(self, validation_error: str) -> str: prompt = f""" The above JSON object is invalid for the following reason: ''' {validation_error} ''' The following is a revised JSON object: """ return prompt ================================================ FILE: python/src/typechat/_internal/ts_conversion/__init__.py ================================================ from dataclasses import dataclass from typing_extensions import TypeAliasType from typechat._internal.ts_conversion.python_type_to_ts_nodes import python_type_to_typescript_nodes from typechat._internal.ts_conversion.ts_node_to_string import ts_declaration_to_str __all__ = [ "python_type_to_typescript_schema", "TypeScriptSchemaConversionResult", ] @dataclass class TypeScriptSchemaConversionResult: typescript_schema_str: str """The TypeScript declarations generated from the Python declarations.""" typescript_type_reference: str """The TypeScript string representation of a given Python type.""" errors: list[str] """Any errors that occurred during conversion.""" def python_type_to_typescript_schema(py_type: type | TypeAliasType) -> TypeScriptSchemaConversionResult: """Converts a Python type to a TypeScript schema.""" node_conversion_result = python_type_to_typescript_nodes(py_type) decl_strs = map(ts_declaration_to_str, node_conversion_result.type_declarations) schema_str = "\n".join(decl_strs) return TypeScriptSchemaConversionResult( typescript_schema_str=schema_str, typescript_type_reference=py_type.__name__, errors=node_conversion_result.errors, ) ================================================ FILE: python/src/typechat/_internal/ts_conversion/python_type_to_ts_nodes.py ================================================ from __future__ import annotations from collections import OrderedDict import inspect import sys import typing import typing_extensions from dataclasses import MISSING, Field, dataclass from types import NoneType, UnionType from typing_extensions import ( Annotated, Any, ClassVar, Doc, Final, Generic, Literal, LiteralString, Never, NoReturn, NotRequired, Protocol, Required, TypeAlias, TypeAliasType, TypeGuard, TypeVar, Union, cast, get_args, get_origin, get_original_bases, get_type_hints, is_typeddict, ) from typechat._internal.ts_conversion.ts_type_nodes import ( AnyTypeReferenceNode, ArrayTypeNode, BooleanTypeReferenceNode, IdentifierNode, IndexSignatureDeclarationNode, InterfaceDeclarationNode, LiteralTypeNode, NeverTypeReferenceNode, NullTypeReferenceNode, NumberTypeReferenceNode, PropertyDeclarationNode, StringTypeReferenceNode, ThisTypeReferenceNode, TopLevelDeclarationNode, TupleTypeNode, TypeAliasDeclarationNode, TypeNode, TypeParameterDeclarationNode, TypeReferenceNode, UnionTypeNode, ) class GenericDeclarationish(Protocol): __parameters__: list[TypeVar] __type_params__: list[TypeVar] # NOTE: may not be present unless running in 3.12 class GenericAliasish(Protocol): __origin__: object __args__: tuple[object, ...] __name__: str class Annotatedish(Protocol): # NOTE: `__origin__` here refers to `SomeType` in `Annnotated[SomeType, ...]` __origin__: object __metadata__: tuple[object, ...] class Dataclassish(Protocol): __dataclass_fields__: dict[str, Field[Any]] # type[TypedDict] # https://github.com/microsoft/pyright/pull/6505#issuecomment-1834431725 class TypeOfTypedDict(Protocol): __total__: bool if sys.version_info >= (3, 12) and typing.TypeAliasType is not typing_extensions.TypeAliasType: # Sometimes typing_extensions aliases TypeAliasType, # sometimes it's its own declaration. def is_type_alias_type(py_type: object) -> TypeGuard[TypeAliasType]: return isinstance(py_type, typing.TypeAliasType | typing_extensions.TypeAliasType) else: def is_type_alias_type(py_type: object) -> TypeGuard[TypeAliasType]: return isinstance(py_type, typing_extensions.TypeAliasType) def is_generic(py_type: object) -> TypeGuard[GenericAliasish]: return hasattr(py_type, "__origin__") and hasattr(py_type, "__args__") def is_dataclass(py_type: object) -> TypeGuard[Dataclassish]: return hasattr(py_type, "__dataclass_fields__") and isinstance(cast(Any, py_type).__dataclass_fields__, dict) TypeReferenceTarget: TypeAlias = type | TypeAliasType | TypeVar | GenericAliasish def is_python_type_or_alias(origin: object) -> TypeGuard[type | TypeAliasType]: return isinstance(origin, type) or is_type_alias_type(origin) _KNOWN_GENERIC_SPECIAL_FORMS: frozenset[Any] = frozenset( [ Required, NotRequired, ClassVar, Final, Annotated, Generic, ] ) _KNOWN_SPECIAL_BASES: frozenset[Any] = frozenset([ typing.TypedDict, typing_extensions.TypedDict, Protocol, # In older versions of Python, `__orig_bases__` will not be defined on `TypedDict`s # derived from the built-in `typing` module (but they will from `typing_extensions`!). # So `get_original_bases` will fetch `__bases__` which will map `TypedDict` to a plain `dict`. dict, ]) @dataclass class TypeScriptNodeTranslationResult: type_declarations: list[TopLevelDeclarationNode] errors: list[str] # TODO: https://github.com/microsoft/pyright/issues/6587 _SELF_TYPE = getattr(typing_extensions, "Self") _LIST_TYPES: set[object] = { list, set, frozenset, # TODO: https://github.com/microsoft/pyright/issues/6582 # collections.abc.MutableSequence, # collections.abc.Sequence, # collections.abc.Set } # TODO: https://github.com/microsoft/pyright/issues/6582 # _DICT_TYPES: set[type] = { # dict, # collections.abc.MutableMapping, # collections.abc.Mapping # } def python_type_to_typescript_nodes(root_py_type: object) -> TypeScriptNodeTranslationResult: # TODO: handle conflicting names declared_types: OrderedDict[object, TopLevelDeclarationNode | None] = OrderedDict() undeclared_types: OrderedDict[object, object] = OrderedDict({root_py_type: root_py_type}) # just a set, really used_names: dict[str, type | TypeAliasType] = {} errors: list[str] = [] def skip_annotations(py_type: object) -> object: origin = py_type while (origin := get_origin(py_type)) and origin in _KNOWN_GENERIC_SPECIAL_FORMS: type_arguments = get_args(py_type) if not type_arguments: errors.append(f"'{origin}' has been used without any type arguments.") return Any py_type = type_arguments[0] continue return py_type def convert_to_type_reference_node(py_type: TypeReferenceTarget) -> TypeNode: py_type_to_declare = py_type if is_generic(py_type): py_type_to_declare = get_origin(py_type) if py_type_to_declare not in declared_types: if is_python_type_or_alias(py_type_to_declare): undeclared_types[py_type_to_declare] = py_type_to_declare elif not isinstance(py_type, TypeVar): errors.append(f"Invalid usage of '{py_type}' as a type annotation.") return AnyTypeReferenceNode if is_generic(py_type): return generic_alias_to_type_reference(py_type) return TypeReferenceNode(IdentifierNode(py_type.__name__)) def generic_alias_to_type_reference(py_type: GenericAliasish) -> TypeReferenceNode: origin = get_origin(py_type) assert origin is not None name = origin.__name__ type_arguments = list(map(convert_to_type_node, get_args(py_type))) return TypeReferenceNode(IdentifierNode(name), type_arguments) def convert_literal_type_arg_to_type_node(py_type: object) -> TypeNode: py_type = skip_annotations(py_type) match py_type: case str() | int() | float(): # no need to match bool, it's a subclass of int return LiteralTypeNode(py_type) case None: return NullTypeReferenceNode case _: errors.append(f"'{py_type}' cannot be used as a literal type.") return AnyTypeReferenceNode def convert_to_type_node(py_type: object) -> TypeNode: py_type = skip_annotations(py_type) if py_type is str or py_type is LiteralString: return StringTypeReferenceNode if py_type is int or py_type is float: return NumberTypeReferenceNode if py_type is bool: return BooleanTypeReferenceNode if py_type is Any or py_type is object: return AnyTypeReferenceNode if py_type is None or py_type is NoneType: return NullTypeReferenceNode if py_type is Never or py_type is NoReturn: return NeverTypeReferenceNode if py_type is _SELF_TYPE: return ThisTypeReferenceNode # TODO: consider handling bare 'tuple' (and list, etc.) # https://docs.python.org/3/library/typing.html#annotating-tuples # Using plain tuple as an annotation is equivalent to using tuple[Any, ...]: origin = get_origin(py_type) if origin is not None: if origin in _LIST_TYPES: (type_arg,) = get_type_argument_nodes(py_type, 1, AnyTypeReferenceNode) if isinstance(type_arg, UnionTypeNode): return TypeReferenceNode(IdentifierNode("Array"), [type_arg]) return ArrayTypeNode(type_arg) if origin is dict: # TODO # Currently, we naively assume all dicts are string-keyed # unless they're annotated with `int` or `float` (note: not `int | float`). key_type_arg, value_type_arg = get_type_argument_nodes(py_type, 2, AnyTypeReferenceNode) if key_type_arg is not NumberTypeReferenceNode: key_type_arg = StringTypeReferenceNode return TypeReferenceNode(IdentifierNode("Record"), [key_type_arg, value_type_arg]) if origin is tuple: # Note that when the type is `tuple[()]`, # `type_args` will be an empty tuple. # Which is nice, because we don't have to special-case anything! type_args = get_args(py_type) if Ellipsis in type_args: if len(type_args) != 2: errors.append( f"The tuple type '{py_type}' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'." ) return ArrayTypeNode(AnyTypeReferenceNode) ellipsis_index = type_args.index(Ellipsis) if ellipsis_index != 1: errors.append( f"The tuple type '{py_type}' is ill-formed because the ellipsis (...) cannot be the first element." ) return ArrayTypeNode(AnyTypeReferenceNode) return ArrayTypeNode(convert_to_type_node(type_args[0])) return TupleTypeNode([convert_to_type_node(py_type_arg) for py_type_arg in type_args]) if origin is Union or origin is UnionType: type_node = [convert_to_type_node(py_type_arg) for py_type_arg in get_args(py_type)] assert len(type_node) > 1 return UnionTypeNode(type_node) if origin is Literal: type_node = [convert_literal_type_arg_to_type_node(py_type_arg) for py_type_arg in get_args(py_type)] assert len(type_node) >= 1 return UnionTypeNode(type_node) assert is_generic(py_type) return convert_to_type_reference_node(py_type) if is_python_type_or_alias(py_type): return convert_to_type_reference_node(py_type) if isinstance(py_type, TypeVar): return convert_to_type_reference_node(py_type) errors.append(f"'{py_type}' cannot be used as a type annotation.") return AnyTypeReferenceNode def declare_property(name: str, py_annotation: type | TypeAliasType, is_typeddict_attribute: bool, optionality_default: bool): """ Declare a property for a given type. If 'optionality_default' is """ current_annotation: object = py_annotation origin: object optional: bool | None = None comment: str | None = None while origin := get_origin(current_annotation): if origin is Annotated and comment is None: current_annotation = cast(Annotatedish, current_annotation) for metadata in current_annotation.__metadata__: if isinstance(metadata, Doc): comment = metadata.documentation break if isinstance(metadata, str): comment = metadata break current_annotation = current_annotation.__origin__ elif origin is Required or origin is NotRequired: if not is_typeddict_attribute: errors.append(f"Optionality cannot be specified with {origin} outside of TypedDicts.") if optional is None: optional = origin is NotRequired else: errors.append(f"{origin} cannot be used within another optionality annotation.") current_annotation = get_args(current_annotation)[0] else: break if optional is None: optional = optionality_default type_annotation = convert_to_type_node(skip_annotations(current_annotation)) return PropertyDeclarationNode(name, optional, comment or "", type_annotation) def reserve_name(val: type | TypeAliasType): type_name = val.__name__ if type_name in used_names: errors.append(f"Cannot create a schema using two types with the same name. {type_name} conflicts between {val} and {used_names[type_name]}") else: used_names[type_name] = val def declare_type(py_type: object): if (is_typeddict(py_type) or is_dataclass(py_type)) and isinstance(py_type, type): comment = py_type.__doc__ or "" if hasattr(py_type, "__type_params__") and cast(GenericDeclarationish, py_type).__type_params__: type_params = [ TypeParameterDeclarationNode(type_param.__name__) for type_param in cast(GenericDeclarationish, py_type).__type_params__ ] elif hasattr(py_type, "__parameters__") and cast(GenericDeclarationish, py_type).__parameters__: type_params = [ TypeParameterDeclarationNode(type_param.__name__) for type_param in cast(GenericDeclarationish, py_type).__parameters__ ] else: type_params = None annotated_members = get_type_hints(py_type, include_extras=True) raw_but_filtered_bases: list[type] = [ base for base in get_original_bases(py_type) if not(base is object or base in _KNOWN_SPECIAL_BASES or get_origin(base) in _KNOWN_GENERIC_SPECIAL_FORMS) ] base_attributes: OrderedDict[str, set[object]] = OrderedDict() for base in raw_but_filtered_bases: for prop, type_hint in get_type_hints(get_origin(base) or base, include_extras=True).items(): base_attributes.setdefault(prop, set()).add(type_hint) bases = [convert_to_type_node(base) for base in raw_but_filtered_bases] properties: list[PropertyDeclarationNode | IndexSignatureDeclarationNode] = [] if is_typeddict(py_type): for attr_name, type_hint in annotated_members.items(): if attribute_identical_in_all_bases(attr_name, type_hint, base_attributes): continue assume_optional = cast(TypeOfTypedDict, py_type).__total__ is False prop = declare_property(attr_name, type_hint, is_typeddict_attribute=True, optionality_default=assume_optional) properties.append(prop) else: # When a dataclass is created with no explicit docstring, @dataclass will # generate one for us; however, we don't want these in the default output. cleaned_signature = str(inspect.signature(py_type)).replace(" -> None", "") dataclass_doc = f"{py_type.__name__}{cleaned_signature}" if comment == dataclass_doc: comment = "" for attr_name, field in cast(Dataclassish, py_type).__dataclass_fields__.items(): type_hint = annotated_members[attr_name] optional = not(field.default is MISSING and field.default_factory is MISSING) prop = declare_property(attr_name, type_hint, is_typeddict_attribute=False, optionality_default=optional) properties.append(prop) reserve_name(py_type) return InterfaceDeclarationNode(py_type.__name__, type_params, comment, bases, properties) if isinstance(py_type, type): errors.append(f"{py_type.__name__} was not a TypedDict, dataclass, or type alias, and cannot be translated.") reserve_name(py_type) return InterfaceDeclarationNode(py_type.__name__, None, "", None, []) if is_type_alias_type(py_type): type_params = [TypeParameterDeclarationNode(type_param.__name__) for type_param in py_type.__type_params__] reserve_name(py_type) return TypeAliasDeclarationNode( py_type.__name__, type_params, f"Comment for {py_type.__name__}.", convert_to_type_node(py_type.__value__), ) raise RuntimeError(f"Cannot declare type {py_type}.") def attribute_identical_in_all_bases(attr_name: str, type_hint: object, base_attributes: dict[str, set[object]]) -> bool: """ We typically want to omit attributes with type hints that are identical to those declared in all base types. """ return attr_name in base_attributes and len(base_attributes[attr_name]) == 1 and type_hint in base_attributes[attr_name] def get_type_argument_nodes(py_type: object, count: int, default: TypeNode) -> list[TypeNode]: py_type_args = get_args(py_type) result: list[TypeNode] = [] if len(py_type_args) != count: errors.append(f"Expected '{count}' type arguments for '{py_type}'.") for i in range(count): if i < len(py_type_args): type_node = convert_to_type_node(py_type_args[i]) else: type_node = default result.append(type_node) return result while undeclared_types: py_type = undeclared_types.popitem()[0] declared_types[py_type] = None declared_types[py_type] = declare_type(py_type) type_declarations = cast(list[TopLevelDeclarationNode], list(declared_types.values())) assert None not in type_declarations return TypeScriptNodeTranslationResult(type_declarations, errors) ================================================ FILE: python/src/typechat/_internal/ts_conversion/ts_node_to_string.py ================================================ import json from typing_extensions import assert_never from typechat._internal.ts_conversion.ts_type_nodes import ( ArrayTypeNode, IdentifierNode, IndexSignatureDeclarationNode, InterfaceDeclarationNode, LiteralTypeNode, NullTypeReferenceNode, PropertyDeclarationNode, TopLevelDeclarationNode, TupleTypeNode, TypeAliasDeclarationNode, TypeNode, TypeReferenceNode, UnionTypeNode, ) def comment_to_str(comment_text: str, indentation: str) -> str: comment_text = comment_text.strip() if not comment_text: return "" lines = [line.strip() for line in comment_text.splitlines()] return "\n".join([f"{indentation}// {line}" for line in lines]) + "\n" def ts_type_to_str(type_node: TypeNode) -> str: match type_node: case TypeReferenceNode(name, type_arguments): assert isinstance(name, IdentifierNode) if type_arguments is None: return name.text return f"{name.text}<{', '.join([ts_type_to_str(arg) for arg in type_arguments])}>" case ArrayTypeNode(element_type): assert type(element_type) is not UnionTypeNode # if type(element_type) is UnionTypeNode: # return f"Array<{ts_type_to_str(element_type)}>" return f"{ts_type_to_str(element_type)}[]" case TupleTypeNode(element_types): return f"[{', '.join([ts_type_to_str(element_type) for element_type in element_types])}]" case UnionTypeNode(types): # Remove duplicates, but try to preserve order of types, # and put null at the end if it's present. str_set: set[str] = set() type_strs: list[str] = [] nullable = False for type_node in types: if type_node is NullTypeReferenceNode: nullable = True continue type_str = ts_type_to_str(type_node) if type_str not in str_set: str_set.add(type_str) type_strs.append(type_str) if nullable: type_strs.append("null") return " | ".join(type_strs) case LiteralTypeNode(value): return json.dumps(value) # case _: # raise NotImplementedError(f"Unhandled type {type(type_node)}") assert_never(type_node) def object_member_to_str(member: PropertyDeclarationNode | IndexSignatureDeclarationNode) -> str: match member: case PropertyDeclarationNode(name, is_optional, comment, annotation): comment = comment_to_str(comment, " ") if not name.isidentifier(): name = json.dumps(name) return f"{comment} {name}{'?' if is_optional else ''}: {ts_type_to_str(annotation)};" case IndexSignatureDeclarationNode(key_type, value_type): return f"[key: {ts_type_to_str(key_type)}]: {ts_type_to_str(value_type)};" # case _: # raise NotImplementedError(f"Unhandled member type {type(member)}") assert_never(member) def ts_declaration_to_str(declaration: TopLevelDeclarationNode) -> str: match declaration: case InterfaceDeclarationNode(name, type_parameters, comment, base_types, members): comment = comment_to_str(comment, "") type_param_str = f"<{', '.join([param.name for param in type_parameters])}>" if type_parameters else "" base_type_str = ( f" extends {', '.join([ts_type_to_str(base_type) for base_type in base_types])}" if base_types else "" ) members_str = "\n".join([f"{object_member_to_str(member)}" for member in members]) + "\n" if members else "" return f"{comment}interface {name}{type_param_str}{base_type_str} {{\n{members_str}}}\n" case TypeAliasDeclarationNode(name, type_parameters, comment, target): type_param_str = f"<{', '.join([param.name for param in type_parameters])}>" if type_parameters else "" return f"type {name}{type_param_str} = {ts_type_to_str(target)}\n" # case _: # raise NotImplementedError(f"Unhandled declaration type {type(declaration)}") assert_never(declaration) ================================================ FILE: python/src/typechat/_internal/ts_conversion/ts_type_nodes.py ================================================ from __future__ import annotations from dataclasses import dataclass from typing_extensions import TypeAlias TypeNode: TypeAlias = "TypeReferenceNode | UnionTypeNode | LiteralTypeNode | ArrayTypeNode | TupleTypeNode" @dataclass class IdentifierNode: text: str @dataclass class QualifiedNameNode: left: QualifiedNameNode | IdentifierNode right: IdentifierNode @dataclass class TypeReferenceNode: name: QualifiedNameNode | IdentifierNode type_arguments: list[TypeNode] | None = None @dataclass class UnionTypeNode: types: list[TypeNode] @dataclass class LiteralTypeNode: value: str | int | float | bool @dataclass class ArrayTypeNode: element_type: TypeNode @dataclass class TupleTypeNode: element_types: list[TypeNode] @dataclass class InterfaceDeclarationNode: name: str type_parameters: list[TypeParameterDeclarationNode] | None comment: str base_types: list[TypeNode] | None members: list[PropertyDeclarationNode | IndexSignatureDeclarationNode] @dataclass class TypeParameterDeclarationNode: name: str constraint: TypeNode | None = None @dataclass class PropertyDeclarationNode: name: str is_optional: bool comment: str type: TypeNode @dataclass class IndexSignatureDeclarationNode: key_type: TypeNode value_type: TypeNode @dataclass class TypeAliasDeclarationNode: name: str type_parameters: list[TypeParameterDeclarationNode] | None comment: str type: TypeNode TopLevelDeclarationNode: TypeAlias = "InterfaceDeclarationNode | TypeAliasDeclarationNode" StringTypeReferenceNode = TypeReferenceNode(IdentifierNode("string")) NumberTypeReferenceNode = TypeReferenceNode(IdentifierNode("number")) BooleanTypeReferenceNode = TypeReferenceNode(IdentifierNode("boolean")) AnyTypeReferenceNode = TypeReferenceNode(IdentifierNode("any")) NullTypeReferenceNode = TypeReferenceNode(IdentifierNode("null")) NeverTypeReferenceNode = TypeReferenceNode(IdentifierNode("never")) ThisTypeReferenceNode = TypeReferenceNode(IdentifierNode("this")) ================================================ FILE: python/src/typechat/_internal/validator.py ================================================ import json from typing_extensions import Generic, TypeVar import pydantic import pydantic_core from typechat._internal.result import Failure, Result, Success T = TypeVar("T", covariant=True) class TypeChatValidator(Generic[T]): """ Validates an object against a given Python type. """ _adapted_type: pydantic.TypeAdapter[T] def __init__(self, py_type: type[T]): """ Args: py_type: The schema type to validate against. """ super().__init__() self._adapted_type = pydantic.TypeAdapter(py_type) def validate_object(self, obj: object) -> Result[T]: """ Validates the given Python object according to the associated schema type. Returns a `Success[T]` object containing the object if validation was successful. Otherwise, returns a `Failure` object with a `message` property describing the error. """ try: # TODO: Switch to `validate_python` when validation modes are exposed. # https://github.com/pydantic/pydantic-core/issues/712 # We'd prefer to keep `validate_object` as the core method and # allow translators to concern themselves with the JSON instead. # However, under Pydantic's `strict` mode, a `dict` isn't considered compatible # with a dataclass. So for now, jump back to JSON and validate the string. json_str = pydantic_core.to_json(obj) typed_dict = self._adapted_type.validate_json(json_str, strict=True) return Success(typed_dict) except pydantic.ValidationError as validation_error: return _handle_error(validation_error) def _handle_error(validation_error: pydantic.ValidationError) -> Failure: error_strings: list[str] = [] for error in validation_error.errors(include_url=False): error_string = "" loc_path = error["loc"] if loc_path: error_string += f"Validation path `{'.'.join(map(str, loc_path))}` " else: error_string += "Root validation " input = error["input"] error_string += f"failed for value `{json.dumps(input)}` because:\n {error['msg']}" error_strings.append(error_string) if len(error_strings) > 1: failure_message = "Several possible issues may have occurred with the given data.\n\n" else: failure_message = "" failure_message += "\n".join(error_strings) return Failure(failure_message) ================================================ FILE: python/src/typechat/py.typed ================================================ ================================================ FILE: python/tests/__init__.py ================================================ # SPDX-FileCopyrightText: Microsoft Corporation # # SPDX-License-Identifier: MIT ================================================ FILE: python/tests/__py3.11_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts ================================================ // Entry point is: 'Derived' interface Derived { my_attr_1: string; my_attr_2: number; } ================================================ FILE: python/tests/__py3.11_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts ================================================ // Entry point is: 'D_or_E' type D_or_E = D | E // This is the definition of the class E. interface E extends C { tag: "E"; next: this | null; } // This is a generic class named C. interface C { x?: T; c: C; } // This is the definition of the class D. interface D extends C { tag?: "D"; // This comes from string metadata // within an Annotated hint. y: boolean | null; z?: number[] | null; other?: IndirectC; non_class?: NonClass; // This comes from later metadata. multiple_metadata?: string; } interface NonClass { a: number; "my-dict": Record; } type IndirectC = C ================================================ FILE: python/tests/__py3.12+_snapshots__/test_generic_alias_3/test_generic_alias3.schema.d.ts ================================================ // Entry point is: 'FirstOrSecond' type FirstOrSecond = First | Second interface Second { kind: "second"; second_attr: T; } interface First { kind: "first"; first_attr: T; } ================================================ FILE: python/tests/__py3.12+_snapshots__/test_generic_alias_4/test_generic_alias4.schema.d.ts ================================================ // Entry point is: 'Nested' interface Nested { item: FirstOrSecond; } type FirstOrSecond = First | Second interface Second { kind: "second"; second_attr: T; } interface First { kind: "first"; first_attr: T; } ================================================ FILE: python/tests/__py3.12+_snapshots__/test_type_alias_syntax/test_type_alias_union1.schema.d.ts ================================================ // Entry point is: 'StrOrInt' type StrOrInt = string | number ================================================ FILE: python/tests/__py3.12_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts ================================================ // Entry point is: 'Derived' // ERRORS: // !!! Cannot create a schema using two types with the same name. C conflicts between .C'> and .C'> interface Derived extends C, C { } interface C { my_attr_2: number; } interface C { my_attr_1: string; } ================================================ FILE: python/tests/__py3.12_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts ================================================ // Entry point is: 'D_or_E' type D_or_E = D | E // This is the definition of the class E. interface E extends C { tag: "E"; next: this | null; } // This is a generic class named C. interface C { x?: T; c: C; } // This is the definition of the class D. interface D extends C { tag?: "D"; // This comes from string metadata // within an Annotated hint. y: boolean | null; z?: number[] | null; other?: IndirectC; non_class?: NonClass; // This comes from later metadata. multiple_metadata?: string; } interface NonClass { a: number; "my-dict": Record; } type IndirectC = C ================================================ FILE: python/tests/__py3.13_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts ================================================ // Entry point is: 'Derived' // ERRORS: // !!! Cannot create a schema using two types with the same name. C conflicts between .C'> and .C'> interface Derived extends C, C { } interface C { my_attr_2: number; } interface C { my_attr_1: string; } ================================================ FILE: python/tests/__py3.13_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts ================================================ // Entry point is: 'D_or_E' type D_or_E = D | E // This is the definition of the class E. interface E extends C { tag: "E"; next: this | null; } // This is a generic class named C. interface C { x?: T; c: C; } // This is the definition of the class D. interface D extends C { tag?: "D"; // This comes from string metadata // within an Annotated hint. y: boolean | null; z?: number[] | null; other?: IndirectC; non_class?: NonClass; // This comes from later metadata. multiple_metadata?: string; } interface NonClass { a: number; "my-dict": Record; } type IndirectC = C ================================================ FILE: python/tests/__py3.14_snapshots__/test_conflicting_names_1/test_conflicting_names_1.schema.d.ts ================================================ // Entry point is: 'Derived' // ERRORS: // !!! Cannot create a schema using two types with the same name. C conflicts between .C'> and .C'> interface Derived extends C, C { } interface C { my_attr_2: number; } interface C { my_attr_1: string; } ================================================ FILE: python/tests/__py3.14_snapshots__/test_hello_world/test_generic_alias1.schema.d.ts ================================================ // Entry point is: 'D_or_E' type D_or_E = D | E // This is the definition of the class E. interface E extends C { tag: "E"; next: this | null; } // This is a generic class named C. interface C { x?: T; c: C; } // This is the definition of the class D. interface D extends C { tag?: "D"; // This comes from string metadata // within an Annotated hint. y: boolean | null; z?: number[] | null; other?: IndirectC; non_class?: NonClass; // This comes from later metadata. multiple_metadata?: string; } interface NonClass { a: number; "my-dict": Record; } type IndirectC = C ================================================ FILE: python/tests/__snapshots__/test_coffeeshop/test_coffeeshop_schema.schema.d.ts ================================================ // Entry point is: 'Cart' interface Cart { type: "Cart"; items: Array; } // Represents any text that could not be understood. interface UnknownText { type: "UnknownText"; // The text that wasn't understood text: string; } interface LineItem { type: "LineItem"; product: BakeryProduct | LatteDrink | CoffeeDrink | EspressoDrink | UnknownText; quantity: number; } interface EspressoDrink { type: "EspressoDrink"; name: "espresso" | "lungo" | "ristretto" | "macchiato"; temperature?: "hot" | "extra hot" | "warm" | "iced"; // The default is 'doppio' size?: "solo" | "doppio" | "triple" | "quad"; options?: Array; } interface LattePreparation { type: "LattePreparation"; name: "for here cup" | "lid" | "with room" | "to go" | "dry" | "wet"; } interface Caffeine { type: "Caffeine"; name: "regular" | "two thirds caf" | "half caf" | "one third caf" | "decaf"; } interface Topping { type: "Topping"; name: "cinnamon" | "foam" | "ice" | "nutmeg" | "whipped cream" | "water"; optionQuantity?: "no" | "light" | "regular" | "extra"; } interface Syrup { type: "Syrup"; name: "almond syrup" | "buttered rum syrup" | "caramel syrup" | "cinnamon syrup" | "hazelnut syrup" | "orange syrup" | "peppermint syrup" | "raspberry syrup" | "toffee syrup" | "vanilla syrup"; optionQuantity?: "no" | "light" | "regular" | "extra"; } interface Sweetener { type: "Sweetener"; name: "equal" | "honey" | "splenda" | "sugar" | "sugar in the raw" | "sweet n low" | "espresso shot"; optionQuantity?: "no" | "light" | "regular" | "extra"; } interface Creamer { type: "Creamer"; 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"; } interface CoffeeDrink { type: "CoffeeDrink"; name: "americano" | "coffee"; temperature?: "hot" | "extra hot" | "warm" | "iced"; // The default is 'grande' size?: "short" | "tall" | "grande" | "venti"; options?: Array; } interface LatteDrink { type: "LatteDrink"; name: "cappuccino" | "flat white" | "latte" | "latte macchiato" | "mocha" | "chai latte"; temperature?: "hot" | "extra hot" | "warm" | "iced"; // The default is 'grande' size?: "short" | "tall" | "grande" | "venti"; options?: Array; } interface BakeryProduct { type: "BakeryProduct"; name: "apple bran muffin" | "blueberry muffin" | "lemon poppyseed muffin" | "bagel"; options?: Array; } interface BakeryPreparation { type: "BakeryPreparation"; name: "warmed" | "cut in half"; } interface BakeryOption { type: "BakeryOption"; name: "butter" | "strawberry jam" | "cream cheese"; optionQuantity?: "no" | "light" | "regular" | "extra"; } ================================================ FILE: python/tests/__snapshots__/test_dataclasses/test_data_classes.schema.d.ts ================================================ // Entry point is: 'Response' interface Response { attr_1: string; // Hello! attr_2: number; attr_3: string | null; attr_4?: string; attr_5?: string | null; attr_6?: string[]; attr_7?: Options; _underscore_attr_1?: number; } // TODO: someone add something here. interface Options { } ================================================ FILE: python/tests/__snapshots__/test_generic_alias_1/test_generic_alias1.schema.d.ts ================================================ // Entry point is: 'FirstOrSecond' type FirstOrSecond = First | Second interface Second { kind: "second"; second_attr: T; } interface First { kind: "first"; first_attr: T; } ================================================ FILE: python/tests/__snapshots__/test_generic_alias_2/test_generic_alias2.schema.d.ts ================================================ // Entry point is: 'Nested' interface Nested { item: FirstOrSecond; } type FirstOrSecond = First | Second interface Second { kind: "second"; second_attr: T; } interface First { kind: "first"; first_attr: T; } ================================================ FILE: python/tests/__snapshots__/test_translator.ambr ================================================ # serializer version: 1 # name: test_translator_with_immediate_pass list([ dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello", "b": true, "c": 1234 }', }), ]) # --- # name: test_translator_with_invalid_json list([ dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello" "b": true }', }), dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), dict({ 'content': '{ "a": "hello" "b": true }', 'role': 'assistant', }), dict({ 'content': ''' The above JSON object is invalid for the following reason: ''' Error: expected `,` or `}` at line 1 column 16 Attempted to parse: { "a": "hello" "b": true } ''' The following is a revised JSON object: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello" "b": true, "c": 1234 }', }), ]) # --- # name: test_translator_with_single_failure list([ dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello", "b": true }', }), dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), dict({ 'content': '{ "a": "hello", "b": true }', 'role': 'assistant', }), dict({ 'content': ''' The above JSON object is invalid for the following reason: ''' Validation path `c` failed for value `{"a": "hello", "b": true}` because: Field required ''' The following is a revised JSON object: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello", "b": true, "c": 1234 }', }), ]) # --- # name: test_translator_with_single_failure_and_list_preamble_1 list([ dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': 'Hey, I need some stuff.', 'role': 'user', }), dict({ 'content': 'Okay, what kind of stuff?', 'role': 'assistant', }), dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello", "b": true }', }), dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': 'Hey, I need some stuff.', 'role': 'user', }), dict({ 'content': 'Okay, what kind of stuff?', 'role': 'assistant', }), dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), dict({ 'content': '{ "a": "hello", "b": true }', 'role': 'assistant', }), dict({ 'content': ''' The above JSON object is invalid for the following reason: ''' Validation path `c` failed for value `{"a": "hello", "b": true}` because: Field required ''' The following is a revised JSON object: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello", "b": true, "c": 1234 }', }), ]) # --- # name: test_translator_with_single_failure_and_str_preamble list([ dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': 'Just so you know, I need some stuff.', 'role': 'user', }), dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello", "b": true }', }), dict({ 'kind': 'CLIENT REQUEST', 'payload': list([ dict({ 'content': 'Just so you know, I need some stuff.', 'role': 'user', }), dict({ 'content': ''' You are a service that translates user requests into JSON objects of type "ExampleABC" according to the following TypeScript definitions: ``` interface ExampleABC { a: string; b: boolean; c: number; } ``` The following is a user request: ''' Get me stuff. ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: ''', 'role': 'user', }), dict({ 'content': '{ "a": "hello", "b": true }', 'role': 'assistant', }), dict({ 'content': ''' The above JSON object is invalid for the following reason: ''' Validation path `c` failed for value `{"a": "hello", "b": true}` because: Field required ''' The following is a revised JSON object: ''', 'role': 'user', }), ]), }), dict({ 'kind': 'MODEL RESPONSE', 'payload': '{ "a": "hello", "b": true, "c": 1234 }', }), ]) # --- ================================================ FILE: python/tests/__snapshots__/test_tuple_errors_1/test_tuples_2.schema.d.ts ================================================ // Entry point is: 'TupleContainer' // ERRORS: // !!! '()' cannot be used as a type annotation. // !!! '()' cannot be used as a type annotation. // !!! '()' cannot be used as a type annotation. // !!! The tuple type 'tuple[...]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'. // !!! The tuple type 'tuple[int, int, ...]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'. // !!! The tuple type 'tuple[..., int]' is ill-formed because the ellipsis (...) cannot be the first element. // !!! The tuple type 'tuple[..., ...]' is ill-formed because the ellipsis (...) cannot be the first element. // !!! The tuple type 'tuple[int, ..., int]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'. // !!! The tuple type 'tuple[int, ..., int, ...]' is ill-formed. Tuples with an ellipsis can only take the form 'tuple[SomeType, ...]'. interface TupleContainer { empty_tuples_args_1: [any, any]; empty_tuples_args_2: any[]; arbitrary_length_1: any[]; arbitrary_length_2: any[]; arbitrary_length_3: any[]; arbitrary_length_4: any[]; arbitrary_length_5: any[]; arbitrary_length_6: any[]; } ================================================ FILE: python/tests/__snapshots__/test_tuples_1/test_tuples_1.schema.d.ts ================================================ // Entry point is: 'TupleContainer' interface TupleContainer { empty_tuple: []; tuple_1: [number]; tuple_2: [number, string]; tuple_3: [number, string]; arbitrary_length_1: number[]; arbitrary_length_2: number[]; arbitrary_length_3: number[]; arbitrary_length_4: number[]; arbitrary_length_5: number[] | [number]; arbitrary_length_6: number[] | [number] | [number, number]; } ================================================ FILE: python/tests/coffeeshop_deprecated.py ================================================ from typing import List, Literal, NotRequired, TypeAlias, TypedDict, Union from typechat import python_type_to_typescript_schema # This version of coffeeshop uses older constructs for # types like List and Union. It is included here for # testing purposes. class UnknownText(TypedDict): """ Represents any text that could not be understood. """ type: Literal["UnknownText"] text: str class Caffeine(TypedDict): type: Literal["Caffeine"] name: Literal["regular", "two thirds caf", "half caf", "one third caf", "decaf"] class Milk(TypedDict): type: Literal["Milk"] name: Literal[ "whole milk", "two percent milk", "nonfat milk", "coconut milk", "soy milk", "almond milk", "oat milk" ] class Creamer(TypedDict): type: Literal["Creamer"] name: Literal[ "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", ] class Topping(TypedDict): type: Literal["Topping"] name: Literal["cinnamon", "foam", "ice", "nutmeg", "whipped cream", "water"] optionQuantity: NotRequired["OptionQuantity"] class LattePreparation(TypedDict): type: Literal["LattePreparation"] name: Literal["for here cup", "lid", "with room", "to go", "dry", "wet"] class Sweetener(TypedDict): type: Literal["Sweetener"] name: Literal["equal", "honey", "splenda", "sugar", "sugar in the raw", "sweet n low", "espresso shot"] optionQuantity: NotRequired["OptionQuantity"] CaffeineOptions = Union[Caffeine, Milk, Creamer] LatteOptions = Union[CaffeineOptions, Topping, LattePreparation, Sweetener] CoffeeTemperature: TypeAlias = Literal["hot", "extra hot", "warm", "iced"] CoffeeSize: TypeAlias = Literal["short", "tall", "grande", "venti"] EspressoSize: TypeAlias = Literal["solo", "doppio", "triple", "quad"] OptionQuantity: TypeAlias = Literal["no", "light", "regular", "extra"] class Syrup(TypedDict): type: Literal["Syrup"] name: Literal[ "almond syrup", "buttered rum syrup", "caramel syrup", "cinnamon syrup", "hazelnut syrup", "orange syrup", "peppermint syrup", "raspberry syrup", "toffee syrup", "vanilla syrup", ] optionQuantity: NotRequired[OptionQuantity] class LatteDrink(TypedDict): type: Literal["LatteDrink"] name: Literal["cappuccino", "flat white", "latte", "latte macchiato", "mocha", "chai latte"] temperature: NotRequired["CoffeeTemperature"] size: NotRequired["CoffeeSize"] # The default is 'grande' options: NotRequired[List[Union[Creamer, Sweetener, Syrup, Topping, Caffeine, LattePreparation]]] class EspressoDrink(TypedDict): type: Literal["EspressoDrink"] name: Literal["espresso", "lungo", "ristretto", "macchiato"] temperature: NotRequired["CoffeeTemperature"] size: NotRequired["EspressoSize"] # The default is 'doppio' options: NotRequired[List[Union[Creamer, Sweetener, Syrup, Topping, Caffeine, LattePreparation]]] class CoffeeDrink(TypedDict): type: Literal["CoffeeDrink"] name: Literal["americano", "coffee"] temperature: NotRequired[CoffeeTemperature] size: NotRequired[CoffeeSize] # The default is "grande" options: NotRequired[List[Union[Creamer, Sweetener, Syrup, Topping, Caffeine, LattePreparation]]] class BakeryOption(TypedDict): type: Literal["BakeryOption"] name: Literal["butter", "strawberry jam", "cream cheese"] optionQuantity: NotRequired["OptionQuantity"] class BakeryPreparation(TypedDict): type: Literal["BakeryPreparation"] name: Literal["warmed", "cut in half"] class BakeryProduct(TypedDict): type: Literal["BakeryProduct"] name: Literal["apple bran muffin", "blueberry muffin", "lemon poppyseed muffin", "bagel"] options: NotRequired[List[BakeryOption | BakeryPreparation]] Product = Union[BakeryProduct, LatteDrink, CoffeeDrink, UnknownText] class LineItem(TypedDict): type: Literal["LineItem"] product: Product quantity: int class Cart(TypedDict): type: Literal["Cart"] items: List[LineItem | UnknownText] result = python_type_to_typescript_schema(Cart) print(f"// Entry point is: '{result.typescript_type_reference}'") print("// TypeScript Schema:\n") print(result.typescript_schema_str) if result.errors: print("// Errors:") for err in result.errors: print(f"// - {err}\n") ================================================ FILE: python/tests/test_coffeeshop.py ================================================ from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc, Any from typechat import python_type_to_typescript_schema from .utilities import TypeScriptSchemaSnapshotExtension class UnknownText(TypedDict): """ Represents any text that could not be understood. """ type: Literal["UnknownText"] text: Annotated[str, Doc("The text that wasn't understood")] class Caffeine(TypedDict): type: Literal["Caffeine"] name: Literal["regular", "two thirds caf", "half caf", "one third caf", "decaf"] class Milk(TypedDict): type: Literal["Milk"] name: Literal[ "whole milk", "two percent milk", "nonfat milk", "coconut milk", "soy milk", "almond milk", "oat milk" ] class Creamer(TypedDict): type: Literal["Creamer"] name: Literal[ "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", ] class Topping(TypedDict): type: Literal["Topping"] name: Literal["cinnamon", "foam", "ice", "nutmeg", "whipped cream", "water"] optionQuantity: NotRequired["OptionQuantity"] class LattePreparation(TypedDict): type: Literal["LattePreparation"] name: Literal["for here cup", "lid", "with room", "to go", "dry", "wet"] class Sweetener(TypedDict): type: Literal["Sweetener"] name: Literal["equal", "honey", "splenda", "sugar", "sugar in the raw", "sweet n low", "espresso shot"] optionQuantity: NotRequired["OptionQuantity"] CaffeineOptions = Caffeine | Milk | Creamer LatteOptions = CaffeineOptions | Topping | LattePreparation | Sweetener CoffeeTemperature = Literal["hot", "extra hot", "warm", "iced"] CoffeeSize = Literal["short", "tall", "grande", "venti"] EspressoSize = Literal["solo", "doppio", "triple", "quad"] OptionQuantity = Literal["no", "light", "regular", "extra"] class Syrup(TypedDict): type: Literal["Syrup"] name: Literal[ "almond syrup", "buttered rum syrup", "caramel syrup", "cinnamon syrup", "hazelnut syrup", "orange syrup", "peppermint syrup", "raspberry syrup", "toffee syrup", "vanilla syrup", ] optionQuantity: NotRequired[OptionQuantity] class LatteDrink(TypedDict): type: Literal["LatteDrink"] name: Literal["cappuccino", "flat white", "latte", "latte macchiato", "mocha", "chai latte"] temperature: NotRequired["CoffeeTemperature"] size: NotRequired[Annotated["CoffeeSize", Doc("The default is 'grande'")]] options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]] class EspressoDrink(TypedDict): type: Literal["EspressoDrink"] name: Literal["espresso", "lungo", "ristretto", "macchiato"] temperature: NotRequired["CoffeeTemperature"] size: NotRequired[Annotated["EspressoSize", Doc("The default is 'doppio'")]] options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]] class CoffeeDrink(TypedDict): type: Literal["CoffeeDrink"] name: Literal["americano", "coffee"] temperature: NotRequired[CoffeeTemperature] size: NotRequired[Annotated[CoffeeSize, Doc("The default is 'grande'")]] options: NotRequired[list[Creamer | Sweetener | Syrup | Topping | Caffeine | LattePreparation]] class BakeryOption(TypedDict): type: Literal["BakeryOption"] name: Literal["butter", "strawberry jam", "cream cheese"] optionQuantity: NotRequired["OptionQuantity"] class BakeryPreparation(TypedDict): type: Literal["BakeryPreparation"] name: Literal["warmed", "cut in half"] class BakeryProduct(TypedDict): type: Literal["BakeryProduct"] name: Literal["apple bran muffin", "blueberry muffin", "lemon poppyseed muffin", "bagel"] options: NotRequired[list[BakeryOption | BakeryPreparation]] Product = BakeryProduct | LatteDrink | CoffeeDrink | EspressoDrink | UnknownText class LineItem(TypedDict): type: Literal["LineItem"] product: Product quantity: int class Cart(TypedDict): type: Literal["Cart"] items: list[LineItem | UnknownText] def test_coffeeshop_schema(snapshot: Any): assert(python_type_to_typescript_schema(Cart) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)) ================================================ FILE: python/tests/test_conflicting_names_1.py ================================================ from typing import Any, TypedDict, cast from typechat import python_type_to_typescript_schema from .utilities import PyVersionedTypeScriptSchemaSnapshotExtension def a(): class C(TypedDict): my_attr_1: str return C def b(): class C(TypedDict): my_attr_2: int return C A = a() B = b() class Derived(A, B): # type: ignore pass def test_conflicting_names_1(snapshot: Any): assert python_type_to_typescript_schema(cast(type, Derived)) == snapshot(extension_class=PyVersionedTypeScriptSchemaSnapshotExtension) ================================================ FILE: python/tests/test_dataclasses.py ================================================ from typing_extensions import Any from typing import Annotated from dataclasses import dataclass, field from typechat import python_type_to_typescript_schema from .utilities import TypeScriptSchemaSnapshotExtension @dataclass class Options: """ TODO: someone add something here. """ ... @dataclass class Response: attr_1: str attr_2: Annotated[int, "Hello!"] attr_3: str | None attr_4: str = "hello!" attr_5: str | None = None attr_6: list[str] = field(default_factory=list) attr_7: Options = field(default_factory=Options) _underscore_attr_1: int = 123 def do_something(self): print(f"{self.attr_1=}") def test_data_classes(snapshot: Any): assert(python_type_to_typescript_schema(Response) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)) ================================================ FILE: python/tests/test_generic_alias_1.py ================================================ from typing_extensions import TypeAliasType, Any from typing import Literal, TypedDict, TypeVar, Generic from typechat import python_type_to_typescript_schema from .utilities import TypeScriptSchemaSnapshotExtension T = TypeVar("T", covariant=True) class First(Generic[T], TypedDict): kind: Literal["first"] first_attr: T class Second(Generic[T], TypedDict): kind: Literal["second"] second_attr: T FirstOrSecond = TypeAliasType("FirstOrSecond", First[T] | Second[T], type_params=(T,)) def test_generic_alias1(snapshot: Any): assert(python_type_to_typescript_schema(FirstOrSecond) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)) ================================================ FILE: python/tests/test_generic_alias_2.py ================================================ from typing_extensions import TypeAliasType, Any from typing import Literal, TypedDict, Generic, TypeVar from typechat import python_type_to_typescript_schema from .utilities import TypeScriptSchemaSnapshotExtension T = TypeVar("T", covariant=True) class First(Generic[T], TypedDict): kind: Literal["first"] first_attr: T class Second(Generic[T], TypedDict): kind: Literal["second"] second_attr: T FirstOrSecond = TypeAliasType("FirstOrSecond", First[T] | Second[T], type_params=(T,)) class Nested(TypedDict): item: FirstOrSecond[str] def test_generic_alias2(snapshot: Any): assert(python_type_to_typescript_schema(Nested) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension)) ================================================ FILE: python/tests/test_generic_alias_3.py ================================================ from typing import Any from .utilities import check_snapshot_for_module_string_if_3_12_plus module_str = """ from typing import Literal, TypedDict class First[T](TypedDict): kind: Literal["first"] first_attr: T class Second[T](TypedDict): kind: Literal["second"] second_attr: T type FirstOrSecond[T] = First[T] | Second[T] """ def test_generic_alias3(snapshot: Any): check_snapshot_for_module_string_if_3_12_plus(snapshot, input_type_str="FirstOrSecond", module_str=module_str) ================================================ FILE: python/tests/test_generic_alias_4.py ================================================ from typing import Any from .utilities import check_snapshot_for_module_string_if_3_12_plus module_str = """ from typing import Literal, TypedDict class First[T](TypedDict): kind: Literal["first"] first_attr: T class Second[T](TypedDict): kind: Literal["second"] second_attr: T type FirstOrSecond[T] = First[T] | Second[T] class Nested(TypedDict): item: FirstOrSecond[str] """ def test_generic_alias4(snapshot: Any): check_snapshot_for_module_string_if_3_12_plus(snapshot, input_type_str="Nested", module_str=module_str) ================================================ FILE: python/tests/test_hello_world.py ================================================ from typing import Annotated, Literal, NotRequired, Optional, Required, Self, TypedDict, TypeVar, Generic, Any from typing_extensions import TypeAliasType from typechat import python_type_to_typescript_schema from .utilities import PyVersionedTypeScriptSchemaSnapshotExtension T = TypeVar("T", covariant=True) class C(Generic[T], TypedDict): "This is a generic class named C." x: NotRequired[T] c: "C[int | float | None]" IndirectC = TypeAliasType("IndirectC", C[int]) class D(C[str], total=False): "This is the definition of the class D." tag: Literal["D"] y: Required[Annotated[bool | None, "This comes from string metadata\nwithin an Annotated hint."]] z: Optional[list[int]] other: IndirectC non_class: "NonClass" multiple_metadata: Annotated[str, None, str, "This comes from later metadata.", int] NonClass = TypedDict("NonClass", {"a": int, "my-dict": dict[str, int]}) class E(C[str]): "This is the definition of the class E." tag: Literal["E"] next: Self | None D_or_E = TypeAliasType("D_or_E", D | E) def test_generic_alias1(snapshot: Any): assert(python_type_to_typescript_schema(D_or_E) == snapshot(extension_class=PyVersionedTypeScriptSchemaSnapshotExtension)) ================================================ FILE: python/tests/test_translator.py ================================================ import asyncio from dataclasses import dataclass from typing_extensions import Any, Iterator, Literal, TypedDict, override import typechat class ConvoRecord(TypedDict): kind: Literal["CLIENT REQUEST", "MODEL RESPONSE"] payload: str | list[typechat.PromptSection] class FixedModel(typechat.TypeChatLanguageModel): responses: Iterator[str] conversation: list[ConvoRecord] "A model which responds with one of a series of responses." def __init__(self, responses: list[str]) -> None: super().__init__() self.responses = iter(responses) self.conversation = [] @override async def complete(self, prompt: str | list[typechat.PromptSection]) -> typechat.Result[str]: # Capture a snapshot because the translator # can choose to pass in the same underlying list. if isinstance(prompt, list): prompt = prompt.copy() self.conversation.append({ "kind": "CLIENT REQUEST", "payload": prompt }) response = next(self.responses) self.conversation.append({ "kind": "MODEL RESPONSE", "payload": response }) return typechat.Success(response) @dataclass class ExampleABC: a: str b: bool c: int v = typechat.TypeChatValidator(ExampleABC) def test_translator_with_immediate_pass(snapshot: Any): m = FixedModel([ '{ "a": "hello", "b": true, "c": 1234 }', ]) t = typechat.TypeChatJsonTranslator(m, v, ExampleABC) asyncio.run(t.translate("Get me stuff.")) assert m.conversation == snapshot def test_translator_with_single_failure(snapshot: Any): m = FixedModel([ '{ "a": "hello", "b": true }', '{ "a": "hello", "b": true, "c": 1234 }', ]) t = typechat.TypeChatJsonTranslator(m, v, ExampleABC) asyncio.run(t.translate("Get me stuff.")) assert m.conversation == snapshot def test_translator_with_invalid_json(snapshot: Any): m = FixedModel([ '{ "a": "hello" "b": true }', '{ "a": "hello" "b": true, "c": 1234 }', ]) t = typechat.TypeChatJsonTranslator(m, v, ExampleABC) asyncio.run(t.translate("Get me stuff.")) assert m.conversation == snapshot def test_translator_with_single_failure_and_str_preamble(snapshot: Any): m = FixedModel([ '{ "a": "hello", "b": true }', '{ "a": "hello", "b": true, "c": 1234 }', ]) t = typechat.TypeChatJsonTranslator(m, v, ExampleABC) asyncio.run(t.translate( "Get me stuff.", prompt_preamble="Just so you know, I need some stuff.", )) assert m.conversation == snapshot def test_translator_with_single_failure_and_list_preamble_1(snapshot: Any): m = FixedModel([ '{ "a": "hello", "b": true }', '{ "a": "hello", "b": true, "c": 1234 }', ]) t = typechat.TypeChatJsonTranslator(m, v, ExampleABC) asyncio.run(t.translate("Get me stuff.", prompt_preamble=[ {"role": "user", "content": "Hey, I need some stuff."}, {"role": "assistant", "content": "Okay, what kind of stuff?"}, ])) assert m.conversation == snapshot ================================================ FILE: python/tests/test_tuple_errors_1.py ================================================ from dataclasses import dataclass from typing import Any from typechat import python_type_to_typescript_schema from .utilities import TypeScriptSchemaSnapshotExtension @dataclass class TupleContainer: empty_tuples_args_1: tuple[(), ()] # type: ignore empty_tuples_args_2: tuple[(), ...] # type: ignore # Arbitrary-length tuples have exactly two type arguments – the type and an ellipsis. # Any other tuple form that uses an ellipsis is invalid. arbitrary_length_1: tuple[...] # type: ignore arbitrary_length_2: tuple[int, int, ...] # type: ignore arbitrary_length_3: tuple[..., int] # type: ignore arbitrary_length_4: tuple[..., ...] # type: ignore arbitrary_length_5: tuple[int, ..., int] # type: ignore arbitrary_length_6: tuple[int, ..., int, ...] # type: ignore def test_tuples_2(snapshot: Any): assert python_type_to_typescript_schema(TupleContainer) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension) ================================================ FILE: python/tests/test_tuples_1.py ================================================ from dataclasses import dataclass from typing import Any from typechat import python_type_to_typescript_schema from .utilities import TypeScriptSchemaSnapshotExtension @dataclass class TupleContainer: # The empty tuple can be annotated as tuple[()]. empty_tuple: tuple[()] tuple_1: tuple[int] tuple_2: tuple[int, str] tuple_3: tuple[int, str] | tuple[float, str] # Arbitrary-length homogeneous tuples can be expressed using one type and an ellipsis, for example tuple[int, ...]. arbitrary_length_1: tuple[int, ...] arbitrary_length_2: tuple[int, ...] | list[int] arbitrary_length_3: tuple[int, ...] | tuple[int, ...] arbitrary_length_4: tuple[int, ...] | tuple[float, ...] arbitrary_length_5: tuple[int, ...] | tuple[int] arbitrary_length_6: tuple[int, ...] | tuple[int] | tuple[int, int] def test_tuples_1(snapshot: Any): assert python_type_to_typescript_schema(TupleContainer) == snapshot(extension_class=TypeScriptSchemaSnapshotExtension) ================================================ FILE: python/tests/test_type_alias_syntax.py ================================================ from typing import Any from .utilities import check_snapshot_for_module_string_if_3_12_plus module_str = "type StrOrInt = str | int" def test_type_alias_union1(snapshot: Any): check_snapshot_for_module_string_if_3_12_plus(snapshot, "StrOrInt", module_str) ================================================ FILE: python/tests/test_validator.py ================================================ from dataclasses import dataclass import typechat @dataclass class Example: a: str b: int c: bool v = typechat.TypeChatValidator(Example) def test_dict_valid_as_dataclass(): r = v.validate_object({"a": "hello!", "b": 42, "c": True}) assert r == typechat.Success(Example(a="hello!", b=42, c=True)) ================================================ FILE: python/tests/utilities.py ================================================ from pathlib import Path import sys import types from typing_extensions import Any, override import pytest from syrupy.extensions.single_file import SingleFileSnapshotExtension, WriteMode from syrupy.location import PyTestLocation from typechat._internal.ts_conversion import TypeScriptSchemaConversionResult, python_type_to_typescript_schema class TypeScriptSchemaSnapshotExtension(SingleFileSnapshotExtension): _write_mode = WriteMode.TEXT file_extension = "schema.d.ts" @override def serialize(self, data: TypeScriptSchemaConversionResult, *, exclude: Any = None, include: Any = None, matcher: Any = None, ) -> str: result_str = f"// Entry point is: '{data.typescript_type_reference}'\n\n" if data.errors: result_str += "// ERRORS:\n" for err in data.errors: result_str += f"// !!! {err}\n" result_str += "\n" result_str += data.typescript_schema_str return result_str class PyVersionedTypeScriptSchemaSnapshotExtension(TypeScriptSchemaSnapshotExtension): py_ver_dir: str = f"__py{sys.version_info.major}.{sys.version_info.minor}_snapshots__" @override @classmethod def dirname(cls, *, test_location: PyTestLocation) -> str: result = Path(test_location.filepath).parent.joinpath( f"{cls.py_ver_dir}", test_location.basename, ) return str(result) class PyVersioned3_12_PlusSnapshotExtension(PyVersionedTypeScriptSchemaSnapshotExtension): py_ver_dir: str = f"__py3.12+_snapshots__" def check_snapshot_for_module_string_if_3_12_plus(snapshot: Any, input_type_str: str, module_str: str): if sys.version_info < (3, 12): pytest.skip("requires python 3.12 or higher") module = types.ModuleType("test_module") exec(module_str, module.__dict__) type_obj = eval(input_type_str, globals(), module.__dict__) assert(python_type_to_typescript_schema(type_obj) == snapshot(extension_class=PyVersioned3_12_PlusSnapshotExtension)) @pytest.fixture def snapshot_schema(snapshot: Any): return snapshot.with_defaults(extension_class=TypeScriptSchemaSnapshotExtension) ================================================ FILE: site/.eleventy.js ================================================ const dateFormatter = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", day: "numeric" }); const listFormatter = new Intl.ListFormat("en-US", { style: "long", type: "conjunction" }); /** * * @param {import("@11ty/eleventy").UserConfig} eleventyConfig */ module.exports = async function (eleventyConfig) { const shiki = await import("shiki"); const { EleventyHtmlBasePlugin } = await import("@11ty/eleventy"); eleventyConfig.addPlugin(EleventyHtmlBasePlugin); eleventyConfig.addPassthroughCopy("./src/css"); eleventyConfig.addPassthroughCopy("./src/js"); eleventyConfig.addFilter("formatDate", value => dateFormatter.format(value)); eleventyConfig.addFilter("formatList", value => listFormatter.format(value)); eleventyConfig.setNunjucksEnvironmentOptions({ throwOnUndefined: true, }); eleventyConfig.amendLibrary("md", () => { }); eleventyConfig.on("eleventy.before", async () => { const highlighter = await shiki.getHighlighter({ langs: [ "typescript", "javascript", "tsx", "jsx", "jsonc", "json", "html", "diff", "bat", "sh", "python", "py", ], theme: "dark-plus" }); eleventyConfig.amendLibrary("md", (mdLib) => mdLib.set({ highlight: (code, lang) => highlighter.codeToHtml(code, { lang }), }) ); }); return { dir: { input: "src", output: "_site" }, pathPrefix: "TypeChat", }; } ================================================ FILE: site/.gitignore ================================================ _site ================================================ FILE: site/jsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "module": "nodenext", "lib": ["esnext", "es2021.intl"], "noEmit": true, "checkJs": true }, "include": [ "./.eleventy.js", ], "exclude": [] } ================================================ FILE: site/package.json ================================================ { "name": "typechat-site", "private": true, "version": "0.0.1", "description": "Website for TypeChat", "main": "index.js", "scripts": { "build": "eleventy", "serve": "eleventy --serve" }, "repository": { "type": "git", "url": "git+https://github.com/microsoft/TypeChat.git" }, "author": "", "license": "MIT", "bugs": { "url": "https://github.com/microsoft/TypeChat/issues" }, "homepage": "https://github.com/microsoft/TypeChat#readme", "devDependencies": { "@11ty/eleventy": "^3.1.2", "shiki": "^0.14.3" } } ================================================ FILE: site/src/_data/docsTOC.json ================================================ [ { "groupName": "Home", "pages": [ { "title": "Introduction", "url": "/docs/introduction/"}, { "title": "Examples", "url": "/docs/examples/"}, { "title": "Techniques", "url": "/docs/techniques/"}, { "title": "FAQ", "url": "/docs/faq/"}, { "title": "Basic Usage for TypeScript", "url": "/docs/typescript/basic-usage/"} ] } ] ================================================ FILE: site/src/_data/headernav.json ================================================ [ { "title": "Home", "dest": "/" }, { "title": "Docs", "dest": "/docs/" }, { "title": "Blog", "dest": "/blog/" }, { "title": "GitHub", "dest": "https://github.com/microsoft/TypeChat", "isExternal": true } ] ================================================ FILE: site/src/_includes/base.njk ================================================ {% if title %}{{title}} - TypeChat{% else %}TypeChat{% endif %} {% include "header-prologue.njk" %} {{ content | safe }} {% include "footer.njk" %} ================================================ FILE: site/src/_includes/blog.njk ================================================ --- layout: base ---
{{ content | safe }}
================================================ FILE: site/src/_includes/doc-page.njk ================================================ --- layout: docs ---

{{title}}

{{content | safe}} ================================================ FILE: site/src/_includes/docs.njk ================================================ --- layout: base title: Docs ---
{{ content | safe }}
================================================ FILE: site/src/_includes/footer.njk ================================================ ================================================ FILE: site/src/_includes/header-prologue.njk ================================================ Skip to main content
TypeChat
================================================ FILE: site/src/blog/announcing-typechat-0-1-0.md ================================================ --- title: Announcing TypeChat 0.1.0 layout: blog tags: post date: 2024-03-25 authors: ["Daniel Rosenwasser"] --- # {{title}} *{{date | formatDate}}{% if authors %} by {{authors | formatList}}{% endif %}* Today we've released a new version of TypeChat for TypeScript and JavaScript. To get it, you can run ```sh npm install typechat ``` As a refresher, TypeChat is an experimental library for getting structured output (like JSON) from AI language models. The 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. When they don't, TypeChat can use validation errors to guide language models to repair their responses. You 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. Here's a few things that are new to TypeChat for TypeScript. ## Pluggable Validators The original version of TypeChat actually leveraged the raw contents of a TypeScript schema file. It looked something like this: ```ts // Load up the contents of our "Response" schema. const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8"); const translator = typechat.createJsonTranslator(model, schema, "SomeType"); // Process requests interactively. typechat.processRequests("> ", /*inputFile*/ undefined, async (request) => { const response = await translator.translate(request); if (response.success) { console.log(`❌ ${response.message}`); return; } console.log("The request was translated into the following value:") console.log(response.data); }); ``` This worked, but had a few issues: 1. The schema file had to be self-contained. Everything had to be in the same file for TypeChat. 1. The schema file also had to be present if you weren't running in-place. This often meant copying the schema file along to the output directory if you weren't using something like ts-node, tsx, or tsimp. 1. The schema was fixed. While possible to generate a text schema on the fly, it's an error-prone task. While 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. The first is that we've broken out a piece of `TypeChatJsonTranslator` into a more granular concept: a `TypeChatJsonValidator`. A `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. This means that to construct a `TypeChatJsonTranslator`, you need to make a `TypeChatJsonValidator` first; but it also means that validators are swappable. Here's what using that looks like now: ```ts import fs from "fs"; import path from "path"; import { createLanguageModel, createJsonTranslator } from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { SentimentResponse } from "./sentimentSchema"; const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8"); const validator = createTypeScriptJsonValidator(schema, "SentimentResponse"); const translator = createJsonTranslator(model, validator); translator.translate("hello world!").then(response => { if (!response.success) { console.log(response.message); return; } console.log(`The sentiment is ${response.data.sentiment}`); }); ``` Notice that instead of passing the schema into `createJsonTranslator`, we're passing it into `createTypeScriptJsonValidator` which we need to import from `typechat/ts`. The created validator the needs to be passed into `createJsonTranslator`. For existing calls to `createJsonTranslator`, you'll probably see a message like: > TS2554: Expected 2 arguments, but got 3. you'll need to drop the name of the type, and substitute the argument schema with a validator. Here's the effective diff: ```diff import { createJsonTranslator, createLanguageModel, processRequests } from "typechat"; + import { createTypeScriptJsonValidator } from "typechat/ts"; import { SentimentResponse } from "./sentimentSchema"; // ... - const translator = createJsonTranslator(model, schema, "Sentiment") + const validator = createTypeScriptJsonValidator(schema, "SentimentResponse"); + const translator = createJsonTranslator(model, validator); // ... ``` ## Zod Validators The second change builds on pluggable validators: TypeChat makes it possible to create validators from Zod schemas. [If you're not familiar with Zod](https://zod.dev/), it's a popular library in the TypeScript/JavaScript ecosystem for validating data. One strength of this library is that as Zod type validator objects are constructed, static types can be derived from them. But for TypeChat, its more notable strength is the ability to construct schemas *dynamically*. To 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. ```ts // sentimentSchema.ts import { z } from "zod"; export const SentimentResponse = z.object({ sentiment: z.enum(["negative", "neutral", "positive"]) .describe("The sentiment of the text") }); // Maps the property "SentimentResponse" to the above Zod validator. export const SentimentSchema = { SentimentResponse }; ``` Note 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). Next, we have to construct a TypeChat Zod validator. We pass in the object map of types, and specify which type we want the model to conform to: ```ts // main.ts import { createJsonTranslator, createLanguageModel } from "typechat"; import { createZodJsonValidator } from "typechat/zod"; import { SentimentSchema } from "./sentimentSchema"; const model = createLanguageModel(process.env); const validator = createZodJsonValidator(SentimentSchema, "SentimentResponse"); const translator = createJsonTranslator(model, validator); translator.translate("hello world!").then(response => { if (!response.success) { console.log(response.message); return; } console.log(`The sentiment is ${response.data.sentiment}`); }); ``` That's it! While using a Zod schema has lots of advantages, you may still prefer the ergonomics of writing a plain TypeScript schema. Either option works. For more information, [see the changes on GitHub](https://github.com/microsoft/TypeChat/pull/147). ## A `validateInstance` Hook Another new addition to TypeChat is the `validateInstance` hook on `TypeChatJsonTranslator`s. It allows you to tack on an extra level of validation beyond what the internal validator will perform. ```ts import { createJsonTranslator, error, success } from "typechat"; // ... const translator = createJsonTranslator(model, validator); translator.validateInstance = summary => { for (const person of summary.people) { if (person.age < 0) { return error( `'{person.name}' has a negative age, that doesn't make sense.` ) } } return success(summary) } ``` If `validateInstance` returns a TypeChat `Error`, then the translator will use the message to repair the AI response. You can [see specifics of this change on GitHub](https://github.com/microsoft/TypeChat/pull/115). ## Other Changes Other changes to be aware of are: * `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)). * 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)). ## What's Next? We'll be trying to improve TypeChat based on the feedback we receive. We'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. Give 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! ================================================ FILE: site/src/blog/index.njk ================================================ --- title: Blog --- {% set latestPost = collections.post[0] %} {% if latestPost %} Redirecting to {{latestPost.data.title}} {%endif%} {% include "header-prologue.njk" %} {% include "footer.njk" %}
{#-

Blog Posts

{% endfor %}
-#} ================================================ FILE: site/src/blog/introducing-typechat.md ================================================ --- title: Introducing TypeChat layout: blog tags: post date: 2023-07-20 authors: ["Anders Hejlsberg", "Steve Lucco", "Daniel Rosenwasser", "Pierce Boggan", "Umesh Madan", "Mike Hopcroft", "Gayathri Chandrasekaran"] --- # {{title}} *{{date | formatDate}}{% if authors %} by {{authors | formatList}}{% endif %}* In the last few months, we've seen a rush of excitement around the newest wave of large language models. While chat assistants have been the most direct application, there's a big question around how to best integrate these models into existing app interfaces. In other words, how do we *augment* traditional UI with natural language interfaces? How do we use AI to take a user request and turn it into something our apps can operate on? And how do we make sure our apps are safe, and doing work that developers and users alike can trust? Today we're releasing **TypeChat**, an experimental library that aims to answer these questions. It uses the type definitions in your codebase to retrieve structured AI responses that are type-safe. You can get up and running with TypeChat today by running ``` npm install typechat ``` and hooking it up with any language model to work with your app. But let's first quickly explore why TypeChat exists. ## Pampering and Parsing The current wave of LLMs default to conversational *natural* language — languages that humans communicate in like English. Parsing 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". Natural language might have structure, but it's hard for typical software to reconstruct it from raw text. Surprisingly, we can ask LLMs to respond in the form of JSON, and they generally respond with something sensible! > **User:** > > Translate the following request into JSON. > > > Could I get a blueberry muffin and a grande latte? > > Respond only in JSON like the following: > > ```json > { > "items": [ > { "name": "croissant", "quantity": 2 }, > { "name": "latte", "quantity": 1, "size": "tall" } > ] > } > ``` > > **ChatBot:** > > ```json > { > "items": [ > { > "name": "blueberry muffin", > "quantity": 1 > }, > { > "name": "latte", > "quantity": 1, > "size": "grande" > } > ] > } > ``` This is good — though this example shows the best-case response. While 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. ## Just Add Types! Luckily **types** do precisely that. What 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. Because 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. > **User:** > > Translate the following request into JSON. > > > Could I get a blueberry muffin and a grande latte? > > Respond only in JSON that satisfies the `Response` type: > > ```ts > type Response = { > items: Item[]; > }; > > type Item = { > name: string; > quantity: number; > size?: string; > notes?: string; > } > ``` > > **ChatBot:** > > ```json > { > "items": [ > { > "name": "blueberry muffin", > "quantity": 1 > }, > { > "name": "latte", > "quantity": 1, > "size": "grande" > } > ] > } > ``` This is pretty great! TypeScript has shown that it's well-suited to precisely describe JSON. But what happens when a language model stumbles and makes up a response that doesn't conform to our types? Well because these types are valid TypeScript code, we can validate the response against them using the TypeScript compiler itself! In fact, the error feedback from the compiler can even be used to guide repairs. When put together, we can get a robust process for getting well-typed responses that our apps can further massage, validate with a user, etc. In other words, **types are all you need**. ## Enter TypeChat The technique of combining a human prompt and a "response schema" is not necessarily unique — but it is promising. And as we've focused on translating user intent to structured data, we've found that TypeScript is very well-suited for the task. We'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. [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. Here's the basic code to hook TypeChat up to an LLM and decide if a sentence is negative, neutral, or positive. ```ts // ./src/sentimentSchema.ts // The following is a schema definition for determining the sentiment of a some user input. export interface SentimentResponse { /** The sentiment of the text. */ sentiment: "negative" | "neutral" | "positive"; } ``` ```ts // ./src/main.ts import * as fs from "fs"; import * as path from "path"; import dotenv from "dotenv"; import * as typechat from "typechat"; import { SentimentResponse } from "./sentimentSchema"; // Load environment variables. dotenv.config({ path: path.join(__dirname, "../.env") }); // Create a language model based on the environment variables. const model = typechat.createLanguageModel(process.env); // Load up the contents of our "Response" schema. const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8"); const translator = typechat.createJsonTranslator(model, schema, "SentimentResponse"); // Process requests interactively. typechat.processRequests("😀> ", /*inputFile*/ undefined, async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } console.log(`The sentiment is ${response.data.sentiment}`); }); ``` TypeChat can be used in a number of different ways. The way we've discussed here so far is all about using a "data schema" to turn some user intent into a structured response; however, TypeChat also makes it possible to use an "API schema" to construct basic programs. We have some [docs](/docs/) and [examples](/docs/examples/) to get a sense of the different ways you can use TypeChat. ## Open and Pluggable First of all, TypeChat is open-source. We'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. Second, TypeChat is built in a way that is meant to be model-neutral. While 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. ## Try It Today! We'd love to know if TypeChat is something that's useful and interests you! As we mentioned, we'll be welcoming you on [GitHub](https://github.com/Microsoft/TypeChat) if you have any question, suggestions, and more. Happy Hacking! ================================================ FILE: site/src/css/noscript-styles.css ================================================ .typechat-hero button { display: none !important; } .typechat-docs-smol-nav { display: none !important; } ================================================ FILE: site/src/css/styles.css ================================================ .skip-to-main { position: absolute; opacity: 0; z-index: -999999999; margin: 0 auto; padding: 2rem 0; background-color: #000; color: #fff; top: 0; left: 0; width: 100%; height: 60px; text-align: center; } .skip-to-main:focus { opacity: 1; z-index: 999999999; } .with-sidebar { display: flex; flex-wrap: wrap; gap: 2rem; } .with-sidebar > :first-child { flex-basis: 140px; flex-grow: 1; } .with-sidebar > :last-child { flex-basis: 0; flex-grow: 999; min-inline-size: 50%; } :root { --typechat-monospace: Consolas, Menlo, Monaco, Roboto, monospace; --typechat-inline-code-color: #a10615; --typechat-rounding-radius: 0.5rem; } .typechat-cap-content-width { max-width: 1000px; } .typechat-hero .typechat-code-copy { background-color: #212529; color: #fff; font-style: var(--typechat-monospace); border-radius: var(--typechat-rounding-radius); padding: 0.75rem 1rem; text-align: center; } .typechat-hero .typechat-code-copy code { background-color: inherit; color: inherit; } .typechat-hero .typechat-code-copy button { height: 100%; width: fit-content; right: 0; top: 0; border: none; /* border-radius: var(--typechat-rounding-radius) 0 0 var(--typechat-rounding-radius); */ border-radius: 0 var(--typechat-rounding-radius) var(--typechat-rounding-radius) 0; } .typechat-prose-content :is(pre, blockquote) { padding: 1rem; border-radius: 0.5rem; box-shadow: 0px 2px 5px #666; } .typechat-prose-content blockquote { background-color: #f5f8fa; } .typechat-prose-content pre:focus { outline: 3px solid #0078d4; outline-offset: 2px; } .typechat-prose-content code { font-family: var(--typechat-monospace); color: var(--typechat-inline-code-color); font-size: inherit; } .typechat-prose-content :not(pre) code { word-break: break-all; } .typechat-prose-content blockquote > *:last-child { margin-bottom: 0; } .typechat-prose-content :not(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) { margin-top: 1rem; } .typechat-prose-content table { margin-bottom: 1rem; border-collapse: collapse; } .typechat-prose-content td, th { border: 1px solid #666; border-left: 0; border-right: 0; padding: 0.5rem; } .typechat-prose-content th { border-top: 0; } ================================================ FILE: site/src/docs/examples.md ================================================ --- layout: doc-page title: Examples --- To see TypeChat in action, check out the examples found in [`/typescript/examples`](https://github.com/microsoft/TypeChat/tree/main/typescript/examples). Each 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. We are working to reproduce outputs with other models. Generally, models trained on both code and natural language text have high accuracy. We recommend reading each example in the following order. | Name | Description | | ---- | ----------- | | [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!" | | [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. | [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. | | [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. | | [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. | | [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. | ## Step 1: Configure development environment ### Option 1: Local Machine You can experiment with these TypeChat examples on your local machine with just Node.js. Ensure [Node.js (18.16.0 LTS or newer)](https://nodejs.org/en) or newer is installed. ``` git clone https://github.com/microsoft/TypeChat cd TypeChat/typescript npm install ``` ### Option 2: GitHub Codespaces GitHub Codespaces enables you to try TypeChat quickly in a development environment hosted in the cloud. On the TypeChat repository page: 1. Click the green button labeled `<> Code` 2. Select the `Codespaces` tab. 3. Click the green `Create codespace` button.
If this is your first time creating a codespace, read this. If 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. Once 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. Remember 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. For more information, see the [GitHub Codespaces Overview](https://docs.github.com/en/codespaces/overview)
## Step 2: Build TypeChat Examples Build TypeChat and the examples by running the following command in the repository root: ``` npm run build-all ``` ## Step 3: Configure environment variables Currently, the examples are running on OpenAI or Azure OpenAI endpoints. To use an OpenAI endpoint, include the following environment variables: | Variable | Value | |----------|-------| | `OPENAI_MODEL`| The OpenAI model name (e.g. `gpt-3.5-turbo` or `gpt-4`) | | `OPENAI_API_KEY` | Your OpenAI API key | To use an Azure OpenAI endpoint, include the following environment variables: | Variable | Value | |----------|-------| | `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`) | | `AZURE_OPENAI_API_KEY` | Your Azure OpenAI API key | We recommend setting environment variables by creating a `.env` file in the root directory of the project that looks like the following: ``` # For OpenAI OPENAI_MODEL=... OPENAI_API_KEY=... # For Azure OpenAI AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=... ``` ## Step 4: Run the examples Examples can be found in the `typescript/examples` directory. To 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 F5 to launch it in debug mode. Note 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. To run an example with one of these input files, run `node ./dist/main.js `. For example, in the `coffeeShop` directory, you can run: ``` node ./dist/main.js ./dist/input.txt ``` ================================================ FILE: site/src/docs/faq.md ================================================ --- layout: doc-page title: Frequently Asked Questions (FAQ) --- ### What is TypeChat? TypeChat 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. After defining your types, TypeChat takes care of the rest by: 1. Constructing a prompt to the LLM using types. 2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction. 3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent. Types are all you need! ### Why is TypeChat useful? If 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. TypeChat 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. ### What are the benefits of using TypeChat? TypeChat was created with the purpose of increasing safety in natural language interfaces. We believe TypeChat has three key primary benefits when working with large language models: 1. Accurate: Large language models do a great job matching user intent to scoped types. TypeChat's validation and repair cleans up the rest! 2. Approachable: No more prompt engineering! Types are all you need. You probably have them already lying around. 3. Safety: Types constrain domain and model uncertainty. Repeating back the instance confirms that it aligns with user intent before taking action. ### How does TypeChat work? How does TypeChat relate to TypeScript? TypeChat 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. Once 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. ### How reliable is TypeChat? TypeChat 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. ### What languages does TypeChat support? Currently 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. ================================================ FILE: site/src/docs/index.njk ================================================ --- title: Docs --- {% set firstDoc = docsTOC[0].pages[0] %} {% if firstDoc %} Redirecting to {{firstDoc.title}} {%endif%} {% include "header-prologue.njk" %} {% include "footer.njk" %} ================================================ FILE: site/src/docs/introduction.md ================================================ --- layout: doc-page title: Introduction --- Large language models (or LLMs) can give us surprisingly rich answers to requests. Maybe you've tried chatting with a language model and been delighted at how effective it is to brainstorm with it. > **User:** > > It's rainy in Seattle (surprise!). Got 3 quick suggestions for what I should do this weekend? Keep it short and direct. > > **ChatGPT:** > > 1. Visit the Seattle Art Museum (SAM) for a day of artistic inspiration and exploration. > 2. Enjoy a cozy afternoon at a local coffee shop, sipping your favorite brew and diving into a good book. > 3. Embrace the rainy weather by taking a scenic drive to Snoqualmie Falls and witness the majestic waterfall in all its splendor. These models are great chat assistants on their own, but can they be used to add natural language interfaces to an application? The answer as many of us have seen is "yes - sort of", and it really depends on how we ask the model. ## Adding Structure Remember, by default these models respond in *natural language* which is how we read and write today; but traditional programs are kind of bad at working with these answers. Most software expects to work off of a level of structure over bytes, characters, JSON blobs, etc. — not natural languages like English. When we don't have structure, we have to parse out what we're interested in, and it might not always be practical. One way to add some structure and make parsing easier might be to ask for a list of answers: > **User:** > > Provide 3 suggestions for specific places to go to in Seattle on a rainy day. > Respond in the form of a bulleted list. > Be succinct. > > **ChatGPT:** > > - Seattle Art Museum: Explore the extensive art collection and enjoy the indoor exhibits. > - Chihuly Garden and Glass: Admire the stunning glass artwork of Dale Chihuly in a covered garden and gallery. > - Pacific Science Center: Engage in interactive exhibits, planetarium shows, and IMAX films for a day of educational fun. Maybe a bulleted list is enough to go off of, but how would we break this data up? Strip the leading bullet points and split on `": "` to separate the venue from its description? What if a venue has a `": "` in its name? It turns out that we're still left with problems parsing out natural language. Now what if we asked the model for a much more structured format like JSON? As a matter of fact, many language models are great at generating JSON! > **User:** > 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 > > ```json > [ > { "venue": "...", "description": "..." }, > { "venue": "...", "description": "..." } > ] > ``` > **ChatGPT:** > ```json > [ > { > "venue": "Seattle Art Museum", > "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." > }, > { > "venue": "Chihuly Garden and Glass", > "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." > }, > { > "venue": "Museum of Pop Culture", > "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." > } > ] > ``` Here we asked a model to respond directly with JSON code and provided a sort of "template" or example response to guide its answer. As you can see, it worked pretty well! Example responses are great, but we wondered if there was another approach. In our case, we asked whether we could simply guide a model with TypeScript types. > **User:** > 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: > > ```ts > interface Response { > data: Array<{ venue: string, description: string }>; > } > ``` > > **ChatGPT:** > ```json > { > "data": [ > { > "venue": "Museum of Pop Culture", > "description": "Explore the fascinating world of popular culture through interactive exhibits, music memorabilia, and multimedia displays at the Museum of Pop Culture." > }, > { > "venue": "Seattle Art Museum", > "description": "Indulge in the diverse collection of artworks from around the world at the Seattle Art Museum, featuring both contemporary and classical pieces." > }, > { > "venue": "Pike Place Market", > "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." > } > ] > } > ``` This works fairly well! And despite being conceptually simple, it's a very powerful way to bridge the gap between natural language and something our applications can work with. But 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. This is the key idea behind TypeChat. By 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. ## Introducing TypeChat TypeChat makes it easy to build natural language interfaces using types. Simply 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. After defining your types, TypeChat takes care of the rest by: 1. Constructing a prompt to the LLM using types. 2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction. 3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent. Types are all you need! ================================================ FILE: site/src/docs/python/basic-usage.md ================================================ --- layout: doc-page title: Basic Python Usage --- TypeChat is currently a small library, so we can get a solid understanding just by going through the following example: ```py import asyncio import sys from dotenv import dotenv_values from typechat import (Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests) import schema as sentiment # See below for what's in schema.py. async def main(): env_vals = dotenv_values() model = create_language_model(env_vals) validator = TypeChatValidator(sentiment.Sentiment) translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment) async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: result = result.value print(f"The sentiment is {result['sentiment']}") filename = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("😀> ", filename, request_handler) asyncio.run(main()) ``` Let's break it down step-by-step. ## Providing a Model TypeChat can be used with any language model. As long as you have a class with the following shape... ```py class TypeChatLanguageModel(Protocol): async def complete(self, prompt: str | list[PromptSection]) -> Result[str]: """ Represents a AI language model that can complete prompts. TypeChat uses an implementation of this protocol to communicate with an AI service that can translate natural language requests to JSON instances according to a provided schema. The `create_language_model` function can create an instance. """ ... ``` then you should be able to try TypeChat out with such a model. The key thing here is providing a `complete` method. `complete` is just a function that takes a `string` and eventually returns a string (wrapped in a `Result`) if all goes well. For convenience, TypeChat provides two functions out of the box to connect to the OpenAI API and Azure's OpenAI Services. You can call these directly. ```py def create_openai_language_model( api_key: str, model: str, endpoint: str = "https://api.openai.com/v1/chat/completions", org: str = "" ): ... def create_azure_openai_language_model(api_key: str, endpoint: str): ... ``` For even more convenience, TypeChat also provides a function to infer whether you're using OpenAI or Azure OpenAI. ```ts def create_language_model( vals: dict[str, str | None] ) -> TypeChatLanguageModel: ... ``` With `create_language_model`, you can populate your environment variables and pass them in. Based on whether `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` is set, you'll get a model of the appropriate type. The `TypeChatLanguageModel` returned by these functions has a few writable attributes you might find useful: - `max_retry_attempts` - `retry_pause_seconds` - `timeout_seconds` Though note that these are unstable. Regardless of how you decide to construct your model, it is important to avoid committing credentials directly in source. One 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`. You can use a library like [`python-dotenv`](https://pypi.org/project/python-dotenv/) to help load these up. ```py from dotenv import load_dotenv load_dotenv() // ... import typechat model = typechat.create_language_model(os.environ) ``` ## Defining and Loading the Schema TypeChat describes types to language models to help guide their responses. To 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. Here's what our schema file `schema.py` look like: ```py from dataclasses import dataclass from typing import Literal @dataclass class Sentiment: """ The following is a schema definition for determining the sentiment of a some user input. """ sentiment: Literal["negative", "neutral", "positive"] ``` Here, we're saying that the `sentiment` attribute has to be one of three possible strings: `negative`, `neutral`, or `positive`. We did this with [the `typing.Literal` hint](https://docs.python.org/3/library/typing.html#typing.Literal). We 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`. If we declared `Sentiment` as a `TypedDict`, TypeChat would provide us with a `dict`. That would mean that to access the value of `sentiment`, we would have to write `value["sentiment"]`. Note 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. TypeChat also understands constructs like `Annotated` and `Doc` to add comments to individual attributes. ## Creating a Validator A validator really has two jobs generating a textual schema for language models, and making sure any data fits a given shape. The built-in validator looks roughly like this: ```py class TypeChatValidator(Generic[T]): """ Validates an object against a given Python type. """ def __init__(self, py_type: type[T]): """ Args: py_type: The schema type to validate against. """ ... def validate_object(self, obj: object) -> Result[T]: """ Validates the given Python object according to the associated schema type. Returns a `Success[T]` object containing the object if validation was successful. Otherwise, returns a `Failure` object with a `message` property describing the error. """ ... ``` To construct a validator, we just have to pass in the type we defined: ```py import schema as sentiment validator = TypeChatValidator(sentiment.Sentiment) ``` ## Creating a JSON Translator A `TypeChatJsonTranslator` brings all these concepts together. A translator takes a language model, a validator, and our expected type, and provides a way to translate some user input into objects following our schema. To do so, it crafts a prompt based on the schema, reaches out to the model, parses out JSON data, and attempts validation. Optionally, it will craft repair prompts and retry if validation fails. ```py translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment) ``` When we are ready to translate a user request, we can call the `translate` method. ```ts translator.translate("Hello world! 🙂"); ``` We'll come back to this. ## Creating a "REPL" TypeChat exports a `process_requests` function that makes it easy to experiment with TypeChat. Depending on its second argument, it either creates an interactive command line (if given `None`), or reads lines from the given a file path. ```ts async def request_handler(message: str): ... filename = sys.argv[1] if len(sys.argv) == 2 else None await process_requests("😀> ", filename, request_handler) ``` `process_requests` takes 3 things. First, there's the prompt string - this is what a user will see before their own input in interactive scenarios. You can make this playful. We like to use emoji here. 😄 Next, we take a text file name. Input strings will be read from this file one line at a time. If the file name was `None`, `process_requests` will work on standard input and provide an interactive prompt (assuming `sys.stdin.isatty()` is true). By checking `sys.argv`, our script makes our program interactive unless the person running the program provided an input file as a command line argument (e.g. `python ./example.py inputFile.txt`). Finally, there's the request handler. We'll fill that in next. ## Translating Requests Our handler receives some user input (the `message` string) each time it's called. It's time to pass that string into over to our `translator` object. ```ts async def request_handler(message: str): result = await translator.translate(message) if isinstance(result, Failure): print(result.message) else: print(f"The sentiment is {result.value.sentiment}") ``` We're calling the `translate` method on each string and getting a response. If something goes wrong, TypeChat will retry requests up to a maximum specified by `retry_max_attempts` on our `model`. However, if the initial request as well as all retries fail, `result` will be a `typechat.Failure` and we'll be able to grab a `message` explaining what went wrong. In the ideal case, `result` will be a `typechat.Success` and we'll be able to access our well-typed `value` property! This will correspond to the type that we passed in when we created our translator object (i.e. `Sentiment`). That's it! You should now have a basic idea of TypeChat's APIs and how to get started with a new project. 🎉 ================================================ FILE: site/src/docs/techniques.md ================================================ --- layout: doc-page title: Techniques --- This document defines techniques for working with TypeChat. ### Schema Engineering TypeChat 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. To 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_. To maximize success with TypeChat, we recommend the following best practices when defining Response Model types: * Keep it simple (primitives, arrays, and objects). * Only use types that are representable as JSON (i.e. no classes). * Make data structures as flat and regular as possible. * Include comments on types and properties that describe intent in natural language. * Restrict use of generics. * Avoid deep inheritance hierarchies. * Don't use conditional, mapped, and indexed access types. * Allow room for LLMs to color slightly outside the lines (e.g. use `string` instead of literal types). * Include an escape hatch to suppress hallucinations. The 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). However, 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. ================================================ FILE: site/src/docs/typescript/basic-usage.md ================================================ --- layout: doc-page title: Basic TypeScript Usage --- TypeChat is currently a small library, so we can get a solid understanding just by understanding the following example: ```ts import fs from "fs"; import path from "path"; import { createJsonTranslator, createLanguageModel } from "typechat"; import { processRequests } from "typechat/interactive"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { SentimentResponse } from "./sentimentSchema"; // Create a model. const model = createLanguageModel(process.env); // Create a validator. const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8"); const validator = createTypeScriptJsonValidator(schema, "SentimentResponse"); // Create a translator. const translator = createJsonTranslator(model, validator); // Process requests interactively or from the input file specified on the command line processRequests("😀> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } console.log(`The sentiment is ${response.data.sentiment}`); }); ``` Let's break it down step-by-step. ## Providing a Model TypeChat can be used with any language model. As long as you can construct an object with the following properties: ```ts export interface TypeChatLanguageModel { /** * Optional property that specifies the maximum number of retry attempts (the default is 3). */ retryMaxAttempts?: number; /** * Optional property that specifies the delay before retrying in milliseconds (the default is 1000ms). */ retryPauseMs?: number; /** * Obtains a completion from the language model for the given prompt. * @param prompt The prompt string. */ complete(prompt: string): Promise>; } ``` then you should be able to try TypeChat out with such a model. The key thing here is that only `complete` is required. `complete` is just a function that takes a `string` and eventually returns a `string` if all goes well. For convenience, TypeChat provides two functions out of the box to connect to the OpenAI API and Azure's OpenAI Services. You can call these directly. ```ts export function createOpenAILanguageModel(apiKey: string, model: string, endPoint? string): TypeChatLanguageModel; export function createAzureOpenAILanguageModel(apiKey: string, endPoint: string): TypeChatLanguageModel; ``` For even more convenience, TypeChat also provides a function to infer whether you're using OpenAI or Azure OpenAI. ```ts export function createLanguageModel(env: Record): TypeChatLanguageModel ``` With `createLanguageModel`, you can populate your environment variables and pass them in. Based on whether `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` is set, you'll get a model of the appropriate type. Regardless, of how you decide to construct your model, it is important to avoid committing credentials directly in source. One 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`. You can use a library like [`dotenv`](https://www.npmjs.com/package/dotenv) to help load these up. ```ts import dotenv from "dotenv"; dotenv.config(/*...*/); // ... import * as typechat from "typechat"; const model = typechat.createLanguageModel(process.env); ``` ## Defining and Loading the Schema TypeChat describes types to language models to help guide their responses. In this case, we are using a `TypeScriptJsonValidator` which uses the TypeScript compiler to validate data against a set of types. That means that we'll be writing out the types of the data we expect to get back in a `.ts` file. Here's what our schema file `sentimentSchema.ts` look like: ```ts // The following is a schema definition for determining the sentiment of a some user input. export interface SentimentResponse { sentiment: "negative" | "neutral" | "positive"; // The sentiment of the text } ``` It also means we will need to manually load up an input `.ts` file verbatim. ```ts // Load up the type from our schema. import type { SentimentResponse } from "./sentimentSchema"; // Load up the schema file contents. const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8"); ``` Note: 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. This introduces some complications to certain kinds of builds, since our input files need to be treated as local assets. One 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. Another is to use a utility like [`copyfiles`](https://www.npmjs.com/package/copyfiles) to move specific schema files to the output directory. If you're using a bundler, there might be custom way to import a file as a raw string as well. Regardless, [our examples](https://github.com/microsoft/TypeChat/tree/main/typescript/examples) should work with either of the first two options. Alternatively, 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. Here's what our schema would look like if we went down that path. ```ts import { z } from "zod"; export const SentimentResponse = z.object({ sentiment: z.enum(["negative", "neutral", "positive"]).describe("The sentiment of the text") }); export const SentimentSchema = { SentimentResponse }; ``` ## Creating a Validator A validator really has two jobs generating a textual schema for language models, and making sure any data fits a given shape. The interface looks roughly like this: ```ts /** * An object that represents a TypeScript schema for JSON objects. */ export interface TypeChatJsonValidator { /** * Return a string containing TypeScript source code for the validation schema. */ getSchemaText(): string; /** * Return the name of the JSON object target type in the schema. */ getTypeName(): string; /** * Validates the given JSON object according to the associated TypeScript schema. Returns a * `Success` object containing the JSON object if validation was successful. Otherwise, returns * an `Error` object with a `message` property describing the error. * @param jsonText The JSON object to validate. * @returns The JSON object or an error message. */ validate(jsonObject: object): Result; } ``` In 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. TypeChat ships with two validators. ### `TypeScriptJsonValidator` A `TypeScriptJsonValidator` operates off of TypeScript text files. To create one, we have to import `createTypeScriptJsonValidator` out of `typechat/ts`: ```ts import { createTypeScriptJsonValidator } from "typechat/ts"; ``` We'll also need to actually import the type from our schema. ```ts import { SentimentResponse } from "./sentimentSchema"; ``` With our schema text and this type, we have enough to create a validator: ```ts const validator = createTypeScriptJsonValidator(schema, "SentimentResponse"); ``` We provided the text of the schema and the name of the type we want returned data to satisfy. We 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). ### Zod Validators If you chose to define your schema with Zod, you can use the `createZodJsonValidator` function: ```ts import { createZodJsonValidator } from "typechat/zod"; ``` Instead 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: ```ts export const MyType = z.object(/*...*/); export const MyOtherType = z.object(/*...*/); export let myObj = { MyType, MyOtherType, } ``` From above, that was just `SentimentSchema`: ```ts export const SentimentSchema = { SentimentResponse }; ``` So we'll need to import that object... ```ts import { SentimentSchema } from "./sentimentSchema"; ``` and provide it, along with our expected type name, to `createZodJsonValidator`: ```ts const validator = createZodJsonValidator(SentimentSchema, "SentimentResponse"); ``` ## Creating a JSON Translator A `TypeChatJsonTranslator` brings these together. ```ts import { createJsonTranslator } from "typechat"; ``` A translator takes both a model and a validator, and provides a way to translate some user input into objects following our schema. To do so, it crafts a prompt based on the schema, reaches out to the model, parses out JSON data, and attempts validation. Optionally, it will craft repair prompts and retry if validation fails. ```ts const translator = createJsonTranslator(model, validator); ``` When we are ready to translate a user request, we can call the `translate` method. ```ts translator.translate("Hello world! 🙂"); ``` We'll come back to this. ## Creating the Prompt TypeChat exports a `processRequests` function that makes it easy to experiment with TypeChat. We need to import it from `typechat/interactive`. ```ts import { processRequests } from "typechat/interactive"; ``` It either creates an interactive command line prompt, or reads lines in from a file. ```ts typechat.processRequests("😀> ", process.argv[2], async (request) => { // ... }); ``` `processRequests` takes 3 things. First, there's the prompt prefix - this is what a user will see before their own text in interactive scenarios. You can make this playful. We like to use emoji here. 😄 Next, we take a text file name. Input strings will be read from this file on a per-line basis. If the file name was `undefined`, `processRequests` will work on standard input and provide an interactive prompt. Using `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`). Finally, there's the request handler. We'll fill that in next. ## Translating Requests Our handler receives some user input (the `request` string) each time it's called. It's time to pass that string into over to our `translator` object. ```ts typechat.processRequests("😀> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } console.log(`The sentiment is ${response.data.sentiment}`); }); ``` We're calling the `translate` method on each string and getting a response. If something goes wrong, TypeChat will retry requests up to a maximum specified by `retryMaxAttempts` on our `model`. However, 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. In the ideal case, `response.success` will be `true` and we'll be able to access our well-typed `data` property! This will correspond to the type that we passed in when we created our translator object (i.e. `SentimentResponse`). That's it! You should now have a basic idea of TypeChat's APIs and how to get started with a new project. 🎉 ================================================ FILE: site/src/index.njk ================================================ --- layout: base ---

TypeChat

TypeChat helps get well-typed responses from language models to build pragmatic natural language interfaces.

All powered through your types.

Docs GitHub
npm install typechat
================================================ FILE: site/src/js/interactivity.js ================================================ // @ts-check { /** @type {any} */ let lastTimeout; /** @type {HTMLButtonElement | null} */ const copyButton = document.querySelector(".typechat-code-copy button"); copyButton?.addEventListener("click", async () => { clearTimeout(lastTimeout); try { await navigator.clipboard?.writeText("npm install typechat"); copyButton.textContent = "✅"; copyButton.title = copyButton.ariaLabel = "Command copied." } catch { copyButton.textContent = "❌"; copyButton.title = copyButton.ariaLabel = "Error copying." } lastTimeout = setTimeout(() => { copyButton.textContent = "📋"; copyButton.title = copyButton.ariaLabel = "Copy 'npm install' command." }, 1500); }); } { const selectElements = /** @type {HTMLCollectionOf} */ (document.getElementsByClassName("nav-on-change")); for (const select of selectElements) { const change = () => { window.location.pathname = select.value; }; select.onchange = change; // if (select.options.length === 1 && window.location.pathname !== select.value) { // change(); // } } } ================================================ FILE: typescript/.gitignore ================================================ build/ dist/ out/ node_modules/ .env *.map *.out.txt *.bat # Copied at publish time from repo root SECURITY.md # Local development and debugging .scratch/ **/.vscode/* **/tsconfig.debug.json !**/.vscode/launch.json **/build.bat ================================================ FILE: typescript/.npmignore ================================================ src/ examples/ node_modules/ *.map *.ts !*.d.ts tsconfig.json .gitignore ================================================ FILE: typescript/LICENSE ================================================ MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: typescript/README.md ================================================ # TypeChat TypeChat is a library that makes it easy to build natural language interfaces using types. Building 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. TypeChat replaces _prompt engineering_ with _schema engineering_. Simply 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. After defining your types, TypeChat takes care of the rest by: 1. Constructing a prompt to the LLM using types. 2. Validating the LLM response conforms to the schema. If the validation fails, repair the non-conforming output through further language model interaction. 3. Summarizing succinctly (without use of a LLM) the instance and confirm that it aligns with user intent. Types are all you need! # Getting Started Install TypeChat: ```sh npm install typechat ``` You can also build TypeChat from source: ```sh git clone https://github.com/microsoft/TypeChat cd TypeChat/typescript npm install npm run build ``` To 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. To learn more about TypeChat, visit the [documentation](https://microsoft.github.io/TypeChat) which includes more information on TypeChat and how to get started. ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ================================================ FILE: typescript/examples/README.md ================================================ To see TypeChat in action, check out the examples found in this directory. Each 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. We are working to reproduce outputs with other models. Generally, models trained on both code and natural language text have high accuracy. We recommend reading each example in the following order. | Name | Description | | ---- | ----------- | | [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!" | | [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. | [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. | | [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. | | [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. | | [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. | ## Step 1: Configure your development environment ### Option 1: Local Machine You can experiment with these TypeChat examples on your local machine with just Node.js. Ensure [Node.js (18.16.0 LTS or newer)](https://nodejs.org/en) or newer is installed. ``` git clone https://github.com/microsoft/TypeChat cd TypeChat/typescript npm install ``` ### Option 2: GitHub Codespaces GitHub Codespaces enables you to try TypeChat quickly in a development environment hosted in the cloud. On the TypeChat repository page: 1. Click the green button labeled `<> Code` 2. Select the `Codespaces` tab. 3. Click the green `Create codespace` button.
If this is your first time creating a codespace, read this. If 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. Once 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. Remember 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. For more information, see the [GitHub Codespaces Overview](https://docs.github.com/en/codespaces/overview)
## Step 2: Build TypeChat Samples Build TypeChat and the examples by running the following command in the repository root: ```sh npm run build-all ``` ## Step 3: Configure environment variables Currently, the examples are running on OpenAI or Azure OpenAI endpoints. To use an OpenAI endpoint, include the following environment variables: | Variable | Value | |----------|-------| | `OPENAI_MODEL`| The OpenAI model name (e.g. `gpt-3.5-turbo` or `gpt-4`) | | `OPENAI_API_KEY` | Your OpenAI API key | | `OPENAI_ENDPOINT` | OpenAI API Endpoint - *optional*, defaults to `"https://api.openai.com/v1/chat/completions"` | | `OPENAI_ORGANIZATION` | OpenAI Organization - *optional*, defaults to `""` | To use an Azure OpenAI endpoint, include the following environment variables: | Variable | Value | |----------|-------| | `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`) | | `AZURE_OPENAI_API_KEY` | Your Azure OpenAI API key | We recommend setting environment variables by creating a `.env` file in the root directory of the project that looks like the following: ```ini # For OpenAI OPENAI_MODEL=... OPENAI_API_KEY=... # For Azure OpenAI AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=... ``` ## Step 4: Run the examples Examples can be found in the `examples` directory. To 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 F5 to launch it in debug mode. Note 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. To run an example with one of these input files, run `node ./dist/main.js `. For example, in the `coffeeShop` directory, you can run: ```sh node ./dist/main.js ./dist/input.txt ``` ================================================ FILE: typescript/examples/calendar/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "/**" ], "program": "${workspaceFolder}/dist/main.js", "console": "externalTerminal" } ] } ================================================ FILE: typescript/examples/calendar/README.md ================================================ # Calendar The 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. # Try Calendar To run the Calendar example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For example, we could use natural language to describe an event coming up soon: **Input**: ``` 📅> I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024 ``` **Output**: ```json { "actions": [ { "actionType": "add event", "event": { "day": "Friday March 15, 2024", "timeRange": { "startTime": "12:00 pm", "endTime": "2:00 pm" }, "description": "get my tires changed" } } ] } ``` ================================================ FILE: typescript/examples/calendar/package.json ================================================ { "name": "calendar", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/calendar/src/calendarActionsSchema.ts ================================================ // The following types define the structure of an object of type CalendarActions that represents a list of requested calendar actions export type CalendarActions = { actions: Action[]; }; export type Action = | AddEventAction | RemoveEventAction | AddParticipantsAction | ChangeTimeRangeAction | ChangeDescriptionAction | FindEventsAction | UnknownAction; export type AddEventAction = { actionType: 'add event'; event: Event; }; export type RemoveEventAction = { actionType: 'remove event'; eventReference: EventReference; }; export type AddParticipantsAction = { actionType: 'add participants'; // event to be augmented; if not specified assume last event discussed eventReference?: EventReference; // new participants (one or more) participants: string[]; }; export type ChangeTimeRangeAction = { actionType: 'change time range'; // event to be changed eventReference?: EventReference; // new time range for the event timeRange: EventTimeRange; }; export type ChangeDescriptionAction = { actionType: 'change description'; // event to be changed eventReference?: EventReference; // new description for the event description: string; }; export type FindEventsAction = { actionType: 'find events'; // one or more event properties to use to search for matching events eventReference: EventReference; }; // if the user types text that can not easily be understood as a calendar action, this action is used export interface UnknownAction { actionType: 'unknown'; // text typed by the user that the system did not understand text: string; } export type EventTimeRange = { startTime?: string; endTime?: string; duration?: string; }; export type Event = { // date (example: March 22, 2024) or relative date (example: after EventReference) day: string; timeRange: EventTimeRange; description: string; location?: string; // a list of people or named groups like 'team' participants?: string[]; }; // properties used by the requester in referring to an event // these properties are only specified if given directly by the requester export type EventReference = { // date (example: March 22, 2024) or relative date (example: after EventReference) day?: string; // (examples: this month, this week, in the next two days) dayRange?: string; timeRange?: EventTimeRange; description?: string; location?: string; participants?: string[]; }; ================================================ FILE: typescript/examples/calendar/src/expectedOutput.txt ================================================ I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024 Valid instance: { "actions": [ { "actionType": "add event", "event": { "day": "March 15, 2024", "timeRange": { "startTime": "12:00", "endTime": "2:00" }, "description": "get tires changed" } } ] } Search for any meetings with Gavin this week Valid instance: { "actions": [ { "actionType": "find events", "eventReference": { "dayRange": "this week", "participants": [ "Gavin" ] } } ] } Set up an event for friday named Jeffs pizza party at 6pm Valid instance: { "actions": [ { "actionType": "add event", "event": { "day": "friday", "timeRange": { "startTime": "6pm" }, "description": "Jeffs pizza party" } } ] } Please add Jennifer to the scrum next Thursday Valid instance: { "actions": [ { "actionType": "add participants", "eventReference": { "day": "next Thursday", "description": "scrum" }, "participants": [ "Jennifer" ] } ] } Will you please add an appointment with Jerri Skinner at 9 am? I need it to last 2 hours Valid instance: { "actions": [ { "actionType": "add event", "event": { "day": "today", "timeRange": { "startTime": "9 am", "duration": "2 hours" }, "description": "appointment with Jerri Skinner" } } ] } Do I have any plan with Rosy this month? Valid instance: { "actions": [ { "actionType": "find events", "eventReference": { "dayRange": "this month", "participants": [ "Rosy" ] } } ] } I 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 Valid instance: { "actions": [ { "actionType": "add event", "event": { "day": "Monday", "timeRange": { "startTime": "10am" }, "description": "meeting with boss" } }, { "actionType": "add event", "event": { "day": "tomorrow", "timeRange": { "startTime": "3pm" }, "description": "appointment with Sally, May, and Boris", "participants": [ "Sally", "May", "Boris" ] } }, { "actionType": "add participants", "participants": [ "Jesse", "Abby" ] }, { "actionType": "change time range", "timeRange": { "duration": "90 minutes" } } ] } Add meeting with team today at 2 Valid instance: { "actions": [ { "actionType": "add event", "event": { "day": "today", "timeRange": { "startTime": "2:00", "duration": "1 hour" }, "description": "meeting with team", "participants": [ "team" ] } } ] } can you record lunch with Luis at 12pm on Friday and also add Isobel to the Wednesday ping pong game at 4pm Error: JSON instance does not match schema Type 'null' is not assignable to type 'string | undefined'. Type 'null' is not assignable to type 'string | undefined'. Type 'null' is not assignable to type 'string[] | undefined'. Valid instance: { "actions": [ { "actionType": "add event", "event": { "day": "Friday", "timeRange": { "startTime": "12pm", "duration": "1h" }, "description": "lunch with Luis", "participants": [ "Luis" ] } }, { "actionType": "add participants", "eventReference": { "day": "Wednesday", "timeRange": { "startTime": "4pm", "duration": "1h" }, "description": "ping pong game" }, "participants": [ "Isobel" ] } ] } I 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 Valid instance: { "actions": [ { "actionType": "add event", "event": { "day": "today", "timeRange": { "startTime": "2:00 pm", "duration": "1 hour" }, "description": "meeting with Jenny", "participants": [ "Jenny" ] } }, { "actionType": "add event", "event": { "day": "today", "timeRange": { "startTime": "3:30 pm", "duration": "1 hour" }, "description": "dry cleaner" } }, { "actionType": "add event", "event": { "day": "today", "timeRange": { "startTime": "4:30 pm", "duration": "1 hour" }, "description": "soccer game" } } ] } ================================================ FILE: typescript/examples/calendar/src/input.txt ================================================ I need to get my tires changed from 12:00 to 2:00 pm on Friday March 15, 2024 Search for any meetings with Gavin this week Set up an event for friday named Jeffs pizza party at 6pm Please add Jennifer to the scrum next Thursday Will you please add an appointment with Jerri Skinner at 9 am? I need it to last 2 hours Do I have any plan with Rosy this month? I 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 Add meeting with team today at 2 can you record lunch with Luis at 12pm on Friday and also add Isobel to the Wednesday ping pong game at 4pm I 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 ================================================ FILE: typescript/examples/calendar/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createJsonTranslator, createLanguageModel } from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { processRequests } from "typechat/interactive"; import { CalendarActions } from './calendarActionsSchema'; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "calendarActionsSchema.ts"), "utf8"); const validator = createTypeScriptJsonValidator(schema, "CalendarActions"); const translator = createJsonTranslator(model, validator); //translator.stripNulls = true; // Process requests interactively or from the input file specified on the command line processRequests("📅> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } const calendarActions = response.data; console.log(JSON.stringify(calendarActions, undefined, 2)); if (calendarActions.actions.some(item => item.actionType === "unknown")) { console.log("I didn't understand the following:"); for (const action of calendarActions.actions) { if (action.actionType === "unknown") console.log(action.text); } return; } }); ================================================ FILE: typescript/examples/calendar/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/examples/coffeeShop/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "/**" ], "program": "${workspaceFolder}/dist/main.js", "console": "externalTerminal" } ] } ================================================ FILE: typescript/examples/coffeeShop/README.md ================================================ # Coffee Shop The Coffee Shop example shows how to capture user intent as a set of "nouns". In this case, the nouns are items in a coffee order, where valid items are defined starting from the [`Cart`](./src/coffeeShopSchema.ts) type. This 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). # Try Coffee Shop To run the Coffee Shop example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt) and [`src/input2.txt`](./src/input2.txt). For example, we could use natural language to describe our coffee shop order: **Input**: ``` ☕> we'd like a cappuccino with a pack of sugar ``` **Output**: ```json { "items": [ { "type": "lineitem", "product": { "type": "LatteDrinks", "name": "cappuccino", "options": [ { "type": "Sweeteners", "name": "sugar", "optionQuantity": "regular" } ] }, "quantity": 1 } ] } ``` ================================================ FILE: typescript/examples/coffeeShop/package.json ================================================ { "name": "coffeeshop", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/coffeeShop/src/coffeeShopSchema.ts ================================================ // The following is a schema definition for ordering lattes. export interface Cart { items: (LineItem | UnknownText)[]; } // Use this type for order items that match nothing else export interface UnknownText { type: "unknown", text: string; // The text that wasn't understood } export interface LineItem { type: "lineitem", product: Product; quantity: number; } export type Product = BakeryProducts | LatteDrinks | EspressoDrinks | CoffeeDrinks; export interface BakeryProducts { type: "BakeryProducts"; name: "apple bran muffin" | "blueberry muffin" | "lemon poppyseed muffin" | "bagel"; options: (BakeryOptions | BakeryPreparations)[]; } export interface BakeryOptions { type: "BakeryOptions"; name: "butter" | "strawberry jam" | "cream cheese"; optionQuantity?: OptionQuantity; } export interface BakeryPreparations { type: "BakeryPreparations"; name: "warmed" | "cut in half"; } export interface LatteDrinks { type: "LatteDrinks"; name: "cappuccino" | "flat white" | "latte" | "latte macchiato" | "mocha" | "chai latte"; temperature?: CoffeeTemperature; size?: CoffeeSize; // The default is "grande" options?: (Milks | Sweeteners | Syrups | Toppings | Caffeines | LattePreparations)[]; } export interface EspressoDrinks { type: "EspressoDrinks"; name: "espresso" | "lungo" | "ristretto" | "macchiato"; temperature?: CoffeeTemperature; size?: EspressoSize; // The default is "doppio" options?: (Creamers | Sweeteners | Syrups | Toppings | Caffeines | LattePreparations)[]; } export interface CoffeeDrinks { type: "CoffeeDrinks"; name: "americano" | "coffee"; temperature?: CoffeeTemperature; size?: CoffeeSize; // The default is "grande" options?: (Creamers | Sweeteners | Syrups | Toppings | Caffeines | LattePreparations)[]; } export interface Syrups { type: "Syrups"; name: "almond syrup" | "buttered rum syrup" | "caramel syrup" | "cinnamon syrup" | "hazelnut syrup" | "orange syrup" | "peppermint syrup" | "raspberry syrup" | "toffee syrup" | "vanilla syrup"; optionQuantity?: OptionQuantity; } export interface Caffeines { type: "Caffeines"; name: "regular" | "two thirds caf" | "half caf" | "one third caf" | "decaf"; } export interface Milks { type: "Milks"; name: "whole milk" | "two percent milk" | "nonfat milk" | "coconut milk" | "soy milk" | "almond milk" | "oat milk"; } export interface Creamers { type: "Creamers"; 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"; } export interface Toppings { type: "Toppings"; name: "cinnamon" | "foam" | "ice" | "nutmeg" | "whipped cream" | "water"; optionQuantity?: OptionQuantity; } export interface LattePreparations { type: "LattePreparations"; name: "for here cup" | "lid" | "with room" | "to go" | "dry" | "wet"; } export interface Sweeteners { type: "Sweeteners"; name: "equal" | "honey" | "splenda" | "sugar" | "sugar in the raw" | "sweet n low" | "espresso shot"; optionQuantity?: OptionQuantity; } export type CoffeeTemperature = "hot" | "extra hot" | "warm" | "iced"; export type CoffeeSize = "short" | "tall" | "grande" | "venti"; export type EspressoSize = "solo" | "doppio" | "triple" | "quad"; export type OptionQuantity = "no" | "light" | "regular" | "extra" | number; ================================================ FILE: typescript/examples/coffeeShop/src/input.txt ================================================ i'd like a latte that's it i'll have a dark roast coffee thank you get me a coffee please could i please get two mochas that's all we need twenty five flat whites and that'll do it how about a tall cappuccino i'd like a venti iced latte i'd like a iced venti latte i'd like a venti latte iced i'd like a latte iced venti we'll also have a short tall latte i wanna latte macchiato with vanilla how about a peppermint latte may i also get a decaf soy vanilla syrup caramel latte with sugar and foam i want a latte with peppermint syrup with peppermint syrup i'd like a decaf half caf latte can I get a skim soy latte i'd like a light nutmeg espresso that's it can i have an cappuccino no foam can i have an espresso with no nutmeg we want a light whipped no foam mocha with extra hazelnut and cinnamon i'd like a latte cut in half i'd like a strawberry latte i want a five pump caramel flat white i want a flat white with five pumps of caramel syrup i want a two pump peppermint three squirt raspberry skinny vanilla latte with a pump of caramel and two sugars i want a latte cappuccino espresso and an apple muffin i'd like a tall decaf latte iced a grande cappuccino double espresso and a warmed poppyseed muffin sliced in half we'd like a latte with soy and a coffee with soy i want a latte latte macchiato and a chai latte we'd like a cappuccino with two pumps of vanilla make that cappuccino with three pumps of vanilla we'd like a cappuccino with a pack of sugar make that cappuccino with two packs of sugar we'd like a cappuccino with a pack of sugar make that with two packs of sugar i'd like a flat white with two equal add three equal to the flat white i'd like a flat white with two equal two tall lattes. the first one with no foam. the second one with whole milk. two tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande. un petit cafe en lille kaffe a raspberry latte a strawberry latte roses are red two lawnmowers, a grande latte and a tall tree ================================================ FILE: typescript/examples/coffeeShop/src/input2.txt ================================================ two tall lattes. the first one with no foam. the second one with whole milk. two tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande. un petit cafe en lille kaffe a raspberry latte a strawberry latte roses are red two lawnmowers, a grande latte and a tall tree ================================================ FILE: typescript/examples/coffeeShop/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createJsonTranslator, createLanguageModel } from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { processRequests } from "typechat/interactive"; import { Cart } from "./coffeeShopSchema"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "coffeeShopSchema.ts"), "utf8"); const validator = createTypeScriptJsonValidator(schema, "Cart"); const translator = createJsonTranslator(model, validator); function processOrder(cart: Cart) { // Process the items in the cart void cart; } // Process requests interactively or from the input file specified on the command line processRequests("☕> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } const cart = response.data; console.log(JSON.stringify(cart, undefined, 2)); if (cart.items.some(item => item.type === "unknown")) { console.log("I didn't understand the following:"); for (const item of cart.items) { if (item.type === "unknown") console.log(item.text); } return; } processOrder(cart); console.log("Success!"); }); ================================================ FILE: typescript/examples/coffeeShop/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/examples/coffeeShop-zod/README.md ================================================ # Coffee Shop The Coffee Shop example shows how to capture user intent as a set of "nouns". In this case, the nouns are items in a coffee order, where valid items are defined starting from the [`Cart`](./src/coffeeShopSchema.ts) type. This 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). # Try Coffee Shop To run the Coffee Shop example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt) and [`src/input2.txt`](./src/input2.txt). For example, we could use natural language to describe our coffee shop order: **Input**: ``` ☕> we'd like a cappuccino with a pack of sugar ``` **Output**: ```json { "items": [ { "type": "lineitem", "product": { "type": "LatteDrinks", "name": "cappuccino", "options": [ { "type": "Sweeteners", "name": "sugar", "optionQuantity": "regular" } ] }, "quantity": 1 } ] } ``` ================================================ FILE: typescript/examples/coffeeShop-zod/package.json ================================================ { "name": "coffeeshop-zod", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "zod": "^3.22.4" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1", "typescript": "^5.3.3" } } ================================================ FILE: typescript/examples/coffeeShop-zod/src/coffeeShopSchema.ts ================================================ import { z } from "zod"; export const OptionQuantity = z.union([z.literal('no'), z.literal('light'), z.literal('regular'), z.literal('extra'), z.number()]); export const BakeryOptions = z.object({ type: z.literal('BakeryOptions'), name: z.enum(['butter', 'strawberry jam', 'cream cheese']), optionQuantity: OptionQuantity.optional() }); export const BakeryPreparations = z.object({ type: z.literal('BakeryPreparations'), name: z.enum(['warmed', 'cut in half']) }); export const BakeryProducts = z.object({ type: z.literal('BakeryProducts'), name: z.enum(['apple bran muffin', 'blueberry muffin', 'lemon poppyseed muffin', 'bagel']), options: z.discriminatedUnion("type", [BakeryOptions, BakeryPreparations]).array() }) export const CoffeeTemperature = z.enum(['hot', 'extra hot', 'warm', 'iced']); export const CoffeeSize = z.enum(['short', 'tall', 'grande', 'venti']); export const Milks = z.object({ type: z.literal('Milks'), name: z.enum(['whole milk', 'two percent milk', 'nonfat milk', 'coconut milk', 'soy milk', 'almond milk', 'oat milk']) }) export const Sweeteners = z.object({ type: z.literal('Sweeteners'), name: z.enum(['equal', 'honey', 'splenda', 'sugar', 'sugar in the raw', 'sweet n low', 'espresso shot']), optionQuantity: OptionQuantity.optional() }); export const Syrups = z.object({ type: z.literal('Syrups'), name: z.enum(['almond syrup', 'buttered rum syrup', 'caramel syrup', 'cinnamon syrup', 'hazelnut syrup', 'orange syrup', 'peppermint syrup', 'raspberry syrup', 'toffee syrup', 'vanilla syrup']), optionQuantity: OptionQuantity.optional() }); export const Toppings = z.object({ type: z.literal('Toppings'), name: z.enum(['cinnamon', 'foam', 'ice', 'nutmeg', 'whipped cream', 'water']), optionQuantity: OptionQuantity.optional() }); export const Caffeines = z.object({ type: z.literal('Caffeines'), name: z.enum(['regular', 'two thirds caf', 'half caf', 'one third caf', 'decaf']) }); export const LattePreparations = z.object({ type: z.literal('LattePreparations'), name: z.enum(['for here cup', 'lid', 'with room', 'to go', 'dry', 'wet']) }); export const LatteDrinks = z.object({ type: z.literal('LatteDrinks'), name: z.enum(['cappuccino', 'flat white', 'latte', 'latte macchiato', 'mocha', 'chai latte']), temperature: CoffeeTemperature.optional(), size: CoffeeSize.describe("The default is 'grande'"), options: z.discriminatedUnion("type", [Milks, Sweeteners, Syrups, Toppings, Caffeines, LattePreparations]).array().optional(), }); export const EspressoSize = z.enum(['solo', 'doppio', 'triple', 'quad']); export const Creamers = z.object({ type: z.literal('Creamers'), name: z.enum(['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']) }); export const EspressoDrinks = z.object({ type: z.literal('EspressoDrinks'), name: z.enum(['espresso', 'lungo', 'ristretto', 'macchiato']), temperature: CoffeeTemperature.optional(), size: EspressoSize.optional().describe("The default is 'doppio'"), options: z.discriminatedUnion("type", [Creamers, Sweeteners, Syrups, Toppings, Caffeines, LattePreparations]).array().optional() }); export const CoffeeDrinks = z.object({ type: z.literal('CoffeeDrinks'), name: z.enum(['americano', 'coffee']), temperature: CoffeeTemperature.optional(), size: CoffeeSize.optional().describe("The default is 'grande'"), options: z.discriminatedUnion("type", [Creamers, Sweeteners, Syrups, Toppings, Caffeines, LattePreparations]).array().optional() }); export const Product = z.discriminatedUnion("type", [BakeryProducts, LatteDrinks, EspressoDrinks, CoffeeDrinks]); export const LineItem = z.object({ type: z.literal('lineitem'), product: Product, quantity: z.number() }); export const UnknownText = z.object({ type: z.literal('unknown'), text: z.string().describe("The text that wasn't understood") }); export const Cart = z.object({ items: z.discriminatedUnion("type", [LineItem, UnknownText]).array() }); export const CoffeeShopSchema = { Cart: Cart.describe("A schema definition for ordering coffee and bakery products"), UnknownText: UnknownText.describe("Use this type for order items that match nothing else"), LineItem, Product, BakeryProducts, BakeryOptions, BakeryPreparations, LatteDrinks, EspressoDrinks, CoffeeDrinks, Syrups, Caffeines, Milks, Creamers, Toppings, LattePreparations, Sweeteners, CoffeeTemperature, CoffeeSize, EspressoSize, OptionQuantity }; ================================================ FILE: typescript/examples/coffeeShop-zod/src/input.txt ================================================ i'd like a latte that's it i'll have a dark roast coffee thank you get me a coffee please could i please get two mochas that's all we need twenty five flat whites and that'll do it how about a tall cappuccino i'd like a venti iced latte i'd like a iced venti latte i'd like a venti latte iced i'd like a latte iced venti we'll also have a short tall latte i wanna latte macchiato with vanilla how about a peppermint latte may i also get a decaf soy vanilla syrup caramel latte with sugar and foam i want a latte with peppermint syrup with peppermint syrup i'd like a decaf half caf latte can I get a skim soy latte i'd like a light nutmeg espresso that's it can i have an cappuccino no foam can i have an espresso with no nutmeg we want a light whipped no foam mocha with extra hazelnut and cinnamon i'd like a latte cut in half i'd like a strawberry latte i want a five pump caramel flat white i want a flat white with five pumps of caramel syrup i want a two pump peppermint three squirt raspberry skinny vanilla latte with a pump of caramel and two sugars i want a latte cappuccino espresso and an apple muffin i'd like a tall decaf latte iced a grande cappuccino double espresso and a warmed poppyseed muffin sliced in half we'd like a latte with soy and a coffee with soy i want a latte latte macchiato and a chai latte we'd like a cappuccino with two pumps of vanilla make that cappuccino with three pumps of vanilla we'd like a cappuccino with a pack of sugar make that cappuccino with two packs of sugar we'd like a cappuccino with a pack of sugar make that with two packs of sugar i'd like a flat white with two equal add three equal to the flat white i'd like a flat white with two equal two tall lattes. the first one with no foam. the second one with whole milk. two tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande. un petit cafe en lille kaffe a raspberry latte a strawberry latte roses are red two lawnmowers, a grande latte and a tall tree ================================================ FILE: typescript/examples/coffeeShop-zod/src/input2.txt ================================================ two tall lattes. the first one with no foam. the second one with whole milk. two tall lattes. the first one with no foam. the second one with whole milk. actually make the first one a grande. un petit cafe en lille kaffe a raspberry latte a strawberry latte roses are red two lawnmowers, a grande latte and a tall tree ================================================ FILE: typescript/examples/coffeeShop-zod/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import { createJsonTranslator, createLanguageModel } from "typechat"; import { createZodJsonValidator } from "typechat/zod"; import { processRequests } from "typechat/interactive"; import { z } from "zod"; import { CoffeeShopSchema } from "./coffeeShopSchema"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const validator = createZodJsonValidator(CoffeeShopSchema, "Cart"); const translator = createJsonTranslator(model, validator); function processOrder(cart: z.TypeOf) { // Process the items in the cart void cart; } // Process requests interactively or from the input file specified on the command line processRequests("☕> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } const cart = response.data; console.log(JSON.stringify(cart, undefined, 2)); if (cart.items.some(item => item.type === "unknown")) { console.log("I didn't understand the following:"); for (const item of cart.items) { if (item.type === "unknown") console.log(item.text); } return; } processOrder(cart); console.log("Success!"); }); ================================================ FILE: typescript/examples/coffeeShop-zod/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/examples/crossword/README.md ================================================ # Crossword The 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. ## Target models This 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. # Try Crossword To run the Crossword example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For example, given the following input statement: **Input**: ``` 🏁> What is the clue for 61 across ``` **Output**: ``` "Monogram in French fashion" ``` ================================================ FILE: typescript/examples/crossword/package.json ================================================ { "name": "crossword", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt src/**/*.jpeg dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/crossword/src/crosswordSchema.ts ================================================ // The following is a schema definition for determining the sentiment of a some user input. export type GetClueText = { actionName: "getClueText"; parameters: { clueNumber: number; clueDirection: "across" | "down"; value: string; }; }; // This gives the answer for the requested crossword clue export type GetAnswerValue = { actionName: "getAnswerValue"; parameters: { proposedAnswer: string; clueNumber: number; clueDirection: "across" | "down"; }; }; export type UnknownAction = { actionName: "unknown"; parameters: { // text typed by the user that the system did not understand text: string; }; }; export type CrosswordActions = | GetClueText | GetAnswerValue | UnknownAction; ================================================ FILE: typescript/examples/crossword/src/input.txt ================================================ What is the clue for 1 down Give me a hint for solving 4 down ================================================ FILE: typescript/examples/crossword/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createLanguageModel } from "typechat"; import { processRequests } from "typechat/interactive"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { CrosswordActions } from "./crosswordSchema"; import { createCrosswordActionTranslator } from "./translator"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "crosswordSchema.ts"), "utf8"); const rawImage = fs.readFileSync(path.join(__dirname, "puzzleScreenshot.jpeg"),"base64"); const screenshot = `data:image/jpeg;base64,${rawImage}`; const validator = createTypeScriptJsonValidator(schema, "CrosswordActions"); const translator = createCrosswordActionTranslator(model, validator, screenshot); // Process requests interactively or from the input file specified on the command line processRequests("🏁> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } console.log(JSON.stringify(response.data)); }); ================================================ FILE: typescript/examples/crossword/src/translator.ts ================================================ import { TypeChatLanguageModel, createJsonTranslator, TypeChatJsonTranslator, MultimodalPromptContent, PromptContent, } from "typechat"; import { TypeScriptJsonValidator } from "typechat/ts"; export function createCrosswordActionTranslator( model: TypeChatLanguageModel, validator: TypeScriptJsonValidator, crosswordImage: string ): TypeChatJsonTranslator { const _imageContent = crosswordImage; const _translator = createJsonTranslator(model, validator); _translator.createRequestPrompt = createRequestPrompt return _translator; function createRequestPrompt(request: string): PromptContent { const screenshotSection = getScreenshotPromptSection(_imageContent); const contentSections = [ { type: "text", text: "You are a virtual assistant that can help users to complete requests by interacting with the UI of a webpage.", }, ...screenshotSection, { type: "text", text: ` Use the layout information provided to answer user queries. The responses should be translated into JSON objects of type ${_translator.validator.getTypeName()} using the typescript schema below: ''' ${_translator.validator.getSchemaText()} ''' `, }, { type: "text", text: ` The following is a user request: ''' ${request} ''' The following is the assistant's response translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: `, }, ] as MultimodalPromptContent[]; return contentSections; } function getScreenshotPromptSection(screenshot: string | undefined) { let screenshotSection = []; if (screenshot) { screenshotSection.push({ type: "text", text: "Here is a screenshot of the currently visible webpage", }); screenshotSection.push({ type: "image_url", image_url: { url: screenshot, detail: "high" }, }); screenshotSection.push({ type: "text", text: `Use the top left corner as coordinate 0,0 and draw a virtual grid of 1x1 pixels, where x values increase for each pixel as you go from left to right, and y values increase as you go from top to bottom. `, }); } return screenshotSection; } } ================================================ FILE: typescript/examples/crossword/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/examples/healthData/README.md ================================================ # Health Data Agent This example requires GPT-4. Demonstrates 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. 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"). ## Target models For best and consistent results, use **gpt-4**. ## Try the Health Data Agent To run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). ## Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For example, given the following input statement: **Input**: ```console 🤧> I am taking klaritin for my allergies ``` **Output**: ================================================ FILE: typescript/examples/healthData/package.json ================================================ { "name": "health-data", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "typescript": "^5.1.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.3.1", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/healthData/src/healthDataSchema.ts ================================================ // The following is a schema definition for enetring health data. export interface HealthDataResponse { // ONLY present when ALL required information is known. // Otherwise, use 'message' to keep asking questions. data?: HealthData; // Use this to ask questions and give pertinent responses message?: string; // Use this parts of the user request not translated, off topic, etc. notTranslated?: string; } export interface HealthData { medication?: Medication[]; condition?: Condition[]; other?: OtherHealthData[]; } // Meds, pills etc. export interface Medication { // Fix any spelling mistakes, especially phonetic spelling name: string; // E.g. 2 tablets, 1 cup. Required dose: ApproxQuantity; // E.g. twice a day. Required frequency: ApproxQuantity; // E.g. 50 mg. Required strength: ApproxQuantity; } // Disease, Ailment, Injury, Sickness export interface Condition { // Fix any spelling mistakes, especially phonetic spelling name: string; // When the condition started. startDate: ApproxDatetime; // Always ask for current status of the condition status: "active" | "recurrence" | "relapse" | "inactive" | "remission" | "resolved" | "unknown"; // If the condition was no longer active endDate?: ApproxDatetime; } // Use for health data that match nothing else. E.g. immunization, blood prssure etc export interface OtherHealthData { text: string; when?: ApproxDatetime; } export interface ApproxQuantity { // Default: "unknown" displayText: string; // Only specify if precise quantities are available quantity?: Quantity; } export interface ApproxDatetime { // Default: "unknown" displayText: string; // If precise timestamp can be set timestamp?: string; } export interface Quantity { // Exact number value: number; // Units like mg, kg, cm, pounds, liter, ml, tablet, pill, cup, per-day, per-week, etc. units: string; } ================================================ FILE: typescript/examples/healthData/src/input.txt ================================================ # # Conversations with a Health Data Agent # For each conversation: # You start with the first line # Then type the next line in response # # ================ # USE GPT4 # ================ # Conversation: i want to record my shingles August 2016 It lasted 3 months I also broke my foot I broke it in high school 2001 The foot took a year to be ok # Conversation: klaritin 2 tablets 3 times a day 300 mg actually that is 1 tablet @clear # Conversation: klaritin 1 pill, morning and before bedtime Can't remember Actually, that is 3 tablets 500 mg @clear #Conversation I am taking binadryl now As needed. Groceery store strength That is all I have I also got allergies. Pollen @clear # Conversation: Robotussin 1 cup Daily, as needed Robotussin with Codeine Put down strength as I don't know @clear # Conversation: Hey Melatonin 1 3mg tablet every night @clear # Conversation: I got the flu Started 2 weeks ago Its gone now. Only lasted about a week I took some sudafed though I took 2 sudafed twice a day. Regular strength @clear ================================================ FILE: typescript/examples/healthData/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createLanguageModel } from "typechat"; import { processRequests } from "typechat/interactive"; import { HealthDataResponse } from "./healthDataSchema"; import { createHealthDataTranslator } from "./translator"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const healthInstructions = ` Help me enter my health data step by step. Ask specific questions to gather required and optional fields I have not already providedStop asking if I don't know the answer Automatically fix my spelling mistakes My health data may be complex: always record and return ALL of it. Always return a response: - If you don't understand what I say, ask a question. - At least respond with an OK message. `; const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "healthDataSchema.ts"), "utf8"); const translator = createHealthDataTranslator(model, schema, "HealthDataResponse", healthInstructions); // Process requests interactively or from the input file specified on the command line processRequests("🤧> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log("Translation Failed ❌"); console.log(`Context: ${response.message}`); } else { const healthData = response.data; console.log("Translation Succeeded! ✅\n"); console.log("JSON View"); console.log(JSON.stringify(healthData, undefined, 2)); const message = healthData.message; const notTranslated = healthData.notTranslated; if (message) { console.log(`\n📝: ${message}`); } if (notTranslated) { console.log(`\n🤔: I did not understand\n ${notTranslated}`) } } }); ================================================ FILE: typescript/examples/healthData/src/translator.ts ================================================ import {Result, TypeChatLanguageModel, createJsonTranslator, TypeChatJsonTranslator} from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; type ChatMessage = { source: "system" | "user" | "assistant"; body: object; }; export interface TranslatorWithHistory { _chatHistory: ChatMessage[]; _maxPromptLength: number; _additionalAgentInstructions: string; _translator: TypeChatJsonTranslator; translate(request: string): Promise>; } export function createHealthDataTranslator(model: TypeChatLanguageModel, schema: string, typename: string, additionalAgentInstructions: string): TranslatorWithHistory { const _chatHistory: ChatMessage[] = []; const _maxPromptLength = 2048; const _additionalAgentInstructions = additionalAgentInstructions; const validator = createTypeScriptJsonValidator(schema, typename); const _translator = createJsonTranslator(model, validator); _translator.createRequestPrompt = createRequestPrompt; const customtranslator: TranslatorWithHistory = { _chatHistory, _maxPromptLength, _additionalAgentInstructions, _translator, translate, }; return customtranslator; async function translate(request: string): Promise> { const response = await _translator.translate(request); if (response.success) { _chatHistory.push({ source: "assistant", body: response.data }); } return response; } function createRequestPrompt(intent: string): string { // TODO: drop history entries if we exceed the max_prompt_length const historyStr = JSON.stringify(_chatHistory, undefined, 2); const now = new Date(); const prompt = ` user: You are a service that translates user requests into JSON objects of type "${typename}" according to the following TypeScript definitions: ''' ${schema} ''' user: Use precise date and times RELATIVE TO CURRENT DATE: ${now.toLocaleDateString()} CURRENT TIME: ${now.toTimeString().split(' ')[0]} Also turn ranges like next week and next month into precise dates user: ${_additionalAgentInstructions} system: IMPORTANT CONTEXT for the user request: ${historyStr} user: The following is a user request: ''' ${intent} ''' The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: """ `; return prompt; } } ================================================ FILE: typescript/examples/healthData/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/examples/math/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "/**" ], "program": "${workspaceFolder}/dist/main.js", "console": "externalTerminal" } ] } ================================================ FILE: typescript/examples/math/README.md ================================================ # Math The 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. # Try Math To run the Math example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For 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. **Input**: ``` 🟰> multiply two by three, then multiply four by five, then sum the results ``` **Output**: ``` import { API } from "./schema"; function program(api: API) { const step1 = api.mul(2, 3); const step2 = api.mul(4, 5); return api.add(step1, step2); } Running program: mul(2, 3) mul(4, 5) add(6, 20) Result: 26 ``` ================================================ FILE: typescript/examples/math/package.json ================================================ { "name": "math", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/math/src/input.txt ================================================ 1 + 2 1 + 2 * 3 2 * 3 + 4 * 5 2 3 * 4 5 * + multiply two by three, then multiply four by five, then sum the results ================================================ FILE: typescript/examples/math/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createLanguageModel, getData } from "typechat"; import { processRequests } from "typechat/interactive"; import { createModuleTextFromProgram, createProgramTranslator, evaluateJsonProgram } from "typechat/ts"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "mathSchema.ts"), "utf8"); const translator = createProgramTranslator(model, schema); // Process requests interactively or from the input file specified on the command line processRequests("🧮 > ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } const program = response.data; console.log(getData(createModuleTextFromProgram(program))); console.log("Running program:"); const result = await evaluateJsonProgram(program, handleCall); console.log(`Result: ${typeof result === "number" ? result : "Error"}`); }); async function handleCall(func: string, args: any[]): Promise { console.log(`${func}(${args.map(arg => typeof arg === "number" ? arg : JSON.stringify(arg, undefined, 2)).join(", ")})`); switch (func) { case "add": return args[0] + args[1]; case "sub": return args[0] - args[1]; case "mul": return args[0] * args[1]; case "div": return args[0] / args[1]; case "neg": return -args[0]; case "id": return args[0]; } return NaN; } ================================================ FILE: typescript/examples/math/src/mathSchema.ts ================================================ // This is a schema for writing programs that evaluate expressions. export type API = { // Add two numbers add(x: number, y: number): number; // Subtract two numbers sub(x: number, y: number): number; // Multiply two numbers mul(x: number, y: number): number; // Divide two numbers div(x: number, y: number): number; // Negate a number neg(x: number): number; // Identity function id(x: number): number; // Unknown request unknown(text: string): number; } ================================================ FILE: typescript/examples/math/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true, } } ================================================ FILE: typescript/examples/multiSchema/README.md ================================================ # MultiSchema This application demonstrates a simple way to write a **super-app** that automatically routes user requests to child apps. In this example, the child apps are existing TypeChat chat examples: * CoffeeShop * Restaurant * Calendar * Sentiment * Math * Plugins * HealthData ## Target Models Works with GPT-3.5 Turbo and GPT-4. Sub-apps like HealthData and Plugins work best with GPT-4. # Usage Example prompts can be found in [`src/input.txt`](src/input.txt). ================================================ FILE: typescript/examples/multiSchema/package.json ================================================ { "name": "multi-schema", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 -f ../../examples/**/src/**/*Schema.ts src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "typechat": "^0.1.0", "find-config": "^1.0.0", "music": "^0.0.1", "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.3.1", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/multiSchema/src/agent.ts ================================================ // TypeScript file for TypeChat agents. import { Result, TypeChatJsonTranslator, TypeChatLanguageModel, createJsonTranslator, getData, success } from "typechat"; import { Program, createModuleTextFromProgram, createProgramTranslator, createTypeScriptJsonValidator, evaluateJsonProgram } from "typechat/ts"; export type AgentInfo = { name: string; description: string; }; export interface AgentClassificationResponse { agenInfo : AgentInfo; } export type MessageHandler = (message: string) => Promise>; export interface Agent extends AgentInfo { handleMessage(message: string): Promise>; }; interface JsonPrintAgent extends Agent { _translator: TypeChatJsonTranslator; } export function createJsonPrintAgent( name: string, description: string, model: TypeChatLanguageModel, schema: string, typeName: string ): JsonPrintAgent { const validator = createTypeScriptJsonValidator(schema, typeName) const _translator = createJsonTranslator(model, validator); const jsonPrintAgent: JsonPrintAgent = { _translator, name: name, description: description, handleMessage: _handleMessage, }; return jsonPrintAgent; async function _handleMessage(request: string): Promise> { const response = await _translator.translate(request); if (response.success) { console.log("Translation Succeeded! ✅\n") console.log("JSON View") console.log(JSON.stringify(response.data, undefined, 2)) } else { console.log("Translation Failed ❌") console.log(`Context: ${response.message}`) } return response; } } interface MathAgent extends Agent { _translator: TypeChatJsonTranslator; //_handleCall(func: string, args: any[]): Promise; } export function createJsonMathAgent (name: string, description: string, model: TypeChatLanguageModel, schema: string): MathAgent { async function _handleCall(func: string, args: any[]): Promise { // implementation goes here console.log(`${func}(${args.map(arg => typeof arg === "number" ? arg : JSON.stringify(arg, undefined, 2)).join(", ")})`); switch (func) { case "add": return args[0] + args[1]; case "sub": return args[0] - args[1]; case "mul": return args[0] * args[1]; case "div": return args[0] / args[1]; case "neg": return -args[0]; case "id": return args[0]; } return NaN; } const _translator = createProgramTranslator(model, schema); const mathAgent : MathAgent = { _translator, name: name, description: description, handleMessage: _handleMessage, }; return mathAgent; async function _handleMessage(request: string): Promise> { const response = await _translator.translate(request); if (!response.success) { console.log(response.message); return response; } const program = response.data; console.log(getData(createModuleTextFromProgram(program))); console.log("Running program:"); const result = await evaluateJsonProgram(program, _handleCall); console.log(`Result: ${typeof result === "number" ? result : "Error"}`); return success("Successful evaluation" as any); } } ================================================ FILE: typescript/examples/multiSchema/src/classificationSchema.ts ================================================ export interface TaskClassification { name: string; description: string; } /** * Represents the response of a task classification. */ export interface TaskClassificationResponse { // Describe the kind of task to perform. taskType: string; } ================================================ FILE: typescript/examples/multiSchema/src/input.txt ================================================ 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. I also want an espresso with extra foam and a muffin with jam And book me a lunch with Claude Debussy next week at 12.30 at Le Petit Chien! I bought 4 shoes for 12.50 each. How much did I spend? Its cold! Its cold and I want hot cafe to warm me up The coffee is cold The coffee is awful (2*4)+(9*7) ================================================ FILE: typescript/examples/multiSchema/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createLanguageModel } from "typechat"; import { processRequests } from "typechat/interactive"; import { createJsonMathAgent, createJsonPrintAgent } from "./agent"; import { createAgentRouter } from "./router"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const taskClassificationSchema = fs.readFileSync(path.join(__dirname, "classificationSchema.ts"), "utf8"); const router = createAgentRouter(model, taskClassificationSchema, "TaskClassificationResponse") const sentimentSchema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8"); const sentimentAgent = createJsonPrintAgent ("Sentiment", "Statements with sentiments, emotions, feelings, impressions about places, things, the surroundings", model, sentimentSchema, "SentimentResponse" ); router.registerAgent("Sentiment", sentimentAgent); const coffeeShopSchema = fs.readFileSync(path.join(__dirname, "coffeeShopSchema.ts"), "utf8"); const coffeeShopAgent = createJsonPrintAgent( "CoffeeShop", "Order Coffee Drinks (Italian names included) and Baked Goods", model, coffeeShopSchema, "Cart" ); router.registerAgent("CoffeeShop", coffeeShopAgent); const calendarSchema = fs.readFileSync(path.join(__dirname, "calendarActionsSchema.ts"), "utf8"); const calendarAgent = createJsonPrintAgent( "Calendar", "Actions related to calendars, appointments, meetings, schedules", model, calendarSchema, "CalendarActions" ); router.registerAgent("Calendar", calendarAgent); const orderSchema = fs.readFileSync(path.join(__dirname, "foodOrderViewSchema.ts"), "utf8"); const restaurantOrderAgent = createJsonPrintAgent( "Restaurant", "Order pizza, beer and salads", model, orderSchema, "Order" ); router.registerAgent("Restaurant", restaurantOrderAgent); const mathSchema = fs.readFileSync(path.join(__dirname, "mathSchema.ts"), "utf8"); const mathAgent = createJsonMathAgent( "Math", "Calculations using the four basic math operations", model, mathSchema ); router.registerAgent("Math", mathAgent); // Process requests interactively or from the input file specified on the command line processRequests("🔀> ", process.argv[2], async (request) => { await router.routeRequest(request); }); ================================================ FILE: typescript/examples/multiSchema/src/router.ts ================================================ import { Result, TypeChatJsonTranslator, TypeChatLanguageModel, createJsonTranslator } from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { Agent, MessageHandler } from "./agent"; import { TaskClassification, TaskClassificationResponse } from "./classificationSchema"; export interface AgentRouter { _taskTypes: TaskClassification[]; _agentMap: { [name: string]: Agent }; _taskClassifier: TypeChatJsonTranslator _handlerUnknownTask: MessageHandler; registerAgent(name: string, agent: Agent): Promise routeRequest(request: string): Promise } export function createAgentRouter(model: TypeChatLanguageModel, schema: string, typeName: string): AgentRouter { const validator = createTypeScriptJsonValidator(schema, typeName) const taskClassifier = createJsonTranslator(model, validator); const router: AgentRouter = { _taskTypes: [], _agentMap: {}, _taskClassifier: taskClassifier, _handlerUnknownTask: handlerUnknownTask, registerAgent, routeRequest: routeRequest, }; router._taskTypes.push({ name: "No Match", description: "Handles all unrecognized requests" }); return router; async function handlerUnknownTask(request: string): Promise> { console.log(`🤖The request "${request}" was not recognized by any agent.`); return { success: false, message: `The request "${request}" was not recognized by any agent.` }; } async function registerAgent(name: string, agent: Agent): Promise { if (!router._agentMap[name]) { router._agentMap[name] = agent; // Add the agent's task type to the list of task types router._taskTypes.push({name: name, description: agent.description}); } return; } async function routeRequest(request:string): Promise { const initClasses = JSON.stringify(router._taskTypes, undefined, 2); const fullRequest = ` Classify "${request}" using the following classification table:\n ${initClasses}\n`; const response = await router._taskClassifier.translate(request, [{ role: "assistant", content: `${fullRequest}` }]); if (response.success) { if (response.data.taskType != "No Match") { const agentName = response.data.taskType; console.log(`🤖 The task will be handled by the ${agentName} Agent.`); const agent = router._agentMap[agentName]; await agent.handleMessage(request); } else { router._handlerUnknownTask(request); } } else { console.log("🙈 Sorry, we could not find an agent to handle your request.\n") console.log(`Context: ${response.message}`) } return } } ================================================ FILE: typescript/examples/multiSchema/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true }, "references": [ { "path": "../../music/src" } ] } ================================================ FILE: typescript/examples/music/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "/**" ], "program": "${workspaceFolder}/dist/main.js", "console": "externalTerminal" } ] } ================================================ FILE: typescript/examples/music/README.md ================================================ # Music The 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. # Try Music A Spotify Premium account is required to run this example. To run the Music example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). This example also requires additional setup to use the Spotify API: 1. Go to https://developer.spotify.com/dashboard. 2. Log into Spotify with your user account if you are not already logged in. 3. Click the button in the upper right labeled "Create App". 4. 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. 5. Click the settings button and copy down the Client ID and Client Secret (the client secret requires you to click 'View client secret'). 6. 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. # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For example, use natural language to start playing a song with the Spotify player: **Input**: ``` 🎵> play shake it off by taylor swift ``` **Output**: ``` Plan Validated: { "@steps": [ { "@func": "searchTracks", "@args": [ "shake it off taylor swift" ] }, { "@func": "play", "@args": [ { "@ref": 0 } ] } ] } import { API } from "./schema"; function program(api: API) { const step1 = api.searchTracks("shake it off taylor swift"); return api.play(step1); } Playing... Shake It Off ``` ================================================ FILE: typescript/examples/music/migrations.md ================================================ # Local Music DB Migrations Tracks table ```[SQL] CREATE TABLE tracks ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, artist_id INTEGER NOT NULL, album_id INTEGER, duration INTEGER, release_date TEXT, genre TEXT, ); ``` Albums table ```[SQL] CREATE TABLE albums ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, artist_id INTEGER NOT NULL, release_date TEXT, genre TEXT, ); ``` Playlists table ```[SQL] CREATE TABLE playlists ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, user_id INTEGER NOT NULL, creation_date TEXT, description TEXT, ); ``` Artists table ```[SQL] CREATE TABLE artists ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, country TEXT, genre TEXT ); ``` ================================================ FILE: typescript/examples/music/package.json ================================================ { "name": "music", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt src/**/*.html dist" }, "exports": { "./*": ["./dist/*.js"] }, "author": "", "license": "MIT", "dependencies": { "axios": "^1.6.2", "chalk": "^2.3.1", "dotenv": "^16.3.1", "express": "^4.18.2", "find-config": "^1.0.0", "open": "^7.0.4", "sqlite3": "^5.1.6", "typechat": "^0.1.0", "typescript": "^5.3.3" }, "devDependencies": { "@types/express": "^4.17.17", "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "@types/spotify-api": "^0.0.22", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/music/src/authz.ts ================================================ import open from "open"; import chalk from "chalk"; import express from "express"; import dotenv from "dotenv"; import path from "path"; dotenv.config({ path: path.join(__dirname, "../../../.env") }); import { Server } from "http"; type AuthzHandlerFn = (token: string | undefined) => void; type AuthzHandler = AuthzHandlerFn | undefined; type AuthzServer = Server | undefined; const scope = [ "user-read-private", "playlist-read-collaborative", "playlist-modify-private", "playlist-read-private", "playlist-modify-public", "streaming", "user-library-read", "user-top-read", "user-read-playback-state", "user-modify-playback-state", "user-read-recently-played", "user-read-currently-playing", "user-library-modify", "ugc-image-upload", ].join("%20"); const baseClientId = process.env.SPOTIFY_APP_CLI; const defaultPort = process.env.SPOTIFY_APP_PORT; export class Authzor { url: string; app: express.Express; handler: AuthzHandler; server: AuthzServer; private redirectCount = 0; constructor( public port = defaultPort, public showDialog = false, public clientId = baseClientId ) { const redirectUri = "http://localhost:" + port + "/callback"; this.url = "https://accounts.spotify.com/authorize" + "?client_id=" + clientId + "&response_type=token" + "&scope=" + scope + "&show_dialog=" + showDialog + "&redirect_uri=" + redirectUri; this.app = express(); this.app.get("/callback", (req, res) => { if (req.query.error) { console.log( chalk.red("Something went wrong. Error: "), req.query.error ); } else { // update this when implementing re-auth on token expire if (this.redirectCount === 0) { res.sendFile(__dirname + "/callback.html"); this.redirectCount++; } } }); this.app.get("/token", (req, res) => { res.sendStatus(200); const token = req.query.access_token as string; if (token) { if (this.handler) { this.handler(token); } } this.close(); }); } authorize(connect: boolean, handler: AuthzHandlerFn) { if (baseClientId && connect) { this.handler = handler; this.server = this.app.listen(this.port, () => { if (this.showDialog) { console.log( chalk.blue( "Opening the Spotify Login Dialog in your browser..." ) ); } open(this.url, { wait: false }); }); } else { handler(undefined); } } close() { if (this.server) { this.server.close(); } } } ================================================ FILE: typescript/examples/music/src/callback.html ================================================ ================================================ FILE: typescript/examples/music/src/chatifyActionsSchema.ts ================================================ // This is a schema for writing programs that control a Spotify music player type Track = { name: string }; type TrackList = Track[]; type Playlist = TrackList; export type API = { // play track list play( // track list to play trackList: TrackList, // start playing at this track index startIndex?: number, // play this many tracks count?: number ): void; // print a list of tracks printTracks(trackList: TrackList): void; // see what is up next getQueue(): void; // show now playing status(): void; // control playback // pause playback pause(): void; // next track next(): void; // previous track previous(): void; // turn shuffle on shuffleOn(): void; // turn shuffle off shuffleOff(): void; // resume playing resume(): void; // list available playback devices listDevices(): void; // select playback device by keyword selectDevice(keyword: string): void; // set volume setVolume(newVolumeLevel: number): void; // change volume changeVolume(volumeChangeAmount: number): void; // query is a Spotify search expression such as 'Rock Lobster' or 'te kanawa queen of night' searchTracks(query: string): TrackList; // return the last track list shown to the user // for example, if the user types "play the third one" the player plays the third track // from the last track list shown getLastTrackList(): TrackList; // list all playlists listPlaylists(): void; // get playlist by name getPlaylist(name: string): Playlist; // get album by name; if name is "", use the currently playing track getAlbum(name: string): TrackList; // Return a list of the user's favorite tracks getFavorites(count?: number): TrackList; // apply a filter to match tracks filterTracks( // track list to filter trackList: TrackList, // filter type is one of "genre", "artist", "name"; name does a fuzzy match on the track name // for example, filterType: "name", filter: "color" matches "Red Red Wine" filterType: "genre" | "artist" | "name", filter: string, negate?: boolean ): TrackList; // create a Spotify playlist from a list of tracks createPlaylist(trackList: TrackList, name: string): void; // Delete playlist given by playlist deletePlaylist(playlist: Playlist): void; // call this function for requests that weren't understood unknownAction(text: string): void; // call this function if the user asks a non-music question; non-music non-questions use UnknownAction nonMusicQuestion(text: string): void; }; ================================================ FILE: typescript/examples/music/src/dbInterface.ts ================================================ import sqlite3 from "sqlite3"; const dbPath = "../musicLibrary.db" type Row = { [key:string] : unknown } function executeQuery(query: string, params: any[] = []): Row[] | void { const db = new sqlite3.Database(dbPath, (error) => { if (error) { console.log(`Error executing query: ${query} against ${dbPath}`); return; } db.all(query, params, (error: Error, rows: Row[]) => { db.close(); if (error) { console.log(`Error executing query: ${query} against ${dbPath}`); return; } return rows; }); }); } export function insertTracks(tracks: SpotifyApi.TrackObjectFull[]) { let insertQuery = 'INSERT INTO tracks (id, title, artist_id, album_id, duration, release_data, genre)\nVALUES\n'; for (const track of tracks) { // TODO: genre insertQuery += ` (${track.id},${track.name},${track.artists[0].id},${track.album.id},${track.duration_ms},${track.album.release_date})`; } } export function getArtists() { const query = "SELECT * FROM artists"; const artists = executeQuery(query); return artists; } ================================================ FILE: typescript/examples/music/src/endpoints.ts ================================================ import axios from "axios"; import { SpotifyService } from "./service"; export const limitMax = 50; export async function search( query: SpotifyApi.SearchForItemParameterObject, service: SpotifyService ) { const config = { headers: { Authorization: `Bearer ${service.retrieveToken()}`, }, }; const searchUrl = getUrlWithParams( "https://api.spotify.com/v1/search", query ); try { const spotifyResult = await axios.get(searchUrl, config); return spotifyResult.data as SpotifyApi.SearchResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getTop( service: SpotifyService, limit = limitMax, offset = 0 ) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; const tracksUrl = getUrlWithParams("https://api.spotify.com/v1/me/tracks", { limit, offset, }); try { const spotifyResult = await axios.get(tracksUrl, config); return spotifyResult.data as SpotifyApi.PagingObject; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getTopK(service: SpotifyService, k = limitMax) { if (k > limitMax) { const topTracks = [] as SpotifyApi.PlaylistTrackObject[]; let offset = 0; while (k > 0) { let count = limitMax; if (k < count) { count = k; } const hist = await getTop(service, count, offset); if (hist && hist.items) { topTracks.push(...hist.items); } k -= limitMax; offset += limitMax; } return topTracks; } else { const hist = await getTop(service, k); if (hist && hist.items) { return hist.items; } } return undefined; } export async function getArtist(service: SpotifyService, id: string) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; const artistsUrl = getUrlWithParams("https://api.spotify.com/v1/artists", { ids: id, }); try { const spotifyResult = await axios.get(artistsUrl, config); return spotifyResult.data as SpotifyApi.MultipleArtistsResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getHistoryURL(service: SpotifyService, url: string) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; console.log(url); try { const spotifyResult = await axios.get(url, config); const spotData = spotifyResult.data as SpotifyApi.UsersRecentlyPlayedTracksResponse; return spotData; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getRecent( service: SpotifyService, after = Date.parse("2023-01-01T00:00:00.000Z") ) { const playHistory = [] as SpotifyApi.PlayHistoryObject[]; console.log(new Date(after).toLocaleString()); const params = { limit: 50, after, }; let nextURL: string | null | undefined = getUrlWithParams( "https://api.spotify.com/v1/me/player/recently-played", params ); while (nextURL) { const hist = await getHistoryURL(service, nextURL); if (hist && hist.items) { console.log(hist.items.length); playHistory.push(...hist.items); } nextURL = hist?.next; console.log(nextURL); } return playHistory; } export async function getUserProfile(service: SpotifyService) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const spotifyResult = await axios.get( "https://api.spotify.com/v1/me", config ); return spotifyResult.data as SpotifyApi.UserProfileResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getPlaybackState(service: SpotifyService) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const spotifyResult = await axios.get( "https://api.spotify.com/v1/me/player", config ); return spotifyResult.data as SpotifyApi.CurrentPlaybackResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function transferPlayback( service: SpotifyService, deviceId: string, play = false ) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; const xferUrl = "https://api.spotify.com/v1/me/player/"; const params = { device_ids: [deviceId], play }; try { const spotifyResult = await axios.put(xferUrl, params, config); return spotifyResult.data; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function play( service: SpotifyService, deviceId: string, uris?: string[], contextUri?: string, trackNumber?: number, seekms?: number ) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; const smallTrack: SpotifyApi.PlayParameterObject = {}; if (contextUri) { smallTrack.context_uri = contextUri; if (trackNumber) { smallTrack.offset = { position: trackNumber }; if (seekms) { smallTrack.position_ms = seekms; } } } else if (uris) { smallTrack.uris = uris; } const playUrl = getUrlWithParams( "https://api.spotify.com/v1/me/player/play", { device_id: deviceId } ); try { const spotifyResult = await axios.put(playUrl, smallTrack, config); return spotifyResult.data; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getDevices(service: SpotifyService) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const spotifyResult = await axios.get( "https://api.spotify.com/v1/me/player/devices", config ); return spotifyResult.data as SpotifyApi.UserDevicesResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function pause(service: SpotifyService, deviceId: string) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; const pauseUrl = getUrlWithParams( "https://api.spotify.com/v1/me/player/pause", { device_id: deviceId } ); try { const spotifyResult = await axios.put(pauseUrl, {}, config); return spotifyResult.data; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } } export async function getQueue(service: SpotifyService) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const spotifyResult = await axios.get( `https://api.spotify.com/v1/me/player/queue?limit=50`, config ); return spotifyResult.data as SpotifyApi.UsersQueueResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function previous(service: SpotifyService, deviceId: string) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const spotifyResult = await axios.post( `https://api.spotify.com/v1/me/player/previous?device_id=${deviceId}`, {}, config ); return spotifyResult.data; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function shuffle( service: SpotifyService, deviceId: string, newShuffleState: boolean ) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const spotifyResult = await axios.put( `https://api.spotify.com/v1/me/player/shuffle?state=${newShuffleState}&device_id=${deviceId}`, {}, config ); return spotifyResult.data; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function next(service: SpotifyService, deviceId: string) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const spotifyResult = await axios.post( `https://api.spotify.com/v1/me/player/next?device_id=${deviceId}`, {}, config ); return spotifyResult.data; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getPlaylists(service: SpotifyService) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const getUri = "https://api.spotify.com/v1/me/playlists"; const spotifyResult = await axios.get(getUri, config); return spotifyResult.data as SpotifyApi.ListOfCurrentUsersPlaylistsResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getAlbumTracks(service: SpotifyService, albumId: string) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const getUri = `https://api.spotify.com/v1/albums/${encodeURIComponent( albumId )}/tracks`; const spotifyResult = await axios.get(getUri, config); return spotifyResult.data as SpotifyApi.AlbumTracksResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function getPlaylistTracks( service: SpotifyService, playlistId: string ) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const getUri = `https://api.spotify.com/v1/playlists/${encodeURIComponent( playlistId )}/tracks`; const spotifyResult = await axios.get(getUri, config); return spotifyResult.data as SpotifyApi.PlaylistTrackResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function deletePlaylist( service: SpotifyService, playlistId: string ) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const deleteUri = `https://api.spotify.com/v1/playlists/${encodeURIComponent( playlistId )}/followers`; const spotifyResult = await axios.delete(deleteUri, config); return spotifyResult.data as SpotifyApi.UnfollowPlaylistResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function createPlaylist( service: SpotifyService, name: string, userId: string, uris: string[], description = "" ) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; try { const createUri = `https://api.spotify.com/v1/users/${userId}/playlists`; const spotifyResult = await axios.post( createUri, { name, public: false, description }, config ); const playlistResponse = spotifyResult.data as SpotifyApi.CreatePlaylistResponse; const addTracksResult = await axios.post( `https://api.spotify.com/v1/playlists/${playlistResponse.id}/tracks`, { uris }, config ); return addTracksResult.data as SpotifyApi.AddTracksToPlaylistResponse; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } return undefined; } export async function setVolume(service: SpotifyService, amt = limitMax) { const config = { headers: { Authorization: `Bearer ${service.retrieveUser().token}`, }, }; const volumeUrl = getUrlWithParams( "https://api.spotify.com/v1/me/player/volume?volume_percent", { volume_percent: amt, } ); try { const spotifyResult = await axios.put(volumeUrl, {}, config); return spotifyResult.data; } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } } function getUrlWithParams(urlString: string, queryParams: Record) { const params = new URLSearchParams(queryParams); const url = new URL(urlString); url.search = params.toString(); return url.toString(); } ================================================ FILE: typescript/examples/music/src/input.txt ================================================ play Taylor Swift Shake It Off get my top 20 favorites and make a playlist named animalTracks of the tracks that have animals in their names get my favorite 100 tracks from the last two months and show only the ones by Bach make it loud get 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 toggle shuffle on and skip to the next track go back to the last song play my playlist class8 play the fourth one show me my queue ================================================ FILE: typescript/examples/music/src/localParser.ts ================================================ import chalk from "chalk"; import axios from "axios"; import path from "path"; import dotenv from "dotenv"; dotenv.config({ path: path.join(__dirname, "../../../.env") }); export async function parseOut(request: string, surl: string) { try { const result = await axios.post(surl, { Text: request, }); console.log(result.data); } catch (e) { if (e instanceof axios.AxiosError) { console.log(e.message); } else { throw e; } } } export function localParser(userPrompt: string) { userPrompt = userPrompt.trim(); const surl = process.env.PARSER_SERVICE_ENDPOINT; if (surl) { parseOut(userPrompt, surl); } if ( userPrompt === "play" || userPrompt === "resume" || userPrompt === "pause" || userPrompt === "next" || userPrompt === "previous" ) { console.log(chalk.green("Instance parsed locally:")); return JSON.stringify({ "@steps": [ { "@func": userPrompt === "play" ? "resume" : userPrompt, "@args": [], }, ], }); } else if (userPrompt.startsWith("play")) { const matchedPlaySelect = userPrompt.match( /play (T|t|track|Track|#|number|Number|no.|No.)?\s?([0-9]+)/ ); if (matchedPlaySelect) { const trackOffset = +matchedPlaySelect[2]; console.log(chalk.green("Instance parsed locally:")); return JSON.stringify({ "@steps": [ { "@func": "getLastTrackList", "@args": [], }, { "@func": "play", "@args": [{ "@ref": 0 }, trackOffset - 1], }, ], }); } } else if (userPrompt.startsWith("shuffle")) { const matchedShuffleSet = userPrompt.match( /shuffle (on|off|true|false|yes|no)/ ); if (matchedShuffleSet) { const shuffleArg = matchedShuffleSet[1]; let shuffleFunc = ""; if (["on", "true", "yes"].includes(shuffleArg)) { shuffleFunc = "shuffleOn"; } else if (["off", "false", "no"].includes(shuffleArg)) { shuffleFunc = "shuffleOff"; } if (shuffleFunc.length > 0) { return JSON.stringify({ "@steps": [ { "@func": shuffleFunc, "@args": [], }, ], }); } } } return undefined; } ================================================ FILE: typescript/examples/music/src/main.ts ================================================ import fs from "fs"; import path from "path"; import readline from "readline/promises"; import { Authzor } from "./authz"; import chalk from "chalk"; import dotenv from "dotenv"; import * as Filter from "./trackFilter"; import { createLanguageModel, getData, } from "typechat"; import { createProgramTranslator, Program, createModuleTextFromProgram, evaluateJsonProgram, } from "typechat/ts"; import { AlbumTrackCollection, ITrackCollection, PlaylistTrackCollection, TrackCollection, } from "./trackCollections"; import { applyFilterExpr } from "./trackFilter"; import { play, getUserProfile, getDevices, search, setVolume, limitMax, getTopK, createPlaylist, deletePlaylist, getPlaylists, getPlaybackState, getPlaylistTracks, pause, next, previous, shuffle, getAlbumTracks, getQueue, getRecent, } from "./endpoints"; import { listAvailableDevices, printStatus, selectDevice } from "./playback"; import { SpotifyService, User } from "./service"; import { localParser } from "./localParser"; dotenv.config({ path: path.join(__dirname, "../../../.env") }); const schemaFilename = "chatifyActionsSchema.ts"; const model = createLanguageModel(process.env); // open schema file containing ts definitions const schemaText = fs.readFileSync( path.join(__dirname, schemaFilename), "utf8" ); const keys = { clientId: process.env.SPOTIFY_APP_CLI, clientSecret: process.env.SPOTIFY_APP_CLISEC, }; export interface IClientContext { service: SpotifyService; deviceId?: string; user: User; lastTrackList?: SpotifyApi.TrackObjectFull[]; lastTrackOffset: number; lastTrackCount: number; } async function printTrackNames( tracks: SpotifyApi.TrackObjectFull[], context: IClientContext ) { let count = 1; for (const track of tracks) { let prefix = ""; if (context && tracks.length > 1) { prefix = `T${count}: `; } console.log(chalk.cyanBright(`${prefix}${track.name}`)); const artists = " Artists: " + track.artists.map((artist) => chalk.green(artist.name)).join(", "); console.log(artists); console.log(" Album: " + chalk.rgb(181, 101, 29)(track.album.name)); count++; } if (tracks.length > 1) { context.lastTrackList = tracks; context.lastTrackOffset = 0; context.lastTrackCount = count; } } async function printPlaylist( playlist: SpotifyApi.PlaylistObjectSimplified, fetchedTracks: SpotifyApi.TrackObjectFull[], context: IClientContext ) { console.log(chalk.cyanBright(`Starting playlist --> ${playlist.name}`)); console.log( chalk.cyanBright(`--------------------------------------------`) ); const playlistTotalTracks = playlist.tracks.total; console.log( chalk.cyan( `First ${fetchedTracks.length} out of ${playlistTotalTracks} songs in list` ) ); fetchedTracks.forEach((track, i) => { console.log( chalk.cyan( ` ${i < 99 ? (i < 9 ? " " : " ") : ""}${i + 1} - ${track.name}` ) ); }); console.log( chalk.cyanBright(`--------------------------------------------`) ); } function chalkPlan(plan: Program) { console.log(chalk.green("Plan Validated:")); const lines = JSON.stringify(plan, null, 4).split("\n"); for (let i = 0; i < lines.length; i++) { lines[i] = lines[i].replace( /"([^"]+)"(:?)|([0-9]+)/g, (match, word, colon, integer) => { if (integer) { return chalk.hex("#B5CEA8")(integer); } else if (colon) { return `"${chalk.cyan(word)}":`; } else { return `"${chalk.rgb(181, 101, 29)(word)}"`; } } ); console.log(lines[i]); } } async function getClientContext(token: string) { const clientData = { clientId: keys.clientId ? keys.clientId : "", clientSecret: keys.clientSecret ? keys.clientSecret : "", }; const service = new SpotifyService(clientData); service.storeUser({ username: "musicLover", token, }); await service.init(); const userdata = await getUserProfile(service); const user = service.retrieveUser(); user.id = userdata?.id; user.username = userdata?.display_name; const devices = await getDevices(service); let deviceId; if (devices && devices.devices.length > 0) { const activeDevice = devices.devices.find((device) => device.is_active) ?? devices.devices[0]; deviceId = activeDevice.id; } return { deviceId, service, } as IClientContext; } const translator = createProgramTranslator(model, schemaText); async function handleCall( func: string, args: unknown[], clientContext: IClientContext ): Promise { let result: ITrackCollection | undefined = undefined; switch (func) { case "play": { const input = args[0] as ITrackCollection; if (input && input.getTrackCount() > 0) { let startIndex = args[1] ? +args[1] : 0; const count = args[2] ? +args[2] : 1; if (startIndex < 0) { startIndex = input.getTrackCount() + startIndex; } const fetchedTracks = await input.getTracks( clientContext.service ); const contextUri = input.getContext(); const tracks = fetchedTracks!.slice( startIndex, startIndex + count ); const uris = tracks.map((track) => track.uri); console.log(chalk.cyanBright("Playing...")); printTrackNames(tracks, clientContext); if (clientContext.deviceId) { await play( clientContext.service, clientContext.deviceId, uris, contextUri ); } } else if (clientContext.deviceId) { await play(clientContext.service, clientContext.deviceId); } break; } case "printTracks": { const input = args[0] as ITrackCollection; if (input) { const fetchedTracks = await input.getTracks( clientContext.service ); const playlist = input.getPlaylist(); if (playlist) { printPlaylist(playlist, fetchedTracks, clientContext); } else { printTrackNames(fetchedTracks, clientContext); } } break; } case "status": { await printStatus(clientContext); break; } case "getQueue": { const currentQueue = await getQueue(clientContext.service); if (currentQueue) { // not yet supporting episidoes const filtered = currentQueue.queue.filter( (item) => item.type === "track" ) as SpotifyApi.TrackObjectFull[]; console.log(chalk.magentaBright("Current Queue:")); console.log( chalk.cyanBright( `--------------------------------------------` ) ); await printTrackNames(filtered, clientContext); console.log( chalk.cyanBright( `--------------------------------------------` ) ); await printStatus(clientContext); } break; } case "pause": { if (clientContext.deviceId) { await pause(clientContext.service, clientContext.deviceId); await printStatus(clientContext); } break; } case "next": { if (clientContext.deviceId) { await next(clientContext.service, clientContext.deviceId); await printStatus(clientContext); } break; } case "previous": { if (clientContext.deviceId) { await previous(clientContext.service, clientContext.deviceId); await printStatus(clientContext); } break; } case "shuffleOn": { if (clientContext.deviceId) { await shuffle( clientContext.service, clientContext.deviceId, true ); await printStatus(clientContext); } break; } case "shuffleOff": { if (clientContext.deviceId) { await shuffle( clientContext.service, clientContext.deviceId, false ); await printStatus(clientContext); } break; } case "resume": { if (clientContext.deviceId) { await play(clientContext.service, clientContext.deviceId); await printStatus(clientContext); } break; } case "listDevices": { await listAvailableDevices(clientContext); break; } case "selectDevice": { if (clientContext.deviceId) { const keyword = args[0] as string; await selectDevice(keyword, clientContext); } break; } case "setVolume": { let newVolumeLevel = args[0] as number; if (newVolumeLevel > 50) { newVolumeLevel = 50; } console.log( chalk.yellowBright(`setting volume to ${newVolumeLevel} ...`) ); await setVolume(clientContext.service, newVolumeLevel); break; } case "changeVolume": { const volumeChangeAmount = args[0] as number; const playback = await getPlaybackState(clientContext.service); if (playback && playback.device) { const volpct = playback.device.volume_percent || 50; let nv = Math.floor( (1.0 + volumeChangeAmount / 100.0) * volpct ); if (nv > 50) { nv = 50; } console.log(chalk.yellowBright(`setting volume to ${nv} ...`)); await setVolume(clientContext.service, nv); } break; } case "searchTracks": { const queryString = args[0] as string; const query: SpotifyApi.SearchForItemParameterObject = { q: queryString, type: "track", limit: 50, offset: 0, }; const data = await search(query, clientContext.service); if (data && data.tracks) { result = new TrackCollection( data.tracks.items, data.tracks.items.length ); 1; } break; } case "getLastTrackList": { if (clientContext && clientContext.lastTrackList) { result = new TrackCollection( clientContext.lastTrackList, clientContext.lastTrackCount ); } break; } case "listPlaylists": { const playlists = await getPlaylists(clientContext.service); if (playlists) { for (const playlist of playlists.items) { console.log(chalk.magentaBright(`${playlist.name}`)); } } break; } case "getPlaylist": { const playlistName = args[0] as string; const playlists = await getPlaylists(clientContext.service); const playlist = playlists?.items.find((playlist) => { return playlist.name .toLowerCase() .includes(playlistName.toLowerCase()); }); if (playlist) { const playlistResponse = await getPlaylistTracks( clientContext.service, playlist.id ); // TODO: add paging if (playlistResponse) { result = new PlaylistTrackCollection( playlist, playlistResponse.items.map((item) => item.track!) ); } } break; } case "getAlbum": { const name = args[0] as string; if (name.length > 0) { // search for album by name and load it as track collection } else { // get album of current playing track and load it as track collection const status = await getPlaybackState(clientContext.service); if (status && status.item && status.item.type === "track") { const track = status.item as SpotifyApi.TrackObjectFull; const album = track.album; // TODO: add paging const getTracksResponse = await getAlbumTracks( clientContext.service, album.id ); if (status.is_playing) { await play( clientContext.service, clientContext.deviceId!, [], album.uri, status.item.track_number - 1, status.progress_ms ? status.progress_ms : 0 ); } if (getTracksResponse) { result = new AlbumTrackCollection( album, getTracksResponse.items ); } } } break; } case "getFavorites": { const countOption = args[0] as number; let count = limitMax; if (countOption !== undefined) { count = countOption; } const tops = await getTopK(clientContext.service, count); if (tops) { const tracks = tops.map((pto) => pto.track!); result = new TrackCollection(tracks, tracks.length); } break; } case "filterTracks": { const trackCollection = args[0] as ITrackCollection; let filterType = args[1] as string; const filterText = args[2] as string; const negate = args[3] as boolean; // TODO: add filter validation to overall instance validation if (filterType === "name") { filterType = "description"; } const filter = filterType + ":" + filterText; const parseResult = Filter.parseFilter(filter); if (parseResult.ast) { const trackList = await trackCollection.getTracks( clientContext.service ); if (trackList) { const tracks = await applyFilterExpr( clientContext, model, parseResult.ast, trackList, negate ); result = new TrackCollection(tracks, tracks.length); } } else { console.log(parseResult.diagnostics); } break; } case "createPlaylist": { const input = args[0] as ITrackCollection; const name = args[1] as string; const trackList = await input.getTracks(clientContext.service); if (input && trackList.length > 0) { const uris = trackList.map((track) => (track ? track.uri : "")); await createPlaylist( clientContext.service, name, clientContext.service.retrieveUser().id!, uris, name ); console.log(`playlist ${name} created with tracks:`); printTrackNames(trackList, clientContext); } else { console.log(chalk.red("no input tracks for createPlaylist")); } break; } case "deletePlaylist": { const playlistCollection = args[0] as PlaylistTrackCollection; if (playlistCollection) { const playlist = playlistCollection.getPlaylist(); await deletePlaylist(clientContext.service, playlist.id); console.log( chalk.magentaBright(`playlist ${playlist.name} deleted`) ); break; } break; } case "unknownAction": { const text = args[0] as string; console.log(`Text not understood in this context: ${text}`); break; } case "nonMusicQuestion": { const text = args[0] as string; const ret = await model.complete(text); if (ret.success) { console.log(ret.data); } break; } } return result; } // set this to false to just look at llm generation without Spotify connection const spotifyConnect = true; export async function index(context: IClientContext) { let playHistory = await getRecent( context.service, Date.parse("2018-01-01T00:00:00.00Z") ); if (playHistory) { console.log(playHistory?.length); let trackNames = ''; playHistory.map((item) => { trackNames += item.track.name + '\n'; }); fs.writeFileSync("bigFetch.txt", trackNames); } } function checkAck(input: string, program: Program): Program | undefined { const linput = input.toLocaleLowerCase(); if (["y","yes","ok"].includes(linput)) { return program; } else { return undefined; } } // whether to confirm each action with the user const confirmMode = true; // Process requests interactively (no batch mode for now) async function musicApp() { const authz = new Authzor(); authz.authorize(spotifyConnect, async (token) => { let context: IClientContext | undefined = undefined; if (token) { context = await getClientContext(token); } else { console.log( chalk.yellow( "Spotify connection not active: showing plans only" ) ); } const musicPrompt = "🎵> "; const confirmPrompt = "👍👎 (answer y/n)> "; const stdio = readline.createInterface({ input: process.stdin, output: process.stdout }); while (true) { const request = await stdio.question(musicPrompt); if (request.toLowerCase() === "quit" || request.toLowerCase() === "exit") { stdio.close(); return; } const localResult = localParser(request); let program: Program | undefined = undefined; if (localResult) { program = JSON.parse(localResult) as Program; } else { const response = await translator.translate(request); if (!response.success) { console.log(response.message); continue; } program = response.data; } if (program !== undefined) { chalkPlan(program); console.log(getData(createModuleTextFromProgram(program))); if (confirmMode && (!localResult)) { const input = await stdio.question(confirmPrompt); program = checkAck(input, program); if (program === undefined) { console.log("Thanks for the feedback. Canceling execution...") continue; } } if (context !== undefined) { const result = await evaluateJsonProgram( program, async (func, args) => { return await handleCall(func, args, context!); } ); if (result !== undefined) { const collection = result as ITrackCollection; const trackList = await collection.getTracks( context.service ); if (trackList) { printTrackNames(trackList, context); context.lastTrackList = trackList; } } } } } }); } musicApp(); ================================================ FILE: typescript/examples/music/src/playback.ts ================================================ import { getDevices, getPlaybackState, transferPlayback } from "./endpoints"; import { IClientContext } from "./main"; import chalk from "chalk"; // convert milliseconds to elapsed minutes and seconds as a string function msToElapsedMinSec(ms: number) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; // add leading zero if needed if (remainingSeconds < 10) { return `${minutes}:0${remainingSeconds}`; } else { return `${minutes}:${remainingSeconds}`; } } const pauseSymbol = "⏸️"; const playSymbol = "▶️"; export function chalkStatus(status: SpotifyApi.CurrentPlaybackResponse) { if (status.item) { let timePart = msToElapsedMinSec(status.item.duration_ms); if (status.progress_ms) { timePart = `${msToElapsedMinSec(status.progress_ms)}/${timePart}`; } let symbol = status.is_playing ? playSymbol : pauseSymbol; console.log( `${symbol} ${timePart} ${chalk.cyanBright(status.item.name)}` ); if (status.item.type === "track") { const artists = " Artists: " + status.item.artists .map((artist) => chalk.green(artist.name)) .join(", "); console.log(artists); } } } export async function printStatus(context: IClientContext) { const status = await getPlaybackState(context.service); if (status) { chalkStatus(status); } else { console.log("Nothing playing according to Spotify."); } const devices = await getDevices(context.service); if (devices && devices.devices.length > 0) { const activeDevice = devices.devices.find((device) => device.is_active) ?? devices.devices[0]; if (activeDevice) { console.log( " Active device: " + chalk.magenta(`${activeDevice.name} of type ${activeDevice.type}`) ); } else { for (const device of devices.devices) { console.log( chalk.magenta( ` Device ${device.name} of type ${device.type} is available` ) ); } } } } export async function selectDevice(keyword: string, context: IClientContext) { const devices = await getDevices(context.service); if (devices && devices.devices.length > 0) { for (const device of devices.devices) { if ( device.name.toLowerCase().includes(keyword.toLowerCase()) || device.type.toLowerCase().includes(keyword.toLowerCase()) ) { const status = await getPlaybackState(context.service); if (status) { if (status.device.id === device.id) { console.log( chalk.green(`Device ${device.name} is already selected`) ); return; } await transferPlayback( context.service, device.id!, status.is_playing ); } context.deviceId = device.id!; console.log( chalk.green(`Selected device ${device.name} of type ${device.type}`) ); } } } else { console.log(chalk.red("No devices matched keyword")); } } export async function listAvailableDevices(context: IClientContext) { const devices = await getDevices(context.service); if (devices && devices.devices.length > 0) { let count = 0; for (const device of devices.devices) { console.log( chalk.magenta( `Device ${device.name} of type ${device.type} is available` ) ); count++; } } } ================================================ FILE: typescript/examples/music/src/service.ts ================================================ import axios from 'axios'; export type ClientData = { clientId: string; clientSecret: string; }; export type User = { username?: string; token: string; id?: string; }; export class SpotifyService { private accessToken?: string; private clientId: string; private clientSecret: string; private loggedIn: boolean; private loggedInUser: User | null; constructor(clientData: ClientData) { this.clientId = clientData.clientId; this.clientSecret = clientData.clientSecret; this.loggedIn = false; this.loggedInUser = null; } storeToken(token: string): string { this.accessToken = token; return this.accessToken; } retrieveToken(): string { if (!this.accessToken) { throw new Error('SpotifyService: no accessToken'); } return this.accessToken; } isLoggedIn(): boolean { return this.loggedIn; } retrieveUser(): User { if (this.loggedInUser === null) { throw new Error('SpotifyService: no loggedInUser'); } return this.loggedInUser; } storeUser(user: User) { this.loggedInUser = user; } async init(): Promise { const authConfig = { headers: { Authorization: `Basic ${Buffer.from( `${this.clientId}:${this.clientSecret}` ).toString('base64')}`, }, }; try { const authData = await axios.post( 'https://accounts.spotify.com/api/token', 'grant_type=client_credentials', authConfig ); this.storeToken(authData.data.access_token); return authData.data; } catch (e) { // TODO: REVIEW: should we really be returning the response // data in an error condition? if (e instanceof axios.AxiosError) { // TODO: REVIEW: the type returned here may not be Promise return e.response?.data; } else { throw e; } } } } ================================================ FILE: typescript/examples/music/src/trackCollections.ts ================================================ import { SpotifyService } from "./service"; // for now, no paging of track lists; later add offset and count export interface ITrackCollection { getTrackCount(): number; getTracks(service: SpotifyService): Promise; getContext(): string | undefined; getPlaylist(): SpotifyApi.PlaylistObjectSimplified | undefined; } export class TrackCollection implements ITrackCollection { contextUri: string | undefined = undefined; constructor(public tracks: SpotifyApi.TrackObjectFull[], public trackCount:number) { } getContext() { return this.contextUri; } async getTracks( service: SpotifyService ) { return this.tracks; } getTrackCount(): number { return this.trackCount; } getPlaylist(): SpotifyApi.PlaylistObjectSimplified | undefined { return undefined; } } export class PlaylistTrackCollection extends TrackCollection { constructor(public playlist: SpotifyApi.PlaylistObjectSimplified, tracks: SpotifyApi.TrackObjectFull[]) { super(tracks,0); this.contextUri = playlist.uri; this.trackCount = tracks.length; } getPlaylist() { return this.playlist; } } export class AlbumTrackCollection extends TrackCollection { constructor(public album: SpotifyApi.AlbumObjectSimplified, tracks: SpotifyApi.TrackObjectSimplified[] ) { super([], 0); this.contextUri = album.uri; this.trackCount = tracks.length; this.tracks = tracks.map((albumItem) => { const fullTrack = albumItem as SpotifyApi.TrackObjectFull; fullTrack.album = album; return fullTrack; }); } } ================================================ FILE: typescript/examples/music/src/trackFilter.ts ================================================ import { TypeChatLanguageModel } from "typechat"; import { getArtist } from "./endpoints"; import { IClientContext } from "./main"; export enum FilterTokenType { Genre, Artist, Year, Description, Colon, AND, OR, LParen, RParen, Value, } // split a string into an array of non-whitespace strings function splitNonWhitespace(str: string): string[] { const nested = str.split(/\s+/).map((w) => w.split(/\b/)); return nested.flat().filter((w) => w.length > 0); } interface FilterToken { type: FilterTokenType; rawValue?: string; } function tokenize(filter: string) { const nonws = splitNonWhitespace(filter); const tokens: FilterToken[] = []; for (const rawtok of nonws) { const tok = rawtok.toLowerCase(); if (tok === 'and') { tokens.push({ type: FilterTokenType.AND }); } else if (tok === 'or') { tokens.push({ type: FilterTokenType.OR }); } else if (tok === '(') { tokens.push({ type: FilterTokenType.LParen }); } else if (tok === ')') { tokens.push({ type: FilterTokenType.RParen }); } else if (tok === ':') { tokens.push({ type: FilterTokenType.Colon }); } else if (tok === 'genre') { tokens.push({ type: FilterTokenType.Genre }); } else if (tok === 'artist') { tokens.push({ type: FilterTokenType.Artist }); } else if (tok === 'year') { tokens.push({ type: FilterTokenType.Year }); } else if (tok === 'description') { tokens.push({ type: FilterTokenType.Description }); } else { tokens.push({ type: FilterTokenType.Value, rawValue: rawtok }); } } return tokens; } export enum FilterConstraintType { Genre = 'genre', Artist = 'artist', Year = 'year', Description = 'description', } export enum FilterCombinerType { AND = 'AND', OR = 'OR', } export interface FilterCombiner { type: 'combiner'; combinerType: FilterCombinerType; operands: FilterNode[]; } export interface FilterConstraint { type: 'constraint'; constraintType: FilterConstraintType; constraintValue: string; } export type FilterNode = FilterConstraint | FilterCombiner; export interface IFilterResult { diagnostics?: string[]; ast?: FilterNode; } function makeFilterCombiner(combinerType = FilterCombinerType.AND) { return { type: 'combiner', combinerType, operands: [] } as FilterCombiner; } // map filter token type to filter constraint type const filterConstraintTypeMap = new Map([ [FilterTokenType.Genre, FilterConstraintType.Genre], [FilterTokenType.Artist, FilterConstraintType.Artist], [FilterTokenType.Year, FilterConstraintType.Year], [FilterTokenType.Description, FilterConstraintType.Description], ]); function makeFilterConstraint( constraintType: FilterConstraintType, constraintValue?: string ) { return { type: 'constraint', constraintType, constraintValue, } as FilterConstraint; } function isValueBoundary(tokenType: FilterTokenType) { return ( tokenType !== FilterTokenType.Colon && tokenType !== FilterTokenType.Value ); } export function filterNodeToString(node: FilterNode, depth = 0): string { if (node.type === 'combiner') { return ( '(' + node.combinerType + ' ' + node.operands .map((op) => filterNodeToString(op, depth + 1)) .join(' ') + ')' ); } else { return node.constraintType + ':' + node.constraintValue; } } function simplifyFilterNode(ast: FilterNode): FilterNode { if (ast.type === 'combiner') { if (ast.operands.length === 1) { return simplifyFilterNode(ast.operands[0]); } else { for (let i = 0; i < ast.operands.length; i++) { ast.operands[i] = simplifyFilterNode(ast.operands[i]); } return ast; } } else { return ast; } } interface FilterStackFrame { pendingOr?: FilterCombiner; andExpr: FilterCombiner; } export function parseFilter(filter: string): IFilterResult { const tokens = tokenize(filter); let pendingConstraint: FilterConstraint | undefined = undefined; const stack: FilterStackFrame[] = [{ andExpr: makeFilterCombiner() }]; for (const token of tokens) { if (isValueBoundary(token.type)) { if (pendingConstraint) { stack[stack.length - 1].andExpr.operands.push( pendingConstraint ); pendingConstraint = undefined; } } if ( token.type === FilterTokenType.Genre || token.type === FilterTokenType.Artist || token.type === FilterTokenType.Year || token.type === FilterTokenType.Description ) { if (pendingConstraint !== undefined) { return { diagnostics: ['Nested constraint prefix'] }; } else { pendingConstraint = makeFilterConstraint( filterConstraintTypeMap.get(token.type)!, '' ); } } else if (token.type === FilterTokenType.Colon) { if (!pendingConstraint) { return { diagnostics: ["Expected constraint type before ':'"] }; } } else if (token.type === FilterTokenType.AND) { // do nothing; always in an AND } else if (token.type === FilterTokenType.OR) { const orNode = makeFilterCombiner(FilterCombinerType.OR); const top = stack[stack.length - 1]; if (top.pendingOr) { top.pendingOr.operands.push(top.andExpr); orNode.operands.push(top.pendingOr); } else { orNode.operands.push(top.andExpr); } top.pendingOr = orNode; top.andExpr = makeFilterCombiner(); } else if (token.type === FilterTokenType.LParen) { stack.push({ andExpr: makeFilterCombiner() }); } else if (token.type === FilterTokenType.RParen) { if (stack.length === 1) { return { diagnostics: ['Mismatched )'] }; } const prevTop = stack.pop()!; if (prevTop.pendingOr) { prevTop.pendingOr.operands.push(prevTop.andExpr); stack[stack.length - 1].andExpr.operands.push( prevTop.pendingOr ); } else { stack[stack.length - 1].andExpr.operands.push(prevTop.andExpr); } } else if (token.type === FilterTokenType.Value) { if (!pendingConstraint) { console.log(token.rawValue); return { diagnostics: [ 'Unexpected: value without constraint prefix', ], }; } else { if (pendingConstraint.constraintValue.length > 0) { pendingConstraint.constraintValue += ' '; } pendingConstraint.constraintValue += token.rawValue!; } } } if (pendingConstraint) { stack[stack.length - 1].andExpr.operands.push(pendingConstraint); } if (stack.length !== 1) { return { diagnostics: ['Mismatched ('] }; } const top = stack[0]; if (top.pendingOr) { top.pendingOr.operands.push(top.andExpr); return { ast: simplifyFilterNode(top.pendingOr) }; } else { return { ast: simplifyFilterNode(top.andExpr) }; } } const filterDiag = false; export async function applyFilterExpr( clientContext: IClientContext, model: TypeChatLanguageModel, filterExpr: FilterNode, tracks: SpotifyApi.TrackObjectFull[], negate = false ): Promise { if (tracks.length === 0) { return tracks; } switch (filterExpr.type) { case "constraint": switch (filterExpr.constraintType) { case FilterConstraintType.Genre: { process.stdout.write( `fetching genre for ${tracks.length} tracks` ); const genre = filterExpr.constraintValue; const results = [] as SpotifyApi.TrackObjectFull[]; for (const track of tracks) { process.stdout.write("."); const wrapper = await getArtist( clientContext.service, track.album.artists[0].id ); if (wrapper) { let hit = wrapper.artists[0].genres.includes(genre); if (negate) { hit = !hit; } if (hit) { results.push(track); } } } process.stdout.write("\n"); tracks = results; break; } case FilterConstraintType.Artist: { const results = [] as SpotifyApi.TrackObjectFull[]; for (const track of tracks) { let hit = false; for (const artist of track.artists) { if (filterDiag) { console.log( `${artist.name.toLowerCase()} vs ${filterExpr.constraintValue.toLowerCase()}` ); } if ( artist.name .toLowerCase() .includes( filterExpr.constraintValue.toLowerCase() ) ) { hit = true; } if (negate) { hit = !hit; } if (hit) { results.push(track); } if (hit) { break; } } } process.stdout.write("\n"); tracks = results; break; } case FilterConstraintType.Year: { const results = [] as SpotifyApi.TrackObjectFull[]; for (const track of tracks) { // TODO year ranges if (filterDiag) { console.log( `${track.album.release_date} vs ${filterExpr.constraintValue}` ); } if ( track.album.release_date.includes( filterExpr.constraintValue ) ) { results.push(track); } } tracks = results; break; } case FilterConstraintType.Description: { const results = [] as SpotifyApi.TrackObjectFull[]; const indicesResult = await llmFilter( model, filterExpr.constraintValue, tracks ); if (indicesResult.success) { if (indicesResult.data) { const indices = JSON.parse(indicesResult.data) as { trackNumbers: number[]; }; for (const j of indices.trackNumbers) { results.push(tracks[j]); } } } tracks = results; break; } } break; case "combiner": if (filterExpr.combinerType === FilterCombinerType.AND) { for (const childExpr of filterExpr.operands) { tracks = await applyFilterExpr( clientContext, model, childExpr, tracks, negate ); } } else if ( filterExpr.combinerType === FilterCombinerType.OR ) { let subTracks = [] as SpotifyApi.TrackObjectFull[]; for (const childExpr of filterExpr.operands) { subTracks = subTracks.concat( await applyFilterExpr( clientContext, model, childExpr, tracks, negate ) ); } tracks = uniqueTracks(subTracks); } break; } return tracks; } function uniqueTracks(tracks: SpotifyApi.TrackObjectFull[]) { const map = new Map(); for (const track of tracks) { map.set(track.id, track); } return [...map.values()]; } async function llmFilter( model: TypeChatLanguageModel, description: string, tracks: SpotifyApi.TrackObjectFull[] ) { let prompt = "The following is a numbered list of music tracks, one track per line\n"; for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; prompt += `${i}: ${track.name}\n`; } prompt += `Use the following TypeScript type to output the track names that match the description ${description}: type Matches = { trackNumbers: number[]; };\n`; prompt += `Here is a JSON object of type Matches containing the track numbers of the tracks that match ${description}:\n`; const ret = await model.complete(prompt); return ret; } // the remainder is for testing const testFilters = [ 'artist:elton john OR artist: bach', 'genre:baroque AND description:animals', 'genre:baroque OR description:animals', 'genre:baroque OR description:animals OR artist:bach', 'genre:baroque OR (description:animals OR artist:bach)', 'genre:baroque (description : animals OR artist: bach)', 'genre:baroque artist:toscanini (description:animals OR artist:bach AND artist:swift)', 'genre:baroque artist:toscanini year: 1941 (description:animals OR artist:bach AND artist:swift)', 'genre:grunge artist:cobain year: 1992-1997 OR (description:animals AND artist:swift)', 'genre:grunge artist:cobain year: 1992-1997 OR (description:animals AND artist:swift) OR (genre:baroque AND artist:bach)', ]; // if this is the main module, run some tests if (require.main === module) { for (const filter of testFilters) { const result = parseFilter(filter); console.log(filter); if (result.diagnostics) { console.log(result.diagnostics); } else if (result.ast) { console.log(filterNodeToString(result.ast)); } } } ================================================ FILE: typescript/examples/music/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node", "spotify-api"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "inlineSourceMap": true, "declaration": true, "composite": true } } ================================================ FILE: typescript/examples/restaurant/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "/**" ], "program": "${workspaceFolder}/dist/main.js", "console": "externalTerminal" } ] } ================================================ FILE: typescript/examples/restaurant/README.md ================================================ # Restaurant The Restaurant example shows how to capture user intent as a set of "nouns", but with more complex linguistic input. This 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. This example also shows how we can create a "user intent summary" to display to a user. It uses a natural language experience for placing an order with the [`Order`](./src/foodOrderViewSchema.ts) type. # Try Restaurant To run the Restaurant example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For example, given the following order: **Input**: ``` 🍕> 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. ``` **Output**: *This is GPT-4-0613 output; GPT-3.5-turbo and most other models miss this one.* ``` 1 large pizza with mushrooms 1 large pizza with sausage 1 small pizza with sausage 1 whole Greek salad 1 Pale Ale 1 Mack and Jacks ``` > **Note** > > Across different models, you may see that model responses may not correspond to the user intent. > In the above example, some models may not be able to capture the fact that the order is still only for 3 pizzas, > and that "make one sausage a small" is not a request for a new pizza. > > ```diff > 1 large pizza with mushrooms > - 1 large pizza with sausage > + 2 large pizza with sausage > 1 small pizza with sausage > 1 whole Greek salad > 1 Pale Ale > 1 Mack and Jacks > ``` > > The output here from GPT 3.5-turbo incorrectly shows 1 mushroom pizza and 3 sausage pizzas. Because 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. In 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: ```typescript function printOrder(order: Order) { ``` Having a validated, typed data structure simplifies the task of generating a succinct summary suitable for user confirmation. ================================================ FILE: typescript/examples/restaurant/package.json ================================================ { "name": "restaurant", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/restaurant/src/foodOrderViewSchema.ts ================================================ // an order from a restaurant that serves pizza, beer, and salad export type Order = { items: (OrderItem | UnknownText)[]; }; export type OrderItem = Pizza | Beer | Salad; // Use this type for order items that match nothing else export interface UnknownText { itemType: "unknown", text: string; // The text that wasn't understood } export type Pizza = { itemType: "pizza"; // default: large size?: "small" | "medium" | "large" | "extra large"; // toppings requested (examples: pepperoni, arugula) addedToppings?: string[]; // toppings requested to be removed (examples: fresh garlic, anchovies) removedToppings?: string[]; // default: 1 quantity?: number; // used if the requester references a pizza by name name?: "Hawaiian" | "Yeti" | "Pig In a Forest" | "Cherry Bomb"; }; export type Beer = { itemType: "beer"; // examples: Mack and Jacks, Sierra Nevada Pale Ale, Miller Lite kind: string; // default: 1 quantity?: number; }; export const saladSize = ["half", "whole"]; export const saladStyle = ["Garden", "Greek"]; export type Salad = { itemType: "salad"; // default: half portion?: string; // default: Garden style?: string; // ingredients requested (examples: parmesan, croutons) addedIngredients?: string[]; // ingredients requested to be removed (example: red onions) removedIngredients?: string[]; // default: 1 quantity?: number; }; ================================================ FILE: typescript/examples/restaurant/src/input.txt ================================================ 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. I'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. I'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. 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. I would like to order one with basil and one with extra sauce. Throw in a salad and an ale. I 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. I'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. I sure am hungry for a pizza with pepperoni and a salad with no croutons. And I'm thirsty for 3 Pale Ales give me three regular salads and two Greeks and make the regular ones with no red onions I'll take four large pepperoni pizzas. Put extra sauce on two of them. plus an M&J and a Pale Ale I'll take a yeti, a pale ale and a large with olives and take the extra cheese off the yeti and add a Greek I'll take a medium Pig with no arugula I'll take a small Pig with no arugula and a Greek with croutons and no red onions ================================================ FILE: typescript/examples/restaurant/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createJsonTranslator, createLanguageModel, } from "typechat"; import { processRequests, } from "typechat/interactive"; import { createTypeScriptJsonValidator, } from "typechat/ts"; import { Order } from "./foodOrderViewSchema"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const viewSchema = fs.readFileSync( path.join(__dirname, "foodOrderViewSchema.ts"), "utf8" ); const validator = createTypeScriptJsonValidator(viewSchema, "Order"); const translator = createJsonTranslator(model, validator); const saladIngredients = [ "lettuce", "tomatoes", "red onions", "olives", "peppers", "parmesan", "croutons", ]; const pizzaToppings = [ "pepperoni", "sausage", "mushrooms", "basil", "extra cheese", "extra sauce", "anchovies", "pineapple", "olives", "arugula", "Canadian bacon", "Mama Lil's Peppers", ]; // a function that takes two arrays of strings a and b and removes from a and b // all strings that are in both a and b function removeCommonStrings(a: string[], b: string[]) { const aSet = new Set(a); const bSet = new Set(b); for (const item of aSet) { if (bSet.has(item)) { aSet.delete(item); bSet.delete(item); } } return [Array.from(aSet), Array.from(bSet)]; } const namedPizzas = new Map([ ["Hawaiian", ["pineapple", "Canadian bacon"]], ["Yeti", ["extra cheese", "extra sauce"]], ["Pig In a Forest", ["mushrooms", "basil", "Canadian bacon", "arugula"]], ["Cherry Bomb", ["pepperoni", "sausage", "Mama Lil's Peppers"]], ]); function printOrder(order: Order) { if (order.items && order.items.length > 0) { for (const item of order.items) { if (item.itemType === "unknown") { break; } switch (item.itemType) { case "pizza": { if (item.name) { const addedToppings = namedPizzas.get(item.name); if (addedToppings) { if (item.addedToppings) { item.addedToppings = item.addedToppings.concat(addedToppings); } else { item.addedToppings = addedToppings; } } } if (!item.size) { item.size = "large"; } let quantity = 1; if (item.quantity) { quantity = item.quantity; } let pizzaStr = ` ${quantity} ${item.size} pizza`; if (item.addedToppings && item.removedToppings) { [item.addedToppings, item.removedToppings] = removeCommonStrings(item.addedToppings, item.removedToppings); } if (item.addedToppings && item.addedToppings.length > 0) { pizzaStr += " with"; for (const [index, addedTopping] of item.addedToppings.entries()) { if (pizzaToppings.includes(addedTopping)) { pizzaStr += `${index === 0 ? " " : ", "}${addedTopping}`; } else { console.log(`We are out of ${addedTopping}`); } } } if (item.removedToppings && item.removedToppings.length > 0) { pizzaStr += " and without"; for (const [ index, removedTopping, ] of item.removedToppings.entries()) { pizzaStr += `${index === 0 ? " " : ", "}${removedTopping}`; } } console.log(pizzaStr); break; } case "beer": { let quantity = 1; if (item.quantity) { quantity = item.quantity; } const beerStr = ` ${quantity} ${item.kind}`; console.log(beerStr); break; } case "salad": { let quantity = 1; if (item.quantity) { quantity = item.quantity; } if (!item.portion) { item.portion = "half"; } if (!item.style) { item.style = "Garden"; } let saladStr = ` ${quantity} ${item.portion} ${item.style} salad`; if (item.addedIngredients && item.removedIngredients) { [item.addedIngredients, item.removedIngredients] = removeCommonStrings(item.addedIngredients, item.removedIngredients); } if (item.addedIngredients && item.addedIngredients.length > 0) { saladStr += " with"; for (const [ index, addedIngredient, ] of item.addedIngredients.entries()) { if (saladIngredients.includes(addedIngredient)) { saladStr += `${index === 0 ? " " : ", "}${addedIngredient}`; } else { console.log(`We are out of ${addedIngredient}`); } } } if (item.removedIngredients && item.removedIngredients.length > 0) { saladStr += " without"; for (const [ index, removedIngredient, ] of item.removedIngredients.entries()) { saladStr += `${index === 0 ? " " : ", "}${removedIngredient}`; } } console.log(saladStr); break; } } } } } // Process requests interactively or from the input file specified on the command line processRequests("🍕> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } const order = response.data; if (order.items.some((item) => item.itemType === "unknown")) { console.log("I didn't understand the following:"); for (const item of order.items) { if (item.itemType === "unknown") console.log(item.text); } } printOrder(order); }); ================================================ FILE: typescript/examples/restaurant/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/examples/sentiment/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "/**" ], "program": "${workspaceFolder}/dist/main.js", "console": "externalTerminal" } ] } ================================================ FILE: typescript/examples/sentiment/README.md ================================================ # Sentiment The 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. # Try Sentiment To run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For example, given the following input statement: **Input**: ``` 😀> TypeChat is awesome! ``` **Output**: ``` The sentiment is positive ``` ================================================ FILE: typescript/examples/sentiment/package.json ================================================ { "name": "sentiment", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0", "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1" } } ================================================ FILE: typescript/examples/sentiment/src/input.txt ================================================ hello, world TypeChat is awesome! I'm having a good day it's very rainy outside ================================================ FILE: typescript/examples/sentiment/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; import { createJsonTranslator, createLanguageModel } from "typechat"; import { processRequests } from "typechat/interactive"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { SentimentResponse } from "./sentimentSchema"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8"); const validator = createTypeScriptJsonValidator(schema, "SentimentResponse"); const translator = createJsonTranslator(model, validator); // Process requests interactively or from the input file specified on the command line processRequests("😀> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } console.log(`The sentiment is ${response.data.sentiment}`); }); ================================================ FILE: typescript/examples/sentiment/src/sentimentSchema.ts ================================================ // The following is a schema definition for determining the sentiment of a some user input. export interface SentimentResponse { sentiment: "negative" | "neutral" | "positive"; // The sentiment of the text } ================================================ FILE: typescript/examples/sentiment/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/examples/sentiment-zod/README.md ================================================ # Sentiment The 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. # Try Sentiment To run the Sentiment example, follow the instructions in the [examples README](../README.md#step-1-configure-your-development-environment). # Usage Example prompts can be found in [`src/input.txt`](./src/input.txt). For example, given the following input statement: **Input**: ``` 😀> TypeChat is awesome! ``` **Output**: ``` The sentiment is positive ``` ================================================ FILE: typescript/examples/sentiment-zod/package.json ================================================ { "name": "sentiment-zod", "version": "0.0.1", "private": true, "description": "", "main": "dist/main.js", "scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*.txt dist" }, "author": "", "license": "MIT", "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", "typechat": "^0.1.0" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.10.4", "copyfiles": "^2.4.1", "typescript": "^5.3.3" } } ================================================ FILE: typescript/examples/sentiment-zod/src/input.txt ================================================ hello, world TypeChat is awesome! I'm having a good day it's very rainy outside ================================================ FILE: typescript/examples/sentiment-zod/src/main.ts ================================================ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; import { createJsonTranslator, createLanguageModel } from "typechat"; import { processRequests } from "typechat/interactive"; import { createZodJsonValidator } from "typechat/zod"; import { SentimentSchema } from "./sentimentSchema"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const validator = createZodJsonValidator(SentimentSchema, "SentimentResponse"); const translator = createJsonTranslator(model, validator); // Process requests interactively or from the input file specified on the command line processRequests("😀> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } console.log(`The sentiment is ${response.data.sentiment}`); }); ================================================ FILE: typescript/examples/sentiment-zod/src/sentimentSchema.ts ================================================ import { z } from "zod"; export const SentimentResponse = z.object({ sentiment: z.enum(["negative", "neutral", "positive"]).describe("The sentiment of the text") }); export const SentimentSchema = { SentimentResponse }; ================================================ FILE: typescript/examples/sentiment-zod/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "outDir": "../dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "inlineSourceMap": true } } ================================================ FILE: typescript/package.json ================================================ { "name": "typechat", "author": "Microsoft", "version": "0.1.2", "license": "MIT", "description": "TypeChat is an experimental library that makes it easy to build natural language interfaces using types.", "keywords": [ "schema", "LLM", "prompt", "TypeScript", "validation" ], "homepage": "https://github.com/microsoft/TypeChat#readme", "repository": { "type": "git", "url": "git+https://github.com/microsoft/TypeChat.git" }, "bugs": { "url": "https://github.com/microsoft/TypeChat/issues" }, "scripts": { "build": "tsc -p src", "build-all": "npm run build --workspaces", "prepare": "npm run build-all", "prepublishOnly": "node -e \"require('fs').copyFileSync('../SECURITY.md','SECURITY.md')\"", "postpublish": "node -e \"require('fs').unlinkSync('SECURITY.md')\"", "publish-package": "npm publish", "publish-package:dry-run": "npm publish --dry-run" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": "./dist/index.js", "./ts": "./dist/ts/index.js", "./zod": "./dist/zod/index.js", "./interactive": "./dist/interactive/index.js" }, "engines": { "node": ">=18" }, "files": [ "dist", "LICENSE", "README.md", "SECURITY.md" ], "dependencies": {}, "peerDependencies": { "typescript": "^5.3.3", "zod": "^3.22.4" }, "peerDependenciesMeta": { "typescript": { "optional": true }, "zod": { "optional": true } }, "devDependencies": { "@types/node": "^20.10.4" }, "workspaces": [ "./", "./examples/*" ] } ================================================ FILE: typescript/src/index.ts ================================================ export * from './result'; export * from './model'; export * from './typechat'; ================================================ FILE: typescript/src/interactive/index.ts ================================================ export * from './interactive'; ================================================ FILE: typescript/src/interactive/interactive.ts ================================================ import fs from "fs"; import readline from "readline/promises"; /** * A request processor for interactive input or input from a text file. If an input file name is specified, * the callback function is invoked for each line in file. Otherwise, the callback function is invoked for * each line of interactive input until the user types "quit" or "exit". * @param interactivePrompt Prompt to present to user. * @param inputFileName Input text file name, if any. * @param processRequest Async callback function that is invoked for each interactive input or each line in text file. */ export async function processRequests(interactivePrompt: string, inputFileName: string | undefined, processRequest: (request: string) => Promise) { if (inputFileName) { const lines = fs.readFileSync(inputFileName).toString().split(/\r?\n/); for (const line of lines) { if (line.length) { console.log(interactivePrompt + line); await processRequest(line); } } } else { const stdio = readline.createInterface({ input: process.stdin, output: process.stdout }); while (true) { const input = await stdio.question(interactivePrompt); if (input.toLowerCase() === "quit" || input.toLowerCase() === "exit") { break; } else if (input.length) { await processRequest(input); } } stdio.close(); } } ================================================ FILE: typescript/src/model.ts ================================================ import { Result, success, error } from "./result"; /** * Represents a section of an LLM prompt with an associated role. TypeChat uses the "user" role for * prompts it generates and the "assistant" role for previous LLM responses (which will be part of * the prompt in repair attempts). TypeChat currently doesn't use the "system" role. */ export interface PromptSection { /** * Specifies the role of this section. */ role: "system" | "user" | "assistant"; /** * Specifies the content of this section. */ content: PromptContent; } export type PromptContent = | string | MultimodalPromptContent[]; /** * GPT-4-vision, GPT-4-omni and GPT-4-turbo allow multi-modal input, where images and text can * be part of the prompt. To support this, the content section of the prompt has an array of objects. */ export type MultimodalPromptContent = | string | TextPromptContent | ImagePromptContent; export type TextPromptContent = { type: "text"; text: string; }; export type ImagePromptContent = { type: "image_url"; image_url: ImageUrl; }; export type ImageUrl = { /* * This could be a URL to a hosted image, or the base64-encoded image content. */ url: string; /* * Controls how the model processes the image and generates its textual understanding. * In "low" mode, the model treats the image as 512x512px, while "high" mode considers * the image at full size. */ detail?: "auto" | "low" | "high"; }; /** * Represents a AI language model that can complete prompts. TypeChat uses an implementation of this * interface to communicate with an AI service that can translate natural language requests to JSON * instances according to a provided schema. The `createLanguageModel`, `createOpenAILanguageModel`, * and `createAzureOpenAILanguageModel` functions create instances of this interface. */ export interface TypeChatLanguageModel { /** * Optional property that specifies the maximum number of retry attempts (the default is 3). */ retryMaxAttempts?: number; /** * Optional property that specifies the delay before retrying in milliseconds (the default is 1000ms). */ retryPauseMs?: number; /** * Obtains a completion from the language model for the given prompt. * @param prompt A prompt string or an array of prompt sections. If a string is specified, * it is converted into a single "user" role prompt section. */ complete(prompt: string | PromptSection[]): Promise>; } /** * Creates a language model encapsulation of an OpenAI or Azure OpenAI REST API endpoint * chosen by environment variables. * * If an `OPENAI_API_KEY` environment variable exists, the `createOpenAILanguageModel` function * is used to create the instance. The `OPENAI_ENDPOINT` and `OPENAI_MODEL` environment variables * must also be defined or an exception will be thrown. * * If an `AZURE_OPENAI_API_KEY` environment variable exists, the `createAzureOpenAILanguageModel` function * is used to create the instance. The `AZURE_OPENAI_ENDPOINT` environment variable must also be defined * or an exception will be thrown. * * If none of these key variables are defined, an exception is thrown. * @returns An instance of `TypeChatLanguageModel`. */ export function createLanguageModel(env: Record): TypeChatLanguageModel { if (env.OPENAI_API_KEY) { const apiKey = env.OPENAI_API_KEY ?? missingEnvironmentVariable("OPENAI_API_KEY"); const model = env.OPENAI_MODEL ?? missingEnvironmentVariable("OPENAI_MODEL"); const endPoint = env.OPENAI_ENDPOINT ?? "https://api.openai.com/v1/chat/completions"; const org = env.OPENAI_ORGANIZATION ?? ""; return createOpenAILanguageModel(apiKey, model, endPoint, org); } if (env.AZURE_OPENAI_API_KEY) { const apiKey = env.AZURE_OPENAI_API_KEY ?? missingEnvironmentVariable("AZURE_OPENAI_API_KEY"); const endPoint = env.AZURE_OPENAI_ENDPOINT ?? missingEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); return createAzureOpenAILanguageModel(apiKey, endPoint); } missingEnvironmentVariable("OPENAI_API_KEY or AZURE_OPENAI_API_KEY"); } /** * Creates a language model encapsulation of an OpenAI REST API endpoint. * @param apiKey The OpenAI API key. * @param model The model name. * @param endPoint The URL of the OpenAI REST API endpoint. Defaults to "https://api.openai.com/v1/chat/completions". * @param org The OpenAI organization id. * @returns An instance of `TypeChatLanguageModel`. */ export function createOpenAILanguageModel(apiKey: string, model: string, endPoint = "https://api.openai.com/v1/chat/completions", org = ""): TypeChatLanguageModel { const headers = { "Authorization": `Bearer ${apiKey}`, "OpenAI-Organization": org }; return createFetchLanguageModel(endPoint, headers, { model }); } /** * Creates a language model encapsulation of an Azure OpenAI REST API endpoint. * @param endPoint The URL of the OpenAI REST API endpoint. The URL must be in the format * "https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version={API-version}". * Example deployment names are "gpt-35-turbo" and "gpt-4". An example API versions is "2023-05-15". * @param apiKey The Azure OpenAI API key. * @returns An instance of `TypeChatLanguageModel`. */ export function createAzureOpenAILanguageModel(apiKey: string, endPoint: string): TypeChatLanguageModel { const headers = { // Needed when using managed identity "Authorization": `Bearer ${apiKey}`, // Needed when using regular API key "api-key": apiKey }; return createFetchLanguageModel(endPoint, headers, {}); } /** * Common OpenAI REST API endpoint encapsulation using the fetch API. */ function createFetchLanguageModel(url: string, headers: object, defaultParams: object) { const model: TypeChatLanguageModel = { complete }; return model; async function complete(prompt: string | PromptSection[]) { let retryCount = 0; const retryMaxAttempts = model.retryMaxAttempts ?? 3; const retryPauseMs = model.retryPauseMs ?? 1000; const messages = typeof prompt === "string" ? [{ role: "user", content: prompt }] : prompt; while (true) { const options = { method: "POST", body: JSON.stringify({ ...defaultParams, messages, temperature: 0, n: 1 }), headers: { "content-type": "application/json", ...headers } } const response = await fetch(url, options); if (response.ok) { const json = await response.json() as { choices: { message: PromptSection }[] }; if (typeof json.choices[0].message.content === "string") { return success(json.choices[0].message.content ?? ""); } else { return error(`REST API unexpected response format: ${JSON.stringify(json.choices[0].message.content)}`); } } if (!isTransientHttpError(response.status) || retryCount >= retryMaxAttempts) { return error(`REST API error ${response.status}: ${response.statusText}`); } await sleep(retryPauseMs); retryCount++; } } } /** * Returns true of the given HTTP status code represents a transient error. */ function isTransientHttpError(code: number): boolean { switch (code) { case 429: // TooManyRequests case 500: // InternalServerError case 502: // BadGateway case 503: // ServiceUnavailable case 504: // GatewayTimeout return true; } return false; } /** * Sleeps for the given number of milliseconds. */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Throws an exception for a missing environment variable. */ function missingEnvironmentVariable(name: string): never { throw new Error(`Missing environment variable: ${name}`); } ================================================ FILE: typescript/src/result.ts ================================================ /** * An object representing a successful operation with a result of type `T`. */ export type Success = { success: true, data: T }; /** * An object representing an operation that failed for the reason given in `message`. */ export type Error = { success: false, message: string }; /** * An object representing a successful or failed operation of type `T`. */ export type Result = Success | Error; /** * Returns a `Success` object. * @param data The value for the `data` property of the result. * @returns A `Success` object. */ export function success(data: T): Success { return { success: true, data }; } /** * Returns an `Error` object. * @param message The value for the `message` property of the result. * @returns An `Error` object. */ export function error(message: string): Error { return { success: false, message }; } /** * Obtains the value associated with a successful `Result` or throws an exception if * the result is an error. * @param result The `Result` from which to obtain the `data` property. * @returns The value of the `data` property. */ export function getData(result: Result) { if (result.success) { return result.data; } throw new Error(result.message); } ================================================ FILE: typescript/src/ts/index.ts ================================================ export * from './validate'; export * from './program'; ================================================ FILE: typescript/src/ts/program.ts ================================================ import { Result, error, success } from "../result"; import { TypeChatLanguageModel } from "../model"; import { createTypeScriptJsonValidator } from "./validate"; import { TypeChatJsonTranslator, createJsonTranslator } from "../typechat"; const programSchemaText = `// A program consists of a sequence of function calls that are evaluated in order. export type Program = { "@steps": FunctionCall[]; } // A function call specifies a function name and a list of argument expressions. Arguments may contain // nested function calls and result references. export type FunctionCall = { // Name of the function "@func": string; // Arguments for the function, if any "@args"?: Expression[]; }; // An expression is a JSON value, a function call, or a reference to the result of a preceding expression. export type Expression = JsonValue | FunctionCall | ResultReference; // A JSON value is a string, a number, a boolean, null, an object, or an array. Function calls and result // references can be nested in objects and arrays. export type JsonValue = string | number | boolean | null | { [x: string]: Expression } | Expression[]; // A result reference represents the value of an expression from a preceding step. export type ResultReference = { // Index of the previous expression in the "@steps" array "@ref": number; }; `; /** * A program consists of a sequence of function calls that are evaluated in order. */ export type Program = { "@steps": FunctionCall[]; } /** * A function call specifies a function name and a list of argument expressions. Arguments may contain * nested function calls and result references. */ export type FunctionCall = { // Name of the function "@func": string; // Arguments for the function, if any "@args"?: Expression[]; }; /** * An expression is a JSON value, a function call, or a reference to the result of a preceding expression. */ export type Expression = JsonValue | FunctionCall | ResultReference; /** * A JSON value is a string, a number, a boolean, null, an object, or an array. Function calls and result * references can be nested in objects and arrays. */ export type JsonValue = string | number | boolean | null | { [x: string]: Expression } | Expression[]; /** * A result reference represents the value of an expression from a preceding step. */ export type ResultReference = { // Index of the previous expression in the "@steps" array "@ref": number; }; /** * Transforms a JSON program object into an equivalent TypeScript module suitable for type checking. * The generated module takes the form: * * import { API } from "./schema"; * function program(api: API) { * const step1 = api.someFunction1(...); * const step2 = api.someFunction2(...); * return api.someFunction3(...); * } * * @param jsonObject A JSON program object. * @returns A `Success` with the module source code or an `Error` explaining why the JSON object * couldn't be transformed. */ export function createModuleTextFromProgram(jsonObject: object): Result { const steps = (jsonObject as Program)["@steps"]; if (!(Array.isArray(steps) && steps.every(step => typeof step === "object" && step !== null && step.hasOwnProperty("@func")))) { return error("JSON object is not a valid program"); } let hasError = false; let functionBody = ""; let currentStep = 0; while (currentStep < steps.length) { functionBody += ` ${currentStep === steps.length - 1 ? `return` : `const step${currentStep + 1} =`} ${exprToString(steps[currentStep])};\n`; currentStep++; } return hasError ? error("JSON program contains an invalid expression") : success(`import { API } from "./schema";\nfunction program(api: API) {\n${functionBody}}`); function exprToString(expr: unknown): string { return typeof expr === "object" && expr !== null ? objectToString(expr as Record) : JSON.stringify(expr); } function objectToString(obj: Record) { if (obj.hasOwnProperty("@ref")) { const index = obj["@ref"]; if (typeof index === "number" && index < currentStep && Object.keys(obj).length === 1) { return `step${index + 1}`; } } else if (obj.hasOwnProperty("@func")) { const func = obj["@func"]; const hasArgs = obj.hasOwnProperty("@args"); const args = hasArgs ? obj["@args"] : []; if (typeof func === "string" && (Array.isArray(args)) && Object.keys(obj).length === (hasArgs ? 2 : 1)) { return `api.${func}(${arrayToString(args)})`; } } else if (Array.isArray(obj)) { return `[${arrayToString(obj)}]`; } else { return `{ ${Object.keys(obj).map(key => `${JSON.stringify(key)}: ${exprToString(obj[key])}`).join(", ")} }`; } hasError = true; return ""; } function arrayToString(array: unknown[]) { return array.map(exprToString).join(", "); } } /** * Evaluates a JSON program using a simple interpreter. Function calls in the program are passed to the `onCall` * callback function for validation and dispatch. Thus, unlike JavaScript's `eval`, access to external functionality * and resources is entirely controlled by the host application. Note that `onCall` is expected to return a `Promise` * such that function dispatch can be implemented asynchronously if desired. * @param program The JSON program to evaluate. * @param onCall A callback function for handling function calls in the program. * @returns A `Promise` with the value of the last expression in the program. */ export async function evaluateJsonProgram(program: Program, onCall: (func: string, args: unknown[]) => Promise) { const results: unknown[] = []; for (const expr of program["@steps"]) { results.push(await evaluate(expr)); } return results.length > 0 ? results[results.length - 1] : undefined; async function evaluate(expr: unknown): Promise { return typeof expr === "object" && expr !== null ? await evaluateObject(expr as Record) : expr; } async function evaluateObject(obj: Record) { if (obj.hasOwnProperty("@ref")) { const index = obj["@ref"]; if (typeof index === "number" && index < results.length) { return results[index]; } } else if (obj.hasOwnProperty("@func")) { const func = obj["@func"]; const args = obj.hasOwnProperty("@args") ? obj["@args"] : []; if (typeof func === "string" && Array.isArray(args)) { return await onCall(func, await evaluateArray(args)); } } else if (Array.isArray(obj)) { return evaluateArray(obj); } else { const values = await Promise.all(Object.values(obj).map(evaluate)); return Object.fromEntries(Object.keys(obj).map((k, i) => [k, values[i]])); } } function evaluateArray(array: unknown[]) { return Promise.all(array.map(evaluate)); } } /** * Creates an object that can translate natural language requests into simple programs, represented as JSON, that compose * functions from a specified API. The resulting programs can be safely evaluated using the `evaluateJsonProgram` * function. * @param model The language model to use for translating requests into programs. * @param schema The TypeScript source code for the target API. The source code must export a type named `API`. * @returns A `TypeChatJsonTranslator` instance. */ export function createProgramTranslator(model: TypeChatLanguageModel, schema: string): TypeChatJsonTranslator { const validator = createTypeScriptJsonValidator(schema, "Program"); validator.createModuleTextFromJson = createModuleTextFromProgram; const translator = createJsonTranslator(model, validator); translator.createRequestPrompt = createRequestPrompt; translator.createRepairPrompt = createRepairPrompt; return translator; function createRequestPrompt(request: string) { return `You are a service that translates user requests into programs represented as JSON using the following TypeScript definitions:\n` + `\`\`\`\n${programSchemaText}\`\`\`\n` + `The programs can call functions from the API defined in the following TypeScript definitions:\n` + `\`\`\`\n${validator.getSchemaText()}\`\`\`\n` + `The following is a user request:\n` + `"""\n${request}\n"""\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`; } function createRepairPrompt(validationError: string) { return `The JSON program object is invalid for the following reason:\n` + `"""\n${validationError}\n"""\n` + `The following is a revised JSON program object:\n`; } } ================================================ FILE: typescript/src/ts/validate.ts ================================================ import ts from 'typescript'; import { Result, success, error } from '../result'; import { TypeChatJsonValidator } from "../typechat"; const libText = `interface Array { length: number, [n: number]: T } interface Object { toString(): string } interface Function { prototype: unknown } interface CallableFunction extends Function {} interface NewableFunction extends Function {} interface String { readonly length: number } interface Boolean { valueOf(): boolean } interface Number { valueOf(): number } interface RegExp { test(string: string): boolean }`; /** * Represents an object that can validate JSON strings according to a given TypeScript schema. */ export interface TypeScriptJsonValidator extends TypeChatJsonValidator { /** * Transform JSON into TypeScript code for validation. Returns a `Success` object if the conversion is * successful, or an `Error` object if the JSON can't be transformed. The returned TypeScript source code is * expected to be an ECMAScript module that imports one or more types from `"./schema"` and combines those * types and a representation of the JSON object in a manner suitable for type-checking by the TypeScript compiler. */ createModuleTextFromJson(jsonObject: object): Result; } /** * Returns a JSON validator for a given TypeScript schema. Validation is performed by an in-memory instance of * the TypeScript compiler. The specified type argument `T` must be the same type as `typeName` in the given `schema`. * @param schema A string containing the TypeScript source code for the JSON schema. * @param typeName The name of the JSON target type in the schema. * @returns A `TypeChatJsonValidator` instance. */ export function createTypeScriptJsonValidator(schema: string, typeName: string): TypeScriptJsonValidator { const options = { ...ts.getDefaultCompilerOptions(), strict: true, skipLibCheck: true, noLib: true, types: [] }; const rootProgram = createProgramFromModuleText(""); const validator: TypeScriptJsonValidator = { getSchemaText: () => schema, getTypeName: () => typeName, createModuleTextFromJson, validate }; return validator; function validate(jsonObject: object) { const moduleResult = validator.createModuleTextFromJson(jsonObject); if (!moduleResult.success) { return moduleResult; } const program = createProgramFromModuleText(moduleResult.data, rootProgram); const syntacticDiagnostics = program.getSyntacticDiagnostics(); const programDiagnostics = syntacticDiagnostics.length ? syntacticDiagnostics : program.getSemanticDiagnostics(); if (programDiagnostics.length) { const diagnostics = programDiagnostics.map(d => typeof d.messageText === "string" ? d.messageText : d.messageText.messageText).join("\n"); return error(diagnostics); } return success(jsonObject as T); } function createModuleTextFromJson(jsonObject: object) { return success(`import { ${typeName} } from './schema';\nconst json: ${typeName} = ${JSON.stringify(jsonObject, undefined, 2)};\n`); } function createProgramFromModuleText(moduleText: string, oldProgram?: ts.Program) { const fileMap = new Map([ createFileMapEntry("/lib.d.ts", libText), createFileMapEntry("/schema.ts", schema), createFileMapEntry("/json.ts", moduleText) ]); const host: ts.CompilerHost = { getSourceFile: fileName => fileMap.get(fileName), getDefaultLibFileName: () => "lib.d.ts", writeFile: () => {}, getCurrentDirectory: () => "/", getCanonicalFileName: fileName => fileName, useCaseSensitiveFileNames: () => true, getNewLine: () => "\n", fileExists: fileName => fileMap.has(fileName), readFile: fileName => "", }; return ts.createProgram(Array.from(fileMap.keys()), options, host, oldProgram); } function createFileMapEntry(filePath: string, fileText: string): [string, ts.SourceFile] { return [filePath, ts.createSourceFile(filePath, fileText, ts.ScriptTarget.Latest)]; } } ================================================ FILE: typescript/src/tsconfig.json ================================================ { "compilerOptions": { "target": "es2021", "lib": ["es2021"], "module": "node16", "types": ["node"], "esModuleInterop": true, "outDir": "../dist", "skipLibCheck": true, "strict": true, "exactOptionalPropertyTypes": true, "declaration": true } } ================================================ FILE: typescript/src/typechat.ts ================================================ import { Result, success, error } from "./result"; import { TypeChatLanguageModel, PromptSection, PromptContent } from "./model"; /** * Represents an object that can translate natural language requests in JSON objects of the given type. */ export interface TypeChatJsonTranslator { /** * The associated `TypeChatLanguageModel`. */ model: TypeChatLanguageModel; /** * The associated `TypeChatJsonValidator`. */ validator: TypeChatJsonValidator; /** * A boolean indicating whether to attempt repairing JSON objects that fail to validate. The default is `true`, * but an application can set the property to `false` to disable repair attempts. */ attemptRepair: boolean; /** * A boolean indicating whether to delete properties with null values from parsed JSON objects. Some language * models (e.g. gpt-3.5-turbo) have a tendency to assign null values to optional properties instead of omitting * them. The default for this property is `false`, but an application can set the property to `true` for schemas * that don't permit null values. */ stripNulls: boolean; /** * Creates an AI language model prompt from the given request. This function is called by `completeAndValidate` * to obtain the prompt. An application can assign a new function to provide a different prompt. * @param request The natural language request. * @returns A prompt that combines the request with the schema and type name of the underlying validator. */ createRequestPrompt(request: string): PromptContent; /** * Creates a repair prompt to append to an original prompt/response in order to repair a JSON object that * failed to validate. This function is called by `completeAndValidate` when `attemptRepair` is true and the * JSON object produced by the original prompt failed to validate. An application can assign a new function * to provide a different repair prompt. * @param validationError The error message returned by the validator. * @returns A repair prompt constructed from the error message. */ createRepairPrompt(validationError: string): string; /** * Optionally implements additional validation logic beyond what is expressed in the schema. This function is * called following successful schema validation of an instance. By default the function just returns a * `Success` for the given instance, but an application can assign a new function that implements any * additional validation. * @param instance The instance to validate. * @returns A `Success` with the final validated instance, or an `Error` explaining the validation failure. */ validateInstance(instance: T): Result; /** * Translates a natural language request into an object of type `T`. If the JSON object returned by * the language model fails to validate and the `attemptRepair` property is `true`, a second * attempt to translate the request will be made. The prompt for the second attempt will include the * diagnostics produced for the first attempt. This often helps produce a valid instance. * @param request The natural language request. * @param promptPreamble An optional string or array of prompt sections to prepend to the generated * prompt. If a string is specified, it is converted into a single "user" role prompt section. * @returns A promise for the resulting object. */ translate(request: string, promptPreamble?: string | PromptSection[]): Promise>; } /** * An object that represents a TypeScript schema for JSON objects. */ export interface TypeChatJsonValidator { /** * Return a string containing TypeScript source code for the validation schema. */ getSchemaText(): string; /** * Return the name of the JSON object target type in the schema. */ getTypeName(): string; /** * Validates the given JSON object according to the associated TypeScript schema. Returns a * `Success` object containing the JSON object if validation was successful. Otherwise, returns * an `Error` object with a `message` property describing the error. * @param jsonText The JSON object to validate. * @returns The JSON object or an error message. */ validate(jsonObject: object): Result; } /** * Creates an object that can translate natural language requests into JSON objects of the given type. * The specified type argument `T` must be the same type as `typeName` in the given `schema`. The function * creates a `TypeChatJsonValidator` and stores it in the `validator` property of the returned instance. * @param model The language model to use for translating requests into JSON. * @param validator A string containing the TypeScript source code for the JSON schema. * @param typeName The name of the JSON target type in the schema. * @returns A `TypeChatJsonTranslator` instance. */ export function createJsonTranslator(model: TypeChatLanguageModel, validator: TypeChatJsonValidator): TypeChatJsonTranslator { const typeChat: TypeChatJsonTranslator = { model, validator, attemptRepair: true, stripNulls: false, createRequestPrompt, createRepairPrompt, validateInstance: success, translate }; return typeChat; function createRequestPrompt(request: string) { return `You are a service that translates user requests into JSON objects of type "${validator.getTypeName()}" according to the following TypeScript definitions:\n` + `\`\`\`\n${validator.getSchemaText()}\`\`\`\n` + `The following is a user request:\n` + `"""\n${request}\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`; } function createRepairPrompt(validationError: string) { return `The JSON object is invalid for the following reason:\n` + `"""\n${validationError}\n"""\n` + `The following is a revised JSON object:\n`; } async function translate(request: string, promptPreamble?: string | PromptSection[]) { const preamble: PromptSection[] = typeof promptPreamble === "string" ? [{ role: "user", content: promptPreamble }] : promptPreamble ?? []; let prompt: PromptSection[] = [...preamble, { role: "user", content: typeChat.createRequestPrompt(request) }]; let attemptRepair = typeChat.attemptRepair; while (true) { const response = await model.complete(prompt); if (!response.success) { return response; } const responseText = response.data; const startIndex = responseText.indexOf("{"); const endIndex = responseText.lastIndexOf("}"); if (!(startIndex >= 0 && endIndex > startIndex)) { return error(`Response is not JSON:\n${responseText}`); } const jsonText = responseText.slice(startIndex, endIndex + 1); let jsonObject; try { jsonObject = JSON.parse(jsonText) as object; } catch (e) { return error(e instanceof SyntaxError ? e.message : "JSON parse error"); } if (typeChat.stripNulls) { stripNulls(jsonObject); } const schemaValidation = validator.validate(jsonObject); const validation = schemaValidation.success ? typeChat.validateInstance(schemaValidation.data) : schemaValidation; if (validation.success) { return validation; } if (!attemptRepair) { return error(`JSON validation failed: ${validation.message}\n${jsonText}`); } prompt.push({ role: "assistant", content: responseText }); prompt.push({ role: "user", content: typeChat.createRepairPrompt(validation.message) }); attemptRepair = false; } } } /** * Recursively delete properties with null values from the given object. This function assumes there are no * circular references in the object. * @param obj The object in which to strip null valued properties. */ function stripNulls(obj: any) { let keysToDelete: string[] | undefined; for (const k in obj) { const value = obj[k]; if (value === null) { (keysToDelete ??= []).push(k); } else { if (Array.isArray(value)) { if (value.some(x => x === null)) { obj[k] = value.filter(x => x !== null); } } if (typeof value === "object") { stripNulls(value); } } } if (keysToDelete) { for (const k of keysToDelete) { delete obj[k]; } } } ================================================ FILE: typescript/src/zod/index.ts ================================================ export * from './validate'; ================================================ FILE: typescript/src/zod/validate.ts ================================================ import { z } from "zod"; import { success, error } from '../result'; import { TypeChatJsonValidator } from '../typechat'; /** * Returns a JSON validator for a given Zod schema. The schema is supplied as an object where each property provides * a name for an associated Zod type. The `validate` method of the returned object validates a JSON object against the * supplied schema, the `getSchemaText` method obtains the TypeScript source text representation of the schema, and * the `getTypeName` method obtains the name of the given target type in the schema. * @param schema A schema object where each property provides a name for an associated Zod type. * @param targetType The name in the schema of the target type for JSON validation. * @returns A `TypeChatJsonValidator>`, where T is the schema and K is the target type name. */ export function createZodJsonValidator, K extends keyof T & string>(schema: T, typeName: K): TypeChatJsonValidator> { let schemaText: string; const validator: TypeChatJsonValidator> = { getSchemaText: () => schemaText ??= getZodSchemaAsTypeScript(schema), getTypeName: () => typeName, validate }; return validator; function validate(jsonObject: object) { const result = schema[typeName].safeParse(jsonObject); if (!result.success) { return error(result.error.issues.map(({ path, message }) => `${path.map(key => `[${JSON.stringify(key)}]`).join("")}: ${message}`).join("\"")); } return success(result.data as z.TypeOf); } } function getTypeKind(type: z.ZodType) { return (type._def as z.ZodTypeDef & { typeName: z.ZodFirstPartyTypeKind }).typeName; } function getTypeIdentity(type: z.ZodType): object { switch (getTypeKind(type)) { case z.ZodFirstPartyTypeKind.ZodObject: return (type._def as z.ZodObjectDef).shape(); case z.ZodFirstPartyTypeKind.ZodEnum: return (type._def as z.ZodEnumDef).values; case z.ZodFirstPartyTypeKind.ZodUnion: return (type._def as z.ZodUnionDef).options; } return type; } const enum TypePrecedence { Union = 0, Intersection = 1, Object = 2 } function getTypePrecedence(type: z.ZodType): TypePrecedence { switch (getTypeKind(type)) { case z.ZodFirstPartyTypeKind.ZodEnum: case z.ZodFirstPartyTypeKind.ZodUnion: case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion: return TypePrecedence.Union; case z.ZodFirstPartyTypeKind.ZodIntersection: return TypePrecedence.Intersection; } return TypePrecedence.Object; } /** * Returns the TypeScript source code corresponding to a Zod schema. The schema is supplied as an object where each * property provides a name for an associated Zod type. The return value is a string containing the TypeScript source * code corresponding to the schema. Each property of the schema object is emitted as a named `interface` or `type` * declaration for the associated type and is referenced by that name in the emitted type declarations. Other types * referenced in the schema are emitted in their structural form. * @param schema A schema object where each property provides a name for an associated Zod type. * @returns The TypeScript source code corresponding to the schema. */ export function getZodSchemaAsTypeScript(schema: Record): string { let result = ""; let startOfLine = true; let indent = 0; const entries = Array.from(Object.entries(schema)); let namedTypes = new Map(entries.map(([name, type]) => [getTypeIdentity(type), name])); for (const [name, type] of entries) { if (result) { appendNewLine(); } const description = type._def.description; if (description) { for (const comment of description.split("\n")) { append(`// ${comment}`); appendNewLine(); } } if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodObject) { append(`interface ${name} `); appendObjectType(type as z.ZodObject); } else { append(`type ${name} = `); appendTypeDefinition(type); append(";"); } appendNewLine(); } return result; function append(s: string) { if (startOfLine) { result += " ".repeat(indent); startOfLine = false; } result += s; } function appendNewLine() { append("\n"); startOfLine = true; } function appendType(type: z.ZodType, minPrecedence = 0) { const name = namedTypes.get(getTypeIdentity(type)); if (name) { append(name); } else { const parenthesize = getTypePrecedence(type) < minPrecedence; if (parenthesize) append("("); appendTypeDefinition(type); if (parenthesize) append(")"); } } function appendTypeDefinition(type: z.ZodType) { switch (getTypeKind(type)) { case z.ZodFirstPartyTypeKind.ZodString: return append("string"); case z.ZodFirstPartyTypeKind.ZodNumber: return append("number"); case z.ZodFirstPartyTypeKind.ZodBoolean: return append("boolean"); case z.ZodFirstPartyTypeKind.ZodDate: return append("Date"); case z.ZodFirstPartyTypeKind.ZodUndefined: return append("undefined"); case z.ZodFirstPartyTypeKind.ZodNull: return append("null"); case z.ZodFirstPartyTypeKind.ZodUnknown: return append("unknown"); case z.ZodFirstPartyTypeKind.ZodArray: return appendArrayType(type); case z.ZodFirstPartyTypeKind.ZodObject: return appendObjectType(type); case z.ZodFirstPartyTypeKind.ZodUnion: return appendUnionOrIntersectionTypes((type._def as z.ZodUnionDef).options, TypePrecedence.Union); case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion: return appendUnionOrIntersectionTypes([...(type._def as z.ZodDiscriminatedUnionDef).options.values()], TypePrecedence.Union); case z.ZodFirstPartyTypeKind.ZodIntersection: return appendUnionOrIntersectionTypes((type._def as z.ZodUnionDef).options, TypePrecedence.Intersection); case z.ZodFirstPartyTypeKind.ZodTuple: return appendTupleType(type); case z.ZodFirstPartyTypeKind.ZodRecord: return appendRecordType(type); case z.ZodFirstPartyTypeKind.ZodLiteral: return appendLiteral((type._def as z.ZodLiteralDef).value); case z.ZodFirstPartyTypeKind.ZodEnum: return append((type._def as z.ZodEnumDef).values.map(value => JSON.stringify(value)).join(" | ")); case z.ZodFirstPartyTypeKind.ZodOptional: return appendUnionOrIntersectionTypes([(type._def as z.ZodOptionalDef).innerType, z.undefined()], TypePrecedence.Union); case z.ZodFirstPartyTypeKind.ZodReadonly: return appendReadonlyType(type); } append("any"); } function appendArrayType(arrayType: z.ZodType) { appendType((arrayType._def as z.ZodArrayDef).type, TypePrecedence.Object); append("[]"); } function appendObjectType(objectType: z.ZodType) { append("{"); appendNewLine(); indent++; for (let [name, type] of Object.entries((objectType._def as z.ZodObjectDef).shape())) { const comment = type.description; append(name); if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodOptional) { append("?"); type = (type._def as z.ZodOptionalDef).innerType; } append(": "); appendType(type); append(";"); if (comment) append(` // ${comment}`); appendNewLine(); } indent--; append("}"); } function appendUnionOrIntersectionTypes(types: readonly z.ZodType[], minPrecedence: TypePrecedence) { let first = true; for (const type of types) { if (!first) append(minPrecedence === TypePrecedence.Intersection ? " & " : " | "); appendType(type, minPrecedence); first = false; } } function appendTupleType(tupleType: z.ZodType) { append("["); let first = true; for (let type of (tupleType._def as z.ZodTupleDef).items) { if (!first) append(", "); if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodOptional) { appendType((type._def as z.ZodOptionalDef).innerType, TypePrecedence.Object); append("?"); } else { appendType(type); } first = false; } const rest = (tupleType._def as z.ZodTupleDef).rest; if (rest) { if (!first) append(", "); append("..."); appendType(rest, TypePrecedence.Object); append("[]"); } append("]"); } function appendRecordType(recordType: z.ZodType) { append("Record<"); appendType((recordType._def as z.ZodRecordDef).keyType); append(", "); appendType((recordType._def as z.ZodRecordDef).valueType); append(">"); } function appendLiteral(value: unknown) { append(typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? JSON.stringify(value) : "any"); } function appendReadonlyType(readonlyType: z.ZodType) { append("Readonly<"); appendType((readonlyType._def as z.ZodReadonlyDef).innerType); append(">"); } }