From f725d5b117270534d9d4682c52a9a53311c900e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ianar=C3=A9=20s=C3=A9vi?= Date: Tue, 15 Sep 2026 19:24:17 +0200 Subject: [PATCH 1/3] :sparkles: add RAG document CRUD --- src/v2/client.ts | 68 ++++---- .../clientOptions/baseAnnotationParameters.ts | 27 ++++ .../baseRagDocumentUploadParameters.ts | 29 ++++ src/v2/clientOptions/index.ts | 2 + src/v2/http/mindeeApiV2.ts | 111 ++++++++++++- src/v2/parsing/baseRagAnnotationResponse.ts | 40 +++++ src/v2/parsing/baseResponse.ts | 8 +- src/v2/parsing/index.ts | 2 + .../inference/field/inferenceFields.ts | 12 +- src/v2/parsing/inference/field/objectField.ts | 57 +++---- src/v2/parsing/job/job.ts | 16 +- src/v2/product/baseProduct.ts | 47 +++++- src/v2/product/extraction/extraction.ts | 23 ++- .../ragDocuments/annotatedBaseField.ts | 25 +++ .../ragDocuments/annotatedFields.ts | 55 +++++++ .../ragDocuments/annotatedListField.ts | 83 ++++++++++ .../ragDocuments/annotatedObjectField.ts | 58 +++++++ .../ragDocuments/annotatedSimpleField.ts | 27 ++++ .../extractionRagAnnotationResponse.ts | 39 +++++ .../extraction/ragDocuments/fieldFactory.ts | 40 +++++ .../product/extraction/ragDocuments/index.ts | 7 + .../extraction/ragDocuments/params/index.ts | 2 + .../params/ragDocumentAnnotationParameters.ts | 63 ++++++++ .../params/ragDocumentUploadParameters.ts | 12 ++ .../extraction/ragDocuments/ragAnnotation.ts | 18 +++ .../{ => extraction}/extraction.spec.ts | 4 +- .../extractionParameter.spec.ts | 2 +- .../v2/product/extraction/ragDocumentsTest.ts | 152 ++++++++++++++++++ .../search/ragDocumentSearch.integration.ts | 20 +++ 29 files changed, 954 insertions(+), 95 deletions(-) create mode 100644 src/v2/clientOptions/baseAnnotationParameters.ts create mode 100644 src/v2/clientOptions/baseRagDocumentUploadParameters.ts create mode 100644 src/v2/parsing/baseRagAnnotationResponse.ts create mode 100644 src/v2/product/extraction/ragDocuments/annotatedBaseField.ts create mode 100644 src/v2/product/extraction/ragDocuments/annotatedFields.ts create mode 100644 src/v2/product/extraction/ragDocuments/annotatedListField.ts create mode 100644 src/v2/product/extraction/ragDocuments/annotatedObjectField.ts create mode 100644 src/v2/product/extraction/ragDocuments/annotatedSimpleField.ts create mode 100644 src/v2/product/extraction/ragDocuments/extractionRagAnnotationResponse.ts create mode 100644 src/v2/product/extraction/ragDocuments/fieldFactory.ts create mode 100644 src/v2/product/extraction/ragDocuments/index.ts create mode 100644 src/v2/product/extraction/ragDocuments/params/index.ts create mode 100644 src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts create mode 100644 src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts create mode 100644 src/v2/product/extraction/ragDocuments/ragAnnotation.ts rename tests/v2/product/{ => extraction}/extraction.spec.ts (99%) rename tests/v2/product/{ => extraction}/extractionParameter.spec.ts (97%) create mode 100644 tests/v2/product/extraction/ragDocumentsTest.ts diff --git a/src/v2/client.ts b/src/v2/client.ts index 82b562fc..35a438b3 100644 --- a/src/v2/client.ts +++ b/src/v2/client.ts @@ -35,7 +35,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 +60,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. - */ - 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. + * Enqueues a product inference job without waiting for completion. */ - 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, @@ -252,4 +224,36 @@ 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); + } } diff --git a/src/v2/clientOptions/baseAnnotationParameters.ts b/src/v2/clientOptions/baseAnnotationParameters.ts new file mode 100644 index 00000000..064dfac3 --- /dev/null +++ b/src/v2/clientOptions/baseAnnotationParameters.ts @@ -0,0 +1,27 @@ +/** + * Base parameters for document annotations. + */ +export abstract class BaseAnnotationParameters { + /** + * UUID of the annotated document. + */ + public readonly documentId: string; + + /** + * Default constructor. + * @param documentId The UUID of the document. + */ + protected constructor(documentId: string) { + if (!documentId || documentId.trim().length === 0) { + throw new Error("documentId cannot be null or whitespace."); + } + + // Note: DocumentId is included in the request URL path, it is not a parameter. + this.documentId = documentId.trim(); + } + + /** + * Gets the request parameters for the upload request. + */ + public abstract getRequestParameters(): Record; +} diff --git a/src/v2/clientOptions/baseRagDocumentUploadParameters.ts b/src/v2/clientOptions/baseRagDocumentUploadParameters.ts new file mode 100644 index 00000000..9646677e --- /dev/null +++ b/src/v2/clientOptions/baseRagDocumentUploadParameters.ts @@ -0,0 +1,29 @@ +/** + * 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. + * @param modelId UUID of the model that the uploaded RAG document is linked to. + */ + protected constructor(modelId: string) { + if (!modelId || modelId.trim().length === 0) { + throw new Error("modelId cannot be null or whitespace."); + } + this.modelId = modelId.trim(); + } + + /** + * 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..ee0a01bf 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, @@ -160,6 +162,113 @@ 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 = new FormData(); + 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`, + queryParams: parameters.getRequestParameters(), + 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, + hostname: this.settings.hostname, + path: `/v2/products/${product.slug}/rag-documents/${parameters.documentId}`, + queryParams: 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/ragDocuments/annotatedBaseField.ts b/src/v2/product/extraction/ragDocuments/annotatedBaseField.ts new file mode 100644 index 00000000..6f2503cd --- /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; + + /** + * 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) { + 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..839a44a3 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedFields.ts @@ -0,0 +1,55 @@ +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; + } +} diff --git a/src/v2/product/extraction/ragDocuments/annotatedListField.ts b/src/v2/product/extraction/ragDocuments/annotatedListField.ts new file mode 100644 index 00000000..fc98977f --- /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); + }); + } + + /** + * SimpleField 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; + } + + /** + * SimpleField 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..31a5ee7a --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedObjectField.ts @@ -0,0 +1,58 @@ +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..9b647409 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/annotatedSimpleField.ts @@ -0,0 +1,27 @@ +import { AnnotatedBaseField } from "./annotatedBaseField.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. + * @param value Field value, one of: string, boolean, number, null. + * @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. + */ + constructor( + value: string | boolean | number | null, + selected: boolean, + guidelines: string + ) { + super(selected, guidelines); + this.value = 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..b0a40e44 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/fieldFactory.ts @@ -0,0 +1,40 @@ +/** + * 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["value"], + serverResponse["selected"], + serverResponse["guidelines"] + ); + } + + 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..f876358c --- /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"; + +/** + * 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. + * @param documentId UUID of the annotated document. + * @param status New public status to apply to the document. + * @param annotation Field-level RAG annotation and guidelines configuration. + */ + constructor( + documentId: string, + status?: string, + annotation?: RagAnnotation | string | Record | null + ) { + super(documentId); + + this.status = status; + + if (annotation === null || annotation === undefined) { + this.annotation = null; + } else if (annotation instanceof RagAnnotation) { + this.annotation = annotation; + } else if (typeof annotation === "string") { + const parsedJson = JSON.parse(annotation); + this.annotation = new RagAnnotation(parsedJson); + } else if (typeof annotation === "object" && !Array.isArray(annotation)) { + this.annotation = new RagAnnotation(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..357e3d0e --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts @@ -0,0 +1,12 @@ +import { BaseRagDocumentUploadParameters } from "@/v2/clientOptions/baseRagDocumentUploadParameters.js"; + +export class RagDocumentUploadParameters extends BaseRagDocumentUploadParameters { + + /** + * Default constructor. + * @param modelId UUID of the model that the uploaded RAG document is linked to. + */ + constructor(modelId: string) { + super(modelId); + } +} diff --git a/src/v2/product/extraction/ragDocuments/ragAnnotation.ts b/src/v2/product/extraction/ragDocuments/ragAnnotation.ts new file mode 100644 index 00000000..debc9269 --- /dev/null +++ b/src/v2/product/extraction/ragDocuments/ragAnnotation.ts @@ -0,0 +1,18 @@ +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 = serverResponse["fields"] + ? new AnnotatedFields(serverResponse["fields"]) + : ({} as AnnotatedFields); + } +} 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/ragDocumentsTest.ts b/tests/v2/product/extraction/ragDocumentsTest.ts new file mode 100644 index 00000000..00ad4b0f --- /dev/null +++ b/tests/v2/product/extraction/ragDocumentsTest.ts @@ -0,0 +1,152 @@ +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 +} from "@/v2/product/extraction/ragDocuments/extractionRagAnnotationResponse.js"; +import { RagAnnotation } from "@/v2/product/extraction/ragDocuments/ragAnnotation.js"; +import { + RagDocumentUploadParameters +} from "@/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.js"; +import { + RagDocumentAnnotationParameters +} from "@/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.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("invalid-model-id"); + const reqParams = parameters.getRequestParameters(); + + assert.strictEqual(reqParams["model_id"], "invalid-model-id"); + }); + + it("should init PATCH parameters", () => { + // Pass an empty dictionary (or appropriate default) to match the StringDict constructor + const annotation = new RagAnnotation({}); + const parameters = new RagDocumentAnnotationParameters( + "invalid-document-id", + "Active", + annotation + ); + const reqParams = parameters.getRequestParameters(); + + assert.strictEqual(parameters.documentId, "invalid-document-id"); + assert.strictEqual(reqParams["status"], "Active"); + assert.strictEqual(reqParams["annotation"], annotation); + }); + + 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(Object.keys(localeField.fields).length, 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(Object.keys(lineItem0.fields).length, 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(Object.keys(lineItem1.fields).length, 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(Object.keys(lineItem2.fields).length, 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); + }); }); From 040bee67696cc54055a2dee01fa44cd2939d8aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Thu, 17 Sep 2026 13:08:26 +0200 Subject: [PATCH 2/3] add to client --- src/v2/client.ts | 210 +++++++++++++++++- .../clientOptions/baseAnnotationParameters.ts | 19 +- .../baseRagDocumentUploadParameters.ts | 18 +- src/v2/http/mindeeApiV2.ts | 10 +- src/v2/product/extraction/index.ts | 1 + .../ragDocuments/annotatedListField.ts | 4 +- .../params/ragDocumentAnnotationParameters.ts | 30 +-- .../params/ragDocumentUploadParameters.ts | 15 +- .../extraction/ragDocuments.integration.ts | 54 +++++ ...gDocumentsTest.ts => ragDocuments.spec.ts} | 33 ++- 10 files changed, 327 insertions(+), 67 deletions(-) create mode 100644 tests/v2/product/extraction/ragDocuments.integration.ts rename tests/v2/product/extraction/{ragDocumentsTest.ts => ragDocuments.spec.ts} (85%) diff --git a/src/v2/client.ts b/src/v2/client.ts index 35a438b3..9e764e34 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. @@ -153,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 ); @@ -249,11 +253,203 @@ export class Client { 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?: 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); + } + + 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.mindeeApi.reqPatchRagAnnotation(product, parameters); + + if (initialResponse.status !== "Processing") { + return initialResponse; + } + + const pollingOptionsInstance = pollingOptions instanceof PollingOptions + ? pollingOptions + : new PollingOptions(pollingOptions); + + return await this.pollForRagDocument(product, initialResponse, pollingOptionsInstance); + } + + /** + * 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 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) { + await setTimeout( + pollingOptions.delaySec * 1000, + undefined, + pollingOptions.recurringTimerOptions + ); + + logger.debug( + `Poll attempt ${retryCount} of ${pollingOptions.maxRetries}` + ); + + const response = await this.getRagDocument(product, documentId); + + retryCount++; + + switch (response.status) { + case "Processing": + continue; + case "Failed": + throw new MindeeError("Job 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 index 064dfac3..8193bbd8 100644 --- a/src/v2/clientOptions/baseAnnotationParameters.ts +++ b/src/v2/clientOptions/baseAnnotationParameters.ts @@ -1,3 +1,12 @@ +import { MindeeConfigurationError } from "@/errors/index.js"; + +/** + * Constructor parameters for BaseAnnotationParameters and its subclasses. + */ +export interface BaseAnnotationParametersConstructor { + documentId: string; +} + /** * Base parameters for document annotations. */ @@ -9,15 +18,13 @@ export abstract class BaseAnnotationParameters { /** * Default constructor. - * @param documentId The UUID of the document. */ - protected constructor(documentId: string) { - if (!documentId || documentId.trim().length === 0) { - throw new Error("documentId cannot be null or whitespace."); + protected constructor(params: BaseAnnotationParametersConstructor) { + if (params.documentId === undefined || params.documentId === null || params.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.trim(); + this.documentId = params.documentId.trim(); } /** diff --git a/src/v2/clientOptions/baseRagDocumentUploadParameters.ts b/src/v2/clientOptions/baseRagDocumentUploadParameters.ts index 9646677e..99499472 100644 --- a/src/v2/clientOptions/baseRagDocumentUploadParameters.ts +++ b/src/v2/clientOptions/baseRagDocumentUploadParameters.ts @@ -1,3 +1,12 @@ +import { MindeeConfigurationError } from "@/errors/index.js"; + +/** + * Constructor parameters for BaseRagDocumentUploadParameters and its subclasses. + */ +export interface BaseRagDocumentUploadParametersConstructor { + modelId: string; +} + /** * Base parameters for document upload operations. */ @@ -9,13 +18,12 @@ export abstract class BaseRagDocumentUploadParameters { /** * Default constructor. - * @param modelId UUID of the model that the uploaded RAG document is linked to. */ - protected constructor(modelId: string) { - if (!modelId || modelId.trim().length === 0) { - throw new Error("modelId cannot be null or whitespace."); + protected constructor(params: BaseRagDocumentUploadParametersConstructor) { + if (params.modelId === undefined || params.modelId === null || params.modelId === "") { + throw new MindeeConfigurationError("Model ID must be provided"); } - this.modelId = modelId.trim(); + this.modelId = params.modelId.trim(); } /** diff --git a/src/v2/http/mindeeApiV2.ts b/src/v2/http/mindeeApiV2.ts index ee0a01bf..9938a5da 100644 --- a/src/v2/http/mindeeApiV2.ts +++ b/src/v2/http/mindeeApiV2.ts @@ -51,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, }; @@ -198,7 +197,7 @@ export class MindeeApiV2 { parameters: BaseRagDocumentUploadParameters, inputSource: LocalInputSource ): Promise> { - const form = new FormData(); + const form = this.#paramsToFormData(parameters.getRequestParameters()); form.set("file", new Blob([inputSource.fileObject]), inputSource.filename); const options: RequestOptions = { @@ -206,7 +205,6 @@ export class MindeeApiV2 { headers: this.settings.baseHeaders, hostname: this.settings.hostname, path: `/v2/products/${product.slug}/rag-documents`, - queryParams: parameters.getRequestParameters(), body: form, timeoutSecs: this.settings.timeoutSecs, }; @@ -228,12 +226,14 @@ export class MindeeApiV2 { product: P, parameters: BaseAnnotationParameters ): Promise> { + const form = this.#paramsToFormData(parameters.getRequestParameters()); + const options: RequestOptions = { method: "PATCH", headers: this.settings.baseHeaders, hostname: this.settings.hostname, path: `/v2/products/${product.slug}/rag-documents/${parameters.documentId}`, - queryParams: parameters.getRequestParameters(), + body: form, timeoutSecs: this.settings.timeoutSecs, }; const response: BaseHttpResponse = await sendRequestAndReadResponse(this.settings.dispatcher, options); 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/annotatedListField.ts b/src/v2/product/extraction/ragDocuments/annotatedListField.ts index fc98977f..59b8a610 100644 --- a/src/v2/product/extraction/ragDocuments/annotatedListField.ts +++ b/src/v2/product/extraction/ragDocuments/annotatedListField.ts @@ -26,7 +26,7 @@ export class AnnotatedListField extends AnnotatedBaseField { } /** - * SimpleField items from the list. + * AnnotatedSimpleField items from the list. */ public get simpleItems(): Array { const result: Array = []; @@ -44,7 +44,7 @@ export class AnnotatedListField extends AnnotatedBaseField { } /** - * SimpleField items from the list. + * AnnotatedObjectField items from the list. */ public get objectItems(): Array { const result: Array = []; diff --git a/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts b/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts index f876358c..552e9836 100644 --- a/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts +++ b/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts @@ -1,5 +1,6 @@ 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. @@ -17,28 +18,27 @@ export class RagDocumentAnnotationParameters extends BaseAnnotationParameters { /** * Default constructor. - * @param documentId UUID of the annotated document. - * @param status New public status to apply to the document. - * @param annotation Field-level RAG annotation and guidelines configuration. */ constructor( - documentId: string, - status?: string, - annotation?: RagAnnotation | string | Record | null + params: BaseAnnotationParametersConstructor & + { + status?: string, + annotation?: RagAnnotation | string | Record | null + } ) { - super(documentId); + super({ ...params }); - this.status = status; + this.status = params.status; - if (annotation === null || annotation === undefined) { + if (params.annotation === null || params.annotation === undefined) { this.annotation = null; - } else if (annotation instanceof RagAnnotation) { - this.annotation = annotation; - } else if (typeof annotation === "string") { - const parsedJson = JSON.parse(annotation); + } 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 annotation === "object" && !Array.isArray(annotation)) { - this.annotation = new RagAnnotation(annotation); + } else if (typeof params.annotation === "object" && !Array.isArray(params.annotation)) { + this.annotation = new RagAnnotation(params.annotation); } else { throw new Error("Invalid RAG Annotation format."); } diff --git a/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts b/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts index 357e3d0e..68dbb676 100644 --- a/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts +++ b/src/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.ts @@ -1,12 +1,11 @@ -import { BaseRagDocumentUploadParameters } from "@/v2/clientOptions/baseRagDocumentUploadParameters.js"; +import { BaseRagDocumentUploadParameters, BaseRagDocumentUploadParametersConstructor +} from "@/v2/clientOptions/baseRagDocumentUploadParameters.js"; +/** + * Parameters for uploading a file to an extraction RAG database. + */ export class RagDocumentUploadParameters extends BaseRagDocumentUploadParameters { - - /** - * Default constructor. - * @param modelId UUID of the model that the uploaded RAG document is linked to. - */ - constructor(modelId: string) { - super(modelId); + constructor(params: BaseRagDocumentUploadParametersConstructor & {}) { + super({ ...params }); } } diff --git a/tests/v2/product/extraction/ragDocuments.integration.ts b/tests/v2/product/extraction/ragDocuments.integration.ts new file mode 100644 index 00000000..4427b8d0 --- /dev/null +++ b/tests/v2/product/extraction/ragDocuments.integration.ts @@ -0,0 +1,54 @@ +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); + }); +}); diff --git a/tests/v2/product/extraction/ragDocumentsTest.ts b/tests/v2/product/extraction/ragDocuments.spec.ts similarity index 85% rename from tests/v2/product/extraction/ragDocumentsTest.ts rename to tests/v2/product/extraction/ragDocuments.spec.ts index 00ad4b0f..897a590a 100644 --- a/tests/v2/product/extraction/ragDocumentsTest.ts +++ b/tests/v2/product/extraction/ragDocuments.spec.ts @@ -4,15 +4,11 @@ import { describe, it } from "node:test"; import { promises as fs } from "fs"; import { V2_PRODUCT_PATH } from "../../../index.js"; import { - ExtractionRagAnnotationResponse -} from "@/v2/product/extraction/ragDocuments/extractionRagAnnotationResponse.js"; -import { RagAnnotation } from "@/v2/product/extraction/ragDocuments/ragAnnotation.js"; + ExtractionRagAnnotationResponse, RagAnnotation +} from "@/v2/product/extraction/ragDocuments/index.js"; import { - RagDocumentUploadParameters -} from "@/v2/product/extraction/ragDocuments/params/ragDocumentUploadParameters.js"; -import { - RagDocumentAnnotationParameters -} from "@/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.js"; + RagDocumentUploadParameters, RagDocumentAnnotationParameters +} from "@/v2/product/extraction/ragDocuments/params/index.js"; describe("MindeeV2 - Extraction RagDocuments", () => { @@ -29,20 +25,19 @@ describe("MindeeV2 - Extraction RagDocuments", () => { } it("should init POST parameters", () => { - const parameters = new RagDocumentUploadParameters("invalid-model-id"); + 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", () => { - // Pass an empty dictionary (or appropriate default) to match the StringDict constructor const annotation = new RagAnnotation({}); - const parameters = new RagDocumentAnnotationParameters( - "invalid-document-id", - "Active", - annotation - ); + const parameters = new RagDocumentAnnotationParameters({ + documentId: "invalid-document-id", + status: "Active", + annotation: annotation + }); const reqParams = parameters.getRequestParameters(); assert.strictEqual(parameters.documentId, "invalid-document-id"); @@ -90,7 +85,7 @@ describe("MindeeV2 - Extraction RagDocuments", () => { assert.strictEqual(localeField.selected, false); assert.strictEqual(localeField.guidelines, null); assert.ok(localeField.fields); - assert.strictEqual(Object.keys(localeField.fields).length, 3); + 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); @@ -114,7 +109,7 @@ describe("MindeeV2 - Extraction RagDocuments", () => { const lineItem0 = lineItemsField.objectItems[0]; assert.ok(lineItem0.fields); - assert.strictEqual(Object.keys(lineItem0.fields).length, 8); + 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); @@ -126,7 +121,7 @@ describe("MindeeV2 - Extraction RagDocuments", () => { const lineItem1 = lineItemsField.objectItems[1]; assert.ok(lineItem1.fields); - assert.strictEqual(Object.keys(lineItem1.fields).length, 8); + 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); @@ -138,7 +133,7 @@ describe("MindeeV2 - Extraction RagDocuments", () => { const lineItem2 = lineItemsField.objectItems[2]; assert.ok(lineItem2.fields); - assert.strictEqual(Object.keys(lineItem2.fields).length, 8); + 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); From 490ecf6d993cd872e7f2eb61a33668498fa2e027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Thu, 17 Sep 2026 22:19:04 +0200 Subject: [PATCH 3/3] fix for json string --- src/v2/product/extraction/ragDocuments/annotatedFields.ts | 8 ++++++++ .../params/ragDocumentAnnotationParameters.ts | 6 +++--- tests/v2/product/extraction/ragDocuments.spec.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/v2/product/extraction/ragDocuments/annotatedFields.ts b/src/v2/product/extraction/ragDocuments/annotatedFields.ts index 839a44a3..830bb4ee 100644 --- a/src/v2/product/extraction/ragDocuments/annotatedFields.ts +++ b/src/v2/product/extraction/ragDocuments/annotatedFields.ts @@ -52,4 +52,12 @@ export class AnnotatedFields extends Map { + return Object.fromEntries(this); + } } diff --git a/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts b/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts index 552e9836..4bffee45 100644 --- a/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts +++ b/src/v2/product/extraction/ragDocuments/params/ragDocumentAnnotationParameters.ts @@ -47,15 +47,15 @@ export class RagDocumentAnnotationParameters extends BaseAnnotationParameters { /** * Gets the request parameters for the upload request. */ - public getRequestParameters(): Record { - const parameters: Record = {}; + public getRequestParameters(): Record { + const parameters: Record = {}; if (this.status) { parameters["status"] = this.status; } if (this.annotation) { - parameters["annotation"] = this.annotation; + parameters["annotation"] = JSON.stringify(this.annotation); } return parameters; diff --git a/tests/v2/product/extraction/ragDocuments.spec.ts b/tests/v2/product/extraction/ragDocuments.spec.ts index 897a590a..44ad5c1b 100644 --- a/tests/v2/product/extraction/ragDocuments.spec.ts +++ b/tests/v2/product/extraction/ragDocuments.spec.ts @@ -42,7 +42,7 @@ describe("MindeeV2 - Extraction RagDocuments", () => { assert.strictEqual(parameters.documentId, "invalid-document-id"); assert.strictEqual(reqParams["status"], "Active"); - assert.strictEqual(reqParams["annotation"], annotation); + assert.strictEqual(reqParams["annotation"], JSON.stringify(annotation)); }); it("should load a POST response from a JSON string", async () => {