Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

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.
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.
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:

Expand All @@ -24,7 +32,8 @@ Install TypeChat:
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):
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
Expand All @@ -33,9 +42,13 @@ 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 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) which includes more information on TypeChat and how to get started.
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

Expand Down
7 changes: 5 additions & 2 deletions python/examples/sentiment/demo.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import asyncio

import sys

from dotenv import dotenv_values
from typechat import (Failure, TypeChatJsonTranslator, TypeChatValidator,
create_language_model, process_requests)

import schema as sentiment
from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests


async def main():
env_vals = dotenv_values()
Expand Down
268 changes: 268 additions & 0 deletions site/src/docs/python/basic-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
---
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. 🎉
Loading