fix(ts-oss): stop prototype keys from short-circuiting embedding lookup (#6903)
This commit is contained in:
@@ -1000,7 +1000,8 @@ export class Memory {
|
||||
|
||||
for (const mem of extractedMemories) {
|
||||
const text = mem.text;
|
||||
if (!text || !(text in embedMap)) continue;
|
||||
if (!text || !Object.prototype.hasOwnProperty.call(embedMap, text))
|
||||
continue;
|
||||
|
||||
const memHash = createHash("md5").update(text).digest("hex");
|
||||
if (existingHashes.has(memHash) || seenHashes.has(memHash)) {
|
||||
@@ -1943,8 +1944,12 @@ export class Memory {
|
||||
metadata: Record<string, any>,
|
||||
): Promise<string> {
|
||||
const memoryId = uuidv4();
|
||||
const embedding =
|
||||
existingEmbeddings[data] || (await this.embedder.embed(data, "add"));
|
||||
const embedding = Object.prototype.hasOwnProperty.call(
|
||||
existingEmbeddings,
|
||||
data,
|
||||
)
|
||||
? existingEmbeddings[data]
|
||||
: await this.embedder.embed(data, "add");
|
||||
|
||||
const memoryMetadata = {
|
||||
...metadata,
|
||||
@@ -1987,9 +1992,12 @@ export class Memory {
|
||||
}
|
||||
const textChanged = newData !== prevValue;
|
||||
|
||||
const embedding =
|
||||
existingEmbeddings[newData] ||
|
||||
(await this.embedder.embed(newData, "update"));
|
||||
const embedding = Object.prototype.hasOwnProperty.call(
|
||||
existingEmbeddings,
|
||||
newData,
|
||||
)
|
||||
? existingEmbeddings[newData]
|
||||
: await this.embedder.embed(newData, "update");
|
||||
|
||||
const sanitizedMetadata = stripIdentityKeys(metadata);
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/// <reference types="jest" />
|
||||
/** add() with infer=false must embed Object.prototype-colliding text instead of resolving it off the prototype chain. */
|
||||
import { Memory } from "../src/memory";
|
||||
import { MemoryVectorStore } from "../src/vector_stores/memory";
|
||||
import type { SearchResult } from "../src/types";
|
||||
|
||||
const mockEmbedding = new Array(1536).fill(0.1);
|
||||
const mockEmbed = jest.fn().mockResolvedValue(mockEmbedding);
|
||||
|
||||
jest.mock("../src/embeddings/openai", () => ({
|
||||
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
|
||||
embed: mockEmbed,
|
||||
embedBatch: jest.fn(),
|
||||
embeddingDims: 1536,
|
||||
})),
|
||||
}));
|
||||
|
||||
function createMemory(): Memory {
|
||||
return new Memory({
|
||||
version: "v1.1",
|
||||
embedder: {
|
||||
provider: "openai",
|
||||
config: { apiKey: "test-key", model: "text-embedding-3-small" },
|
||||
},
|
||||
vectorStore: {
|
||||
provider: "memory",
|
||||
config: {
|
||||
collectionName: `test-proto-${Date.now()}-${Math.random()}`,
|
||||
dimension: 1536,
|
||||
dbPath: ":memory:",
|
||||
},
|
||||
},
|
||||
llm: {
|
||||
provider: "openai",
|
||||
config: { apiKey: "test-key", model: "gpt-5-mini" },
|
||||
},
|
||||
historyDbPath: ":memory:",
|
||||
});
|
||||
}
|
||||
|
||||
describe("add() with infer=false and Object.prototype-colliding text", () => {
|
||||
let memory: Memory;
|
||||
let insertSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
memory = createMemory();
|
||||
mockEmbed.mockClear();
|
||||
insertSpy = jest.spyOn(MemoryVectorStore.prototype, "insert");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
insertSpy.mockRestore();
|
||||
await memory.reset();
|
||||
});
|
||||
|
||||
test.each([
|
||||
"constructor",
|
||||
"toString",
|
||||
"valueOf",
|
||||
"hasOwnProperty",
|
||||
"__proto__",
|
||||
])(
|
||||
'embeds "%s" instead of resolving it off Object.prototype',
|
||||
async (text) => {
|
||||
const result: SearchResult = await memory.add(text, {
|
||||
userId: "u1",
|
||||
infer: false,
|
||||
});
|
||||
|
||||
expect(mockEmbed).toHaveBeenCalledWith(text, "add");
|
||||
|
||||
const [storedVectors] =
|
||||
insertSpy.mock.calls[insertSpy.mock.calls.length - 1];
|
||||
expect(Array.isArray(storedVectors[0])).toBe(true);
|
||||
expect(storedVectors[0]).toEqual(mockEmbedding);
|
||||
expect(
|
||||
storedVectors[0].every((n: unknown) => typeof n === "number"),
|
||||
).toBe(true);
|
||||
|
||||
expect(result.results[0].memory).toBe(text);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("update() metadata-only on Object.prototype-colliding text (#6323)", () => {
|
||||
let memory: Memory;
|
||||
let updateSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
memory = createMemory();
|
||||
updateSpy = jest.spyOn(MemoryVectorStore.prototype, "update");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
updateSpy.mockRestore();
|
||||
await memory.reset();
|
||||
});
|
||||
|
||||
test.each([
|
||||
"constructor",
|
||||
"toString",
|
||||
"valueOf",
|
||||
"hasOwnProperty",
|
||||
"__proto__",
|
||||
])(
|
||||
're-embeds "%s" on a metadata-only update instead of resolving it off Object.prototype',
|
||||
async (text) => {
|
||||
const added: SearchResult = await memory.add(text, {
|
||||
userId: "u1",
|
||||
infer: false,
|
||||
});
|
||||
const memoryId = added.results[0].id!;
|
||||
mockEmbed.mockClear();
|
||||
|
||||
await memory.update(memoryId, { metadata: { pinned: true } });
|
||||
|
||||
expect(mockEmbed).toHaveBeenCalledWith(text, "update");
|
||||
|
||||
const [, storedVector] =
|
||||
updateSpy.mock.calls[updateSpy.mock.calls.length - 1];
|
||||
expect(Array.isArray(storedVector)).toBe(true);
|
||||
expect(storedVector).toEqual(mockEmbedding);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user