Actions 是 ChatKit SDK 前端无需用户提交消息即可触发流式响应的一种方式。它们也可用于触发 ChatKit SDK 外部的副作用。
触发 Actions
响应与微件的用户交互
可以通过将 action 附加到 ActionConfig 到任何支持它的微件节点来触发 Actions。例如,您可以响应按钮上的点击事件。当用户点击此按钮时,该 action 将被发送到您的服务器,您可以在服务器上更新微件、运行推理、流式传输新的对话项等。
1
2
3
4
5
6
7
Button(
label="Example",
onClickAction=ActionConfig(
type="example",
payload={"id": 123},
)
)也可以通过您的前端使用命令式方式发送 Actions sendAction()。当您需要 ChatKit 响应其外部的交互时,这可能最为有用;但如果您需要在客户端和服务器端同时进行响应,它也可用于串联操作(详见下文)。
1
2
3
4
await chatKit.sendAction({
type: "example",
payload: { id: 123 },
});处理 Actions
在服务器上
默认情况下,Actions 会被发送到您的服务器。您可以通过实现以下逻辑在服务器上处理 Actions: action 方法与 ChatKitServer.
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 eNOTE: 与任何客户端/服务器交互一样,操作及其负载由客户端发送,因此应将其视为不受信任的数据。
客户端
有时您会希望在客户端集成中处理操作。为此,您需要通过添加以下内容来指定该操作应发送到您的客户端操作处理器: handler="client to the ActionConfig.
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 and ActionConfig 不是强类型的。不过,我们确实在 create 上提供了一个 Action 辅助函数,以便轻松生成 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,这些字段的值将被包含在 payload 所有源自以下内部的操作: Form.
表单值的键位于 payload 通过其 name e.g.
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 and pattern 在配置了验证规则的字段上,并在表单存在任何无效字段时阻止提交。
未来我们可能会添加新的验证模式,以提供更好的用户体验、更具表现力的验证以及自定义错误显示等。在此之前,组件并非处理需要复杂验证的表单的理想载体。如果您有此需求,更好的模式是使用客户端操作处理来触发模态框,在其中展示自定义表单,然后将结果传回 ChatKit,通过 sendAction.
将其视为 Card as a Form
您可以传入 asForm=True to Card 它将表现为 Form,运行验证并将收集到的字段传递给 Card 的 confirm action.
负载键名冲突
如果您的负载上存在与其他预定义键名的命名冲突,该表单值将被忽略。这可能是一个 bug,因此当我们发现这种情况时,我们会发出一个 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