Slack, Telegram & Google Chat
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written on 2026-10-01 against Slack Bolt for Python (slack-bolt 1.30.0 with slack-sdk 3.44.1), Bolt for
JavaScript (@slack/bolt 5.1.0) and Bolt for Java 1.52.0; python-telegram-bot 22.8 and TelegramBots for Java 10.3.0
(telegrambots-longpolling and the matching client and meta artifacts) against Telegram Bot API 10.3; and the current
Google Chat and Google Workspace add-ons documentation, with the LangChain 1.4.3 core on Python and the Spring AI 2.0.1
core on Spring Boot 4.1.1 (see the version baseline). The
snippets are illustrative: they were written from the official documentation and are not compiled or run by this
repository. What was checked: the Telegram Bot API page was read for the sendMessageDraft parameters, the
python-telegram-bot source for Bot.send_message_draft, and the TelegramBots 10.3.0 meta jar for a SendMessageDraft
method class; the Slack and Google pages were read for the concepts below. What was not checked: every Slack and Google
method name in the Java and JavaScript snippets, which you should compare with the linked pages before relying on them.
Platform policies and plan requirements change often; statements about them were read on 2026-10-01.
Slack, Telegram and Google Chat are the three mainstream chat channels where a RAG assistant can be installed with a
self-service developer console and no business verification. They are also the three that differ most in how an answer
is displayed while it is being produced, which is why they share a page: each one is an adapter around the same
ask(conversationId, userId, text) core described in
Channel-Agnostic Architecture, and each one maps the
core’s Token events onto a different native mechanism. The back-end details of the core are in
LangChain: Streaming and
Spring AI: Streaming & Reactive.
Three channels, three streaming models
| Slack | Telegram | Google Chat | |
|---|---|---|---|
Where the assistant lives |
A Slack app: the app’s Messages tab (agent view), channels via mentions, optionally the older side-panel Assistant |
A bot created with BotFather; the streaming method works in private chats |
A Chat app configured as a Workspace add-on, in direct messages and spaces |
How events arrive |
Events API over HTTPS, or Socket Mode (a WebSocket the app opens) |
|
An HTTPS endpoint (or Apps Script, or Cloud Run and similar) receives an event per interaction |
Native token streaming |
Yes: a stream is started, appended to and stopped; Bolt wraps this in helpers |
Yes, as an ephemeral draft: |
No native token stream; reply once synchronously, or send and update messages through the Chat API |
Progress indication |
Assistant status ("is thinking") with optional rotating messages |
The draft itself (an empty text shows a placeholder); |
A first message that is updated, or the synchronous acknowledgement |
Time budget per request |
Acknowledge the event quickly (three seconds for most Slack payloads); do the work after acknowledging |
Webhook calls should return promptly; long polling has no response deadline per update |
30 seconds to respond; after that acknowledge with an empty response and post through the Chat API |
Identity keys |
|
|
|
The consequence for the adapter is the capability negotiation from the architecture page: the Slack adapter streams tokens into a Slack stream, the Telegram adapter throttles drafts and finishes with a normal message, and the Google Chat adapter usually posts one finished message (or a placeholder that is later edited).
Identity and conversation mapping
All three platforms hand the adapter stable identifiers, but they are not the same shape, and the mapping decides who
sees which documents and which memory is reused. The rules follow the architecture page: derive userId from the
platform’s user identifier inside a workspace or bot (never from display names), and derive conversationId from the
thread or chat, so a Postgres checkpointer or JDBC chat memory keeps the history of that thread only.
| Platform | userId |
conversationId |
|---|---|---|
Slack |
|
|
Telegram |
|
|
Google Chat |
|
|
Mapping a platform user to a document access identity is a separate step: a Slack or Telegram ID proves nothing about which documents the person may read, so link accounts with a sign-in flow (Authorization Code with PKCE) or restrict these channels to public documentation, as discussed in Access Control & Privacy.
Slack
Slack’s current documentation for AI apps lives in the Bolt guides (the older "AI apps" URL redirects to the Bolt for
JavaScript page on adding agent features). Two experiences exist. In the agent view the conversation happens in the
app’s Messages tab and arrives as ordinary message and app_mention events with thread information; Slack’s page
describes this as the default experience for apps going forward. The older Assistant class models the side-panel
experience with three events: assistant_thread_started (the user opened a new thread, a good moment to set suggested
prompts), assistant_thread_context_changed (the user moved to another channel) and ordinary user messages in the
assistant thread. For streaming, Slack offers three Web API methods, chat.startStream, chat.appendStream and
chat.stopStream, and the Bolt and Slack SDK libraries wrap them in helpers. The required scopes include
assistant:write, chat:write and im:history in the Java guide’s example; check the scopes list of the page for your
language and surface, because it differs by feature. Whether a particular workspace plan can install AI apps is a Slack
account matter and was not verified here.
Python: Bolt Assistant with a streamed answer
The adapter below uses the Assistant class, calls the Python core from the architecture page and streams tokens with
the chat_stream() helper of the Slack SDK, passing the team and user so Slack can address the stream. Tokens and the
signing secret come from environment variables; the values shown are placeholders.
import asyncio
import os
from slack_bolt import App, Assistant
from slack_bolt.adapter.socket_mode import SocketModeHandler
from docsassistant.core import Citation, Done, Token # see Channel-Agnostic Architecture
app = App(
token=os.environ["SLACK_BOT_TOKEN"], # placeholder: xoxb-... bot token
signing_secret=os.environ["SLACK_SIGNING_SECRET"], # placeholder: from the app's Basic Information page
)
assistant = Assistant()
core = build_core() # DocsAssistantCore, built once at startup
@assistant.thread_started
def start_thread(say, set_suggested_prompts):
say("Hi, ask me anything about the documentation.")
set_suggested_prompts(prompts=[
{"title": "Install", "message": "How do I install the library?"},
{"title": "Release notes", "message": "What changed in the latest release?"},
])
@assistant.user_message
def answer(payload, context, client, set_status, set_title):
team_id, user_id = context.team_id, payload["user"]
channel_id, thread_ts = payload["channel"], payload["thread_ts"]
conversation_id = f"slack:{team_id}:{channel_id}:{thread_ts}"
set_status(status="is searching the docs...")
streamer = client.chat_stream(
channel=channel_id,
thread_ts=thread_ts,
recipient_team_id=team_id,
recipient_user_id=user_id,
)
async def run() -> list[Citation]:
sources: list[Citation] = []
async for event in core.ask(conversation_id, f"slack:{team_id}:{user_id}", payload["text"]):
if isinstance(event, Token):
streamer.append(markdown_text=event.text)
elif isinstance(event, Citation):
sources.append(event)
elif isinstance(event, Done):
break
return sources
sources = asyncio.run(run()) # Bolt's sync listeners run in pool threads with no running loop
footer = "\n\n" + "\n".join(f"[{c.marker}] <{c.url}|{c.title}>" for c in sources)
streamer.append(markdown_text=footer)
streamer.stop(blocks=[feedback_block()]) # thumbs up / down, see below
app.use(assistant)
if __name__ == "__main__":
SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() # placeholder: xapp-... app-level token
Source: Python Slack SDK: Web client (message streaming helpers) and Bolt for Python: Adding agent features.
Several choices in this adapter are deliberate. Socket Mode keeps development simple because no public URL is needed;
for production the HTTP Events API with signature verification is the usual choice, and Bolt verifies the signature
itself when given the signing secret. Each listener call drives the async core with its own asyncio.run(), which is
safe because Bolt’s sync listeners run on pool threads with no running event loop; if the core keeps loop-bound
clients, use Bolt’s AsyncApp and AsyncAssistant instead and await the core directly. The status message ("is
searching the docs…") disappears when the app replies.
feedback_block() is a hypothetical helper returning the feedback buttons Block Kit element, which Slack’s agent
guide shows attached on stop so that users can rate answers; connect the click to the feedback loop described in
Observability & Tracing. Citations are rendered as mrkdwn links, the
Slack idiom; Markdown links from the model would not render the same way.
JavaScript: Bolt events with sayStream
In Bolt for JavaScript the same adapter is a listener for messages in the app’s Messages tab. The guide exposes a
sayStream utility to listeners for app.event and app.message, and setStatus for the progress line.
import { App } from "@slack/bolt";
const app = new App({
token: process.env.SLACK_BOT_TOKEN, // placeholder: xoxb-... bot token
signingSecret: process.env.SLACK_SIGNING_SECRET, // placeholder
});
app.message(async ({ message, sayStream, setStatus }) => {
if (!("text" in message) || message.subtype) return;
await setStatus({ status: "Searching the docs..." });
const streamer = sayStream();
// docsAssistant.ask is a thin client for the shared core, for example the /v1/ask SSE API
for await (const event of docsAssistant.ask(message.thread_ts ?? message.ts, message.user, message.text)) {
if (event.type === "token") {
await streamer.append({ markdown_text: event.text });
}
}
await streamer.stop();
});
await app.start(Number(process.env.PORT ?? 3000));
A TypeScript adapter does not have to embed the core: calling the SSE endpoint built in
Serving RAG as an API keeps a single implementation, and the adapter
only converts events. The for await loop above hides that call behind docsAssistant.ask, a hypothetical client.
Java: Bolt for Java Assistant
Bolt for Java documents the Assistant class with handlers for the thread start and user message events. The sample
below follows that documentation and hands the text to the Spring AI core; it posts the complete answer once, because
no streaming helper for Java was found in the page read for this article. Slack’s Web API streaming methods can be
called through the generic client when you need them.
import com.slack.api.bolt.App;
import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.middleware.builtin.Assistant;
import com.slack.api.model.assistant.SuggestedPrompt;
import java.util.Collections;
AppConfig config = AppConfig.builder()
.singleTeamBotToken(System.getenv("SLACK_BOT_TOKEN")) // placeholder: xoxb-... bot token
.signingSecret(System.getenv("SLACK_SIGNING_SECRET")) // placeholder
.build();
App app = new App(config);
Assistant assistant = new Assistant(app.executorService());
assistant.threadStarted((req, ctx) -> {
try {
ctx.say("Hi, ask me anything about the documentation.");
ctx.setSuggestedPrompts(Collections.singletonList(
SuggestedPrompt.create("How do I install the library?")));
} catch (Exception e) {
ctx.logger.error("Failed to handle assistant thread started event", e);
}
});
assistant.userMessage((req, ctx) -> {
try {
ctx.setStatus("is searching the docs...");
var event = req.getEvent();
String conversationId = "slack:" + ctx.getTeamId() + ":" + event.getChannel() + ":" + event.getThreadTs();
String answer = core.ask(conversationId, "slack:" + ctx.getTeamId() + ":" + event.getUser(), event.getText())
.collectList()
.map(Slack::renderMarkdown) // join tokens, append the source links
.block();
ctx.say(answer);
} catch (Exception e) {
ctx.logger.error("Failed to answer", e);
}
});
app.assistant(assistant);
Source: Bolt for Java: AI apps. Slack::renderMarkdown is a
hypothetical helper of the adapter. core is the AssistantCore of the architecture page. The exact Assistant
constructor, the event accessors on req and ctx.getTeamId() were not verified against the library.
Blocking inside a listener is acceptable here because Bolt for Java runs listeners on its own executor after the event has been acknowledged, but a slow answer still ties up a thread. For heavy traffic, move the work to a queue as shown in the architecture page.
Telegram
Telegram bots use the Bot API over HTTPS. A bot is created with BotFather, which hands out the token that must be kept
in a secret store (never in the repository), and updates are received either by long polling (getUpdates) or by a
webhook that Telegram calls. For streaming, Bot API 10.3 documents the sendMessageDraft method, which streams a
partial message to a user while the message is being generated. The parameters read on 2026-10-01:
| Parameter | Required | Meaning |
|---|---|---|
|
yes |
The target private chat. |
|
yes |
A non-zero identifier of the draft. Calls with the same |
|
no |
0 to 4096 characters after entity parsing; an empty text shows a "Thinking…" placeholder. |
|
no |
The target topic and the formatting of the text. |
|
no |
Shows the user a button to stop further drafts; the bot then receives an update |
|
no |
Keeps the draft visible after the stop button is pressed; to preserve it fully the bot should send it as a normal message. |
The draft is ephemeral: the documentation describes it as a temporary 30-second preview, and the final text must be
sent with sendMessage to persist it in the chat. A sendRichMessageDraft method exists for rich messages with the
same stop parameters. Because the draft is only a preview, an adapter must send the final message even if streaming
failed halfway. The method targets private chats, so group chats still need the edit-based pattern of the
architecture page.
Python: python-telegram-bot
python-telegram-bot 22.8 exposes Bot.send_message_draft(chat_id, draft_id, text=None, message_thread_id=None,
parse_mode=…, entities=None); its source says the method was added in 22.6 and opened to all bots in 22.7. The
handler below throttles draft updates so that the adapter does not exceed Telegram’s flood limits and finishes with a
plain message.
import os
import time
from telegram import Update
from telegram.ext import Application, ContextTypes, MessageHandler, filters
from docsassistant.core import Citation, Done, Token
UPDATE_INTERVAL = 0.5 # seconds between draft updates; tune against Telegram's rate limits
async def on_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
user = update.effective_user
core = context.application.bot_data["core"]
draft_id = update.update_id or 1 # any non-zero identifier unique to this answer
await context.bot.send_message_draft(chat_id=chat.id, draft_id=draft_id) # empty text = thinking placeholder
text, sources, last_push = "", [], 0.0
async for event in core.ask(f"telegram:{chat.id}", f"telegram:{user.id}", update.effective_message.text):
if isinstance(event, Token):
text += event.text
if time.monotonic() - last_push >= UPDATE_INTERVAL:
await context.bot.send_message_draft(chat_id=chat.id, draft_id=draft_id, text=text[:4096])
last_push = time.monotonic()
elif isinstance(event, Citation):
sources.append(event)
elif isinstance(event, Done):
break
links = "\n".join(f"[{c.marker}] {c.title}: {c.url}" for c in sources)
await context.bot.send_message(chat_id=chat.id, text=(text + "\n\n" + links)[:4096]) # persists the answer
def main() -> None:
application = (Application.builder()
.token(os.environ["TELEGRAM_BOT_TOKEN"]) # placeholder: from BotFather
.concurrent_updates(True) # one chat's long answer must not block the others
.build())
application.bot_data["core"] = build_core()
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_text))
application.run_polling()
if __name__ == "__main__":
main()
Source: Telegram Bot API: sendMessageDraft, python-telegram-bot documentation and the library repository.
Longer answers than 4096 characters must be split into several messages; the adapter above truncates for brevity. To
offer the stop button, pass can_stop=True (a keyword the library follows from the Bot API; verify it against your
installed version) and handle the stopped_message_generation update by cancelling the running ask call, which for
the LangChain core means cancelling the task that iterates the stream.
Java: TelegramBots
The TelegramBots library 10.3.0 splits into a meta module with the API object classes, a client module and
long-polling and webhook modules. The meta jar contains a SendMessageDraft method class whose builder has the fields
chatId, messageThreadId, draftId, text, parseMode, entities, canStop and keepOnStop, matching the Bot
API. The sketch uses the long-polling consumer and a blocking subscription to the Spring AI core.
import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient;
import org.telegram.telegrambots.longpolling.TelegramBotsLongPollingApplication;
import org.telegram.telegrambots.longpolling.util.LongPollingSingleThreadUpdateConsumer;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.methods.send.SendMessageDraft;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.generics.TelegramClient;
public class DocsTelegramBot implements LongPollingSingleThreadUpdateConsumer {
private final TelegramClient client;
private final AssistantCore core;
DocsTelegramBot(String token, AssistantCore core) {
this.client = new OkHttpTelegramClient(token);
this.core = core;
}
@Override
public void consume(Update update) {
if (update.getMessage() == null || !update.getMessage().hasText()) {
return;
}
long chatId = update.getMessage().getChatId();
long userId = update.getMessage().getFrom().getId();
int draftId = update.getUpdateId(); // any non-zero id unique to this answer
var text = new StringBuilder();
core.ask("telegram:" + chatId, "telegram:" + userId, update.getMessage().getText())
.filter(e -> e instanceof CoreEvent.Token)
.sample(java.time.Duration.ofMillis(500)) // throttle drafts
.doOnNext(e -> {
text.setLength(0);
text.append(Telegram.joinTokensSoFar(e)); // hypothetical helper
send(SendMessageDraft.builder().chatId(chatId).draftId(draftId).text(text.toString()).build());
})
.blockLast();
send(SendMessage.builder().chatId(chatId).text(text.toString()).build());
}
private void send(org.telegram.telegrambots.meta.api.methods.botapimethods.BotApiMethod<?> method) {
try {
client.execute(method);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public static void main(String[] args) throws Exception {
String token = System.getenv("TELEGRAM_BOT_TOKEN"); // placeholder: from BotFather
try (var app = new TelegramBotsLongPollingApplication()) {
app.registerBot(token, new DocsTelegramBot(token, buildCore()));
Thread.currentThread().join();
}
}
}
Source: TelegramBots repository and wiki and
Telegram Bot API: sendMessageDraft. The bot registration call
signature and Telegram.joinTokensSoFar (a hypothetical accumulator) are sketches; the meta jar was inspected only to
confirm that SendMessageDraft exists, not compiled against.
In a Spring Boot application, prefer the starter or webhook module and register the bot as a bean so the application
lifecycle stops polling cleanly. Blocking in consume is acceptable with the single-thread consumer for demos; the
multi-threaded consumer variants of the library are better for production.
Google Chat
Google Chat apps are now built as Google Workspace add-ons: a Chat app is an add-on that has the Chat extension configured, and the platform sends the app an interaction event for each message, mention or button click. The developer documentation covers three hosting shapes: an Apps Script project, an HTTP endpoint you host (Cloud Run, Cloud Functions or any server), and other event-driven deployments. Two rules shape the adapter.
-
An event must be answered within 30 seconds. A synchronous reply is a message object (text, cards). For longer work the app acknowledges within 30 seconds with an empty response and then calls the Google Chat API to create (and later update) messages. The synchronous reply needs no authentication; asynchronous calls need the app’s own credentials, typically a service account.
-
There is no token streaming. A RAG answer is therefore either a single message, or a short "searching…" message that is later updated through the Chat API, in the same spirit as the edit-based streaming of the architecture page.
In add-on events the payload carries the user in chat.user, and the message and space under chat.messagePayload.
HTTP endpoints must verify that requests come from Google by checking the bearer token on each call; follow the
authentication page of the documentation rather than a hand-rolled check.
A Python HTTP endpoint
The endpoint below answers the event after the work is done when it is fast enough, and uses the asynchronous path otherwise. It reads the Chat API credentials from a service-account file named by an environment variable.
import os
from fastapi import BackgroundTasks, FastAPI, Request
from google.oauth2 import service_account
from googleapiclient.discovery import build
from docsassistant.core import Citation, Done, Token
app = FastAPI()
core = build_core()
credentials = service_account.Credentials.from_service_account_file(
os.environ["GOOGLE_APPLICATION_CREDENTIALS"], # placeholder: path to the app's service-account key file
scopes=["https://www.googleapis.com/auth/chat.bot"],
)
chat_api = build("chat", "v1", credentials=credentials)
@app.post("/google-chat")
async def receive(request: Request, background: BackgroundTasks):
verify_google_bearer_token(request) # see the Chat app authentication docs
event = await request.json()
payload = event["chat"]["messagePayload"]
message = payload["message"]
background.add_task(
answer,
space=payload["space"]["name"],
thread=message["thread"]["name"],
user=event["chat"]["user"]["name"],
text=message.get("argumentText") or message["text"],
)
return {} # empty acknowledgement within 30 seconds
async def answer(space: str, thread: str, user: str, text: str) -> None:
body, sources = "", []
async for ev in core.ask(f"gchat:{space}:{thread}", f"gchat:{user}", text):
if isinstance(ev, Token):
body += ev.text
elif isinstance(ev, Citation):
sources.append(ev)
links = "\n".join(f"[{c.marker}] <{c.url}|{c.title}>" for c in sources)
chat_api.spaces().messages().create(
parent=space,
body={"text": f"{body}\n\n{links}", "thread": {"name": thread}},
messageReplyOption="REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD",
).execute()
Source: Build Google Chat apps as add-ons, Chat app events and responses and Chat API: spaces.messages.create.
verify_google_bearer_token is a placeholder for the verification the documentation describes. The google-api-python-client
call is synchronous, so in production run it in a thread pool or use the async client; the messageReplyOption
value, the exact event field names for your add-on configuration and the Chat text format (which uses its own
formatting syntax for links) should be checked against the linked pages.
ADK and A2A quickstarts
Google publishes quickstarts that connect a Chat app to an agent instead of to your own core. The ADK quickstart builds a Chat app that talks to an agent built with the Agent Development Kit and hosted on Vertex AI Agent Engine; the Chat app, written in Apps Script in the quickstart or as a web server, stores the agent’s resource name and a service account key in its configuration, calls the agent, and posts the progress and the result asynchronously through the Chat API. The A2A quickstart does the same with an Agent2Agent protocol agent (the sample is an ADK multi-agent auditor deployed to Agent Engine). A third quickstart targets a Gemini Enterprise agent. Prerequisites in the pages read: a Google Workspace business or enterprise account, a Google Cloud project with billing and a service account with the Vertex AI User role.
For DocsAssistant, the A2A route is the interesting one: the core can be exposed as an A2A server (A2A is not covered elsewhere in this guide; see the quickstart and the A2A specification), and the Chat adapter then only needs to be an A2A client. That keeps the Chat adapter a thin translator, but adds a network hop and a second identity (the service account) that must be allowed to call the agent. The credentials in the quickstart are a service account key; prefer workload identity or short-lived credentials where your platform supports them, and keep keys out of the repository.
Choosing between the three
-
Choose Slack when the audience is already in a Slack workspace and wants answers in threads with citations and feedback buttons; the streaming helpers make this the closest to a web chat, and the cost is app review and workspace admin installs.
-
Choose Telegram for public or community assistants and personal use; it needs the least setup, streaming works in private chats, and the draft model keeps the persisted history clean. Group chats and compliance-heavy contexts are weaker fits.
-
Choose Google Chat when the organization runs on Google Workspace; plan for one finished message per answer, the 30-second rule and a service account for asynchronous replies.
For secrets, signature checks, retries and rate limits shared by all channels see Operating Chat Channels. For Microsoft’s equivalent see Microsoft Teams, and for the other messaging channels see WhatsApp Business.
References
-
Slack, Adding agent features (Bolt for JavaScript): https://docs.slack.dev/tools/bolt-js/concepts/adding-agent-features/
-
Slack, Using the Assistant class (Bolt for JavaScript): https://docs.slack.dev/tools/bolt-js/concepts/using-the-assistant-class
-
Slack, Adding agent features (Bolt for Python): https://docs.slack.dev/tools/bolt-python/concepts/adding-agent-features/
-
Slack, Python SDK web client: https://docs.slack.dev/tools/python-slack-sdk/web/
-
Slack, Bolt for Java AI apps: https://docs.slack.dev/tools/java-slack-sdk/guides/ai-apps/
-
Telegram Bot API: https://core.telegram.org/bots/api
-
Telegram Bot API changelog: https://core.telegram.org/bots/api-changelog
-
python-telegram-bot documentation: https://docs.python-telegram-bot.org/
-
TelegramBots for Java: https://github.com/rubenlagus/TelegramBots
-
Google Chat developer documentation: https://developers.google.com/workspace/chat
-
Build a Google Chat app with an ADK AI agent: https://developers.google.com/workspace/add-ons/chat/quickstart-adk-agent
-
Build a Google Chat app with an Agent2Agent agent: https://developers.google.com/workspace/add-ons/chat/quickstart-a2a-agent
-
Build a Google Chat app with a Gemini Enterprise AI agent: https://developers.google.com/workspace/add-ons/chat/quickstart-ge-agent
-
Google Chat app events and responses: https://developers.google.com/workspace/chat/api/guides/message-formats/events