-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathserver.ts
More file actions
1341 lines (1182 loc) · 49.4 KB
/
Copy pathserver.ts
File metadata and controls
1341 lines (1182 loc) · 49.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import express from 'express';
import path from 'path';
import fs from 'fs';
import os from 'os';
// vite is imported dynamically inside startServer() to avoid crashing serverless runtimes
import { GoogleGenAI, Type } from '@google/genai';
import dotenv from 'dotenv';
import multer from 'multer';
import bcrypt from 'bcryptjs';
import { db, seedInitialData, UserRecord, PolicyDocumentRecord, UserRole, PolicyAccessLevel } from './server/db';
import { authenticateUser, requireRole, signUserToken, AuthenticatedRequest } from './server/auth';
import { extractTextFromFileAsync, processDocumentChunks, searchPolicyChunks, isAccessPermitted } from './server/documentProcessor';
dotenv.config({ override: true });
const app = express();
const PORT = Number(process.env.PORT) || 3009;
app.use(express.json({ limit: '10mb' }));
// Cross-Origin Resource Sharing (CORS) Middleware
app.use((req, res, next) => {
const origin = req.headers.origin || '*';
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
});
// Multer Upload Configuration for Admin Policy Documents
const storage = multer.diskStorage({
destination: (req, file, cb) => {
let uploadDir = path.join(process.cwd(), 'uploads', 'policies');
try {
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
} catch {
// In serverless / read-only environments (e.g. Vercel /var/task), fall back to os.tmpdir()
uploadDir = path.join(os.tmpdir(), 'csa-uploads', 'policies');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname);
cb(null, `policy-${uniqueSuffix}${ext}`);
}
});
const upload = multer({
storage,
limits: { fileSize: 25 * 1024 * 1024 }
});
// Lazy GoogleGenAI client
let aiClient: GoogleGenAI | null = null;
function getAi(): GoogleGenAI | null {
if (!process.env.GEMINI_API_KEY) {
return null;
}
if (!aiClient) {
aiClient = new GoogleGenAI({
apiKey: process.env.GEMINI_API_KEY,
httpOptions: {
headers: {
'User-Agent': 'aistudio-build'
}
}
});
}
return aiClient;
}
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', hasGeminiKey: !!process.env.GEMINI_API_KEY });
});
// Helper for fallback text responses
function sanitizeJsonString(raw: string): string {
let clean = raw.trim();
if (clean.startsWith('```json')) {
clean = clean.replace(/^```json/, '').replace(/```$/, '').trim();
} else if (clean.startsWith('```')) {
clean = clean.replace(/^```/, '').replace(/```$/, '').trim();
}
return clean;
}
// 1. Analyze Turn (Multi-Agent Pipeline)
app.post('/api/analyze-turn', async (req, res) => {
try {
const { customerMessage, conversationHistory = [], scenario, lastAgentMessage, knowledgeDocs = [] } = req.body;
const ai = getAi();
if (!ai) {
// Fallback structured simulation if no API key
return res.json({
intent: scenario?.category === 'Billing' ? 'Duplicate Billing / Refund Dispute' : 'Customer Inquiry',
intentConfidence: 94,
sentiment: 'negative',
sentimentConfidence: 89,
frustrationLevel: 72,
frustrationTrend: 'increasing',
emotions: ['Frustration', 'Urgency', 'Disappointment'],
relevantKnowledge: {
kbId: 'KB-102',
title: 'Duplicate Subscription Charges & Billing Disputes',
relevantSection: 'Section 3.2: Duplicate Charge Reversal',
policySnippet: 'When duplicate charges occur due to gateway sync issues, verify both transaction IDs and issue immediate full credit. Inform customer: Funds reappear within 3-5 business days.',
source: 'Refund Policy → Section 3.2',
confidence: 96,
troubleshootingSteps: [
'Verify both transaction timestamps in billing portal',
'Confirm identical descriptor and $49 charge',
'Authorize immediate refund reversal',
'Communicate 3-5 business days banking clearance window'
],
isVerified: true
},
escalationRisk: 68,
escalationLevel: 'high',
riskReasons: [
'Customer mentioned previous support ticket was ignored',
'Financial discrepancy creates high anxiety',
'Customer threatened bank dispute and account cancellation'
],
recommendedIntervention: 'Acknowledge prior ticket delay with sincerity, confirm immediate refund initiation, and provide the 3-5 day banking timeline.',
coachWhisper: '💡 Acknowledge their previous unanswered email first before stating the refund timeline.',
alertType: 'warning',
suggestedResponses: {
quick: "I'm so sorry about the duplicate charge and prior delay. I've initiated your $49 refund right away.",
professional: "I apologize for the delay on your previous inquiry and the duplicate charge. I have verified the discrepancy and processed an immediate $49 refund, which will reflect in 3-5 business days.",
empathetic: "I completely understand how frustrating it is to see duplicate charges and not receive a prompt reply. Let me make this right immediately—I've verified the error and authorized your full refund now.",
concise: "Apologies for the duplicate charge. I've processed your $49 refund, visible in 3-5 business days.",
detailed: "Thank you for bringing this to our attention. I reviewed our billing records, confirmed the duplicate $49 charge from the gateway sync, and processed an immediate reversal to your card. You'll receive a confirmation receipt shortly, and funds will return in 3-5 business days.",
deEscalation: "I am genuinely sorry for the stress this caused and that your previous email was missed. You will not have to dispute anything—I have already processed your $49 refund and verified your account is clean."
},
whyReasons: [
'Validating prior ignored communication instantly stops escalation to supervisor',
'Cites verified KB-102 refund reversal protocol with 100% compliance',
'Assures proactive ownership without asking customer to do manual legwork'
],
counterfactual: {
alternativeResponse: "You have to wait 5 business days for billing tickets to process.",
predictedRiskDrop: -25,
reasoning: "A dismissive response would increase escalation risk from 68% to 93% and trigger supervisor demand."
},
agentEvaluation: lastAgentMessage ? {
tone: 'Empathetic',
empathyScore: 88,
clarityScore: 92,
concisenessScore: 90,
grammarScore: 98,
policyComplianceScore: 95,
problemNoticed: 'Good empathy; make sure to specify 3-5 day banking window.',
coachingAdvice: 'Strong ownership. Reassure customer with the exact refund transaction ID.'
} : undefined
});
}
const kbContext = knowledgeDocs.length > 0
? knowledgeDocs.map((d: any) => `[${d.id}] ${d.title}\n${d.summary}\n${d.content}`).join('\n\n')
: 'Standard Support Knowledge Base: Refund policy permits 100% refund within 30 days. Reversals take 3-5 business days.';
const systemPrompt = `You are an expert AI Customer Support Coaching Engine.
Analyze the latest customer turn and conversation state in a real-time support training session.
Scenario Context:
Title: ${scenario?.title || 'Support Case'}
Category: ${scenario?.category || 'General'}
Customer Persona: ${scenario?.customerPersona?.name || 'Customer'} (${scenario?.customerPersona?.type || 'Customer'})
Objectives: ${scenario?.sessionObjectives || 'Resolve issue without escalation'}
Knowledge Base Articles (RAG):
${kbContext}
Recent Conversation:
${conversationHistory.map((m: any) => `${m.sender.toUpperCase()}: ${m.text}`).join('\n')}
Latest Customer Message: "${customerMessage}"
${lastAgentMessage ? `Last Agent Message to Evaluate: "${lastAgentMessage}"` : ''}
Output ONLY valid JSON adhering strictly to this structure:
{
"intent": "Brief intent label (e.g. Duplicate Charge Refund)",
"intentConfidence": 95,
"sentiment": "positive" | "neutral" | "negative" | "very_negative",
"sentimentConfidence": 90,
"frustrationLevel": 75,
"frustrationTrend": "increasing" | "decreasing" | "stable",
"emotions": ["Frustration", "Urgency"],
"relevantKnowledge": {
"kbId": "KB-101",
"title": "Article Title",
"relevantSection": "Section 3.2",
"policySnippet": "Exact verified quote from KB",
"source": "Refund Policy → Section 3.2",
"confidence": 94,
"troubleshootingSteps": ["Step 1", "Step 2"],
"isVerified": true
},
"escalationRisk": 70,
"escalationLevel": "low" | "moderate" | "high" | "critical",
"riskReasons": ["Reason 1", "Reason 2"],
"recommendedIntervention": "Actionable coaching instruction",
"coachWhisper": "💡 Short punchy 1-sentence tip",
"alertType": "info" | "warning" | "critical",
"suggestedResponses": {
"quick": "Short reply",
"professional": "Formal polite reply",
"empathetic": "Emotion-first reply",
"concise": "Direct minimal reply",
"detailed": "Thorough step-by-step reply",
"deEscalation": "High empathy de-escalating reply"
},
"whyReasons": ["Reason 1 why this response works", "Reason 2"],
"counterfactual": {
"alternativeResponse": "Example poor or dismissive response",
"predictedRiskDrop": -30,
"reasoning": "Why the poor response would trigger escalation"
},
"agentEvaluation": ${lastAgentMessage ? `{
"tone": "Empathetic" | "Polite" | "Professional" | "Robotic" | "Defensive" | "Dismissive",
"empathyScore": 85,
"clarityScore": 90,
"concisenessScore": 88,
"grammarScore": 95,
"policyComplianceScore": 92,
"problemNoticed": "Specific critique if any",
"coachingAdvice": "Specific improvement tip"
}` : 'null'}
}`;
const response = await ai.models.generateContent({
model: 'gemini-3.7-flash',
contents: systemPrompt,
config: {
responseMimeType: 'application/json',
temperature: 0.3
}
});
const jsonText = sanitizeJsonString(response.text || '{}');
const parsed = JSON.parse(jsonText);
res.json(parsed);
} catch (error: any) {
console.error('Error in /api/analyze-turn:', error);
res.status(500).json({ error: error.message || 'Analysis failed' });
}
});
// 2. Simulate Customer Turn (Dynamic Emotional State)
app.post('/api/simulate-customer', async (req, res) => {
try {
const { scenario, conversationHistory = [], agentResponse, currentCustomerState } = req.body;
const ai = getAi();
const currentState = currentCustomerState || {
frustration: scenario?.customerPersona?.baseFrustration || 60,
trust: scenario?.customerPersona?.trust || 40,
patience: scenario?.customerPersona?.patience || 40,
satisfaction: scenario?.customerPersona?.satisfaction || 30,
escalationIntent: scenario?.customerPersona?.escalationIntent || 45
};
if (!ai) {
// Rule-based dynamic emotional state updater
const text = (agentResponse || '').toLowerCase();
const isEmpathetic = text.includes('sorry') || text.includes('understand') || text.includes('apologize') || text.includes('refund');
const isDismissive = text.includes('policy') && !isEmpathetic;
const frustrationDelta = isEmpathetic ? -25 : (isDismissive ? +20 : -5);
const trustDelta = isEmpathetic ? +20 : (isDismissive ? -15 : +5);
const satDelta = isEmpathetic ? +25 : -10;
const newFrustration = Math.max(5, Math.min(100, currentState.frustration + frustrationDelta));
const newTrust = Math.max(5, Math.min(100, currentState.trust + trustDelta));
const newSat = Math.max(5, Math.min(100, currentState.satisfaction + satDelta));
const newEscalation = Math.max(0, Math.min(100, currentState.escalationIntent - (isEmpathetic ? 30 : -15)));
const isResolved = newFrustration <= 20 && newSat >= 70;
const isEscalated = newEscalation >= 85 || newFrustration >= 90;
let nextCustomerMessage = "Thank you for checking that for me. Does that mean I'll receive a confirmation email once it's posted?";
if (isResolved) {
nextCustomerMessage = "Thank you so much! That solves my problem completely. I really appreciate your quick help and understanding.";
} else if (isEscalated) {
nextCustomerMessage = "I have had enough of this runaround! Please transfer me to your supervisor or manager right now.";
} else if (newFrustration > 50) {
nextCustomerMessage = "Okay, but how long is this actually going to take? I need to be 100% sure this won't happen again next month.";
}
return res.json({
nextCustomerMessage,
updatedCustomerState: {
frustration: newFrustration,
trust: newTrust,
patience: Math.max(5, Math.min(100, currentState.patience + (isEmpathetic ? 15 : -15))),
satisfaction: newSat,
escalationIntent: newEscalation
},
isResolved,
isEscalated,
stateChangeExplanation: isEmpathetic
? "Agent expressed genuine empathy and offered immediate solution: Frustration dropped -25%, Trust rose +20%."
: "Agent provided informational response without deep empathy: Frustration adjusted moderately."
});
}
const prompt = `You are roleplaying as a realistic customer in a support training simulator.
Persona Details:
Name: ${scenario?.customerPersona?.name || 'Customer'}
Personality Type: ${scenario?.customerPersona?.type || 'Customer'}
Behavior: ${scenario?.customerPersona?.behaviorDescription || 'Customer with an issue'}
Scenario Problem: ${scenario?.initialProblem || 'Support issue'}
Escalation Trigger: ${scenario?.escalationTrigger || 'Robotic answers or refusal to help'}
Current Hidden Emotional State:
Frustration: ${currentState.frustration}%
Trust: ${currentState.trust}%
Patience: ${currentState.patience}%
Satisfaction: ${currentState.satisfaction}%
Escalation Intent: ${currentState.escalationIntent}%
Conversation Transcript so far:
${conversationHistory.map((m: any) => `${m.sender.toUpperCase()}: ${m.text}`).join('\n')}
Agent's Latest Response:
"${agentResponse}"
Task:
1. Evaluate how the agent's response impacts the customer's emotions (empathy/clear solution reduces frustration; robotic/dismissive/blaming increases frustration).
2. Calculate new emotional state percentages (0-100).
3. If satisfaction is >= 75% and frustration <= 25%, mark isResolved: true and express genuine satisfaction.
4. If escalationIntent >= 85% or frustration >= 90%, mark isEscalated: true and demand a manager.
5. Generate the customer's next natural, conversational reply in character.
Output ONLY valid JSON:
{
"nextCustomerMessage": "Customer's next spoken message",
"updatedCustomerState": {
"frustration": 45,
"trust": 60,
"patience": 50,
"satisfaction": 55,
"escalationIntent": 20
},
"isResolved": false,
"isEscalated": false,
"stateChangeExplanation": "Brief explanation of how agent response affected state"
}`;
const response = await ai.models.generateContent({
model: 'gemini-3.7-flash',
contents: prompt,
config: {
responseMimeType: 'application/json',
temperature: 0.7
}
});
const parsed = JSON.parse(sanitizeJsonString(response.text || '{}'));
res.json(parsed);
} catch (error: any) {
console.error('Error in /api/simulate-customer:', error);
res.status(500).json({ error: error.message || 'Simulation failed' });
}
});
// 3. Generate Scenario with AI (Trainer Tool)
app.post('/api/generate-scenario', async (req, res) => {
try {
const { prompt: userPrompt, category = 'Billing', difficulty = 'hard' } = req.body;
const ai = getAi();
if (!ai) {
return res.json({
id: `SCENARIO-${Date.now().toString().slice(-4)}`,
title: `Simulated ${category} Scenario: ${userPrompt || 'Customer Dispute'}`,
category,
difficulty,
customerPersona: {
id: `persona-${Date.now()}`,
name: 'Jordan Miller',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&auto=format&fit=crop&q=80',
type: difficulty === 'expert' ? 'Angry' : (difficulty === 'hard' ? 'Highly frustrated' : 'Impatient'),
behaviorDescription: 'Fast-paced customer who expects immediate answers and transparent accountability.',
baseFrustration: difficulty === 'expert' ? 85 : 70,
patience: 30,
trust: 35,
satisfaction: 20,
escalationIntent: 60
},
initialProblem: userPrompt || 'Subscription billed twice on renewal.',
customerOpeningMessage: `Hi, I am experiencing an issue regarding ${userPrompt || 'my bill'}. This is unacceptable and I need this resolved right now.`,
expectedResolution: 'Apologize sincerely, confirm the issue against policy, initiate appropriate resolution, and reassure timelines.',
escalationTrigger: 'Giving canned responses without checking logs or asking customer to repeat themselves.',
successCriteria: [
'Acknowledge customer emotions immediately',
'Apply correct policy from Knowledge Base',
'Provide clear timeline and resolution steps',
'Avoid escalation to supervisor'
],
sessionObjectives: `Resolve ${userPrompt || 'the support dispute'} within 3-4 turns while de-escalating customer frustration.`,
relevantKbIds: ['KB-101', 'KB-102'],
targetResolutionTurns: 4
});
}
const aiPrompt = `Generate a comprehensive, realistic customer support training scenario.
User Prompt: "${userPrompt}"
Category: ${category}
Difficulty: ${difficulty} (easy, medium, hard, expert)
Return ONLY valid JSON matching this schema:
{
"id": "SCENARIO-${Date.now().toString().slice(-4)}",
"title": "Descriptive Scenario Title",
"category": "${category}",
"difficulty": "${difficulty}",
"customerPersona": {
"id": "persona-gen-${Date.now()}",
"name": "Full Customer Name",
"avatar": "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&auto=format&fit=crop&q=80",
"type": "Angry" | "Highly frustrated" | "Confused" | "Impatient" | "Professional" | "Technically knowledgeable",
"behaviorDescription": "Detailed behavioral description of customer",
"baseFrustration": 75,
"patience": 25,
"trust": 30,
"satisfaction": 20,
"escalationIntent": 65
},
"initialProblem": "Detailed summary of the customer's issue",
"customerOpeningMessage": "Opening message that the AI customer will say",
"expectedResolution": "Clear guide on what the agent should do to succeed",
"escalationTrigger": "What agent mistakes cause the customer to escalate",
"successCriteria": [
"Criteria 1",
"Criteria 2",
"Criteria 3",
"Criteria 4"
],
"sessionObjectives": "Clear objective statement for the agent",
"relevantKbIds": ["KB-101", "KB-102"],
"targetResolutionTurns": 4
}`;
const response = await ai.models.generateContent({
model: 'gemini-3.7-flash',
contents: aiPrompt,
config: {
responseMimeType: 'application/json',
temperature: 0.7
}
});
const parsed = JSON.parse(sanitizeJsonString(response.text || '{}'));
res.json(parsed);
} catch (error: any) {
console.error('Error in /api/generate-scenario:', error);
res.status(500).json({ error: error.message || 'Scenario generation failed' });
}
});
// 4. Generate AI Performance Report
app.post('/api/generate-report', async (req, res) => {
try {
const { scenario, messages = [], durationSeconds = 180, coachingLevel = 'beginner' } = req.body;
const ai = getAi();
if (!ai || messages.length === 0) {
return res.json({
score: {
overall: 89,
intentHandling: 94,
knowledgeUsage: 90,
empathy: 88,
tone: 92,
clarity: 95,
resolution: 90,
escalationHandling: 86,
policyCompliance: 96,
resolutionQuality: {
problemIdentification: 95,
correctSolution: 92,
knowledgeAccuracy: 94,
customerSatisfaction: 88,
resolutionCompleteness: 90,
overallQuality: 92
}
},
startingSentiment: 'negative',
endingSentiment: 'positive',
sentimentImprovement: 68,
resolved: true,
escalated: false,
timelineEvents: [
{
turn: 1,
timestamp: '00:15',
type: 'sentiment_shift',
description: 'Customer initiated session with high frustration on duplicate billing.',
severity: 'warning'
},
{
turn: 1,
timestamp: '00:45',
type: 'kb_retrieved',
description: 'RAG Knowledge KB-102 retrieved with 94% relevance match.',
severity: 'normal'
},
{
turn: 2,
timestamp: '01:30',
type: 'empathy_bonus',
description: 'Agent warmly acknowledged prior ticket delay, reducing customer frustration by 35%.',
severity: 'positive'
},
{
turn: 3,
timestamp: '02:45',
type: 'resolution_milestone',
description: 'Full refund authorized; customer confirmed complete satisfaction.',
severity: 'positive'
}
],
topStrengths: [
'Excellent empathy and emotional validation on first contact turn',
'Strict adherence to verified Knowledge Base refund timelines',
'Fast resolution without unnecessary transfers'
],
topWeaknesses: [
'Could have proactively shared confirmation receipt ID before customer asked'
],
recommendedTrainings: [
'Advanced Financial Dispute De-escalation',
'VIP Customer Care & Retention Mastery'
],
xpEarned: 240,
responseComparisons: messages.filter((m: any) => m.sender === 'agent').slice(0, 2).map((m: any, idx: number) => ({
turnNumber: idx + 1,
originalAgentText: m.text,
aiImprovedText: `I completely understand how concerning this is. I have already verified the error in our system and processed your full refund, which will appear in 3-5 business days.`,
improvementExplanation: 'Adds direct emotional validation and highlights active ownership of the solution.'
}))
});
}
const transcript = messages.map((m: any, i: number) => `Turn ${i + 1} [${m.sender.toUpperCase()}]: ${m.text}`).join('\n');
const prompt = `You are the AI Performance Evaluation Engine for a customer support training platform.
Evaluate this completed support session transcript:
Scenario Title: ${scenario?.title || 'Support Session'}
Scenario Category: ${scenario?.category || 'General'}
Customer Persona: ${scenario?.customerPersona?.name} (${scenario?.customerPersona?.type})
Session Duration: ${durationSeconds} seconds
Coaching Level Used: ${coachingLevel}
Transcript:
${transcript}
Task:
Calculate comprehensive multi-dimensional scores (0-100), evaluate whether the customer issue was resolved or escalated, construct a chronological coaching timeline, highlight top strengths/weaknesses, and generate Before vs After response comparisons.
Return ONLY valid JSON matching this schema:
{
"score": {
"overall": 88,
"intentHandling": 92,
"knowledgeUsage": 90,
"empathy": 85,
"tone": 90,
"clarity": 94,
"resolution": 88,
"escalationHandling": 84,
"policyCompliance": 96,
"resolutionQuality": {
"problemIdentification": 95,
"correctSolution": 90,
"knowledgeAccuracy": 94,
"customerSatisfaction": 86,
"resolutionCompleteness": 88,
"overallQuality": 91
}
},
"startingSentiment": "very_negative" | "negative" | "neutral",
"endingSentiment": "positive" | "neutral" | "negative",
"sentimentImprovement": 65,
"resolved": true,
"escalated": false,
"timelineEvents": [
{
"turn": 1,
"timestamp": "00:20",
"type": "sentiment_shift" | "kb_retrieved" | "risk_spike" | "empathy_bonus" | "policy_check" | "resolution_milestone",
"description": "Event description",
"severity": "normal" | "positive" | "warning" | "critical"
}
],
"topStrengths": ["Strength 1", "Strength 2", "Strength 3"],
"topWeaknesses": ["Weakness 1", "Weakness 2"],
"recommendedTrainings": ["Training Module 1", "Training Module 2"],
"xpEarned": 220,
"responseComparisons": [
{
"turnNumber": 1,
"originalAgentText": "Agent's actual message",
"aiImprovedText": "Polished AI improved version",
"improvementExplanation": "Why this improved version is better"
}
]
}`;
const response = await ai.models.generateContent({
model: 'gemini-3.7-flash',
contents: prompt,
config: {
responseMimeType: 'application/json',
temperature: 0.3
}
});
const parsed = JSON.parse(sanitizeJsonString(response.text || '{}'));
res.json(parsed);
} catch (error: any) {
console.error('Error in /api/generate-report:', error);
res.status(500).json({ error: error.message || 'Report generation failed' });
}
});
// 5. Counterfactual Simulation
app.post('/api/counterfactual', async (req, res) => {
try {
const { scenario, customerMessage, customAgentResponse } = req.body;
const ai = getAi();
if (!ai) {
return res.json({
predictedCustomerReaction: "Thank you for looking into this so quickly! That puts my mind at ease.",
predictedFrustrationDelta: -35,
predictedEscalationRisk: 22,
reasoning: "Your response explicitly acknowledged the customer's prior frustration and committed to a concrete timeline, eliminating escalation pressure."
});
}
const prompt = `Simulate counterfactual customer reaction.
Scenario: ${scenario?.title || 'Support Case'}
Customer Message: "${customerMessage}"
Agent Proposed Response: "${customAgentResponse}"
Evaluate how the customer would react to this response. Output ONLY valid JSON:
{
"predictedCustomerReaction": "Realistic customer response",
"predictedFrustrationDelta": -30,
"predictedEscalationRisk": 25,
"reasoning": "Detailed analysis of why this response works or fails"
}`;
const response = await ai.models.generateContent({
model: 'gemini-3.7-flash',
contents: prompt,
config: {
responseMimeType: 'application/json',
temperature: 0.4
}
});
res.json(JSON.parse(sanitizeJsonString(response.text || '{}')));
} catch (error: any) {
console.error('Error in /api/counterfactual:', error);
res.status(500).json({ error: error.message });
}
});
// 6. Multilingual Translation
app.post('/api/translate', async (req, res) => {
try {
const { text, targetLang = 'English' } = req.body;
const ai = getAi();
if (!ai) {
return res.json({
translatedText: text,
detectedLang: 'English',
intent: 'General Inquiry'
});
}
const prompt = `Translate this customer support text into ${targetLang} and identify its intent.
Text: "${text}"
Output JSON:
{
"translatedText": "Translated text",
"detectedLang": "Language name",
"intent": "Intent label"
}`;
const response = await ai.models.generateContent({
model: 'gemini-3.7-flash',
contents: prompt,
config: {
responseMimeType: 'application/json',
temperature: 0.2
}
});
res.json(JSON.parse(sanitizeJsonString(response.text || '{}')));
} catch (error: any) {
console.error('Error in /api/translate:', error);
res.status(500).json({ error: error.message });
}
});
/* ==========================================================================
AUTHENTICATION & USER MANAGEMENT ENDPOINTS
========================================================================== */
// 1. Auth: Login
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ success: false, error: 'Email and password are required.', message: 'Email and password are required.' });
}
const user = db.getUserByEmail(email);
if (!user) {
return res.status(401).json({ success: false, error: 'Invalid email or password.', message: 'Invalid email or password.' });
}
if (user.status === 'inactive') {
return res.status(403).json({ success: false, error: 'Account is deactivated. Please contact your administrator.', message: 'Account is deactivated. Please contact your administrator.' });
}
const isValidPassword = await bcrypt.compare(password, user.passwordHash);
if (!isValidPassword) {
return res.status(401).json({ success: false, error: 'Invalid email or password.', message: 'Invalid email or password.' });
}
const token = signUserToken(user);
db.updateUser(user.id, { lastLogin: new Date().toISOString() });
db.addAuditLog({
userName: user.name,
userEmail: user.email,
userRole: user.role,
action: 'USER_LOGIN',
category: 'auth',
details: `Successful login as ${user.role}`
});
res.status(200).json({
success: true,
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
status: user.status,
createdAt: user.createdAt,
lastLogin: user.lastLogin
}
});
} catch (err: any) {
console.error('Login error:', err);
res.status(500).json({ success: false, error: 'Login failed due to an internal error.', message: 'Login failed due to an internal error.' });
}
});
// 2. Auth: Get Current User Profile
app.get('/api/auth/me', authenticateUser, (req: AuthenticatedRequest, res) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const user = db.getUserById(req.user.userId);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json({
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
status: user.status,
createdAt: user.createdAt,
lastLogin: user.lastLogin
}
});
});
// 3. Auth: Logout
app.post('/api/auth/logout', authenticateUser, (req: AuthenticatedRequest, res) => {
if (req.user) {
db.addAuditLog({
userName: req.user.name,
userEmail: req.user.email,
userRole: req.user.role,
action: 'USER_LOGOUT',
category: 'auth',
details: 'User logged out'
});
}
res.json({ status: 'ok', message: 'Logged out successfully' });
});
// 4. Admin: List All Users
app.get('/api/admin/users', authenticateUser, requireRole('admin'), (req, res) => {
const users = db.getUsers().map(u => ({
id: u.id,
name: u.name,
email: u.email,
role: u.role,
status: u.status,
createdAt: u.createdAt,
lastLogin: u.lastLogin
}));
res.json(users);
});
// 5. Admin: Create New User
app.post('/api/admin/users', authenticateUser, requireRole('admin'), async (req: AuthenticatedRequest, res) => {
try {
const { name, email, password, role = 'employee' } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ error: 'Name, email, and password are required.' });
}
if (!['admin', 'trainer', 'employee'].includes(role)) {
return res.status(400).json({ error: 'Invalid role. Role must be admin, trainer, or employee.' });
}
const existing = db.getUserByEmail(email);
if (existing) {
return res.status(400).json({ error: 'A user with this email address already exists.' });
}
const passwordHash = await bcrypt.hash(password, 10);
const newUser: UserRecord = {
id: `usr-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
name: name.trim(),
email: email.trim().toLowerCase(),
passwordHash,
role: role as UserRole,
status: 'active',
createdAt: new Date().toISOString()
};
db.createUser(newUser);
db.addAuditLog({
userName: req.user?.name || 'Admin',
userEmail: req.user?.email || 'admin',
userRole: req.user?.role || 'admin',
action: 'CREATE_USER',
category: 'user',
details: `Created new user ${newUser.name} (${newUser.email}) with role ${newUser.role}`,
resource: newUser.id
});
res.json({
message: 'User created successfully',
user: {
id: newUser.id,
name: newUser.name,
email: newUser.email,
role: newUser.role,
status: newUser.status,
createdAt: newUser.createdAt
}
});
} catch (err: any) {
console.error('Error creating user:', err);
res.status(500).json({ error: 'Failed to create user.' });
}
});
// 6. Admin: Update User
app.put('/api/admin/users/:id', authenticateUser, requireRole('admin'), async (req: AuthenticatedRequest, res) => {
try {
const { id } = req.params;
const { name, role, status, password } = req.body;
const user = db.getUserById(id);
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
const updates: Partial<UserRecord> = {};
if (name) updates.name = name.trim();
if (role && ['admin', 'trainer', 'employee'].includes(role)) updates.role = role as UserRole;
if (status && ['active', 'inactive'].includes(status)) updates.status = status;
if (password) updates.passwordHash = await bcrypt.hash(password, 10);
const updated = db.updateUser(id, updates);
db.addAuditLog({
userName: req.user?.name || 'Admin',
userEmail: req.user?.email || 'admin',
userRole: req.user?.role || 'admin',
action: 'UPDATE_USER',
category: 'user',
details: `Updated user ${user.name} details: ${Object.keys(updates).join(', ')}`,
resource: id
});
res.json({
message: 'User updated successfully',
user: updated ? {
id: updated.id,
name: updated.name,
email: updated.email,
role: updated.role,
status: updated.status,
createdAt: updated.createdAt,
lastLogin: updated.lastLogin
} : null
});
} catch (err: any) {
res.status(500).json({ error: 'Failed to update user.' });
}
});
// 7. Admin: Delete User
app.delete('/api/admin/users/:id', authenticateUser, requireRole('admin'), (req: AuthenticatedRequest, res) => {
const { id } = req.params;
const user = db.getUserById(id);
if (!user) return res.status(404).json({ error: 'User not found.' });
if (user.id === req.user?.userId) {
return res.status(400).json({ error: 'You cannot delete your own admin account while logged in.' });
}
db.deleteUser(id);
db.addAuditLog({
userName: req.user?.name || 'Admin',
userEmail: req.user?.email || 'admin',
userRole: req.user?.role || 'admin',
action: 'DELETE_USER',
category: 'user',
details: `Deleted user ${user.name} (${user.email})`,
resource: id
});
res.json({ message: 'User deleted successfully' });
});
/* ==========================================================================
ADMIN POLICY MANAGEMENT & RAG ENDPOINTS
========================================================================== */
// 8. Admin: Upload Policy Files (Single, Multi-file, or Folder batch)
app.post('/api/admin/policies/upload', authenticateUser, requireRole('admin'), upload.array('files'), async (req: AuthenticatedRequest, res) => {
try {
const files = req.files as Express.Multer.File[];
if (!files || files.length === 0) {
return res.status(400).json({ error: 'No policy files uploaded.' });
}
const { category = 'General', accessLevel = 'EMPLOYEE' } = req.body;
const uploadedDocs: PolicyDocumentRecord[] = [];
for (const file of files) {
const docId = `pol-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
// === AUTO-VERSIONING: Deactivate old versions of same document ===
const existingPolicies = db.getPolicies();
const baseName = file.originalname.replace(/\.[^/.]+$/, '').trim().toLowerCase();
const previousVersions = existingPolicies.filter(p => {
const existingBase = p.originalName.replace(/\.[^/.]+$/, '').trim().toLowerCase();
return existingBase === baseName && p.isActive;
});
let newVersion = 1;
if (previousVersions.length > 0) {
const maxVersion = Math.max(...previousVersions.map(p => p.version || 1));
newVersion = maxVersion + 1;
// Mark all previous versions as inactive
for (const prev of previousVersions) {
db.savePolicy({ ...prev, isActive: false, status: 'inactive' });
// Mark all chunks of old version as inactive
const oldChunks = db.getChunks().filter(c => c.documentId === prev.id);
const inactiveChunks = oldChunks.map(c => ({ ...c, isActive: false }));
db.saveChunks(inactiveChunks);
}
db.addAuditLog({
userName: req.user?.name || 'Admin',
userEmail: req.user?.email || 'admin',