操作是 ChatKit SDK 前端触发流式响应(无需用户提交消息)的一种方式。它们还可用于触发 ChatKit SDK 之外的副作用。
触发操作
响应用户与组件(widgets)的交互
可以通过将 ActionConfig 附加到任何支持它的组件节点来触发操作。例如,您可以响应按钮的点击事件。当用户点击该按钮时,操作将发送到您的服务器,您可以在服务器上更新组件、运行推理、流式传输新的会话项等。
1
2
3
4
5
6
7
Button(
label="Example",
onClickAction=ActionConfig(
type="example",
payload={"id": 123},
)
)操作也可以由您的前端通过 sendAction() 以命令式方式发送。当您需要 ChatKit 响应 ChatKit 之外发生的交互时,这非常有用;它也可用于在需要同时响应客户端和服务器(下文将详细介绍)时链接多个操作。
1
2
3
4
await chatKit.sendAction({
type: "example",
payload: { id: 123 },
});处理操作
在服务器端
默认情况下,操作会发送到您的服务器。您可以通过在 ChatKitServer 上实现 action 方法来处理服务器端的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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="item_123",
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 e in self.generate(context, thread):
yield e注意:与任何客户端/服务器交互一样,操作及其有效负载(payload)均由客户端发送,应被视为不可信数据。
在客户端
有时您希望在客户端集成中处理操作。为此,您需要通过在 ActionConfig 中添加 handler="client" 来指定该操作应发送到您的客户端操作处理程序。
1
2
3
4
5
6
7
8
Button(
label="Example",
onClickAction=ActionConfig(
type="example",
payload={"id": 123},
handler="client"
)
)然后,当触发该操作时,它将被传递给您在实例化 ChatKit 时提供的回调函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function handleWidgetAction(action: {type: string, Record<string, unknown>}) {
if (action.type === "example") {
const res = await doSomething(action)
// You can fire off actions to your server from here as well.
// e.g. if you want to 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 不是强类型的。不过,我们确实在 Action 上提供了一个 create 辅助方法,可以轻松地从一组强类型操作中生成 ActionConfig。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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.model_validate(action)
# Usage in a widget
# Action provides a create helper which makes it easy to generate
# ActionConfigs from strongly typed actions.
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)使用组件和操作创建自定义表单
当接收用户输入的组件节点被挂载在 Form 内时,来自这些字段的值将被包含在源自该 Form 内的所有操作的 payload 中。
表单值在 payload 中以其 name 为键,例如:
Select(name="title")→action.payload.titleSelect(name="todo.title")→action.payload.todo.title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Form(
direction="col",
validation="native"
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", type="submit")
]
)
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"):
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']
# ...验证
Form 使用基础的原生表单验证;它会在配置了 required 和 pattern 的字段上强制执行这些规则,并在表单包含任何无效字段时阻止提交。
我们未来可能会添加具有更好用户体验、更具表达力的验证、自定义错误显示等功能的验证模式。在此之前,组件并不是处理复杂且验证逻辑繁琐的表单的最佳媒介。如果您有此需求,更好的模式是使用客户端操作处理来触发模态框(modal),在模态框中显示自定义表单,然后通过 sendAction 将结果传回 ChatKit。
将 Card 视为 Form
您可以将 asForm=True 传递给 Card,它将表现得像一个 Form,运行验证并将收集到的字段传递给 Card 的 confirm 操作。
有效负载键冲突
如果与有效负载中其他现有的预定义键发生命名冲突,表单值将被忽略。这通常是一个错误,因此当我们检测到这种情况时,将触发一个 error 事件。
控制组件中的加载状态交互
使用 ActionConfig.loadingBehavior 来控制操作如何触发组件中的不同加载状态。
1
2
3
4
5
6
7
Button(
label="This make 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