PageSourceSearch

https://sante.gov.ml/templates/santemali/msds-intent-router-v2-shadow.browser.js?v=20260923-V2A6

js sante.gov.ml collected 2026-09-24 18:53:06 UTC 32,688 bytes, 765 lines download raw bytes

1'use strict';
2
3const SCHEMA_VERSION = '2.0.0';
4const ROUTER_NAME = 'MSDS_INTENT_ROUTER_V2_SHADOW';
5
6const INTENTS = Object.freeze([
7    'HEALTH_INFORMATION',
8    'CLINICAL_PERSONAL',
9    'CLINICAL_PROTOCOL_GUIDANCE',
10    'EPIDEMIOLOGICAL_DATA',
11    'DOCUMENT_SEARCH',
12    'DOCUMENT_VERIFICATION',
13    'ADMINISTRATIVE_PROCEDURE',
14    'MEDICINE_INFORMATION',
15    'AMO_MEDICINE',
16    'FACILITY_SEARCH',
17    'MSDS_SERVICE',
18    'INSTITUTION_INFORMATION',
19    'PORTAL_NAVIGATION',
20    'NEWS_PUBLICATION',
21    'CONVERSATIONAL',
22    'OUT_OF_SCOPE',
23    'AMBIGUOUS'
24]);
25
26const SOURCES = Object.freeze([
27    'SISE',
28    'JOOMLA_DOCUMENTS',
29    'JOOMLA_CONTENT',
30    'AMO_CATALOG',
31    'CLINICAL_ENGINE',
32    'MSDS_KNOWLEDGE',
33    'FACILITY_DIRECTORY',
34    'SITE_NAVIGATION',
35    'NEWS_CONTENT',
36    'DOCTRUST',
37    'HEALTH_INFORMATION_ENGINE',
38    'CLARIFICATION_HANDLER',
39    'CONVERSATION_HANDLER',
40    'OUT_OF_SCOPE_HANDLER',
41    'NONE'
42]);
43
44const CONFIDENCE_LEVELS = Object.freeze(['HIGH', 'MEDIUM', 'LOW']);
45
46const PATTERNS = Object.freeze({
47    diseases: Object.freeze([
48        ['dengue', /\bdengue\b/],
49        ['paludisme', /\b(?:paludisme|palu|malaria)\b/],
50        ['cholera', /\bcholera\b/],
51        ['tuberculose', /\b(?:tuberculose|tb)\b/],
52        ['vih', /\b(?:vih|sida)\b/],
53        ['rougeole', /\brougeole\b/],
54        ['meningite', /\bmeningite\b/],
55        ['ebola', /\bebola\b/],
56        ['covid-19', /\b(?:covid|covid 19|coronavirus)\b/],
57        ['lepre', /\b(?:lepre|maladie de hansen)\b/],
58        ['cancer', /\b(?:cancer|tumeur|oncologie)\b/]
59    ]),
60    medicines: Object.freeze([
61        ['paracetamol', /\b(?:paracetamol|doliprane|efferalgan)\b/],
62        ['amoxicilline', /\bamoxicilline\b/],
63        ['ibuprofene', /\bibuprofene\b/]
64    ]),
65    symptoms: Object.freeze([
66        ['FEVER', /\b(?:fievre|corps chaud)\b/],
67        ['COUGH', /\b(?:toux|tousse|tousser)\b/],
68        ['DIARRHEA', /\bdiarrhee\b/],
69        ['VOMITING', /\b(?:vomissement|vomissements|vomis|vomir)\b/],
70        ['BREATHING_DIFFICULTY', /\b(?:difficulte a respirer|respire mal|ne respire plus|etouffe)\b/],
71        ['URINARY_PROBLEM_UNCLEAR', /\b(?:urine de lui meme|urine sur lui|fait pipi sur lui|probleme urinaire)\b/]
72    ]),
73    facilityTypes: Object.freeze([
74        ['CSCOM', /\bcscom\b/],
75        ['CSREF', /\bcsref\b/],
76        ['CHU', /\bchu\b/],
77        ['HOSPITAL', /\b(?:hopital|hopitaux)\b/],
78        ['PHARMACY', /\bpharmacie\b/],
79        ['CLINIC', /\bclinique\b/],
80        ['LABORATORY', /\blaboratoire\b/]
81    ]),
82    services: Object.freeze([
83        ['DPM', /\b(?:dpm|direction de la pharmacie(?: et du medicament)?)\b/],
84        ['CANAM', /\b(?:canam|caisse nationale d assurance maladie)\b/],
85        ['ANAM', /\b(?:anam|agence nationale d assistance medicale)\b/],
86        ['ANTIM', /\b(?:antim|agence nationale de telesante)\b/],
87        ['DGSHP', /\b(?:dgshp|direction generale de la sante)\b/]
88    ]),
89    months: Object.freeze([
90        [1, /\bjanvier\b/], [2, /\bfevrier\b/], [3, /\bmars\b/],
91        [4, /\bavril\b/], [5, /\bmai\b/], [6, /\bjuin\b/],
92        [7, /\bjuillet\b/], [8, /\baout\b/], [9, /\bseptembre\b/],
93        [10, /\boctobre\b/], [11, /\bnovembre\b/], [12, /\bdecembre\b/]
94    ])
95});
96
97function normalizeInput(questionOriginale, questionNormalisee) {
98    const source = typeof questionNormalisee === 'string' && questionNormalisee.trim()
99        ? questionNormalisee
100        : (typeof questionOriginale === 'string' ? questionOriginale : '');
101
102    return source
103        .toLowerCase()
104        .normalize('NFD')
105        .replace(/[\u0300-\u036f]/g, '')
106        .replace(/[’']/g, ' ')
107        .replace(/[-–—]/g, ' ')
108        .replace(/[^a-z0-9\s]/g, ' ')
109        .replace(/\s+/g, ' ')
110        .trim();
111}
112
113function firstMatch(text, entries) {
114    const found = entries.find(([, pattern]) => pattern.test(text));
115    return found ? found[0] : null;
116}
117
118function createEmptyFacets() {
119    return {
120        disease: null,
121        symptom: null,
122        medicine: null,
123        healthTopic: null,
124        documentType: null,
125        artifact: null,
126        procedure: null,
127        service: null,
128        institution: null,
129        coverageScheme: null,
130        territory: null,
131        originTerritory: null,
132        temporal: {
133            date: null,
134            year: null,
135            month: null,
136            week: null,
137            period: null,
138            recency: 'UNSPECIFIED'
139        },
140        facilityType: null,
141        personRole: null,
142        audience: 'UNSPECIFIED',
143        questionType: 'UNSPECIFIED',
144        requestedAttributes: []
145    };
146}
147
148function extractFacets(text) {
149    const facets = createEmptyFacets();
150
151    facets.disease = firstMatch(text, PATTERNS.diseases);
152    facets.medicine = firstMatch(text, PATTERNS.medicines);
153    facets.symptom = firstMatch(text, PATTERNS.symptoms);
154    facets.facilityType = firstMatch(text, PATTERNS.facilityTypes);
155    facets.service = firstMatch(text, PATTERNS.services);
156
157    if (/\b(?:chaleur extreme|forte chaleur|canicule|coup de chaleur|maladies? liees? a la chaleur)\b/.test(text)) {
158        facets.healthTopic = 'HEAT_ILLNESS';
159    }
160
161    if (/\bamo\b|assurance maladie obligatoire/.test(text)) {
162        facets.coverageScheme = 'AMO';
163    } else if (/\bramed\b/.test(text)) {
164        facets.coverageScheme = 'RAMED';
165    } else if (/\bramu\b/.test(text)) {
166        facets.coverageScheme = 'RAMU';
167    }
168
169    if (/\bloi\b/.test(text)) {
170        facets.documentType = 'LAW';
171        facets.artifact = 'LEGAL_TEXT';
172    } else if (/\bdecret\b/.test(text)) {
173        facets.documentType = 'DECREE';
174        facets.artifact = 'LEGAL_TEXT';
175    } else if (/\barrete\b/.test(text)) {
176        facets.documentType = 'ORDER';
177        facets.artifact = 'LEGAL_TEXT';
178    } else if (/\b(?:rapport|rapports)\b/.test(text)) {
179        facets.documentType = 'REPORT';
180    } else if (/\b(?:protocole|protocoles)\b/.test(text)) {
181        facets.documentType = 'PROTOCOL';
182    } else if (/\b(?:guide|manuel)\b/.test(text)) {
183        facets.documentType = 'GUIDE';
184    } else if (/\bcertificats? medicaux?\b/.test(text)) {
185        facets.documentType = 'CERTIFICATE';
186        facets.artifact = 'MEDICAL_CERTIFICATE';
187    } else if (/\b(?:liste|nomenclature|catalogue)\b/.test(text)) {
188        facets.documentType = 'LIST';
189    } else if (/\b(?:publication|publications)\b/.test(text)) {
190        facets.documentType = 'PUBLICATION';
191    } else if (/\b(?:formulaire|formulaires)\b/.test(text)) {
192        facets.documentType = 'FORM';
193    } else if (/\b(?:document|documents|documentation)\b/.test(text)) {
194        facets.documentType = 'OTHER';
195    }
196
197    if (facets.coverageScheme === 'AMO' && facets.documentType === 'LIST') {
198        facets.artifact = 'AMO_CATALOG';
199    }
200
201    if (/\b(?:bulletin|bulletins)\b.*\bepidemiolog/.test(text)) {
202        facets.artifact = 'EPIDEMIOLOGICAL_BULLETIN';
203    }
204
205    if (/\b(?:tableau de bord|dashboard)\b.*\bepidemiolog/.test(text)) {
206        facets.artifact = 'EPIDEMIOLOGICAL_DASHBOARD';
207    }
208
209    if (facets.documentType === 'REPORT' && facets.disease) {
210        facets.artifact = 'WEEKLY_EPIDEMIOLOGICAL_REPORT';
211    }
212
213    if (/\bimport(?:er|ation)?\b.*\b(?:produits? pharmaceutiques?|medicaments?)\b/.test(text)) {
214        facets.procedure = 'PHARMACEUTICAL_IMPORT';
215    } else if (/\b(?:obtenir|demander|demande)\b.*\bautorisation\b|\bautorisation\b/.test(text)) {
216        facets.procedure = 'AUTHORIZATION_REQUEST';
217    } else if (/\bcertificats? medicaux?\b/.test(text)) {
218        facets.procedure = 'CERTIFICATE_REQUEST';
219    } else if (/\b(?:telecharger|download)\b/.test(text)) {
220        facets.procedure = 'DOCUMENT_DOWNLOAD';
221    }
222
223    if (/\b(?:ministere de la sante|msds)\b/.test(text)) {
224        facets.institution = 'MSDS';
225    } else if (/\bministere\b/.test(text)) {
226        facets.institution = 'MINISTRY_UNSPECIFIED';
227    }
228
229    if (/\bfrance\b/.test(text)) {
230        facets.originTerritory = { name: 'France', role: 'ORIGIN' };
231    }
232
233    if (/\bmali\b/.test(text)) {
234        facets.territory = {
235            name: 'Mali',
236            role: facets.procedure === 'PHARMACEUTICAL_IMPORT' ? 'DESTINATION' : 'SUBJECT'
237        };
238    } else if (/\bbamako\b/.test(text)) {
239        facets.territory = { name: 'Bamako', role: 'SUBJECT' };
240    }
241
242    const yearMatch = text.match(/\b(19\d{2}|20\d{2})\b/);
243    if (yearMatch) {
244        facets.temporal.year = Number(yearMatch[1]);
245        facets.temporal.recency = 'HISTORICAL';
246    }
247
248    const monthMatch = PATTERNS.months.find(([, pattern]) => pattern.test(text));
249    if (monthMatch) {
250        facets.temporal.month = monthMatch[0];
251    }
252
253    const weekMatch = text.match(/\b(?:semaine|s)\s*(\d{1,2})\b/);
254    if (weekMatch) {
255        facets.temporal.week = Number(weekMatch[1]);
256    }
257
258    const dateMatch = text.match(/\b(\d{1,2})\s+(janvier|fevrier|mars|avril|mai|juin|juillet|aout|septembre|octobre|novembre|decembre)\s+(19\d{2}|20\d{2})\b/);
259    if (dateMatch) {
260        const monthNumber = PATTERNS.months.find(([, pattern]) => pattern.test(dateMatch[2]))?.[0];
261        facets.temporal.date = [dateMatch[3], String(monthNumber).padStart(2, '0'), String(dateMatch[1]).padStart(2, '0')].join('-');
262    }
263
264    if (/\bcette semaine\b|\bsemaine actuelle\b/.test(text)) {
265        facets.temporal.period = 'CURRENT_WEEK';
266        facets.temporal.recency = 'CURRENT';
267    } else if (/\bsemaine precedente\b|\bsemaine passee\b/.test(text)) {
268        facets.temporal.period = 'PREVIOUS_WEEK';
269    } else if (/\bderniere semaine\b/.test(text) && facets.temporal.month !== null) {
270        facets.temporal.period = 'LAST_WEEK_OF_MONTH';
271    } else if (/\bderniere semaine\b/.test(text) && facets.temporal.year !== null) {
272        facets.temporal.period = 'LAST_WEEK_OF_YEAR';
273    } else if (facets.temporal.year !== null) {
274        facets.temporal.period = 'YEAR';
275    }
276
277    if (/\b(?:dernier|derniere|derniers|dernieres|plus recent|recente|latest)\b/.test(text)
278        && !/\bderniere semaine\b/.test(text)) {
279        facets.temporal.recency = 'LATEST';
280    }
281
282    if (/\b(?:j ai|je souffre|je ressens|chez moi|pour moi)\b/.test(text)) {
283        facets.personRole = 'SELF';
284    } else if (/\b(?:mon enfant|mon fils|ma fille)\b/.test(text)) {
285        facets.personRole = 'CHILD';
286    } else if (/\b(?:mon bebe|nourrisson)\b/.test(text)) {
287        facets.personRole = 'INFANT';
288    } else if (/\bquelqu un\b|\bune personne\b/.test(text)) {
289        facets.personRole = 'THIRD_PARTY';
290    }
291
292    if (/\b(?:professionnels? de sante|medecins?|infirmiers?|pratiques? medicales?)\b/.test(text)) {
293        facets.audience = 'HEALTH_PROFESSIONAL';
294    } else if (facets.procedure) {
295        facets.audience = 'ADMINISTRATIVE_USER';
296    } else {
297        facets.audience = 'GENERAL_PUBLIC';
298    }
299
300    if (/\b(?:combien de cas|nombre de cas)\b/.test(text)) facets.questionType = 'COUNT';
301    else if (/\b(?:triage|trier)\b/.test(text)) facets.questionType = 'TRIAGE';
302    else if (/\b(?:comment se soigne|comment soigner|traiter|traitement)\b/.test(text)) facets.questionType = 'TREATMENT';
303    else if (/\b(?:disponible|disponibles|disponibilite)\b/.test(text)) facets.questionType = 'AVAILABILITY';
304    else if (/\b(?:ou est|ou trouver|plus proche|localiser|adresse)\b/.test(text)) facets.questionType = 'LOCATION';
305    else if (/\b(?:telephone|tel|contact)\b/.test(text)) facets.questionType = 'CONTACT';
306    else if (/\b(?:qui est|nom du|nom de la)\b/.test(text)) facets.questionType = 'IDENTITY';
307    else if (/\b(?:c est quoi|qu est ce que|definition|symptomes? de)\b/.test(text)) facets.questionType = 'DEFINITION';
308    else if (/\b(?:telecharger|download)\b/.test(text)) facets.questionType = 'DOWNLOAD';
309    else if (/\b(?:verifier|authenticite)\b/.test(text)) facets.questionType = 'VERIFICATION';
310    else if (facets.procedure) facets.questionType = 'PROCEDURE';
311    else if (/\b(?:rapport|situation epidemiologique|donnees epidemiologiques)\b/.test(text)) facets.questionType = 'DATA_REQUEST';
312
313    if (/\bprix\b/.test(text)) facets.requestedAttributes.push('PRICE');
314    if (/\b(?:dci|denomination commune)\b/.test(text)) facets.requestedAttributes.push('DCI');
315    if (/\b(?:dose|dosage|posologie)\b/.test(text)) facets.requestedAttributes.push('DOSAGE');
316    if (facets.questionType === 'CONTACT') facets.requestedAttributes.push('CONTACT');
317    if (facets.questionType === 'AVAILABILITY') facets.requestedAttributes.push('AVAILABILITY');
318
319    return facets;
320}
321
322function addCandidate(candidates, intent, subintent, strength, reasons, options = {}) {
323    candidates.push({ intent, subintent, strength, reasons, ...options });
324}
325
326function detectIntentCandidates(text, facets) {
327    const candidates = [];
328    const hasDisease = Boolean(facets.disease);
329    const hasTemporal = facets.temporal.period !== null || facets.temporal.year !== null;
330    const report = facets.documentType === 'REPORT';
331    const count = facets.questionType === 'COUNT';
332    const epidemiologyTerm = /\b(?:epidemiolog(?:ie|ique|iques)?|surveillance sanitaire|sise|nombre de cas|combien de cas)\b/.test(text);
333    const personal = facets.personRole !== null;
334    const personalClinical = personal && (facets.symptom || /\b(?:malade|douleur|se soigne|soigner)\b/.test(text));
335    const definition = facets.questionType === 'DEFINITION';
336    const amoList = facets.coverageScheme === 'AMO' && facets.documentType === 'LIST';
337
338    if (/^(?:bonjour|bonsoir|salut|salam|hello)$/.test(text)) {
339        addCandidate(candidates, 'CONVERSATIONAL', 'GREETING', 6, [['GREETING', 'salutation détectée']]
339);
340    } else if (/^(?:merci|merci beaucoup)$/.test(text)) {
341        addCandidate(candidates, 'CONVERSATIONAL', 'THANKS', 6, [['THANKS', 'remerciement détecté']]);
342    } else if (/^(?:oui|non|yes|no|d accord|ok)$/.test(text)) {
343        addCandidate(candidates, 'CONVERSATIONAL', 'CONFIRMATION', 5, [['CONFIRMATION', 'réponse conversationnelle courte détectée']]);
344    }
345
346    if (/\b(?:resultat|resultats)\b.*\b(?:bac|baccalaureat|def|examen scolaire)\b|\b(?:bac|baccalaureat|def)\b.*\b(?:resultat|resultats)\b/.test(text)) {
347        addCandidate(candidates, 'OUT_OF_SCOPE', 'EDUCATION_RESULTS', 6, [['OUT_OF_SCOPE_EDUCATION', 'demande de résultats scolaires détectée']]);
348    }
349
350    if (/\b(?:doctrust|verifier l authenticite|authenticite d un document|verifier un document)\b/.test(text)) {
351        addCandidate(candidates, 'DOCUMENT_VERIFICATION', 'AUTHENTICITY_CHECK', 6, [['DOCUMENT_VERIFICATION', 'vérification documentaire détectée']]);
352    }
353
354    if (amoList) {
355        addCandidate(candidates, 'DOCUMENT_SEARCH', 'CATALOG', 6, [
356            ['AMO_EXPLICIT', 'AMO détectée'],
357            ['DOCUMENT_LIST', 'liste ou catalogue détecté']
358        ]);
359    }
360
361    if (hasDisease && ((count && hasTemporal) || (report && hasTemporal) || epidemiologyTerm)) {
362        const subintent = count
363            ? 'DISEASE_COUNT'
364            : (report ? 'WEEKLY_REPORT' : 'WEEKLY_SITUATION');
365        addCandidate(candidates, 'EPIDEMIOLOGICAL_DATA', subintent, 6, [
366            ['DISEASE_EXPLICIT', `maladie ${facets.disease} détectée`],
367            [report ? 'EPIDEMIOLOGICAL_REPORT' : 'EPIDEMIOLOGICAL_QUERY', report ? 'rapport épidémiologique probable détecté' : 'demande épidémiologique détectée'],
368            ['TEMPORAL_SIGNAL', 'période ou année détectée']
369        ]);
370    } else if (epidemiologyTerm && (hasTemporal || /\b(?:situation|donnees|tableau de bord|rapport)\b/.test(text))) {
371        addCandidate(candidates, 'EPIDEMIOLOGICAL_DATA', report ? 'WEEKLY_REPORT' : 'WEEKLY_SITUATION', 5, [
372            ['EPIDEMIOLOGICAL_QUERY', 'demande épidémiologique explicite détectée'],
373            ['TEMPORAL_SIGNAL', 'période ou année détectée']
374        ]);
375    }
376
377    if (facets.procedure === 'PHARMACEUTICAL_IMPORT') {
378        addCandidate(candidates, 'ADMINISTRATIVE_PROCEDURE', 'PHARMACEUTICAL_IMPORT', 6, [
379            ['IMPORT_ACTION', 'importation détectée'],
380            ['PHARMACEUTICAL_OBJECT', 'produits pharmaceutiques détectés']
381        ]);
382    }
383
384    if (facets.documentType === 'CERTIFICATE' && facets.questionType === 'AVAILABILITY') {
385        addCandidate(candidates, 'ADMINISTRATIVE_PROCEDURE', 'CERTIFICATE_AVAILABILITY', 3, [
386            ['MEDICAL_CERTIFICATE', 'certificat médical détecté'],
387            ['AVAILABILITY_QUESTION', 'question de disponibilité détectée']
388        ], { clarificationRequired: true });
389    }
390
391    if (facets.procedure === 'AUTHORIZATION_REQUEST') {
392        addCandidate(candidates, 'ADMINISTRATIVE_PROCEDURE', 'APPLICATION_PROCESS', 3, [
393            ['AUTHORIZATION_REQUEST', 'demande d’autorisation détectée']
394        ], { clarificationRequired: true });
395    }
396
397    if (/\b(?:triage|protocole|protocoles|pratiques medicales)\b/.test(text)
398        && (facets.healthTopic || hasDisease || /\b(?:maladie|sante|medical)\b/.test(text))) {
399        addCandidate(candidates, 'CLINICAL_PROTOCOL_GUIDANCE', /\btriage\b/.test(text) ? 'TRIAGE_GUIDANCE' : 'PROTOCOL_SEARCH', 5, [
400            ['PROTOCOL_OR_TRIAGE', 'protocole ou triage détecté'],
401            ['HEALTH_TOPIC', 'sujet de santé détecté']
402        ]);
403    }
404
405    if (personalClinical) {
406        const unclearUrinary = facets.symptom === 'URINARY_PROBLEM_UNCLEAR';
407        addCandidate(candidates, 'CLINICAL_PERSONAL', unclearUrinary ? 'UNCLEAR_SYMPTOM' : 'SYMPTOM_REPORT', unclearUrinary ? 1 : 5, [
408            ['PERSONAL_SITUATION', 'situation concernant une personne détectée'],
409            [unclearUrinary ? 'UNCLEAR_URINARY_WORDING' : 'SYMPTOM_EXPLICIT', unclearUrinary ? 'formulation urinaire ambiguë détectée' : 'symptôme explicite détecté']
410        ], { clarificationRequired: unclearUrinary });
411    }
412
413    if (facets.medicine && facets.coverageScheme === 'AMO' && !amoList) {
414        addCandidate(candidates, 'AMO_MEDICINE', facets.requestedAttributes.includes('PRICE') ? 'PRICE_LOOKUP' : 'COVERAGE_LOOKUP', 6, [
415            ['MEDICINE_EXPLICIT', `médicament ${facets.medicine} détecté`],
416            ['AMO_EXPLICIT', 'AMO détectée']
417        ]);
418    }
419
420    if (facets.facilityType && facets.questionType === 'LOCATION') {
421        addCandidate(candidates, 'FACILITY_SEARCH', /\bplus proche\b/.test(text) ? 'NEAREST' : 'BY_LOCATION', 6, [
422            ['FACILITY_TYPE', `type de structure ${facets.facilityType} détecté`],
423            ['LOCATION_QUESTION', 'demande de localisation détectée']
424        ]);
425    }
426
427    if (facets.service && ['CONTACT', 'IDENTITY', 'DEFINITION'].includes(facets.questionType)) {
428        addCandidate(candidates, 'MSDS_SERVICE', facets.questionType === 'CONTACT' ? 'SERVICE_CONTACT' : 'SERVICE_ROLE', 5, [
429            ['MSDS_SERVICE_EXPLICIT', `service ${facets.service} détecté`],
430            ['SERVICE_QUESTION', 'question sur un service détectée']
431        ]);
432    }
433
434    if (/\b(?:qui est|nom du|nom de la)\b.*\bministre(?: de la sante)?\b|\bministre de la sante\b/.test(text)) {
435        addCandidate(candidates, 'INSTITUTION_INFORMATION', 'CURRENT_LEADER', 6, [
436            ['MINISTER_ROLE', 'rôle de ministre détecté'],
437            ['IDENTITY_QUESTION', 'question d’identité détectée']
438        ]);
439    }
440
441    if (/\b(?:actualite|actualites|nouvelles|news|communique)\b/.test(text)
442        || (/\b(?:publication|publications)\b/.test(text) && facets.temporal.recency === 'LATEST')) {
443        addCandidate(candidates, 'NEWS_PUBLICATION', 'LATEST_NEWS', 5, [
444            ['NEWS_TERM', 'actualité ou publication récente détectée'],
445            ['RECENCY_LATEST', 'récence explicite détectée']
446        ]);
447    }
448
449    if (/\b(?:comment utiliser|comment acceder|ou se trouve la rubrique|naviguer sur|portail|site web)\b/.test(text)
450        && !facets.facilityType) {
451        addCandidate(candidates, 'PORTAL_NAVIGATION', 'FEATURE_HELP', 4, [
452            ['PORTAL_NAVIGATION', 'demande de navigation sur le portail détectée']
453        ]);
454    }
455
456    if (facets.medicine && facets.coverageScheme !== 'AMO') {
457        addCandidate(candidates, 'MEDICINE_INFORMATION', facets.requestedAttributes.includes('PRICE') ? 'PRICE_INFORMATION' : 'GENERAL_INFORMATION', 4, [
458            ['MEDICINE_EXPLICIT', `médicament ${facets.medicine} détecté`],
459            [facets.requestedAttributes.includes('PRICE') ? 'PRICE_QUESTION' : 'MEDICINE_QUESTION', facets.requestedAttributes.includes('PRICE') ? 'prix demandé' : 'information médicament demandée']
460        ]);
461    }
462
463    if (hasDisease && definition && !personal) {
464        addCandidate(candidates, 'HEALTH_INFORMATION', /\bsymptomes? de\b/.test(text) ? 'SYMPTOMS_OVERVIEW' : 'DISEASE_OVERVIEW', 5, [
465            ['DISEASE_EXPLICIT', `maladie ${facets.disease} détectée`],
466            ['GENERAL_INFORMATION_QUESTION', 'question générale de santé détectée']
467        ]);
468    }
469
470    const legalDocument = ['LAW', 'DECREE', 'ORDER'].includes(facets.documentType);
471    const downloadDocument = facets.questionType === 'DOWNLOAD' && /\b(?:prodess|document|rapport|guide|plan)\b/.test(text);
472    const genericDocument = report || legalDocument || downloadDocument
473        || (facets.documentType && !amoList && facets.documentType !== 'CERTIFICATE');
474
475    if (genericDocument && !(report && hasDisease && (hasTemporal || epidemiologyTerm))
476        && !(/\b(?:publication|publications)\b/.test(text) && facets.temporal.recency === 'LATEST')) {
477        const subintent = legalDocument
478            ? 'LEGAL_TEXT'
479            : (facets.temporal.recency === 'LATEST' ? 'LATEST_OFFICIAL_DOCUMENT' : (report ? 'REPORT' : 'DOCUMENT_LOOKUP'));
480        addCandidate(candidates, 'DOCUMENT_SEARCH', subintent, legalDocument ? 6 : 4, [
481            [legalDocument ? 'LEGAL_DOCUMENT' : 'DOCUMENT_EXPLICIT', legalDocument ? 'texte juridique détecté' : 'document explicite détecté'],
482            ...(facets.temporal.year ? [['YEAR_EXPLICIT', `année ${facets.temporal.year} détectée`]] : [])
483        ]);
484    }
485
486    if (facets.documentType === 'CERTIFICATE' && facets.questionType !== 'AVAILABILITY') {
487        addCandidate(candidates, 'AMBIGUOUS', 'MISSING_OBJECT', 0, [
488            ['MEDICAL_CERTIFICATE', 'certificat médical détecté'],
489            ['MISSING_CERTIFICATE_TYPE', 'type ou usage du certificat non précisé']
490        ], { alternatives: ['ADMINISTRATIVE_PROCEDURE', 'DOCUMENT_SEARCH'], clarificationRequired: true });
491    }
492
493    if (hasDisease && !definition && !personal && !epidemiologyTerm && !report) {
494        addCandidate(candidates, 'AMBIGUOUS', 'TOPIC_ONLY', 0, [
495            ['DISEASE_TOPIC_ONLY', `maladie ${facets.disease} citée sans demande précise`]
496        ], { alternatives: ['HEALTH_INFORMATION', 'EPIDEMIOLOGICAL_DATA'], clarificationRequired: true });
497    }
498
499    if (facets.coverageScheme === 'AMO' && !facets.medicine && !amoList) {
500        addCandidate(candidates, 'AMBIGUOUS', 'TOPIC_ONLY', 0, [
501            ['AMO_TOPIC_ONLY', 'AMO citée sans demande précise']
502        ], { alternatives: ['HEALTH_INFORMATION', 'DOCUMENT_SEARCH'], clarificationRequired: true });
503    }
504
505    if (/^(?:et )?(?:son prix|la semaine precedente|pourquoi cette hausse|pourquoi cette baisse)$/.test(text)) {
506        addCandidate(candidates, 'AMBIGUOUS', 'MISSING_OBJECT', 0, [
507            ['CONTEXT_REQUIRED', 'formulation de suivi sans contexte utilisé']
508        ], { alternatives: [], clarificationRequired: true });
509    }
510
511    return candidates;
512}
513
514const SEMANTIC_PRIORITY = Object.freeze([
515    'CONVERSATIONAL',
516    'OUT_OF_SCOPE',
517    'DOCUMENT_VERIFICATION',
518    'EPIDEMIOLOGICAL_DATA',
519    'ADMINISTRATIVE_PROCEDURE',
520    'CLINICAL_PROTOCOL_GUIDANCE',
521    'CLINICAL_PERSONAL',
522    'AMO_MEDICINE',
523    'FACILITY_SEARCH',
524    'MSDS_SERVICE',
525    'INSTITUTION_INFORMATION',
526    'NEWS_PUBLICATION',
527    'PORTAL_NAVIGATION',
528    'MEDICINE_INFORMATION',
529    'HEALTH_INFORMATION',
530    'DOCUMENT_SEARCH',
531    'AMBIGUOUS'
532]);
533
534function resolveSemanticPriority(candidates) {
535    if (!candidates.length) {
536        return {
537            intent: 'AMBIGUOUS',
538            subintent: 'MISSING_OBJECT',
539            strength: 0,
540            reasons: [['NO_CLEAR_INTENT', 'aucune intention suffisamment précise détectée']],
541            alternatives: [],
542            clarificationRequired: true
543        };
544    }
545
546    return [...candidates].sort((a, b) => {
547        if (b.strength !== a.strength) return b.strength - a.strength;
548        return SEMANTIC_PRIORITY.indexOf(a.intent) - SEMANTIC_PRIORITY.indexOf(b.intent);
549    })[0];
550}
551
552function computeConfidence(candidate) {
553    if (candidate.intent === 'AMBIGUOUS') return 'LOW';
554    if (candidate.strength >= 4) return 'HIGH';
555    if (candidate.strength >= 2) return 'MEDIUM';
556    return 'LOW';
557}
558
559function uniqueSources(values) {
560    return [...new Set(values.filter((value) => SOURCES.includes(value)))];
561}
562
563function buildSourcePlan(candidate, confidence) {
564    const defaults = {
565        HEALTH_INFORMATION: ['HEALTH_INFORMATION_ENGINE', ['JOOMLA_CONTENT']],
566        CLINICAL_PERSONAL: ['CLINICAL_ENGINE', ['CLARIFICATION_HANDLER', 'FACILITY_DIRECTORY']],
567        CLINICAL_PROTOCOL_GUIDANCE: ['JOOMLA_DOCUMENTS', ['CLINICAL_ENGINE', 'JOOMLA_CONTENT']],
568        EPIDEMIOLOGICAL_DATA: ['SISE', ['JOOMLA_DOCUMENTS', 'JOOMLA_CONTENT']],
569        DOCUMENT_SEARCH: ['JOOMLA_DOCUMENTS', ['JOOMLA_CONTENT']],
570        DOCUMENT_VERIFICATION: ['DOCTRUST', ['JOOMLA_CONTENT']],
571        ADMINISTRATIVE_PROCEDURE: ['JOOMLA_DOCUMENTS', ['MSDS_KNOWLEDGE', 'JOOMLA_CONTENT']],
572        MEDICINE_INFORMATION: ['HEALTH_INFORMATION_ENGINE', ['JOOMLA_CONTENT']],
573        AMO_MEDICINE: ['AMO_CATALOG', ['JOOMLA_DOCUMENTS']],
574        FACILITY_SEARCH: ['FACILITY_DIRECTORY', ['SITE_NAVIGATION']],
575        MSDS_SERVICE: ['MSDS_KNOWLEDGE', ['FACILITY_DIRECTORY', 'JOOMLA_CONTENT']],
576        INSTITUTION_INFORMATION: ['MSDS_KNOWLEDGE', ['JOOMLA_CONTENT']],
577        PORTAL_NAVIGATION: ['SITE_NAVIGATION', ['JOOMLA_CONTENT']],
578        NEWS_PUBLICATION: ['NEWS_CONTENT', ['JOOMLA_CONTENT']],
579        CONVERSATIONAL: ['CONVERSATION_HANDLER', []],
580        OUT_OF_SCOPE: ['OUT_OF_SCOPE_HANDLER', []],
581        AMBIGUOUS: ['CLARIFICATION_HANDLER', []]
582    };
583
584    const [preferredSource, fallbacks] = defaults[candidate.intent] || ['NONE', []];
585    const clarificationRequired = candidate.clarificationRequired === true
586        || confidence === 'LOW'
587        || candidate.intent === 'AMBIGUOUS';
588
589    return {
590        status: clarificationRequired ? 'CLARIFICATION_REQUIRED' : 'CANDIDATE_ONLY',
591        preferredSource,
592        fallbackSources: uniqueSources(fallbacks.filter((source) => source !== preferredSource)),
593        clarificationRequired
594    };
595}
596
597function buildReasons(candidate) {
598    return (candidate.reasons || []).slice(0, 5).map(([code, label]) => ({ code, label }));
599}
600
601function adaptSafetyAssessment(safetyAssessment) {
602    if (!safetyAssessment || typeof safetyAssessment !== 'object') {
603        return {
604            status: 'UNAVAILABLE',
605            categories: [],
606            confidence: 'LOW',
607            detector: 'NONE',
608            preemptionCandidate: false
609        };
610    }
611
612    const statuses = ['NONE_DETECTED', 'POTENTIAL', 'HIGH_CONFIDENCE', 'UNAVAILABLE'];
613    const categoriesAllowed = [
614        'RESPIRATORY_DISTRESS', 'LOSS_OF_CONSCIOUSNESS', 'CONVULSION',
615        'MAJOR_BLEEDING', 'SEVERE_CHEST_PAIN', 'UNABLE_TO_DRINK',
616        'POISONING', 'SELF_HARM', 'OTHER_CRITICAL'
617    ];
618    const detectors = ['EXISTING_CLINICAL_ANALYZER', 'CRITICAL_SAFETY_ADAPTER', 'NONE'];
619    const status = statuses.includes(safetyAssessment.status) ? safetyAssessment.status : 'UNAVAILABLE';
620    const categories = Array.isArray(safetyAssessment.categories)
621        ? [...new Set(safetyAssessment.categories.filter((category) => categoriesAllowed.includes(category)))]
622        : [];
623    const confidence = CONFIDENCE_LEVELS.includes(safetyAssessment.confidence)
624        ? safetyAssessment.confidence
625        : (status === 'HIGH_CONFIDENCE' ? 'HIGH' : 'LOW');
626    const detector = detectors.includes(safetyAssessment.detector)
627        ? safetyAssessment.detector
628        : 'NONE';
629
630    return {
631        status,
632        categories,
633        confidence,
634        detector,
635        preemptionCandidate: status === 'HIGH_CONFIDENCE' || status === 'POTENTIAL'
636    };
637}
638
639function buildContextSignal(contextSnapshot) {
640    return {
641        availability: contextSnapshot === null || contextSnapshot === undefined ? 'NONE' : 'IGNORED_IN_SHADOW_V1',
642        activeDomain: null,
643        relation: 'CONTEXT_NOT_USED',
644        source: null,
645        freshness: 'UNKNOWN'
646    };
647}
648
649function buildAmbiguity(candidate, confidence) {
650    const detected = candidate.intent === 'AMBIGUOUS'
651        || candidate.clarificationRequired === true
652        || confidence === 'LOW';
653
654    return {
655        detected,
656        alternatives: Array.isArray(candidate.alternatives)
657            ? candidate.alternatives.slice(0, 2)
658            : []
659    };
660}
661
662function buildNeutralResult(safetyAssessment = null, contextSnapshot = null) {
663    const neutralCandidate = {
664        intent: 'AMBIGUOUS',
665        subintent: 'MISSING_OBJECT',
666        strength: 0,
667        reasons: [['FAIL_OPEN', 'classification indisponible']],
668        alternatives: [],
669        clarificationRequired: true
670    };
671
672    return {
673        schemaVersion: SCHEMA_VERSION,
674        router: ROUTER_NAME,
675        mode: 'SHADOW',
676        intent: neutralCandidate.intent,
677        subintent: neutralCandidate.subintent,
678        confidence: 'LOW',
679        facets: createEmptyFacets(),
680        sourcePlan: buildSourcePlan(neutralCandidate, 'LOW'),
681        safetySignal: adaptSafetyAssessment(safetyAssessment),
682        contextSignal: buildContextSignal(contextSnapshot),
683        ambiguity: buildAmbiguity(neutralCandidate, 'LOW'),
684        reasons: buildReasons(neutralCandidate)
685    };
686}
687
688function routeIntentV2Shadow(input = {}) {
689    try {
690        const safeInput = input && typeof input === 'object' ? input : {};
691        const questionOriginale = safeInput.questionOriginale;
692        const questionNormalisee = safeInput.questionNormalisee;
693        const contextSnapshot = safeInput.contextSnapshot ?? null;
694        const safetyAssessment = safeInput.safetyAssessment ?? null;
695        const text = normalizeInput(questionOriginale, questionNormalisee);
696
697        if (!text) {
698            return buildNeutralResult(safetyAssessment, contextSnapshot);
699        }
700
701        const facets = extractFacets(text);
702        const candidates = detectIntentCandidates(text, facets);
703        const selected = resolveSemanticPriority(candidates);
704        const confidence = computeConfidence(selected);
705
706        return {
707            schemaVersion: SCHEMA_VERSION,
708            router: ROUTER_NAME,
709            mode: 'SHADOW',
710            intent: selected.intent,
711            subintent: selected.subintent,
712            confidence,
713            facets,
714            sourcePlan: buildSourcePlan(selected, confidence),
715            safetySignal: adaptSafetyAssessment(safetyAssessment),
716            contextSignal: buildContextSignal(contextSnapshot),
717            ambiguity: buildAmbiguity(selected, confidence),
718            reasons: buildReasons(selected)
719        };
720    } catch (error) {
721        try {
722            return buildNeutralResult(null, null);
723        } catch (neutralError) {
724            return {
725                schemaVersion: SCHEMA_VERSION,
726                router: ROUTER_NAME,
727                mode: 'SHADOW',
728                intent: 'AMBIGUOUS',
729                subintent: 'MISSING_OBJECT',
730                confidence: 'LOW',
731                facets: createEmptyFacets(),
732                sourcePlan: {
733                    status: 'CLARIFICATION_REQUIRED',
734                    preferredSource: 'CLARIFICATION_HANDLER',
735                    fallbackSources: [],
736                    clarificationRequired: true
737                },
738                safetySignal: {
739                    status: 'UNAVAILABLE', categories: [], confidence: 'LOW', detector: 'NONE', preemptionCandidate: false
740                },
741                contextSignal: {
742                    availability: 'NONE', activeDomain: null, relation: 'CONTEXT_NOT_USED', source: null, freshness: 'UNKNOWN'
743                },
744                ambiguity: { detected: true, alternatives: [] },
745                reasons: [{ code: 'FAIL_OPEN', label: 'classification indisponible' }]
746            };
747        }
748    }
749}
750
751window.MSDS_INTENT_ROUTER_V2_SHADOW = Object.freeze({
752    routeIntentV2Shadow,
753    normalizeInput,
754    extractFacets,
755    detectIntentCandidates,
756    resolveSemanticPriority,
757    buildSourcePlan,
758    computeConfidence,
759    buildReasons,
760    adaptSafetyAssessment,
761    buildNeutralResult,
762    INTENTS,
763    SOURCES,
764    CONFIDENCE_LEVELS
765});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.