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
Linear

agents 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

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

Le code du bot Slack

L'exemple contient deux projets : l'agent dans agent/ et le bot Slack à la racine.

FichierCe qu'il montre
agent/main.pyL'instance agents.Agent exécutée par la CLI, avec le serveur MCP de Linear et le format de réponse TriageReply
agent/instructions.pyLes instructions de tri complètes : ordre de classification, gestion des doublons, règles de sécurité
agent/check_linear_mcp.pyUne validation en lecture seule qui interroge Linear sans rien écrire
agent_client.pyRésolution de l'agent et lecture du TriageReply d'un tour
bot.pyGestionnaires 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

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

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

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

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

Ajouter les permissions du bot

Ouvrez OAuth & Permissions et ajoutez les scopes de token de bot suivants :

  • channels:history
  • chat:write
S'abonner aux messages

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

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 @AgentsFeedbackBot

Pour trouver l'ID du canal dans la version desktop ou web de Slack :

  1. Ouvrez le canal.
  2. Sélectionnez le nom du canal dans l'en-tête de la conversation.
  3. Ouvrez l'onglet About.
  4. Faites défiler jusqu'en bas et copiez l'ID du canal.

Les ID de canal Slack ressemblent à C0123456789.

Avertissement

Gardez les deux tokens Slack secrets. Toute personne qui les détient peut contrôler l'application.

3. Configurer et inspecter l'agent

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 info

agents 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

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

Poursuivez lorsqu'il renvoie des tickets du projet visé, sans rien avoir créé, mis à jour ni supprimé.

5. Mettre l'agent en service

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 serve

La 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 :

Error
✕ Cannot serve agent: registration failed at https://...: the name feedback-triage-your-id-local is already bound to another workflow or deployment
The worker cannot receive sessions until it is registered.
6. Exécuter le bot

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

Poursuivez lorsqu'il affiche :

Output
Listening to Slack channel C0123456789 as bot user U0123456789...

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

7. Tester le flux complet

Tester le traitement silencieux

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

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 :

@AgentsFeedbackBot

Le bot devrait combiner le détail en mémoire tampon avec la session existante et poursuivre le tri.

Tester un doublon

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

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

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

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

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-key

Sans cette association, le déploiement affiche un avertissement et l'agent déployé refuse de démarrer :

Error
✕ MCP server 'linear' reads its auth headers from LINEAR_MCP_TOKEN, which is not set in the worker environment.

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

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 one

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

Sécurité et limitations

Avertissement

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_issue peut créer et mettre à jour des tickets.
  • threads.json est 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

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

Dépannage

uv run --frozen ne trouve pas uv.lock

uv 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_TOKEN

Failed 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_TOKEN

agents serve refuse de démarrer en mentionnant LINEAR_MCP_TOKEN

Error
✕ MCP server 'linear' reads its auth headers from LINEAR_MCP_TOKEN, which is not set in the worker environment. Export each in your shell, or put it in .env and start the worker with `uv run --env-file .env`.

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.

Linear renvoie auth_insufficient_scope

Linear 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'agent

agents 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

Le bot ne répond pas

Vérifiez les points suivants :

  • agents serve est toujours en cours d'exécution.
  • bot.py est toujours en cours d'exécution.
  • AGENT_NAME correspond 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.channels sont activés.
  • Le token du bot et le token de l'application commencent respectivement par xoxb- et xapp-.
Le bot ignore une réponse dans un thread

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

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

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

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.Agent que la CLI charge depuis main.py.