Agregar soporte para webhooks/eventos en las actualizaciones de clave-valor de Discourse AI artifact (o permitir a los administradores deshabilitar el sandboxing)

Me gustaría proponer permitir que los artefactos de IA envíen webhooks cada vez que se actualicen los datos almacenados en un artefacto de IA. Hay muchas razones por las que esto sería ventajoso, pero intentaré mantener mi argumento breve.

Muchas organizaciones que utilizan Discourse también hacen uso de diversas automatizaciones, y las herramientas para automatizaciones son cada vez más populares, fáciles de configurar y amigables para principiantes. Los artefactos de IA pueden contener datos valiosos que pueden utilizarse de manera muy efectiva en dichas automatizaciones, con la ventaja adicional de que ya están en formato JSON.

En mi caso, utilizo n8n en una configuración de Docker Compose junto a mi contenedor de Discourse y ya lo uso para automatizar una amplia variedad de cosas, incluidas acciones en mis instancias de Discourse a través de una red Docker. Sin embargo, una de las instancias de Discourse que mantengo es para una organización educativa/empresa y los artefactos de IA se utilizan para registrar notas de los estudiantes, diarios de lecciones, etc. Este tipo de datos sería realmente beneficioso para mejorar los flujos de trabajo de los docentes, generar resúmenes, etc., mediante una herramienta de automatización como n8n.

Sería técnicamente posible consultar los artefactos en busca de actualizaciones, pero eso podría empezar a exigir innecesariamente los recursos del sistema y, en el mejor de los casos, sería un desperdicio de recursos, además de requerir amplias habilidades técnicas para configurarlo, algo de lo que muchos administradores carecen.

Huelga decir que esto también sería altamente beneficioso para los clientes empresariales de Discourse.

Una solución alternativa podría ser que el JavaScript del artefacto llame a un webhook externo después de llamar a window.discourseArtifact.set(...).

Sin embargo, esto tendría algunas limitaciones:

  1. Se ejecuta en el lado del cliente y, por lo tanto, no es autoritativo.

  2. Solo se dispara si el JavaScript del artefacto se ejecuta correctamente en el navegador del usuario.

  3. Puede verse afectado por CORS, herramientas de privacidad del navegador, bloqueadores de redes o restricciones de sandbox.

  4. La carga útil no puede considerarse confiable como proveniente de Discourse a menos que se implemente una verificación adicional en el lado del servidor.

  5. Los secretos no pueden incrustarse de forma segura en el JavaScript del artefacto.

Un evento/webhook en el lado del servidor sería mucho más fiable y seguro.

No soy desarrollador de Ruby, así que he estado hablando con GPT-5.5 sobre esto y también ha ofrecido algunas perspectivas interesantes…

Basándome en la arquitectura actual de webhooks de Discourse, la implementación más limpia probablemente no sea una función personalizada de “llamar a esta URL” dentro de los artefactos de IA. Debería implementarse como un tipo de evento de webhook normal de Discourse, respaldado por un DiscourseEvent interno estándar.

Forma óptima de implementación

Sugiero que los desarrolladores de Discourse lo implementen en dos capas:

  1. Eventos internos emitidos cuando cambian los registros KV del artefacto.

  2. Tipos de eventos de webhook que se suscriban a esos eventos internos y utilicen el sistema de entrega de webhooks existente.

Esto lo mantiene consistente con el resto de Discourse. Los webhooks existentes ya funcionan mapeando DiscourseEvent internos a llamadas WebHook.enqueue_* en config/initializers/012-web_hook_events.rb; por ejemplo, los eventos de tema, publicación, usuario, categoría, etiqueta, revisable, notificación y «me gusta» se conectan de esta manera.

La ruta de almacenamiento del artefacto es lo suficientemente sencilla: ArtifactKeyValuesController#set busca o inicializa un registro de clave-valor, asigna key/value/public y lo guarda; destroy busca el registro del usuario actual por clave y lo elimina. El modelo en sí es AiArtifactKeyValue, pertenece a un artefacto y a un usuario, tiene key, value y public, y hace cumplir la unicidad por artefacto/usuario/clave.

Nombres de eventos propuestos

Probablemente usaría tres eventos de webhook específicos:

ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted

Alternativamente, podría funcionar un solo evento:

ai_artifact_key_value_changed

…pero tres eventos encajan mejor con el estilo existente de Discourse. Discourse ya tiene nombres de eventos de webhook separados como post_created, post_edited, post_destroyed, calendar_event_created, calendar_event_updated, etc.

Archivos/clases que probablemente tocarían

1. Agregar nuevos tipos de eventos de webhook

WebHookEventType define actualmente constantes numéricas, un enum group y un hash TYPES de nombres de eventos a IDs.

Podrían agregar algo como:

AI_ARTIFACT = 20

enum :group,
  {
    # grupos existentes...
    ai_artifact: 18,
  },
  scopes: false

TYPES = {
  # tipos existentes...
  ai_artifact_key_value_created: 2001,
  ai_artifact_key_value_updated: 2002,
  ai_artifact_key_value_deleted: 2003,
}

Los IDs exactos dependerían de los responsables de mantenimiento de Discourse; solo necesitan evitar conflictos.

También agregarían entradas de inicialización en db/fixtures/007_web_hook_event_types.rb, ya que los tipos de eventos de webhook existentes se inicializan allí con IDs, nombres y grupos.

Ejemplo:

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

La interfaz de administración de webhooks debería capturarlos automáticamente, ya que Admin::WebHooksController#index ya serializa los tipos de eventos activos agrupados para la interfaz. El serializador de tipos de eventos ya expone id, name y group.

2. Ocultar los eventos cuando Discourse AI esté deshabilitado

WebHookEventType.active ya oculta los eventos de webhook dependientes de plugins cuando sus funciones están deshabilitadas, como los eventos de solved, assign, votación de temas, chat y calendario.

Así que podrían agregar:

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

También podrían querer verificar una configuración específica de artefacto si existe o se agrega.

3. Emitir eventos internos desde el modelo, no solo desde el controlador

Aunque la ruta de escritura actual se basa en el controlador, el lugar más robusto es probablemente el modelo, utilizando callbacks de confirmación:

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

Los callbacks a nivel de modelo también capturarían futuras rutas de escritura, no solo ArtifactKeyValuesController#set. La parte after_commit es importante porque la entrega del webhook solo debe ponerse en cola después de que el cambio en la base de datos se haya confirmado de forma segura.

Una sutileza: probablemente no deberían disparar un evento de “actualización” cuando el artefacto llama a set() con el mismo valor y nada cambia realmente. Verificar previous_changes evita webhooks ruidosos.

4. Agregar un serializador de carga útil de webhook

Los webhooks de Discourse generan cargas útiles a través de serializadores. El método genérico WebHook.enqueue_object_hooks puede tomar un serializador, y WebHook.generate_payload serializa el objeto con un guardián de usuario del sistema.

Podrían agregar algo como:

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

Recomendaría encarecidamente no incluir value de forma predeterminada. Los datos KV del artefacto pueden ser privados por usuario y el modelo tiene un indicador public. Si Discourse quiere admitir valores, podrían hacerlo explícito:

include_value: false
include_public_values: true
include_private_values: false

Pero la versión 1 segura probablemente debería omitir los valores por completo.

5. Conectar eventos internos a la entrega de webhooks

Podrían agregar manipuladores a config/initializers/012-web_hook_events.rb, reflejando los patrones existentes.

Algo como:

%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

Esto reutilizaría la tubería de trabajos de webhook existente. WebHook.enqueue_hooks busca webhooks activos para el evento y pone en cola Jobs::EmitWebHookEvent. El trabajo ya maneja la entrega, reintentos, registro, encabezados, firmas y visibilidad para administradores.

Incluir category_id y tag_ids es un buen detalle porque los trabajos de webhook existentes pueden filtrar por categoría y etiqueta. El trabajo de webhook ya verifica las restricciones de categoría y etiqueta antes de enviar. Dado que un artefacto de IA pertenece a una publicación y el artefacto pertenece a una publicación en el modelo, debería ser posible derivar el contexto de categoría/tema.

Cómo podría verse el webhook entregado

Dado que el cuerpo del webhook de Discourse utiliza event_type como raíz de nivel superior, esto probablemente se vería así:

{
  "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"
  }
}

El nombre del evento estaría en los encabezados, consistente con la entrega de webhooks existente:

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

Esos encabezados se alinean con cómo EmitWebHookEvent construye los encabezados de webhook hoy en día.

Los desarrolladores probablemente tendrían que decidir esto:

¿Se deben incluir los valores?
Mi voto: no de forma predeterminada. Tal vez permitir solo valores públicos, o hacer que la inclusión de valores sea una configuración de administrador. Los datos privados por usuario del artefacto no deben salir silenciosamente del sitio.

¿Debería haber un evento o tres?
Tres es más limpio para los suscriptores de webhooks. Un evento con un campo action es más simple internamente. El estilo existente de Discourse se inclina hacia múltiples nombres de eventos.

¿Debería aplicarse el filtrado por categoría/etiqueta?
Creo que sí. El artefacto está adjunto a una publicación/tema, por lo que los filtros de categoría y etiqueta son significativos.

¿Debería esto implementarse en el núcleo de Discourse o en el plugin Discourse AI?
El modelo de datos vive en el plugin Discourse AI, pero el registro de eventos de webhook vive en el núcleo. Dado que los plugins empaquetados como solved/chat/calendar ya tienen tipos de eventos de webhook en la lista compartida WebHookEventType, es posible que Discourse se sienta cómodo agregando eventos de artefactos de IA allí también. El método active puede ocultarlos cuando la IA esté deshabilitada.

¿Debería esto emitir también un DiscourseEvent interno incluso si no existe ningún webhook?
Sí. Eso brinda a los autores de plugins un punto de conexión limpio incluso aparte de los webhooks.

Complejidad

Lo enmarcaría como moderada pero muy contenida.

El trabajo probable es:

  1. Agregar 3 constantes de tipo de evento.

  2. Agregar 1 grupo de webhook.

  3. Agregar 3 registros de inicialización.

  4. Agregar 1 serializador.

  5. Agregar 3 callbacks de modelo o desencadenadores de eventos a nivel de servicio.

  6. Agregar manipuladores de inicializador de webhook.

  7. Agregar pruebas para crear/actualizar/eliminar.

  8. Agregar una decisión de privacidad sobre si se incluye value.

El sistema de entrega, reintentos, firmas, registro de eventos, interfaz de ping/reentrega y configuración de webhooks de administrador ya están allí. La característica se trata principalmente de hacer visibles las mutaciones de KV del artefacto para ese sistema existente.

Algo que retomar una vez implementemos Workflows, el nuevo enfoque para la automatización en Discourse.

2 Me gusta

¡Vaya, eso suena bastante interesante de verdad! ¿He perdido alguna información al respecto aquí en Meta y hay alguna cronología?