diff --git a/python/examples/drawing/__init__.py b/python/examples/drawing/__init__.py new file mode 100644 index 00000000..b9584561 --- /dev/null +++ b/python/examples/drawing/__init__.py @@ -0,0 +1 @@ +# 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..0f499084 --- /dev/null +++ b/python/examples/drawing/demo.py @@ -0,0 +1,60 @@ +import asyncio +import sys +import tkinter as tk + +from dotenv import dotenv_values + +import schema +from render import render_drawing + +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(schema.Drawing) + 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): + print("[Sending request...]") + history.append(request) + result: Success[schema.Drawing] | Failure = await translator.translate("\n".join(history)) + if isinstance(result, Failure): + print("Failure:", result.message) + else: + 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) + + canvas.delete("all") + render_drawing(canvas, value) + print("Click in drawing to continue...") + window.mainloop() + + 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/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 new file mode 100644 index 00000000..41093e04 --- /dev/null +++ b/python/examples/drawing/render.py @@ -0,0 +1,105 @@ +import tkinter as tk + +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 +} + + +def render_drawing(canvas: tk.Canvas, 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 "", + ) + 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 "", + ) + if ellipse.text: + canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=ellipse.text, fill="black") + + def draw_arrow(arrow: Arrow): + 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", + ) + + 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}") + + +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"), + ), + ], + ) + + 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() diff --git a/python/examples/drawing/schema.py b/python/examples/drawing/schema.py new file mode 100644 index 00000000..035415f6 --- /dev/null +++ b/python/examples/drawing/schema.py @@ -0,0 +1,80 @@ +"""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.""" + + type: Literal["Style"] + + 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 + + +@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.")] = None + style: Annotated[Optional[Style], Doc("Optional style settings for the box.")] = None + + +@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.")] = None + style: Annotated[Optional[Style], Doc("Optional style settings for the ellipse.")] = None + + +@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.")] = None + head_size: Annotated[Optional[int], Doc("Size of the arrowhead, if present.")] = None + + +@dataclass +class UnknownText: + """Used for input that does not match any other specified type.""" + + type: Literal["UnknownText"] + + text: Annotated[str, Doc("The text that wasn't understood.")] + + +@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.")] 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" ] }, { 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)