Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quotientai",
"version": "0.0.4",
"version": "0.0.5",
"description": "TypeScript client for QuotientAI API",
"main": "dist/quotientai/index.js",
"types": "dist/quotientai/index.d.ts",
Expand All @@ -14,7 +14,8 @@
"build": "tsc",
"test": "vitest run",
"coverage": "vitest run --coverage",
"lint": "eslint . --ext .ts"
"lint": "eslint . --ext .ts",
"test:watch": "vitest"
},
"dependencies": {
"axios": "^1.6.7",
Expand Down
4 changes: 2 additions & 2 deletions quotientai/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { TokenData } from './types';
import { QuotientAIError } from './exceptions';
import { logError, QuotientAIError } from './exceptions';

export class BaseQuotientClient {
private apiKey: string;
Expand Down Expand Up @@ -76,7 +76,7 @@ export class BaseQuotientClient {
JSON.stringify({ token, expires_at: expiry })
);
} catch (error) {
throw new QuotientAIError('Could not create directory for token. If you see this error please notify us at contact@quotientai.co');
logError(new QuotientAIError('Could not create directory for token. If you see this error please notify us at contact@quotientai.co'));
}
}

Expand Down
110 changes: 90 additions & 20 deletions quotientai/exceptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

export function logError(error: Error, context?: string) {
const timestamp = new Date().toISOString();
const contextStr = context ? `[${context}] ` : '';
const stack = error.stack || '';

console.error(`[${timestamp}] ${contextStr}${error.name}: ${error.message}`);
console.error(stack);
}

export class QuotientAIError extends Error {
constructor(message: string) {
super(message);
Expand Down Expand Up @@ -79,34 +88,66 @@ export class APITimeoutError extends APIConnectionError {

export class BadRequestError extends APIStatusError {
status = 400;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'BadRequestError';
}
}

export class AuthenticationError extends APIStatusError {
status = 401;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'AuthenticationError';
}
}

export class PermissionDeniedError extends APIStatusError {
status = 403;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'PermissionDeniedError';
}
}

export class NotFoundError extends APIStatusError {
status = 404;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'NotFoundError';
}
}

export class ConflictError extends APIStatusError {
status = 409;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'ConflictError';
}
}

export class UnprocessableEntityError extends APIStatusError {
status = 422;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'UnprocessableEntityError';
}
}

export class RateLimitError extends APIStatusError {
status = 429;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'RateLimitError';
}
}

export class InternalServerError extends APIStatusError {
status = 500;
constructor(message: string, response: AxiosResponse, body?: any) {
super(message, response, body);
this.name = 'InternalServerError';
}
}

export function parseUnprocessableEntityError(response: AxiosResponse): string {
Expand All @@ -123,9 +164,13 @@ export function parseUnprocessableEntityError(response: AxiosResponse): string {
return `missing required fields: ${missingFields.join(', ')}`;
}
}
throw new APIResponseValidationError(response, body);
const error = new APIResponseValidationError(response, body);
logError(error, 'parseUnprocessableEntityError');
return 'Invalid response format';
} catch (error) {
throw new APIResponseValidationError(response, null);
const apiError = new APIResponseValidationError(response, null);
logError(apiError, 'parseUnprocessableEntityError');
return 'Invalid response format';
}
}

Expand All @@ -135,9 +180,13 @@ export function parseBadRequestError(response: AxiosResponse): string {
if ('detail' in body) {
return body.detail;
}
throw new APIResponseValidationError(response, body);
const error = new APIResponseValidationError(response, body);
logError(error, 'parseBadRequestError');
return 'Invalid request format';
} catch (error) {
throw new APIResponseValidationError(response, null);
const apiError = new APIResponseValidationError(response, null);
logError(apiError, 'parseBadRequestError');
return 'Invalid request format';
}
}

Expand All @@ -153,50 +202,66 @@ export function handleErrors() {
try {
const response = await originalMethod.apply(this, args);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
} catch (err) {
if (axios.isAxiosError(err)) {
const axiosError = err as AxiosError;

if (axiosError.response) {
const { status, data } = axiosError.response;

switch (status) {
case 400: {
const message = parseBadRequestError(axiosError.response);
throw new BadRequestError(message, axiosError.response, data);
const error = new BadRequestError(message, axiosError.response, data);
logError(error, `${target.constructor.name}.${propertyKey}`);
return null;
}
case 401:
throw new AuthenticationError(
case 401: {
const error = new AuthenticationError(
'unauthorized: the request requires user authentication. ensure your API key is correct.',
axiosError.response,
data
);
case 403:
throw new PermissionDeniedError(
logError(error, `${target.constructor.name}.${propertyKey}`);
return null;
}
case 403: {
const error = new PermissionDeniedError(
'forbidden: the server understood the request, but it refuses to authorize it.',
axiosError.response,
data
);
case 404:
throw new NotFoundError(
logError(error, `${target.constructor.name}.${propertyKey}`);
return null;
}
case 404: {
const error = new NotFoundError(
'not found: the server can not find the requested resource.',
axiosError.response,
data
);
logError(error, `${target.constructor.name}.${propertyKey}`);
return null;
}
case 422: {
const unprocessableMessage = parseUnprocessableEntityError(axiosError.response);
throw new UnprocessableEntityError(
const error = new UnprocessableEntityError(
unprocessableMessage,
axiosError.response,
data
);
logError(error, `${target.constructor.name}.${propertyKey}`);
return null;
}
default:
throw new APIStatusError(
default: {
const error = new APIStatusError(
`unexpected status code: ${status}. contact support@quotientai.co for help.`,
axiosError.response,
data
);
logError(error, `${target.constructor.name}.${propertyKey}`);
return null;
}
}
}

Expand All @@ -207,15 +272,20 @@ export function handleErrors() {
delay *= 2; // Exponential backoff
continue;
}
throw new APITimeoutError(axiosError.config);
const error = new APITimeoutError(axiosError.config);
logError(error, `${target.constructor.name}.${propertyKey}`);
return null;
}

throw new APIConnectionError(
const connectionError = new APIConnectionError(
'connection error. please try again later.',
axiosError.config || { url: 'unknown' }
);
logError(connectionError, `${target.constructor.name}.${propertyKey}`);
return null;
}
throw error;
logError(err as Error, `${target.constructor.name}.${propertyKey}`);
return null;
}
}
};
Expand Down
34 changes: 21 additions & 13 deletions quotientai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,35 @@ import { ModelsResource } from './resources/models';
import { RunsResource } from './resources/runs';
import { MetricsResource } from './resources/metrics';
import { LogsResource } from './resources/logs';
import { logError } from './exceptions';

export class QuotientAI {
public auth: AuthResource;
public prompts: PromptsResource;
public datasets: DatasetsResource;
public models: ModelsResource;
public runs: RunsResource;
public metrics: MetricsResource;
public logs: LogsResource;
public logger: QuotientLogger;
export class QuotientAI {
public auth: AuthResource = null!;
public prompts: PromptsResource = null!;
public datasets: DatasetsResource = null!;
public models: ModelsResource = null!;
public runs: RunsResource = null!;
public metrics: MetricsResource = null!;
public logs: LogsResource = null!;
public logger: QuotientLogger = null!;

constructor(apiKey?: string) {
const key = apiKey || process.env.QUOTIENT_API_KEY;
if (!key) {
throw new Error(
const error = new Error(
'Could not find API key. Either pass apiKey to QuotientAI() or ' +
'set the QUOTIENT_API_KEY environment variable. ' +
'If you do not have an API key, you can create one at https://app.quotientai.co in your settings page'
);
logError(error, 'QuotientAI.constructor');
return;
} else {
const client = new BaseQuotientClient(key);
this.initializeResources(client);
}
}

const client = new BaseQuotientClient(key);

private initializeResources(client: BaseQuotientClient): void {
// Initialize resources
this.auth = new AuthResource(client);
this.prompts = new PromptsResource(client);
Expand Down Expand Up @@ -64,10 +70,12 @@ export class QuotientAI {
);

if (invalidParameters.length > 0) {
throw new Error(
const error = new Error(
`Invalid parameters: ${invalidParameters.join(', ')}. ` +
`Valid parameters are: ${validParameters.join(', ')}`
);
logError(error, 'QuotientAI.evaluate');
return null;
}

return this.runs.create({
Expand Down
31 changes: 20 additions & 11 deletions quotientai/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LogEntry, LoggerConfig, LogDocument } from './types';
import { ValidationError } from './exceptions';
import { ValidationError, logError } from './exceptions';

interface LogsResource {
create(params: LogEntry): Promise<any>;
Expand Down Expand Up @@ -32,7 +32,8 @@ export class QuotientLogger {
this.configured = true;

if (this.sampleRate < 0 || this.sampleRate > 1) {
throw new Error('sample_rate must be between 0.0 and 1.0');
logError(new Error('sample_rate must be between 0.0 and 1.0'));
return this;
}

return this;
Expand Down Expand Up @@ -76,9 +77,9 @@ export class QuotientLogger {
}

// Validate document format
private validateDocuments(documents: (string | LogDocument)[]): void {
private validateDocuments(documents: (string | LogDocument)[]): boolean {
if (!documents || documents.length === 0) {
return;
return true;
}

for (let i = 0; i < documents.length; i++) {
Expand All @@ -88,35 +89,43 @@ export class QuotientLogger {
} else if (typeof doc === 'object' && doc !== null) {
const validation = this.isValidLogDocument(doc);
if (!validation.valid) {
throw new ValidationError(
logError(new ValidationError(
`Invalid document format at index ${i}: ${validation.error}. ` +
"Documents must be either strings or JSON objects with a 'page_content' string property and an optional 'metadata' object. " +
"To fix this, ensure each document follows the format: { page_content: 'your text content', metadata?: { key: 'value' } }"
);
));
return false;
}
} else {
throw new ValidationError(
logError(new ValidationError(
`Invalid document type at index ${i}. Found ${typeof doc}, but documents must be either strings or JSON objects with a 'page_content' property. ` +
"To fix this, provide documents as either simple strings or properly formatted objects: { page_content: 'your text content' }"
);
));
return false;
}
}
return true;
}

// log a message
// params: Omit<LogEntry, 'app_name' | 'environment'>
async log(params: Omit<LogEntry, 'app_name' | 'environment'>): Promise<any> {
if (!this.configured) {
throw new Error('Logger is not configured. Please call init() before logging.');
logError(new Error('Logger is not configured. Please call init() before logging.'));
return null;
}

if (!this.appName || !this.environment) {
throw new Error('Logger is not properly configured. app_name and environment must be set.');
logError(new Error('Logger is not properly configured. app_name and environment must be set.'));
return null;
}

// Validate documents format
if (params.documents) {
this.validateDocuments(params.documents);
const isValid = this.validateDocuments(params.documents);
if (!isValid) {
return null;
}
}

// Merge default tags with any tags provided at log time
Expand Down
Loading