CognxSafeTrack commited on
Commit
2d900bc
·
1 Parent(s): a8486f0

feat: Implement fuzzy matching for commands and input guardrails in whatsapp.ts

Browse files
Files changed (1) hide show
  1. apps/api/src/services/whatsapp.ts +58 -6
apps/api/src/services/whatsapp.ts CHANGED
@@ -20,6 +20,39 @@ export class WhatsAppService {
20
  return 'UNKNOWN';
21
  }
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  static async handleIncomingMessage(phone: string, text: string, audioUrl?: string) {
24
  const traceId = audioUrl ? `[STT-FLOW-${phone.slice(-4)}]` : `[TXT-FLOW-${phone.slice(-4)}]`;
25
  const normalizedText = this.normalizeCommand(text);
@@ -29,7 +62,7 @@ export class WhatsAppService {
29
  let user = await prisma.user.findUnique({ where: { phone } });
30
 
31
  if (!user) {
32
- const isInscription = normalizedText === 'INSCRIPTION' || normalizedText.includes('INSCRI');
33
 
34
  if (isInscription) {
35
  console.log(`${traceId} New user registration triggered for ${phone}`);
@@ -69,7 +102,7 @@ export class WhatsAppService {
69
  }
70
 
71
  // 1.5. Testing / Cheat Codes (Only for registered users)
72
- if (normalizedText === 'INSCRIPTION') {
73
  await prisma.enrollment.deleteMany({ where: { userId: user.id } });
74
  await prisma.userProgress.deleteMany({ where: { userId: user.id } });
75
  await prisma.response.deleteMany({ where: { userId: user.id } });
@@ -116,7 +149,7 @@ export class WhatsAppService {
116
  return;
117
  }
118
 
119
- if (normalizedText === 'SEED') {
120
  // Reply immediately so the webhook doesn't time out
121
  console.log(`[SEED] Triggered by user ${user.id}`);
122
  try {
@@ -307,8 +340,8 @@ export class WhatsAppService {
307
 
308
  if (activeEnrollment) {
309
  const intent = this.detectIntent(text);
310
- const isSuite = normalizedText === 'SUITE' || normalizedText === '2';
311
- const isApprofondir = normalizedText.includes('APPROFONDIR') || normalizedText === '1';
312
 
313
  // Handle SUITE Priority
314
  if (isSuite) {
@@ -464,7 +497,26 @@ export class WhatsAppService {
464
  where: { trackId: activeEnrollment.trackId, dayNumber: activeEnrollment.currentDay }
465
  });
466
 
467
- if (trackDayFallback && text.length > 3) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  // Fetch previous responses to provide context to the AI Coach
469
  const previousResponsesData = await prisma.response.findMany({
470
  where: { userId: user.id, enrollmentId: activeEnrollment.id },
 
20
  return 'UNKNOWN';
21
  }
22
 
23
+ private static levenshteinDistance(a: string, b: string): number {
24
+ const matrix: number[][] = [];
25
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
26
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
27
+
28
+ for (let i = 1; i <= b.length; i++) {
29
+ for (let j = 1; j <= a.length; j++) {
30
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
31
+ matrix[i][j] = matrix[i - 1][j - 1];
32
+ } else {
33
+ matrix[i][j] = Math.min(
34
+ matrix[i - 1][j - 1] + 1, // substitution
35
+ matrix[i][j - 1] + 1, // insertion
36
+ matrix[i - 1][j] + 1 // deletion
37
+ );
38
+ }
39
+ }
40
+ }
41
+ return matrix[b.length][a.length];
42
+ }
43
+
44
+ private static isFuzzyMatch(text: string, target: string, threshold = 0.8): boolean {
45
+ const normalized = text.trim().toUpperCase();
46
+ const tar = target.toUpperCase();
47
+ if (normalized === tar) return true;
48
+ if (normalized.includes(tar) || tar.includes(normalized)) return true;
49
+
50
+ const distance = this.levenshteinDistance(normalized, tar);
51
+ const maxLength = Math.max(normalized.length, tar.length);
52
+ const similarity = 1 - distance / maxLength;
53
+ return similarity >= threshold;
54
+ }
55
+
56
  static async handleIncomingMessage(phone: string, text: string, audioUrl?: string) {
57
  const traceId = audioUrl ? `[STT-FLOW-${phone.slice(-4)}]` : `[TXT-FLOW-${phone.slice(-4)}]`;
58
  const normalizedText = this.normalizeCommand(text);
 
62
  let user = await prisma.user.findUnique({ where: { phone } });
63
 
64
  if (!user) {
65
+ const isInscription = this.isFuzzyMatch(normalizedText, 'INSCRIPTION') || normalizedText.includes('INSCRI');
66
 
67
  if (isInscription) {
68
  console.log(`${traceId} New user registration triggered for ${phone}`);
 
102
  }
103
 
104
  // 1.5. Testing / Cheat Codes (Only for registered users)
105
+ if (this.isFuzzyMatch(normalizedText, 'INSCRIPTION')) {
106
  await prisma.enrollment.deleteMany({ where: { userId: user.id } });
107
  await prisma.userProgress.deleteMany({ where: { userId: user.id } });
108
  await prisma.response.deleteMany({ where: { userId: user.id } });
 
149
  return;
150
  }
151
 
152
+ if (this.isFuzzyMatch(normalizedText, 'SEED')) {
153
  // Reply immediately so the webhook doesn't time out
154
  console.log(`[SEED] Triggered by user ${user.id}`);
155
  try {
 
340
 
341
  if (activeEnrollment) {
342
  const intent = this.detectIntent(text);
343
+ const isSuite = this.isFuzzyMatch(normalizedText, 'SUITE') || normalizedText === '2';
344
+ const isApprofondir = this.isFuzzyMatch(normalizedText, 'APPROFONDIR') || normalizedText === '1';
345
 
346
  // Handle SUITE Priority
347
  if (isSuite) {
 
497
  where: { trackId: activeEnrollment.trackId, dayNumber: activeEnrollment.currentDay }
498
  });
499
 
500
+ if (trackDayFallback) {
501
+ // 🚨 Guardrail: Contenu Vide / Gibberish 🚨
502
+ const wordCount = text.trim().split(/\s+/).length;
503
+ if (wordCount < 3 || text.length < 5) {
504
+ console.log(`${traceId} Guardrail: Input too short or potential gibberish: "${text}"`);
505
+ await scheduleMessage(user.id, user.language === 'WOLOF'
506
+ ? "Ma déggul li nga wax mbir mi... Mën nga ko gën a firi ci ab kàddu gatt (3-4 kàddu) ?"
507
+ : "Je n'ai pas bien compris ton activité. Peux-tu me réexpliquer en quelques mots ce que tu vends et à qui ?");
508
+ return;
509
+ }
510
+
511
+ // 🚨 Guardrail: Enrollment Priority 🚨
512
+ if (!user.activity || !user.language) {
513
+ console.log(`${traceId} Blocking AI feedback: Enrollment incomplete for User ${user.id}`);
514
+ await scheduleMessage(user.id, user.language === 'WOLOF'
515
+ ? "Baax na, waaye laaj bi des na... Bindal 'INSCRIPTION' ngir tàmbali."
516
+ : "C'est noté, mais il faut d'abord terminer ton inscription. Envoie 'INSCRIPTION' pour commencer.");
517
+ return;
518
+ }
519
+
520
  // Fetch previous responses to provide context to the AI Coach
521
  const previousResponsesData = await prisma.response.findMany({
522
  where: { userId: user.id, enrollmentId: activeEnrollment.id },