diff --git a/src/v2/client.ts b/src/v2/client.ts index b1547342..57806218 100644 --- a/src/v2/client.ts +++ b/src/v2/client.ts @@ -10,6 +10,8 @@ import { MindeeApiV2 } from "./http/mindeeApiV2.js"; import { MindeeHttpErrorV2 } from "./http/errors.js"; import { PollingOptions, PollingOptionsConstructor } from "./clientOptions/index.js"; import { BaseProduct } from "@/v2/product/baseProduct.js"; +import { BaseSearch } from "@/v2/search/baseSearch.js"; +import { Models } from "@/v2/search/models/models.js"; /** * Options for the V2 Mindee Client. @@ -60,9 +62,31 @@ export class Client { * @param name Optional name filter. * @param modelType Optional model type filter. * @returns a `Promise` containing the search response. + * @deprecated Use `search(Models, {})` instead. */ async searchModels(name?: string, modelType?: string): Promise { - return await this.mindeeApi.reqGetSearchModel(name, modelType); + return await this.search(Models, { name: name, modelType: modelType }); + } + + /** + * Searches for resources matching the given criteria. + * @param search + * @param searchParameters Search parameters. + * @returns a `Promise` containing the search response. + */ + 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. */ diff --git a/src/v2/clientOptions/baseParameters.ts b/src/v2/clientOptions/baseProductParameters.ts similarity index 73% rename from src/v2/clientOptions/baseParameters.ts rename to src/v2/clientOptions/baseProductParameters.ts index 579172a2..b258101a 100644 --- a/src/v2/clientOptions/baseParameters.ts +++ b/src/v2/clientOptions/baseProductParameters.ts @@ -1,10 +1,9 @@ -import { FormData } from "undici"; import { MindeeConfigurationError } from "@/errors/index.js"; /** * Constructor parameters for BaseParameters and its subclasses. */ -export interface BaseParametersConstructor { +export interface BaseProductParametersConstructor { modelId: string; alias?: string; webhookIds?: string[]; @@ -25,7 +24,7 @@ export interface BaseParametersConstructor { * webhookIds: ["YOUR_WEBHOOK_ID_1", "YOUR_WEBHOOK_ID_2"], * }; */ -export abstract class BaseParameters { +export abstract class BaseProductParameters { /** * Model ID to use for the inference. **Required.** */ @@ -47,7 +46,7 @@ export abstract class BaseParameters { */ closeFile?: boolean; - protected constructor(params: BaseParametersConstructor) { + protected constructor(params: BaseProductParametersConstructor) { if (params.modelId === undefined || params.modelId === null || params.modelId === "") { throw new MindeeConfigurationError("Model ID must be provided"); } @@ -58,20 +57,20 @@ export abstract class BaseParameters { } /** - * Returns the form data to send to the API. - * @returns A `FormData` object. + * Gets the request parameters for the enqueue request. + * @returns A `Record` mapping parameter names to their string values. */ - getFormData(): FormData { - const form = new FormData(); + getRequestParameters(): Record { + const parameters: Record = {}; - form.set("model_id", this.modelId); + parameters["model_id"] = this.modelId; if (this.alias !== undefined && this.alias !== null) { - form.set("alias", this.alias); + parameters["alias"] = this.alias; } if (this.webhookIds && this.webhookIds.length > 0) { - form.set("webhook_ids", this.webhookIds.join(",")); + parameters["webhook_ids"] = this.webhookIds.join(","); } - return form; + return parameters; } } diff --git a/src/v2/clientOptions/baseSearchParameters.ts b/src/v2/clientOptions/baseSearchParameters.ts new file mode 100644 index 00000000..58cddc5d --- /dev/null +++ b/src/v2/clientOptions/baseSearchParameters.ts @@ -0,0 +1,44 @@ +/** + * Constructor parameters for BaseSearchParameters and its subclasses. + */ +export interface BaseSearchParametersConstructor { + page?: number; + perPage?: number; +} + +/** + * Base parameters for searches. + */ +export abstract class BaseSearchParameters { + /** + * 1-based page index. + */ + page?: number; + + /** + * Number of items per page. + */ + perPage?: number; + + protected constructor(params: BaseSearchParametersConstructor) { + this.page = params.page; + this.perPage = params.perPage; + } + + /** + * Gets the request parameters for the search request. + * @returns A `Record` mapping parameter names to their string values. + */ + getRequestParameters(): Record { + const parameters: Record = {}; + + if (this.page !== null && this.page !== undefined && this.page > 0) { + parameters["page"] = this.page.toString(); + } + if (this.perPage !== null && this.perPage !== undefined && this.perPage > 0) { + parameters["per_page"] = this.perPage.toString(); + } + + return parameters; + } +} diff --git a/src/v2/clientOptions/index.ts b/src/v2/clientOptions/index.ts index 4b5dd6a3..4637d0ae 100644 --- a/src/v2/clientOptions/index.ts +++ b/src/v2/clientOptions/index.ts @@ -3,4 +3,5 @@ export type { PollingOptionsConstructor, TimerOptions, } from "./pollingOptions.js"; -export { BaseParameters } from "./baseParameters.js"; +export { BaseProductParameters } from "./baseProductParameters.js"; +export { BaseSearchParameters } from "./baseSearchParameters.js"; diff --git a/src/v2/http/mindeeApiV2.ts b/src/v2/http/mindeeApiV2.ts index f29a058c..81942753 100644 --- a/src/v2/http/mindeeApiV2.ts +++ b/src/v2/http/mindeeApiV2.ts @@ -1,6 +1,8 @@ import { ApiSettings } from "./apiSettings.js"; import { Dispatcher } from "undici"; -import { BaseParameters } from "@/v2/index.js"; +import { BaseProductParameters } from "@/v2/index.js"; +import { BaseSearchParameters } from "@/v2/clientOptions/baseSearchParameters.js"; +import { FormData } from "undici"; import { BaseResponse, ErrorResponse, @@ -17,7 +19,7 @@ import { MindeeDeserializationError, MindeeError } from "@/errors/index.js"; import { MindeeHttpErrorV2 } from "./errors.js"; import { logger } from "@/logger.js"; import { BaseProduct } from "@/v2/product/baseProduct.js"; -import { SearchResponse } from "@/v2/parsing/search/index.js"; +import { BaseSearch } from "@/v2/search/baseSearch.js"; /** * Mindee V2 API handler. @@ -30,25 +32,25 @@ export class MindeeApiV2 { } /** - * Search for models available to the account. - * @param name Optional name filter. - * @param modelType Optional model type filter. + * Searches for resources matching the given criteria. + * @param search + * @param parameters Search parameters. * @returns a `Promise` containing the search response. */ - async reqGetSearchModel(name?: string, modelType?: string): Promise { - const queryParams: Record = {}; - if (name) queryParams["name"] = name; - if (modelType) queryParams["model_type"] = modelType; + async reqGetSearch( + search: S, + parameters: BaseSearchParameters + ): Promise> { const options: RequestOptions = { method: "GET", headers: this.settings.baseHeaders, hostname: this.settings.hostname, - path: "/v2/search/models", - queryParams: queryParams, + path: `/v2/search/${search.slug}`, + queryParams: parameters.getRequestParameters(), timeoutSecs: this.settings.timeoutSecs, }; const response: BaseHttpResponse = await sendRequestAndReadResponse(this.settings.dispatcher, options); - return this.#processResponse(response, SearchResponse); + return this.#processResponse(response, search.responseClass) as InstanceType; } /** @@ -60,9 +62,10 @@ export class MindeeApiV2 { async reqPostProductEnqueue( product: typeof BaseProduct, inputSource: InputSource, - params: BaseParameters + params: BaseProductParameters ): Promise { - const form = params.getFormData(); + const form = this.#paramsToFormData(params.getRequestParameters()); + if (inputSource instanceof LocalInputSource) { form.set("file", new Blob([inputSource.fileObject]), inputSource.filename); } else { @@ -157,6 +160,14 @@ export class MindeeApiV2 { return this.#processResponse(response, product.responseClass) as InstanceType; } + #paramsToFormData(params: Record): FormData { + const form = new FormData(); + for (const [key, value] of Object.entries(params)) { + form.set(key, value); + } + return form; + } + #processResponse( result: BaseHttpResponse, responseClass: ResponseConstructor, diff --git a/src/v2/index.ts b/src/v2/index.ts index d58f6d47..dce146ce 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,11 +1,12 @@ export * as http from "./http/index.js"; export * as parsing from "./parsing/index.js"; export * as product from "./product/index.js"; +export * as search from "./search/index.js"; export { Client } from "./client.js"; export { JobResponse, ErrorResponse, LocalResponse, } from "./parsing/index.js"; -export type { BaseParameters, TimerOptions } from "./clientOptions/index.js"; +export type { BaseProductParameters, TimerOptions } from "./clientOptions/index.js"; export { PollingOptions } from "./clientOptions/index.js"; diff --git a/src/v2/parsing/search/baseSearchResponse.ts b/src/v2/parsing/search/baseSearchResponse.ts new file mode 100644 index 00000000..25ac0147 --- /dev/null +++ b/src/v2/parsing/search/baseSearchResponse.ts @@ -0,0 +1,27 @@ +import { StringDict } from "@/parsing/index.js"; +import { BaseResponse } from "@/v2/parsing/baseResponse.js"; +import { PaginationMetadata } from "./paginationMetadata.js"; + +/** + * Base class for search responses. + */ +export abstract class BaseSearchResponse extends BaseResponse { + /** + * Pagination metadata. + */ + public pagination: PaginationMetadata; + + protected constructor(serverResponse: StringDict) { + super(serverResponse); + this.pagination = new PaginationMetadata(serverResponse["pagination"]); + } + + protected abstract bodyLines(): string[]; + + toString(): string { + const lines: string[] = this.bodyLines(); + lines.push("Pagination Metadata", "###################"); + lines.push(this.pagination.toString()); + return lines.join("\n"); + } +} diff --git a/src/v2/parsing/search/index.ts b/src/v2/parsing/search/index.ts index da3ce8c4..1b43de94 100644 --- a/src/v2/parsing/search/index.ts +++ b/src/v2/parsing/search/index.ts @@ -2,3 +2,5 @@ export { PaginationMetadata } from "./paginationMetadata.js"; export { SearchModel } from "./searchModel.js"; export { SearchResponse } from "./searchResponse.js"; export { ModelWebhook } from "./modelWebhook.js"; +export { BaseSearchResponse } from "./baseSearchResponse.js"; +export { SearchRagDocument } from "./searchRagDocument.js"; diff --git a/src/v2/parsing/search/searchModels.ts b/src/v2/parsing/search/searchModels.ts new file mode 100644 index 00000000..fd36420c --- /dev/null +++ b/src/v2/parsing/search/searchModels.ts @@ -0,0 +1,24 @@ +import { SearchModel } from "@/v2/parsing/search/searchModel.js"; +import { StringDict } from "@/parsing/index.js"; + +export class SearchModels extends Array { + + constructor(serverResponse: StringDict[] = []) { + super(); + this.push(...serverResponse.map((item: StringDict) => new SearchModel(item))); + } + + toString(): string { + if (this.length === 0) { + return "\n"; + } + const lines: string[] = []; + for (const model of this) { + lines.push(`* :Name: ${model.name}`); + lines.push(` :ID: ${model.id}`); + lines.push(` :Model Type: ${model.modelType}`); + } + return lines.join("\n"); + } + +} diff --git a/src/v2/parsing/search/searchRagDocument.ts b/src/v2/parsing/search/searchRagDocument.ts new file mode 100644 index 00000000..3358be24 --- /dev/null +++ b/src/v2/parsing/search/searchRagDocument.ts @@ -0,0 +1,53 @@ +import { StringDict } from "@/parsing/index.js"; + +/** + * Individual RAG document information. + */ +export class SearchRagDocument { + /** + * Unique identifier of the RAG document. + */ + public id: string; + + /** + * Model identifier linked to the RAG document. + */ + public modelId: string; + + /** + * Original filename of the uploaded document. + */ + public filename: string; + + /** + * Date and time of the document creation. + */ + public createdAt: Date; + + /** + * 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; + + /** + * Current status of the RAG document. + */ + public status: string; + + constructor(serverResponse: StringDict) { + this.id = serverResponse["id"]; + this.modelId = serverResponse["model_id"]; + this.filename = serverResponse["filename"]; + this.createdAt = new Date(serverResponse["created_at"]); + this.totalMatches = serverResponse["total_matches"]; + this.lastMatchAt = serverResponse["last_match_at"] + ? new Date(serverResponse["last_match_at"]) + : undefined; + this.status = serverResponse["status"]; + } +} diff --git a/src/v2/parsing/search/searchRagDocuments.ts b/src/v2/parsing/search/searchRagDocuments.ts new file mode 100644 index 00000000..c658728b --- /dev/null +++ b/src/v2/parsing/search/searchRagDocuments.ts @@ -0,0 +1,30 @@ +import { SearchRagDocument } from "@/v2/parsing/search/searchRagDocument.js"; +import { StringDict } from "@/parsing/index.js"; + +/** + * List of RAG documents. + */ +export class SearchRagDocuments extends Array { + + constructor(serverResponse: StringDict[] = []) { + super(); + this.push(...serverResponse.map((item: StringDict) => new SearchRagDocument(item))); + } + + toString(): string { + if (this.length === 0) { + return "\n"; + } + const lines: string[] = []; + for (const ragDocument of this) { + lines.push(`* :ID: ${ragDocument.id}`); + lines.push(` :Model ID: ${ragDocument.modelId}`); + lines.push(` :Filename: ${ragDocument.filename}`); + lines.push(` :Created At: ${ragDocument.createdAt}`); + lines.push(` :Total Matches: ${ragDocument.totalMatches}`); + lines.push(` :Last Match At: ${ragDocument.lastMatchAt}`); + lines.push(` :Status: ${ragDocument.status}`); + } + return lines.join("\n"); + } +} diff --git a/src/v2/parsing/search/searchResponse.ts b/src/v2/parsing/search/searchResponse.ts index 4a3a1ecf..14d13db9 100644 --- a/src/v2/parsing/search/searchResponse.ts +++ b/src/v2/parsing/search/searchResponse.ts @@ -1,39 +1,12 @@ import { StringDict } from "@/parsing/index.js"; -import { BaseResponse } from "@/v2/parsing/baseResponse.js"; -import { PaginationMetadata } from "./paginationMetadata.js"; -import { SearchModel } from "./searchModel.js"; +import { ModelSearchResponse } from "@/v2/search/models/modelSearchResponse.js"; /** * Models search response. + * @deprecated Use `ModelSearchResponse` instead. */ -export class SearchResponse extends BaseResponse { - /** - * List of models returned by the search. - */ - public models: SearchModel[]; - - /** - * Pagination metadata. - */ - public pagination: PaginationMetadata; - +export class SearchResponse extends ModelSearchResponse { constructor(serverResponse: StringDict) { super(serverResponse); - this.models = (serverResponse["models"] ?? []).map( - (model: StringDict) => new SearchModel(model) - ); - this.pagination = new PaginationMetadata(serverResponse["pagination"]); - } - - toString(): string { - const lines: string[] = ["Models", "#######"]; - for (const model of this.models) { - lines.push(`* :Name: ${model.name}`); - lines.push(` :ID: ${model.id}`); - lines.push(` :Model Type: ${model.modelType}`); - } - lines.push("Pagination", "##########"); - lines.push(this.pagination.toString()); - return lines.join("\n"); } } diff --git a/src/v2/product/baseProduct.ts b/src/v2/product/baseProduct.ts index 0ff28734..f5f57041 100644 --- a/src/v2/product/baseProduct.ts +++ b/src/v2/product/baseProduct.ts @@ -1,4 +1,4 @@ -import { BaseParameters } from "@/v2/index.js"; +import { BaseProductParameters } from "@/v2/index.js"; import { ResponseConstructor } from "@/v2/parsing/index.js"; /** @@ -7,12 +7,17 @@ import { ResponseConstructor } from "@/v2/parsing/index.js"; * Child classes are passed to the Client when making requests. */ export abstract class BaseProduct { - static get parametersClass(): new (...args: any[]) => BaseParameters { + /** Parameter class accepted by this product. */ + static get parametersClass(): new (...args: any[]) => BaseProductParameters { throw new Error("Must define static parameters property"); } + + /** Response class returned by this product. */ static get responseClass(): ResponseConstructor { throw new Error("Must define static response property"); } + + /** API slug for this product. */ static get slug(): string { throw new Error("Must define static slug property"); } diff --git a/src/v2/product/classification/classification.ts b/src/v2/product/classification/classification.ts index 268ae49a..a957d1dc 100644 --- a/src/v2/product/classification/classification.ts +++ b/src/v2/product/classification/classification.ts @@ -6,15 +6,17 @@ import { BaseProduct } from "@/v2/product/baseProduct.js"; * Automatically sort any image or scanned document into categories. */ export class Classification extends BaseProduct { - /** Parameter class accepted by this product. */ + /** @inheritDoc */ static get parametersClass() { return ClassificationParameters; } - /** Response class returned by this product. */ + + /** @inheritDoc */ static get responseClass() { return ClassificationResponse; } - /** API slug for this product. */ + + /** @inheritDoc */ static get slug() { return "classification"; } diff --git a/src/v2/product/classification/params/classificationParameters.ts b/src/v2/product/classification/params/classificationParameters.ts index acef9cbf..16c5f47f 100644 --- a/src/v2/product/classification/params/classificationParameters.ts +++ b/src/v2/product/classification/params/classificationParameters.ts @@ -1,4 +1,4 @@ -import { BaseParameters, BaseParametersConstructor } from "@/v2/clientOptions/baseParameters.js"; +import { BaseProductParameters, BaseProductParametersConstructor } from "@/v2/clientOptions/baseProductParameters.js"; import { logger } from "@/logger.js"; /** @@ -18,8 +18,8 @@ import { logger } from "@/logger.js"; * } * }; */ -export class ClassificationParameters extends BaseParameters { - constructor(params: BaseParametersConstructor & {}) { +export class ClassificationParameters extends BaseProductParameters { + constructor(params: BaseProductParametersConstructor & {}) { super({ ...params }); logger.debug("Classification parameters initialized."); } diff --git a/src/v2/product/crop/crop.ts b/src/v2/product/crop/crop.ts index 223999fe..12681b13 100644 --- a/src/v2/product/crop/crop.ts +++ b/src/v2/product/crop/crop.ts @@ -6,15 +6,17 @@ import { BaseProduct } from "@/v2/product/baseProduct.js"; * Identify the borders of documents on each page, matching each one to a category. */ export class Crop extends BaseProduct { - /** Parameter class accepted by this product. */ + /** @inheritDoc */ static get parametersClass() { return CropParameters; } - /** Response class returned by this product. */ + + /** @inheritDoc */ static get responseClass() { return CropResponse; } - /** API slug for this product. */ + + /** @inheritDoc */ static get slug() { return "crop"; } diff --git a/src/v2/product/crop/params/cropParameters.ts b/src/v2/product/crop/params/cropParameters.ts index 1c2a6fb9..33cfeb3a 100644 --- a/src/v2/product/crop/params/cropParameters.ts +++ b/src/v2/product/crop/params/cropParameters.ts @@ -1,4 +1,4 @@ -import { BaseParameters, BaseParametersConstructor } from "@/v2/clientOptions/baseParameters.js"; +import { BaseProductParameters, BaseProductParametersConstructor } from "@/v2/clientOptions/baseProductParameters.js"; import { logger } from "@/logger.js"; /** @@ -18,8 +18,8 @@ import { logger } from "@/logger.js"; * } * }; */ -export class CropParameters extends BaseParameters { - constructor(params: BaseParametersConstructor & {}) { +export class CropParameters extends BaseProductParameters { + constructor(params: BaseProductParametersConstructor & {}) { super({ ...params }); logger.debug("Crop parameters initialized."); } diff --git a/src/v2/product/extraction/extraction.ts b/src/v2/product/extraction/extraction.ts index f04dff20..44c5df7d 100644 --- a/src/v2/product/extraction/extraction.ts +++ b/src/v2/product/extraction/extraction.ts @@ -6,15 +6,17 @@ import { BaseProduct } from "@/v2/product/baseProduct.js"; * Automatically extract structured data from any image or scanned document. */ export class Extraction extends BaseProduct { - /** Parameter class accepted by this product. */ + /** @inheritDoc */ static get parametersClass() { return ExtractionParameters; } - /** Response class returned by this product. */ + + /** @inheritDoc */ static get responseClass() { return ExtractionResponse; } - /** API slug for this product. */ + + /** @inheritDoc */ static get slug() { return "extraction"; } diff --git a/src/v2/product/extraction/params/extractionParameters.ts b/src/v2/product/extraction/params/extractionParameters.ts index 6bb8124e..adce2738 100644 --- a/src/v2/product/extraction/params/extractionParameters.ts +++ b/src/v2/product/extraction/params/extractionParameters.ts @@ -1,7 +1,6 @@ -import { FormData } from "undici"; import { StringDict } from "@/parsing/stringDict.js"; import { DataSchema } from "./dataSchema.js"; -import { BaseParameters, BaseParametersConstructor } from "@/v2/clientOptions/baseParameters.js"; +import { BaseProductParameters, BaseProductParametersConstructor } from "@/v2/clientOptions/baseProductParameters.js"; import { logger } from "@/logger.js"; /** @@ -22,7 +21,7 @@ import { logger } from "@/logger.js"; * } * }; */ -export class ExtractionParameters extends BaseParameters { +export class ExtractionParameters extends BaseProductParameters { /** * Use Retrieval-Augmented Generation during inference. */ @@ -51,7 +50,7 @@ export class ExtractionParameters extends BaseParameters { */ dataSchema?: DataSchema | StringDict | string; - constructor(params: BaseParametersConstructor & { + constructor(params: BaseProductParametersConstructor & { rag?: boolean; rawText?: boolean; polygon?: boolean; @@ -76,32 +75,28 @@ export class ExtractionParameters extends BaseParameters { logger.debug("Extraction parameters initialized."); } - getFormData(): FormData { - const form = new FormData(); - - form.set("model_id", this.modelId); + getRequestParameters(): Record { + const parameters = super.getRequestParameters(); if (this.rag !== undefined && this.rag !== null) { - form.set("rag", this.rag.toString()); + parameters["rag"] = this.rag.toString(); } if (this.polygon !== undefined && this.polygon !== null) { - form.set("polygon", this.polygon.toString().toLowerCase()); + parameters["polygon"] = this.polygon.toString().toLowerCase(); } if (this.confidence !== undefined && this.confidence !== null) { - form.set("confidence", this.confidence.toString().toLowerCase()); + parameters["confidence"] = this.confidence.toString().toLowerCase(); } if (this.rawText !== undefined && this.rawText !== null) { - form.set("raw_text", this.rawText.toString().toLowerCase()); + parameters["raw_text"] = this.rawText.toString().toLowerCase(); } if (this.textContext !== undefined && this.textContext !== null) { - form.set("text_context", this.textContext); + parameters["text_context"] = this.textContext; } if (this.dataSchema !== undefined && this.dataSchema !== null) { - form.set("data_schema", this.dataSchema.toString()); - } - if (this.webhookIds && this.webhookIds.length > 0) { - form.set("webhook_ids", this.webhookIds.join(",")); + parameters["data_schema"] = this.dataSchema.toString(); } - return form; + + return parameters; } } diff --git a/src/v2/product/ocr/ocr.ts b/src/v2/product/ocr/ocr.ts index 19329ac9..cf2afab8 100644 --- a/src/v2/product/ocr/ocr.ts +++ b/src/v2/product/ocr/ocr.ts @@ -6,15 +6,17 @@ import { BaseProduct } from "@/v2/product/baseProduct.js"; * Extract raw text (OCR) from any image or scanned document. */ export class Ocr extends BaseProduct { - /** Parameter class accepted by this product. */ + /** @inheritDoc */ static get parametersClass() { return OcrParameters; } - /** Response class returned by this product. */ + + /** @inheritDoc */ static get responseClass() { return OcrResponse; } - /** API slug for this product. */ + + /** @inheritDoc */ static get slug() { return "ocr"; } diff --git a/src/v2/product/ocr/params/ocrParameters.ts b/src/v2/product/ocr/params/ocrParameters.ts index 340ec412..2c408c32 100644 --- a/src/v2/product/ocr/params/ocrParameters.ts +++ b/src/v2/product/ocr/params/ocrParameters.ts @@ -1,4 +1,4 @@ -import { BaseParameters, BaseParametersConstructor } from "@/v2/clientOptions/baseParameters.js"; +import { BaseProductParameters, BaseProductParametersConstructor } from "@/v2/clientOptions/baseProductParameters.js"; import { logger } from "@/logger.js"; /** @@ -18,8 +18,8 @@ import { logger } from "@/logger.js"; * } * }; */ -export class OcrParameters extends BaseParameters { - constructor(params: BaseParametersConstructor & {}) { +export class OcrParameters extends BaseProductParameters { + constructor(params: BaseProductParametersConstructor & {}) { super({ ...params }); logger.debug("OCR parameters initialized."); } diff --git a/src/v2/product/split/params/splitParameters.ts b/src/v2/product/split/params/splitParameters.ts index d9a49d8c..fd90ae29 100644 --- a/src/v2/product/split/params/splitParameters.ts +++ b/src/v2/product/split/params/splitParameters.ts @@ -1,7 +1,7 @@ import { - BaseParameters, - BaseParametersConstructor, -} from "@/v2/clientOptions/baseParameters.js"; + BaseProductParameters, + BaseProductParametersConstructor, +} from "@/v2/clientOptions/baseProductParameters.js"; import { logger } from "@/logger.js"; /** @@ -21,8 +21,8 @@ import { logger } from "@/logger.js"; * } * }; */ -export class SplitParameters extends BaseParameters { - constructor(params: BaseParametersConstructor & {}) { +export class SplitParameters extends BaseProductParameters { + constructor(params: BaseProductParametersConstructor & {}) { super({ ...params }); logger.debug("Split parameters initialized."); } diff --git a/src/v2/product/split/split.ts b/src/v2/product/split/split.ts index 2b6ef48e..296cf81e 100644 --- a/src/v2/product/split/split.ts +++ b/src/v2/product/split/split.ts @@ -6,15 +6,17 @@ import { BaseProduct } from "@/v2/product/baseProduct.js"; * Break a multipage source file into separate documents, associating a class for each one. */ export class Split extends BaseProduct { - /** Parameter class accepted by this product. */ + /** @inheritDoc */ static get parametersClass() { return SplitParameters; } - /** Response class returned by this product. */ + + /** @inheritDoc */ static get responseClass() { return SplitResponse; } - /** API slug for this product. */ + + /** @inheritDoc */ static get slug() { return "split"; } diff --git a/src/v2/search/baseSearch.ts b/src/v2/search/baseSearch.ts new file mode 100644 index 00000000..9b076d81 --- /dev/null +++ b/src/v2/search/baseSearch.ts @@ -0,0 +1,24 @@ +import { ResponseConstructor } from "@/v2/parsing/index.js"; +import { BaseSearchParameters } from "@/v2/clientOptions/index.js"; + +/** + * Base class for all V2 search definitions. + * + * Child classes are passed to the Client when making requests. + */ +export abstract class BaseSearch { + /** Parameter class used for the search query. */ + static get parametersClass(): new (...args: any[]) => BaseSearchParameters { + throw new Error("Must define static parametersClass property"); + } + + /** Response class returned by the search. */ + static get responseClass(): ResponseConstructor { + throw new Error("Must define static response property"); + } + + /** API slug for the search. */ + static get slug(): string { + throw new Error("Must define static slug property"); + } +} diff --git a/src/v2/search/index.ts b/src/v2/search/index.ts new file mode 100644 index 00000000..88afbcec --- /dev/null +++ b/src/v2/search/index.ts @@ -0,0 +1,6 @@ +export * as models from "./models/index.js"; +export * as ragDocuments from "./ragDocuments/index.js"; +export { ModelSearchParameters, ModelSearchResponse } from "./models/index.js"; +export { RagDocumentSearchParameters, RagDocumentSearchResponse } from "./ragDocuments/index.js"; +export { Models } from "./models/index.js"; +export { RagDocuments } from "./ragDocuments/index.js"; diff --git a/src/v2/search/models/index.ts b/src/v2/search/models/index.ts new file mode 100644 index 00000000..a58db1f0 --- /dev/null +++ b/src/v2/search/models/index.ts @@ -0,0 +1,4 @@ +export { ModelSearchParameters } from "./modelSearchParameters.js"; +export { ModelSearchResponse } from "./modelSearchResponse.js"; +export { Models } from "./models.js"; + diff --git a/src/v2/search/models/modelSearchParameters.ts b/src/v2/search/models/modelSearchParameters.ts new file mode 100644 index 00000000..c01bb31d --- /dev/null +++ b/src/v2/search/models/modelSearchParameters.ts @@ -0,0 +1,42 @@ +import { BaseSearchParameters, BaseSearchParametersConstructor } from "@/v2/clientOptions/baseSearchParameters.js"; + +/** + * Constructor parameters for ModelSearchParameters. + */ +export interface ModelSearchParametersConstructor extends BaseSearchParametersConstructor { + name?: string; + modelType?: string; +} + +/** + * Search parameters for models. + */ +export class ModelSearchParameters extends BaseSearchParameters { + /** + * Case-insensitive search term for the model name + */ + name?: string; + + /** + * Case-insensitive search term for the model type + */ + modelType?: string; + + constructor(params: ModelSearchParametersConstructor = {}) { + super(params); + this.name = params.name; + this.modelType = params.modelType; + } + + /** @inheritdoc */ + getRequestParameters(): Record { + const parameters = super.getRequestParameters(); + if (this.name) { + parameters["name"] = this.name; + } + if (this.modelType) { + parameters["model_type"] = this.modelType; + } + return parameters; + } +} diff --git a/src/v2/search/models/modelSearchResponse.ts b/src/v2/search/models/modelSearchResponse.ts new file mode 100644 index 00000000..e685369d --- /dev/null +++ b/src/v2/search/models/modelSearchResponse.ts @@ -0,0 +1,23 @@ +import { StringDict } from "@/parsing/index.js"; +import { BaseSearchResponse } from "@/v2/parsing/search/baseSearchResponse.js"; +import { SearchModels } from "@/v2/parsing/search/searchModels.js"; + +/** + * Models search response. + */ +export class ModelSearchResponse extends BaseSearchResponse { + + /** + * List of models returned by the search. + */ + public models: SearchModels; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.models = new SearchModels(serverResponse["models"] ?? []); + } + + protected bodyLines(): string[] { + return ["Models", "#######", this.models.toString()]; + } +} diff --git a/src/v2/search/models/models.ts b/src/v2/search/models/models.ts new file mode 100644 index 00000000..9799652f --- /dev/null +++ b/src/v2/search/models/models.ts @@ -0,0 +1,23 @@ +import { ModelSearchParameters } from "@/v2/search/index.js"; +import { BaseSearch } from "@/v2/search/baseSearch.js"; +import { ModelSearchResponse } from "@/v2/search/models/modelSearchResponse.js"; + +/** + * Search for models. + */ +export class Models extends BaseSearch { + /** @inheritDoc */ + static get parametersClass() { + return ModelSearchParameters; + } + + /** @inheritDoc */ + static get responseClass() { + return ModelSearchResponse; + } + + /** @inheritDoc */ + static get slug() { + return "models"; + } +} diff --git a/src/v2/search/ragDocuments/index.ts b/src/v2/search/ragDocuments/index.ts new file mode 100644 index 00000000..5e57ce92 --- /dev/null +++ b/src/v2/search/ragDocuments/index.ts @@ -0,0 +1,3 @@ +export { RagDocumentSearchParameters } from "./ragDocumentSearchParameters.js"; +export { RagDocumentSearchResponse } from "./ragDocumentSearchResponse.js"; +export { RagDocuments } from "./ragDocuments.js"; diff --git a/src/v2/search/ragDocuments/ragDocumentSearchParameters.ts b/src/v2/search/ragDocuments/ragDocumentSearchParameters.ts new file mode 100644 index 00000000..6e78eab2 --- /dev/null +++ b/src/v2/search/ragDocuments/ragDocumentSearchParameters.ts @@ -0,0 +1,45 @@ +import { MindeeConfigurationError } from "@/errors/index.js"; +import { BaseSearchParameters, BaseSearchParametersConstructor } from "@/v2/clientOptions/baseSearchParameters.js"; + +/** + * Constructor parameters for RagDocumentSearchParameters. + */ +export interface RagDocumentSearchParametersConstructor extends BaseSearchParametersConstructor { + modelId?: string; + filename?: string; +} + +/** + * Search parameters for RAG Documents. + */ +export class RagDocumentSearchParameters extends BaseSearchParameters { + /** + * Model identifier to search in. + */ + modelId?: string; + + /** + * Case-insensitive substring search on filename. + */ + filename?: string; + + constructor(params: RagDocumentSearchParametersConstructor = {}) { + super(params); + this.modelId = params.modelId; + this.filename = params.filename; + } + + /** @inheritdoc */ + getRequestParameters(): Record { + const parameters = super.getRequestParameters(); + if (this.modelId) { + parameters["model_id"] = this.modelId; + } else { + throw new MindeeConfigurationError("ModelId is required in RagDocumentSearchParameters"); + } + if (this.filename) { + parameters["filename"] = this.filename; + } + return parameters; + } +} diff --git a/src/v2/search/ragDocuments/ragDocumentSearchResponse.ts b/src/v2/search/ragDocuments/ragDocumentSearchResponse.ts new file mode 100644 index 00000000..37080608 --- /dev/null +++ b/src/v2/search/ragDocuments/ragDocumentSearchResponse.ts @@ -0,0 +1,23 @@ +import { StringDict } from "@/parsing/index.js"; +import { BaseSearchResponse } from "@/v2/parsing/search/index.js"; +import { SearchRagDocuments } from "@/v2/parsing/search/searchRagDocuments.js"; + +/** + * RAG documents search response. + */ +export class RagDocumentSearchResponse extends BaseSearchResponse { + + /** + * Paginated list of matching RAG documents. + */ + public ragDocuments: SearchRagDocuments; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.ragDocuments = new SearchRagDocuments(serverResponse["rag_documents"] ?? []); + } + + protected bodyLines(): string[] { + return ["RAG Documents", "################", this.ragDocuments.toString()]; + } +} diff --git a/src/v2/search/ragDocuments/ragDocuments.ts b/src/v2/search/ragDocuments/ragDocuments.ts new file mode 100644 index 00000000..23e4ad3e --- /dev/null +++ b/src/v2/search/ragDocuments/ragDocuments.ts @@ -0,0 +1,23 @@ +import { BaseSearch } from "@/v2/search/baseSearch.js"; +import { RagDocumentSearchParameters } from "@/v2/search/index.js"; +import { RagDocumentSearchResponse } from "@/v2/search/ragDocuments/ragDocumentSearchResponse.js"; + +/** + * Search for RAG Documents. + */ +export class RagDocuments extends BaseSearch { + /** @inheritDoc */ + static get parametersClass() { + return RagDocumentSearchParameters; + } + + /** @inheritDoc */ + static get responseClass() { + return RagDocumentSearchResponse; + } + + /** @inheritDoc */ + static get slug() { + return "rag-documents"; + } +} diff --git a/tests/v1/extraction/invoiceSplitter.integration.ts b/tests/v1/extraction/invoiceSplitter.integration.ts index d09bd897..582d53bc 100644 --- a/tests/v1/extraction/invoiceSplitter.integration.ts +++ b/tests/v1/extraction/invoiceSplitter.integration.ts @@ -11,7 +11,7 @@ import { hasAllOptionalDependencies } from "../../helpers/optionalDeps.js"; const hasOptionals = hasAllOptionalDependencies(); describe("MindeeV1 - Integration - InvoiceSplitterV1 #OptionalDepsRequired", - { timeout: 60000, skip: !hasOptionals }, () => { + { timeout: 80000, skip: !hasOptionals }, () => { let client: mindee.v1.Client; beforeEach(() => { diff --git a/tests/v1/extraction/multiReceipts.integration.ts b/tests/v1/extraction/multiReceipts.integration.ts index 0ef87aa0..c766cab6 100644 --- a/tests/v1/extraction/multiReceipts.integration.ts +++ b/tests/v1/extraction/multiReceipts.integration.ts @@ -14,7 +14,7 @@ const apiKey = process.env.MINDEE_API_KEY; let client: Client; let sourceDoc: LocalInputSource; describe("MindeeV1 - Integration - Multi-Receipt Extraction #OptionalDepsRequired", - { timeout: 60000, skip: !hasOptionals }, () => { + { timeout: 80000, skip: !hasOptionals }, () => { describe("A Multi-Receipt PDF", () => { before(async () => { sourceDoc = new PathInput({ diff --git a/tests/v1/extras/extras.integration.ts b/tests/v1/extras/extras.integration.ts index f9822f2d..37721e65 100644 --- a/tests/v1/extras/extras.integration.ts +++ b/tests/v1/extras/extras.integration.ts @@ -5,7 +5,7 @@ import path from "path"; import { V1_PRODUCT_PATH } from "../../index.js"; -describe("MindeeV1 - Integration - Extras", { timeout: 70000 }, () => { +describe("MindeeV1 - Integration - Extras", { timeout: 80000 }, () => { let client: mindee.v1.Client; beforeEach(() => { diff --git a/tests/v1/input/sources.integration.ts b/tests/v1/input/sources.integration.ts index f3476b39..d62b08e5 100644 --- a/tests/v1/input/sources.integration.ts +++ b/tests/v1/input/sources.integration.ts @@ -8,7 +8,7 @@ import { InvoiceV4 } from "@/v1/product/index.js"; import { V1_PRODUCT_PATH } from "../../index.js"; import { PathInput, Base64Input, BufferInput, BytesInput, UrlInput } from "@/index.js"; -describe("MindeeV1 - Integration - File Input", { timeout: 60000 }, () => { +describe("MindeeV1 - Integration - File Input", { timeout: 80000 }, () => { let client: mindee.v1.Client; let filePath: string; diff --git a/tests/v1/input/urlInputSource.integration.ts b/tests/v1/input/urlInputSource.integration.ts index f5b79af1..92d4f3fd 100644 --- a/tests/v1/input/urlInputSource.integration.ts +++ b/tests/v1/input/urlInputSource.integration.ts @@ -4,7 +4,7 @@ import { UrlInput } from "@/index.js"; import { Client } from "@/v1/index.js"; import { InvoiceV4 } from "@/v1/product/index.js"; -describe("MindeeV1 - Integration - URL Input", { timeout: 60000 }, () => { +describe("MindeeV1 - Integration - URL Input", { timeout: 80000 }, () => { it("should retrieve and parse a remote file with redirection", async () => { const apiKey = process.env.MINDEE_API_KEY; if (!apiKey) { diff --git a/tests/v1/workflows/workflow.integration.ts b/tests/v1/workflows/workflow.integration.ts index 25cc1e74..dfb9e5ef 100644 --- a/tests/v1/workflows/workflow.integration.ts +++ b/tests/v1/workflows/workflow.integration.ts @@ -9,7 +9,7 @@ import { FinancialDocumentV1 } from "@/v1/product/index.js"; import { RAGExtra } from "@/v1/parsing/common/extras/ragExtra.js"; import { V1_PRODUCT_PATH } from "../../index.js"; -describe("MindeeV1 - Integration - Workflow calls", { timeout: 60000 }, () => { +describe("MindeeV1 - Integration - Workflow calls", { timeout: 80000 }, () => { let client: mindee.v1.Client; let sample: LocalInputSource; let workflowId: string; diff --git a/tests/v2/parsing/search.spec.ts b/tests/v2/parsing/search.spec.ts deleted file mode 100644 index 4c84de98..00000000 --- a/tests/v2/parsing/search.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import path from "path"; -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { LocalResponse } from "@/v2/index.js"; -import { SearchResponse } from "@/v2/parsing/search/index.js"; -import { V2_RESOURCE_PATH } from "../../index.js"; - -const filePath = path.join(V2_RESOURCE_PATH, "search/models.json"); - -describe("MindeeV2 - Search Models", () => { - it("should load search models locally", async () => { - const localResponse = new LocalResponse(filePath); - const response = await localResponse.deserializeResponse(SearchResponse); - - assert.ok(response instanceof SearchResponse); - - assert.strictEqual(response.models.length, 5); - assert.strictEqual(response.pagination.totalItems, 5); - assert.strictEqual(response.pagination.page, 1); - assert.strictEqual(response.pagination.perPage, 50); - assert.strictEqual(response.pagination.totalPages, 1); - - const firstModel = response.models[0]; - assert.strictEqual(firstModel.name, "Extraction With Webhooks"); - assert.strictEqual(firstModel.id, "afde5151-aa11-aa11-9289-fa04e50ca3b9"); - assert.strictEqual(firstModel.modelType, "extraction"); - - assert.strictEqual(firstModel.webhooks.length, 2); - assert.strictEqual(firstModel.webhooks[0].id, "a2286ed9-aa11-aa11-bdc5-2f8496c5641a"); - assert.strictEqual(firstModel.webhooks[0].name, "FAILURE"); - assert.strictEqual(firstModel.webhooks[0].url, "https://failure.mindee.com"); - - const lastModel = response.models[response.models.length - 1]; - assert.strictEqual(lastModel.name, "Extraction Without Webhooks Key"); - assert.strictEqual(lastModel.id, "e14e0923-ee55-ee55-a335-8d2110917d7b"); - }); -}); diff --git a/tests/v2/search/modelSearch.integration.ts b/tests/v2/search/modelSearch.integration.ts new file mode 100644 index 00000000..1ddfed0c --- /dev/null +++ b/tests/v2/search/modelSearch.integration.ts @@ -0,0 +1,34 @@ +import { describe } from "node:test"; +import assert from "node:assert/strict"; +import { Client } from "@/v2/index.js"; +import { beforeEach } from "node:test"; +import { it } from "node:test"; +import { Models, ModelSearchResponse } from "@/v2/search/index.js"; + +describe("MindeeV2 - Integration - Model Search", { timeout: 120000 }, () => { + let client: Client; + + beforeEach(() => { + const apiKey = process.env["MINDEE_V2_API_KEY"] ?? ""; + + client = new Client({ apiKey: apiKey, debug: true }); + }); + + it("model search must have results", async () => { + const response: ModelSearchResponse = await client.search(Models, {}); + assert.ok(response); + assert.ok(response.models.length > 0); + assert.ok(response.pagination); + assert.ok(response.pagination.totalItems >= 1); + assert.equal(response.pagination.page, 1); + }); + + it("model search must return empty", async () => { + const response: ModelSearchResponse = await client.search(Models, { name: "je n'existe pas tralala" }); + assert.ok(response); + assert.equal(response.models.length, 0); + assert.ok(response.pagination); + assert.equal(response.pagination.totalItems, 0); + assert.equal(response.pagination.page, 1); + }); +}); diff --git a/tests/v2/search/modelSearch.spec.ts b/tests/v2/search/modelSearch.spec.ts new file mode 100644 index 00000000..6e58a015 --- /dev/null +++ b/tests/v2/search/modelSearch.spec.ts @@ -0,0 +1,38 @@ +import path from "path"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { LocalResponse } from "@/v2/index.js"; +import { ModelSearchResponse } from "@/v2/search/index.js"; +import { V2_RESOURCE_PATH } from "../../index.js"; + +const filePath = path.join(V2_RESOURCE_PATH, "search/models.json"); + +describe("MindeeV2 - Search Models", () => { + it("should load search models locally", async () => { + const localResponse = new LocalResponse(filePath); + const response = await localResponse.deserializeResponse(ModelSearchResponse); + + assert.ok(response instanceof ModelSearchResponse); + + assert.strictEqual(response.models.length, 5); + assert.strictEqual(response.pagination.totalItems, 5); + assert.strictEqual(response.pagination.page, 1); + assert.strictEqual(response.pagination.perPage, 50); + assert.strictEqual(response.pagination.totalPages, 1); + + const firstItem = response.models[0]; + assert.strictEqual(firstItem.name, "Extraction With Webhooks"); + assert.strictEqual(firstItem.id, "afde5151-aa11-aa11-9289-fa04e50ca3b9"); + assert.strictEqual(firstItem.modelType, "extraction"); + + assert.strictEqual(firstItem.webhooks.length, 2); + assert.strictEqual(firstItem.webhooks[0].id, "a2286ed9-aa11-aa11-bdc5-2f8496c5641a"); + assert.strictEqual(firstItem.webhooks[0].name, "FAILURE"); + assert.strictEqual(firstItem.webhooks[0].url, "https://failure.mindee.com"); + + const lastItem = response.models[response.models.length - 1]; + assert.strictEqual(lastItem.name, "Extraction Without Webhooks Key"); + assert.strictEqual(lastItem.id, "e14e0923-ee55-ee55-a335-8d2110917d7b"); + }); +}); diff --git a/tests/v2/search/ragDocumentSearch.integration.ts b/tests/v2/search/ragDocumentSearch.integration.ts new file mode 100644 index 00000000..df434623 --- /dev/null +++ b/tests/v2/search/ragDocumentSearch.integration.ts @@ -0,0 +1,27 @@ +import { describe } from "node:test"; +import assert from "node:assert/strict"; +import { Client } from "@/v2/index.js"; +import { beforeEach } from "node:test"; +import { it } from "node:test"; +import { RagDocuments, RagDocumentSearchResponse } from "@/v2/search/index.js"; + +describe("MindeeV2 - Integration - RAG Document Search", { timeout: 120000 }, () => { + let client: Client; + let findocModelId: string; + + beforeEach(() => { + const apiKey = process.env["MINDEE_V2_API_KEY"] ?? ""; + findocModelId = process.env["MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID"] ?? ""; + + client = new Client({ apiKey: apiKey, debug: true }); + }); + + it("RAG Document search must have results", async () => { + const response: RagDocumentSearchResponse = await client.search(RagDocuments, { modelId: findocModelId }); + assert.ok(response); + assert.ok(response.ragDocuments.length > 0); + assert.ok(response.pagination); + assert.ok(response.pagination.totalItems >= 1); + assert.equal(response.pagination.page, 1); + }); +}); diff --git a/tests/v2/search/ragDocumentSearch.spec.ts b/tests/v2/search/ragDocumentSearch.spec.ts new file mode 100644 index 00000000..064db50d --- /dev/null +++ b/tests/v2/search/ragDocumentSearch.spec.ts @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import path from "path"; +import { V2_RESOURCE_PATH } from "../../index.js"; +import { LocalResponse } from "@/v2/index.js"; +import { RagDocumentSearchResponse } from "@/v2/search/index.js"; + + +const filePath = path.join(V2_RESOURCE_PATH, "search/rag_documents.json"); + +describe("MindeeV2 - Search RAG Documents", () => { + it("should load search RAG Documents locally", async () => { + const localResponse = new LocalResponse(filePath); + const response = await localResponse.deserializeResponse(RagDocumentSearchResponse); + + assert.ok(response instanceof RagDocumentSearchResponse); + + assert.strictEqual(response.ragDocuments.length, 3); + assert.strictEqual(response.pagination.totalItems, 3); + assert.strictEqual(response.pagination.page, 1); + assert.strictEqual(response.pagination.perPage, 50); + assert.strictEqual(response.pagination.totalPages, 1); + + const firstItem = response.ragDocuments[0]; + assert.strictEqual(firstItem.id, "cc831599-c545-48b7-aa27-6d7ccd5b8d32"); + assert.strictEqual(firstItem.modelId, "12345678-1234-1234-1234-123456789abc"); + assert.strictEqual(firstItem.filename, "invoice_01.pdf"); + assert.deepStrictEqual(firstItem.createdAt, new Date("2026-06-30T13:13:46.168586Z")); + assert.strictEqual(firstItem.totalMatches, 0); + assert.strictEqual(firstItem.lastMatchAt, undefined); + assert.strictEqual(firstItem.status, "Processing"); + + const secondItem = response.ragDocuments[1]; + assert.strictEqual(secondItem.id, "27467e4c-5602-4315-90d9-3d2da69b05ab"); + assert.strictEqual(secondItem.modelId, "12345678-1234-1234-1234-123456789abc"); + assert.strictEqual(secondItem.filename, "invoice_02.pdf"); + assert.deepStrictEqual(secondItem.createdAt, new Date("2026-06-30T13:13:46.168586Z")); + assert.strictEqual(secondItem.totalMatches, 0); + assert.strictEqual(secondItem.lastMatchAt, undefined); + assert.strictEqual(secondItem.status, "Draft"); + + const thirdItem = response.ragDocuments[2]; + assert.strictEqual(thirdItem.id, "a6bcae7d-0439-476b-8a63-5a39ec05dc21"); + assert.strictEqual(thirdItem.modelId, "12345678-1234-1234-1234-jobid1234567"); + assert.strictEqual(thirdItem.filename, "invoice_03.pdf"); + assert.deepStrictEqual(thirdItem.createdAt, new Date("2026-06-17T14:35:46.228006Z")); + assert.strictEqual(thirdItem.totalMatches, 5); + assert.deepStrictEqual(thirdItem.lastMatchAt, new Date("2026-06-18T14:35:46.248006Z")); + assert.strictEqual(thirdItem.status, "Active"); + }); +});