Skip to content

Commit 31f0778

Browse files
authored
Merge pull request #62 from openai/widget-template
Add WidgetTemplate with dynamic widget types; add deprecation warning to named widget & action classes
2 parents b925d14 + d1110ab commit 31f0778

14 files changed

+940
-37
lines changed

chatkit/actions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, Generic, Literal, TypeVar, get_args, get_origin
44

55
from pydantic import BaseModel, Field
6+
from typing_extensions import deprecated
67

78
Handler = Literal["client", "server"]
89
LoadingBehavior = Literal["auto", "none", "self", "container"]
@@ -11,6 +12,14 @@
1112
DEFAULT_LOADING_BEHAVIOR: LoadingBehavior = "auto"
1213

1314

15+
_direct_usage_of_action_classes_deprecated = deprecated(
16+
"Direct usage of named action classes is deprecated. "
17+
"Use WidgetTemplate to build widgets from .widget files instead. "
18+
"Visit https://widgets.chatkit.studio/ to author widget files."
19+
)
20+
21+
22+
@_direct_usage_of_action_classes_deprecated
1423
class ActionConfig(BaseModel):
1524
type: str
1625
payload: Any = None
@@ -22,6 +31,7 @@ class ActionConfig(BaseModel):
2231
TPayload = TypeVar("TPayload")
2332

2433

34+
@_direct_usage_of_action_classes_deprecated
2535
class Action(BaseModel, Generic[TType, TPayload]):
2636
type: TType = Field(default=TType, frozen=True) # pyright: ignore
2737
payload: TPayload = None # pyright: ignore - default to None to allow no-payload actions

chatkit/server.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
is_streaming_req,
7070
)
7171
from .version import __version__
72-
from .widgets import Markdown, Text, WidgetComponent, WidgetComponentBase, WidgetRoot
72+
from .widgets import WidgetComponent, WidgetComponentBase, WidgetRoot
7373

7474
DEFAULT_PAGE_SIZE = 20
7575
DEFAULT_ERROR_MESSAGE = "An error occurred when generating a response."
@@ -82,6 +82,11 @@ def diff_widget(
8282
Compare two WidgetRoots and return a list of deltas.
8383
"""
8484

85+
def is_streaming_text(component: WidgetComponentBase) -> bool:
86+
return getattr(component, "type", None) in {"Markdown", "Text"} and isinstance(
87+
getattr(component, "value", None), str
88+
)
89+
8590
def full_replace(before: WidgetComponentBase, after: WidgetComponentBase) -> bool:
8691
if (
8792
before.type != after.type
@@ -108,10 +113,10 @@ def full_replace_value(before_value: Any, after_value: Any) -> bool:
108113

109114
for field in before.model_fields_set.union(after.model_fields_set):
110115
if (
111-
isinstance(before, (Markdown, Text))
112-
and isinstance(after, (Markdown, Text))
116+
is_streaming_text(before)
117+
and is_streaming_text(after)
113118
and field == "value"
114-
and after.value.startswith(before.value)
119+
and getattr(after, "value", "").startswith(getattr(before, "value", ""))
115120
):
116121
# Appends to the value prop of Markdown or Text do not trigger a full replace
117122
continue
@@ -129,11 +134,11 @@ def full_replace_value(before_value: Any, after_value: Any) -> bool:
129134

130135
def find_all_streaming_text_components(
131136
component: WidgetComponent | WidgetRoot,
132-
) -> dict[str, Markdown | Text]:
137+
) -> dict[str, WidgetComponentBase]:
133138
components = {}
134139

135140
def recurse(component: WidgetComponent | WidgetRoot):
136-
if isinstance(component, (Markdown, Text)) and component.id:
141+
if is_streaming_text(component) and component.id:
137142
components[component.id] = component
138143

139144
if hasattr(component, "children"):
@@ -154,16 +159,19 @@ def recurse(component: WidgetComponent | WidgetRoot):
154159
f"Node {id} was not present when the widget was initially rendered. All nodes with ID must persist across all widget updates."
155160
)
156161

157-
if before_node.value != after_node.value:
158-
if not after_node.value.startswith(before_node.value):
162+
before_value = str(getattr(before_node, "value", None))
163+
after_value = str(getattr(after_node, "value", None))
164+
165+
if before_value != after_value:
166+
if not after_value.startswith(before_value):
159167
raise ValueError(
160168
f"Node {id} was updated with a new value that is not a prefix of the initial value. All widget updates must be cumulative."
161169
)
162-
done = not after_node.streaming
170+
done = not getattr(after_node, "streaming", False)
163171
deltas.append(
164172
WidgetStreamingTextValueDelta(
165173
component_id=id,
166-
delta=after_node.value[len(before_node.value) :],
174+
delta=after_value[len(before_value) :],
167175
done=done,
168176
)
169177
)

0 commit comments

Comments
 (0)