From 4713d02295287c55542c69837440b977cdb5d706 Mon Sep 17 00:00:00 2001 From: amirk Date: Mon, 25 Nov 2024 11:13:57 +0200 Subject: [PATCH 01/53] feat: rag-engine impl. - with examples, no tests yet --- .../studio/conversational-rag/rag-engine.ts | 48 +++++++++++++++++ src/AI21.ts | 2 + src/APIClient.ts | 54 ++++++++++++++++++- src/fetch/NodeFetch.ts | 5 +- src/resources/index.ts | 1 + src/resources/rag/index.ts | 1 + src/resources/rag/ragEngine.ts | 37 +++++++++++++ src/types/API.ts | 2 +- src/types/rag/FileResponse.ts | 15 ++++++ src/types/rag/ListFilesFilters.ts | 4 ++ src/types/rag/UploadFileRequest.ts | 8 +++ src/types/rag/UploadFileResponse.ts | 4 ++ src/types/rag/index.ts | 10 ++++ 13 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 examples/studio/conversational-rag/rag-engine.ts create mode 100644 src/resources/rag/ragEngine.ts create mode 100644 src/types/rag/FileResponse.ts create mode 100644 src/types/rag/ListFilesFilters.ts create mode 100644 src/types/rag/UploadFileRequest.ts create mode 100644 src/types/rag/UploadFileResponse.ts diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts new file mode 100644 index 0000000..4850164 --- /dev/null +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -0,0 +1,48 @@ +import { AI21 } from 'ai21'; +import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; + +async function waitForFileProcessing(client: AI21, fileId: string, interval: number = 3000): Promise { + while (true) { + const file: FileResponse = await client.ragEngine.get(fileId); + + if (file.status === 'PROCESSED') { + return file; + } + + console.log(`File status is '${file.status}'. Waiting for it to be 'PROCESSED'...`); + await new Promise(resolve => setTimeout(resolve, interval)); + } +} + +async function uploadQueryUpdateDelete() { + const client = new AI21({ apiKey: process.env.AI21_API_KEY }); + try { + const uploadFileResponse: UploadFileResponse = await client.ragEngine.create( + '/Users/amirkoblyansky/Documents/ukraine.txt', {path: "test10"}); + + const fileId = uploadFileResponse.fileId + let file: FileResponse = await waitForFileProcessing(client, fileId); + console.log(file); + + console.log("Now updating the file labels"); + await client.ragEngine.update(uploadFileResponse.fileId, {labels: ["test99"], publicUrl: "https://www.miri.com"}); + file = await client.ragEngine.get(fileId); + console.log(file); + + console.log("Now deleting the file"); + await client.ragEngine.delete(uploadFileResponse.fileId); + } catch (error) { + console.error('Error:', error); + } +} + +async function listFiles() { + const client = new AI21({ apiKey: process.env.AI21_API_KEY }); + const files = await client.ragEngine.list({limit: 10}); + console.log(files); +} + +uploadQueryUpdateDelete().catch(console.error); + +listFiles().catch(console.error); + diff --git a/src/AI21.ts b/src/AI21.ts index ce44b86..30af6dc 100644 --- a/src/AI21.ts +++ b/src/AI21.ts @@ -6,6 +6,7 @@ import { APIClient } from './APIClient'; import { Headers } from './types'; import * as Runtime from './runtime'; import { ConversationalRag } from './resources/rag/conversationalRag'; +import { RAGEngine } from 'resources'; export interface ClientOptions { baseURL?: string | undefined; @@ -67,6 +68,7 @@ export class AI21 extends APIClient { // Resources chat: Chat = new Chat(this); conversationalRag: ConversationalRag = new ConversationalRag(this); + ragEngine: RAGEngine = new RAGEngine(this); // eslint-disable-next-line @typescript-eslint/no-unused-vars protected override authHeaders(_: Types.FinalRequestOptions): Types.Headers { diff --git a/src/APIClient.ts b/src/APIClient.ts index a42cd74..a801aeb 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -12,6 +12,9 @@ import { import { AI21EnvConfig } from './EnvConfig'; import { createFetchInstance } from './runtime'; import { Fetch } from 'fetch'; +import { createReadStream } from 'fs'; +import { basename as getBasename } from 'path'; +import FormData from 'form-data'; const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { @@ -61,6 +64,49 @@ export abstract class APIClient { return this.makeRequest('delete', path, opts); } + upload(path: string, filePath: string, opts?: RequestOptions): Promise { + const formDataRequest = this.makeFormDataRequest(path, filePath, opts); + return this.performRequest(formDataRequest).then( + (response) => this.fetch.handleResponse(response) as Rsp, + ); + } + + protected makeFormDataRequest(path: string, filePath: string, opts?: RequestOptions): FinalRequestOptions { + const formData = new FormData(); + const fileStream = createReadStream(filePath); + const fileName = getBasename(filePath); + + formData.append('file', fileStream, fileName); + + if (opts?.body) { + const body = opts.body as Record; + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach(item => formData.append(key, item)); + } else { + formData.append(key, value); + } + } + } + + const headers = { + ...opts?.headers, + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` + }; + console.log(headers); + console.log("-------------------------"); + console.log(formData.getHeaders()); + console.log("-------------------------"); + + const options: FinalRequestOptions = { + method: 'post', + path: path, + body: formData, + headers, + }; + return options; + } + protected getUserAgent(): string { const platform = this.isRunningInBrowser() ? @@ -96,12 +142,18 @@ export abstract class APIClient { } private async performRequest(options: FinalRequestOptions): Promise { - const url = `${this.baseURL}${options.path}`; + let url = `${this.baseURL}${options.path}`; + + if (options.query) { + const queryString = new URLSearchParams(options.query as Record).toString(); + url += `?${queryString}`; + } const headers = { ...this.defaultHeaders(options), ...options.headers, }; + const response = await this.fetch.call(url, { ...options, headers }); if (!response.ok) { diff --git a/src/fetch/NodeFetch.ts b/src/fetch/NodeFetch.ts index 4c7c764..3d67bf3 100644 --- a/src/fetch/NodeFetch.ts +++ b/src/fetch/NodeFetch.ts @@ -1,16 +1,19 @@ import { FinalRequestOptions, CrossPlatformResponse } from 'types'; import { BaseFetch } from './BaseFetch'; import { Stream, NodeSSEDecoder } from '../streaming'; +import FormData from 'form-data'; export class NodeFetch extends BaseFetch { async call(url: string, options: FinalRequestOptions): Promise { const nodeFetchModule = await import('node-fetch'); const nodeFetch = nodeFetchModule.default; + const body = options.body instanceof FormData ? options.body : JSON.stringify(options.body); + return nodeFetch(url, { method: options.method, headers: options?.headers ? (options.headers as Record) : undefined, - body: options?.body ? JSON.stringify(options.body) : undefined, + body, }); } diff --git a/src/resources/index.ts b/src/resources/index.ts index da47cbf..74ae577 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -1,2 +1,3 @@ export { Chat, Completions } from './chat'; export { ConversationalRag } from './rag'; +export { RAGEngine } from './rag'; diff --git a/src/resources/rag/index.ts b/src/resources/rag/index.ts index 2bb5fca..c349bd2 100644 --- a/src/resources/rag/index.ts +++ b/src/resources/rag/index.ts @@ -1 +1,2 @@ export { ConversationalRag } from './conversationalRag'; +export { RAGEngine } from './ragEngine'; diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts new file mode 100644 index 0000000..1fcdef4 --- /dev/null +++ b/src/resources/rag/ragEngine.ts @@ -0,0 +1,37 @@ +import * as Models from '../../types'; +import { APIResource } from '../../APIResource'; +import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types/rag'; +import { FileResponse } from 'types/rag/FileResponse'; + + +const RAG_ENGINE_PATH = '/library/files'; + + +export class RAGEngine extends APIResource { + create(filePath: string, body: UploadFileRequest, options?: Models.RequestOptions) { + return this.client.upload(RAG_ENGINE_PATH, filePath, { + body: body, + ...options, + } as Models.RequestOptions) as Promise; + } + + get(fileId: string, options?: Models.RequestOptions) { + return this.client.get( + `${RAG_ENGINE_PATH}/${fileId}`, options as Models.RequestOptions) as Promise; + } + + delete(fileId: string, options?: Models.RequestOptions) { + return this.client.delete( + `${RAG_ENGINE_PATH}/${fileId}`, options as Models.RequestOptions) as Promise; + } + + list(body: ListFilesFilters | null, options?: Models.RequestOptions) { + return this.client.get( + RAG_ENGINE_PATH, {query: body, ...options} as Models.RequestOptions) as Promise; + } + + update(fileId: string, body: UpdateFileRequest, options?: Models.RequestOptions) { + return this.client.put( + `${RAG_ENGINE_PATH}/${fileId}`, {body, ...options} as Models.RequestOptions) as Promise; + } +} diff --git a/src/types/API.ts b/src/types/API.ts index 5bf2cbe..b6471ea 100644 --- a/src/types/API.ts +++ b/src/types/API.ts @@ -10,7 +10,7 @@ export type RequestOptions | ArrayBuffer method?: HTTPMethod; path?: string; query?: Req | undefined; - body?: Req | null | undefined; + body?: Req | FormData | null | undefined; headers?: Headers | undefined; maxRetries?: number; diff --git a/src/types/rag/FileResponse.ts b/src/types/rag/FileResponse.ts new file mode 100644 index 0000000..24fb111 --- /dev/null +++ b/src/types/rag/FileResponse.ts @@ -0,0 +1,15 @@ +export interface FileResponse { + fileId: string; + name: string; + fileType: string; + sizeBytes: number; + createdBy: string; + creationDate: Date; + lastUpdated: Date; + status: string; + path?: string | null; + labels?: string[] | null; + publicUrl?: string | null; + errorCode?: number | null; + errorMessage?: string | null; +} diff --git a/src/types/rag/ListFilesFilters.ts b/src/types/rag/ListFilesFilters.ts new file mode 100644 index 0000000..69603c2 --- /dev/null +++ b/src/types/rag/ListFilesFilters.ts @@ -0,0 +1,4 @@ +export interface ListFilesFilters { + offset?: number | null; + limit?: number | null; +} diff --git a/src/types/rag/UploadFileRequest.ts b/src/types/rag/UploadFileRequest.ts new file mode 100644 index 0000000..b95d9c0 --- /dev/null +++ b/src/types/rag/UploadFileRequest.ts @@ -0,0 +1,8 @@ +export interface UpdateFileRequest { + labels?: string[] | null; + publicUrl?: string| null; + } + + export interface UploadFileRequest extends UpdateFileRequest { + path?: string | null; + } \ No newline at end of file diff --git a/src/types/rag/UploadFileResponse.ts b/src/types/rag/UploadFileResponse.ts new file mode 100644 index 0000000..b5fa16c --- /dev/null +++ b/src/types/rag/UploadFileResponse.ts @@ -0,0 +1,4 @@ +export interface UploadFileResponse { + id: string; + fileId: string; +} diff --git a/src/types/rag/index.ts b/src/types/rag/index.ts index 7dc7954..03ee777 100644 --- a/src/types/rag/index.ts +++ b/src/types/rag/index.ts @@ -5,3 +5,13 @@ export { type ConversationalRagSource } from './ConversationalRagSource'; export { type ConversationalRagResponse } from './ConversationalRagResponse'; export { type RetrievalStrategy } from './RetrievalStrategy'; + +export { type UploadFileRequest } from './UploadFileRequest'; + +export { type FileResponse } from './FileResponse'; + +export { type UploadFileResponse } from './UploadFileResponse'; + +export { type ListFilesFilters } from './ListFilesFilters'; + +export { type UpdateFileRequest } from './UploadFileRequest'; From ceaa9e9453d71060e7605a314c05615e0b8897f8 Mon Sep 17 00:00:00 2001 From: amirk Date: Mon, 25 Nov 2024 11:14:06 +0200 Subject: [PATCH 02/53] feat: rag-engine impl. - with examples, no tests yet --- .../studio/conversational-rag/rag-engine.ts | 28 ++++++++++++------- src/APIClient.ts | 14 ++++++---- src/resources/rag/ragEngine.ts | 22 +++++++++------ src/types/rag/UploadFileRequest.ts | 12 ++++---- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 4850164..efcc2d4 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,7 +1,11 @@ import { AI21 } from 'ai21'; import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; -async function waitForFileProcessing(client: AI21, fileId: string, interval: number = 3000): Promise { +async function waitForFileProcessing( + client: AI21, + fileId: string, + interval: number = 3000, +): Promise { while (true) { const file: FileResponse = await client.ragEngine.get(fileId); @@ -10,7 +14,7 @@ async function waitForFileProcessing(client: AI21, fileId: string, interval: num } console.log(`File status is '${file.status}'. Waiting for it to be 'PROCESSED'...`); - await new Promise(resolve => setTimeout(resolve, interval)); + await new Promise((resolve) => setTimeout(resolve, interval)); } } @@ -18,18 +22,23 @@ async function uploadQueryUpdateDelete() { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { const uploadFileResponse: UploadFileResponse = await client.ragEngine.create( - '/Users/amirkoblyansky/Documents/ukraine.txt', {path: "test10"}); - - const fileId = uploadFileResponse.fileId + '/Users/amirkoblyansky/Documents/ukraine.txt', + { path: 'test10' }, + ); + + const fileId = uploadFileResponse.fileId; let file: FileResponse = await waitForFileProcessing(client, fileId); console.log(file); - console.log("Now updating the file labels"); - await client.ragEngine.update(uploadFileResponse.fileId, {labels: ["test99"], publicUrl: "https://www.miri.com"}); + console.log('Now updating the file labels'); + await client.ragEngine.update(uploadFileResponse.fileId, { + labels: ['test99'], + publicUrl: 'https://www.miri.com', + }); file = await client.ragEngine.get(fileId); console.log(file); - console.log("Now deleting the file"); + console.log('Now deleting the file'); await client.ragEngine.delete(uploadFileResponse.fileId); } catch (error) { console.error('Error:', error); @@ -38,11 +47,10 @@ async function uploadQueryUpdateDelete() { async function listFiles() { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); - const files = await client.ragEngine.list({limit: 10}); + const files = await client.ragEngine.list({ limit: 10 }); console.log(files); } uploadQueryUpdateDelete().catch(console.error); listFiles().catch(console.error); - diff --git a/src/APIClient.ts b/src/APIClient.ts index a801aeb..5c35b04 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -71,7 +71,11 @@ export abstract class APIClient { ); } - protected makeFormDataRequest(path: string, filePath: string, opts?: RequestOptions): FinalRequestOptions { + protected makeFormDataRequest( + path: string, + filePath: string, + opts?: RequestOptions, + ): FinalRequestOptions { const formData = new FormData(); const fileStream = createReadStream(filePath); const fileName = getBasename(filePath); @@ -82,7 +86,7 @@ export abstract class APIClient { const body = opts.body as Record; for (const [key, value] of Object.entries(body)) { if (Array.isArray(value)) { - value.forEach(item => formData.append(key, item)); + value.forEach((item) => formData.append(key, item)); } else { formData.append(key, value); } @@ -91,12 +95,12 @@ export abstract class APIClient { const headers = { ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, }; console.log(headers); - console.log("-------------------------"); + console.log('-------------------------'); console.log(formData.getHeaders()); - console.log("-------------------------"); + console.log('-------------------------'); const options: FinalRequestOptions = { method: 'post', diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index 1fcdef4..babf851 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -3,10 +3,8 @@ import { APIResource } from '../../APIResource'; import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; - const RAG_ENGINE_PATH = '/library/files'; - export class RAGEngine extends APIResource { create(filePath: string, body: UploadFileRequest, options?: Models.RequestOptions) { return this.client.upload(RAG_ENGINE_PATH, filePath, { @@ -17,21 +15,29 @@ export class RAGEngine extends APIResource { get(fileId: string, options?: Models.RequestOptions) { return this.client.get( - `${RAG_ENGINE_PATH}/${fileId}`, options as Models.RequestOptions) as Promise; + `${RAG_ENGINE_PATH}/${fileId}`, + options as Models.RequestOptions, + ) as Promise; } delete(fileId: string, options?: Models.RequestOptions) { return this.client.delete( - `${RAG_ENGINE_PATH}/${fileId}`, options as Models.RequestOptions) as Promise; + `${RAG_ENGINE_PATH}/${fileId}`, + options as Models.RequestOptions, + ) as Promise; } list(body: ListFilesFilters | null, options?: Models.RequestOptions) { - return this.client.get( - RAG_ENGINE_PATH, {query: body, ...options} as Models.RequestOptions) as Promise; + return this.client.get(RAG_ENGINE_PATH, { + query: body, + ...options, + } as Models.RequestOptions) as Promise; } update(fileId: string, body: UpdateFileRequest, options?: Models.RequestOptions) { - return this.client.put( - `${RAG_ENGINE_PATH}/${fileId}`, {body, ...options} as Models.RequestOptions) as Promise; + return this.client.put(`${RAG_ENGINE_PATH}/${fileId}`, { + body, + ...options, + } as Models.RequestOptions) as Promise; } } diff --git a/src/types/rag/UploadFileRequest.ts b/src/types/rag/UploadFileRequest.ts index b95d9c0..ad4ca8c 100644 --- a/src/types/rag/UploadFileRequest.ts +++ b/src/types/rag/UploadFileRequest.ts @@ -1,8 +1,8 @@ export interface UpdateFileRequest { - labels?: string[] | null; - publicUrl?: string| null; - } + labels?: string[] | null; + publicUrl?: string | null; +} - export interface UploadFileRequest extends UpdateFileRequest { - path?: string | null; - } \ No newline at end of file +export interface UploadFileRequest extends UpdateFileRequest { + path?: string | null; +} From 657fa5eb7846b7f99119f63b01653b2b7b2cafd5 Mon Sep 17 00:00:00 2001 From: amirk Date: Mon, 25 Nov 2024 11:32:47 +0200 Subject: [PATCH 03/53] feat: makeFormDataRequest - change type and disable lint --- src/APIClient.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/APIClient.ts b/src/APIClient.ts index 5c35b04..277a55b 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -71,11 +71,7 @@ export abstract class APIClient { ); } - protected makeFormDataRequest( - path: string, - filePath: string, - opts?: RequestOptions, - ): FinalRequestOptions { + protected makeFormDataRequest(path: string, filePath: string, opts?: RequestOptions): FinalRequestOptions { const formData = new FormData(); const fileStream = createReadStream(filePath); const fileName = getBasename(filePath); @@ -83,10 +79,11 @@ export abstract class APIClient { formData.append('file', fileStream, fileName); if (opts?.body) { - const body = opts.body as Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const body = opts.body as Record; for (const [key, value] of Object.entries(body)) { if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); + value.forEach(item => formData.append(key, item)); } else { formData.append(key, value); } @@ -95,12 +92,12 @@ export abstract class APIClient { const headers = { ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` }; console.log(headers); - console.log('-------------------------'); + console.log("-------------------------"); console.log(formData.getHeaders()); - console.log('-------------------------'); + console.log("-------------------------"); const options: FinalRequestOptions = { method: 'post', From d68e935bc45c34e02c820e68ea68b90dc3477b59 Mon Sep 17 00:00:00 2001 From: amirk Date: Mon, 25 Nov 2024 11:38:38 +0200 Subject: [PATCH 04/53] feat: remove log --- src/APIClient.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/APIClient.ts b/src/APIClient.ts index 277a55b..1296710 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -64,14 +64,17 @@ export abstract class APIClient { return this.makeRequest('delete', path, opts); } - upload(path: string, filePath: string, opts?: RequestOptions): Promise { + async upload(path: string, filePath: string, opts?: RequestOptions): Promise { const formDataRequest = this.makeFormDataRequest(path, filePath, opts); - return this.performRequest(formDataRequest).then( - (response) => this.fetch.handleResponse(response) as Rsp, - ); + const response = await this.performRequest(formDataRequest); + return this.fetch.handleResponse(response) as Rsp; } - protected makeFormDataRequest(path: string, filePath: string, opts?: RequestOptions): FinalRequestOptions { + protected makeFormDataRequest( + path: string, + filePath: string, + opts?: RequestOptions, + ): FinalRequestOptions { const formData = new FormData(); const fileStream = createReadStream(filePath); const fileName = getBasename(filePath); @@ -83,7 +86,7 @@ export abstract class APIClient { const body = opts.body as Record; for (const [key, value] of Object.entries(body)) { if (Array.isArray(value)) { - value.forEach(item => formData.append(key, item)); + value.forEach((item) => formData.append(key, item)); } else { formData.append(key, value); } @@ -92,12 +95,8 @@ export abstract class APIClient { const headers = { ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, }; - console.log(headers); - console.log("-------------------------"); - console.log(formData.getHeaders()); - console.log("-------------------------"); const options: FinalRequestOptions = { method: 'post', From 329eb3ead78eb5eafb5301d4cc262597d9cca01f Mon Sep 17 00:00:00 2001 From: amirk Date: Tue, 26 Nov 2024 12:35:50 +0200 Subject: [PATCH 05/53] feat: add upload file object override --- .../studio/conversational-rag/rag-engine.ts | 2 +- src/APIClient.ts | 73 +++++++++++++++++-- src/resources/chat/completions.ts | 2 +- src/resources/rag/ragEngine.ts | 10 +-- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index efcc2d4..4964411 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -47,7 +47,7 @@ async function uploadQueryUpdateDelete() { async function listFiles() { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); - const files = await client.ragEngine.list({ limit: 10 }); + const files = await client.ragEngine.list({ limit: 4 }); console.log(files); } diff --git a/src/APIClient.ts b/src/APIClient.ts index 1296710..439fd4d 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -26,6 +26,36 @@ const validatePositiveInteger = (name: string, n: unknown): number => { return n; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const appendBodyToFormData = (formData: FormData, body: Record): void => { + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach(item => formData.append(key, item)); + } else { + formData.append(key, value); + } + } +} + +export type FilePathOrFileObject = +| string +| File; + +function makeFormDataFromFilePath(filePath: string): FormData { + const formData = new FormData(); + const fileStream = createReadStream(filePath); + const fileName = getBasename(filePath); + + formData.append('file', fileStream, fileName); + return formData; +} + +function makeFormDataFromFileObject(file: File): FormData { + const formData = new FormData(); + formData.append('file', file); + return formData; +} + export abstract class APIClient { protected baseURL: string; protected maxRetries: number; @@ -64,10 +94,41 @@ export abstract class APIClient { return this.makeRequest('delete', path, opts); } - async upload(path: string, filePath: string, opts?: RequestOptions): Promise { - const formDataRequest = this.makeFormDataRequest(path, filePath, opts); - const response = await this.performRequest(formDataRequest); - return this.fetch.handleResponse(response) as Rsp; + upload(path: string, file: string, opts?: RequestOptions): Promise; + upload(path: string, file: File, opts?: RequestOptions): Promise; + + + upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { + let formData: FormData; + + if (typeof file === 'string') { + formData = makeFormDataFromFilePath(file); + } else if (file instanceof File) { + formData = makeFormDataFromFileObject(file); + } else { + throw new AI21Error('Invalid file type for upload'); + } + + if (opts?.body) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appendBodyToFormData(formData, opts.body as Record); + } + + const headers = { + ...opts?.headers, + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` + }; + + const options: FinalRequestOptions = { + method: 'post', + path: path, + body: formData, + headers, + }; + + return this.performRequest(options).then( + (response) => this.fetch.handleResponse(response) as Rsp, + ); } protected makeFormDataRequest( @@ -133,6 +194,7 @@ export abstract class APIClient { const options = { method, path, + ...opts, }; @@ -145,7 +207,8 @@ export abstract class APIClient { let url = `${this.baseURL}${options.path}`; if (options.query) { - const queryString = new URLSearchParams(options.query as Record).toString(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const queryString = new URLSearchParams(options.query as Record).toString(); url += `?${queryString}`; } diff --git a/src/resources/chat/completions.ts b/src/resources/chat/completions.ts index fb14d8b..00afe32 100644 --- a/src/resources/chat/completions.ts +++ b/src/resources/chat/completions.ts @@ -24,7 +24,7 @@ export class Completions extends APIResource { { body, ...options, - stream: body.stream ?? false, + stream: body.stream ?? false, } as Models.RequestOptions, ) as Promise | Promise>; } diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index babf851..7543805 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -6,35 +6,35 @@ import { FileResponse } from 'types/rag/FileResponse'; const RAG_ENGINE_PATH = '/library/files'; export class RAGEngine extends APIResource { - create(filePath: string, body: UploadFileRequest, options?: Models.RequestOptions) { + create(filePath: string, body: UploadFileRequest, options?: Models.RequestOptions): Promise { return this.client.upload(RAG_ENGINE_PATH, filePath, { body: body, ...options, } as Models.RequestOptions) as Promise; } - get(fileId: string, options?: Models.RequestOptions) { + get(fileId: string, options?: Models.RequestOptions): Promise { return this.client.get( `${RAG_ENGINE_PATH}/${fileId}`, options as Models.RequestOptions, ) as Promise; } - delete(fileId: string, options?: Models.RequestOptions) { + delete(fileId: string, options?: Models.RequestOptions): Promise { return this.client.delete( `${RAG_ENGINE_PATH}/${fileId}`, options as Models.RequestOptions, ) as Promise; } - list(body: ListFilesFilters | null, options?: Models.RequestOptions) { + list(body: ListFilesFilters | null, options?: Models.RequestOptions): Promise { return this.client.get(RAG_ENGINE_PATH, { query: body, ...options, } as Models.RequestOptions) as Promise; } - update(fileId: string, body: UpdateFileRequest, options?: Models.RequestOptions) { + update(fileId: string, body: UpdateFileRequest, options?: Models.RequestOptions): Promise { return this.client.put(`${RAG_ENGINE_PATH}/${fileId}`, { body, ...options, From 6131ee85594b2daf13f49243a696bd43765856ff Mon Sep 17 00:00:00 2001 From: amirk Date: Tue, 26 Nov 2024 12:35:57 +0200 Subject: [PATCH 06/53] feat: add upload file object override --- src/APIClient.ts | 19 +++++++------------ src/resources/chat/completions.ts | 2 +- src/resources/rag/ragEngine.ts | 6 +++++- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/APIClient.ts b/src/APIClient.ts index 439fd4d..01a7e79 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -30,16 +30,14 @@ const validatePositiveInteger = (name: string, n: unknown): number => { const appendBodyToFormData = (formData: FormData, body: Record): void => { for (const [key, value] of Object.entries(body)) { if (Array.isArray(value)) { - value.forEach(item => formData.append(key, item)); + value.forEach((item) => formData.append(key, item)); } else { formData.append(key, value); } } -} +}; -export type FilePathOrFileObject = -| string -| File; +export type FilePathOrFileObject = string | File; function makeFormDataFromFilePath(filePath: string): FormData { const formData = new FormData(); @@ -97,7 +95,6 @@ export abstract class APIClient { upload(path: string, file: string, opts?: RequestOptions): Promise; upload(path: string, file: File, opts?: RequestOptions): Promise; - upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { let formData: FormData; @@ -113,10 +110,10 @@ export abstract class APIClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any appendBodyToFormData(formData, opts.body as Record); } - + const headers = { ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, }; const options: FinalRequestOptions = { @@ -126,9 +123,7 @@ export abstract class APIClient { headers, }; - return this.performRequest(options).then( - (response) => this.fetch.handleResponse(response) as Rsp, - ); + return this.performRequest(options).then((response) => this.fetch.handleResponse(response) as Rsp); } protected makeFormDataRequest( @@ -194,7 +189,7 @@ export abstract class APIClient { const options = { method, path, - + ...opts, }; diff --git a/src/resources/chat/completions.ts b/src/resources/chat/completions.ts index 00afe32..fb14d8b 100644 --- a/src/resources/chat/completions.ts +++ b/src/resources/chat/completions.ts @@ -24,7 +24,7 @@ export class Completions extends APIResource { { body, ...options, - stream: body.stream ?? false, + stream: body.stream ?? false, } as Models.RequestOptions, ) as Promise | Promise>; } diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index 7543805..7f49175 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -6,7 +6,11 @@ import { FileResponse } from 'types/rag/FileResponse'; const RAG_ENGINE_PATH = '/library/files'; export class RAGEngine extends APIResource { - create(filePath: string, body: UploadFileRequest, options?: Models.RequestOptions): Promise { + create( + filePath: string, + body: UploadFileRequest, + options?: Models.RequestOptions, + ): Promise { return this.client.upload(RAG_ENGINE_PATH, filePath, { body: body, ...options, From 54a96b115b8036fb8729a5b3131c2d615876068d Mon Sep 17 00:00:00 2001 From: amirk Date: Tue, 26 Nov 2024 14:34:49 +0200 Subject: [PATCH 07/53] feat: FilePathOrFileObject --- .../studio/conversational-rag/rag-engine.ts | 8 ++++++++ src/AI21.ts | 2 +- src/APIClient.ts | 5 +---- src/resources/rag/ragEngine.ts | 18 +++++++++++++++--- src/types/rag/FilePathOrFileObject.ts | 1 + src/types/rag/index.ts | 2 ++ 6 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 src/types/rag/FilePathOrFileObject.ts diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 4964411..0316741 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -26,6 +26,14 @@ async function uploadQueryUpdateDelete() { { path: 'test10' }, ); + // const fileContent = Buffer.from('This is the content of the file.'); + // const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); + + // // Use the File object in the create method + // const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(dummyFile, { + // path: 'test10', + // }); + const fileId = uploadFileResponse.fileId; let file: FileResponse = await waitForFileProcessing(client, fileId); console.log(file); diff --git a/src/AI21.ts b/src/AI21.ts index 30af6dc..5804d46 100644 --- a/src/AI21.ts +++ b/src/AI21.ts @@ -6,7 +6,7 @@ import { APIClient } from './APIClient'; import { Headers } from './types'; import * as Runtime from './runtime'; import { ConversationalRag } from './resources/rag/conversationalRag'; -import { RAGEngine } from 'resources'; +import { RAGEngine } from './resources'; export interface ClientOptions { baseURL?: string | undefined; diff --git a/src/APIClient.ts b/src/APIClient.ts index 01a7e79..2779405 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -15,6 +15,7 @@ import { Fetch } from 'fetch'; import { createReadStream } from 'fs'; import { basename as getBasename } from 'path'; import FormData from 'form-data'; +import { FilePathOrFileObject } from 'types/rag'; const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { @@ -37,7 +38,6 @@ const appendBodyToFormData = (formData: FormData, body: Record): vo } }; -export type FilePathOrFileObject = string | File; function makeFormDataFromFilePath(filePath: string): FormData { const formData = new FormData(); @@ -92,9 +92,6 @@ export abstract class APIClient { return this.makeRequest('delete', path, opts); } - upload(path: string, file: string, opts?: RequestOptions): Promise; - upload(path: string, file: File, opts?: RequestOptions): Promise; - upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { let formData: FormData; diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index 7f49175..3178c3a 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -1,17 +1,29 @@ import * as Models from '../../types'; import { APIResource } from '../../APIResource'; -import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types/rag'; +import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest, FilePathOrFileObject } from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; const RAG_ENGINE_PATH = '/library/files'; export class RAGEngine extends APIResource { create( - filePath: string, + file: string, + body: UploadFileRequest, + options?: Models.RequestOptions, + ): Promise; + + create( + file: File, + body: UploadFileRequest, + options?: Models.RequestOptions, + ): Promise; + + create( + file: FilePathOrFileObject, body: UploadFileRequest, options?: Models.RequestOptions, ): Promise { - return this.client.upload(RAG_ENGINE_PATH, filePath, { + return this.client.upload(RAG_ENGINE_PATH, file, { body: body, ...options, } as Models.RequestOptions) as Promise; diff --git a/src/types/rag/FilePathOrFileObject.ts b/src/types/rag/FilePathOrFileObject.ts new file mode 100644 index 0000000..cace19b --- /dev/null +++ b/src/types/rag/FilePathOrFileObject.ts @@ -0,0 +1 @@ +export type FilePathOrFileObject = string | File; diff --git a/src/types/rag/index.ts b/src/types/rag/index.ts index 03ee777..76431a3 100644 --- a/src/types/rag/index.ts +++ b/src/types/rag/index.ts @@ -15,3 +15,5 @@ export { type UploadFileResponse } from './UploadFileResponse'; export { type ListFilesFilters } from './ListFilesFilters'; export { type UpdateFileRequest } from './UploadFileRequest'; + +export { type FilePathOrFileObject } from './FilePathOrFileObject'; From 48f2a8d829d83ceac2193953ac3856f194daca5f Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 27 Nov 2024 12:16:23 +0200 Subject: [PATCH 08/53] feat: support node path and file object\ and browser file object --- .../studio/conversational-rag/rag-engine.ts | 21 +++-- src/APIClient.ts | 45 ++--------- src/files/form-utils.ts | 77 +++++++++++++++++++ vite.config.ts | 2 +- 4 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 src/files/form-utils.ts diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 0316741..7143ab6 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,5 +1,5 @@ import { AI21 } from 'ai21'; -import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; +import { FilePathOrFileObject, FileResponse, UploadFileResponse } from '../../../src/types/rag'; async function waitForFileProcessing( client: AI21, @@ -18,21 +18,14 @@ async function waitForFileProcessing( } } -async function uploadQueryUpdateDelete() { +async function uploadQueryUpdateDelete(fileInput) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { const uploadFileResponse: UploadFileResponse = await client.ragEngine.create( - '/Users/amirkoblyansky/Documents/ukraine.txt', + fileInput, { path: 'test10' }, ); - // const fileContent = Buffer.from('This is the content of the file.'); - // const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); - - // // Use the File object in the create method - // const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(dummyFile, { - // path: 'test10', - // }); const fileId = uploadFileResponse.fileId; let file: FileResponse = await waitForFileProcessing(client, fileId); @@ -59,6 +52,10 @@ async function listFiles() { console.log(files); } -uploadQueryUpdateDelete().catch(console.error); +const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt' +const fileContent = Buffer.from('This is the content of the file.'); +const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); + +uploadQueryUpdateDelete(dummyFile).catch(console.error); -listFiles().catch(console.error); +// listFiles().catch(console.error); diff --git a/src/APIClient.ts b/src/APIClient.ts index 2779405..140d2fe 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -15,7 +15,8 @@ import { Fetch } from 'fetch'; import { createReadStream } from 'fs'; import { basename as getBasename } from 'path'; import FormData from 'form-data'; -import { FilePathOrFileObject } from 'types/rag'; +import { FilePathOrFileObject } from './types/rag'; +import { createFormData, getBoundary, appendBodyToFormData } from './files/form-utils'; const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { @@ -27,33 +28,6 @@ const validatePositiveInteger = (name: string, n: unknown): number => { return n; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const appendBodyToFormData = (formData: FormData, body: Record): void => { - for (const [key, value] of Object.entries(body)) { - if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); - } else { - formData.append(key, value); - } - } -}; - - -function makeFormDataFromFilePath(filePath: string): FormData { - const formData = new FormData(); - const fileStream = createReadStream(filePath); - const fileName = getBasename(filePath); - - formData.append('file', fileStream, fileName); - return formData; -} - -function makeFormDataFromFileObject(file: File): FormData { - const formData = new FormData(); - formData.append('file', file); - return formData; -} - export abstract class APIClient { protected baseURL: string; protected maxRetries: number; @@ -92,16 +66,8 @@ export abstract class APIClient { return this.makeRequest('delete', path, opts); } - upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { - let formData: FormData; - - if (typeof file === 'string') { - formData = makeFormDataFromFilePath(file); - } else if (file instanceof File) { - formData = makeFormDataFromFileObject(file); - } else { - throw new AI21Error('Invalid file type for upload'); - } + async upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { + const formData = await createFormData(file); if (opts?.body) { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -110,7 +76,7 @@ export abstract class APIClient { const headers = { ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + 'Content-Type': `multipart/form-data; boundary=${await getBoundary(formData)}`, }; const options: FinalRequestOptions = { @@ -224,3 +190,4 @@ export abstract class APIClient { ); } } + diff --git a/src/files/form-utils.ts b/src/files/form-utils.ts new file mode 100644 index 0000000..2438494 --- /dev/null +++ b/src/files/form-utils.ts @@ -0,0 +1,77 @@ +import { FilePathOrFileObject } from 'types/rag'; +import * as Runtime from '../runtime'; + +export type UnifiedFormData = FormData | import('form-data'); + +import { Readable } from 'stream'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, value); + } + } + }; + +// Convert WHATWG ReadableStream to Node.js Readable Stream +function convertReadableStream(whatwgStream: ReadableStream): Readable { + const reader = whatwgStream.getReader(); + + return new Readable({ + async read() { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + } else { + this.push(value); + } + }, + }); +} + + +export async function createFormData(file: FilePathOrFileObject): Promise { + + if (Runtime.isBrowser) { + const formData = new FormData(); + + if (file instanceof window.File) { + formData.append('file', file); + } else { + throw new Error('Unsupported file type in browser'); + } + + return formData; + } else { // Node environment: + + const { default: FormDataNode } = await import('form-data'); + const formData = new FormDataNode(); + + if (typeof file === 'string') { + const { createReadStream } = await import('fs'); + formData.append('file', createReadStream(file), { filename: file.split('/').pop() }); + } else if (Buffer.isBuffer(file)) { + formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); + } else if (file instanceof File) { + const nodeStream = convertReadableStream(file.stream()); + formData.append('file', nodeStream, file.name); + } else { + throw new Error('Unsupported file type in Node.js'); + } + + return formData; + } +} + +export async function getBoundary(formData: UnifiedFormData): Promise { + if (!Runtime.isBrowser) { + const { default: FormDataNode } = await import('form-data'); + if (formData instanceof FormDataNode) { + return formData.getBoundary(); + } + } + return undefined; +} diff --git a/vite.config.ts b/vite.config.ts index b09b01b..9fd9c09 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ formats: ['es', 'cjs', 'umd'], }, rollupOptions: { - external: ['node-fetch'], + external: ['node-fetch', 'fs', 'path', 'stream', 'form-data'], output: { globals: { 'node-fetch': 'fetch', From c055ca225b7dc50e58e992c471ae567b297708c9 Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 27 Nov 2024 12:16:36 +0200 Subject: [PATCH 09/53] feat: support node path and file object\ and browser file object --- .../studio/conversational-rag/rag-engine.ts | 15 ++- src/APIClient.ts | 1 - src/files/form-utils.ts | 99 +++++++++---------- src/resources/rag/ragEngine.ts | 20 ++-- 4 files changed, 65 insertions(+), 70 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 7143ab6..9e4371e 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,5 +1,5 @@ import { AI21 } from 'ai21'; -import { FilePathOrFileObject, FileResponse, UploadFileResponse } from '../../../src/types/rag'; +import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; async function waitForFileProcessing( client: AI21, @@ -21,11 +21,9 @@ async function waitForFileProcessing( async function uploadQueryUpdateDelete(fileInput) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { - const uploadFileResponse: UploadFileResponse = await client.ragEngine.create( - fileInput, - { path: 'test10' }, - ); - + const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(fileInput, { + path: 'test10', + }); const fileId = uploadFileResponse.fileId; let file: FileResponse = await waitForFileProcessing(client, fileId); @@ -52,10 +50,11 @@ async function listFiles() { console.log(files); } -const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt' +const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt'; const fileContent = Buffer.from('This is the content of the file.'); const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); +uploadQueryUpdateDelete(filePath).catch(console.error); uploadQueryUpdateDelete(dummyFile).catch(console.error); -// listFiles().catch(console.error); +listFiles().catch(console.error); diff --git a/src/APIClient.ts b/src/APIClient.ts index 140d2fe..ea94993 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -190,4 +190,3 @@ export abstract class APIClient { ); } } - diff --git a/src/files/form-utils.ts b/src/files/form-utils.ts index 2438494..17cfb9a 100644 --- a/src/files/form-utils.ts +++ b/src/files/form-utils.ts @@ -7,71 +7,70 @@ import { Readable } from 'stream'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { - for (const [key, value] of Object.entries(body)) { - if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); - } else { - formData.append(key, value); - } + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, value); } - }; + } +}; // Convert WHATWG ReadableStream to Node.js Readable Stream function convertReadableStream(whatwgStream: ReadableStream): Readable { - const reader = whatwgStream.getReader(); + const reader = whatwgStream.getReader(); - return new Readable({ - async read() { - const { done, value } = await reader.read(); - if (done) { - this.push(null); - } else { - this.push(value); - } - }, - }); + return new Readable({ + async read() { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + } else { + this.push(value); + } + }, + }); } - export async function createFormData(file: FilePathOrFileObject): Promise { + if (Runtime.isBrowser) { + const formData = new FormData(); - if (Runtime.isBrowser) { - const formData = new FormData(); - - if (file instanceof window.File) { - formData.append('file', file); - } else { - throw new Error('Unsupported file type in browser'); - } - - return formData; - } else { // Node environment: + if (file instanceof window.File) { + formData.append('file', file); + } else { + throw new Error('Unsupported file type in browser'); + } - const { default: FormDataNode } = await import('form-data'); - const formData = new FormDataNode(); + return formData; + } else { + // Node environment: - if (typeof file === 'string') { - const { createReadStream } = await import('fs'); - formData.append('file', createReadStream(file), { filename: file.split('/').pop() }); - } else if (Buffer.isBuffer(file)) { - formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); - } else if (file instanceof File) { - const nodeStream = convertReadableStream(file.stream()); - formData.append('file', nodeStream, file.name); - } else { - throw new Error('Unsupported file type in Node.js'); - } + const { default: FormDataNode } = await import('form-data'); + const formData = new FormDataNode(); - return formData; + if (typeof file === 'string') { + const { createReadStream } = await import('fs'); + formData.append('file', createReadStream(file), { filename: file.split('/').pop() }); + } else if (Buffer.isBuffer(file)) { + formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); + } else if (file instanceof File) { + const nodeStream = convertReadableStream(file.stream()); + formData.append('file', nodeStream, file.name); + } else { + throw new Error('Unsupported file type in Node.js'); } + + return formData; + } } export async function getBoundary(formData: UnifiedFormData): Promise { - if (!Runtime.isBrowser) { - const { default: FormDataNode } = await import('form-data'); - if (formData instanceof FormDataNode) { - return formData.getBoundary(); - } + if (!Runtime.isBrowser) { + const { default: FormDataNode } = await import('form-data'); + if (formData instanceof FormDataNode) { + return formData.getBoundary(); } - return undefined; + } + return undefined; } diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index 3178c3a..1afccab 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -1,22 +1,20 @@ import * as Models from '../../types'; import { APIResource } from '../../APIResource'; -import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest, FilePathOrFileObject } from '../../types/rag'; +import { + UploadFileResponse, + UploadFileRequest, + ListFilesFilters, + UpdateFileRequest, + FilePathOrFileObject, +} from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; const RAG_ENGINE_PATH = '/library/files'; export class RAGEngine extends APIResource { - create( - file: string, - body: UploadFileRequest, - options?: Models.RequestOptions, - ): Promise; + create(file: string, body: UploadFileRequest, options?: Models.RequestOptions): Promise; - create( - file: File, - body: UploadFileRequest, - options?: Models.RequestOptions, - ): Promise; + create(file: File, body: UploadFileRequest, options?: Models.RequestOptions): Promise; create( file: FilePathOrFileObject, From 02b40dfd0902b3c8f125f1e77a81b761d1cdd52c Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 28 Nov 2024 10:02:37 +0200 Subject: [PATCH 10/53] feat: functioning browser upload --- .../studio/conversational-rag/rag-engine.ts | 8 +-- src/APIClient.ts | 65 ++++++------------- src/fetch/BrowserFetch.ts | 3 +- src/files/form-utils.ts | 50 +++++++------- src/files/index.ts | 1 + src/resources/rag/ragEngine.ts | 20 ++++-- src/types/API.ts | 2 + src/types/index.ts | 1 + vite.config.ts | 2 +- 9 files changed, 73 insertions(+), 79 deletions(-) create mode 100644 src/files/index.ts diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 9e4371e..86b951f 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -18,11 +18,11 @@ async function waitForFileProcessing( } } -async function uploadQueryUpdateDelete(fileInput) { +async function uploadQueryUpdateDelete(fileInput, label) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(fileInput, { - path: 'test10', + path: label, }); const fileId = uploadFileResponse.fileId; @@ -54,7 +54,7 @@ const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt'; const fileContent = Buffer.from('This is the content of the file.'); const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); -uploadQueryUpdateDelete(filePath).catch(console.error); -uploadQueryUpdateDelete(dummyFile).catch(console.error); +uploadQueryUpdateDelete(filePath, "abc123").catch(console.error); +uploadQueryUpdateDelete(dummyFile, "test2").catch(console.error); listFiles().catch(console.error); diff --git a/src/APIClient.ts b/src/APIClient.ts index ea94993..63b4587 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -8,15 +8,12 @@ import { HTTPMethod, Headers, CrossPlatformResponse, + UnifiedFormData, } from './types'; import { AI21EnvConfig } from './EnvConfig'; import { createFetchInstance } from './runtime'; import { Fetch } from 'fetch'; -import { createReadStream } from 'fs'; -import { basename as getBasename } from 'path'; -import FormData from 'form-data'; -import { FilePathOrFileObject } from './types/rag'; -import { createFormData, getBoundary, appendBodyToFormData } from './files/form-utils'; +import { getBoundary, appendBodyToFormData } from './files/form-utils'; const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { @@ -66,17 +63,16 @@ export abstract class APIClient { return this.makeRequest('delete', path, opts); } - async upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { - const formData = await createFormData(file); - + async upload(path: string, formData: UnifiedFormData, opts?: RequestOptions): Promise { if (opts?.body) { // eslint-disable-next-line @typescript-eslint/no-explicit-any appendBodyToFormData(formData, opts.body as Record); } + const boundary = await getBoundary(formData); const headers = { ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${await getBoundary(formData)}`, + 'Content-Type': `multipart/form-data; boundary=${boundary}`, }; const options: FinalRequestOptions = { @@ -89,43 +85,6 @@ export abstract class APIClient { return this.performRequest(options).then((response) => this.fetch.handleResponse(response) as Rsp); } - protected makeFormDataRequest( - path: string, - filePath: string, - opts?: RequestOptions, - ): FinalRequestOptions { - const formData = new FormData(); - const fileStream = createReadStream(filePath); - const fileName = getBasename(filePath); - - formData.append('file', fileStream, fileName); - - if (opts?.body) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const body = opts.body as Record; - for (const [key, value] of Object.entries(body)) { - if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); - } else { - formData.append(key, value); - } - } - } - - const headers = { - ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, - }; - - const options: FinalRequestOptions = { - method: 'post', - path: path, - body: formData, - headers, - }; - return options; - } - protected getUserAgent(): string { const platform = this.isRunningInBrowser() ? @@ -141,6 +100,20 @@ export abstract class APIClient { 'User-Agent': this.getUserAgent(), ...this.authHeaders(opts), }; + // if (opts?.body instanceof FormData) { + // return { + // Accept: 'application/json', + // 'User-Agent': this.getUserAgent(), + // ...this.authHeaders(opts), + // }; + // } else { + // return { + // Accept: 'application/json', + // 'Content-Type': 'application/json', + // 'User-Agent': this.getUserAgent(), + // ...this.authHeaders(opts), + // }; + // } } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/fetch/BrowserFetch.ts b/src/fetch/BrowserFetch.ts index 0872dcb..523eb41 100644 --- a/src/fetch/BrowserFetch.ts +++ b/src/fetch/BrowserFetch.ts @@ -5,11 +5,12 @@ import { Stream, BrowserSSEDecoder } from '../streaming'; export class BrowserFetch extends BaseFetch { call(url: string, options: FinalRequestOptions): Promise { const controller = new AbortController(); + const body = options.body instanceof FormData ? options.body : JSON.stringify(options.body); return fetch(url, { method: options.method, headers: options?.headers ? (options.headers as HeadersInit) : undefined, - body: options?.body ? JSON.stringify(options.body) : undefined, + body, signal: controller.signal, }); } diff --git a/src/files/form-utils.ts b/src/files/form-utils.ts index 17cfb9a..b15b2e6 100644 --- a/src/files/form-utils.ts +++ b/src/files/form-utils.ts @@ -1,9 +1,7 @@ import { FilePathOrFileObject } from 'types/rag'; import * as Runtime from '../runtime'; -export type UnifiedFormData = FormData | import('form-data'); - -import { Readable } from 'stream'; +import { UnifiedFormData } from 'types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { @@ -17,22 +15,30 @@ export const appendBodyToFormData = (formData: UnifiedFormData, body: Record { + if (Runtime.isNode) { + const { Readable } = await import('stream'); // Inline import of the stream module + const reader = whatwgStream.getReader(); + + return new Readable({ + async read() { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + } else { + this.push(value); + } + }, + }); + } else { + throw new Error('convertReadableStream is not supported in the browser environment.'); + } + } } -export async function createFormData(file: FilePathOrFileObject): Promise { +export class CreateFormData { + async createFormData(file: FilePathOrFileObject): Promise { if (Runtime.isBrowser) { const formData = new FormData(); @@ -45,28 +51,28 @@ export async function createFormData(file: FilePathOrFileObject): Promise { - if (!Runtime.isBrowser) { + if (Runtime.isNode) { const { default: FormDataNode } = await import('form-data'); if (formData instanceof FormDataNode) { return formData.getBoundary(); diff --git a/src/files/index.ts b/src/files/index.ts new file mode 100644 index 0000000..10ea620 --- /dev/null +++ b/src/files/index.ts @@ -0,0 +1 @@ +export * from './form-utils'; \ No newline at end of file diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index 1afccab..e35dc76 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -8,23 +8,33 @@ import { FilePathOrFileObject, } from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; +import { CreateFormData } from '../../files'; const RAG_ENGINE_PATH = '/library/files'; export class RAGEngine extends APIResource { - create(file: string, body: UploadFileRequest, options?: Models.RequestOptions): Promise; + async create( + file: string, + body: UploadFileRequest, + options?: Models.RequestOptions, + ): Promise; - create(file: File, body: UploadFileRequest, options?: Models.RequestOptions): Promise; + async create( + file: File, + body: UploadFileRequest, + options?: Models.RequestOptions, + ): Promise; - create( + async create( file: FilePathOrFileObject, body: UploadFileRequest, options?: Models.RequestOptions, ): Promise { - return this.client.upload(RAG_ENGINE_PATH, file, { + const formData = await new CreateFormData().createFormData(file); + return this.client.upload(RAG_ENGINE_PATH, formData, { body: body, ...options, - } as Models.RequestOptions) as Promise; + } as Models.RequestOptions) as Promise; } get(fileId: string, options?: Models.RequestOptions): Promise { diff --git a/src/types/API.ts b/src/types/API.ts index b6471ea..1a70581 100644 --- a/src/types/API.ts +++ b/src/types/API.ts @@ -29,3 +29,5 @@ export type Headers = Record; // Platforms specific types for NodeJS and Browser export type CrossPlatformResponse = Response | import('node-fetch').Response; export type CrossPlatformReadableStream = ReadableStream | import('stream/web').ReadableStream; + +export type UnifiedFormData = FormData | import('form-data'); diff --git a/src/types/index.ts b/src/types/index.ts index 1a735ed..28b96ed 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,6 +35,7 @@ export { type DefaultQuery, type Headers, type CrossPlatformResponse, + type UnifiedFormData, } from './API'; export { diff --git a/vite.config.ts b/vite.config.ts index 9fd9c09..b09b01b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ formats: ['es', 'cjs', 'umd'], }, rollupOptions: { - external: ['node-fetch', 'fs', 'path', 'stream', 'form-data'], + external: ['node-fetch'], output: { globals: { 'node-fetch': 'fetch', From b71e5981d07f73a5b6680043717d796319553f17 Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 28 Nov 2024 11:18:24 +0200 Subject: [PATCH 11/53] feat: reorganized --- .../studio/conversational-rag/rag-engine.ts | 38 +++------ src/APIClient.ts | 41 ++++------ src/fetch/BaseFetch.ts | 5 +- src/files/BaseFilesHandler.ts | 21 +++++ src/files/BrowserFilesHandler.ts | 24 ++++++ src/files/NodeFilesHandler.ts | 65 +++++++++++++++ src/files/form-utils.ts | 82 ------------------- src/files/index.ts | 3 +- src/resources/rag/ragEngine.ts | 4 +- src/runtime.ts | 10 +++ 10 files changed, 155 insertions(+), 138 deletions(-) create mode 100644 src/files/BaseFilesHandler.ts create mode 100644 src/files/BrowserFilesHandler.ts create mode 100644 src/files/NodeFilesHandler.ts delete mode 100644 src/files/form-utils.ts diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 86b951f..51c9afe 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,40 +1,26 @@ import { AI21 } from 'ai21'; import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; -async function waitForFileProcessing( - client: AI21, - fileId: string, - interval: number = 3000, -): Promise { - while (true) { - const file: FileResponse = await client.ragEngine.get(fileId); - - if (file.status === 'PROCESSED') { - return file; - } - - console.log(`File status is '${file.status}'. Waiting for it to be 'PROCESSED'...`); - await new Promise((resolve) => setTimeout(resolve, interval)); - } +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); } -async function uploadQueryUpdateDelete(fileInput, label) { +async function uploadGetUpdateDelete(fileInput, label) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(fileInput, { path: label, }); - const fileId = uploadFileResponse.fileId; - let file: FileResponse = await waitForFileProcessing(client, fileId); + let file: FileResponse = await client.ragEngine.get(uploadFileResponse.fileId); console.log(file); - - console.log('Now updating the file labels'); + await sleep(1000); // Give it a sec to start process before updating + console.log('Now updating the file labels and publicUrl...'); await client.ragEngine.update(uploadFileResponse.fileId, { labels: ['test99'], publicUrl: 'https://www.miri.com', }); - file = await client.ragEngine.get(fileId); + file = await client.ragEngine.get(uploadFileResponse.fileId); console.log(file); console.log('Now deleting the file'); @@ -50,11 +36,13 @@ async function listFiles() { console.log(files); } +/* Simulate a file upload passing file path */ const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt'; -const fileContent = Buffer.from('This is the content of the file.'); -const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); +uploadGetUpdateDelete(filePath, Date.now().toString()).catch(console.error); -uploadQueryUpdateDelete(filePath, "abc123").catch(console.error); -uploadQueryUpdateDelete(dummyFile, "test2").catch(console.error); +/* Simulate a file upload passing File instance */ +const fileContent = Buffer.from('Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.'); +const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); +uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); listFiles().catch(console.error); diff --git a/src/APIClient.ts b/src/APIClient.ts index 63b4587..f3d5341 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -8,12 +8,12 @@ import { HTTPMethod, Headers, CrossPlatformResponse, - UnifiedFormData, } from './types'; import { AI21EnvConfig } from './EnvConfig'; -import { createFetchInstance } from './runtime'; +import { createFetchInstance, createFilesHandlerInstance } from './runtime'; import { Fetch } from 'fetch'; -import { getBoundary, appendBodyToFormData } from './files/form-utils'; +import { FilePathOrFileObject } from 'types/rag'; +import { BaseFilesHandler } from 'files/BaseFilesHandler'; const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { @@ -30,22 +30,26 @@ export abstract class APIClient { protected maxRetries: number; protected timeout: number; protected fetch: Fetch; + protected filesHandler: BaseFilesHandler; constructor({ baseURL, maxRetries = AI21EnvConfig.MAX_RETRIES, timeout = AI21EnvConfig.TIMEOUT_SECONDS, fetch = createFetchInstance(), + filesHandler = createFilesHandlerInstance(), }: { baseURL: string; maxRetries?: number | undefined; timeout: number | undefined; fetch?: Fetch; + filesHandler?: BaseFilesHandler; }) { this.baseURL = baseURL; this.maxRetries = validatePositiveInteger('maxRetries', maxRetries); this.timeout = validatePositiveInteger('timeout', timeout); this.fetch = fetch; + this.filesHandler = filesHandler; } get(path: string, opts?: RequestOptions): Promise { return this.makeRequest('get', path, opts); @@ -63,16 +67,17 @@ export abstract class APIClient { return this.makeRequest('delete', path, opts); } - async upload(path: string, formData: UnifiedFormData, opts?: RequestOptions): Promise { + async upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { + const formData = await this.filesHandler.createFormData(file); + if (opts?.body) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - appendBodyToFormData(formData, opts.body as Record); + this.filesHandler.appendBodyToFormData(formData, opts.body as Record); } - const boundary = await getBoundary(formData); const headers = { ...opts?.headers, - 'Content-Type': `multipart/form-data; boundary=${boundary}`, + ...this.filesHandler.getMultipartFormDataHeaders(formData), }; const options: FinalRequestOptions = { @@ -94,26 +99,16 @@ export abstract class APIClient { } protected defaultHeaders(opts: FinalRequestOptions): Headers { - return { + const defaultHeaders = { Accept: 'application/json', - 'Content-Type': 'application/json', 'User-Agent': this.getUserAgent(), ...this.authHeaders(opts), }; - // if (opts?.body instanceof FormData) { - // return { - // Accept: 'application/json', - // 'User-Agent': this.getUserAgent(), - // ...this.authHeaders(opts), - // }; - // } else { - // return { - // Accept: 'application/json', - // 'Content-Type': 'application/json', - // 'User-Agent': this.getUserAgent(), - // ...this.authHeaders(opts), - // }; - // } + + if (opts?.body instanceof FormData) { + return defaultHeaders; + } + return { ...defaultHeaders, 'Content-Type': 'application/json' }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/fetch/BaseFetch.ts b/src/fetch/BaseFetch.ts index 6d643f2..7ae9931 100644 --- a/src/fetch/BaseFetch.ts +++ b/src/fetch/BaseFetch.ts @@ -3,10 +3,7 @@ import { AI21Error } from '../errors'; import { FinalRequestOptions, CrossPlatformResponse } from '../types'; import { APIResponseProps } from '../types/API'; -export type APIResponse = { - data?: T; - response: CrossPlatformResponse; -}; + export abstract class BaseFetch { abstract call(url: string, options: FinalRequestOptions): Promise; async handleResponse({ response, options }: APIResponseProps) { diff --git a/src/files/BaseFilesHandler.ts b/src/files/BaseFilesHandler.ts new file mode 100644 index 0000000..b3cde0f --- /dev/null +++ b/src/files/BaseFilesHandler.ts @@ -0,0 +1,21 @@ +import { UnifiedFormData } from "types"; +import { FilePathOrFileObject } from "types/rag"; + + export abstract class BaseFilesHandler { + abstract createFormData(file: FilePathOrFileObject): Promise; + + abstract getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, value); + } + } + }; + + + } \ No newline at end of file diff --git a/src/files/BrowserFilesHandler.ts b/src/files/BrowserFilesHandler.ts new file mode 100644 index 0000000..7b5f5a7 --- /dev/null +++ b/src/files/BrowserFilesHandler.ts @@ -0,0 +1,24 @@ +import { UnifiedFormData } from "types"; +import { FilePathOrFileObject } from "types/rag"; +import { BaseFilesHandler } from "./BaseFilesHandler"; + + + export class BrowserFilesHandler extends BaseFilesHandler { + + async createFormData(file: FilePathOrFileObject): Promise { + const formData = new FormData(); + + if (file instanceof window.File) { + formData.append('file', file); + } else { + throw new Error('Unsupported file type in browser'); + } + + return formData; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { + return {}; + } + } diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts new file mode 100644 index 0000000..43ebd55 --- /dev/null +++ b/src/files/NodeFilesHandler.ts @@ -0,0 +1,65 @@ +import { UnifiedFormData } from "types"; +import { FilePathOrFileObject } from "types/rag"; +import { BaseFilesHandler } from "./BaseFilesHandler"; + + export class NodeFilesHandler extends BaseFilesHandler { + + async convertReadableStream(whatwgStream: ReadableStream): Promise { + const { Readable } = await import('stream'); + const reader = whatwgStream.getReader(); + + return new Readable({ + async read() { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + } else { + this.push(value); + } + }, + }); + } + + async getBoundary(formData: UnifiedFormData): Promise { + const { default: FormDataNode } = await import('form-data'); + if (formData instanceof FormDataNode) { + return formData.getBoundary(); + } + else { + throw new Error('getBoundary invoked with native browser FormData instance instead of NodeJS form-data'); + } + } + + + async createFormData(file: FilePathOrFileObject): Promise { + const { default: FormDataNode } = await import('form-data'); + const formData = new FormDataNode(); + + if (typeof file === 'string') { + const fs = (await import('fs')).default; + formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); + } else if (Buffer.isBuffer(file)) { + formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); + } else if (file instanceof File) { + const nodeStream = await this.convertReadableStream(file.stream()); + formData.append('file', nodeStream, file.name); + } else { + throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); + } + + return formData; + } + + getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { + if (formData instanceof FormData) { + return null; + } + + const boundary = formData.getBoundary(); + return { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + }; + } + + } + \ No newline at end of file diff --git a/src/files/form-utils.ts b/src/files/form-utils.ts deleted file mode 100644 index b15b2e6..0000000 --- a/src/files/form-utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { FilePathOrFileObject } from 'types/rag'; -import * as Runtime from '../runtime'; - -import { UnifiedFormData } from 'types'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { - for (const [key, value] of Object.entries(body)) { - if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); - } else { - formData.append(key, value); - } - } -}; - -// Convert WHATWG ReadableStream to Node.js Readable Stream -class NodeReadableStream { - async convertReadableStream(whatwgStream: ReadableStream): Promise { - if (Runtime.isNode) { - const { Readable } = await import('stream'); // Inline import of the stream module - const reader = whatwgStream.getReader(); - - return new Readable({ - async read() { - const { done, value } = await reader.read(); - if (done) { - this.push(null); - } else { - this.push(value); - } - }, - }); - } else { - throw new Error('convertReadableStream is not supported in the browser environment.'); - } - } -} - -export class CreateFormData { - async createFormData(file: FilePathOrFileObject): Promise { - if (Runtime.isBrowser) { - const formData = new FormData(); - - if (file instanceof window.File) { - formData.append('file', file); - } else { - throw new Error('Unsupported file type in browser'); - } - - return formData; - } else { - // Node environment: - const { default: FormDataNode } = await import('form-data'); - const formData = new FormDataNode(); - - if (typeof file === 'string') { - const fs = (await import('fs')).default; - formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); - } else if (Buffer.isBuffer(file)) { - formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); - } else if (file instanceof File) { - const nodeStream = await new NodeReadableStream().convertReadableStream(file.stream()); - formData.append('file', nodeStream, file.name); - } else { - throw new Error('Unsupported file type in Node.js'); - } - - return formData; - } - } -} - -export async function getBoundary(formData: UnifiedFormData): Promise { - if (Runtime.isNode) { - const { default: FormDataNode } = await import('form-data'); - if (formData instanceof FormDataNode) { - return formData.getBoundary(); - } - } - return undefined; -} diff --git a/src/files/index.ts b/src/files/index.ts index 10ea620..bb90c1b 100644 --- a/src/files/index.ts +++ b/src/files/index.ts @@ -1 +1,2 @@ -export * from './form-utils'; \ No newline at end of file +export { BrowserFilesHandler } from './BrowserFilesHandler'; +export { NodeFilesHandler } from './NodeFilesHandler'; diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index e35dc76..249577b 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -8,7 +8,6 @@ import { FilePathOrFileObject, } from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; -import { CreateFormData } from '../../files'; const RAG_ENGINE_PATH = '/library/files'; @@ -30,8 +29,7 @@ export class RAGEngine extends APIResource { body: UploadFileRequest, options?: Models.RequestOptions, ): Promise { - const formData = await new CreateFormData().createFormData(file); - return this.client.upload(RAG_ENGINE_PATH, formData, { + return this.client.upload(RAG_ENGINE_PATH, file, { body: body, ...options, } as Models.RequestOptions) as Promise; diff --git a/src/runtime.ts b/src/runtime.ts index 9f4d483..0c3b461 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,4 +1,7 @@ +import { BrowserFilesHandler } from './files/BrowserFilesHandler'; import { BrowserFetch, Fetch, NodeFetch } from './fetch'; +import { NodeFilesHandler } from './files/NodeFilesHandler'; +import { BaseFilesHandler } from './files/BaseFilesHandler'; export const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; @@ -26,3 +29,10 @@ export function createFetchInstance(): Fetch { return new NodeFetch(); } + +export function createFilesHandlerInstance(): BaseFilesHandler { + if (isBrowser || isWebWorker) { + return new BrowserFilesHandler(); + } + return new NodeFilesHandler(); +} From a93d32c761c914977e2b5750227677c261460ad4 Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 28 Nov 2024 11:18:30 +0200 Subject: [PATCH 12/53] feat: reorganized --- .../studio/conversational-rag/rag-engine.ts | 6 +- src/fetch/BaseFetch.ts | 1 - src/files/BaseFilesHandler.ts | 34 +++--- src/files/BrowserFilesHandler.ts | 36 +++--- src/files/NodeFilesHandler.ts | 109 +++++++++--------- src/runtime.ts | 2 +- 6 files changed, 91 insertions(+), 97 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 51c9afe..62eada1 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -2,7 +2,7 @@ import { AI21 } from 'ai21'; import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } async function uploadGetUpdateDelete(fileInput, label) { @@ -41,7 +41,9 @@ const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt'; uploadGetUpdateDelete(filePath, Date.now().toString()).catch(console.error); /* Simulate a file upload passing File instance */ -const fileContent = Buffer.from('Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.'); +const fileContent = Buffer.from( + 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', +); const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); diff --git a/src/fetch/BaseFetch.ts b/src/fetch/BaseFetch.ts index 7ae9931..6ee2f14 100644 --- a/src/fetch/BaseFetch.ts +++ b/src/fetch/BaseFetch.ts @@ -3,7 +3,6 @@ import { AI21Error } from '../errors'; import { FinalRequestOptions, CrossPlatformResponse } from '../types'; import { APIResponseProps } from '../types/API'; - export abstract class BaseFetch { abstract call(url: string, options: FinalRequestOptions): Promise; async handleResponse({ response, options }: APIResponseProps) { diff --git a/src/files/BaseFilesHandler.ts b/src/files/BaseFilesHandler.ts index b3cde0f..3e1e465 100644 --- a/src/files/BaseFilesHandler.ts +++ b/src/files/BaseFilesHandler.ts @@ -1,21 +1,19 @@ -import { UnifiedFormData } from "types"; -import { FilePathOrFileObject } from "types/rag"; +import { UnifiedFormData } from 'types'; +import { FilePathOrFileObject } from 'types/rag'; - export abstract class BaseFilesHandler { - abstract createFormData(file: FilePathOrFileObject): Promise; +export abstract class BaseFilesHandler { + abstract createFormData(file: FilePathOrFileObject): Promise; - abstract getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null; + abstract getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { - for (const [key, value] of Object.entries(body)) { - if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); - } else { - formData.append(key, value); - } - } - }; - - - } \ No newline at end of file + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, value); + } + } + }; +} diff --git a/src/files/BrowserFilesHandler.ts b/src/files/BrowserFilesHandler.ts index 7b5f5a7..444ead4 100644 --- a/src/files/BrowserFilesHandler.ts +++ b/src/files/BrowserFilesHandler.ts @@ -1,24 +1,22 @@ -import { UnifiedFormData } from "types"; -import { FilePathOrFileObject } from "types/rag"; -import { BaseFilesHandler } from "./BaseFilesHandler"; +import { UnifiedFormData } from 'types'; +import { FilePathOrFileObject } from 'types/rag'; +import { BaseFilesHandler } from './BaseFilesHandler'; +export class BrowserFilesHandler extends BaseFilesHandler { + async createFormData(file: FilePathOrFileObject): Promise { + const formData = new FormData(); - export class BrowserFilesHandler extends BaseFilesHandler { - - async createFormData(file: FilePathOrFileObject): Promise { - const formData = new FormData(); - - if (file instanceof window.File) { - formData.append('file', file); - } else { - throw new Error('Unsupported file type in browser'); - } - - return formData; + if (file instanceof window.File) { + formData.append('file', file); + } else { + throw new Error('Unsupported file type in browser'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { - return {}; - } + return formData; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { + return {}; } +} diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 43ebd55..9368498 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -1,65 +1,62 @@ -import { UnifiedFormData } from "types"; -import { FilePathOrFileObject } from "types/rag"; -import { BaseFilesHandler } from "./BaseFilesHandler"; +import { UnifiedFormData } from 'types'; +import { FilePathOrFileObject } from 'types/rag'; +import { BaseFilesHandler } from './BaseFilesHandler'; - export class NodeFilesHandler extends BaseFilesHandler { +export class NodeFilesHandler extends BaseFilesHandler { + async convertReadableStream(whatwgStream: ReadableStream): Promise { + const { Readable } = await import('stream'); + const reader = whatwgStream.getReader(); - async convertReadableStream(whatwgStream: ReadableStream): Promise { - const { Readable } = await import('stream'); - const reader = whatwgStream.getReader(); - - return new Readable({ - async read() { - const { done, value } = await reader.read(); - if (done) { - this.push(null); - } else { - this.push(value); - } - }, - }); - } - - async getBoundary(formData: UnifiedFormData): Promise { - const { default: FormDataNode } = await import('form-data'); - if (formData instanceof FormDataNode) { - return formData.getBoundary(); - } - else { - throw new Error('getBoundary invoked with native browser FormData instance instead of NodeJS form-data'); + return new Readable({ + async read() { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + } else { + this.push(value); } + }, + }); + } + + async getBoundary(formData: UnifiedFormData): Promise { + const { default: FormDataNode } = await import('form-data'); + if (formData instanceof FormDataNode) { + return formData.getBoundary(); + } else { + throw new Error( + 'getBoundary invoked with native browser FormData instance instead of NodeJS form-data', + ); } - + } - async createFormData(file: FilePathOrFileObject): Promise { - const { default: FormDataNode } = await import('form-data'); - const formData = new FormDataNode(); - - if (typeof file === 'string') { - const fs = (await import('fs')).default; - formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); - } else if (Buffer.isBuffer(file)) { - formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); - } else if (file instanceof File) { - const nodeStream = await this.convertReadableStream(file.stream()); - formData.append('file', nodeStream, file.name); - } else { - throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); - } - - return formData; + async createFormData(file: FilePathOrFileObject): Promise { + const { default: FormDataNode } = await import('form-data'); + const formData = new FormDataNode(); + + if (typeof file === 'string') { + const fs = (await import('fs')).default; + formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); + } else if (Buffer.isBuffer(file)) { + formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); + } else if (file instanceof File) { + const nodeStream = await this.convertReadableStream(file.stream()); + formData.append('file', nodeStream, file.name); + } else { + throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); } - getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { - if (formData instanceof FormData) { - return null; - } + return formData; + } - const boundary = formData.getBoundary(); - return { - 'Content-Type': `multipart/form-data; boundary=${boundary}`, - }; - } - + getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { + if (formData instanceof FormData) { + return null; } - \ No newline at end of file + + const boundary = formData.getBoundary(); + return { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + }; + } +} diff --git a/src/runtime.ts b/src/runtime.ts index 0c3b461..a11f298 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -32,7 +32,7 @@ export function createFetchInstance(): Fetch { export function createFilesHandlerInstance(): BaseFilesHandler { if (isBrowser || isWebWorker) { - return new BrowserFilesHandler(); + return new BrowserFilesHandler(); } return new NodeFilesHandler(); } From eac5e6e4d40edc2728973314df80edf76f1b31cf Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 28 Nov 2024 13:05:54 +0200 Subject: [PATCH 13/53] feat: wip --- src/fetch/BaseFetch.ts | 5 +++++ src/files/BrowserFilesHandler.ts | 1 + src/files/NodeFilesHandler.ts | 16 +++++----------- src/types/API.ts | 3 ++- src/types/index.ts | 1 + 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/fetch/BaseFetch.ts b/src/fetch/BaseFetch.ts index 6ee2f14..7b27749 100644 --- a/src/fetch/BaseFetch.ts +++ b/src/fetch/BaseFetch.ts @@ -3,6 +3,11 @@ import { AI21Error } from '../errors'; import { FinalRequestOptions, CrossPlatformResponse } from '../types'; import { APIResponseProps } from '../types/API'; +export type APIResponse = { + data?: T; + response: CrossPlatformResponse; +}; + export abstract class BaseFetch { abstract call(url: string, options: FinalRequestOptions): Promise; async handleResponse({ response, options }: APIResponseProps) { diff --git a/src/files/BrowserFilesHandler.ts b/src/files/BrowserFilesHandler.ts index 444ead4..909132f 100644 --- a/src/files/BrowserFilesHandler.ts +++ b/src/files/BrowserFilesHandler.ts @@ -17,6 +17,7 @@ export class BrowserFilesHandler extends BaseFilesHandler { // eslint-disable-next-line @typescript-eslint/no-unused-vars getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { + /* In browser, we don't need to set any additional headers for multipart/form-data, as the browser will handle it */ return {}; } } diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 9368498..7e586b5 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -1,6 +1,7 @@ import { UnifiedFormData } from 'types'; import { FilePathOrFileObject } from 'types/rag'; import { BaseFilesHandler } from './BaseFilesHandler'; +import { FormDataNode } from 'types/API'; export class NodeFilesHandler extends BaseFilesHandler { async convertReadableStream(whatwgStream: ReadableStream): Promise { @@ -19,15 +20,9 @@ export class NodeFilesHandler extends BaseFilesHandler { }); } - async getBoundary(formData: UnifiedFormData): Promise { - const { default: FormDataNode } = await import('form-data'); - if (formData instanceof FormDataNode) { - return formData.getBoundary(); - } else { - throw new Error( - 'getBoundary invoked with native browser FormData instance instead of NodeJS form-data', - ); - } + getBoundary(formData: UnifiedFormData): string { + const formDataNode = formData as FormDataNode; + return formDataNode.getBoundary(); } async createFormData(file: FilePathOrFileObject): Promise { @@ -51,9 +46,8 @@ export class NodeFilesHandler extends BaseFilesHandler { getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { if (formData instanceof FormData) { - return null; + throw new Error('getMultipartFormDataHeaders invoked with native browser FormData instance instead of NodeJS form-data'); } - const boundary = formData.getBoundary(); return { 'Content-Type': `multipart/form-data; boundary=${boundary}`, diff --git a/src/types/API.ts b/src/types/API.ts index 1a70581..fad3a23 100644 --- a/src/types/API.ts +++ b/src/types/API.ts @@ -30,4 +30,5 @@ export type Headers = Record; export type CrossPlatformResponse = Response | import('node-fetch').Response; export type CrossPlatformReadableStream = ReadableStream | import('stream/web').ReadableStream; -export type UnifiedFormData = FormData | import('form-data'); +export type FormDataNode = import('form-data'); +export type UnifiedFormData = FormData | FormDataNode; diff --git a/src/types/index.ts b/src/types/index.ts index 28b96ed..250ed7d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -36,6 +36,7 @@ export { type Headers, type CrossPlatformResponse, type UnifiedFormData, + type FormDataNode, } from './API'; export { From a628702266bf10637839f14c9a8ac9ea2cdb65af Mon Sep 17 00:00:00 2001 From: amirk Date: Sun, 1 Dec 2024 10:46:08 +0200 Subject: [PATCH 14/53] feat: wip --- src/files/NodeFilesHandler.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 7e586b5..b7fea94 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -1,12 +1,11 @@ import { UnifiedFormData } from 'types'; import { FilePathOrFileObject } from 'types/rag'; import { BaseFilesHandler } from './BaseFilesHandler'; -import { FormDataNode } from 'types/API'; export class NodeFilesHandler extends BaseFilesHandler { - async convertReadableStream(whatwgStream: ReadableStream): Promise { + async convertReadableStream(readableStream: ReadableStream): Promise { const { Readable } = await import('stream'); - const reader = whatwgStream.getReader(); + const reader = readableStream.getReader(); return new Readable({ async read() { @@ -20,11 +19,6 @@ export class NodeFilesHandler extends BaseFilesHandler { }); } - getBoundary(formData: UnifiedFormData): string { - const formDataNode = formData as FormDataNode; - return formDataNode.getBoundary(); - } - async createFormData(file: FilePathOrFileObject): Promise { const { default: FormDataNode } = await import('form-data'); const formData = new FormDataNode(); @@ -46,7 +40,9 @@ export class NodeFilesHandler extends BaseFilesHandler { getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { if (formData instanceof FormData) { - throw new Error('getMultipartFormDataHeaders invoked with native browser FormData instance instead of NodeJS form-data'); + throw new Error( + 'getMultipartFormDataHeaders invoked with native browser FormData instance instead of NodeJS form-data', + ); } const boundary = formData.getBoundary(); return { From 6ff4df799949fb143b617b962539a006318a9900 Mon Sep 17 00:00:00 2001 From: amirk Date: Sun, 1 Dec 2024 14:44:05 +0200 Subject: [PATCH 15/53] feat: basic unit tests for rag engine --- ...Rag.test.ts => conversational-rag.test.ts} | 101 +++++++++++++++++ tests/unittests/resources/rag-engine.test.ts | 107 ++++++++++++++++++ 2 files changed, 208 insertions(+) rename tests/unittests/resources/chat/{conversationalRag.test.ts => conversational-rag.test.ts} (56%) create mode 100644 tests/unittests/resources/rag-engine.test.ts diff --git a/tests/unittests/resources/chat/conversationalRag.test.ts b/tests/unittests/resources/chat/conversational-rag.test.ts similarity index 56% rename from tests/unittests/resources/chat/conversationalRag.test.ts rename to tests/unittests/resources/chat/conversational-rag.test.ts index 2578d08..2d5473f 100644 --- a/tests/unittests/resources/chat/conversationalRag.test.ts +++ b/tests/unittests/resources/chat/conversational-rag.test.ts @@ -1,10 +1,15 @@ import * as Models from '../../../../src/types'; import {ConversationalRag} from "../../../../src/resources/rag/conversationalRag"; import { APIClient } from '../../../../src/APIClient'; +import { RAGEngine } from '../../../../src/resources/rag/ragEngine'; class MockAPIClient extends APIClient { public post = jest.fn(); + public upload = jest.fn(); + public get = jest.fn(); + public delete = jest.fn(); + public put = jest.fn(); } @@ -96,3 +101,99 @@ describe('ConversationalRag', () => { expect(response).toEqual(expectedResponse); }); }); + +describe('RAGEngine', () => { + let ragEngine: RAGEngine; + let mockClient: MockAPIClient; + const dummyAPIKey = "test-api-key"; + + beforeEach(() => { + mockClient = new MockAPIClient({ + baseURL: 'https://api.example.com', + maxRetries: 3, + timeout: 5000, + }); + + ragEngine = new RAGEngine(mockClient); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should upload a file and return the response', async () => { + const fileInput = 'path/to/file.txt'; + const body = { path: 'label' }; + const expectedResponse = { fileId: '12345' }; + + mockClient.upload.mockResolvedValue(expectedResponse); + + const response = await ragEngine.create(fileInput, body); + + expect(mockClient.upload).toHaveBeenCalledWith( + '/library/files', + fileInput, + { body, headers: undefined } + ); + expect(response).toEqual(expectedResponse); + }); + + it('should get a file by ID and return the response', async () => { + const fileId = '12345'; + const expectedResponse = { id: fileId, name: 'file.txt' }; + + mockClient.get.mockResolvedValue(expectedResponse); + + const response = await ragEngine.get(fileId); + + expect(mockClient.get).toHaveBeenCalledWith( + `/library/files/${fileId}`, + undefined + ); + expect(response).toEqual(expectedResponse); + }); + + it('should delete a file by ID', async () => { + const fileId = '12345'; + + mockClient.delete.mockResolvedValue(null); + + const response = await ragEngine.delete(fileId); + + expect(mockClient.delete).toHaveBeenCalledWith( + `/library/files/${fileId}`, + undefined + ); + expect(response).toBeNull(); + }); + + it('should update a file by ID and return null', async () => { + const fileId = '12345'; + const body = { labels: ['test'], publicUrl: 'https://example.com' }; + + mockClient.put.mockResolvedValue(null); + + const response = await ragEngine.update(fileId, body); + + expect(mockClient.put).toHaveBeenCalledWith( + `/library/files/${fileId}`, + { body, headers: undefined } + ); + expect(response).toBeNull(); + }); + + it('should list files and return the response', async () => { + const filters = { limit: 4 }; + const expectedResponse = [{ id: '12345', name: 'file.txt' }]; + + mockClient.get.mockResolvedValue(expectedResponse); + + const response = await ragEngine.list(filters); + + expect(mockClient.get).toHaveBeenCalledWith( + '/library/files', + { query: filters, headers: undefined } + ); + expect(response).toEqual(expectedResponse); + }); +}); diff --git a/tests/unittests/resources/rag-engine.test.ts b/tests/unittests/resources/rag-engine.test.ts new file mode 100644 index 0000000..540639e --- /dev/null +++ b/tests/unittests/resources/rag-engine.test.ts @@ -0,0 +1,107 @@ +import * as Models from '../../../src/types'; +import { RAGEngine } from '../../../src/resources/rag/ragEngine'; +import { APIClient } from '../../../src/APIClient'; + +class MockAPIClient extends APIClient { + public upload = jest.fn(); + public get = jest.fn(); + public delete = jest.fn(); + public put = jest.fn(); +} + +describe('RAGEngine', () => { + let ragEngine: RAGEngine; + let mockClient: MockAPIClient; + const dummyAPIKey = "test-api-key"; + const options: Models.RequestOptions = { headers: { 'Authorization': `Bearer ${dummyAPIKey}` } }; + + beforeEach(() => { + mockClient = new MockAPIClient({ + baseURL: 'https://api.example.com', + maxRetries: 3, + timeout: 5000, + }); + + ragEngine = new RAGEngine(mockClient); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should upload a file and return the fileId', async () => { + const fileInput = 'path/to/file.txt'; + const body = { path: 'label' }; + const expectedResponse = { fileId: '12345' }; + + mockClient.upload.mockResolvedValue(expectedResponse); + + const response = await ragEngine.create(fileInput, body, options); + + expect(mockClient.upload).toHaveBeenCalledWith( + '/library/files', + fileInput, + { body, headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + ); + expect(response).toEqual(expectedResponse); + }); + + it('should get a file by ID and return the response', async () => { + const fileId = '12345'; + const expectedResponse = { id: fileId, name: 'file.txt' }; + + mockClient.get.mockResolvedValue(expectedResponse); + + const response = await ragEngine.get(fileId, options); + + expect(mockClient.get).toHaveBeenCalledWith( + `/library/files/${fileId}`, + { headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + ); + expect(response).toEqual(expectedResponse); + }); + + it('should delete a file by ID', async () => { + const fileId = '12345'; + + mockClient.delete.mockResolvedValue(null); + + const response = await ragEngine.delete(fileId, options); + + expect(mockClient.delete).toHaveBeenCalledWith( + `/library/files/${fileId}`, + { headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + ); + expect(response).toBeNull(); + }); + + it('should update a file by ID and return null', async () => { + const fileId = '12345'; + const body = { labels: ['test'], publicUrl: 'https://example.com' }; + + mockClient.put.mockResolvedValue(null); + + const response = await ragEngine.update(fileId, body, options); + + expect(mockClient.put).toHaveBeenCalledWith( + `/library/files/${fileId}`, + { body, headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + ); + expect(response).toBeNull(); + }); + + it('should list files and return the response', async () => { + const filters = { limit: 4 }; + const expectedResponse = [{ id: '12345', name: 'file.txt' }]; + + mockClient.get.mockResolvedValue(expectedResponse); + + const response = await ragEngine.list(filters, options); + + expect(mockClient.get).toHaveBeenCalledWith( + '/library/files', + { query: filters, headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + ); + expect(response).toEqual(expectedResponse); + }); +}); From ab469bec78059cc9813dfd6f1d693ec8dceadee0 Mon Sep 17 00:00:00 2001 From: amirk Date: Mon, 2 Dec 2024 14:58:41 +0200 Subject: [PATCH 16/53] feat: wip --- .../studio/conversational-rag/rag-engine.ts | 2 +- src/APIClient.ts | 64 ++++++++++++------- src/fetch/BrowserFetch.ts | 3 +- src/fetch/NodeFetch.ts | 5 +- src/files/BaseFilesHandler.ts | 17 +---- src/files/BrowserFilesHandler.ts | 21 ++---- src/files/NodeFilesHandler.ts | 22 ++----- src/types/API.ts | 8 ++- src/types/index.ts | 1 - 9 files changed, 61 insertions(+), 82 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 62eada1..ed78201 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -11,7 +11,7 @@ async function uploadGetUpdateDelete(fileInput, label) { const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(fileInput, { path: label, }); - + console.log(uploadFileResponse); let file: FileResponse = await client.ragEngine.get(uploadFileResponse.fileId); console.log(file); await sleep(1000); // Give it a sec to start process before updating diff --git a/src/APIClient.ts b/src/APIClient.ts index f3d5341..cf6701a 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -8,6 +8,7 @@ import { HTTPMethod, Headers, CrossPlatformResponse, + UnifiedFormData, } from './types'; import { AI21EnvConfig } from './EnvConfig'; import { createFetchInstance, createFilesHandlerInstance } from './runtime'; @@ -25,6 +26,19 @@ const validatePositiveInteger = (name: string, n: unknown): number => { return n; }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, value); + } + } + }; + + export abstract class APIClient { protected baseURL: string; protected maxRetries: number; @@ -52,38 +66,38 @@ export abstract class APIClient { this.filesHandler = filesHandler; } get(path: string, opts?: RequestOptions): Promise { - return this.makeRequest('get', path, opts); + return this.prepareAndExecuteRequest('get', path, opts); } post(path: string, opts?: RequestOptions): Promise { - return this.makeRequest('post', path, opts); + return this.prepareAndExecuteRequest('post', path, opts); } put(path: string, opts?: RequestOptions): Promise { - return this.makeRequest('put', path, opts); + return this.prepareAndExecuteRequest('put', path, opts); } delete(path: string, opts?: RequestOptions): Promise { - return this.makeRequest('delete', path, opts); + return this.prepareAndExecuteRequest('delete', path, opts); } async upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { - const formData = await this.filesHandler.createFormData(file); + const formDataRequest = await this.filesHandler.prepareFormDataRequest(file); if (opts?.body) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.filesHandler.appendBodyToFormData(formData, opts.body as Record); + appendBodyToFormData(formDataRequest.formData, opts.body as Record); } const headers = { ...opts?.headers, - ...this.filesHandler.getMultipartFormDataHeaders(formData), + ...formDataRequest.headers, }; const options: FinalRequestOptions = { method: 'post', path: path, - body: formData, + body: formDataRequest.formData, headers, }; @@ -105,10 +119,7 @@ export abstract class APIClient { ...this.authHeaders(opts), }; - if (opts?.body instanceof FormData) { - return defaultHeaders; - } - return { ...defaultHeaders, 'Content-Type': 'application/json' }; + return { ...defaultHeaders, ...opts.headers }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -116,27 +127,34 @@ export abstract class APIClient { return {}; } - private makeRequest(method: HTTPMethod, path: string, opts?: RequestOptions): Promise { + private buildFullUrl(path: string, query?: Record): string { + let url = `${this.baseURL}${path}`; + if (query) { + const queryString = new URLSearchParams(query as Record).toString(); + url += `?${queryString}`; + } + return url; + } + + private prepareAndExecuteRequest(method: HTTPMethod, path: string, opts?: RequestOptions): Promise { const options = { method, path, - ...opts, - }; + } as FinalRequestOptions; + + if (options?.body) { + options.body = JSON.stringify(options.body); + options.headers = { ...options.headers, 'Content-Type': 'application/json' }; + } - return this.performRequest(options as FinalRequestOptions).then( + return this.performRequest(options).then( (response) => this.fetch.handleResponse(response) as Rsp, ); } private async performRequest(options: FinalRequestOptions): Promise { - let url = `${this.baseURL}${options.path}`; - - if (options.query) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const queryString = new URLSearchParams(options.query as Record).toString(); - url += `?${queryString}`; - } + const url = this.buildFullUrl(options.path, options.query as Record); const headers = { ...this.defaultHeaders(options), diff --git a/src/fetch/BrowserFetch.ts b/src/fetch/BrowserFetch.ts index 523eb41..15e0492 100644 --- a/src/fetch/BrowserFetch.ts +++ b/src/fetch/BrowserFetch.ts @@ -5,12 +5,11 @@ import { Stream, BrowserSSEDecoder } from '../streaming'; export class BrowserFetch extends BaseFetch { call(url: string, options: FinalRequestOptions): Promise { const controller = new AbortController(); - const body = options.body instanceof FormData ? options.body : JSON.stringify(options.body); return fetch(url, { method: options.method, headers: options?.headers ? (options.headers as HeadersInit) : undefined, - body, + body: options?.body ? (options.body as BodyInit) : undefined, signal: controller.signal, }); } diff --git a/src/fetch/NodeFetch.ts b/src/fetch/NodeFetch.ts index 3d67bf3..4b209af 100644 --- a/src/fetch/NodeFetch.ts +++ b/src/fetch/NodeFetch.ts @@ -1,19 +1,16 @@ import { FinalRequestOptions, CrossPlatformResponse } from 'types'; import { BaseFetch } from './BaseFetch'; import { Stream, NodeSSEDecoder } from '../streaming'; -import FormData from 'form-data'; export class NodeFetch extends BaseFetch { async call(url: string, options: FinalRequestOptions): Promise { const nodeFetchModule = await import('node-fetch'); const nodeFetch = nodeFetchModule.default; - const body = options.body instanceof FormData ? options.body : JSON.stringify(options.body); - return nodeFetch(url, { method: options.method, headers: options?.headers ? (options.headers as Record) : undefined, - body, + body: options?.body ? (options.body as import('form-data') | string) : undefined, }); } diff --git a/src/files/BaseFilesHandler.ts b/src/files/BaseFilesHandler.ts index 3e1e465..fe5841a 100644 --- a/src/files/BaseFilesHandler.ts +++ b/src/files/BaseFilesHandler.ts @@ -1,19 +1,6 @@ -import { UnifiedFormData } from 'types'; +import { FormDataRequest } from 'types/API'; import { FilePathOrFileObject } from 'types/rag'; export abstract class BaseFilesHandler { - abstract createFormData(file: FilePathOrFileObject): Promise; - - abstract getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { - for (const [key, value] of Object.entries(body)) { - if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); - } else { - formData.append(key, value); - } - } - }; + abstract prepareFormDataRequest(file: FilePathOrFileObject): Promise; } diff --git a/src/files/BrowserFilesHandler.ts b/src/files/BrowserFilesHandler.ts index 909132f..387e0c4 100644 --- a/src/files/BrowserFilesHandler.ts +++ b/src/files/BrowserFilesHandler.ts @@ -1,23 +1,12 @@ -import { UnifiedFormData } from 'types'; import { FilePathOrFileObject } from 'types/rag'; import { BaseFilesHandler } from './BaseFilesHandler'; +import { FormDataRequest } from 'types/API'; export class BrowserFilesHandler extends BaseFilesHandler { - async createFormData(file: FilePathOrFileObject): Promise { + async prepareFormDataRequest(file: FilePathOrFileObject): Promise { const formData = new FormData(); - - if (file instanceof window.File) { - formData.append('file', file); - } else { - throw new Error('Unsupported file type in browser'); - } - - return formData; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { - /* In browser, we don't need to set any additional headers for multipart/form-data, as the browser will handle it */ - return {}; + formData.append('file', file); + // Note that when uploading files in a browser, the browser handles the multipart/form-data headers + return { formData, headers: {} }; } } diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index b7fea94..08c29d3 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -1,9 +1,9 @@ -import { UnifiedFormData } from 'types'; import { FilePathOrFileObject } from 'types/rag'; import { BaseFilesHandler } from './BaseFilesHandler'; +import { FormDataRequest } from 'types/API'; export class NodeFilesHandler extends BaseFilesHandler { - async convertReadableStream(readableStream: ReadableStream): Promise { + private async convertReadableStream(readableStream: ReadableStream): Promise { const { Readable } = await import('stream'); const reader = readableStream.getReader(); @@ -19,15 +19,13 @@ export class NodeFilesHandler extends BaseFilesHandler { }); } - async createFormData(file: FilePathOrFileObject): Promise { + async prepareFormDataRequest(file: FilePathOrFileObject): Promise { const { default: FormDataNode } = await import('form-data'); const formData = new FormDataNode(); if (typeof file === 'string') { const fs = (await import('fs')).default; formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); - } else if (Buffer.isBuffer(file)) { - formData.append('file', file, { filename: 'TODO - add filename to buffer flow' }); } else if (file instanceof File) { const nodeStream = await this.convertReadableStream(file.stream()); formData.append('file', nodeStream, file.name); @@ -35,18 +33,8 @@ export class NodeFilesHandler extends BaseFilesHandler { throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); } - return formData; - } + const formDataHeaders = { 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` }; - getMultipartFormDataHeaders(formData: UnifiedFormData): Record | null { - if (formData instanceof FormData) { - throw new Error( - 'getMultipartFormDataHeaders invoked with native browser FormData instance instead of NodeJS form-data', - ); - } - const boundary = formData.getBoundary(); - return { - 'Content-Type': `multipart/form-data; boundary=${boundary}`, - }; + return { formData, headers: formDataHeaders }; } } diff --git a/src/types/API.ts b/src/types/API.ts index fad3a23..273e76f 100644 --- a/src/types/API.ts +++ b/src/types/API.ts @@ -10,7 +10,7 @@ export type RequestOptions | ArrayBuffer method?: HTTPMethod; path?: string; query?: Req | undefined; - body?: Req | FormData | null | undefined; + body?: Req | UnifiedFormData | string | null | undefined; headers?: Headers | undefined; maxRetries?: number; @@ -30,5 +30,7 @@ export type Headers = Record; export type CrossPlatformResponse = Response | import('node-fetch').Response; export type CrossPlatformReadableStream = ReadableStream | import('stream/web').ReadableStream; -export type FormDataNode = import('form-data'); -export type UnifiedFormData = FormData | FormDataNode; +export type UnifiedFormData = FormData | import('form-data'); + +export type FormDataRequest = { formData: UnifiedFormData; headers: Headers }; + diff --git a/src/types/index.ts b/src/types/index.ts index 250ed7d..28b96ed 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -36,7 +36,6 @@ export { type Headers, type CrossPlatformResponse, type UnifiedFormData, - type FormDataNode, } from './API'; export { From f438185c18a6aa31c7eb1e3a630ebb8b356974fe Mon Sep 17 00:00:00 2001 From: amirk Date: Mon, 2 Dec 2024 16:54:43 +0200 Subject: [PATCH 17/53] feat: wip --- .../studio/conversational-rag/rag-engine.ts | 13 ++++---- src/APIClient.ts | 30 +++++++++---------- src/resources/rag/ragEngine.ts | 22 ++++---------- src/types/API.ts | 1 - src/types/rag/UpdateFileRequest.ts | 5 ++++ src/types/rag/UploadFileRequest.ts | 10 +++---- src/types/rag/index.ts | 2 +- tests/unittests/resources/rag-engine.test.ts | 8 ++--- 8 files changed, 43 insertions(+), 48 deletions(-) create mode 100644 src/types/rag/UpdateFileRequest.ts diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index ed78201..58814d5 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -5,18 +5,21 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function uploadGetUpdateDelete(fileInput, label) { +async function uploadGetUpdateDelete(fileInput, path) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { - const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(fileInput, { - path: label, - }); + const uploadFileResponse: UploadFileResponse = await client.ragEngine.create( + { + file: fileInput, + path: path, + }); console.log(uploadFileResponse); let file: FileResponse = await client.ragEngine.get(uploadFileResponse.fileId); console.log(file); await sleep(1000); // Give it a sec to start process before updating console.log('Now updating the file labels and publicUrl...'); - await client.ragEngine.update(uploadFileResponse.fileId, { + await client.ragEngine.update({ + fileId: uploadFileResponse.fileId, labels: ['test99'], publicUrl: 'https://www.miri.com', }); diff --git a/src/APIClient.ts b/src/APIClient.ts index cf6701a..2689b12 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -26,18 +26,16 @@ const validatePositiveInteger = (name: string, n: unknown): number => { return n; }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { - for (const [key, value] of Object.entries(body)) { - if (Array.isArray(value)) { - value.forEach((item) => formData.append(key, item)); - } else { - formData.append(key, value); - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const appendBodyToFormData = (formData: UnifiedFormData, body: Record): void => { + for (const [key, value] of Object.entries(body)) { + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, value); } - }; - + } +}; export abstract class APIClient { protected baseURL: string; @@ -136,7 +134,11 @@ export abstract class APIClient { return url; } - private prepareAndExecuteRequest(method: HTTPMethod, path: string, opts?: RequestOptions): Promise { + private prepareAndExecuteRequest( + method: HTTPMethod, + path: string, + opts?: RequestOptions, + ): Promise { const options = { method, path, @@ -148,9 +150,7 @@ export abstract class APIClient { options.headers = { ...options.headers, 'Content-Type': 'application/json' }; } - return this.performRequest(options).then( - (response) => this.fetch.handleResponse(response) as Rsp, - ); + return this.performRequest(options).then((response) => this.fetch.handleResponse(response) as Rsp); } private async performRequest(options: FinalRequestOptions): Promise { diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index 249577b..66a4a22 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -5,32 +5,20 @@ import { UploadFileRequest, ListFilesFilters, UpdateFileRequest, - FilePathOrFileObject, } from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; const RAG_ENGINE_PATH = '/library/files'; export class RAGEngine extends APIResource { - async create( - file: string, - body: UploadFileRequest, - options?: Models.RequestOptions, - ): Promise; - - async create( - file: File, - body: UploadFileRequest, - options?: Models.RequestOptions, - ): Promise; async create( - file: FilePathOrFileObject, body: UploadFileRequest, options?: Models.RequestOptions, ): Promise { + const {file, ...bodyWithoutFile} = body return this.client.upload(RAG_ENGINE_PATH, file, { - body: body, + body: bodyWithoutFile, ...options, } as Models.RequestOptions) as Promise; } @@ -49,15 +37,15 @@ export class RAGEngine extends APIResource { ) as Promise; } - list(body: ListFilesFilters | null, options?: Models.RequestOptions): Promise { + list(body?: ListFilesFilters, options?: Models.RequestOptions): Promise { return this.client.get(RAG_ENGINE_PATH, { query: body, ...options, } as Models.RequestOptions) as Promise; } - update(fileId: string, body: UpdateFileRequest, options?: Models.RequestOptions): Promise { - return this.client.put(`${RAG_ENGINE_PATH}/${fileId}`, { + update(body: UpdateFileRequest, options?: Models.RequestOptions): Promise { + return this.client.put(`${RAG_ENGINE_PATH}/${body.fileId}`, { body, ...options, } as Models.RequestOptions) as Promise; diff --git a/src/types/API.ts b/src/types/API.ts index 273e76f..aec9368 100644 --- a/src/types/API.ts +++ b/src/types/API.ts @@ -33,4 +33,3 @@ export type CrossPlatformReadableStream = ReadableStream | import('s export type UnifiedFormData = FormData | import('form-data'); export type FormDataRequest = { formData: UnifiedFormData; headers: Headers }; - diff --git a/src/types/rag/UpdateFileRequest.ts b/src/types/rag/UpdateFileRequest.ts new file mode 100644 index 0000000..601805a --- /dev/null +++ b/src/types/rag/UpdateFileRequest.ts @@ -0,0 +1,5 @@ +export interface UpdateFileRequest { + fileId: string; + labels?: string[] | null; + publicUrl?: string | null; +} diff --git a/src/types/rag/UploadFileRequest.ts b/src/types/rag/UploadFileRequest.ts index ad4ca8c..db76887 100644 --- a/src/types/rag/UploadFileRequest.ts +++ b/src/types/rag/UploadFileRequest.ts @@ -1,8 +1,8 @@ -export interface UpdateFileRequest { - labels?: string[] | null; - publicUrl?: string | null; -} +import { FilePathOrFileObject } from "./FilePathOrFileObject"; -export interface UploadFileRequest extends UpdateFileRequest { +export interface UploadFileRequest { + file: FilePathOrFileObject path?: string | null; + labels?: string[] | null; + publicUrl?: string | null; } diff --git a/src/types/rag/index.ts b/src/types/rag/index.ts index 76431a3..4676d0d 100644 --- a/src/types/rag/index.ts +++ b/src/types/rag/index.ts @@ -14,6 +14,6 @@ export { type UploadFileResponse } from './UploadFileResponse'; export { type ListFilesFilters } from './ListFilesFilters'; -export { type UpdateFileRequest } from './UploadFileRequest'; +export { type UpdateFileRequest } from './UpdateFileRequest'; export { type FilePathOrFileObject } from './FilePathOrFileObject'; diff --git a/tests/unittests/resources/rag-engine.test.ts b/tests/unittests/resources/rag-engine.test.ts index 540639e..460c3d6 100644 --- a/tests/unittests/resources/rag-engine.test.ts +++ b/tests/unittests/resources/rag-engine.test.ts @@ -31,12 +31,12 @@ describe('RAGEngine', () => { it('should upload a file and return the fileId', async () => { const fileInput = 'path/to/file.txt'; - const body = { path: 'label' }; + const body = { file: fileInput, path: 'label' }; const expectedResponse = { fileId: '12345' }; mockClient.upload.mockResolvedValue(expectedResponse); - const response = await ragEngine.create(fileInput, body, options); + const response = await ragEngine.create(body); expect(mockClient.upload).toHaveBeenCalledWith( '/library/files', @@ -77,11 +77,11 @@ describe('RAGEngine', () => { it('should update a file by ID and return null', async () => { const fileId = '12345'; - const body = { labels: ['test'], publicUrl: 'https://example.com' }; + const body = { fileId, labels: ['test'], publicUrl: 'https://example.com' }; mockClient.put.mockResolvedValue(null); - const response = await ragEngine.update(fileId, body, options); + const response = await ragEngine.update(body); expect(mockClient.put).toHaveBeenCalledWith( `/library/files/${fileId}`, From 382e69fef3829396844ec2ab21585d54ff069364 Mon Sep 17 00:00:00 2001 From: amirk Date: Mon, 2 Dec 2024 16:54:51 +0200 Subject: [PATCH 18/53] feat: wip --- examples/studio/conversational-rag/rag-engine.ts | 9 ++++----- src/resources/rag/ragEngine.ts | 15 +++------------ src/types/rag/UploadFileRequest.ts | 4 ++-- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 58814d5..90a4ac3 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -8,11 +8,10 @@ function sleep(ms) { async function uploadGetUpdateDelete(fileInput, path) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { - const uploadFileResponse: UploadFileResponse = await client.ragEngine.create( - { - file: fileInput, - path: path, - }); + const uploadFileResponse: UploadFileResponse = await client.ragEngine.create({ + file: fileInput, + path: path, + }); console.log(uploadFileResponse); let file: FileResponse = await client.ragEngine.get(uploadFileResponse.fileId); console.log(file); diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/ragEngine.ts index 66a4a22..c3023c6 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/ragEngine.ts @@ -1,22 +1,13 @@ import * as Models from '../../types'; import { APIResource } from '../../APIResource'; -import { - UploadFileResponse, - UploadFileRequest, - ListFilesFilters, - UpdateFileRequest, -} from '../../types/rag'; +import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; const RAG_ENGINE_PATH = '/library/files'; export class RAGEngine extends APIResource { - - async create( - body: UploadFileRequest, - options?: Models.RequestOptions, - ): Promise { - const {file, ...bodyWithoutFile} = body + async create(body: UploadFileRequest, options?: Models.RequestOptions): Promise { + const { file, ...bodyWithoutFile } = body; return this.client.upload(RAG_ENGINE_PATH, file, { body: bodyWithoutFile, ...options, diff --git a/src/types/rag/UploadFileRequest.ts b/src/types/rag/UploadFileRequest.ts index db76887..b6bd993 100644 --- a/src/types/rag/UploadFileRequest.ts +++ b/src/types/rag/UploadFileRequest.ts @@ -1,7 +1,7 @@ -import { FilePathOrFileObject } from "./FilePathOrFileObject"; +import { FilePathOrFileObject } from './FilePathOrFileObject'; export interface UploadFileRequest { - file: FilePathOrFileObject + file: FilePathOrFileObject; path?: string | null; labels?: string[] | null; publicUrl?: string | null; From 7ca7640e919b24b456ee292cccc848962250e7a0 Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 09:38:54 +0200 Subject: [PATCH 19/53] feat: add file path check before opening + rag examples improved --- .prettierignore | 1 + .../conversational-rag/files/meerkat.txt | 1 + .../studio/conversational-rag/rag-engine.ts | 58 ++++++++--- src/AI21.ts | 4 +- src/files/NodeFilesHandler.ts | 3 + src/resources/index.ts | 2 +- src/resources/rag/{ragEngine.ts => files.ts} | 14 +-- src/resources/rag/index.ts | 2 +- .../resources/chat/conversational-rag.test.ts | 97 ------------------- tests/unittests/resources/rag-engine.test.ts | 16 +-- 10 files changed, 66 insertions(+), 132 deletions(-) create mode 100644 .prettierignore create mode 100644 examples/studio/conversational-rag/files/meerkat.txt rename src/resources/rag/{ragEngine.ts => files.ts} (82%) diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..314f02b --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.txt \ No newline at end of file diff --git a/examples/studio/conversational-rag/files/meerkat.txt b/examples/studio/conversational-rag/files/meerkat.txt new file mode 100644 index 0000000..db86219 --- /dev/null +++ b/examples/studio/conversational-rag/files/meerkat.txt @@ -0,0 +1 @@ +The meerkat (Suricata suricatta) or suricate is a small mongoose found in southern Africa. It is characterised by a broad head, large eyes, a pointed snout, long legs, a thin tapering tail, and a brindled coat pattern. The head-and-body length is around 24–35 cm (9.4–13.8 in), and the weight is typically between 0.62 and 0.97 kg (1.4 and 2.1 lb). The coat is light grey to yellowish-brown with alternate, poorly-defined light and dark bands on the back. Meerkats have foreclaws adapted for digging and have the ability to thermoregulate to survive in their harsh, dry habitat. Three subspecies are recognised. \ No newline at end of file diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 90a4ac3..9e490d4 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,32 +1,57 @@ import { AI21 } from 'ai21'; import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; +import path from 'path'; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } +async function waitForFileProcessing( + client: AI21, + fileId: string, + timeout: number = 30000, + interval: number = 1000, +) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const file: FileResponse = await client.files.get(fileId); + if (file.status !== 'PROCESSING') { + return file; + } + await sleep(interval); + } + + throw new Error(`File processing timed out after ${timeout}ms`); +} + async function uploadGetUpdateDelete(fileInput, path) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { - const uploadFileResponse: UploadFileResponse = await client.ragEngine.create({ + const uploadFileResponse: UploadFileResponse = await client.files.create({ file: fileInput, path: path, }); console.log(uploadFileResponse); - let file: FileResponse = await client.ragEngine.get(uploadFileResponse.fileId); - console.log(file); - await sleep(1000); // Give it a sec to start process before updating - console.log('Now updating the file labels and publicUrl...'); - await client.ragEngine.update({ - fileId: uploadFileResponse.fileId, - labels: ['test99'], - publicUrl: 'https://www.miri.com', - }); - file = await client.ragEngine.get(uploadFileResponse.fileId); + + let file: FileResponse = await waitForFileProcessing(client, uploadFileResponse.fileId); console.log(file); + if (file.status === 'PROCESSED') { + console.log('Now updating the file labels and publicUrl...'); + await client.files.update({ + fileId: uploadFileResponse.fileId, + labels: ['test99'], + publicUrl: 'https://www.miri.com', + }); + file = await client.files.get(uploadFileResponse.fileId); + console.log(file); + } else { + console.log(`File did not processed well, ended with status ${file.status}`); + } + console.log('Now deleting the file'); - await client.ragEngine.delete(uploadFileResponse.fileId); + await client.files.delete(uploadFileResponse.fileId); } catch (error) { console.error('Error:', error); } @@ -34,15 +59,16 @@ async function uploadGetUpdateDelete(fileInput, path) { async function listFiles() { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); - const files = await client.ragEngine.list({ limit: 4 }); + const files = await client.files.list({ limit: 4 }); console.log(files); } -/* Simulate a file upload passing file path */ -const filePath = '/Users/amirkoblyansky/Documents/ukraine.txt'; +/* Simulate file upload passing a path to file */ +const filePath = path.join(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); // Use process.cwd() to get the current working directory + uploadGetUpdateDelete(filePath, Date.now().toString()).catch(console.error); -/* Simulate a file upload passing File instance */ +/* Simulate file upload passing File instance */ const fileContent = Buffer.from( 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', ); diff --git a/src/AI21.ts b/src/AI21.ts index 5804d46..5f65881 100644 --- a/src/AI21.ts +++ b/src/AI21.ts @@ -6,7 +6,7 @@ import { APIClient } from './APIClient'; import { Headers } from './types'; import * as Runtime from './runtime'; import { ConversationalRag } from './resources/rag/conversationalRag'; -import { RAGEngine } from './resources'; +import { Files } from './resources'; export interface ClientOptions { baseURL?: string | undefined; @@ -68,7 +68,7 @@ export class AI21 extends APIClient { // Resources chat: Chat = new Chat(this); conversationalRag: ConversationalRag = new ConversationalRag(this); - ragEngine: RAGEngine = new RAGEngine(this); + files: Files = new Files(this); // eslint-disable-next-line @typescript-eslint/no-unused-vars protected override authHeaders(_: Types.FinalRequestOptions): Types.Headers { diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 08c29d3..8c99444 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -25,6 +25,9 @@ export class NodeFilesHandler extends BaseFilesHandler { if (typeof file === 'string') { const fs = (await import('fs')).default; + if (!fs.existsSync(file)) { + throw new Error(`File not found: ${file}`); + } formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); } else if (file instanceof File) { const nodeStream = await this.convertReadableStream(file.stream()); diff --git a/src/resources/index.ts b/src/resources/index.ts index 74ae577..1ac1372 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -1,3 +1,3 @@ export { Chat, Completions } from './chat'; export { ConversationalRag } from './rag'; -export { RAGEngine } from './rag'; +export { Files } from './rag'; diff --git a/src/resources/rag/ragEngine.ts b/src/resources/rag/files.ts similarity index 82% rename from src/resources/rag/ragEngine.ts rename to src/resources/rag/files.ts index c3023c6..84e2620 100644 --- a/src/resources/rag/ragEngine.ts +++ b/src/resources/rag/files.ts @@ -3,12 +3,12 @@ import { APIResource } from '../../APIResource'; import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types/rag'; import { FileResponse } from 'types/rag/FileResponse'; -const RAG_ENGINE_PATH = '/library/files'; +const FILES_PATH = '/library/files'; -export class RAGEngine extends APIResource { +export class Files extends APIResource { async create(body: UploadFileRequest, options?: Models.RequestOptions): Promise { const { file, ...bodyWithoutFile } = body; - return this.client.upload(RAG_ENGINE_PATH, file, { + return this.client.upload(FILES_PATH, file, { body: bodyWithoutFile, ...options, } as Models.RequestOptions) as Promise; @@ -16,27 +16,27 @@ export class RAGEngine extends APIResource { get(fileId: string, options?: Models.RequestOptions): Promise { return this.client.get( - `${RAG_ENGINE_PATH}/${fileId}`, + `${FILES_PATH}/${fileId}`, options as Models.RequestOptions, ) as Promise; } delete(fileId: string, options?: Models.RequestOptions): Promise { return this.client.delete( - `${RAG_ENGINE_PATH}/${fileId}`, + `${FILES_PATH}/${fileId}`, options as Models.RequestOptions, ) as Promise; } list(body?: ListFilesFilters, options?: Models.RequestOptions): Promise { - return this.client.get(RAG_ENGINE_PATH, { + return this.client.get(FILES_PATH, { query: body, ...options, } as Models.RequestOptions) as Promise; } update(body: UpdateFileRequest, options?: Models.RequestOptions): Promise { - return this.client.put(`${RAG_ENGINE_PATH}/${body.fileId}`, { + return this.client.put(`${FILES_PATH}/${body.fileId}`, { body, ...options, } as Models.RequestOptions) as Promise; diff --git a/src/resources/rag/index.ts b/src/resources/rag/index.ts index c349bd2..d86c128 100644 --- a/src/resources/rag/index.ts +++ b/src/resources/rag/index.ts @@ -1,2 +1,2 @@ export { ConversationalRag } from './conversationalRag'; -export { RAGEngine } from './ragEngine'; +export { Files } from './files'; diff --git a/tests/unittests/resources/chat/conversational-rag.test.ts b/tests/unittests/resources/chat/conversational-rag.test.ts index 2d5473f..818000e 100644 --- a/tests/unittests/resources/chat/conversational-rag.test.ts +++ b/tests/unittests/resources/chat/conversational-rag.test.ts @@ -1,7 +1,6 @@ import * as Models from '../../../../src/types'; import {ConversationalRag} from "../../../../src/resources/rag/conversationalRag"; import { APIClient } from '../../../../src/APIClient'; -import { RAGEngine } from '../../../../src/resources/rag/ragEngine'; class MockAPIClient extends APIClient { @@ -101,99 +100,3 @@ describe('ConversationalRag', () => { expect(response).toEqual(expectedResponse); }); }); - -describe('RAGEngine', () => { - let ragEngine: RAGEngine; - let mockClient: MockAPIClient; - const dummyAPIKey = "test-api-key"; - - beforeEach(() => { - mockClient = new MockAPIClient({ - baseURL: 'https://api.example.com', - maxRetries: 3, - timeout: 5000, - }); - - ragEngine = new RAGEngine(mockClient); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should upload a file and return the response', async () => { - const fileInput = 'path/to/file.txt'; - const body = { path: 'label' }; - const expectedResponse = { fileId: '12345' }; - - mockClient.upload.mockResolvedValue(expectedResponse); - - const response = await ragEngine.create(fileInput, body); - - expect(mockClient.upload).toHaveBeenCalledWith( - '/library/files', - fileInput, - { body, headers: undefined } - ); - expect(response).toEqual(expectedResponse); - }); - - it('should get a file by ID and return the response', async () => { - const fileId = '12345'; - const expectedResponse = { id: fileId, name: 'file.txt' }; - - mockClient.get.mockResolvedValue(expectedResponse); - - const response = await ragEngine.get(fileId); - - expect(mockClient.get).toHaveBeenCalledWith( - `/library/files/${fileId}`, - undefined - ); - expect(response).toEqual(expectedResponse); - }); - - it('should delete a file by ID', async () => { - const fileId = '12345'; - - mockClient.delete.mockResolvedValue(null); - - const response = await ragEngine.delete(fileId); - - expect(mockClient.delete).toHaveBeenCalledWith( - `/library/files/${fileId}`, - undefined - ); - expect(response).toBeNull(); - }); - - it('should update a file by ID and return null', async () => { - const fileId = '12345'; - const body = { labels: ['test'], publicUrl: 'https://example.com' }; - - mockClient.put.mockResolvedValue(null); - - const response = await ragEngine.update(fileId, body); - - expect(mockClient.put).toHaveBeenCalledWith( - `/library/files/${fileId}`, - { body, headers: undefined } - ); - expect(response).toBeNull(); - }); - - it('should list files and return the response', async () => { - const filters = { limit: 4 }; - const expectedResponse = [{ id: '12345', name: 'file.txt' }]; - - mockClient.get.mockResolvedValue(expectedResponse); - - const response = await ragEngine.list(filters); - - expect(mockClient.get).toHaveBeenCalledWith( - '/library/files', - { query: filters, headers: undefined } - ); - expect(response).toEqual(expectedResponse); - }); -}); diff --git a/tests/unittests/resources/rag-engine.test.ts b/tests/unittests/resources/rag-engine.test.ts index 460c3d6..a774b86 100644 --- a/tests/unittests/resources/rag-engine.test.ts +++ b/tests/unittests/resources/rag-engine.test.ts @@ -1,5 +1,5 @@ import * as Models from '../../../src/types'; -import { RAGEngine } from '../../../src/resources/rag/ragEngine'; +import { Files } from '../../../src/resources/rag/files'; import { APIClient } from '../../../src/APIClient'; class MockAPIClient extends APIClient { @@ -10,7 +10,7 @@ class MockAPIClient extends APIClient { } describe('RAGEngine', () => { - let ragEngine: RAGEngine; + let files: Files; let mockClient: MockAPIClient; const dummyAPIKey = "test-api-key"; const options: Models.RequestOptions = { headers: { 'Authorization': `Bearer ${dummyAPIKey}` } }; @@ -22,7 +22,7 @@ describe('RAGEngine', () => { timeout: 5000, }); - ragEngine = new RAGEngine(mockClient); + files = new Files(mockClient); }); afterEach(() => { @@ -36,7 +36,7 @@ describe('RAGEngine', () => { mockClient.upload.mockResolvedValue(expectedResponse); - const response = await ragEngine.create(body); + const response = await files.create(body); expect(mockClient.upload).toHaveBeenCalledWith( '/library/files', @@ -52,7 +52,7 @@ describe('RAGEngine', () => { mockClient.get.mockResolvedValue(expectedResponse); - const response = await ragEngine.get(fileId, options); + const response = await files.get(fileId, options); expect(mockClient.get).toHaveBeenCalledWith( `/library/files/${fileId}`, @@ -66,7 +66,7 @@ describe('RAGEngine', () => { mockClient.delete.mockResolvedValue(null); - const response = await ragEngine.delete(fileId, options); + const response = await files.delete(fileId, options); expect(mockClient.delete).toHaveBeenCalledWith( `/library/files/${fileId}`, @@ -81,7 +81,7 @@ describe('RAGEngine', () => { mockClient.put.mockResolvedValue(null); - const response = await ragEngine.update(body); + const response = await files.update(body); expect(mockClient.put).toHaveBeenCalledWith( `/library/files/${fileId}`, @@ -96,7 +96,7 @@ describe('RAGEngine', () => { mockClient.get.mockResolvedValue(expectedResponse); - const response = await ragEngine.list(filters, options); + const response = await files.list(filters, options); expect(mockClient.get).toHaveBeenCalledWith( '/library/files', From 9705d0b72b540fdf655981453104d049f16f93b0 Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 10:17:31 +0200 Subject: [PATCH 20/53] feat: fix tests --- .../studio/conversational-rag/rag-engine.ts | 2 +- src/APIClient.ts | 2 +- src/files/BaseFilesHandler.ts | 2 +- src/files/BrowserFilesHandler.ts | 2 +- src/files/NodeFilesHandler.ts | 2 +- src/resources/rag/conversationalRag.ts | 4 ++-- src/resources/rag/files.ts | 4 ++-- .../{rag => files}/FilePathOrFileObject.ts | 0 src/types/{rag => files}/FileResponse.ts | 0 src/types/{rag => files}/ListFilesFilters.ts | 0 src/types/{rag => files}/UpdateFileRequest.ts | 0 src/types/{rag => files}/UploadFileRequest.ts | 0 src/types/{rag => files}/UploadFileResponse.ts | 0 src/types/files/index.ts | 11 +++++++++++ src/types/index.ts | 9 +++++++++ src/types/rag/index.ts | 12 ------------ tests/unittests/resources/rag-engine.test.ts | 17 +++++++++++------ 17 files changed, 40 insertions(+), 27 deletions(-) rename src/types/{rag => files}/FilePathOrFileObject.ts (100%) rename src/types/{rag => files}/FileResponse.ts (100%) rename src/types/{rag => files}/ListFilesFilters.ts (100%) rename src/types/{rag => files}/UpdateFileRequest.ts (100%) rename src/types/{rag => files}/UploadFileRequest.ts (100%) rename src/types/{rag => files}/UploadFileResponse.ts (100%) create mode 100644 src/types/files/index.ts diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 9e490d4..1f10721 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,5 +1,5 @@ import { AI21 } from 'ai21'; -import { FileResponse, UploadFileResponse } from '../../../src/types/rag'; +import { FileResponse, UploadFileResponse } from '../../../src/types'; import path from 'path'; function sleep(ms) { diff --git a/src/APIClient.ts b/src/APIClient.ts index 2689b12..be74b8f 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -9,11 +9,11 @@ import { Headers, CrossPlatformResponse, UnifiedFormData, + FilePathOrFileObject, } from './types'; import { AI21EnvConfig } from './EnvConfig'; import { createFetchInstance, createFilesHandlerInstance } from './runtime'; import { Fetch } from 'fetch'; -import { FilePathOrFileObject } from 'types/rag'; import { BaseFilesHandler } from 'files/BaseFilesHandler'; const validatePositiveInteger = (name: string, n: unknown): number => { diff --git a/src/files/BaseFilesHandler.ts b/src/files/BaseFilesHandler.ts index fe5841a..94d05b4 100644 --- a/src/files/BaseFilesHandler.ts +++ b/src/files/BaseFilesHandler.ts @@ -1,5 +1,5 @@ +import { FilePathOrFileObject } from 'types'; import { FormDataRequest } from 'types/API'; -import { FilePathOrFileObject } from 'types/rag'; export abstract class BaseFilesHandler { abstract prepareFormDataRequest(file: FilePathOrFileObject): Promise; diff --git a/src/files/BrowserFilesHandler.ts b/src/files/BrowserFilesHandler.ts index 387e0c4..59c1d6b 100644 --- a/src/files/BrowserFilesHandler.ts +++ b/src/files/BrowserFilesHandler.ts @@ -1,4 +1,4 @@ -import { FilePathOrFileObject } from 'types/rag'; +import { FilePathOrFileObject } from 'types'; import { BaseFilesHandler } from './BaseFilesHandler'; import { FormDataRequest } from 'types/API'; diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 8c99444..1080003 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -1,4 +1,4 @@ -import { FilePathOrFileObject } from 'types/rag'; +import { FilePathOrFileObject } from 'types'; import { BaseFilesHandler } from './BaseFilesHandler'; import { FormDataRequest } from 'types/API'; diff --git a/src/resources/rag/conversationalRag.ts b/src/resources/rag/conversationalRag.ts index bc5caa0..3ec423b 100644 --- a/src/resources/rag/conversationalRag.ts +++ b/src/resources/rag/conversationalRag.ts @@ -1,7 +1,7 @@ import * as Models from '../../types'; import { APIResource } from '../../APIResource'; -import { ConversationalRagRequest } from '../../types/rag/ConversationalRagRequest'; -import { ConversationalRagResponse } from '../../types/rag/ConversationalRagResponse'; +import { ConversationalRagRequest } from '../../types'; +import { ConversationalRagResponse } from '../../types'; export class ConversationalRag extends APIResource { create(body: ConversationalRagRequest, options?: Models.RequestOptions) { diff --git a/src/resources/rag/files.ts b/src/resources/rag/files.ts index 84e2620..f3bf691 100644 --- a/src/resources/rag/files.ts +++ b/src/resources/rag/files.ts @@ -1,7 +1,7 @@ import * as Models from '../../types'; import { APIResource } from '../../APIResource'; -import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types/rag'; -import { FileResponse } from 'types/rag/FileResponse'; +import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types'; +import { FileResponse } from 'types/files/FileResponse'; const FILES_PATH = '/library/files'; diff --git a/src/types/rag/FilePathOrFileObject.ts b/src/types/files/FilePathOrFileObject.ts similarity index 100% rename from src/types/rag/FilePathOrFileObject.ts rename to src/types/files/FilePathOrFileObject.ts diff --git a/src/types/rag/FileResponse.ts b/src/types/files/FileResponse.ts similarity index 100% rename from src/types/rag/FileResponse.ts rename to src/types/files/FileResponse.ts diff --git a/src/types/rag/ListFilesFilters.ts b/src/types/files/ListFilesFilters.ts similarity index 100% rename from src/types/rag/ListFilesFilters.ts rename to src/types/files/ListFilesFilters.ts diff --git a/src/types/rag/UpdateFileRequest.ts b/src/types/files/UpdateFileRequest.ts similarity index 100% rename from src/types/rag/UpdateFileRequest.ts rename to src/types/files/UpdateFileRequest.ts diff --git a/src/types/rag/UploadFileRequest.ts b/src/types/files/UploadFileRequest.ts similarity index 100% rename from src/types/rag/UploadFileRequest.ts rename to src/types/files/UploadFileRequest.ts diff --git a/src/types/rag/UploadFileResponse.ts b/src/types/files/UploadFileResponse.ts similarity index 100% rename from src/types/rag/UploadFileResponse.ts rename to src/types/files/UploadFileResponse.ts diff --git a/src/types/files/index.ts b/src/types/files/index.ts new file mode 100644 index 0000000..4affbf0 --- /dev/null +++ b/src/types/files/index.ts @@ -0,0 +1,11 @@ +export { type UploadFileRequest } from './UploadFileRequest'; + +export { type FileResponse } from './FileResponse'; + +export { type UploadFileResponse } from './UploadFileResponse'; + +export { type ListFilesFilters } from './ListFilesFilters'; + +export { type UpdateFileRequest } from './UpdateFileRequest'; + +export { type FilePathOrFileObject } from './FilePathOrFileObject'; diff --git a/src/types/index.ts b/src/types/index.ts index 28b96ed..d40670f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -44,3 +44,12 @@ export { type ConversationalRagSource, type RetrievalStrategy, } from './rag'; + +export { + type UploadFileRequest, + type UploadFileResponse, + type FileResponse, + type ListFilesFilters, + type UpdateFileRequest, + type FilePathOrFileObject, +} from './files'; diff --git a/src/types/rag/index.ts b/src/types/rag/index.ts index 4676d0d..7dc7954 100644 --- a/src/types/rag/index.ts +++ b/src/types/rag/index.ts @@ -5,15 +5,3 @@ export { type ConversationalRagSource } from './ConversationalRagSource'; export { type ConversationalRagResponse } from './ConversationalRagResponse'; export { type RetrievalStrategy } from './RetrievalStrategy'; - -export { type UploadFileRequest } from './UploadFileRequest'; - -export { type FileResponse } from './FileResponse'; - -export { type UploadFileResponse } from './UploadFileResponse'; - -export { type ListFilesFilters } from './ListFilesFilters'; - -export { type UpdateFileRequest } from './UpdateFileRequest'; - -export { type FilePathOrFileObject } from './FilePathOrFileObject'; diff --git a/tests/unittests/resources/rag-engine.test.ts b/tests/unittests/resources/rag-engine.test.ts index a774b86..b85907b 100644 --- a/tests/unittests/resources/rag-engine.test.ts +++ b/tests/unittests/resources/rag-engine.test.ts @@ -31,7 +31,7 @@ describe('RAGEngine', () => { it('should upload a file and return the fileId', async () => { const fileInput = 'path/to/file.txt'; - const body = { file: fileInput, path: 'label' }; + const body = { file: fileInput, path: 'path' }; const expectedResponse = { fileId: '12345' }; mockClient.upload.mockResolvedValue(expectedResponse); @@ -41,7 +41,9 @@ describe('RAGEngine', () => { expect(mockClient.upload).toHaveBeenCalledWith( '/library/files', fileInput, - { body, headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + { + body: { path: 'path' }, + } ); expect(response).toEqual(expectedResponse); }); @@ -56,7 +58,7 @@ describe('RAGEngine', () => { expect(mockClient.get).toHaveBeenCalledWith( `/library/files/${fileId}`, - { headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + { ...options } ); expect(response).toEqual(expectedResponse); }); @@ -70,7 +72,7 @@ describe('RAGEngine', () => { expect(mockClient.delete).toHaveBeenCalledWith( `/library/files/${fileId}`, - { headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + { ...options } ); expect(response).toBeNull(); }); @@ -85,7 +87,7 @@ describe('RAGEngine', () => { expect(mockClient.put).toHaveBeenCalledWith( `/library/files/${fileId}`, - { body, headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + { body }, ); expect(response).toBeNull(); }); @@ -100,7 +102,10 @@ describe('RAGEngine', () => { expect(mockClient.get).toHaveBeenCalledWith( '/library/files', - { query: filters, headers: { 'Authorization': `Bearer ${dummyAPIKey}` } } + { + query: filters, + headers: { 'Authorization': `Bearer ${dummyAPIKey}` } + } ); expect(response).toEqual(expectedResponse); }); From c88bf0200f9c3482dc3f76c74f7290d74c587a8e Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 10:22:02 +0200 Subject: [PATCH 21/53] feat: reorg imports --- src/AI21.ts | 2 +- src/resources/{rag => files}/files.ts | 0 src/resources/files/index.ts | 1 + src/resources/index.ts | 2 +- .../rag/{conversationalRag.ts => conversational-rag.ts} | 0 src/resources/rag/index.ts | 3 +-- tests/unittests/resources/chat/conversational-rag.test.ts | 2 +- tests/unittests/resources/rag-engine.test.ts | 2 +- 8 files changed, 6 insertions(+), 6 deletions(-) rename src/resources/{rag => files}/files.ts (100%) create mode 100644 src/resources/files/index.ts rename src/resources/rag/{conversationalRag.ts => conversational-rag.ts} (100%) diff --git a/src/AI21.ts b/src/AI21.ts index 5f65881..36b8d27 100644 --- a/src/AI21.ts +++ b/src/AI21.ts @@ -5,7 +5,7 @@ import { Chat } from './resources/chat'; import { APIClient } from './APIClient'; import { Headers } from './types'; import * as Runtime from './runtime'; -import { ConversationalRag } from './resources/rag/conversationalRag'; +import { ConversationalRag } from './resources/rag/conversational-rag'; import { Files } from './resources'; export interface ClientOptions { diff --git a/src/resources/rag/files.ts b/src/resources/files/files.ts similarity index 100% rename from src/resources/rag/files.ts rename to src/resources/files/files.ts diff --git a/src/resources/files/index.ts b/src/resources/files/index.ts new file mode 100644 index 0000000..21742ad --- /dev/null +++ b/src/resources/files/index.ts @@ -0,0 +1 @@ +export { Files } from './files'; diff --git a/src/resources/index.ts b/src/resources/index.ts index 1ac1372..12d4ba4 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -1,3 +1,3 @@ export { Chat, Completions } from './chat'; export { ConversationalRag } from './rag'; -export { Files } from './rag'; +export { Files } from './files'; diff --git a/src/resources/rag/conversationalRag.ts b/src/resources/rag/conversational-rag.ts similarity index 100% rename from src/resources/rag/conversationalRag.ts rename to src/resources/rag/conversational-rag.ts diff --git a/src/resources/rag/index.ts b/src/resources/rag/index.ts index d86c128..187dc07 100644 --- a/src/resources/rag/index.ts +++ b/src/resources/rag/index.ts @@ -1,2 +1 @@ -export { ConversationalRag } from './conversationalRag'; -export { Files } from './files'; +export { ConversationalRag } from './conversational-rag'; diff --git a/tests/unittests/resources/chat/conversational-rag.test.ts b/tests/unittests/resources/chat/conversational-rag.test.ts index 818000e..9d6db39 100644 --- a/tests/unittests/resources/chat/conversational-rag.test.ts +++ b/tests/unittests/resources/chat/conversational-rag.test.ts @@ -1,5 +1,5 @@ import * as Models from '../../../../src/types'; -import {ConversationalRag} from "../../../../src/resources/rag/conversationalRag"; +import {ConversationalRag} from "../../../../src/resources/rag/conversational-rag"; import { APIClient } from '../../../../src/APIClient'; diff --git a/tests/unittests/resources/rag-engine.test.ts b/tests/unittests/resources/rag-engine.test.ts index b85907b..f1d4f36 100644 --- a/tests/unittests/resources/rag-engine.test.ts +++ b/tests/unittests/resources/rag-engine.test.ts @@ -1,5 +1,5 @@ import * as Models from '../../../src/types'; -import { Files } from '../../../src/resources/rag/files'; +import { Files } from '../../../src/resources/files/files'; import { APIClient } from '../../../src/APIClient'; class MockAPIClient extends APIClient { From e3b08107e73b341013bd5c8b128fbfa003c66009 Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 10:31:01 +0200 Subject: [PATCH 22/53] feat: convert upload to non async --- src/APIClient.ts | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/APIClient.ts b/src/APIClient.ts index be74b8f..e84bff3 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -15,6 +15,7 @@ import { AI21EnvConfig } from './EnvConfig'; import { createFetchInstance, createFilesHandlerInstance } from './runtime'; import { Fetch } from 'fetch'; import { BaseFilesHandler } from 'files/BaseFilesHandler'; +import { FormDataRequest } from 'types/API'; const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { @@ -79,27 +80,27 @@ export abstract class APIClient { return this.prepareAndExecuteRequest('delete', path, opts); } - async upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { - const formDataRequest = await this.filesHandler.prepareFormDataRequest(file); - - if (opts?.body) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - appendBodyToFormData(formDataRequest.formData, opts.body as Record); - } - - const headers = { - ...opts?.headers, - ...formDataRequest.headers, - }; - - const options: FinalRequestOptions = { - method: 'post', - path: path, - body: formDataRequest.formData, - headers, - }; - - return this.performRequest(options).then((response) => this.fetch.handleResponse(response) as Rsp); + upload(path: string, file: FilePathOrFileObject, opts?: RequestOptions): Promise { + return this.filesHandler.prepareFormDataRequest(file).then((formDataRequest: FormDataRequest) => { + if (opts?.body) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appendBodyToFormData(formDataRequest.formData, opts.body as Record); + } + + const headers = { + ...opts?.headers, + ...formDataRequest.headers, + }; + + const options: FinalRequestOptions = { + method: 'post', + path: path, + body: formDataRequest.formData, + headers, + }; + + return this.performRequest(options).then((response) => this.fetch.handleResponse(response) as Rsp); + }); } protected getUserAgent(): string { From 29997e0fcca72480a8e6d52784537ab569a61040 Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 10:46:10 +0200 Subject: [PATCH 23/53] feat: node fetch casting --- src/APIClient.ts | 6 +++--- src/fetch/NodeFetch.ts | 3 ++- src/types/API.ts | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/APIClient.ts b/src/APIClient.ts index e84bff3..1bfe18d 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -86,19 +86,19 @@ export abstract class APIClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any appendBodyToFormData(formDataRequest.formData, opts.body as Record); } - + const headers = { ...opts?.headers, ...formDataRequest.headers, }; - + const options: FinalRequestOptions = { method: 'post', path: path, body: formDataRequest.formData, headers, }; - + return this.performRequest(options).then((response) => this.fetch.handleResponse(response) as Rsp); }); } diff --git a/src/fetch/NodeFetch.ts b/src/fetch/NodeFetch.ts index 4b209af..f649e4d 100644 --- a/src/fetch/NodeFetch.ts +++ b/src/fetch/NodeFetch.ts @@ -1,6 +1,7 @@ import { FinalRequestOptions, CrossPlatformResponse } from 'types'; import { BaseFetch } from './BaseFetch'; import { Stream, NodeSSEDecoder } from '../streaming'; +import { NodeHTTPBody } from 'types/API'; export class NodeFetch extends BaseFetch { async call(url: string, options: FinalRequestOptions): Promise { @@ -10,7 +11,7 @@ export class NodeFetch extends BaseFetch { return nodeFetch(url, { method: options.method, headers: options?.headers ? (options.headers as Record) : undefined, - body: options?.body ? (options.body as import('form-data') | string) : undefined, + body: options?.body ? (options.body as NodeHTTPBody) : undefined, }); } diff --git a/src/types/API.ts b/src/types/API.ts index aec9368..67b64fd 100644 --- a/src/types/API.ts +++ b/src/types/API.ts @@ -33,3 +33,5 @@ export type CrossPlatformReadableStream = ReadableStream | import('s export type UnifiedFormData = FormData | import('form-data'); export type FormDataRequest = { formData: UnifiedFormData; headers: Headers }; + +export type NodeHTTPBody = string | import('form-data'); From 90eb4492ee18847b2273ad3e8802e68af0c427fb Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 12:45:11 +0200 Subject: [PATCH 24/53] feat: disable examples for non node env --- examples/studio/conversational-rag/rag-engine.ts | 13 +++++++++---- src/index.ts | 9 ++++++++- tests/unittests/resources/rag-engine.test.ts | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 1f10721..b407c92 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,5 +1,4 @@ -import { AI21 } from 'ai21'; -import { FileResponse, UploadFileResponse } from '../../../src/types'; +import { AI21, FileResponse, UploadFileResponse, isNode } from 'ai21'; import path from 'path'; function sleep(ms) { @@ -73,6 +72,12 @@ const fileContent = Buffer.from( 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', ); const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); -uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); -listFiles().catch(console.error); +if (isNode){ + uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); + listFiles().catch(console.error); +} +else{ + // TODO - add node support for files + console.log('Cannot run uploads in not Node environment'); +} diff --git a/src/index.ts b/src/index.ts index db356a3..015eb97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,9 +38,16 @@ export { type ConversationalRagResponse, type ConversationalRagSource, type RetrievalStrategy, + type UploadFileRequest, + type UploadFileResponse, + type FileResponse, + type ListFilesFilters, + type UpdateFileRequest, + type FilePathOrFileObject, } from './types'; export { APIClient } from './APIClient'; export { AI21Error, MissingAPIKeyError } from './errors'; export { Stream } from './streaming'; export { APIResource } from './APIResource'; -export { Chat, Completions, ConversationalRag } from './resources'; +export { Chat, Completions, ConversationalRag, Files } from './resources'; +export { isBrowser, isNode } from './runtime'; diff --git a/tests/unittests/resources/rag-engine.test.ts b/tests/unittests/resources/rag-engine.test.ts index f1d4f36..88b2dc1 100644 --- a/tests/unittests/resources/rag-engine.test.ts +++ b/tests/unittests/resources/rag-engine.test.ts @@ -104,7 +104,7 @@ describe('RAGEngine', () => { '/library/files', { query: filters, - headers: { 'Authorization': `Bearer ${dummyAPIKey}` } + ...options, } ); expect(response).toEqual(expectedResponse); From 8914f611cab1b4cba1b657c19e2a9249652a197d Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 12:46:30 +0200 Subject: [PATCH 25/53] feat: disable examples for non node env --- examples/studio/conversational-rag/rag-engine.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index b407c92..1a6a67a 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -73,11 +73,10 @@ const fileContent = Buffer.from( ); const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); -if (isNode){ +if (isNode) { uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); listFiles().catch(console.error); -} -else{ +} else { // TODO - add node support for files console.log('Cannot run uploads in not Node environment'); } From 585e2781571a231a76c4b90b8f5a39aaa5e45a06 Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 13:14:35 +0200 Subject: [PATCH 26/53] feat: log --- examples/studio/conversational-rag/rag-engine.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 1a6a67a..c7d70da 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -74,6 +74,7 @@ const fileContent = Buffer.from( const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); if (isNode) { + console.log('Running file upload in Node environment'); uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); listFiles().catch(console.error); } else { From 18b46a086547ee43ee488ef9321491f30c2a4af2 Mon Sep 17 00:00:00 2001 From: amirk Date: Wed, 4 Dec 2024 13:42:45 +0200 Subject: [PATCH 27/53] feat: swap condition --- examples/studio/conversational-rag/rag-engine.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index c7d70da..04fae5f 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,4 +1,4 @@ -import { AI21, FileResponse, UploadFileResponse, isNode } from 'ai21'; +import { AI21, FileResponse, UploadFileResponse, isBrowser } from 'ai21'; import path from 'path'; function sleep(ms) { @@ -73,11 +73,10 @@ const fileContent = Buffer.from( ); const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); -if (isNode) { +if (isBrowser) { + console.log('Cannot run upload examples in Browser environment'); +} else { console.log('Running file upload in Node environment'); uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); listFiles().catch(console.error); -} else { - // TODO - add node support for files - console.log('Cannot run uploads in not Node environment'); } From 79e616b355bf5bfdc431d058b1290b1892b8d29d Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 14:24:33 +0200 Subject: [PATCH 28/53] test: Trying integration test --- .github/workflows/integration-tests.yml | 2 +- .../studio/conversational-rag/rag-engine.ts | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index c7bee9d..fbbfd51 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | npm install - npm install ai21 + npm run build - name: Run Integration Tests env: diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 04fae5f..510d32b 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,4 +1,4 @@ -import { AI21, FileResponse, UploadFileResponse, isBrowser } from 'ai21'; +import { AI21, FileResponse, UploadFileResponse } from 'ai21'; import path from 'path'; function sleep(ms) { @@ -31,7 +31,7 @@ async function uploadGetUpdateDelete(fileInput, path) { file: fileInput, path: path, }); - console.log(uploadFileResponse); + console.log(`Uploaded file with id ${uploadFileResponse}`); let file: FileResponse = await waitForFileProcessing(client, uploadFileResponse.fileId); console.log(file); @@ -62,20 +62,21 @@ async function listFiles() { console.log(files); } -/* Simulate file upload passing a path to file */ -const filePath = path.join(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); // Use process.cwd() to get the current working directory - -uploadGetUpdateDelete(filePath, Date.now().toString()).catch(console.error); - -/* Simulate file upload passing File instance */ -const fileContent = Buffer.from( - 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', -); -const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); +const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; if (isBrowser) { console.log('Cannot run upload examples in Browser environment'); } else { + /* Simulate file upload passing a path to file */ + const filePath = path.join(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); // Use process.cwd() to get the current working directory + + uploadGetUpdateDelete(filePath, Date.now().toString()).catch(console.error); + + /* Simulate file upload passing File instance */ + const fileContent = Buffer.from( + 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', + ); + const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); console.log('Running file upload in Node environment'); uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); listFiles().catch(console.error); From 5888d678c575738195489acb95dfc37d6a6f5d4e Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 14:28:44 +0200 Subject: [PATCH 29/53] fix: Added log --- examples/studio/conversational-rag/rag-engine.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 510d32b..37da3b3 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -27,6 +27,7 @@ async function waitForFileProcessing( async function uploadGetUpdateDelete(fileInput, path) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { + console.log(`Uploading file with id ${fileInput}`); const uploadFileResponse: UploadFileResponse = await client.files.create({ file: fileInput, path: path, From e251c1b6e2e1d2ba7aaf71851491d948ae540eec Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 14:33:38 +0200 Subject: [PATCH 30/53] fix: Run sync --- .../studio/conversational-rag/rag-engine.ts | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 37da3b3..3721e94 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -60,7 +60,7 @@ async function uploadGetUpdateDelete(fileInput, path) { async function listFiles() { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); const files = await client.files.list({ limit: 4 }); - console.log(files); + console.log(`Listed files: ${files}`); } const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; @@ -68,17 +68,25 @@ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'u if (isBrowser) { console.log('Cannot run upload examples in Browser environment'); } else { - /* Simulate file upload passing a path to file */ - const filePath = path.join(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); // Use process.cwd() to get the current working directory + /* Run all operations sequentially */ + (async () => { + try { + // First operation - upload file from path + const filePath = path.join(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); + await uploadGetUpdateDelete(filePath, Date.now().toString()); - uploadGetUpdateDelete(filePath, Date.now().toString()).catch(console.error); + // Second operation - upload file from File instance + const fileContent = Buffer.from( + 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', + ); + const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); + console.log('Running file upload in Node environment'); + await uploadGetUpdateDelete(dummyFile, Date.now().toString()); - /* Simulate file upload passing File instance */ - const fileContent = Buffer.from( - 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', - ); - const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); - console.log('Running file upload in Node environment'); - uploadGetUpdateDelete(dummyFile, Date.now().toString()).catch(console.error); - listFiles().catch(console.error); + // Finally, list the files + await listFiles(); + } catch (error) { + console.error(error); + } + })(); } From 9cccbc9b20e072a8dbd0c6d942f7f4bc650c6958 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 14:50:41 +0200 Subject: [PATCH 31/53] fix: Added logs --- .../studio/conversational-rag/rag-engine.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 3721e94..a899b2a 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -27,33 +27,41 @@ async function waitForFileProcessing( async function uploadGetUpdateDelete(fileInput, path) { const client = new AI21({ apiKey: process.env.AI21_API_KEY }); try { - console.log(`Uploading file with id ${fileInput}`); + console.log(`Starting upload for file:`, typeof fileInput); const uploadFileResponse: UploadFileResponse = await client.files.create({ file: fileInput, path: path, }); - console.log(`Uploaded file with id ${uploadFileResponse}`); + console.log(`✓ Upload completed. File ID: ${uploadFileResponse.fileId}`); + console.log('Waiting for file processing...'); let file: FileResponse = await waitForFileProcessing(client, uploadFileResponse.fileId); - console.log(file); + console.log(`✓ File processing completed with status: ${file.status}`); if (file.status === 'PROCESSED') { - console.log('Now updating the file labels and publicUrl...'); + console.log('Starting file update...'); await client.files.update({ fileId: uploadFileResponse.fileId, labels: ['test99'], publicUrl: 'https://www.miri.com', }); file = await client.files.get(uploadFileResponse.fileId); - console.log(file); + console.log('✓ File update completed'); } else { - console.log(`File did not processed well, ended with status ${file.status}`); + console.log(`⚠ File processing failed with status ${file.status}`); + return; // Exit early if processing failed } - console.log('Now deleting the file'); + console.log('Starting file deletion...'); await client.files.delete(uploadFileResponse.fileId); + console.log('✓ File deletion completed'); + + // Add buffer time between operations + await sleep(2000); + } catch (error) { - console.error('Error:', error); + console.error('❌ Error in uploadGetUpdateDelete:', error); + throw error; } } @@ -71,22 +79,29 @@ if (isBrowser) { /* Run all operations sequentially */ (async () => { try { + console.log('=== Starting first operation ==='); // First operation - upload file from path const filePath = path.join(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); await uploadGetUpdateDelete(filePath, Date.now().toString()); + console.log('=== First operation completed ===\n'); + await sleep(2000); + console.log('=== Starting second operation ==='); // Second operation - upload file from File instance const fileContent = Buffer.from( 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', ); const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); - console.log('Running file upload in Node environment'); await uploadGetUpdateDelete(dummyFile, Date.now().toString()); + console.log('=== Second operation completed ===\n'); + await sleep(2000); - // Finally, list the files + console.log('=== Starting file listing ==='); await listFiles(); + console.log('=== File listing completed ==='); } catch (error) { - console.error(error); + console.error('❌ Main execution error:', error); + process.exit(1); // Exit with error code if something fails } })(); } From 9de08448f00b77168651315bab9b4987d6632938 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 14:56:09 +0200 Subject: [PATCH 32/53] fix: Added logs of env --- examples/studio/conversational-rag/rag-engine.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index a899b2a..28b08f0 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -55,10 +55,9 @@ async function uploadGetUpdateDelete(fileInput, path) { console.log('Starting file deletion...'); await client.files.delete(uploadFileResponse.fileId); console.log('✓ File deletion completed'); - + // Add buffer time between operations await sleep(2000); - } catch (error) { console.error('❌ Error in uploadGetUpdateDelete:', error); throw error; @@ -76,6 +75,15 @@ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'u if (isBrowser) { console.log('Cannot run upload examples in Browser environment'); } else { + /* Log environment details */ + console.log('=== Environment Information ==='); + console.log(`Node.js Version: ${process.version}`); + console.log(`Platform: ${process.platform}`); + console.log(`Architecture: ${process.arch}`); + console.log(`Process ID: ${process.pid}`); + console.log(`Current Working Directory: ${process.cwd()}`); + console.log('===========================\n'); + /* Run all operations sequentially */ (async () => { try { From 4e7f794e6ccbcab9f809b9c77a5b882ac6063ec2 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:04:32 +0200 Subject: [PATCH 33/53] fix: Added logs of env --- .../studio/conversational-rag/rag-engine.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 28b08f0..254464c 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -1,5 +1,6 @@ import { AI21, FileResponse, UploadFileResponse } from 'ai21'; import path from 'path'; +import fs from 'fs'; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -72,6 +73,23 @@ async function listFiles() { const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; +const createNodeFile = (content: Buffer, filename: string, type: string) => { + if (process.platform === 'linux') { + console.log('Running on Linux (GitHub Actions)'); + // Special handling for Linux (GitHub Actions) + return { + name: filename, + type: type, + buffer: content, + [Symbol.toStringTag]: 'File' + }; + } else { + console.log('Running on other platforms'); + // Regular handling for other platforms + return new File([content], filename, { type }); + } +}; + if (isBrowser) { console.log('Cannot run upload examples in Browser environment'); } else { @@ -89,7 +107,10 @@ if (isBrowser) { try { console.log('=== Starting first operation ==='); // First operation - upload file from path - const filePath = path.join(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); + const filePath = path.resolve(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); + } await uploadGetUpdateDelete(filePath, Date.now().toString()); console.log('=== First operation completed ===\n'); await sleep(2000); @@ -99,7 +120,7 @@ if (isBrowser) { const fileContent = Buffer.from( 'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.', ); - const dummyFile = new File([fileContent], 'example.txt', { type: 'text/plain' }); + const dummyFile = createNodeFile(fileContent, 'example.txt', 'text/plain'); await uploadGetUpdateDelete(dummyFile, Date.now().toString()); console.log('=== Second operation completed ===\n'); await sleep(2000); From 8ff04decda0af57ff6cb399cc797a34a197ebe5f Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:13:10 +0200 Subject: [PATCH 34/53] fix: Added more logs --- .../studio/conversational-rag/rag-engine.ts | 5 ++- src/files/NodeFilesHandler.ts | 44 ++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 254464c..8b002ab 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -81,7 +81,7 @@ const createNodeFile = (content: Buffer, filename: string, type: string) => { name: filename, type: type, buffer: content, - [Symbol.toStringTag]: 'File' + [Symbol.toStringTag]: 'File', }; } else { console.log('Running on other platforms'); @@ -110,7 +110,10 @@ if (isBrowser) { const filePath = path.resolve(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt'); if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); + } else { + console.log(`File found: ${filePath}`); } + await uploadGetUpdateDelete(filePath, Date.now().toString()); console.log('=== First operation completed ===\n'); await sleep(2000); diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 1080003..d7eb78b 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -20,24 +20,38 @@ export class NodeFilesHandler extends BaseFilesHandler { } async prepareFormDataRequest(file: FilePathOrFileObject): Promise { - const { default: FormDataNode } = await import('form-data'); - const formData = new FormDataNode(); + console.log('Preparing form data request for Node.js'); + try { + const FormData = await import('form-data').then(m => m.default || m); + console.log('Successfully imported form-data module'); + + const formData = new FormData(); + console.log('Created new FormData instance'); - if (typeof file === 'string') { - const fs = (await import('fs')).default; - if (!fs.existsSync(file)) { - throw new Error(`File not found: ${file}`); + if (typeof file === 'string') { + const fs = await import('fs').then(m => m.default || m); + if (!fs.existsSync(file)) { + throw new Error(`File not found: ${file}`); + } + console.log(`Appending file from path: ${file}`); + formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); + } else if (file instanceof File) { + console.log('Converting ReadableStream to Node stream'); + const nodeStream = await this.convertReadableStream(file.stream()); + console.log('Appending file from File instance'); + formData.append('file', nodeStream, file.name); + } else { + throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); } - formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); - } else if (file instanceof File) { - const nodeStream = await this.convertReadableStream(file.stream()); - formData.append('file', nodeStream, file.name); - } else { - throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); - } - const formDataHeaders = { 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` }; + const formDataHeaders = { 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` }; + console.log('FormData preparation completed successfully'); - return { formData, headers: formDataHeaders }; + return { formData, headers: formDataHeaders }; + } catch (error) { + console.error('Error in prepareFormDataRequest:', error); + console.error('Error details:', error instanceof Error ? error.message : String(error)); + throw error; + } } } From e2129a6c8c9994f3cb1ff1903fb4bde0b54fdf54 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:17:16 +0200 Subject: [PATCH 35/53] fix: Checked env --- examples/studio/conversational-rag/rag-engine.ts | 2 +- src/files/NodeFilesHandler.ts | 6 +++--- src/runtime.ts | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/studio/conversational-rag/rag-engine.ts b/examples/studio/conversational-rag/rag-engine.ts index 8b002ab..5dcc0c0 100644 --- a/examples/studio/conversational-rag/rag-engine.ts +++ b/examples/studio/conversational-rag/rag-engine.ts @@ -113,7 +113,7 @@ if (isBrowser) { } else { console.log(`File found: ${filePath}`); } - + await uploadGetUpdateDelete(filePath, Date.now().toString()); console.log('=== First operation completed ===\n'); await sleep(2000); diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index d7eb78b..de95d22 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -22,14 +22,14 @@ export class NodeFilesHandler extends BaseFilesHandler { async prepareFormDataRequest(file: FilePathOrFileObject): Promise { console.log('Preparing form data request for Node.js'); try { - const FormData = await import('form-data').then(m => m.default || m); + const FormData = await import('form-data').then((m) => m.default || m); console.log('Successfully imported form-data module'); - + const formData = new FormData(); console.log('Created new FormData instance'); if (typeof file === 'string') { - const fs = await import('fs').then(m => m.default || m); + const fs = await import('fs').then((m) => m.default || m); if (!fs.existsSync(file)) { throw new Error(`File not found: ${file}`); } diff --git a/src/runtime.ts b/src/runtime.ts index a11f298..cbc58a5 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -24,15 +24,19 @@ export const isNode = export function createFetchInstance(): Fetch { if (isBrowser || isWebWorker) { + console.log('Creating BrowserFetch instance'); return new BrowserFetch(); } + console.log('Creating NodeFetch instance'); return new NodeFetch(); } export function createFilesHandlerInstance(): BaseFilesHandler { if (isBrowser || isWebWorker) { + console.log('Creating BrowserFilesHandler instance'); return new BrowserFilesHandler(); } + console.log('Creating NodeFilesHandler instance'); return new NodeFilesHandler(); } From ac11d5c940c490460d745a0fcd5662ddca9f1547 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:24:27 +0200 Subject: [PATCH 36/53] ci: Added form-data to bundle --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index b09b01b..416c3b1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ formats: ['es', 'cjs', 'umd'], }, rollupOptions: { - external: ['node-fetch'], + external: ['node-fetch', 'form-data'], output: { globals: { 'node-fetch': 'fetch', From 49d82c61609010e2fc70edd7f60a56beb85f0d58 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:26:58 +0200 Subject: [PATCH 37/53] ci: Added form-data to bundle --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 416c3b1..3ded12f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ formats: ['es', 'cjs', 'umd'], }, rollupOptions: { - external: ['node-fetch', 'form-data'], + external: ['node-fetch', 'form-data', 'fs', 'stream'], output: { globals: { 'node-fetch': 'fetch', From 1b5b3145b1e3e5493667c294b1eb0c0dc2b1633b Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:34:44 +0200 Subject: [PATCH 38/53] fix: Node file checks --- src/files/NodeFilesHandler.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index de95d22..9299d6a 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -22,24 +22,33 @@ export class NodeFilesHandler extends BaseFilesHandler { async prepareFormDataRequest(file: FilePathOrFileObject): Promise { console.log('Preparing form data request for Node.js'); try { - const FormData = await import('form-data').then((m) => m.default || m); + const FormData = await import('form-data').then(m => m.default || m); console.log('Successfully imported form-data module'); - + const formData = new FormData(); console.log('Created new FormData instance'); if (typeof file === 'string') { - const fs = await import('fs').then((m) => m.default || m); + const fs = await import('fs').then(m => m.default || m); if (!fs.existsSync(file)) { throw new Error(`File not found: ${file}`); } console.log(`Appending file from path: ${file}`); formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); - } else if (file instanceof File) { - console.log('Converting ReadableStream to Node stream'); - const nodeStream = await this.convertReadableStream(file.stream()); - console.log('Appending file from File instance'); - formData.append('file', nodeStream, file.name); + } else if (file && typeof file === 'object') { + console.log('Processing file object:', file); + if ('buffer' in file) { + // Handle Node.js file-like object + console.log('Appending file from buffer'); + formData.append('file', file.buffer, { filename: file.name, contentType: file.type }); + } else if ('stream' in file && typeof file.stream === 'function') { + // Handle File object + console.log('Converting and appending file from stream'); + const nodeStream = await this.convertReadableStream(file.stream()); + formData.append('file', nodeStream, { filename: file.name, contentType: file.type }); + } else { + throw new Error(`Invalid file object structure: ${JSON.stringify(file)}`); + } } else { throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); } From e3b31ac0ac3500df7d2dddab1483ef0cee24a57e Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:35:08 +0200 Subject: [PATCH 39/53] fix: Node file checks --- src/files/NodeFilesHandler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 9299d6a..e54b67f 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -22,14 +22,14 @@ export class NodeFilesHandler extends BaseFilesHandler { async prepareFormDataRequest(file: FilePathOrFileObject): Promise { console.log('Preparing form data request for Node.js'); try { - const FormData = await import('form-data').then(m => m.default || m); + const FormData = await import('form-data').then((m) => m.default || m); console.log('Successfully imported form-data module'); - + const formData = new FormData(); console.log('Created new FormData instance'); if (typeof file === 'string') { - const fs = await import('fs').then(m => m.default || m); + const fs = await import('fs').then((m) => m.default || m); if (!fs.existsSync(file)) { throw new Error(`File not found: ${file}`); } From 670fc695f27bc0eb87fa4fab852c3d30fd9fb970 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 15:40:08 +0200 Subject: [PATCH 40/53] fix: check type --- src/files/NodeFilesHandler.ts | 53 +++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index e54b67f..1c1eb71 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -4,45 +4,56 @@ import { FormDataRequest } from 'types/API'; export class NodeFilesHandler extends BaseFilesHandler { private async convertReadableStream(readableStream: ReadableStream): Promise { - const { Readable } = await import('stream'); - const reader = readableStream.getReader(); + try { + if (typeof window === 'undefined') { + const { Readable } = await import('stream'); + const reader = readableStream.getReader(); - return new Readable({ - async read() { - const { done, value } = await reader.read(); - if (done) { - this.push(null); - } else { - this.push(value); - } - }, - }); + return new Readable({ + async read() { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + } else { + this.push(value); + } + }, + }); + } else { + throw new Error('Stream conversion is not supported in browser environment'); + } + } catch (error) { + console.error('Error in convertReadableStream:', error); + throw error; + } } async prepareFormDataRequest(file: FilePathOrFileObject): Promise { console.log('Preparing form data request for Node.js'); try { - const FormData = await import('form-data').then((m) => m.default || m); + const FormData = await import('form-data').then(m => m.default || m); console.log('Successfully imported form-data module'); - + const formData = new FormData(); console.log('Created new FormData instance'); if (typeof file === 'string') { - const fs = await import('fs').then((m) => m.default || m); - if (!fs.existsSync(file)) { - throw new Error(`File not found: ${file}`); + if (typeof window === 'undefined') { + const fs = await import('fs').then(m => m.default || m); + if (!fs.existsSync(file)) { + throw new Error(`File not found: ${file}`); + } + console.log(`Appending file from path: ${file}`); + formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); + } else { + throw new Error('File system operations are not supported in browser environment'); } - console.log(`Appending file from path: ${file}`); - formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); } else if (file && typeof file === 'object') { console.log('Processing file object:', file); if ('buffer' in file) { - // Handle Node.js file-like object console.log('Appending file from buffer'); formData.append('file', file.buffer, { filename: file.name, contentType: file.type }); } else if ('stream' in file && typeof file.stream === 'function') { - // Handle File object console.log('Converting and appending file from stream'); const nodeStream = await this.convertReadableStream(file.stream()); formData.append('file', nodeStream, { filename: file.name, contentType: file.type }); From 09be7a4971681acac830d08205c9ae3b939b68ab Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 16:19:14 +0200 Subject: [PATCH 41/53] fix: Moved to factory --- src/APIClient.ts | 2 +- src/factory.ts | 21 +++++++ src/files/NodeFilesHandler.ts | 113 ++++++++++++++++++---------------- src/runtime.ts | 24 -------- 4 files changed, 83 insertions(+), 77 deletions(-) create mode 100644 src/factory.ts diff --git a/src/APIClient.ts b/src/APIClient.ts index 1bfe18d..b57fa34 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -12,7 +12,7 @@ import { FilePathOrFileObject, } from './types'; import { AI21EnvConfig } from './EnvConfig'; -import { createFetchInstance, createFilesHandlerInstance } from './runtime'; +import { createFetchInstance, createFilesHandlerInstance } from './factory'; import { Fetch } from 'fetch'; import { BaseFilesHandler } from 'files/BaseFilesHandler'; import { FormDataRequest } from 'types/API'; diff --git a/src/factory.ts b/src/factory.ts new file mode 100644 index 0000000..e984ca2 --- /dev/null +++ b/src/factory.ts @@ -0,0 +1,21 @@ +import { BrowserFilesHandler } from './files/BrowserFilesHandler'; +import { BrowserFetch, Fetch, NodeFetch } from './fetch'; +import { NodeFilesHandler } from './files/NodeFilesHandler'; +import { BaseFilesHandler } from './files/BaseFilesHandler'; +import { isBrowser, isWebWorker } from 'runtime'; + +export function createFetchInstance(): Fetch { + if (isBrowser || isWebWorker) { + return new BrowserFetch(); + } + + return new NodeFetch(); +} + +export function createFilesHandlerInstance(): BaseFilesHandler { + if (isBrowser || isWebWorker) { + return new BrowserFilesHandler(); + } + + return new NodeFilesHandler(); +} diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 1c1eb71..ef6a3fb 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -1,77 +1,86 @@ import { FilePathOrFileObject } from 'types'; import { BaseFilesHandler } from './BaseFilesHandler'; import { FormDataRequest } from 'types/API'; +import { isNode } from 'runtime'; export class NodeFilesHandler extends BaseFilesHandler { private async convertReadableStream(readableStream: ReadableStream): Promise { - try { - if (typeof window === 'undefined') { - const { Readable } = await import('stream'); - const reader = readableStream.getReader(); + if (!isNode) { + throw new Error('Stream conversion is not supported in browser environment'); + } - return new Readable({ - async read() { - const { done, value } = await reader.read(); - if (done) { - this.push(null); - } else { - this.push(value); - } - }, - }); - } else { - throw new Error('Stream conversion is not supported in browser environment'); - } - } catch (error) { - console.error('Error in convertReadableStream:', error); - throw error; + const { Readable } = await import('stream'); + const reader = readableStream.getReader(); + + return new Readable({ + async read() { + const { done, value } = await reader.read(); + done ? this.push(null) : this.push(value); + }, + }); + } + + private async handleStringFile(filePath: string, formData: any): Promise { + // eslint-disable-line @typescript-eslint/no-explicit-any + if (!isNode) { + throw new Error('File system operations are not supported in browser environment'); + } + + const fs = await import('fs').then((m) => m.default || m); + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); } + + formData.append('file', fs.createReadStream(filePath), { + filename: filePath.split('/').pop(), + }); } async prepareFormDataRequest(file: FilePathOrFileObject): Promise { - console.log('Preparing form data request for Node.js'); try { - const FormData = await import('form-data').then(m => m.default || m); - console.log('Successfully imported form-data module'); - + const FormData = await import('form-data').then((m) => m.default || m); const formData = new FormData(); - console.log('Created new FormData instance'); if (typeof file === 'string') { - if (typeof window === 'undefined') { - const fs = await import('fs').then(m => m.default || m); - if (!fs.existsSync(file)) { - throw new Error(`File not found: ${file}`); - } - console.log(`Appending file from path: ${file}`); - formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() }); - } else { - throw new Error('File system operations are not supported in browser environment'); - } - } else if (file && typeof file === 'object') { - console.log('Processing file object:', file); - if ('buffer' in file) { - console.log('Appending file from buffer'); - formData.append('file', file.buffer, { filename: file.name, contentType: file.type }); - } else if ('stream' in file && typeof file.stream === 'function') { - console.log('Converting and appending file from stream'); - const nodeStream = await this.convertReadableStream(file.stream()); - formData.append('file', nodeStream, { filename: file.name, contentType: file.type }); - } else { - throw new Error(`Invalid file object structure: ${JSON.stringify(file)}`); - } - } else { + await this.handleStringFile(file, formData); + return this.createFormDataResponse(formData); + } + + if (!file || typeof file !== 'object') { throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); } - const formDataHeaders = { 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` }; - console.log('FormData preparation completed successfully'); + if ('buffer' in file) { + formData.append('file', file.buffer, { + filename: file.name, + contentType: file.type, + }); + return this.createFormDataResponse(formData); + } + + if ('stream' in file && typeof file.stream === 'function') { + const nodeStream = await this.convertReadableStream(file.stream()); + formData.append('file', nodeStream, { + filename: file.name, + contentType: file.type, + }); + return this.createFormDataResponse(formData); + } - return { formData, headers: formDataHeaders }; + throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); } catch (error) { console.error('Error in prepareFormDataRequest:', error); - console.error('Error details:', error instanceof Error ? error.message : String(error)); throw error; } } + + private createFormDataResponse(formData: any): FormDataRequest { + // eslint-disable-line @typescript-eslint/no-explicit-any + return { + formData, + headers: { + 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`, + }, + }; + } } diff --git a/src/runtime.ts b/src/runtime.ts index cbc58a5..a398a34 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,8 +1,3 @@ -import { BrowserFilesHandler } from './files/BrowserFilesHandler'; -import { BrowserFetch, Fetch, NodeFetch } from './fetch'; -import { NodeFilesHandler } from './files/NodeFilesHandler'; -import { BaseFilesHandler } from './files/BaseFilesHandler'; - export const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; /** @@ -21,22 +16,3 @@ export const isWebWorker = export const isNode = typeof process !== 'undefined' && Boolean(process.version) && Boolean(process.versions?.node); - -export function createFetchInstance(): Fetch { - if (isBrowser || isWebWorker) { - console.log('Creating BrowserFetch instance'); - return new BrowserFetch(); - } - - console.log('Creating NodeFetch instance'); - return new NodeFetch(); -} - -export function createFilesHandlerInstance(): BaseFilesHandler { - if (isBrowser || isWebWorker) { - console.log('Creating BrowserFilesHandler instance'); - return new BrowserFilesHandler(); - } - console.log('Creating NodeFilesHandler instance'); - return new NodeFilesHandler(); -} From d24dc65278c14da53c7a2059196b6447c0874def Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 16:19:27 +0200 Subject: [PATCH 42/53] fix: ignore ts --- src/files/NodeFilesHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index ef6a3fb..cf21c43 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -20,8 +20,8 @@ export class NodeFilesHandler extends BaseFilesHandler { }); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private async handleStringFile(filePath: string, formData: any): Promise { - // eslint-disable-line @typescript-eslint/no-explicit-any if (!isNode) { throw new Error('File system operations are not supported in browser environment'); } @@ -74,8 +74,8 @@ export class NodeFilesHandler extends BaseFilesHandler { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private createFormDataResponse(formData: any): FormDataRequest { - // eslint-disable-line @typescript-eslint/no-explicit-any return { formData, headers: { From af96613171423b17c28e27a8263fe98238b43345 Mon Sep 17 00:00:00 2001 From: asafg Date: Wed, 4 Dec 2024 16:24:37 +0200 Subject: [PATCH 43/53] fix: Import of runtime --- src/factory.ts | 2 +- src/files/NodeFilesHandler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index e984ca2..b7ce18e 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -2,7 +2,7 @@ import { BrowserFilesHandler } from './files/BrowserFilesHandler'; import { BrowserFetch, Fetch, NodeFetch } from './fetch'; import { NodeFilesHandler } from './files/NodeFilesHandler'; import { BaseFilesHandler } from './files/BaseFilesHandler'; -import { isBrowser, isWebWorker } from 'runtime'; +import { isBrowser, isWebWorker } from './runtime'; export function createFetchInstance(): Fetch { if (isBrowser || isWebWorker) { diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index cf21c43..551f30a 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -1,7 +1,7 @@ import { FilePathOrFileObject } from 'types'; import { BaseFilesHandler } from './BaseFilesHandler'; import { FormDataRequest } from 'types/API'; -import { isNode } from 'runtime'; +import { isNode } from '../runtime'; export class NodeFilesHandler extends BaseFilesHandler { private async convertReadableStream(readableStream: ReadableStream): Promise { From bf6ad2a11292dd7932c537d16eb490abac02d353 Mon Sep 17 00:00:00 2001 From: asafg Date: Thu, 5 Dec 2024 11:48:06 +0200 Subject: [PATCH 44/53] refactor: Renamed methods --- src/files/NodeFilesHandler.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/files/NodeFilesHandler.ts b/src/files/NodeFilesHandler.ts index 551f30a..d2e1739 100644 --- a/src/files/NodeFilesHandler.ts +++ b/src/files/NodeFilesHandler.ts @@ -21,7 +21,7 @@ export class NodeFilesHandler extends BaseFilesHandler { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async handleStringFile(filePath: string, formData: any): Promise { + private async createStreamFromFilePath(filePath: string, formData: any): Promise { if (!isNode) { throw new Error('File system operations are not supported in browser environment'); } @@ -42,8 +42,8 @@ export class NodeFilesHandler extends BaseFilesHandler { const formData = new FormData(); if (typeof file === 'string') { - await this.handleStringFile(file, formData); - return this.createFormDataResponse(formData); + await this.createStreamFromFilePath(file, formData); + return this.createFormDataRequest(formData); } if (!file || typeof file !== 'object') { @@ -55,7 +55,7 @@ export class NodeFilesHandler extends BaseFilesHandler { filename: file.name, contentType: file.type, }); - return this.createFormDataResponse(formData); + return this.createFormDataRequest(formData); } if ('stream' in file && typeof file.stream === 'function') { @@ -64,7 +64,7 @@ export class NodeFilesHandler extends BaseFilesHandler { filename: file.name, contentType: file.type, }); - return this.createFormDataResponse(formData); + return this.createFormDataRequest(formData); } throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`); @@ -75,7 +75,7 @@ export class NodeFilesHandler extends BaseFilesHandler { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private createFormDataResponse(formData: any): FormDataRequest { + private createFormDataRequest(formData: any): FormDataRequest { return { formData, headers: { From e70ecbcd88f502d3337a3e83c54cc38f6a96290b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 5 Dec 2024 09:59:20 +0000 Subject: [PATCH 45/53] chore(release): 1.1.0-rc.1 [skip ci] # [1.1.0-rc.1](https://github.com/AI21Labs/ai21-typescript/compare/v1.0.3...v1.1.0-rc.1) (2024-12-05) ### Bug Fixes * Added log ([5888d67](https://github.com/AI21Labs/ai21-typescript/commit/5888d678c575738195489acb95dfc37d6a6f5d4e)) * Added logs ([9cccbc9](https://github.com/AI21Labs/ai21-typescript/commit/9cccbc9b20e072a8dbd0c6d942f7f4bc650c6958)) * Added logs of env ([4e7f794](https://github.com/AI21Labs/ai21-typescript/commit/4e7f794e6ccbcab9f809b9c77a5b882ac6063ec2)) * Added logs of env ([9de0844](https://github.com/AI21Labs/ai21-typescript/commit/9de08448f00b77168651315bab9b4987d6632938)) * Added more logs ([8ff04de](https://github.com/AI21Labs/ai21-typescript/commit/8ff04decda0af57ff6cb399cc797a34a197ebe5f)) * check type ([670fc69](https://github.com/AI21Labs/ai21-typescript/commit/670fc695f27bc0eb87fa4fab852c3d30fd9fb970)) * Checked env ([e2129a6](https://github.com/AI21Labs/ai21-typescript/commit/e2129a6c8c9994f3cb1ff1903fb4bde0b54fdf54)) * ignore ts ([d24dc65](https://github.com/AI21Labs/ai21-typescript/commit/d24dc65278c14da53c7a2059196b6447c0874def)) * Import of runtime ([af96613](https://github.com/AI21Labs/ai21-typescript/commit/af96613171423b17c28e27a8263fe98238b43345)) * Moved to factory ([09be7a4](https://github.com/AI21Labs/ai21-typescript/commit/09be7a4971681acac830d08205c9ae3b939b68ab)) * Node file checks ([e3b31ac](https://github.com/AI21Labs/ai21-typescript/commit/e3b31ac0ac3500df7d2dddab1483ef0cee24a57e)) * Node file checks ([1b5b314](https://github.com/AI21Labs/ai21-typescript/commit/1b5b3145b1e3e5493667c294b1eb0c0dc2b1633b)) * Run sync ([e251c1b](https://github.com/AI21Labs/ai21-typescript/commit/e251c1b6e2e1d2ba7aaf71851491d948ae540eec)) ### Features * add file path check before opening + rag examples improved ([7ca7640](https://github.com/AI21Labs/ai21-typescript/commit/7ca7640e919b24b456ee292cccc848962250e7a0)) * add upload file object override ([6131ee8](https://github.com/AI21Labs/ai21-typescript/commit/6131ee85594b2daf13f49243a696bd43765856ff)) * add upload file object override ([329eb3e](https://github.com/AI21Labs/ai21-typescript/commit/329eb3ead78eb5eafb5301d4cc262597d9cca01f)) * basic unit tests for rag engine ([6ff4df7](https://github.com/AI21Labs/ai21-typescript/commit/6ff4df799949fb143b617b962539a006318a9900)) * convert upload to non async ([e3b0810](https://github.com/AI21Labs/ai21-typescript/commit/e3b08107e73b341013bd5c8b128fbfa003c66009)) * disable examples for non node env ([8914f61](https://github.com/AI21Labs/ai21-typescript/commit/8914f611cab1b4cba1b657c19e2a9249652a197d)) * disable examples for non node env ([90eb449](https://github.com/AI21Labs/ai21-typescript/commit/90eb4492ee18847b2273ad3e8802e68af0c427fb)) * FilePathOrFileObject ([54a96b1](https://github.com/AI21Labs/ai21-typescript/commit/54a96b115b8036fb8729a5b3131c2d615876068d)) * fix tests ([9705d0b](https://github.com/AI21Labs/ai21-typescript/commit/9705d0b72b540fdf655981453104d049f16f93b0)) * functioning browser upload ([02b40df](https://github.com/AI21Labs/ai21-typescript/commit/02b40dfd0902b3c8f125f1e77a81b761d1cdd52c)) * log ([585e278](https://github.com/AI21Labs/ai21-typescript/commit/585e2781571a231a76c4b90b8f5a39aaa5e45a06)) * makeFormDataRequest - change type and disable lint ([657fa5e](https://github.com/AI21Labs/ai21-typescript/commit/657fa5eb7846b7f99119f63b01653b2b7b2cafd5)) * node fetch casting ([29997e0](https://github.com/AI21Labs/ai21-typescript/commit/29997e0fcca72480a8e6d52784537ab569a61040)) * rag-engine impl. - with examples, no tests yet ([ceaa9e9](https://github.com/AI21Labs/ai21-typescript/commit/ceaa9e9453d71060e7605a314c05615e0b8897f8)) * rag-engine impl. - with examples, no tests yet ([4713d02](https://github.com/AI21Labs/ai21-typescript/commit/4713d02295287c55542c69837440b977cdb5d706)) * remove log ([d68e935](https://github.com/AI21Labs/ai21-typescript/commit/d68e935bc45c34e02c820e68ea68b90dc3477b59)) * reorg imports ([c88bf02](https://github.com/AI21Labs/ai21-typescript/commit/c88bf0200f9c3482dc3f76c74f7290d74c587a8e)) * reorganized ([a93d32c](https://github.com/AI21Labs/ai21-typescript/commit/a93d32c761c914977e2b5750227677c261460ad4)) * reorganized ([b71e598](https://github.com/AI21Labs/ai21-typescript/commit/b71e5981d07f73a5b6680043717d796319553f17)) * support node path and file object\ and browser file object ([c055ca2](https://github.com/AI21Labs/ai21-typescript/commit/c055ca225b7dc50e58e992c471ae567b297708c9)) * support node path and file object\ and browser file object ([48f2a8d](https://github.com/AI21Labs/ai21-typescript/commit/48f2a8d829d83ceac2193953ac3856f194daca5f)) * swap condition ([18b46a0](https://github.com/AI21Labs/ai21-typescript/commit/18b46a086547ee43ee488ef9321491f30c2a4af2)) * wip ([382e69f](https://github.com/AI21Labs/ai21-typescript/commit/382e69fef3829396844ec2ab21585d54ff069364)) * wip ([f438185](https://github.com/AI21Labs/ai21-typescript/commit/f438185c18a6aa31c7eb1e3a630ebb8b356974fe)) * wip ([ab469be](https://github.com/AI21Labs/ai21-typescript/commit/ab469bec78059cc9813dfd6f1d693ec8dceadee0)) * wip ([a628702](https://github.com/AI21Labs/ai21-typescript/commit/a628702266bf10637839f14c9a8ac9ea2cdb65af)) * wip ([eac5e6e](https://github.com/AI21Labs/ai21-typescript/commit/eac5e6e4d40edc2728973314df80edf76f1b31cf)) --- package.json | 2 +- src/version.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b1104f0..50edba4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ai21", - "version": "1.0.3", + "version": "1.1.0-rc.1", "description": "AI21 TypeScript SDK", "main": "./dist/bundle.cjs.js", "types": "./dist/index.d.ts", diff --git a/src/version.ts b/src/version.ts index 30cbd1a..5259e39 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.3'; +export const VERSION = '1.1.0-rc.1'; From 4baa92d24057aa9502aa4393a37ca17979a5c737 Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 5 Dec 2024 14:47:41 +0200 Subject: [PATCH 46/53] feat: files readme --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 606abc2..3d49bbd 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,35 @@ for await (const chunk of streamResponse) { The `AI21` class provides a `chat` property that gives you access to the Chat API. You can use this to generate text, complete prompts, and more. +### Files + + +The `AI21` class provides a `files` property that gives you access to the Files API. You can use this to upload files to the AI21 Studio. These files will be used as context for the conversational-rag engine. + + +```typescript +import { AI21 } from 'ai21'; + +const client = new AI21({ + apiKey: process.env.AI21_API_KEY, // or pass it in directly +}); + +const fileUploadResponse = await client.files.upload({ + file: 'path/to/file', + labels: ['science', 'biology'], + path: 'path/to/file', +}); + + +const file = await client.files.get(fileUploadResponse.fileId); + + +const convRagResponse = await client.conversationalRag.create({ + messages: [{ role: 'user', content: 'This question presumes that the answer can be found within the uploaded files.' }], + }); +``` + + ## Configuration The `AI21` class accepts several configuration options, which you can pass in when creating a new instance: From 04b35e57243a64dec54de73f88b52e12f314dd9f Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 5 Dec 2024 15:41:21 +0200 Subject: [PATCH 47/53] feat: files readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3d49bbd..3b3760e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ yarn add ai21 To use the AI21 API Client, you'll need to have an API key. You can obtain an API key by signing up for an account on the AI21 website. +The `AI21` class provides a `chat` property that gives you access to the Chat API. You can use this to generate text, complete prompts, and more. + Here's an example of how to use the `AI21` class to interact with the API: ```typescript @@ -55,12 +57,10 @@ for await (const chunk of streamResponse) { } ``` -The `AI21` class provides a `chat` property that gives you access to the Chat API. You can use this to generate text, complete prompts, and more. - ### Files -The `AI21` class provides a `files` property that gives you access to the Files API. You can use this to upload files to the AI21 Studio. These files will be used as context for the conversational-rag engine. +The `AI21` class provides a `files` property that gives you access to the Files API. You can use this to upload files to the AI21 Studio, which can then be utilized as context for the conversational RAG engine ```typescript From 3059f8d8481a9ff24dde99cd131d78b50b041948 Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 5 Dec 2024 15:46:26 +0200 Subject: [PATCH 48/53] feat: files readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b3760e..b8f1d63 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,10 @@ const client = new AI21({ apiKey: process.env.AI21_API_KEY, // or pass it in directly }); -const fileUploadResponse = await client.files.upload({ - file: 'path/to/file', +const fileUploadResponse = await client.files.create({ + file: './articles/article1.pdf', labels: ['science', 'biology'], - path: 'path/to/file', + path: 'virtual-path/to/science-articles', }); From c0e1b1e59951411858a12bef52a11f638a667fca Mon Sep 17 00:00:00 2001 From: amirk Date: Thu, 5 Dec 2024 16:08:42 +0200 Subject: [PATCH 49/53] docs: readme --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++----- package.json | 3 +++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b8f1d63..a4c7b8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,24 @@ -# AI21 API Client +

+ AI21 Labs TypeScript SDK +

+ +

+Test +Integration Tests +Package version +Supported Node.js versions +Semantic Release Support +License +

+ + +- [Installation](#Installation) 💿 +- [Examples](#examples-tldr) 🗂️ +- [AI21 Official Documentation](#Documentation) +- [Chat](#Chat-Usage) +- [Conversational RAG (Beta)](#Conversational-RAG-Usage) +- [Files](#Files-Usage) + The AI21 API Client is a TypeScript library that provides a convenient interface for interacting with the AI21 API. It abstracts away the low-level details of making API requests and handling responses, allowing developers to focus on building their applications. @@ -16,7 +36,22 @@ or yarn add ai21 ``` -## Usage +## Examples (tl;dr) + +If you want to quickly get a glance how to use the AI21 Python SDK and jump straight to business, you can check out the examples. Take a look at our models and see them in action! Several examples and demonstrations have been put together to show our models' functionality and capabilities. + +### [Check out the Examples](examples/) + +Feel free to dive in, experiment, and adapt these examples to suit your needs. We believe they'll help you get up and running quickly. + +## Documentation + +--- + +The full documentation for the REST API can be found on [docs.ai21.com](https://docs.ai21.com/). + + +## Chat-Usage To use the AI21 API Client, you'll need to have an API key. You can obtain an API key by signing up for an account on the AI21 website. @@ -46,7 +81,7 @@ The client supports streaming responses for real-time processing. Here are examp #### Using Async Iterator ```typescript -const streamResponse = await ai21.chat.completions.create({ +const streamResponse = await client.chat.completions.create({ model: 'jamba-1.5-mini', messages: [{ role: 'user', content: 'Write a story about a space cat' }], stream: true, @@ -56,11 +91,11 @@ for await (const chunk of streamResponse) { console.log(chunk.choices[0]?.delta?.content || ''); } ``` - -### Files +--- +### Files-Usage -The `AI21` class provides a `files` property that gives you access to the Files API. You can use this to upload files to the AI21 Studio, which can then be utilized as context for the conversational RAG engine +The `AI21` class provides a `files` property that gives you access to the Files API. You can use it to upload, retrieve, update, list, and delete files. ```typescript @@ -79,10 +114,26 @@ const fileUploadResponse = await client.files.create({ const file = await client.files.get(fileUploadResponse.fileId); +``` + +--- +### Conversational-RAG-Usage + + +The `AI21` class provides a `conversationalRag` property that gives you access to the Conversational RAG API. You can use it to ask questions that are answered based on the files you uploaded. + + +```typescript +import { AI21 } from 'ai21'; + +const client = new AI21({ + apiKey: process.env.AI21_API_KEY, // or pass it in directly +}); const convRagResponse = await client.conversationalRag.create({ messages: [{ role: 'user', content: 'This question presumes that the answer can be found within the uploaded files.' }], }); + ``` diff --git a/package.json b/package.json index 50edba4..2d88713 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,8 @@ { "name": "ai21", + "engines": { + "node": ">=18.0.0" + }, "version": "1.1.0-rc.1", "description": "AI21 TypeScript SDK", "main": "./dist/bundle.cjs.js", From ed0ef9786ab6c5727a72e18a93480b192261efff Mon Sep 17 00:00:00 2001 From: asafg Date: Thu, 5 Dec 2024 16:14:32 +0200 Subject: [PATCH 50/53] docs: Fixed badge tests url --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a4c7b8c..f5b7d6a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@

-Test -Integration Tests +Test +Integration Tests Package version Supported Node.js versions Semantic Release Support From 3b8a0739918c7600e2580a7e45f39c49d056857a Mon Sep 17 00:00:00 2001 From: asafg Date: Thu, 5 Dec 2024 16:19:04 +0200 Subject: [PATCH 51/53] docs: Fixed text --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f5b7d6a..02a9905 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ - [Installation](#Installation) 💿 - [Examples](#examples-tldr) 🗂️ - [AI21 Official Documentation](#Documentation) -- [Chat](#Chat-Usage) -- [Conversational RAG (Beta)](#Conversational-RAG-Usage) -- [Files](#Files-Usage) +- [Chat](#Chat) +- [Conversational RAG (Beta)](#Conversational-RAG) +- [Files](#Files) The AI21 API Client is a TypeScript library that provides a convenient interface for interacting with the AI21 API. It abstracts away the low-level details of making API requests and handling responses, allowing developers to focus on building their applications. @@ -38,7 +38,7 @@ yarn add ai21 ## Examples (tl;dr) -If you want to quickly get a glance how to use the AI21 Python SDK and jump straight to business, you can check out the examples. Take a look at our models and see them in action! Several examples and demonstrations have been put together to show our models' functionality and capabilities. +If you want to quickly get a glance how to use the AI21 Typescript SDK and jump straight to business, you can check out the examples. Take a look at our models and see them in action! Several examples and demonstrations have been put together to show our models' functionality and capabilities. ### [Check out the Examples](examples/) @@ -46,12 +46,10 @@ Feel free to dive in, experiment, and adapt these examples to suit your needs. W ## Documentation ---- - The full documentation for the REST API can be found on [docs.ai21.com](https://docs.ai21.com/). -## Chat-Usage +## Chat To use the AI21 API Client, you'll need to have an API key. You can obtain an API key by signing up for an account on the AI21 website. @@ -92,7 +90,7 @@ for await (const chunk of streamResponse) { } ``` --- -### Files-Usage +### Files The `AI21` class provides a `files` property that gives you access to the Files API. You can use it to upload, retrieve, update, list, and delete files. @@ -117,7 +115,7 @@ const file = await client.files.get(fileUploadResponse.fileId); ``` --- -### Conversational-RAG-Usage +### Conversational-RAG The `AI21` class provides a `conversationalRag` property that gives you access to the Conversational RAG API. You can use it to ask questions that are answered based on the files you uploaded. @@ -145,6 +143,7 @@ The `AI21` class accepts several configuration options, which you can pass in wh - `apiKey`: Your AI21 API Key - `maxRetries`: The maximum number of retries for failed requests (default: `3`) - `timeout`: The request timeout in seconds +- `dangerouslyAllowBrowser`: Set to `true` to allow the client to be used in a browser environment. ## API Reference From f087f36cc9b6e692e550d3b5e4af31576cdac4e2 Mon Sep 17 00:00:00 2001 From: asafg Date: Thu, 5 Dec 2024 16:22:52 +0200 Subject: [PATCH 52/53] docs: Added env text --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 02a9905..5e7461d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,23 @@ The AI21 API Client is a TypeScript library that provides a convenient interface for interacting with the AI21 API. It abstracts away the low-level details of making API requests and handling responses, allowing developers to focus on building their applications. +## Environment Support + +This client supports both Node.js and browser environments: + +- **Node.js**: Works out of the box with Node.js >=18.0.0 +- **Browser**: Requires explicit opt-in by setting `dangerouslyAllowBrowser: true` in the client options + +```typescript +// Browser usage example +const client = new AI21({ + apiKey: process.env.AI21_API_KEY, // or pass it in directly + dangerouslyAllowBrowser: true // Required for browser environments +}); +``` + +> ⚠️ **Security Notice**: Using this client in the browser could expose your API key to end users. Only enable `dangerouslyAllowBrowser` if you understand the security implications and have implemented appropriate security measures. + ## Installation You can install the AI21 API Client using npm or yarn: From 43b0954f515689b3e0ea7480638f9d2afd4235d0 Mon Sep 17 00:00:00 2001 From: asafg Date: Thu, 5 Dec 2024 16:26:43 +0200 Subject: [PATCH 53/53] docs: Moved description --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e7461d..b09bec3 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ License

+The AI21 API Client is a TypeScript library that provides a convenient interface for interacting with the AI21 API. It abstracts away the low-level details of making API requests and handling responses, allowing developers to focus on building their applications. - [Installation](#Installation) 💿 - [Examples](#examples-tldr) 🗂️ @@ -20,8 +21,6 @@ - [Files](#Files) -The AI21 API Client is a TypeScript library that provides a convenient interface for interacting with the AI21 API. It abstracts away the low-level details of making API requests and handling responses, allowing developers to focus on building their applications. - ## Environment Support This client supports both Node.js and browser environments: