Skip to content

Commit 524469f

Browse files
authored
Merge pull request #68 from openai/widget-template-also-handles-basic-root
Update WidgetTemplate to handle widgets with BasicRoot
2 parents 7124c4c + e03c83d commit 524469f

File tree

4 files changed

+102
-15
lines changed

4 files changed

+102
-15
lines changed

chatkit/widgets.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,13 @@
44
import json
55
from datetime import datetime
66
from pathlib import Path
7-
from typing import (
8-
Annotated,
9-
Any,
10-
Literal,
11-
)
7+
from typing import Annotated, Any, Literal
128

139
from jinja2 import Environment, StrictUndefined, Template
1410
from pydantic import (
1511
BaseModel,
1612
ConfigDict,
1713
Field,
18-
TypeAdapter,
1914
model_serializer,
2015
)
2116
from typing_extensions import NotRequired, TypedDict, deprecated
@@ -1147,8 +1142,6 @@ class WidgetTemplate:
11471142
```
11481143
"""
11491144

1150-
adapter: TypeAdapter[DynamicWidgetRoot] = TypeAdapter(DynamicWidgetRoot)
1151-
11521145
def __init__(self, definition: dict[str, Any]):
11531146
self.version = definition["version"]
11541147
if self.version != "1.0":
@@ -1163,7 +1156,7 @@ def __init__(self, definition: dict[str, Any]):
11631156
self.data_schema = definition.get("jsonSchema", {})
11641157

11651158
@classmethod
1166-
def from_file(cls, file_path: str) -> "WidgetTemplate":
1159+
def from_file(cls, file_path: str) -> WidgetTemplate:
11671160
path = Path(file_path)
11681161
if not path.is_absolute():
11691162
caller_frame = inspect.stack()[1]
@@ -1178,10 +1171,19 @@ def from_file(cls, file_path: str) -> "WidgetTemplate":
11781171
def build(
11791172
self, data: dict[str, Any] | BaseModel | None = None
11801173
) -> DynamicWidgetRoot:
1181-
if data is None:
1182-
data = {}
1183-
if isinstance(data, BaseModel):
1184-
data = data.model_dump()
1185-
rendered = self.template.render(**data)
1174+
rendered = self.template.render(**self._normalize_data(data))
11861175
widget_dict = json.loads(rendered)
1187-
return self.adapter.validate_python(widget_dict)
1176+
return DynamicWidgetRoot.model_validate(widget_dict)
1177+
1178+
def build_basic(self, data: dict[str, Any] | BaseModel | None = None) -> BasicRoot:
1179+
"""Separate method for building basic root widgets until BasicRoot is supported for streamed widgets."""
1180+
rendered = self.template.render(**self._normalize_data(data))
1181+
widget_dict = json.loads(rendered)
1182+
return BasicRoot.model_validate(widget_dict)
1183+
1184+
def _normalize_data(
1185+
self, data: dict[str, Any] | BaseModel | None
1186+
) -> dict[str, Any]:
1187+
if data is None:
1188+
return {}
1189+
return data.model_dump() if isinstance(data, BaseModel) else data
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"type": "Basic",
3+
"children": [
4+
{
5+
"type": "Col",
6+
"gap": 1,
7+
"children": [
8+
{
9+
"type": "Title",
10+
"value": "Harry Potter",
11+
"size": "sm"
12+
},
13+
{
14+
"type": "Text",
15+
"value": "The boy who lived",
16+
"size": "sm"
17+
}
18+
]
19+
}
20+
]
21+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"version": "1.0",
3+
"name": "Author preview",
4+
"template": "{\"type\":\"Basic\",\"children\":[{\"type\":\"Col\",\"gap\":1,\"children\":[{\"type\":\"Title\",\"value\":{{ (name) | tojson }},\"size\":\"sm\"},{\"type\":\"Text\",\"value\":{{ (bio) | tojson }},\"size\":\"sm\"}]}]}",
5+
"jsonSchema": {
6+
"$schema": "https://json-schema.org/draft/2020-12/schema",
7+
"type": "object",
8+
"properties": {
9+
"name": {
10+
"type": "string",
11+
"minLength": 1
12+
},
13+
"bio": {
14+
"type": "string",
15+
"minLength": 1
16+
}
17+
},
18+
"required": [
19+
"name",
20+
"bio"
21+
],
22+
"additionalProperties": false
23+
},
24+
"outputJsonPreview": {
25+
"type": "Basic",
26+
"children": [
27+
{
28+
"type": "Col",
29+
"gap": 1,
30+
"children": [
31+
{
32+
"type": "Title",
33+
"value": "Harry Potter",
34+
"size": "sm"
35+
},
36+
{
37+
"type": "Text",
38+
"value": "The boy who lived",
39+
"size": "sm"
40+
}
41+
]
42+
}
43+
]
44+
},
45+
"encodedWidget": "eyJpZCI6IndpZ194djV6dGlxayIsIm5hbWUiOiJBdXRob3IgcHJldmlldyIsInZpZXciOiI8QmFzaWM-XG4gIDxDb2wgZ2FwPXsxfT5cbiAgICA8VGl0bGUgdmFsdWU9e25hbWV9IHNpemU9XCJzbVwiIC8-XG4gICAgPFRleHQgdmFsdWU9e2Jpb30gc2l6ZT1cInNtXCIgLz5cbiAgPC9Db2w-XG48L0Jhc2ljPiIsImRlZmF1bHRTdGF0ZSI6eyJuYW1lIjoiSGFycnkgUG90dGVyIiwiYmlvIjoiVGhlIGJveSB3aG8gbGl2ZWQifSwic2NoZW1hTW9kZSI6InpvZCIsImpzb25TY2hlbWEiOnsidHlwZSI6Im9iamVjdCIsInByb3BlcnRpZXMiOnsidGl0bGUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsidGl0bGUiXSwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlfSwic2NoZW1hIjoiaW1wb3J0IHsgeiB9IGZyb20gXCJ6b2RcIlxuXG5jb25zdCBQcm9maWxlID0gei5vYmplY3Qoe1xuICBuYW1lOiB6LnN0cmluZygpLnRyaW0oKS5taW4oMSksXG4gIGJpbzogei5zdHJpbmcoKS50cmltKCkubWluKDEpLFxufSk7XG5cbmV4cG9ydCBkZWZhdWx0IFByb2ZpbGU7Iiwic3RhdGVzIjpbXSwic2NoZW1hVmFsaWRpdHkiOiJ2YWxpZCIsInZpZXdWYWxpZGl0eSI6InZhbGlkIiwiZGVmYXVsdFN0YXRlVmFsaWRpdHkiOiJ2YWxpZCJ9"
46+
}

tests/test_widgets.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from chatkit.server import diff_widget
88
from chatkit.types import WidgetItem
99
from chatkit.widgets import (
10+
BasicRoot,
1011
Card,
1112
DynamicWidgetComponent,
1213
DynamicWidgetRoot,
@@ -241,3 +242,20 @@ def test_widget_template_from_file(
241242

242243
assert isinstance(widget, DynamicWidgetRoot)
243244
assert widget.model_dump(exclude_none=True) == expected_widget_dict
245+
246+
247+
def test_widget_template_with_basic_root():
248+
template = WidgetTemplate.from_file("assets/widgets/basic_root.widget")
249+
250+
with open("tests/assets/widgets/basic_root.json", "r") as file:
251+
expected_widget_dict = json.load(file)
252+
253+
widget = template.build_basic(
254+
{
255+
"name": "Harry Potter",
256+
"bio": "The boy who lived",
257+
},
258+
)
259+
260+
assert isinstance(widget, BasicRoot)
261+
assert widget.model_dump(exclude_none=True) == expected_widget_dict

0 commit comments

Comments
 (0)