Las acciones permiten que el frontend del SDK de ChatKit inicie una respuesta en streaming sin que el usuario tenga que enviar un mensaje. También pueden producir efectos secundarios fuera del SDK de ChatKit.
Activar acciones
Cuando el usuario interactúa con los widgets
Para activar una acción, asocia un ActionConfig a cualquier nodo de widget que lo admita. Por ejemplo, puedes responder a eventos de clic en botones. Cuando el usuario haga clic en el botón del siguiente ejemplo, la acción se enviará a tu servidor, donde podrás actualizar el widget, ejecutar una inferencia o transmitir nuevos elementos del hilo, entre otras cosas.
button = Button(
label="Example",
onClickAction=ActionConfig(
type="example",
payload={"id": 123},
),
)También puedes enviar acciones de forma imperativa desde tu frontend con sendAction(). Esta opción suele ser especialmente útil cuando necesitas que ChatKit responda a una interacción que ocurre fuera de ChatKit. También puedes usarla para encadenar acciones cuando necesitas responder tanto en el cliente como en el servidor, como se explica más adelante.
await chatKit.sendAction({
type: "example",
payload: { id: 123 },
});Manejar acciones
En el servidor
De forma predeterminada, las acciones se envían a tu servidor. Para manejarlas allí, implementa el método action en 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 eventTrata las acciones y sus cargas útiles como datos no confiables, ya que el cliente las envía a tu servidor.
En el cliente
A veces querrás manejar las acciones en tu integración del lado del cliente. Para indicar que una acción debe enviarse al controlador de acciones del cliente, agrega handler="client" al ActionConfig.
button = Button(
label="Example",
onClickAction=ActionConfig(type="example", payload={"id": 123}, handler="client"),
)Cuando se active la acción, se pasará como argumento a la función de callback que proporciones al crear la instancia de 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 },
});Acciones con tipado fuerte
De forma predeterminada, Action y ActionConfig no tienen tipado fuerte. Sin embargo, el método auxiliar create de Action genera instancias de ActionConfig a partir de un conjunto de acciones con tipado fuerte.
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.")],
)
)Crear formularios personalizados con widgets y acciones
Cuando se montan nodos de widget que aceptan datos del usuario dentro de un Form, los valores de esos campos se incluyen en el payload de todas las acciones que se originan dentro del Form.
En el payload, el valor de cada campo del formulario se guarda bajo la clave que indica su name. Por ejemplo:
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.")],
)
)Validación
Form usa la validación nativa básica de formularios. Aplica las reglas required y pattern a los campos que las tengan configuradas y bloquea el envío si algún campo no es válido.
En el futuro, es posible que agreguemos modos de validación que ofrezcan una mejor experiencia de usuario, permitan definir reglas más expresivas o personalizar la forma de mostrar los errores, entre otras mejoras. Por ahora, los widgets no son la mejor opción para formularios complejos con reglas de validación difíciles de implementar. Si necesitas un formulario así, conviene manejar la acción en el cliente para abrir una ventana modal, mostrar allí un formulario personalizado y devolver el resultado a ChatKit con sendAction.
Usar Card como Form
Puedes pasar asForm=True a Card para que se comporte como un Form. Así, la tarjeta ejecuta la validación y pasa los valores de los campos a su acción confirm.
Conflictos entre claves de la carga útil
Si el nombre de un campo coincide con una clave predefinida que ya existe en la carga útil, se ignora el valor de ese campo del formulario. Esta coincidencia probablemente se deba a un error, por lo que emitiremos un evento error si la detectamos.
Controlar cómo las acciones activan los estados de carga de los widgets
Usa ActionConfig.loadingBehavior para controlar cómo las acciones activan los distintos estados de carga de un widget.
button = Button(
label="This may take a while...",
onClickAction=ActionConfig(
type="long_running_action_that_should_block_other_ui_interactions",
loadingBehavior="container",
),
)| Valor | Comportamiento |
|---|---|
auto |
La acción se adapta al modo en que se usa. (predeterminado) |
self |
La acción activa el estado de carga en el nodo del widget al que está asociada. |
container |
La acción activa el estado de carga en todo el contenedor del widget. El widget se vuelve ligeramente más transparente y deja de responder a las interacciones. |
none |
Sin estado de carga |
Usar el comportamiento auto
En general, recomendamos usar auto, el valor predeterminado. Con auto, el estado de carga depende de dónde esté asociada la acción. Por ejemplo:
Button.onClickAction→selfSelect.onChangeAction→noneCard.confirm.action→container