Skip to content

Commit

Permalink
fix: use workaround for dynamic import (chroma-core#956)
Browse files Browse the repository at this point in the history
resolves chroma-core#953

## Description of changes
 -  Bug fixes
- implement a workaround in
clients/js/src/embeddings/WebAIEmbeddingFunction.ts to resolve chroma-core#953

## Test plan
At present, we lack a testing setup specifically tailored for a browser
environment. Implementing this would be a necessary step to implement
tests for changes in this branch.

I've tested it locally using this change:
https://github.com/jeffchuber/nextjs-chroma/pull/1/files
- http://localhost:3000/api/hello (node version) works fine
- http://localhost:3000 (browser version) works fine
  • Loading branch information
perzeuss authored and tazarov committed Aug 31, 2023
1 parent 7414f8b commit 89024a0
Show file tree
Hide file tree
Showing 5 changed files with 1,891 additions and 1,325 deletions.
29 changes: 28 additions & 1 deletion clients/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
"author": "",
"license": "Apache-2.0",
"devDependencies": {
"@openapi-generator-plus/typescript-fetch-client-generator": "^1.5.0",
"@types/jest": "^29.5.0",
"jest": "^29.5.0",
"npm-run-all": "^4.1.5",
"openapi-generator-plus": "^2.6.0",
"@openapi-generator-plus/typescript-fetch-client-generator": "^1.5.0",
"prettier": "2.8.7",
"rimraf": "^5.0.0",
"ts-jest": "^29.1.0",
Expand Down Expand Up @@ -47,7 +47,34 @@
"prettier": "prettier --write .",
"release": "run-s build test:run && npm publish"
},
"engines": {
"node": ">=14.17.0"
},
"dependencies": {
"isomorphic-fetch": "^3.0.0"
},
"peerDependencies": {
"@visheratin/web-ai": "^1.0.0",
"@visheratin/web-ai-node": "^1.0.0",
"@xenova/transformers": "^2.0.0",
"cohere-ai": "^6.0.0",
"openai": "^3.0.0 | ^4.0.0"
},
"peerDependenciesMeta": {
"@visheratin/web-ai": {
"optional": true
},
"@visheratin/web-ai-node": {
"optional": true
},
"@xenova/transformers": {
"optional": true
},
"cohere-ai": {
"optional": true
},
"openai": {
"optional": true
}
}
}
9 changes: 4 additions & 5 deletions clients/js/src/embeddings/TransformersEmbeddingFunction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { importOptionalModule } from "../utils";
import { IEmbeddingFunction } from "./IEmbeddingFunction";

// Dynamically import module
Expand Down Expand Up @@ -26,11 +27,9 @@ export class TransformersEmbeddingFunction implements IEmbeddingFunction {
progress_callback?: Function | null;
} = {}) {
try {
// Since Transformers.js is an ESM package, we use the dynamic `import` syntax instead of `require`.
// Also, since we use `"module": "commonjs"` in tsconfig.json, we use the following workaround to ensure
// the dynamic import is not transpiled to a `require` statement.
// For more information, see https://github.com/microsoft/TypeScript/issues/43329#issuecomment-1008361973
TransformersApi = Function('return import("@xenova/transformers")')();
// Use dynamic import to support browser environments because we do not have a bundler that handles browser support.
// The util importOptionalModule is used to prevent issues when bundlers try to locate the dependency even when it's optional.
TransformersApi = importOptionalModule("@xenova/transformers");
} catch (e) {
throw new Error(
"Please install the @xenova/transformers package to use the TransformersEmbeddingFunction, `npm install -S @xenova/transformers`."
Expand Down
19 changes: 11 additions & 8 deletions clients/js/src/embeddings/WebAIEmbeddingFunction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { importOptionalModule } from "../utils";
import { IEmbeddingFunction } from "./IEmbeddingFunction";

/**
Expand Down Expand Up @@ -157,15 +158,15 @@ export class WebAIEmbeddingFunction implements IEmbeddingFunction {
) {
this.proxy = proxy ? proxy : true;
try {
// @ts-ignore
const webAI = await import("@visheratin/web-ai");
const webAI = await importOptionalModule("@visheratin/web-ai");
if (wasmPath) {
webAI.SessionParams.wasmRoot = wasmPath;
}
switch (modality) {
case "text": {
// @ts-ignore
const webAIText = await import("@visheratin/web-ai/text");
const webAIText = await importOptionalModule(
"@visheratin/web-ai/text"
);
let id = "mini-lm-v2-quant"; //default text model
if (modelID) {
id = modelID;
Expand All @@ -182,8 +183,9 @@ export class WebAIEmbeddingFunction implements IEmbeddingFunction {
);
}
case "image": {
// @ts-ignore
const webAIImage = await import("@visheratin/web-ai/image");
const webAIImage = await importOptionalModule(
"@visheratin/web-ai/image"
);
let id = "efficientformer-l1-feature-quant"; //default image model
if (modelID) {
id = modelID;
Expand All @@ -200,8 +202,9 @@ export class WebAIEmbeddingFunction implements IEmbeddingFunction {
);
}
case "multimodal": {
// @ts-ignore
const webAIImage = await import("@visheratin/web-ai/multimodal");
const webAIImage = await importOptionalModule(
"@visheratin/web-ai/multimodal"
);
let id = "clip-base-quant"; //default multimodal model
if (modelID) {
id = modelID;
Expand Down
108 changes: 63 additions & 45 deletions clients/js/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,84 @@
import { Api } from "./generated"
import { Api } from "./generated";
import Count200Response = Api.Count200Response;

// a function to convert a non-Array object to an Array
export function toArray<T>(obj: T | Array<T>): Array<T> {
if (Array.isArray(obj)) {
return obj;
} else {
return [obj];
}
if (Array.isArray(obj)) {
return obj;
} else {
return [obj];
}
}

// a function to convert an array to array of arrays
export function toArrayOfArrays<T>(obj: Array<Array<T>> | Array<T>): Array<Array<T>> {
if (Array.isArray(obj[0])) {
return obj as Array<Array<T>>;
} else {
return [obj] as Array<Array<T>>;
}
export function toArrayOfArrays<T>(
obj: Array<Array<T>> | Array<T>
): Array<Array<T>> {
if (Array.isArray(obj[0])) {
return obj as Array<Array<T>>;
} else {
return [obj] as Array<Array<T>>;
}
}

// we need to override constructors to make it work with jest
// https://stackoverflow.com/questions/76007003/jest-tobeinstanceof-expected-constructor-array-received-constructor-array
export function repack(value: unknown): any {
if (Boolean(value) && typeof value === "object") {
if (Array.isArray(value)) {
return new Array(...value);
} else {
return { ...value };
}
if (Boolean(value) && typeof value === "object") {
if (Array.isArray(value)) {
return new Array(...value);
} else {
return value;
return { ...value };
}
} else {
return value;
}
}

export async function handleError(error: unknown) {

if (error instanceof Response) {
try {
const res = await error.json();
if ("error" in res) {
return { error: res.error };
}
} catch (e: unknown) {
return {
//@ts-ignore
error:
e && typeof e === "object" && "message" in e
? e.message
: "unknown error",
};
}
if (error instanceof Response) {
try {
const res = await error.json();
if ("error" in res) {
return { error: res.error };
}
} catch (e: unknown) {
return {
//@ts-ignore
error:
e && typeof e === "object" && "message" in e
? e.message
: "unknown error",
};
}
return { error };
}
return { error };
}

export async function handleSuccess(response: Response | string | Count200Response) {
switch (true) {
case response instanceof Response:
return repack(await (response as Response).json());
case typeof response === "string":
return repack((response as string)); // currently version is the only thing that return non-JSON
default:
return repack(response);
}
export async function handleSuccess(
response: Response | string | Count200Response
) {
switch (true) {
case response instanceof Response:
return repack(await (response as Response).json());
case typeof response === "string":
return repack(response as string); // currently version is the only thing that return non-JSON
default:
return repack(response);
}
}

/**
* Dynamically imports a specified module, providing a workaround for browser environments.
* This function is necessary because we dynamically import optional dependencies
* which can cause issues with bundlers that detect the import and throw an error
* on build time when the dependency is not installed.
* Using this workaround, the dynamic import is only evaluated on runtime
* where we work with try-catch when importing optional dependencies.
*
* @param {string} moduleName - Specifies the module to import.
* @returns {Promise<any>} Returns a Promise that resolves to the imported module.
*/
export async function importOptionalModule(moduleName: string) {
return Function(`return import("${moduleName}")`)();
}
Loading

0 comments on commit 89024a0

Please sign in to comment.