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.