diff --git a/README.md b/README.md index 250bfb1..3cf2a79 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ This package provides an easy way to use the [Langflow API](https://docs.datasta - [Flow reponses](#flow-reponses) - [Streaming](#streaming) - [File upload](#file-upload) + - [Images](#images) + - [File uploads](#file-uploads) - [Logs](#logs) - [Fetching the logs](#fetching-the-logs) - [Streaming the logs](#streaming-the-logs) @@ -196,23 +198,68 @@ There's more [documentation and examples of a streaming response in the Langflow ### File upload -[Langflow documentation for file upload API](https://docs.langflow.org/api/upload-file-1). +#### Images + +The [Langflow v1 file upload API](https://docs.langflow.org/api-files#filesv1-endpoints) supports uploading image files to a flow, that can then be used in that flow. Chat input components support files as input as well as text. You need to upload your file first, using the file upload function, then provide the file path to the flow as a tweak. ```js -const flow = client.flow(flowId) +const buffer = await readFile(path); +const file = new File([buffer], "image.jpg", { type: "image/jpeg" }); -const file = await flow.uploadFile(pathToFile); +const flow = client.flow(flowId) +const file = await flow.uploadFile(file); console.log(file); // => { flowId: "XXX", filePath: "YYY" } -const response = await flow.tweak("ChatInput-abcd": { files: file.filePath }).run("What can you see in this image?"); +const response = await flow + .tweak("ChatInput-abcd": { files: file.filePath }) + .run("What can you see in this image?"); ``` > [!WARNING] > DataStax Langflow doesn't make file upload available, you will receive a 501 Not Implemented error. +#### File uploads + +The [Langflow v2 file upload API](https://docs.langflow.org/api-files#filesv2-endpoints) supports uploading files to a user. These can then be used with the [File component](https://docs.langflow.org/components-data#file). + +> [!WARNING] +> The v2 file upload and the File component don't support image uploads. For images you should use the [v1 file upload API](#images). + +You can upload files like this: + +```js +const buffer = await readFile(path); +const file = new File([buffer], "document.pdf", { type: "application/pdf" }); + +const fileUpload = await client.files.upload(file); +console.log(file); +// => { path: "abc123/document.pdf", name: "document", ... } +``` + +You can then send them to the file component in a flow using a tweak. + +```js +const flow = client.flow(flowId); +flow.tweak("File-abc123", { + path: fileUpload.path, +}); +``` + +You can also list your uploaded files with the `files.list()` function: + +```js +const files = await client.files.list(); + +console.log(files); +// [{ path: "...", }, ...] +``` + +> [!NOTE] +> TODO: other file methods available through the API: download, edit, delete, and delete all + ### Logs [Langflow documentation for the logs API](https://docs.langflow.org/api/logs). diff --git a/package-lock.json b/package-lock.json index 71ffde2..1acee02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.3.1", "license": "Apache-2.0", "dependencies": { - "mime": "^4.0.6", "undici": "^7.2.1" }, "devDependencies": { @@ -20,7 +19,7 @@ "globals": "^15.14.0", "prettier": "^3.4.1", "tsx": "^4.19.2", - "typescript": "^5.7.2", + "typescript": "^5.8.3", "typescript-eslint": "^8.16.0" } }, @@ -33,74 +32,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", @@ -118,346 +49,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -916,10 +507,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1060,10 +652,11 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1696,10 +1289,11 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1998,20 +1592,6 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.6.tgz", - "integrity": "sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==", - "funding": [ - "https://github.com/sponsors/broofa" - ], - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2449,10 +2029,11 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2529,10 +2110,11 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 2369ce0..0aa9012 100644 --- a/package.json +++ b/package.json @@ -55,11 +55,10 @@ "globals": "^15.14.0", "prettier": "^3.4.1", "tsx": "^4.19.2", - "typescript": "^5.7.2", + "typescript": "^5.8.3", "typescript-eslint": "^8.16.0" }, "dependencies": { - "mime": "^4.0.6", "undici": "^7.2.1" }, "repository": { diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..28becaf --- /dev/null +++ b/src/files.ts @@ -0,0 +1,56 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { LangflowClient } from "./index.js"; +import { UserFile } from "./user_file.js"; +import { LangflowUploadResponseUserFile } from "./types.js"; +import { FormData, Headers } from "undici"; + +export class Files { + client: LangflowClient; + + constructor(client: LangflowClient) { + this.client = client; + } + + async upload(file: File | Blob, options: { signal?: AbortSignal } = {}) { + const formData = new FormData(); + formData.append("file", file); + + const headers = new Headers(); + headers.set("Accept", "application/json"); + + const response = (await this.client.request({ + path: "/v2/files", + method: "POST", + body: formData, + headers, + signal: options.signal, + })) as LangflowUploadResponseUserFile; + return new UserFile(response); + } + + async list(options: { signal?: AbortSignal } = {}) { + const headers = new Headers(); + headers.set("Accept", "application/json"); + const { signal } = options; + const response = (await this.client.request({ + path: "/v2/files", + method: "GET", + headers, + signal, + })) as LangflowUploadResponseUserFile[]; + return response.map((file) => new UserFile(file)); + } +} diff --git a/src/flow.ts b/src/flow.ts index e8c361c..f184a20 100644 --- a/src/flow.ts +++ b/src/flow.ts @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import mime from "mime"; -import { FormData } from "undici"; +import { FormData, Headers } from "undici"; import { LangflowClient } from "./index.js"; import { FlowResponse } from "./flow_response.js"; @@ -27,9 +26,6 @@ import { StreamEvent, } from "./types.js"; -import { readFile } from "node:fs/promises"; -import { extname, basename } from "node:path"; - export class Flow { client: LangflowClient; id: string; @@ -110,11 +106,8 @@ export class Flow { return streamingResult; } - async uploadFile(path: string, options: { signal?: AbortSignal } = {}) { - const data = await readFile(path); + async uploadFile(file: File | Blob, options: { signal?: AbortSignal } = {}) { const { signal } = options; - const type = mime.getType(extname(path)); - const file = new File([data], basename(path), type ? { type } : {}); const form = new FormData(); form.append("file", file); diff --git a/src/index.ts b/src/index.ts index 2a1507f..3c1752d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,16 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { fetch } from "undici"; +import { fetch, Headers } from "undici"; import pkg from "../package.json" with { type: "json" }; import { LangflowError, LangflowRequestError } from "./errors.js"; import { Flow } from "./flow.js"; import { Logs } from "./logs.js"; +import { Files } from "./files.js"; import type { LangflowClientOptions, RequestOptions, Tweaks } from "./types.js"; import { DATASTAX_LANGFLOW_BASE_URL } from "./consts.js"; -import { platform, arch } from "os"; +import { platform, arch } from "node:os"; import { NDJSONStream } from "./ndjson.js"; export class LangflowClient { @@ -32,6 +33,7 @@ export class LangflowClient { fetch: typeof fetch; defaultHeaders: Headers; logs: Logs; + files: Files; constructor(opts: LangflowClientOptions) { this.baseUrl = this.#resolveBaseUrl(opts); @@ -61,6 +63,7 @@ export class LangflowClient { throw new TypeError("langflowId is not supported"); } this.logs = new Logs(this); + this.files = new Files(this); } #resolveBaseUrl(opts: LangflowClientOptions) { @@ -108,14 +111,16 @@ export class LangflowClient { } } - #setHeaders(headers: Headers) { + #setHeaders(headers: Headers | Record) { + const newHeaders = + headers instanceof Headers ? headers : new Headers(headers); for (const [header, value] of this.defaultHeaders.entries()) { - if (!headers.has(header)) { - headers.set(header, value); + if (!newHeaders.has(header)) { + newHeaders.set(header, value); } } if (this.apiKey) { - this.#setApiKey(this.apiKey, headers); + this.#setApiKey(this.apiKey, newHeaders); } } diff --git a/src/test/files.test.ts b/src/test/files.test.ts new file mode 100644 index 0000000..b252679 --- /dev/null +++ b/src/test/files.test.ts @@ -0,0 +1,117 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it } from "node:test"; +import * as assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { LangflowClient } from "../index.js"; +import { createMockFetch, assertBlobEqual } from "./utils.js"; +import { Headers, FormData } from "undici"; +import { UserFile } from "../user_file.js"; + +const baseUrl = "http://localhost:3000"; + +describe("Files API v2", () => { + const uploadResponse = { + id: "b8fdff49-024e-48e2-acdd-7cd1e4d32d46", + name: "bodi", + path: "579f0128-52e1-4cf7-b5d4-5091d2697f1e/b8fdff49-024e-48e2-acdd-7cd1e4d32d46.jpg", + size: 29601, + provider: undefined, + }; + + const listUserFile = { + ...uploadResponse, + created_at: "2025-06-11T07:34:43.603Z", + updated_at: "2025-06-11T07:34:43.603Z", + user_id: "user_id1234", + }; + + describe("upload", async () => { + const buffer = await readFile( + new URL("fixtures/bodi.jpg", import.meta.url) + ); + + it("uploads a File object successfully", async () => { + const file = new File([buffer], "bodi.jpg"); + + const fetcher = createMockFetch(uploadResponse, async (input, init) => { + const url = new URL(input.toString()); + assert.equal(url.pathname, "/api/v2/files"); + assert.equal(init?.method, "POST"); + const body = init?.body as FormData; + assert.equal(body.get("file"), file); + assert.equal( + new Headers(init?.headers).get("Accept"), + "application/json" + ); + }); + + const client = new LangflowClient({ + baseUrl, + fetch: fetcher, + }); + + const userUpload = await client.files.upload(file); + assert.equal(userUpload.id, uploadResponse.id); + assert.ok(userUpload instanceof UserFile); + }); + + it("uploads a Blob object successfully", async () => { + const blob = new Blob([buffer]); + + const fetcher = createMockFetch(uploadResponse, async (input, init) => { + const url = new URL(input.toString()); + assert.equal(url.pathname, "/api/v2/files"); + assert.equal(init?.method, "POST"); + const body = init?.body as FormData; + const uploadedFile = body.get("file") as File; + await assertBlobEqual(uploadedFile, blob); + assert.equal( + new Headers(init?.headers).get("Accept"), + "application/json" + ); + }); + + const client = new LangflowClient({ + baseUrl, + fetch: fetcher, + }); + + const userUpload = await client.files.upload(blob); + assert.equal(userUpload.id, uploadResponse.id); + assert.ok(userUpload instanceof UserFile); + }); + }); + + describe("list", async () => { + const fetcher = createMockFetch([listUserFile], async (input, init) => { + const url = new URL(input.toString()); + assert.equal(url.pathname, "/api/v2/files"); + assert.equal(init?.method, "GET"); + assert.equal( + new Headers(init?.headers).get("Accept"), + "application/json" + ); + }); + + const client = new LangflowClient({ + baseUrl, + fetch: fetcher, + }); + + const listOfFiles = await client.files.list(); + assert.deepEqual(listOfFiles, [new UserFile(listUserFile)]); + }); +}); diff --git a/src/test/flow.test.ts b/src/test/flow.test.ts index 500ce09..19649c7 100644 --- a/src/test/flow.test.ts +++ b/src/test/flow.test.ts @@ -21,8 +21,9 @@ import { FlowResponse } from "../flow_response.js"; import { UploadResponse } from "../upload_response.js"; import { describe, it } from "node:test"; -import * as assert from "node:assert"; +import * as assert from "node:assert/strict"; import { join } from "node:path"; +import { readFile } from "node:fs/promises"; describe("Flow", () => { const baseUrl = "http://localhost:7860"; @@ -58,8 +59,8 @@ describe("Flow", () => { it("sends a request to the server at the run endpoint providing options", async () => { const fetcher = createMockFetch( { session_id: "session-id", outputs: [] }, - (input, init) => { - assert.equal(input, `${baseUrl}/api/v1/run/flow-id`); + async (input, init) => { + assert.equal(String(input), `${baseUrl}/api/v1/run/flow-id`); assert.equal(init?.method, "POST"); } ); @@ -77,8 +78,8 @@ describe("Flow", () => { it("sends a request to the server at the run endpoint providing options", async () => { const fetcher = createMockFetch( { session_id: "session-id", outputs: [] }, - (input, init) => { - assert.equal(input, `${baseUrl}/api/v1/run/flow-id`); + async (input, init) => { + assert.equal(String(input), `${baseUrl}/api/v1/run/flow-id`); assert.equal(init?.method, "POST"); assert.match(String(init?.body), /"input_type":"chat"/); } @@ -102,22 +103,29 @@ describe("Flow", () => { it("reads the file and sends it to the server", async () => { const fetcher = createMockFetch( { flowId, file_path: "folder/date-filename.jpg" }, - (input, init) => { - assert.equal(input, `${baseUrl}/api/v1/files/upload/${flowId}`); + async (input, init) => { + assert.equal( + String(input), + `${baseUrl}/api/v1/files/upload/${flowId}` + ); assert.equal(init?.method, "POST"); assert.ok(init?.body instanceof FormData); assert.ok(init?.body.has("file")); } ); + + const buffer = await readFile( + join(import.meta.dirname, "fixtures", "bodi.jpg") + ); + const file = new File([buffer], "bodi.jpg", { type: "image/jpeg" }); + const client = new LangflowClient({ baseUrl: "http://localhost:7860", fetch: fetcher, }); const flow = new Flow(client, flowId); - const result = await flow.uploadFile( - join(import.meta.dirname, "fixtures", "bodi.jpg") - ); + const result = await flow.uploadFile(file); assert.ok(result instanceof UploadResponse); assert.equal(result.flowId, flowId); }); diff --git a/src/test/flow_response.test.ts b/src/test/flow_response.test.ts index 7f278c6..12fedcc 100644 --- a/src/test/flow_response.test.ts +++ b/src/test/flow_response.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "node:test"; -import * as assert from "node:assert"; +import * as assert from "node:assert/strict"; import { join } from "node:path"; import { readFile } from "node:fs/promises"; import { FlowResponse } from "../flow_response.js"; diff --git a/src/test/index.test.ts b/src/test/index.test.ts index 7d7b550..010e7e4 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -21,7 +21,7 @@ import { DATASTAX_LANGFLOW_BASE_URL } from "../consts.js"; import { createMockFetch } from "./utils.js"; import { describe, it } from "node:test"; -import * as assert from "node:assert"; +import * as assert from "node:assert/strict"; describe("LangflowClient", () => { describe("with a DataStax API URL", () => { @@ -79,9 +79,9 @@ describe("LangflowClient", () => { it("makes a request to the baseURL with the full path to the method", async () => { const fetcher = createMockFetch( { session_id: "session-id", outputs: [] }, - (input, init) => { + async (input, init) => { assert.equal( - input, + String(input), `${baseUrl}/lf/${langflowId}/api/v1/run/flow-id` ); assert.equal(init?.method, "POST"); @@ -110,7 +110,7 @@ describe("LangflowClient", () => { it("includes the API key in the Authorization header", async () => { const fetcher = createMockFetch( { session_id: "session-id", outputs: [] }, - (input, init) => { + async (input, init) => { const headers = new Headers(init?.headers); assert.equal(headers.get("Authorization"), `Bearer ${apiKey}`); } @@ -137,7 +137,7 @@ describe("LangflowClient", () => { it("includes a user agent in the headers", async () => { const fetcher = createMockFetch( { session_id: "session-id", outputs: [] }, - (input, init) => { + async (input, init) => { const headers = new Headers(init?.headers); const userAgent = headers.get("User-Agent"); assert.ok(userAgent); @@ -168,7 +168,7 @@ describe("LangflowClient", () => { it("throws a LangflowError if the response is not ok", async () => { const response = { details: "blah" }; - const fetcher = createMockFetch(response, () => {}, { + const fetcher = createMockFetch(response, async () => {}, { ok: false, status: 401, statusText: "Unauthorized", @@ -200,7 +200,7 @@ describe("LangflowClient", () => { }); it("throws a LangflowRequestError if the request fails", async () => { - const fetcher = createMockFetch({ details: "blah" }, () => {}, { + const fetcher = createMockFetch({ details: "blah" }, async () => {}, { ok: false, status: 500, statusText: "Internal Server Error", @@ -293,8 +293,8 @@ describe("LangflowClient", () => { it("makes a request to the baseURL with the full path to the method", async () => { const fetcher = createMockFetch( { session_id: "session-id", outputs: [] }, - (input, init) => { - assert.equal(input, `${baseUrl}/api/v1/run/flow-id`); + async (input, init) => { + assert.equal(String(input), `${baseUrl}/api/v1/run/flow-id`); assert.equal(init?.method, "POST"); } ); @@ -319,7 +319,7 @@ describe("LangflowClient", () => { it("includes the API key in the Authorization header", async () => { const fetcher = createMockFetch( { session_id: "session-id", outputs: [] }, - (input, init) => { + async (input, init) => { const headers = new Headers(init?.headers); assert.equal(headers.get("x-api-key"), apiKey); } @@ -344,7 +344,7 @@ describe("LangflowClient", () => { it("throws a LangflowError if the response is not ok", async () => { const response = { details: "blah" }; - const fetcher = createMockFetch(response, () => {}, { + const fetcher = createMockFetch(response, async () => {}, { ok: false, status: 401, statusText: "Unauthorized", @@ -399,7 +399,7 @@ describe("LangflowClient", () => { ]; const fetcher = createMockFetch( events, - (input, init) => { + async (input, init) => { const url = new URL(input); assert.equal(url.searchParams.get("stream"), "true"); assert.equal(url.pathname, "/api/v1/run/flow-id"); @@ -435,7 +435,7 @@ describe("LangflowClient", () => { it("includes the API key in x-api-key header for streams", async () => { const fetcher = createMockFetch( {}, - (input, init) => { + async (input, init) => { const headers = new Headers(init?.headers); assert.equal(headers.get("x-api-key"), apiKey); }, @@ -492,7 +492,7 @@ describe("LangflowClient", () => { }); it("throws LangflowError if stream response has no body", async () => { - const fetcher = createMockFetch({}, () => {}, { + const fetcher = createMockFetch({}, async () => {}, { ok: true, }); @@ -516,7 +516,7 @@ describe("LangflowClient", () => { }); it("throws LangflowError if stream response is not ok", async () => { - const fetcher = createMockFetch({}, () => {}, { + const fetcher = createMockFetch({}, async () => {}, { ok: false, status: 401, statusText: "Unauthorized", diff --git a/src/test/logs.test.ts b/src/test/logs.test.ts index b924388..ce23d7a 100644 --- a/src/test/logs.test.ts +++ b/src/test/logs.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "node:test"; -import * as assert from "node:assert"; +import * as assert from "node:assert/strict"; import { LangflowClient } from "../index.js"; import { createMockFetch } from "./utils.js"; import { LangflowRequestError } from "../errors.js"; @@ -15,7 +15,7 @@ describe("Logs", () => { "2025-02-13T12:01:00.000Z": "Second log message", }; - const fetcher = createMockFetch(logData, (input, init) => { + const fetcher = createMockFetch(logData, async (input, init) => { const url = new URL(input.toString()); assert.equal(url.pathname, "/logs"); assert.equal(init?.method, "GET"); @@ -40,7 +40,7 @@ describe("Logs", () => { it("fetches logs with timestamp option", async () => { const timestamp = Date.now(); - const fetcher = createMockFetch({}, (input) => { + const fetcher = createMockFetch({}, async (input) => { const url = new URL(input.toString()); assert.equal(url.searchParams.get("timestamp"), timestamp.toString()); }); @@ -54,7 +54,7 @@ describe("Logs", () => { }); it("fetches logs with lines_before option", async () => { - const fetcher = createMockFetch({}, (input) => { + const fetcher = createMockFetch({}, async (input) => { const url = new URL(input.toString()); assert.equal(url.searchParams.get("lines_before"), "10"); }); @@ -69,7 +69,7 @@ describe("Logs", () => { it("fetches logs with lines_after option", async () => { const timestamp = Date.now(); - const fetcher = createMockFetch({}, (input) => { + const fetcher = createMockFetch({}, async (input) => { const url = new URL(input.toString()); assert.equal(url.searchParams.get("lines_after"), "5"); assert.equal(url.searchParams.get("timestamp"), timestamp.toString()); @@ -93,7 +93,7 @@ describe("Logs", () => { const fetcher = createMockFetch( logData, - (input, init) => { + async (input, init) => { const url = new URL(input.toString()); assert.equal(url.pathname, "/logs-stream"); assert.equal(init?.method, "GET"); @@ -129,7 +129,7 @@ describe("Logs", () => { }); it("handles stream errors", async () => { - const fetcher = createMockFetch({}, () => {}, { + const fetcher = createMockFetch({}, async () => {}, { ok: false, status: 500, statusText: "Internal Server Error", diff --git a/src/test/ndjson.test.ts b/src/test/ndjson.test.ts index fdc2394..20f3559 100644 --- a/src/test/ndjson.test.ts +++ b/src/test/ndjson.test.ts @@ -15,7 +15,7 @@ import { NDJSONStream } from "../ndjson.js"; import { describe, it } from "node:test"; -import * as assert from "node:assert"; +import * as assert from "node:assert/strict"; describe("NDJSONStream", () => { async function collectStream(readable: ReadableStream) { diff --git a/src/test/user_file.test.ts b/src/test/user_file.test.ts new file mode 100644 index 0000000..40ad9e6 --- /dev/null +++ b/src/test/user_file.test.ts @@ -0,0 +1,40 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it } from "node:test"; +import * as assert from "node:assert/strict"; +import { UserFile } from "../user_file.js"; + +const userFileData = { + id: "b8fdff49-024e-48e2-acdd-7cd1e4d32d46", + name: "bodi", + path: "579f0128-52e1-4cf7-b5d4-5091d2697f1e/b8fdff49-024e-48e2-acdd-7cd1e4d32d46.jpg", + size: 29601, + provider: undefined, + created_at: "2025-06-11T07:34:43.603Z", + updated_at: "2025-06-11T07:34:43.603Z", + user_id: "user_id1234", +}; + +describe("UserFile", () => { + const userFile = new UserFile(userFileData); + + it("converts created_at string to date", () => { + assert.ok(userFile.created_at instanceof Date); + }); + + it("converts updated_at string to date", () => { + assert.ok(userFile.updated_at instanceof Date); + }); +}); diff --git a/src/test/utils.ts b/src/test/utils.ts index 7802076..ad42f97 100644 --- a/src/test/utils.ts +++ b/src/test/utils.ts @@ -13,12 +13,13 @@ // limitations under the License. import { type Response, Request, RequestInit } from "undici"; +import { AssertionError } from "node:assert"; type RequestInfo = string | URL | Request; export function createMockFetch( response: T, - spiesOn: (input: RequestInfo, init?: RequestInit) => void, + spiesOn: (input: RequestInfo, init?: RequestInit) => Promise, options?: { ok?: boolean; status?: number; @@ -26,8 +27,11 @@ export function createMockFetch( body?: ReadableStream; } ): (input: RequestInfo, init?: RequestInit) => Promise { - return function (input: RequestInfo, init?: RequestInit): Promise { - spiesOn(input, init); + return async function ( + input: RequestInfo, + init?: RequestInit + ): Promise { + await spiesOn(input, init); if (options?.status && options.status >= 500) { return Promise.reject( new TypeError(options?.statusText ?? "Internal Server Error") @@ -43,3 +47,37 @@ export function createMockFetch( } as Response); }; } + +export async function assertBlobEqual(actual: Blob, expected: Blob) { + const arrayBufferActual = await actual.arrayBuffer(); + const arrayBufferExpected = await expected.arrayBuffer(); + if (arrayBufferActual.byteLength !== arrayBufferExpected.byteLength) { + throw new AssertionError({ + message: "Files are different sizes", + actual, + expected, + }); + } + + const uint8ArrayActual = new Uint8Array(arrayBufferActual); + const uint8ArrayExpected = new Uint8Array(arrayBufferExpected); + + let result = true; + + for (let i = 0; i < uint8ArrayActual.length; i++) { + if (uint8ArrayActual[i] !== uint8ArrayExpected[i]) { + result = false; + break; + } + } + + if (result) { + return true; + } else { + throw new AssertionError({ + message: "Files are the same size but the contents are different", + actual, + expected, + }); + } +} diff --git a/src/types.ts b/src/types.ts index bc7a51c..f7a8c3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { fetch, FormData } from "undici"; +import type { fetch, FormData, Headers } from "undici"; import { InputTypes, OutputTypes } from "./consts.js"; @@ -41,7 +41,7 @@ export interface RequestOptions { method: string; query?: Record; body?: string | FormData; - headers: Headers; + headers: Headers | Record; signal?: AbortSignal; } @@ -59,6 +59,17 @@ export interface LangflowUploadResponse { file_path: string; } +export interface LangflowUploadResponseUserFile { + id: string; + user_id: string; + name: string; + path: string; + size: number; + provider?: string; + updated_at?: string; + created_at?: string; +} + type TokenStreamEvent = { event: "token"; data: { diff --git a/src/user_file.ts b/src/user_file.ts new file mode 100644 index 0000000..db2c2d1 --- /dev/null +++ b/src/user_file.ts @@ -0,0 +1,46 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { LangflowUploadResponseUserFile } from "./types.js"; + +export class UserFile { + id: string; + name: string; + path: string; + size: number; + provider?: string; + user_id?: string; + created_at?: Date; + updated_at?: Date; + + constructor({ + id, + name, + path, + size, + provider, + user_id, + created_at, + updated_at, + }: LangflowUploadResponseUserFile) { + this.id = id; + this.name = name; + this.path = path; + this.size = size; + this.provider = provider; + this.user_id = user_id; + this.created_at = created_at ? new Date(created_at) : undefined; + this.updated_at = updated_at ? new Date(updated_at) : undefined; + } +}