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>
143 lines
4 KiB
JavaScript
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();
|
|
}
|