ChatKit에서의 액션
ChatKit에서의 액션 (Actions in ChatKit)
액션은 사용자가 메시지를 제출하지 않아도 ChatKit SDK 프론트엔드가 스트리밍 응답을 트리거하는 방법이에요. ChatKit SDK 밖에서 부수 효과(side-effect)를 트리거하는 데도 쓸 수 있어요.
출처: 문서
본문
액션 트리거하기
위젯과의 사용자 상호작용에 대한 응답으로
액션은 이를 지원하는 위젯 노드에 ActionConfig를 붙이면 트리거할 수 있어요. 예를 들어 Button의 클릭 이벤트에 반응할 수 있어요. 사용자가 이 버튼을 클릭하면 액션이 서버로 전송되고, 서버에서 위젯을 업데이트하거나 추론을 실행하거나 새 스레드 항목을 스트리밍하는 등의 작업을 할 수 있어요.
button = Button(
label="Example",
onClickAction=ActionConfig(
type="example",
payload={"id": 123},
),
)
액션은 프론트엔드에서 sendAction()으로 직접 명령적으로 보낼 수도 있어요. ChatKit 밖에서 일어나는 상호작용에 ChatKit이 응답해야 할 때 가장 유용하겠지만, 클라이언트와 서버 양쪽에서 응답해야 할 때 액션을 체이닝하는 데도 쓸 수 있어요.
await chatKit.sendAction({
type: "example",
payload: { id: 123 },
});
액션 처리하기
서버에서
기본적으로 액션은 서버로 전송돼요. ChatKitServer에 action 메서드를 구현하면 서버에서 액션을 처리할 수 있어요.
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",
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
액션과 그 payload는 클라이언트가 서버로 보내는 것이므로 신뢰할 수 없는 데이터로 취급하세요.
클라이언트에서
때로는 클라이언트 통합에서 액션을 처리하고 싶을 때가 있어요. 그러려면 ActionConfig에 handler="client"를 추가해서 액션을 클라이언트 측 액션 핸들러로 보내야 한다고 지정하면 돼요.
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 },
});
강한 타입의 액션 (Strongly typed actions)
기본적으로 Action과 ActionConfig는 강한 타입이 아니에요. 그러나 Action에 create 헬퍼를 노출해서 강한 타입 액션 집합에서 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 안에 마운트되면, 그 필드들의 값이 Form 안에서 시작된 모든 액션의 payload에 포함돼요.
Form 값은 payload에서 name으로 키가 매겨져요. 예를 들어:
Select(name="title")→action.payload.titleSelect(name="todo.title")→action.payload.todo.title
form = 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", 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.")],
)
)
검증 (Validation)
Form은 기본적인 기본(native) 폼 검증을 사용해요. required와 pattern을 설정한 필드에 적용하고, 폼에 유효하지 않은 필드가 있으면 제출을 막아요.
향후 더 나은 UX, 더 표현력 있는 검증, 커스텀 오류 표시 등을 가진 새로운 검증 모드를 추가할 수도 있어요. 그 전까지는 위젯이 까다로운 검증이 필요한 복잡한 폼에는 좋은 매체가 아니에요. 이런 필요가 있다면, 클라이언트 측 액션 처리를 써서 모달을 띄우고 거기서 커스텀 폼을 보여준 뒤, 결과를 sendAction으로 ChatKit에 다시 전달하는 패턴이 더 좋아요.
Card를 Form으로 취급하기
Card에 asForm=True를 전달하면 Form처럼 동작해요. 검증을 실행하고 수집한 필드를 Card의 confirm 액션에 전달해요.
Payload 키 충돌
payload의 다른 기존 사전 정의 키와 이름이 충돌하면 폼 값은 무시돼요. 이건 아마 버그라서, 그런 경우 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