iachat/app/controllers/webhooks/captain/hermes_builder_callback_controller.rb
Rodribm10 40fd0c8f50 feat(captain/hermes-builder): UI Vue + endpoints pra chat com Construtor
Tela "Construtor" no painel Captain (acessivel em sidebar pra admins) que
permite criar novo agente Hermes via chat guiado com agente Construtor
(profile Hermes separado).

Backend (admin scope):
- POST /api/v1/accounts/:id/captain/hermes_builder — manda mensagem do
  admin pro gateway do Construtor (Hermes na porta 8646)
- GET — retorna historico da sessao (Rails.cache, TTL 4h)
- DELETE /reset — limpa sessao
- POST /webhooks/captain/builder_callback — recebe respostas async do
  Construtor via plugin captain-http-callback do Hermes
- HermesBuilder::Storage (Rails.cache) — persiste msgs por session_key
  (account_id + user_id) com role/content/created_at
- HermesBuilder::Dispatcher — encaminha pro webhook do Construtor com
  HMAC opcional via ENV HERMES_BUILDER_WEBHOOK_SECRET

Frontend:
- Pagina Vue HermesBuilder/Index.vue — chat simples com:
  * Lista de mensagens com bubbles user/construtor
  * Indicador "digitando..." enquanto aguarda resposta
  * Input com Enter pra enviar / Shift+Enter pra nova linha
  * Polling 2s pra novas msgs
  * Botao Limpar conversa
- API client em api/captain/hermesBuilder.js
- Rota captain_hermes_builder_index (admin only)
- Item no sidebar Captain "Construtor (Hermes)"
- i18n keys CAPTAIN.HERMES_BUILDER em pt_BR + en

UX flow:
  Admin abre tela → digita "olá" → Construtor pergunta nome → admin
  responde → marca, persona, tabela (com opcao copiar de existente),
  regras, FAQs, identidade → resumo → confirmar → Construtor chama
  save_agent_spec → JSON salvo em /tmp/agent-specs/<slug>.json pra
  revisao posterior. NAO cria filesystem do profile nem registros DB
  (etapa SEPARADA, prox sessao).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:00:41 -03:00

65 lines
2.5 KiB
Ruby

# Recebe callback do Hermes Construtor (plugin captain-http-callback).
#
# Construtor responde async via POST pra esta URL com:
# { content: "<resposta>", reply_to: ..., metadata: {...}, timestamp: ... }
#
# Este controller identifica a sessão do admin (por session_id no metadata
# OU pelo cache key derivado de account_id que veio na query string) e
# armazena a resposta no Rails.cache pra UI poder ler via polling.
class Webhooks::Captain::HermesBuilderCallbackController < ApplicationController
skip_before_action :verify_authenticity_token, raise: false
def process_payload
content = params[:content].to_s.strip
return head :bad_request if content.blank?
session_key = resolve_session_key
if session_key.blank?
Rails.logger.warn('[HermesBuilder::Callback] no session_key resolvable — ignorando')
return head :ok
end
HermesBuilder::Storage.append(session_key, role: 'construtor', content: content)
Rails.logger.info("[HermesBuilder::Callback] reply received for #{session_key} (#{content.length} chars)")
head :ok
rescue StandardError => e
Rails.logger.error("[HermesBuilder::Callback] error: #{e.class}: #{e.message}")
head :internal_server_error
end
private
# Estratégia: usar o session_id do metadata (Hermes propaga o chat_id).
# Fallback: account_id da query string + último user que mandou msg
# (raro, mas evita perder resposta).
def resolve_session_key
chat_id = params[:metadata]&.[](:chat_id) || params.dig(:metadata, 'chat_id')
if chat_id.is_a?(String) && chat_id.include?('builder-')
# Formato: webhook:construtor-admin:session:builder-<account>-<user>
session_id = chat_id.split(':').last
return "hermes_builder:#{session_id}" if session_id.start_with?('builder-')
end
account_id = params[:account_id]
return nil if account_id.blank?
# Fallback: pega últimas 5 sessões do account, retorna a mais recente
# com mensagens. Aceitável pra MVP com 1 admin testando por vez.
recent_session_key_for(account_id)
end
def recent_session_key_for(account_id)
return nil unless Rails.cache.respond_to?(:redis)
pattern = "hermes_builder:builder-#{account_id}-*"
keys = Rails.cache.redis.with { |c| c.keys(pattern) }
return nil if keys.blank?
keys.first.sub(/^.*?(hermes_builder:.*)$/, '\1')
rescue StandardError => e
Rails.logger.warn("[HermesBuilder::Callback] recent_session_key fallback failed: #{e.class} - #{e.message}")
nil
end
end