Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion chatkit/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class AgentContext(BaseModel, Generic[TContext]):
def generate_id(
self, type: StoreItemType, thread: ThreadMetadata | None = None
) -> str:
"""Generate a new store-backed id for the given item type."""
if type == "thread":
return self.store.generate_thread_id(self.request_context)
return self.store.generate_item_id(
Expand All @@ -121,6 +122,7 @@ async def stream_widget(
widget: WidgetRoot | AsyncGenerator[WidgetRoot, None],
copy_text: str | None = None,
) -> None:
"""Stream a widget into the thread by enqueueing widget events."""
async for event in stream_widget(
self.thread,
widget,
Expand All @@ -134,6 +136,7 @@ async def stream_widget(
async def end_workflow(
self, summary: WorkflowSummary | None = None, expanded: bool = False
) -> None:
"""Finalize the active workflow item, optionally attaching a summary."""
if not self.workflow_item:
# No workflow to end
return
Expand All @@ -150,6 +153,7 @@ async def end_workflow(
self.workflow_item = None

async def start_workflow(self, workflow: Workflow) -> None:
"""Begin streaming a new workflow item."""
self.workflow_item = WorkflowItem(
id=self.generate_id("workflow"),
created_at=datetime.now(),
Expand All @@ -161,9 +165,10 @@ async def start_workflow(self, workflow: Workflow) -> None:
# Defer sending added event until we have tasks
return

await self.stream(ThreadItemAddedEvent(item=self.workflow_item))
await self.stream(ThreadItemAddedEvent(item=self.workflow_item))

async def update_workflow_task(self, task: Task, task_index: int) -> None:
"""Update an existing workflow task and stream the delta."""
if self.workflow_item is None:
raise ValueError("Workflow is not set")
# ensure reference is updated in case task is a copy
Expand All @@ -179,6 +184,7 @@ async def update_workflow_task(self, task: Task, task_index: int) -> None:
)

async def add_workflow_task(self, task: Task) -> None:
"""Append a workflow task and stream the appropriate event."""
self.workflow_item = self.workflow_item or WorkflowItem(
id=self.generate_id("workflow"),
created_at=datetime.now(),
Expand All @@ -202,6 +208,7 @@ async def add_workflow_task(self, task: Task) -> None:
)

async def stream(self, event: ThreadStreamEvent) -> None:
"""Enqueue a ThreadStreamEvent for downstream processing."""
await self._events.put(event)

def _complete(self):
Expand Down Expand Up @@ -352,6 +359,7 @@ class StreamingThoughtTracker(BaseModel):
async def stream_agent_response(
context: AgentContext, result: RunResultStreaming
) -> AsyncIterator[ThreadStreamEvent]:
"""Convert a streamed Agents SDK run into ChatKit ThreadStreamEvents."""
current_item_id = None
current_tool_call = None
ctx = context
Expand Down Expand Up @@ -1003,4 +1011,5 @@ async def to_agent_input(


def simple_to_agent_input(thread_items: Sequence[ThreadItem] | ThreadItem):
"""Helper that converts thread items using the default ThreadItemConverter."""
return _DEFAULT_CONVERTER.to_agent_input(thread_items)
5 changes: 5 additions & 0 deletions chatkit/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ async def stream_widget(
copy_text: str | None = None,
generate_id: Callable[[StoreItemType], str] = default_generate_id,
) -> AsyncIterator[ThreadStreamEvent]:
"""Stream a widget root (or async sequence of roots) as ThreadStreamEvents."""
item_id = generate_id("message")

if not isinstance(widget, AsyncGenerator):
Expand Down Expand Up @@ -277,6 +278,7 @@ def __init__(
store: Store[TContext],
attachment_store: AttachmentStore[TContext] | None = None,
):
"""Create a ChatKitServer with the backing Store and optional AttachmentStore."""
self.store = store
self.attachment_store = attachment_store

Expand Down Expand Up @@ -314,6 +316,7 @@ async def add_feedback( # noqa: B027
feedback: FeedbackKind,
context: TContext,
) -> None:
"""Persist user feedback for one or more thread items."""
pass

def action(
Expand All @@ -323,6 +326,7 @@ def action(
sender: WidgetItem | None,
context: TContext,
) -> AsyncIterator[ThreadStreamEvent]:
"""Handle a widget or client-dispatched action and yield response events."""
raise NotImplementedError(
"The action() method must be overridden to react to actions. "
"See https://github.com/openai/chatkit-python/blob/main/docs/widgets.md#widget-actions"
Expand Down Expand Up @@ -381,6 +385,7 @@ async def handle_stream_cancelled(
async def process(
self, request: str | bytes | bytearray, context: TContext
) -> StreamingResult | NonStreamingResult:
"""Parse an incoming ChatKit request and route it to streaming or non-streaming handlers."""
parsed_request = TypeAdapter[ChatKitReq](ChatKitReq).validate_json(request)
logger.info(f"Received request op: {parsed_request.type}")

Expand Down
14 changes: 14 additions & 0 deletions chatkit/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ class NotFoundError(Exception):
class AttachmentStore(ABC, Generic[TContext]):
@abstractmethod
async def delete_attachment(self, attachment_id: str, context: TContext) -> None:
"""Delete an attachment by id."""
pass

async def create_attachment(
self, input: AttachmentCreateParams, context: TContext
) -> Attachment:
"""Create an attachment record from upload metadata."""
raise NotImplementedError(
f"{type(self).__name__} must override create_attachment() to support two-phase file upload"
)
Expand All @@ -78,10 +80,12 @@ def generate_item_id(

@abstractmethod
async def load_thread(self, thread_id: str, context: TContext) -> ThreadMetadata:
"""Load a thread's metadata by id."""
pass

@abstractmethod
async def save_thread(self, thread: ThreadMetadata, context: TContext) -> None:
"""Persist thread metadata (title, status, etc.)."""
pass

@abstractmethod
Expand All @@ -93,20 +97,24 @@ async def load_thread_items(
order: str,
context: TContext,
) -> Page[ThreadItem]:
"""Load a page of thread items with pagination controls."""
pass

@abstractmethod
async def save_attachment(self, attachment: Attachment, context: TContext) -> None:
"""Persist attachment metadata."""
pass

@abstractmethod
async def load_attachment(
self, attachment_id: str, context: TContext
) -> Attachment:
"""Load attachment metadata by id."""
pass

@abstractmethod
async def delete_attachment(self, attachment_id: str, context: TContext) -> None:
"""Delete attachment metadata by id."""
pass

@abstractmethod
Expand All @@ -117,32 +125,38 @@ async def load_threads(
order: str,
context: TContext,
) -> Page[ThreadMetadata]:
"""Load a page of threads with pagination controls."""
pass

@abstractmethod
async def add_thread_item(
self, thread_id: str, item: ThreadItem, context: TContext
) -> None:
"""Persist a newly created thread item."""
pass

@abstractmethod
async def save_item(
self, thread_id: str, item: ThreadItem, context: TContext
) -> None:
"""Upsert a thread item by id."""
pass

@abstractmethod
async def load_item(
self, thread_id: str, item_id: str, context: TContext
) -> ThreadItem:
"""Load a thread item by id."""
pass

@abstractmethod
async def delete_thread(self, thread_id: str, context: TContext) -> None:
"""Delete a thread and its items."""
pass

@abstractmethod
async def delete_thread_item(
self, thread_id: str, item_id: str, context: TContext
) -> None:
"""Delete a thread item by id."""
pass
22 changes: 22 additions & 0 deletions docs/concepts/actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Actions

ChatKit actions are interaction events triggered by widgets or client code that let the client and server run logic or start a model response independently of user messages.

## Widget actions

Widget actions are specified in the widget definition itself (for example, `Button.onClickAction`), so every interaction carries a typed action payload plus the widget item that fired it. By default actions are routed to the server, but you can set `handler: "client"` when you want to intercept the action in the browser first.

### Server-handled actions

If you leave the handler unset, the action is delivered to `ChatKitServer.action(thread, action, sender, context)`, where `sender` is the widget item that triggered it when that item is available. Server handling is the right choice when you need to mutate thread state, stream widget or message updates, or start an agent response without a new user message. Record important interactions as hidden context so the model can react on the next turn (for example, “user clicked confirm”), and treat `action.payload` as untrusted input that must be validated and authorized before you persist anything.

### Client-handled actions

When you set `handler: "client"`, the action flows into the client SDK’s `widgets.onAction` callback so you can do immediate UI work such as opening dialogs, navigating, or running local validation. Client handlers can still forward a follow-up action to the server with `chatkit.sendCustomAction()` after local logic finishes. The server thread stays unchanged unless you explicitly send that follow-up action or a message.

## Client-sent actions using the chatkit.sendCustomAction() command

Your client integration can also initiate actions directly with `chatkit.sendCustomAction(action, itemId?)`, optionally namespaced to a specific widget item. The server receives these in `ChatKitServer.action` just like a widget-triggered action and can stream widgets, messages, or client effects in response. This pattern is useful when a flow starts outside a widget—or after a client-handled action—but you still want the server to persist results or involve the model.

## Related guides
- [Handle widget actions](../guides/add-features/handle-widget-actions.md)
30 changes: 30 additions & 0 deletions docs/concepts/entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Entities

Entities are structured pieces of information your system can recognize during a conversation, such as names, dates, IDs, or product-specific objects.

They represent meaningful objects in your app’s domain. For example:

- In a notes app, entities might be documents.
- In a news site, they might be articles.
- In an online store, they might be products.

When referenced, entities can link messages to real data and power richer actions and previews.

## Entity sources for assistant messages

Entities can be used as cited sources in assistant responses.

**References:**

- The [EntitySource](../../api/chatkit/types/#chatkit.types.EntitySource) Pydantic model definition
- [Add annotations in assistant messages](../guides/add-features/add-annotations.md#annotating-with-custom-entities).

## Entity tags as @-mentions in user messages

Users can tag your entities in the composer using @-mentions.

**References**:

- The [Entity](https://openai.github.io/chatkit-js/api/openai/chatkit-react/type-aliases/entity/) TypeScript type definition
- The [UserMessageTagContent](../../api/chatkit/types/#chatkit.types.UserMessageTagContent) Pydantic model definition
- [Allow @-mentions in user messages](../guides/add-features/allow-mentions.md).
54 changes: 54 additions & 0 deletions docs/concepts/thread-items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Thread items

Thread items are the individual records that make up a thread. This include user and assistant messages, widgets, workflows, and internal markers that guide processing. ChatKit orders and paginates them through your store implementation.

They drive two core experiences:

- **Model input**: Your server's [`respond`](../../api/chatkit/server/#chatkit.server.ChatKitServer.respond) logic will read items to build model input so the model sees the full conversation during an active turn and when resuming past threads. See [Compose model input](../guides/compose-model-input.md).
- **UI rendering**: ChatKit.js renders items incrementally for the active thread during streaming, and re-renders the persisted items when past threads are loaded.

## User messages

[`UserMessageItem`](../../api/chatkit/types/#chatkit.types.UserMessageItem)s represent end-user input. A user message can include the entered text, optional `quoted_text` for reply-style UI, and attachment metadata. User text is plain (no Markdown rendering) but can include @-mentions/tags; see [Allow @-mentions in user messages](../guides/add-features/allow-mentions.md).

## Assistant messages

[`AssistantMessageItem`](../../api/chatkit/types/#chatkit.types.AssistantMessageItem)s represent assistant responses. Content can include text, tool call outputs, widgets, and annotations. Text is Markdown-rendered and can carry inline annotations; see [Add annotations in assistant messages](../guides/add-features/add-annotations.md).

### Markdown support

Markdown in assistant messages supports:

- GitHub-flavored Markdown: Lists, headings, code fences, inline code, blockquotes, links—all with streaming-friendly layout.
- Lists: Ordered/unordered lists stay stable while streaming (Safari-safe markers, no reflow glitches).
- Line breaks: Single newlines render as `<br>` when `breakNewLines` is enabled.
- Code blocks: Syntax-highlighted, copyable, and streamed smoothly; copy buttons are always present.
- Math: LaTeX via remark/rehype math plugins for inline and block equations.
- Tables: Automatic sizing with horizontal scroll for wide outputs.
- Inline annotations: Markdown directives spawn interactive annotations wired into ChatKit handlers.

## Hidden context items

Hidden context items serve as model input but are not rendered in the chat UI. Use them to pass non-visible signals (for example, widget actions or system context) so the model can respond to what the user did, not just what they typed.

- [`HiddenContextItem`](../../api/chatkit/types/#chatkit.types.HiddenContextItem): Your integration’s hidden context; you control the schema and how it is converted for the model.
- [`SDKHiddenContextItem`](../../api/chatkit/types/#chatkit.types.SDKHiddenContextItem): Hidden context inserted by the ChatKit Python SDK for its own operations; you normally leave it alone unless you override conversion behavior.


## ThreadItemConverter

[`ThreadItemConverter`](../../api/chatkit/agents/#chatkit.agents.ThreadItemConverter) maps stored thread items into model-ready input items. Defaults cover messages, widgets, workflows, and tasks; override it to handle attachments, tags, or hidden context in the format your model expects. Combine converter tweaks with prompting so the model sees a coherent view of rich items (for example, summarizing widgets or tasks into text the model can consume).

## Thread item actions

Thread item actions are quick action buttons attached to an assistant turn that let users act on the output, such as retrying, copying, or submitting feedback.

They can be configured client-side with the [threadItemActions option](https://openai.github.io/chatkit-js/api/openai/chatkit-react/type-aliases/threaditemactionsoption/).


## Related guides
- [Persist ChatKit threads and messages](../guides/persist-chatkit-data.md)
- [Compose model inputs](../guides/compose-model-input.md)
- [Add annotations in assistant messages](../guides/add-features/add-annotations.md)
- [Allow @-mentions in user messages](../guides/add-features/allow-mentions.md)
- [Handle feedback](../guides/add-features/handle-feedback.md)
46 changes: 46 additions & 0 deletions docs/concepts/thread-stream-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Thread stream events

[`ThreadStreamEvent`](../../api/chatkit/types/#chatkit.types.ThreadStreamEvent)s are the Server-Sent Event (SSE) payloads streamed by ChatKitServer while responding to a user message or action. They keep the client UI in sync with server-side processing and drive persistence in your store.

## Thread metadata updates

ChatKitServer emits these after it creates a thread or notices metadata changes (title, status, etc.) so the UI stays in sync.

- [`ThreadCreatedEvent`](../../api/chatkit/types/#chatkit.types.ThreadCreatedEvent): introduce a new thread
- [`ThreadUpdatedEvent`](../../api/chatkit/types/#chatkit.types.ThreadUpdatedEvent): update the current thread metadata such as title or status

## Thread item events

Thread item events drive the conversation state. ChatKitServer processes these events to persist conversation state before streaming them back to the client.

- [`ThreadItemAddedEvent`](../../api/chatkit/types/#chatkit.types.ThreadItemAddedEvent): introduce a new item (message, tool call, workflow, widget, etc).
- [`ThreadItemUpdatedEvent`](../../api/chatkit/types/#chatkit.types.ThreadItemUpdatedEvent): mutate a pending item (e.g., stream text deltas, workflow task updates).
- [`ThreadItemDoneEvent`](../../api/chatkit/types/#chatkit.types.ThreadItemDoneEvent): mark an item complete and persist it.
- [`ThreadItemRemovedEvent`](../../api/chatkit/types/#chatkit.types.ThreadItemRemovedEvent): delete an item by id.
- [`ThreadItemReplacedEvent`](../../api/chatkit/types/#chatkit.types.ThreadItemReplacedEvent): swap an item in place.

Note: `ThreadItemAddedEvent` does not persist the item. `ChatKitServer` saves on `ThreadItemDoneEvent`/`ThreadItemReplacedEvent`, tracks pending items in between, and handles store writes for all `ThreadItem*Event`s.

## Errors

Stream [`ErrorEvent`](../../api/chatkit/types/#chatkit.types.ErrorEvent)s for user-facing errors in the chat UI. You can configure a custom message and whether a retry button is shown to the user.

## Progress updates

Stream [`ProgressUpdateEvent`](../../api/chatkit/types/#chatkit.types.ProgressUpdateEvent)s to show the user transient status while work is in flight.

See [Show progress for long-running tools](../guides/add-features/show-progress-for-long-running-tools.md) for more info.

## Client effects

Use [`ClientEffectEvent`](../../api/chatkit/types/#chatkit.types.ClientEffectEvent) to trigger fire-and-forget behavior on the client such as opening a dialog or pushing updates.

See [Send client effects](../guides/add-features/send-client-effects.md) for more info.

## Stream options

[`StreamOptionsEvent`](../../api/chatkit/types/#chatkit.types.StreamOptionsEvent) configures runtime stream behavior (for example, allowing user cancellation). `ChatKitServer` emits one at the start of every stream using `get_stream_options`; override that method to change defaults such as `allow_cancel`.


## Related guides
- [Stream responses back to your user](../guides/stream-thread-events.md)
16 changes: 16 additions & 0 deletions docs/concepts/threads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Threads

Threads are the core unit of ChatKit: a single conversation timeline that groups messages, tool calls, widgets, and related metadata.

## Lifecycle
- When a user submits a message and no thread exists, `ChatKitServer` creates one by calling your store's [`save_thread`](../../api/chatkit/store/#chatkit.store.Store.save_thread).
- As responses stream back, `ChatKitServer` automatically persists thread items as they are completed—see [Thread items](thread-items.md) and [Stream responses back to your user](../guides/stream-thread-events.md) for how events drive storage.
- Update titles or metadata intentionally in your integration (e.g., after summarizing a topic) by calling [`store.save_thread`](../../api/chatkit/store/#chatkit.store.Store.save_thread) with the new values.
- When history is enabled client-side, ChatKit retrieves past threads. The user can continue any previous thread by default.
- Archive or close threads according to your policies: mark them read-only (e.g., [disable new messages](../guides/add-features/disable-new-messages.md)) or delete them if you no longer want them discoverable.


## Related guides
- [Persist ChatKit threads and messages](../guides/persist-chatkit-data.md)
- [Save thread titles](../guides/add-features/save-thread-titles.md)
- [Disable new messages for a thread](../guides/add-features/disable-new-messages.md)
Loading