VoidBot/packages/rag/src/affinity-projection.ts
2026-07-12 02:21:43 +01:00

73 lines
2.2 KiB
TypeScript

import { createHash } from "node:crypto";
import { cosineSimilarity, type TextEmbedder } from "./hash-embedder";
export interface AffinityCard {
id: string;
text: string;
}
export interface EmbeddedAffinityCard extends AffinityCard {
contentHash: string;
embedderId: string;
embedding: number[];
}
export interface AffinityMessage {
id: string;
text: string;
observedAt: string;
}
export interface AffinityScore {
cardId: string;
messageId: string;
observedAt: string;
similarity: number;
}
export async function embedAffinityCards(
embedder: TextEmbedder,
cards: AffinityCard[],
): Promise<EmbeddedAffinityCard[]> {
const usable = cards.filter((card) => card.id.trim() && card.text.trim());
const embeddings = await embedder.embedDocuments(usable.map((card) => card.text));
if (embeddings.length !== usable.length) {
throw new Error(`Affinity embedder returned ${embeddings.length} card vectors for ${usable.length} cards.`);
}
return usable.map((card, index) => ({
...card,
contentHash: createHash("sha256").update(card.text).digest("hex"),
embedderId: embedder.id,
embedding: embeddings[index] ?? [],
}));
}
export async function scoreAffinityMessages(
embedder: TextEmbedder,
cards: EmbeddedAffinityCard[],
messages: AffinityMessage[],
): Promise<AffinityScore[]> {
const mismatched = cards.find((card) => card.embedderId !== embedder.id);
if (mismatched) {
throw new Error(`Affinity card ${mismatched.id} uses ${mismatched.embedderId}, not ${embedder.id}.`);
}
const usable = messages.filter((message) => message.id.trim() && message.text.trim());
const embeddings = await embedder.embedDocuments(usable.map((message) => message.text));
if (embeddings.length !== usable.length) {
throw new Error(`Affinity embedder returned ${embeddings.length} message vectors for ${usable.length} messages.`);
}
return cards.flatMap((card) => usable.map((message, index) => ({
cardId: card.id,
messageId: message.id,
observedAt: message.observedAt,
similarity: clampSimilarity(cosineSimilarity(card.embedding, embeddings[index] ?? [])),
})));
}
function clampSimilarity(value: number): number {
return Number.isFinite(value) ? Math.max(-1, Math.min(1, value)) : 0;
}