Com as ações, o frontend do SDK do ChatKit pode iniciar uma resposta em streaming sem que o usuário envie uma mensagem. As ações também podem produzir efeitos colaterais fora do SDK do ChatKit.
Como disparar ações
Em resposta a interações do usuário com widgets
Para disparar uma ação, associe um ActionConfig a qualquer node de widget que aceite essa configuração. Por exemplo, você pode responder a eventos de clique em botões. Quando o usuário clica no botão, o ChatKit envia a ação ao seu servidor, onde você pode atualizar o widget, executar inferência ou transmitir novos itens da conversa em streaming, entre outras operações.
button = Button(
label="Example",
onClickAction=ActionConfig(
type="example",
payload={"id": 123},
),
)O frontend também pode enviar ações diretamente com sendAction(). Isso costuma ser mais útil quando o ChatKit precisa responder a uma interação que ocorre fora dele. Você também pode usar esse método para encadear ações quando precisar responder tanto no cliente quanto no servidor, como descrito a seguir.
await chatKit.sendAction({
type: "example",
payload: { id: 123 },
});Como tratar ações
No servidor
Por padrão, o ChatKit envia as ações ao seu servidor. Para tratá-las no servidor, implemente o método action em 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 eventTrate as ações e seus payloads como dados não confiáveis, pois quem os envia ao servidor é o cliente.
No cliente
Em alguns casos, você pode querer tratar as ações na integração do lado do cliente. Para que o ChatKit envie a ação ao manipulador de ações do cliente, adicione handler="client" ao ActionConfig.
button = Button(
label="Example",
onClickAction=ActionConfig(type="example", payload={"id": 123}, handler="client"),
)Quando a ação for disparada, o ChatKit a encaminhará ao callback que você fornecer ao instanciá-lo.
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 },
});Ações com tipagem forte
Por padrão, Action e ActionConfig não têm tipagem forte. No entanto, o método auxiliar create de Action gera configurações do tipo ActionConfig a partir de um conjunto de ações com tipagem forte.
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.")],
)
)Use widgets e ações para criar formulários personalizados
Quando você monta nodes de widget que recebem dados do usuário dentro de um Form, os valores desses campos são incluídos no payload de todas as ações originadas nesse Form.
No payload, o name de cada campo define a chave usada para acessar seu valor. Veja os exemplos abaixo.
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.")],
)
)Validação
O Form usa a validação nativa básica de formulários. Ele aplica as regras required e pattern aos campos em que elas estão configuradas e impede o envio se algum campo for inválido.
No futuro, poderemos adicionar modos de validação que melhorem a experiência do usuário e permitam definir regras mais expressivas e personalizar a exibição de erros, entre outros recursos. Por enquanto, widgets são pouco adequados para formulários complexos com regras de validação difíceis de implementar. Nesse caso, uma abordagem melhor é tratar a ação no cliente para abrir uma janela modal, exibir nela um formulário personalizado e enviar o resultado de volta ao ChatKit com sendAction.
Como usar Card como Form
Você pode passar asForm=True para Card para que ele se comporte como um Form, execute a validação e encaminhe os valores coletados nos campos à ação confirm do Card.
Conflitos entre chaves do payload
Se o nome de um campo coincidir com o de uma chave predefinida já presente no payload, o ChatKit ignorará o valor desse campo do formulário. Como isso provavelmente indica um bug, o ChatKit emite um evento error ao detectar o conflito.
Controle os estados de carregamento dos widgets durante as interações
Use ActionConfig.loadingBehavior para controlar como as ações ativam diferentes estados de carregamento em um widget.
button = Button(
label="This may take a while...",
onClickAction=ActionConfig(
type="long_running_action_that_should_block_other_ui_interactions",
loadingBehavior="container",
),
)| Valor | Comportamento |
|---|---|
auto |
O comportamento da ação se adapta à forma como ela é usada. (padrão) |
self |
A ação ativa o estado de carregamento no node do widget ao qual ela está associada. |
container |
A ação ativa o estado de carregamento em todo o contêiner do widget. O widget fica ligeiramente mais transparente e deixa de responder a interações. |
none |
Nenhum estado de carregamento |
Como usar o comportamento auto
Em geral, recomendamos usar auto, que é o padrão. Com auto, o estado de carregamento depende de onde você associa a ação, como mostram os exemplos abaixo.
Button.onClickAction→selfSelect.onChangeAction→noneCard.confirm.action→container