Skip to content

Commit 6e49f79

Browse files
Add docs for Python usage.
1 parent 11f5992 commit 6e49f79

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
---
2+
layout: doc-page
3+
title: Basic Python Usage
4+
---
5+
6+
TypeChat is currently a small library, so we can get a solid understanding just by going through the following example:
7+
8+
```py
9+
import asyncio
10+
11+
import sys
12+
import schema as sentiment
13+
from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests
14+
15+
async def main():
16+
env_vals = dotenv_values()
17+
18+
# Create a model.
19+
model = create_language_model(env_vals)
20+
21+
# Create a validator
22+
validator = TypeChatValidator(sentiment.Sentiment)
23+
24+
# Create a translator.
25+
translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)
26+
27+
async def request_handler(message: str):
28+
result = await translator.translate(message)
29+
if isinstance(result, Failure):
30+
print(result.message)
31+
else:
32+
result = result.value
33+
print(f"The sentiment is {result.sentiment}")
34+
35+
# Process requests interactively or from the input file specified on the command line.
36+
file_path = sys.argv[1] if len(sys.argv) == 2 else None
37+
await process_requests("😀> ", file_path, request_handler)
38+
```
39+
40+
Let's break it down step-by-step.
41+
42+
## Providing a Model
43+
44+
TypeChat can be used with any language model.
45+
As long as you have a class with the following shape...
46+
47+
```py
48+
class TypeChatLanguageModel(Protocol):
49+
50+
async def complete(self, prompt: str | list[PromptSection]) -> Result[str]:
51+
"""
52+
Represents a AI language model that can complete prompts.
53+
54+
TypeChat uses an implementation of this protocol to communicate
55+
with an AI service that can translate natural language requests to JSON
56+
instances according to a provided schema.
57+
The `create_language_model` function can create an instance.
58+
"""
59+
...
60+
```
61+
62+
then you should be able to try TypeChat out with such a model.
63+
64+
The key thing here is providing a `complete` method.
65+
`complete` is just a function that takes a `string` and eventually returns a `string` if all goes well.
66+
67+
For convenience, TypeChat provides two functions out of the box to connect to the OpenAI API and Azure's OpenAI Services.
68+
You can call these directly.
69+
70+
```py
71+
def create_openai_language_model(
72+
api_key: str,
73+
model: str,
74+
endpoint: str = "https://api.openai.com/v1/chat/completions",
75+
org: str = ""
76+
):
77+
...
78+
79+
def create_azure_openai_language_model(api_key: str, endpoint: str):
80+
```
81+
82+
For even more convenience, TypeChat also provides a function to infer whether you're using OpenAI or Azure OpenAI.
83+
84+
```ts
85+
def create_language_model(vals: dict[str, str | None]) -> TypeChatLanguageModel:
86+
```
87+
88+
With `create_language_model`, you can populate your environment variables and pass them in.
89+
Based on whether `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` is set, you'll get a model of the appropriate type.
90+
91+
The `TypeChatLanguageModel` returned by these functions has a few attributes you might find useful:
92+
93+
- `max_retry_attempts`
94+
- `retry_pause_seconds`
95+
- `timeout_seconds`
96+
97+
Though note that these are unstable.
98+
99+
Regardless, of how you decide to construct your model, it is important to avoid committing credentials directly in source.
100+
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`.
101+
You can use a library like [`python-dotenv`](https://pypi.org/project/python-dotenv/) to help load these up.
102+
103+
```py
104+
from dotenv import load_dotenv
105+
load_dotenv()
106+
107+
// ...
108+
109+
import typechat
110+
model = typechat.create_language_model(os.environ)
111+
```
112+
113+
## Defining and Loading the Schema
114+
115+
TypeChat describes types to language models to help guide their responses.
116+
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.
117+
Here's what our schema file `schema.py` look like:
118+
119+
```py
120+
from dataclasses import dataclass
121+
from typing import Literal
122+
123+
@dataclass
124+
class Sentiment:
125+
"""
126+
The following is a schema definition for determining the sentiment of a some user input.
127+
"""
128+
129+
sentiment: Literal["negative", "neutral", "positive"]
130+
```
131+
132+
Here, we're saying that the `sentiment` attribute has to be one of three possible strings: `negative`, `neutral`, or `positive`.
133+
We did this with [the `typing.Literal` hint](https://docs.python.org/3/library/typing.html#typing.Literal).
134+
135+
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`.
136+
If we declared `Sentiment` as a `TypedDict`, TypeChat would provide us with a `dict`.
137+
That would mean that to access the value of `sentiment`, we would have to write `value["sentiment"]`.
138+
139+
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.
140+
TypeChat also understands constructs like `Annotated` and `Doc` to add comments to individual attributes.
141+
142+
## Creating a Validator
143+
144+
A validator really has two jobs generating a textual schema for language models, and making sure any data fits a given shape.
145+
The built-in validator looks roughly like this:
146+
147+
```py
148+
class TypeChatValidator(Generic[T]):
149+
"""
150+
Validates an object against a given Python type.
151+
"""
152+
153+
def __init__(self, py_type: type[T]):
154+
"""
155+
Args:
156+
157+
py_type: The schema type to validate against.
158+
"""
159+
...
160+
161+
def validate_object(self, obj: object) -> Result[T]:
162+
"""
163+
Validates the given Python object according to the associated schema type.
164+
165+
Returns a `Success[T]` object containing the object if validation was successful.
166+
Otherwise, returns a `Failure` object with a `message` property describing the error.
167+
"""
168+
...
169+
```
170+
171+
To construct a validator, we just have to pass in the type we defined:
172+
173+
```py
174+
import schema as sentiment
175+
validator = TypeChatValidator(sentiment.Sentiment)
176+
```
177+
178+
## Creating a JSON Translator
179+
180+
A `TypeChatJsonTranslator` brings all these concepts together.
181+
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.
182+
To do so, it crafts a prompt based on the schema, reaches out to the model, parses out JSON data, and attempts validation.
183+
Optionally, it will craft repair prompts and retry if validation fails.
184+
185+
```py
186+
translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)
187+
```
188+
189+
When we are ready to translate a user request, we can call the `translate` method.
190+
191+
```ts
192+
translator.translate("Hello world! 🙂");
193+
```
194+
195+
We'll come back to this.
196+
197+
## Creating the Prompt
198+
199+
TypeChat exports a `process_requests` function that makes it easy to experiment with TypeChat.
200+
Depending on its second argument, it either creates an interactive command line prompt (if given `None`), or reads lines in from the given a file path.
201+
202+
```ts
203+
async def request_handler(message: str):
204+
...
205+
206+
file_path = sys.argv[1] if len(sys.argv) == 2 else None
207+
await process_requests("😀> ", file_path, request_handler)
208+
```
209+
210+
`process_requests` takes 3 things.
211+
First, there's the prompt prefix - this is what a user will see before their own text in interactive scenarios.
212+
You can make this playful.
213+
We like to use emoji here. 😄
214+
215+
Next, we take a text file name.
216+
Input strings will be read from this file on a per-line basis.
217+
If the file name was `None`, `process_requests` will work on standard input and provide an interactive prompt.
218+
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`).
219+
220+
Finally, there's the request handler.
221+
We'll fill that in next.
222+
223+
## Translating Requests
224+
225+
Our handler receives some user input (the `message` string) each time it's called.
226+
It's time to pass that string into over to our `translator` object.
227+
228+
```ts
229+
async def request_handler(message: str):
230+
result = await translator.translate(message)
231+
if isinstance(result, Failure):
232+
print(result.message)
233+
else:
234+
result = result.value
235+
print(f"The sentiment is {result.sentiment}")
236+
```
237+
238+
We're calling the `translate` method on each string and getting a response.
239+
If something goes wrong, TypeChat will retry requests up to a maximum specified by `retry_max_attempts` on our `model`.
240+
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.
241+
242+
In the ideal case, `result` will be a `typechat.Success` and we'll be able to access our well-typed `value` property!
243+
This will correspond to the type that we passed in when we created our translator object (i.e. `Sentiment`).
244+
245+
That's it!
246+
You should now have a basic idea of TypeChat's APIs and how to get started with a new project. 🎉

0 commit comments

Comments
 (0)