Commit Graph

6317 Commits

Author SHA1 Message Date
Rodribm10
0f39945f43 feat(captain/mcp): get_assistant_scenario tool — Construtor copia identidade de outro agente
Construtor atende 'copiar maps/endereço/telefone/wifi da Lara' sem o admin
redigitar. Tool retorna o markdown bruto do scenario (default
Daniela_Reservas) do assistant fonte; LLM extrai os campos relevantes.

Cobre o gap entre get_assistant_pricing (preços estruturados) e
get_assistant_faqs (Q&As): essa retorna prompt CRU pra LLM interpretar
campos não estruturados (contatos, links, wifi, persona).

Hot-patched + USR1 no Puma e Construtor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:55:13 -03:00
Rodribm10
dd9e11da14 fix(captain/mcp): get_assistant_faqs — não filtrar por documentable_type
Filtro original só retornava FAQs com documentable_type=NULL, mas a maior
parte das FAQs aprovadas das Jasmines (Juliana, Bianca, Lara, Nina) tem
documentable_type='User' ou 'Conversation' (origem: histórico de
conversas). Resultado: tool retornava "0 FAQs aprovados" pra todos exceto
Valentina (única com FAQs criadas direto sem origem documentável).

Removido o filtro. Status='approved' já é suficiente — admin reviewou.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:49:54 -03:00
Rodribm10
59747e5400 fix(captain/mcp): tools do Construtor — typo _context: → context:
Tool registry chama tool.call(args, context: foo) mas as 3 tools read-only
do Construtor (get_assistant_faqs, get_assistant_pricing, save_agent_spec)
estavam declaradas como def call(args, _context:), com underscore. No
Ruby isso muda o nome do parâmetro keyword — ArgumentError:
'missing keyword: :_context' quando o Construtor tentava copiar FAQs/
pricing de um assistant existente.

Corrigido pra context: (sem underscore) com rubocop disable de
Lint/UnusedMethodArgument já que essas tools não usam o context.

Hot-patched via docker cp + Puma USR1 antes do deploy pro Rodrigo seguir
testando o Construtor sem esperar build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:41:23 -03:00
Rodribm10
280d250983 feat(captain/hermes): script hermes-provision pra Construtor autônomo
Bash script em bin/hermes-provision (instalado em /usr/local/bin/ na VPS)
que recebe spec JSON via stdin e provisiona um agente Hermes ponta-a-ponta:
- Valida spec (slug regex, preços 0-5000, períodos/buckets do catálogo)
- Aloca porta livre no range 8650-8699
- Gera HMAC secret via openssl rand
- Cria Captain::Unit (find_or_create), Captain::PricingCategory/Amount,
  Captain::Assistant (engine=hermes) via docker exec rails runner
- Copia template profile da Valentina, patcheia config.yaml com porta +
  X-Captain-Assistant-Id (parent_id se setado, senão self id)
- Escreve SOUL.md/SKILL.md do spec
- Gera webhook_subscriptions.json com secret
- Cria systemd unit hermes@<slug>.service e enable+start
- rsync profile pra repo de backup git local
- Idempotente: re-rodar com mesmo slug não duplica nada (find_or_create)
- --dry-run valida sem escrever
- --rollback <slug> destrói tudo (DB + systemd + filesystem)

Construtor (Hermes daemon) chama via terminal skill nativa:
  echo '<spec>' | /usr/local/bin/hermes-provision

Próximo passo: atualizar SOUL.md/SKILL.md do Construtor pra invocar
o script ao final do fluxo socrático.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:59:30 -03:00
Rodribm10
7995bc6fe6 feat(captain): pricing em DB + colunas de provisionamento Hermes
Migra a tabela de preços do PricingTables.rb hardcoded pras tabelas
captain_pricing_categories + captain_pricing_amounts no DB. Mantém a
mesma API pública Captain::Mcp::PricingTables.calculate(...) — código
chama o banco via novos modelos Captain::PricingCategory e
Captain::PricingAmount.

Seed db/seed_pricing_tables.rb faz backfill idempotente pra Dolce Amore
(unit 4) e Express (unit 5) com a mesma estrutura que tava no Ruby.

Adiciona em captain_assistants:
- hermes_subscription_secret (gerado pelo script de provisionamento)
- hermes_port (alocado no range 8650-8699)
- parent_assistant_id (link informativo Hermes → captain_interno parent
  pra sombrear FAQs/scenarios via header X-Captain-Assistant-Id)

Adiciona em captain_units: extra_person_fee + currency.

Primeiro milestone do roadmap arquitetural pro Construtor autônomo
(decisões em memory/project_construtor_autonomo_decisions.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:04:50 -03:00
Rodribm10
a2bb613e68 feat(captain/hermes): debounce — inbox.typing_delay como buffer + agrupar msgs
Antes: inbox.typing_delay funcionava só pro Captain interno
(schedule_internal_response). Hermes ignorava o campo e disparava
OutgoingJob na hora — campo da UI era cosmético pra inboxes Hermes.

Agora:
- schedule_hermes_response cancela jobs OutgoingJob pendentes pra mesma
  conversa e enfileira com wait=inbox.typing_delay (debounce window).
- OutgoingJob agrupa todas msgs incoming entre a última resposta real
  do agente (ignora reactions) e a msg âncora; dispatch envia o texto
  concatenado pro Hermes via novo content_override no Client#dispatch.

Resultado: cliente que digita "Oi" + "quero pernoite Master" em segundos
vê o agente esperar até o buffer vencer e responder UMA vez cobrindo
ambas as falas, em vez de 2 respostas atropelando o pensamento.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:46:24 -03:00
Rodribm10
e662913b21 fix(captain/hermes): auto-react idempotente — bloqueia retry duplicate
OutgoingJob faz retry no DispatchError (até 3x ActiveJob + Sidekiq).
Cada retry chamava AutoReactService.maybe_react! e criava uma reaction
nova — observado em prod 02/05 quando o env var
CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_5 estava faltando, gerando
401 → 6 reações duplicadas no inbox EXPRESS.

Adiciona guard already_reacted? que checa se já existe Message outgoing
com external_source='hermes_auto_react' e in_reply_to=msg.id antes de
criar uma nova. Defesa contra futuro 5xx/timeout do Hermes daemon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:32:47 -03:00
Rodribm10
3182002bd9 feat(captain): engine column + DB-driven Hermes routing + Express pricing
Marca cada Captain::Assistant com engine ('captain_interno' | 'hermes')
e move o roteamento Hermes do env var pro banco — admin troca engine
re-apontando a inbox no painel, sem deploy. Mantém fallback pras env
vars antigas (CAPTAIN_HERMES_INBOX_IDS etc) durante a migração gradual,
pra não quebrar Valentina antes da re-associação.

Frontend: badge "Hermes" (âmbar) ou "Interno" (cinza) ao lado de cada
assistant no dropdown switcher e no card da listagem, com chaves i18n
em en + pt_BR.

Tabela de preço (pricing_tables.rb): adiciona unit Express (id=5) e
estende a estrutura pra aceitar preço por dia da semana
(mon_wed/thu_sun) — necessário pro Express, retrocompatível com Dolce
Amore (preço único).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 07:54:01 -03:00
Rodribm10
5f6aed05c9 fix(captain/hermes-builder): callback resolve session via last_session
Bug: Hermes nao propaga chat_id no metadata do callback. Callback
controller nao conseguia resolver qual session armazenar a resposta.
WARN "no session_key resolvable — ignorando" descartava todas as
respostas do Construtor.

Fix: HermesBuilder::Storage.remember_last_session() grava ultima
session por account quando admin chama /start ou /create. Callback
le essa key via last_session_for(account_id). MVP-safe pra 1 admin
por conta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:53:43 -03:00
Rodribm10
f362949579 fix(captain/hermes-builder): troca componente Button por <button> HTML
Componente Button (components-next/button) provavelmente nao propaga
@click direito ou tem quirk com variant="primary" (nao esta em
VARIANT_OPTIONS que aceita solid|outline|faded|link|ghost). Botao
"Iniciar criação" nao disparava startSession.

Troca por <button> HTML simples com classe start-button estilizada.
Garante que @click vai direto pro handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:40:44 -03:00
Rodribm10
cb067e1f17 feat(captain/hermes): auto-react cobre saudacoes e despedidas
Estende AutoReactService com 2 padroes novos:
- GREETING_REGEX (bom dia, boa noite, oi, ola, hello) -> 👋
  apenas na PRIMEIRA mensagem da conversa pra nao ficar forcado
- FAREWELL_REGEX (tchau, ate logo, abraço, bjs, flw) -> ❤️

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:21:43 -03:00
Rodribm10
041766b427 fix(captain/hermes-builder): namespace controller correto + botao iniciar
Bug: controller estava em enterprise/api/v1/accounts/captain/ com namespace
Enterprise:: — convencao Chatwoot eh enterprise/app/controllers/api/v1/...
direto, classe Api::V1::Accounts::Captain::HermesBuilderController. Sem
namespace Enterprise::. 404 acontecia porque rotas registravam Captain::
sem prefixo Enterprise::.

Move controller pro path correto. Remove diretorios vazios criados.

UX: adiciona endpoint POST /start que envia comando-gatilho oculto pro
Construtor comecar fluxo socratico — admin nao precisa digitar primeira
mensagem. Vue mostra empty state com botao "Iniciar criacao" em vez de
exigir mensagem inicial.

i18n keys novas: START + EMPTY_STATE atualizado em pt_BR + en.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:06:04 -03:00
Rodribm10
bfa06597f2 feat(captain/hermes): auto-react deterministico antes do LLM
Pattern matching no Captain dispara reaction <1s sem esperar Codex
processar. Resposta de texto continua vindo do Hermes normalmente —
auto-react eh COMPLEMENTAR.

Padroes detectados (no incoming message):
- "obrigado/valeu/vlw/thanks" -> 🙏
- "ok/fechado/perfeito/blz/combinado" -> 👍
- attachment image (msg sem texto) -> 😍
- attachment audio (msg curta) -> 🙏

Conservative: só dispara em casos CLAROS. Em duvida, deixa pro LLM
decidir via react_to_message tool.

Plugado em Captain::Hermes::OutgoingJob#perform — chamado antes do
dispatch pro Hermes. Falha silenciosa (rescue) — nao bloqueia fluxo.

SOUL.md atualizado: regra de frequencia (~30% das msgs) + nota sobre
auto-react pra LLM nao duplicar reacao em casos cobertos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:59:28 -03:00
Rodribm10
c7300dfbcf fix(captain/hermes): nao enviar typing_off explicito
Antes: typing_on -> sleep delay -> create msg DB -> typing_off -> SendReplyJob
envia via wuzapi -> msg chega no celular (2-5s depois).

Resultado visual quebrado: cliente ve "digitando..." sumir antes da msg
chegar, gap visivel.

Agora: typing_on -> sleep -> create msg -> deixa rolar. WhatsApp do
cliente cancela typing automaticamente quando msg eh entregue. Sequencia
fica natural.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:29:07 -03:00
Rodribm10
35358a42b1 fix(captain/hermes-builder): i18n keys no caminho correto
Convencao Chatwoot pra features Captain novas: keys no root level
(CAPTAIN_HERMES_BUILDER, igual a CAPTAIN_ROLETA, CAPTAIN_FUNNEL etc) e
nao como sub-key de CAPTAIN.

Move CAPTAIN.HERMES_BUILDER -> CAPTAIN_HERMES_BUILDER em pt_BR e en.
Atualiza referencias t() no Vue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:17:53 -03:00
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
Rodribm10
d35084334c feat(captain/mcp): tools read-only pro Skill Construtor
4 tools novas (admin scope, read-only) usadas pelo profile Hermes
"construtor" durante criacao guiada de novos agentes:

- list_assistants: lista todos os assistants da conta com badge engine
  (Captain interno vs Hermes), counts de scenarios/FAQs, marca/unidade
- get_assistant_pricing: retorna tabela de preços parsed (Captain::Mcp::
  PricingTables se Hermes, scenario fallback se Captain interno)
- get_assistant_faqs: retorna FAQs aprovados em markdown (até 50)
- save_agent_spec: salva JSON da especificacao em /tmp/agent-specs/<slug>.json
  (NÃO cria filesystem do profile nem registros DB — só persiste spec
  pra revisao/provisionamento separado depois)

Construtor coleta perguntas socraticas, oferece "copiar de existente" pra
acelerar quando marca for igual (Prime VL/AL/ADE preços iguais), e ao
final salva spec.json pra admin revisar antes de provisionar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:24:02 -03:00
Rodribm10
48fad2977b feat(captain/hermes): payload enriquecido + humanizadores + notif Pix proativa
Captain::Hermes::Client (enterprise/app/services/captain/hermes/client.rb):
- text_for_hermes: transcreve audio via Whisper antes de enviar pro Hermes
  (reusa Captain::OpenAiMessageBuilderService)
- image_urls_for_hermes: URLs publicas de imagens da message; plugin
  captain-webhook do Hermes baixa em /tmp/ e popula event.media_urls pra
  vision multimodal (gpt-4o-mini auxiliary)
- contact_history_snapshot: dados eager pro [ctx] (last_reservation_*,
  total_conversations, ultima_suite, etc) — memoria do contato direto no
  prompt sem precisar tool call
- notify_event + build_event_payload: dispara webhook sintetico pro Hermes
  pra eventos do sistema (Pix pago etc) — Valentina manda mensagem
  espontanea sem cliente perguntar

Captain::Payments::ConfirmationService:
- Hook notify_hermes_proactively! enfileira NotifyPaymentConfirmedJob
  apos confirmacao de Pix, somente se inbox estiver no fluxo Hermes
  (Captain interno continua igual sem mudanca)

Captain::Hermes::NotifyPaymentConfirmedJob (NOVO):
- Monta system_message "[SISTEMA: pagamento_confirmado]\n..." e dispara
  webhook pro Hermes Valentina
- Valentina (via SOUL.md) interpreta como evento do Captain e manda
  mensagem celebrativa pro cliente

Captain::Hermes::DelayedReplyJob (NOVO) — humanizadores:
- Liga indicador "digitando..." (composing) via wuzapi
- Aguarda delay configuravel via Captain::Assistant.config['response_delay']
  (modos: none, fixed, typing_simulation com chars_per_second + min/max)
- Posta msg outgoing
- Desliga typing
- Fallback no HermesCallbackController posta direto se class nao carregada

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:15:50 -03:00
Rodribm10
9ed3491d55 feat(captain/mcp): suite de 9 tools MCP + pricing tables Dolce Amore
Tools novas em enterprise/app/services/captain/mcp/tools/:
- generate_pix: Pix Inter via PricingTable + fallback link reserva-1001
- check_pix_payment: consulta Inter + dispara ConfirmationService
- send_suite_images: fotos da galeria (Captain::GalleryItem) via wuzapi
- reschedule_reservation: remarca reserva (regra 3h antecedência Dolce)
- update_contact: persiste nome/CPF/email/notas no Contact
- get_contact_history: markdown do histórico do cliente on-demand
- react_to_message: reage com emoji via wuzapi (is_reaction=true)

Captain::Mcp::PricingTables: tabela hardcoded Dolce Amore (8 categorias x 4
periodos + regras de pessoa extra). Backend e fonte de verdade — LLM nao
inventa preco.

add_label_tool: cria Label oficial automaticamente se nao existir, aceita
conversation_id em arguments.

mcp_controller: aceita X-Captain-Account-Id/Assistant-Id/Inbox-Id como
fallback de contexto.

tool_registry: 9 tools ativas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:13:16 -03:00
Rodribm10
60759b955c fix(captain): força provider :openai quando há config dedicada de embedding
RubyLLM auto-detecta provider pelo prefixo do nome do modelo (ex:
`gemini-*` → provider Gemini → exige `gemini_api_key`). Quando temos
config dedicada de embedding (CAPTAIN_EMBEDDING_API_KEY) apontando pra
endpoint OpenAI-compatible (ex: Gemini OpenAI-compat em
generativelanguage.googleapis.com/v1beta/openai), queremos que o RubyLLM
mande a request via OpenAI client mesmo que o nome do modelo bata com
outro provider.

Solução: passar provider: :openai e assume_model_exists: true ao chamar
embed quando dedicated_embedding_config? retornar true. Sem isso, o
RubyLLM falha com `Missing configuration for Gemini: gemini_api_key`
mesmo com a key correta setada.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:18:50 -03:00
Rodribm10
2ade066468 feat(captain): EmbeddingService aceita provider de embedding dedicado
Permite trocar provider de embedding sem afetar o provider de chat. Útil
quando OpenAI key tradicional está fora (ban, billing, etc) mas você
quer usar outro provider OpenAI-compatible só pra embeddings — exemplo
clássico: Gemini OpenAI-compatible em
https://generativelanguage.googleapis.com/v1beta/openai com modelo
gemini-embedding-001 + dimensions=1536 (pra bater com schema pgvector).

Env vars novas (com fallback pro legacy_openai_settings se não setadas):

  CAPTAIN_EMBEDDING_API_KEY     — API key dedicada pra embeddings
  CAPTAIN_EMBEDDING_ENDPOINT    — base URL sem /v1 (default herda OpenAI)
  CAPTAIN_EMBEDDING_DIMENSIONS  — força redução do vector (ex: 1536)

Quando CAPTAIN_EMBEDDING_API_KEY está vazia, comportamento é idêntico ao
de antes (legacy_openai_settings). Backward-compatible.

Também aceita as variáveis via InstallationConfig (UI) ou ENV — ENV tem
precedência (padrão Chatwoot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:08:18 -03:00
Rodribm10
8fab08ba57 fix(captain): regra de pessoa extra do Dolce Amore — taxa a partir da 3ª
Bug reportado por cliente real: "valor extra não é a partir da 3ª pessoa?
Porque aí é para casal, ou seja sempre vai ter duas pessoas".

Cliente está certo. A base do quarto pra casal JÁ INCLUI 2 pessoas; taxa
extra começa na 3ª pessoa, não na 2ª. A documentação anterior (modelo +
skill Hermes + scenario 21 em prod) tinha "a partir da 2ª pessoa", o que
fazia Valentina cobrar extra pra ambos do casal — erro.

Mudanças:
- Texto da regra: "a partir da 3ª pessoa" pra Apartamento/Suítes/Mini
  Chalé 45, com nota explícita "base do quarto já inclui 2 pessoas".
- Exemplo de cálculo refeito: 4 pessoas Suíte Master pernoite sex/sáb =
  180 + (2 × 45) = R$ 270 (era 180 + 3×45 = 315, errado).

Categorias maiores (Chalé 2 Suítes, Suíte Ouro, Chalé Master 4 Suítes)
mantidas como antes (4ª e 8ª pessoa) — Rodrigo precisa revisar essas
separadamente já que cada uma tem capacidade diferente.

Aplicado em 3 lugares:
- db/seed_prompts/_modelos/scenarios/jasmine_dolce_amore__daniela_reservas.md
- /root/.hermes/profiles/valentina/skills/dolce-amore-reservas/SKILL.md
- DB do Captain prod scenario 21 (via update_columns, +363 chars)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:52:09 -03:00
Rodribm10
713bb16012 feat(captain): MCP controller aceita Bearer token além de HMAC
Hermes Agent (cliente MCP) usa `--auth header` que envia
`Authorization: Bearer <token>` — padrão MCP. Antes o Captain MCP server
só aceitava HMAC-SHA256 (X-Hub-Signature-256), incompatível com o que
Hermes manda nativamente.

Agora aceita qualquer um dos 2 modos:
- Bearer token (recomendado, padrão MCP) — Hermes envia automaticamente
- HMAC-SHA256 do body — pra clientes que preferem assinar payload

Ambos validados com ActiveSupport::SecurityUtils.secure_compare contra
o mesmo CAPTAIN_MCP_SECRET. Sem secret = validação desabilitada (PoC/dev).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:42:57 -03:00
Rodribm10
23911ea878 feat(captain): MCP server (HTTP) expondo tools pro Hermes Agent
Implementa servidor MCP (Model Context Protocol) HTTP no Captain pra
o Hermes Agent invocar tools do Captain via `hermes mcp add`. Substrato
pra integração de Nível 2 onde o agente consulta tools quando precisa
executar ações reais (buscar FAQ, adicionar label, futuramente Pix etc).

Arquivos:

- app/controllers/webhooks/captain/mcp_controller.rb
  Endpoint POST /webhooks/captain/mcp. Valida HMAC (CAPTAIN_MCP_SECRET),
  parseia JSON-RPC, despacha pro Server. Extrai params._captain_context
  com multi-tenant ids (conversation_id, inbox_id, account_id, etc).

- enterprise/app/services/captain/mcp/server.rb
  Subset MCP suficiente: initialize, tools/list, tools/call, ping,
  notifications/initialized. JSON-RPC síncrono (sem SSE).

- enterprise/app/services/captain/mcp/tool_registry.rb
  Lista centralizada de tools.

- enterprise/app/services/captain/mcp/tools/base_tool.rb
  Interface base pras tools (.name, .description, .input_schema, #call).

- enterprise/app/services/captain/mcp/tools/add_label_tool.rb
  Tool 1: aplica label na conversation atual.

- enterprise/app/services/captain/mcp/tools/faq_lookup_tool.rb
  Tool 2: busca semântica em FAQs (Captain::AssistantResponse via pgvector
  cosine). Reaproveita SearchReplyDocumentationService pra paridade com
  o caminho legado do Captain.

- config/routes.rb
  Rota POST /webhooks/captain/mcp.

Conexão pelo Hermes:
  hermes mcp add captain-tools --url http://CAPTAIN_HOST/webhooks/captain/mcp

Auth: HMAC X-Hub-Signature-256 quando CAPTAIN_MCP_SECRET setado.

TODO próxima sprint: generate_pix_tool, send_suite_images_tool. Handoff
fica via automation hoje (UI Chatwoot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:32:38 -03:00
Rodribm10
cd519a73c4 fix(captain): converte markdown bold pra formato WhatsApp no callback Hermes
Hermes (e LLMs default em geral) emitem **negrito** no formato markdown
padrão. WhatsApp usa formato próprio: *negrito* (single asterisk). Sem
conversão, o cliente vê asteriscos literais no WhatsApp, parecendo bug.

Defesa em camadas:
1. SOUL.md da Valentina foi atualizado com regra explícita de formato
   WhatsApp (single asterisk pra bold, underscore pra itálico, etc).
2. Este controller faz normalização defensiva no callback recebido do
   Hermes: regex `**texto**` -> `*texto*` antes de criar a mensagem
   outgoing. Não afeta o resto do conteúdo.

normalize_for_whatsapp() é trivialmente reversível e idempotente
(executar 2x é igual a 1x).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:24:57 -03:00
Rodribm10
d781f4a048 feat(hermes): plugin captain-webhook (stable session_chat_id)
Plugin pro Hermes Agent que SUBSTITUI o WebhookAdapter built-in pra
suportar session_chat_id estável derivado de campo no payload.

Por que existe
--------------
O WebhookAdapter built-in monta a chave de sessão como:

    session_chat_id = f"webhook:{route}:{delivery_id}"

delivery_id é único por POST → cada msg cria sessão nova no Hermes. OK pra
webhooks one-shot, ERRADO pra integração de chat onde múltiplas mensagens
da mesma conversa precisam compartilhar memória de sessão.

Como funciona
-------------
Quando o caller (Captain) inclui `conversation_id` ou `hermes_session_id`
no payload, o plugin reescreve chat_id pra:

    session_chat_id = f"webhook:{route}:session:{conversation_id}"

Mesma conversation_id em múltiplas POSTs → mesma sessão Hermes →
contexto e memória preservados. Sem o campo, fallback ao comportamento
default (session nova por POST). 100% backward-compatible.

Implementação
-------------
- kind: platform — registra com name="webhook" pra substituir built-in
  (Hermes prioriza platform_registry sobre código built-in em
  gateway/run.py:_create_adapter)
- Herda WebhookAdapter — só override `handle_message` (rewrite chat_id)
  e `connect` (recupera gateway_runner via _gateway_runner_ref pq o
  plugin path não seta isso explicitamente)
- Outros adapters (HMAC, rate limit, idempotency, parsing, deliver
  dispatch) — herdados sem cópia

Validado end-to-end na VPS (profile valentina):
- POST com conversation_id=99999 (msg 1) → session:99999 criada
- POST com conversation_id=99999 (msg 2) → MESMA session reutilizada
- Hermes responde via Codex em ~10s (2 turnos cumulativos)
- http_callback faz POST de volta no Captain (HTTP 200)
- Logs mostram: [captain-webhook] Stable session: ... -> session:99999

Combinado com captain-http-callback, completa o ciclo Captain ↔ Hermes:
Captain manda webhook com conversation_id → Hermes processa em sessão
estável → http_callback POSTa resposta de volta → Captain envia ao
WhatsApp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:16:05 -03:00
Rodribm10
35de8b7fde feat(captain): cliente Captain ↔ Hermes (outgoing job + callback endpoint)
Implementa o lado Captain da integração Nível 2 (Hermes como cérebro).
Ativação por inbox via env var CAPTAIN_HERMES_INBOX_IDS — inboxes não
listadas seguem usando o orquestrador interno do Captain (Daniela_Reservas
etc) sem mudança alguma. Princípio "só adiciona, não retira".

Componentes:

- enterprise/app/services/captain/hermes.rb
  Módulo helper de config (env vars, URLs, secrets per-inbox).

- enterprise/app/services/captain/hermes/client.rb
  Service que monta payload (msg + contexto da conversa/inbox/contato) e
  faz POST autenticado via HMAC-SHA256 (X-Hub-Signature-256) no webhook
  do Hermes Agent (porta 8644). DispatchError em falha de rede/HTTP.

- enterprise/app/jobs/captain/hermes/outgoing_job.rb
  Wrapper Sidekiq do Client. Retry 3x em DispatchError.

- app/controllers/webhooks/captain/hermes_callback_controller.rb
  Recebe callback do plugin captain-http-callback do Hermes. Valida HMAC
  se CAPTAIN_HERMES_CALLBACK_SECRET setado, identifica conversation pela
  última pending da inbox (janela 5min) e cria mensagem outgoing.

- config/routes.rb
  Rota POST /webhooks/captain/hermes_callback (fora de /api/v1/accounts).

- enterprise/app/services/enterprise/message_templates/hook_execution_service.rb
  Branch novo no schedule_captain_response: se Hermes habilitado pra inbox,
  dispara HermesOutgoingJob; senão, fluxo Captain interno como antes.

Env vars (todas opcionais; sem set = Hermes desabilitado em todas inboxes):
- CAPTAIN_HERMES_INBOX_IDS (CSV de inbox.id)
- CAPTAIN_HERMES_WEBHOOK_BASE_URL (default http://172.17.0.1:8644)
- CAPTAIN_HERMES_CALLBACK_SECRET (HMAC validar callbacks de entrada)
- CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id> (HMAC assinar saídas)

Limitação: identificação da conversation no callback usa última pending
da inbox dentro de 5min. OK pra PoC com 1 conversa de teste por vez. Em
produção, melhorar mapeando delivery_id ↔ conversation_id em Redis.

Próximo passo manual (admin VPS): criar subscription no Hermes:
  hermes webhook subscribe captain-inbox-1 \\
    --prompt 'Cliente disse: {message}. Responda como Daniela ...' \\
    --deliver http_callback \\
    --deliver-chat-id 'http://CAPTAIN_HOST/webhooks/captain/hermes_callback?inbox_id=1'

Depois set CAPTAIN_HERMES_INBOX_IDS=1 + CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_1
no stack do Captain e testar pela inbox Angelina.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:22:22 -03:00
Rodribm10
89b471831d feat(hermes): plugin captain-http-callback (HTTP delivery adapter)
Adiciona plugin externo pro Hermes Agent que entrega a resposta do agente
como POST HTTP a uma URL configurável — em vez de empurrar pra plataforma
de mensageria (Telegram, Slack, etc) como o Hermes faz por default.

Por quê:
O Hermes nativamente entrega respostas em plataformas conhecidas. Quando
integramos o Hermes como cérebro de outro backend (Captain / Chatwoot),
precisamos da resposta de volta via HTTP pro backend continuar o fluxo
(mandar pro cliente WhatsApp, atualizar conversa, etc). O Hermes não tem
deliver type "http_callback" built-in, então criamos via API de plugin
oficial deles (kind: platform).

Arquivos:
- plugin.yaml — manifest (kind=platform)
- __init__.py — entrypoint (re-exporta register)
- adapter.py — HttpCallbackAdapter implementando BasePlatformAdapter
- README.md — uso, formato do POST, signing HMAC opcional

Como funciona:
1. Backend (Captain) → POST /webhooks/<rota> no Hermes (entrada)
2. Hermes processa via Codex/Anthropic/Gemini conforme config dele
3. Hermes invoca este plugin (deliver=http_callback)
4. Plugin POSTa resposta na URL configurada via --deliver-chat-id
5. Backend recebe e roteia pro destinatário real

Validado end-to-end no Hermes da VPS com:
- Subscription criada via `hermes webhook subscribe ... --deliver http_callback`
- POST simulando msg do cliente → resposta chegou no servidor de teste
  em ~11s (tempo de processamento via subscription Codex)
- Plugin enabled e descoberto via `hermes plugins list`

Próximo passo (separado, em outro PR): cliente Captain (outgoing + incoming
endpoint) que conecta o Captain ao Hermes via este plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:13:15 -03:00
Rodribm10
7700afd508 feat(captain): adiciona Hermes Gateway como 3ª opção de LLM provider
Acrescenta valor 'openai_hermes_gateway' ao CAPTAIN_LLM_PROVIDER, sem mexer
nas opções existentes (openai_api e openai_codex_oauth continuam intactos).

Quando ativado, o Captain chama o Hermes Agent rodando em modo gateway HTTP
local (CAPTAIN_HERMES_GATEWAY_URL, default http://host.docker.internal:9877).
O Hermes faz o roteamento multi-modelo (Codex/Anthropic/Gemini) usando o
OAuth dele em ~/.hermes/auth.json — o Captain não precisa fazer OAuth direto.

Configs novas em installation_config.yml:
- CAPTAIN_HERMES_GATEWAY_URL — URL do gateway (default host.docker.internal:9877)
- CAPTAIN_HERMES_GATEWAY_MODEL — modelo no formato <provider>/<model>
- CAPTAIN_HERMES_GATEWAY_API_KEY — opcional, dummy se gateway local não exige

Embeddings e Files API continuam apontando pra OpenAI tradicional via
legacy_openai_settings — Hermes Gateway não expõe esses endpoints.

Specs cobrem: dummy key, custom api_key override, custom model, defaults,
trailing slash strip, light_model por provider, hermes_gateway? predicate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:24:31 -03:00
Rodribm10
9b8ef5a828 feat(captain): Dolce Amore como nova unidade + handoff silencioso PrimeAL/Dolce
Dolce Amore (Valentina, assistant 6):
- Novo assistant principal + 5 cenários (daniela_reservas, disponibilidade_suites,
  maria_fotos, outras_unidades, reclamacoes_ouvidoria) com tabela de preços
  motel-first (Apartamento, Master, Luxo, Temática, Mini Chalé 45, Chalé 2 Suítes,
  Chalé Master 4 Suítes, Suíte Ouro), regras Inter Pix, e tom adequado a motelaria.
- Cross-reference da Dolce nas 4 outras unidades (Express, PrimeAL, PrimeVL, Qnn01)
  no scenario outras_unidades, com aviso "use só se cliente perguntar por Natal".

Handoff silencioso PrimeAL + Dolce (Bianca + Valentina):
- Mensagem ÚNICA "Um momento." substitui as antigas frases robotizadas
  ("vou te encaminhar pra atendente local...").
- Nova regra "ROTEIE PRO CENÁRIO PRIMEIRO": orquestradora deve sempre tentar
  rotear pra cenário (handoff_to_daniela_reservas, handoff_to_maria_fotos, etc)
  antes de considerar handoff humano.
- Nova regra "NA DÚVIDA, TRANSFERE": após descartar todos os 6 cenários, se a
  pergunta não cabe em nenhum deles, handoff silencioso pra humano.
- Proibição explícita de mencionar nomes de cenário (Daniela, Maria) pro cliente.
- maria_fotos: REGRA #0 — se foto pedida não está na galeria (numeração inexistente,
  característica fora do mapa, área não-suíte), responde "Um momento." + handoff
  em vez de oferecer alternativa.
- daniela_reservas: novo gatilho de handoff "pergunta sobre reserva fora do prompt".

Rollback documentado em docs/captain/rollbacks/2026-05-01_handoff_silencioso.md
com snapshot completo dos prompts ANTES da mudança em prompts_snapshot.json,
permitindo reversão via update_columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:22:08 -03:00
Rodribm10
1b31e88934 chore(captain): ajustes de unit + migration + schema + seed README
Pequenos ajustes em Captain::Unit (app + enterprise), migration de seed
inicial dos prompts Jasmine/Daniela, schema regenerado, e atualização do
README de seed_prompts pra refletir o estado atual dos modelos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:21:38 -03:00
Rodribm10
d12d8bc0b6 feat(reports): mantém filtros do BotReports na URL sem trocar de rota
BotReports agora carrega inboxes no mount e passa navigate-on-entity-filter=false
pro ReportFilters, que ganha prop pra atualizar a URL com os filtros aplicados
sem disparar router.push pra outra rota. Permite compartilhar/recuperar visão
filtrada do BotReports via URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:21:10 -03:00
Rodribm10
4e798944cf fix(captain): provision unit via RPC SECURITY DEFINER (RLS bypass)
Anon key não tinha permissão de INSERT em reserva_hotel.unidades — RLS
exige authenticated + tenant_member, não atendido. POST direto falhava
sem feedback útil.

Solução: RPC reserva_hotel.provision_unidade(...) com SECURITY DEFINER
que faz upsert idempotente bypassando RLS, com validações de tenant +
marca dentro da função. EXECUTE granted to anon.

Service agora chama /rpc/provision_unidade em vez de POST /unidades.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:11:57 -03:00
Rodribm10
c5cd15665e feat(captain): provisionamento automático de Captain::Unit em reserva_hotel.unidades
Hook after_commit on:create no Captain::Unit dispara
ProvisionUnitInSupabaseJob, que upserta a unit em reserva_hotel.unidades
via Supabase REST (UNIQUE on tenant_id+chatwoot_unit_id) e grava IDs no
Captain::Unit (supabase_unit_id, supabase_tenant_id, supabase_marca_id).

Sem isso, criar nova unidade no painel Pix não habilitava roleta — a row
no Supabase ficava ausente e OfferService caía em "tenant não resolvido".

Inclui rake captain:reprovision_unit_in_supabase[id] + provision_all
pra reconciliação manual e migration retroativa.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:21:20 -03:00
Rodribm10
e7f3723938 fix(captain): Accept-Encoding identity nos clients Supabase (gzip silencioso)
Supabase REST manda response gzip por default. Faraday default não tem
middleware de descompressão, então JSON.parse(gzip_bytes) explodia em
JSON::ParserError, capturado pelo rescue → array vazio silencioso.

OfferService#fetch_unidade retornava [] mesmo com row presente,
caindo em "Sem unidade vinculada — tenant não resolvido".

Fix em offer_service, weekly_report_service, notify_revealed_job e
notify_revealed_scheduler_job. (get_reserva_preco_tool já tinha o fix.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:21:10 -03:00
Rodribm10
f0db8b0361 fix(captain): captain_inbox.unit -> captain_unit (bug bloqueante roleta + tools)
CaptainInbox.belongs_to :captain_unit (não existe método .unit).
OfferService quebrava antes de criar draw — roleta nunca disparava
em prod mesmo após pix confirmado. Mesmo bug em get_reserva_preco_tool
e create_reservation_intent_tool (silenciosamente caíam em fallback nil).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:11:00 -03:00
Rodribm10
f8709ab7a5
fix(captain): evita callback de embedding ao excluir FAQ (#13)
Co-authored-by: Kilo-Oracle <kilo-oracle@kilo-stack.local>
2026-04-30 13:14:48 -03:00
Rodribm10
c8574d2d7c
chore: adiciona comentário de status no README (#12) 2026-04-29 22:23:30 -03:00
Rodribm10
4f31e11b86
Merge pull request #11 from Rodribm10/kilo/auto-fix-erro-adicionar-faq
fix(captain): evita erro ao adicionar FAQ em conversa
2026-04-27 14:52:06 -03:00
Kilo-Oracle
60079a1b9e fix(captain): evita erro ao adicionar FAQ com pergunta longa
Some checks failed
Build and Push to GHCR (multi-arch) / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / merge (push) Has been cancelled
2026-04-27 15:12:09 +00:00
Rodribm10
21f5fcce6a fix(retention): cohort endpoint 500 — Pundit policy + SQL binding
Dois bugs que faziam o cohort retornar 500 e a página de Retenção mostrar
"Falha ao carregar cohort":

1. `Captain::AssistantPolicy` não tinha `cohort?` → Pundit batia em
   NoMethodError no `check_authorization`. Adicionado como leitura pública
   da assistente, igual `show?`/`playground?`.

2. `RetentionCohortService#cohort_activity` chamava `exec_query(sql, name, [@account.id])`
   passando array de valores onde a API espera bind objects. A SQL ainda
   interpolava `account_id = $1` direto na string (sem placeholder ligado).
   Migrado pra `ActiveRecord::Base.sanitize_sql_array` com `?`, igual ao
   resto da base. Mantém parametrização e remove acoplamento com posicional.

Validado em prod via hot-patch (USR2): GET retention/cohort agora 200 com
3 cohorts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:46:20 -03:00
Rodribm10
d831ee4d33 feat(reports): Painel Diretoria — Onda 1A (leitura)
Primeira onda do roadmap de indicadores executivos do Grupo Nova. Mede
ADOÇÃO DO CANAL DIGITAL, não a operação total — banner explícito alerta
que reservas fechadas manualmente na recepção ainda não estão capturadas
(Onda 1B vai adicionar marcação manual via botão na conversa).

Backend:
- V2::Reports::ConversionFunnelBuilder — leads (novo/retorno/total),
  reservas (criadas != draft, pagas in active/completed/confirmed),
  taxas de conversão. Filtro opcional por inbox.
- V2::Reports::InboxBenchmarkingBuilder — uma linha por inbox com
  brand_name (via Captain::UnitInbox -> Unit -> Brand)
- Endpoints GET /reports/conversion_funnel e /reports/inbox_benchmarking
- RSpec do ConversionFunnelBuilder

Frontend:
- Rota top-level Reports → Painel Diretoria
- DirectoryDashboard.vue: banner de adoção + filtros + cards + funil + tabela
  benchmarking agrupada por marca com variação vs média
- API client getConversionFunnel + getInboxBenchmarking
- i18n EN + PT

Memórias suporte: feedback_metricas_adocao_canal.md + project_painel_diretoria_roadmap.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:44:59 -03:00
Rodribm10
617eadbfe4 feat(reports): breakdown auto/manual no BotReports + nomes corretos
Engrenagem fechada e nomes que casam com o que a métrica mede:

Cards superiores (taxas):
- "Tempo de resolução" -> "Resolvidas pelo bot %"
  Tooltip: bot resolveu sozinho (sem humano via Chatwoot ou WhatsApp) / total
- "Taxa de entrega" -> "Transferidas pra humano %"
  Tooltip: agora soma auto (Jasmine chamou) + manual (humano respondeu sem
  handoff explícito). Junto com a resolução, fecha ~100%.

Cards de detalhe (segunda linha, contagem absoluta):
- "Resolvidas pelo bot" — quantas o bot fechou sozinho
- "Transferência automática (Jasmine)" — bot_handoff explícito (loop, timeout,
  max turns, intent)
- "Tomada manual (agente)" — humano respondeu (UI ou WhatsApp echo) SEM a
  Jasmine ter chamado bot_handoff. Era o "bucket fantasma" antes.

Backend:
- BotMetricsBuilder.metrics inclui bot_resolutions_count, auto_handoffs_count,
  manual_takeovers_count
- handoff_rate agora é (auto + manual) / total — daí a engrenagem fechar
- manual_takeovers_count: conversas com mensagem outgoing humana
  (sender_type='User' OR NULL) MENOS as que tiveram conversation_bot_handoff

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:01:50 -03:00
Rodribm10
e9b8b6e587 feat(reports): filtro por inbox no Relatório do Bot
Hoje as métricas e séries do BotReports agregam toda a conta — não dá pra ver
"a Jasmine da PrimeAL está errando mais que a do Qnn01". Cada unidade tem
prompt próprio, então um sintoma localizado precisa de medição localizada.

Backend:
- Inbox#has_many :reporting_events (relação inversa que faltava)
- BotMetricsBuilder aceita inbox_id e filtra bot_conversations + base_reporting_events
- bot_metrics endpoint passa inbox_id pelos params permitidos
- count_report_builder já suporta scope=inbox; agora funciona pra
  bot_resolutions_count e bot_handoffs_count graças à relação acima

Frontend:
- BotReports.vue: ReportFilters com filter-type='inboxes' e dropdown ativo
- Quando uma inbox é escolhida, requestPayload inclui inboxId/type/id e os
  fetches (BotMetrics + ReportContainer) passam o filtro
- API client getBotMetrics aceita inboxId; getBotSummary aceita type+id
- Sem inbox selecionada: comportamento antigo (agregação da conta)

Bonus na rake task de retroativo:
- rebuild_bot_resolved.rake: Message.unscope(:order) pra evitar conflito
  PG::InvalidColumnReference (DISTINCT + ORDER BY default scope)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:47:46 -03:00
Rodribm10
7cd2ea1258 fix(reports): tratar resposta humana via WhatsApp como interação humana
Bug do BotReports descoberto pelo Rodrigo:
- A regra de "conversation_bot_resolved" só desqualificava conversas com
  outgoing.sender_type='User' (atendente respondendo pelo Chatwoot UI)
- Mas mensagem outgoing vinda do webhook WhatsApp com IsFromMe=true (atendente
  respondeu direto pelo celular do hotel) é gravada com sender=nil
- Resultado: a Jasmine ganhava crédito mesmo quando humano respondia fora
  do Chatwoot. Taxa de resolução pelo bot inflada.

Fix prospectivo:
- ReportingEventListener#create_bot_resolved_event agora desqualifica via
  human_outgoing_messages? (sender_type='User' OU sender_type IS NULL)
- Captain::Assistant (a Jasmine) usa sender_type='Captain::Assistant' e segue
  fora do filtro, como antes
- Spec novo cobrindo o caso WhatsApp echo

Retroativo:
- lib/tasks/rebuild_bot_resolved.rake — task idempotente que purga
  reporting_events de conversation_bot_resolved gerados sob a regra antiga.
- DRY-RUN por padrão, APPLY=true pra deletar, ACCOUNT_ID pra restringir,
  SNAPSHOT_PATH pra trilha de auditoria

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:35:08 -03:00
Rodribm10
429567495f fix(reports): inbox_id reativo + nome da inbox visível na tab Novas × Retorno
Bugs originais (tabela Reports → Inbox → unidade específica → Novas × Retorno):
1. Backend recebia sempre inbox_id=1 — useFunctionGetter('inboxes/getInboxById', route.params.id)
   passava string crua, não Ref reativa, então o getter ficava travado no ID inicial
2. UX: tab não mostrava qual unidade estava sendo filtrada

Correções:
- InboxReportsShow.vue: passa inboxIdParam computed pra useFunctionGetter (reativo agora)
- Passa inbox.name como prop pro InboxLeadsReport
- InboxLeadsReport.vue: header com título + label "Caixa de entrada: <nome>" no topo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:14:44 -03:00
Rodribm10
3897db325e feat(reports): aba "Novas × Retorno" no Inbox Report
Mede por inbox/período: leads novos (1ª conversa do contato em qualquer
inbox da rede), retorno (conversa anterior resolved há >24h) e outras
(conversa anterior open ou resolved <24h). Categorias somadas batem com
o conversations_count nativo do report — bucket "outras" garante o
fechamento.

- Novo builder V2::Reports::InboxLeadsSummaryBuilder com CTE única
- Endpoint GET /api/v2/accounts/:id/reports/inbox_leads_summary
- Tabs no InboxReportsShow (Visão Geral | Novas × Retorno)
- Componente InboxLeadsReport com 3 metric cards + barras empilhadas
- API client + Pinia (state/getters/actions/mutations)
- i18n en + pt_BR
- RSpec do builder cobrindo classificação e isolamento por inbox

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:22:43 -03:00
Rodribm10
3336c68ee3 docs(precos): renomeia arquivos pra refletir escopo POR MARCA
- 1001_noites/precos_qnn01.md → precos_marca_1001_noites.md
- express/precos_express_al.md → precos_marca_express.md
- prime/precos_marca_prime.md (já estava OK)

Conteúdo reorganizado pra deixar explícito:
- Preços são iguais entre unidades da mesma marca (per feedback_prompt_scope_by_brand)
- Variação por unidade é só na DISPONIBILIDADE de categorias (ex: Pole Dance só em algumas 1001 Noites; Qnn01 não tem)
- Lista unidades cadastradas vs pendentes no Captain
- Aponta sync espelhado no Obsidian (Grupo 1001 Innova/Tabelas de Preço/)

README.md atualizado refletindo nova estrutura.
Não muda comportamento da Jasmine — só doc.
2026-04-25 20:02:55 -03:00
Rodribm10
ffeb1aa65a docs(captain): histórico de fixes pra evitar retrabalho do Reviewer
Cria docs/captain/historico_fixes.md com registro estruturado (YAML) das
correções aplicadas nos prompts/infra do Captain. Pré-popula com os 2 fixes
de hoje (v99 - preços Express/Qnn01 + sync automático; v100 - comportamento
humano + valores curto).

Pra que serve:
- Auditoria humana (saber o que mudou quando e por quê)
- Defesa contra Captain Reviewer redescobrir bug já corrigido (comparar
  data da conversa-fonte com data do fix correspondente)
- Base pra eventual Nível 3 do Reviewer (filtrar conversas anteriores ao
  último commit em _modelos/ via git log)

Não muda comportamento da Jasmine — é doc + infra passiva.
2026-04-25 17:13:35 -03:00
Rodribm10
6eb7f99ea4 feat(captain): comportamento humano + fix "valores curto" nas 4 Jasmines
Bug raiz observado em conversa real (Rayssa Lorranny / PrimeAL, 25/04 12:44):
- Jasmine alucinou "não tenho a tabela exata por horas aqui neste momento"
- Pediu "qual dia?" quando cliente disse só "Valores" (deveria mostrar tabela completa)
- Mencionou "tabela qui-dom/feriado" entre parênteses na resposta (entrega que é IA)

3 mudanças aplicadas nos 4 daniela_reservas (PrimeAL, PrimeVL, Qnn01, Express):

1. Nova seção "🤖➡️👤 SE COMPORTE COMO HUMANA" no topo, com lista de frases
   proibidas (todas que entregam IA) e exemplos de substituição humana
2. Nova "🚨 REGRA DE OURO — VALORES CURTO" antes de B): cliente disse só
   "valor"/"valores"/"preço" sem especificar → manda tabela completa direto,
   nunca pergunta dia/suíte primeiro
3. 3 proibições novas em "🚫 Proibições": dizer que não tem tabela,
   mencionar "tabela qui-dom/seg-qua" na resposta, responder pergunta com pergunta

Inclui também o questionario_nova_unidade.md já postado no Mattermost top-team.

Refs: análise da conversa Rayssa Lorranny / Operacoes 2026-04-25
2026-04-25 13:52:34 -03:00