fix(ts): entity store isolation, backward compat, pgvector + redis init fixes (#4841)
This commit is contained in:
@@ -308,7 +308,7 @@ export default class MemoryClient {
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return Array.isArray(response) ? response : (response?.results ?? response);
|
||||
return response;
|
||||
}
|
||||
|
||||
async search(
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
ENTITY_BOOST_WEIGHT,
|
||||
ScoredResult,
|
||||
} from "../utils/scoring";
|
||||
import { getDefaultVectorStoreDbPath } from "../utils/sqlite";
|
||||
|
||||
// Entity params that must be passed via filters - check both snake_case and camelCase
|
||||
const ENTITY_PARAMS = [
|
||||
@@ -247,12 +248,10 @@ export class Memory {
|
||||
...this.config.vectorStore.config,
|
||||
collectionName: entityCollectionName,
|
||||
};
|
||||
// For file-based stores (memory/SQLite), use a separate DB path for entities
|
||||
if (entityConfig.dbPath) {
|
||||
entityConfig.dbPath = entityConfig.dbPath.replace(
|
||||
/\.db$/,
|
||||
"_entities.db",
|
||||
);
|
||||
// For file-based stores (memory/SQLite), always use a separate DB for entities
|
||||
if (this.config.vectorStore.provider === "memory") {
|
||||
const basePath = entityConfig.dbPath || getDefaultVectorStoreDbPath();
|
||||
entityConfig.dbPath = basePath.replace(/\.db$/, "_entities.db");
|
||||
}
|
||||
this._entityStore = VectorStoreFactory.create(
|
||||
this.config.vectorStore.provider,
|
||||
|
||||
@@ -197,7 +197,7 @@ describe("MemoryVectorStore (better-sqlite3)", () => {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.id).toBe("id-1");
|
||||
expect(result!.payload.data).toBe("hello");
|
||||
expect(result!.payload.userId).toBe("u1");
|
||||
expect(result!.payload.user_id).toBe("u1");
|
||||
});
|
||||
|
||||
it("get returns null for non-existent id", async () => {
|
||||
@@ -253,7 +253,7 @@ describe("MemoryVectorStore (better-sqlite3)", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const results = await store.search(v, 10, { userId: "alice" });
|
||||
const results = await store.search(v, 10, { user_id: "alice" });
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe("id-1");
|
||||
});
|
||||
@@ -320,7 +320,7 @@ describe("MemoryVectorStore (better-sqlite3)", () => {
|
||||
expect(all).toHaveLength(3);
|
||||
expect(totalAll).toBe(3);
|
||||
|
||||
const [filtered, totalFiltered] = await store.list({ userId: "alice" });
|
||||
const [filtered, totalFiltered] = await store.list({ user_id: "alice" });
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(totalFiltered).toBe(2);
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe("backward compat: MemoryVectorStore", () => {
|
||||
);
|
||||
|
||||
const results = await store.search(normalize([1, 0, 0]), 10, {
|
||||
userId: "user2",
|
||||
user_id: "user2",
|
||||
});
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe("b");
|
||||
|
||||
@@ -19,6 +19,24 @@ export class MemoryVectorStore implements VectorStore {
|
||||
private dimension: number;
|
||||
private dbPath: string;
|
||||
|
||||
private static readonly CAMEL_TO_SNAKE: Record<string, string> = {
|
||||
userId: "user_id",
|
||||
agentId: "agent_id",
|
||||
runId: "run_id",
|
||||
};
|
||||
|
||||
private normalizePayload(payload: Record<string, any>): Record<string, any> {
|
||||
for (const [camel, snake] of Object.entries(
|
||||
MemoryVectorStore.CAMEL_TO_SNAKE,
|
||||
)) {
|
||||
if (camel in payload && !(snake in payload)) {
|
||||
payload[snake] = payload[camel];
|
||||
delete payload[camel];
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
constructor(config: VectorStoreConfig) {
|
||||
this.dimension = config.dimension || 1536; // Default OpenAI dimension
|
||||
this.dbPath = config.dbPath || getDefaultVectorStoreDbPath();
|
||||
@@ -246,7 +264,7 @@ export class MemoryVectorStore implements VectorStore {
|
||||
}[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const payload = JSON.parse(row.payload);
|
||||
const payload = this.normalizePayload(JSON.parse(row.payload));
|
||||
const memoryVector: MemoryVector = {
|
||||
id: row.id,
|
||||
vector: Array.from(
|
||||
@@ -351,7 +369,7 @@ export class MemoryVectorStore implements VectorStore {
|
||||
row.vector.byteOffset,
|
||||
row.vector.byteLength / 4,
|
||||
);
|
||||
const payload = JSON.parse(row.payload);
|
||||
const payload = this.normalizePayload(JSON.parse(row.payload));
|
||||
const memoryVector: MemoryVector = {
|
||||
id: row.id,
|
||||
vector: Array.from(vector),
|
||||
@@ -378,7 +396,7 @@ export class MemoryVectorStore implements VectorStore {
|
||||
.get(vectorId) as any;
|
||||
if (!row) return null;
|
||||
|
||||
const payload = JSON.parse(row.payload);
|
||||
const payload = this.normalizePayload(JSON.parse(row.payload));
|
||||
return {
|
||||
id: row.id,
|
||||
payload,
|
||||
@@ -418,7 +436,7 @@ export class MemoryVectorStore implements VectorStore {
|
||||
const results: VectorStoreResult[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const payload = JSON.parse(row.payload);
|
||||
const payload = this.normalizePayload(JSON.parse(row.payload));
|
||||
const memoryVector: MemoryVector = {
|
||||
id: row.id,
|
||||
vector: Array.from(
|
||||
|
||||
@@ -22,6 +22,7 @@ export class PGVector implements VectorStore {
|
||||
private useHnsw: boolean;
|
||||
private readonly dbName: string;
|
||||
private config: PGVectorConfig;
|
||||
private _initPromise?: Promise<void>;
|
||||
|
||||
constructor(config: PGVectorConfig) {
|
||||
this.collectionName = config.collectionName || "memories";
|
||||
@@ -41,6 +42,13 @@ export class PGVector implements VectorStore {
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (!this._initPromise) {
|
||||
this._initPromise = this._doInitialize();
|
||||
}
|
||||
return this._initPromise;
|
||||
}
|
||||
|
||||
private async _doInitialize(): Promise<void> {
|
||||
try {
|
||||
await this.client.connect();
|
||||
|
||||
|
||||
@@ -256,17 +256,25 @@ export class RedisDB implements VectorStore {
|
||||
const modulesResponse =
|
||||
(await this.client.moduleList()) as unknown as any[];
|
||||
|
||||
// Parse module list to find search module
|
||||
const hasSearch = modulesResponse.some((module: any[]) => {
|
||||
const moduleMap = new Map();
|
||||
for (let i = 0; i < module.length; i += 2) {
|
||||
moduleMap.set(module[i], module[i + 1]);
|
||||
const hasSearch = modulesResponse.some((mod: any) => {
|
||||
// node-redis v4+ returns objects: { name: "search", ver: ..., ... }
|
||||
if (typeof mod === "object" && !Array.isArray(mod) && mod.name) {
|
||||
const name = String(mod.name).toLowerCase();
|
||||
return name === "search" || name === "searchlight";
|
||||
}
|
||||
const moduleName = moduleMap.get("name");
|
||||
return (
|
||||
moduleName?.toLowerCase() === "search" ||
|
||||
moduleName?.toLowerCase() === "searchlight"
|
||||
);
|
||||
// Fallback: legacy flat array format [key, value, key, value, ...]
|
||||
if (Array.isArray(mod)) {
|
||||
const moduleMap = new Map();
|
||||
for (let i = 0; i < mod.length; i += 2) {
|
||||
moduleMap.set(mod[i], mod[i + 1]);
|
||||
}
|
||||
const name = moduleMap.get("name");
|
||||
return (
|
||||
name?.toLowerCase() === "search" ||
|
||||
name?.toLowerCase() === "searchlight"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!hasSearch) {
|
||||
|
||||
@@ -85,14 +85,16 @@ describe("MemoryVectorStore - search", () => {
|
||||
expect(results).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("filters by userId", async () => {
|
||||
const results = await store.search(vec([1, 0, 0, 0]), 10, { userId: "u2" });
|
||||
expect(results.every((r) => r.payload.userId === "u2")).toBe(true);
|
||||
test("filters by user_id", async () => {
|
||||
const results = await store.search(vec([1, 0, 0, 0]), 10, {
|
||||
user_id: "u2",
|
||||
});
|
||||
expect(results.every((r) => r.payload.user_id === "u2")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns empty when filter matches nothing", async () => {
|
||||
const results = await store.search(vec([1, 0, 0, 0]), 10, {
|
||||
userId: "nobody",
|
||||
user_id: "nobody",
|
||||
});
|
||||
expect(results).toHaveLength(0);
|
||||
});
|
||||
@@ -168,10 +170,10 @@ describe("MemoryVectorStore - list", () => {
|
||||
expect(results).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("filters by userId", async () => {
|
||||
const [results, count] = await store.list({ userId: "u1" });
|
||||
test("filters by user_id", async () => {
|
||||
const [results, count] = await store.list({ user_id: "u1" });
|
||||
expect(count).toBe(2);
|
||||
expect(results.every((r) => r.payload.userId === "u1")).toBe(true);
|
||||
expect(results.every((r) => r.payload.user_id === "u1")).toBe(true);
|
||||
});
|
||||
|
||||
test("respects limit", async () => {
|
||||
|
||||
@@ -95,19 +95,19 @@ describe("MemoryVectorStore – full backward compat", () => {
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].id).toBe("id-1");
|
||||
|
||||
// Search with filters
|
||||
const filtered = await store.search(vec1, 2, { userId: "u1" });
|
||||
// Search with filters (camelCase in payload is normalized to snake_case)
|
||||
const filtered = await store.search(vec1, 2, { user_id: "u1" });
|
||||
expect(filtered.length).toBe(2);
|
||||
|
||||
// Update
|
||||
const vec3 = new Array(1536).fill(0);
|
||||
vec3[2] = 1.0;
|
||||
await store.update("id-1", vec3, { data: "updated", userId: "u1" });
|
||||
await store.update("id-1", vec3, { data: "updated", user_id: "u1" });
|
||||
const updated = await store.get("id-1");
|
||||
expect(updated!.payload.data).toBe("updated");
|
||||
|
||||
// List
|
||||
const [listed, count] = await store.list({ userId: "u1" });
|
||||
const [listed, count] = await store.list({ user_id: "u1" });
|
||||
expect(count).toBe(2);
|
||||
|
||||
// List with limit
|
||||
|
||||
Reference in New Issue
Block a user