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
100 changes: 100 additions & 0 deletions docs/guides/add-features/add-annotations.md
Original file line number Diff line number Diff line change
@@ -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.
129 changes: 129 additions & 0 deletions docs/guides/add-features/allow-mentions.md
Original file line number Diff line number Diff line change
@@ -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=(
"<ARTICLE_TAG>\n"
f"ID: {tag.id}\n"
f"Title: {tag.text}\n"
f"Summary: {summary}\n"
"</ARTICLE_TAG>"
),
)
```


## 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:

```
- <ARTICLE_TAG>...</ARTICLE_TAG> 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).
50 changes: 50 additions & 0 deletions docs/guides/add-features/disable-new-messages.md
Original file line number Diff line number Diff line change
@@ -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
```

41 changes: 41 additions & 0 deletions docs/guides/add-features/handle-feedback.md
Original file line number Diff line number Diff line change
@@ -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.
Loading