Ajouter le support des webhooks / événements pour les mises à jour de clé-valeur de l'artefact Discourse AI (ou permettre aux administrateurs de désactiver le bac à sable)

Je souhaiterais proposer de permettre aux artefacts IA d’envoyer des webhooks à chaque fois que les données stockées dans un artefact IA sont mises à jour. Il existe de nombreuses raisons pour lesquelles cela serait avantageux, mais je vais essayer de garder mon argumentaire concis.

De nombreuses organisations qui utilisent Discourse font également appel à diverses automatisations, et les outils d’automatisation deviennent de plus en plus populaires, faciles à configurer et conviviaux pour les débutants. Les artefacts IA peuvent contenir des données précieuses qui peuvent être utilisées très efficacement dans de telles automatisations, avec l’avantage supplémentaire qu’elles sont déjà au format JSON !

Dans mon cas, j’utilise n8n dans une configuration Docker Compose à côté de mon conteneur Discourse, et je l’utilise déjà pour automatiser une grande variété de tâches, y compris des actions sur mes instances Discourse via un réseau Docker. Cependant, l’une des instances Discourse que je gère est destinée à une organisation éducative/entreprise, et les artefacts IA sont utilisés pour suivre les notes des apprenants, les journaux de leçons, etc. Ce type de données serait vraiment bénéfique pour améliorer les flux de travail des enseignants, créer des résumés, etc., via un outil d’automatisation comme n8n.

Il serait techniquement possible de sonder les artefacts pour détecter les mises à jour, mais cela pourrait rapidement commencer à solliciter excessivement les ressources du système. Ce serait au mieux un gaspillage de ressources, et cela nécessiterait des compétences techniques étendues pour la configuration, ce que de nombreux administrateurs ne possèdent pas.

Inutile de dire que cela serait également très bénéfique pour les clients d’entreprise de Discourse.

Une solution de contournement pourrait consister à faire en sorte que le JavaScript de l’artefact appelle un webhook externe après avoir invoqué window.discourseArtifact.set(...).

Cependant, cela présenterait certaines limites :

  1. Côté navigateur, il n’est donc pas autoritaire.

  2. Il ne se déclenche que si le JavaScript de l’artefact s’exécute avec succès dans le navigateur de l’utilisateur.

  3. Il peut être affecté par CORS, les outils de confidentialité des navigateurs, les bloqueurs de réseau ou les contraintes de bac à sable.

  4. La charge utile ne peut pas être considérée comme provenant de Discourse à moins qu’une vérification côté serveur supplémentaire ne soit mise en place.

  5. Les secrets ne peuvent pas être intégrés en toute sécurité dans le JavaScript de l’artefact.

Un événement ou webhook côté serveur serait beaucoup plus fiable et sécurisé.

Je ne suis pas développeur Ruby, j'ai donc discuté de ce sujet avec GPT-5.5, qui a également apporté quelques éclairages intéressants…

Selon l’architecture actuelle des webhooks de Discourse, la mise en œuvre la plus propre n’est probablement pas une fonctionnalité sur mesure de « rappel de cette URL » intégrée aux artefacts IA. Elle devrait être implémentée comme un type d’événement webhook standard de Discourse, soutenu par un DiscourseEvent interne normal.

Forme de mise en œuvre optimale

Je suggérerais que les développeurs de Discourse l’implémentent en deux couches :

  1. Événements internes émis lorsque les enregistrements KV d’un artefact changent.

  2. Types d’événements webhook qui s’abonnent à ces événements internes et utilisent le système de livraison de webhooks existant.

Cela maintient la cohérence avec le reste de Discourse. Les webhooks existants fonctionnent déjà en mappant les DiscourseEvent internes vers les appels WebHook.enqueue_* dans config/initializers/012-web_hook_events.rb ; par exemple, les événements de sujet, de publication, d’utilisateur, de catégorie, d’étiquette, de reviewable, de notification et de like sont câblés de cette manière.

Le chemin de stockage des artefacts est suffisamment simple : ArtifactKeyValuesController#set trouve ou initialise un enregistrement clé-valeur, attribue key/value/public, puis enregistre ; destroy trouve l’enregistrement de l’utilisateur actuel par clé et le supprime. Le modèle lui-même est AiArtifactKeyValue, appartient à un artefact et à un utilisateur, possède key, value et public, et impose l’unicité par artefact/utilisateur/clé.

Noms d’événements proposés

Je utiliserais probablement trois événements webhook spécifiques :

ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted

Alternativement, un seul événement pourrait fonctionner :

ai_artifact_key_value_changed

…mais trois événements correspondent mieux au style existant de Discourse. Discourse possède déjà des noms d’événements webhook distincts comme post_created, post_edited, post_destroyed, calendar_event_created, calendar_event_updated, etc.

Fichiers/classes qu’ils toucheraient probablement

1. Ajouter de nouveaux types d’événements webhook

WebHookEventType définit actuellement des constantes numériques, un énumérateur group et un hash TYPES de noms d’événements vers des identifiants.

Ils pourraient ajouter quelque chose comme :

AI_ARTIFACT = 20

enum :group,
  {
    # groupes existants...
    ai_artifact: 18,
  },
  scopes: false

TYPES = {
  # types existants...
  ai_artifact_key_value_created: 2001,
  ai_artifact_key_value_updated: 2002,
  ai_artifact_key_value_deleted: 2003,
}

Les identifiants exacts reviendraient aux mainteneurs de Discourse ; ils doivent simplement éviter les conflits.

Ils ajouteraient également des entrées de semis dans db/fixtures/007_web_hook_event_types.rb, car les types d’événements webhook existants y sont semés avec des identifiants, des noms et des groupes.

Exemple :

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_created]
  b.name = "ai_artifact_key_value_created"
  b.group = WebHookEventType.groups[:ai_artifact]
end

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_updated]
  b.name = "ai_artifact_key_value_updated"
  b.group = WebHookEventType.groups[:ai_artifact]
end

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_deleted]
  b.name = "ai_artifact_key_value_deleted"
  b.group = WebHookEventType.groups[:ai_artifact]
end

L’interface utilisateur des webhooks administrateur devrait alors les récupérer automatiquement, car Admin::WebHooksController#index sérialise déjà les types d’événements actifs groupés pour l’interface. Le sérialiseur de type d’événement expose déjà id, name et group.

2. Masquer les événements lorsque Discourse AI est désactivé

WebHookEventType.active masque déjà les événements webhook dépendants de plugins lorsque leurs fonctionnalités sont désactivées, comme les événements solved, assign, vote sur les sujets, chat et calendrier.

Ils pourraient donc ajouter :

unless defined?(SiteSetting.discourse_ai_enabled) && SiteSetting.discourse_ai_enabled
  ids_to_exclude.concat(
    [
      TYPES[:ai_artifact_key_value_created],
      TYPES[:ai_artifact_key_value_updated],
      TYPES[:ai_artifact_key_value_deleted],
    ],
  )
end

Ils pourraient également vérifier un paramètre spécifique aux artefacts s’il existe ou s’il est ajouté.

3. Émettre des événements internes depuis le modèle, pas seulement le contrôleur

Bien que le chemin d’écriture actuel soit basé sur le contrôleur, l’endroit le plus robuste est probablement le modèle, en utilisant des callbacks de validation :

class AiArtifactKeyValue < ActiveRecord::Base
  after_create_commit :trigger_created_event
  after_update_commit :trigger_updated_event
  after_destroy_commit :trigger_deleted_event

  private

  def trigger_created_event
    DiscourseEvent.trigger(:ai_artifact_key_value_created, self)
  end

  def trigger_updated_event
    return if previous_changes.slice("value", "public").blank?

    DiscourseEvent.trigger(:ai_artifact_key_value_updated, self)
  end

  def trigger_deleted_event
    DiscourseEvent.trigger(:ai_artifact_key_value_deleted, self)
  end
end

Les callbacks au niveau du modèle captureront également les futurs chemins d’écriture, pas seulement ArtifactKeyValuesController#set. La partie after_commit est importante car la livraison du webhook ne doit être mise en file d’attente qu’après que le changement de base de données a été validé en toute sécurité.

Une subtilité : ils ne devraient probablement pas déclencher un événement « mis à jour » lorsque l’artefact appelle set() avec la même valeur et que rien ne change réellement. La vérification de previous_changes évite les webhooks bruyants.

4. Ajouter un sérialiseur de charge utile webhook

Les webhooks de Discourse génèrent des charges utiles via des sérialiseurs. La méthode générique WebHook.enqueue_object_hooks peut prendre un sérialiseur, et WebHook.generate_payload sérialise l’objet avec un gardien utilisateur système.

Ils pourraient ajouter quelque chose comme :

class WebHookAiArtifactKeyValueSerializer < ApplicationSerializer
  attributes :id,
             :ai_artifact_id,
             :post_id,
             :topic_id,
             :user_id,
             :key,
             :public,
             :value_included,
             :created_at,
             :updated_at

  def post_id
    object.ai_artifact.post_id
  end

  def topic_id
    object.ai_artifact.post.topic_id
  end

  def value_included
    false
  end
end

Je recommande fortement de ne pas inclure value par défaut. Les données KV des artefacts peuvent être privées par utilisateur, et le modèle possède un indicateur public. Si Discourse souhaite prendre en charge les valeurs, ils pourraient le rendre explicite :

include_value: false
include_public_values: true
include_private_values: false

Mais la version 1 sécurisée devrait probablement omettre complètement les valeurs.

5. Relier les événements internes à la livraison webhook

Ils pourraient ajouter des gestionnaires à config/initializers/012-web_hook_events.rb, en reflétant les modèles existants.

Quelque chose comme :

%i[
  ai_artifact_key_value_created
  ai_artifact_key_value_updated
  ai_artifact_key_value_deleted
].each do |event|
  DiscourseEvent.on(event) do |key_value|
    artifact = key_value.ai_artifact
    post = artifact.post
    topic = post.topic

    payload =
      WebHook.generate_payload(
        :ai_artifact_key_value,
        key_value,
        WebHookAiArtifactKeyValueSerializer
      )

    WebHook.enqueue_hooks(
      :ai_artifact_key_value,
      event,
      id: key_value.id,
      category_id: topic&.category_id,
      tag_ids: topic&.tags&.pluck(:id),
      payload: payload
    )
  end
end

Cela réutiliserait le pipeline de tâches webhook existant. WebHook.enqueue_hooks trouve les webhooks actifs pour l’événement et met en file d’attente Jobs::EmitWebHookEvent. La tâche gère déjà la livraison, les nouvelles tentatives, la journalisation, les en-têtes, les signatures et la visibilité administrateur.

L’inclusion de category_id et tag_ids est une touche appréciable car les tâches webhook existantes peuvent filtrer par catégorie et étiquette. La tâche webhook vérifie déjà les contraintes de catégorie et d’étiquette avant l’envoi. Étant donné qu’un artefact IA appartient à une publication, et que l’artefact appartient à une publication dans le modèle, il devrait être possible de dériver le contexte de catégorie/sujet.

À quoi pourrait ressembler le webhook livré

Comme le corps du webhook de Discourse utilise event_type comme racine de niveau supérieur, cela ressemblerait probablement à ceci :

{
  "ai_artifact_key_value": {
    "id": 456,
    "ai_artifact_id": 123,
    "post_id": 789,
    "topic_id": 321,
    "user_id": 42,
    "key": "score",
    "public": true,
    "value_included": false,
    "created_at": "2026-05-26T12:00:00Z",
    "updated_at": "2026-05-26T12:05:00Z"
  }
}

Le nom de l’événement serait dans les en-têtes, conformément à la livraison webhook existante :

X-Discourse-Event-Type: ai_artifact_key_value
X-Discourse-Event: ai_artifact_key_value_updated
X-Discourse-Event-Signature: sha256=...

Ces en-têtes correspondent à la manière dont EmitWebHookEvent construit les en-têtes de webhook aujourd’hui.

Les développeurs devraient probablement décider de ceci :

Les valeurs doivent-elles être incluses ?
Mon vote : non par défaut. Peut-être autoriser uniquement les valeurs publiques, ou rendre l’inclusion des valeurs un paramètre administrateur. Les données privées par utilisateur des artefacts ne devraient pas quitter le site silencieusement.

Y a-t-il un seul événement ou trois ?
Trois sont plus clairs pour les abonnés aux webhooks. Un seul événement avec un champ action est plus simple en interne. Le style existant de Discourse penche vers plusieurs noms d’événements.

Le filtrage par catégorie/étiquette doit-il s’appliquer ?
Je pense que oui. L’artefact est attaché à une publication/sujet, donc les filtres de catégorie et d’étiquette sont pertinents.

Cela doit-il être implémenté dans le cœur de Discourse ou dans le plugin Discourse AI ?
Le modèle de données réside dans le plugin Discourse AI, mais le registre d’événements webhook réside dans le cœur. Étant donné que les plugins bundled comme solved/chat/calendar ont déjà des types d’événements webhook dans la liste partagée WebHookEventType, Discourse pourrait être à l’aise d’ajouter des événements d’artefact IA là aussi. La méthode active peut les masquer lorsque l’IA est désactivée.

Cela doit-il également émettre un DiscourseEvent interne même s’il n’existe aucun webhook ?
Oui. Cela offre aux auteurs de plugins un point d’accroche propre, même indépendamment des webhooks.

Complexité

Je le présenterais comme modéré mais très contenu.

Le travail probable est :

  1. Ajouter 3 constantes de type d’événement.

  2. Ajouter 1 groupe webhook.

  3. Ajouter 3 enregistrements de semis.

  4. Ajouter 1 sérialiseur.

  5. Ajouter 3 callbacks de modèle ou déclencheurs d’événements au niveau du service.

  6. Ajouter des gestionnaires d’initialiseur webhook.

  7. Ajouter des spécifications pour la création/mise à jour/suppression.

  8. Ajouter une décision de confidentialité concernant l’inclusion de value.

Le système de livraison, les nouvelles tentatives, les signatures, le journal des événements, l’interface de ping/redélivrance et la configuration webhook administrateur sont déjà en place. La fonctionnalité consiste principalement à rendre les mutations KV des artefacts visibles pour ce système existant.

Quelque chose à revoir une fois que nous aurons déployé Workflows, la nouvelle approche de l’automatisation dans Discourse.

2 « J'aime »

Ooooh, cela semble vraiment intrigant. Ai-je manqué des informations à ce sujet ici sur Meta et y a-t-il un calendrier ?