アクションを使うと、ユーザーがメッセージを送信しなくても、ChatKit SDK のフロントエンドからストリーミング応答を開始できます。アクションをきっかけに、ChatKit SDK の外部で状態の変更などの処理を実行することもできます。
アクションの起動
ウィジェットの操作をきっかけとした起動
ActionConfig を設定できるウィジェットノードであれば、どのノードからでもアクションを起動できます。たとえば、ボタンにアクションを設定すると、クリックイベントに応答できます。ユーザーがそのボタンをクリックすると、アクションがサーバーに送られます。サーバーでは、ウィジェットの更新や推論の実行、新しいスレッド項目のストリーミングなどができます。
button = Button(
label="Example",
onClickAction=ActionConfig(
type="example",
payload={"id": 123},
),
)フロントエンドから sendAction() を呼び出して、アクションを直接送信することもできます。特に便利なのは、ChatKit の外部で行われた操作に ChatKit 側で応答したい場合です。後述するように、クライアントとサーバーの両方で応答する必要がある場合にも、このメソッドを使って複数のアクションを連鎖させることができます。
await chatKit.sendAction({
type: "example",
payload: { id: 123 },
});アクションの処理
サーバー側での処理
デフォルトでは、アクションはサーバーに送信されます。サーバー側でアクションを処理するには、action メソッドを ChatKitServer に実装します。
class MyChatKitServer(ChatKitServer[RequestContext]):
async def action(
self,
thread: ThreadMetadata,
action: Action[str, Any],
sender: WidgetItem | None,
context: RequestContext,
) -> AsyncIterator[Event]:
if action.type == "example":
await do_thing(action.payload["id"])
# Often you'll want to add a HiddenContextItem so the model
# can see that the user did something.
await self.store.add_thread_item(
thread.id,
HiddenContextItem(
id=self.store.generate_item_id("message", thread, context),
thread_id=thread.id,
created_at=datetime.now(),
content="<USER_ACTION>The user did a thing</USER_ACTION>",
),
context,
)
# Then you might want to run inference to stream a response
# back to the user.
async for event in self.generate(context, thread):
yield eventアクションとそのペイロードはクライアントからサーバーに送られてくるため、信頼できないデータとして扱ってください。
クライアント側での処理
アクションをクライアント側で処理したい場合は、handler="client" を ActionConfig に追加して、アクションの送信先をクライアント側のアクションハンドラーに指定します。
button = Button(
label="Example",
onClickAction=ActionConfig(type="example", payload={"id": 123}, handler="client"),
)この設定でアクションを起動すると、ChatKit のインスタンス作成時に指定したコールバックにアクションが渡されます。
async function handleWidgetAction(action) {
if (action.type === "example") {
const res = await doSomething(action);
// You can fire off actions to your server from here as well.
// For example, stream new thread items or update a widget.
await chatKit.sendAction({
type: "example_complete",
payload: res,
});
}
}
chatKit.setOptions({
// Other options...
widgets: { onAction: handleWidgetAction },
});厳密に型付けされたアクション
デフォルトでは、Action と ActionConfig の型付けは厳密ではありません。厳密に型付けした一連のアクションを使う場合は、create ヘルパーが利用できます。このヘルパーは Action に用意されており、それらのアクションから ActionConfig を生成します。
class ExamplePayload(BaseModel):
id: int
ExampleAction = Action[Literal["example"], ExamplePayload]
OtherAction = Action[Literal["other"], None]
AppAction = Annotated[
ExampleAction | OtherAction,
Field(discriminator="type"),
]
ActionAdapter: TypeAdapter[AppAction] = TypeAdapter(AppAction)
def parse_app_action(action: Action[str, Any]) -> AppAction:
return ActionAdapter.validate_python(action)
# Usage in a widget
# Action provides a create helper which makes it easy to generate
# ActionConfigs from strongly typed actions.
button = Button(
label="Example",
onClickAction=ExampleAction.create(ExamplePayload(id=123)),
)
# usage in action handler
class MyChatKitServer(ChatKitServer[RequestContext]):
async def action(
self,
thread: ThreadMetadata,
action: Action[str, Any],
sender: WidgetItem | None,
context: RequestContext,
) -> AsyncIterator[Event]:
# add custom error handling if needed
app_action = parse_app_action(action)
if app_action.type == "example":
await do_thing(app_action.payload.id)
yield ThreadItemDoneEvent(
item=AssistantMessageItem(
id=self.store.generate_item_id("message", thread, context),
thread_id=thread.id,
created_at=datetime.now(),
content=[AssistantMessageContent(text="Action complete.")],
)
)ウィジェットとアクションを使ったカスタムフォームの作成
ユーザー入力を受け付けるウィジェットノードを Form 内にマウントすると、各フィールドの値がアクションの payload に含まれます。これは、その Form 内で起動するすべてのアクションに適用されます。
フォームの値は payload に格納され、各フィールドの name がキーになります。たとえば、次のように値を参照できます。
Select(name="title")→action.payload.titleSelect(name="todo.title")→action.payload.todo.title
form = Form(
direction="col",
onSubmitAction=ActionConfig(
type="update_todo",
payload={"id": todo.id},
),
children=[
Title(value="Edit Todo"),
Text(value="Title", color="secondary", size="sm"),
Text(
value=todo.title,
editable=EditableProps(name="title", required=True),
),
Text(value="Description", color="secondary", size="sm"),
Text(
value=todo.description,
editable=EditableProps(name="description"),
),
Button(label="Save", submit=True),
],
)
class MyChatKitServer(ChatKitServer[RequestContext]):
async def action(
self,
thread: ThreadMetadata,
action: Action[str, Any],
sender: WidgetItem | None,
context: RequestContext,
) -> AsyncIterator[Event]:
if action.type == "update_todo":
todo_id = action.payload["id"]
# Any action that originates from within the Form will
# include title and description.
title = action.payload["title"]
description = action.payload["description"]
await update_todo(todo_id, title, description)
yield ThreadItemDoneEvent(
item=AssistantMessageItem(
id=self.store.generate_item_id("message", thread, context),
thread_id=thread.id,
created_at=datetime.now(),
content=[AssistantMessageContent(text="Todo updated.")],
)
)入力検証
Form は、ブラウザに組み込まれた基本的なフォーム検証機能を使います。各フィールドに設定された required や pattern の制約を適用し、無効なフィールドが 1 つでもあれば送信をブロックします。
今後、より使いやすく、柔軟な検証ルールやエラー表示のカスタマイズにも対応した検証モードを追加する可能性があります。現時点では、複雑な入力検証が必要なフォームにウィジェットはあまり適していません。そのような場合は、クライアント側のアクションハンドラーでモーダルを開き、そこにカスタムフォームを表示してから、入力結果を sendAction で ChatKit に渡す方法が適しています。
Card の Form としての利用
asForm=True を Card に渡すと、カードは Form として動作します。入力を検証し、各フィールドから取得した値をカードの confirm アクションに渡します。
ペイロードのキーの衝突
フィールド名がペイロードにあらかじめ定義されている別のキーと重複すると、そのフィールドの値は無視されます。こうした重複はバグの可能性が高いため、検出時に error イベントが発行されます。
ウィジェットの読み込み状態の制御
ActionConfig.loadingBehavior を使うと、アクションの起動時にウィジェットのどの部分を読み込み状態にするかを指定できます。
button = Button(
label="This may take a while...",
onClickAction=ActionConfig(
type="long_running_action_that_should_block_other_ui_interactions",
loadingBehavior="container",
),
)| 値 | 動作 |
|---|---|
auto |
アクションを設定した場所に応じて動作が決まります。(デフォルト) |
self |
アクションを設定したウィジェットノードが読み込み状態になります。 |
container |
ウィジェットのコンテナ全体が読み込み状態になります。ウィジェットの表示がやや薄くなり、操作できなくなります。 |
none |
読み込み状態には移行しません。 |
auto の動作
通常は、デフォルトの auto を推奨します。auto では、アクションを設定した場所に応じて読み込み状態が決まります。たとえば、次のように動作します。
Button.onClickAction→selfSelect.onChangeAction→noneCard.confirm.action→container