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)

getUpdates long polling, or a webhook

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: sendMessageDraft animates a preview, then sendMessage persists the answer

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); sendChatAction for typing

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

team_id, user and channel plus thread_ts

chat.id, from.id and optionally message_thread_id

chat.user.name, space.name and thread.name

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).

sequenceDiagram participant U as "User" participant P as "Platform" participant A as "Channel adapter" participant C as "DocsAssistant core" U->>P: "Question" P->>A: "Event (message, update or interaction)" A-->>P: "Acknowledge quickly" A->>C: "ask(conversationId, userId, text)" loop "Token events" C-->>A: "Token" A-->>P: "Stream chunk, draft or nothing yet" end C-->>A: "Citation and Done" A->>P: "Final message with source links" P-->>U: "Answer"

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

slack:<team_id>:<user_id> (user IDs are unique per workspace, so include the team; enterprise grids add their own enterprise ID)

slack:<team_id>:<channel_id>:<thread_ts>; in the Assistant side panel the thread is the assistant thread

Telegram

telegram:<from.id>

telegram:<chat.id> (private chats), with :<message_thread_id> for topics

Google Chat

gchat:<user resource name> (for example users/<id>)

gchat:<space resource name>:<thread resource name>

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

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

chat_id

yes

The target private chat.

draft_id

yes

A non-zero identifier of the draft. Calls with the same draft_id animate the changes; a different one replaces the draft without animation.

text

no

0 to 4096 characters after entity parsing; an empty text shows a "Thinking…​" placeholder.

message_thread_id, parse_mode, entities

no

The target topic and the formatting of the text.

can_stop

no

Shows the user a button to stop further drafts; the bot then receives an update stopped_message_generation (a MessageGenerationStopped object).

keep_on_stop

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()

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()

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