From 095d621b03d76f630fcdfe7ca11dc81e28ddaf20 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 3 Nov 2026 11:25:46 +0000 Subject: [PATCH] docs(chats): describe thread context, the follow setting and connector options Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto --- docs/content/features/addons.md | 24 ++++++++++ docs/content/features/chats-connectors.md | 56 ++++++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/docs/content/features/addons.md b/docs/content/features/addons.md index 384ac4dbcb..7716d7834b 100644 --- a/docs/content/features/addons.md +++ b/docs/content/features/addons.md @@ -2228,6 +2228,8 @@ The manifest declares the capability. It needs no tool and no `service` flag: | `kinds[].page` | Optional. The add-on page that adds and edits bindings; it must be a declared page. The Connectors page links to it. | | `kinds[].drafts` | Optional. `true` when the add-on serves the op `draft`. The host then shows the live answer of a direct conversation as a draft instead of a working message. | | `kinds[].previewMs` | Optional. The shortest time between two previews (edits or drafts) of one binding, 250 to 10000 ms. Without it the host waits one second. | +| `kinds[].history` | Optional, manifest `api` 2. `{"thread": true, "maxMessages": N}`: the add-on serves the op `history` for threads, and returns at most `maxMessages` (1 to 200; without it the host asks for 30). A kind without it is never asked, so it keeps the behaviour of a platform that cannot give old messages. | +| `kinds[].options` | Optional, manifest `api` 2, at most 4. A choice that each binding of the kind makes: `{"id", "title", "description", "values": [{"id", "title"}], "default"}` with 2 to 8 values and a default among them. The host shows the choice on the binding page, checks the owner's value against the declaration, stores it, and sends the value in effect to the add-on in `options` of every `poll`. What the option means is the add-on's own rule. | **Outbound: the connector route.** The host calls `POST /connector` on the add-on with the same bearer credential and incarnation header as every other route and a body @@ -2240,6 +2242,28 @@ reference of a message's `media`; the answer is `{"file": {"name", "mime", "data with at most 1 MiB of data) and, for a kind that declares `drafts`, `draft` (`chat`, `thread`, `draftId`, `text`). +- **Options.** A `poll` carries `options`, a map from each declared option id to the + value in effect (the owner's choice, or the default). The host sends it with every + poll, so a change reaches the add-on within one poll. The shared route gives it to the + platform with `connector.OptionsFrom(ctx)`. The Slack add-on uses it for `followThreads` + (`own`, `any` or `never`): the add-on decides which messages are `mention`, so the host + only forwards what the add-on marks as addressed to the bot. +- **The op `history`.** For a kind that declares `history`, the host calls + `{"op": "history", "chat", "thread", "limit", "before"}` when a message in a thread + makes a new chat, at most once for that chat. `limit` is the most messages to return + (never more than `maxMessages`); `before` is the ID of the message the host handles + now, which is not returned. The answer is `{"messages": [...], "truncated": bool}`, oldest + first, the newest `limit` messages of the thread. A message is `{"messageId", + "senderId", "senderName", "senderUsername", "text", "ts", "own", "media", + "mediaNames"}`: `own` marks a message of the bot, `media` is the number of files and + `mediaNames` at most 8 names; the bytes of a file are never part of a history. + `truncated` says that older messages exist. The text is what people wrote: the host + treats it as untrusted, flattens it, bounds it (30 messages and 8 KiB by default), + and gives it to the first turn only. A failure or a `rate_limited` answer is + logged and the turn goes on without the context; an add-on does not wait out a rate + limit in this op. The add-on should read only the thread it is asked for. Core knows no + platform: Slack's `conversations.replies` is the add-on's own business. The shared route + serves the op for a platform that implements `connector.HistoryReader`. - A `send` with a `messageId` replaces that message of the bot (the host's working message) with the first part of the text and sends the rest as new messages. When the message cannot be edited (it was deleted, or is too old), the first part goes diff --git a/docs/content/features/chats-connectors.md b/docs/content/features/chats-connectors.md index 7fd502fca2..d6dcc98ee8 100644 --- a/docs/content/features/chats-connectors.md +++ b/docs/content/features/chats-connectors.md @@ -173,8 +173,28 @@ curl -X POST .../api/chats/connectors -d '{"kind": "slack", "name": "Team app", - **A direct message** with the app is a conversation with its sender, like a Telegram chat. A reply in a thread of the direct message is a conversation of that thread. - **A mention** of the app in a channel is answered in the thread of that message. The - thread is one chat, shared by everyone who writes in it, and the app follows it: - a later reply in that thread reaches the agent without a new mention (for 24 hours). + thread is one chat, shared by everyone who writes in it. Which threads the app then + follows, so that a later reply reaches the agent without a new mention, is the + setting **Threads the bot follows** (`followThreads` in `options`): + - `own` (the default): the app follows the threads it started, that is, a mention + outside a thread. A mention inside a thread that other people started is answered, + and the thread is not followed, so the app does not answer what people write to + each other. + - `any`: the app also follows a thread that other people started, once it is + mentioned in it. + - `never`: every message needs a mention. + + A followed thread stays followed for 24 hours after its last message. The add-on keeps + the list in its data directory, so a restart of LocalAI or a new Socket Mode link does + not make the app forget its threads. The file holds channel IDs and message + timestamps only, never a token. The app does not call Slack for a message to find + out whether it follows the thread. +- **Earlier messages of the thread.** When a mention starts a new chat in a thread, the + app reads the earlier messages of that thread once (see + [Earlier messages of a thread](#earlier-messages-of-a-thread)). This needs the scopes + `channels:history`, `groups:history`, `im:history` and `mpim:history`, which the + manifest of the add-on page asks for. An app made with fewer scopes answers without + that context; add the scopes and reinstall the app. - **Other channel messages** are not read. - **Answers** are converted from Markdown to Slack's `mrkdwn` (bold, italics, links, lists, code blocks) and split at 4,000 characters, the limit Slack asks for. A turn @@ -274,6 +294,33 @@ gives the same events without one. the server logs it. The binding stays on, so the agent answers again once you restore the folder or choose another one. +### Earlier messages of a thread + +When the bot is mentioned in a thread, the people there often have talked for a while. +For a platform that can give them (Slack; not Telegram, because a bot cannot read the +history of a chat), LocalAI asks the add-on for the earlier messages of that thread when +the mention makes a new chat, and gives them to the agent with the first message: + +- **Only that thread.** The bot reads the messages of the thread in which it is + mentioned, nothing else: not the channel, not other threads, not files. For a file it + sees the name and the count. A mention outside a thread reads nothing. +- **Once.** The messages are read when the chat of the thread starts. Later turns of the + chat do not read them again, and they are not copied into the transcript. +- **Bounded.** At most 30 messages and 8 KiB of text. If there are more, the newest are + kept, oldest first, one line `Name: text` each. The add-on can lower the number of + messages. A thread of more than 1,000 messages is not read at all. +- **Untrusted.** The agent gets the lines in a block that says they are messages from + other people and not instructions. Control characters and line breaks are removed and + the characters `<` and `>` are replaced, so a message cannot close the block. Messages + of the bot itself are included, so the agent sees its earlier answers. +- **It never blocks the answer.** If the platform refuses, is slow or limits the calls, + the agent answers without the context. The server logs the reason once. + +To switch it off for a binding, turn off **Read the earlier messages of a thread** under +**How it answers**, or send `"thread_context": false`. An update without `thread_context` +keeps the stored value. The setting has no effect on a platform that cannot give +earlier messages. + ## How the bot answers Each binding has three settings, under **How it answers** on the add-on page and in @@ -319,6 +366,11 @@ that part. | `window` | As `sender`, but a message that comes `window_minutes` (1 to 10,080) or more after the same person's previous message starts a new chat. A thread keeps its chat. | | `thread` | Each message starts a new chat. A reply to an answer of the bot, or to the message that started a chat, goes on in that chat; on Slack, so does a thread under either one. | +Some add-ons add settings of their own to a binding (for example Slack's **Threads the +bot follows**). They appear under **How it answers**, and in the API as `options`, a +map from the option id to a value. The add-on declares the allowed values, and an +unknown option or value is a 400. An update without `options` keeps the stored values. + ### Files the agent makes (`send_files`) On by default. After the answer, the files that the turn wrote or changed go to the