Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New (Python) example: simple drawings (rectangle, ellipse, arrow) #238

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bb9e32f
Install python-dotenv and tabulate
gvanrossum Apr 17, 2024
6737581
Small working demo that can draw boxes on a canvas
gvanrossum Apr 17, 2024
de03aa9
Add chat history to drawing demo
gvanrossum Apr 17, 2024
2edaedd
[WIP] Hack on translation a bit
gvanrossum Apr 17, 2024
09ea859
New API for chat history
gvanrossum Apr 18, 2024
c52aa53
Improved the schema with ChatGPT's help. Added Arrow and Ellipse.
gvanrossum Apr 19, 2024
5d07a2a
rendering program using Tkinter
gvanrossum Apr 19, 2024
f46a836
Add a simple rendering function (most of it written by ChatGPT)
gvanrossum Apr 19, 2024
131ad4f
Added dashed and dotted arrows (some help from ChatGPT)
gvanrossum Apr 19, 2024
3f44129
Merge commit '66fd7bb' into drawing
gvanrossum Apr 20, 2024
b90faa7
Rip oput history, for now
gvanrossum Apr 22, 2024
02e28e4
Make pyright happy
gvanrossum Apr 22, 2024
3e1f3e6
Merge branch 'main' into drawing
gvanrossum Apr 22, 2024
e9ac5cf
Keep pydantic also happy
gvanrossum Apr 22, 2024
9e14f42
Add (flawed) history based on prompt_preamble
gvanrossum Apr 22, 2024
75c2efe
Prune history
gvanrossum Apr 22, 2024
970d50b
Add temporary logging
gvanrossum Apr 22, 2024
7560273
Alternate prompting scheme from #241
gvanrossum Apr 22, 2024
439f521
Revert changes to translator.py
gvanrossum May 2, 2024
b0fdd8d
Merge branch 'main' into drawing
gvanrossum May 2, 2024
d385e40
Use Anders' suggestion for history
gvanrossum May 2, 2024
42ce16a
Switch schema to dataclasses
gvanrossum May 2, 2024
c6da914
Black formatting
gvanrossum May 2, 2024
390558e
Remove unused imports
gvanrossum May 2, 2024
e42b2dd
Clarify Failure output
gvanrossum May 2, 2024
11edfb7
Revert verbose logging
gvanrossum May 2, 2024
3f099dc
Reuse drawing window
gvanrossum May 2, 2024
5c6e15f
Don't crash if we cannot import readline (e.g. on Windows)
gvanrossum May 8, 2024
0894ef6
Add crude progress messages
gvanrossum May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/examples/drawing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Let's draw some diagrams
60 changes: 60 additions & 0 deletions python/examples/drawing/demo.py
Original file line number Diff line number Diff line change
@@ -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("<Button-1>", 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))
5 changes: 5 additions & 0 deletions python/examples/drawing/input.txt
Original file line number Diff line number Diff line change
@@ -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
105 changes: 105 additions & 0 deletions python/examples/drawing/render.py
Original file line number Diff line number Diff line change
@@ -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()
80 changes: 80 additions & 0 deletions python/examples/drawing/schema.py
Original file line number Diff line number Diff line change
@@ -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.")]
4 changes: 3 additions & 1 deletion python/notebooks/coffeeShop.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand Down
7 changes: 5 additions & 2 deletions python/src/typechat/_internal/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading