Files
helix-engage/scripts/test-ai-flow.ts

253 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Test script — simulates a CC agent (Rekha) receiving a call and using the AI assistant.
* Tests: auth → lead lookup → patient data → doctor data → appointments → calls → activities → AI response
*
* Run: cd helix-engage && npx tsx scripts/test-ai-flow.ts
*/
const GQL = 'http://localhost:4000/graphql';
const SUB = 'fortytwo-dev';
const ORIGIN = 'http://fortytwo-dev.localhost:4010';
// Rekha's credentials
const AGENT_EMAIL = 'rekha.cc@globalhospital.com';
const AGENT_PASSWORD = 'Global@123';
// Simulated incoming call — Priya's phone
const CALLER_PHONE = '9949879837';
let token = '';
let failures = 0;
async function gql(query: string, variables?: any) {
const h: Record<string, string> = { 'Content-Type': 'application/json', 'X-Workspace-Subdomain': SUB };
if (token) h['Authorization'] = `Bearer ${token}`;
const r = await fetch(GQL, { method: 'POST', headers: h, body: JSON.stringify({ query, variables }) });
const d: any = await r.json();
if (d.errors) throw new Error(d.errors[0].message);
return d.data;
}
function assert(condition: boolean, label: string, detail?: string) {
if (condition) {
console.log(`${label}${detail ? `${detail}` : ''}`);
} else {
console.log(`${label}${detail ? `${detail}` : ''}`);
failures++;
}
}
async function main() {
console.log('🧪 Helix Engage — AI Flow Test\n');
console.log(`Agent: ${AGENT_EMAIL}`);
console.log(`Incoming call from: ${CALLER_PHONE}\n`);
// ═══════════════════════════════════════
// STEP 1: Login as Rekha
// ═══════════════════════════════════════
console.log('1⃣ LOGIN');
const d1 = await gql(`mutation { getLoginTokenFromCredentials(email: "${AGENT_EMAIL}", password: "${AGENT_PASSWORD}", origin: "${ORIGIN}") { loginToken { token } } }`);
const lt = d1.getLoginTokenFromCredentials.loginToken.token;
const d2 = await gql(`mutation { getAuthTokensFromLoginToken(loginToken: "${lt}", origin: "${ORIGIN}") { tokens { accessOrWorkspaceAgnosticToken { token } } } }`);
token = d2.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token;
assert(!!token, 'Authenticated as Rekha');
// Verify role
const profile = await gql('{ currentUser { email workspaceMember { name { firstName lastName } roles { label } } } }');
const roles = profile.currentUser.workspaceMember.roles.map((r: any) => r.label);
const name = `${profile.currentUser.workspaceMember.name.firstName} ${profile.currentUser.workspaceMember.name.lastName}`;
assert(roles.includes('HelixEngage User'), `Role: ${roles.join(', ')}`);
assert(name.includes('Rekha'), `Name: ${name}`);
// ═══════════════════════════════════════
// STEP 2: Look up caller by phone number
// ═══════════════════════════════════════
console.log('\n2⃣ LEAD LOOKUP (by phone: 9949879837)');
const leadsData = await gql(`{ leads(first: 50) { edges { node {
id name contactName { firstName lastName }
contactPhone { primaryPhoneNumber }
contactEmail { primaryEmail }
source status interestedService assignedAgent
leadScore contactAttempts firstContacted lastContacted
aiSummary aiSuggestedAction
patientId campaignId
} } } }`);
const leads = leadsData.leads.edges.map((e: any) => e.node);
assert(leads.length > 0, `Found ${leads.length} leads total`);
// Match by phone
const matchedLead = leads.find((l: any) => {
const phone = l.contactPhone?.primaryPhoneNumber ?? '';
return phone.includes(CALLER_PHONE) || CALLER_PHONE.includes(phone);
});
assert(!!matchedLead, 'Matched lead by phone', matchedLead?.name);
assert(matchedLead?.contactName?.firstName === 'Priya', `First name: ${matchedLead?.contactName?.firstName}`);
assert(matchedLead?.status === 'APPOINTMENT_SET', `Status: ${matchedLead?.status}`);
assert(!!matchedLead?.aiSummary, 'Has AI summary', matchedLead?.aiSummary?.substring(0, 80) + '...');
assert(!!matchedLead?.aiSuggestedAction, 'Has AI suggested action', matchedLead?.aiSuggestedAction);
assert(!!matchedLead?.patientId, 'Linked to patient', matchedLead?.patientId);
assert(!!matchedLead?.campaignId, 'Linked to campaign', matchedLead?.campaignId);
const leadId = matchedLead?.id;
const patientId = matchedLead?.patientId;
// ═══════════════════════════════════════
// STEP 3: Fetch patient record
// ═══════════════════════════════════════
console.log('\n3⃣ PATIENT RECORD');
if (patientId) {
const patientData = await gql(`{ patients(first: 1, filter: { id: { eq: "${patientId}" } }) { edges { node {
id name fullName { firstName lastName }
phones { primaryPhoneNumber }
emails { primaryEmail }
dateOfBirth gender patientType
} } } }`);
const patient = patientData.patients.edges[0]?.node;
assert(!!patient, 'Patient record found', patient?.name);
assert(patient?.gender === 'FEMALE', `Gender: ${patient?.gender}`);
assert(patient?.patientType === 'RETURNING', `Type: ${patient?.patientType}`);
assert(!!patient?.dateOfBirth, `DOB: ${patient?.dateOfBirth}`);
} else {
assert(false, 'No patient ID to look up');
}
// ═══════════════════════════════════════
// STEP 4: Fetch appointments for this patient
// ═══════════════════════════════════════
console.log('\n4⃣ APPOINTMENTS');
if (patientId) {
const apptData = await gql(`{ appointments(first: 20, filter: { patientId: { eq: "${patientId}" } }) { edges { node {
id name scheduledAt durationMin appointmentType status
doctorName department reasonForVisit
doctorId
} } } }`);
const appts = apptData.appointments.edges.map((e: any) => e.node);
assert(appts.length >= 2, `Found ${appts.length} appointments`);
const completed = appts.filter((a: any) => a.status === 'COMPLETED');
const scheduled = appts.filter((a: any) => a.status === 'SCHEDULED');
assert(completed.length >= 1, `${completed.length} completed`);
assert(scheduled.length >= 1, `${scheduled.length} upcoming`);
for (const a of appts) {
const status = a.status === 'COMPLETED' ? '✓' : '📅';
assert(!!a.doctorId, `${status} ${a.name} — doctor linked`, a.doctorId);
}
}
// ═══════════════════════════════════════
// STEP 5: Fetch doctor record (Dr. Patel)
// ═══════════════════════════════════════
console.log('\n5⃣ DOCTOR LOOKUP');
const doctorsData = await gql(`{ doctors(first: 10) { edges { node {
id name fullName { firstName lastName }
department specialty qualifications yearsOfExperience
branchClinic visitingHours
consultationFeeNew { amountMicros currencyCode }
consultationFeeFollowUp { amountMicros currencyCode }
active registrationNumber
portalUserId
} } } }`);
const doctors = doctorsData.doctors.edges.map((e: any) => e.node);
assert(doctors.length === 5, `Found ${doctors.length} doctors`);
const drPatel = doctors.find((d: any) => d.name?.includes('Patel'));
assert(!!drPatel, 'Found Dr. Patel', drPatel?.specialty);
assert(!!drPatel?.visitingHours, `Hours: ${drPatel?.visitingHours}`);
assert(!!drPatel?.consultationFeeNew, `Fee (new): ₹${(drPatel?.consultationFeeNew?.amountMicros ?? 0) / 1_000_000}`);
assert(!!drPatel?.portalUserId, 'Linked to workspace member', drPatel?.portalUserId);
const drSharma = doctors.find((d: any) => d.name?.includes('Sharma'));
assert(!!drSharma, 'Found Dr. Sharma', drSharma?.specialty);
// ═══════════════════════════════════════
// STEP 6: Fetch call history for this lead
// ═══════════════════════════════════════
console.log('\n6⃣ CALL HISTORY');
if (leadId) {
const callsData = await gql(`{ calls(first: 20, filter: { leadId: { eq: "${leadId}" } }) { edges { node {
id name direction callStatus agentName
startedAt durationSec disposition
} } } }`);
const calls = callsData.calls.edges.map((e: any) => e.node);
assert(calls.length >= 3, `Found ${calls.length} calls for Priya`);
for (const c of calls) {
console.log(` ${c.direction} | ${c.disposition ?? c.callStatus} | ${c.name}`);
}
}
// ═══════════════════════════════════════
// STEP 7: Fetch lead activities
// ═══════════════════════════════════════
console.log('\n7⃣ LEAD ACTIVITIES');
if (leadId) {
const activitiesData = await gql(`{ leadActivities(first: 30, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node {
id activityType summary occurredAt performedBy channel
} } } }`);
const activities = activitiesData.leadActivities.edges.map((e: any) => e.node);
assert(activities.length >= 8, `Found ${activities.length} activities for Priya`);
for (const a of activities.slice(0, 5)) {
console.log(` ${a.activityType} | ${a.summary}`);
}
if (activities.length > 5) console.log(` ... and ${activities.length - 5} more`);
}
// ═══════════════════════════════════════
// STEP 8: Simulate AI context assembly
// ═══════════════════════════════════════
console.log('\n8⃣ AI CONTEXT ASSEMBLY');
console.log(' What the AI assistant would receive:\n');
console.log(` Caller: ${matchedLead?.contactName?.firstName} ${matchedLead?.contactName?.lastName}`);
console.log(` Phone: ${CALLER_PHONE}`);
console.log(` Status: ${matchedLead?.status}`);
console.log(` Service: ${matchedLead?.interestedService}`);
console.log(` AI Summary: ${matchedLead?.aiSummary}`);
console.log(` Suggested Action: ${matchedLead?.aiSuggestedAction}`);
console.log(` Doctor: Dr. Patel — ${drPatel?.specialty}`);
console.log(` Next Appointment: ${drPatel?.visitingHours} at ${drPatel?.branchClinic}`);
console.log(` Fee: ₹${(drPatel?.consultationFeeNew?.amountMicros ?? 0) / 1_000_000} (new) / ₹${(drPatel?.consultationFeeFollowUp?.amountMicros ?? 0) / 1_000_000} (follow-up)`);
// ═══════════════════════════════════════
// STEP 9: Test AI chat endpoint (if sidecar is running)
// ═══════════════════════════════════════
console.log('\n9⃣ AI CHAT (via sidecar — skipped if not running)');
try {
const aiRes = await fetch('http://localhost:4100/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({
message: 'Tell me about the patient on the line. When is their next appointment?',
context: { callerPhone: CALLER_PHONE, leadId, leadName: `${matchedLead?.contactName?.firstName} ${matchedLead?.contactName?.lastName}` },
}),
signal: AbortSignal.timeout(15000),
});
if (aiRes.ok) {
const aiData = await aiRes.json();
assert(!!aiData.reply, 'AI responded');
console.log(`\n 🤖 AI says:\n "${aiData.reply}"\n`);
assert(aiData.sources?.length > 0, `Sources: ${aiData.sources?.join(', ')}`);
} else {
console.log(` ⏭️ Sidecar returned ${aiRes.status} — skipping AI test`);
}
} catch {
console.log(' ⏭️ Sidecar not running — AI chat test skipped');
}
// ═══════════════════════════════════════
// RESULTS
// ═══════════════════════════════════════
console.log('\n' + '═'.repeat(50));
if (failures === 0) {
console.log('🎉 ALL TESTS PASSED');
} else {
console.log(`⚠️ ${failures} test(s) failed`);
}
console.log('═'.repeat(50));
}
main().catch(e => { console.error('💥', e.message); process.exit(1); });