-- ========================================================================== -- Agencia PSI — Migracao: Bot de auto-triagem WhatsApp (Grupo 3.7) -- ========================================================================== -- Criado por: Leonardo Nohama -- Data: 2026-04-23 · Sao Carlos/SP — Brasil -- -- Bot que coleta nome + motivo + preferencias antes de encaminhar o -- paciente pro fluxo humano. Evita que terapeuta tenha que fazer -- perguntas basicas toda vez que chega um lead novo no WhatsApp. -- -- Modelo: -- conversation_bots → config (1 por tenant) -- conversation_bot_sessions → estado ativo por thread -- -- Fluxo: -- 1. Paciente novo manda inbound -- 2. Edge evolution-whatsapp-inbound cria session no step 0 e envia -- greeting + primeira pergunta (steps[0].prompt) -- 3. Proxima inbound: salva resposta em collected_data[steps[0].variable], -- avanca step, envia proxima pergunta -- 4. Quando passa do ultimo step: envia closing_message, marca session -- como 'completed', cria conversation_note com resumo das respostas -- -- Interrupcoes: -- - Paciente pede opt-out (keywords) → bot sai, session 'opted_out' -- - Humano assume a conversa (conversation_assignments) → bot sai, 'abandoned_manual' -- - Session sem resposta > idle_timeout → 'abandoned_idle' (job futuro) -- -- UNIQUE parcial garante 1 sessao ativa por thread. -- ========================================================================== -- --------------------------------------------------------------------------- -- Tabela: conversation_bots (config) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.conversation_bots ( tenant_id UUID PRIMARY KEY REFERENCES public.tenants(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL DEFAULT false, greeting_message TEXT NOT NULL DEFAULT 'Olá! 👋 Sou o assistente virtual. Vou te fazer algumas perguntas rápidas pra a equipe preparar seu atendimento.', closing_message TEXT NOT NULL DEFAULT 'Obrigado! Recebemos suas informações e a equipe entrará em contato em breve. 💙', -- Array de steps: [{ "prompt": "...", "variable": "...", "type": "text" }] steps JSONB NOT NULL DEFAULT jsonb_build_array( jsonb_build_object('prompt', 'Qual seu nome completo?', 'variable', 'nome_completo', 'type', 'text'), jsonb_build_object('prompt', 'O que te levou a buscar atendimento? Pode me contar brevemente.', 'variable', 'motivo', 'type', 'text'), jsonb_build_object('prompt', 'Prefere atendimento online ou presencial?', 'variable', 'modalidade', 'type', 'text'), jsonb_build_object('prompt', 'Qual o melhor dia e horário pra você? (Ex: terça à tarde)', 'variable', 'horario_preferido', 'type', 'text') ), -- Gatilho: quem dispara o bot? trigger_mode TEXT NOT NULL DEFAULT 'new_contact' CHECK (trigger_mode IN ('new_contact', 'all_unassigned', 'keyword')), -- Usado quando trigger_mode='keyword' trigger_keywords TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], -- Abandono automatico: se session fica ativa sem avancar por N min idle_timeout_minutes INT NOT NULL DEFAULT 30 CHECK (idle_timeout_minutes >= 5 AND idle_timeout_minutes <= 1440), -- Se bot deve encerrar quando paciente usa keyword de opt-out respect_optout BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); DROP TRIGGER IF EXISTS trg_conv_bots_updated_at ON public.conversation_bots; CREATE TRIGGER trg_conv_bots_updated_at BEFORE UPDATE ON public.conversation_bots FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); COMMENT ON TABLE public.conversation_bots IS 'Config do bot de triagem WhatsApp por tenant. steps contem array de perguntas.'; -- --------------------------------------------------------------------------- -- Tabela: conversation_bot_sessions (estado) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.conversation_bot_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, thread_key TEXT NOT NULL, contact_number TEXT, current_step INT NOT NULL DEFAULT 0, collected_data JSONB NOT NULL DEFAULT '{}'::jsonb, status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'completed', 'abandoned_idle', 'abandoned_manual', 'opted_out')), started_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_advance_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ, abandoned_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); DROP TRIGGER IF EXISTS trg_bot_sessions_updated_at ON public.conversation_bot_sessions; CREATE TRIGGER trg_bot_sessions_updated_at BEFORE UPDATE ON public.conversation_bot_sessions FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); -- 1 sessao ativa por thread CREATE UNIQUE INDEX IF NOT EXISTS uq_bot_sessions_active_per_thread ON public.conversation_bot_sessions (tenant_id, thread_key) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_bot_sessions_tenant_status ON public.conversation_bot_sessions (tenant_id, status, started_at DESC); COMMENT ON TABLE public.conversation_bot_sessions IS 'Estado do bot por thread. UNIQUE parcial garante 1 ativa por (tenant, thread).'; -- --------------------------------------------------------------------------- -- RLS -- --------------------------------------------------------------------------- ALTER TABLE public.conversation_bots ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "conv_bots: select membros" ON public.conversation_bots; CREATE POLICY "conv_bots: select membros" ON public.conversation_bots FOR SELECT TO authenticated USING ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.tenant_id = conversation_bots.tenant_id AND tm.user_id = auth.uid() AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "conv_bots: write admins" ON public.conversation_bots; CREATE POLICY "conv_bots: write admins" ON public.conversation_bots FOR ALL TO authenticated USING ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.tenant_id = conversation_bots.tenant_id AND tm.user_id = auth.uid() AND tm.role IN ('clinic_admin', 'tenant_admin') AND tm.status = 'active' ) ) WITH CHECK ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.tenant_id = conversation_bots.tenant_id AND tm.user_id = auth.uid() AND tm.role IN ('clinic_admin', 'tenant_admin') AND tm.status = 'active' ) ); ALTER TABLE public.conversation_bot_sessions ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "bot_sessions: select membros" ON public.conversation_bot_sessions; CREATE POLICY "bot_sessions: select membros" ON public.conversation_bot_sessions FOR SELECT TO authenticated USING ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.tenant_id = conversation_bot_sessions.tenant_id AND tm.user_id = auth.uid() AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "bot_sessions: write service_role" ON public.conversation_bot_sessions; CREATE POLICY "bot_sessions: write service_role" ON public.conversation_bot_sessions FOR ALL TO service_role USING (true) WITH CHECK (true);