From bb9e32f2057d3f7e5eb309c55bd2af9d16b138d1 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 17 Apr 2024 14:13:02 -0700 Subject: [PATCH 01/26] Install python-dotenv and tabulate --- python/notebooks/coffeeShop.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/notebooks/coffeeShop.ipynb b/python/notebooks/coffeeShop.ipynb index 9a95f51f..c5ecc602 100644 --- a/python/notebooks/coffeeShop.ipynb +++ b/python/notebooks/coffeeShop.ipynb @@ -7,7 +7,9 @@ "outputs": [], "source": [ "%pip install --upgrade setuptools\n", - "%pip install --upgrade gradio" + "%pip install --upgrade gradio\n", + "%pip install --upgrade python-dotenv\n", + "%pip install --upgrade tabulate" ] }, { From 673758122bc3b93241e8b92297b678c0c845a55f Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 17 Apr 2024 14:19:40 -0700 Subject: [PATCH 02/26] Small working demo that can draw boxes on a canvas The idea is that you can write a small amount of JavaScript that reads the JSON and renders in an HTML5 canvas, or some Python that renders it in Tkinter, or anything else that strikes your fancy. I would like to use this to draw diagrams representing the stack and memory layout for an interpreter, to be used in internal docs. --- python/examples/drawing/__init__.py | 2 ++ python/examples/drawing/demo.py | 37 ++++++++++++++++++++++++ python/examples/drawing/schema.py | 44 +++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 python/examples/drawing/__init__.py create mode 100644 python/examples/drawing/demo.py create mode 100644 python/examples/drawing/schema.py diff --git a/python/examples/drawing/__init__.py b/python/examples/drawing/__init__.py new file mode 100644 index 00000000..ea063947 --- /dev/null +++ b/python/examples/drawing/__init__.py @@ -0,0 +1,2 @@ +# Let's draw some diagrams + diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py new file mode 100644 index 00000000..4efffc50 --- /dev/null +++ b/python/examples/drawing/demo.py @@ -0,0 +1,37 @@ +import asyncio +import json +import sys +from typing import Any + +import schema as drawing +from dotenv import dotenv_values + +from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests + + +async def main(file_path: str | None): + env_vals = dotenv_values() + model = create_language_model(env_vals) + validator = TypeChatValidator(drawing.Drawing) + translator = TypeChatJsonTranslator(model, validator, drawing.Drawing) + print(translator._schema_str) + + async def request_handler(message: str): + result: Success[drawing.Drawing] | Failure = await translator.translate(message) + if isinstance(result, Failure): + print(result.message) + else: + value = result.value + print(json.dumps(value, indent=2)) + if any(item["type"] == "Unknown" for item in value["items"]): + print("I did not understand the following") + for item in value["items"]: + if item["type"] == "Unknown": + print(item["text"]) + + await process_requests("~> ", file_path, request_handler) + + +if __name__ == "__main__": + file_path = sys.argv[1] if len(sys.argv) == 2 else None + asyncio.run(main(file_path)) diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py new file mode 100644 index 00000000..97df5802 --- /dev/null +++ b/python/examples/drawing/schema.py @@ -0,0 +1,44 @@ +"""TypeChat schema for simple line drawings.""" + + +from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc + + +class Style(TypedDict): + type: Literal["Style"] + corners: Literal["rounded", "sharp"] + # We'll add things like line thickness, color, fill, etc. later + + +class Box(TypedDict): + """A rectangular box. + + The coordinate system has origin top left, x points right, y points down. + Measurements are in pixels. + + There can also be text in the box. There are optional style properties. + + """ + type: Literal["Box"] + x: Annotated[int, Doc("Top left corner coordinates")] + y: int + width: Annotated[int, Doc("Size of the box")] + height: int + text: Annotated[str, Doc("Text centered in the box")] + style: Annotated[Style | None, Doc("Box drawing style (optional)")] + + +class UnknownText(TypedDict): + """ + Use this type for input that match nothing else + """ + + type: Literal["Unknown"] + text: Annotated[str, Doc("The text that wasn't understood")] + + +class Drawing(TypedDict): + """ + A drawing is a list of boxes. (We'll add other elements later, like arrows.) + """ + items: list[Box | UnknownText] From de03aa90608066135f47eace560489f6bab2b154 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 17 Apr 2024 16:02:10 -0700 Subject: [PATCH 03/26] Add chat history to drawing demo --- python/examples/drawing/demo.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 4efffc50..77cbf96b 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -1,12 +1,11 @@ import asyncio import json import sys -from typing import Any import schema as drawing from dotenv import dotenv_values -from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests +from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, PromptSection, create_language_model, process_requests async def main(file_path: str | None): @@ -14,20 +13,25 @@ async def main(file_path: str | None): model = create_language_model(env_vals) validator = TypeChatValidator(drawing.Drawing) translator = TypeChatJsonTranslator(model, validator, drawing.Drawing) - print(translator._schema_str) + # print(translator._schema_str) + + history: list[PromptSection] = [] async def request_handler(message: str): - result: Success[drawing.Drawing] | Failure = await translator.translate(message) + result: Success[drawing.Drawing] | Failure = await translator.translate(message, prompt_preamble=history) if isinstance(result, Failure): print(result.message) else: value = result.value - print(json.dumps(value, indent=2)) + output = json.dumps(value, indent=2) + print(output) if any(item["type"] == "Unknown" for item in value["items"]): print("I did not understand the following") for item in value["items"]: if item["type"] == "Unknown": print(item["text"]) + history.append({"role": "user", "content": message}) + history.append({"role": "assistant", "content": output}) await process_requests("~> ", file_path, request_handler) From 2edaedd80a96cdce00ed82c78d8058c283610c4e Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 17 Apr 2024 16:02:48 -0700 Subject: [PATCH 04/26] [WIP] Hack on translation a bit --- python/src/typechat/_internal/model.py | 1 + python/src/typechat/_internal/translator.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index da52e306..062ce26c 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -92,6 +92,7 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa except Exception as e: if retry_count >= self.max_retry_attempts: return Failure(str(e) or f"{repr(e)} raised from within internal TypeChat language model.") + print(f"retrying ({e!r}) ...") await asyncio.sleep(self.retry_pause_seconds) retry_count += 1 diff --git a/python/src/typechat/_internal/translator.py b/python/src/typechat/_internal/translator.py index 141a4772..7e9369b1 100644 --- a/python/src/typechat/_internal/translator.py +++ b/python/src/typechat/_internal/translator.py @@ -63,9 +63,9 @@ async def translate(self, request: str, *, prompt_preamble: str | list[PromptSec """ request = self._create_request_prompt(request) - prompt: str | list[PromptSection] + prompt: list[PromptSection] if prompt_preamble is None: - prompt = request + prompt = [{"role": "user", "content": request}] else: if isinstance(prompt_preamble, str): prompt_preamble = [{"role": "user", "content": prompt_preamble}] @@ -73,6 +73,9 @@ async def translate(self, request: str, *, prompt_preamble: str | list[PromptSec num_repairs_attempted = 0 while True: + print("--------- NEXT REQUEST ---------") + for thing in prompt: print(thing) + print() completion_response = await self.model.complete(prompt) if isinstance(completion_response, Failure): return completion_response @@ -93,7 +96,8 @@ 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)}" + print("Trying to repair", repr(error_message)) + prompt.append({"role": "user", "content": self._create_repair_prompt(error_message)}) def _create_request_prompt(self, intent: str) -> str: prompt = f""" From 09ea85934cb5ecb9764aae9eb153adbc5024b799 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 17 Apr 2024 17:29:24 -0700 Subject: [PATCH 05/26] New API for chat history --- python/examples/drawing/demo.py | 10 +-- python/src/typechat/_internal/translator.py | 73 ++++++++++++++++----- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 77cbf96b..ef40316b 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -15,10 +15,10 @@ async def main(file_path: str | None): translator = TypeChatJsonTranslator(model, validator, drawing.Drawing) # print(translator._schema_str) - history: list[PromptSection] = [] + history: list[tuple[str, str]] = [] - async def request_handler(message: str): - result: Success[drawing.Drawing] | Failure = await translator.translate(message, prompt_preamble=history) + async def request_handler(request: str): + result: Success[drawing.Drawing] | Failure = await translator.translate(request, chat_history=history) if isinstance(result, Failure): print(result.message) else: @@ -30,8 +30,8 @@ async def request_handler(message: str): for item in value["items"]: if item["type"] == "Unknown": print(item["text"]) - history.append({"role": "user", "content": message}) - history.append({"role": "assistant", "content": output}) + else: + history.append((request, output)) await process_requests("~> ", file_path, request_handler) diff --git a/python/src/typechat/_internal/translator.py b/python/src/typechat/_internal/translator.py index 7e9369b1..253e5f42 100644 --- a/python/src/typechat/_internal/translator.py +++ b/python/src/typechat/_internal/translator.py @@ -9,6 +9,8 @@ T = TypeVar("T", covariant=True) + + class TypeChatJsonTranslator(Generic[T]): """ Represents an object that can translate natural language requests in JSON objects of the given type. @@ -49,7 +51,13 @@ 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, + request: str, + *, + prompt_preamble: str | list[PromptSection] | None = None, + chat_history: list[tuple[str, str]] | None = None, # (input, output) pairs + ) -> 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,26 +65,44 @@ async def translate(self, request: str, *, prompt_preamble: str | list[PromptSec This often helps produce a valid instance. Args: - request: A natural language request. - prompt_preamble: An optional string or list of prompt sections to prepend to the generated prompt.\ + 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. + chat_history: An optional list of (input, output) pairs from previous interactions. + + The "messages" list sent to the model has the following structure: + + - The prompt_preamble, if any (unaltered) + - The description of the schema, with a suitable prefix ("You are a service ...") + - Past user inputs and bot outputs from chat_history, with suitable markup + - The final user input, from 'request', with suitable markup + - When in repair mode, additional repair prompts + + The prompt preamble is send unaltered. + The others are marked up using stereotypical phrases to indicate their role in the conversation. """ - request = self._create_request_prompt(request) + messages: list[PromptSection] = [] - prompt: list[PromptSection] - if prompt_preamble is None: - prompt = [{"role": "user", "content": request}] - else: + if prompt_preamble: if isinstance(prompt_preamble, str): - prompt_preamble = [{"role": "user", "content": prompt_preamble}] - prompt = [*prompt_preamble, {"role": "user", "content": request}] + messages.append({"role": "user", "content": prompt_preamble}) + else: + messages.extend(prompt_preamble) + + messages.append({"role": "user", "content": self._create_system_prompt()}) # Maybe role: system? + + for input, output in chat_history or []: + messages.append({"role": "user", "content": self._create_user_request(input)}) + messages.append({"role": "assistant", "content": self._create_bot_output(output)}) + + messages.append({"role": "user", "content": self._create_user_request(request, challenge=True)}) num_repairs_attempted = 0 while True: print("--------- NEXT REQUEST ---------") - for thing in prompt: print(thing) + for thing in messages: print(thing) print() - completion_response = await self.model.complete(prompt) + completion_response = await self.model.complete(messages) if isinstance(completion_response, Failure): return completion_response @@ -97,21 +123,36 @@ async def translate(self, request: str, *, prompt_preamble: str | list[PromptSec return Failure(error_message) num_repairs_attempted += 1 print("Trying to repair", repr(error_message)) - prompt.append({"role": "user", "content": 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""" + def _create_system_prompt(self) -> str: + return f""" You are a service that translates user requests into JSON objects of type "{self._type_name}" according to the following TypeScript definitions: ``` {self._schema_str} ``` +""" + + def _create_user_request(self, input: str, challenge: bool = False) -> str: + prompt = f""" The following is a user request: ''' -{intent} +{input} ''' +""" + if challenge: + prompt += """ The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: """ return prompt + + def _create_bot_output(self, output: str) -> str: + return f""" +The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: +``` +{output} +``` +""" def _create_repair_prompt(self, validation_error: str) -> str: prompt = f""" From c52aa535a58facbd6889e0b279224fa5da004f05 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 18 Apr 2024 17:48:49 -0700 Subject: [PATCH 06/26] Improved the schema with ChatGPT's help. Added Arrow and Ellipse. --- python/examples/drawing/schema.py | 67 ++++++++++++++++++------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py index 97df5802..0315676f 100644 --- a/python/examples/drawing/schema.py +++ b/python/examples/drawing/schema.py @@ -1,44 +1,53 @@ -"""TypeChat schema for simple line drawings.""" - - -from typing_extensions import Literal, NotRequired, TypedDict, Annotated, Doc +from typing_extensions import Literal, TypedDict, Annotated, Doc, Optional class Style(TypedDict): type: Literal["Style"] - corners: Literal["rounded", "sharp"] - # We'll add things like line thickness, color, fill, etc. later + corners: Annotated[Literal["rounded", "sharp"], Doc("Corner style of the drawing elements.")] + line_thickness: Annotated[Optional[int], Doc("Thickness of the lines.")] + line_color: Annotated[Optional[str], Doc("CSS-style color code for line color.")] + fill_color: Annotated[Optional[str], Doc("CSS-style color code for fill color.")] class Box(TypedDict): - """A rectangular box. - - The coordinate system has origin top left, x points right, y points down. - Measurements are in pixels. - - There can also be text in the box. There are optional style properties. - - """ + """A rectangular box defined by a coordinate system with the origin at the top left.""" type: Literal["Box"] - x: Annotated[int, Doc("Top left corner coordinates")] - y: int - width: Annotated[int, Doc("Size of the box")] - height: int - text: Annotated[str, Doc("Text centered in the box")] - style: Annotated[Style | None, Doc("Box drawing style (optional)")] + x: Annotated[int, Doc("X-coordinate of the top left corner.")] + y: Annotated[int, Doc("Y-coordinate of the top left corner.")] + width: Annotated[int, Doc("Width of the box.")] + height: Annotated[int, Doc("Height of the box.")] + text: Annotated[Optional[str], Doc("Optional text centered in the box.")] + style: Annotated[Optional[Style], Doc("Optional style settings for the box.")] + + +class Ellipse(TypedDict): + """An ellipse defined by its bounding box dimensions.""" + type: Literal["Ellipse"] + x: Annotated[int, Doc("X-coordinate of the top left corner of the bounding box.")] + y: Annotated[int, Doc("Y-coordinate of the top left corner of the bounding box.")] + width: Annotated[int, Doc("Width of the bounding box.")] + height: Annotated[int, Doc("Height of the bounding box.")] + text: Annotated[Optional[str], Doc("Optional text centered in the box.")] + style: Annotated[Optional[Style], Doc("Optional style settings for the ellipse.")] + + +class Arrow(TypedDict): + """A line with a directional arrow at one or both ends, defined by start and end points.""" + type: Literal["Arrow"] + start_x: Annotated[int, Doc("Starting X-coordinate.")] + start_y: Annotated[int, Doc("Starting Y-coordinate.")] + end_x: Annotated[int, Doc("Ending X-coordinate.")] + end_y: Annotated[int, Doc("Ending Y-coordinate.")] + style: Annotated[Optional[Style], Doc("Optional style settings for the arrow.")] + head_size: Annotated[Optional[int], Doc("Size of the arrowhead, if present.")] class UnknownText(TypedDict): - """ - Use this type for input that match nothing else - """ - + """Used for input that does not match any other specified type.""" type: Literal["Unknown"] - text: Annotated[str, Doc("The text that wasn't understood")] + text: Annotated[str, Doc("The text that wasn't understood.")] class Drawing(TypedDict): - """ - A drawing is a list of boxes. (We'll add other elements later, like arrows.) - """ - items: list[Box | UnknownText] + """A collection of graphical elements including boxes, ellipses, arrows, and unrecognized text.""" + items: Annotated[list[Box | Arrow | Ellipse | UnknownText], Doc("List of drawable elements.")] From 5d07a2aeaebd5d60518187228080b3bcabb49b3a Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 18 Apr 2024 18:01:52 -0700 Subject: [PATCH 07/26] rendering program using Tkinter --- python/examples/drawing/render.py | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 python/examples/drawing/render.py diff --git a/python/examples/drawing/render.py b/python/examples/drawing/render.py new file mode 100644 index 00000000..b59e08fa --- /dev/null +++ b/python/examples/drawing/render.py @@ -0,0 +1,58 @@ +import tkinter as tk +from tkinter import Canvas + +def render_drawing(drawing): + # Create a new Tkinter window + window = tk.Tk() + window.title("Drawing") + + # Create a canvas widget + canvas = Canvas(window, width=800, height=600, bg='white') + canvas.pack() + + # Function to draw a box with text if provided + def draw_box(box): + x1, y1 = box['x'], box['y'] + x2, y2 = x1 + box['width'], y1 + box['height'] + fill = box['style'].get('fill_color', '') if 'style' in box else '' + canvas.create_rectangle(x1, y1, x2, y2, outline='black', fill=fill) + if 'text' in box and box['text']: + canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=box['text'], fill='black') + + # Function to draw an ellipse with text if provided + def draw_ellipse(ellipse): + x1, y1 = ellipse['x'], ellipse['y'] + x2, y2 = x1 + ellipse['width'], y1 + ellipse['height'] + fill = ellipse['style'].get('fill_color', '') if ellipse['style'] else '' + canvas.create_oval(x1, y1, x2, y2, outline='black', fill=fill) + if 'text' in ellipse and ellipse['text']: + canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=ellipse['text'], fill='black') + + # Function to draw an arrow + def draw_arrow(arrow): + x1, y1 = arrow['start_x'], arrow['start_y'] + x2, y2 = arrow['end_x'], arrow['end_y'] + canvas.create_line(x1, y1, x2, y2, arrow=tk.LAST) + + # Iterate through each item in the drawing and render it + for item in drawing['items']: + if item['type'] == 'Box': + draw_box(item) + elif item['type'] == 'Ellipse': + draw_ellipse(item) + elif item['type'] == 'Arrow': + draw_arrow(item) + + # Start the Tkinter event loop + window.mainloop() + +# Example usage: +drawing = { + 'items': [ + {'type': 'Box', 'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Hello'}, + {'type': 'Ellipse', 'x': 200, 'y': 50, 'width': 150, 'height': 100, 'text': 'World', 'style': {'fill_color': 'lightblue'}}, + {'type': 'Arrow', 'start_x': 50, 'start_y': 200, 'end_x': 150, 'end_y': 200} + ] +} + +render_drawing(drawing) From f46a8361f7e8c65c55537b9accaed63d722ddd5b Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 18 Apr 2024 18:41:24 -0700 Subject: [PATCH 08/26] Add a simple rendering function (most of it written by ChatGPT) --- python/examples/drawing/demo.py | 5 ++++- python/examples/drawing/render.py | 33 ++++++++++++++++++------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index ef40316b..e0a17170 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -2,9 +2,11 @@ import json import sys -import schema as drawing from dotenv import dotenv_values +import schema as drawing +from render import render_drawing + from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, PromptSection, create_language_model, process_requests @@ -32,6 +34,7 @@ async def request_handler(request: str): print(item["text"]) else: history.append((request, output)) + render_drawing(value) await process_requests("~> ", file_path, request_handler) diff --git a/python/examples/drawing/render.py b/python/examples/drawing/render.py index b59e08fa..8e1b93b4 100644 --- a/python/examples/drawing/render.py +++ b/python/examples/drawing/render.py @@ -1,20 +1,20 @@ import tkinter as tk -from tkinter import Canvas def render_drawing(drawing): # Create a new Tkinter window window = tk.Tk() window.title("Drawing") + window.configure(bg='white') # Set the background color of the window # Create a canvas widget - canvas = Canvas(window, width=800, height=600, bg='white') - canvas.pack() + canvas = tk.Canvas(window, width=800, height=600, bg='white', highlightthickness=0) + canvas.pack(padx=10, pady=10) # Adds 10 pixels of padding on all sides # Function to draw a box with text if provided def draw_box(box): x1, y1 = box['x'], box['y'] x2, y2 = x1 + box['width'], y1 + box['height'] - fill = box['style'].get('fill_color', '') if 'style' in box else '' + fill = box['style'].get('fill_color', '') if box['style'] else '' canvas.create_rectangle(x1, y1, x2, y2, outline='black', fill=fill) if 'text' in box and box['text']: canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=box['text'], fill='black') @@ -32,7 +32,7 @@ def draw_ellipse(ellipse): def draw_arrow(arrow): x1, y1 = arrow['start_x'], arrow['start_y'] x2, y2 = arrow['end_x'], arrow['end_y'] - canvas.create_line(x1, y1, x2, y2, arrow=tk.LAST) + canvas.create_line(x1, y1, x2, y2, arrow=tk.LAST, fill='black') # Iterate through each item in the drawing and render it for item in drawing['items']: @@ -43,16 +43,21 @@ def draw_arrow(arrow): elif item['type'] == 'Arrow': draw_arrow(item) + # Button to close the window (pretty ugly -- use Cmd-W/Ctrl-W instead) + # quit_button = tk.Button(window, text="Quit", command=window.quit) + # quit_button.pack(side=tk.BOTTOM, pady=10) + # Start the Tkinter event loop window.mainloop() # Example usage: -drawing = { - 'items': [ - {'type': 'Box', 'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Hello'}, - {'type': 'Ellipse', 'x': 200, 'y': 50, 'width': 150, 'height': 100, 'text': 'World', 'style': {'fill_color': 'lightblue'}}, - {'type': 'Arrow', 'start_x': 50, 'start_y': 200, 'end_x': 150, 'end_y': 200} - ] -} - -render_drawing(drawing) +if __name__ == '__main__': + drawing = { + 'items': [ + {'type': 'Box', 'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Hello', 'style': None}, + {'type': 'Ellipse', 'x': 200, 'y': 50, 'width': 150, 'height': 100, 'text': 'World', 'style': {'fill_color': 'lightblue'}}, + {'type': 'Arrow', 'start_x': 50, 'start_y': 200, 'end_x': 150, 'end_y': 200} + ] + } + + render_drawing(drawing) From 131ad4f1e07253bf95754081ec68ff2e7ff61b61 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 18 Apr 2024 19:32:29 -0700 Subject: [PATCH 09/26] Added dashed and dotted arrows (some help from ChatGPT) --- python/examples/drawing/render.py | 15 +++++++++++++-- python/examples/drawing/schema.py | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/python/examples/drawing/render.py b/python/examples/drawing/render.py index 8e1b93b4..463c301b 100644 --- a/python/examples/drawing/render.py +++ b/python/examples/drawing/render.py @@ -1,5 +1,12 @@ import tkinter as tk +# Map line style to dash patterns +dash_pattern = { + 'solid': None, + 'dashed': (4, 4), # 4 pixels drawn, 4 pixels space + 'dotted': (1, 1) # 1 pixel drawn, 1 pixel space +} + def render_drawing(drawing): # Create a new Tkinter window window = tk.Tk() @@ -32,7 +39,10 @@ def draw_ellipse(ellipse): def draw_arrow(arrow): x1, y1 = arrow['start_x'], arrow['start_y'] x2, y2 = arrow['end_x'], arrow['end_y'] - canvas.create_line(x1, y1, x2, y2, arrow=tk.LAST, fill='black') + line_style = (arrow['style'].get('line_style', 'solid') # Default line style + if arrow['style'] else 'solid') + + canvas.create_line(x1, y1, x2, y2, dash=dash_pattern[line_style], arrow=tk.LAST, fill='black') # Iterate through each item in the drawing and render it for item in drawing['items']: @@ -56,7 +66,8 @@ def draw_arrow(arrow): 'items': [ {'type': 'Box', 'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Hello', 'style': None}, {'type': 'Ellipse', 'x': 200, 'y': 50, 'width': 150, 'height': 100, 'text': 'World', 'style': {'fill_color': 'lightblue'}}, - {'type': 'Arrow', 'start_x': 50, 'start_y': 200, 'end_x': 150, 'end_y': 200} + {'type': 'Arrow', 'start_x': 50, 'start_y': 200, 'end_x': 150, 'end_y': 200, + 'style': {'type': 'Style', 'line_style': 'dashed'}, 'head_size': 10} ] } diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py index 0315676f..77b24f8f 100644 --- a/python/examples/drawing/schema.py +++ b/python/examples/drawing/schema.py @@ -7,6 +7,7 @@ class Style(TypedDict): line_thickness: Annotated[Optional[int], Doc("Thickness of the lines.")] line_color: Annotated[Optional[str], Doc("CSS-style color code for line color.")] fill_color: Annotated[Optional[str], Doc("CSS-style color code for fill color.")] + line_style: Annotated[Optional[str], Doc("Style of the line: 'solid', 'dashed', 'dotted'.")] class Box(TypedDict): From b90faa72463ec63c9416411636ec31bac64cc37f Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 22 Apr 2024 13:20:06 -0700 Subject: [PATCH 10/26] Rip oput history, for now --- python/examples/drawing/demo.py | 5 +- python/src/typechat/_internal/model.py | 1 - python/src/typechat/_internal/translator.py | 61 +++------------------ 3 files changed, 10 insertions(+), 57 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index e0a17170..178c66cc 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -17,10 +17,8 @@ async def main(file_path: str | None): translator = TypeChatJsonTranslator(model, validator, drawing.Drawing) # print(translator._schema_str) - history: list[tuple[str, str]] = [] - async def request_handler(request: str): - result: Success[drawing.Drawing] | Failure = await translator.translate(request, chat_history=history) + result: Success[drawing.Drawing] | Failure = await translator.translate(request) if isinstance(result, Failure): print(result.message) else: @@ -33,7 +31,6 @@ async def request_handler(request: str): if item["type"] == "Unknown": print(item["text"]) else: - history.append((request, output)) render_drawing(value) await process_requests("~> ", file_path, request_handler) diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index 062ce26c..da52e306 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -92,7 +92,6 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa except Exception as e: if retry_count >= self.max_retry_attempts: return Failure(str(e) or f"{repr(e)} raised from within internal TypeChat language model.") - print(f"retrying ({e!r}) ...") await asyncio.sleep(self.retry_pause_seconds) retry_count += 1 diff --git a/python/src/typechat/_internal/translator.py b/python/src/typechat/_internal/translator.py index 253e5f42..6c649c1e 100644 --- a/python/src/typechat/_internal/translator.py +++ b/python/src/typechat/_internal/translator.py @@ -9,8 +9,6 @@ T = TypeVar("T", covariant=True) - - class TypeChatJsonTranslator(Generic[T]): """ Represents an object that can translate natural language requests in JSON objects of the given type. @@ -51,13 +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, - chat_history: list[tuple[str, str]] | None = None, # (input, output) pairs - ) -> 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`. @@ -66,42 +58,23 @@ async def translate( Args: input: A natural language request. - prompt_preamble: An optional string or list of prompt sections to prepend to the generated prompt. + 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. - chat_history: An optional list of (input, output) pairs from previous interactions. - - The "messages" list sent to the model has the following structure: - - - The prompt_preamble, if any (unaltered) - - The description of the schema, with a suitable prefix ("You are a service ...") - - Past user inputs and bot outputs from chat_history, with suitable markup - - The final user input, from 'request', with suitable markup - - When in repair mode, additional repair prompts - - The prompt preamble is send unaltered. - The others are marked up using stereotypical phrases to indicate their role in the conversation. """ + messages: list[PromptSection] = [] + messages.append({"role": "user", "content": input}) if prompt_preamble: if isinstance(prompt_preamble, str): - messages.append({"role": "user", "content": prompt_preamble}) + prompt_preamble = [{"role": "user", "content": prompt_preamble}] else: messages.extend(prompt_preamble) - messages.append({"role": "user", "content": self._create_system_prompt()}) # Maybe role: system? - - for input, output in chat_history or []: - messages.append({"role": "user", "content": self._create_user_request(input)}) - messages.append({"role": "assistant", "content": self._create_bot_output(output)}) - - messages.append({"role": "user", "content": self._create_user_request(request, challenge=True)}) + messages.append({"role": "user", "content": self._create_request_prompt(input)}) num_repairs_attempted = 0 while True: - print("--------- NEXT REQUEST ---------") - for thing in messages: print(thing) - print() completion_response = await self.model.complete(messages) if isinstance(completion_response, Failure): return completion_response @@ -122,37 +95,21 @@ async def translate( if num_repairs_attempted >= self._max_repair_attempts: return Failure(error_message) num_repairs_attempted += 1 - print("Trying to repair", repr(error_message)) messages.append({"role": "user", "content": self._create_repair_prompt(error_message)}) - def _create_system_prompt(self) -> str: - return f""" + def _create_request_prompt(self, intent: str) -> str: + prompt = f""" You are a service that translates user requests into JSON objects of type "{self._type_name}" according to the following TypeScript definitions: ``` {self._schema_str} ``` -""" - - def _create_user_request(self, input: str, challenge: bool = False) -> str: - prompt = f""" The following is a user request: ''' -{input} +{intent} ''' -""" - if challenge: - prompt += """ The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: """ return prompt - - def _create_bot_output(self, output: str) -> str: - return f""" -The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: -``` -{output} -``` -""" def _create_repair_prompt(self, validation_error: str) -> str: prompt = f""" From 02e28e4f5f459c1cbaf56a5b9dac77a9cc78de92 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 22 Apr 2024 13:33:11 -0700 Subject: [PATCH 11/26] Make pyright happy --- python/examples/drawing/demo.py | 2 +- python/examples/drawing/render.py | 20 +++++++++++--------- python/examples/drawing/schema.py | 12 ++++++------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 178c66cc..6fff7791 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -7,7 +7,7 @@ import schema as drawing from render import render_drawing -from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, PromptSection, create_language_model, process_requests +from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests async def main(file_path: str | None): diff --git a/python/examples/drawing/render.py b/python/examples/drawing/render.py index 463c301b..f88fd48c 100644 --- a/python/examples/drawing/render.py +++ b/python/examples/drawing/render.py @@ -1,13 +1,15 @@ import tkinter as tk +from schema import Drawing, Box, Ellipse, Arrow + # Map line style to dash patterns dash_pattern = { - 'solid': None, + 'solid': '', 'dashed': (4, 4), # 4 pixels drawn, 4 pixels space 'dotted': (1, 1) # 1 pixel drawn, 1 pixel space } -def render_drawing(drawing): +def render_drawing(drawing: Drawing): # Create a new Tkinter window window = tk.Tk() window.title("Drawing") @@ -18,7 +20,7 @@ def render_drawing(drawing): canvas.pack(padx=10, pady=10) # Adds 10 pixels of padding on all sides # Function to draw a box with text if provided - def draw_box(box): + def draw_box(box: Box): x1, y1 = box['x'], box['y'] x2, y2 = x1 + box['width'], y1 + box['height'] fill = box['style'].get('fill_color', '') if box['style'] else '' @@ -27,7 +29,7 @@ def draw_box(box): canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=box['text'], fill='black') # Function to draw an ellipse with text if provided - def draw_ellipse(ellipse): + def draw_ellipse(ellipse: Ellipse): x1, y1 = ellipse['x'], ellipse['y'] x2, y2 = x1 + ellipse['width'], y1 + ellipse['height'] fill = ellipse['style'].get('fill_color', '') if ellipse['style'] else '' @@ -36,7 +38,7 @@ def draw_ellipse(ellipse): canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=ellipse['text'], fill='black') # Function to draw an arrow - def draw_arrow(arrow): + def draw_arrow(arrow: Arrow): x1, y1 = arrow['start_x'], arrow['start_y'] x2, y2 = arrow['end_x'], arrow['end_y'] line_style = (arrow['style'].get('line_style', 'solid') # Default line style @@ -62,13 +64,13 @@ def draw_arrow(arrow): # Example usage: if __name__ == '__main__': - drawing = { + drawing: Drawing = { 'items': [ {'type': 'Box', 'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Hello', 'style': None}, - {'type': 'Ellipse', 'x': 200, 'y': 50, 'width': 150, 'height': 100, 'text': 'World', 'style': {'fill_color': 'lightblue'}}, + {'type': 'Ellipse', 'x': 200, 'y': 50, 'width': 150, 'height': 100, 'text': 'World', 'style': {'type': 'Style', 'fill_color': 'lightblue'}}, {'type': 'Arrow', 'start_x': 50, 'start_y': 200, 'end_x': 150, 'end_y': 200, - 'style': {'type': 'Style', 'line_style': 'dashed'}, 'head_size': 10} - ] + 'style': {'type': 'Style', 'line_style': 'dashed'}, 'head_size': 10}, + ], } render_drawing(drawing) diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py index 77b24f8f..accf22a8 100644 --- a/python/examples/drawing/schema.py +++ b/python/examples/drawing/schema.py @@ -1,13 +1,13 @@ -from typing_extensions import Literal, TypedDict, Annotated, Doc, Optional +from typing_extensions import Literal, TypedDict, Annotated, Doc, NotRequired, Optional class Style(TypedDict): type: Literal["Style"] - corners: Annotated[Literal["rounded", "sharp"], Doc("Corner style of the drawing elements.")] - line_thickness: Annotated[Optional[int], Doc("Thickness of the lines.")] - line_color: Annotated[Optional[str], Doc("CSS-style color code for line color.")] - fill_color: Annotated[Optional[str], Doc("CSS-style color code for fill color.")] - line_style: Annotated[Optional[str], Doc("Style of the line: 'solid', 'dashed', 'dotted'.")] + corners: Annotated[NotRequired[Literal["rounded", "sharp"]], Doc("Corner style of the drawing elements.")] + line_thickness: Annotated[NotRequired[int], Doc("Thickness of the lines.")] + line_color: Annotated[NotRequired[str], Doc("CSS-style color code for line color.")] + fill_color: Annotated[NotRequired[str], Doc("CSS-style color code for fill color.")] + line_style: Annotated[NotRequired[str], Doc("Style of the line: 'solid', 'dashed', 'dotted'.")] class Box(TypedDict): From e9ac5cf5eb10f1ad4bf51b644c7b329afe516a09 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 22 Apr 2024 13:56:43 -0700 Subject: [PATCH 12/26] Keep pydantic also happy --- python/examples/drawing/schema.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py index accf22a8..02d9fa55 100644 --- a/python/examples/drawing/schema.py +++ b/python/examples/drawing/schema.py @@ -3,11 +3,11 @@ class Style(TypedDict): type: Literal["Style"] - corners: Annotated[NotRequired[Literal["rounded", "sharp"]], Doc("Corner style of the drawing elements.")] - line_thickness: Annotated[NotRequired[int], Doc("Thickness of the lines.")] - line_color: Annotated[NotRequired[str], Doc("CSS-style color code for line color.")] - fill_color: Annotated[NotRequired[str], Doc("CSS-style color code for fill color.")] - line_style: Annotated[NotRequired[str], Doc("Style of the line: 'solid', 'dashed', 'dotted'.")] + corners: NotRequired[Annotated[Literal["rounded", "sharp"], Doc("Corner style of the drawing elements.")]] + line_thickness: NotRequired[Annotated[int, Doc("Thickness of the lines.")]] + line_color: NotRequired[Annotated[str, Doc("CSS-style color code for line color.")]] + fill_color: NotRequired[Annotated[str, Doc("CSS-style color code for fill color.")]] + line_style: NotRequired[Annotated[str, Doc("Style of the line: 'solid', 'dashed', 'dotted'.")]] class Box(TypedDict): From 9e14f42228d8dd729d8baa7733d353e30636bbe3 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 22 Apr 2024 14:01:32 -0700 Subject: [PATCH 13/26] Add (flawed) history based on prompt_preamble --- python/examples/drawing/demo.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 6fff7791..00608b34 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -7,7 +7,7 @@ import schema as drawing from render import render_drawing -from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests +from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, PromptSection, create_language_model, process_requests async def main(file_path: str | None): @@ -17,8 +17,10 @@ async def main(file_path: str | None): translator = TypeChatJsonTranslator(model, validator, drawing.Drawing) # print(translator._schema_str) + history: list[PromptSection] = [] + async def request_handler(request: str): - result: Success[drawing.Drawing] | Failure = await translator.translate(request) + result: Success[drawing.Drawing] | Failure = await translator.translate(request, prompt_preamble=history) if isinstance(result, Failure): print(result.message) else: @@ -31,6 +33,8 @@ async def request_handler(request: str): if item["type"] == "Unknown": print(item["text"]) else: + history.append({"role": "user", "content": request}) + history.append({"role": "assistant", "content": output}) render_drawing(value) await process_requests("~> ", file_path, request_handler) From 75c2efe03075a16b1010bcc5ccca16d2daced94c Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 22 Apr 2024 16:36:39 -0700 Subject: [PATCH 14/26] Prune history --- python/examples/drawing/demo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 00608b34..01877d6f 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -20,6 +20,11 @@ async def main(file_path: str | None): history: list[PromptSection] = [] async def request_handler(request: str): + if len(history) > 10: + # Prune history + old = history[:-10] + old = [item for item in old if item["role"] == "user"] + history[:-10] = old result: Success[drawing.Drawing] | Failure = await translator.translate(request, prompt_preamble=history) if isinstance(result, Failure): print(result.message) From 970d50b798d8ce0868b18e8a69b4e96f35a23652 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 22 Apr 2024 16:36:52 -0700 Subject: [PATCH 15/26] Add temporary logging --- python/src/typechat/_internal/model.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index da52e306..a3b544a2 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -65,6 +65,13 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa if isinstance(prompt, str): prompt = [{"role": "user", "content": prompt}] + print("===============>") + for i, item in enumerate(prompt): + role, content = item["role"], item["content"] + print(f"---------- {i} {role} ----------") + print(content) + print("end ===========>") + body = { **self.default_params, "messages": prompt, @@ -85,6 +92,11 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa dict[Literal["choices"], list[dict[Literal["message"], PromptSection]]], response.json() ) + + print("<===============") + print(json_result["choices"][0]["message"]["content"] or "") + print("<=============== end") + 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: From 756027325dc9c95a4232f045e2a413655635376b Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 22 Apr 2024 16:02:41 -0700 Subject: [PATCH 16/26] Alternate prompting scheme from #241 --- python/src/typechat/_internal/translator.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/python/src/typechat/_internal/translator.py b/python/src/typechat/_internal/translator.py index 6c649c1e..63751a13 100644 --- a/python/src/typechat/_internal/translator.py +++ b/python/src/typechat/_internal/translator.py @@ -64,14 +64,14 @@ async def translate(self, input: str, *, prompt_preamble: str | list[PromptSecti messages: list[PromptSection] = [] - messages.append({"role": "user", "content": input}) + messages.append({"role": "system", "content": self._create_request_prompt(input)}) + if prompt_preamble: if isinstance(prompt_preamble, str): prompt_preamble = [{"role": "user", "content": prompt_preamble}] - else: - messages.extend(prompt_preamble) + messages.extend(prompt_preamble) - messages.append({"role": "user", "content": self._create_request_prompt(input)}) + messages.append({"role": "user", "content": input}) num_repairs_attempted = 0 while True: @@ -103,11 +103,7 @@ def _create_request_prompt(self, intent: str) -> str: ``` {self._schema_str} ``` -The following is a user request: -''' -{intent} -''' -The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: +You translate each user request into a JSON object with 2 spaces of indentation and no properties with the value undefined. """ return prompt From 439f52112efdf6c12820634dd9e789b3228f0b04 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 13:02:21 -0700 Subject: [PATCH 17/26] Revert changes to translator.py --- python/src/typechat/_internal/translator.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/python/src/typechat/_internal/translator.py b/python/src/typechat/_internal/translator.py index 63751a13..6c649c1e 100644 --- a/python/src/typechat/_internal/translator.py +++ b/python/src/typechat/_internal/translator.py @@ -64,14 +64,14 @@ async def translate(self, input: str, *, prompt_preamble: str | list[PromptSecti messages: list[PromptSection] = [] - messages.append({"role": "system", "content": self._create_request_prompt(input)}) - + messages.append({"role": "user", "content": input}) if prompt_preamble: if isinstance(prompt_preamble, str): prompt_preamble = [{"role": "user", "content": prompt_preamble}] - messages.extend(prompt_preamble) + else: + messages.extend(prompt_preamble) - messages.append({"role": "user", "content": input}) + messages.append({"role": "user", "content": self._create_request_prompt(input)}) num_repairs_attempted = 0 while True: @@ -103,7 +103,11 @@ def _create_request_prompt(self, intent: str) -> str: ``` {self._schema_str} ``` -You translate each user request into a JSON object with 2 spaces of indentation and no properties with the value undefined. +The following is a user request: +''' +{intent} +''' +The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined: """ return prompt From d385e4057691c9bdde55fb745e5a9cb21e462824 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 13:11:50 -0700 Subject: [PATCH 18/26] Use Anders' suggestion for history (Just append the inputs, separated by newlines.) --- python/examples/drawing/demo.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 01877d6f..e7eba0ca 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -17,15 +17,11 @@ async def main(file_path: str | None): translator = TypeChatJsonTranslator(model, validator, drawing.Drawing) # print(translator._schema_str) - history: list[PromptSection] = [] + history: list[str] = [] async def request_handler(request: str): - if len(history) > 10: - # Prune history - old = history[:-10] - old = [item for item in old if item["role"] == "user"] - history[:-10] = old - result: Success[drawing.Drawing] | Failure = await translator.translate(request, prompt_preamble=history) + history.append(request) + result: Success[drawing.Drawing] | Failure = await translator.translate("\n".join(history)) if isinstance(result, Failure): print(result.message) else: @@ -38,8 +34,6 @@ async def request_handler(request: str): if item["type"] == "Unknown": print(item["text"]) else: - history.append({"role": "user", "content": request}) - history.append({"role": "assistant", "content": output}) render_drawing(value) await process_requests("~> ", file_path, request_handler) From 42ce16a722c08893bd412a0d54f48f72997e53ef Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 15:55:50 -0700 Subject: [PATCH 19/26] Switch schema to dataclasses --- python/examples/drawing/__init__.py | 1 - python/examples/drawing/demo.py | 37 +++++---- python/examples/drawing/input.txt | 5 ++ python/examples/drawing/render.py | 116 +++++++++++++++------------- python/examples/drawing/schema.py | 61 ++++++++++----- 5 files changed, 132 insertions(+), 88 deletions(-) create mode 100644 python/examples/drawing/input.txt diff --git a/python/examples/drawing/__init__.py b/python/examples/drawing/__init__.py index ea063947..b9584561 100644 --- a/python/examples/drawing/__init__.py +++ b/python/examples/drawing/__init__.py @@ -1,2 +1 @@ # Let's draw some diagrams - diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index e7eba0ca..f6c9f1af 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -4,37 +4,44 @@ from dotenv import dotenv_values -import schema as drawing +import schema from render import render_drawing -from typechat import Success, Failure, TypeChatJsonTranslator, TypeChatValidator, PromptSection, create_language_model, process_requests +from typechat import ( + Success, + Failure, + TypeChatJsonTranslator, + TypeChatValidator, + PromptSection, + create_language_model, + process_requests, +) async def main(file_path: str | None): env_vals = dotenv_values() model = create_language_model(env_vals) - validator = TypeChatValidator(drawing.Drawing) - translator = TypeChatJsonTranslator(model, validator, drawing.Drawing) + validator = TypeChatValidator(schema.Drawing) + translator = TypeChatJsonTranslator(model, validator, schema.Drawing) # print(translator._schema_str) history: list[str] = [] async def request_handler(request: str): history.append(request) - result: Success[drawing.Drawing] | Failure = await translator.translate("\n".join(history)) + result: Success[schema.Drawing] | Failure = await translator.translate("\n".join(history)) if isinstance(result, Failure): print(result.message) else: - value = result.value - output = json.dumps(value, indent=2) - print(output) - if any(item["type"] == "Unknown" for item in value["items"]): - print("I did not understand the following") - for item in value["items"]: - if item["type"] == "Unknown": - print(item["text"]) - else: - render_drawing(value) + value: schema.Drawing = result.value + print(value) + if any(isinstance(item, schema.UnknownText) for item in value.items): + print("Unknown text detected. Please provide more context:") + for item in value.items: + if isinstance(item, schema.UnknownText): + print(" ", item.text) + + render_drawing(value) await process_requests("~> ", file_path, request_handler) diff --git a/python/examples/drawing/input.txt b/python/examples/drawing/input.txt new file mode 100644 index 00000000..72f46146 --- /dev/null +++ b/python/examples/drawing/input.txt @@ -0,0 +1,5 @@ +draw three red squares in a diagonal +red is the fill color +make the corners touch +add labels "foo", etc. +make them pink diff --git a/python/examples/drawing/render.py b/python/examples/drawing/render.py index f88fd48c..e7173fa9 100644 --- a/python/examples/drawing/render.py +++ b/python/examples/drawing/render.py @@ -1,76 +1,86 @@ import tkinter as tk -from schema import Drawing, Box, Ellipse, Arrow +from schema import Style, Box, Ellipse, Arrow, Drawing, UnknownText + # Map line style to dash patterns dash_pattern = { - 'solid': '', - 'dashed': (4, 4), # 4 pixels drawn, 4 pixels space - 'dotted': (1, 1) # 1 pixel drawn, 1 pixel space + "solid": "", + "dashed": (4, 4), # 4 pixels drawn, 4 pixels space + "dotted": (1, 1), # 1 pixel drawn, 1 pixel space } + def render_drawing(drawing: Drawing): - # Create a new Tkinter window window = tk.Tk() window.title("Drawing") - window.configure(bg='white') # Set the background color of the window + window.configure(bg="white") - # Create a canvas widget - canvas = tk.Canvas(window, width=800, height=600, bg='white', highlightthickness=0) - canvas.pack(padx=10, pady=10) # Adds 10 pixels of padding on all sides + canvas = tk.Canvas(window, width=800, height=600, bg="white", highlightthickness=0) + canvas.pack(padx=10, pady=10) - # Function to draw a box with text if provided def draw_box(box: Box): - x1, y1 = box['x'], box['y'] - x2, y2 = x1 + box['width'], y1 + box['height'] - fill = box['style'].get('fill_color', '') if box['style'] else '' - canvas.create_rectangle(x1, y1, x2, y2, outline='black', fill=fill) - if 'text' in box and box['text']: - canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=box['text'], fill='black') + x1, y1 = box.x, box.y + x2, y2 = x1 + box.width, y1 + box.height + canvas.create_rectangle(x1, y1, x2, y2, outline=getattr(box.style, "line_color", None) or "black", fill=getattr(box.style, "fill_color", None) or "") + if box.text: + canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=box.text, fill="black") - # Function to draw an ellipse with text if provided def draw_ellipse(ellipse: Ellipse): - x1, y1 = ellipse['x'], ellipse['y'] - x2, y2 = x1 + ellipse['width'], y1 + ellipse['height'] - fill = ellipse['style'].get('fill_color', '') if ellipse['style'] else '' - canvas.create_oval(x1, y1, x2, y2, outline='black', fill=fill) - if 'text' in ellipse and ellipse['text']: - canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=ellipse['text'], fill='black') + x1, y1 = ellipse.x, ellipse.y + x2, y2 = x1 + ellipse.width, y1 + ellipse.height + canvas.create_oval(x1, y1, x2, y2, outline=getattr(ellipse.style, "line_color", None) or "black", fill=getattr(ellipse.style, "fill_color", None) or "") + if ellipse.text: + canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=ellipse.text, fill="black") - # Function to draw an arrow def draw_arrow(arrow: Arrow): - x1, y1 = arrow['start_x'], arrow['start_y'] - x2, y2 = arrow['end_x'], arrow['end_y'] - line_style = (arrow['style'].get('line_style', 'solid') # Default line style - if arrow['style'] else 'solid') - - canvas.create_line(x1, y1, x2, y2, dash=dash_pattern[line_style], arrow=tk.LAST, fill='black') + canvas.create_line( + arrow.start_x, + arrow.start_y, + arrow.end_x, + arrow.end_y, + dash=dash_pattern[getattr(arrow.style, "line_style", None) or "solid"], + arrow=tk.LAST, + fill=getattr(arrow.style, "line_color", None) or "black", + ) - # Iterate through each item in the drawing and render it - for item in drawing['items']: - if item['type'] == 'Box': - draw_box(item) - elif item['type'] == 'Ellipse': - draw_ellipse(item) - elif item['type'] == 'Arrow': - draw_arrow(item) + for item in drawing.items: + match item: + case Box(): + draw_box(item) + case Ellipse(): + draw_ellipse(item) + case Arrow(): + draw_arrow(item) + case UnknownText(): + print(f"Unknown text: {item.text}") - # Button to close the window (pretty ugly -- use Cmd-W/Ctrl-W instead) - # quit_button = tk.Button(window, text="Quit", command=window.quit) - # quit_button.pack(side=tk.BOTTOM, pady=10) - - # Start the Tkinter event loop window.mainloop() -# Example usage: -if __name__ == '__main__': - drawing: Drawing = { - 'items': [ - {'type': 'Box', 'x': 50, 'y': 50, 'width': 100, 'height': 100, 'text': 'Hello', 'style': None}, - {'type': 'Ellipse', 'x': 200, 'y': 50, 'width': 150, 'height': 100, 'text': 'World', 'style': {'type': 'Style', 'fill_color': 'lightblue'}}, - {'type': 'Arrow', 'start_x': 50, 'start_y': 200, 'end_x': 150, 'end_y': 200, - 'style': {'type': 'Style', 'line_style': 'dashed'}, 'head_size': 10}, + +if __name__ == "__main__": + example_drawing = Drawing( + type="Drawing", + items=[ + Box(type="Box", x=50, y=50, width=100, height=100, text="Hello", style=Style(type="Style")), + Ellipse( + type="Ellipse", + x=200, + y=50, + width=150, + height=100, + text="World", + style=Style(type="Style", fill_color="lightblue"), + ), + Arrow( + type="Arrow", + start_x=50, + start_y=200, + end_x=150, + end_y=200, + style=Style(type="Style", line_style="dashed"), + ), ], - } + ) - render_drawing(drawing) + render_drawing(example_drawing) diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py index 02d9fa55..ccb10e3c 100644 --- a/python/examples/drawing/schema.py +++ b/python/examples/drawing/schema.py @@ -1,54 +1,77 @@ -from typing_extensions import Literal, TypedDict, Annotated, Doc, NotRequired, Optional +"""Schema for a drawing with boxes, ellipses, arrows, etc.""" +from dataclasses import dataclass +from typing_extensions import Literal, Annotated, Doc, Optional + + +@dataclass +class Style: + """Style settings for drawing elements.""" -class Style(TypedDict): type: Literal["Style"] - corners: NotRequired[Annotated[Literal["rounded", "sharp"], Doc("Corner style of the drawing elements.")]] - line_thickness: NotRequired[Annotated[int, Doc("Thickness of the lines.")]] - line_color: NotRequired[Annotated[str, Doc("CSS-style color code for line color.")]] - fill_color: NotRequired[Annotated[str, Doc("CSS-style color code for fill color.")]] - line_style: NotRequired[Annotated[str, Doc("Style of the line: 'solid', 'dashed', 'dotted'.")]] + corners: Annotated[Optional[Literal["rounded", "sharp"]], Doc("Corner style of the drawing elements.")] = None + line_thickness: Annotated[Optional[int], Doc("Thickness of the lines.")] = None + line_color: Annotated[Optional[str], Doc("CSS-style color code for line color.")] = None + fill_color: Annotated[Optional[str], Doc("CSS-style color code for fill color.")] = None + line_style: Annotated[Optional[str], Doc("Style of the line: 'solid', 'dashed', 'dotted'.")] = None -class Box(TypedDict): + +@dataclass +class Box: """A rectangular box defined by a coordinate system with the origin at the top left.""" + type: Literal["Box"] + x: Annotated[int, Doc("X-coordinate of the top left corner.")] y: Annotated[int, Doc("Y-coordinate of the top left corner.")] width: Annotated[int, Doc("Width of the box.")] height: Annotated[int, Doc("Height of the box.")] - text: Annotated[Optional[str], Doc("Optional text centered in the box.")] - style: Annotated[Optional[Style], Doc("Optional style settings for the box.")] + text: Annotated[Optional[str], Doc("Optional text centered in the box.")] = None + style: Annotated[Optional[Style], Doc("Optional style settings for the box.")] = None -class Ellipse(TypedDict): +@dataclass +class Ellipse: """An ellipse defined by its bounding box dimensions.""" + type: Literal["Ellipse"] + x: Annotated[int, Doc("X-coordinate of the top left corner of the bounding box.")] y: Annotated[int, Doc("Y-coordinate of the top left corner of the bounding box.")] width: Annotated[int, Doc("Width of the bounding box.")] height: Annotated[int, Doc("Height of the bounding box.")] - text: Annotated[Optional[str], Doc("Optional text centered in the box.")] - style: Annotated[Optional[Style], Doc("Optional style settings for the ellipse.")] + text: Annotated[Optional[str], Doc("Optional text centered in the box.")] = None + style: Annotated[Optional[Style], Doc("Optional style settings for the ellipse.")] = None -class Arrow(TypedDict): +@dataclass +class Arrow: """A line with a directional arrow at one or both ends, defined by start and end points.""" + type: Literal["Arrow"] + start_x: Annotated[int, Doc("Starting X-coordinate.")] start_y: Annotated[int, Doc("Starting Y-coordinate.")] end_x: Annotated[int, Doc("Ending X-coordinate.")] end_y: Annotated[int, Doc("Ending Y-coordinate.")] - style: Annotated[Optional[Style], Doc("Optional style settings for the arrow.")] - head_size: Annotated[Optional[int], Doc("Size of the arrowhead, if present.")] + style: Annotated[Optional[Style], Doc("Optional style settings for the arrow.")] = None + head_size: Annotated[Optional[int], Doc("Size of the arrowhead, if present.")] = None -class UnknownText(TypedDict): +@dataclass +class UnknownText: """Used for input that does not match any other specified type.""" - type: Literal["Unknown"] + + type: Literal["UnknownText"] + text: Annotated[str, Doc("The text that wasn't understood.")] -class Drawing(TypedDict): +@dataclass +class Drawing: """A collection of graphical elements including boxes, ellipses, arrows, and unrecognized text.""" + + type: Literal["Drawing"] + items: Annotated[list[Box | Arrow | Ellipse | UnknownText], Doc("List of drawable elements.")] From c6da9147d082a090e4949948ed37addf8cc32c7c Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 15:58:45 -0700 Subject: [PATCH 20/26] Black formatting --- python/examples/drawing/render.py | 28 +++++++++++++++++++++++++--- python/examples/drawing/schema.py | 5 ++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/python/examples/drawing/render.py b/python/examples/drawing/render.py index e7173fa9..50651162 100644 --- a/python/examples/drawing/render.py +++ b/python/examples/drawing/render.py @@ -22,14 +22,28 @@ def render_drawing(drawing: Drawing): def draw_box(box: Box): x1, y1 = box.x, box.y x2, y2 = x1 + box.width, y1 + box.height - canvas.create_rectangle(x1, y1, x2, y2, outline=getattr(box.style, "line_color", None) or "black", fill=getattr(box.style, "fill_color", None) or "") + canvas.create_rectangle( + x1, + y1, + x2, + y2, + outline=getattr(box.style, "line_color", None) or "black", + fill=getattr(box.style, "fill_color", None) or "", + ) if box.text: canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=box.text, fill="black") def draw_ellipse(ellipse: Ellipse): x1, y1 = ellipse.x, ellipse.y x2, y2 = x1 + ellipse.width, y1 + ellipse.height - canvas.create_oval(x1, y1, x2, y2, outline=getattr(ellipse.style, "line_color", None) or "black", fill=getattr(ellipse.style, "fill_color", None) or "") + canvas.create_oval( + x1, + y1, + x2, + y2, + outline=getattr(ellipse.style, "line_color", None) or "black", + fill=getattr(ellipse.style, "fill_color", None) or "", + ) if ellipse.text: canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=ellipse.text, fill="black") @@ -62,7 +76,15 @@ def draw_arrow(arrow: Arrow): example_drawing = Drawing( type="Drawing", items=[ - Box(type="Box", x=50, y=50, width=100, height=100, text="Hello", style=Style(type="Style")), + Box( + type="Box", + x=50, + y=50, + width=100, + height=100, + text="Hello", + style=Style(type="Style"), + ), Ellipse( type="Ellipse", x=200, diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py index ccb10e3c..035415f6 100644 --- a/python/examples/drawing/schema.py +++ b/python/examples/drawing/schema.py @@ -10,7 +10,10 @@ class Style: type: Literal["Style"] - corners: Annotated[Optional[Literal["rounded", "sharp"]], Doc("Corner style of the drawing elements.")] = None + corners: Annotated[ + Optional[Literal["rounded", "sharp"]], + Doc("Corner style of the drawing elements."), + ] = None line_thickness: Annotated[Optional[int], Doc("Thickness of the lines.")] = None line_color: Annotated[Optional[str], Doc("CSS-style color code for line color.")] = None fill_color: Annotated[Optional[str], Doc("CSS-style color code for fill color.")] = None From 390558ef22fba2cbece3358da0ff3f7eabf1ef7a Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 16:06:06 -0700 Subject: [PATCH 21/26] Remove unused imports --- python/examples/drawing/demo.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index f6c9f1af..41c22161 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -1,5 +1,4 @@ import asyncio -import json import sys from dotenv import dotenv_values @@ -12,7 +11,6 @@ Failure, TypeChatJsonTranslator, TypeChatValidator, - PromptSection, create_language_model, process_requests, ) From e42b2dd1f537e246ddb715c4108d6531d27ba8ae Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 16:25:34 -0700 Subject: [PATCH 22/26] Clarify Failure output --- python/examples/drawing/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 41c22161..13218a08 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -29,7 +29,7 @@ async def request_handler(request: str): history.append(request) result: Success[schema.Drawing] | Failure = await translator.translate("\n".join(history)) if isinstance(result, Failure): - print(result.message) + print("Failure:", result.message) else: value: schema.Drawing = result.value print(value) From 11edfb777d3d99e153a50de14f6154ccea134c90 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 16:26:06 -0700 Subject: [PATCH 23/26] Revert verbose logging --- python/src/typechat/_internal/model.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index a3b544a2..da52e306 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -65,13 +65,6 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa if isinstance(prompt, str): prompt = [{"role": "user", "content": prompt}] - print("===============>") - for i, item in enumerate(prompt): - role, content = item["role"], item["content"] - print(f"---------- {i} {role} ----------") - print(content) - print("end ===========>") - body = { **self.default_params, "messages": prompt, @@ -92,11 +85,6 @@ async def complete(self, prompt: str | list[PromptSection]) -> Success[str] | Fa dict[Literal["choices"], list[dict[Literal["message"], PromptSection]]], response.json() ) - - print("<===============") - print(json_result["choices"][0]["message"]["content"] or "") - print("<=============== end") - 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: From 3f099dcad565423fb3ec1eb3bfc0ac4bbdb60532 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 2 May 2024 16:38:32 -0700 Subject: [PATCH 24/26] Reuse drawing window --- python/examples/drawing/demo.py | 11 ++++++++++- python/examples/drawing/render.py | 17 +++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index 13218a08..d93eb619 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -1,5 +1,6 @@ import asyncio import sys +import tkinter as tk from dotenv import dotenv_values @@ -23,6 +24,12 @@ async def main(file_path: str | None): translator = TypeChatJsonTranslator(model, validator, schema.Drawing) # print(translator._schema_str) + window = tk.Tk() + window.title("Click to continue...") + canvas = tk.Canvas(window, width=800, height=600, bg="white", highlightthickness=0) + canvas.pack(padx=10, pady=10) + canvas.bind("", lambda event: window.quit()) + history: list[str] = [] async def request_handler(request: str): @@ -39,7 +46,9 @@ async def request_handler(request: str): if isinstance(item, schema.UnknownText): print(" ", item.text) - render_drawing(value) + canvas.delete("all") + render_drawing(canvas, value) + window.mainloop() await process_requests("~> ", file_path, request_handler) diff --git a/python/examples/drawing/render.py b/python/examples/drawing/render.py index 50651162..41093e04 100644 --- a/python/examples/drawing/render.py +++ b/python/examples/drawing/render.py @@ -11,13 +11,7 @@ } -def render_drawing(drawing: Drawing): - window = tk.Tk() - window.title("Drawing") - window.configure(bg="white") - - canvas = tk.Canvas(window, width=800, height=600, bg="white", highlightthickness=0) - canvas.pack(padx=10, pady=10) +def render_drawing(canvas: tk.Canvas, drawing: Drawing): def draw_box(box: Box): x1, y1 = box.x, box.y @@ -69,8 +63,6 @@ def draw_arrow(arrow: Arrow): case UnknownText(): print(f"Unknown text: {item.text}") - window.mainloop() - if __name__ == "__main__": example_drawing = Drawing( @@ -105,4 +97,9 @@ def draw_arrow(arrow: Arrow): ], ) - render_drawing(example_drawing) + window = tk.Tk() + window.title("Drawing") + canvas = tk.Canvas(window, width=800, height=600, bg="white", highlightthickness=0) + canvas.pack(padx=10, pady=10) + render_drawing(canvas, example_drawing) + window.mainloop() From 5c6e15f43a214876124ee9d6cb3608f28b1cff69 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 8 May 2024 13:42:00 -0700 Subject: [PATCH 25/26] Don't crash if we cannot import readline (e.g. on Windows) --- python/src/typechat/_internal/interactive.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/src/typechat/_internal/interactive.py b/python/src/typechat/_internal/interactive.py index 8ce7ca6d..d6f29595 100644 --- a/python/src/typechat/_internal/interactive.py +++ b/python/src/typechat/_internal/interactive.py @@ -20,8 +20,11 @@ async def process_requests(interactive_prompt: str, input_file_name: str | None, print(interactive_prompt + line) await process_request(line) else: - # Use readline to enable input editing and history - import readline # type: ignore + try: + # Use readline to enable input editing and history + import readline # type: ignore + except ImportError: + pass while True: try: line = input(interactive_prompt) From 0894ef6c4b18488130ba233c7794d6abc6890e56 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 8 May 2024 13:43:01 -0700 Subject: [PATCH 26/26] Add crude progress messages --- python/examples/drawing/demo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/examples/drawing/demo.py b/python/examples/drawing/demo.py index d93eb619..0f499084 100644 --- a/python/examples/drawing/demo.py +++ b/python/examples/drawing/demo.py @@ -33,6 +33,7 @@ async def main(file_path: str | None): history: list[str] = [] async def request_handler(request: str): + print("[Sending request...]") history.append(request) result: Success[schema.Drawing] | Failure = await translator.translate("\n".join(history)) if isinstance(result, Failure): @@ -48,6 +49,7 @@ async def request_handler(request: str): canvas.delete("all") render_drawing(canvas, value) + print("Click in drawing to continue...") window.mainloop() await process_requests("~> ", file_path, request_handler)