Quando o cliente pede um horário PONTUAL fora dos disponíveis (mais cedo/mais
tarde), a IA usa solicitar_horario_especial → registra um pedido na agenda do
satélite (POST /pedido) e responde "vou confirmar com a secretária e retorno"
(sem garantir o horário). brain.ts injeta essa regra no prompt. Par do satélite
(scoreodonto): janela flex + fila da secretária humana.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
listar_horarios inclui `local` (nome da sala alugada ou "Clínica") e codifica o
sala_id no slot_id; agendar_horario repassa sala_id ao /book para reservar no
quarto certo. Par do satélite (scoreodonto): folga do dentista (A) + salas (B).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brain.ts busca GET /status da agenda-bridge (aberto hoje?, próximo dia aberto,
grade da semana, feriados) e injeta no system prompt via formatClinicStatus,
junto da data já existente. Regra: a Secretária só oferece horário/agenda quando
a clínica está ABERTA — em dia fechado/feriado, informa o horário e oferece o
próximo dia disponível. (Par do satélite: scoreodonto #2.)
Testado em dev: ao pedirem "hoje" num domingo, ofereceu "amanhã" nos horários
configurados.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Núcleo desta mudança (separação por sessão):
- migrate.ts: sec_numbers.agent_id (FK→sec_agents, ON DELETE SET NULL) e
sec_calendar.instance_id, com índices (aditivo/idempotente).
- ext-api routes.ts: runtime resolve o agente pelo NÚMERO que recebeu a
mensagem (sec_numbers[instanceId].agent_id, fallback: 1º ativo); rotas
admin /secretaria/numbers e /secretaria/calendar aceitam os novos campos.
- brain.ts/tools.ts: instanceId no contexto; sec_calendar interno filtrado
por sessão; parse de jid robusto (.pop(), suporta 2 e 3 partes).
- secretaria/routes.ts: paridade (agent_id em numbers, instance_id no calendar).
Testado em dev (newwhats.dev refrescado): agentes resolvidos por instância;
fallback e FK ON DELETE SET NULL verificados via ext-api.
Obs.: por estarem no mesmo working tree não-commitado do newwhats.local,
ext-api/routes.ts e secretaria/tools.ts também trazem o trabalho acumulado
da integração do satélite (ações de inbox: delete/react/typing/labels/forward
e admin /secretaria/*; tools identificar_numero/cadastrar_paciente do odonto).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- brain: detecta 429/cota (quota/billing/insufficient/exceeded/limite da api/
rate limit — inclui a msg PT do Gemini) e emite ext:provider.exhausted por
provider da chain (throttle de 6h por provider), com fallback informado.
- routes: handler ext:provider.exhausted monta notificação profissional e envia
por WhatsApp ao admin (admin_notify_phone da config secretaria), resolvendo o
JID canônico via onWhatsApp (trata 9º dígito BR; pula se número não existe).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
listar_horarios/agendar_horario chamam a ponte do scoreodonto (/api/nw/agenda
slots|book) quando configurada — slot_id sintético (dentista|start|end), telefone
derivado do JID do contato — com fallback ao calendário interno preservado.
brain.ts injeta a config da agenda (agenda_url/secret/clinica_id) no toolCtx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isRecoverableError inclui fetch failed/timeout/econnreset/etimedout/und_err —
um blip de rede no primeiro provider agora cai para o próximo da chain (openai→
gemini→anthropic→ollama) em vez de virar "[Erro ao chamar IA: fetch failed]".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
text-embedding-004 foi descontinuado (404 'model not found'). Migra para
gemini-embedding-001 com outputDimensionality=768 (vetores leves p/ JSON+cosseno).
Validado: embed() retorna vetor 768d via Gemini.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Antes, com openai_key presente, qualquer falha (ex.: 429 insufficient_quota)
retornava null sem tentar o Gemini. Agora tenta OpenAI e, em falha, usa Gemini.
Aviso documentado: OpenAI (1536d) e Gemini (768d) são incompatíveis — trocar de
provider exige reindexar (sync-knowledge + memória de contato).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A secretária passa a "lembrar" do cliente entre conversas:
- tabela sec_contact_memory (fato + embedding JSON), chave = ext_chat_id/contact_name.
- na sumarização (a cada ~10 trocas) extrai fatos duradouros do cliente via LLM
e salva com embedding + dedup por similaridade (>0.92).
- buildSystemPrompt injeta "MEMÓRIA DO CLIENTE" com os fatos mais relevantes à
pergunta (cosseno); sem chave de embedding usa os mais recentes; tudo com
fallback silencioso (não regride).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adiciona busca semântica na Base de Conhecimento da secretária:
- embeddings.ts: chunking, embed (OpenAI text-embedding-3-small ou Gemini
text-embedding-004) e similaridade cosseno — tudo na aplicação, sem pgvector.
- tabela sec_knowledge_chunks (vetor em JSON/text) com índices.
- brain.ts: no nó 'knowledge', injeta só os top-4 trechos relevantes à pergunta
do usuário; indexação lazy por hash do conteúdo. Fallback para o conteúdo
inteiro se não houver chave de embedding ou em qualquer falha (não regride).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O createTable de sec_messages não tinha usage_tokens/provider_used/model_used,
mas brain.ts insere esses campos — quebrando com "column does not exist" em
qualquer banco criado antes dessas colunas (ex.: clones). Adiciona as colunas
ao createTable e um bloco idempotente hasColumn/alterTable (padrão já usado no
arquivo) que garante as colunas em tabelas pré-existentes no boot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>