Bot de Slack con IA local
Qué cubre este artículo
- Cómo construir una app de Slack con el framework Bolt y Ollama.
- Socket Mode vs. HTTP Events: por qué Socket Mode es ideal para self-hosters.
- Slash commands, menciones, contexto de threads y bloques interactivos.
- Extensiones: RAG, carga de archivos, contexto multiusuario.
- Despliegue y seguridad en entornos empresariales.
Introducción
Slack es el estándar en equipos tech y cuenta con una excelente API oficial para bots. Las apps de Slack soportan slash commands, menciones, botones, modales y threads. Lo mejor para self-hosters: Socket Mode conecta el bot a Slack por WebSocket, sin necesidad de endpoint público, sin reverse proxy ni agujeros en el firewall.
Con Ollama detrás, el bot de Slack se convierte en un asistente interno del equipo: conocimiento empresarial mediante RAG, respuestas a preguntas sobre código, resúmenes, todo sin enviar contenido de Slack a servidores de IA en la nube (aunque Slack en sí, por supuesto, sigue viéndolo).
Casos de uso típicos
- Bot de conocimiento interno: “¿Cómo solicito vacaciones?” → El bot responde desde el wiki mediante RAG.
- Asistente de DevOps: “/ki ¿por qué falla la build?” → Analiza fragmentos de logs.
- Ayuda en onboarding: Los nuevos empleados consultan al bot en lugar de molestar a colegas.
- Bot de revisión de código: Carga un diff, obtén comentarios de revisión de un modelo local.
- Resúmenes de reuniones: Resume el contenido de threads automáticamente.
- Primer nivel de IT Help Desk: Captura preguntas frecuentes, escala el resto.
Requisitos previos
- Workspace de Slack (el plan gratuito funciona, pero cuidado: el historial de mensajes solo se conserva 90 días)
- Permisos de admin en el workspace (o autorización para instalar apps)
- Servidor con Ollama + Python 3.10+
Paso 1: Crear la app de Slack
En api.slack.com/apps → “Create New App” → “From scratch”:
- Activa Socket Mode (Settings → Socket Mode → Enable) → Genera un token a nivel de app
xapp-...(Scope:connections:write) - Bot Token Scopes (OAuth & Permissions):
app_mentions:read,chat:write,commands,channels:history,im:history,files:read - Crea un slash command:
/ki(la Request URL no importa en Socket Mode, puedes dejar un valor dummy) - Event Subscriptions: Suscríbete a
app_mention,message.im - Install to Workspace → Copia el bot token
xoxb-...
Dos tokens: xapp- (a nivel de app, para Socket Mode) + xoxb- (token del bot). Ambos en .env:
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b
Paso 2: Bot con 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 = ("Eres el asistente IA interno. Responde de forma concisa y precisa, "
"en español. Para código: usa bloques de markdown.")
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"]
# Carga el historial del thread, ¡Slack lo proporciona!
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:]: # el primer mensaje es el actual
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"))
# Elimina el marcado de mención <@U123>
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):
# Solo mensajes directos (tipo de canal "im"), no respuestas del bot
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 requiere ACK en <3 segundos
prompt = command.get("text", "").strip()
if not prompt:
respond("Uso: `/ki <pregunta>`")
return
# La respuesta puede tardar >3s, usa respond retrasado
answer = ask_ollama(prompt)
respond(answer) # por defecto "ephemeral", solo quien pregunta la ve
if __name__ == "__main__":
SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
Eso es todo, sin servidor web, sin puertos, sin dominios. El bot se conecta por WebSocket a Slack y está disponible al instante.
Socket Mode vs. HTTP Events
| Aspecto | Socket Mode | HTTP Events API |
|---|---|---|
| Conexión | El bot abre un WebSocket a Slack | Slack hace push a tu endpoint HTTPS |
| Endpoint público | No necesario | Obligatorio (dominio + TLS + reverse proxy) |
| Firewall/NAT | Funciona en cualquier lugar | Requiere puertos de entrada |
| Escalabilidad | 1 conexión por proceso | Cualquier número de instancias |
| Mejor para | Self-hosting, bots internos | Apps públicas del Slack Marketplace |
Para bots internos de equipo, Socket Mode es claramente la opción superior.
Extensiones
Bloques interactivos (botones, modales)
@app.command("/ki")
def cmd_ki(ack, respond, command):
ack()
respond(blocks=[
{"type": "section", "text": {"type": "mrkdwn",
"text": f"*Pregunta:* {command['text']}"}},
{"type": "actions", "elements": [
{"type": "button", "text": {"type": "plain_text",
"text": "Como código"}, "action_id": "fmt_code",
"value": command["text"]},
{"type": "button", "text": {"type": "plain_text",
"text": "Detallado"}, "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 + "\nResponde solo con un bloque de código.")
respond(answer)
RAG: Conocimiento empresarial interno
El bot busca primero en la base de datos vectorial (exportación de wiki, manuales, runbooks segmentados con 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\nDocumentos internos relevantes:\n{context}\n" \
"Cita la fuente al final de tu respuesta."
# ...
Importante: Por canal o DM puede ser necesario compartir contexto, no todos los canales deben acceder a todos los documentos. Construye filtros de acceso en la búsqueda (filter={"must": [{"key": "allowed_channels"...}]}).
Analizar cargas de archivos
El usuario sube un archivo → el bot lo descarga desde Slack → lo envía a la IA:
@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 similar: actualizaciones de mensajes
Slack permite chat.update: escribir la respuesta de forma incremental en el mismo post:
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)
Despliegue
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 requiere solo conexiones salientes, por lo que el bot funciona detrás de cualquier firewall, incluso en una LAN doméstica sin redirección de puertos.
Seguridad y cumplimiento normativo
- Dos tokens, ambos secretos:
xoxb(permisos del bot) yxapp(conexión Socket). Si se filtra en api.slack.com, revócalos inmediatamente. - Mantén los permisos al mínimo: solo lo que el bot necesita.
channels:historysolo si requieres contexto de hilos en canales. - RGPD: Slack es un servicio estadounidense (Salesforce), los contenidos de chat van a servidores Slack. Para comunicaciones internas confidenciales: aloja Mattermost o Matrix tú mismo.
- Auditoría: registra las invocaciones del bot (quién preguntó qué, qué documentos se recuperaron), consulta Audit Logging.
- Aislamiento de canales: el bot no debe llevar cada documento interno a cada canal. Refleja los derechos de acceso en el filtro RAG.
Errores comunes
- ACK de 3 segundos: los slash commands y las acciones deben reconocerse en menos de 3s, de lo contrario aparece “operation_timeout”. Primero
ack(), luego responde lentamente víarespond(). - Markup de menciones:
event["text"]contiene<@U0123ABC>, elimínalo antes del prompt. - El evento message genera demasiados disparos:
messagese activa para todos los mensajes visibles, filtra porchannel_type=="im"o por prefijos personalizados, de lo contrario el bot procesa cada post del canal. - Eventos duplicados: Slack a veces envía eventos dos veces (reintentos), implementa deduplicación por
event_id. - Límite del plan gratuito: historial de 90 días, los mensajes de hilos más antiguos desaparecen y el contexto se interrumpe.
Enlaces útiles
- IRC-Coding.de: tutoriales avanzados sobre Slack-Bolt, manejo de WebSocket y Block Kit.
- Bot para Mattermost: alternativa a Slack autohospedada.
- Bot para Teams: alternativa de Microsoft.
- Comparación de plataformas: todas las plataformas en perspectiva.
- Ollama: servidor de modelos.
- RAG local: vincula el conocimiento de la empresa.
Puntos clave:
- Socket Mode no requiere endpoint público, ideal para autohospedaje.
- Bolt Framework (Python/JS) es la forma oficial y más simple.
- Los slash commands necesitan ACK en menos de 3s: proporciona la respuesta después via respond().
- Slack proporciona el historial de hilos: el contexto sale casi gratis.
- Para contenidos confidenciales: aloja Mattermost o Matrix en lugar de Slack.
Preguntas frecuentes
¿Qué es Socket Mode?
¿Necesito permisos de administrador?
¿Puede solo el que pregunta ver la respuesta?
¿Cómo obtiene el bot el contexto de un hilo?
¿Es un bot de Slack conforme al RGPD?
¿Cuánto cuesta?
¿Múltiples usuarios simultáneamente?
El bot responde a todo, ¿cómo filtro?
Fuentes y lecturas complementarias
- Slack Bolt for Python: Documentación oficial del framework.
- Slack API: Referencia de API y gestión de aplicaciones.
- Socket Mode: Documentación de Socket Mode.
- IRC-Coding.de: Tutoriales de programación.


