Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: RAG functionality #24

Merged
merged 44 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4713d02
feat: rag-engine impl. - with examples, no tests yet
amirai21 Nov 25, 2024
ceaa9e9
feat: rag-engine impl. - with examples, no tests yet
amirai21 Nov 25, 2024
657fa5e
feat: makeFormDataRequest - change type and disable lint
amirai21 Nov 25, 2024
d68e935
feat: remove log
amirai21 Nov 25, 2024
329eb3e
feat: add upload file object override
amirai21 Nov 26, 2024
6131ee8
feat: add upload file object override
amirai21 Nov 26, 2024
54a96b1
feat: FilePathOrFileObject
amirai21 Nov 26, 2024
48f2a8d
feat: support node path and file object\ and browser file object
amirai21 Nov 27, 2024
c055ca2
feat: support node path and file object\ and browser file object
amirai21 Nov 27, 2024
02b40df
feat: functioning browser upload
amirai21 Nov 28, 2024
b71e598
feat: reorganized
amirai21 Nov 28, 2024
a93d32c
feat: reorganized
amirai21 Nov 28, 2024
eac5e6e
feat: wip
amirai21 Nov 28, 2024
a628702
feat: wip
amirai21 Dec 1, 2024
6ff4df7
feat: basic unit tests for rag engine
amirai21 Dec 1, 2024
ab469be
feat: wip
amirai21 Dec 2, 2024
f438185
feat: wip
amirai21 Dec 2, 2024
382e69f
feat: wip
amirai21 Dec 2, 2024
7ca7640
feat: add file path check before opening + rag examples improved
amirai21 Dec 4, 2024
9705d0b
feat: fix tests
amirai21 Dec 4, 2024
c88bf02
feat: reorg imports
amirai21 Dec 4, 2024
e3b0810
feat: convert upload to non async
amirai21 Dec 4, 2024
29997e0
feat: node fetch casting
amirai21 Dec 4, 2024
90eb449
feat: disable examples for non node env
amirai21 Dec 4, 2024
8914f61
feat: disable examples for non node env
amirai21 Dec 4, 2024
585e278
feat: log
amirai21 Dec 4, 2024
18b46a0
feat: swap condition
amirai21 Dec 4, 2024
79e616b
test: Trying integration test
asafgardin Dec 4, 2024
5888d67
fix: Added log
asafgardin Dec 4, 2024
e251c1b
fix: Run sync
asafgardin Dec 4, 2024
9cccbc9
fix: Added logs
asafgardin Dec 4, 2024
9de0844
fix: Added logs of env
asafgardin Dec 4, 2024
4e7f794
fix: Added logs of env
asafgardin Dec 4, 2024
8ff04de
fix: Added more logs
asafgardin Dec 4, 2024
e2129a6
fix: Checked env
asafgardin Dec 4, 2024
ac11d5c
ci: Added form-data to bundle
asafgardin Dec 4, 2024
49d82c6
ci: Added form-data to bundle
asafgardin Dec 4, 2024
1b5b314
fix: Node file checks
asafgardin Dec 4, 2024
e3b31ac
fix: Node file checks
asafgardin Dec 4, 2024
670fc69
fix: check type
asafgardin Dec 4, 2024
09be7a4
fix: Moved to factory
asafgardin Dec 4, 2024
d24dc65
fix: ignore ts
asafgardin Dec 4, 2024
af96613
fix: Import of runtime
asafgardin Dec 4, 2024
bf6ad2a
refactor: Renamed methods
asafgardin Dec 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install dependencies
run: |
npm install
npm install ai21
npm run build

- name: Run Integration Tests
env:
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.txt
1 change: 1 addition & 0 deletions examples/studio/conversational-rag/files/meerkat.txt
Original file line number Diff line number Diff line change
@@ -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.
139 changes: 139 additions & 0 deletions examples/studio/conversational-rag/rag-engine.ts
Original file line number Diff line number Diff line change
@@ -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
}
})();
}
4 changes: 3 additions & 1 deletion src/AI21.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
86 changes: 73 additions & 13 deletions src/APIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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<string, any>): 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<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
return this.makeRequest('get', path, opts);
return this.prepareAndExecuteRequest('get', path, opts);
}

post<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
return this.makeRequest('post', path, opts);
return this.prepareAndExecuteRequest('post', path, opts);
}

put<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
return this.makeRequest('put', path, opts);
return this.prepareAndExecuteRequest('put', path, opts);
}

delete<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
return this.makeRequest('delete', path, opts);
return this.prepareAndExecuteRequest('delete', path, opts);
}

upload<Req, Rsp>(path: string, file: FilePathOrFileObject, opts?: RequestOptions<Req>): Promise<Rsp> {
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<string, any>);
}

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<Rsp>(response) as Rsp);
});
}

protected getUserAgent(): string {
Expand All @@ -70,38 +112,56 @@ 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
protected authHeaders(opts: FinalRequestOptions): Headers {
return {};
}

private makeRequest<Req, Rsp>(method: HTTPMethod, path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
private buildFullUrl(path: string, query?: Record<string, unknown>): string {
let url = `${this.baseURL}${path}`;
if (query) {
const queryString = new URLSearchParams(query as Record<string, string>).toString();
url += `?${queryString}`;
}
return url;
}

private prepareAndExecuteRequest<Req, Rsp>(
method: HTTPMethod,
path: string,
opts?: RequestOptions<Req>,
): Promise<Rsp> {
const options = {
method,
path,
...opts,
};
} as FinalRequestOptions;

return this.performRequest(options as FinalRequestOptions).then(
(response) => this.fetch.handleResponse<Rsp>(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<Rsp>(response) as Rsp);
}

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

const headers = {
...this.defaultHeaders(options),
...options.headers,
};

const response = await this.fetch.call(url, { ...options, headers });

if (!response.ok) {
Expand Down
21 changes: 21 additions & 0 deletions src/factory.ts
Original file line number Diff line number Diff line change
@@ -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();
}
1 change: 1 addition & 0 deletions src/fetch/BaseFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type APIResponse<T> = {
data?: T;
response: CrossPlatformResponse;
};

export abstract class BaseFetch {
abstract call(url: string, options: FinalRequestOptions): Promise<CrossPlatformResponse>;
async handleResponse<T>({ response, options }: APIResponseProps) {
Expand Down
2 changes: 1 addition & 1 deletion src/fetch/BrowserFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/fetch/NodeFetch.ts
Original file line number Diff line number Diff line change
@@ -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<CrossPlatformResponse> {
Expand All @@ -10,7 +11,7 @@ export class NodeFetch extends BaseFetch {
return nodeFetch(url, {
method: options.method,
headers: options?.headers ? (options.headers as Record<string, string>) : undefined,
body: options?.body ? JSON.stringify(options.body) : undefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Josephasafg I don't like how this ended with, perhaps you'll have an idea for something better :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amirai21 I think its pretty good and for the sake of moving forward, lets make a custom type called
type NodeHTTPBody = import('form-data') | string;

and one for the browser as well and call it a day for now. Maybe we'll improve it in the future, but for now I think its good enough. wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds great, exporting them to custom type.

body: options?.body ? (options.body as NodeHTTPBody) : undefined,
});
}

Expand Down
Loading
Loading