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
---
================================================
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" %}
{% 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.
================================================
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