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'; import { AiConfigService } from '../config/ai-config.service'; @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, private aiConfig: AiConfigService, ) { const cfg = aiConfig.getConfig(); this.aiModel = createAiModel({ provider: cfg.provider, model: cfg.model, anthropicApiKey: config.get('ai.anthropicApiKey'), openaiApiKey: config.get('ai.openaiApiKey'), }); this.platformApiKey = config.get('platform.apiKey') ?? ''; } async loadCallContext(leadId: string | null, callerPhone: string | null): Promise { 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( `{ 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( `{ appointments(first: 10, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { id scheduledAt status 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.status}`); } } } 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( `{ 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 { 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 ''; } } }