diff --git a/packages/wasm-tools/README.md b/packages/wasm-tools/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ab0d0e6b354acabae4c5b79700f63756018696cb --- /dev/null +++ b/packages/wasm-tools/README.md @@ -0,0 +1,7 @@ +## Usage + +```ts +import { TestTool } from "@llamaindex/wasm-tools"; +const testTool = new TestTool(); +testTool.call("1"); // get post has id = 1 (url: https://jsonplaceholder.typicode.com/todos?id=1) +``` diff --git a/packages/wasm-tools/assembly/base.ts b/packages/wasm-tools/assembly/base.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea06267f606066a2e3f4241dab9cc3898d0b9006 --- /dev/null +++ b/packages/wasm-tools/assembly/base.ts @@ -0,0 +1,55 @@ +export class ToolParameterProperty { + type: string; + description: string | null = null; + + constructor(type: string, description: string | null = null) { + this.type = type; + this.description = description; + } +} + +// Because AssemblyScript does not support Record<string, ToolParameterProperty> yet, +// we have to use an array of key-value pairs instead. +// When loading the metadata in application, we will convert +// the array ToolParameterPropertyRecord[] to Record<string, ToolParameterProperty>. +export class ToolParameterPropertyRecord { + key: string; + value: ToolParameterProperty; + + constructor(key: string, value: ToolParameterProperty) { + this.key = key; + this.value = value; + } +} + +export class ToolParameters { + type: string; + properties: ToolParameterPropertyRecord[]; + required: string[] | null = null; + + constructor( + type: string, + properties: ToolParameterPropertyRecord[], + required: string[] | null = null, + ) { + this.type = type; + this.properties = properties; + this.required = required; + } +} + +export class ToolMetadata { + name: string; + description: string; + parameters: ToolParameters | null = null; + + constructor( + name: string, + description: string, + parameters: ToolParameters | null = null, + ) { + this.name = name; + this.description = description; + this.parameters = parameters; + } +} diff --git a/packages/wasm-tools/assembly/http.ts b/packages/wasm-tools/assembly/http.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4a677e4f9140a99b6070e58cc4d9e2f06344fad --- /dev/null +++ b/packages/wasm-tools/assembly/http.ts @@ -0,0 +1 @@ +export declare function get(url: string, headersString: string): void; diff --git a/packages/wasm-tools/assembly/test-tool/index.ts b/packages/wasm-tools/assembly/test-tool/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3863e299afe7b599723b870d988bc6c3752c56ce --- /dev/null +++ b/packages/wasm-tools/assembly/test-tool/index.ts @@ -0,0 +1,27 @@ +import { + ToolMetadata, + ToolParameterProperty, + ToolParameterPropertyRecord, + ToolParameters, +} from "../base"; +import * as http from "../http"; +export * from "../base"; + +export const defaultMetadata: ToolMetadata = new ToolMetadata( + "Test Tool", + "This is a test tool", + new ToolParameters( + "object", + [ + new ToolParameterPropertyRecord( + "query", + new ToolParameterProperty("string", "The text query to search"), + ), + ], + ["query"], + ), +); + +export function call(id: string): void { + http.get(`https://jsonplaceholder.typicode.com/todos?id=${id}`, ""); +} diff --git a/packages/wasm-tools/assembly/tsconfig.json b/packages/wasm-tools/assembly/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..798b474eab6c6861a91ba6fe88168a48dbecaf61 --- /dev/null +++ b/packages/wasm-tools/assembly/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./**/*.ts"] +} diff --git a/packages/wasm-tools/bin/compile.js b/packages/wasm-tools/bin/compile.js new file mode 100644 index 0000000000000000000000000000000000000000..68b743ad04c86aa816aaa66f92520096b5043e4e --- /dev/null +++ b/packages/wasm-tools/bin/compile.js @@ -0,0 +1,17 @@ +import { execSync } from "child_process"; +import { readdirSync } from "fs"; + +// get list of tools from folder names inside assembly folder +const tools = readdirSync("assembly").filter((dir) => !dir.includes(".")); + +// loop through each tool, compile it to wasm and verify it +tools.forEach((tool) => { + try { + execSync( + `asc assembly/${tool}/index.ts -b dist/${tool}.wasm -t dist/${tool}.wat --exportRuntime --sourceMap --optimize`, + ); + } catch (error) { + console.error(`Error compiling module ${tool}:`, error.message); + process.exit(1); + } +}); diff --git a/packages/wasm-tools/package.json b/packages/wasm-tools/package.json new file mode 100644 index 0000000000000000000000000000000000000000..521648502671eba859c4892182cf796ae63052a6 --- /dev/null +++ b/packages/wasm-tools/package.json @@ -0,0 +1,59 @@ +{ + "name": "@llamaindex/wasm-tools", + "version": "0.0.1", + "license": "MIT", + "type": "module", + "dependencies": { + "@types/node": "^18.19.14", + "@assemblyscript/loader": "^0.19.9" + }, + "devDependencies": { + "assemblyscript": "^0.19.9", + "@swc/cli": "^0.3.9", + "@swc/core": "^1.4.2", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "types": "./dist/index.d.ts", + "main": "./dist/cjs/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./*": { + "import": { + "types": "./dist/*.d.ts", + "default": "./dist/*.js" + }, + "require": { + "types": "./dist/*.d.ts", + "default": "./dist/cjs/*.js" + } + } + }, + "files": [ + "dist", + "CHANGELOG.md" + ], + "repository": { + "type": "git", + "url": "https://github.com/run-llama/LlamaIndexTS.git", + "directory": "packages/tools" + }, + "scripts": { + "build": "rm -rf ./dist && pnpm run build:esm && pnpm run build:cjs && pnpm run build:type && pnpm run build:wasm", + "build:esm": "swc src -d dist --strip-leading-paths --config-file ../../.swcrc", + "build:cjs": "swc src -d dist/cjs --strip-leading-paths --config-file ../../.cjs.swcrc", + "build:type": "tsc -p tsconfig.json", + "build:wasm": "node bin/compile.js" + } +} diff --git a/packages/wasm-tools/src/factory.ts b/packages/wasm-tools/src/factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..062a0292115b3e4ebc4b15cd669560045237652f --- /dev/null +++ b/packages/wasm-tools/src/factory.ts @@ -0,0 +1,155 @@ +// @ts-ignore +import loader from "@assemblyscript/loader"; +import fs from "fs"; +import type { BaseTool, ToolMetadata } from "./types.js"; +import { arrayKVtoObject, transformObject } from "./utils/object.js"; + +export default class ToolFactory { + /** + * Transform the metadata from the assemblyscript raw format to the application format + * Convert asm string to ts string + * Convert asm array to ts array + * Convert the properties from an Array to a Record<string, { type: string; description?: string }> + * Convert the argsKwargs from an Array to a Record<string, any> + */ + private static wasmInstanceToConfigs(wasmInstance: any): BaseTool { + const { __pin, __unpin, __getString, __getArray, __newString } = + wasmInstance.exports; + + const getObjectByAddress = (address: any, classWrapper: any) => { + const object = classWrapper.wrap(__pin(address)); + __unpin(address); + return object; + }; + + const { + ToolMetadata, + ToolParameters, + ToolParameterPropertyRecord, + ToolParameterProperty, + } = wasmInstance.exports; + + const { defaultMetadata, call } = wasmInstance.exports; + const metadata = transformObject( + getObjectByAddress(defaultMetadata, ToolMetadata), + { + name: __getString, + description: __getString, + parameters: (parameters) => { + if (!parameters) return null; + const parametersObj = getObjectByAddress(parameters, ToolParameters); + return transformObject(parametersObj, { + type: __getString, + required: (required) => { + const requiredArray = __getArray(required); + return requiredArray.map(__getString); + }, + properties: (properties) => { + const propertiesArray = __getArray(properties); + const arr = propertiesArray.map((property: any) => { + return transformObject( + getObjectByAddress(property, ToolParameterPropertyRecord), + { + key: __getString, + value: (value) => { + return transformObject( + getObjectByAddress(value, ToolParameterProperty), + { + type: __getString, + description: __getString, + }, + ); + }, + }, + ); + }); + return arrayKVtoObject(arr); + }, + }); + }, + }, + ) as ToolMetadata; + + // Wrap assemblyscript function to a ts function + const callFunction = (...args: string[]): string => { + const argsString = args.map((arg) => __pin(__newString(arg))); + return __getString(call(...argsString)); + }; + + return { + metadata, + call: callFunction, + }; + } + + private static initWasmInstanceFromFile = (filePath: string) => { + const wasmFile = fs.readFileSync( + `node_modules/@llamaindex/tools/dist/${filePath}.wasm`, + ); + + const wasmInstance = loader.instantiateSync(wasmFile, { + http: { + // import fetch from JavaScript and use it in WebAssembly + get(url: string, headersString: string) { + const stringHeaders = wasmInstance.exports + .__getString(headersString) + .split(",,,,"); + + stringHeaders.pop(); + const headers: Record<string, string> = {}; + for (let i = 0; i < stringHeaders.length; i++) { + headers[stringHeaders[i]] = stringHeaders[i + 1]; + i++; + } + fetch(wasmInstance.exports.__getString(url), { + headers: { + ...headers, + }, + mode: "no-cors", + method: "GET", + }) + .then((fetched) => { + fetched.json().then((data) => { + console.log("Response from API call: ", data); + // Add callback to handle data if needed + return wasmInstance.exports.__newString(JSON.stringify(data)); + }); + }) + .catch((err) => { + console.error(wasmInstance.exports.__newString(err.message)); + }); + }, + }, + }); + + return wasmInstance; + }; + + private static getToolConfigs = (filePath: string): BaseTool => { + const wasmInstance = this.initWasmInstanceFromFile(filePath); + const toolConfigs = this.wasmInstanceToConfigs(wasmInstance); + return toolConfigs; + }; + + private static configsToToolClass = (toolConfigs: BaseTool) => { + return class implements BaseTool { + call = toolConfigs.call; + metadata: ToolMetadata; + constructor(metadata: ToolMetadata) { + this.metadata = metadata || toolConfigs.metadata; + } + }; + }; + + public static get toolList(): string[] { + return fs + .readdirSync("node_modules/@llamaindex/tools/dist") + .filter((file) => file.endsWith(".wasm")) + .map((file) => file.replace(".wasm", "")); + } + + public static toClass = (tool: string) => { + const toolConfigs = this.getToolConfigs(tool); + return this.configsToToolClass(toolConfigs); + }; +} diff --git a/packages/wasm-tools/src/index.ts b/packages/wasm-tools/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b11290199593198b179a15252d50e3ed9d748d3f --- /dev/null +++ b/packages/wasm-tools/src/index.ts @@ -0,0 +1 @@ +export * from "./tools.js"; diff --git a/packages/wasm-tools/src/tools.ts b/packages/wasm-tools/src/tools.ts new file mode 100644 index 0000000000000000000000000000000000000000..84064c2c0e098db0819d65d2999073d6739ddbcb --- /dev/null +++ b/packages/wasm-tools/src/tools.ts @@ -0,0 +1,3 @@ +import ToolFactory from "./factory.js"; + +export const TestTool = ToolFactory.toClass("test-tool"); diff --git a/packages/wasm-tools/src/types.ts b/packages/wasm-tools/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..6cf3b37fbc191b4dbd404fed278ede807d28df10 --- /dev/null +++ b/packages/wasm-tools/src/types.ts @@ -0,0 +1,16 @@ +export type ToolParameters = { + type: string | "object"; + properties: Record<string, { type: string; description?: string }>; + required?: string[]; +}; + +export interface ToolMetadata { + description: string; + name: string; + parameters?: ToolParameters; +} + +export interface BaseTool { + call?: (...args: any[]) => any; + metadata: ToolMetadata; +} diff --git a/packages/wasm-tools/src/utils/object.ts b/packages/wasm-tools/src/utils/object.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e305d2a09766cbe46bc34d0bb789c5ff2ff039f --- /dev/null +++ b/packages/wasm-tools/src/utils/object.ts @@ -0,0 +1,23 @@ +export const transformObject = ( + obj: any, + transfomer: Record<string, (value: any) => any>, +) => { + const newObj: Record<string, any> = {}; + for (const key in transfomer) { + newObj[key] = transfomer[key](obj[key]); + } + return newObj; +}; + +export const arrayKVtoObject = ( + array: { + key: string; + value: any; + }[], +) => { + const obj: Record<string, any> = {}; + for (const item of array) { + obj[item.key] = item.value; + } + return obj; +}; diff --git a/packages/wasm-tools/tsconfig.json b/packages/wasm-tools/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..f59d0bd7ecbaee9d6e31b30bb7cb71fc7d3b5ff4 --- /dev/null +++ b/packages/wasm-tools/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "emitDeclarationOnly": true, + "module": "node16", + "moduleResolution": "node16", + "skipLibCheck": true, + "strict": true + }, + "include": ["./src"], + "exclude": ["node_modules"], + "references": [ + { + "path": "../env/tsconfig.json" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e64988699293cd9e0bc949d8562feaac2c4d151a..469504a9099e4b3aa0b17d71fbcbad6554228f4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -388,6 +388,28 @@ importers: packages/tsconfig: {} + packages/wasm-tools: + dependencies: + '@assemblyscript/loader': + specifier: ^0.19.9 + version: 0.19.23 + '@types/node': + specifier: ^18.19.14 + version: 18.19.14 + devDependencies: + '@swc/cli': + specifier: ^0.3.9 + version: 0.3.9(@swc/core@1.4.2) + '@swc/core': + specifier: ^1.4.2 + version: 1.4.2 + assemblyscript: + specifier: ^0.19.9 + version: 0.19.23 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -555,6 +577,10 @@ packages: - encoding dev: false + /@assemblyscript/loader@0.19.23: + resolution: {integrity: sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw==} + dev: false + /@aws-crypto/sha256-js@5.2.0: resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} engines: {node: '>=16.0.0'} @@ -5122,6 +5148,15 @@ packages: - utf-8-validate dev: false + /assemblyscript@0.19.23: + resolution: {integrity: sha512-fwOQNZVTMga5KRsfY80g7cpOl4PsFQczMwHzdtgoqLXaYhkhavufKb0sB0l3T1DUxpAufA0KNhlbpuuhZUwxMA==} + hasBin: true + dependencies: + binaryen: 102.0.0-nightly.20211028 + long: 5.2.3 + source-map-support: 0.5.21 + dev: true + /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true @@ -5315,6 +5350,11 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + /binaryen@102.0.0-nightly.20211028: + resolution: {integrity: sha512-GCJBVB5exbxzzvyt8MGDv/MeUjs6gkXDvf4xOIItRBptYl0Tz5sm1o/uG95YK0L0VeG5ajDu3hRtkBP2kzqC5w==} + hasBin: true + dev: true + /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: @@ -8344,7 +8384,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.2 + fast-glob: 3.3.1 ignore: 5.3.1 merge2: 1.4.1 slash: 3.0.0