From 719448120aab0dc56b4579b701e9af6c06ec3fe9 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Sat, 18 Apr 2026 21:37:32 -0300 Subject: [PATCH] docs(spec): Captain semantic memory (episodic contact layer) Spec do Epico A - adiciona Camada 3 (memoria semantica episodica do contato) ao Captain AI, mantendo as 3 camadas existentes inalteradas. Decisoes fechadas no brainstorming: - Extracao ao resolver conversa OU silencio > 30min (100% automatico) - Validacao: evidence obrigatoria, confidence >= 0.5 (alternativas B/C/D documentadas como fallback) - Scope global no recall, atribuicao por source_unit_id pra relatorios - 9 tipos iniciais, limite 5 fatos/conversa, 50 ativos/contato - TTL por tipo + supersedencia automatica por contradicao - LGPD soft-30d -> hard-delete via cron - 2 feature flags independentes, default OFF - Epico B (LangGraph/inteligencia) sera spec separado pos-producao Custo estimado: ~R$ 47/mes no grupo todo. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...26-04-18-captain-semantic-memory-design.md | 499 ++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-18-captain-semantic-memory-design.md diff --git a/docs/superpowers/specs/2026-04-18-captain-semantic-memory-design.md b/docs/superpowers/specs/2026-04-18-captain-semantic-memory-design.md new file mode 100644 index 000000000..e3712d5ad --- /dev/null +++ b/docs/superpowers/specs/2026-04-18-captain-semantic-memory-design.md @@ -0,0 +1,499 @@ +# Spec — Memória Semântica Episódica do Cliente (Captain) + +**Data:** 2026-04-18 +**Branch:** `feat/captain-semantic-memory` +**Épico:** A (memória). Épico B (evolução do orchestrator) será spec separado após este entrar em produção. + +--- + +## 1. Contexto e motivação + +Hoje o Captain (agente de WhatsApp da fazer.ai para o Grupo 1001 Noites) tem 3 camadas de memória funcionais: + +| Camada | Onde | Natureza | +|---|---|---| +| Identity | `contact.custom_attributes` (cpf, total_reservas, ultima_suite…) | Key-value, determinístico | +| Working memory | últimas N mensagens da conversa (bruto) | Literal, volátil | +| Knowledge | `captain_assistant_responses.embedding` (FAQ) | Semântica (guardrail) | + +**Falta a Camada 3 — memória episódica do cliente:** fatos acumulados ao longo do relacionamento (preferências, datas comemorativas, reclamações, padrões). Sem ela, clientes recorrentes são tratados como novos; experiência é genérica; o valor de "grupo que conhece o hóspede" não existe. + +**Objetivo:** adicionar Camada 3 ao Captain, sem alterar as outras 3, com memória semântica real (embeddings + cosine + pgvector), fatos extraídos automaticamente ao fim de cada conversa, injetados no contexto do LLM via top-K retrieval. + +**Fora de escopo:** refatorar as 3 camadas existentes; trocar gem `agents` por LangChain/LangGraph; outbound proativo (campanhas) — virarão features derivadas após este épico estável. + +## 2. Problema concreto que resolvemos + +- Cliente volta em 6 meses → agente começa do zero, pede dados de novo. +- Cliente mencionou preferência por Stilo com hidro 5x → agente nunca oferece proativamente. +- Cliente reclamou de ar-condicionado → na próxima reserva, agente não sabe evitar a mesma suíte. +- Cliente passa em Prime Ceilândia, depois Prime Águas Lindas → agente da 2ª unidade não sabe quem é. +- Grupo não consegue gerar relatórios de *"top reclamações por unidade nos últimos 90 dias"* porque esse dado não está estruturado. + +## 3. Decisões tomadas no brainstorming + +| # | Decisão | Valor | +|---|---|---| +| 1 | **Quando extrair** | Ao resolver conversa OU após silêncio > 30min | +| 2 | **Validação** | 100% automático com salvaguardas (evidence obrigatória, confidence ≥ 0.5). Alternativas B/C/D documentadas como plano de contingência | +| 3 | **Scope** | Global no recall (experiência unificada no grupo), atribuição por unidade no registro (`source_unit_id` pra relatórios) | +| 4 | **Taxonomia** | 9 tipos fixos iniciais (ver §5) | +| 5 | **Limites** | Até 5 fatos por conversa, até 50 ativos por contato (LRU ao atingir) | +| 6 | **Envelhecimento** | TTL por tipo, soft-delete após expirar, redução de peso no recall antes de deletar | +| 7 | **Conflitos** | Supersedência automática via checker de contradição semântica + lexical | +| 8 | **LGPD** | Soft-delete 30d → hard-delete via cron diário. Botão "esquecer tudo" no painel | +| 9 | **Feature flags** | `extraction_enabled` e `recall_enabled` independentes, por account, default OFF | +| 10 | **Decomposição** | Épico A (este) e Épico B (evolução orchestrator) serão sequenciais, sem esperar 30 dias entre um e outro | + +## 4. Arquitetura — 4 camadas integradas em runtime + +``` +mensagem entra + │ + ▼ +┌─────────────────┐ key-value, ~1ms, determinístico +│ 1. IDENTITY │ ← contact.custom_attributes (hoje, inalterado) +└─────────────────┘ + │ + ▼ +┌─────────────────┐ literal, ~5ms, volátil +│ 2. WORKING MEM │ ← últimas N mensagens brutas (hoje, inalterado) +└─────────────────┘ + │ + ▼ +┌─────────────────┐ semântica, ~100-200ms, top-5 ← NOVO +│ 3. EPISODIC │ ← captain_contact_memories por cosine +└─────────────────┘ + │ + ▼ +┌─────────────────┐ semântica (guardrail pós-geração) +│ 4. KNOWLEDGE │ ← captain_assistant_responses (hoje, inalterado) +└─────────────────┘ + │ + ▼ +LLM responde +``` + +Camadas 1 e 2 rodam antes do LLM; Camada 3 também (novo); Camada 4 roda depois como guardrail. Só a Camada 3 é adicionada — nenhuma outra é modificada. + +## 5. Taxonomia dos fatos — 9 tipos iniciais + +| `memory_type` | Exemplo | Scope default | TTL | Ao expirar | +|---|---|---|---|---| +| `preferencia` | "Prefere Stilo com hidromassagem" | global | 365d | Reduz peso no recall (×0.7), não deleta | +| `data_comemorativa` | "Aniversário casamento 14/02" | global | Nunca | — | +| `vinculo_social` | "Vem com esposa Mariana" | global | 730d | Deleta | +| `padrao_comportamental` | "Chega entre 21h-22h" | unit | 365d | Reduz peso | +| `reclamacao` | "Ar-condicionado da 204 barulhento em mar/26" | unit | 180d | Deleta | +| `feedback_positivo` | "Elogiou a Dona Cida do café" | unit | 365d | Deleta | +| `restricao` | "Alérgico a amendoim" | global | Nunca | — | +| `vinculo_comercial` | "Funcionário Caixa, desconto corp" | global | 365d | Reduz peso | +| `contexto_pessoal` | "Trabalha viajando, hospedagem rápida" | global | 365d | Reduz peso | + +Tipos novos podem ser adicionados depois sem migration (campo string no schema). + +## 6. Modelo de dados + +### 6.1 Tabela `captain_contact_memories` + +```sql +CREATE TABLE captain_contact_memories ( + id BIGSERIAL PRIMARY KEY, + account_id BIGINT NOT NULL REFERENCES accounts(id), + contact_id BIGINT NOT NULL REFERENCES contacts(id), + memory_type VARCHAR NOT NULL, + content TEXT NOT NULL, + evidence TEXT NOT NULL, + confidence FLOAT NOT NULL, + scope VARCHAR NOT NULL DEFAULT 'global', + embedding vector(1536), + source_conversation_id BIGINT REFERENCES conversations(id), + source_unit_id BIGINT REFERENCES captain_units(id), + source_inbox_id BIGINT REFERENCES inboxes(id), + expires_at TIMESTAMP, + last_verified_at TIMESTAMP NOT NULL, + superseded_at TIMESTAMP, + superseded_by_id BIGINT REFERENCES captain_contact_memories(id), + deleted_at TIMESTAMP, + metadata JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); +``` + +### 6.2 Índices + +```sql +CREATE INDEX idx_ccm_recall ON captain_contact_memories (account_id, contact_id) + WHERE deleted_at IS NULL AND superseded_at IS NULL; + +CREATE INDEX idx_ccm_embedding ON captain_contact_memories + USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +CREATE INDEX idx_ccm_analytics ON captain_contact_memories + (source_unit_id, memory_type, created_at); + +CREATE INDEX idx_ccm_hard_delete ON captain_contact_memories (deleted_at) + WHERE deleted_at IS NOT NULL; + +CREATE INDEX idx_ccm_superseded ON captain_contact_memories (superseded_by_id) + WHERE superseded_at IS NOT NULL; +``` + +### 6.3 Validações no model + +- `memory_type` ∈ enum dos 9 tipos. +- `evidence` presence (garantia anti-hallucination — fato sem trecho literal = rejeitado no extractor). +- `confidence` entre 0 e 1. +- `content` non-empty, max 1000 chars (fatos devem ser concisos). + +## 7. Componentes (arquivos novos) + +### 7.1 Model + +**`enterprise/app/models/captain/contact_memory.rb`** +- `has_neighbors :embedding` (gem `neighbor`) +- Enum `memory_type` com os 9 valores +- Scopes: + - `.active` — `WHERE deleted_at IS NULL AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())` + - `.for_contact(id)`, `.by_type(t)`, `.scope_compatible(unit_id)` +- Instance methods: + - `soft_delete!` — seta `deleted_at = now` + - `supersede_by!(other)` — seta `superseded_at`, `superseded_by_id` + - `recall_weight` — retorna 1.0 ou 0.7 (reduz peso se expired) +- Callbacks: + - `after_commit on: [:create, :update] if saved_change_to_content?` → enfileira `UpdateEmbeddingJob` + - `after_create_commit` → enfileira `ContradictionCheckerJob` + +### 7.2 Services + +**`enterprise/app/services/captain/contact_memories/extraction_service.rb`** +- `initialize(conversation:)` +- `#call` → retorna array de hashes dos fatos válidos. NÃO persiste (separação). +- Monta prompt incluindo: histórico completo da conversa, taxonomia dos 9 tipos, exemplos de evidence bem-feito, JSON schema esperado. +- LLM: `gpt-4o-mini`, JSON mode, temperature 0. +- Valida cada fato retornado: `type` válido, `evidence` non-empty, `confidence` ≥ 0.5, `content` dentro do limite. +- Descarta silenciosamente os inválidos. Retorna no máximo 5. + +**`enterprise/app/services/captain/contact_memories/recall_service.rb`** +- `initialize(contact:, query_text:, unit_id: nil)` +- `#call` → retorna array ordenado de top-K memories (default K=5). +- Gera embedding da query. +- `Captain::ContactMemory.active.for_contact(contact.id).scope_compatible(unit_id).nearest_neighbors(:embedding, embedding, distance: 'cosine').limit(K)`. +- Aplica `recall_weight` ao cosine score final. +- Timeout de 500ms via `Timeout::timeout`. Em caso de erro ou timeout, retorna `[]`. + +**`enterprise/app/services/captain/contact_memories/contradiction_checker_service.rb`** +- `initialize(memory:)` +- `#call` → marca fatos anteriores como `superseded_by` se detectar contradição. +- Busca candidatos: `active.for_contact.by_type(memory.memory_type).where.not(id: memory.id).where('embedding <=> ? < 0.6', memory.embedding)`. +- Para cada candidato (max 3), chama LLM barato (gpt-4o-mini, 1 pergunta binária): *"Esses 2 fatos se contradizem? sim/não"*. +- Se "sim": `candidate.supersede_by!(memory)`. + +**`enterprise/app/services/captain/contact_memories/prompt_injection_service.rb`** +- `initialize(memories:)` +- `#call` → retorna string XML formatada pra injetar no system prompt. +- Formato: + ```xml + + Prefere Stilo com hidromassagem + Aniversário casamento 14/02 + + ``` +- Se `memories.empty?` → retorna string vazia. + +### 7.3 Jobs + +**`enterprise/app/jobs/captain/contact_memories/extract_from_conversation_job.rb`** +- Argumentos: `conversation_id`. +- Passos: + 1. Checa feature flag `captain_contact_memory_extraction_enabled` na account. Se OFF, early return. + 2. Carrega conversation. + 3. Chama `ExtractionService`. + 4. Persiste os fatos válidos com `source_conversation_id`, `source_unit_id` (vem de `conversation.captain_unit_id` ou inferido via inbox), `source_inbox_id`. + 5. Cada fato criado aciona `UpdateEmbeddingJob` via `after_commit`. +- Idempotência: se já existe `ContactMemory` com o mesmo `source_conversation_id`, skip (não re-extrai). + +**`enterprise/app/jobs/captain/contact_memories/update_embedding_job.rb`** +- Argumento: `memory_id`. +- Gera embedding via `RubyLLM.embed(content, model: 'text-embedding-3-small')`. +- Salva `embedding`. +- Retry 3x, backoff exponencial. Se todas falharem, deixa `embedding = NULL` (fato fica invisível no recall). + +**`enterprise/app/jobs/captain/contact_memories/contradiction_checker_job.rb`** +- Argumento: `memory_id`. +- Chama `ContradictionCheckerService`. +- Roda APÓS `update_embedding_job` concluir (encadeado). + +**`enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb`** +- Cron a cada 10 min (Sidekiq-Cron). +- Busca conversas ativas cuja última mensagem foi há mais de 30 minutos E que ainda não têm `ContactMemory` com `source_conversation_id` igual. +- Enfileira `ExtractFromConversationJob` pra cada uma. + +**`enterprise/app/jobs/captain/contact_memories/aging_job.rb`** +- Cron semanal (domingo madrugada). +- Itera tipos com TTL: + - Para tipos com "Deleta": `WHERE expires_at < NOW() AND deleted_at IS NULL` → soft-delete. + - Para tipos com "Reduz peso": marca `expires_at` no futuro (recall já aplica ×0.7 baseado em `expires_at < NOW()`). +- Aplica limite macio de 50 fatos ativos por contato: LRU por `last_verified_at` → soft-delete excedente. + +**`enterprise/app/jobs/captain/contact_memories/hard_delete_expired_job.rb`** +- Cron diário. +- `WHERE deleted_at < 30.days.ago` → `DELETE`. +- Loga count por account. + +### 7.4 Integração no `AgentRunnerService` (arquivo existente — alteração) + +**`enterprise/app/services/captain/assistant/agent_runner_service.rb`** + +Alteração mínima, antes do `Agents::Runner.with_agents(...).run(message, ...)`: + +```ruby +if account.captain_contact_memory_recall_enabled? + memories = Captain::ContactMemories::RecallService.new( + contact: @conversation.contact, + query_text: message, + unit_id: @conversation.captain_unit_id + ).call + + memory_block = Captain::ContactMemories::PromptInjectionService.new( + memories: memories + ).call + + system_prompt_with_memory = [system_prompt, memory_block].compact_blank.join("\n\n") +else + system_prompt_with_memory = system_prompt +end +``` + +System prompt do orchestrator (Jasmine) e de cada scenario (Daniela etc) passam a receber `system_prompt_with_memory`. + +Instrução adicionada ao system prompt base: *"Se houver bloco , use apenas se for natural na conversa. Nunca mencione 'memória', 'registro' ou 'lembrei'. Aja como quem já conhece o cliente."* + +### 7.5 Controllers e UI + +**`enterprise/app/controllers/api/v1/accounts/contacts/memories_controller.rb`** + +``` +GET /api/v1/accounts/:aid/contacts/:cid/memories # list +PATCH /api/v1/accounts/:aid/contacts/:cid/memories/:id # edit content +DELETE /api/v1/accounts/:aid/contacts/:cid/memories/:id # soft delete +DELETE /api/v1/accounts/:aid/contacts/:cid/memories # forget all (LGPD) +``` + +Autorização via Pundit: `ContactMemoryPolicy` — só agentes da account, com permissão de edição de contato. + +**UI:** nova tab "Memória" no detalhe do contato. Componente Vue: + +`app/javascript/dashboard/routes/dashboard/contact/components/ContactMemories.vue` +- Tabs: "Ativas" | "Histórico" (supersedidas/expiradas) +- Lista: type badge, content, confidence bar, idade, origem (link pra conversa) +- Ações por fato: "editar" (inline), "esquecer" +- Botão master: "Esquecer tudo deste contato" (modal de confirmação com texto livre) + +**Configurações do Captain** (página existente): nova seção "Memória do Cliente" com 2 toggles: +- "Aprender com conversas (extraction)" +- "Usar memória nas respostas (recall)" + +Cada toggle mostra: status, data da última mudança, quem mudou, custo acumulado do mês (se extraction). + +### 7.6 Migrations + +- `db/migrate/_create_captain_contact_memories.rb` — tabela + índices. +- Feature flags via `InstallationConfig` seed (padrão já existe no projeto) — NÃO é migration. Adiciona entries `captain_contact_memory_extraction_enabled` (default false) e `captain_contact_memory_recall_enabled` (default false). + +## 8. Data flows + +### 8.1 Fluxo de aprendizado (assíncrono) + +``` +conversation.resolved (Wisper event) OU silence_detector_job (cron 10min) + │ + ▼ +ExtractFromConversationJob (Sidekiq, fila captain_heavy) + │ + ├── feature flag extraction_enabled OFF? → early return + │ + ▼ +ExtractionService + │ prompt estruturado + histórico + │ LLM gpt-4o-mini, JSON mode, temp 0 + │ valida evidence + confidence + tipo + │ retorna até 5 fatos válidos + ▼ +Captain::ContactMemory.create! (N vezes) + │ + ▼ after_commit +UpdateEmbeddingJob (Sidekiq, fila captain_embeddings) + │ text-embedding-3-small + │ retry 3x backoff exp + ▼ after embedding salvo +ContradictionCheckerJob (Sidekiq, fila captain_heavy) + │ busca candidatos cosine ≥ 0.6 mesmo tipo/contato + │ LLM binário por candidato + │ supersede os contraditórios +``` + +### 8.2 Fluxo de resposta (síncrono, caminho crítico) + +``` +message recebida → ResponseBuilderJob (já existe) + │ + ▼ +AgentRunnerService.call (alterado) + │ + ├── feature flag recall_enabled OFF? → pula recall + │ + ▼ +RecallService.call + │ embedding da query + │ nearest_neighbors top-5 com filtro scope + │ timeout 500ms + ▼ +PromptInjectionService.call + │ monta bloco XML + ▼ +system_prompt += memory_block + │ + ▼ +Agents::Runner.run (fluxo existente, inalterado) +``` + +## 9. Error handling + +| Falha | Comportamento | Impacto no cliente | +|---|---|---| +| LLM extraction falha (timeout/rate limit) | Retry Sidekiq padrão (3x backoff), depois desiste, loga error | Zero — aprende da próxima conversa | +| LLM retorna JSON inválido | Parse tenta repair (regex simples), se falhar descarta conversa, loga warning | Zero | +| Embedding API falha | Retry 3x, depois deixa `embedding = NULL` | Fato existe mas invisível no recall até próximo update | +| Recall service excede 500ms | Retorna `[]`, loga warning com p99 | Agente responde sem memória dessa vez | +| Recall service lança erro | `rescue StandardError => e; log; return []` | Zero — degradação graciosa | +| Contradição ambígua (>1 candidato alto) | Mantém novo, marca antigos como superseded | Zero | +| Feature flag OFF em runtime | Short-circuit no início | Zero | +| Conversation sem unit_id | `source_unit_id = NULL` — fato fica global | Zero | +| Contact tem 50+ fatos ativos | Aging job aplica LRU na próxima execução semanal | Zero | + +**Princípio invariante:** Camada 3 NUNCA bloqueia resposta do agente. Qualquer falha degrada para o comportamento atual (Camadas 1+2+4). + +## 10. LGPD e privacidade + +- **Soft-delete** por 30 dias permite desfazer exclusão acidental. +- **Hard-delete** automático após 30 dias via cron. +- **Botão "Esquecer tudo"** faz soft-delete em massa de todos os fatos do contato. +- **Transparência:** cliente pode pedir ao agente *"o que vocês sabem sobre mim?"* → tool nova `list_my_memories` (fora do escopo deste épico, mas arquitetura permite). +- **Auditoria:** toda mudança manual (edit, delete) loga `changed_by_user_id` + `changed_at` em `metadata`. +- **Retenção:** fatos sem interação em 365+ dias expiram conforme TTL da tabela. + +## 11. Testes + +### 11.1 Model specs +- Validações: memory_type enum, evidence presence, confidence range, content length. +- Scopes: active, by_type, scope_compatible. +- Soft-delete e supersede methods. +- has_neighbors busca funcional com fixtures. + +### 11.2 Service specs +- **ExtractionService** com 10+ fixtures de conversas reais (sanitizadas) cobrindo: conversa curta, longa, sem fatos, multi-tipo, com alucinação induzida (verificar que descarta). +- **RecallService** com base pre-populada: ordering por cosine correto, scope filter correto, timeout respeitado. +- **ContradictionCheckerService** com pares: idênticos, contraditórios, relacionados mas não contraditórios. +- **PromptInjectionService** formato XML, vazio quando sem fatos. + +### 11.3 Job specs +- Idempotência: rodar 2x a mesma `conversation_id` não duplica fatos. +- SilenceDetector detecta corretamente conversas elegíveis e ignora não-elegíveis. +- AgingJob aplica TTL correto por tipo. +- HardDeleteExpired só apaga > 30d. + +### 11.4 Integration specs +- `ResponseBuilderJob` com recall ON vs OFF → apenas o ON injeta bloco `` no prompt. +- End-to-end: conversa resolved → fato aprendido → próxima mensagem do mesmo contato usa fato no recall. +- Feature flags: extraction OFF não chama LLM (asserção de mock). + +### 11.5 LGPD specs +- `forget_all_memories!` faz soft-delete em todos os fatos do contato. +- Cron hard-delete remove registros > 30d. + +## 12. Feature flags e rollout + +**Flags (InstallationConfig):** +- `captain_contact_memory_extraction_enabled` — default `false` +- `captain_contact_memory_recall_enabled` — default `false` + +**Rollout sugerido:** +1. Deploy com flags OFF — nada muda em produção. +2. Account piloto (Prime Águas Lindas): liga `extraction`. Observa 1 semana: custo real, qualidade dos fatos, erros. +3. Se ok, liga `recall` na mesma piloto. Observa 1 semana: latência P99 agente, reação dos clientes. +4. Expande pras outras 3 marcas em ondas de 48h cada. +5. Se qualquer sinal ruim (custo > previsto, reclamação, latência) → desliga só a flag problemática. + +## 13. Observabilidade + +**Métricas (Prometheus/OTEL, padrão do projeto):** +- `captain_memory_extraction_count` (counter, por account, por tipo) +- `captain_memory_extraction_cost_brl` (counter, por account) +- `captain_memory_extraction_latency_seconds` (histogram) +- `captain_memory_recall_hit_rate` (gauge — % de mensagens onde recall retornou ≥1 fato) +- `captain_memory_recall_latency_seconds` (histogram, p50/p95/p99) +- `captain_memory_active_facts_count` (gauge, por account) +- `captain_memory_supersedence_count` (counter, por tipo) + +**Logs estruturados:** +- Cada extração: conversation_id, fatos extraídos (tipo + confidence), descartados (com razão). +- Cada recall: contact_id, query length, top-5 hits com score, tempo total. +- Cada supersedência: old_id, new_id, reason. + +**Dashboard admin:** uma view no Chatwoot admin com custo mensal por account, fatos por tipo, latência P99, taxa de supersedência. + +## 14. Custos estimados + +Baseado em ~3.000 conversas resolvidas/mês no grupo todo: + +| Item | Volume | Custo unit | Total/mês | +|---|---|---|---| +| Extração LLM (gpt-4o-mini) | ~3.000 | R$ 0,015 | **R$ 45,00** | +| Embedding (text-embedding-3-small) | ~15.000 | R$ 0,00005 | **R$ 0,75** | +| Recall embedding (1/msg) | ~20.000 | R$ 0,00005 | **R$ 1,00** | +| Checagem contradição | ~200 | R$ 0,001 | **R$ 0,20** | +| Storage pgvector | 150k fatos em 1 ano | — | R$ 0 | +| **Total estimado** | | | **~R$ 47/mês** | + +Feature flag `extraction_enabled` OFF zera 95% do custo. `recall_enabled` OFF zera ~4%. + +## 15. Roadmap de features derivadas (documentado, fora deste épico) + +**Prioridade alta — atacar após este épico estável ≥ 30 dias em produção:** + +- **Outbound proativo por `data_comemorativa`** — cron diário varre fatos com data nos próximos 7 dias, dispara template de proposta personalizada cruzando `preferencia`. Vira canal de vendas passivo. +- **Relatórios por unidade** — página admin com filtros de `source_unit_id` + `memory_type`. Top reclamações, top elogios, perfil do cliente por unidade. +- **Tool `list_my_memories`** no agente pra atender direito de transparência LGPD via WhatsApp. + +**Prioridade média — avaliar a partir de dados do épico A:** + +- **Épico B — Evolução do orchestrator:** LangGraph via microserviço Python com checkpoints persistidos, retomada exata de conversas longas, paralelismo de tools. Será spec separado. Pode ou não acontecer — depende se a memória já resolve a dor percebida. + +**Prioridade baixa — observar antes:** + +- Reclassificação manual de fatos (operador reclassifica tipo). +- Import de fatos de outras fontes (CRM, reservas Plug-Play). +- Multi-idioma (hoje assume pt-BR). + +## 16. Riscos e mitigações + +| Risco | Probabilidade | Impacto | Mitigação | +|---|---|---|---| +| LLM alucinar fatos (inventar coisas) | Média | Alto | Evidence obrigatória + confidence ≥ 0.5 + alternativas B/C/D prontas pra ativar | +| Custo explodir | Baixa | Médio | Flag extraction_enabled desliga 95% do custo | +| Latência do recall piorar resposta | Baixa | Médio | Timeout 500ms com fallback vazio | +| Fatos obsoletos/conflitantes poluírem | Média | Médio | Supersedência automática + aging + limite 50 | +| Vazamento de info entre unidades | Baixa | Alto (LGPD/reputação) | Scope field per-fato pra isolar quando sensível | +| Operador confuso pela nova UI | Baixa | Baixo | Docs + toggle começa OFF, rollout gradual | +| Regressão nas Camadas 1/2/4 | Muito baixa | Alto | Princípio: Camada 3 nunca bloqueia. Todas as alterações são aditivas. | + +## 17. Próximos passos + +1. **Revisão do spec** (user) — ajustes inline se necessário. +2. **Invocar `writing-plans`** — gera plano executável multi-step com tarefas, ordem, owners, critérios de aceitação. +3. **Execução** — via `executing-plans` ou subagent-driven-development, conforme preferência. +4. **Deploy com flags OFF** → **piloto gradual** → **rollout completo**. +5. **Observar 30 dias** em produção com memória ativa em todas as contas. +6. **Abrir Épico B** com dados reais como input do brainstorming.