Files
memind/memory-v2-weaviate.mjs
T
2026-07-03 09:46:03 +08:00

201 lines
6.5 KiB
JavaScript

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: [],
};
},
};
}