function normalizeUrl(value, envName = 'MEMORY_WEAVIATE_URL') { const raw = String(value ?? '').trim(); if (!raw) return null; try { const url = new URL(raw); if (!['http:', 'https:'].includes(url.protocol)) { throw new Error(`Unsupported protocol: ${url.protocol}`); } return url.toString().replace(/\/$/, ''); } catch (err) { throw new Error(`Invalid ${envName}: ${err instanceof Error ? err.message : err}`); } } function normalizeName(value, fallback, label) { const raw = String(value ?? fallback).trim(); if (!/^[a-zA-Z0-9_-]+$/.test(raw)) throw new Error(`Invalid ${label}: ${raw}`); return raw; } function normalizeMemory(item) { const text = String(item?.text ?? item?.content ?? item?.memoryText ?? '').trim(); if (!text) return null; return { id: item?.id == null ? null : String(item.id), label: item?.label ?? item?.type ?? 'semantic', text, score: item?.score == null ? null : Number(item.score), createdAt: item?.createdAt ?? item?.created_at ?? null, }; } function normalizeVector(value) { if (!Array.isArray(value)) return null; const vector = value.map((item) => Number(item)); return vector.length && vector.every(Number.isFinite) ? vector : null; } function timeoutSignal(timeoutMs) { if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return null; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); return { signal: controller.signal, clear() { clearTimeout(timeout); }, }; } export function createWeaviateHttpClient({ url, apiKey = null, fetchImpl = globalThis.fetch, timeoutMs = 3000, } = {}) { const resolvedUrl = normalizeUrl(url); if (!resolvedUrl) throw new Error('createWeaviateHttpClient requires MEMORY_WEAVIATE_URL'); if (typeof fetchImpl !== 'function') throw new Error('createWeaviateHttpClient requires fetch'); return { async search({ collection, vector, userId, limit = 8 } = {}) { const resolvedCollection = normalizeName(collection, 'MemindMemory', 'MEMORY_WEAVIATE_COLLECTION'); const resolvedVector = normalizeVector(vector); if (!resolvedVector) return []; const resolvedLimit = Math.max(1, Math.min(50, Number(limit) || 8)); const whereType = `GetObjects${resolvedCollection}WhereInpObj`; const query = ` query MemoryV2Search($vector: [Float!]!, $where: ${whereType}) { Get { ${resolvedCollection}(nearVector: { vector: $vector }, limit: ${resolvedLimit}, where: $where) { content text type label created_at _additional { id certainty distance } } } } `; const variables = { vector: resolvedVector, where: userId ? { path: ['user_id'], operator: 'Equal', valueText: String(userId), } : null, }; const timeout = timeoutSignal(Number(timeoutMs)); try { const response = await fetchImpl(`${resolvedUrl}/v1/graphql`, { method: 'POST', headers: { 'content-type': 'application/json', ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}), }, body: JSON.stringify({ query, variables }), ...(timeout?.signal ? { signal: timeout.signal } : {}), }); if (!response?.ok) { throw new Error(`Weaviate request failed: ${response?.status ?? 'unknown'}`); } const data = await response.json(); if (Array.isArray(data?.errors) && data.errors.length) { throw new Error(`Weaviate GraphQL error: ${data.errors[0]?.message ?? 'unknown'}`); } return Array.isArray(data?.data?.Get?.[resolvedCollection]) ? data.data.Get[resolvedCollection].map((item) => ({ id: item?._additional?.id, label: item?.label ?? item?.type, text: item?.text ?? item?.content, score: item?._additional?.certainty ?? ( item?._additional?.distance == null ? null : 1 - Number(item._additional.distance) ), createdAt: item?.createdAt ?? item?.created_at, })) : []; } finally { timeout?.clear(); } }, }; } export function createWeaviateMemoryBackend({ enabled = false, url = null, collection = 'MemindMemory', client = null, embedQuery = null, defaultLimit = 8, unavailableReason = null, } = {}) { const resolvedUrl = normalizeUrl(url); const resolvedCollection = normalizeName(collection, 'MemindMemory', 'MEMORY_WEAVIATE_COLLECTION'); const configured = Boolean(enabled && resolvedUrl); const hasClient = Boolean(client?.search); const hasEmbedding = typeof embedQuery === 'function'; const wired = Boolean(configured && hasClient && hasEmbedding); const reason = unavailableReason ?? (enabled ? (!resolvedUrl ? 'url_not_configured' : (!hasClient ? 'client_not_configured' : 'embedding_module_not_configured')) : 'not_configured'); async function resolveVector(input) { const explicit = normalizeVector(input?.embedding); if (explicit) return explicit; if (typeof embedQuery !== 'function' || !input?.query) return null; return normalizeVector(await embedQuery(input.query, input)); } return { name: 'weaviate', category: 'semantic', role: 'knowledge-graph-fusion', flag: 'MEMORY_WEAVIATE_ENABLED', unavailableReason: reason, url: resolvedUrl, collection: resolvedCollection, isAvailable() { return Boolean(wired); }, getUnavailableReason() { return this.isAvailable() ? null : reason; }, async resolve(input = {}) { if (!this.isAvailable()) { return { memories: [], semanticMemories: [], behaviorSummary: null, activeGoals: [] }; } const vector = await resolveVector(input); if (!vector) { return { memories: [], semanticMemories: [], behaviorSummary: null, activeGoals: [] }; } const rows = await client.search({ url: resolvedUrl, collection: resolvedCollection, vector, userId: input.userId, limit: input.limit ?? defaultLimit, }); const memories = (Array.isArray(rows) ? rows : []).map(normalizeMemory).filter(Boolean); return { memories, semanticMemories: memories.map((memory) => memory.text), behaviorSummary: null, activeGoals: [], }; }, }; }