액션을 사용하면 사용자가 메시지를 보내지 않아도 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액션과 페이로드는 클라이언트가 서버로 보내므로 신뢰할 수 없는 데이터로 취급하세요.
클라이언트에서 처리하기
클라이언트의 ChatKit 연동 코드에서 액션을 처리해야 할 때도 있습니다. 이때는 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이 설정되어 있으면 해당 조건을 적용하고, 유효하지 않은 필드가 하나라도 있으면 폼 제출을 차단합니다.
향후 사용자 경험을 개선하고 더 다양한 검증 조건과 사용자 지정 오류 표시 등을 지원하는 새로운 유효성 검사 모드를 추가할 수 있습니다. 현재로서는 검증 규칙이 까다로운 복잡한 폼을 구현하는 데 위젯이 적합하지 않습니다. 이런 폼이 필요하다면 클라이언트에서 액션을 처리해 모달을 열고 그 안에 사용자 지정 폼을 표시한 다음, sendAction으로 결과를 ChatKit에 전달하는 편이 좋습니다.
Card를 Form으로 사용하기
asForm=True를 Card에 전달하면 카드가 Form처럼 동작합니다. 유효성 검사를 실행하고 수집한 필드 값을 카드의 confirm 액션에 전달합니다.
페이로드 키 충돌
폼 필드 이름이 페이로드에 미리 정의된 다른 키와 충돌하면 해당 폼 값은 무시됩니다. 이런 충돌은 버그일 가능성이 높으므로 ChatKit은 충돌을 감지하면 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