Skip to content

Commit b71e598

Browse files
amirai21asafgardin
authored andcommitted
feat: reorganized
1 parent 02b40df commit b71e598

File tree

10 files changed

+155
-138
lines changed

10 files changed

+155
-138
lines changed
Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,26 @@
11
import { AI21 } from 'ai21';
22
import { FileResponse, UploadFileResponse } from '../../../src/types/rag';
33

4-
async function waitForFileProcessing(
5-
client: AI21,
6-
fileId: string,
7-
interval: number = 3000,
8-
): Promise<FileResponse> {
9-
while (true) {
10-
const file: FileResponse = await client.ragEngine.get(fileId);
11-
12-
if (file.status === 'PROCESSED') {
13-
return file;
14-
}
15-
16-
console.log(`File status is '${file.status}'. Waiting for it to be 'PROCESSED'...`);
17-
await new Promise((resolve) => setTimeout(resolve, interval));
18-
}
4+
function sleep(ms) {
5+
return new Promise(resolve => setTimeout(resolve, ms));
196
}
207

21-
async function uploadQueryUpdateDelete(fileInput, label) {
8+
async function uploadGetUpdateDelete(fileInput, label) {
229
const client = new AI21({ apiKey: process.env.AI21_API_KEY });
2310
try {
2411
const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(fileInput, {
2512
path: label,
2613
});
2714

28-
const fileId = uploadFileResponse.fileId;
29-
let file: FileResponse = await waitForFileProcessing(client, fileId);
15+
let file: FileResponse = await client.ragEngine.get(uploadFileResponse.fileId);
3016
console.log(file);
31-
32-
console.log('Now updating the file labels');
17+
await sleep(1000); // Give it a sec to start process before updating
18+
console.log('Now updating the file labels and publicUrl...');
3319
await client.ragEngine.update(uploadFileResponse.fileId, {
3420
labels: ['test99'],
3521
publicUrl: 'https://www.miri.com',
3622
});
37-
file = await client.ragEngine.get(fileId);
23+
file = await client.ragEngine.get(uploadFileResponse.fileId);
3824
console.log(file);
3925

4026
console.log('Now deleting the file');
@@ -50,11 +36,13 @@ async function listFiles() {
5036
console.log(files);
5137
}
5238

39+
/* Simulate a file upload passing file path */
5340
const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt';
54-
const fileContent = Buffer.from('This is the content of the file.');
55-
const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' });
41+
uploadGetUpdateDelete(filePath, Date.now().toString()).catch(console.error);
5642

57-
uploadQueryUpdateDelete(filePath, "abc123").catch(console.error);
58-
uploadQueryUpdateDelete(dummyFile, "test2").catch(console.error);
43+
/* Simulate a file upload passing File instance */
44+
const fileContent = Buffer.from('Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.');
45+
const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' });
46+
uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error);
5947

6048
listFiles().catch(console.error);

src/APIClient.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import {
88
HTTPMethod,
99
Headers,
1010
CrossPlatformResponse,
11-
UnifiedFormData,
1211
} from './types';
1312
import { AI21EnvConfig } from './EnvConfig';
14-
import { createFetchInstance } from './runtime';
13+
import { createFetchInstance, createFilesHandlerInstance } from './runtime';
1514
import { Fetch } from 'fetch';
16-
import { getBoundary, appendBodyToFormData } from './files/form-utils';
15+
import { FilePathOrFileObject } from 'types/rag';
16+
import { BaseFilesHandler } from 'files/BaseFilesHandler';
1717

1818
const validatePositiveInteger = (name: string, n: unknown): number => {
1919
if (typeof n !== 'number' || !Number.isInteger(n)) {
@@ -30,22 +30,26 @@ export abstract class APIClient {
3030
protected maxRetries: number;
3131
protected timeout: number;
3232
protected fetch: Fetch;
33+
protected filesHandler: BaseFilesHandler;
3334

3435
constructor({
3536
baseURL,
3637
maxRetries = AI21EnvConfig.MAX_RETRIES,
3738
timeout = AI21EnvConfig.TIMEOUT_SECONDS,
3839
fetch = createFetchInstance(),
40+
filesHandler = createFilesHandlerInstance(),
3941
}: {
4042
baseURL: string;
4143
maxRetries?: number | undefined;
4244
timeout: number | undefined;
4345
fetch?: Fetch;
46+
filesHandler?: BaseFilesHandler;
4447
}) {
4548
this.baseURL = baseURL;
4649
this.maxRetries = validatePositiveInteger('maxRetries', maxRetries);
4750
this.timeout = validatePositiveInteger('timeout', timeout);
4851
this.fetch = fetch;
52+
this.filesHandler = filesHandler;
4953
}
5054
get<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
5155
return this.makeRequest('get', path, opts);
@@ -63,16 +67,17 @@ export abstract class APIClient {
6367
return this.makeRequest('delete', path, opts);
6468
}
6569

66-
async upload<Req, Rsp>(path: string, formData: UnifiedFormData, opts?: RequestOptions<Req>): Promise<Rsp> {
70+
async upload<Req, Rsp>(path: string, file: FilePathOrFileObject, opts?: RequestOptions<Req>): Promise<Rsp> {
71+
const formData = await this.filesHandler.createFormData(file);
72+
6773
if (opts?.body) {
6874
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69-
appendBodyToFormData(formData, opts.body as Record<string, any>);
75+
this.filesHandler.appendBodyToFormData(formData, opts.body as Record<string, any>);
7076
}
7177

72-
const boundary = await getBoundary(formData);
7378
const headers = {
7479
...opts?.headers,
75-
'Content-Type': `multipart/form-data; boundary=${boundary}`,
80+
...this.filesHandler.getMultipartFormDataHeaders(formData),
7681
};
7782

7883
const options: FinalRequestOptions = {
@@ -94,26 +99,16 @@ export abstract class APIClient {
9499
}
95100

96101
protected defaultHeaders(opts: FinalRequestOptions): Headers {
97-
return {
102+
const defaultHeaders = {
98103
Accept: 'application/json',
99-
'Content-Type': 'application/json',
100104
'User-Agent': this.getUserAgent(),
101105
...this.authHeaders(opts),
102106
};
103-
// if (opts?.body instanceof FormData) {
104-
// return {
105-
// Accept: 'application/json',
106-
// 'User-Agent': this.getUserAgent(),
107-
// ...this.authHeaders(opts),
108-
// };
109-
// } else {
110-
// return {
111-
// Accept: 'application/json',
112-
// 'Content-Type': 'application/json',
113-
// 'User-Agent': this.getUserAgent(),
114-
// ...this.authHeaders(opts),
115-
// };
116-
// }
107+
108+
if (opts?.body instanceof FormData) {
109+
return defaultHeaders;
110+
}
111+
return { ...defaultHeaders, 'Content-Type': 'application/json' };
117112
}
118113

119114
// eslint-disable-next-line @typescript-eslint/no-unused-vars

src/fetch/BaseFetch.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { AI21Error } from '../errors';
33
import { FinalRequestOptions, CrossPlatformResponse } from '../types';
44
import { APIResponseProps } from '../types/API';
55

6-
export type APIResponse<T> = {
7-
data?: T;
8-
response: CrossPlatformResponse;
9-
};
6+
107
export abstract class BaseFetch {
118
abstract call(url: string, options: FinalRequestOptions): Promise<CrossPlatformResponse>;
129
async handleResponse<T>({ response, options }: APIResponseProps) {

src/files/BaseFilesHandler.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { UnifiedFormData } from "types";
2+
import { FilePathOrFileObject } from "types/rag";
3+
4+
export abstract class BaseFilesHandler {
5+
abstract createFormData(file: FilePathOrFileObject): Promise<UnifiedFormData>;
6+
7+
abstract getMultipartFormDataHeaders(formData: UnifiedFormData): Record<string, string> | null;
8+
9+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10+
appendBodyToFormData = (formData: UnifiedFormData, body: Record<string, any>): void => {
11+
for (const [key, value] of Object.entries(body)) {
12+
if (Array.isArray(value)) {
13+
value.forEach((item) => formData.append(key, item));
14+
} else {
15+
formData.append(key, value);
16+
}
17+
}
18+
};
19+
20+
21+
}

src/files/BrowserFilesHandler.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { UnifiedFormData } from "types";
2+
import { FilePathOrFileObject } from "types/rag";
3+
import { BaseFilesHandler } from "./BaseFilesHandler";
4+
5+
6+
export class BrowserFilesHandler extends BaseFilesHandler {
7+
8+
async createFormData(file: FilePathOrFileObject): Promise<UnifiedFormData> {
9+
const formData = new FormData();
10+
11+
if (file instanceof window.File) {
12+
formData.append('file', file);
13+
} else {
14+
throw new Error('Unsupported file type in browser');
15+
}
16+
17+
return formData;
18+
}
19+
20+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
21+
getMultipartFormDataHeaders(formData: UnifiedFormData): Record<string, string> | null {
22+
return {};
23+
}
24+
}

src/files/NodeFilesHandler.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { UnifiedFormData } from "types";
2+
import { FilePathOrFileObject } from "types/rag";
3+
import { BaseFilesHandler } from "./BaseFilesHandler";
4+
5+
export class NodeFilesHandler extends BaseFilesHandler {
6+
7+
async convertReadableStream(whatwgStream: ReadableStream): Promise<NodeJS.ReadableStream> {
8+
const { Readable } = await import('stream');
9+
const reader = whatwgStream.getReader();
10+
11+
return new Readable({
12+
async read() {
13+
const { done, value } = await reader.read();
14+
if (done) {
15+
this.push(null);
16+
} else {
17+
this.push(value);
18+
}
19+
},
20+
});
21+
}
22+
23+
async getBoundary(formData: UnifiedFormData): Promise<string | undefined> {
24+
const { default: FormDataNode } = await import('form-data');
25+
if (formData instanceof FormDataNode) {
26+
return formData.getBoundary();
27+
}
28+
else {
29+
throw new Error('getBoundary invoked with native browser FormData instance instead of NodeJS form-data');
30+
}
31+
}
32+
33+
34+
async createFormData(file: FilePathOrFileObject): Promise<UnifiedFormData> {
35+
const { default: FormDataNode } = await import('form-data');
36+
const formData = new FormDataNode();
37+
38+
if (typeof file === 'string') {
39+
const fs = (await import('fs')).default;
40+
formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() });
41+
} else if (Buffer.isBuffer(file)) {
42+
formData.append('file', file, { filename: 'TODO - add filename to buffer flow' });
43+
} else if (file instanceof File) {
44+
const nodeStream = await this.convertReadableStream(file.stream());
45+
formData.append('file', nodeStream, file.name);
46+
} else {
47+
throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`);
48+
}
49+
50+
return formData;
51+
}
52+
53+
getMultipartFormDataHeaders(formData: UnifiedFormData): Record<string, string> | null {
54+
if (formData instanceof FormData) {
55+
return null;
56+
}
57+
58+
const boundary = formData.getBoundary();
59+
return {
60+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
61+
};
62+
}
63+
64+
}
65+

src/files/form-utils.ts

Lines changed: 0 additions & 82 deletions
This file was deleted.

src/files/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from './form-utils';
1+
export { BrowserFilesHandler } from './BrowserFilesHandler';
2+
export { NodeFilesHandler } from './NodeFilesHandler';

src/resources/rag/ragEngine.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
FilePathOrFileObject,
99
} from '../../types/rag';
1010
import { FileResponse } from 'types/rag/FileResponse';
11-
import { CreateFormData } from '../../files';
1211

1312
const RAG_ENGINE_PATH = '/library/files';
1413

@@ -30,8 +29,7 @@ export class RAGEngine extends APIResource {
3029
body: UploadFileRequest,
3130
options?: Models.RequestOptions,
3231
): Promise<UploadFileResponse> {
33-
const formData = await new CreateFormData().createFormData(file);
34-
return this.client.upload<Models.UnifiedFormData, UploadFileResponse>(RAG_ENGINE_PATH, formData, {
32+
return this.client.upload<Models.UnifiedFormData, UploadFileResponse>(RAG_ENGINE_PATH, file, {
3533
body: body,
3634
...options,
3735
} as Models.RequestOptions<Models.UnifiedFormData>) as Promise<UploadFileResponse>;

0 commit comments

Comments
 (0)