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) {
|
for (const mem of extractedMemories) {
|
||||||
const text = mem.text;
|
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");
|
const memHash = createHash("md5").update(text).digest("hex");
|
||||||
if (existingHashes.has(memHash) || seenHashes.has(memHash)) {
|
if (existingHashes.has(memHash) || seenHashes.has(memHash)) {
|
||||||
@@ -1943,8 +1944,12 @@ export class Memory {
|
|||||||
metadata: Record<string, any>,
|
metadata: Record<string, any>,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const memoryId = uuidv4();
|
const memoryId = uuidv4();
|
||||||
const embedding =
|
const embedding = Object.prototype.hasOwnProperty.call(
|
||||||
existingEmbeddings[data] || (await this.embedder.embed(data, "add"));
|
existingEmbeddings,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
? existingEmbeddings[data]
|
||||||
|
: await this.embedder.embed(data, "add");
|
||||||
|
|
||||||
const memoryMetadata = {
|
const memoryMetadata = {
|
||||||
...metadata,
|
...metadata,
|
||||||
@@ -1987,9 +1992,12 @@ export class Memory {
|
|||||||
}
|
}
|
||||||
const textChanged = newData !== prevValue;
|
const textChanged = newData !== prevValue;
|
||||||
|
|
||||||
const embedding =
|
const embedding = Object.prototype.hasOwnProperty.call(
|
||||||
existingEmbeddings[newData] ||
|
existingEmbeddings,
|
||||||
(await this.embedder.embed(newData, "update"));
|
newData,
|
||||||
|
)
|
||||||
|
? existingEmbeddings[newData]
|
||||||
|
: await this.embedder.embed(newData, "update");
|
||||||
|
|
||||||
const sanitizedMetadata = stripIdentityKeys(metadata);
|
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