SPB Git

Repository Public

chatkit-actions.md 9.6 KB 269 lines markdown

1# Actions in ChatKit23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45Actions are a way for the ChatKit SDK frontend to trigger a streaming response without the user submitting a message. They can also be used to trigger side-effects outside ChatKit SDK.67## Triggering actions89### In response to user interaction with widgets1011Actions can be triggered by attaching an `ActionConfig` to any widget node that supports it. For example, you can respond to click events on Buttons. When a user clicks on this button, the action will be sent to your server where you can update the widget, run inference, stream new thread items, etc.1213```python14button = Button(15    label="Example",16    onClickAction=ActionConfig(17        type="example",18        payload={"id": 123},19    ),20)21```222324Actions can also be sent imperatively by your frontend with `sendAction()`. This is probably most useful when you need ChatKit to respond to interaction happening outside ChatKit, but it can also be used to chain actions when you need to respond on both the client and the server (more on that below).2526```javascript27await chatKit.sendAction({28  type: "example",29  payload: { id: 123 },30});31```323334## Handling actions3536### On the server3738By default, actions are sent to your server. You can handle actions on your server by implementing the `action` method on `ChatKitServer`.3940```python41class MyChatKitServer(ChatKitServer[RequestContext]):42    async def action(43        self,44        thread: ThreadMetadata,45        action: Action[str, Any],46        sender: WidgetItem | None,47        context: RequestContext,48    ) -> AsyncIterator[Event]:49        if action.type == "example":50            await do_thing(action.payload["id"])5152            # Often you'll want to add a HiddenContextItem so the model53            # can see that the user did something.54            await self.store.add_thread_item(55                thread.id,56                HiddenContextItem(57                    id="item_123",58                    thread_id=thread.id,59                    created_at=datetime.now(),60                    content="<USER_ACTION>The user did a thing</USER_ACTION>",61                ),62                context,63            )6465            # Then you might want to run inference to stream a response66            # back to the user.67            async for event in self.generate(context, thread):68                yield event69```707172Treat actions and their payloads as untrusted data because the client sends them to your server.7374### Client7576Sometimes you’ll want to handle actions in your client integration. To do that you need to specify that the action should be sent to your client-side action handler by adding `handler="client"` to the `ActionConfig`.7778```python79button = Button(80    label="Example",81    onClickAction=ActionConfig(type="example", payload={"id": 123}, handler="client"),82)83```848586Then, when the action is triggered, it will then be passed to a callback that you provide when instantiating ChatKit.8788```javascript89async function handleWidgetAction(action) {90  if (action.type === "example") {91    const res = await doSomething(action);9293    // You can fire off actions to your server from here as well.94    // For example, stream new thread items or update a widget.95    await chatKit.sendAction({96      type: "example_complete",97      payload: res,98    });99  }100}101102chatKit.setOptions({103  // Other options...104  widgets: { onAction: handleWidgetAction },105});106```107108109## Strongly typed actions110111By default `Action` and `ActionConfig` are not strongly typed. However, we do expose a `create` helper on `Action` that generates `ActionConfig`s from a set of strongly-typed actions.112113```python114class ExamplePayload(BaseModel):115    id: int116117118ExampleAction = Action[Literal["example"], ExamplePayload]119OtherAction = Action[Literal["other"], None]120121AppAction = Annotated[122    ExampleAction | OtherAction,123    Field(discriminator="type"),124]125126ActionAdapter: TypeAdapter[AppAction] = TypeAdapter(AppAction)127128129def parse_app_action(action: Action[str, Any]) -> AppAction:130    return ActionAdapter.validate_python(action)131132133# Usage in a widget134# Action provides a create helper which makes it easy to generate135# ActionConfigs from strongly typed actions.136button = Button(137    label="Example",138    onClickAction=ExampleAction.create(ExamplePayload(id=123)),139)140141142# usage in action handler143class MyChatKitServer(ChatKitServer[RequestContext]):144    async def action(145        self,146        thread: ThreadMetadata,147        action: Action[str, Any],148        sender: WidgetItem | None,149        context: RequestContext,150    ) -> AsyncIterator[Event]:151        # add custom error handling if needed152        app_action = parse_app_action(action)153        if app_action.type == "example":154            await do_thing(app_action.payload.id)155            yield ThreadItemDoneEvent(156                item=AssistantMessageItem(157                    id=self.store.generate_item_id("message", thread, context),158                    thread_id=thread.id,159                    created_at=datetime.now(),160                    content=[AssistantMessageContent(text="Action complete.")],161                )162            )163```164165166## Use widgets and actions to create custom forms167168When widget nodes that take user input are mounted inside a `Form`, the values from those fields will be included in the `payload` of all actions that originate from within the `Form`.169170Form values are keyed in the `payload` by their `name` e.g.171172- `Select(name="title")` → `action.payload.title`173- `Select(name="todo.title")` → `action.payload.todo.title`174175```python176form = Form(177    direction="col",178    validation="native",179    onSubmitAction=ActionConfig(180        type="update_todo",181        payload={"id": todo.id},182    ),183    children=[184        Title(value="Edit Todo"),185        Text(value="Title", color="secondary", size="sm"),186        Text(187            value=todo.title,188            editable=EditableProps(name="title", required=True),189        ),190        Text(value="Description", color="secondary", size="sm"),191        Text(192            value=todo.description,193            editable=EditableProps(name="description"),194        ),195        Button(label="Save", submit=True),196    ],197)198199200class MyChatKitServer(ChatKitServer[RequestContext]):201    async def action(202        self,203        thread: ThreadMetadata,204        action: Action[str, Any],205        sender: WidgetItem | None,206        context: RequestContext,207    ) -> AsyncIterator[Event]:208        if action.type == "update_todo":209            todo_id = action.payload["id"]210            # Any action that originates from within the Form will211            # include title and description.212            title = action.payload["title"]213            description = action.payload["description"]214215            await update_todo(todo_id, title, description)216            yield ThreadItemDoneEvent(217                item=AssistantMessageItem(218                    id=self.store.generate_item_id("message", thread, context),219                    thread_id=thread.id,220                    created_at=datetime.now(),221                    content=[AssistantMessageContent(text="Todo updated.")],222                )223            )224```225226227### Validation228229`Form` uses basic native form validation; enforcing `required` and `pattern` on fields where they are configured and blocking submission when the form has any invalid field.230231We may add new validation modes with better UX, more expressive validation, custom error display, etc in the future. Until then, widgets are not a great medium for complex forms with tricky validation. If you have this need, a better pattern would be to use client side action handling to trigger a modal, show a custom form there, then pass the result back into ChatKit with `sendAction`.232233### Treating `Card` as a `Form`234235You can pass `asForm=True` to `Card` and it will behave as a `Form`, running validation and passing collected fields to the Card’s `confirm` action.236237### Payload key collisions238239If there is a naming collision with some other existing pre-defined key on your payload, the form value will be ignored. This is probably a bug, so we’ll emit an `error` event when we see this.240241## Control loading state interactions in widgets242243Use `ActionConfig.loadingBehavior` to control how actions trigger different loading states in a widget.244245```python246button = Button(247    label="This may take a while...",248    onClickAction=ActionConfig(249        type="long_running_action_that_should_block_other_ui_interactions",250        loadingBehavior="container",251    ),252)253```254255256| Value       | Behavior                                                                                                                        |257| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |258| `auto`      | The action will adapt to how it’s being used. (_default_)                                                                       |259| `self`      | The action triggers loading state on the widget node that the action was bound to.                                              |260| `container` | The action triggers loading state on the entire widget container. This causes the widget to fade out slightly and become inert. |261| `none`      | No loading state                                                                                                                |262263### Using `auto` behavior264265Generally, we recommend using `auto`, which is the default. `auto` triggers loading states based on where the action is bound, for example:266267- `Button.onClickAction` → `self`268- `Select.onChangeAction` → `none`269- `Card.confirm.action` → `container`
Files main
Menu

Theme