diff --git a/app/controllers/webhooks/captain/hermes_builder_callback_controller.rb b/app/controllers/webhooks/captain/hermes_builder_callback_controller.rb new file mode 100644 index 000000000..362ff2297 --- /dev/null +++ b/app/controllers/webhooks/captain/hermes_builder_callback_controller.rb @@ -0,0 +1,64 @@ +# Recebe callback do Hermes Construtor (plugin captain-http-callback). +# +# Construtor responde async via POST pra esta URL com: +# { content: "", 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-- + 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 diff --git a/app/javascript/dashboard/api/captain/hermesBuilder.js b/app/javascript/dashboard/api/captain/hermesBuilder.js new file mode 100644 index 000000000..4782271c0 --- /dev/null +++ b/app/javascript/dashboard/api/captain/hermesBuilder.js @@ -0,0 +1,22 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class HermesBuilder extends ApiClient { + constructor() { + super('captain/hermes_builder', { accountScoped: true }); + } + + fetchMessages() { + return axios.get(this.url); + } + + sendMessage(text) { + return axios.post(this.url, { text }); + } + + reset() { + return axios.delete(`${this.url}/reset`); + } +} + +export default new HermesBuilder(); diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index e346e138b..7ac74128f 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -424,6 +424,12 @@ const menuItems = computed(() => { activeOn: ['captain_roleta_index'], to: accountScopedRoute('captain_roleta_index'), }, + { + name: 'HermesBuilder', + label: t('SIDEBAR.CAPTAIN_HERMES_BUILDER'), + activeOn: ['captain_hermes_builder_index'], + to: accountScopedRoute('captain_hermes_builder_index'), + }, { name: 'Funnel', label: t('SIDEBAR.CAPTAIN_FUNNEL'), diff --git a/app/javascript/dashboard/i18n/locale/en/captain.json b/app/javascript/dashboard/i18n/locale/en/captain.json index f80e866b0..fba5e0fa8 100644 --- a/app/javascript/dashboard/i18n/locale/en/captain.json +++ b/app/javascript/dashboard/i18n/locale/en/captain.json @@ -436,6 +436,20 @@ } }, "CAPTAIN": { + "HERMES_BUILDER": { + "TITLE": "Agent Builder", + "DESCRIPTION": "Create new Hermes agents through a guided chat with the Builder.", + "HEADER_TITLE": "Agent Builder", + "HEADER_DESCRIPTION": "Chat with the Builder to create a new Hermes agent. It asks questions and saves the spec as JSON for review at the end.", + "RESET": "Clear conversation", + "RESET_CONFIRM": "Clear current conversation with the Builder?", + "EMPTY_STATE": "Type \"hello\" to start. The Builder will guide you.", + "PLACEHOLDER": "Type and press Enter to send (Shift+Enter for new line)", + "SEND": "Send", + "SESSION_LABEL": "Session:", + "SEND_FAILED": "Send failed: {message}", + "RESET_FAILED": "Failed to clear session." + }, "BANNER": { "RESPONSES": "You have used more than 80% of your responses limit. To continue using Captain AI, please upgrade.", "DOCUMENTS": "Documents limit reached. Please upgrade to continue using Captain AI." diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 065b4e0f3..d4bcc4ce5 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -350,6 +350,7 @@ "CAPTAIN_GALLERY": "Gallery", "CAPTAIN_RESERVATIONS": "Reservations", "CAPTAIN_ROLETA": "Roulette — Redeem", + "CAPTAIN_HERMES_BUILDER": "Builder (Hermes)", "CAPTAIN_FUNNEL": "Conversion Funnel", "CAPTAIN_LIFECYCLE": "Customer Journey", "CAPTAIN_REPORTS": "AI Reports", diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/captain.json b/app/javascript/dashboard/i18n/locale/pt_BR/captain.json index 183529ed2..b6a123cc1 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/captain.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/captain.json @@ -437,6 +437,20 @@ } }, "CAPTAIN": { + "HERMES_BUILDER": { + "TITLE": "Construtor de Agentes", + "DESCRIPTION": "Crie novos agentes Hermes via chat guiado com o Construtor.", + "HEADER_TITLE": "Construtor de Agentes", + "HEADER_DESCRIPTION": "Converse com o Construtor pra criar um novo agente Hermes. Ele faz perguntas e ao final salva a especificação em JSON pra revisão.", + "RESET": "Limpar conversa", + "RESET_CONFIRM": "Limpar conversa atual com o Construtor?", + "EMPTY_STATE": "Mande \"olá\" pra começar. O Construtor vai te guiar.", + "PLACEHOLDER": "Escreva e Enter pra enviar (Shift+Enter pula linha)", + "SEND": "Enviar", + "SESSION_LABEL": "Sessão:", + "SEND_FAILED": "Erro ao enviar: {message}", + "RESET_FAILED": "Falha ao limpar sessão." + }, "BANNER": { "RESPONSES": "Você usou mais de 80% do seu limite de respostas. Para continuar usando o Capitão IA, faça um upgrade.", "DOCUMENTS": "Limite de documentos atingido. Faça um upgrade para continuar usando o Capitão IA." diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/settings.json b/app/javascript/dashboard/i18n/locale/pt_BR/settings.json index ccb476592..aab1b7116 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/settings.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/settings.json @@ -349,6 +349,7 @@ "CAPTAIN_GALLERY": "Galeria", "CAPTAIN_RESERVATIONS": "Reservas", "CAPTAIN_ROLETA": "Roleta — Resgate", + "CAPTAIN_HERMES_BUILDER": "Construtor (Hermes)", "CAPTAIN_FUNNEL": "Funil de Conversão", "CAPTAIN_LIFECYCLE": "Jornada do Cliente", "CAPTAIN_REPORTS": "Relatórios IA", diff --git a/app/javascript/dashboard/routes/dashboard/captain/builder/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/builder/Index.vue new file mode 100644 index 000000000..d496d6946 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/captain/builder/Index.vue @@ -0,0 +1,316 @@ + + +