Skip to content

Commit 0e4019e

Browse files
authored
Merge pull request #24 from AI21Labs/library-upload
feat: RAG functionality
2 parents e5399f7 + bf6ad2a commit 0e4019e

33 files changed

+589
-34
lines changed

.github/workflows/integration-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
npm install
25-
npm install ai21
25+
npm run build
2626
2727
- name: Run Integration Tests
2828
env:

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { AI21, FileResponse, UploadFileResponse } from 'ai21';
2+
import path from 'path';
3+
import fs from 'fs';
4+
5+
function sleep(ms) {
6+
return new Promise((resolve) => setTimeout(resolve, ms));
7+
}
8+
9+
async function waitForFileProcessing(
10+
client: AI21,
11+
fileId: string,
12+
timeout: number = 30000,
13+
interval: number = 1000,
14+
) {
15+
const startTime = Date.now();
16+
17+
while (Date.now() - startTime < timeout) {
18+
const file: FileResponse = await client.files.get(fileId);
19+
if (file.status !== 'PROCESSING') {
20+
return file;
21+
}
22+
await sleep(interval);
23+
}
24+
25+
throw new Error(`File processing timed out after ${timeout}ms`);
26+
}
27+
28+
async function uploadGetUpdateDelete(fileInput, path) {
29+
const client = new AI21({ apiKey: process.env.AI21_API_KEY });
30+
try {
31+
console.log(`Starting upload for file:`, typeof fileInput);
32+
const uploadFileResponse: UploadFileResponse = await client.files.create({
33+
file: fileInput,
34+
path: path,
35+
});
36+
console.log(`✓ Upload completed. File ID: ${uploadFileResponse.fileId}`);
37+
38+
console.log('Waiting for file processing...');
39+
let file: FileResponse = await waitForFileProcessing(client, uploadFileResponse.fileId);
40+
console.log(`✓ File processing completed with status: ${file.status}`);
41+
42+
if (file.status === 'PROCESSED') {
43+
console.log('Starting file update...');
44+
await client.files.update({
45+
fileId: uploadFileResponse.fileId,
46+
labels: ['test99'],
47+
publicUrl: 'https://www.miri.com',
48+
});
49+
file = await client.files.get(uploadFileResponse.fileId);
50+
console.log('✓ File update completed');
51+
} else {
52+
console.log(`⚠ File processing failed with status ${file.status}`);
53+
return; // Exit early if processing failed
54+
}
55+
56+
console.log('Starting file deletion...');
57+
await client.files.delete(uploadFileResponse.fileId);
58+
console.log('✓ File deletion completed');
59+
60+
// Add buffer time between operations
61+
await sleep(2000);
62+
} catch (error) {
63+
console.error('❌ Error in uploadGetUpdateDelete:', error);
64+
throw error;
65+
}
66+
}
67+
68+
async function listFiles() {
69+
const client = new AI21({ apiKey: process.env.AI21_API_KEY });
70+
const files = await client.files.list({ limit: 4 });
71+
console.log(`Listed files: ${files}`);
72+
}
73+
74+
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
75+
76+
const createNodeFile = (content: Buffer, filename: string, type: string) => {
77+
if (process.platform === 'linux') {
78+
console.log('Running on Linux (GitHub Actions)');
79+
// Special handling for Linux (GitHub Actions)
80+
return {
81+
name: filename,
82+
type: type,
83+
buffer: content,
84+
[Symbol.toStringTag]: 'File',
85+
};
86+
} else {
87+
console.log('Running on other platforms');
88+
// Regular handling for other platforms
89+
return new File([content], filename, { type });
90+
}
91+
};
92+
93+
if (isBrowser) {
94+
console.log('Cannot run upload examples in Browser environment');
95+
} else {
96+
/* Log environment details */
97+
console.log('=== Environment Information ===');
98+
console.log(`Node.js Version: ${process.version}`);
99+
console.log(`Platform: ${process.platform}`);
100+
console.log(`Architecture: ${process.arch}`);
101+
console.log(`Process ID: ${process.pid}`);
102+
console.log(`Current Working Directory: ${process.cwd()}`);
103+
console.log('===========================\n');
104+
105+
/* Run all operations sequentially */
106+
(async () => {
107+
try {
108+
console.log('=== Starting first operation ===');
109+
// First operation - upload file from path
110+
const filePath = path.resolve(process.cwd(), 'examples/studio/conversational-rag/files', 'meerkat.txt');
111+
if (!fs.existsSync(filePath)) {
112+
throw new Error(`File not found: ${filePath}`);
113+
} else {
114+
console.log(`File found: ${filePath}`);
115+
}
116+
117+
await uploadGetUpdateDelete(filePath, Date.now().toString());
118+
console.log('=== First operation completed ===\n');
119+
await sleep(2000);
120+
121+
console.log('=== Starting second operation ===');
122+
// Second operation - upload file from File instance
123+
const fileContent = Buffer.from(
124+
'Opossums are members of the marsupial order Didelphimorphia endemic to the Americas.',
125+
);
126+
const dummyFile = createNodeFile(fileContent, 'example.txt', 'text/plain');
127+
await uploadGetUpdateDelete(dummyFile, Date.now().toString());
128+
console.log('=== Second operation completed ===\n');
129+
await sleep(2000);
130+
131+
console.log('=== Starting file listing ===');
132+
await listFiles();
133+
console.log('=== File listing completed ===');
134+
} catch (error) {
135+
console.error('❌ Main execution error:', error);
136+
process.exit(1); // Exit with error code if something fails
137+
}
138+
})();
139+
}

src/AI21.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { Chat } from './resources/chat';
55
import { APIClient } from './APIClient';
66
import { Headers } from './types';
77
import * as Runtime from './runtime';
8-
import { ConversationalRag } from './resources/rag/conversationalRag';
8+
import { ConversationalRag } from './resources/rag/conversational-rag';
9+
import { Files } from './resources';
910

1011
export interface ClientOptions {
1112
baseURL?: string | undefined;
@@ -67,6 +68,7 @@ export class AI21 extends APIClient {
6768
// Resources
6869
chat: Chat = new Chat(this);
6970
conversationalRag: ConversationalRag = new ConversationalRag(this);
71+
files: Files = new Files(this);
7072

7173
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7274
protected override authHeaders(_: Types.FinalRequestOptions): Types.Headers {

src/APIClient.ts

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import {
88
HTTPMethod,
99
Headers,
1010
CrossPlatformResponse,
11+
UnifiedFormData,
12+
FilePathOrFileObject,
1113
} from './types';
1214
import { AI21EnvConfig } from './EnvConfig';
13-
import { createFetchInstance } from './runtime';
15+
import { createFetchInstance, createFilesHandlerInstance } from './factory';
1416
import { Fetch } from 'fetch';
17+
import { BaseFilesHandler } from 'files/BaseFilesHandler';
18+
import { FormDataRequest } from 'types/API';
1519

1620
const validatePositiveInteger = (name: string, n: unknown): number => {
1721
if (typeof n !== 'number' || !Number.isInteger(n)) {
@@ -23,42 +27,80 @@ const validatePositiveInteger = (name: string, n: unknown): number => {
2327
return n;
2428
};
2529

30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
const appendBodyToFormData = (formData: UnifiedFormData, body: Record<string, any>): void => {
32+
for (const [key, value] of Object.entries(body)) {
33+
if (Array.isArray(value)) {
34+
value.forEach((item) => formData.append(key, item));
35+
} else {
36+
formData.append(key, value);
37+
}
38+
}
39+
};
40+
2641
export abstract class APIClient {
2742
protected baseURL: string;
2843
protected maxRetries: number;
2944
protected timeout: number;
3045
protected fetch: Fetch;
46+
protected filesHandler: BaseFilesHandler;
3147

3248
constructor({
3349
baseURL,
3450
maxRetries = AI21EnvConfig.MAX_RETRIES,
3551
timeout = AI21EnvConfig.TIMEOUT_SECONDS,
3652
fetch = createFetchInstance(),
53+
filesHandler = createFilesHandlerInstance(),
3754
}: {
3855
baseURL: string;
3956
maxRetries?: number | undefined;
4057
timeout: number | undefined;
4158
fetch?: Fetch;
59+
filesHandler?: BaseFilesHandler;
4260
}) {
4361
this.baseURL = baseURL;
4462
this.maxRetries = validatePositiveInteger('maxRetries', maxRetries);
4563
this.timeout = validatePositiveInteger('timeout', timeout);
4664
this.fetch = fetch;
65+
this.filesHandler = filesHandler;
4766
}
4867
get<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
49-
return this.makeRequest('get', path, opts);
68+
return this.prepareAndExecuteRequest('get', path, opts);
5069
}
5170

5271
post<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
53-
return this.makeRequest('post', path, opts);
72+
return this.prepareAndExecuteRequest('post', path, opts);
5473
}
5574

5675
put<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
57-
return this.makeRequest('put', path, opts);
76+
return this.prepareAndExecuteRequest('put', path, opts);
5877
}
5978

6079
delete<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
61-
return this.makeRequest('delete', path, opts);
80+
return this.prepareAndExecuteRequest('delete', path, opts);
81+
}
82+
83+
upload<Req, Rsp>(path: string, file: FilePathOrFileObject, opts?: RequestOptions<Req>): Promise<Rsp> {
84+
return this.filesHandler.prepareFormDataRequest(file).then((formDataRequest: FormDataRequest) => {
85+
if (opts?.body) {
86+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
87+
appendBodyToFormData(formDataRequest.formData, opts.body as Record<string, any>);
88+
}
89+
90+
const headers = {
91+
...opts?.headers,
92+
...formDataRequest.headers,
93+
};
94+
95+
const options: FinalRequestOptions = {
96+
method: 'post',
97+
path: path,
98+
body: formDataRequest.formData,
99+
headers,
100+
};
101+
102+
return this.performRequest(options).then((response) => this.fetch.handleResponse<Rsp>(response) as Rsp);
103+
});
62104
}
63105

64106
protected getUserAgent(): string {
@@ -70,38 +112,56 @@ export abstract class APIClient {
70112
}
71113

72114
protected defaultHeaders(opts: FinalRequestOptions): Headers {
73-
return {
115+
const defaultHeaders = {
74116
Accept: 'application/json',
75-
'Content-Type': 'application/json',
76117
'User-Agent': this.getUserAgent(),
77118
...this.authHeaders(opts),
78119
};
120+
121+
return { ...defaultHeaders, ...opts.headers };
79122
}
80123

81124
// eslint-disable-next-line @typescript-eslint/no-unused-vars
82125
protected authHeaders(opts: FinalRequestOptions): Headers {
83126
return {};
84127
}
85128

86-
private makeRequest<Req, Rsp>(method: HTTPMethod, path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
129+
private buildFullUrl(path: string, query?: Record<string, unknown>): string {
130+
let url = `${this.baseURL}${path}`;
131+
if (query) {
132+
const queryString = new URLSearchParams(query as Record<string, string>).toString();
133+
url += `?${queryString}`;
134+
}
135+
return url;
136+
}
137+
138+
private prepareAndExecuteRequest<Req, Rsp>(
139+
method: HTTPMethod,
140+
path: string,
141+
opts?: RequestOptions<Req>,
142+
): Promise<Rsp> {
87143
const options = {
88144
method,
89145
path,
90146
...opts,
91-
};
147+
} as FinalRequestOptions;
92148

93-
return this.performRequest(options as FinalRequestOptions).then(
94-
(response) => this.fetch.handleResponse<Rsp>(response) as Rsp,
95-
);
149+
if (options?.body) {
150+
options.body = JSON.stringify(options.body);
151+
options.headers = { ...options.headers, 'Content-Type': 'application/json' };
152+
}
153+
154+
return this.performRequest(options).then((response) => this.fetch.handleResponse<Rsp>(response) as Rsp);
96155
}
97156

98157
private async performRequest(options: FinalRequestOptions): Promise<APIResponseProps> {
99-
const url = `${this.baseURL}${options.path}`;
158+
const url = this.buildFullUrl(options.path, options.query as Record<string, unknown>);
100159

101160
const headers = {
102161
...this.defaultHeaders(options),
103162
...options.headers,
104163
};
164+
105165
const response = await this.fetch.call(url, { ...options, headers });
106166

107167
if (!response.ok) {

src/factory.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { BrowserFilesHandler } from './files/BrowserFilesHandler';
2+
import { BrowserFetch, Fetch, NodeFetch } from './fetch';
3+
import { NodeFilesHandler } from './files/NodeFilesHandler';
4+
import { BaseFilesHandler } from './files/BaseFilesHandler';
5+
import { isBrowser, isWebWorker } from './runtime';
6+
7+
export function createFetchInstance(): Fetch {
8+
if (isBrowser || isWebWorker) {
9+
return new BrowserFetch();
10+
}
11+
12+
return new NodeFetch();
13+
}
14+
15+
export function createFilesHandlerInstance(): BaseFilesHandler {
16+
if (isBrowser || isWebWorker) {
17+
return new BrowserFilesHandler();
18+
}
19+
20+
return new NodeFilesHandler();
21+
}

src/fetch/BaseFetch.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type APIResponse<T> = {
77
data?: T;
88
response: CrossPlatformResponse;
99
};
10+
1011
export abstract class BaseFetch {
1112
abstract call(url: string, options: FinalRequestOptions): Promise<CrossPlatformResponse>;
1213
async handleResponse<T>({ response, options }: APIResponseProps) {

src/fetch/BrowserFetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class BrowserFetch extends BaseFetch {
99
return fetch(url, {
1010
method: options.method,
1111
headers: options?.headers ? (options.headers as HeadersInit) : undefined,
12-
body: options?.body ? JSON.stringify(options.body) : undefined,
12+
body: options?.body ? (options.body as BodyInit) : undefined,
1313
signal: controller.signal,
1414
});
1515
}

src/fetch/NodeFetch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FinalRequestOptions, CrossPlatformResponse } from 'types';
22
import { BaseFetch } from './BaseFetch';
33
import { Stream, NodeSSEDecoder } from '../streaming';
4+
import { NodeHTTPBody } from 'types/API';
45

56
export class NodeFetch extends BaseFetch {
67
async call(url: string, options: FinalRequestOptions): Promise<CrossPlatformResponse> {
@@ -10,7 +11,7 @@ export class NodeFetch extends BaseFetch {
1011
return nodeFetch(url, {
1112
method: options.method,
1213
headers: options?.headers ? (options.headers as Record<string, string>) : undefined,
13-
body: options?.body ? JSON.stringify(options.body) : undefined,
14+
body: options?.body ? (options.body as NodeHTTPBody) : undefined,
1415
});
1516
}
1617

0 commit comments

Comments
 (0)