From 1d06e8a7d33342410666ca2ecb7fe878853c9a4d Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Mon, 1 Dec 2025 17:11:36 -0800 Subject: [PATCH 1/2] Add more add-feature pages --- docs/guides/add-features/allow-mentions.md | 129 ++++++++++++++++++ .../add-features/disable-new-messages.md | 50 +++++++ docs/guides/add-features/handle-feedback.md | 41 ++++++ .../show-progress-for-long-running-tools.md | 104 ++++++++++++++ docs/guides/stream-thread-events.md | 63 +++------ mkdocs.yml | 10 +- 6 files changed, 350 insertions(+), 47 deletions(-) create mode 100644 docs/guides/add-features/allow-mentions.md create mode 100644 docs/guides/add-features/disable-new-messages.md create mode 100644 docs/guides/add-features/handle-feedback.md create mode 100644 docs/guides/add-features/show-progress-for-long-running-tools.md diff --git a/docs/guides/add-features/allow-mentions.md b/docs/guides/add-features/allow-mentions.md new file mode 100644 index 0000000..eeb4685 --- /dev/null +++ b/docs/guides/add-features/allow-mentions.md @@ -0,0 +1,129 @@ +# Allow @-mentions in user messages + +Mentions travel through ChatKit as structured tags so the model can resolve entities instead of guessing from free text. Send `input_tag` parts from the client and translate them into model-readable context on the server. + +## Enable as-you-type entity lookup in the composer + +To enable entity tagging as @-mentions in the composer, configure [`entities.onTagSearch`](https://openai.github.io/chatkit-js/api/openai/chatkit/type-aliases/entitiesoption/#ontagsearch) as a ChatKit.js option. + +It should return a list of [Entity](https://openai.github.io/chatkit-js/api/openai/chatkit/type-aliases/entity/) objects that match the query string. + + +```ts +const chatkit = useChatKit({ + // ... + entities: { + onTagSearch: async (query: string) => { + return [ + { + id: "article_123", + title: "The Future of AI", + group: "Trending", + icon: "globe", + data: { type: "article" } + }, + { + id: "article_124", + title: "One weird trick to improve your sleep", + group: "Trending", + icon: "globe", + data: { type: "article" } + }, + ] + }, + }, +}) +``` + +## Convert tags into model input in your server + +Override `ThreadItemConverter.tag_to_message_content` to describe what each tag refers to. + +Example converter method that wraps the tagged entity details in custom markup: + +```python +from chatkit.agents import ThreadItemConverter +from chatkit.types import UserMessageTagContent +from openai.types.responses import ResponseInputTextParam + +class MyThreadItemConverter(ThreadItemConverter): + async def tag_to_message_content( + self, tag: UserMessageTagContent + ) -> ResponseInputTextParam: + if tag.type == "article": + # Load or unpack the entity the tag refers to + summary = await fetch_article_summary(tag.id) + return ResponseInputTextParam( + type="input_text", + text=( + "\n" + f"ID: {tag.id}\n" + f"Title: {tag.text}\n" + f"Summary: {summary}\n" + "" + ), + ) +``` + + +## Pair mentions with retrieval tool calls + +When the referenced content is too large to inline, keep the tag lean (id + short summary) and let the model fetch details via a tool. In your system prompt, tell the assistant to call the retrieval tool when it sees an `ARTICLE_TAG`. + +Example tool paired with the converter above: + +```python +from agents import Agent, StopAtTools, RunContextWrapper, function_tool +from chatkit.agents import AgentContext + +@function_tool(description_override="Fetch full article content by id.") +async def fetch_article(ctx: RunContextWrapper[AgentContext], article_id: str): + article = await load_article_content(article_id) + return { + "title": article.title, + "content": article.body, + "url": article.url, + } + +assistant = Agent[AgentContext]( + ..., + tools=[fetch_article], +) +``` + +In `tag_to_message_content`, include the id the tool expects (for example, `tag.id` or `tag.data["article_id"]`). The model can then decide to call `fetch_article` to pull the full text instead of relying solely on the brief summary in the tag. + +## Prompt the model about mentions + +Add short system guidance to help the assistant understand the input item that adds details about the @-mention. + +For example: + +``` +- ... is a summary of an article the user referenced. +- Use it as trusted context when answering questions about that article. +- Do not restate the summary verbatim; answer the user’s question concisely. +- Call the `fetch_article` tool with the article id from the tag when more + detail is needed or the user asks for specifics not in the summary. +``` + +Combined with the converter above, the model receives explicit, disambiguated entity context while users keep a rich mention UI. + + +## Handle clicks and previews + +Clicks and hover previews apply to the tagged entities shown in past user messages. Mark an entity as interactive when you return it from `onTagSearch` so the client knows to wire these callbacks: + +```ts +{ + id: "article_123", + title: "The Future of AI", + group: "Trending", + icon: "globe", + interactive: true, // clickable/previewable + data: { type: "article" } +} +``` + +- `entities.onClick` fires when a user clicks a tag in the transcript. Handle navigation or open a detail view. See the [onClick option](https://openai.github.io/chatkit-js/api/openai/chatkit/type-aliases/entitiesoption/#onclick). +- `entities.onRequestPreview` runs when the user hovers or taps a tag that has `interactive: true`. Return a `BasicRoot` widget; you can build one with `WidgetTemplate.build_basic(...)` if you are building the preview widgets server-side. See the [onRequestPreview option](https://openai.github.io/chatkit-js/api/openai/chatkit/type-aliases/entitiesoption/#onrequestpreview). diff --git a/docs/guides/add-features/disable-new-messages.md b/docs/guides/add-features/disable-new-messages.md new file mode 100644 index 0000000..e3f66cf --- /dev/null +++ b/docs/guides/add-features/disable-new-messages.md @@ -0,0 +1,50 @@ +# Disable new messages for a thread + +There are two ways to stop new user messages: temporarily lock a thread or permanently close it when the conversation is finished. + +| State | When to use | Input UI | What the user sees | +|---------|------------------------------------------------|------------------------------------------------|--------------------| +| 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. | +| 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. | + +## Update thread status (lock, close, or re-open) + +Update `thread.status`—whether moving between active, locked, or closed—and persist it. + +```python +from chatkit.types import ActiveStatus, LockedStatus, ClosedStatus + +# lock +thread.status = LockedStatus(reason="Escalated to support.") +await store.save_thread(thread, context=context) + +# close (final) +thread.status = ClosedStatus(reason="Resolved.") +await store.save_thread(thread, context=context) + +# re-open +thread.status = ActiveStatus() +await store.save_thread(thread, context=context) +``` + +If you update the thread status within the `respond` method, ChatKit will emit a `ThreadUpdatedEvent` so connected clients update immediately. + +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. + +## Block server-side work when locked or closed + +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: + +```python +class MyChatKitServer(...): + async def respond(thread, input_user_message, context): + if thread.status.type in {"locked", "closed"}: + return + # normal processing + + async def action(thread, action, sender, context): + if thread.status.type in {"locked", "closed"}: + return + # normal processing +``` + diff --git a/docs/guides/add-features/handle-feedback.md b/docs/guides/add-features/handle-feedback.md new file mode 100644 index 0000000..6db33fe --- /dev/null +++ b/docs/guides/add-features/handle-feedback.md @@ -0,0 +1,41 @@ +# Handle feedback + +## Enable feedback actions on the client + +Collect thumbs up/down feedback so you can flag broken answers, retrain on good ones, or alert humans. Enable the message actions in the client by setting [`threadItemActions.feedback`](https://openai.github.io/chatkit-js/api/openai/chatkit/type-aliases/threaditemactionsoption/); ChatKit.js renders the controls and sends an `items.feedback` request when a user clicks them. + +```tsx +const chatkit = useChatKit({ + // ... + threadItemActions: { + feedback: true, + }, +}) +``` + +## Implement `add_feedback` on your server + +Override the `add_feedback` method on your server to persist the signal anywhere you like. + +```python +from chatkit.server import ChatKitServer +from chatkit.types import FeedbackKind + +class MyChatKitServer(ChatKitServer[RequestContext]): + async def add_feedback( + self, + thread_id: str, + item_ids: list[str], + feedback: FeedbackKind, + context: RequestContext, + ) -> None: + # Example: write to your analytics/QA store + await record_feedback( + thread_id=thread_id, + item_ids=item_ids, + sentiment=feedback, + user_id=context.user_id, + ) +``` + +`item_ids` can include assistant messages, tool calls, or widgets. If you need to ignore certain items (for example, hidden system prompts), filter them here before recording. diff --git a/docs/guides/add-features/show-progress-for-long-running-tools.md b/docs/guides/add-features/show-progress-for-long-running-tools.md new file mode 100644 index 0000000..5d8d3ea --- /dev/null +++ b/docs/guides/add-features/show-progress-for-long-running-tools.md @@ -0,0 +1,104 @@ +# Show progress for long-running tools + +Long-running tools can feel stalled without feedback. Use progress updates for lightweight status pings and workflows for structured, persisted task checklists. + +| | Progress updates (`ProgressUpdateEvent`) | Workflow items (`Workflow`, `WorkflowTask*`) | +|---------------------|------------------------------------------|----------------------------------------------| +| Purpose | Quick, ephemeral status text | Structured list of tasks with statuses | +| Persistence | Not saved to the thread | Persisted as thread items | +| UI | Inline, transient shimmer text | Collapsible checklist widget | +| When new content streams | Automatically cleared and replaced by streamed content | Remains visible above the streamed content | +| Best for | Reporting current phase ("Indexing…") | Multi-step plans users may revisit later | +| How to emit | `ctx.context.stream(ProgressUpdateEvent(...))` | `start_workflow`, `add_workflow_task`, `update_workflow_task`, `end_workflow` | + +## Progress updates + +Emit `ProgressUpdateEvent` when you need lightweight, real-time status. They stream immediately to the client and disappear after the turn—they are not stored in the thread. + +### From tools + +Inside a tool, use `AgentContext.stream` to enqueue progress events. They are delivered to the client immediately and are not persisted as thread items. + +```python +from agents import RunContextWrapper, function_tool +from chatkit.agents import AgentContext +from chatkit.types import ProgressUpdateEvent + +@function_tool() +async def ingest_files(ctx: RunContextWrapper[AgentContext], paths: list[str]): + await ctx.context.stream(ProgressUpdateEvent(icon="upload", text="Uploading...")) + await upload(paths) + + await ctx.context.stream( + ProgressUpdateEvent(icon="search", text="Indexing and chunking...") + ) + await index_files(paths) + + await ctx.context.stream(ProgressUpdateEvent(icon="check", text="Done")) +``` + +`stream_agent_response` will forward these events for you alongside any assistant text or tool call updates. + +### From custom pipelines + +If you are not using the Agents SDK, yield `ProgressUpdateEvent` directly from the `respond` or `action` methods while your backend works: + +```python +async def respond(...): + yield ProgressUpdateEvent(icon="search", text="Searching tickets...") + results = await search_tickets() + + yield ProgressUpdateEvent(icon="code", text="Generating summary...") + yield from await stream_summary(results) +``` + +Use short, action-oriented messages and throttle updates to meaningful stages instead of every percent to avoid noisy streams. + +## Workflow items + +Use workflows when you want a persisted, user-visible checklist of tasks. They render as a widget in the transcript and survive after the turn. Combine with progress updates if you need both a checklist and lightweight status text. + +Workflows support multiple task variants (custom, search, thought, file, image); see [`Task`](../../../api/chatkit/types/#chatkit.types.Task). Summaries shown when closing a workflow use [`WorkflowSummary`](../../../api/chatkit/types/#chatkit.types.WorkflowSummary) (for example, `CustomSummary` in the snippet below). + +Example streaming workflow updates using `AgentContext` helpers: + +```python +from agents import RunContextWrapper, function_tool +from chatkit.agents import AgentContext +from chatkit.types import CustomSummary, CustomTask, Workflow + +@function_tool() +async def long_running_tool_with_steps(ctx: RunContextWrapper[AgentContext]): + # Create an empty workflow container + await ctx.context.start_workflow(Workflow(type="custom", tasks=[])) + + # Add and update the first task + discovery = CustomTask(title="Search data sources", status_indicator="loading") + await ctx.context.add_workflow_task(discovery) + + # Run the first task + await search_my_data_sources() + + await ctx.context.update_workflow_task( + discovery.model_copy(update={"status_indicator": "complete"}), task_index=0 + ) + + # Add a follow-up task + summary = CustomTask(title="Summarize findings", status_indicator="loading") + await ctx.context.add_workflow_task(summary) + + # Run the second task + await summarize_my_findings() + + await ctx.context.update_workflow_task( + summary.model_copy(update={"status_indicator": "complete"}), task_index=1 + ) + + # Close the workflow and collapse it in the UI + await ctx.context.end_workflow( + summary=CustomSummary(title="Analysis complete"), + expanded=False, + ) +``` + +Workflows are saved as thread items by `stream_agent_response` when you yield the associated events; they show up for all participants and remain visible in history. diff --git a/docs/guides/stream-thread-events.md b/docs/guides/stream-thread-events.md index 6655418..263b647 100644 --- a/docs/guides/stream-thread-events.md +++ b/docs/guides/stream-thread-events.md @@ -34,43 +34,6 @@ async def long_running_tool(ctx: RunContextWrapper[AgentContext]): # Tool logic omitted for brevity ``` -Example streaming workflow updates using `AgentContext` helpers: - -```python -@function_tool() -async def long_running_tool_with_steps(ctx: RunContextWrapper[AgentContext]): - # Create an empty workflow container - await ctx.context.start_workflow(Workflow(type="custom", tasks=[])) - - # Add and update the first task - discovery = CustomTask(title="Search data sources", status_indicator="loading") - await ctx.context.add_workflow_task(discovery) - - # Run the first task - await search_my_data_sources() - - await ctx.context.update_workflow_task( - discovery.model_copy(update={"status_indicator": "complete"}), task_index=0 - ) - - # Add a follow-up task - summary = CustomTask(title="Summarize findings", status_indicator="loading") - await ctx.context.add_workflow_task(summary) - - # Run the second task - await summarize_my_findings() - - await ctx.context.update_workflow_task( - summary.model_copy(update={"status_indicator": "complete"}), task_index=1 - ) - - # Close the workflow and collapse it in the UI - await ctx.context.end_workflow( - summary=CustomSummary(title="Analysis complete"), - expanded=False, - ) -``` - ### Handle guardrail triggers Guardrail tripwires raise `InputGuardrailTripwireTriggered` or `OutputGuardrailTripwireTriggered` once partial assistant output has been rolled back. Catch them around `stream_agent_response` and optionally send a user-facing event so the client knows why the turn stopped. @@ -120,7 +83,11 @@ class MyChatKitServer(ChatKitServer[MyRequestContext]): When you stream thread events manually, remember that tools cannot `yield` events. If you skip `stream_agent_response`, you must merge any tool-emitted events yourself—for example, by reading from `AgentContext._events` (populated by `ctx.context.stream(...)` or workflow helpers) and interleaving them with your own `respond` events. -## Thread lifecycle events +## Event types at a glance + +Use these when emitting events directly (or alongside `stream_agent_response`). Thread lifecycle events become part of conversation history; the others are ephemeral runtime signals that shape client behavior but are not persisted. + +### Thread lifecycle events Thread item events drive the conversation state. ChatKitServer processes these events to persist conversation state before streaming them back to the client. @@ -132,7 +99,7 @@ Thread item events drive the conversation state. ChatKitServer processes these e 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 +### Errors Stream an `ErrorEvent` for user-facing errors. @@ -148,11 +115,19 @@ async def respond(...) -> AsyncIterator[ThreadStreamEvent]: # Rest of your respond method ``` -## Client effects +### Progress updates + +Stream `ProgressUpdateEvent` to show the user transient status while work is in flight. + +See [Show progress for long-running tools](add-features/show-progress-for-long-running-tools.md) for more info. + +### Client effects Use `ClientEffectEvent` to trigger fire-and-forget behavior on the client such as opening a dialog or pushing updates. -## Stream options +See [Send client effects](add-features/send-client-effects.md) for more info. + +### Stream options `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`. @@ -164,6 +139,10 @@ Add features: * [Accept attachments](add-features/accept-attachments.md) * [Make client tool calls](add-features/make-client-tool-calls.md) * [Send client effects](add-features/send-client-effects.md) +* [Show progress for long-running tools](add-features/show-progress-for-long-running-tools.md) * [Stream widgets](add-features/stream-widgets.md) * [Handle widget actionss](add-features/handle-widget-actions.md) -* [Create custom forms](add-features/create-custom-forms.md) \ No newline at end of file +* [Create custom forms](add-features/create-custom-forms.md) +* [Handle feedback](add-features/handle-feedback.md) +* [Allow @-mentions in user messages](add-features/allow-mentions.md) +* [Disable new messages for a thread](add-features/disable-new-messages.md) diff --git a/mkdocs.yml b/mkdocs.yml index c6985af..abfdc14 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,14 +50,14 @@ nav: - Accept attachments: guides/add-features/accept-attachments.md - Make client tool calls: guides/add-features/make-client-tool-calls.md - Send client effects: guides/add-features/send-client-effects.md + - Show progress for long-running tools: guides/add-features/show-progress-for-long-running-tools.md - Stream widgets: guides/add-features/stream-widgets.md - Handle widget actions: guides/add-features/handle-widget-actions.md - Create custom forms: guides/add-features/create-custom-forms.md - # - Allow @-mentions in user messages - # - Add citations and sources in assistant messages - # - Show progress for long-running tools - # - Lock threads - # - Handle feedback + - Handle feedback: guides/add-features/handle-feedback.md + - Allow @-mentions in user messages: guides/add-features/allow-mentions.md + - Disable new messages for a thread: guides/add-features/disable-new-messages.md + # - Add annotations in assistant messages - API Reference: - Overview: api/chatkit/index.md - Modules: From 2e1209a2c4f4957e7f783eceeb79655c9e38d036 Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Mon, 1 Dec 2025 22:37:15 -0800 Subject: [PATCH 2/2] Annotations guide --- docs/guides/add-features/add-annotations.md | 100 ++++++++++++++++++++ docs/guides/stream-thread-events.md | 1 + mkdocs.yml | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 docs/guides/add-features/add-annotations.md diff --git a/docs/guides/add-features/add-annotations.md b/docs/guides/add-features/add-annotations.md new file mode 100644 index 0000000..231d0ef --- /dev/null +++ b/docs/guides/add-features/add-annotations.md @@ -0,0 +1,100 @@ +# Add annotations in assistant messages + +ChatKit renders clickable inline citations when assistant text includes `annotations` and rolls every reference into a collapsed **Sources** list beneath each message. You can let the model emit annotations directly or attach sources yourself before streaming the message. + +## Use model-emitted citations + +When you stream a Responses run through `stream_agent_response`, ChatKit automatically converts any `file_citation`, `container_file_citation`, and `url_citation` annotations returned by the OpenAI API into ChatKit `Annotation` objects and attaches them to streamed message content. + +Provide the model with citable evidence via tools to receive citation annotations, most commonly: + +- `FileSearchTool` for uploaded documents (emits `file_citation` / `container_file_citation`) +- `WebSearchTool` for live URLs (emits `url_citation`) + +No additional server-side wiring is required beyond calling `stream_agent_response`. If the model emits citation annotations from tool usage, ChatKit will forward them automatically as `Annotation` objects on the corresponding content parts. + + +## Attach sources manually + +If you build assistant messages yourself, include annotations on each `AssistantMessageContent` item. + +```python +from datetime import datetime +from chatkit.types import ( + Annotation, + AssistantMessageContent, + AssistantMessageItem, + FileSource, + ThreadItemDoneEvent, + URLSource, +) + +text = "Quarterly revenue grew 12% year over year." +annotations = [ + Annotation( + source=FileSource(filename="q1_report.pdf", title="Q1 Report"), + index=len(text) - 1, # attach near the end of the sentence + ), + Annotation( + source=URLSource( + url="https://example.com/press-release", + title="Press release", + ), + index=len(text) - 1, + ), +] + +yield ThreadItemDoneEvent( + item=AssistantMessageItem( + id=self.store.generate_item_id("message", thread, context), + thread_id=thread.id, + created_at=datetime.now(), + content=[AssistantMessageContent(text=text, annotations=annotations)], + ) +) +``` + +`index` is the character position to place the footnote marker; re-use the same index when multiple citations support the same claim so the footnote numbers stay grouped. + +## Annotating with custom entities + +Inline annotations are not yet supported for entity sources, but you can still attach `EntitySource` items as annotations so they appear in the Sources list below the message. + +```python +from datetime import datetime +from chatkit.types import ( + Annotation, + AssistantMessageContent, + AssistantMessageItem, + EntitySource, + ThreadItemDoneEvent, +) + +annotations = [ + Annotation( + source=EntitySource( + id="customer_123", + title="ACME Corp", + description="Enterprise plan · 500 seats", + icon="suitcase", + data={"href": "https://crm.example.com/customers/123"}, + ) + ) +] + +yield ThreadItemDoneEvent( + item=AssistantMessageItem( + id=self.store.generate_item_id("message", thread, context), + thread_id=thread.id, + created_at=datetime.now(), + content=[ + AssistantMessageContent( + text="Here are the ACME account details for reference.", + annotations=annotations, + ) + ], + ) +) +``` + +Provide richer previews and navigation by handling [`entities.onRequestPreview`](https://openai.github.io/chatkit-js/api/openai/chatkit/type-aliases/entitiesoption/#onrequestpreview) and [`entities.onClick`](https://openai.github.io/chatkit-js/api/openai/chatkit/type-aliases/entitiesoption/#onclick) in ChatKit.js, using the `data` payload to pass entity information and deep link into your app. diff --git a/docs/guides/stream-thread-events.md b/docs/guides/stream-thread-events.md index 263b647..ce7ab9c 100644 --- a/docs/guides/stream-thread-events.md +++ b/docs/guides/stream-thread-events.md @@ -145,4 +145,5 @@ Add features: * [Create custom forms](add-features/create-custom-forms.md) * [Handle feedback](add-features/handle-feedback.md) * [Allow @-mentions in user messages](add-features/allow-mentions.md) +* [Add annotations in assistant messages](add-features/add-annotations.md) * [Disable new messages for a thread](add-features/disable-new-messages.md) diff --git a/mkdocs.yml b/mkdocs.yml index abfdc14..cd15fe7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,8 +56,8 @@ nav: - Create custom forms: guides/add-features/create-custom-forms.md - Handle feedback: guides/add-features/handle-feedback.md - Allow @-mentions in user messages: guides/add-features/allow-mentions.md + - Add annotations in assistant messages: guides/add-features/add-annotations.md - Disable new messages for a thread: guides/add-features/disable-new-messages.md - # - Add annotations in assistant messages - API Reference: - Overview: api/chatkit/index.md - Modules: