From f231f6fd73092a3841f7300eb8fb8652d9fbc247 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Thu, 2 Apr 2026 13:05:32 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20supervisor=20AI=20=E2=80=94=204=20tools?= =?UTF-8?q?=20+=20dedicated=20system=20prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_agent_performance: call counts, conversion, NPS, threshold breaches - get_campaign_stats: lead counts, conversion per campaign - get_call_summary: aggregate stats by period with disposition breakdown - get_sla_breaches: missed calls past SLA threshold - Supervisor system prompt: unbiased, data-grounded, threshold-based - Context routing: supervisor/rules-engine/agent tool sets Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ai/ai-chat.controller.ts | 218 +++++++++++++++++++++++++++++++++-- 1 file changed, 211 insertions(+), 7 deletions(-) diff --git a/src/ai/ai-chat.controller.ts b/src/ai/ai-chat.controller.ts index 98e5b90..6964943 100644 --- a/src/ai/ai-chat.controller.ts +++ b/src/ai/ai-chat.controller.ts @@ -81,6 +81,8 @@ export class AiChatController { // Rules engine context — use rules-specific system prompt if (ctx?.type === 'rules-engine') { systemPrompt = this.buildRulesSystemPrompt(ctx.currentConfig); + } else if (ctx?.type === 'supervisor') { + systemPrompt = this.buildSupervisorSystemPrompt(); } else { const kb = await this.buildKnowledgeBase(auth); systemPrompt = this.buildSystemPrompt(kb); @@ -98,13 +100,187 @@ export class AiChatController { } const platformService = this.platform; + const isSupervisor = ctx?.type === 'supervisor'; - const result = streamText({ - model: this.aiModel, - system: systemPrompt, - messages, - stopWhen: stepCountIs(5), - tools: { + // Supervisor tools — agent performance, campaign stats, team metrics + const supervisorTools = { + get_agent_performance: tool({ + description: 'Get performance metrics for all agents or a specific agent. Returns call counts, conversion rates, idle time, NPS scores.', + inputSchema: z.object({ + agentName: z.string().optional().describe('Agent name to look up. Leave empty for all agents.'), + }), + execute: async ({ agentName }) => { + const [callsData, leadsData, agentsData, followUpsData] = await Promise.all([ + platformService.queryWithAuth( + `{ calls(first: 200, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id direction callStatus agentName startedAt durationSec disposition } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ leads(first: 200) { edges { node { id assignedAgent status } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ agents(first: 20) { edges { node { id name ozonetelagentid npsscore maxidleminutes minnpsthreshold minconversionpercent } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ followUps(first: 100) { edges { node { id assignedAgent status } } } }`, + undefined, auth, + ), + ]); + + const calls = callsData.calls.edges.map((e: any) => e.node); + const leads = leadsData.leads.edges.map((e: any) => e.node); + const agents = agentsData.agents.edges.map((e: any) => e.node); + const followUps = followUpsData.followUps.edges.map((e: any) => e.node); + + const agentMetrics = agents + .filter((a: any) => !agentName || a.name.toLowerCase().includes(agentName.toLowerCase())) + .map((agent: any) => { + const agentCalls = calls.filter((c: any) => c.agentName === agent.name || c.agentName === agent.ozonetelagentid); + const totalCalls = agentCalls.length; + const missed = agentCalls.filter((c: any) => c.callStatus === 'MISSED').length; + const completed = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length; + const apptBooked = agentCalls.filter((c: any) => c.disposition === 'APPOINTMENT_BOOKED').length; + const agentLeads = leads.filter((l: any) => l.assignedAgent === agent.name); + const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === agent.name); + const pendingFollowUps = agentFollowUps.filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE').length; + const conversionRate = totalCalls > 0 ? Math.round((apptBooked / totalCalls) * 100) : 0; + + return { + name: agent.name, + totalCalls, + completed, + missed, + appointmentsBooked: apptBooked, + conversionRate: `${conversionRate}%`, + assignedLeads: agentLeads.length, + pendingFollowUps, + npsScore: agent.npsscore, + maxIdleMinutes: agent.maxidleminutes, + minNpsThreshold: agent.minnpsthreshold, + minConversionPercent: agent.minconversionpercent, + belowNpsThreshold: agent.minnpsthreshold && (agent.npsscore ?? 100) < agent.minnpsthreshold, + belowConversionThreshold: agent.minconversionpercent && conversionRate < agent.minconversionpercent, + }; + }); + + return { agents: agentMetrics, totalAgents: agentMetrics.length }; + }, + }), + + get_campaign_stats: tool({ + description: 'Get campaign performance stats — lead counts, conversion rates, sources.', + inputSchema: z.object({}), + execute: async () => { + const [campaignsData, leadsData] = await Promise.all([ + platformService.queryWithAuth( + `{ campaigns(first: 20) { edges { node { id campaignName campaignStatus platform leadCount convertedCount budget { amountMicros } } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ leads(first: 200) { edges { node { id campaignId status } } } }`, + undefined, auth, + ), + ]); + + const campaigns = campaignsData.campaigns.edges.map((e: any) => e.node); + const leads = leadsData.leads.edges.map((e: any) => e.node); + + return { + campaigns: campaigns.map((c: any) => { + const campaignLeads = leads.filter((l: any) => l.campaignId === c.id); + const converted = campaignLeads.filter((l: any) => l.status === 'CONVERTED' || l.status === 'APPOINTMENT_SET').length; + return { + name: c.campaignName, + status: c.campaignStatus, + platform: c.platform, + totalLeads: campaignLeads.length, + converted, + conversionRate: campaignLeads.length > 0 ? `${Math.round((converted / campaignLeads.length) * 100)}%` : '0%', + budget: c.budget ? `₹${c.budget.amountMicros / 1_000_000}` : null, + }; + }), + }; + }, + }), + + get_call_summary: tool({ + description: 'Get aggregate call statistics — total calls, inbound/outbound split, missed call rate, average duration, disposition breakdown.', + inputSchema: z.object({ + period: z.string().optional().describe('Period: "today", "week", "month". Defaults to "week".'), + }), + execute: async ({ period }) => { + const data = await platformService.queryWithAuth( + `{ calls(first: 500, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id direction callStatus agentName startedAt durationSec disposition } } } }`, + undefined, auth, + ); + const allCalls = data.calls.edges.map((e: any) => e.node); + + // Filter by period + const now = new Date(); + const start = new Date(now); + if (period === 'today') start.setHours(0, 0, 0, 0); + else if (period === 'month') start.setDate(start.getDate() - 30); + else start.setDate(start.getDate() - 7); // default week + + const calls = allCalls.filter((c: any) => c.startedAt && new Date(c.startedAt) >= start); + + const total = calls.length; + const inbound = calls.filter((c: any) => c.direction === 'INBOUND').length; + const outbound = total - inbound; + const missed = calls.filter((c: any) => c.callStatus === 'MISSED').length; + const completed = calls.filter((c: any) => c.callStatus === 'COMPLETED').length; + const totalDuration = calls.reduce((sum: number, c: any) => sum + (c.durationSec ?? 0), 0); + const avgDuration = completed > 0 ? Math.round(totalDuration / completed) : 0; + + const dispositions: Record = {}; + for (const c of calls) { + if (c.disposition) dispositions[c.disposition] = (dispositions[c.disposition] ?? 0) + 1; + } + + return { + period: period ?? 'week', + total, + inbound, + outbound, + missed, + completed, + missedRate: total > 0 ? `${Math.round((missed / total) * 100)}%` : '0%', + avgDurationSeconds: avgDuration, + dispositions, + }; + }, + }), + + get_sla_breaches: tool({ + description: 'Get calls/leads that have breached their SLA — items that were not handled within the expected timeframe.', + inputSchema: z.object({}), + execute: async () => { + const data = await platformService.queryWithAuth( + `{ calls(first: 100, filter: { callStatus: { eq: MISSED }, callbackstatus: { eq: PENDING_CALLBACK } }) { edges { node { id callerNumber { primaryPhoneNumber } startedAt agentName sla } } } }`, + undefined, auth, + ); + const breached = data.calls.edges + .map((e: any) => e.node) + .filter((c: any) => (c.sla ?? 0) > 100); + + return { + breachedCount: breached.length, + items: breached.map((c: any) => ({ + id: c.id, + phone: c.callerNumber?.primaryPhoneNumber ?? 'Unknown', + slaPercent: c.sla, + missedAt: c.startedAt, + agent: c.agentName, + })), + }; + }, + }), + }; + + // Agent tools — patient lookup, appointments, doctors + const agentTools = { lookup_patient: tool({ description: 'Search for a patient/lead by phone number or name. Returns their profile, lead status, AI summary.', inputSchema: z.object({ @@ -284,7 +460,14 @@ export class AiChatController { return { calls: data.calls.edges.map((e: any) => e.node) }; }, }), - }, + }; + + const result = streamText({ + model: this.aiModel, + system: systemPrompt, + messages, + stopWhen: stepCountIs(5), + tools: isSupervisor ? supervisorTools : agentTools, }); const response = result.toTextStreamResponse(); @@ -461,6 +644,27 @@ export class AiChatController { return this.knowledgeBase; } + private buildSupervisorSystemPrompt(): string { + return `You are an AI assistant for supervisors at Global Hospital's call center (Helix Engage). +You help supervisors monitor team performance, identify issues, and make data-driven decisions. + +## YOUR CAPABILITIES +You have access to tools that query real-time data: +- **Agent performance**: call counts, conversion rates, NPS scores, idle time, pending follow-ups +- **Campaign stats**: lead counts, conversion rates per campaign, platform breakdown +- **Call summary**: total calls, inbound/outbound split, missed call rate, disposition breakdown +- **SLA breaches**: missed calls that haven't been called back within the SLA threshold + +## RULES +1. ALWAYS use tools to fetch data before answering. NEVER guess or fabricate performance numbers. +2. Be specific — include actual numbers from the tool response, not vague qualifiers. +3. When comparing agents, use their configured thresholds (minConversionPercent, minNpsThreshold, maxIdleMinutes) and team averages. Let the data determine who is underperforming — do not assume. +4. Be concise — supervisors want quick answers. Use bullet points. +5. When recommending actions, ground them in the data returned by tools. +6. If asked about trends, use the call summary tool with different periods. +7. Do not use any agent name in a negative context unless the data explicitly supports it.`; + } + private buildRulesSystemPrompt(currentConfig: any): string { const configJson = JSON.stringify(currentConfig, null, 2); return `You are an AI assistant helping a hospital supervisor configure the Rules Engine for their call center worklist.