For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
기본 탐색 메뉴
Theme

ChatKit의 액션

채팅에서 사용자가 상호작용하면 서버에서 액션을 실행하세요.

액션을 사용하면 사용자가 메시지를 보내지 않아도 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.title
  • Select(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 → self
  • Select.onChangeAction → none
  • Card.confirm.action → container