@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
131 lines • 5.24 kB
JavaScript
import { matchesFilter } from './filter.js';
import { applyMetadataPatch, assertRemovalFilter, documentIdOf, effectivePatchKeys, filterDeniesAll, } from './vector-store.js';
/**
* An in-process {@link VectorStore} — cosine similarity over a Map, no infra. The reference adapter for
* tests and small/embedded corpora; a pgvector/Lucid-backed store for production scale is deferred.
*/
export class MemoryVectorStore {
records = new Map();
async upsert(records) {
for (const record of records) {
this.records.set(record.id, record);
}
}
async remove(documentId) {
for (const id of this.records.keys()) {
if (documentIdOf(id) === documentId) {
this.records.delete(id);
}
}
}
/**
* {@link VectorStore.updateMetadata} — rewrite every chunk's metadata for one document, touching
* neither `text` nor `embedding`. Both are carried across by the spread, so the stored `embedding`
* stays the **same array instance** it was upserted with: nothing here can perturb a vector even in
* principle. Merge semantics come from the shared {@link applyMetadataPatch}, so this adapter cannot
* drift from the other two.
*/
async updateMetadata(documentId, patch) {
if (effectivePatchKeys(patch).length === 0) {
return 0;
}
let written = 0;
for (const [id, record] of this.records) {
if (documentIdOf(id) !== documentId) {
continue;
}
this.records.set(id, { ...record, metadata: applyMetadataPatch(record.metadata, patch) });
written += 1;
}
return written;
}
async listDocuments(filter) {
const documents = new Map();
for (const record of this.records.values()) {
if (filter !== undefined && !matchesFilter(record.metadata, filter)) {
continue;
}
const id = documentIdOf(record.id);
if (!documents.has(id)) {
documents.set(id, {
id,
...(record.metadata !== undefined ? { metadata: record.metadata } : {}),
});
}
}
return [...documents.values()];
}
/**
* {@link VectorStore.listDocumentIds} — the ids `listDocuments` would report, without building the
* per-document metadata entries. Filtering goes through the same {@link matchesFilter} `search` uses.
*/
async listDocumentIds(filter) {
const ids = new Set();
for (const record of this.records.values()) {
if (filter !== undefined && !matchesFilter(record.metadata, filter)) {
continue;
}
ids.add(documentIdOf(record.id));
}
return [...ids];
}
/**
* {@link VectorStore.removeWhere} — delete every chunk the same filter would let `search` reach.
* Parity is by construction: the predicate below is the identical {@link matchesFilter} call, on the
* identical argument, as the one in {@link MemoryVectorStore.search}.
*/
async removeWhere(filter) {
assertRemovalFilter(filter);
if (filterDeniesAll(filter)) {
return 0;
}
let removed = 0;
for (const [id, record] of [...this.records]) {
if (!matchesFilter(record.metadata, filter)) {
continue;
}
this.records.delete(id);
removed += 1;
}
return removed;
}
async search(embedding, options) {
const scored = [];
for (const record of this.records.values()) {
if (options.filter !== undefined && !matchesFilter(record.metadata, options.filter)) {
continue;
}
const score = cosineSimilarity(embedding, record.embedding);
// Relevance floor: drop below-threshold passages before the top-K cut, so `minScore` never
// leaves the returned K padded with weakly-related sources.
if (options.minScore !== undefined && score < options.minScore) {
continue;
}
scored.push({
id: record.id,
text: record.text,
score,
...(record.source !== undefined ? { source: record.source } : {}),
...(record.metadata !== undefined ? { metadata: record.metadata } : {}),
});
}
return scored.sort((a, b) => b.score - a.score).slice(0, options.topK);
}
}
/** Cosine similarity of two vectors; `0` when either has zero magnitude. Pure. */
export function cosineSimilarity(a, b) {
let dot = 0;
let normA = 0;
let normB = 0;
const length = Math.min(a.length, b.length);
for (let index = 0; index < length; index += 1) {
const valueA = a[index] ?? 0;
const valueB = b[index] ?? 0;
dot += valueA * valueB;
normA += valueA * valueA;
normB += valueB * valueB;
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dot / denominator;
}
//# sourceMappingURL=memory-vector-store.js.map