@whyparabola/cabar 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +312 -2
- package/dist/cabar-B-R-rHby.d.cts +202 -0
- package/dist/cabar-B-R-rHby.d.ts +202 -0
- package/dist/chunk-UE4FTY7E.js +388 -0
- package/dist/index.cjs +681 -0
- package/dist/index.d.cts +255 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +289 -0
- package/dist/langchain.cjs +358 -0
- package/dist/langchain.d.cts +35 -0
- package/dist/langchain.d.ts +35 -0
- package/dist/langchain.js +50 -0
- package/package.json +84 -4
- package/src/cabar.ts +485 -0
- package/src/context.ts +67 -0
- package/src/index.ts +20 -0
- package/src/langchain.ts +75 -0
- package/src/normalize.ts +40 -0
- package/src/prism.ts +482 -0
- package/src/renderings.ts +170 -0
- package/src/shared.ts +85 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ADAPTIVE_DEFAULTS,
|
|
3
|
+
CHAR4_COUNTER,
|
|
4
|
+
CabarSelector,
|
|
5
|
+
DEFAULTS,
|
|
6
|
+
DEFAULT_RECENCY_DECAY_DAYS,
|
|
7
|
+
claimsRendering,
|
|
8
|
+
cosineSimilarity,
|
|
9
|
+
embeddingSimilarity,
|
|
10
|
+
estimateTokens,
|
|
11
|
+
extract,
|
|
12
|
+
jaccard,
|
|
13
|
+
mention,
|
|
14
|
+
normalizeScores,
|
|
15
|
+
recencyDecay,
|
|
16
|
+
splitSentences,
|
|
17
|
+
stripUndefined,
|
|
18
|
+
tokenize,
|
|
19
|
+
truncateToTokens,
|
|
20
|
+
verbatim
|
|
21
|
+
} from "./chunk-UE4FTY7E.js";
|
|
22
|
+
|
|
23
|
+
// src/prism.ts
|
|
24
|
+
var FIDELITY_RANK = { mention: 0, claims: 1, extract: 2, verbatim: 3 };
|
|
25
|
+
var DEFAULTS2 = {
|
|
26
|
+
budgetTokens: 4e3,
|
|
27
|
+
maxUnits: 12,
|
|
28
|
+
tailBudgetTokens: 600,
|
|
29
|
+
tauFloor: 0.15,
|
|
30
|
+
recencyLambda: 0.3,
|
|
31
|
+
recencyDecayDays: DEFAULT_RECENCY_DECAY_DAYS,
|
|
32
|
+
noveltyWeight: 1,
|
|
33
|
+
guaranteedTokens: 0,
|
|
34
|
+
hysteresisBonus: 0
|
|
35
|
+
};
|
|
36
|
+
var ADAPTIVE_DEFAULTS2 = {
|
|
37
|
+
minKeep: 3,
|
|
38
|
+
relFloor: 0.4,
|
|
39
|
+
novFloor: 0.2,
|
|
40
|
+
maxCeilingTokens: 4e3
|
|
41
|
+
};
|
|
42
|
+
var DEFAULT_FIDELITY_FACTORS = {
|
|
43
|
+
verbatim: 1,
|
|
44
|
+
extract: 0.85,
|
|
45
|
+
claims: 0.75,
|
|
46
|
+
mention: 0.3
|
|
47
|
+
};
|
|
48
|
+
var MS_PER_DAY = 864e5;
|
|
49
|
+
var ALL_FIDELITIES = ["verbatim", "extract", "claims", "mention"];
|
|
50
|
+
var PrismSelector = class {
|
|
51
|
+
/**
|
|
52
|
+
* MCKP-greedy multi-fidelity selection: build each unit's rendering menu,
|
|
53
|
+
* greedily apply the densest feasible ADD/UPGRADE move, run the KMN
|
|
54
|
+
* best-single swap, then floor the remainder into a mention tail.
|
|
55
|
+
*/
|
|
56
|
+
static select(units, query, opts = {}) {
|
|
57
|
+
const cfg = { ...DEFAULTS2, ...stripUndefined(opts) };
|
|
58
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
59
|
+
const counter = opts.tokenCounter ?? CHAR4_COUNTER;
|
|
60
|
+
const phi = { ...DEFAULT_FIDELITY_FACTORS, ...opts.fidelityFactors };
|
|
61
|
+
const adaptive = opts.adaptive === true;
|
|
62
|
+
const minKeep = opts.minKeep ?? ADAPTIVE_DEFAULTS2.minKeep;
|
|
63
|
+
const relFloor = opts.relFloor ?? ADAPTIVE_DEFAULTS2.relFloor;
|
|
64
|
+
const novFloor = opts.novFloor ?? ADAPTIVE_DEFAULTS2.novFloor;
|
|
65
|
+
const maxCeilingTokens = opts.maxCeilingTokens ?? ADAPTIVE_DEFAULTS2.maxCeilingTokens;
|
|
66
|
+
const topScore = units.length > 0 ? Math.max(...units.map((u) => u.score)) : 1;
|
|
67
|
+
const effectiveBudget = adaptive ? maxCeilingTokens : cfg.budgetTokens;
|
|
68
|
+
const budget = Math.max(0, effectiveBudget - cfg.guaranteedTokens);
|
|
69
|
+
const unitById = /* @__PURE__ */ new Map();
|
|
70
|
+
const inputIndex = /* @__PURE__ */ new Map();
|
|
71
|
+
units.forEach((u, i) => {
|
|
72
|
+
unitById.set(u.id, u);
|
|
73
|
+
inputIndex.set(u.id, i);
|
|
74
|
+
});
|
|
75
|
+
const menuOf = /* @__PURE__ */ new Map();
|
|
76
|
+
let candidateTokens = 0;
|
|
77
|
+
for (const u of units) {
|
|
78
|
+
const vCost = counter.count(u.content);
|
|
79
|
+
candidateTokens += vCost;
|
|
80
|
+
const menu = [];
|
|
81
|
+
for (const f of ALL_FIDELITIES) {
|
|
82
|
+
const rendered = renderFidelity(u, f, query, counter);
|
|
83
|
+
if (rendered === null) continue;
|
|
84
|
+
const cost = counter.count(rendered);
|
|
85
|
+
if (f !== "verbatim" && cost >= vCost) continue;
|
|
86
|
+
menu.push({ fidelity: f, rendered, cost, tokens: tokenize(rendered), phi: phi[f], rank: FIDELITY_RANK[f] });
|
|
87
|
+
}
|
|
88
|
+
menuOf.set(u.id, menu);
|
|
89
|
+
}
|
|
90
|
+
const recencyOf = (u) => {
|
|
91
|
+
if (!u.timestamp || cfg.recencyLambda === 0) return 1;
|
|
92
|
+
const ageInDays = Math.max(0, (now.getTime() - u.timestamp.getTime()) / MS_PER_DAY);
|
|
93
|
+
const decay = recencyDecay(ageInDays, cfg.recencyDecayDays);
|
|
94
|
+
return cfg.recencyLambda * decay + (1 - cfg.recencyLambda);
|
|
95
|
+
};
|
|
96
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
97
|
+
let selectedTokens = 0;
|
|
98
|
+
let totalUtility = 0;
|
|
99
|
+
let moves = 0;
|
|
100
|
+
const prevRank = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const p of opts.prevSelected ?? []) {
|
|
102
|
+
const r = FIDELITY_RANK[p.fidelity];
|
|
103
|
+
const existing = prevRank.get(p.unitId);
|
|
104
|
+
if (existing === void 0 || r < existing) prevRank.set(p.unitId, r);
|
|
105
|
+
}
|
|
106
|
+
const noveltyOf = (u, tokens, excludeId) => {
|
|
107
|
+
let maxSim = 0;
|
|
108
|
+
for (const [id, r] of chosen) {
|
|
109
|
+
if (id === excludeId) continue;
|
|
110
|
+
const sim = opts.similarity ? opts.similarity(u, unitById.get(id)) : jaccard(tokens, r.tokens);
|
|
111
|
+
if (sim > maxSim) maxSim = sim;
|
|
112
|
+
}
|
|
113
|
+
return 1 - maxSim;
|
|
114
|
+
};
|
|
115
|
+
const utilityOf = (u, r, excludeId) => {
|
|
116
|
+
const noveltyRaw = noveltyOf(u, r.tokens, excludeId);
|
|
117
|
+
const effectiveNovelty = 1 - cfg.noveltyWeight * (1 - noveltyRaw);
|
|
118
|
+
let g = u.score * recencyOf(u) * effectiveNovelty * r.phi;
|
|
119
|
+
const pr = prevRank.get(u.id);
|
|
120
|
+
if (cfg.hysteresisBonus > 0 && pr !== void 0 && r.rank >= pr) {
|
|
121
|
+
g *= 1 + cfg.hysteresisBonus;
|
|
122
|
+
}
|
|
123
|
+
return g;
|
|
124
|
+
};
|
|
125
|
+
const better = (a, b) => b === null || a.density > b.density || a.density === b.density && a.deltaG > b.deltaG;
|
|
126
|
+
for (; ; ) {
|
|
127
|
+
let best = null;
|
|
128
|
+
for (const u of units) {
|
|
129
|
+
const menu = menuOf.get(u.id);
|
|
130
|
+
const current = chosen.get(u.id);
|
|
131
|
+
if (!current) {
|
|
132
|
+
if (!adaptive && chosen.size >= cfg.maxUnits) continue;
|
|
133
|
+
for (const r of menu) {
|
|
134
|
+
if (r.cost <= 0) continue;
|
|
135
|
+
if (selectedTokens + r.cost > budget) continue;
|
|
136
|
+
if (adaptive && chosen.size >= minKeep) {
|
|
137
|
+
const relFrac = u.score / (topScore || 1);
|
|
138
|
+
const nov = noveltyOf(u, r.tokens, null);
|
|
139
|
+
if (relFrac < relFloor || nov < novFloor) continue;
|
|
140
|
+
}
|
|
141
|
+
const deltaG = utilityOf(u, r, null);
|
|
142
|
+
if (deltaG <= 0) continue;
|
|
143
|
+
const move = { unitId: u.id, rendering: r, deltaG, deltaC: r.cost, density: deltaG / r.cost };
|
|
144
|
+
if (better(move, best)) best = move;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const baseG = utilityOf(u, current, u.id);
|
|
148
|
+
for (const r of menu) {
|
|
149
|
+
const deltaC = r.cost - current.cost;
|
|
150
|
+
if (deltaC <= 0) continue;
|
|
151
|
+
if (selectedTokens + deltaC > budget) continue;
|
|
152
|
+
const deltaG = utilityOf(u, r, u.id) - baseG;
|
|
153
|
+
if (deltaG <= 0) continue;
|
|
154
|
+
const move = { unitId: u.id, rendering: r, deltaG, deltaC, density: deltaG / deltaC };
|
|
155
|
+
if (better(move, best)) best = move;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!best) break;
|
|
160
|
+
selectedTokens += best.deltaC;
|
|
161
|
+
totalUtility += best.deltaG;
|
|
162
|
+
chosen.set(best.unitId, best.rendering);
|
|
163
|
+
moves++;
|
|
164
|
+
}
|
|
165
|
+
if (adaptive || cfg.maxUnits >= 1) {
|
|
166
|
+
let bestSingle = null;
|
|
167
|
+
for (const u of units) {
|
|
168
|
+
const vr = menuOf.get(u.id).find((r) => r.fidelity === "verbatim");
|
|
169
|
+
if (!vr || vr.cost > budget) continue;
|
|
170
|
+
const utility = u.score * recencyOf(u) * vr.phi;
|
|
171
|
+
if (!bestSingle || utility > bestSingle.utility) bestSingle = { unit: u, rendering: vr, utility };
|
|
172
|
+
}
|
|
173
|
+
if (bestSingle && bestSingle.utility > totalUtility) {
|
|
174
|
+
chosen.clear();
|
|
175
|
+
chosen.set(bestSingle.unit.id, bestSingle.rendering);
|
|
176
|
+
selectedTokens = bestSingle.rendering.cost;
|
|
177
|
+
totalUtility = bestSingle.utility;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const eligible = units.filter((u) => !chosen.has(u.id) && u.score >= cfg.tauFloor).sort((a, b) => b.score - a.score || inputIndex.get(a.id) - inputIndex.get(b.id));
|
|
181
|
+
const mentionLines = [];
|
|
182
|
+
let mentionUsed = 0;
|
|
183
|
+
for (const u of eligible) {
|
|
184
|
+
const line = mention(u, counter);
|
|
185
|
+
const lineTokens = counter.count(mentionLines.length === 0 ? line : `
|
|
186
|
+
${line}`);
|
|
187
|
+
if (mentionUsed + lineTokens > cfg.tailBudgetTokens) break;
|
|
188
|
+
mentionLines.push(line);
|
|
189
|
+
mentionUsed += lineTokens;
|
|
190
|
+
}
|
|
191
|
+
const mentionBlock = mentionLines.join("\n");
|
|
192
|
+
const verbatimBlock = eligible.map((u) => u.content.trim()).join("\n");
|
|
193
|
+
let tailBlock = eligible.length === 0 ? "" : counter.count(verbatimBlock) < counter.count(mentionBlock) ? verbatimBlock : mentionBlock;
|
|
194
|
+
if (counter.count(tailBlock) > cfg.tailBudgetTokens) {
|
|
195
|
+
tailBlock = truncateToTokens(tailBlock, cfg.tailBudgetTokens, counter);
|
|
196
|
+
}
|
|
197
|
+
const tailTokens = counter.count(tailBlock);
|
|
198
|
+
const selected = [...chosen.entries()].map(([id, r]) => ({ unit: unitById.get(id), fidelity: r.fidelity, rendered: r.rendered, tokens: r.cost })).sort((a, b) => tsKey(a.unit) - tsKey(b.unit) || a.unit.id.localeCompare(b.unit.id));
|
|
199
|
+
const fidelityHistogram = { verbatim: 0, extract: 0, claims: 0, mention: 0 };
|
|
200
|
+
for (const s of selected) fidelityHistogram[s.fidelity]++;
|
|
201
|
+
return {
|
|
202
|
+
selected,
|
|
203
|
+
tail: eligible,
|
|
204
|
+
tailBlock,
|
|
205
|
+
stats: {
|
|
206
|
+
candidateTokens,
|
|
207
|
+
selectedTokens,
|
|
208
|
+
tailTokens,
|
|
209
|
+
budgetTokens: effectiveBudget,
|
|
210
|
+
utilizedTokens: cfg.guaranteedTokens + selectedTokens + tailTokens,
|
|
211
|
+
fidelityHistogram,
|
|
212
|
+
moves
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function renderFidelity(u, f, query, counter) {
|
|
218
|
+
switch (f) {
|
|
219
|
+
case "verbatim":
|
|
220
|
+
return verbatim(u);
|
|
221
|
+
case "extract":
|
|
222
|
+
return extract(u, query, counter);
|
|
223
|
+
case "claims":
|
|
224
|
+
return claimsRendering(u);
|
|
225
|
+
case "mention":
|
|
226
|
+
return mention(u, counter);
|
|
227
|
+
default: {
|
|
228
|
+
const _exhaustive = f;
|
|
229
|
+
return _exhaustive;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function tsKey(u) {
|
|
234
|
+
return u.timestamp ? u.timestamp.getTime() : 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/context.ts
|
|
238
|
+
function isPrism(r) {
|
|
239
|
+
return "tailBlock" in r;
|
|
240
|
+
}
|
|
241
|
+
function toContext(result, opts = {}) {
|
|
242
|
+
const separator = opts.separator ?? "\n\n";
|
|
243
|
+
const heading = opts.tailHeading ?? "Other possibly relevant notes:";
|
|
244
|
+
const format = opts.format ?? ((text) => text);
|
|
245
|
+
const parts = isPrism(result) ? result.selected.map((s) => format(s.rendered, s.unit.id)) : result.selected.map((u) => format(u.content, u.id));
|
|
246
|
+
const tail = isPrism(result) ? result.tailBlock : result.tailSummary;
|
|
247
|
+
if (tail) parts.push(heading ? `${heading}
|
|
248
|
+
${tail}` : tail);
|
|
249
|
+
return parts.join(separator);
|
|
250
|
+
}
|
|
251
|
+
function shadowReport(units, query, opts = {}) {
|
|
252
|
+
const counter = opts.tokenCounter ?? CHAR4_COUNTER;
|
|
253
|
+
const result = CabarSelector.select(units, query, opts);
|
|
254
|
+
const sendAllTokens = units.reduce((n, u) => n + counter.count(u.content), 0);
|
|
255
|
+
const cabarTokens = result.stats.selectedTokens + result.stats.tailSummaryTokens;
|
|
256
|
+
const kept = new Set([...result.selected, ...result.tail].map((u) => u.id));
|
|
257
|
+
return {
|
|
258
|
+
sendAllTokens,
|
|
259
|
+
cabarTokens,
|
|
260
|
+
savedFraction: sendAllTokens === 0 ? 0 : 1 - cabarTokens / sendAllTokens,
|
|
261
|
+
selectedIds: result.selected.map((u) => u.id),
|
|
262
|
+
tailIds: result.tail.map((u) => u.id),
|
|
263
|
+
droppedIds: units.filter((u) => !kept.has(u.id)).map((u) => u.id),
|
|
264
|
+
result
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
export {
|
|
268
|
+
ADAPTIVE_DEFAULTS,
|
|
269
|
+
CHAR4_COUNTER,
|
|
270
|
+
CabarSelector,
|
|
271
|
+
DEFAULTS,
|
|
272
|
+
DEFAULT_RECENCY_DECAY_DAYS,
|
|
273
|
+
PrismSelector,
|
|
274
|
+
claimsRendering,
|
|
275
|
+
cosineSimilarity,
|
|
276
|
+
embeddingSimilarity,
|
|
277
|
+
estimateTokens,
|
|
278
|
+
extract,
|
|
279
|
+
jaccard,
|
|
280
|
+
mention,
|
|
281
|
+
normalizeScores,
|
|
282
|
+
recencyDecay,
|
|
283
|
+
shadowReport,
|
|
284
|
+
splitSentences,
|
|
285
|
+
toContext,
|
|
286
|
+
tokenize,
|
|
287
|
+
truncateToTokens,
|
|
288
|
+
verbatim
|
|
289
|
+
};
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/langchain.ts
|
|
21
|
+
var langchain_exports = {};
|
|
22
|
+
__export(langchain_exports, {
|
|
23
|
+
CabarCompressor: () => CabarCompressor
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(langchain_exports);
|
|
26
|
+
var import_document_compressors = require("@langchain/core/retrievers/document_compressors");
|
|
27
|
+
var import_documents = require("@langchain/core/documents");
|
|
28
|
+
|
|
29
|
+
// src/shared.ts
|
|
30
|
+
function estimateTokens(text) {
|
|
31
|
+
return Math.ceil(text.length / 4);
|
|
32
|
+
}
|
|
33
|
+
var CHAR4_COUNTER = { name: "char4", count: estimateTokens };
|
|
34
|
+
var DEFAULT_RECENCY_DECAY_DAYS = 365;
|
|
35
|
+
function recencyDecay(ageInDays, decayDays = DEFAULT_RECENCY_DECAY_DAYS) {
|
|
36
|
+
if (ageInDays < 0) throw new RangeError("ageInDays must be >= 0");
|
|
37
|
+
if (decayDays <= 0) throw new RangeError("decayDays must be > 0");
|
|
38
|
+
return Math.exp(-ageInDays / decayDays);
|
|
39
|
+
}
|
|
40
|
+
function stripUndefined(obj) {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const key of Object.keys(obj)) {
|
|
43
|
+
if (obj[key] !== void 0) out[key] = obj[key];
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/renderings.ts
|
|
49
|
+
function truncateToTokens(text, maxTokens, counter) {
|
|
50
|
+
if (counter.count(text) <= maxTokens) return text;
|
|
51
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
52
|
+
let out = "";
|
|
53
|
+
for (const w of words) {
|
|
54
|
+
const candidate = out ? `${out} ${w}` : w;
|
|
55
|
+
if (counter.count(`${candidate}\u2026`) > maxTokens) break;
|
|
56
|
+
out = candidate;
|
|
57
|
+
}
|
|
58
|
+
if (!out) out = words[0] ?? "";
|
|
59
|
+
return `${out}\u2026`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/cabar.ts
|
|
63
|
+
var DEFAULTS = {
|
|
64
|
+
budgetTokens: 4e3,
|
|
65
|
+
maxUnits: 8,
|
|
66
|
+
tailBudgetTokens: 600,
|
|
67
|
+
tauFloor: 0.15,
|
|
68
|
+
recencyLambda: 0.3,
|
|
69
|
+
recencyDecayDays: DEFAULT_RECENCY_DECAY_DAYS,
|
|
70
|
+
noveltyWeight: 1,
|
|
71
|
+
guaranteedTokens: 0,
|
|
72
|
+
costExponent: 1
|
|
73
|
+
};
|
|
74
|
+
var ADAPTIVE_DEFAULTS = {
|
|
75
|
+
minKeep: 3,
|
|
76
|
+
relFloor: 0.4,
|
|
77
|
+
novFloor: 0.2,
|
|
78
|
+
maxCeilingTokens: 4e3
|
|
79
|
+
};
|
|
80
|
+
var MS_PER_DAY = 864e5;
|
|
81
|
+
function tokenize(text) {
|
|
82
|
+
const matches = text.toLowerCase().match(/[a-z0-9]{3,}/g);
|
|
83
|
+
return new Set(matches ?? []);
|
|
84
|
+
}
|
|
85
|
+
function jaccard(a, b) {
|
|
86
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
87
|
+
let intersection = 0;
|
|
88
|
+
for (const t of a) {
|
|
89
|
+
if (b.has(t)) intersection++;
|
|
90
|
+
}
|
|
91
|
+
const union = a.size + b.size - intersection;
|
|
92
|
+
return union === 0 ? 0 : intersection / union;
|
|
93
|
+
}
|
|
94
|
+
var CabarSelector = class {
|
|
95
|
+
/**
|
|
96
|
+
* Select which units fit the budget, greedily maximizing utility/cost
|
|
97
|
+
* density, with a best-single-vs-greedy swap (budgeted-max-coverage 2-approx
|
|
98
|
+
* guarantee) and an extractive tail summary for the rest.
|
|
99
|
+
*/
|
|
100
|
+
static select(units, query, opts = {}) {
|
|
101
|
+
const cfg = { ...DEFAULTS, ...stripUndefined(opts) };
|
|
102
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
103
|
+
const counter = opts.tokenCounter ?? CHAR4_COUNTER;
|
|
104
|
+
const count = (t) => counter.count(t);
|
|
105
|
+
const adaptive = opts.adaptive === true;
|
|
106
|
+
const minKeep = opts.minKeep ?? ADAPTIVE_DEFAULTS.minKeep;
|
|
107
|
+
const relFloor = opts.relFloor ?? ADAPTIVE_DEFAULTS.relFloor;
|
|
108
|
+
const novFloor = opts.novFloor ?? ADAPTIVE_DEFAULTS.novFloor;
|
|
109
|
+
const maxCeilingTokens = opts.maxCeilingTokens ?? ADAPTIVE_DEFAULTS.maxCeilingTokens;
|
|
110
|
+
const capToMaxUnits = opts.capToMaxUnits === true;
|
|
111
|
+
const topScore = units.length > 0 ? Math.max(...units.map((u) => u.score)) : 1;
|
|
112
|
+
const costOf = /* @__PURE__ */ new Map();
|
|
113
|
+
const tokensOf = /* @__PURE__ */ new Map();
|
|
114
|
+
let candidateTokens = 0;
|
|
115
|
+
for (const u of units) {
|
|
116
|
+
const cost = count(u.content);
|
|
117
|
+
costOf.set(u.id, cost);
|
|
118
|
+
tokensOf.set(u.id, tokenize(u.content));
|
|
119
|
+
candidateTokens += cost;
|
|
120
|
+
}
|
|
121
|
+
const effectiveBudget = adaptive ? maxCeilingTokens : cfg.budgetTokens;
|
|
122
|
+
const remainingBudget = Math.max(0, effectiveBudget - cfg.guaranteedTokens);
|
|
123
|
+
const recencyOf = (u) => {
|
|
124
|
+
if (!u.timestamp || cfg.recencyLambda === 0) return 1;
|
|
125
|
+
const ageInDays = Math.max(0, (now.getTime() - u.timestamp.getTime()) / MS_PER_DAY);
|
|
126
|
+
const decay = recencyDecay(ageInDays, cfg.recencyDecayDays);
|
|
127
|
+
return cfg.recencyLambda * decay + (1 - cfg.recencyLambda);
|
|
128
|
+
};
|
|
129
|
+
const noveltyOf = (u, selectedSoFar) => {
|
|
130
|
+
if (selectedSoFar.length === 0) return 1;
|
|
131
|
+
const a = tokensOf.get(u.id);
|
|
132
|
+
let maxSim = 0;
|
|
133
|
+
for (const s of selectedSoFar) {
|
|
134
|
+
const sim2 = opts.similarity ? opts.similarity(u, s) : jaccard(a, tokensOf.get(s.id));
|
|
135
|
+
if (sim2 > maxSim) maxSim = sim2;
|
|
136
|
+
}
|
|
137
|
+
return 1 - maxSim;
|
|
138
|
+
};
|
|
139
|
+
const utilityOf = (u, selectedSoFar) => {
|
|
140
|
+
const r = u.score;
|
|
141
|
+
const rho = recencyOf(u);
|
|
142
|
+
const noveltyRaw = noveltyOf(u, selectedSoFar);
|
|
143
|
+
const effectiveNovelty = 1 - cfg.noveltyWeight * (1 - noveltyRaw);
|
|
144
|
+
return r * rho * effectiveNovelty;
|
|
145
|
+
};
|
|
146
|
+
const selected = [];
|
|
147
|
+
const selectedIds = /* @__PURE__ */ new Set();
|
|
148
|
+
let selectedTokens = 0;
|
|
149
|
+
let greedyTotalUtility = 0;
|
|
150
|
+
while (adaptive && !capToMaxUnits || selected.length < cfg.maxUnits) {
|
|
151
|
+
let best = null;
|
|
152
|
+
for (const u of units) {
|
|
153
|
+
if (selectedIds.has(u.id)) continue;
|
|
154
|
+
const cost = costOf.get(u.id);
|
|
155
|
+
if (selectedTokens + cost > remainingBudget) continue;
|
|
156
|
+
const utility = utilityOf(u, selected);
|
|
157
|
+
if (utility <= 0) continue;
|
|
158
|
+
const density = cost > 0 ? utility / Math.pow(cost, cfg.costExponent) : Infinity;
|
|
159
|
+
if (!best || density > best.density) {
|
|
160
|
+
best = { unit: u, utility, density };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!best) break;
|
|
164
|
+
if (adaptive && selected.length >= minKeep) {
|
|
165
|
+
const relFrac = best.unit.score / (topScore || 1);
|
|
166
|
+
const nov = noveltyOf(best.unit, selected);
|
|
167
|
+
if (relFrac < relFloor || nov < novFloor) break;
|
|
168
|
+
}
|
|
169
|
+
selected.push(best.unit);
|
|
170
|
+
selectedIds.add(best.unit.id);
|
|
171
|
+
selectedTokens += costOf.get(best.unit.id);
|
|
172
|
+
greedyTotalUtility += best.utility;
|
|
173
|
+
}
|
|
174
|
+
let swapped = false;
|
|
175
|
+
if (adaptive || cfg.maxUnits >= 1) {
|
|
176
|
+
let bestSingle = null;
|
|
177
|
+
for (const u of units) {
|
|
178
|
+
const cost = costOf.get(u.id);
|
|
179
|
+
if (cost > remainingBudget) continue;
|
|
180
|
+
const utility = utilityOf(u, []);
|
|
181
|
+
if (!bestSingle || utility > bestSingle.utility) {
|
|
182
|
+
bestSingle = { unit: u, utility };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (bestSingle && bestSingle.utility > greedyTotalUtility) {
|
|
186
|
+
swapped = true;
|
|
187
|
+
selected.length = 0;
|
|
188
|
+
selected.push(bestSingle.unit);
|
|
189
|
+
selectedIds.clear();
|
|
190
|
+
selectedIds.add(bestSingle.unit.id);
|
|
191
|
+
selectedTokens = costOf.get(bestSingle.unit.id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const tail = units.filter((u) => !selectedIds.has(u.id) && u.score >= cfg.tauFloor);
|
|
195
|
+
const extractive = buildTailSummary(tail, query, cfg.tailBudgetTokens, count);
|
|
196
|
+
const verbatim = tail.map((u) => u.content.trim()).join("\n");
|
|
197
|
+
let tailSummary = tail.length === 0 ? "" : count(verbatim) < count(extractive) ? verbatim : extractive;
|
|
198
|
+
if (count(tailSummary) > cfg.tailBudgetTokens) {
|
|
199
|
+
tailSummary = counter === CHAR4_COUNTER ? truncateToTokens2(tailSummary, cfg.tailBudgetTokens) : truncateToTokens(tailSummary, cfg.tailBudgetTokens, counter);
|
|
200
|
+
}
|
|
201
|
+
const tailSummaryTokens = count(tailSummary);
|
|
202
|
+
const result = {
|
|
203
|
+
selected,
|
|
204
|
+
tail,
|
|
205
|
+
tailSummary,
|
|
206
|
+
stats: {
|
|
207
|
+
candidateCount: units.length,
|
|
208
|
+
candidateTokens,
|
|
209
|
+
selectedTokens,
|
|
210
|
+
tailSummaryTokens,
|
|
211
|
+
budgetTokens: effectiveBudget,
|
|
212
|
+
utilizedTokens: cfg.guaranteedTokens + selectedTokens + tailSummaryTokens
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
if (!opts.explain) return result;
|
|
216
|
+
const sim = (a, b) => opts.similarity ? opts.similarity(a, b) : jaccard(tokensOf.get(a.id), tokensOf.get(b.id));
|
|
217
|
+
const unitCap = !adaptive || capToMaxUnits;
|
|
218
|
+
result.decisions = units.map((u) => {
|
|
219
|
+
const tokens = costOf.get(u.id);
|
|
220
|
+
const others = selected.filter((s) => s.id !== u.id);
|
|
221
|
+
let maxSim = 0;
|
|
222
|
+
let closest;
|
|
223
|
+
for (const s of others) {
|
|
224
|
+
const v = sim(u, s);
|
|
225
|
+
if (v > maxSim) [maxSim, closest] = [v, s.id];
|
|
226
|
+
}
|
|
227
|
+
const base = { id: u.id, tokens, relevance: u.score, recency: recencyOf(u), novelty: 1 - maxSim };
|
|
228
|
+
if (selectedIds.has(u.id)) {
|
|
229
|
+
return { ...base, status: "selected", reason: swapped ? "best single unit: worth more alone than the greedy set" : "selected" };
|
|
230
|
+
}
|
|
231
|
+
if (u.score < cfg.tauFloor) {
|
|
232
|
+
return { ...base, status: "dropped", reason: `relevance ${u.score.toFixed(2)} is below tauFloor ${cfg.tauFloor}` };
|
|
233
|
+
}
|
|
234
|
+
let reason;
|
|
235
|
+
if (swapped) reason = "a single unit beat the greedy set";
|
|
236
|
+
else if (maxSim >= 1 - (adaptive ? novFloor : 0.2) && closest)
|
|
237
|
+
reason = `redundant with ${closest} (similarity ${maxSim.toFixed(2)})`;
|
|
238
|
+
else if (selectedTokens + tokens > remainingBudget) reason = `doesn't fit: needs ${tokens} tokens, ${Math.max(0, remainingBudget - selectedTokens)} left`;
|
|
239
|
+
else if (unitCap && selected.length >= cfg.maxUnits) reason = `maxUnits (${cfg.maxUnits}) reached`;
|
|
240
|
+
else if (adaptive && u.score / (topScore || 1) < relFloor) reason = `below the adaptive relevance cliff (${(u.score / (topScore || 1)).toFixed(2)} of the top score)`;
|
|
241
|
+
else reason = "adaptive stop: lower value per token than what was kept";
|
|
242
|
+
return { ...base, status: "tail", reason, ...reason.startsWith("redundant") ? { redundantWith: closest } : {} };
|
|
243
|
+
});
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
function truncateToTokens2(text, budgetTokens) {
|
|
248
|
+
const maxChars = budgetTokens * 4;
|
|
249
|
+
if (text.length <= maxChars) return text;
|
|
250
|
+
const cut = text.slice(0, maxChars);
|
|
251
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
252
|
+
return (lastSpace > maxChars * 0.8 ? cut.slice(0, lastSpace) : cut).trimEnd() + "\u2026";
|
|
253
|
+
}
|
|
254
|
+
function buildTailSummary(tail, query, tailBudgetTokens, count) {
|
|
255
|
+
if (tail.length === 0 || tailBudgetTokens <= 0) return "";
|
|
256
|
+
const queryTokens = tokenize(query);
|
|
257
|
+
const sentences = [];
|
|
258
|
+
let order = 0;
|
|
259
|
+
for (const unit of tail) {
|
|
260
|
+
const parts = unit.content.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
|
|
261
|
+
for (const text of parts) {
|
|
262
|
+
const tokens = tokenize(text);
|
|
263
|
+
let overlap = 0;
|
|
264
|
+
for (const t of tokens) {
|
|
265
|
+
if (queryTokens.has(t)) overlap++;
|
|
266
|
+
}
|
|
267
|
+
sentences.push({
|
|
268
|
+
text,
|
|
269
|
+
score: overlap + unit.score,
|
|
270
|
+
unitId: unit.id,
|
|
271
|
+
timestamp: unit.timestamp,
|
|
272
|
+
order: order++
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
sentences.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
278
|
+
const lines = [];
|
|
279
|
+
let usedTokens = 0;
|
|
280
|
+
for (const s of sentences) {
|
|
281
|
+
const normalized = s.text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
282
|
+
if (seen.has(normalized)) continue;
|
|
283
|
+
const idPart = s.timestamp ? `${s.unitId}@${s.timestamp.toISOString().slice(0, 10)}` : s.unitId;
|
|
284
|
+
const line = `[${idPart}] ${s.text}`;
|
|
285
|
+
const lineTokens = count(lines.length === 0 ? line : `
|
|
286
|
+
${line}`);
|
|
287
|
+
if (usedTokens + lineTokens > tailBudgetTokens) break;
|
|
288
|
+
seen.add(normalized);
|
|
289
|
+
lines.push(line);
|
|
290
|
+
usedTokens += lineTokens;
|
|
291
|
+
}
|
|
292
|
+
return lines.join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/normalize.ts
|
|
296
|
+
function normalizeScores(units, opts = {}) {
|
|
297
|
+
if (units.length === 0) return [];
|
|
298
|
+
const method = opts.method ?? "minmax";
|
|
299
|
+
const raw = units.map((u) => opts.higherIsBetter === false ? -u.score : u.score);
|
|
300
|
+
for (const s of raw) {
|
|
301
|
+
if (!Number.isFinite(s)) throw new RangeError("normalizeScores: every score must be a finite number");
|
|
302
|
+
}
|
|
303
|
+
const hi = Math.max(...raw);
|
|
304
|
+
const lo = Math.min(...raw);
|
|
305
|
+
let map;
|
|
306
|
+
if (method === "max") {
|
|
307
|
+
if (lo < 0) throw new RangeError("normalizeScores: method 'max' needs non-negative scores, use 'minmax'");
|
|
308
|
+
map = (s) => hi === 0 ? 1 : s / hi;
|
|
309
|
+
} else {
|
|
310
|
+
map = (s) => hi === lo ? 1 : (s - lo) / (hi - lo);
|
|
311
|
+
}
|
|
312
|
+
return units.map((u, i) => ({ ...u, score: map(raw[i]) }));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/langchain.ts
|
|
316
|
+
var DEFAULT_SCORE_KEYS = ["relevance_score", "relevanceScore", "score"];
|
|
317
|
+
function toDate(v) {
|
|
318
|
+
if (v instanceof Date) return v;
|
|
319
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
320
|
+
const d = new Date(v);
|
|
321
|
+
return Number.isNaN(d.getTime()) ? void 0 : d;
|
|
322
|
+
}
|
|
323
|
+
return void 0;
|
|
324
|
+
}
|
|
325
|
+
var CabarCompressor = class extends import_document_compressors.BaseDocumentCompressor {
|
|
326
|
+
opts;
|
|
327
|
+
constructor(opts = {}) {
|
|
328
|
+
super();
|
|
329
|
+
this.opts = opts;
|
|
330
|
+
}
|
|
331
|
+
async compressDocuments(documents, query) {
|
|
332
|
+
if (documents.length === 0) return [];
|
|
333
|
+
const { scoreKeys = DEFAULT_SCORE_KEYS, timestampKey = "timestamp", includeTail = true, ...selectOpts } = this.opts;
|
|
334
|
+
const scored = documents.map((doc) => {
|
|
335
|
+
const key = scoreKeys.find((k) => typeof doc.metadata?.[k] === "number" && Number.isFinite(doc.metadata[k]));
|
|
336
|
+
return key ? doc.metadata[key] : null;
|
|
337
|
+
});
|
|
338
|
+
const useRank = scored.some((s) => s === null);
|
|
339
|
+
const units = normalizeScores(
|
|
340
|
+
documents.map((doc, i) => ({
|
|
341
|
+
id: String(i),
|
|
342
|
+
content: doc.pageContent,
|
|
343
|
+
score: useRank ? documents.length - i : scored[i],
|
|
344
|
+
timestamp: toDate(doc.metadata?.[timestampKey])
|
|
345
|
+
}))
|
|
346
|
+
);
|
|
347
|
+
const result = CabarSelector.select(units, query, selectOpts);
|
|
348
|
+
const out = result.selected.map((u) => documents[Number(u.id)]);
|
|
349
|
+
if (includeTail && result.tailSummary) {
|
|
350
|
+
out.push(new import_documents.Document({ pageContent: result.tailSummary, metadata: { cabar: "tail", units: result.tail.map((u) => Number(u.id)) } }));
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
356
|
+
0 && (module.exports = {
|
|
357
|
+
CabarCompressor
|
|
358
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseDocumentCompressor } from '@langchain/core/retrievers/document_compressors';
|
|
2
|
+
import { DocumentInterface } from '@langchain/core/documents';
|
|
3
|
+
import { b as CabarSelectOptions } from './cabar-B-R-rHby.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LangChain.js adapter: CABAR as a document compressor.
|
|
7
|
+
*
|
|
8
|
+
* import { ContextualCompressionRetriever } from '@langchain/classic/retrievers/contextual_compression';
|
|
9
|
+
* import { CabarCompressor } from '@whyparabola/cabar/langchain';
|
|
10
|
+
*
|
|
11
|
+
* const retriever = new ContextualCompressionRetriever({
|
|
12
|
+
* baseCompressor: new CabarCompressor({ budgetTokens: 2000 }),
|
|
13
|
+
* baseRetriever: vectorStore.asRetriever(24),
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface CabarCompressorOptions extends CabarSelectOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Metadata keys holding a relevance score, tried in order. Default:
|
|
20
|
+
* `relevance_score` (rerankers), `relevanceScore`, `score`. Documents without
|
|
21
|
+
* any of them are scored by their position in the retriever's order.
|
|
22
|
+
*/
|
|
23
|
+
scoreKeys?: string[];
|
|
24
|
+
/** Metadata key holding a date (Date, ISO string or epoch ms) for recency. Default: `timestamp`. */
|
|
25
|
+
timestampKey?: string;
|
|
26
|
+
/** Append the tail summary as one extra Document (metadata `{ cabar: 'tail' }`). Default true. */
|
|
27
|
+
includeTail?: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare class CabarCompressor extends BaseDocumentCompressor {
|
|
30
|
+
private readonly opts;
|
|
31
|
+
constructor(opts?: CabarCompressorOptions);
|
|
32
|
+
compressDocuments(documents: DocumentInterface[], query: string): Promise<DocumentInterface[]>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { CabarCompressor, type CabarCompressorOptions };
|