collabSupport/services/callTest/twilioEnrichment.js
jmcqueen 1eaabbcee1 Add /voicereport store digest and harden Webex CDR feed handling.
Groups cdr_feed legs by Correlation ID for call-level summaries, fixes report-column field parsing and Docker proxy routing, and adds /calltest post-call Twilio and CDR enrichment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 13:43:11 -04:00

143 lines
4 KiB
JavaScript

// 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();
}