mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: call control, recording, CDR, missed calls, live call assist
- Call Control API (CONFERENCE/HOLD/MUTE/KICK_CALL) - Recording pause/unpause - Fetch CDR Detailed (call history with recordings) - Abandon Calls (missed calls from Ozonetel) - Call Assist WebSocket gateway (Deepgram STT + OpenAI suggestions) - Call Assist service (lead context loading) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.58",
|
"@ai-sdk/anthropic": "^3.0.58",
|
||||||
"@ai-sdk/openai": "^3.0.41",
|
"@ai-sdk/openai": "^3.0.41",
|
||||||
|
"@deepgram/sdk": "^5.0.0",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
@@ -833,6 +834,18 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@deepgram/sdk": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "http://localhost:4873/@deepgram/sdk/-/sdk-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-x1wMiOgDGqcLEaQpQBQLTtk5mLbXbYgcBEpp7cfJIyEtqdIGgijCZH+a/esiVp+xIcTYYroTxG47RVppZOHbWw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "http://localhost:4873/@emnapi/core/-/core-1.9.0.tgz",
|
"resolved": "http://localhost:4873/@emnapi/core/-/core-1.9.0.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.58",
|
"@ai-sdk/anthropic": "^3.0.58",
|
||||||
"@ai-sdk/openai": "^3.0.41",
|
"@ai-sdk/openai": "^3.0.41",
|
||||||
|
"@deepgram/sdk": "^5.0.0",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { OzonetelAgentModule } from './ozonetel/ozonetel-agent.module';
|
|||||||
import { GraphqlProxyModule } from './graphql-proxy/graphql-proxy.module';
|
import { GraphqlProxyModule } from './graphql-proxy/graphql-proxy.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
import { WorklistModule } from './worklist/worklist.module';
|
import { WorklistModule } from './worklist/worklist.module';
|
||||||
|
import { CallAssistModule } from './call-assist/call-assist.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -26,6 +27,7 @@ import { WorklistModule } from './worklist/worklist.module';
|
|||||||
GraphqlProxyModule,
|
GraphqlProxyModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
WorklistModule,
|
WorklistModule,
|
||||||
|
CallAssistModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
136
src/call-assist/call-assist.gateway.ts
Normal file
136
src/call-assist/call-assist.gateway.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import {
|
||||||
|
WebSocketGateway,
|
||||||
|
SubscribeMessage,
|
||||||
|
MessageBody,
|
||||||
|
ConnectedSocket,
|
||||||
|
OnGatewayDisconnect,
|
||||||
|
} from '@nestjs/websockets';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { Socket } from 'socket.io';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import { CallAssistService } from './call-assist.service';
|
||||||
|
|
||||||
|
type SessionState = {
|
||||||
|
deepgramWs: WebSocket | null;
|
||||||
|
transcript: string;
|
||||||
|
context: string;
|
||||||
|
suggestionTimer: NodeJS.Timeout | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
@WebSocketGateway({
|
||||||
|
cors: { origin: process.env.CORS_ORIGIN ?? '*', credentials: true },
|
||||||
|
namespace: '/call-assist',
|
||||||
|
})
|
||||||
|
export class CallAssistGateway implements OnGatewayDisconnect {
|
||||||
|
private readonly logger = new Logger(CallAssistGateway.name);
|
||||||
|
private readonly sessions = new Map<string, SessionState>();
|
||||||
|
private readonly deepgramApiKey: string;
|
||||||
|
|
||||||
|
constructor(private readonly callAssist: CallAssistService) {
|
||||||
|
this.deepgramApiKey = process.env.DEEPGRAM_API_KEY ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('call-assist:start')
|
||||||
|
async handleStart(
|
||||||
|
@ConnectedSocket() client: Socket,
|
||||||
|
@MessageBody() data: { ucid: string; leadId?: string; callerPhone?: string },
|
||||||
|
) {
|
||||||
|
this.logger.log(`Call assist start: ucid=${data.ucid} lead=${data.leadId ?? 'none'}`);
|
||||||
|
|
||||||
|
const context = await this.callAssist.loadCallContext(
|
||||||
|
data.leadId ?? null,
|
||||||
|
data.callerPhone ?? null,
|
||||||
|
);
|
||||||
|
client.emit('call-assist:context', { context: context.substring(0, 200) + '...' });
|
||||||
|
|
||||||
|
const session: SessionState = {
|
||||||
|
deepgramWs: null,
|
||||||
|
transcript: '',
|
||||||
|
context,
|
||||||
|
suggestionTimer: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.deepgramApiKey) {
|
||||||
|
const dgUrl = `wss://api.deepgram.com/v1/listen?model=nova-2&language=en&smart_format=true&interim_results=true&endpointing=300&sample_rate=16000&encoding=linear16&channels=1`;
|
||||||
|
|
||||||
|
const dgWs = new WebSocket(dgUrl, {
|
||||||
|
headers: { Authorization: `Token ${this.deepgramApiKey}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
dgWs.on('open', () => {
|
||||||
|
this.logger.log(`Deepgram connected for ${data.ucid}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
dgWs.on('message', (raw: WebSocket.Data) => {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(raw.toString());
|
||||||
|
const text = result.channel?.alternatives?.[0]?.transcript;
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
const isFinal = result.is_final;
|
||||||
|
client.emit('call-assist:transcript', { text, isFinal });
|
||||||
|
|
||||||
|
if (isFinal) {
|
||||||
|
session.transcript += `Customer: ${text}\n`;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
dgWs.on('error', (err) => {
|
||||||
|
this.logger.error(`Deepgram error: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
dgWs.on('close', () => {
|
||||||
|
this.logger.log(`Deepgram closed for ${data.ucid}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
session.deepgramWs = dgWs;
|
||||||
|
} else {
|
||||||
|
this.logger.warn('DEEPGRAM_API_KEY not set — transcription disabled');
|
||||||
|
client.emit('call-assist:error', { message: 'Transcription not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI suggestion every 10 seconds
|
||||||
|
session.suggestionTimer = setInterval(async () => {
|
||||||
|
if (!session.transcript.trim()) return;
|
||||||
|
const suggestion = await this.callAssist.getSuggestion(session.transcript, session.context);
|
||||||
|
if (suggestion) {
|
||||||
|
client.emit('call-assist:suggestion', { text: suggestion });
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
this.sessions.set(client.id, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('call-assist:audio')
|
||||||
|
handleAudio(
|
||||||
|
@ConnectedSocket() client: Socket,
|
||||||
|
@MessageBody() audioData: ArrayBuffer,
|
||||||
|
) {
|
||||||
|
const session = this.sessions.get(client.id);
|
||||||
|
if (session?.deepgramWs?.readyState === WebSocket.OPEN) {
|
||||||
|
session.deepgramWs.send(Buffer.from(audioData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('call-assist:stop')
|
||||||
|
handleStop(@ConnectedSocket() client: Socket) {
|
||||||
|
this.cleanup(client.id);
|
||||||
|
this.logger.log(`Call assist stopped: ${client.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(client: Socket) {
|
||||||
|
this.cleanup(client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanup(clientId: string) {
|
||||||
|
const session = this.sessions.get(clientId);
|
||||||
|
if (session) {
|
||||||
|
if (session.suggestionTimer) clearInterval(session.suggestionTimer);
|
||||||
|
if (session.deepgramWs) {
|
||||||
|
try { session.deepgramWs.close(); } catch {}
|
||||||
|
}
|
||||||
|
this.sessions.delete(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/call-assist/call-assist.module.ts
Normal file
10
src/call-assist/call-assist.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CallAssistGateway } from './call-assist.gateway';
|
||||||
|
import { CallAssistService } from './call-assist.service';
|
||||||
|
import { PlatformModule } from '../platform/platform.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PlatformModule],
|
||||||
|
providers: [CallAssistGateway, CallAssistService],
|
||||||
|
})
|
||||||
|
export class CallAssistModule {}
|
||||||
123
src/call-assist/call-assist.service.ts
Normal file
123
src/call-assist/call-assist.service.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { generateText } from 'ai';
|
||||||
|
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||||
|
import { createAiModel } from '../ai/ai-provider';
|
||||||
|
import type { LanguageModel } from 'ai';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CallAssistService {
|
||||||
|
private readonly logger = new Logger(CallAssistService.name);
|
||||||
|
private readonly aiModel: LanguageModel | null;
|
||||||
|
private readonly platformApiKey: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private config: ConfigService,
|
||||||
|
private platform: PlatformGraphqlService,
|
||||||
|
) {
|
||||||
|
this.aiModel = createAiModel(config);
|
||||||
|
this.platformApiKey = config.get<string>('platform.apiKey') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCallContext(leadId: string | null, callerPhone: string | null): Promise<string> {
|
||||||
|
const authHeader = this.platformApiKey ? `Bearer ${this.platformApiKey}` : '';
|
||||||
|
if (!authHeader) return 'No platform context available.';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (leadId) {
|
||||||
|
const leadResult = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ leads(filter: { id: { eq: "${leadId}" } }) { edges { node {
|
||||||
|
id name contactName { firstName lastName }
|
||||||
|
contactPhone { primaryPhoneNumber }
|
||||||
|
source status interestedService
|
||||||
|
lastContacted contactAttempts
|
||||||
|
aiSummary aiSuggestedAction
|
||||||
|
} } } }`,
|
||||||
|
undefined, authHeader,
|
||||||
|
);
|
||||||
|
const lead = leadResult.leads.edges[0]?.node;
|
||||||
|
if (lead) {
|
||||||
|
const name = lead.contactName
|
||||||
|
? `${lead.contactName.firstName} ${lead.contactName.lastName}`.trim()
|
||||||
|
: lead.name;
|
||||||
|
parts.push(`CALLER: ${name}`);
|
||||||
|
parts.push(`Phone: ${lead.contactPhone?.primaryPhoneNumber ?? callerPhone}`);
|
||||||
|
parts.push(`Source: ${lead.source ?? 'Unknown'}`);
|
||||||
|
parts.push(`Interested in: ${lead.interestedService ?? 'Not specified'}`);
|
||||||
|
parts.push(`Contact attempts: ${lead.contactAttempts ?? 0}`);
|
||||||
|
if (lead.aiSummary) parts.push(`AI Summary: ${lead.aiSummary}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apptResult = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ appointments(first: 10, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
||||||
|
id scheduledAt appointmentStatus doctorName department reasonForVisit patientId
|
||||||
|
} } } }`,
|
||||||
|
undefined, authHeader,
|
||||||
|
);
|
||||||
|
const appts = apptResult.appointments.edges
|
||||||
|
.map((e: any) => e.node)
|
||||||
|
.filter((a: any) => a.patientId === leadId);
|
||||||
|
if (appts.length > 0) {
|
||||||
|
parts.push('\nPAST APPOINTMENTS:');
|
||||||
|
for (const a of appts) {
|
||||||
|
const date = a.scheduledAt ? new Date(a.scheduledAt).toLocaleDateString('en-IN') : '?';
|
||||||
|
parts.push(`- ${date}: ${a.doctorName ?? '?'} (${a.department ?? '?'}) — ${a.appointmentStatus}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (callerPhone) {
|
||||||
|
parts.push(`CALLER: Unknown (${callerPhone})`);
|
||||||
|
parts.push('No lead record found — this may be a new enquiry.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const docResult = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ doctors(first: 20) { edges { node {
|
||||||
|
fullName { firstName lastName } department specialty clinic { clinicName }
|
||||||
|
} } } }`,
|
||||||
|
undefined, authHeader,
|
||||||
|
);
|
||||||
|
const docs = docResult.doctors.edges.map((e: any) => e.node);
|
||||||
|
if (docs.length > 0) {
|
||||||
|
parts.push('\nAVAILABLE DOCTORS:');
|
||||||
|
for (const d of docs) {
|
||||||
|
const name = d.fullName ? `Dr. ${d.fullName.firstName} ${d.fullName.lastName}`.trim() : 'Unknown';
|
||||||
|
parts.push(`- ${name} — ${d.department ?? '?'} — ${d.clinic?.clinicName ?? '?'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('\n') || 'No context available.';
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Failed to load call context: ${err}`);
|
||||||
|
return 'Context loading failed.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSuggestion(transcript: string, context: string): Promise<string> {
|
||||||
|
if (!this.aiModel || !transcript.trim()) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { text } = await generateText({
|
||||||
|
model: this.aiModel,
|
||||||
|
system: `You are a real-time call assistant for Global Hospital Bangalore.
|
||||||
|
You listen to the customer's words and provide brief, actionable suggestions for the CC agent.
|
||||||
|
|
||||||
|
${context}
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Keep suggestions under 2 sentences
|
||||||
|
- Focus on actionable next steps the agent should take NOW
|
||||||
|
- If customer mentions a doctor or department, suggest available slots
|
||||||
|
- If customer wants to cancel or reschedule, note relevant appointment details
|
||||||
|
- If customer sounds upset, suggest empathetic response
|
||||||
|
- Do NOT repeat what the agent already knows`,
|
||||||
|
prompt: `Conversation transcript so far:\n${transcript}\n\nProvide a brief suggestion for the agent based on what was just said.`,
|
||||||
|
maxOutputTokens: 150,
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`AI suggestion failed: ${err}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common';
|
import { Controller, Post, Get, Body, Query, Logger, HttpException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OzonetelAgentService } from './ozonetel-agent.service';
|
import { OzonetelAgentService } from './ozonetel-agent.service';
|
||||||
|
|
||||||
@@ -136,6 +136,72 @@ export class OzonetelAgentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('call-control')
|
||||||
|
async callControl(
|
||||||
|
@Body() body: {
|
||||||
|
action: 'CONFERENCE' | 'HOLD' | 'UNHOLD' | 'MUTE' | 'UNMUTE' | 'KICK_CALL';
|
||||||
|
ucid: string;
|
||||||
|
conferenceNumber?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (!body.action || !body.ucid) {
|
||||||
|
throw new HttpException('action and ucid required', 400);
|
||||||
|
}
|
||||||
|
if (body.action === 'CONFERENCE' && !body.conferenceNumber) {
|
||||||
|
throw new HttpException('conferenceNumber required for CONFERENCE action', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Call control: ${body.action} ucid=${body.ucid}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.ozonetelAgent.callControl(body);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error.response?.data?.message ?? error.message ?? 'Call control failed';
|
||||||
|
throw new HttpException(message, error.response?.status ?? 502);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('recording')
|
||||||
|
async recording(
|
||||||
|
@Body() body: { ucid: string; action: 'pause' | 'unPause' },
|
||||||
|
) {
|
||||||
|
if (!body.ucid || !body.action) {
|
||||||
|
throw new HttpException('ucid and action required', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.ozonetelAgent.pauseRecording(body);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error.response?.data?.message ?? error.message ?? 'Recording control failed';
|
||||||
|
throw new HttpException(message, error.response?.status ?? 502);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('missed-calls')
|
||||||
|
async missedCalls() {
|
||||||
|
const result = await this.ozonetelAgent.getAbandonCalls();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('call-history')
|
||||||
|
async callHistory(
|
||||||
|
@Query('date') date?: string,
|
||||||
|
@Query('status') status?: string,
|
||||||
|
@Query('callType') callType?: string,
|
||||||
|
) {
|
||||||
|
const targetDate = date ?? new Date().toISOString().split('T')[0];
|
||||||
|
this.logger.log(`Call history: date=${targetDate} status=${status ?? 'all'} type=${callType ?? 'all'}`);
|
||||||
|
|
||||||
|
const result = await this.ozonetelAgent.fetchCDR({
|
||||||
|
date: targetDate,
|
||||||
|
status,
|
||||||
|
callType,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private mapToOzonetelDisposition(disposition: string): string {
|
private mapToOzonetelDisposition(disposition: string): string {
|
||||||
// Campaign only has 'General Enquiry' configured currently
|
// Campaign only has 'General Enquiry' configured currently
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
|
|||||||
@@ -193,6 +193,165 @@ export class OzonetelAgentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async callControl(params: {
|
||||||
|
action: 'CONFERENCE' | 'HOLD' | 'UNHOLD' | 'MUTE' | 'UNMUTE' | 'KICK_CALL';
|
||||||
|
ucid: string;
|
||||||
|
conferenceNumber?: string;
|
||||||
|
}): Promise<{ status: string; message: string; ucid?: string }> {
|
||||||
|
const url = `https://${this.apiDomain}/ca_apis/CallControl_V4`;
|
||||||
|
const did = process.env.OZONETEL_DID ?? '918041763265';
|
||||||
|
const agentPhoneName = process.env.OZONETEL_SIP_ID ?? '523590';
|
||||||
|
|
||||||
|
this.logger.log(`Call control: action=${params.action} ucid=${params.ucid} conference=${params.conferenceNumber ?? 'none'}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await this.getToken();
|
||||||
|
const body: Record<string, string> = {
|
||||||
|
userName: this.accountId,
|
||||||
|
action: params.action,
|
||||||
|
ucid: params.ucid,
|
||||||
|
did,
|
||||||
|
agentPhoneName,
|
||||||
|
};
|
||||||
|
if (params.conferenceNumber) {
|
||||||
|
body.conferenceNumber = params.conferenceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(url, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Call control response: ${JSON.stringify(response.data)}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
const responseData = error?.response?.data ? JSON.stringify(error.response.data) : '';
|
||||||
|
this.logger.error(`Call control failed: ${error.message} ${responseData}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pauseRecording(params: {
|
||||||
|
ucid: string;
|
||||||
|
action: 'pause' | 'unPause';
|
||||||
|
}): Promise<{ status: string; message: string }> {
|
||||||
|
const url = `https://${this.apiDomain}/CAServices/Call/Record.php`;
|
||||||
|
|
||||||
|
this.logger.log(`Recording ${params.action}: ucid=${params.ucid}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
params: {
|
||||||
|
userName: this.accountId,
|
||||||
|
apiKey: this.apiKey,
|
||||||
|
action: params.action,
|
||||||
|
ucid: params.ucid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Recording control response: ${JSON.stringify(response.data)}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
const responseData = error?.response?.data ? JSON.stringify(error.response.data) : '';
|
||||||
|
this.logger.error(`Recording control failed: ${error.message} ${responseData}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAbandonCalls(params?: {
|
||||||
|
fromTime?: string;
|
||||||
|
toTime?: string;
|
||||||
|
campaignName?: string;
|
||||||
|
}): Promise<Array<{
|
||||||
|
monitorUCID: string;
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
campaign: string;
|
||||||
|
callerID: string;
|
||||||
|
did: string;
|
||||||
|
agentID: string;
|
||||||
|
agent: string;
|
||||||
|
hangupBy: string;
|
||||||
|
callTime: string;
|
||||||
|
}>> {
|
||||||
|
const url = `https://${this.apiDomain}/ca_apis/abandonCalls`;
|
||||||
|
|
||||||
|
this.logger.log('Fetching abandon calls');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await this.getToken();
|
||||||
|
const body: Record<string, string> = { userName: this.accountId };
|
||||||
|
if (params?.fromTime) body.fromTime = params.fromTime;
|
||||||
|
if (params?.toTime) body.toTime = params.toTime;
|
||||||
|
if (params?.campaignName) body.campaignName = params.campaignName;
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
this.logger.log(`Abandon calls response: ${JSON.stringify(data).substring(0, 200)}`);
|
||||||
|
if (data.status === 'success' && Array.isArray(data.message)) {
|
||||||
|
return data.message;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Abandon calls failed: ${error.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCDR(params: {
|
||||||
|
date: string; // YYYY-MM-DD
|
||||||
|
campaignName?: string;
|
||||||
|
status?: string;
|
||||||
|
callType?: string;
|
||||||
|
}): Promise<Array<Record<string, any>>> {
|
||||||
|
const url = `https://${this.apiDomain}/ca_reports/fetchCDRDetails`;
|
||||||
|
|
||||||
|
this.logger.log(`Fetch CDR: date=${params.date}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await this.getToken();
|
||||||
|
const body: Record<string, string> = {
|
||||||
|
userName: this.accountId,
|
||||||
|
fromDate: `${params.date} 00:00:00`,
|
||||||
|
toDate: `${params.date} 23:59:59`,
|
||||||
|
};
|
||||||
|
if (params.campaignName) body.campaignName = params.campaignName;
|
||||||
|
if (params.status) body.status = params.status;
|
||||||
|
if (params.callType) body.callType = params.callType;
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
if (data.status === 'success' && Array.isArray(data.details)) {
|
||||||
|
return data.details;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error: any) {
|
||||||
|
const responseData = error?.response?.data ? JSON.stringify(error.response.data) : '';
|
||||||
|
this.logger.error(`Fetch CDR failed: ${error.message} ${responseData}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async logoutAgent(params: {
|
async logoutAgent(params: {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user