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