export class OllamaError extends Error { constructor(status, body) { super(`ollama ${status}: ${body}`); this.status = status; } } export async function embedText(text, { model = 'nomic-embed-text', timeoutMs = 60_000 } = {}) { const url = (process.env.OLLAMA_URL || 'http://192.168.1.185:11434') + '/api/embeddings'; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model, prompt: text }), signal: AbortSignal.timeout(timeoutMs) }); if (!res.ok) throw new OllamaError(res.status, await res.text()); const j = await res.json(); return j.embedding; } export function padTo(vector, dim) { if (vector.length === dim) return vector; if (vector.length > dim) return vector.slice(0, dim); const out = vector.slice(); while (out.length < dim) out.push(0); return out; } // Split text into chunks of at most `size` chars, breaking on line boundaries // where possible (never mid-word-loss): accumulate lines until adding the next // would exceed `size`. A single over-long line is hard-split. Returns [] for empty. export function chunkText(text, size = 1500) { const s = (text || '').trim(); if (!s) return []; const chunks = []; let cur = ''; for (const line of s.split('\n')) { if (line.length > size) { if (cur) { chunks.push(cur); cur = ''; } for (let i = 0; i < line.length; i += size) chunks.push(line.slice(i, i + size)); continue; } if (cur.length + line.length + 1 > size) { if (cur) chunks.push(cur); cur = line; } else { cur = cur ? cur + '\n' + line : line; } } if (cur) chunks.push(cur); return chunks; } // Embed possibly-long text by chunking, embedding each chunk, and mean-pooling // the resulting vectors element-wise. Returns a single embedding vector. // 1 chunk => identical to embedText. Caps the number of chunks to bound cost. export async function embedTextPooled(text, { model = 'nomic-embed-text', timeoutMs = 60_000, maxChunks = 64, chunkSize = 1500 } = {}) { let chunks = chunkText(text, chunkSize); if (chunks.length === 0) chunks = ['']; if (chunks.length > maxChunks) chunks = chunks.slice(0, maxChunks); const vecs = []; for (const c of chunks) vecs.push(await embedText(c, { model, timeoutMs })); const dim = vecs[0].length; const pooled = new Array(dim).fill(0); for (const v of vecs) for (let i = 0; i < dim; i++) pooled[i] += (v[i] || 0); for (let i = 0; i < dim; i++) pooled[i] /= vecs.length; return pooled; }