feat: Memory Echo chunk-level — détecte connexions au niveau section
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled

Quand la similarité whole-note ne passe pas le seuil, vérifie les chunks.
Si une section spécifique de la note A résonne avec une section de la note B,
la connexion est créée avec le snippet précis qui a matché.

SQL: cross-join LATERAL sur NoteEmbeddingChunk avec pgvector <=>.
Fallback gracieux si la table chunks est vide ou erreur.
This commit is contained in:
Antigravity
2026-06-20 17:09:34 +00:00
parent 52c4cb1dee
commit eab4b3e27b
2 changed files with 115 additions and 73 deletions

View File

@@ -259,15 +259,43 @@ export class MemoryEchoService {
continue
}
// Calculate cosine similarity
// Calculate cosine similarity — whole note level
const similarity = cosineSimilarity(note1.embedding!, note2.embedding!)
// Similarity threshold for meaningful connections (adjusted by feedback)
const baseThreshold = this.pairSimilarityThreshold(note1, note2, demoMode)
const adjustedThreshold = baseThreshold
+ (notePenalty.get(note1.id) || 0)
+ (notePenalty.get(note2.id) || 0)
if (similarity >= adjustedThreshold) {
// Also check chunk-level similarity for more precise connections
// This catches cases where two notes share a similar SECTION even if overall different
let bestChunkSimilarity = similarity
let chunkSnippet: string | undefined
if (similarity < adjustedThreshold) {
// Only check chunks if whole-note similarity didn't pass — saves DB queries
try {
const chunkRows: Array<{ content: string; embedding: string }> = await prisma.$queryRawUnsafe(
`SELECT a.content AS content,
1 - (a."embedding"::vector <=> b."embedding"::vector) AS chunk_sim
FROM "NoteEmbeddingChunk" a
CROSS JOIN LATERAL (
SELECT "embedding"
FROM "NoteEmbeddingChunk"
WHERE "noteId" = $2 AND "embedding" IS NOT NULL
ORDER BY "embedding"::vector <=> a."embedding"::vector ASC
LIMIT 1
) b
WHERE a."noteId" = $1 AND a."embedding" IS NOT NULL
ORDER BY chunk_sim DESC
LIMIT 1`,
note1.id, note2.id
)
if (chunkRows.length > 0 && chunkRows[0]) {
const row = chunkRows[0] as any
if (row.chunk_sim && row.chunk_sim > bestChunkSimilarity) {
bestChunkSimilarity = row.chunk_sim
chunkSnippet = row.content?.slice(0, 200)
}
}
} catch {}
}
if (bestChunkSimilarity >= adjustedThreshold) {
connections.push({
note1: {
id: note1.id,
@@ -281,9 +309,10 @@ export class MemoryEchoService {
content: this.connectionPlainText(note2.title, note2.content || ''),
createdAt: note2.createdAt
},
similarityScore: similarity,
similarityScore: bestChunkSimilarity,
insight: '', // Will be generated by AI
daysApart
daysApart,
...(chunkSnippet ? { contextSnippet: chunkSnippet } : {})
})
}
}