diff --git a/src/v2/client.ts b/src/v2/client.ts index 82b562fc..66f8d0b3 100644 --- a/src/v2/client.ts +++ b/src/v2/client.ts @@ -12,6 +12,7 @@ import { PollingOptions, PollingOptionsConstructor } from "./clientOptions/index import { BaseProduct } from "@/v2/product/baseProduct.js"; import { BaseSearch } from "@/v2/search/baseSearch.js"; import { ModelSearch } from "@/v2/search/models/modelSearch.js"; +import { LocalInputSource } from "@/input/index.js"; /** * Options for the V2 Mindee Client. @@ -35,7 +36,9 @@ export interface ClientOptions { * Mindee Client V2 class that centralizes most basic operations. */ export class Client { - /** Mindee V2 API handler. */ + /** + * Mindee V2 API handler. + */ protected mindeeApi: MindeeApiV2; /** @@ -58,38 +61,8 @@ export class Client { } /** - * Search for models available to the account. - * @param name Optional name filter. - * @param modelType Optional model type filter. - * @returns a `Promise` containing the search response. - * @deprecated Use `search(ModelSearch, {})` instead. + * Enqueues a product inference job without waiting for completion. */ - async searchModels(name?: string, modelType?: string): Promise { - return await this.search(ModelSearch, { name: name, modelType: modelType }); - } - - /** - * Search for resources matching the given criteria. - * @param search Search definition class to use. - * @param searchParameters Search parameters. - * @returns a `Promise` containing the search response with the matching resources. - */ - async search( - search: S, - searchParameters: InstanceType | ConstructorParameters[0], - ): Promise> { - if (!searchParameters) { - throw new MindeeError("Search parameters are required."); - } - - const paramsInstance = searchParameters instanceof search.parametersClass - ? searchParameters - : new search.parametersClass(searchParameters); - - return await this.mindeeApi.reqGetSearch(search, paramsInstance); - } - - /** Enqueues a product inference job without waiting for completion. */ async enqueue

( product: P, inputSource: InputSource, @@ -181,15 +154,18 @@ export class Client { product: P, inputSource: InputSource, params: InstanceType | ConstructorParameters[0], - pollingOptions?: PollingOptionsConstructor, + pollingOptions?: PollingOptions | PollingOptionsConstructor, ): Promise> { - const paramsInstance = new product.parametersClass(params); - - const pollingOptionsInstance = new PollingOptions(pollingOptions); - + const paramsInstance = params instanceof product.parametersClass + ? params + : new product.parametersClass(params); const jobResponse: JobResponse = await this.enqueue( product, inputSource, paramsInstance ); + + const pollingOptionsInstance = pollingOptions instanceof PollingOptions + ? pollingOptions + : new PollingOptions(pollingOptions); return await this.pollForResult( product, pollingOptionsInstance, jobResponse ); @@ -252,4 +228,234 @@ export class Client { "You can increase poll attempts by passing the pollingOptions argument to enqueueAndGetResult()" ); } + + /** + * Search for models available to the account. + * @param name Optional name filter. + * @param modelType Optional model type filter. + * @returns a `Promise` containing the search response. + * @deprecated Use `search(ModelSearch, {})` instead. + */ + async searchModels(name?: string, modelType?: string): Promise { + return await this.search(ModelSearch, { name: name, modelType: modelType }); + } + + /** + * Search for resources matching the given criteria. + * @param search Search definition class to use. + * @param searchParameters Search parameters. + * @returns a `Promise` containing the search response with the matching resources. + */ + async search( + search: S, + searchParameters: InstanceType | ConstructorParameters[0], + ): Promise> { + if (!searchParameters) { + throw new MindeeError("Search parameters are required."); + } + const paramsInstance = searchParameters instanceof search.parametersClass + ? searchParameters + : new search.parametersClass(searchParameters); + return await this.mindeeApi.reqGetSearch(search, paramsInstance); + } + + /** + * Not recommended for general use, prefer `getReadyRagDocumentPoll`. + * You will need to poll until the document is ready for use. + * Get a document's info and annotations from the RAG database. + * + * @param product the product the RAG database belongs to. + * @param documentId the document's ID. + */ + async getRagDocument

( + product: P, + documentId: string + ): Promise> { + logger.debug(`Getting RAG document ID: ${documentId}`); + return await this.mindeeApi.reqGetRagAnnotation(product, documentId); + } + + /** + * Get a document's info and annotations from the RAG database. + * + * @param product the product the RAG database belongs to. + * @param documentId the document's ID. + * @param pollingOptions options for the polling loop, see {@link PollingOptions}. + * @returns a `Promise` containing the RAG document annotation. + */ + async getReadyRagDocumentPoll

( + product: P, + documentId: string, + pollingOptions?: PollingOptions | PollingOptionsConstructor + ): Promise> { + const initialResponse = await this.getRagDocument(product, documentId); + if (initialResponse.status !== "Processing") { + return initialResponse; + } + const pollingOptionsInstance = pollingOptions instanceof PollingOptions + ? pollingOptions + : new PollingOptions(pollingOptions); + return await this.pollForRagDocument(product, initialResponse, pollingOptionsInstance); + } + + /** + * Not recommended for general use, prefer `uploadAndGetRagDocumentPoll`. + * You will need to poll until the document is ready for use. + * Add a document to the RAG database. + * + * @param product The product the RAG database belongs to. + * @param inputSource The file to upload. + * @param parameters The parameters to use for the upload. + */ + async uploadRagDocument

( + product: P, + inputSource: LocalInputSource, + parameters: InstanceType | ConstructorParameters[0] + ): Promise> { + logger.debug("Adding a document to the RAG database"); + const paramsInstance = parameters instanceof product.ragDocumentUploadClass + ? parameters + : new product.ragDocumentUploadClass(parameters); + await inputSource.init(); + return await this.mindeeApi.reqPostRagDocument(product, paramsInstance, inputSource); + } + + /** + * Add a document to the RAG database and return the initial annotation. + * + * @param product The product the RAG database belongs to. + * @param inputSource The file to upload. + * @param parameters The parameters to use for the upload. + * @param pollingOptions options for the polling loop, see {@link PollingOptions}. + */ + async uploadAndGetRagDocumentPoll

( + product: P, + inputSource: LocalInputSource, + parameters: InstanceType | ConstructorParameters[0], + pollingOptions?: PollingOptions | PollingOptionsConstructor + ): Promise> { + const initialResponse = await this.uploadRagDocument(product, inputSource, parameters); + if (initialResponse.status !== "Processing") { + return initialResponse; + } + + const pollingOptionsInstance = pollingOptions instanceof PollingOptions + ? pollingOptions + : new PollingOptions(pollingOptions); + return await this.pollForRagDocument(product, initialResponse, pollingOptionsInstance); + } + + /** + * Not recommended for general use, prefer `updateAndGetRagAnnotationPoll`. + * You will need to poll until the document is ready for use. + * Update a document's annotations in the RAG database. + * + * @param product The product the RAG database belongs to. + * @param parameters The parameters to use for the update. + */ + async updateRagAnnotation

( + product: P, + parameters: InstanceType | ConstructorParameters[0] + ): Promise> { + const paramsInstance = parameters instanceof product.annotationParametersClass + ? parameters + : new product.annotationParametersClass(parameters); + + logger.debug(`Updating RAG document ID: ${paramsInstance.documentId}`); + + return await this.mindeeApi.reqPatchRagAnnotation(product, paramsInstance); + } + + /** + * Update a document's annotations in the RAG database and poll until ready. + * + * @param product The product the RAG database belongs to. + * @param parameters The parameters to use for the update. + * @param pollingOptions options for the polling loop, see {@link PollingOptions}. + * @returns a `Promise` containing the RAG document annotation. + */ + async updateAndGetRagAnnotationPoll

( + product: P, + parameters: InstanceType | ConstructorParameters[0], + pollingOptions?: PollingOptions | PollingOptionsConstructor + ): Promise> { + const initialResponse = await this.updateRagAnnotation(product, parameters); + if (initialResponse.status !== "Processing") { + return initialResponse; + } + + const pollingOptionsInstance = pollingOptions instanceof PollingOptions + ? pollingOptions + : new PollingOptions(pollingOptions); + + return await this.pollForRagDocument(product, initialResponse, pollingOptionsInstance); + } + + /** + * Deletes a document from the RAG database. + * @param product the product the RAG database belongs to. + * @param documentId the document's ID. + */ + async deleteRagDocument

( + product: P, + documentId: string + ): Promise { + return await this.mindeeApi.reqDeleteRagDocument(product, documentId); + } + + /** + * Poll until the document is finished processing or the max number of attempts is reached. + * + * @param product The product the RAG database belongs to. + * @param initialResponse The initial response containing the document's ID. + * @param pollingOptions Options for the polling loop, see {@link PollingOptions}. + * @returns A `Promise` containing the RAG document annotation. + * @protected + */ + protected async pollForRagDocument

( + product: P, + initialResponse: InstanceType, + pollingOptions: PollingOptions + ): Promise> { + logger.debug(`Polling for RAG document ID: ${initialResponse.id}`); + const maxRetries = pollingOptions.maxRetries + 1; + + logger.debug( + `Waiting ${pollingOptions.initialDelaySec} seconds before attempting to retrieve the result...` + ); + await setTimeout( + pollingOptions.initialDelaySec * 1000, + undefined, + pollingOptions.initialTimerOptions + ); + + const documentId = initialResponse.id; + let retryCount = 1; + + while (retryCount < maxRetries) { + logger.debug( + `Poll attempt ${retryCount} of ${pollingOptions.maxRetries}` + ); + + const response = await this.getRagDocument(product, documentId); + + retryCount++; + + switch (response.status) { + case "Processing": + await setTimeout( + pollingOptions.delaySec * 1000, + undefined, + pollingOptions.recurringTimerOptions + ); + continue; + case "Failed": + throw new MindeeError("RAG document failed without an error payload."); + default: + return response; + } + } + + throw new MindeeError(`RAG polling not complete after ${retryCount} attempts.`); + } } diff --git a/src/v2/clientOptions/baseAnnotationParameters.ts b/src/v2/clientOptions/baseAnnotationParameters.ts new file mode 100644 index 00000000..e5a83a48 --- /dev/null +++ b/src/v2/clientOptions/baseAnnotationParameters.ts @@ -0,0 +1,35 @@ +import { MindeeConfigurationError } from "@/errors/index.js"; + +/** + * Constructor parameters for BaseAnnotationParameters and its subclasses. + */ +export interface BaseAnnotationParametersConstructor { + documentId: string; +} + +/** + * Base parameters for document annotations. + */ +export abstract class BaseAnnotationParameters { + /** + * UUID of the annotated document. + */ + public readonly documentId: string; + + /** + * Default constructor. + */ + protected constructor(params: BaseAnnotationParametersConstructor) { + const documentId = params.documentId?.trim(); + if (!documentId) { + throw new MindeeConfigurationError("Document ID must be provided"); + } + // Note: documentId is included in the request URL path, it is not a parameter. + this.documentId = documentId; + } + + /** + * Gets the request parameters for the upload request. + */ + public abstract getRequestParameters(): Record; +} diff --git a/src/v2/clientOptions/baseProductParameters.ts b/src/v2/clientOptions/baseProductParameters.ts index 96c205e3..f2a243b6 100644 --- a/src/v2/clientOptions/baseProductParameters.ts +++ b/src/v2/clientOptions/baseProductParameters.ts @@ -36,10 +36,11 @@ export abstract class BaseProductParameters { closeFile?: boolean; protected constructor(params: BaseProductParametersConstructor) { - if (params.modelId === undefined || params.modelId === null || params.modelId === "") { + const modelId = params.modelId?.trim(); + if (!modelId) { throw new MindeeConfigurationError("Model ID must be provided"); } - this.modelId = params.modelId; + this.modelId = modelId; this.alias = params.alias; this.webhookIds = params.webhookIds; this.closeFile = params.closeFile; diff --git a/src/v2/clientOptions/baseRagDocumentUploadParameters.ts b/src/v2/clientOptions/baseRagDocumentUploadParameters.ts new file mode 100644 index 00000000..abef231a --- /dev/null +++ b/src/v2/clientOptions/baseRagDocumentUploadParameters.ts @@ -0,0 +1,38 @@ +import { MindeeConfigurationError } from "@/errors/index.js"; + +/** + * Constructor parameters for BaseRagDocumentUploadParameters and its subclasses. + */ +export interface BaseRagDocumentUploadParametersConstructor { + modelId: string; +} + +/** + * Base parameters for document upload operations. + */ +export abstract class BaseRagDocumentUploadParameters { + /** + * UUID of the model that the uploaded RAG document is linked to. + */ + public readonly modelId: string; + + /** + * Default constructor. + */ + protected constructor(params: BaseRagDocumentUploadParametersConstructor) { + const modelId = params.modelId?.trim(); + if (!modelId) { + throw new MindeeConfigurationError("Model ID must be provided"); + } + this.modelId = modelId; + } + + /** + * Gets the request parameters for the upload request. + */ + public getRequestParameters(): Record { + const parameters: Record = {}; + parameters["model_id"] = this.modelId; + return parameters; + } +} diff --git a/src/v2/clientOptions/index.ts b/src/v2/clientOptions/index.ts index 4637d0ae..55a1c52d 100644 --- a/src/v2/clientOptions/index.ts +++ b/src/v2/clientOptions/index.ts @@ -5,3 +5,5 @@ export type { } from "./pollingOptions.js"; export { BaseProductParameters } from "./baseProductParameters.js"; export { BaseSearchParameters } from "./baseSearchParameters.js"; +export { BaseRagDocumentUploadParameters } from "./baseRagDocumentUploadParameters.js"; +export { BaseAnnotationParameters } from "./baseAnnotationParameters.js"; diff --git a/src/v2/http/mindeeApiV2.ts b/src/v2/http/mindeeApiV2.ts index 00d2b848..b59bbe18 100644 --- a/src/v2/http/mindeeApiV2.ts +++ b/src/v2/http/mindeeApiV2.ts @@ -1,7 +1,9 @@ import { ApiSettings } from "./apiSettings.js"; import { Dispatcher } from "undici"; import { BaseProductParameters } from "@/v2/index.js"; -import { BaseSearchParameters } from "@/v2/clientOptions/baseSearchParameters.js"; +import { + BaseSearchParameters, BaseAnnotationParameters, BaseRagDocumentUploadParameters +} from "@/v2/clientOptions/index.js"; import { FormData } from "undici"; import { BaseResponse, @@ -49,12 +51,11 @@ export class MindeeApiV2 { } else { form.set("url", (inputSource as UrlInput).url); } - const path = `/v2/products/${product.slug}/enqueue`; const options: RequestOptions = { method: "POST", headers: this.settings.baseHeaders, hostname: this.settings.hostname, - path: path, + path: `/v2/products/${product.slug}/enqueue`, body: form, timeoutSecs: this.settings.timeoutSecs, }; @@ -160,6 +161,116 @@ export class MindeeApiV2 { return this.#processResponse(response, search.responseClass) as InstanceType; } + /** + * Get a document's info and annotations from the RAG database. + * @param product the product definition to use. + * @param documentId the document id to get. + * @returns a `Promise` containing an annotation response. + */ + async reqGetRagAnnotation

( + product: P, + documentId: string + ): Promise> { + const options: RequestOptions = { + method: "GET", + headers: this.settings.baseHeaders, + hostname: this.settings.hostname, + path: `/v2/products/${product.slug}/rag-documents/${documentId}`, + timeoutSecs: this.settings.timeoutSecs, + }; + const response: BaseHttpResponse = await sendRequestAndReadResponse(this.settings.dispatcher, options); + return this.#processResponse( + response, + product.annotationResponseClass + ) as InstanceType; + } + + /** + * Add a document to the RAG database. + * @param product the product definition to use. + * @param parameters the parameters to use. + * @param inputSource the file to upload. + * @returns a `Promise` containing an annotation response. + */ + async reqPostRagDocument

( + product: P, + parameters: BaseRagDocumentUploadParameters, + inputSource: LocalInputSource + ): Promise> { + const form = this.#paramsToFormData(parameters.getRequestParameters()); + form.set("file", new Blob([inputSource.fileObject]), inputSource.filename); + + const options: RequestOptions = { + method: "POST", + headers: this.settings.baseHeaders, + hostname: this.settings.hostname, + path: `/v2/products/${product.slug}/rag-documents`, + body: form, + timeoutSecs: this.settings.timeoutSecs, + }; + + const response: BaseHttpResponse = await sendRequestAndReadResponse(this.settings.dispatcher, options); + return this.#processResponse( + response, + product.annotationResponseClass + ) as InstanceType; + } + + /** + * Update a document's annotations in the RAG database. + * @param product the product definition to use. + * @param parameters the parameters to use. + * @returns a `Promise` containing an annotation response. + */ + async reqPatchRagAnnotation

( + product: P, + parameters: BaseAnnotationParameters + ): Promise> { + const options: RequestOptions = { + method: "PATCH", + headers: { + ...this.settings.baseHeaders, + // eslint-disable-next-line @typescript-eslint/naming-convention + "Content-Type": "application/json", + }, + hostname: this.settings.hostname, + path: `/v2/products/${product.slug}/rag-documents/${parameters.documentId}`, + body: JSON.stringify(parameters.getRequestParameters()), + timeoutSecs: this.settings.timeoutSecs, + }; + const response: BaseHttpResponse = await sendRequestAndReadResponse(this.settings.dispatcher, options); + return this.#processResponse( + response, + product.annotationResponseClass + ) as InstanceType; + } + + /** + * Deletes a document from the RAG database. + * @param product the product definition to use. + * @param documentId the document's ID. + * @returns true if the document was deleted successfully, false otherwise. + */ + async reqDeleteRagDocument

( + product: P, + documentId: string + ): Promise { + const options: RequestOptions = { + method: "DELETE", + headers: this.settings.baseHeaders, + hostname: this.settings.hostname, + path: `/v2/products/${product.slug}/rag-documents/${documentId}`, + timeoutSecs: this.settings.timeoutSecs, + }; + const response: BaseHttpResponse = await sendRequestAndReadResponse(this.settings.dispatcher, options); + return response.messageObj?.statusCode >= 200 && response.messageObj?.statusCode < 400; + } + + /** + * Transforms a set of parameters into a FormData object. + * @param params the parameters to transform. + * @private + */ #paramsToFormData(params: Record): FormData { const form = new FormData(); for (const [key, value] of Object.entries(params)) { diff --git a/src/v2/parsing/baseRagAnnotationResponse.ts b/src/v2/parsing/baseRagAnnotationResponse.ts new file mode 100644 index 00000000..d87cc837 --- /dev/null +++ b/src/v2/parsing/baseRagAnnotationResponse.ts @@ -0,0 +1,40 @@ +import { BaseResponse } from "./baseResponse.js"; +import { StringDict, parseDate } from "@/parsing/index.js"; + +/** + * Base class for all RAG document responses from the V2 API. + */ +export class BaseRagAnnotationResponse extends BaseResponse { + /** + * Unique identifier of the RAG document. + */ + id: string; + + /** + * Original filename of the uploaded document. + */ + filename: string; + + /** + * Date and time of the document creation. + */ + createdAt: Date; + + /** + * Current status of the RAG document. + */ + status: string; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.id = serverResponse["id"]; + this.filename = serverResponse["filename"]; + this.createdAt = parseDate(serverResponse["created_at"])!; + this.status = serverResponse["status"]; + } +} + +/** + * Constructor signature for typed v2 response classes. + */ +export type AnnotationResponseConstructor = new (serverResponse: StringDict) => T; diff --git a/src/v2/parsing/baseResponse.ts b/src/v2/parsing/baseResponse.ts index f9d9af36..10dc6506 100644 --- a/src/v2/parsing/baseResponse.ts +++ b/src/v2/parsing/baseResponse.ts @@ -1,7 +1,9 @@ import { StringDict } from "@/parsing/stringDict.js"; import { logger } from "@/logger.js"; -/** Base response contract for v2 product responses. */ +/** + * Base response contract for v2 product responses. + */ export abstract class BaseResponse { /** * Raw text representation of the API's response. @@ -25,5 +27,7 @@ export abstract class BaseResponse { } } -/** Constructor signature for typed v2 response classes. */ +/** + * Constructor signature for typed v2 response classes. + */ export type ResponseConstructor = new (serverResponse: StringDict) => T; diff --git a/src/v2/parsing/index.ts b/src/v2/parsing/index.ts index 97b584b7..743fc8e1 100644 --- a/src/v2/parsing/index.ts +++ b/src/v2/parsing/index.ts @@ -17,4 +17,6 @@ export { export { LocalResponse } from "./localResponse.js"; export { BaseResponse } from "./baseResponse.js"; export type { ResponseConstructor } from "./baseResponse.js"; +export { BaseRagAnnotationResponse } from "./baseRagAnnotationResponse.js"; +export type { AnnotationResponseConstructor } from "./baseRagAnnotationResponse.js"; export * as field from "./inference/field/index.js"; diff --git a/src/v2/parsing/inference/field/inferenceFields.ts b/src/v2/parsing/inference/field/inferenceFields.ts index 8f392ff3..8013e7af 100644 --- a/src/v2/parsing/inference/field/inferenceFields.ts +++ b/src/v2/parsing/inference/field/inferenceFields.ts @@ -17,7 +17,9 @@ export class InferenceFields extends Map; @@ -58,7 +58,7 @@ export class Job { if (serverResponse["error"]) { this.error = new ErrorResponse(serverResponse["error"]); } - this.createdAt = parseDate(serverResponse["created_at"]); + this.createdAt = parseDate(serverResponse["created_at"])!; if (!serverResponse["completed_at"]) { this.completedAt = undefined; } else { diff --git a/src/v2/product/baseProduct.ts b/src/v2/product/baseProduct.ts index f5f57041..7cdbb83d 100644 --- a/src/v2/product/baseProduct.ts +++ b/src/v2/product/baseProduct.ts @@ -1,5 +1,9 @@ import { BaseProductParameters } from "@/v2/index.js"; -import { ResponseConstructor } from "@/v2/parsing/index.js"; +import { + ResponseConstructor, AnnotationResponseConstructor +} from "@/v2/parsing/index.js"; +import { BaseAnnotationParameters } from "@/v2/clientOptions/index.js"; +import { BaseRagDocumentUploadParameters } from "@/v2/clientOptions/index.js"; /** * Base class for all V2 product definitions. @@ -7,18 +11,45 @@ import { ResponseConstructor } from "@/v2/parsing/index.js"; * Child classes are passed to the Client when making requests. */ export abstract class BaseProduct { - /** Parameter class accepted by this product. */ + /** + * API slug for this product. + */ + static get slug(): string { + throw new Error("Must define static slug property"); + } + + /** + * Parameter class accepted by this product. + */ static get parametersClass(): new (...args: any[]) => BaseProductParameters { - throw new Error("Must define static parameters property"); + throw new Error("Must define static parametersClass property"); } - /** Response class returned by this product. */ + /** + * Response class returned by this product. + */ static get responseClass(): ResponseConstructor { - throw new Error("Must define static response property"); + throw new Error("Must define static responseClass property"); } - /** API slug for this product. */ - static get slug(): string { - throw new Error("Must define static slug property"); + /** + * Annotation Response class returned by this product. + */ + static get annotationResponseClass(): AnnotationResponseConstructor { + throw new Error("Must define static annotationResponseClass property"); + } + + /** + * Annotation Parameters class for this product. + */ + static get annotationParametersClass(): new (...args: any[]) => BaseAnnotationParameters { + throw new Error("Must define static annotationParametersClass property"); + } + + /** + * Document Upload class for this product. + */ + static get ragDocumentUploadClass(): new (...args: any[]) => BaseRagDocumentUploadParameters { + throw new Error("Must define static ragDocumentUploadClass property"); } } diff --git a/src/v2/product/extraction/extraction.ts b/src/v2/product/extraction/extraction.ts index 44c5df7d..1dcf0281 100644 --- a/src/v2/product/extraction/extraction.ts +++ b/src/v2/product/extraction/extraction.ts @@ -1,11 +1,20 @@ import { ExtractionResponse } from "./extractionResponse.js"; import { ExtractionParameters } from "./params/index.js"; import { BaseProduct } from "@/v2/product/baseProduct.js"; +import { + ExtractionRagAnnotationResponse +} from "./ragDocuments/index.js"; +import { RagDocumentAnnotationParameters, RagDocumentUploadParameters } from "./ragDocuments/params/index.js"; /** * Automatically extract structured data from any image or scanned document. */ export class Extraction extends BaseProduct { + /** @inheritDoc */ + static get slug() { + return "extraction"; + } + /** @inheritDoc */ static get parametersClass() { return ExtractionParameters; @@ -17,7 +26,17 @@ export class Extraction extends BaseProduct { } /** @inheritDoc */ - static get slug() { - return "extraction"; + static get annotationResponseClass() { + return ExtractionRagAnnotationResponse; + } + + /** @inheritDoc */ + static get annotationParametersClass() { + return RagDocumentAnnotationParameters; + } + + /** @inheritDoc */ + static get ragDocumentUploadClass() { + return RagDocumentUploadParameters; } } diff --git a/src/v2/product/extraction/index.ts b/src/v2/product/extraction/index.ts index fd696008..538dcc10 100644 --- a/src/v2/product/extraction/index.ts +++ b/src/v2/product/extraction/index.ts @@ -6,3 +6,4 @@ export { ExtractionActiveOptions } from "./extractionActiveOptions.js"; export { ExtractionResponse } from "./extractionResponse.js"; export { ExtractionResult } from "./extractionResult.js"; export { DataSchemaActiveOption } from "./dataSchemaActiveOption.js"; +export * as ragDocuments from "./ragDocuments/index.js"; diff --git a/src/v2/product/extraction/ragDocuments/annotatedBaseField.ts b/src/v2/product/extraction/ragDocuments/annotatedBaseField.ts new file mode 100644 index 00000000..f45e47a6 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedBaseField.ts @@ -0,0 +1,25 @@ +/** + * Base class for annotated fields. + */ +export abstract class AnnotatedBaseField { + /** + * When true, use the RAG information for the final result. When false, use the Data Schema information. + */ + public selected: boolean; + + /** + * Guidelines or instructions for processing this field. + */ + public guidelines: string | null; + + /** + * Default constructor. + * @param selected When true, use the RAG information for the final result. + * When false, use the Data Schema information. + * @param guidelines Guidelines or instructions for processing this field. + */ + protected constructor(selected: boolean, guidelines: string | null = null) { + this.selected = selected; + this.guidelines = guidelines; + } +} diff --git a/src/v2/product/extraction/ragDocuments/annotatedFields.ts b/src/v2/product/extraction/ragDocuments/annotatedFields.ts new file mode 100644 index 00000000..830bb4ee --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedFields.ts @@ -0,0 +1,63 @@ +import { AnnotatedSimpleField } from "./annotatedSimpleField.js"; +import { AnnotatedObjectField } from "./annotatedObjectField.js"; +import { AnnotatedListField } from "./annotatedListField.js"; +import { StringDict } from "@/parsing/index.js"; +import { createAnnotatedField } from "@/v2/product/extraction/ragDocuments/fieldFactory.js"; + +export class AnnotatedFields extends Map { + constructor(serverResponse: StringDict) { + super(Object.entries(serverResponse).map( ([key, value]) => { + return [key, createAnnotatedField(value)]; + })); + } + + /** + * Returns a field as a `AnnotatedSimpleField`, or throws if the type mismatches. + */ + getSimpleField(fieldName: string): AnnotatedSimpleField { + const field = this.get(fieldName); + if (field === undefined) { + throw new Error(`The field '${fieldName}' was not found.`); + } + if (field.constructor.name !== "AnnotatedSimpleField") { + throw new Error(`The field '${fieldName}' is not a AnnotatedSimpleField.`); + } + return field as AnnotatedSimpleField; + } + + /** + * Returns a field as an `AnnotatedObjectField`, or throws if the type mismatches. + */ + getObjectField(fieldName: string): AnnotatedObjectField { + const field = this.get(fieldName); + if (field === undefined) { + throw new Error(`The field '${fieldName}' was not found.`); + } + if (field.constructor.name !== "AnnotatedObjectField") { + throw new Error(`The field '${fieldName}' is not an ObjectField.`); + } + return field as AnnotatedObjectField; + } + + /** + * Returns a field as a `AnnotatedListField`, or throws if the type mismatches. + */ + getListField(fieldName: string): AnnotatedListField { + const field = this.get(fieldName); + if (field === undefined) { + throw new Error(`The field '${fieldName}' was not found.`); + } + if (field.constructor.name !== "AnnotatedListField") { + throw new Error(`The field '${fieldName}' is not a ListField.`); + } + return field as AnnotatedListField; + } + + /** + * Serializes the fields to API format. + * Needed because a `Map` is otherwise serialized as an empty object by `JSON.stringify`. + */ + toJSON(): Record { + return Object.fromEntries(this); + } +} diff --git a/src/v2/product/extraction/ragDocuments/annotatedListField.ts b/src/v2/product/extraction/ragDocuments/annotatedListField.ts new file mode 100644 index 00000000..59b8a610 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedListField.ts @@ -0,0 +1,83 @@ +import { MindeeDeserializationError } from "@/errors/index.js"; +import { StringDict } from "@/parsing/stringDict.js"; +import { AnnotatedBaseField } from "./annotatedBaseField.js"; +import { AnnotatedObjectField } from "./annotatedObjectField.js"; +import { AnnotatedSimpleField } from "./annotatedSimpleField.js"; +import { createAnnotatedField } from "./fieldFactory.js"; + +/** List-valued inference field. */ +export class AnnotatedListField extends AnnotatedBaseField { + /** + * Items contained in the list. + */ + public items: Array; + + constructor(serverResponse: StringDict) { + super(serverResponse["selected"], serverResponse["guidelines"]); + + if (!Array.isArray(serverResponse["items"])) { + throw new MindeeDeserializationError( + `Expected "items" to be an array in ${JSON.stringify(serverResponse)}.` + ); + } + this.items = serverResponse["items"].map((item) => { + return createAnnotatedField(item); + }); + } + + /** + * AnnotatedSimpleField items from the list. + */ + public get simpleItems(): Array { + const result: Array = []; + + for (const item of this.items) { + if (item instanceof AnnotatedSimpleField) { + result.push(item); + } else { + throw new MindeeDeserializationError( + `All items must be AnnotatedSimpleField, found item of type ${item.constructor.name}.` + ); + } + } + return result; + } + + /** + * AnnotatedObjectField items from the list. + */ + public get objectItems(): Array { + const result: Array = []; + + for (const item of this.items) { + if (item instanceof AnnotatedObjectField) { + result.push(item); + } else { + throw new MindeeDeserializationError( + `All items must be AnnotatedObjectField, found item of type ${item.constructor.name}.` + ); + } + } + return result; + } + + /** Returns a readable representation of list items. */ + toString(): string { + if (!this.items || this.items.length === 0) { + return "\n"; + } + + const parts: string[] = [""]; + for (const item of this.items) { + if (!item) continue; + + if (item instanceof AnnotatedObjectField) { + parts.push(item.toStringFromList()); + } else { + parts.push(item.toString()); + } + } + return parts.join("\n * "); + } + +} diff --git a/src/v2/product/extraction/ragDocuments/annotatedObjectField.ts b/src/v2/product/extraction/ragDocuments/annotatedObjectField.ts new file mode 100644 index 00000000..8f16a1e5 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedObjectField.ts @@ -0,0 +1,56 @@ +import { AnnotatedBaseField } from "./annotatedBaseField.js"; +import { StringDict } from "@/parsing/index.js"; +import { AnnotatedFields } from "./annotatedFields.js"; +import { AnnotatedSimpleField } from "@/v2/product/extraction/ragDocuments/annotatedSimpleField.js"; +import { AnnotatedListField } from "@/v2/product/extraction/ragDocuments/annotatedListField.js"; + +export class AnnotatedObjectField extends AnnotatedBaseField { + /** Nested fields carried by this object. */ + readonly fields: AnnotatedFields; + + constructor(serverResponse: StringDict) { + super(serverResponse["selected"], serverResponse["guidelines"]); + + this.fields = new AnnotatedFields(serverResponse["fields"]); + } + + /** + * Retrieves a AnnotatedSimpleField by its name if it exists and is of the correct type. + * + * @param {string} fieldName - The name of the field to retrieve. + * @return {AnnotatedSimpleField} The field instance if it exists and is valid, or undefined if not. + * @throws {Error} If the field does not exist or is of wrong type. + */ + public getSimpleField(fieldName: string): AnnotatedSimpleField { + return this.fields.getSimpleField(fieldName); + } + + /** + * Retrieves a AnnotatedListField by its name if it exists and is of the correct type. + * + * @param {string} fieldName - The name of the field to retrieve. + * @return {AnnotatedListField} The field instance if it exists and is valid, or undefined if not. + * @throws {Error} If the field does not exist or is of wrong type. + */ + public getListField(fieldName: string): AnnotatedListField { + return this.fields.getListField(fieldName); + } + + /** + * Retrieves an AnnotatedObjectField by its name if it exists and is of the correct type. + * + * @param {string} fieldName - The name of the field to retrieve. + * @return {AnnotatedObjectField} The field instance if it exists and is valid, or undefined if not. + * @throws {Error} If the field does not exist or is of wrong type. + */ + public getObjectField(fieldName: string): AnnotatedObjectField { + return this.fields.getObjectField(fieldName); + } + + /** + * Returns a compact representation suitable for list items. + */ + toStringFromList(): string{ + return this.fields? this.fields.toString().substring(4) : ""; + } +} diff --git a/src/v2/product/extraction/ragDocuments/annotatedSimpleField.ts b/src/v2/product/extraction/ragDocuments/annotatedSimpleField.ts new file mode 100644 index 00000000..20aaa996 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedSimpleField.ts @@ -0,0 +1,20 @@ +import { AnnotatedBaseField } from "./annotatedBaseField.js"; +import { StringDict } from "@/parsing/index.js"; + +/** + * A SimpleField with additional configuration for annotation. + */ +export class AnnotatedSimpleField extends AnnotatedBaseField { + /** + * Field value, one of: string, boolean, number, null. + */ + public value: string | boolean | number | null; + + /** + * Default constructor. + */ + constructor(serverResponse: StringDict) { + super(serverResponse["selected"], serverResponse["guidelines"]); + this.value = serverResponse["value"]; + } +} diff --git a/src/v2/product/extraction/ragDocuments/extractionRagAnnotationResponse.ts b/src/v2/product/extraction/ragDocuments/extractionRagAnnotationResponse.ts new file mode 100644 index 00000000..4dada417 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/extractionRagAnnotationResponse.ts @@ -0,0 +1,39 @@ +import { BaseRagAnnotationResponse } from "@/v2/parsing/baseRagAnnotationResponse.js"; +import { RagAnnotation } from "./ragAnnotation.js"; +import { parseDate, StringDict } from "@/parsing/index.js"; + +/** + * Response for a RAG document. + */ +export class ExtractionRagAnnotationResponse extends BaseRagAnnotationResponse { + /** + * Model identifier linked to the RAG document. + */ + public modelId: string; + + /** + * Number of times this document was used in an inference. + */ + public totalMatches: number; + + /** + * Date and time of the latest matching inference, if any. + */ + public lastMatchAt: Date | null; + + /** + * Annotation metadata associated with the document. + */ + public annotation: RagAnnotation | null; + + constructor(serverResponse: StringDict) { + super(serverResponse); + + this.modelId = serverResponse["model_id"]; + this.totalMatches = serverResponse["total_matches"]; + this.lastMatchAt = parseDate(serverResponse["last_match_at"]); + this.annotation = serverResponse["annotation"] + ? new RagAnnotation(serverResponse["annotation"]) + : null; + } +} diff --git a/src/v2/product/extraction/ragDocuments/fieldFactory.ts b/src/v2/product/extraction/ragDocuments/fieldFactory.ts new file mode 100644 index 00000000..510555b2 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/fieldFactory.ts @@ -0,0 +1,36 @@ +/** + * Factory helper. + */ +import { StringDict } from "@/parsing/stringDict.js"; +import { MindeeDeserializationError } from "@/errors/index.js"; +import { AnnotatedListField } from "./annotatedListField.js"; +import { AnnotatedObjectField } from "./annotatedObjectField.js"; +import { AnnotatedSimpleField } from "./annotatedSimpleField.js"; + +/** + * Create an annotated field from a server response. + * @param serverResponse Server response. + */ +export function createAnnotatedField(serverResponse: StringDict) { + if (typeof serverResponse !== "object" || serverResponse === null) { + throw new MindeeDeserializationError( + `Unrecognized field format ${JSON.stringify(serverResponse)}.` + ); + } + + if ("items" in serverResponse) { + return new AnnotatedListField(serverResponse); + } + + if ("fields" in serverResponse) { + return new AnnotatedObjectField(serverResponse); + } + + if ("value" in serverResponse) { + return new AnnotatedSimpleField(serverResponse); + } + + throw new MindeeDeserializationError( + `Unrecognized field format in ${JSON.stringify(serverResponse)}.` + ); +} diff --git a/src/v2/product/extraction/ragDocuments/index.ts b/src/v2/product/extraction/ragDocuments/index.ts new file mode 100644 index 00000000..959886df --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/index.ts @@ -0,0 +1,7 @@ +export { ExtractionRagAnnotationResponse } from "./extractionRagAnnotationResponse.js"; +export { RagAnnotation } from "./ragAnnotation.js"; +export { AnnotatedFields } from "./annotatedFields.js"; +export { AnnotatedSimpleField } from "./annotatedSimpleField.js"; +export { AnnotatedListField } from "./annotatedListField.js"; +export { AnnotatedObjectField } from "./annotatedObjectField.js"; +export * as params from "./params/index.js"; diff --git a/src/v2/product/extraction/ragDocuments/params/index.ts b/src/v2/product/extraction/ragDocuments/params/index.ts new file mode 100644 index 00000000..60ff6e0a --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/params/index.ts @@ -0,0 +1,2 @@ +export { RagDocumentUploadParameters } from "./ragDocumentUploadParameters.js"; +export { RagDocumentAnnotationParameters } from "./ragDocumentAnnotationParameters.js"; diff --git a/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts b/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts new file mode 100644 index 00000000..552e9836 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts @@ -0,0 +1,63 @@ +import { BaseAnnotationParameters } from "@/v2/clientOptions/baseAnnotationParameters.js"; +import { RagAnnotation } from "@/v2/product/extraction/ragDocuments/ragAnnotation.js"; +import { BaseAnnotationParametersConstructor } from "@/v2/clientOptions/baseAnnotationParameters.js"; + +/** + * Annotation parameters for RAG documents. + */ +export class RagDocumentAnnotationParameters extends BaseAnnotationParameters { + /** + * New public status to apply to the document (for example, to deactivate it). + */ + public readonly status?: string; + + /** + * Field-level RAG annotation and guidelines configuration for the document. + */ + public readonly annotation?: RagAnnotation | null; + + /** + * Default constructor. + */ + constructor( + params: BaseAnnotationParametersConstructor & + { + status?: string, + annotation?: RagAnnotation | string | Record | null + } + ) { + super({ ...params }); + + this.status = params.status; + + if (params.annotation === null || params.annotation === undefined) { + this.annotation = null; + } else if (params.annotation instanceof RagAnnotation) { + this.annotation = params.annotation; + } else if (typeof params.annotation === "string") { + const parsedJson = JSON.parse(params.annotation); + this.annotation = new RagAnnotation(parsedJson); + } else if (typeof params.annotation === "object" && !Array.isArray(params.annotation)) { + this.annotation = new RagAnnotation(params.annotation); + } else { + throw new Error("Invalid RAG Annotation format."); + } + } + + /** + * Gets the request parameters for the upload request. + */ + public getRequestParameters(): Record { + const parameters: Record = {}; + + if (this.status) { + parameters["status"] = this.status; + } + + if (this.annotation) { + parameters["annotation"] = this.annotation; + } + + return parameters; + } +} diff --git a/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts b/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts new file mode 100644 index 00000000..68dbb676 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts @@ -0,0 +1,11 @@ +import { BaseRagDocumentUploadParameters, BaseRagDocumentUploadParametersConstructor +} from "@/v2/clientOptions/baseRagDocumentUploadParameters.js"; + +/** + * Parameters for uploading a file to an extraction RAG database. + */ +export class RagDocumentUploadParameters extends BaseRagDocumentUploadParameters { + constructor(params: BaseRagDocumentUploadParametersConstructor & {}) { + super({ ...params }); + } +} diff --git a/src/v2/product/extraction/ragDocuments/ragAnnotation.ts b/src/v2/product/extraction/ragDocuments/ragAnnotation.ts new file mode 100644 index 00000000..144e1966 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/ragAnnotation.ts @@ -0,0 +1,16 @@ +import { AnnotatedFields } from "./annotatedFields.js"; +import { StringDict } from "@/parsing/index.js"; + +/** + * A RAG annotation enriched with field-level configuration. + */ +export class RagAnnotation { + /** + * Annotated fields. + */ + public fields: AnnotatedFields; + + constructor(serverResponse: StringDict) { + this.fields = new AnnotatedFields(serverResponse["fields"] ?? {}); + } +} diff --git a/tests/v2/product/extraction.spec.ts b/tests/v2/product/extraction/extraction.spec.ts similarity index 99% rename from tests/v2/product/extraction.spec.ts rename to tests/v2/product/extraction/extraction.spec.ts index d3075188..3c52479e 100644 --- a/tests/v2/product/extraction.spec.ts +++ b/tests/v2/product/extraction/extraction.spec.ts @@ -7,8 +7,8 @@ import { promises as fs } from "node:fs"; import { describe, it } from "node:test"; import path from "path"; -import { V2_PRODUCT_PATH } from "../../index.js"; -import { loadV2Response } from "./utils.js"; +import { V2_PRODUCT_PATH } from "../../../index.js"; +import { loadV2Response } from "../utils.js"; const findocPath = path.join(V2_PRODUCT_PATH, "extraction", "financial_document"); const extractionPath = path.join(V2_PRODUCT_PATH, "extraction"); diff --git a/tests/v2/product/extractionParameter.spec.ts b/tests/v2/product/extraction/extractionParameter.spec.ts similarity index 97% rename from tests/v2/product/extractionParameter.spec.ts rename to tests/v2/product/extraction/extractionParameter.spec.ts index 84427a28..dcc8a01a 100644 --- a/tests/v2/product/extractionParameter.spec.ts +++ b/tests/v2/product/extraction/extractionParameter.spec.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { before, describe, it } from "node:test"; import { promises as fs } from "fs"; import { StringDict } from "@/parsing/index.js"; -import { V2_PRODUCT_PATH } from "../../index.js"; +import { V2_PRODUCT_PATH } from "../../../index.js"; import { extraction } from "@/v2/product/index.js"; let expectedDataSchemaDict: StringDict; diff --git a/tests/v2/product/extraction/ragDocuments.integration.ts b/tests/v2/product/extraction/ragDocuments.integration.ts new file mode 100644 index 00000000..06dede4d --- /dev/null +++ b/tests/v2/product/extraction/ragDocuments.integration.ts @@ -0,0 +1,103 @@ +import path from "path"; +import assert from "node:assert/strict"; +import { describe, it, beforeEach } from "node:test"; +import { V2_PRODUCT_PATH } from "../../../index.js"; +import { Client, PathInput } from "@/index.js"; +import { Extraction } from "@/v2/product/index.js"; + + +describe("MindeeV2 - Extraction RagDocuments", { timeout: 180000 }, () => { + let client: Client; + let extractionModelId: string; + + beforeEach(() => { + const apiKey = process.env["MINDEE_V2_API_KEY"] ?? ""; + extractionModelId = process.env["MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID"] ?? ""; + if (!extractionModelId) { + throw new Error("Missing MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID environment variable."); + } + + client = new Client({ apiKey: apiKey, debug: true }); + }); + + it("should perform the entire lifecycle of a RAG document", async () => { + const inputSource = new PathInput( + { inputPath: path.join(V2_PRODUCT_PATH, "extraction/financial_document/default_sample.jpg") } + ); + + const postResponse = await client.uploadAndGetRagDocumentPoll( + Extraction, + inputSource, + { modelId: extractionModelId } + ); + assert.ok(postResponse); + + const postAnnotation = postResponse.annotation; + assert.ok(postAnnotation?.fields); + + const documentId = postResponse.id; + assert.ok(documentId); + + assert.equal(postResponse.status, "Draft"); + + postAnnotation.fields.getSimpleField("supplier_name").selected = true; + postAnnotation.fields.getSimpleField("supplier_name").guidelines = "I am the walrus!"; + postAnnotation.fields.getSimpleField("invoice_number").selected = true; + postAnnotation.fields.getSimpleField("invoice_number").guidelines = "koo koo katchoo!"; + + const patchAnnotationResponse = await client.updateRagAnnotation( + Extraction, + { documentId: documentId, annotation: postAnnotation } + ); + assert.ok(patchAnnotationResponse); + const patchAnnotation = patchAnnotationResponse.annotation; + assert.equal( + patchAnnotation!.fields.getSimpleField("supplier_name").guidelines, "I am the walrus!" + ); + assert.equal( + patchAnnotation!.fields.getSimpleField("supplier_name").selected, true + ); + assert.equal( + patchAnnotation!.fields.getSimpleField("invoice_number").guidelines, "koo koo katchoo!" + ); + assert.equal( + patchAnnotation!.fields.getSimpleField("invoice_number").selected, true + ); + + const getResponse = await client.getReadyRagDocumentPoll( + Extraction, + documentId + ); + assert.ok(getResponse); + const getAnnotation = getResponse.annotation; + assert.ok(getAnnotation); + assert.equal(getResponse.status, "Draft"); + assert.equal( + getAnnotation.fields.getSimpleField("supplier_name").guidelines, "I am the walrus!" + ); + assert.equal( + getAnnotation.fields.getSimpleField("supplier_name").selected, true + ); + assert.equal( + getAnnotation.fields.getSimpleField("invoice_number").guidelines, "koo koo katchoo!" + ); + assert.equal( + getAnnotation.fields.getSimpleField("invoice_number").selected, true + ); + + const patchStatusResponse = await client.updateAndGetRagAnnotationPoll( + Extraction, + { documentId: documentId, status: "Active" }, + { initialDelaySec: 1.2 } + ); + assert.ok(patchStatusResponse); + assert.equal(patchStatusResponse.status, "Active"); + + const deleteResponse = await client.deleteRagDocument(Extraction, documentId); + assert.ok(deleteResponse); + + await assert.rejects(async () => { + await client.getRagDocument(Extraction, documentId); + }); + }); +}); diff --git a/tests/v2/product/extraction/ragDocuments.spec.ts b/tests/v2/product/extraction/ragDocuments.spec.ts new file mode 100644 index 00000000..ed76ea2d --- /dev/null +++ b/tests/v2/product/extraction/ragDocuments.spec.ts @@ -0,0 +1,157 @@ +import path from "path"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { promises as fs } from "fs"; +import { V2_PRODUCT_PATH } from "../../../index.js"; +import { + ExtractionRagAnnotationResponse, RagAnnotation +} from "@/v2/product/extraction/ragDocuments/index.js"; +import { + RagDocumentUploadParameters, RagDocumentAnnotationParameters +} from "@/v2/product/extraction/ragDocuments/params/index.js"; + + +describe("MindeeV2 - Extraction RagDocuments", () => { + + /** + * Init a response from a JSON file. + */ + async function getResponse(relativePath: string) { + const fileContents = await fs.readFile( + path.join(V2_PRODUCT_PATH, relativePath) + ); + const dict = JSON.parse(fileContents.toString()); + return new ExtractionRagAnnotationResponse(dict); + } + + it("should init POST parameters", () => { + const parameters = new RagDocumentUploadParameters({ modelId: "invalid-model-id" }); + const reqParams = parameters.getRequestParameters(); + + assert.strictEqual(reqParams["model_id"], "invalid-model-id"); + }); + + it("should init PATCH parameters", () => { + const annotation = new RagAnnotation({ + fields: { + "hello": { + "selected": true, + "guidelines": null, + "value": null + } + } + }); + const parameters = new RagDocumentAnnotationParameters({ + documentId: "invalid-document-id", + status: "Active", + annotation: annotation + }); + + assert.strictEqual(parameters.documentId, "invalid-document-id"); + // server expects JSON string + assert.strictEqual( + JSON.stringify(parameters.getRequestParameters()), + '{"status":"Active","annotation":{"fields":{"hello":{"selected":true,"guidelines":null,"value":null}}}}' + ); + }); + + it("should load a POST response from a JSON string", async () => { + const response = await getResponse("extraction/rag_documents/post_response.json"); + + assert.ok(response); + assert.strictEqual(response.id, "cc831599-c545-48b7-aa27-6d7ccd5b8d32"); + assert.strictEqual(response.status, "Processing"); + assert.strictEqual(response.annotation, null); + }); + + it("should load a GET response from a JSON string", async () => { + const response = await getResponse("extraction/rag_documents/get_response_draft.json"); + + assert.ok(response); + assert.strictEqual(response.id, "cc831599-c545-48b7-aa27-6d7ccd5b8d32"); + assert.strictEqual(response.status, "Draft"); + assert.ok(response.annotation); + + const fields = response.annotation.fields; + assert.ok(fields); + + // null simple field + const tipField = fields.getSimpleField("tip"); + assert.ok(tipField); + assert.strictEqual(tipField.selected, false); + assert.strictEqual(tipField.guidelines, null); + assert.strictEqual(tipField.value, null); + + // filled simple field + const dateField = fields.getSimpleField("date"); + assert.ok(dateField); + assert.strictEqual(dateField.selected, false); + assert.strictEqual(dateField.guidelines, null); + assert.strictEqual(dateField.value, "2019-11-02"); + + // filled object field + const localeField = fields.getObjectField("locale"); + assert.ok(localeField); + assert.strictEqual(localeField.selected, false); + assert.strictEqual(localeField.guidelines, null); + assert.ok(localeField.fields); + assert.strictEqual(localeField.fields.size, 3); + assert.strictEqual(localeField.getSimpleField("country").value, "US"); + assert.strictEqual(localeField.getSimpleField("currency").value, "USD"); + assert.strictEqual(localeField.getSimpleField("language").value, null); + + // list of simple fields + const referenceNumbersField = fields.getListField("reference_numbers"); + assert.ok(referenceNumbersField); + assert.strictEqual(referenceNumbersField.selected, false); + assert.strictEqual(referenceNumbersField.guidelines, null); + assert.ok(referenceNumbersField.simpleItems); + assert.strictEqual(referenceNumbersField.simpleItems.length, 1); + assert.strictEqual(referenceNumbersField.simpleItems[0].value, "2412/2019"); + + // list of object fields + const lineItemsField = fields.getListField("line_items"); + assert.ok(lineItemsField); + assert.strictEqual(lineItemsField.selected, false); + assert.strictEqual(lineItemsField.guidelines, null); + assert.ok(lineItemsField.objectItems); + assert.strictEqual(lineItemsField.objectItems.length, 3); + + const lineItem0 = lineItemsField.objectItems[0]; + assert.ok(lineItem0.fields); + assert.strictEqual(lineItem0.fields.size, 8); + assert.strictEqual(lineItem0.getSimpleField("description").value, "Front and rear brake cables"); + assert.strictEqual(lineItem0.getSimpleField("quantity").value, 1); + assert.strictEqual(lineItem0.getSimpleField("unit_price").value, 100); + assert.strictEqual(lineItem0.getSimpleField("total_price").value, 100); + assert.strictEqual(lineItem0.getSimpleField("tax_rate").value, null); + assert.strictEqual(lineItem0.getSimpleField("tax_amount").value, null); + assert.strictEqual(lineItem0.getSimpleField("product_code").value, null); + assert.strictEqual(lineItem0.getSimpleField("unit_measure").value, null); + + const lineItem1 = lineItemsField.objectItems[1]; + assert.ok(lineItem1.fields); + assert.strictEqual(lineItem1.fields.size, 8); + assert.strictEqual(lineItem1.getSimpleField("description").value, "New set of pedal arms"); + assert.strictEqual(lineItem1.getSimpleField("quantity").value, 2); + assert.strictEqual(lineItem1.getSimpleField("unit_price").value, 25); + assert.strictEqual(lineItem1.getSimpleField("total_price").value, 50); + assert.strictEqual(lineItem1.getSimpleField("tax_rate").value, null); + assert.strictEqual(lineItem1.getSimpleField("tax_amount").value, null); + assert.strictEqual(lineItem1.getSimpleField("product_code").value, null); + assert.strictEqual(lineItem1.getSimpleField("unit_measure").value, null); + + const lineItem2 = lineItemsField.objectItems[2]; + assert.ok(lineItem2.fields); + assert.strictEqual(lineItem2.fields.size, 8); + assert.strictEqual(lineItem2.getSimpleField("description").value, "Labor 3hrs"); + assert.strictEqual(lineItem2.getSimpleField("quantity").value, 3); + assert.strictEqual(lineItem2.getSimpleField("unit_price").value, 15); + assert.strictEqual(lineItem2.getSimpleField("total_price").value, 45); + assert.strictEqual(lineItem2.getSimpleField("tax_rate").value, null); + assert.strictEqual(lineItem2.getSimpleField("tax_amount").value, null); + assert.strictEqual(lineItem2.getSimpleField("product_code").value, null); + assert.strictEqual(lineItem2.getSimpleField("unit_measure").value, null); + }); +}); + diff --git a/tests/v2/search/ragDocumentSearch.integration.ts b/tests/v2/search/ragDocumentSearch.integration.ts index 585b14a6..cc210860 100644 --- a/tests/v2/search/ragDocumentSearch.integration.ts +++ b/tests/v2/search/ragDocumentSearch.integration.ts @@ -12,6 +12,9 @@ describe("MindeeV2 - Integration - RAG Document Search", { timeout: 120000 }, () beforeEach(() => { const apiKey = process.env["MINDEE_V2_API_KEY"] ?? ""; findocModelId = process.env["MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID"] ?? ""; + if (!findocModelId) { + throw new Error("Missing MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID environment variable."); + } client = new Client({ apiKey: apiKey, debug: true }); }); @@ -20,8 +23,25 @@ describe("MindeeV2 - Integration - RAG Document Search", { timeout: 120000 }, () const response: RagDocumentSearchResponse = await client.search(RagDocumentSearch, { modelId: findocModelId }); assert.ok(response); assert.ok(response.ragDocuments.length > 0); + for (const ragDoc of response.ragDocuments) { + assert.ok(ragDoc.id); + assert.ok(ragDoc.createdAt); + assert.ok(ragDoc.filename); + assert.ok(ragDoc.totalMatches >= 0); + } assert.ok(response.pagination); assert.ok(response.pagination.totalItems >= 1); assert.equal(response.pagination.page, 1); }); + + it("RAG Document search must return empty", async () => { + const response: RagDocumentSearchResponse = await client.search( + RagDocumentSearch, { modelId: findocModelId, filename: "invoice_32GB-RAM_450k-USD.pdf" } + ); + assert.ok(response); + assert.equal(response.ragDocuments.length, 0); + assert.ok(response.pagination); + assert.equal(response.pagination.totalItems, 0); + assert.equal(response.pagination.page, 1); + }); });