Skip to content

Commit 689e751

Browse files
committed
refactor(core): refactorisation de computeResponseScore() - fonction de calcul du score pour un keyword mise à part
1 parent bd1412a commit 689e751

4 files changed

Lines changed: 134 additions & 93 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
hasLevenshteinDistanceLessThan,
3+
longestCommonSubstringWeightedLength,
4+
normalizeText,
5+
} from "../../../../utils/nlp.mjs";
6+
7+
const LEVENSHTEIN_THRESHOLD = 3; // Seuil de similarité (tolérance des fautes d'orthographe et des fautes de frappe)
8+
const WORD_LENGTH_FACTOR = 0.1; // Prise en compte de la taille des keywords (plus les keywords sont grands, plus ils doivent avoir un poids important)
9+
10+
export function computeKeywordScore(userInput, keyword, next, options = {}) {
11+
let distanceScore = 0;
12+
let matchScore = 0;
13+
const MATCH_SCORE_IDENTITY =
14+
options && options.identity_bonus ? options.identity_bonus : 0;
15+
// On prend en compte les keywords négatifs (on ne doit pas les voir dans la question de l'utilisateur)
16+
const isNegativeKeyword = keyword.startsWith("! ");
17+
keyword = keyword.replace(/^!\s/, "");
18+
keyword = normalizeText(keyword, { keepCase: true });
19+
if (userInput.includes(keyword)) {
20+
// Test de l'identité stricte
21+
let strictIdentityMatch = false;
22+
if (next.needsProcessing) {
23+
// Si on utilise la directive !Next, on vérifie que le keyword n'est pas entouré de lettres ou de chiffres dans le message de l'utilisateur
24+
const regexStrictIdentityMatch = new RegExp(`\\b${keyword}\\b`);
25+
if (regexStrictIdentityMatch.test(userInput)) {
26+
strictIdentityMatch = true;
27+
}
28+
} else {
29+
strictIdentityMatch = true;
30+
}
31+
if (strictIdentityMatch) {
32+
// En cas d'identité stricte, on monte le score d'une valeur définie par MATCH_SCORE_IDENTITY, ou alors on le diminue si on avait un keyword négatif
33+
matchScore = isNegativeKeyword
34+
? matchScore - MATCH_SCORE_IDENTITY * 2
35+
: matchScore + MATCH_SCORE_IDENTITY;
36+
// On privilégie les correspondances sur les keywords plus longs
37+
matchScore = matchScore + keyword.length * WORD_LENGTH_FACTOR;
38+
}
39+
} else if (
40+
(userInput.length > 5) &
41+
(keyword.length > 4 && !isNegativeKeyword)
42+
) {
43+
// Sinon : test de la similarité (seulement si le message de l'utilisateur n'est pas très court)
44+
// On calcule la distance de Levenshtein entre le keyword et la question de l'utilisateur (en parcourant les n-grammes du message de l'utilisateur et en prenant en compte la longueur du n-gramme ; avec n = nombre de mots du keyword)
45+
const levenshteinDistance = hasLevenshteinDistanceLessThan(
46+
userInput,
47+
keyword,
48+
LEVENSHTEIN_THRESHOLD,
49+
WORD_LENGTH_FACTOR,
50+
);
51+
distanceScore =
52+
levenshteinDistance > 1
53+
? distanceScore + levenshteinDistance
54+
: distanceScore;
55+
if (!next.needsProcessing) {
56+
// On prend en compte la plus longue chaîne commune de caractères (sauf si on doit passer au message seulement s'il y a présence du keyword [cas d'un quiz] : dans ce cas, on doit être plus strict et tester seulement la proximité avec la distance de Levenshtein pour simplement autoriser quelques fautes d'orthographe)
57+
distanceScore =
58+
distanceScore +
59+
longestCommonSubstringWeightedLength(
60+
userInput,
61+
keyword,
62+
WORD_LENGTH_FACTOR,
63+
);
64+
}
65+
}
66+
return { matchScore, distanceScore };
67+
}
Lines changed: 64 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
1-
import {
2-
hasLevenshteinDistanceLessThan,
3-
cosineSimilarity,
4-
longestCommonSubstringWeightedLength,
5-
normalizeText,
6-
} from "../../../../utils/nlp.mjs";
1+
import { cosineSimilarity } from "../../../../utils/nlp.mjs";
2+
import { computeKeywordScore } from "./computeKeywordScore.mjs";
73

8-
const LEVENSHTEIN_THRESHOLD = 3; // Seuil de similarité (tolérance des fautes d'orthographe et des fautes de frappe)
94
const MATCH_SCORE_IDENTITY = 30; // Pour régler le fait de privilégier l'identité d'un keyword à la simple similarité
10-
const WORD_LENGTH_FACTOR = 0.1; // Prise en compte de la taille des keywords (plus les keywords sont grands, plus ils doivent avoir un poids important)
115

126
function buildKeywordsList(next, response) {
13-
// Si on a la directive !Next, alors on ne teste pas la correspondance avec le titre, mais seulement avec les keywords (sauf s'il n'y a pas de keyword)
7+
// Si on a la directive !Next, on inclut seulement les keywords dans la liste des termes à tester (sauf s'il n'y a pas de keyword)
148
// Sinon on inclut le titre
15-
// On met tout en minuscule
16-
const keywords =
9+
10+
const useOnlyKeywords =
1711
next.needsProcessing &&
1812
response.keywords.length > 0 &&
19-
next.ignoreKeywords !== true
20-
? response.keywords.map((keyword) => keyword.toLowerCase())
21-
: response.keywords
22-
.concat(response.title)
23-
.map((keyword) => keyword.toLowerCase());
24-
return keywords;
13+
next.ignoreKeywords !== true;
14+
15+
const baseList = useOnlyKeywords
16+
? response.keywords
17+
: [...response.keywords, response.title];
18+
19+
return baseList.map((k) => k.toLowerCase());
2520
}
2621

2722
function calculateCosineSimilarityScore(
@@ -37,6 +32,35 @@ function calculateCosineSimilarityScore(
3732
return cosSim ? cosSim + 0.5 : 0;
3833
}
3934

35+
function adjustScore(
36+
response,
37+
matchScore,
38+
distanceScore,
39+
bestDistanceScore,
40+
next,
41+
yaml,
42+
) {
43+
// si on a un score de distance négatif, c'est qu'il y avait des keywords négatifs : donc le matchscore doit être égal à 0
44+
if (distanceScore < 0) {
45+
matchScore = 0;
46+
}
47+
if (
48+
(matchScore == 0 || (yaml && yaml.searchInContent)) &&
49+
!next.needsProcessing
50+
) {
51+
// En cas de simple similarité : on monte quand même le score. Mais si on est dans le mode où on va directement à une réponse en testant la présence de keywords, la correspondance doit être stricte, on ne fait pas de calcul de similarité
52+
if (distanceScore > bestDistanceScore) {
53+
matchScore = matchScore + distanceScore;
54+
bestDistanceScore = distanceScore;
55+
}
56+
}
57+
// Si on a la directive !Next : titre réponse, alors on augmente de manière importante le matchScore si on a un matchScore > 0.5 et que la réponse correspond au titre de la réponse voulue dans la directive
58+
if (matchScore > 0.5 && next.needsProcessing && response.title == next.goto) {
59+
matchScore = matchScore + MATCH_SCORE_IDENTITY;
60+
}
61+
return { matchScore, bestDistanceScore };
62+
}
63+
4064
export function computeResponseScore({
4165
chatbot,
4266
userInput,
@@ -45,13 +69,12 @@ export function computeResponseScore({
4569
yaml,
4670
}) {
4771
const next = chatbot.nextMessage;
48-
let bestDistanceScore = 0;
49-
// Si on a la directive !Next, alors on ne teste pas la correspondance avec le titre, mais seulement avec les keywords (sauf s'il n'y a pas de keyword)
50-
// Sinon on inclut le titre
51-
// On met tout en minuscule
5272
const keywords = buildKeywordsList(next, response);
73+
74+
let bestDistanceScore = 0;
5375
let matchScore = 0;
5476
let distanceScore = 0;
77+
5578
// Si le YAML indique de faire une recherche dans le contenu avec la similarité vectorielle, on prend comme base de score le cosine similarity entre le message de l'utilisateur et le contenu vectoriel de la réponse
5679
if (yaml && yaml.searchInContent) {
5780
matchScore = calculateCosineSimilarityScore(
@@ -61,77 +84,28 @@ export function computeResponseScore({
6184
next,
6285
);
6386
}
87+
88+
// On calcule les scores pour chaque keyword
6489
for (let keyword of keywords) {
65-
// On prend en compte les keywords négatifs (on ne doit pas les voir dans la question de l'utilisateur)
66-
const isNegativeKeyword = keyword.startsWith("! ");
67-
keyword = keyword.replace(/^!\s/, "");
68-
keyword = normalizeText(keyword, { keepCase: true });
69-
if (userInput.includes(keyword)) {
70-
// Test de l'identité stricte
71-
let strictIdentityMatch = false;
72-
if (next.needsProcessing) {
73-
// Si on utilise la directive !Next, on vérifie que le keyword n'est pas entouré de lettres ou de chiffres dans le message de l'utilisateur
74-
const regexStrictIdentityMatch = new RegExp(`\\b${keyword}\\b`);
75-
if (regexStrictIdentityMatch.test(userInput)) {
76-
strictIdentityMatch = true;
77-
}
78-
} else {
79-
strictIdentityMatch = true;
80-
}
81-
if (strictIdentityMatch) {
82-
// En cas d'identité stricte, on monte le score d'une valeur définie par MATCH_SCORE_IDENTITY, ou alors on le diminue si on avait un keyword négatif
83-
matchScore = isNegativeKeyword
84-
? matchScore - MATCH_SCORE_IDENTITY * 2
85-
: matchScore + MATCH_SCORE_IDENTITY;
86-
// On privilégie les correspondances sur les keywords plus longs
87-
matchScore = matchScore + keyword.length * WORD_LENGTH_FACTOR;
88-
}
89-
} else if (
90-
(userInput.length > 5) &
91-
(keyword.length > 4 && !isNegativeKeyword)
92-
) {
93-
// Sinon : test de la similarité (seulement si le message de l'utilisateur n'est pas très court)
94-
// On calcule la distance de Levenshtein entre le keyword et la question de l'utilisateur (en parcourant les n-grammes du message de l'utilisateur et en prenant en compte la longueur du n-gramme ; avec n = nombre de mots du keyword)
95-
const levenshteinDistance = hasLevenshteinDistanceLessThan(
96-
userInput,
97-
keyword,
98-
LEVENSHTEIN_THRESHOLD,
99-
WORD_LENGTH_FACTOR,
100-
);
101-
distanceScore =
102-
levenshteinDistance > 1
103-
? distanceScore + levenshteinDistance
104-
: distanceScore;
105-
if (!next.needsProcessing) {
106-
// On prend en compte la plus longue chaîne commune de caractères (sauf si on doit passer au message seulement s'il y a présence du keyword [cas d'un quiz] : dans ce cas, on doit être plus strict et tester seulement la proximité avec la distance de Levenshtein pour simplement autoriser quelques fautes d'orthographe)
107-
distanceScore =
108-
distanceScore +
109-
longestCommonSubstringWeightedLength(
110-
userInput,
111-
keyword,
112-
WORD_LENGTH_FACTOR,
113-
);
114-
}
115-
}
116-
}
117-
// si on a un score de distance négatif, c'est qu'il y avait des keywords négatifs : donc le matchscore doit être égal à 0
118-
if (distanceScore < 0) {
119-
matchScore = 0;
120-
}
121-
if (
122-
(matchScore == 0 || (yaml && yaml.searchInContent)) &&
123-
!next.needsProcessing
124-
) {
125-
// En cas de simple similarité : on monte quand même le score. Mais si on est dans le mode où on va directement à une réponse en testant la présence de keywords, la correspondance doit être stricte, on ne fait pas de calcul de similarité
126-
if (distanceScore > bestDistanceScore) {
127-
matchScore = matchScore + distanceScore;
128-
bestDistanceScore = distanceScore;
129-
}
130-
}
131-
// Si on a la directive !Next : titre réponse, alors on augmente de manière importante le matchScore si on a un matchScore > 0.5 et que la réponse correspond au titre de la réponse voulue dans la directive
132-
if (matchScore > 0.5 && next.needsProcessing && response.title == next.goto) {
133-
matchScore = matchScore + MATCH_SCORE_IDENTITY;
90+
const keywordScore = computeKeywordScore(userInput, keyword, next, {
91+
identity_bonus: MATCH_SCORE_IDENTITY,
92+
});
93+
matchScore = matchScore + keywordScore.matchScore;
94+
distanceScore = distanceScore + keywordScore.distanceScore;
13495
}
13596

97+
// On ajuste le score
98+
const adjustedScore = adjustScore(
99+
response,
100+
matchScore,
101+
distanceScore,
102+
bestDistanceScore,
103+
next,
104+
yaml,
105+
);
106+
107+
matchScore = adjustedScore.matchScore;
108+
bestDistanceScore = adjustedScore.bestDistanceScore;
109+
136110
return matchScore;
137111
}

app/script.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/script.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)