{"version":3,"file":"embeddings-DvRKcQyt.mjs","names":[],"sources":["../src/memory/embeddings.ts"],"sourcesContent":["/**\n * Embedding generation for the PAI federation memory engine (Phase 2.5).\n *\n * Uses @huggingface/transformers with the Snowflake/snowflake-arctic-embed-m-v1.5 model\n * (768 dims, q8 quantization, MTEB strong retrieval quality).\n *\n * The model uses CLS pooling (first token) — NOT mean pooling.\n * For retrieval, queries require a prefix: \"Represent this sentence for searching relevant passages: \"\n * Documents should be embedded WITHOUT a prefix.\n *\n * The pipeline is a lazy singleton — loaded on first call, reused thereafter.\n * This avoids loading the heavy ML model on every CLI invocation.\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nexport const EMBEDDING_DIM = 768;\nconst DEFAULT_EMBEDDING_MODEL = \"Snowflake/snowflake-arctic-embed-m-v1.5\";\n\n/** Query prefix required by Snowflake Arctic Embed for retrieval tasks. */\nconst QUERY_PREFIX = \"Represent this sentence for searching relevant passages: \";\n\n// ---------------------------------------------------------------------------\n// Lazy pipeline singleton\n// ---------------------------------------------------------------------------\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _embeddingPipeline: any = null;\nlet _currentModel: string | null = null;\n\n/**\n * Configure the embedding model to use.\n * Must be called before the first generateEmbedding() call.\n * If the pipeline is already loaded with a different model, it will be reloaded.\n *\n * @param model  HuggingFace model ID (e.g. \"Snowflake/snowflake-arctic-embed-m-v1.5\").\n *               Pass undefined or empty string to use the default model.\n */\nexport function configureEmbeddingModel(model?: string): void {\n  const resolved = model?.trim() || DEFAULT_EMBEDDING_MODEL;\n  if (_currentModel !== null && _currentModel !== resolved) {\n    // Model changed — force reload on next call\n    _embeddingPipeline = null;\n  }\n  _currentModel = resolved;\n}\n\nasync function getEmbedder() {\n  const model = _currentModel ?? DEFAULT_EMBEDDING_MODEL;\n  if (!_embeddingPipeline) {\n    // Dynamic import to avoid loading the ML runtime on startup\n    const { pipeline } = await import(\"@huggingface/transformers\");\n    _embeddingPipeline = await pipeline(\n      \"feature-extraction\",\n      model,\n      { dtype: \"q8\" },\n    );\n  }\n  return _embeddingPipeline;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a normalized 768-dim embedding for the given text.\n *\n * Uses CLS pooling (first token) and L2 normalization (cosine similarity ready).\n *\n * @param text     The text to embed.\n * @param isQuery  If true, prepend the Snowflake query prefix. Use for search queries.\n *                 Documents should be embedded without the prefix (default: false).\n */\nexport async function generateEmbedding(text: string, isQuery: boolean = false): Promise<Float32Array> {\n  const prefix = isQuery ? QUERY_PREFIX : \"\";\n  const input = prefix + text;\n  const extractor = await getEmbedder();\n  // Snowflake Arctic Embed uses CLS pooling (first token), not mean pooling\n  const output = await extractor(input, { pooling: \"cls\", normalize: true });\n  return new Float32Array(output.data);\n}\n\n/**\n * Generate normalized embeddings for several documents in one forward pass.\n *\n * The model call dominates embedding cost, and calling it once per chunk leaves\n * most of the available throughput unused: measured on this machine, one-at-a-\n * time runs at ~5 chunks/s, which turns a six-figure backlog into days of\n * work. Batching amortises the per-call overhead across the batch.\n *\n * The pipeline returns one flat buffer for the whole batch, so it is sliced\n * back apart by row. Order is preserved: result[i] corresponds to texts[i].\n *\n * Documents only — queries take the prefixed single-text path, since a search\n * embeds exactly one string and gains nothing here.\n */\nexport async function generateEmbeddings(texts: string[]): Promise<Float32Array[]> {\n  if (texts.length === 0) return [];\n  if (texts.length === 1) return [await generateEmbedding(texts[0])];\n\n  const extractor = await getEmbedder();\n  const output = await extractor(texts, { pooling: \"cls\", normalize: true });\n  const flat = output.data as Float32Array;\n\n  const dim = flat.length / texts.length;\n  if (!Number.isInteger(dim)) {\n    throw new Error(\n      `Batched embedding returned ${flat.length} values for ${texts.length} inputs — not divisible`\n    );\n  }\n\n  const out: Float32Array[] = [];\n  for (let i = 0; i < texts.length; i++) {\n    // Copy rather than subarray: the caller stores these, and a view would\n    // pin the whole batch buffer in memory for the lifetime of one vector.\n    out.push(new Float32Array(flat.slice(i * dim, (i + 1) * dim)));\n  }\n  return out;\n}\n\n// ---------------------------------------------------------------------------\n// Serialization helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a Float32Array to a Buffer for storage in a SQLite BLOB column.\n */\nexport function serializeEmbedding(vec: Float32Array): Buffer {\n  return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);\n}\n\n/**\n * Deserialize a Buffer (from a SQLite BLOB column) back into a Float32Array.\n */\nexport function deserializeEmbedding(blob: Buffer): Float32Array {\n  return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);\n}\n\n// ---------------------------------------------------------------------------\n// Similarity computation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute cosine similarity between two normalized embedding vectors.\n *\n * Since both vectors are already L2-normalized by the embedding model,\n * cosine similarity reduces to a dot product — but we compute the full\n * formula for correctness when embeddings may not be pre-normalized.\n *\n * Returns a value in [-1, 1] where 1 = identical.\n */\nexport function cosineSimilarity(a: Float32Array, b: Float32Array): number {\n  let dot = 0;\n  let normA = 0;\n  let normB = 0;\n  for (let i = 0; i < a.length; i++) {\n    dot += a[i] * b[i];\n    normA += a[i] * a[i];\n    normB += b[i] * b[i];\n  }\n  const denom = Math.sqrt(normA) * Math.sqrt(normB);\n  if (denom === 0) return 0;\n  return dot / denom;\n}\n"],"mappings":";AAmBA,MAAM,0BAA0B;;AAGhC,MAAM,eAAe;AAOrB,IAAI,qBAA0B;AAC9B,IAAI,gBAA+B;;;;;;;;;AAUnC,SAAgB,wBAAwB,OAAsB;CAC5D,MAAM,WAAW,OAAO,MAAM,IAAI;AAClC,KAAI,kBAAkB,QAAQ,kBAAkB,SAE9C,sBAAqB;AAEvB,iBAAgB;;AAGlB,eAAe,cAAc;CAC3B,MAAM,QAAQ,iBAAiB;AAC/B,KAAI,CAAC,oBAAoB;EAEvB,MAAM,EAAE,aAAa,MAAM,OAAO;AAClC,uBAAqB,MAAM,SACzB,sBACA,OACA,EAAE,OAAO,MAAM,CAChB;;AAEH,QAAO;;;;;;;;;;;AAgBT,eAAsB,kBAAkB,MAAc,UAAmB,OAA8B;CAErG,MAAM,SADS,UAAU,eAAe,MACjB;CAGvB,MAAM,SAAS,OAFG,MAAM,aAAa,EAEN,OAAO;EAAE,SAAS;EAAO,WAAW;EAAM,CAAC;AAC1E,QAAO,IAAI,aAAa,OAAO,KAAK;;;;;;;;;;;;;;;;AAiBtC,eAAsB,mBAAmB,OAA0C;AACjF,KAAI,MAAM,WAAW,EAAG,QAAO,EAAE;AACjC,KAAI,MAAM,WAAW,EAAG,QAAO,CAAC,MAAM,kBAAkB,MAAM,GAAG,CAAC;CAIlE,MAAM,QADS,OADG,MAAM,aAAa,EACN,OAAO;EAAE,SAAS;EAAO,WAAW;EAAM,CAAC,EACtD;CAEpB,MAAM,MAAM,KAAK,SAAS,MAAM;AAChC,KAAI,CAAC,OAAO,UAAU,IAAI,CACxB,OAAM,IAAI,MACR,8BAA8B,KAAK,OAAO,cAAc,MAAM,OAAO,yBACtE;CAGH,MAAM,MAAsB,EAAE;AAC9B,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAGhC,KAAI,KAAK,IAAI,aAAa,KAAK,MAAM,IAAI,MAAM,IAAI,KAAK,IAAI,CAAC,CAAC;AAEhE,QAAO;;;;;AAUT,SAAgB,mBAAmB,KAA2B;AAC5D,QAAO,OAAO,KAAK,IAAI,QAAQ,IAAI,YAAY,IAAI,WAAW;;;;;AAMhE,SAAgB,qBAAqB,MAA4B;AAC/D,QAAO,IAAI,aAAa,KAAK,QAAQ,KAAK,YAAY,KAAK,aAAa,EAAE;;;;;;;;;;;AAgB5E,SAAgB,iBAAiB,GAAiB,GAAyB;CACzE,IAAI,MAAM;CACV,IAAI,QAAQ;CACZ,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAO,EAAE,KAAK,EAAE;AAChB,WAAS,EAAE,KAAK,EAAE;AAClB,WAAS,EAAE,KAAK,EAAE;;CAEpB,MAAM,QAAQ,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK,MAAM;AACjD,KAAI,UAAU,EAAG,QAAO;AACxB,QAAO,MAAM"}