feat: supervisor AI — 4 tools + dedicated system prompt

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 13:05:32 +05:30
parent 1d1f27607f
commit f231f6fd73

View File

@@ -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<any>(
`{ calls(first: 200, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id direction callStatus agentName startedAt durationSec disposition } } } }`,
undefined, auth,
),
platformService.queryWithAuth<any>(
`{ leads(first: 200) { edges { node { id assignedAgent status } } } }`,
undefined, auth,
),
platformService.queryWithAuth<any>(
`{ agents(first: 20) { edges { node { id name ozonetelagentid npsscore maxidleminutes minnpsthreshold minconversionpercent } } } }`,
undefined, auth,
),
platformService.queryWithAuth<any>(
`{ 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<any>(
`{ campaigns(first: 20) { edges { node { id campaignName campaignStatus platform leadCount convertedCount budget { amountMicros } } } } }`,
undefined, auth,
),
platformService.queryWithAuth<any>(
`{ 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<any>(
`{ 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<string, number> = {};
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<any>(
`{ 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.