// services/callTest/twilioEnrichment.js // Post-call Twilio Call resource + Voice Insights enrichment. import { fetchCallInsightsMetrics, fetchCallInsightsSummary, fetchCallResource, } from '../../integrations/twilio/client.js'; import { logger } from '../../utils/logger.js'; import { getCallTestConfig } from './config.js'; import { getSession, updateSession } from './sessionStore.js'; import { renderCallTestTwilioDetailsMarkdown, renderCallTestTwilioInsightsMarkdown, } from '../renderers/callTestRenderer.js'; const LOG_SCOPE = 'calltest:twilio-enrich'; const _detailTimers = new Map(); const _insightTimers = new Map(); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function runDetailsEnrichment(testId, notifyRoom) { const session = getSession(testId); if (!session?.callSid || !session.roomId) return; logger(LOG_SCOPE, `Fetching Twilio details for testId=${testId} callSid=${session.callSid}`, 'info'); const details = await fetchCallResource(session.callSid); const cur = getSession(testId); if (!cur) return; updateSession(testId, { enrichment: { ...(cur.enrichment || {}), twilio: { ...(cur.enrichment?.twilio || {}), details, detailsFetchedAt: new Date().toISOString(), }, }, }); await notifyRoom( session.roomId, renderCallTestTwilioDetailsMarkdown(getSession(testId), details), ); } async function runInsightsEnrichment(testId, notifyRoom, cfg) { const session = getSession(testId); if (!session?.callSid || !session.roomId) return; let summary = null; let metrics = null; let lastReason = null; for (let attempt = 0; attempt <= cfg.twilioInsightsRetries; attempt++) { if (attempt > 0) { await sleep(cfg.twilioInsightsRetryIntervalMs); } summary = await fetchCallInsightsSummary(session.callSid); metrics = await fetchCallInsightsMetrics(session.callSid); const summaryOk = summary.available; const metricsOk = metrics.available; const retryable = summary.retryable || metrics.retryable; if (summaryOk || metricsOk) break; lastReason = summary.reason || metrics.reason; if (!retryable) break; logger( LOG_SCOPE, `Insights not ready for ${session.callSid} (attempt ${attempt + 1}/${cfg.twilioInsightsRetries + 1}): ${lastReason}`, 'debug', ); } const insights = { available: summary?.available || metrics?.available || false, reason: lastReason, summary: summary?.available ? summary.data : null, metrics: metrics?.available ? metrics.data : null, summaryRaw: summary?.raw, metricsRaw: metrics?.raw, }; const cur = getSession(testId); if (!cur) return; updateSession(testId, { enrichment: { ...(cur.enrichment || {}), twilio: { ...(cur.enrichment?.twilio || {}), insights, insightsFetchedAt: new Date().toISOString(), }, }, }); await notifyRoom( session.roomId, renderCallTestTwilioInsightsMarkdown(getSession(testId), insights), ); } export function scheduleTwilioEnrichment(session, notifyRoom) { const cfg = session.config || getCallTestConfig(session.storeNum); if (!cfg.twilioEnrich || !session.callSid || !session.roomId) return; const testId = session.testId; if (!_detailTimers.has(testId)) { const t = setTimeout(() => { _detailTimers.delete(testId); void runDetailsEnrichment(testId, notifyRoom); }, cfg.twilioDetailsDelayMs); if (typeof t.unref === 'function') t.unref(); _detailTimers.set(testId, t); } if (!_insightTimers.has(testId)) { const t = setTimeout(() => { _insightTimers.delete(testId); void runInsightsEnrichment(testId, notifyRoom, cfg); }, cfg.twilioInsightsDelayMs); if (typeof t.unref === 'function') t.unref(); _insightTimers.set(testId, t); } } export function _clearTwilioEnrichmentTimersForTests() { for (const t of _detailTimers.values()) clearTimeout(t); for (const t of _insightTimers.values()) clearTimeout(t); _detailTimers.clear(); _insightTimers.clear(); }