feat: add local vector recall fallback
This commit is contained in:
@@ -49,6 +49,22 @@ function memoryText(collision: Collision): string {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cosine(a: number[], b: number[]): number {
|
||||||
|
const n = Math.min(a.length, b.length);
|
||||||
|
let dot = 0;
|
||||||
|
let aNorm = 0;
|
||||||
|
let bNorm = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const av = a[i] ?? 0;
|
||||||
|
const bv = b[i] ?? 0;
|
||||||
|
dot += av * bv;
|
||||||
|
aNorm += av * av;
|
||||||
|
bNorm += bv * bv;
|
||||||
|
}
|
||||||
|
if (!aNorm || !bNorm) return -1;
|
||||||
|
return dot / (Math.sqrt(aNorm) * Math.sqrt(bNorm));
|
||||||
|
}
|
||||||
|
|
||||||
async function embed(text: string, inputType: 'document' | 'query'): Promise<number[] | null> {
|
async function embed(text: string, inputType: 'document' | 'query'): Promise<number[] | null> {
|
||||||
return (await embedWithVoyage(text, inputType)) ?? embedWithGemini(text, inputType);
|
return (await embedWithVoyage(text, inputType)) ?? embedWithGemini(text, inputType);
|
||||||
}
|
}
|
||||||
@@ -159,8 +175,8 @@ async function recallByVector(collision: Collision): Promise<RecalledCollision |
|
|||||||
const queryVector = await embed(memoryText(collision), 'query');
|
const queryVector = await embed(memoryText(collision), 'query');
|
||||||
if (!queryVector) return null;
|
if (!queryVector) return null;
|
||||||
|
|
||||||
try {
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
try {
|
||||||
const [match] = await db
|
const [match] = await db
|
||||||
.collection<StoredCollision>('collisions')
|
.collection<StoredCollision>('collisions')
|
||||||
.aggregate<StoredCollision>([
|
.aggregate<StoredCollision>([
|
||||||
@@ -180,9 +196,27 @@ async function recallByVector(collision: Collision): Promise<RecalledCollision |
|
|||||||
.toArray();
|
.toArray();
|
||||||
return match ? attachOutcome(match) : null;
|
return match ? attachOutcome(match) : null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[memory] vector recall unavailable: ${(err as Error).message}`);
|
console.warn(`[memory] atlas vector recall unavailable: ${(err as Error).message}`);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const candidates = await db
|
||||||
|
.collection<StoredCollision>('collisions')
|
||||||
|
.find(
|
||||||
|
{
|
||||||
|
podId: collision.podId,
|
||||||
|
id: { $ne: collision.id },
|
||||||
|
embedding: { $exists: true },
|
||||||
|
},
|
||||||
|
{ projection: { _id: 0 }, limit: 100 },
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
let best: { match: StoredCollision; score: number } | null = null;
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate.embedding?.length) continue;
|
||||||
|
const score = cosine(queryVector, candidate.embedding);
|
||||||
|
if (!best || score > best.score) best = { match: candidate, score };
|
||||||
|
}
|
||||||
|
return best && best.score > 0.5 ? attachOutcome(best.match) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recallBySignature(collision: Collision): Promise<RecalledCollision | null> {
|
async function recallBySignature(collision: Collision): Promise<RecalledCollision | null> {
|
||||||
@@ -212,9 +246,10 @@ async function recallBySignature(collision: Collision): Promise<RecalledCollisio
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recall prior collision patterns. Exact Mongo recall is the MVP path;
|
* Recall prior collision patterns. Atlas Vector Search is preferred when
|
||||||
* vector search is an optional broader fallback when Atlas is configured.
|
* available; standalone MongoDB falls back to app-side cosine search over
|
||||||
|
* stored embeddings, then exact signature/file matching.
|
||||||
*/
|
*/
|
||||||
export async function recallSimilar(collision: Collision): Promise<RecalledCollision | null> {
|
export async function recallSimilar(collision: Collision): Promise<RecalledCollision | null> {
|
||||||
return (await recallBySignature(collision)) ?? recallByVector(collision);
|
return (await recallByVector(collision)) ?? recallBySignature(collision);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,13 @@ async function verifyMemoryRecall() {
|
|||||||
}
|
}
|
||||||
const recalled = await recallSimilar({ ...seed, id: `${seed.id}_query` });
|
const recalled = await recallSimilar({ ...seed, id: `${seed.id}_query` });
|
||||||
if (!recalled) fail('memory recall did not find seeded collision');
|
if (!recalled) fail('memory recall did not find seeded collision');
|
||||||
|
const vectorRecalled = await recallSimilar({
|
||||||
|
...seed,
|
||||||
|
id: `${seed.id}_vector_query`,
|
||||||
|
file: 'src/nearby-memory.ts',
|
||||||
|
symbol: 'nearbyMemory',
|
||||||
|
});
|
||||||
|
if (!vectorRecalled) fail('vector memory recall did not find semantically similar collision');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyGraph() {
|
async function verifyGraph() {
|
||||||
|
|||||||
Reference in New Issue
Block a user