@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
67 lines • 3.02 kB
JavaScript
/**
* Fuses several retrievers with Reciprocal Rank Fusion — the robust way to combine, say, dense vector
* search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the lists it
* appears in, so it needs no score normalization between incompatible scales (cosine vs BM25).
* Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce. Itself a
* {@link Retriever}. Mirrors the reference `HybridRetriever` exactly.
*/
export class HybridRetriever {
retrievers;
options;
constructor(retrievers, options = {}) {
this.retrievers = retrievers;
this.options = options;
}
async retrieve(query, options = {}) {
return (await this.retrieveWithUsage(query, options)).passages;
}
/**
* {@link HybridRetriever.retrieve} plus the embedding usage of every leg that reports one (summed), so
* a hybrid over an embedding leg still reaches the ledger and the quota.
*/
async retrieveWithUsage(query, options = {}) {
const k = this.options.k ?? 60;
const fetchTopK = this.options.fetchTopK ?? 20;
const weights = this.options.weights;
const legOptions = {
topK: fetchTopK,
...(options.filter !== undefined ? { filter: options.filter } : {}),
};
const results = await Promise.all(this.retrievers.map(async (retriever) => typeof retriever.retrieveWithUsage === 'function'
? retriever.retrieveWithUsage(query, legOptions)
: { passages: await retriever.retrieve(query, legOptions) }));
const lists = results.map((result) => result.passages);
const usage = sumUsage(results.map((result) => result.usage));
const fused = new Map();
lists.forEach((list, listIndex) => {
const weight = weights?.[listIndex] ?? 1;
list.forEach((passage, rank) => {
const contribution = weight / (k + rank + 1);
const existing = fused.get(passage.id);
if (existing !== undefined) {
existing.score += contribution;
}
else {
fused.set(passage.id, { passage, score: contribution });
}
});
});
const passages = [...fused.values()]
.sort((a, b) => b.score - a.score)
.slice(0, options.topK ?? 5)
.map((entry) => ({ ...entry.passage, score: entry.score }));
return { passages, ...(usage !== undefined ? { usage } : {}) };
}
}
function sumUsage(usages) {
const reported = usages.filter((usage) => usage !== undefined);
if (reported.length === 0) {
return undefined;
}
const modelId = reported.find((usage) => usage.modelId !== undefined)?.modelId;
return {
inputTokens: reported.reduce((total, usage) => total + usage.inputTokens, 0),
...(modelId !== undefined ? { modelId } : {}),
};
}
//# sourceMappingURL=hybrid-retriever.js.map