Repository navigation
Expand file tree
/
Copy pathplannerPolicy.js
More file actions
182 lines (166 loc) · 13.8 KB
/
Copy pathplannerPolicy.js
File metadata and controls
182 lines (166 loc) · 13.8 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
const router = require('./router');
const PERSON_KINDS = new Set(['order_lookup', 'account_action', 'exception_request', 'money', 'privacy']);
const CANNED_LANES = new Set(['money', 'privacy', 'shop', 'account']);
const ENGLISH_WORDS = new Set(['i', 'my', 'me', 'we', 'our', 'you', 'your', 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'have', 'has', 'do', 'does', 'did', 'can', 'could', 'would', 'what', 'where', 'how', 'why', 'please', 'want', 'need', 'to', 'for', 'with', 'this', 'that', 'from']);
const OTHER_LANGUAGE_WORDS = /\b(?:quiero|necesito|reembolso|pedido|factura|donde|como|quero|preciso|meu|minha|obrigado|ich|mein|meine|bitte|rechnung|kann|sipari[sş]|iade|nas[iı]l|merhaba|mujhe|mera|meri|kya|kaise|chahiye|hai|nahi|merci|gracias)\b/i;
function skipPlannerForCanned(route, question) {
if (!CANNED_LANES.has(route?.lane) || route?.responseMode === 'grounded') return false;
const text = String(question || '');
if (/[^\x00-\x7F]/.test(text) || OTHER_LANGUAGE_WORDS.test(text)) return false;
const words = text.toLowerCase().match(/[a-z]+/g) || [];
return words.filter((word) => ENGLISH_WORDS.has(word)).length >= 2;
}
function isPlainAcknowledgment(question) {
const text = String(question || '').trim();
const words = text.match(/[\p{L}\p{N}]+/gu) || [];
if (!words.length || words.length > 8 || /[??\n\r]/.test(text)) return false;
// A missing question mark is common in support requests. A model's
// "acknowledgment" label cannot override these question/problem cues.
if (/^(?:has|is|did|does|can|when|why|how|what)\b/i.test(text) ||
/\b(?:when|why|how|what|not|still|again|now|empty|error|broken|issue|another|but|however|except|yet|missing|failed|failing|stuck|stopped|vanished|lost|cannot|can't|won't|want|need|please|refund|cancel|delete|replace|help)\b/i.test(text)) return false;
return /\b(?:thanks|thank you|gracias|merci|obrigad[oa]|danke|grazie|fixed|resolved|works|working|ok|okay|yes|yeah|great|perfect|understood|got it|all good)\b/i.test(text) ||
/(?:ありがとう|ありがとうございます|谢谢|謝謝|감사합니다|고마워요)/u.test(text);
}
function suppressOffTopic(understanding, route) {
return understanding?.messageKind === 'off_topic' &&
understanding?.wantsPerson !== true &&
!route?.wantHuman && !route?.escalate &&
!CANNED_LANES.has(route?.lane) && !router.isTechLane(route);
}
function personKind(understanding) {
const kind = String(understanding?.supportKind || '').trim();
return PERSON_KINDS.has(kind) ? kind : '';
}
function routeWithUnderstanding(route, understanding, originalQuestion = '') {
const base = route || { area: 'unknown', lane: 'unknown' };
const current = understanding?.wantsPerson === true
? { ...base, wantHuman: true, escalate: true }
: base;
const kind = String(understanding?.supportKind || '').trim();
const translated = router.classify(understanding?.standaloneQuestion || '');
// A checkout quote needs a human decision, but it is not a tracking lookup.
if (current.intent === 'shipping_quote') return current;
if (kind === 'order_lookup') {
return { ...current, area: 'shop', lane: 'shop', escalate: true };
}
if (kind === 'account_action') {
const privacy = current.lane === 'privacy' || translated.lane === 'privacy';
return { ...current, area: privacy ? 'privacy' : 'shop', lane: privacy ? 'privacy' : 'account', escalate: true };
}
if (kind === 'exception_request') {
const money = current.lane === 'money' || translated.lane === 'money';
return { ...current, area: 'shop', lane: money ? 'money' : 'shop', escalate: true };
}
if (kind === 'money') return { ...current, area: 'shop', lane: 'money', escalate: true };
if (kind === 'privacy') return { ...current, area: 'privacy', lane: 'privacy', escalate: true };
const reportedFault = /\b(?:empty|missing|vanished|lost|stopped|failed|failing|broken|error|crash(?:ed|ing)?|doesn['’]?t|won['’]?t|cannot|can['’]?t|not working|not syncing|no answer)\b/i.test(String(originalQuestion || ''));
const weakRoute = current.lane === 'unknown' ||
(current.lane === 'faq' && current.area === 'unknown' &&
(router.isTechLane(translated) || understanding?.messageKind === 'new_symptom' || reportedFault) &&
!router.looksLikeProductQuestion(originalQuestion));
if (kind === 'technical_problem' && weakRoute) {
const area = ['app', 'desktop', 'firmware'].includes(translated.area) ? translated.area : 'unknown';
return { ...current, area, lane: area === 'firmware' ? 'firmware' : 'tech', escalate: true };
}
return current;
}
function suppressAcknowledgment(_understanding, question) {
return isPlainAcknowledgment(question);
}
const LOCALIZED_HANDOFF = {
es: { pending: 'Una persona del equipo debe revisar esto.', thread: 'Una persona del equipo responderá en este hilo.', sent: 'Una persona del equipo ya tiene este caso.', failed: 'No pude entregar el caso al equipo. Escribe a help@omi.me en privado.', duplicate: 'El equipo ya tiene este caso.', issue: 'El problema está registrado en este hilo.' },
pt: { pending: 'Uma pessoa da equipe precisa analisar isso.', thread: 'Uma pessoa da equipe responderá nesta conversa.', sent: 'A equipe já recebeu este caso.', failed: 'Não consegui enviar o caso à equipe. Escreva para help@omi.me em privado.', duplicate: 'A equipe já tem este caso.', issue: 'O problema está registrado nesta conversa.' },
de: { pending: 'Jemand aus dem Team muss das prüfen.', thread: 'Jemand aus dem Team antwortet in diesem Thread.', sent: 'Das Team hat diesen Fall erhalten.', failed: 'Ich konnte den Fall nicht an das Team weiterleiten. Bitte schreibe privat an help@omi.me.', duplicate: 'Das Team hat diesen Fall bereits.', issue: 'Das Problem ist in diesem Thread festgehalten.' },
tr: { pending: 'Ekipten birinin bunu incelemesi gerekiyor.', thread: 'Ekipten biri bu başlıkta yanıt verecek.', sent: 'Ekip bu talebi aldı.', failed: 'Talebi ekibe iletemedim. Lütfen help@omi.me adresine özel olarak yazın.', duplicate: 'Ekip bu talebi zaten aldı.', issue: 'Sorun bu başlıkta kayıtlı.' },
fr: { pending: "Une personne de l'équipe doit examiner cela.", thread: "Une personne de l'équipe répondra dans ce fil.", sent: "L'équipe a reçu ce dossier.", failed: "Je n'ai pas pu transmettre le dossier à l'équipe. Écrivez à help@omi.me en privé.", duplicate: "L'équipe a déjà ce dossier.", issue: 'Le problème est consigné dans ce fil.' },
id: { pending: 'Seseorang dari tim perlu meninjau ini.', thread: 'Seseorang dari tim akan membalas di utas ini.', sent: 'Tim sudah menerima kasus ini.', failed: 'Saya tidak dapat mengirim kasus ini ke tim. Silakan email help@omi.me secara pribadi.', duplicate: 'Tim sudah menerima kasus ini.', issue: 'Masalah ini tercatat di utas ini.' },
'hi-Latn': { pending: 'Team ke kisi vyakti ko iski jaanch karni hogi.', thread: 'Team ka koi vyakti isi thread mein jawab dega.', sent: 'Team ko yeh mamla mil gaya hai.', failed: 'Main yeh mamla team tak nahi pahuncha saka. Kripya help@omi.me par private email karein.', duplicate: 'Team ke paas yeh mamla pehle se hai.', issue: 'Yeh samasya isi thread mein darj hai.' },
'hi-Deva': { pending: 'टीम के किसी व्यक्ति को इसकी जाँच करनी होगी।', thread: 'टीम का कोई व्यक्ति इसी थ्रेड में जवाब देगा।', sent: 'टीम को यह मामला मिल गया है।', failed: 'मैं यह मामला टीम तक नहीं पहुँचा सका। कृपया help@omi.me पर निजी ईमेल करें।', duplicate: 'टीम के पास यह मामला पहले से है।', issue: 'यह समस्या इसी थ्रेड में दर्ज है।' },
ja: { pending: '担当者による確認が必要です。', thread: '担当者がこのスレッドで返信します。', sent: '担当者にこの件を共有しました。', failed: '担当者に届けられませんでした。詳細を help@omi.me に非公開でメールしてください。', duplicate: '担当者にはすでに共有されています。', issue: '問題はこのスレッドに記録されています。' },
zh: { pending: '需要支持团队的工作人员查看这个请求。', thread: '已将您的请求发送给支持团队。请在这个讨论串等待团队回复。', sent: '您的请求已发送给支持团队。', failed: '未能将您的请求发送给支持团队。请通过私密邮件联系 help@omi.me。', duplicate: '支持团队已经收到这个请求。', issue: '问题已记录在这个讨论串中。' },
it: { pending: 'Una persona del team deve esaminare questa richiesta.', thread: 'Ho inviato la richiesta al team. Attendi la risposta del team in questa conversazione.', sent: 'La richiesta è stata inviata al team.', failed: 'Non ho potuto inviare la richiesta al team. Scrivi a help@omi.me in privato.', duplicate: 'Il team ha già ricevuto questa richiesta.', issue: 'Il problema è registrato in questa conversazione.' },
};
function handoffLocale(understanding, question) {
const language = String(understanding?.replyLanguage || 'en').toLowerCase().split('-')[0];
if (language === 'hi') return LOCALIZED_HANDOFF[/\p{Script=Devanagari}/u.test(String(question || '')) ? 'hi-Deva' : 'hi-Latn'];
return LOCALIZED_HANDOFF[language] || null;
}
function handoffFooters(understanding, question) {
const language = String(understanding?.replyLanguage || 'en').toLowerCase().split('-')[0];
if (language === 'en') return null;
const locale = handoffLocale(understanding, question);
if (!locale) return null;
return {
thread: locale.thread || '', sent: locale.sent || '', failed: locale.failed || '',
duplicate: locale.duplicate || '', pending: locale.pending || '', issue: locale.issue || '',
};
}
function safeHandoffAcknowledgment(understanding, question) {
const language = String(understanding?.replyLanguage || '').toLowerCase();
const acknowledgment = String(understanding?.handoffAcknowledgment || '').trim();
if (!language || language === 'en' || !acknowledgment || acknowledgment.length > 220) return '';
if (/[\d@#€$£₹<>\n\r]|https?:\/\//i.test(acknowledgment)) return '';
if (!/\p{Script=Devanagari}/u.test(String(question || '')) &&
/\p{Script=Devanagari}/u.test(acknowledgment)) return '';
return acknowledgment;
}
function personReply(kind, route, question, understanding) {
if (route?.intent === 'shipping_quote') return router.cannedReply(route, question);
const acknowledgment = safeHandoffAcknowledgment(understanding, question);
if (String(understanding?.replyLanguage || 'en').toLowerCase().split('-')[0] !== 'en') {
const localized = acknowledgment || handoffLocale(understanding, question)?.pending;
if (localized) return localized;
}
let reply;
if (kind === 'order_lookup') {
reply = router.cannedReply({ ...route, area: 'shop', lane: 'shop' }, question);
} else if (kind === 'account_action') {
reply = route?.lane === 'privacy'
? router.cannedReply(route, question)
: router.cannedReply({ ...route, lane: 'account' }, question);
} else if (kind === 'privacy') {
reply = router.cannedReply({ ...route, lane: 'privacy' }, question);
} else if (kind === 'money') {
reply = router.cannedReply({ ...route, lane: 'money' }, question);
} else if (route?.wantHuman) {
reply = 'A person needs to review your request.';
} else if (route?.lane === 'money') {
reply = router.cannedReply(route, question);
} else if (/\b(?:replacement|warranty)\b/i.test(String(question || ''))) {
reply = 'You need help with a replacement or warranty request. Coverage or a replacement needs a team review before it can be confirmed.';
} else {
reply = 'A person needs to review this request.';
}
return reply;
}
function isGroundedHowTo(route, question, answer) {
if (route?.lane !== 'faq' || route?.wantHuman || !router.looksLikeProductQuestion(question)) return false;
const text = String(answer || '');
if (!/https:\/\/(?:help|docs)\.omi\.me\//i.test(text)) return false;
const uncertainty = /\b(?:couldn['’]?t find|can['’]?t find|couldn['’]?t verify|could not verify|can['’]?t verify|cannot verify|not sure|can['’]?t confirm|unable to verify|don['’]?t know)\b/i.exec(text);
const actionableStep = /\b(?:open|go to|tap|select|choose|press|connect|update|visit)\b/i.exec(text);
if (uncertainty && (!actionableStep || uncertainty.index < actionableStep.index)) return false;
return true;
}
function verifiedCannedReply(route, question, evidence) {
if (route?.lane !== 'faq' || route?.wantHuman || route?.escalate ||
!router.looksLikeProductQuestion(question)) return '';
const reply = String(router.cannedReply(route, question) || '');
const cited = /\bSource:\s*(https:\/\/(?:help|docs)\.omi\.me\/[^\s)]+)/i.exec(reply);
if (!cited) return '';
const normalize = (url) => String(url || '').replace(/\.md\/?$/i, '').replace(/\/$/, '');
const source = normalize(cited[1]);
const block = String(evidence || '').split(/(?=\[S\d+\s*\|)/).find((part) =>
[...part.matchAll(/https:\/\/(?:help|docs)\.omi\.me\/[^\s)]+/gi)]
.some((match) => normalize(match[0]) === source));
if (!block) return '';
const numbers = (reply.slice(0, cited.index).match(/\b\d+\b/g) || []);
if (numbers.some((number) => !new RegExp(`\\b${number}\\b`).test(block))) return '';
return reply;
}
function personReason(kind) {
if (kind === 'order_lookup') return 'Order lookup requires a verified staff check';
if (kind === 'account_action') return 'Account or data action requires staff access';
return 'Exception request requires staff review';
}
module.exports = { personKind, routeWithUnderstanding, skipPlannerForCanned, suppressAcknowledgment, suppressOffTopic, personReply, personReason, handoffFooters, isGroundedHowTo, verifiedCannedReply };