Skip to content
BotServBotServ
SlackBotKI-BotOllamaBoltSocket ModeTeam-KI

Slack-Bot mit lokaler KI

Slack-Bot mit Ollama bauen: Bolt-Framework, Socket Mode, Slash-Commands, Threads und Erweiterungen für Team-Assistenten.

S

schutzgeist

6 min read
Slack-Bot mit lokaler KI

Slack-Bot mit lokaler KI

Was dieser Artikel behandelt

  • Wie Du eine Slack-App mit dem Bolt-Framework und Ollama baust.
  • Socket Mode vs. HTTP-Events, warum Socket Mode für Self-Hoster ideal ist.
  • Slash-Commands, Mentions, Thread-Kontext und interaktive Blöcke.
  • Erweiterungen: RAG, Datei-Uploads, Mehrbenutzer-Kontext.
  • Deployment und Sicherheit im Unternehmensumfeld.

Einleitung

Slack ist der Standard in Tech-Teams und hat eine hervorragende offizielle Bot-API. Slack-Apps können Slash-Commands, Mentions, Buttons, Modals und Threads. Der Clou für Self-Hoster: Socket Mode verbindet den Bot per WebSocket zu Slack, kein öffentlicher Endpoint, kein Reverse Proxy, keine Firewall-Löcher nötig.

Mit Ollama dahinter wird der Slack-Bot zum internen Team-Assistenten: Firmenwissen per RAG, Code-Fragen, Zusammenfassungen, ohne dass Slack-Inhalte an KI-Clouds gehen (Slack selbst sieht sie natürlich trotzdem).

Typische Anwendungsszenarien

  • Interner Wissensbot: „Wie beantrage ich Urlaub?” → Bot antwortet aus dem Wiki per RAG.
  • DevOps-Assistent: „/ki warum schlägt der Build fehl?” → Log-Ausschnitt analysieren lassen.
  • Onboarding-Helfer: Neue Mitarbeiter fragen den Bot statt Kollegen.
  • Code-Review-Bot: Diff hochladen, Review-Kommentare von lokalem Coding-Modell.
  • Meeting-Zusammenfassungen: Thread-Inhalte zusammenfassen lassen.
  • IT-Helpdesk-First-Level: Häufige Fragen abfangen, Rest eskalieren.

Voraussetzungen

  • Slack-Workspace (Free reicht, aber Achtung: Nachrichten-Verlauf im Free-Plan nur 90 Tage)
  • Admin-Rechte im Workspace (oder Freigabe zum App-Installieren)
  • Server mit Ollama + Python 3.10+

Schritt 1: Slack-App erstellen

Auf api.slack.com/apps → „Create New App” → „From scratch”:

  1. Socket Mode aktivieren (Settings → Socket Mode → Enable) → App-Level-Token xapp-... generieren (Scope: connections:write)
  2. Bot Token Scopes (OAuth & Permissions): app_mentions:read, chat:write, commands, channels:history, im:history, files:read
  3. Slash Command anlegen: /ki (Request URL egal bei Socket Mode, kurzer Dummy)
  4. Event Subscriptions: app_mention, message.im abonnieren
  5. Install to Workspace → Bot-Token xoxb-... kopieren

Zwei Tokens: xapp- (App-Level, Socket Mode) + xoxb- (Bot-Token). Beide in .env:

SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b

Schritt 2: Bot mit Bolt (Python)

pip install slack-bolt ollama python-dotenv

bot.py:

import os
import ollama
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

load_dotenv()

app = App(token=os.environ["SLACK_BOT_TOKEN"])
ollama_client = ollama.Client(host=os.environ["OLLAMA_URL"])
MODEL = os.environ.get("OLLAMA_MODEL", "llama3.1:8b")

SYSTEM = ("Du bist der interne KI-Assistent. Antworte kurz, präzise, "
          "auf Deutsch. Bei Code: Markdown-Codeblöcke.")


def ask_ollama(prompt: str, history: list | None = None) -> str:
    messages = [{"role": "system", "content": SYSTEM}]
    if history:
        messages.extend(history)
    messages.append({"role": "user", "content": prompt})
    resp = ollama_client.chat(model=MODEL, messages=messages)
    return resp["message"]["content"]


# Thread-Verlauf laden — Slack liefert ihn selbst!
def thread_history(client, channel, thread_ts) -> list:
    if not thread_ts:
        return []
    result = client.conversations_replies(channel=channel, ts=thread_ts, limit=20)
    history = []
    for m in result["messages"][1:]:   # erste Nachricht = die aktuelle
        role = "assistant" if m.get("bot_id") else "user"
        history.append({"role": role, "content": m.get("text", "")})
    return history


@app.event("app_mention")
def handle_mention(event, say, client):
    history = thread_history(client, event["channel"], event.get("thread_ts"))
    # Mention-Markup <@U123> entfernen
    text = event["text"].split(">", 1)[-1].strip()
    answer = ask_ollama(text, history)
    say(text=answer, thread_ts=event.get("thread_ts") or event["ts"])


@app.event("message")
def handle_dm(event, say, client):
    # Nur Direktnachrichten (Channel-Typ "im"), keine eigenen Bot-Nachrichten
    if event.get("channel_type") != "im" or event.get("bot_id"):
        return
    history = thread_history(client, event["channel"], event.get("thread_ts"))
    answer = ask_ollama(event.get("text", ""), history)
    say(text=answer, thread_ts=event.get("thread_ts") or event["ts"])


@app.command("/ki")
def cmd_ki(ack, respond, command):
    ack()   # Slack verlangt ACK in <3 Sekunden!
    prompt = command.get("text", "").strip()
    if not prompt:
        respond("Nutzung: `/ki <Frage>`")
        return
    # Antwort kann >3s dauern → delayed respond nutzen
    answer = ask_ollama(prompt)
    respond(answer)   # standardmäßig "ephemeral" — nur der Fragende sieht es


if __name__ == "__main__":
    SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()

Das war’s, kein Webserver, kein Port, keine Domain. Der Bot verbindet sich per WebSocket zu Slack und ist sofort erreichbar.

Socket Mode vs. HTTP Events

AspektSocket ModeHTTP Events API
VerbindungBot baut WebSocket zu Slack aufSlack pusht an Deinen HTTPS-Endpoint
Öffentlicher EndpointNicht nötigNötig (Domain + TLS + Reverse Proxy)
Firewall/NATFunktioniert überallEingehende Ports nötig
Skalierung1 Verbindung pro ProzessBeliebig viele Instanzen
Best forSelf-Hosting, interne BotsÖffentliche Slack-Marketplace-Apps

Für interne Team-Bots ist Socket Mode klar die bessere Wahl.

Erweiterungen

Interaktive Blöcke (Buttons, Modals)

@app.command("/ki")
def cmd_ki(ack, respond, command):
    ack()
    respond(blocks=[
        {"type": "section", "text": {"type": "mrkdwn",
          "text": f"*Frage:* {command['text']}"}},
        {"type": "actions", "elements": [
            {"type": "button", "text": {"type": "plain_text",
             "text": "Als Code"}, "action_id": "fmt_code",
             "value": command["text"]},
            {"type": "button", "text": {"type": "plain_text",
             "text": "Ausführlich"}, "action_id": "fmt_long",
             "value": command["text"]},
        ]},
    ])

@app.action("fmt_code")
def on_code(ack, body, respond):
    ack()
    prompt = body["actions"][0]["value"]
    answer = ask_ollama(prompt + "\nAntworte nur mit einem Codeblock.")
    respond(answer)

RAG: Internes Firmenwissen

Der Bot durchsucht zuerst die Vektordatenbank (Wiki-Export, Handbücher, Runbooks gechunkt mit bge-m3):

def ask_with_rag(prompt, history=None):
    emb = ollama_client.embeddings(model="bge-m3", prompt=prompt)["embedding"]
    hits = qdrant.search("firmenwissen", query_vector=emb, limit=4)
    context = "\n\n".join(h.payload["text"] for h in hits)
    system = SYSTEM + f"\n\nRelevante interne Dokumente:\n{context}\n" \
              "Zitiere die Quelle am Ende der Antwort."
    # ...

Wichtig: Pro Channel/DM kann Kontext-Freigabe nötig sein, nicht jeder Channel darf jedes Dokument sehen. Zugriffsfilter in die Suche einbauen (filter={"must": [{"key": "allowed_channels", ...}]}).

Datei-Uploads analysieren

Nutzer laden eine Datei hoch → Bot lädt sie von Slack → an KI:

@app.event("message")
def handle_file(event, say, client):
    for f in event.get("files", []):
        if f["filetype"] in ("png", "jpg"):
            # Vision-Modell
            data = download_slack_file(f["url_private"])
            resp = ollama_client.chat(model="minicpm-v", messages=[{
                "role": "user", "content": "Was ist auf diesem Bild?",
                "images": [data],
            }])
            say(resp["message"]["content"], thread_ts=event["ts"])

Streaming-Ähnlichkeit: Message-Updates

Slack erlaubt chat.update: Antwort schrittweise ins selbe Posting schreiben:

posted = say("…", thread_ts=event["ts"])
text = ""
for chunk in ollama_client.chat(model=MODEL, messages=msgs, stream=True):
    text += chunk["message"]["content"]
    if len(text) % 100 == 0:
        client.chat_update(channel=channel, ts=posted["ts"], text=text)
client.chat_update(channel=channel, ts=posted["ts"], text=text)

Deployment

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py .
CMD ["python", "bot.py"]
services:
  slack-bot:
    build: .
    restart: always
    env_file: .env
    depends_on: [ollama]
  ollama:
    image: ollama/ollama:latest
    volumes: [ollama_data:/root/.ollama]
volumes:
  ollama_data:

Socket Mode braucht nur ausgehende Verbindungen, der Bot läuft hinter jeder Firewall, auch im Home-LAN ohne Port-Forwarding.

Sicherheit und Compliance

  • Zwei Tokens, beide geheim: xoxb (Bot-Rechte) und xapp (Socket-Verbindung). Bei Leak in api.slack.com revoken.
  • Scopes minimal halten: Nur was der Bot braucht. channels:history nur wenn Thread-Kontext in Channels nötig.
  • DSGVO: Slack ist ein US-Dienst (Salesforce): Chat-Inhalte gehen an Slack-Server. Für vertrauliche interne Kommunikation: Mattermost oder Matrix selbst hosten.
  • Audit: Bot-Aufrufe loggen (wer fragte was, welche Dokumente abgerufen), siehe Audit Logging.
  • Channel-Isolation: Der Bot sollte nicht jedes interne Dokument in jeden Channel bringen: Zugriffsrechte im RAG-Filter spiegeln.

Typische Stolpersteine

  • 3-Sekunden-ACK: Slash-Commands und Actions müssen in <3s ge-ackt werden, sonst „operation_timeout”. Erst ack(), dann langsam antworten via respond().
  • Mention-Markup: event["text"] enthält <@U0123ABC>, vor dem Prompt entfernen.
  • message-Event flutet: message feuert für alle sichtbaren Nachrichten, auf channel_type=="im" oder eigene Präfixe filtern, sonst verarbeitet der Bot jeden Channel-Post.
  • Duplicate Events: Slack sendet Events manchmal doppelt (Retries): event_id-Deduplizierung einbauen.
  • Free-Plan-Limit: 90-Tage-Verlauf, ältere Thread-Nachrichten sind weg, Thread-Kontext bricht dann ab.

Key Takeaways:

  • Socket Mode = kein öffentlicher Endpoint nötig, ideal für Self-Hoster.
  • Bolt-Framework (Python/JS) ist der offizielle, einfachste Weg.
  • Slash-Commands brauchen ACK in <3s: Antwort per respond() nachliefern.
  • Thread-Verlauf liefert Slack selbst: Kontext fast gratis.
  • Für vertrauliche Inhalte: Mattermost oder Matrix statt Slack.

FAQ

Was ist Socket Mode?

Der Bot verbindet sich per WebSocket ausgehend zu Slack: Slack pusht Events über diese Verbindung. Kein öffentlicher HTTPS-Endpoint nötig: ideal hinter Firewall/NAT ohne Port-Forwarding.

Brauche ich Admin-Rechte?

Für das Installieren der App ja (oder App-Approval durch Admin). In vielen Firmen-Workspaces ist App-Installation eingeschränkt, vorher mit dem Workspace-Admin klären.

Kann nur der Fragende die Antwort sehen?

Ja: Slash-Command-Antworten sind standardmäßig „ephemeral” (nur für den Auslöser sichtbar). Für alle sichtbar: response_type: in_channel oder say() nutzen.

Wie bekommt der Bot Thread-Kontext?

Slack liefert ihn selbst: conversations_replies(channel, thread_ts) gibt alle Thread-Nachrichten. Du musst nichts selbst speichern, ein Vorteil gegenüber Telegram/Signal.

Ist ein Slack-Bot DSGVO-konform?

Slack speichert Nachrichten auf US-Servern (EU-Hosting optional bei Enterprise-Plänen). Für interne Team-Bots üblich und vertretbar, für Kundendaten prüfen. Maximale Souveränität: Mattermost selbst hosten.

Was kostet das?

Die Slack-API ist kostenlos (auch im Free-Plan). Es fallen nur Deine Server-Kosten an. Der Free-Plan hat aber 90-Tage-Nachrichten-Limit, für RAG auf Doku besser eigene Datenquelle nutzen.

Mehrere Nutzer gleichzeitig?

Ja: Bolt ist async-fähig. Der Flaschenhals ist Ollama (serielle Inferenz). Bei vielen Nutzern: vLLM-Backend oder Queue mit Wartehinweis.

Der Bot reagiert auf alles, wie filtern?

Das message-Event feuert für jede Nachricht in sichtbaren Channels. Filtere auf channel_type==‘im’ für DMs, auf Erwähnung (@bot) in Channels, oder auf ein Präfix wie ‘!ki’.

Quellen und weiterführende Literatur

Zurück zum KI Blog
Share:

Ähnliche Beiträge