@atlaskit/editor-plugin-autocomplete 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/cjs/pm-plugins/scoring-pipeline.js +43 -17
- package/dist/cjs/pm-plugins/text-predictor.js +27 -13
- package/dist/es2019/pm-plugins/scoring-pipeline.js +32 -14
- package/dist/es2019/pm-plugins/text-predictor.js +18 -8
- package/dist/esm/pm-plugins/scoring-pipeline.js +42 -17
- package/dist/esm/pm-plugins/text-predictor.js +28 -14
- package/dist/types/pm-plugins/scoring-pipeline.d.ts +10 -0
- package/dist/types-ts4.5/pm-plugins/scoring-pipeline.d.ts +10 -0
- package/package.json +2 -2
- package/src/pm-plugins/scoring-pipeline.ts +41 -15
- package/src/pm-plugins/text-predictor.ts +24 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-autocomplete
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`6b36a63af0057`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6b36a63af0057) -
|
|
8
|
+
Updated scoring math for contextual typeahead autocomplete
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
|
|
14
|
+
## 2.0.0
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
|
|
3
20
|
## 1.0.0
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
|
@@ -4,6 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
+
exports.STAGE2_WEIGHT = exports.STAGE1_WEIGHT = exports.MIN_STAGE1_SCORE = void 0;
|
|
7
8
|
exports.rankCandidates = rankCandidates;
|
|
8
9
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
9
10
|
var _combined_l2_l3_pos_tags = _interopRequireDefault(require("./data/combined_l2_l3_pos_tags.json"));
|
|
@@ -26,10 +27,15 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
26
27
|
var ALPHA = 0.5;
|
|
27
28
|
var BETA = 0.5;
|
|
28
29
|
var NEUTRAL_SCORE = 0.5;
|
|
29
|
-
var STAGE1_WEIGHT = 0.
|
|
30
|
-
var STAGE2_WEIGHT = 0.
|
|
31
|
-
var MIN_STAGE1_SCORE = 0.35;
|
|
30
|
+
var STAGE1_WEIGHT = exports.STAGE1_WEIGHT = 0.35;
|
|
31
|
+
var STAGE2_WEIGHT = exports.STAGE2_WEIGHT = 0.65;
|
|
32
|
+
var MIN_STAGE1_SCORE = exports.MIN_STAGE1_SCORE = 0.35;
|
|
32
33
|
var L1_SESSION_CAP = 1.2;
|
|
34
|
+
// Minimum prefix-payload max LM probability before Stage 2 activates.
|
|
35
|
+
// Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
|
|
36
|
+
// falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
|
|
37
|
+
// at 0.00024) from triggering re-ranking.
|
|
38
|
+
var LM_GATE_THRESHOLD = 0.0005;
|
|
33
39
|
|
|
34
40
|
// ─── Grammar Data (loaded once on import) ───────────────────
|
|
35
41
|
|
|
@@ -193,6 +199,7 @@ function getLmScore(word, lmLogits) {
|
|
|
193
199
|
// ─── Public API ─────────────────────────────────────────────
|
|
194
200
|
|
|
195
201
|
function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
|
|
202
|
+
var _grammarMeta$dropped;
|
|
196
203
|
// Stage 1
|
|
197
204
|
var stage1Results = candidates.map(function (candidate) {
|
|
198
205
|
var _scoreStage = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq),
|
|
@@ -206,33 +213,48 @@ function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxT
|
|
|
206
213
|
stage1Score: stage1Score
|
|
207
214
|
};
|
|
208
215
|
});
|
|
209
|
-
var stage1Survivors =
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
var stage1Survivors = [];
|
|
217
|
+
var stage1Rejected = [];
|
|
218
|
+
var _iterator3 = _createForOfIteratorHelper(stage1Results),
|
|
219
|
+
_step3;
|
|
220
|
+
try {
|
|
221
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
222
|
+
var entry = _step3.value;
|
|
223
|
+
if (entry.stage1Score >= MIN_STAGE1_SCORE) {
|
|
224
|
+
stage1Survivors.push(entry);
|
|
225
|
+
} else {
|
|
226
|
+
stage1Rejected.push(entry.candidate.word);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
212
229
|
|
|
213
|
-
|
|
230
|
+
// Grammar Filter
|
|
231
|
+
} catch (err) {
|
|
232
|
+
_iterator3.e(err);
|
|
233
|
+
} finally {
|
|
234
|
+
_iterator3.f();
|
|
235
|
+
}
|
|
214
236
|
var _applyGrammarFilter = applyGrammarFilter(stage1Survivors, previousWord),
|
|
215
237
|
filtered = _applyGrammarFilter.filtered,
|
|
216
238
|
grammarMeta = _applyGrammarFilter.grammarMeta;
|
|
217
239
|
|
|
218
240
|
// Stage 2 + final assembly
|
|
219
241
|
var lmMax = 0;
|
|
220
|
-
if (lmLogits
|
|
242
|
+
if (lmLogits) {
|
|
221
243
|
var values = Object.values(lmLogits);
|
|
222
|
-
lmMax = Math.max.apply(Math, values);
|
|
244
|
+
if (values.length > 0) lmMax = Math.max.apply(Math, values);
|
|
223
245
|
}
|
|
224
246
|
var scored = filtered.map(function (entry) {
|
|
225
247
|
var lmScore = 0;
|
|
226
248
|
var finalScore = entry.stage1Score;
|
|
227
|
-
if (lmLogits &&
|
|
249
|
+
if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
|
|
228
250
|
var rawLm = getLmScore(entry.candidate.word, lmLogits);
|
|
229
251
|
if (rawLm !== 0) {
|
|
230
|
-
// The word was in the top_k! Score it normally.
|
|
231
252
|
var logitDiff = Math.log(rawLm) - Math.log(lmMax);
|
|
232
253
|
lmScore = Math.exp(logitDiff);
|
|
233
|
-
} else {
|
|
234
|
-
lmScore = 0.05;
|
|
235
254
|
}
|
|
255
|
+
// Words absent from the prefix-filtered payload get lmScore = 0,
|
|
256
|
+
// not 0.05, so they don't outrank genuine LM predictions.
|
|
257
|
+
|
|
236
258
|
finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
|
|
237
259
|
}
|
|
238
260
|
return {
|
|
@@ -244,13 +266,17 @@ function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxT
|
|
|
244
266
|
};
|
|
245
267
|
});
|
|
246
268
|
scored.sort(function (a, b) {
|
|
247
|
-
if (b.finalScore !== a.finalScore)
|
|
248
|
-
return b.finalScore - a.finalScore;
|
|
249
|
-
}
|
|
269
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
250
270
|
return a.word.length - b.word.length;
|
|
251
271
|
});
|
|
252
272
|
return {
|
|
253
273
|
candidates: scored,
|
|
254
|
-
grammarMeta: grammarMeta
|
|
274
|
+
grammarMeta: grammarMeta,
|
|
275
|
+
pipelineDebug: {
|
|
276
|
+
initial: candidates.length,
|
|
277
|
+
stage1Rejected: stage1Rejected,
|
|
278
|
+
grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
|
|
279
|
+
final: scored.length
|
|
280
|
+
}
|
|
255
281
|
};
|
|
256
282
|
}
|
|
@@ -573,11 +573,21 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
573
573
|
sessionFreq: node.sessionFreq
|
|
574
574
|
};
|
|
575
575
|
});
|
|
576
|
+
|
|
577
|
+
// Filter the LM payload to only words matching the current prefix so that
|
|
578
|
+
// lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
|
|
579
|
+
var prefix = currentWord.toLowerCase();
|
|
580
|
+
var prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref4) {
|
|
581
|
+
var _ref5 = (0, _slicedToArray2.default)(_ref4, 1),
|
|
582
|
+
word = _ref5[0];
|
|
583
|
+
return word.startsWith(prefix);
|
|
584
|
+
})) : null;
|
|
576
585
|
var _rankCandidates = (0, _scoringPipeline.rankCandidates)(scoringCandidates, contextVector, function (w) {
|
|
577
586
|
return getWordVector(w);
|
|
578
|
-
},
|
|
587
|
+
}, prefixLmLogits, wordTrie.maxTenantFreq, previousWord),
|
|
579
588
|
ranked = _rankCandidates.candidates,
|
|
580
|
-
grammarMeta = _rankCandidates.grammarMeta
|
|
589
|
+
grammarMeta = _rankCandidates.grammarMeta,
|
|
590
|
+
pipelineDebug = _rankCandidates.pipelineDebug;
|
|
581
591
|
var best = ranked[0];
|
|
582
592
|
var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
|
|
583
593
|
if (debugMode) {
|
|
@@ -624,7 +634,7 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
624
634
|
console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
|
|
625
635
|
|
|
626
636
|
// 4. Scoring formula active this prediction
|
|
627
|
-
var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ?
|
|
637
|
+
var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? "Stage1(\xD7".concat(_scoringPipeline.STAGE1_WEIGHT, ") + LM(\xD7").concat(_scoringPipeline.STAGE2_WEIGHT, ")") : 'Stage1 only (no LM logits)';
|
|
628
638
|
// eslint-disable-next-line no-console
|
|
629
639
|
console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
|
|
630
640
|
|
|
@@ -638,17 +648,21 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
638
648
|
}
|
|
639
649
|
}
|
|
640
650
|
|
|
641
|
-
// 6.
|
|
651
|
+
// 6. Pipeline funnel
|
|
652
|
+
// eslint-disable-next-line no-console
|
|
653
|
+
console.log("%c[Pipeline Funnel] %c\uD83D\uDCE5 In: ".concat(pipelineDebug.initial, " | \u274C Stage 1 (< ").concat(_scoringPipeline.MIN_STAGE1_SCORE, "): -").concat(pipelineDebug.stage1Rejected.length, " | \u274C Grammar: -").concat(pipelineDebug.grammarRejected.length, " | \u2705 Final: ").concat(pipelineDebug.final), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
654
|
+
|
|
655
|
+
// 7. Candidate table
|
|
642
656
|
if (ranked.length > 0) {
|
|
643
657
|
var lmCoverage = ranked.slice(0, 10).filter(function (r) {
|
|
644
|
-
return r.lmScore > 0
|
|
658
|
+
return r.lmScore > 0;
|
|
645
659
|
}).length;
|
|
646
660
|
// eslint-disable-next-line no-console
|
|
647
661
|
console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
|
|
648
662
|
var tableData = ranked.slice(0, 10).map(function (r) {
|
|
649
663
|
var rawLogit = 'Not in Payload';
|
|
650
|
-
if (
|
|
651
|
-
var val =
|
|
664
|
+
if (prefixLmLogits) {
|
|
665
|
+
var val = prefixLmLogits[r.word.toLowerCase()];
|
|
652
666
|
if (val !== undefined) {
|
|
653
667
|
rawLogit = Number(val.toFixed(5));
|
|
654
668
|
}
|
|
@@ -694,7 +708,7 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
694
708
|
// ─── Data Loading ────────────────────────────────────────────────────────────
|
|
695
709
|
|
|
696
710
|
var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
|
|
697
|
-
var
|
|
711
|
+
var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
|
|
698
712
|
var url, res, buffer, float32, wordIndex, nWords, dim;
|
|
699
713
|
return _regenerator.default.wrap(function _callee$(_context) {
|
|
700
714
|
while (1) switch (_context.prev = _context.next) {
|
|
@@ -780,7 +794,7 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
780
794
|
}, _callee, null, [[6, 12], [17, 37]]);
|
|
781
795
|
}));
|
|
782
796
|
return function loadVectorsAsync(_x) {
|
|
783
|
-
return
|
|
797
|
+
return _ref6.apply(this, arguments);
|
|
784
798
|
};
|
|
785
799
|
}();
|
|
786
800
|
var initVectors = exports.initVectors = function initVectors(store) {
|
|
@@ -789,10 +803,10 @@ var initVectors = exports.initVectors = function initVectors(store) {
|
|
|
789
803
|
var loadDefaultVocabulary = exports.loadDefaultVocabulary = function loadDefaultVocabulary() {
|
|
790
804
|
// 1. Load the Atlassian Domain (L2)
|
|
791
805
|
var data = _vocabulary_10k.default;
|
|
792
|
-
var terms = Object.entries(data.words).map(function (
|
|
793
|
-
var
|
|
794
|
-
word =
|
|
795
|
-
stats =
|
|
806
|
+
var terms = Object.entries(data.words).map(function (_ref7) {
|
|
807
|
+
var _ref8 = (0, _slicedToArray2.default)(_ref7, 2),
|
|
808
|
+
word = _ref8[0],
|
|
809
|
+
stats = _ref8[1];
|
|
796
810
|
return {
|
|
797
811
|
word: word,
|
|
798
812
|
freq: stats.freq,
|
|
@@ -18,10 +18,15 @@ import grammarTransitionsData from './data/grammar_transitions_10k.json';
|
|
|
18
18
|
const ALPHA = 0.5;
|
|
19
19
|
const BETA = 0.5;
|
|
20
20
|
const NEUTRAL_SCORE = 0.5;
|
|
21
|
-
const STAGE1_WEIGHT = 0.
|
|
22
|
-
const STAGE2_WEIGHT = 0.
|
|
23
|
-
const MIN_STAGE1_SCORE = 0.35;
|
|
21
|
+
export const STAGE1_WEIGHT = 0.35;
|
|
22
|
+
export const STAGE2_WEIGHT = 0.65;
|
|
23
|
+
export const MIN_STAGE1_SCORE = 0.35;
|
|
24
24
|
const L1_SESSION_CAP = 1.2;
|
|
25
|
+
// Minimum prefix-payload max LM probability before Stage 2 activates.
|
|
26
|
+
// Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
|
|
27
|
+
// falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
|
|
28
|
+
// at 0.00024) from triggering re-ranking.
|
|
29
|
+
const LM_GATE_THRESHOLD = 0.0005;
|
|
25
30
|
|
|
26
31
|
// ─── Grammar Data (loaded once on import) ───────────────────
|
|
27
32
|
|
|
@@ -158,6 +163,7 @@ function getLmScore(word, lmLogits) {
|
|
|
158
163
|
// ─── Public API ─────────────────────────────────────────────
|
|
159
164
|
|
|
160
165
|
export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
|
|
166
|
+
var _grammarMeta$dropped;
|
|
161
167
|
// Stage 1
|
|
162
168
|
const stage1Results = candidates.map(candidate => {
|
|
163
169
|
const {
|
|
@@ -172,7 +178,15 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
172
178
|
stage1Score
|
|
173
179
|
};
|
|
174
180
|
});
|
|
175
|
-
const stage1Survivors =
|
|
181
|
+
const stage1Survivors = [];
|
|
182
|
+
const stage1Rejected = [];
|
|
183
|
+
for (const entry of stage1Results) {
|
|
184
|
+
if (entry.stage1Score >= MIN_STAGE1_SCORE) {
|
|
185
|
+
stage1Survivors.push(entry);
|
|
186
|
+
} else {
|
|
187
|
+
stage1Rejected.push(entry.candidate.word);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
176
190
|
|
|
177
191
|
// Grammar Filter
|
|
178
192
|
const {
|
|
@@ -182,22 +196,22 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
182
196
|
|
|
183
197
|
// Stage 2 + final assembly
|
|
184
198
|
let lmMax = 0;
|
|
185
|
-
if (lmLogits
|
|
199
|
+
if (lmLogits) {
|
|
186
200
|
const values = Object.values(lmLogits);
|
|
187
|
-
lmMax = Math.max(...values);
|
|
201
|
+
if (values.length > 0) lmMax = Math.max(...values);
|
|
188
202
|
}
|
|
189
203
|
const scored = filtered.map(entry => {
|
|
190
204
|
let lmScore = 0;
|
|
191
205
|
let finalScore = entry.stage1Score;
|
|
192
|
-
if (lmLogits &&
|
|
206
|
+
if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
|
|
193
207
|
const rawLm = getLmScore(entry.candidate.word, lmLogits);
|
|
194
208
|
if (rawLm !== 0) {
|
|
195
|
-
// The word was in the top_k! Score it normally.
|
|
196
209
|
const logitDiff = Math.log(rawLm) - Math.log(lmMax);
|
|
197
210
|
lmScore = Math.exp(logitDiff);
|
|
198
|
-
} else {
|
|
199
|
-
lmScore = 0.05;
|
|
200
211
|
}
|
|
212
|
+
// Words absent from the prefix-filtered payload get lmScore = 0,
|
|
213
|
+
// not 0.05, so they don't outrank genuine LM predictions.
|
|
214
|
+
|
|
201
215
|
finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
|
|
202
216
|
}
|
|
203
217
|
return {
|
|
@@ -209,13 +223,17 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
209
223
|
};
|
|
210
224
|
});
|
|
211
225
|
scored.sort((a, b) => {
|
|
212
|
-
if (b.finalScore !== a.finalScore)
|
|
213
|
-
return b.finalScore - a.finalScore;
|
|
214
|
-
}
|
|
226
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
215
227
|
return a.word.length - b.word.length;
|
|
216
228
|
});
|
|
217
229
|
return {
|
|
218
230
|
candidates: scored,
|
|
219
|
-
grammarMeta
|
|
231
|
+
grammarMeta,
|
|
232
|
+
pipelineDebug: {
|
|
233
|
+
initial: candidates.length,
|
|
234
|
+
stage1Rejected,
|
|
235
|
+
grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
|
|
236
|
+
final: scored.length
|
|
237
|
+
}
|
|
220
238
|
};
|
|
221
239
|
}
|
|
@@ -21,7 +21,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
|
|
|
21
21
|
import vocabularyData from './data/vocabulary_10k.json';
|
|
22
22
|
import wordIndexData from './data/word_index_10k.json';
|
|
23
23
|
// import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
|
|
24
|
-
import { rankCandidates } from './scoring-pipeline';
|
|
24
|
+
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
|
|
25
25
|
import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
26
26
|
|
|
27
27
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -452,10 +452,16 @@ export const predict = textBefore => {
|
|
|
452
452
|
authorFreq: node.authorFreq,
|
|
453
453
|
sessionFreq: node.sessionFreq
|
|
454
454
|
}));
|
|
455
|
+
|
|
456
|
+
// Filter the LM payload to only words matching the current prefix so that
|
|
457
|
+
// lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
|
|
458
|
+
const prefix = currentWord.toLowerCase();
|
|
459
|
+
const prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(([word]) => word.startsWith(prefix))) : null;
|
|
455
460
|
const {
|
|
456
461
|
candidates: ranked,
|
|
457
|
-
grammarMeta
|
|
458
|
-
|
|
462
|
+
grammarMeta,
|
|
463
|
+
pipelineDebug
|
|
464
|
+
} = rankCandidates(scoringCandidates, contextVector, w => getWordVector(w), prefixLmLogits, wordTrie.maxTenantFreq, previousWord);
|
|
459
465
|
const best = ranked[0];
|
|
460
466
|
const suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
|
|
461
467
|
if (debugMode) {
|
|
@@ -500,7 +506,7 @@ export const predict = textBefore => {
|
|
|
500
506
|
console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
|
|
501
507
|
|
|
502
508
|
// 4. Scoring formula active this prediction
|
|
503
|
-
const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ?
|
|
509
|
+
const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? `Stage1(×${STAGE1_WEIGHT}) + LM(×${STAGE2_WEIGHT})` : 'Stage1 only (no LM logits)';
|
|
504
510
|
// eslint-disable-next-line no-console
|
|
505
511
|
console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
|
|
506
512
|
|
|
@@ -514,15 +520,19 @@ export const predict = textBefore => {
|
|
|
514
520
|
}
|
|
515
521
|
}
|
|
516
522
|
|
|
517
|
-
// 6.
|
|
523
|
+
// 6. Pipeline funnel
|
|
524
|
+
// eslint-disable-next-line no-console
|
|
525
|
+
console.log(`%c[Pipeline Funnel] %c📥 In: ${pipelineDebug.initial} | ❌ Stage 1 (< ${MIN_STAGE1_SCORE}): -${pipelineDebug.stage1Rejected.length} | ❌ Grammar: -${pipelineDebug.grammarRejected.length} | ✅ Final: ${pipelineDebug.final}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
526
|
+
|
|
527
|
+
// 7. Candidate table
|
|
518
528
|
if (ranked.length > 0) {
|
|
519
|
-
const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0
|
|
529
|
+
const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0).length;
|
|
520
530
|
// eslint-disable-next-line no-console
|
|
521
531
|
console.log(`%cLM coverage: ${lmCoverage}/${Math.min(ranked.length, 10)} candidates had real logit scores`, 'color: #888; font-style: italic;');
|
|
522
532
|
const tableData = ranked.slice(0, 10).map(r => {
|
|
523
533
|
let rawLogit = 'Not in Payload';
|
|
524
|
-
if (
|
|
525
|
-
const val =
|
|
534
|
+
if (prefixLmLogits) {
|
|
535
|
+
const val = prefixLmLogits[r.word.toLowerCase()];
|
|
526
536
|
if (val !== undefined) {
|
|
527
537
|
rawLogit = Number(val.toFixed(5));
|
|
528
538
|
}
|
|
@@ -22,10 +22,15 @@ import grammarTransitionsData from './data/grammar_transitions_10k.json';
|
|
|
22
22
|
var ALPHA = 0.5;
|
|
23
23
|
var BETA = 0.5;
|
|
24
24
|
var NEUTRAL_SCORE = 0.5;
|
|
25
|
-
var STAGE1_WEIGHT = 0.
|
|
26
|
-
var STAGE2_WEIGHT = 0.
|
|
27
|
-
var MIN_STAGE1_SCORE = 0.35;
|
|
25
|
+
export var STAGE1_WEIGHT = 0.35;
|
|
26
|
+
export var STAGE2_WEIGHT = 0.65;
|
|
27
|
+
export var MIN_STAGE1_SCORE = 0.35;
|
|
28
28
|
var L1_SESSION_CAP = 1.2;
|
|
29
|
+
// Minimum prefix-payload max LM probability before Stage 2 activates.
|
|
30
|
+
// Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
|
|
31
|
+
// falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
|
|
32
|
+
// at 0.00024) from triggering re-ranking.
|
|
33
|
+
var LM_GATE_THRESHOLD = 0.0005;
|
|
29
34
|
|
|
30
35
|
// ─── Grammar Data (loaded once on import) ───────────────────
|
|
31
36
|
|
|
@@ -189,6 +194,7 @@ function getLmScore(word, lmLogits) {
|
|
|
189
194
|
// ─── Public API ─────────────────────────────────────────────
|
|
190
195
|
|
|
191
196
|
export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
|
|
197
|
+
var _grammarMeta$dropped;
|
|
192
198
|
// Stage 1
|
|
193
199
|
var stage1Results = candidates.map(function (candidate) {
|
|
194
200
|
var _scoreStage = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq),
|
|
@@ -202,33 +208,48 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
202
208
|
stage1Score: stage1Score
|
|
203
209
|
};
|
|
204
210
|
});
|
|
205
|
-
var stage1Survivors =
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
var stage1Survivors = [];
|
|
212
|
+
var stage1Rejected = [];
|
|
213
|
+
var _iterator3 = _createForOfIteratorHelper(stage1Results),
|
|
214
|
+
_step3;
|
|
215
|
+
try {
|
|
216
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
217
|
+
var entry = _step3.value;
|
|
218
|
+
if (entry.stage1Score >= MIN_STAGE1_SCORE) {
|
|
219
|
+
stage1Survivors.push(entry);
|
|
220
|
+
} else {
|
|
221
|
+
stage1Rejected.push(entry.candidate.word);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
208
224
|
|
|
209
|
-
|
|
225
|
+
// Grammar Filter
|
|
226
|
+
} catch (err) {
|
|
227
|
+
_iterator3.e(err);
|
|
228
|
+
} finally {
|
|
229
|
+
_iterator3.f();
|
|
230
|
+
}
|
|
210
231
|
var _applyGrammarFilter = applyGrammarFilter(stage1Survivors, previousWord),
|
|
211
232
|
filtered = _applyGrammarFilter.filtered,
|
|
212
233
|
grammarMeta = _applyGrammarFilter.grammarMeta;
|
|
213
234
|
|
|
214
235
|
// Stage 2 + final assembly
|
|
215
236
|
var lmMax = 0;
|
|
216
|
-
if (lmLogits
|
|
237
|
+
if (lmLogits) {
|
|
217
238
|
var values = Object.values(lmLogits);
|
|
218
|
-
lmMax = Math.max.apply(Math, values);
|
|
239
|
+
if (values.length > 0) lmMax = Math.max.apply(Math, values);
|
|
219
240
|
}
|
|
220
241
|
var scored = filtered.map(function (entry) {
|
|
221
242
|
var lmScore = 0;
|
|
222
243
|
var finalScore = entry.stage1Score;
|
|
223
|
-
if (lmLogits &&
|
|
244
|
+
if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
|
|
224
245
|
var rawLm = getLmScore(entry.candidate.word, lmLogits);
|
|
225
246
|
if (rawLm !== 0) {
|
|
226
|
-
// The word was in the top_k! Score it normally.
|
|
227
247
|
var logitDiff = Math.log(rawLm) - Math.log(lmMax);
|
|
228
248
|
lmScore = Math.exp(logitDiff);
|
|
229
|
-
} else {
|
|
230
|
-
lmScore = 0.05;
|
|
231
249
|
}
|
|
250
|
+
// Words absent from the prefix-filtered payload get lmScore = 0,
|
|
251
|
+
// not 0.05, so they don't outrank genuine LM predictions.
|
|
252
|
+
|
|
232
253
|
finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
|
|
233
254
|
}
|
|
234
255
|
return {
|
|
@@ -240,13 +261,17 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
240
261
|
};
|
|
241
262
|
});
|
|
242
263
|
scored.sort(function (a, b) {
|
|
243
|
-
if (b.finalScore !== a.finalScore)
|
|
244
|
-
return b.finalScore - a.finalScore;
|
|
245
|
-
}
|
|
264
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
246
265
|
return a.word.length - b.word.length;
|
|
247
266
|
});
|
|
248
267
|
return {
|
|
249
268
|
candidates: scored,
|
|
250
|
-
grammarMeta: grammarMeta
|
|
269
|
+
grammarMeta: grammarMeta,
|
|
270
|
+
pipelineDebug: {
|
|
271
|
+
initial: candidates.length,
|
|
272
|
+
stage1Rejected: stage1Rejected,
|
|
273
|
+
grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
|
|
274
|
+
final: scored.length
|
|
275
|
+
}
|
|
251
276
|
};
|
|
252
277
|
}
|
|
@@ -29,7 +29,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
|
|
|
29
29
|
import vocabularyData from './data/vocabulary_10k.json';
|
|
30
30
|
import wordIndexData from './data/word_index_10k.json';
|
|
31
31
|
// import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
|
|
32
|
-
import { rankCandidates } from './scoring-pipeline';
|
|
32
|
+
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
|
|
33
33
|
import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
34
34
|
|
|
35
35
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -570,11 +570,21 @@ export var predict = function predict(textBefore) {
|
|
|
570
570
|
sessionFreq: node.sessionFreq
|
|
571
571
|
};
|
|
572
572
|
});
|
|
573
|
+
|
|
574
|
+
// Filter the LM payload to only words matching the current prefix so that
|
|
575
|
+
// lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
|
|
576
|
+
var prefix = currentWord.toLowerCase();
|
|
577
|
+
var prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref4) {
|
|
578
|
+
var _ref5 = _slicedToArray(_ref4, 1),
|
|
579
|
+
word = _ref5[0];
|
|
580
|
+
return word.startsWith(prefix);
|
|
581
|
+
})) : null;
|
|
573
582
|
var _rankCandidates = rankCandidates(scoringCandidates, contextVector, function (w) {
|
|
574
583
|
return getWordVector(w);
|
|
575
|
-
},
|
|
584
|
+
}, prefixLmLogits, wordTrie.maxTenantFreq, previousWord),
|
|
576
585
|
ranked = _rankCandidates.candidates,
|
|
577
|
-
grammarMeta = _rankCandidates.grammarMeta
|
|
586
|
+
grammarMeta = _rankCandidates.grammarMeta,
|
|
587
|
+
pipelineDebug = _rankCandidates.pipelineDebug;
|
|
578
588
|
var best = ranked[0];
|
|
579
589
|
var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
|
|
580
590
|
if (debugMode) {
|
|
@@ -621,7 +631,7 @@ export var predict = function predict(textBefore) {
|
|
|
621
631
|
console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
|
|
622
632
|
|
|
623
633
|
// 4. Scoring formula active this prediction
|
|
624
|
-
var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ?
|
|
634
|
+
var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? "Stage1(\xD7".concat(STAGE1_WEIGHT, ") + LM(\xD7").concat(STAGE2_WEIGHT, ")") : 'Stage1 only (no LM logits)';
|
|
625
635
|
// eslint-disable-next-line no-console
|
|
626
636
|
console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
|
|
627
637
|
|
|
@@ -635,17 +645,21 @@ export var predict = function predict(textBefore) {
|
|
|
635
645
|
}
|
|
636
646
|
}
|
|
637
647
|
|
|
638
|
-
// 6.
|
|
648
|
+
// 6. Pipeline funnel
|
|
649
|
+
// eslint-disable-next-line no-console
|
|
650
|
+
console.log("%c[Pipeline Funnel] %c\uD83D\uDCE5 In: ".concat(pipelineDebug.initial, " | \u274C Stage 1 (< ").concat(MIN_STAGE1_SCORE, "): -").concat(pipelineDebug.stage1Rejected.length, " | \u274C Grammar: -").concat(pipelineDebug.grammarRejected.length, " | \u2705 Final: ").concat(pipelineDebug.final), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
651
|
+
|
|
652
|
+
// 7. Candidate table
|
|
639
653
|
if (ranked.length > 0) {
|
|
640
654
|
var lmCoverage = ranked.slice(0, 10).filter(function (r) {
|
|
641
|
-
return r.lmScore > 0
|
|
655
|
+
return r.lmScore > 0;
|
|
642
656
|
}).length;
|
|
643
657
|
// eslint-disable-next-line no-console
|
|
644
658
|
console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
|
|
645
659
|
var tableData = ranked.slice(0, 10).map(function (r) {
|
|
646
660
|
var rawLogit = 'Not in Payload';
|
|
647
|
-
if (
|
|
648
|
-
var val =
|
|
661
|
+
if (prefixLmLogits) {
|
|
662
|
+
var val = prefixLmLogits[r.word.toLowerCase()];
|
|
649
663
|
if (val !== undefined) {
|
|
650
664
|
rawLogit = Number(val.toFixed(5));
|
|
651
665
|
}
|
|
@@ -691,7 +705,7 @@ export var predict = function predict(textBefore) {
|
|
|
691
705
|
// ─── Data Loading ────────────────────────────────────────────────────────────
|
|
692
706
|
|
|
693
707
|
export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
694
|
-
var
|
|
708
|
+
var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
|
|
695
709
|
var url, res, buffer, float32, wordIndex, nWords, dim;
|
|
696
710
|
return _regeneratorRuntime.wrap(function _callee$(_context) {
|
|
697
711
|
while (1) switch (_context.prev = _context.next) {
|
|
@@ -777,7 +791,7 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
777
791
|
}, _callee, null, [[6, 12], [17, 37]]);
|
|
778
792
|
}));
|
|
779
793
|
return function loadVectorsAsync(_x) {
|
|
780
|
-
return
|
|
794
|
+
return _ref6.apply(this, arguments);
|
|
781
795
|
};
|
|
782
796
|
}();
|
|
783
797
|
export var initVectors = function initVectors(store) {
|
|
@@ -786,10 +800,10 @@ export var initVectors = function initVectors(store) {
|
|
|
786
800
|
export var loadDefaultVocabulary = function loadDefaultVocabulary() {
|
|
787
801
|
// 1. Load the Atlassian Domain (L2)
|
|
788
802
|
var data = vocabularyData;
|
|
789
|
-
var terms = Object.entries(data.words).map(function (
|
|
790
|
-
var
|
|
791
|
-
word =
|
|
792
|
-
stats =
|
|
803
|
+
var terms = Object.entries(data.words).map(function (_ref7) {
|
|
804
|
+
var _ref8 = _slicedToArray(_ref7, 2),
|
|
805
|
+
word = _ref8[0],
|
|
806
|
+
stats = _ref8[1];
|
|
793
807
|
return {
|
|
794
808
|
word: word,
|
|
795
809
|
freq: stats.freq,
|
|
@@ -26,8 +26,18 @@ export interface GrammarFilterMeta {
|
|
|
26
26
|
prevTags: string[];
|
|
27
27
|
prevWord: string;
|
|
28
28
|
}
|
|
29
|
+
export declare const STAGE1_WEIGHT = 0.35;
|
|
30
|
+
export declare const STAGE2_WEIGHT = 0.65;
|
|
31
|
+
export declare const MIN_STAGE1_SCORE = 0.35;
|
|
32
|
+
export interface PipelineDebug {
|
|
33
|
+
final: number;
|
|
34
|
+
grammarRejected: string[];
|
|
35
|
+
initial: number;
|
|
36
|
+
stage1Rejected: string[];
|
|
37
|
+
}
|
|
29
38
|
export interface RankCandidatesResult {
|
|
30
39
|
candidates: ScoredCandidate[];
|
|
31
40
|
grammarMeta: GrammarFilterMeta | null;
|
|
41
|
+
pipelineDebug: PipelineDebug;
|
|
32
42
|
}
|
|
33
43
|
export declare function rankCandidates(candidates: ScoringCandidate[], contextVector: Float32Array | null, getWordVector: (word: string) => Float32Array | null, lmLogits: Record<string, number> | null, maxTenantFreq: number, previousWord: string): RankCandidatesResult;
|
|
@@ -26,8 +26,18 @@ export interface GrammarFilterMeta {
|
|
|
26
26
|
prevTags: string[];
|
|
27
27
|
prevWord: string;
|
|
28
28
|
}
|
|
29
|
+
export declare const STAGE1_WEIGHT = 0.35;
|
|
30
|
+
export declare const STAGE2_WEIGHT = 0.65;
|
|
31
|
+
export declare const MIN_STAGE1_SCORE = 0.35;
|
|
32
|
+
export interface PipelineDebug {
|
|
33
|
+
final: number;
|
|
34
|
+
grammarRejected: string[];
|
|
35
|
+
initial: number;
|
|
36
|
+
stage1Rejected: string[];
|
|
37
|
+
}
|
|
29
38
|
export interface RankCandidatesResult {
|
|
30
39
|
candidates: ScoredCandidate[];
|
|
31
40
|
grammarMeta: GrammarFilterMeta | null;
|
|
41
|
+
pipelineDebug: PipelineDebug;
|
|
32
42
|
}
|
|
33
43
|
export declare function rankCandidates(candidates: ScoringCandidate[], contextVector: Float32Array | null, getWordVector: (word: string) => Float32Array | null, lmLogits: Record<string, number> | null, maxTenantFreq: number, previousWord: string): RankCandidatesResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-autocomplete",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"wink-nlp": "^2.4.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@atlaskit/editor-common": "^
|
|
36
|
+
"@atlaskit/editor-common": "^114.1.0",
|
|
37
37
|
"react": "^18.2.0"
|
|
38
38
|
},
|
|
39
39
|
"techstack": {
|
|
@@ -49,10 +49,15 @@ interface GrammarTransitions {
|
|
|
49
49
|
const ALPHA = 0.5;
|
|
50
50
|
const BETA = 0.5;
|
|
51
51
|
const NEUTRAL_SCORE = 0.5;
|
|
52
|
-
const STAGE1_WEIGHT = 0.
|
|
53
|
-
const STAGE2_WEIGHT = 0.
|
|
54
|
-
const MIN_STAGE1_SCORE = 0.35;
|
|
52
|
+
export const STAGE1_WEIGHT = 0.35;
|
|
53
|
+
export const STAGE2_WEIGHT = 0.65;
|
|
54
|
+
export const MIN_STAGE1_SCORE = 0.35;
|
|
55
55
|
const L1_SESSION_CAP = 1.2;
|
|
56
|
+
// Minimum prefix-payload max LM probability before Stage 2 activates.
|
|
57
|
+
// Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
|
|
58
|
+
// falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
|
|
59
|
+
// at 0.00024) from triggering re-ranking.
|
|
60
|
+
const LM_GATE_THRESHOLD = 0.0005;
|
|
56
61
|
|
|
57
62
|
// ─── Grammar Data (loaded once on import) ───────────────────
|
|
58
63
|
|
|
@@ -217,9 +222,17 @@ function getLmScore(word: string, lmLogits: Record<string, number> | null): numb
|
|
|
217
222
|
|
|
218
223
|
// ─── Public API ─────────────────────────────────────────────
|
|
219
224
|
|
|
225
|
+
export interface PipelineDebug {
|
|
226
|
+
final: number;
|
|
227
|
+
grammarRejected: string[];
|
|
228
|
+
initial: number;
|
|
229
|
+
stage1Rejected: string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
220
232
|
export interface RankCandidatesResult {
|
|
221
233
|
candidates: ScoredCandidate[];
|
|
222
234
|
grammarMeta: GrammarFilterMeta | null;
|
|
235
|
+
pipelineDebug: PipelineDebug;
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
export function rankCandidates(
|
|
@@ -240,33 +253,39 @@ export function rankCandidates(
|
|
|
240
253
|
);
|
|
241
254
|
return { candidate, semanticScore, freqScore, stage1Score };
|
|
242
255
|
});
|
|
243
|
-
|
|
244
|
-
const
|
|
256
|
+
const stage1Survivors: typeof stage1Results = [];
|
|
257
|
+
const stage1Rejected: string[] = [];
|
|
258
|
+
for (const entry of stage1Results) {
|
|
259
|
+
if (entry.stage1Score >= MIN_STAGE1_SCORE) {
|
|
260
|
+
stage1Survivors.push(entry);
|
|
261
|
+
} else {
|
|
262
|
+
stage1Rejected.push(entry.candidate.word);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
245
265
|
|
|
246
266
|
// Grammar Filter
|
|
247
267
|
const { filtered, grammarMeta } = applyGrammarFilter(stage1Survivors, previousWord);
|
|
248
268
|
|
|
249
269
|
// Stage 2 + final assembly
|
|
250
270
|
let lmMax = 0;
|
|
251
|
-
if (lmLogits
|
|
271
|
+
if (lmLogits) {
|
|
252
272
|
const values = Object.values(lmLogits);
|
|
253
|
-
lmMax = Math.max(...values);
|
|
273
|
+
if (values.length > 0) lmMax = Math.max(...values);
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
const scored: ScoredCandidate[] = filtered.map((entry) => {
|
|
257
277
|
let lmScore = 0;
|
|
258
278
|
let finalScore = entry.stage1Score;
|
|
259
279
|
|
|
260
|
-
if (lmLogits &&
|
|
280
|
+
if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
|
|
261
281
|
const rawLm = getLmScore(entry.candidate.word, lmLogits);
|
|
262
282
|
|
|
263
283
|
if (rawLm !== 0) {
|
|
264
|
-
// The word was in the top_k! Score it normally.
|
|
265
284
|
const logitDiff = Math.log(rawLm) - Math.log(lmMax);
|
|
266
285
|
lmScore = Math.exp(logitDiff);
|
|
267
|
-
} else {
|
|
268
|
-
lmScore = 0.05;
|
|
269
286
|
}
|
|
287
|
+
// Words absent from the prefix-filtered payload get lmScore = 0,
|
|
288
|
+
// not 0.05, so they don't outrank genuine LM predictions.
|
|
270
289
|
|
|
271
290
|
finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
|
|
272
291
|
}
|
|
@@ -281,11 +300,18 @@ export function rankCandidates(
|
|
|
281
300
|
});
|
|
282
301
|
|
|
283
302
|
scored.sort((a, b) => {
|
|
284
|
-
if (b.finalScore !== a.finalScore)
|
|
285
|
-
return b.finalScore - a.finalScore;
|
|
286
|
-
}
|
|
303
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
287
304
|
return a.word.length - b.word.length;
|
|
288
305
|
});
|
|
289
306
|
|
|
290
|
-
return {
|
|
307
|
+
return {
|
|
308
|
+
candidates: scored,
|
|
309
|
+
grammarMeta,
|
|
310
|
+
pipelineDebug: {
|
|
311
|
+
initial: candidates.length,
|
|
312
|
+
stage1Rejected,
|
|
313
|
+
grammarRejected: grammarMeta?.dropped ?? [],
|
|
314
|
+
final: scored.length,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
291
317
|
}
|
|
@@ -20,7 +20,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
|
|
|
20
20
|
import vocabularyData from './data/vocabulary_10k.json';
|
|
21
21
|
import wordIndexData from './data/word_index_10k.json';
|
|
22
22
|
// import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
|
|
23
|
-
import { rankCandidates } from './scoring-pipeline';
|
|
23
|
+
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
|
|
24
24
|
import type { ScoringCandidate } from './scoring-pipeline';
|
|
25
25
|
import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
26
26
|
|
|
@@ -533,11 +533,19 @@ export const predict = (textBefore: string): string | null => {
|
|
|
533
533
|
sessionFreq: node.sessionFreq,
|
|
534
534
|
}));
|
|
535
535
|
|
|
536
|
-
|
|
536
|
+
// Filter the LM payload to only words matching the current prefix so that
|
|
537
|
+
// lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
|
|
538
|
+
const prefix = currentWord.toLowerCase();
|
|
539
|
+
const prefixLmLogits = lmLogits
|
|
540
|
+
? Object.fromEntries(
|
|
541
|
+
Object.entries(lmLogits).filter(([word]) => word.startsWith(prefix))
|
|
542
|
+
)
|
|
543
|
+
: null;
|
|
544
|
+
const { candidates: ranked, grammarMeta, pipelineDebug } = rankCandidates(
|
|
537
545
|
scoringCandidates,
|
|
538
546
|
contextVector,
|
|
539
547
|
(w: string) => getWordVector(w),
|
|
540
|
-
|
|
548
|
+
prefixLmLogits,
|
|
541
549
|
wordTrie.maxTenantFreq,
|
|
542
550
|
previousWord,
|
|
543
551
|
);
|
|
@@ -624,7 +632,7 @@ export const predict = (textBefore: string): string | null => {
|
|
|
624
632
|
// 4. Scoring formula active this prediction
|
|
625
633
|
const formulaLabel =
|
|
626
634
|
lmLogits && Object.keys(lmLogits).length > 0
|
|
627
|
-
?
|
|
635
|
+
? `Stage1(×${STAGE1_WEIGHT}) + LM(×${STAGE2_WEIGHT})`
|
|
628
636
|
: 'Stage1 only (no LM logits)';
|
|
629
637
|
// eslint-disable-next-line no-console
|
|
630
638
|
console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
|
|
@@ -645,9 +653,17 @@ export const predict = (textBefore: string): string | null => {
|
|
|
645
653
|
}
|
|
646
654
|
}
|
|
647
655
|
|
|
648
|
-
// 6.
|
|
656
|
+
// 6. Pipeline funnel
|
|
657
|
+
// eslint-disable-next-line no-console
|
|
658
|
+
console.log(
|
|
659
|
+
`%c[Pipeline Funnel] %c📥 In: ${pipelineDebug.initial} | ❌ Stage 1 (< ${MIN_STAGE1_SCORE}): -${pipelineDebug.stage1Rejected.length} | ❌ Grammar: -${pipelineDebug.grammarRejected.length} | ✅ Final: ${pipelineDebug.final}`,
|
|
660
|
+
'color: #9c27b0; font-weight: bold;',
|
|
661
|
+
'color: inherit;',
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// 7. Candidate table
|
|
649
665
|
if (ranked.length > 0) {
|
|
650
|
-
const lmCoverage = ranked.slice(0, 10).filter((r) => r.lmScore > 0
|
|
666
|
+
const lmCoverage = ranked.slice(0, 10).filter((r) => r.lmScore > 0).length;
|
|
651
667
|
// eslint-disable-next-line no-console
|
|
652
668
|
console.log(
|
|
653
669
|
`%cLM coverage: ${lmCoverage}/${Math.min(ranked.length, 10)} candidates had real logit scores`,
|
|
@@ -656,8 +672,8 @@ export const predict = (textBefore: string): string | null => {
|
|
|
656
672
|
|
|
657
673
|
const tableData = ranked.slice(0, 10).map((r) => {
|
|
658
674
|
let rawLogit: string | number = 'Not in Payload';
|
|
659
|
-
if (
|
|
660
|
-
const val =
|
|
675
|
+
if (prefixLmLogits) {
|
|
676
|
+
const val = prefixLmLogits[r.word.toLowerCase()];
|
|
661
677
|
if (val !== undefined) {
|
|
662
678
|
rawLogit = Number(val.toFixed(5));
|
|
663
679
|
}
|