Skip to content

Commit 48f2a8d

Browse files
amirai21asafgardin
authored andcommitted
feat: support node path and file object\ and browser file object
1 parent 54a96b1 commit 48f2a8d

File tree

4 files changed

+93
-52
lines changed

4 files changed

+93
-52
lines changed

examples/studio/conversational-rag/rag-engine.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AI21 } from 'ai21';
2-
import { FileResponse, UploadFileResponse } from '../../../src/types/rag';
2+
import { FilePathOrFileObject, FileResponse, UploadFileResponse } from '../../../src/types/rag';
33

44
async function waitForFileProcessing(
55
client: AI21,
@@ -18,21 +18,14 @@ async function waitForFileProcessing(
1818
}
1919
}
2020

21-
async function uploadQueryUpdateDelete() {
21+
async function uploadQueryUpdateDelete(fileInput) {
2222
const client = new AI21({ apiKey: process.env.AI21_API_KEY });
2323
try {
2424
const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(
25-
'/Users/amirkoblyansky/Documents/ukraine.txt',
25+
fileInput,
2626
{ path: 'test10' },
2727
);
2828

29-
// const fileContent = Buffer.from('This is the content of the file.');
30-
// const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' });
31-
32-
// // Use the File object in the create method
33-
// const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(dummyFile, {
34-
// path: 'test10',
35-
// });
3629

3730
const fileId = uploadFileResponse.fileId;
3831
let file: FileResponse = await waitForFileProcessing(client, fileId);
@@ -59,6 +52,10 @@ async function listFiles() {
5952
console.log(files);
6053
}
6154

62-
uploadQueryUpdateDelete().catch(console.error);
55+
const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt'
56+
const fileContent = Buffer.from('This is the content of the file.');
57+
const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' });
58+
59+
uploadQueryUpdateDelete(dummyFile).catch(console.error);
6360

64-
listFiles().catch(console.error);
61+
// listFiles().catch(console.error);

src/APIClient.ts

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { Fetch } from 'fetch';
1515
import { createReadStream } from 'fs';
1616
import { basename as getBasename } from 'path';
1717
import FormData from 'form-data';
18-
import { FilePathOrFileObject } from 'types/rag';
18+
import { FilePathOrFileObject } from './types/rag';
19+
import { createFormData, getBoundary, appendBodyToFormData } from './files/form-utils';
1920

2021
const validatePositiveInteger = (name: string, n: unknown): number => {
2122
if (typeof n !== 'number' || !Number.isInteger(n)) {
@@ -27,33 +28,6 @@ const validatePositiveInteger = (name: string, n: unknown): number => {
2728
return n;
2829
};
2930

30-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31-
const appendBodyToFormData = (formData: FormData, body: Record<string, any>): void => {
32-
for (const [key, value] of Object.entries(body)) {
33-
if (Array.isArray(value)) {
34-
value.forEach((item) => formData.append(key, item));
35-
} else {
36-
formData.append(key, value);
37-
}
38-
}
39-
};
40-
41-
42-
function makeFormDataFromFilePath(filePath: string): FormData {
43-
const formData = new FormData();
44-
const fileStream = createReadStream(filePath);
45-
const fileName = getBasename(filePath);
46-
47-
formData.append('file', fileStream, fileName);
48-
return formData;
49-
}
50-
51-
function makeFormDataFromFileObject(file: File): FormData {
52-
const formData = new FormData();
53-
formData.append('file', file);
54-
return formData;
55-
}
56-
5731
export abstract class APIClient {
5832
protected baseURL: string;
5933
protected maxRetries: number;
@@ -92,16 +66,8 @@ export abstract class APIClient {
9266
return this.makeRequest('delete', path, opts);
9367
}
9468

95-
upload<Req, Rsp>(path: string, file: FilePathOrFileObject, opts?: RequestOptions<Req>): Promise<Rsp> {
96-
let formData: FormData;
97-
98-
if (typeof file === 'string') {
99-
formData = makeFormDataFromFilePath(file);
100-
} else if (file instanceof File) {
101-
formData = makeFormDataFromFileObject(file);
102-
} else {
103-
throw new AI21Error('Invalid file type for upload');
104-
}
69+
async upload<Req, Rsp>(path: string, file: FilePathOrFileObject, opts?: RequestOptions<Req>): Promise<Rsp> {
70+
const formData = await createFormData(file);
10571

10672
if (opts?.body) {
10773
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -110,7 +76,7 @@ export abstract class APIClient {
11076

11177
const headers = {
11278
...opts?.headers,
113-
'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
79+
'Content-Type': `multipart/form-data; boundary=${await getBoundary(formData)}`,
11480
};
11581

11682
const options: FinalRequestOptions = {
@@ -224,3 +190,4 @@ export abstract class APIClient {
224190
);
225191
}
226192
}
193+

src/files/form-utils.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { FilePathOrFileObject } from 'types/rag';
2+
import * as Runtime from '../runtime';
3+
4+
export type UnifiedFormData = FormData | import('form-data');
5+
6+
import { Readable } from 'stream';
7+
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
export const appendBodyToFormData = (formData: UnifiedFormData, body: Record<string, any>): void => {
10+
for (const [key, value] of Object.entries(body)) {
11+
if (Array.isArray(value)) {
12+
value.forEach((item) => formData.append(key, item));
13+
} else {
14+
formData.append(key, value);
15+
}
16+
}
17+
};
18+
19+
// Convert WHATWG ReadableStream to Node.js Readable Stream
20+
function convertReadableStream(whatwgStream: ReadableStream): Readable {
21+
const reader = whatwgStream.getReader();
22+
23+
return new Readable({
24+
async read() {
25+
const { done, value } = await reader.read();
26+
if (done) {
27+
this.push(null);
28+
} else {
29+
this.push(value);
30+
}
31+
},
32+
});
33+
}
34+
35+
36+
export async function createFormData(file: FilePathOrFileObject): Promise<UnifiedFormData> {
37+
38+
if (Runtime.isBrowser) {
39+
const formData = new FormData();
40+
41+
if (file instanceof window.File) {
42+
formData.append('file', file);
43+
} else {
44+
throw new Error('Unsupported file type in browser');
45+
}
46+
47+
return formData;
48+
} else { // Node environment:
49+
50+
const { default: FormDataNode } = await import('form-data');
51+
const formData = new FormDataNode();
52+
53+
if (typeof file === 'string') {
54+
const { createReadStream } = await import('fs');
55+
formData.append('file', createReadStream(file), { filename: file.split('/').pop() });
56+
} else if (Buffer.isBuffer(file)) {
57+
formData.append('file', file, { filename: 'TODO - add filename to buffer flow' });
58+
} else if (file instanceof File) {
59+
const nodeStream = convertReadableStream(file.stream());
60+
formData.append('file', nodeStream, file.name);
61+
} else {
62+
throw new Error('Unsupported file type in Node.js');
63+
}
64+
65+
return formData;
66+
}
67+
}
68+
69+
export async function getBoundary(formData: UnifiedFormData): Promise<string | undefined> {
70+
if (!Runtime.isBrowser) {
71+
const { default: FormDataNode } = await import('form-data');
72+
if (formData instanceof FormDataNode) {
73+
return formData.getBoundary();
74+
}
75+
}
76+
return undefined;
77+
}

vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default defineConfig({
1010
formats: ['es', 'cjs', 'umd'],
1111
},
1212
rollupOptions: {
13-
external: ['node-fetch'],
13+
external: ['node-fetch', 'fs', 'path', 'stream', 'form-data'],
1414
output: {
1515
globals: {
1616
'node-fetch': 'fetch',

0 commit comments

Comments
 (0)