Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a57223a
Manually disable `envFile` loading in VS Code. (#214)
DanielRosenwasser Mar 15, 2024
6030f35
Bump follow-redirects from 1.15.5 to 1.15.6 in /typescript (#213)
dependabot[bot] Mar 18, 2024
f193e63
Make typescript and zod optional peer dependencies (#174)
jakebailey Mar 19, 2024
6a15d78
Move processRequests function to 'typechat/interactive' package (#221)
ahejlsberg Mar 20, 2024
11dd316
Add compliance and social media links to footer. (#223)
DanielRosenwasser Mar 21, 2024
27cb545
Blog post for TypeChat 0.1.0. (#222)
DanielRosenwasser Mar 25, 2024
f510155
Fix link.
DanielRosenwasser Mar 27, 2024
6fa23f8
Update inline code color to address contrast issues, remove bottom bo…
DanielRosenwasser Mar 28, 2024
d1130e3
Bump express from 4.18.3 to 4.19.2 in /typescript (#224)
dependabot[bot] Mar 29, 2024
00feebc
Update README.md (#225)
tychotic Mar 31, 2024
6aaf334
Increase timeout, make it (internally) configurable, use `repr()` as …
DanielRosenwasser Apr 3, 2024
ada3f00
Specify code fence languages more often.
DanielRosenwasser Apr 4, 2024
b9f307b
Make it easier to use purely `pip` and `venv` instead of requiring Ha…
DanielRosenwasser Apr 10, 2024
f53b971
Bump tar from 6.2.0 to 6.2.1 in /typescript (#233)
dependabot[bot] Apr 11, 2024
e52f5a2
Add "TypeChat in 5 Minutes" doc (#65)
DanielRosenwasser Apr 16, 2024
11f5992
Update pyright.
DanielRosenwasser Apr 4, 2024
a1930e4
Various doc updates and expose some internals (#236)
DanielRosenwasser Apr 17, 2024
d35918a
Use readline for interactive input (#235)
gvanrossum Apr 17, 2024
66fd7bb
Update pip install instructions for Python until we publish.
DanielRosenwasser Apr 19, 2024
9f644b6
Actually test the Python translator (#240)
DanielRosenwasser Apr 20, 2024
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
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
// TODO:
// * https://github.com/microsoft/vscode-python/issues/23078
// * https://github.com/microsoft/vscode/issues/197603
"python.envFile": "${workspaceFolder}/.file-that-should-hopefully-not-exist"
}
18 changes: 15 additions & 3 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,30 @@ After defining your types, TypeChat takes care of the rest by:

Types are all you need!

## Installation
## Getting Started

TypeChat for Python is not yet on PyPI, but you can try our [examples](./examples/) by cloning this repository.
> [!NOTE]
> TypeChat is not currently published. For now, install from our GitHub repository.

You will need [Python >=3.11](https://www.python.org/downloads/) and [hatch](https://hatch.pypa.io/1.6/install/).
Install TypeChat:

```sh
pip install "typechat @ git+https://github.com/microsoft/TypeChat#subdirectory=python"
```

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) 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
Expand Down
21 changes: 19 additions & 2 deletions python/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,30 @@ 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

Expand Down Expand Up @@ -77,7 +94,7 @@ To use an Azure OpenAI endpoint, include the following environment variables:

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=...
Expand Down
4 changes: 2 additions & 2 deletions python/examples/healthData/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def __init__(
self._additional_agent_instructions = additional_agent_instructions

@override
async def translate(self, request: str, *, prompt_preamble: str | list[PromptSection] | None = None) -> Result[T]:
result = await super().translate(request=request, prompt_preamble=prompt_preamble)
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
Expand Down
2 changes: 1 addition & 1 deletion python/examples/sentiment/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async def request_handler(message: str):
print(result.message)
else:
result = result.value
print(f"The sentiment is {result['sentiment']}")
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)
Expand Down
7 changes: 4 additions & 3 deletions python/examples/sentiment/schema.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing_extensions import Literal, TypedDict, Annotated, Doc
from dataclasses import dataclass
from typing_extensions import Literal, Annotated, Doc


class Sentiment(TypedDict):
@dataclass
class Sentiment:
"""
The following is a schema definition for determining the sentiment of a some user input.
"""
Expand Down
10 changes: 5 additions & 5 deletions python/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion python/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"author": "Microsoft",
"license": "MIT",
"devDependencies": {
"pyright": "1.1.352"
"pyright": "1.1.358"
}
}
25 changes: 19 additions & 6 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ dependencies = [
"typing_extensions>=4.10.0",
]

[project.optional-dependencies]
# Development-time dependencies.
dev = [
"coverage[toml]>=6.5",
"pytest>=8.0.2",
"syrupy>=4.6.1",
]

# 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"
Expand All @@ -44,12 +58,11 @@ path = "src/typechat/__about__.py"
type = "virtual"
path = "../.venv"

dependencies = [
"coverage[toml]>=6.5",
"python-dotenv>=1.0.0",
"pytest>=8.0.2",
"syrupy>=4.6.1",
"spotipy", # for examples
# Include dependencies from optional-dependencies for
# development of the core package along with examples.
features = [
"dev",
"examples"
]

[tool.hatch.envs.default.scripts]
Expand Down
17 changes: 9 additions & 8 deletions python/src/typechat/_internal/interactive.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
from typing import Callable, Awaitable

async def process_requests(interactive_prompt: str, input_file_name: str | None, process_request: Callable[[str], Awaitable[None]]):
Expand All @@ -21,13 +20,15 @@ async def process_requests(interactive_prompt: str, input_file_name: str | None,
print(interactive_prompt + line)
await process_request(line)
else:
print(interactive_prompt, end="", flush=True)
for line in sys.stdin:
lower_line = line.lower().strip()
if lower_line == "quit" or lower_line == "exit":
# Use readline to enable input editing and history
import readline # type: ignore
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)
print(interactive_prompt, end="", flush=True)


24 changes: 15 additions & 9 deletions python/src/typechat/_internal/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ 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
_max_retry_attempts: int = 3
_retry_pause_seconds: float = 1.0

def __init__(self, url: str, headers: dict[str, str], default_params: dict[str, str]):
super().__init__()
Expand Down Expand Up @@ -73,6 +78,7 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa
self.url,
headers=headers,
json=body,
timeout=self.timeout_seconds
)
if response.is_success:
json_result = cast(
Expand All @@ -81,13 +87,13 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa
)
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:
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))
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)
await asyncio.sleep(self.retry_pause_seconds)
retry_count += 1

@override
Expand All @@ -104,7 +110,7 @@ def __del__(self):
except Exception:
pass

def create_language_model(vals: dict[str, str | None]) -> TypeChatLanguageModel:
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`).
Expand Down Expand Up @@ -142,7 +148,7 @@ def required_var(name: str) -> str:
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 = ""):
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.

Expand All @@ -161,7 +167,7 @@ def create_openai_language_model(api_key: str, model: str, endpoint: str = "http
}
return HttpxLanguageModel(url=endpoint, headers=headers, default_params=default_params)

def create_azure_openai_language_model(api_key: str, endpoint: str):
def create_azure_openai_language_model(api_key: str, endpoint: str) -> HttpxLanguageModel:
"""
Creates a language model encapsulation of an Azure OpenAI REST API endpoint.

Expand Down
22 changes: 12 additions & 10 deletions python/src/typechat/_internal/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,33 @@ def __init__(
self._type_name = conversion_result.typescript_type_reference
self._schema_str = conversion_result.typescript_schema_str

async def translate(self, request: str, *, prompt_preamble: str | list[PromptSection] | None = None) -> Result[T]:
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:
request: A natural language request.
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.
"""
request = self._create_request_prompt(request)

prompt: str | list[PromptSection]
if prompt_preamble is None:
prompt = request
else:
messages: list[PromptSection] = []

messages.append({"role": "user", "content": input})
if prompt_preamble:
if isinstance(prompt_preamble, str):
prompt_preamble = [{"role": "user", "content": prompt_preamble}]
prompt = [*prompt_preamble, {"role": "user", "content": request}]
else:
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(prompt)
completion_response = await self.model.complete(messages)
if isinstance(completion_response, Failure):
return completion_response

Expand All @@ -93,7 +95,7 @@ async def translate(self, request: str, *, prompt_preamble: str | list[PromptSec
if num_repairs_attempted >= self._max_repair_attempts:
return Failure(error_message)
num_repairs_attempted += 1
request = f"{text_response}\n{self._create_repair_prompt(error_message)}"
messages.append({"role": "user", "content": self._create_repair_prompt(error_message)})

def _create_request_prompt(self, intent: str) -> str:
prompt = f"""
Expand Down
2 changes: 1 addition & 1 deletion python/src/typechat/_internal/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class TypeChatValidator(Generic[T]):
"""
Validates JSON text against a given Python type.
Validates an object against a given Python type.
"""

_adapted_type: pydantic.TypeAdapter[T]
Expand Down
Loading