Merge Memory V2 runtime facade
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
const DEFAULT_TABLE = 'memory_embeddings';
|
||||
const DEFAULT_LIMIT = 8;
|
||||
|
||||
function isSafeIdentifier(value) {
|
||||
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(String(value ?? ''));
|
||||
}
|
||||
|
||||
function resolveTableName(tableName) {
|
||||
const normalized = String(tableName ?? DEFAULT_TABLE).trim();
|
||||
if (!isSafeIdentifier(normalized)) {
|
||||
throw new Error(`Invalid pgvector table name: ${normalized}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeEmbedding(value) {
|
||||
if (!Array.isArray(value)) return null;
|
||||
const numbers = value.map((item) => Number(item));
|
||||
if (!numbers.length || numbers.some((item) => !Number.isFinite(item))) return null;
|
||||
return numbers;
|
||||
}
|
||||
|
||||
function vectorLiteral(embedding) {
|
||||
return `[${embedding.join(',')}]`;
|
||||
}
|
||||
|
||||
function normalizeRow(row) {
|
||||
const text = String(row?.content ?? row?.memory_text ?? row?.text ?? '').trim();
|
||||
if (!text) return null;
|
||||
return {
|
||||
id: row?.id == null ? null : String(row.id),
|
||||
label: row?.type ?? row?.label ?? 'semantic',
|
||||
text,
|
||||
score: row?.score == null ? null : Number(row.score),
|
||||
createdAt: row?.created_at ?? row?.createdAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createPgvectorMemoryBackend({
|
||||
pool = null,
|
||||
enabled = false,
|
||||
tableName = DEFAULT_TABLE,
|
||||
embedQuery = null,
|
||||
defaultLimit = DEFAULT_LIMIT,
|
||||
unavailableReason = 'not_configured',
|
||||
} = {}) {
|
||||
const resolvedTableName = resolveTableName(tableName);
|
||||
|
||||
async function resolveEmbedding(input) {
|
||||
const explicit = normalizeEmbedding(input?.embedding);
|
||||
if (explicit) return explicit;
|
||||
if (typeof embedQuery !== 'function' || !input?.query) return null;
|
||||
return normalizeEmbedding(await embedQuery(input.query, input));
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'pgvector',
|
||||
category: 'semantic',
|
||||
role: 'primary-vector-store',
|
||||
flag: 'MEMORY_VECTOR_ENABLED',
|
||||
unavailableReason,
|
||||
|
||||
isAvailable() {
|
||||
return Boolean(enabled && pool?.query);
|
||||
},
|
||||
|
||||
getUnavailableReason() {
|
||||
return this.isAvailable() ? null : unavailableReason;
|
||||
},
|
||||
|
||||
async resolve(input = {}) {
|
||||
if (!this.isAvailable()) return { memories: [], semanticMemories: [] };
|
||||
const userId = String(input.userId ?? '').trim();
|
||||
if (!userId) return { memories: [], semanticMemories: [] };
|
||||
const embedding = await resolveEmbedding(input);
|
||||
if (!embedding) return { memories: [], semanticMemories: [] };
|
||||
const limit = Math.max(1, Math.min(50, Number(input.limit ?? defaultLimit) || defaultLimit));
|
||||
const sql = `
|
||||
SELECT id, content, type, created_at, 1 - (embedding <=> $2::vector) AS score
|
||||
FROM ${resolvedTableName}
|
||||
WHERE user_id = $1
|
||||
ORDER BY embedding <=> $2::vector
|
||||
LIMIT $3
|
||||
`;
|
||||
const result = await pool.query(sql, [userId, vectorLiteral(embedding), limit]);
|
||||
const memories = (result?.rows ?? []).map((row) => normalizeRow(row)).filter(Boolean);
|
||||
return {
|
||||
semanticMemories: memories.map((item) => item.text),
|
||||
memories,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user