openai/chatkit-python
Publicmirrored fromhttps://github.com/openai/chatkit-pythonAvailable
docs/guides/browse-past-threads.md
139lines · modecode
| 1 | # Let users browse past threads |
| 2 | |
| 3 | Let users return to previous conversations, see readable titles in a history list, and decide which threads can be continued. |
| 4 | |
| 5 | ## Enable thread history in the client |
| 6 | |
| 7 | The ChatKit React hooks support a built-in history view that lists past threads. History is enabled by default, but you can configure it explicitly when you create your ChatKit controller: |
| 8 | |
| 9 | ```tsx |
| 10 | const chatkit = useChatKit({ |
| 11 | // ... |
| 12 | history: { |
| 13 | enabled: true, |
| 14 | showDelete: true, |
| 15 | showRename: true, |
| 16 | }, |
| 17 | }); |
| 18 | ``` |
| 19 | |
| 20 | With `history.enabled: true`, ChatKit.js will: |
| 21 | |
| 22 | - Fetch threads from your ChatKit server. |
| 23 | - Show them in a history list using `thread.title` when available. |
| 24 | - Let users click a past thread to load its items and continue the conversation. |
| 25 | - Let users delete and rename threads. |
| 26 | |
| 27 | Set `history.enabled: false` if you want a single-thread, stateless chat experience with no history UI. |
| 28 | |
| 29 | ## Show readable titles in history |
| 30 | |
| 31 | Threads start untitled. Give them short, descriptive titles so the history list is easy to scan. |
| 32 | |
| 33 | ### Set a title directly |
| 34 | |
| 35 | Set `thread.title` on the server and persist it with your store: |
| 36 | |
| 37 | ```python |
| 38 | from chatkit.server import ChatKitServer |
| 39 | |
| 40 | |
| 41 | class MyChatKitServer(ChatKitServer[RequestContext]): |
| 42 | async def respond(...): |
| 43 | ... |
| 44 | if not thread.title: |
| 45 | thread.title = "Order #1234" |
| 46 | await self.store.save_thread(thread, context=context) |
| 47 | ``` |
| 48 | |
| 49 | ChatKit will emit a `ThreadUpdatedEvent` so connected clients update the title in their history views. |
| 50 | |
| 51 | ### Auto-generate a title after the first turn |
| 52 | |
| 53 | Generate a concise title after the first assistant turn once you have enough context. Skip if the thread already has a title or if there isn’t enough content to summarize. |
| 54 | |
| 55 | ```python |
| 56 | class MyChatKitServer(ChatKitServer[RequestContext]): |
| 57 | async def respond(...): |
| 58 | updating_thread_title = asyncio.create_task( |
| 59 | self._maybe_update_thread_title(thread, context) |
| 60 | ) |
| 61 | |
| 62 | # Stream your main response |
| 63 | async for event in stream_agent_response(agent_context, result): |
| 64 | yield event |
| 65 | |
| 66 | # Await so the title update streams back as a ThreadUpdatedEvent |
| 67 | await updating_thread_title |
| 68 | |
| 69 | async def _maybe_update_thread_title( |
| 70 | self, thread: ThreadMetadata, context: RequestContext |
| 71 | ) -> None: |
| 72 | if thread.title is not None: |
| 73 | return |
| 74 | items = await self.store.load_thread_items( |
| 75 | thread.id, |
| 76 | after=None, |
| 77 | limit=6, |
| 78 | order="desc", |
| 79 | context=context, |
| 80 | ) |
| 81 | thread.title = await generate_short_title(items.data) # your model call |
| 82 | await self.store.save_thread(thread, context=context) |
| 83 | ``` |
| 84 | |
| 85 | Use any model call you like for `generate_short_title`: run a tiny Agent, a simple completion, or your own heuristic. Keep titles brief (for example, 3–6 words). |
| 86 | |
| 87 | ## Decide which threads can be continued |
| 88 | |
| 89 | By default, users can continue any past thread: selecting it in the history view loads its items and reuses the same thread when they send a new message. |
| 90 | |
| 91 | Use `thread.status` to mark conversations that should no longer accept new messages. Locked and closed threads still appear in history, but the composer UI changes. |
| 92 | |
| 93 | There are two ways to stop new user messages: temporarily lock a thread or permanently close it when the conversation is finished. |
| 94 | |
| 95 | | State | When to use | Input UI | What the user sees | |
| 96 | |---------|------------------------------------------------|------------------------------------------------|--------------------| |
| 97 | | Locked | Temporary pause for moderation or admin action | Composer stays on screen but is disabled; the placeholder shows the lock reason. | The reason for the lock in the disabled composer. | |
| 98 | | Closed | Final state when the conversation is done | The input UI is replaced with an informational banner. | A static default message or a custom reason, if provided. | |
| 99 | |
| 100 | ### Update thread status (lock, close, or re-open) |
| 101 | |
| 102 | |
| 103 | ```python |
| 104 | from chatkit.types import ActiveStatus, LockedStatus, ClosedStatus |
| 105 | |
| 106 | # lock (temporary pause) |
| 107 | thread.status = LockedStatus(reason="Escalated to support.") |
| 108 | await store.save_thread(thread, context=context) |
| 109 | |
| 110 | # close (final state) |
| 111 | thread.status = ClosedStatus(reason="Resolved.") |
| 112 | await store.save_thread(thread, context=context) |
| 113 | |
| 114 | # re-open |
| 115 | thread.status = ActiveStatus() |
| 116 | await store.save_thread(thread, context=context) |
| 117 | ``` |
| 118 | |
| 119 | When you persist a new status during `respond`, ChatKit emits a `ThreadUpdatedEvent` so all viewers see the updated state. |
| 120 | |
| 121 | You can also update the thread status from a custom client-facing endpoint that updates the store directly (outside of the ChatKit server request flow). If the user is currently viewing the thread, have the client call `chatkit.fetchUpdates()` after the status is persisted so the UI picks up the latest thread state. |
| 122 | |
| 123 | ### Block server-side work when locked or closed |
| 124 | |
| 125 | Thread status only affects the composer UI; `ChatKitServer` does not automatically reject actions, tool calls, or imperative message adds. Your integration should short-circuit handlers when a thread is disabled: |
| 126 | |
| 127 | ```python |
| 128 | class MyChatKitServer(...): |
| 129 | async def respond(thread, input_user_message, context): |
| 130 | if thread.status.type in {"locked", "closed"}: |
| 131 | return |
| 132 | # normal processing |
| 133 | |
| 134 | async def action(thread, action, sender, context): |
| 135 | if thread.status.type in {"locked", "closed"}: |
| 136 | return |
| 137 | # normal processing |
| 138 | ``` |
| 139 | |
| 140 | |