Files
mem0/mem0-ts/src/oss/tests/vertexai-embedder.test.ts
T

239 lines
7.6 KiB
TypeScript

/// <reference types="jest" />
const mockPredict = jest.fn();
const mockGetProjectId = jest.fn();
const mockClientConstructor = jest.fn();
jest.mock("@google-cloud/aiplatform", () => {
return {
__esModule: true,
PredictionServiceClient: jest.fn().mockImplementation((...args) => {
mockClientConstructor(...args);
return {
predict: mockPredict,
getProjectId: mockGetProjectId,
};
}),
helpers: {
toValue: jest.fn().mockImplementation((val) => val),
fromValue: jest.fn().mockImplementation((val) => val),
},
};
});
import { VertexAIEmbedder } from "../src/embeddings/vertexai";
const mockEmbedding = [0.1, 0.2, 0.3, 0.4];
/** Echoes one embedding back per instance in the request. */
function predictEchoingInstances() {
return (req: { instances: unknown[] }) =>
Promise.resolve([
{
predictions: req.instances.map(() => ({
embeddings: { values: mockEmbedding },
})),
},
]);
}
describe("VertexAIEmbedder", () => {
beforeEach(() => {
mockPredict.mockReset();
mockClientConstructor.mockReset();
mockGetProjectId.mockReset();
mockGetProjectId.mockResolvedValue("adc-project");
mockPredict.mockResolvedValue([
{
predictions: [
{
embeddings: {
values: mockEmbedding,
},
},
],
},
]);
});
describe("basic functionality", () => {
it("embed() returns the embedding vector", async () => {
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
});
const result = await embedder.embed("hello");
expect(result).toEqual(mockEmbedding);
expect(mockPredict).toHaveBeenCalledTimes(1);
const callArgs = mockPredict.mock.calls[0][0];
expect(callArgs.endpoint).toBe(
"projects/test-project/locations/us-central1/publishers/google/models/gemini-embedding-001",
);
// task_type belongs on the instance (snake_case), parameters carries
// only outputDimensionality.
expect(callArgs.instances).toEqual([
{ content: "hello", task_type: "SEMANTIC_SIMILARITY" },
]);
expect(callArgs.parameters).toEqual({ outputDimensionality: 256 });
});
it("embed() with memory action search uses RETRIEVAL_QUERY", async () => {
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
});
await embedder.embed("hello", "search");
const callArgs = mockPredict.mock.calls[0][0];
expect(callArgs.instances[0].task_type).toBe("RETRIEVAL_QUERY");
});
it("embed() with memory action add uses RETRIEVAL_DOCUMENT", async () => {
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
});
await embedder.embed("hello", "add");
const callArgs = mockPredict.mock.calls[0][0];
expect(callArgs.instances[0].task_type).toBe("RETRIEVAL_DOCUMENT");
});
it("throws error when predictions are empty", async () => {
mockPredict.mockResolvedValue([{}]);
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
});
await expect(embedder.embed("hello")).rejects.toThrow(
"No predictions returned from Vertex AI",
);
});
});
describe("embedBatch() request sizing", () => {
// gemini-embedding-001 (the default model) rejects any predict() call
// carrying more than one input text, so the batch loop must degrade to one
// request per text rather than the 250-instance chunk the older models take.
it("sends one instance per request for gemini-embedding models", async () => {
mockPredict.mockImplementation(predictEchoingInstances());
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
});
const texts = ["a", "b", "c"];
const result = await embedder.embedBatch(texts);
expect(result).toEqual(texts.map(() => mockEmbedding));
expect(mockPredict).toHaveBeenCalledTimes(3);
for (const call of mockPredict.mock.calls) {
expect(call[0].instances.length).toBe(1);
// batch defaults to the "add" action -> RETRIEVAL_DOCUMENT
expect(call[0].instances[0].task_type).toBe("RETRIEVAL_DOCUMENT");
expect(call[0].parameters).toEqual({ outputDimensionality: 256 });
}
expect(
mockPredict.mock.calls.map((c) => c[0].instances[0].content),
).toEqual(texts);
});
it("chunks at 250 instances for text-embedding models", async () => {
mockPredict.mockImplementation(predictEchoingInstances());
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
model: "text-embedding-005",
});
const texts = Array.from({ length: 255 }, (_, i) => `text-${i}`);
const result = await embedder.embedBatch(texts, "search");
expect(result.length).toBe(255);
expect(mockPredict).toHaveBeenCalledTimes(2);
expect(mockPredict.mock.calls[0][0].instances.length).toBe(250);
expect(mockPredict.mock.calls[0][0].instances[0].task_type).toBe(
"RETRIEVAL_QUERY",
);
expect(mockPredict.mock.calls[1][0].instances.length).toBe(5);
});
it("rejects an unknown memory action", async () => {
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
});
await expect(
embedder.embedBatch(["a"], "delete" as unknown as "add"),
).rejects.toThrow("Invalid memory action: delete");
});
});
describe("client initialization", () => {
const PROJECT_ENV_VARS = [
"GCP_PROJECT_ID",
"GOOGLE_CLOUD_PROJECT",
"GCLOUD_PROJECT",
];
let savedEnv: Record<string, string | undefined>;
beforeEach(() => {
savedEnv = {};
for (const key of PROJECT_ENV_VARS) {
savedEnv[key] = process.env[key];
delete process.env[key];
}
});
afterEach(() => {
for (const key of PROJECT_ENV_VARS) {
if (savedEnv[key] === undefined) delete process.env[key];
else process.env[key] = savedEnv[key];
}
});
it("resolves the project ID from credentials when none is configured", async () => {
const embedder = new VertexAIEmbedder({});
await embedder.embed("hello");
expect(mockGetProjectId).toHaveBeenCalledTimes(1);
expect(mockPredict.mock.calls[0][0].endpoint).toBe(
"projects/adc-project/locations/us-central1/publishers/google/models/gemini-embedding-001",
);
});
it("surfaces a helpful error when no project ID can be resolved", async () => {
mockGetProjectId.mockRejectedValue(
new Error("Unable to detect a Project Id"),
);
const embedder = new VertexAIEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
"Vertex AI could not determine a Google Cloud project ID",
);
});
it("builds one client for concurrent calls", async () => {
const embedder = new VertexAIEmbedder({
googleProjectId: "test-project",
});
await Promise.all([
embedder.embed("a"),
embedder.embed("b"),
embedder.embed("c"),
]);
expect(mockClientConstructor).toHaveBeenCalledTimes(1);
});
it("retries initialization after a failure", async () => {
mockGetProjectId.mockRejectedValueOnce(new Error("transient"));
const embedder = new VertexAIEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
"Vertex AI could not determine a Google Cloud project ID",
);
await expect(embedder.embed("hello")).resolves.toEqual(mockEmbedding);
});
});
});