mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 10:07:22 +00:00
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user