diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..681dda24 --- /dev/null +++ b/.vscode/settings.json @@ -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" +} \ No newline at end of file diff --git a/python/README.md b/python/README.md index 134b79c2..54a40e07 100644 --- a/python/README.md +++ b/python/README.md @@ -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 diff --git a/python/examples/README.md b/python/examples/README.md index a783c530..4be5a544 100644 --- a/python/examples/README.md +++ b/python/examples/README.md @@ -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 @@ -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=... diff --git a/python/examples/healthData/translator.py b/python/examples/healthData/translator.py index a1565334..d9c1c46b 100644 --- a/python/examples/healthData/translator.py +++ b/python/examples/healthData/translator.py @@ -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 diff --git a/python/examples/sentiment/demo.py b/python/examples/sentiment/demo.py index 5273e20a..313daddc 100644 --- a/python/examples/sentiment/demo.py +++ b/python/examples/sentiment/demo.py @@ -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) diff --git a/python/examples/sentiment/schema.py b/python/examples/sentiment/schema.py index 1a5831e5..71e46a15 100644 --- a/python/examples/sentiment/schema.py +++ b/python/examples/sentiment/schema.py @@ -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. """ diff --git a/python/package-lock.json b/python/package-lock.json index 5ba1a1dc..747c5e17 100644 --- a/python/package-lock.json +++ b/python/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "MIT", "devDependencies": { - "pyright": "1.1.352" + "pyright": "1.1.358" } }, "node_modules/fsevents": { @@ -27,16 +27,16 @@ } }, "node_modules/pyright": { - "version": "1.1.352", - "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.352.tgz", - "integrity": "sha512-X7fuuB24n3RIVCEPovrAadYJjxeB5RccArug+/oLwQnsHbSaDUQVHHkF/PJHkKpaIPX/RboG+EW8uCNUp1RnwQ==", + "version": "1.1.358", + "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.358.tgz", + "integrity": "sha512-M3zlRf+7bK/8vv5Q1EyfW9hQ2J0yOyS7j2GRwBrl2bJKPTS2AtPy8eR3IAitfOclENxUFDA/4PilHq0IrCj8Yg==", "dev": true, "bin": { "pyright": "index.js", "pyright-langserver": "langserver.index.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "optionalDependencies": { "fsevents": "~2.3.3" diff --git a/python/package.json b/python/package.json index fdba4c47..07b28dbb 100644 --- a/python/package.json +++ b/python/package.json @@ -9,6 +9,6 @@ "author": "Microsoft", "license": "MIT", "devDependencies": { - "pyright": "1.1.352" + "pyright": "1.1.358" } } diff --git a/python/pyproject.toml b/python/pyproject.toml index 84d95a4c..c040a73b 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -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" @@ -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] diff --git a/python/src/typechat/_internal/interactive.py b/python/src/typechat/_internal/interactive.py index cc8c9349..8ce7ca6d 100644 --- a/python/src/typechat/_internal/interactive.py +++ b/python/src/typechat/_internal/interactive.py @@ -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]]): @@ -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) - - \ No newline at end of file diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index 353ecf0f..da52e306 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -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__() @@ -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( @@ -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 @@ -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`). @@ -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. @@ -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. diff --git a/python/src/typechat/_internal/translator.py b/python/src/typechat/_internal/translator.py index 141a4772..6c649c1e 100644 --- a/python/src/typechat/_internal/translator.py +++ b/python/src/typechat/_internal/translator.py @@ -49,7 +49,7 @@ 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`. @@ -57,23 +57,25 @@ async def translate(self, request: str, *, prompt_preamble: str | list[PromptSec 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 @@ -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""" diff --git a/python/src/typechat/_internal/validator.py b/python/src/typechat/_internal/validator.py index 28a5792a..a1d17f1d 100644 --- a/python/src/typechat/_internal/validator.py +++ b/python/src/typechat/_internal/validator.py @@ -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] diff --git a/python/tests/__snapshots__/test_translator.ambr b/python/tests/__snapshots__/test_translator.ambr new file mode 100644 index 00000000..594786fb --- /dev/null +++ b/python/tests/__snapshots__/test_translator.ambr @@ -0,0 +1,137 @@ +# serializer version: 1 +# name: test_translator_with_immediate_pass + list([ + dict({ + 'kind': 'CLIENT REQUEST', + 'payload': list([ + dict({ + 'content': 'Get me 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, "c": 1234 }', + }), + ]) +# --- +# name: test_translator_with_single_failure + list([ + dict({ + 'kind': 'CLIENT REQUEST', + 'payload': list([ + dict({ + 'content': 'Get me 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': ''' + + 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 }', + }), + dict({ + 'kind': 'CLIENT REQUEST', + 'payload': list([ + dict({ + 'content': 'Get me 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': ''' + + 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 }', + }), + ]) +# --- diff --git a/python/tests/test_translator.py b/python/tests/test_translator.py new file mode 100644 index 00000000..1245ef34 --- /dev/null +++ b/python/tests/test_translator.py @@ -0,0 +1,53 @@ + +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]: + 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 \ No newline at end of file diff --git a/site/src/_data/docsTOC.json b/site/src/_data/docsTOC.json index 2976f3dc..b517d40b 100644 --- a/site/src/_data/docsTOC.json +++ b/site/src/_data/docsTOC.json @@ -5,7 +5,8 @@ { "title": "Introduction", "url": "/docs/introduction/"}, { "title": "Examples", "url": "/docs/examples/"}, { "title": "Techniques", "url": "/docs/techniques/"}, - { "title": "FAQ", "url": "/docs/faq/"} + { "title": "FAQ", "url": "/docs/faq/"}, + { "title": "Basic Usage for TypeScript", "url": "/docs/typescript/basic-usage/"} ] } ] diff --git a/site/src/_includes/footer.njk b/site/src/_includes/footer.njk index 4a186153..059cc107 100644 --- a/site/src/_includes/footer.njk +++ b/site/src/_includes/footer.njk @@ -1,15 +1,26 @@ diff --git a/site/src/blog/announcing-typechat-0-1-0.md b/site/src/blog/announcing-typechat-0-1-0.md new file mode 100644 index 00000000..37b32d03 --- /dev/null +++ b/site/src/blog/announcing-typechat-0-1-0.md @@ -0,0 +1,212 @@ +--- +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 PR](https://github.com/microsoft/TypeChat/pull/147)). +* The `processRequests` function for creating a REPL-like prompt now lives in `typechat/interactive` ([see PR](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! + diff --git a/site/src/css/styles.css b/site/src/css/styles.css index 89e7de6e..a7853e0e 100644 --- a/site/src/css/styles.css +++ b/site/src/css/styles.css @@ -37,6 +37,7 @@ :root { --typechat-monospace: Consolas, Menlo, Monaco, Roboto, monospace; + --typechat-inline-code-color: #a10615; --typechat-rounding-radius: 0.5rem; } @@ -80,7 +81,8 @@ .typechat-prose-content code { font-family: var(--typechat-monospace); - font-size: 16px; + color: var(--typechat-inline-code-color); + font-size: inherit; } .typechat-prose-content :not(pre) code { diff --git a/site/src/docs/typescript/basic-usage.md b/site/src/docs/typescript/basic-usage.md new file mode 100644 index 00000000..1ed05cd2 --- /dev/null +++ b/site/src/docs/typescript/basic-usage.md @@ -0,0 +1,316 @@ +--- +layout: doc-page +title: Basic TypeScript Usage +--- + +TypeChat is currently a small library, so let's take a look at some basic usage to understand it. + +```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}`); +}); +``` + +## 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 +``` + +You can populate your environment variables, and based on whether `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` is set, you'll get a model of the appropriate type. + +```ts +import dotenv from "dotenv"; +dotenv.config(/*...*/); +import * as typechat from "typechat"; +const model = typechat.createLanguageModel(process.env); +``` + +Regardless, of how you decide to construct your model, we recommend keeping your secret tokens/API keys in a `.env` file, and specifying `.env` in a `.gitignore`. +You can use a library like [`dotenv`](https://www.npmjs.com/package/dotenv) to help load these up. + +## 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 within 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 failed.. + +```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. 🎉 diff --git a/typescript/README.md b/typescript/README.md index 86d28d8a..e74ad4e9 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -20,20 +20,20 @@ Types are all you need! 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](./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/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. diff --git a/typescript/examples/README.md b/typescript/examples/README.md index e942d7c0..dab7569a 100644 --- a/typescript/examples/README.md +++ b/typescript/examples/README.md @@ -27,7 +27,7 @@ Ensure [Node.js (18.16.0 LTS or newer)](https://nodejs.org/en) or newer is insta ``` git clone https://github.com/microsoft/TypeChat -cd TypeChat +cd TypeChat/typescript npm install ``` @@ -56,7 +56,7 @@ For more information, see the [GitHub Codespaces Overview](https://docs.github.c Build TypeChat and the examples by running the following command in the repository root: -``` +```sh npm run build-all ``` @@ -69,7 +69,7 @@ To use an OpenAI endpoint, include the following environment variables: |----------|-------| | `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_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: @@ -81,7 +81,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=... @@ -102,6 +102,6 @@ Note that there are various sample "prose" files (e.g. `input.txt`) provided in 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 ``` diff --git a/typescript/examples/calendar/package.json b/typescript/examples/calendar/package.json index 840eb676..881c8e34 100644 --- a/typescript/examples/calendar/package.json +++ b/typescript/examples/calendar/package.json @@ -13,12 +13,12 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/calendar/src/main.ts b/typescript/examples/calendar/src/main.ts index f07f0eae..03d71244 100644 --- a/typescript/examples/calendar/src/main.ts +++ b/typescript/examples/calendar/src/main.ts @@ -3,8 +3,9 @@ import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; -import { createJsonTranslator, createLanguageModel, processRequests } from "typechat"; +import { createJsonTranslator, createLanguageModel } from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; +import { processRequests } from "typechat/interactive"; import { CalendarActions } from './calendarActionsSchema'; const dotEnvPath = findConfig(".env"); diff --git a/typescript/examples/coffeeShop-zod/src/main.ts b/typescript/examples/coffeeShop-zod/src/main.ts index 2bfa30f7..a97fe71b 100644 --- a/typescript/examples/coffeeShop-zod/src/main.ts +++ b/typescript/examples/coffeeShop-zod/src/main.ts @@ -1,8 +1,9 @@ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; -import { createJsonTranslator, createLanguageModel, processRequests } from "typechat"; +import { createJsonTranslator, createLanguageModel } from "typechat"; import { createZodJsonValidator } from "typechat/zod"; +import { processRequests } from "typechat/interactive"; import { z } from "zod"; import { CoffeeShopSchema } from "./coffeeShopSchema"; diff --git a/typescript/examples/coffeeShop/package.json b/typescript/examples/coffeeShop/package.json index 199b8c7f..ba9719b3 100644 --- a/typescript/examples/coffeeShop/package.json +++ b/typescript/examples/coffeeShop/package.json @@ -13,12 +13,12 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/coffeeShop/src/main.ts b/typescript/examples/coffeeShop/src/main.ts index f03bd6fe..fd6326f4 100644 --- a/typescript/examples/coffeeShop/src/main.ts +++ b/typescript/examples/coffeeShop/src/main.ts @@ -3,8 +3,9 @@ import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; -import { createJsonTranslator, createLanguageModel, processRequests } from "typechat"; +import { createJsonTranslator, createLanguageModel } from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; +import { processRequests } from "typechat/interactive"; import { Cart } from "./coffeeShopSchema"; const dotEnvPath = findConfig(".env"); diff --git a/typescript/examples/healthData/package.json b/typescript/examples/healthData/package.json index 79029d39..57108be5 100644 --- a/typescript/examples/healthData/package.json +++ b/typescript/examples/healthData/package.json @@ -13,12 +13,12 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.1.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/healthData/src/main.ts b/typescript/examples/healthData/src/main.ts index 82e7e817..fe3e134e 100644 --- a/typescript/examples/healthData/src/main.ts +++ b/typescript/examples/healthData/src/main.ts @@ -3,7 +3,8 @@ import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; -import { createLanguageModel, processRequests } from "typechat"; +import { createLanguageModel } from "typechat"; +import { processRequests } from "typechat/interactive"; import { HealthDataResponse } from "./healthDataSchema"; import { createHealthDataTranslator } from "./translator"; diff --git a/typescript/examples/healthData/src/translator.ts b/typescript/examples/healthData/src/translator.ts index e9a284bb..c8e21658 100644 --- a/typescript/examples/healthData/src/translator.ts +++ b/typescript/examples/healthData/src/translator.ts @@ -1,5 +1,5 @@ import {Result, TypeChatLanguageModel, createJsonTranslator, TypeChatJsonTranslator} from "typechat"; -import { createTypeScriptJsonValidator } from "../../../dist/ts"; +import { createTypeScriptJsonValidator } from "typechat/ts"; type ChatMessage = { source: "system" | "user" | "assistant"; diff --git a/typescript/examples/math/package.json b/typescript/examples/math/package.json index 15df99a6..79df155c 100644 --- a/typescript/examples/math/package.json +++ b/typescript/examples/math/package.json @@ -13,12 +13,12 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/math/src/main.ts b/typescript/examples/math/src/main.ts index 55cea745..cda811c3 100644 --- a/typescript/examples/math/src/main.ts +++ b/typescript/examples/math/src/main.ts @@ -3,7 +3,8 @@ import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; -import { createLanguageModel, getData, processRequests } from "typechat"; +import { createLanguageModel, getData } from "typechat"; +import { processRequests } from "typechat/interactive"; import { createModuleTextFromProgram, createProgramTranslator, evaluateJsonProgram } from "typechat/ts"; const dotEnvPath = findConfig(".env"); diff --git a/typescript/examples/multiSchema/package.json b/typescript/examples/multiSchema/package.json index 0340982b..f5abaf4f 100644 --- a/typescript/examples/multiSchema/package.json +++ b/typescript/examples/multiSchema/package.json @@ -15,12 +15,12 @@ "dotenv": "^16.3.1", "typechat": "^0.1.0", "find-config": "^1.0.0", - "music": "^0.0.1" + "music": "^0.0.1", + "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.3.1", - "copyfiles": "^2.4.1", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/multiSchema/src/main.ts b/typescript/examples/multiSchema/src/main.ts index f241bdee..8820a616 100644 --- a/typescript/examples/multiSchema/src/main.ts +++ b/typescript/examples/multiSchema/src/main.ts @@ -3,7 +3,8 @@ import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; -import { createLanguageModel, processRequests } from "typechat"; +import { createLanguageModel } from "typechat"; +import { processRequests } from "typechat/interactive"; import { createJsonMathAgent, createJsonPrintAgent } from "./agent"; import { createAgentRouter } from "./router"; diff --git a/typescript/examples/music/package.json b/typescript/examples/music/package.json index fd267cd9..5c6fd1f4 100644 --- a/typescript/examples/music/package.json +++ b/typescript/examples/music/package.json @@ -21,14 +21,14 @@ "find-config": "^1.0.0", "open": "^7.0.4", "sqlite3": "^5.1.6", - "typechat": "^0.1.0" + "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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/music/src/trackFilter.ts b/typescript/examples/music/src/trackFilter.ts index bfa4c1ad..cee22111 100644 --- a/typescript/examples/music/src/trackFilter.ts +++ b/typescript/examples/music/src/trackFilter.ts @@ -1,4 +1,4 @@ -import { TypeChatLanguageModel } from "../../../dist"; +import { TypeChatLanguageModel } from "typechat"; import { getArtist } from "./endpoints"; import { IClientContext } from "./main"; diff --git a/typescript/examples/restaurant/package.json b/typescript/examples/restaurant/package.json index 38bd86bb..eebfd3a5 100644 --- a/typescript/examples/restaurant/package.json +++ b/typescript/examples/restaurant/package.json @@ -13,12 +13,12 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/restaurant/src/main.ts b/typescript/examples/restaurant/src/main.ts index b0bf6afb..fd95e89b 100644 --- a/typescript/examples/restaurant/src/main.ts +++ b/typescript/examples/restaurant/src/main.ts @@ -6,8 +6,10 @@ import path from "path"; import { createJsonTranslator, createLanguageModel, - processRequests, } from "typechat"; +import { + processRequests, +} from "typechat/interactive"; import { createTypeScriptJsonValidator, } from "typechat/ts"; diff --git a/typescript/examples/sentiment-zod/src/main.ts b/typescript/examples/sentiment-zod/src/main.ts index 5f521707..d52a3795 100644 --- a/typescript/examples/sentiment-zod/src/main.ts +++ b/typescript/examples/sentiment-zod/src/main.ts @@ -1,7 +1,8 @@ import assert from "assert"; import dotenv from "dotenv"; import findConfig from "find-config"; -import { createJsonTranslator, createLanguageModel, processRequests } from "typechat"; +import { createJsonTranslator, createLanguageModel } from "typechat"; +import { processRequests } from "typechat/interactive"; import { createZodJsonValidator } from "typechat/zod"; import { SentimentSchema } from "./sentimentSchema"; diff --git a/typescript/examples/sentiment/package.json b/typescript/examples/sentiment/package.json index d744058b..0d07ecc8 100644 --- a/typescript/examples/sentiment/package.json +++ b/typescript/examples/sentiment/package.json @@ -13,12 +13,12 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } } diff --git a/typescript/examples/sentiment/src/main.ts b/typescript/examples/sentiment/src/main.ts index b07b2aaa..4a7590e4 100644 --- a/typescript/examples/sentiment/src/main.ts +++ b/typescript/examples/sentiment/src/main.ts @@ -3,7 +3,8 @@ import dotenv from "dotenv"; import findConfig from "find-config"; import fs from "fs"; import path from "path"; -import { createJsonTranslator, createLanguageModel, processRequests } from "typechat"; +import { createJsonTranslator, createLanguageModel } from "typechat"; +import { processRequests } from "typechat/interactive"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { SentimentResponse } from "./sentimentSchema"; diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 00b91101..5748ad4c 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -12,15 +12,23 @@ "./", "./examples/*" ], - "dependencies": { - "typescript": "^5.3.3", - "zod": "^3.22.4" - }, "devDependencies": { "@types/node": "^20.10.4" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.3.3", + "zod": "^3.22.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } } }, "examples/calendar": { @@ -29,13 +37,13 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } }, "examples/coffeeShop": { @@ -45,13 +53,13 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } }, "examples/coffeeShop-zod": { @@ -78,13 +86,13 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.1.3" + "copyfiles": "^2.4.1" } }, "examples/math": { @@ -93,13 +101,13 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } }, "examples/multiSchema": { @@ -110,13 +118,13 @@ "dotenv": "^16.3.1", "find-config": "^1.0.0", "music": "^0.0.1", - "typechat": "^0.1.0" + "typechat": "^0.1.0", + "typescript": "^5.3.3" }, "devDependencies": { "@types/find-config": "1.0.4", "@types/node": "^20.3.1", - "copyfiles": "^2.4.1", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } }, "examples/music": { @@ -130,15 +138,15 @@ "find-config": "^1.0.0", "open": "^7.0.4", "sqlite3": "^5.1.6", - "typechat": "^0.1.0" + "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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } }, "examples/restaurant": { @@ -147,13 +155,13 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } }, "examples/sentiment": { @@ -162,13 +170,13 @@ "dependencies": { "dotenv": "^16.3.1", "find-config": "^1.0.0", - "typechat": "^0.1.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", - "typescript": "^5.3.3" + "copyfiles": "^2.4.1" } }, "examples/sentiment-zod": { @@ -787,9 +795,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -1041,16 +1049,16 @@ } }, "node_modules/express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -1115,9 +1123,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -2540,9 +2548,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", diff --git a/typescript/package.json b/typescript/package.json index 21b9d4f7..ddb93c06 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -27,7 +27,8 @@ "exports": { ".": "./dist/index.js", "./ts": "./dist/ts/index.js", - "./zod": "./dist/zod/index.js" + "./zod": "./dist/zod/index.js", + "./interactive": "./dist/interactive/index.js" }, "engines": { "node": ">=18" @@ -38,10 +39,19 @@ "README.md", "SECURITY.md" ], - "dependencies": { + "dependencies": {}, + "peerDependencies": { "typescript": "^5.3.3", "zod": "^3.22.4" }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + }, "devDependencies": { "@types/node": "^20.10.4" }, diff --git a/typescript/src/index.ts b/typescript/src/index.ts index aae46aba..7029f515 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -1,4 +1,3 @@ export * from './result'; export * from './model'; export * from './typechat'; -export * from './interactive'; diff --git a/typescript/src/interactive/index.ts b/typescript/src/interactive/index.ts new file mode 100644 index 00000000..aad61099 --- /dev/null +++ b/typescript/src/interactive/index.ts @@ -0,0 +1 @@ +export * from './interactive'; diff --git a/typescript/src/interactive.ts b/typescript/src/interactive/interactive.ts similarity index 100% rename from typescript/src/interactive.ts rename to typescript/src/interactive/interactive.ts