/** * CDP Edge — Intelligence Agent + Customer Match * runIntelligenceAgent, customer match Meta/Google, health checks */ import { sha256 } from './utils.js'; import { getHealthMetrics, generateDailyReport, logIntelligence } from './db.js'; import { sendCallMeBot } from './dispatch/whatsapp.js'; import { autoDecideAbWinner } from './ml/ltv.js'; import { analyzeMatchQuality, alertMatchQuality, purgeOldMatchQualityLogs } from './ml/matchquality.js'; import { trainLogisticRegression, extractFeatures, saveWeights, LTV_WEIGHTS_KV_KEY } from './ml/logistic.js'; import { computeRoasFeedback, sendRoasAlert } from './ml/roas.js'; import { runNurtureQueue } from './nurture.js'; import { Env } from '../types.js'; // ── Tipos ─────────────────────────────────────────────────────────────────────── export interface ApiVersionCheck { platform: string; current: string; expected: string; status: 'ok' | 'warning'; } export interface ErrorRateAlert { platform: string; errorRate: number; status: 'ok' | 'warning' | 'critical'; } export interface LtvTrainResult { trained?: boolean; skipped?: string; samples?: number; accuracy?: number; positiveRate?: number; error?: string; } export interface IntelligenceAgentResult { versionResults: ApiVersionCheck[]; errorAlerts: ErrorRateAlert[]; ltvTrainResult: LtvTrainResult; abResult?: { decided: boolean; test_id?: number; winner_name?: string; improvement?: number; }; mqAnalysis?: { total?: number; composite_score?: number; email_rate?: number; fbp_rate?: number; alerts?: any[]; }; cmResult?: { sent?: number; received?: number; skipped?: string; error?: string; }; roasResult?: { campaigns: number; total_revenue: number; best_campaign: string | null; skipped?: string; }; nurtureResult?: { processed: number; sent: number; failed: number; }; lookalikeResult?: { sent: number; seed_type: string; skipped?: string; }; } export interface CustomerMatchResult { sent?: number; received?: number; num_received?: number; skipped?: string; error?: string; } export interface GoogleCustomerMatchExport { hashed_email: string; hashed_phone: string; first_name: string; last_name: string; } // ── Versões esperadas das APIs ──────────────────────────────────────────────── const EXPECTED_API_VERSIONS: Record = { meta: 'v25.0', ga4: 'latest', tiktok: 'v1.3', pinterest: 'v5', reddit: 'v2.0', }; const ALERT_THRESHOLDS = { errorRateCritical: 0.20, errorRateWarning: 0.10, }; // ── Alerta via CallMeBot ────────────────────────────────────────────────────── export async function sendIntelligenceAlert( env: Env, severity: 'critical' | 'warning' | 'info', title: string, details: string ): Promise { const icon = severity === 'critical' ? '🚨' : severity === 'warning' ? '⚠️' : 'ℹ️'; const texto = `${icon} CDP Edge — ${title}\n\n${details}\n\n${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}`; return sendCallMeBot(env, texto); } // ── Check de versões de API ─────────────────────────────────────────────────── export async function checkApiVersionsIntelligence( env: Env, runType: string ): Promise { const results: ApiVersionCheck[] = []; for (const [platform, expected] of Object.entries(EXPECTED_API_VERSIONS)) { const currentMap: Record = { meta: 'v25.0', tiktok: 'v1.3', ga4: 'latest', pinterest: 'v5', reddit: 'v2.0' }; const current = currentMap[platform] || 'unknown'; const isOk = current === expected || expected === 'latest'; const status = isOk ? 'ok' : 'warning'; if (env.DB) { await logIntelligence(env.DB, runType, platform, 'api_version', status, current, expected, isOk ? `${platform} ${current} — versão correta` : `${platform} ${current} desatualizado, esperado ${expected}` ); } results.push({ platform, current, expected, status }); } return results; } // ── Auditoria de taxa de erro ───────────────────────────────────────────────── export async function auditErrorRates( env: Env, runType: string ): Promise { if (!env.DB) return []; const alerts: ErrorRateAlert[] = []; for (const platform of ['meta', 'ga4', 'tiktok']) { const metrics = await getHealthMetrics(env.DB, platform, 24); const errorRate = metrics.events_sent > 0 ? metrics.events_failed / metrics.events_sent : 0; let status: 'ok' | 'warning' | 'critical' = 'ok'; if (errorRate >= ALERT_THRESHOLDS.errorRateCritical) status = 'critical'; else if (errorRate >= ALERT_THRESHOLDS.errorRateWarning) status = 'warning'; const message = `${platform}: ${metrics.events_sent} eventos, ${metrics.events_failed} falhas (${(errorRate * 100).toFixed(1)}%)`; let alertSent: boolean | undefined = false; if (status !== 'ok') { await sendIntelligenceAlert(env, status, `Taxa de Erro Alta — ${platform.toUpperCase()}`, `📊 ${message}\n🎯 Taxa: ${(errorRate * 100).toFixed(1)}% (limite: ${ALERT_THRESHOLDS.errorRateWarning * 100}%)`); alertSent = true; } if (env.DB) { await logIntelligence(env.DB, runType, platform, 'error_rate', status, `${(errorRate * 100).toFixed(1)}%`, `${ALERT_THRESHOLDS.errorRateWarning * 100}%`, message, alertSent ); } if (status !== 'ok') alerts.push({ platform, errorRate, status }); } return alerts; } // ── Treinar modelo LTV (regressão logística com dados reais do D1) ──────────── export async function trainLtvModel(env: Env): Promise { if (!env.DB) return { skipped: 'DB não disponível' }; try { // Busca leads com informação de conversão (compra confirmada) const rows = await env.DB.prepare(` SELECT l.utm_source, l.utm_medium, l.engagement_score, l.intention_level, CAST(julianday('now') - julianday(l.created_at) AS INTEGER) AS days_since_lead, CASE WHEN l.email IS NOT NULL AND l.email != '' THEN 1 ELSE 0 END AS has_email, CASE WHEN l.phone IS NOT NULL AND l.phone != '' THEN 1 ELSE 0 END AS has_phone, CASE WHEN (l.country = 'br' OR l.country = 'BR' OR l.country IS NULL) THEN 1 ELSE 0 END AS is_br, CAST(strftime('%H', l.created_at) AS INTEGER) AS hour, CASE WHEN EXISTS ( SELECT 1 FROM events e WHERE e.user_id = l.user_id AND e.event_name IN ('Purchase', 'purchase', 'PURCHASE') AND e.created_at > l.created_at ) THEN 1 ELSE 0 END AS label FROM leads l WHERE l.created_at >= datetime('now', '-90 days') LIMIT 5000 `).all(); const dataset = (rows.results || []).map((row: any) => ({ features: extractFeatures(row), label: row.label || 0, })); const model = trainLogisticRegression(dataset); if (!model) { console.log('[LTV Train] Dados insuficientes para treinar modelo'); return { skipped: 'dados insuficientes', samples: dataset.length }; } await saveWeights(env.DB, model); // Invalidar cache KV para que próximas requests carreguem o modelo novo if (env.GEO_CACHE) { env.GEO_CACHE.delete(LTV_WEIGHTS_KV_KEY).catch(() => {}); } console.log(`[LTV Train] Modelo treinado: ${dataset.length} samples, accuracy=${(model.accuracy * 100).toFixed(1)}%, positive_rate=${(model.positiveRate * 100).toFixed(1)}%`); return { trained: true, samples: dataset.length, accuracy: model.accuracy, positiveRate: model.positiveRate }; } catch (err: any) { console.error('[LTV Train] Erro:', err?.message || String(err)); return { error: err?.message || String(err) }; } } // ── Runner principal do Intelligence Agent ──────────────────────────────────── export async function runIntelligenceAgent( env: Env, runType: string ): Promise { console.log(`[Intelligence Agent] Iniciando ${runType}`); // 1. Check de versões const versionResults = await checkApiVersionsIntelligence(env, runType); console.log(`[Intelligence Agent] Versões verificadas: ${versionResults.length} plataformas`); // 2. Relatório diário if (env.DB) { const reports = await generateDailyReport(env.DB); console.log(`[Intelligence Agent] Relatórios gerados: ${reports.length}`); } // 3. Auditoria de taxas de erro const errorAlerts = await auditErrorRates(env, runType); if (errorAlerts.length > 0) { console.warn(`[Intelligence Agent] ${errorAlerts.length} alertas de taxa de erro enviados`); } // 4. Treinar modelo LTV (toda semana) const ltvTrainResult = await trainLtvModel(env); if (ltvTrainResult.trained) { console.log(`[Intelligence Agent] LTV model treinado: accuracy=${(ltvTrainResult.accuracy! * 100).toFixed(1)}%`); if (env.DB) { await logIntelligence(env.DB, runType, 'ltv', 'model_training', 'ok', `accuracy=${(ltvTrainResult.accuracy! * 100).toFixed(1)}%`, null, `Modelo LTV re-treinado com ${ltvTrainResult.samples} amostras` ).catch(() => {}); } } else { console.log(`[Intelligence Agent] LTV model: ${ltvTrainResult.skipped || ltvTrainResult.error || 'sem dados'}`); } // 5. Auto-decisão de winner no A/B LTV Test let abResult: IntelligenceAgentResult['abResult'] = undefined; try { const abRes = await autoDecideAbWinner(env); if (abRes?.decided) { abResult = { decided: abRes.decided, test_id: abRes.test_id, winner_name: abRes.winner_name, improvement: abRes.improvement ? parseFloat(abRes.improvement) : undefined, }; console.log(`[Intelligence Agent] A/B LTV winner auto-decidido: test_id=${abResult.test_id}, winner=${abResult.winner_name}`); await sendIntelligenceAlert(env, 'info', `A/B LTV Test — Winner Declarado Automaticamente`, `🏆 Vencedor: ${abResult.winner_name}\n📈 Melhoria: +${abResult.improvement?.toFixed(1) ?? '?'}pp vs controle\n🆔 Test ID: ${abResult.test_id}\n\n✅ Prompt vencedor ativado automaticamente` ); if (env.DB) { await logIntelligence(env.DB, runType, 'ltv', 'ab_auto_winner', 'ok', abResult.winner_name, null, `A/B winner auto-decidido: test ${abResult.test_id}, melhoria ${abResult.improvement?.toFixed(1)}pp` ).catch(() => {}); } } } catch (err: any) { console.error('[Intelligence Agent] A/B auto-decide error:', err?.message || String(err)); } // 6. Match Quality — análise + alertas let mqAnalysis: IntelligenceAgentResult['mqAnalysis'] = undefined; try { const mqRes = await analyzeMatchQuality(env); if (mqRes) { mqAnalysis = mqRes; console.log(`[Intelligence Agent] Match Quality: score=${mqAnalysis.composite_score ?? 0}%, alerts=${mqAnalysis.alerts?.length ?? 0}`); await alertMatchQuality(env, mqRes); if (env.DB && mqAnalysis.total && mqAnalysis.total > 0) { await logIntelligence(env.DB, runType, 'meta', 'match_quality', (mqAnalysis.alerts && mqAnalysis.alerts.length > 0) ? 'warning' : 'ok', `${mqAnalysis.composite_score ?? 0}%`, '45%', `Match quality 2h: email=${mqAnalysis.email_rate ?? 0}%, fbp=${mqAnalysis.fbp_rate ?? 0}%, score=${mqAnalysis.composite_score ?? 0}%` ).catch(() => {}); } } } catch (err: any) { console.error('[Intelligence Agent] Match quality analysis error:', err?.message || String(err)); } // 7. Auditoria mensal adicional if (runType === 'monthly_audit') { if (env.DB) { try { const ltvStats = await env.DB.prepare(` SELECT predicted_ltv_class, COUNT(*) as count FROM user_profiles WHERE predicted_ltv_class IS NOT NULL AND updated_at > datetime('now', '-30 days') GROUP BY predicted_ltv_class `).all(); const summary = ltvStats.results?.map((r: any) => `${r.predicted_ltv_class}: ${r.count}`).join(', ') || 'sem dados'; await logIntelligence(env.DB, runType, 'all', 'ltv_distribution', 'ok', summary, null, `Distribuição LTV últimos 30 dias: ${summary}`); console.log(`[Intelligence Agent] LTV distribution: ${summary}`); } catch (err: any) { console.error('LTV audit error:', err?.message || String(err)); } // Purge de logs antigos de match quality (> 30 dias) if (env.DB) { await purgeOldMatchQualityLogs(env.DB); console.log('[Intelligence Agent] Match quality logs antigos purgados'); } } } // 8. Customer Match sync semanal (high_intent → Meta Audience) const cmResult = await syncMetaCustomAudience(env); console.log(`[Intelligence Agent] Customer Match Meta: sent=${cmResult?.sent ?? 0}, received=${cmResult?.received ?? 0}`); // 9. ROAS Feedback Loop — cruza leads com compras reais por campanha let roasResult: IntelligenceAgentResult['roasResult'] = undefined; try { const report = await computeRoasFeedback(env, 30); if (report) { roasResult = { campaigns: report.campaigns.length, total_revenue: report.total_revenue, best_campaign: report.best_campaign, }; await sendRoasAlert(env, report); console.log(`[Intelligence Agent] ROAS: ${report.campaigns.length} campanhas, R$${report.total_revenue} receita`); } else { roasResult = { campaigns: 0, total_revenue: 0, best_campaign: null, skipped: 'sem dados suficientes' }; } } catch (err: any) { console.error('[Intelligence Agent] ROAS error:', err?.message || String(err)); } // 10. Nurture Queue — processa mensagens agendadas (D+1, D+3, D+7) let nurtureResult: IntelligenceAgentResult['nurtureResult'] = undefined; try { const nr = await runNurtureQueue(env); nurtureResult = { processed: nr.processed, sent: nr.sent, failed: nr.failed }; console.log(`[Intelligence Agent] Nurture: ${nr.sent}/${nr.processed} mensagens enviadas`); } catch (err: any) { console.error('[Intelligence Agent] Nurture error:', err?.message || String(err)); } // 11. Lookalike Dinâmico — compradores confirmados → Meta Audience seed let lookalikeResult: IntelligenceAgentResult['lookalikeResult'] = undefined; try { const lr = await syncMetaLookalikeSeed(env); lookalikeResult = lr; console.log(`[Intelligence Agent] Lookalike seed: sent=${lr.sent}, type=${lr.seed_type}`); } catch (err: any) { console.error('[Intelligence Agent] Lookalike error:', err?.message || String(err)); } console.log(`[Intelligence Agent] ${runType} concluído — LTV, A/B, match quality, customer match, ROAS, nurture, lookalike`); return { versionResults, errorAlerts, ltvTrainResult, abResult, mqAnalysis, cmResult, roasResult, nurtureResult, lookalikeResult, }; } // ── syncMetaCustomAudience — D1 → Meta Custom Audiences ───────────────────── export async function syncMetaCustomAudience(env: Env): Promise { if (!env.META_ACCESS_TOKEN || !env.META_AD_ACCOUNT_ID || !env.META_AUDIENCE_ID) { console.log('[CustomerMatch] Meta: secrets não configurados — pulando sync'); return { skipped: 'META_AD_ACCOUNT_ID ou META_AUDIENCE_ID não configurados' }; } if (!env.DB) return { skipped: 'DB não disponível' }; try { const profiles = await env.DB.prepare(` SELECT email, phone FROM user_profiles WHERE cohort_label IN ('high_intent', 'buyer_lookalike') AND updated_at > datetime('now', '-30 days') AND email IS NOT NULL LIMIT 10000 `).all(); if (!profiles.results || profiles.results.length === 0) { console.log('[CustomerMatch] Meta: nenhum perfil elegível'); return { sent: 0 }; } const data = await Promise.all( profiles.results.map(async (p: any) => [ p.email ? await sha256(p.email) : '', p.phone ? await sha256(p.phone) : '', ]) ); const body = { payload: { schema: ['EMAIL_SHA256', 'PHONE_SHA256'], data } }; const endpoint = `https://graph.facebook.com/v25.0/${env.META_AUDIENCE_ID}/users`; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...body, access_token: env.META_ACCESS_TOKEN }), }); const result = await res.json() as any; if (!res.ok) { console.error('[CustomerMatch] Meta erro:', res.status, result.error?.message || 'unknown'); return { error: result.error?.message, sent: 0 }; } console.log(`[CustomerMatch] Meta: ${profiles.results.length} perfis sincronizados`); return { sent: profiles.results.length, num_received: result.num_received, received: result.num_received }; } catch (err: any) { console.error('[CustomerMatch] Meta fetch error:', err?.message || String(err)); return { error: err?.message || String(err), sent: 0 }; } } // ── syncMetaLookalikeSeed — compradores confirmados → Meta Audience (Fase 7) ── // Seed de Lookalike mais preciso: usa quem REALMENTE comprou (Purchase event) // em vez de quem só teve intenção (cohort_label = high_intent). // Separado do syncMetaCustomAudience para não misturar seeds de qualidade diferente. export async function syncMetaLookalikeSeed(env: Env): Promise<{ sent: number; seed_type: string; skipped?: string; }> { if (!env.META_ACCESS_TOKEN || !env.META_AUDIENCE_ID) { return { sent: 0, seed_type: 'buyer_confirmed', skipped: 'META secrets não configurados' }; } if (!env.DB) return { sent: 0, seed_type: 'buyer_confirmed', skipped: 'DB não disponível' }; try { // Busca perfis de compradores confirmados (Purchase event nos últimos 60 dias) const confirmed = await env.DB.prepare(` SELECT DISTINCT up.email, up.phone, up.first_name, up.last_name FROM user_profiles up JOIN leads l ON l.user_id = up.user_id WHERE l.event_name IN ('Purchase','purchase') AND l.created_at >= datetime('now', '-60 days') AND up.email IS NOT NULL UNION SELECT DISTINCT up.email, up.phone, up.first_name, up.last_name FROM user_profiles up JOIN quiz_sessions qs ON qs.user_id = up.user_id WHERE qs.qualification = 'comprador' AND qs.created_at >= datetime('now', '-30 days') AND up.email IS NOT NULL LIMIT 10000 `).all(); if (!confirmed.results?.length) { return { sent: 0, seed_type: 'buyer_confirmed', skipped: 'nenhum comprador confirmado no período' }; } const data = await Promise.all( confirmed.results.map(async (p: any) => [ p.email ? await sha256(p.email) : '', p.phone ? await sha256(p.phone) : '', ]) ); const body = { payload: { schema: ['EMAIL_SHA256', 'PHONE_SHA256'], data } }; const endpoint = `https://graph.facebook.com/v25.0/${env.META_AUDIENCE_ID}/users`; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...body, access_token: env.META_ACCESS_TOKEN }), }); const result = await res.json() as any; // Persiste histórico do seed if (env.DB) { await env.DB.prepare(` INSERT INTO lookalike_seeds (audience_id, seed_type, profiles_sent, profiles_received, period_days) VALUES (?, 'buyer_confirmed', ?, ?, 60) `).bind( env.META_AUDIENCE_ID, confirmed.results.length, result.num_received ?? null, ).run().catch(() => {}); } if (!res.ok) { console.error('[Lookalike] Meta erro:', result.error?.message); return { sent: 0, seed_type: 'buyer_confirmed', skipped: result.error?.message }; } // Atualiza cohort_label dos compradores para buyer_confirmed await env.DB.prepare(` UPDATE user_profiles SET cohort_label = 'buyer_confirmed', updated_at = datetime('now') WHERE user_id IN ( SELECT DISTINCT user_id FROM leads WHERE event_name IN ('Purchase','purchase') AND created_at >= datetime('now', '-60 days') ) `).run().catch(() => {}); console.log(`[Lookalike] ${confirmed.results.length} compradores confirmados enviados ao Meta`); return { sent: confirmed.results.length, seed_type: 'buyer_confirmed' }; } catch (err: any) { console.error('[Lookalike] syncMetaLookalikeSeed error:', err?.message || String(err)); return { sent: 0, seed_type: 'buyer_confirmed', skipped: err?.message }; } } // ── buildGoogleCustomerMatchExport — gera JSON para Google Ads Customer Match ─ export async function buildGoogleCustomerMatchExport(env: Env): Promise { if (!env.DB) return []; const profiles = await env.DB.prepare(` SELECT email, phone, first_name, last_name FROM user_profiles WHERE cohort_label IN ('high_intent', 'buyer_lookalike') AND updated_at > datetime('now', '-30 days') AND email IS NOT NULL LIMIT 10000 `).all(); if (!profiles.results?.length) return []; const results: GoogleCustomerMatchExport[] = []; for (const p of profiles.results) { const email = p.email as string | null | undefined; const phone = p.phone as string | null | undefined; const firstName = p.first_name as string | undefined; const lastName = p.last_name as string | undefined; const hashed_email = email ? await sha256(email) : ''; const hashed_phone = phone ? await sha256(phone) : ''; if (hashed_email || hashed_phone) { results.push({ hashed_email: hashed_email || '', hashed_phone: hashed_phone || '', first_name: firstName || '', last_name: lastName || '', }); } } return results; }