Construire un bot Slack pour trier les retours
Construisez un bot Slack qui surveille un canal, identifie les retours produit exploitables, recherche les doublons dans Linear et crée des tickets ou commente des tickets existants. Les messages de premier niveau sont traités automatiquement. Les réponses ne poursuivent la même conversation que si quelqu'un mentionne le bot.
Vous allez exécuter l'agent et le bot Slack en local :
Slack channel
↓ Socket Mode
bot.py
↓ the agent's session
agent, served on your computer (`agents serve`)
↓ Linear MCP server, authenticated with a Linear API key
Linearagents serve exécute l'agent sur votre ordinateur et l'enregistre auprès de Mistral. bot.py écoute
Slack et ouvre une session Mistral pour chaque thread Slack.
Fonctionnalités présentées : connecteurs et MCP, sortie structurée, tours (clés d'idempotence, message envoyé pendant un tour), suivi des événements d'une session, arrêt d'un tour, mise à disposition d'un agent.
Exécutez (après la configuration ci-dessous), depuis le dossier de l'exemple :
uv run --frozen --env-file .env python bot.py
Ce que fait le bot
- Examine chaque nouveau message de premier niveau dans un canal Slack.
- Ignore silencieusement les messages non pertinents.
- Pose une seule question courte lorsque le retour est trop vague.
- Classe les retours précis comme question, idée, bug ou demande de fonctionnalité.
- Effectue une recherche dans un projet Linear avant de créer quoi que ce soit.
- Ajoute des informations utiles à un doublon avéré au lieu de créer un nouveau ticket.
- Crée un ticket Triage pour chaque nouveau retour exploitable.
- Affiche un statut temporaire pendant que l'agent travaille.
- Conserve le contexte de suivi dans le thread Slack.
- Ne répond aux réponses du thread que lorsque quelqu'un mentionne le bot.
Le code du bot Slack
L'exemple contient deux projets : l'agent dans agent/ et le bot Slack à la racine.
| Fichier | Ce qu'il montre |
|---|---|
agent/main.py | L'instance agents.Agent exécutée par la CLI, avec le serveur MCP de Linear et le format de réponse TriageReply |
agent/instructions.py | Les instructions de tri complètes : ordre de classification, gestion des doublons, règles de sécurité |
agent/check_linear_mcp.py | Une validation en lecture seule qui interroge Linear sans rien écrire |
agent_client.py | Résolution de l'agent et lecture du TriageReply d'un tour |
bot.py | Gestionnaires Slack : une session par thread stockée dans threads.json, déclenchement sur mention, contexte mis en mémoire tampon, arrêt |
Le fichier README.md de l'exemple contient la procédure de configuration complète et le script de test.
L'agent définit son format de réponse, TriageReply, pour demander au modèle de l'utiliser :
from instructions import INSTRUCTIONS
from pydantic import BaseModel
from mistralai import agents
class TriageReply(BaseModel):
"""The answer to each message. The bot reads it with the same `TriageReply` class."""
respond: bool
message: str
# Replace `your-id` with a unique lowercase identifier, such as your GitHub handle,
# so the served and deployed names cannot collide with anyone else's.
# The bot's AGENT_NAME must match this name exactly.
agent = agents.Agent(
model="mistral-medium-latest",
name="feedback-triage-your-id-local",
instructions=INSTRUCTIONS,
output_type=TriageReply,
mcps={
"linear": agents.RemoteMCP(
url="https://mcp.linear.app/mcp",
auth_header_env_vars={"Authorization": "LINEAR_MCP_TOKEN"},
)
},
)Le bot lit la réponse de chaque tour terminé avec la même classe TriageReply :
import logging
from pydantic import BaseModel
from mistralai import agents
logger = logging.getLogger(__name__)
class TriageReply(BaseModel):
"""The agent's answer to each message: the same class as in `agent/main.py`."""
respond: bool
message: str
def connect(agent_name: str) -> agents.Agent:
return agents.Agent.load_from(agent_name=agent_name)
async def read_reply(session: agents.Session, turn_id: str) -> str | None:
"""The reply to post for a completed turn, or None when the agent chose not to respond."""
match await session.turn(turn_id).result(output_type=TriageReply):
case agents.Completed(output=TriageReply(respond=False)):
return None
case agents.Completed(output=TriageReply(message=message)):
return message
case state:
# No TriageReply: an answer in another shape, or a turn that reached its iteration limit.
logger.warning("Turn %s gave no triage reply: %r", turn_id, state)
return "❌ I couldn’t complete the triage. Please try again."Chaque message Slack est transmis à la session avec son horodatage comme clé d'idempotence. Il démarre un tour, ou réoriente le tour en cours :
# Starts a turn, or steers the running one. Slack's timestamp is the idempotency key,
# so the message reaches the agent once.
turn = await agent.session(thread["session_id"]).send_message(
f"{input_type}\n\n{source}{message}", idempotency_key=event["ts"]
)
if turn.started:
thread["last_turn_id"] = turn.id
thread["status"][turn.id] = {"ts": status["ts"], "top_level": not is_follow_up}Un processus de suivi par thread publie la réponse de chaque tour. Il conserve l'identifiant du dernier événement publié,
pour reprendre la lecture à cet endroit après un redémarrage ou une perte de connexion. Une session terminée déclenche
l'une des erreurs SESSION_GONE :
# A session ends after a time without turns, or when it reaches its lifetime.
SESSION_GONE = (agents.SessionTerminatedError, agents.SessionNotFoundError)async def post_answers(thread_ts: str) -> None:
"""Post the answer of each turn of the thread's session, from the last event posted on."""
thread = threads[thread_ts]
session = agent.session(thread["session_id"])
while thread_ts in threads:
try:
async for event in session.iter_events(after=thread["cursor"]):
if isinstance(event, agents.TurnEnded):
async with locks[thread_ts]:
status = thread["status"].get(event.state.turn_id)
await post_answer(session, thread_ts, event, status)
if thread_ts not in threads:
return
thread["status"].pop(event.state.turn_id, None)
thread["cursor"] = event.id
save()
# The session has ended: a later mention gets NO_SESSION.
forget(thread_ts)
except SESSION_GONE:
forget(thread_ts)
except Exception:
# The connection dropped (ObservationLost), or posting failed. Nothing is missed:
# read again after the last event posted, and post that answer again.
app.logger.exception("Following thread %s failed; reading again", thread_ts)
await asyncio.sleep(10)Prérequis
Vous avez besoin de :
- Python 3.12 et
uv. - Une clé API Mistral dont l'utilisateur possède un rôle dans l'espace de travail donnant accès à Workflows, par exemple Développeur : consultez
Déployer. Cette clé est nécessaire pour
agents serve. - Une clé API Linear autorisant la lecture, la création de tickets et l'ajout de commentaires dans l'équipe cible.
- L'autorisation de créer et d'installer une application Slack dans votre espace de travail.
- Un canal Slack public pour les tests.
1. Créer la clé API Linear
L'agent accède à Linear via le serveur MCP hébergé de Linear plutôt que via un connecteur Mistral Studio, car la clé est un identifiant fixe que l'agent utilise à chaque exécution. Créez une clé dans Linear sous Paramètres → Sécurité et accès → Clés API personnelles, avec un périmètre limité à l'équipe à laquelle vous souhaitez affecter les retours.
Elle nécessite trois autorisations. La lecture et la création de tickets couvrent le cas courant ; la création de commentaires sert au traitement des doublons, et une clé sans cette autorisation échoue seulement lorsque l'agent trouve un doublon et tente d'ajouter des informations :
{"error":"auth_insufficient_scope","message":"Not allowed to access this Linear resource.","status":403}Choisissez un projet et une équipe Linear pour l'agent, puis définissez ces deux valeurs en haut de
agent/instructions.py. La clé peut donner accès à davantage de projets que celui-ci ; les instructions désignent la
cible, elles ne restreignent pas la clé.
2. Créer l'application Slack
Vous avez besoin d'une application Slack avec Socket Mode, de deux périmètres d'autorisation pour le bot et d'un événement, de ses deux tokens et de l'identifiant du canal de test.
Configurer l'application Slack
Ouvrez Your Apps, sélectionnez Create New App, puis choisissez From scratch.
Utilisez :
- Nom de l'application :
AgentsFeedbackBot - Espace de travail : votre espace de travail de test
Activer Socket Mode
Ouvrez Socket Mode et activez-le. Créez un token au niveau de l'application avec le scope connections:write.
Copiez le token commençant par xapp- ; Slack ne l'affiche qu'une seule fois.
Ajouter les permissions du bot
Ouvrez OAuth & Permissions et ajoutez les scopes de token de bot suivants :
channels:historychat:write
S'abonner aux messages
Ouvrez Event Subscriptions, activez les événements, puis abonnez-vous à cet événement de bot :
message.channels
Socket Mode achemine les événements, vous n'avez donc pas besoin d'une URL de requête publique.
Installer l'application
Sélectionnez Install to Workspace et terminez la procédure d'approbation. Copiez le token de bot commençant par
xoxb-.
Si votre organisation vérifie les applications Slack, soumettez l'application une fois son nom, sa description, ses scopes et son abonnement aux événements renseignés. Réinstallez-la chaque fois que vous modifiez les scopes OAuth.
Invitez le bot dans le canal de test :
/invite @AgentsFeedbackBotPour trouver l'ID du canal dans la version desktop ou web de Slack :
- Ouvrez le canal.
- Sélectionnez le nom du canal dans l'en-tête de la conversation.
- Ouvrez l'onglet About.
- Faites défiler jusqu'en bas et copiez l'ID du canal.
Les ID de canal Slack ressemblent à C0123456789.
Gardez les deux tokens Slack secrets. Toute personne qui les détient peut contrôler l'application.
3. Configurer et inspecter l'agent
Depuis le projet agent/, copiez .env.example vers .env et renseignez les deux clés :
MISTRAL_API_KEY=...
LINEAR_MCP_TOKEN="Bearer lin_api_..."LINEAR_MCP_TOKEN contient la valeur complète de l'en-tête Authorization, schéma inclus, car
auth_header_env_vars associe un nom d'en-tête à la variable qui contient cet en-tête. Conservez les guillemets :
sans eux, l'espace après Bearer met fin à la valeur, et --env-file signale
Failed to parse environment file.
Ensuite, installez et inspectez :
cd agent
uv sync
uv run --frozen --env-file .env agents infoagents info affiche le serveur MCP linear avec son hôte. Cela indique que le serveur est déclaré, mais pas
que l'agent peut y accéder. L'étape suivante vérifie cet accès.
4. Valider Linear sans rien écrire
Confirmez que l'agent peut accéder à Linear et rechercher dans le projet cible :
uv run --frozen --env-file .env python check_linear_mcp.pyPoursuivez lorsqu'il renvoie des tickets du projet visé, sans rien avoir créé, mis à jour ni supprimé.
5. Mettre l'agent en service
bot.py s'exécute séparément de l'agent et ne peut donc pas utiliser l'objet Agent du même processus. Démarrez
l'agent avec agents serve pour que le bot puisse ouvrir des sessions auprès de lui :
uv run --frozen --env-file .env agents serveLa commande enregistre l'agent auprès de Mistral Workflows et l'exécute sur votre ordinateur. Gardez ce terminal ouvert.
Mistral identifie les agents mis en service par leur nom. L'exemple utilise your-id comme placeholder : remplacez-le
par un identifiant unique en minuscules, comme votre identifiant GitHub, partout où il apparaît. Le AGENT_NAME
du bot doit correspondre exactement au nom de l'agent.
Faites cette vérification avant de lancer l'agent. Si quelqu'un d'autre utilise déjà le nom, agents serve s'arrête avant le
démarrage de l'agent, se termine avec le code de sortie 1 et affiche la raison fournie par la console de pilotage, par exemple :
6. Exécuter le bot
De retour à la racine de l'exemple, copiez .env.example vers .env et renseignez les deux tokens Slack, l'ID du canal,
votre clé API Mistral et le nom de l'agent :
uv sync
uv run --frozen --env-file .env python bot.pyPoursuivez lorsqu'il affiche :
Le bot Slack a besoin de sa propre clé MISTRAL_API_KEY, car il appelle directement Mistral pour démarrer des sessions :
agents serve exécute l'agent, mais le bot doit toujours s'authentifier pour ouvrir et faire avancer
les sessions.
7. Tester le flux complet
Tester le traitement silencieux
Publiez un message de premier niveau non pertinent :
Has anyone seen the HDMI adapter?🧠 Reviewing this feedback… devrait s'afficher brièvement puis disparaître.
Tester la clarification et le contexte en mémoire tampon
Publiez :
The Managed Agents setup is confusing.Le bot devrait poser une seule question courte de clarification, sans rechercher dans Linear.
Répondez sans mentionner le bot :
Some tools are not disabled as expected.Le bot devrait rester silencieux mais conserver le message. Envoyez une deuxième réponse dans le thread contenant uniquement :
@AgentsFeedbackBotLe bot devrait combiner le détail en mémoire tampon avec la session existante et poursuivre le tri.
Tester un doublon
Utilisez un retour réel correspondant à un ticket existant. L'agent devrait effectuer une recherche dans le projet cible, ajouter une seule fois des informations utiles, puis renvoyer l'identifiant et le lien du ticket existant.
Ce test écrit un vrai commentaire Linear. N'utilisez pas d'informations artificielles dans un backlog de production.
Tester une nouvelle demande
Utilisez un retour réel et précis, sans correspondance existante. L'agent devrait créer un ticket Triage, inclure le lien du thread Slack et renvoyer la nouvelle URL Linear.
Tester le filtrage par mention dans les threads
Une fois que le bot a répondu, envoyez une réponse normale dans le thread sans le mentionner. Il ne devrait rien se passer.
Mentionnez ensuite @AgentsFeedbackBot dans le thread. La même session Mistral devrait répondre.
Tester l'arrêt
Pendant que le bot traite encore un thread, envoyez @AgentsFeedbackBot stop dans ce thread. Le
statut doit passer à ⏹️ Stopped. Lorsqu'aucun tour n'est en cours, le bot répond Nothing is running.
Déployer l'agent
L'agent lit son identifiant Linear depuis LINEAR_MCP_TOKEN dans son environnement d'exécution. Lorsque
Mistral exécute l'agent, cette variable provient d'un secret de l'espace de travail que vous associez lors du déploiement.
Créez d'abord un secret contenant la même valeur d'en-tête complète que dans votre .env (Bearer lin_api_...)
dans console.mistral.ai → Secrets. Déployez ensuite depuis agent/
et associez la variable à ce secret :
uv run --frozen --env-file .env agents deploy . --name feedback-triage-your-id \
--secret-binding LINEAR_MCP_TOKEN=secret:workspace:linear-triage-keySans cette association, le déploiement affiche un avertissement et l'agent déployé refuse de démarrer :
Définissez ensuite la variable AGENT_NAME du bot sur feedback-triage-your-id. Consultez
Transmettre des secrets pour comprendre le fonctionnement des associations.
Comportement des sessions
Chaque message Slack de premier niveau crée une session Mistral. L'horodatage Slack du message racine associe ce thread à sa session :
Top-level message → create session → triage
Thread reply without mention → buffer context → stay silent
Thread reply with mention → send to the session → a new turn, or steers the running onebot.py stocke dans threads.json l'identifiant de session de chaque thread, ses réponses en mémoire tampon et le dernier événement de session
qu'il a publié. Il suit les événements de session de chaque thread. Ainsi, après un redémarrage, le bot reprend
tous les threads stockés et publie les réponses manquées. Il stocke aussi les messages Slack
traités dans chaque thread et ignore ceux que Slack transmet de nouveau. Chaque message est transmis à l'agent avec
son horodatage Slack comme clé d'idempotence. Ainsi, même si deux copies d'une réponse à un thread sont traitées simultanément,
un seul tour démarre. Lorsque la session d'un thread se termine, le bot oublie ce thread.
L'agent répond avec un TriageReply, défini comme output_type sur l'agent, qui contient respond et message.
Lorsque respond vaut false pour un message de premier niveau, le bot supprime son statut et termine la session.
Sécurité et limitations
Toute personne qui peut publier dans le canal partage une même identité Linear — la clé dans LINEAR_MCP_TOKEN.
Définissez le périmètre de cette clé lors de sa création, car elle constitue la seule véritable barrière ici. Le projet cible dans
instructions.py n'est qu'une instruction du prompt, et le serveur MCP de Linear expose de toute façon l'ensemble de ses outils :
l'agent voit les outils de suppression, de merge et d'administration, et seule la vérification de périmètre propre à
Linear les bloque. Tout ce que la clé peut faire, n'importe qui dans le canal peut finir par obtenir
de l'agent.
Une clé API personnelle attribue également chaque ticket et commentaire à la personne qui l'a créée. Utilisez une clé appartenant à un compte de service si vous ne voulez pas que les écritures du bot apparaissent sous votre nom.
- Toute personne pouvant publier dans le canal public configuré peut déclencher l'agent.
- Les restrictions d'outils sont en partie imposées par le prompt, car
save_issuepeut créer et mettre à jour des tickets. threads.jsonest un fichier local : exécutez un seul processus de bot par fichier.- Les pièces jointes textuelles, les captures d'écran et les messages modifiés sont ignorés.
- Le bot gère un seul canal configuré et un seul projet Linear.
- Avec
agents serve, le bot et l'agent s'exécutent tous deux sur votre ordinateur. Pour faire exécuter l'agent par Mistral, consultez Déployer l'agent.
En production, stockez les correspondances entre threads et sessions dans une base de données. Ajoutez des listes d'autorisation d'utilisateurs ou de canaux, la gestion des pièces jointes, de l'observabilité et une couche d'intégration qui valide chaque écriture dans Linear indépendamment du modèle.
Inspecter les sessions dans Mistral Studio
Les sessions gérées par un agent démarré avec agents serve apparaissent dans
Sessions d'agent dans Mistral Studio. Ouvrez
une session pour examiner ses messages, ses appels d'outils et son statut.
Dépannage
uv run --frozen ne trouve pas uv.lockuv run --frozen ne trouve pas uv.lock
Exécutez uv sync une fois dans ce projet, puis réessayez la commande.
Failed to parse environment file sur LINEAR_MCP_TOKENFailed to parse environment file sur LINEAR_MCP_TOKEN
Mettez la valeur entre guillemets. Sinon, l'espace après Bearer la tronque.
agents serve refuse de démarrer en mentionnant LINEAR_MCP_TOKENagents serve refuse de démarrer en mentionnant LINEAR_MCP_TOKEN
La variable est lue là où l'agent s'exécute. Elle doit donc être définie dans le terminal qui exécute
agents serve, pas seulement dans celui du bot.
auth_insufficient_scopeLinear renvoie auth_insufficient_scope
Il manque une autorisation à la clé. Les droits de lecture et de création de tickets ne suffisent pas : le traitement des doublons commente un ticket existant, la clé doit donc également disposer du droit de créer des commentaires pour cette équipe.
agents serve indique que le nom est déjà associéagents serve indique que le nom est déjà associé
Le nom de l'agent est déjà enregistré par quelqu'un d'autre. Remplacez toutes les occurrences de your-id par un identifiant
unique. Un démarrage réussi se termine par Press Ctrl+C to stop.
agents serve ne peut pas enregistrer l'agentagents serve ne peut pas enregistrer l'agent
Vérifiez que MISTRAL_API_KEY est valide et que son utilisateur possède un rôle dans l'espace de travail donnant accès à Workflows
(consultez Déployer pour connaître les rôles). Une clé autorisée à appeler un modèle n'a pas nécessairement l'autorisation
d'enregistrer l'agent.
Le bot ne répond pas
Vérifiez les points suivants :
agents serveest toujours en cours d'exécution.bot.pyest toujours en cours d'exécution.AGENT_NAMEcorrespond exactement au nom de l'agent en service.- L'application est invitée dans le canal défini dans
SLACK_TARGET_CHANNEL_ID. - Socket Mode et
message.channelssont activés. - Le token du bot et le token de l'application commencent respectivement par
xoxb-etxapp-.
Le bot ignore une réponse dans un thread
C'est le comportement attendu, sauf si la réponse mentionne @AgentsFeedbackBot. Les réponses sans mention ne sont mises en mémoire tampon
que lorsque le bot dispose d'une session active pour ce thread.
Le bot indique qu'il n'a plus de session pour le thread
Le thread ne figure pas dans threads.json : le bot l'a démarré avant la suppression du fichier, a terminé sa
session parce que le message ne nécessitait pas de réponse, ou la session s'est terminée d'elle-même après une période sans
tour ou à l'expiration de sa durée de vie. Si une session se termine pendant un tour, ce message s'affiche comme réponse du
tour. Publiez à nouveau le retour dans un nouveau message de premier niveau.
L'agent décrit les actions à venir au lieu de les exécuter
Redémarrez agents serve après avoir modifié agent/instructions.py, puis ouvrez un nouveau thread
Slack. Une session existante peut encore refléter la définition précédente de l'agent.
Le statut temporaire reste affiché ou passe à l'état d'erreur
Examinez les deux terminaux. Le statut est mis à jour uniquement une fois le tour Mistral et les éventuels appels MCP à Linear
terminés. Un tour s'exécute jusqu'à sa fin ou jusqu'à ce que vous l'arrêtiez : mentionnez le bot avec stop dans le thread.
Ressources et prochaines étapes
- Connecteurs et MCP présente les connecteurs Mistral Studio et les serveurs MCP personnalisés, et explique quand un identifiant statique impose d'utiliser plutôt une source MCP.
- Sessions présente l'historique de conversation et le flux d'événements utilisés par
bot.py. - Modes d'exécution compare l'exécution de l'agent dans votre processus à son déploiement avec Studio (auto-hébergé ou géré par Mistral).
- Agent documente la classe
agents.Agentque la CLI charge depuismain.py.