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/.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/README.md b/README.md
index 606abc2..b09bec3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,43 @@
-# AI21 API Client
+
+
+
+
+
+
+
+
+
+
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) 🗂️
+- [AI21 Official Documentation](#Documentation)
+- [Chat](#Chat)
+- [Conversational RAG (Beta)](#Conversational-RAG)
+- [Files](#Files)
+
+
+## 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:
@@ -16,10 +52,25 @@ or
yarn add ai21
```
-## Usage
+## Examples (tl;dr)
+
+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/)
+
+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
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
@@ -44,7 +95,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,
@@ -54,8 +105,51 @@ for await (const chunk of streamResponse) {
console.log(chunk.choices[0]?.delta?.content || '');
}
```
+---
+### 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.
+
+
+```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.create({
+ file: './articles/article1.pdf',
+ labels: ['science', 'biology'],
+ path: 'virtual-path/to/science-articles',
+});
+
+
+const file = await client.files.get(fileUploadResponse.fileId);
+
+```
+
+---
+### 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.
+
+
+```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.' }],
+ });
+
+```
-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.
## Configuration
@@ -65,6 +159,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
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
new file mode 100644
index 0000000..5dcc0c0
--- /dev/null
+++ b/examples/studio/conversational-rag/rag-engine.ts
@@ -0,0 +1,139 @@
+import { AI21, FileResponse, UploadFileResponse } from 'ai21';
+import path from 'path';
+import fs from 'fs';
+
+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 {
+ console.log(`Starting upload for file:`, typeof fileInput);
+ const uploadFileResponse: UploadFileResponse = await client.files.create({
+ file: fileInput,
+ path: path,
+ });
+ 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 processing completed with status: ${file.status}`);
+
+ if (file.status === 'PROCESSED') {
+ 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 update completed');
+ } else {
+ console.log(`⚠ File processing failed with status ${file.status}`);
+ return; // Exit early if processing failed
+ }
+
+ 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;
+ }
+}
+
+async function listFiles() {
+ const client = new AI21({ apiKey: process.env.AI21_API_KEY });
+ const files = await client.files.list({ limit: 4 });
+ console.log(`Listed files: ${files}`);
+}
+
+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 {
+ /* 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 {
+ console.log('=== Starting first operation ===');
+ // First operation - upload file from path
+ 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);
+
+ 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 = createNodeFile(fileContent, 'example.txt', 'text/plain');
+ await uploadGetUpdateDelete(dummyFile, Date.now().toString());
+ console.log('=== Second operation completed ===\n');
+ await sleep(2000);
+
+ console.log('=== Starting file listing ===');
+ await listFiles();
+ console.log('=== File listing completed ===');
+ } catch (error) {
+ console.error('❌ Main execution error:', error);
+ process.exit(1); // Exit with error code if something fails
+ }
+ })();
+}
diff --git a/package.json b/package.json
index b1104f0..2d88713 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,9 @@
{
"name": "ai21",
- "version": "1.0.3",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "version": "1.1.0-rc.1",
"description": "AI21 TypeScript SDK",
"main": "./dist/bundle.cjs.js",
"types": "./dist/index.d.ts",
diff --git a/src/AI21.ts b/src/AI21.ts
index ce44b86..36b8d27 100644
--- a/src/AI21.ts
+++ b/src/AI21.ts
@@ -5,7 +5,8 @@ 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 {
baseURL?: string | undefined;
@@ -67,6 +68,7 @@ export class AI21 extends APIClient {
// Resources
chat: Chat = new Chat(this);
conversationalRag: ConversationalRag = new ConversationalRag(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/APIClient.ts b/src/APIClient.ts
index a42cd74..b57fa34 100644
--- a/src/APIClient.ts
+++ b/src/APIClient.ts
@@ -8,10 +8,14 @@ import {
HTTPMethod,
Headers,
CrossPlatformResponse,
+ UnifiedFormData,
+ FilePathOrFileObject,
} from './types';
import { AI21EnvConfig } from './EnvConfig';
-import { createFetchInstance } from './runtime';
+import { createFetchInstance, createFilesHandlerInstance } from './factory';
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)) {
@@ -23,42 +27,80 @@ 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;
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);
+ 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);
+ }
+
+ 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 {
@@ -70,12 +112,13 @@ 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),
};
+
+ return { ...defaultHeaders, ...opts.headers };
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -83,25 +126,42 @@ 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;
- return this.performRequest(options as FinalRequestOptions).then(
- (response) => this.fetch.handleResponse(response) as Rsp,
- );
+ if (options?.body) {
+ options.body = JSON.stringify(options.body);
+ options.headers = { ...options.headers, 'Content-Type': 'application/json' };
+ }
+
+ return this.performRequest(options).then((response) => this.fetch.handleResponse(response) as Rsp);
}
private async performRequest(options: FinalRequestOptions): Promise {
- const url = `${this.baseURL}${options.path}`;
+ const url = this.buildFullUrl(options.path, options.query as Record);
const headers = {
...this.defaultHeaders(options),
...options.headers,
};
+
const response = await this.fetch.call(url, { ...options, headers });
if (!response.ok) {
diff --git a/src/factory.ts b/src/factory.ts
new file mode 100644
index 0000000..b7ce18e
--- /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/fetch/BaseFetch.ts b/src/fetch/BaseFetch.ts
index 6d643f2..7b27749 100644
--- a/src/fetch/BaseFetch.ts
+++ b/src/fetch/BaseFetch.ts
@@ -7,6 +7,7 @@ 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/fetch/BrowserFetch.ts b/src/fetch/BrowserFetch.ts
index 0872dcb..15e0492 100644
--- a/src/fetch/BrowserFetch.ts
+++ b/src/fetch/BrowserFetch.ts
@@ -9,7 +9,7 @@ export class BrowserFetch extends BaseFetch {
return fetch(url, {
method: options.method,
headers: options?.headers ? (options.headers as HeadersInit) : undefined,
- body: options?.body ? JSON.stringify(options.body) : undefined,
+ body: options?.body ? (options.body as BodyInit) : undefined,
signal: controller.signal,
});
}
diff --git a/src/fetch/NodeFetch.ts b/src/fetch/NodeFetch.ts
index 4c7c764..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 ? JSON.stringify(options.body) : undefined,
+ body: options?.body ? (options.body as NodeHTTPBody) : undefined,
});
}
diff --git a/src/files/BaseFilesHandler.ts b/src/files/BaseFilesHandler.ts
new file mode 100644
index 0000000..94d05b4
--- /dev/null
+++ b/src/files/BaseFilesHandler.ts
@@ -0,0 +1,6 @@
+import { FilePathOrFileObject } from 'types';
+import { FormDataRequest } from 'types/API';
+
+export abstract class BaseFilesHandler {
+ abstract prepareFormDataRequest(file: FilePathOrFileObject): Promise;
+}
diff --git a/src/files/BrowserFilesHandler.ts b/src/files/BrowserFilesHandler.ts
new file mode 100644
index 0000000..59c1d6b
--- /dev/null
+++ b/src/files/BrowserFilesHandler.ts
@@ -0,0 +1,12 @@
+import { FilePathOrFileObject } from 'types';
+import { BaseFilesHandler } from './BaseFilesHandler';
+import { FormDataRequest } from 'types/API';
+
+export class BrowserFilesHandler extends BaseFilesHandler {
+ async prepareFormDataRequest(file: FilePathOrFileObject): Promise {
+ const formData = new FormData();
+ 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
new file mode 100644
index 0000000..d2e1739
--- /dev/null
+++ b/src/files/NodeFilesHandler.ts
@@ -0,0 +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 {
+ if (!isNode) {
+ throw new Error('Stream conversion is not supported in browser environment');
+ }
+
+ 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);
+ },
+ });
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private async createStreamFromFilePath(filePath: string, formData: any): Promise {
+ 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 {
+ try {
+ const FormData = await import('form-data').then((m) => m.default || m);
+ const formData = new FormData();
+
+ if (typeof file === 'string') {
+ await this.createStreamFromFilePath(file, formData);
+ return this.createFormDataRequest(formData);
+ }
+
+ if (!file || typeof file !== 'object') {
+ throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`);
+ }
+
+ if ('buffer' in file) {
+ formData.append('file', file.buffer, {
+ filename: file.name,
+ contentType: file.type,
+ });
+ return this.createFormDataRequest(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.createFormDataRequest(formData);
+ }
+
+ throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`);
+ } catch (error) {
+ console.error('Error in prepareFormDataRequest:', error);
+ throw error;
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private createFormDataRequest(formData: any): FormDataRequest {
+ return {
+ formData,
+ headers: {
+ 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
+ },
+ };
+ }
+}
diff --git a/src/files/index.ts b/src/files/index.ts
new file mode 100644
index 0000000..bb90c1b
--- /dev/null
+++ b/src/files/index.ts
@@ -0,0 +1,2 @@
+export { BrowserFilesHandler } from './BrowserFilesHandler';
+export { NodeFilesHandler } from './NodeFilesHandler';
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/src/resources/files/files.ts b/src/resources/files/files.ts
new file mode 100644
index 0000000..f3bf691
--- /dev/null
+++ b/src/resources/files/files.ts
@@ -0,0 +1,44 @@
+import * as Models from '../../types';
+import { APIResource } from '../../APIResource';
+import { UploadFileResponse, UploadFileRequest, ListFilesFilters, UpdateFileRequest } from '../../types';
+import { FileResponse } from 'types/files/FileResponse';
+
+const FILES_PATH = '/library/files';
+
+export class Files extends APIResource {
+ async create(body: UploadFileRequest, options?: Models.RequestOptions): Promise {
+ const { file, ...bodyWithoutFile } = body;
+ return this.client.upload(FILES_PATH, file, {
+ body: bodyWithoutFile,
+ ...options,
+ } as Models.RequestOptions) as Promise;
+ }
+
+ get(fileId: string, options?: Models.RequestOptions): Promise {
+ return this.client.get(
+ `${FILES_PATH}/${fileId}`,
+ options as Models.RequestOptions,
+ ) as Promise;
+ }
+
+ delete(fileId: string, options?: Models.RequestOptions): Promise {
+ return this.client.delete(
+ `${FILES_PATH}/${fileId}`,
+ options as Models.RequestOptions,
+ ) as Promise;
+ }
+
+ list(body?: ListFilesFilters, options?: Models.RequestOptions): Promise {
+ 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(`${FILES_PATH}/${body.fileId}`, {
+ body,
+ ...options,
+ } as Models.RequestOptions) as Promise;
+ }
+}
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 da47cbf..12d4ba4 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 { Files } from './files';
diff --git a/src/resources/rag/conversationalRag.ts b/src/resources/rag/conversational-rag.ts
similarity index 72%
rename from src/resources/rag/conversationalRag.ts
rename to src/resources/rag/conversational-rag.ts
index bc5caa0..3ec423b 100644
--- a/src/resources/rag/conversationalRag.ts
+++ b/src/resources/rag/conversational-rag.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/index.ts b/src/resources/rag/index.ts
index 2bb5fca..187dc07 100644
--- a/src/resources/rag/index.ts
+++ b/src/resources/rag/index.ts
@@ -1 +1 @@
-export { ConversationalRag } from './conversationalRag';
+export { ConversationalRag } from './conversational-rag';
diff --git a/src/runtime.ts b/src/runtime.ts
index 9f4d483..a398a34 100644
--- a/src/runtime.ts
+++ b/src/runtime.ts
@@ -1,5 +1,3 @@
-import { BrowserFetch, Fetch, NodeFetch } from './fetch';
-
export const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
/**
@@ -18,11 +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) {
- return new BrowserFetch();
- }
-
- return new NodeFetch();
-}
diff --git a/src/types/API.ts b/src/types/API.ts
index 5bf2cbe..67b64fd 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 | UnifiedFormData | string | null | undefined;
headers?: Headers | undefined;
maxRetries?: number;
@@ -29,3 +29,9 @@ 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');
+
+export type FormDataRequest = { formData: UnifiedFormData; headers: Headers };
+
+export type NodeHTTPBody = string | import('form-data');
diff --git a/src/types/files/FilePathOrFileObject.ts b/src/types/files/FilePathOrFileObject.ts
new file mode 100644
index 0000000..cace19b
--- /dev/null
+++ b/src/types/files/FilePathOrFileObject.ts
@@ -0,0 +1 @@
+export type FilePathOrFileObject = string | File;
diff --git a/src/types/files/FileResponse.ts b/src/types/files/FileResponse.ts
new file mode 100644
index 0000000..24fb111
--- /dev/null
+++ b/src/types/files/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/files/ListFilesFilters.ts b/src/types/files/ListFilesFilters.ts
new file mode 100644
index 0000000..69603c2
--- /dev/null
+++ b/src/types/files/ListFilesFilters.ts
@@ -0,0 +1,4 @@
+export interface ListFilesFilters {
+ offset?: number | null;
+ limit?: number | null;
+}
diff --git a/src/types/files/UpdateFileRequest.ts b/src/types/files/UpdateFileRequest.ts
new file mode 100644
index 0000000..601805a
--- /dev/null
+++ b/src/types/files/UpdateFileRequest.ts
@@ -0,0 +1,5 @@
+export interface UpdateFileRequest {
+ fileId: string;
+ labels?: string[] | null;
+ publicUrl?: string | null;
+}
diff --git a/src/types/files/UploadFileRequest.ts b/src/types/files/UploadFileRequest.ts
new file mode 100644
index 0000000..b6bd993
--- /dev/null
+++ b/src/types/files/UploadFileRequest.ts
@@ -0,0 +1,8 @@
+import { FilePathOrFileObject } from './FilePathOrFileObject';
+
+export interface UploadFileRequest {
+ file: FilePathOrFileObject;
+ path?: string | null;
+ labels?: string[] | null;
+ publicUrl?: string | null;
+}
diff --git a/src/types/files/UploadFileResponse.ts b/src/types/files/UploadFileResponse.ts
new file mode 100644
index 0000000..b5fa16c
--- /dev/null
+++ b/src/types/files/UploadFileResponse.ts
@@ -0,0 +1,4 @@
+export interface UploadFileResponse {
+ id: string;
+ fileId: string;
+}
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 1a735ed..d40670f 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 {
@@ -43,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/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';
diff --git a/tests/unittests/resources/chat/conversationalRag.test.ts b/tests/unittests/resources/chat/conversational-rag.test.ts
similarity index 96%
rename from tests/unittests/resources/chat/conversationalRag.test.ts
rename to tests/unittests/resources/chat/conversational-rag.test.ts
index 2578d08..9d6db39 100644
--- a/tests/unittests/resources/chat/conversationalRag.test.ts
+++ b/tests/unittests/resources/chat/conversational-rag.test.ts
@@ -1,10 +1,14 @@
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';
class MockAPIClient extends APIClient {
public post = jest.fn();
+ public upload = jest.fn();
+ public get = jest.fn();
+ public delete = jest.fn();
+ public put = jest.fn();
}
diff --git a/tests/unittests/resources/rag-engine.test.ts b/tests/unittests/resources/rag-engine.test.ts
new file mode 100644
index 0000000..88b2dc1
--- /dev/null
+++ b/tests/unittests/resources/rag-engine.test.ts
@@ -0,0 +1,112 @@
+import * as Models from '../../../src/types';
+import { Files } from '../../../src/resources/files/files';
+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 files: Files;
+ 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,
+ });
+
+ files = new Files(mockClient);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should upload a file and return the fileId', async () => {
+ const fileInput = 'path/to/file.txt';
+ const body = { file: fileInput, path: 'path' };
+ const expectedResponse = { fileId: '12345' };
+
+ mockClient.upload.mockResolvedValue(expectedResponse);
+
+ const response = await files.create(body);
+
+ expect(mockClient.upload).toHaveBeenCalledWith(
+ '/library/files',
+ fileInput,
+ {
+ body: { path: 'path' },
+ }
+ );
+ 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 files.get(fileId, options);
+
+ expect(mockClient.get).toHaveBeenCalledWith(
+ `/library/files/${fileId}`,
+ { ...options }
+ );
+ expect(response).toEqual(expectedResponse);
+ });
+
+ it('should delete a file by ID', async () => {
+ const fileId = '12345';
+
+ mockClient.delete.mockResolvedValue(null);
+
+ const response = await files.delete(fileId, options);
+
+ expect(mockClient.delete).toHaveBeenCalledWith(
+ `/library/files/${fileId}`,
+ { ...options }
+ );
+ expect(response).toBeNull();
+ });
+
+ it('should update a file by ID and return null', async () => {
+ const fileId = '12345';
+ const body = { fileId, labels: ['test'], publicUrl: 'https://example.com' };
+
+ mockClient.put.mockResolvedValue(null);
+
+ const response = await files.update(body);
+
+ expect(mockClient.put).toHaveBeenCalledWith(
+ `/library/files/${fileId}`,
+ { body },
+ );
+ 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 files.list(filters, options);
+
+ expect(mockClient.get).toHaveBeenCalledWith(
+ '/library/files',
+ {
+ query: filters,
+ ...options,
+ }
+ );
+ expect(response).toEqual(expectedResponse);
+ });
+});
diff --git a/vite.config.ts b/vite.config.ts
index b09b01b..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'],
+ external: ['node-fetch', 'form-data', 'fs', 'stream'],
output: {
globals: {
'node-fetch': 'fetch',