Adicione suporte a webhook/eventos para atualizações de chave-valor de artefatos do Discourse AI (ou permita que administradores desativem o sandboxing)

Gostaria de propor permitir que artefatos de IA enviem webhooks sempre que os dados armazenados em um artefato de IA forem atualizados. Existem muitas razões pelas quais isso seria vantajoso, mas tentarei manter meu argumento breve.

Muitas organizações que utilizam o Discourse também fazem uso de várias automações, e as ferramentas para automação estão se tornando cada vez mais populares, fáceis de configurar e amigáveis para iniciantes. Artefatos de IA podem conter dados preciosos que podem ser usados de forma muito eficaz nessas automações, com o bônus adicional de que já estão em JSON!

No meu caso, utilizo o n8n em uma configuração Docker Compose ao lado do meu contêiner Discourse e já o uso para automatizar uma grande variedade de coisas, incluindo ações em minhas instâncias do Discourse através de uma rede Docker. No entanto, uma das instâncias do Discourse que mantenho é para uma organização educacional/empresa, e artefatos de IA são usados para acompanhar anotações de alunos, diários de aula e assim por diante. Esse tipo de dados seria realmente benéfico para melhorar os fluxos de trabalho dos professores, criar resumos e assim por diante, por meio de uma ferramenta de automação como o n8n.

Seria tecnicamente possível fazer polling nos artefatos para atualizações, mas isso poderia facilmente começar a sobrecarregar os recursos do sistema e seria, na melhor das hipóteses, um desperdício de recursos, além de exigir habilidades técnicas extensas para configurar, o que muitos administradores não possuem.

Desnecessário dizer que isso também seria altamente benéfico para os clientes empresariais do Discourse.

Uma solução alternativa poderia ser o JavaScript do artefato chamar um webhook externo após chamar window.discourseArtifact.set(...).

No entanto, isso teria algumas limitações:

  1. É do lado do navegador e, portanto, não é autoritativo.

  2. Só é acionado se o JavaScript do artefato for executado com sucesso no navegador do usuário.

  3. Pode ser afetado por CORS, ferramentas de privacidade do navegador, bloqueadores de rede ou restrições de sandbox.

  4. O payload não pode ser confiável como vindo do Discourse, a menos que uma verificação adicional do lado do servidor seja implementada.

  5. Segredos não podem ser embutidos com segurança no JavaScript do artefato.

Um evento/webhook do lado do servidor seria muito mais confiável e seguro.

Não sou desenvolvedor Ruby, então tenho conversado com o GPT-5.5 sobre isso e ele também ofereceu algumas insights interessantes...

Com base na arquitetura atual de webhooks do Discourse, a implementação limpa provavelmente não seria uma funcionalidade personalizada de “chamar esta URL” dentro dos artefatos de IA. Deve ser implementada como um tipo de evento de webhook normal do Discourse, apoiada por um DiscourseEvent interno normal.

Melhor formato de implementação

Sugiro que os desenvolvedores do Discourse implementem isso em duas camadas:

  1. Eventos internos emitidos quando registros KV de artefatos mudam.

  2. Tipos de evento de webhook que se inscrevem nesses eventos internos e usam o sistema de entrega de webhook existente.

Isso mantém a consistência com o restante do Discourse. Os webhooks existentes já funcionam mapeando DiscourseEvents internos para chamadas WebHook.enqueue_* em config/initializers/012-web_hook_events.rb; por exemplo, eventos de tópico, post, usuário, categoria, tag, reviewable, notificação e like são conectados dessa forma.

O caminho de armazenamento do artefato é simples o suficiente: ArtifactKeyValuesController#set encontra ou inicializa um registro de chave-valor, atribui key/value/public e salva; destroy encontra o registro do usuário atual por chave e o destrói. O modelo em si é AiArtifactKeyValue, pertence a um artefato e a um usuário, possui key, value e public, e impõe unicidade por artefato/usuário/chave.

Nomes de eventos propostos

Eu provavelmente usaria três eventos de webhook específicos:

ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted

Alternativamente, um único evento poderia funcionar:

ai_artifact_key_value_changed

…mas três eventos se encaixam melhor no estilo existente do Discourse. O Discourse já possui nomes de eventos de webhook separados como post_created, post_edited, post_destroyed, calendar_event_created, calendar_event_updated e assim por diante.

Arquivos/classes que provavelmente seriam afetados

1. Adicionar novos tipos de evento de webhook

WebHookEventType atualmente define constantes numéricas, um enum group e um hash TYPES de nomes de eventos para IDs.

Eles poderiam adicionar 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,
}

Os IDs exatos ficariam a critério dos mantenedores do Discourse; eles apenas precisam evitar conflitos.

Eles também adicionariam entradas de seed em db/fixtures/007_web_hook_event_types.rb, porque os tipos de evento de webhook existentes são semeados lá com IDs, nomes e grupos.

Exemplo:

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

A interface de usuário de webhook do administrador então deve capturar isso automaticamente, porque Admin::WebHooksController#index já serializa tipos de evento ativos agrupados para a interface. O serializador de tipo de evento já expõe id, name e group.

2. Ocultar os eventos quando o Discourse AI estiver desabilitado

WebHookEventType.active já oculta eventos de webhook dependentes de plugin quando seus recursos estão desabilitados, como eventos de solved, assign, votação em tópicos, chat e calendário.

Então eles poderiam adicionar:

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

Eles também podem querer verificar uma configuração específica de artefato se existir ou for adicionada.

3. Emitir eventos internos a partir do modelo, não apenas do controlador

Embora o caminho de gravação atual seja baseado no controlador, o local robusto é provavelmente o modelo, usando callbacks de commit:

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

Callbacks no nível do modelo capturariam futuros caminhos de gravação também, não apenas ArtifactKeyValuesController#set. A parte after_commit é importante porque a entrega do webhook deve ser enfileirada apenas após a alteração no banco de dados ser confirmada com segurança.

Uma sutileza: eles provavelmente não devem acionar um evento de “atualização” quando o artefato chamar set() com o mesmo valor e nada mudar realmente. Verificar previous_changes evita webhooks ruidosos.

4. Adicionar um serializador de payload de webhook

Os webhooks do Discourse geram payloads por meio de serializadores. O método genérico WebHook.enqueue_object_hooks pode aceitar um serializador, e WebHook.generate_payload serializa o objeto com um guardião de usuário do sistema.

Eles poderiam adicionar 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

Recomendo fortemente não incluir value por padrão. Os dados KV do artefato podem ser privados por usuário, e o modelo possui uma flag public. Se o Discourse quiser suportar valores, eles poderiam tornar isso explícito:

include_value: false
include_public_values: true
include_private_values: false

Mas a versão segura v1 provavelmente deve omitir valores completamente.

5. Conectar eventos internos à entrega de webhook

Eles poderiam adicionar manipuladores a config/initializers/012-web_hook_events.rb, espelhando padrões 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

Isso reutilizaria o pipeline de trabalho de webhook existente. WebHook.enqueue_hooks encontra webhooks ativos para o evento e enfileira Jobs::EmitWebHookEvent. O trabalho já lida com entrega, retentativas, logs, cabeçalhos, assinaturas e visibilidade de administrador.

Incluir category_id e tag_ids é um toque legal porque os trabalhos de webhook existentes podem filtrar por categoria e tag. O trabalho de webhook já verifica restrições de categoria e tag antes de enviar. Como um artefato de IA pertence a um post, e o artefato pertence a um post no modelo, derivar o contexto de categoria/tópico deve ser possível.

Como o webhook entregue pode parecer

Como o corpo do webhook do Discourse usa event_type como raiz de nível superior, isso provavelmente se pareceria com:

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

O nome do evento estaria nos cabeçalhos, consistente com a entrega de webhook existente:

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

Esses cabeçalhos se alinham com a forma como EmitWebHookEvent constrói cabeçalhos de webhook hoje.

Os desenvolvedores provavelmente precisariam decidir sobre:

Os valores devem ser incluídos?
Meu voto: não por padrão. Talvez permitir apenas valores públicos, ou tornar a inclusão de valor uma configuração de administrador. Dados privados de artefato por usuário não devem sair silenciosamente do site.

Deve haver um evento ou três?
Três é mais limpo para assinantes de webhook. Um evento com um campo action é mais simples internamente. O estilo existente do Discourse tende a múltiplos nomes de evento.

A filtragem por categoria/tag deve se aplicar?
Eu acho que sim. O artefato está anexado a um post/tópico, então filtros de categoria e tag são significativos.

Isso deve ser implementado no núcleo do Discourse ou no plugin Discourse AI?
O modelo de dados vive no plugin Discourse AI, mas o registro de eventos de webhook vive no núcleo. Como plugins vinculados como solved/chat/calendar já possuem tipos de evento de webhook na lista compartilhada WebHookEventType, o Discourse pode se sentir confortável em adicionar eventos de artefato de IA lá também. O método active pode escondê-los quando o AI estiver desabilitado.

Isso também deve emitir um DiscourseEvent interno mesmo se não houver webhook?
Sim. Isso dá aos autores de plugin um ponto de conexão limpo mesmo além dos webhooks.

Complexidade

Eu enquadraria isso como moderada, mas muito contida.

O trabalho provável é:

  1. Adicionar 3 constantes de tipo de evento.

  2. Adicionar 1 grupo de webhook.

  3. Adicionar 3 registros de seed.

  4. Adicionar 1 serializador.

  5. Adicionar 3 callbacks de modelo ou acionadores de evento no nível de serviço.

  6. Adicionar manipuladores de inicializador de webhook.

  7. Adicionar specs para criação/atualização/exclusão.

  8. Adicionar decisão de privacidade sobre se value é incluído.

O sistema de entrega, retentativas, assinaturas, log de eventos, interface de ping/reentrega e configuração de webhook do administrador já estão lá. O recurso trata principalmente de tornar as mutações de KV do artefato visíveis para esse sistema existente.

Algo a ser revisitado assim que implementarmos os Workflows, a nova abordagem para Automação no Discourse.

2 curtidas

Ooooh, isso soa realmente intrigante. Perdi alguma informação sobre isso aqui no Meta e há algum cronograma?