diff --git a/docs/code_samples/default_v2.txt b/docs/code_samples/default_v2.txt index bf95a030..d909f3a0 100644 --- a/docs/code_samples/default_v2.txt +++ b/docs/code_samples/default_v2.txt @@ -33,6 +33,7 @@ const inputSource = new mindee.PathInput({ inputPath: filePath }); // Send for processing const response = mindeeClient.enqueueAndGetInference( + mindee.v2.ExtractionInference, inputSource, inferenceParams ); diff --git a/src/http/apiCore.ts b/src/http/apiCore.ts index 995e1f1d..98819997 100644 --- a/src/http/apiCore.ts +++ b/src/http/apiCore.ts @@ -62,7 +62,7 @@ export async function sendRequestAndReadResponse( } try { const parsedResponse = JSON.parse(responseBody); - logger.debug("JSON parsed successfully, returning object."); + logger.debug("JSON parsed successfully, returning plain object."); return { messageObj: response, data: parsedResponse, diff --git a/src/index.ts b/src/index.ts index 06aef230..cbcf42fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,12 +18,21 @@ export * as v1 from "./v1/index.js"; export * as v2 from "./v2/index.js"; export { Client, + ExtractionParameters, + DataSchema, InferenceFile, - InferenceResponse, + InferenceModel, + ClassificationInference, + ClassificationResponse, + CropInference, + CropResponse, + ExtractionInference, + ExtractionResponse, + OcrInference, + OcrResponse, + SplitInference, + SplitResponse, JobResponse, - RawText, - RagMetadata, - InferenceParameters, - DataSchema, + ErrorResponse, } from "./v2/index.js"; export type { PollingOptions } from "./v2/index.js"; diff --git a/src/input/localInputSource.ts b/src/input/localInputSource.ts index ec27955a..cf18216a 100644 --- a/src/input/localInputSource.ts +++ b/src/input/localInputSource.ts @@ -54,7 +54,7 @@ export abstract class LocalInputSource extends InputSource { ); } this.inputType = inputType; - logger.debug(`Loading file from: ${inputType}`); + logger.debug(`New local input source of type: ${inputType}`); } protected async checkMimetype(): Promise { diff --git a/src/v2/cli.ts b/src/v2/cli.ts index bedb1413..af9c5b8e 100644 --- a/src/v2/cli.ts +++ b/src/v2/cli.ts @@ -2,7 +2,15 @@ import { Command, OptionValues } from "commander"; import { Client } from "./client.js"; import { PathInput } from "../input/index.js"; import * as console from "console"; -import { Inference } from "@/v2/parsing/index.js"; +import { + BaseInference, + ClassificationInference, + CropInference, + ExtractionInference, + OcrInference, + SplitInference, + InferenceResponseConstructor, +} from "@/v2/parsing/inference/index.js"; const program = new Command(); @@ -18,20 +26,25 @@ function initClient(options: OptionValues): Client { }); } -async function callEnqueueAndGetInference( +async function enqueueAndGetInference( + responseType: InferenceResponseConstructor, inputPath: string, - options: any + options: OptionValues ): Promise { const mindeeClient = initClient(options); const inputSource = new PathInput({ inputPath: inputPath }); - const response = await mindeeClient.enqueueAndGetInference(inputSource, { - modelId: options.model, - pollingOptions: { - initialDelaySec: 2, - delaySec: 1.5, - maxRetries: 80, + const response = await mindeeClient.enqueueAndGetInference( + responseType, + inputSource, + { + modelId: options.model, + pollingOptions: { + initialDelaySec: 2, + delaySec: 1.5, + maxRetries: 80, + } } - }); + ); if (!response.inference) { throw Error("Inference could not be retrieved"); } @@ -39,7 +52,7 @@ async function callEnqueueAndGetInference( } function printResponse( - document: Inference, + document: BaseInference, ): void { if (document) { console.log(`\n${document}`); @@ -55,26 +68,37 @@ function addMainOptions(prog: Command) { "-m, --model ", "Model ID (required)" ); - prog.option("-k, --api-key ", "API key for document endpoint"); prog.argument("", "full path to the file"); } export function cli() { program.name("mindee") - .description("Command line interface for Mindee products.") - .option("-d, --debug", "high verbosity mode"); + .description("Command line interface for Mindee V2 products.") + .option("-d, --debug", "high verbosity mode") + .option("-k, --api-key ", "your Mindee API key"); - const inferenceCmd: Command = program.command("parse") - .description("Send a file and retrieve the inference results."); + const inferenceTypes = [ + { name: "extract", description: "Extract data from a document.", responseType: ExtractionInference }, + { name: "crop", description: "Crop a document.", responseType: CropInference }, + { name: "split", description: "Split a document into pages.", responseType: SplitInference }, + { name: "ocr", description: "Read text from a document.", responseType: OcrInference }, + { name: "classify", description: "Classify a document.", responseType: ClassificationInference }, + ]; - addMainOptions(inferenceCmd); + for (const inference of inferenceTypes) { + const inferenceCmd: Command = program.command(inference.name) + .description(inference.description); - inferenceCmd.action(function ( - inputPath: string, - options: OptionValues, - ) { - return callEnqueueAndGetInference(inputPath, options); - }); + addMainOptions(inferenceCmd); + + inferenceCmd.action(function ( + inputPath: string, + options: OptionValues, + ) { + const allOptions = { ...program.opts(), ...options }; + return enqueueAndGetInference(inference.responseType, inputPath, allOptions); + }); + } program.parse(process.argv); } diff --git a/src/v2/client.ts b/src/v2/client.ts index 698b9ac6..0e71be79 100644 --- a/src/v2/client.ts +++ b/src/v2/client.ts @@ -1,12 +1,20 @@ import { setTimeout } from "node:timers/promises"; import { Dispatcher } from "undici"; import { InputSource } from "@/input/index.js"; +import { MindeeError } from "@/errors/index.js"; import { errorHandler } from "@/errors/handler.js"; import { LOG_LEVELS, logger } from "@/logger.js"; -import { ErrorResponse, InferenceResponse, JobResponse } from "./parsing/index.js"; +import { + ErrorResponse, + ExtractionInference, + JobResponse, + InferenceResponseConstructor, + BaseInference, + BaseInferenceResponse, +} from "./parsing/index.js"; import { MindeeApiV2 } from "./http/mindeeApiV2.js"; import { MindeeHttpErrorV2 } from "./http/errors.js"; -import { InferenceParameters } from "./client/index.js"; +import { ExtractionParameters, UtilityParameters, ValidatedPollingOptions } from "./client/index.js"; /** * Options for the V2 Mindee Client. @@ -22,8 +30,6 @@ import { InferenceParameters } from "./client/index.js"; export interface ClientOptions { /** Your API key for all endpoints. */ apiKey?: string; - /** Raise an `Error` on errors. */ - throwOnError?: boolean; /** Log debug messages. */ debug?: boolean; /** Custom Dispatcher instance for the HTTP requests. */ @@ -43,15 +49,14 @@ export class Client { * @param {ClientOptions} options options for the initialization of a client. */ constructor( - { apiKey, throwOnError, debug, dispatcher }: ClientOptions = { + { apiKey, debug, dispatcher }: ClientOptions = { apiKey: undefined, - throwOnError: true, debug: false, dispatcher: undefined, } ) { this.mindeeApi = new MindeeApiV2(dispatcher, apiKey); - errorHandler.throwOnError = throwOnError ?? true; + errorHandler.throwOnError = true; logger.level = debug ?? process.env.MINDEE_DEBUG ? LOG_LEVELS["debug"] @@ -66,33 +71,74 @@ export class Client { * @category Asynchronous * @returns a `Promise` containing the job (queue) corresponding to a document. */ - async enqueueInference( + async enqueueExtraction( inputSource: InputSource, - params: InferenceParameters| ConstructorParameters[0] + params: ExtractionParameters| ConstructorParameters[0] ): Promise { if (inputSource === undefined) { - throw new Error("The 'enqueue' function requires an input document."); + throw new MindeeError("An input document is required."); } - const inferenceParams = params instanceof InferenceParameters + const paramsInstance = params instanceof ExtractionParameters ? params - : new InferenceParameters(params); + : new ExtractionParameters(params); await inputSource.init(); + const jobResponse = await this.mindeeApi.reqPostInferenceEnqueue( + ExtractionInference, inputSource, paramsInstance + ); + if (jobResponse.job.id === undefined || jobResponse.job.id.length === 0) { + logger.error(`Failed enqueueing:\n${jobResponse.getRawHttp()}`); + throw new MindeeError("Enqueueing of the document failed."); + } + logger.debug( + `Successfully enqueued document with job ID: ${jobResponse.job.id}.` + ); + return jobResponse; + } + + async enqueueInference( + responseType: InferenceResponseConstructor, + inputSource: InputSource, + params: UtilityParameters | ConstructorParameters[0] + ): Promise { + if (inputSource === undefined) { + throw new MindeeError("An input document is required."); + } + const paramsInstance = params instanceof UtilityParameters + ? params + : new UtilityParameters(params); - return await this.mindeeApi.reqPostInferenceEnqueue(inputSource, inferenceParams); + await inputSource.init(); + const jobResponse = await this.mindeeApi.reqPostInferenceEnqueue( + responseType, inputSource, paramsInstance + ); + if (jobResponse.job.id === undefined || jobResponse.job.id.length === 0) { + logger.error(`Failed enqueueing:\n${jobResponse.getRawHttp()}`); + throw new MindeeError("Enqueueing of the document failed."); + } + logger.debug( + `Successfully enqueued document with job ID: ${jobResponse.job.id}.` + ); + return jobResponse; } /** * Retrieves an inference. * + * @param responseType class of the inference to retrieve. * @param inferenceId id of the queue to poll. * @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`. * @category Asynchronous - * @returns a `Promise` containing a `Job`, which also contains a `Document` if the - * parsing is complete. + * @returns a `Promise` containing the inference. */ - async getInference(inferenceId: string): Promise { - return await this.mindeeApi.reqGetInference(inferenceId); + async getInference( + responseType: InferenceResponseConstructor, + inferenceId: string + ): Promise> { + logger.debug( + `Attempting to get inference with ID: ${inferenceId} using response type: ${responseType.name}` + ); + return await this.mindeeApi.reqGetInference(responseType, inferenceId); } /** @@ -113,6 +159,7 @@ export class Client { * Send a document to an endpoint and poll the server until the result is sent or * until the maximum number of tries is reached. * + * @param responseType class of the inference to retrieve. * @param inputSource file or URL to parse. * @param params parameters relating to prediction options. * @@ -120,58 +167,73 @@ export class Client { * @category Synchronous * @returns a `Promise` containing parsing results. */ - async enqueueAndGetInference( + async enqueueAndGetInference( + responseType: InferenceResponseConstructor, inputSource: InputSource, - params: InferenceParameters| ConstructorParameters[0] - ): Promise { - const inferenceParams = params instanceof InferenceParameters + params: UtilityParameters | ConstructorParameters[0] + ): Promise> { + const paramsInstance = params instanceof UtilityParameters ? params - : new InferenceParameters(params); + : new UtilityParameters(params); - const pollingOptions = inferenceParams.getValidatedPollingOptions(); + const pollingOptions = paramsInstance.getValidatedPollingOptions(); - const enqueueResponse: JobResponse = await this.enqueueInference(inputSource, params); - if (enqueueResponse.job.id === undefined || enqueueResponse.job.id.length === 0) { - logger.error(`Failed enqueueing:\n${enqueueResponse.getRawHttp()}`); - throw Error("Enqueueing of the document failed."); - } - const queueId: string = enqueueResponse.job.id; - logger.debug( - `Successfully enqueued document with job id: ${queueId}.` + const jobResponse: JobResponse = await this.enqueueInference( + responseType, inputSource, paramsInstance + ); + return await this.pollForInference( + responseType, pollingOptions, jobResponse.job.id ); + } + /** + * Send a document to an endpoint and poll the server until the result is sent or + * until the maximum number of tries is reached. + * @protected + */ + protected async pollForInference( + responseType: InferenceResponseConstructor, + pollingOptions: ValidatedPollingOptions, + queueId: string, + ): Promise> { + logger.debug( + `Waiting ${pollingOptions.initialDelaySec} seconds before polling.` + ); await setTimeout( pollingOptions.initialDelaySec * 1000, undefined, pollingOptions.initialTimerOptions ); + logger.debug( + `Start polling for inference using job ID: ${queueId}.` + ); let retryCounter: number = 1; - let pollResults: JobResponse = await this.getJob(queueId); - while (retryCounter < pollingOptions.maxRetries) { + let pollResults: JobResponse; + while (retryCounter < pollingOptions.maxRetries + 1) { + logger.debug( + `Attempt ${retryCounter} of ${pollingOptions.maxRetries}` + ); + pollResults = await this.getJob(queueId); + const error: ErrorResponse | undefined = pollResults.job.error; + if (error) { + throw new MindeeHttpErrorV2(error); + } + logger.debug(`Job status: ${pollResults.job.status}.`); if (pollResults.job.status === "Failed") { break; } if (pollResults.job.status === "Processed") { - return this.getInference(pollResults.job.id); + return this.getInference(responseType, pollResults.job.id); } - logger.debug( - `Polling server for parsing result with queueId: ${queueId}. -Attempt no. ${retryCounter} of ${pollingOptions.maxRetries}. -Job status: ${pollResults.job.status}.` - ); await setTimeout( pollingOptions.delaySec * 1000, undefined, pollingOptions.recurringTimerOptions ); - pollResults = await this.getJob(queueId); retryCounter++; } - const error: ErrorResponse | undefined = pollResults.job.error; - if (error) { - throw new MindeeHttpErrorV2(error); - } - throw Error( + + throw new MindeeError( "Asynchronous parsing request timed out after " + pollingOptions.delaySec * retryCounter + " seconds" diff --git a/src/v2/client/inferenceParameters.ts b/src/v2/client/baseParameters.ts similarity index 58% rename from src/v2/client/inferenceParameters.ts rename to src/v2/client/baseParameters.ts index 13687a30..e2f3263c 100644 --- a/src/v2/client/inferenceParameters.ts +++ b/src/v2/client/baseParameters.ts @@ -1,6 +1,17 @@ -import { StringDict } from "@/parsing/stringDict.js"; -import { PollingOptions, ValidatedPollingOptions } from "./pollingOptions.js"; -import { DataSchema } from "./dataSchema.js"; +import { ValidatedPollingOptions } from "@/v2/client/pollingOptions.js"; +import { PollingOptions } from "@/v2/index.js"; +import { MindeeConfigurationError } from "@/errors/index.js"; + +/** + * Constructor parameters for BaseParameters and its subclasses. + */ +export interface BaseParametersConstructor { + modelId: string; + alias?: string; + webhookIds?: string[]; + pollingOptions?: PollingOptions; + closeFile?: boolean; +} /** * Parameters accepted by the asynchronous **inference** v2 endpoint. @@ -20,38 +31,16 @@ import { DataSchema } from "./dataSchema.js"; * } * }; */ -export class InferenceParameters { +export abstract class BaseParameters { /** * Model ID to use for the inference. **Required.** */ modelId: string; - /** - * Use Retrieval-Augmented Generation during inference. - */ - rag?: boolean; - /** - * Extract the entire text from the document as strings, and fill the `rawText` attribute. - */ - rawText?: boolean; - /** - * Calculate bounding box polygons for values, and fill the `locations` attribute of fields. - */ - polygon?: boolean; - /** - * Calculate confidence scores for values, and fill the `confidence` attribute of fields. - * Useful for automation. - */ - confidence?: boolean; /** * Use an alias to link the file to your own DB. * If empty, no alias will be used. */ alias?: string; - /** - * Additional text context used by the model during inference. - * *Not recommended*, for specific use only. - */ - textContext?: string; /** * Webhook IDs to call after all processing is finished. * If empty, no webhooks will be used. @@ -66,43 +55,16 @@ export class InferenceParameters { * Set to `false` to keep it open. */ closeFile?: boolean; - /** - * Dynamic changes to the data schema of the model for this inference. - * Not recommended, for specific use only. - */ - dataSchema?: DataSchema | StringDict | string; - constructor(params: { - modelId: string; - rag?: boolean; - rawText?: boolean; - polygon?: boolean; - confidence?: boolean; - alias?: string; - textContext?: string; - webhookIds?: string[]; - pollingOptions?: PollingOptions; - closeFile?: boolean; - dataSchema?: DataSchema | StringDict | string; - }) { + protected constructor(params: BaseParametersConstructor) { + if (params.modelId === undefined || params.modelId === null || params.modelId === "") { + throw new MindeeConfigurationError("Model ID must be provided"); + } this.modelId = params.modelId; - this.rag = params.rag; - this.rawText = params.rawText; - this.polygon = params.polygon; - this.confidence = params.confidence; this.alias = params.alias; - this.textContext = params.textContext; this.webhookIds = params.webhookIds; this.closeFile = params.closeFile; this.pollingOptions = params.pollingOptions; - - if (params.dataSchema !== undefined && params.dataSchema !== null) { - if (!(params.dataSchema instanceof DataSchema)){ - this.dataSchema = new DataSchema(params.dataSchema); - } else { - this.dataSchema = params.dataSchema; - } - } } /** @@ -141,4 +103,22 @@ export class InferenceParameters { } return newAsyncParams as ValidatedPollingOptions; } + + /** + * Returns the form data to send to the API. + * @returns A `FormData` object. + */ + getFormData(): FormData { + const form = new FormData(); + + form.set("model_id", this.modelId); + + if (this.alias !== undefined && this.alias !== null) { + form.set("alias", this.alias); + } + if (this.webhookIds && this.webhookIds.length > 0) { + form.set("webhook_ids", this.webhookIds.join(",")); + } + return form; + } } diff --git a/src/v2/client/extractionParameters.ts b/src/v2/client/extractionParameters.ts new file mode 100644 index 00000000..b63f83be --- /dev/null +++ b/src/v2/client/extractionParameters.ts @@ -0,0 +1,104 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { DataSchema } from "./dataSchema.js"; +import { BaseParameters, BaseParametersConstructor } from "@/v2/client/baseParameters.js"; + +/** + * Parameters accepted by the asynchronous **inference** v2 endpoint. + * + * All fields are optional except `modelId`. + * + * @category ClientV2 + * @example + * const params = { + * modelId: "YOUR_MODEL_ID", + * rag: true, + * alias: "YOUR_ALIAS", + * webhookIds: ["YOUR_WEBHOOK_ID_1", "YOUR_WEBHOOK_ID_2"], + * pollingOptions: { + * initialDelaySec: 2, + * delaySec: 1.5, + * } + * }; + */ +export class ExtractionParameters extends BaseParameters { + /** + * Use Retrieval-Augmented Generation during inference. + */ + rag?: boolean; + /** + * Extract the entire text from the document as strings, and fill the `rawText` attribute. + */ + rawText?: boolean; + /** + * Calculate bounding box polygons for values, and fill the `locations` attribute of fields. + */ + polygon?: boolean; + /** + * Calculate confidence scores for values, and fill the `confidence` attribute of fields. + * Useful for automation. + */ + confidence?: boolean; + /** + * Additional text context used by the model during inference. + * *Not recommended*, for specific use only. + */ + textContext?: string; + /** + * Dynamic changes to the data schema of the model for this inference. + * Not recommended, for specific use only. + */ + dataSchema?: DataSchema | StringDict | string; + + constructor(params: BaseParametersConstructor & { + rag?: boolean; + rawText?: boolean; + polygon?: boolean; + confidence?: boolean; + textContext?: string; + dataSchema?: DataSchema | StringDict | string; + }) { + super({ ...params }); + this.rag = params.rag; + this.rawText = params.rawText; + this.polygon = params.polygon; + this.confidence = params.confidence; + this.textContext = params.textContext; + + if (params.dataSchema !== undefined && params.dataSchema !== null) { + if (!(params.dataSchema instanceof DataSchema)){ + this.dataSchema = new DataSchema(params.dataSchema); + } else { + this.dataSchema = params.dataSchema; + } + } + } + + getFormData(): FormData { + const form = new FormData(); + + form.set("model_id", this.modelId); + + if (this.rag !== undefined && this.rag !== null) { + form.set("rag", this.rag.toString()); + } + if (this.polygon !== undefined && this.polygon !== null) { + form.set("polygon", this.polygon.toString().toLowerCase()); + } + if (this.confidence !== undefined && this.confidence !== null) { + form.set("confidence", this.confidence.toString().toLowerCase()); + } + if (this.rawText !== undefined && this.rawText !== null) { + form.set("raw_text", this.rawText.toString().toLowerCase()); + } + if (this.textContext !== undefined && this.textContext !== null) { + form.set("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(",")); + } + return form; + } +} diff --git a/src/v2/client/index.ts b/src/v2/client/index.ts index a871d3bf..d64ca2b3 100644 --- a/src/v2/client/index.ts +++ b/src/v2/client/index.ts @@ -1,3 +1,4 @@ export { DataSchema } from "./dataSchema.js"; export type { PollingOptions, ValidatedPollingOptions } from "./pollingOptions.js"; -export { InferenceParameters } from "./inferenceParameters.js"; +export { ExtractionParameters } from "./extractionParameters.js"; +export { UtilityParameters } from "./utilityParameters.js"; diff --git a/src/v2/client/utilityParameters.ts b/src/v2/client/utilityParameters.ts new file mode 100644 index 00000000..b46676f0 --- /dev/null +++ b/src/v2/client/utilityParameters.ts @@ -0,0 +1,25 @@ +import { BaseParameters, BaseParametersConstructor } from "@/v2/client/baseParameters.js"; + +/** + * Parameters accepted by the asynchronous **inference** v2 endpoint. + * + * All fields are optional except `modelId`. + * + * @category ClientV2 + * @example + * const params = { + * modelId: "YOUR_MODEL_ID", + * alias: "YOUR_ALIAS", + * webhookIds: ["YOUR_WEBHOOK_ID_1", "YOUR_WEBHOOK_ID_2"], + * pollingOptions: { + * initialDelaySec: 2, + * delaySec: 1.5, + * } + * }; + */ +export class UtilityParameters extends BaseParameters { + + constructor(params: BaseParametersConstructor & {}) { + super({ ...params }); + } +} diff --git a/src/v2/http/mindeeApiV2.ts b/src/v2/http/mindeeApiV2.ts index 3f89533e..72f5f509 100644 --- a/src/v2/http/mindeeApiV2.ts +++ b/src/v2/http/mindeeApiV2.ts @@ -1,12 +1,30 @@ import { ApiSettingsV2 } from "./apiSettingsV2.js"; import { Dispatcher } from "undici"; -import { InferenceParameters } from "@/v2/client/index.js"; -import { ErrorResponse, InferenceResponse, JobResponse } from "@/v2/parsing/index.js"; +import { ExtractionParameters, UtilityParameters } from "@/v2/client/index.js"; +import { + BaseResponse, + ErrorResponse, + ResponseConstructor, + InferenceResponseConstructor, + JobResponse, + CropResponse, + OcrResponse, + SplitResponse, + ExtractionResponse, + BaseInference, ExtractionInference, +} from "@/v2/parsing/index.js"; import { sendRequestAndReadResponse, BaseHttpResponse } from "@/http/apiCore.js"; import { InputSource, LocalInputSource, UrlInput } from "@/input/index.js"; -import { MindeeConfigurationError, MindeeDeserializationError } from "@/errors/index.js"; +import { MindeeDeserializationError } from "@/errors/index.js"; import { MindeeHttpErrorV2 } from "./errors.js"; import { logger } from "@/logger.js"; +import { + BaseInferenceResponse, + CropInference, + OcrInference, + SplitInference +} from "@/v2/parsing/inference/index.js"; + export class MindeeApiV2 { settings: ApiSettingsV2; @@ -15,37 +33,81 @@ export class MindeeApiV2 { this.settings = new ApiSettingsV2({ dispatcher: dispatcher, apiKey: apiKey }); } + #getSlugFromInference( + responseClass: InferenceResponseConstructor + ): string { + switch (responseClass as any) { + case CropInference: + return "utilities/crop"; + case OcrInference: + return "utilities/ocr"; + case SplitInference: + return "utilities/split"; + case ExtractionInference: + return "inferences"; + default: + throw new Error("Unsupported response class."); + } + } + + #getResponseClassFromInference( + inferenceClass: InferenceResponseConstructor + ): ResponseConstructor> { + switch (inferenceClass as any) { + case CropInference: + return CropResponse as any; + case OcrInference: + return OcrResponse as any; + case SplitInference: + return SplitResponse as any; + case ExtractionInference: + return ExtractionResponse as any; + default: + throw new Error("Unsupported inference class."); + } + } + /** - * Sends a file to the inference queue. + * Sends a file to the extraction inference queue. + * @param responseClass Class of the inference to enqueue. * @param inputSource Local file loaded as an input. - * @param params {InferenceParameters} parameters relating to the enqueueing options. + * @param params {ExtractionParameters} parameters relating to the enqueueing options. * @category V2 * @throws Error if the server's response contains one. * @returns a `Promise` containing a job response. */ - async reqPostInferenceEnqueue(inputSource: InputSource, params: InferenceParameters): Promise { + async reqPostInferenceEnqueue( + responseClass: InferenceResponseConstructor, + inputSource: InputSource, + params: ExtractionParameters | UtilityParameters + ): Promise { await inputSource.init(); - if (params.modelId === undefined || params.modelId === null || params.modelId === "") { - throw new MindeeConfigurationError("Model ID must be provided"); - } - const result: BaseHttpResponse = await this.#documentEnqueuePost(inputSource, params); + const slug = this.#getSlugFromInference(responseClass); + const result: BaseHttpResponse = await this.#inferenceEnqueuePost( + inputSource, slug, params + ); if (result.data.error !== undefined) { throw new MindeeHttpErrorV2(result.data.error); } return this.#processResponse(result, JobResponse); } - /** * Requests the job of a queued document from the API. * Throws an error if the server's response contains one. + * @param responseClass * @param inferenceId The document's ID in the queue. * @category Asynchronous * @returns a `Promise` containing either the parsed result, or information on the queue. */ - async reqGetInference(inferenceId: string): Promise { - const queueResponse: BaseHttpResponse = await this.#inferenceResultReqGet(inferenceId, "inferences"); - return this.#processResponse(queueResponse, InferenceResponse); + async reqGetInference( + responseClass: InferenceResponseConstructor, + inferenceId: string, + ): Promise> { + const slug = this.#getSlugFromInference(responseClass); + const queueResponse: BaseHttpResponse = await this.#inferenceResultReqGet(inferenceId, slug); + const actualResponseClass = this.#getResponseClassFromInference(responseClass); + return this.#processResponse(queueResponse, actualResponseClass) as BaseInferenceResponse; } /** @@ -60,8 +122,10 @@ export class MindeeApiV2 { return this.#processResponse(queueResponse, JobResponse); } - #processResponse - (result: BaseHttpResponse, responseType: new (data: { [key: string]: any; }) => T): T { + #processResponse( + result: BaseHttpResponse, + responseClass: ResponseConstructor, + ): T { if (result.messageObj?.statusCode && (result.messageObj?.statusCode > 399 || result.messageObj?.statusCode < 200)) { if (result.data?.status !== null) { throw new MindeeHttpErrorV2(new ErrorResponse(result.data)); @@ -78,7 +142,7 @@ export class MindeeApiV2 { ); } try { - return new responseType(result.data); + return new responseClass(result.data); } catch (e) { logger.error(`Raised '${e}' Couldn't deserialize response object:\n${JSON.stringify(result.data)}`); throw new MindeeDeserializationError("Couldn't deserialize response object."); @@ -89,42 +153,21 @@ export class MindeeApiV2 { * Sends a document to the inference queue. * * @param inputSource Local or remote file as an input. - * @param params {InferenceParameters} parameters relating to the enqueueing options. + * @param slug Slug of the inference to enqueue. + * @param params {ExtractionParameters} parameters relating to the enqueueing options. */ - async #documentEnqueuePost( + async #inferenceEnqueuePost( inputSource: InputSource, - params: InferenceParameters + slug: string, + params: ExtractionParameters | UtilityParameters ): Promise { - const form = new FormData(); - - form.set("model_id", params.modelId); - if (params.rag !== undefined && params.rag !== null) { - form.set("rag", params.rag.toString()); - } - if (params.polygon !== undefined && params.polygon !== null) { - form.set("polygon", params.polygon.toString().toLowerCase()); - } - if (params.confidence !== undefined && params.confidence !== null) { - form.set("confidence", params.confidence.toString().toLowerCase()); - } - if (params.rawText !== undefined && params.rawText !== null) { - form.set("raw_text", params.rawText.toString().toLowerCase()); - } - if (params.textContext !== undefined && params.textContext !== null) { - form.set("text_context", params.textContext); - } - if (params.dataSchema !== undefined && params.dataSchema !== null) { - form.set("data_schema", params.dataSchema.toString()); - } - if (params.webhookIds && params.webhookIds.length > 0) { - form.set("webhook_ids", params.webhookIds.join(",")); - } + const form = params.getFormData(); if (inputSource instanceof LocalInputSource) { form.set("file", new Blob([inputSource.fileObject]), inputSource.filename); } else { form.set("url", (inputSource as UrlInput).url); } - const path = "/v2/inferences/enqueue"; + const path = `/v2/${slug}/enqueue`; const options = { method: "POST", headers: this.settings.baseHeaders, diff --git a/src/v2/index.ts b/src/v2/index.ts index b9f968f6..b8bc8608 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -4,11 +4,19 @@ export { LocalResponse } from "./parsing/localResponse.js"; export { Client } from "./client.js"; export { InferenceFile, - InferenceResponse, + InferenceModel, + ClassificationInference, + ClassificationResponse, + CropInference, + CropResponse, + ExtractionInference, + ExtractionResponse, + OcrInference, + OcrResponse, + SplitInference, + SplitResponse, JobResponse, - RawText, - RagMetadata, ErrorResponse, } from "./parsing/index.js"; -export { InferenceParameters, DataSchema } from "./client/index.js"; +export { ExtractionParameters, DataSchema } from "./client/index.js"; export type { PollingOptions } from "./client/index.js"; diff --git a/src/v2/parsing/index.ts b/src/v2/parsing/index.ts index 60652919..ffb759ef 100644 --- a/src/v2/parsing/index.ts +++ b/src/v2/parsing/index.ts @@ -6,14 +6,27 @@ export type { ErrorDetails } from "./error/index.js"; export { Job, JobResponse, - JobWebhook, + JobWebhook } from "./job/index.js"; -export { InferenceFile } from "./inferenceFile.js"; -export { BaseResponse } from "./baseResponse.js"; -export { Inference } from "./inference.js"; -export { InferenceActiveOptions } from "./inferenceActiveOptions.js"; -export { InferenceModel } from "./inferenceModel.js"; -export { InferenceResponse } from "./inferenceResponse.js"; -export { InferenceResult } from "./inferenceResult.js"; -export { RawText } from "./rawText.js"; -export { RagMetadata } from "./ragMetadata.js"; +export { + BaseInference, + BaseInferenceResponse, + InferenceFile, + InferenceModel, + ExtractionInference, + ExtractionActiveOptions, + ExtractionResponse, + ExtractionResult, + ClassificationResponse, + ClassificationInference, + CropResponse, + CropInference, + OcrResponse, + OcrInference, + SplitResponse, + SplitInference, +} from "./inference/index.js"; +export { LocalResponse } from "./localResponse.js"; +export { RawText, RagMetadata } from "./inference/field/index.js"; +export type { ResponseConstructor, BaseResponse } from "./baseResponse.js"; +export type { InferenceResponseConstructor } from "./inference/index.js"; diff --git a/src/v2/parsing/inference.ts b/src/v2/parsing/inference.ts deleted file mode 100644 index 37a91c7c..00000000 --- a/src/v2/parsing/inference.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { StringDict } from "@/parsing/stringDict.js"; -import { InferenceModel } from "./inferenceModel.js"; -import { InferenceResult } from "./inferenceResult.js"; -import { InferenceFile } from "./inferenceFile.js"; -import { InferenceActiveOptions } from "./inferenceActiveOptions.js"; - -export class Inference { - /** - * Model info for the inference. - */ - public model: InferenceModel; - /** - * File info for the inference. - */ - public file: InferenceFile; - /** - * Result of the inference. - */ - public result: InferenceResult; - /** - * ID of the inference. - */ - public id?: string; - /** - * Active options for the inference. - */ - public activeOptions: InferenceActiveOptions; - - constructor(serverResponse: StringDict) { - this.model = new InferenceModel(serverResponse["model"]); - this.file = new InferenceFile(serverResponse["file"]); - this.result = new InferenceResult(serverResponse["result"]); - this.activeOptions = new InferenceActiveOptions(serverResponse["active_options"]); - } - - toString(): string { - return ( - "Inference\n" + - "#########\n" + - this.model.toString() + "\n" + - this.file.toString() + "\n" + - this.activeOptions.toString() + "\n" + - this.result + "\n" - ); - } -} diff --git a/src/v2/parsing/inference/baseInference.ts b/src/v2/parsing/inference/baseInference.ts new file mode 100644 index 00000000..ab44a6ca --- /dev/null +++ b/src/v2/parsing/inference/baseInference.ts @@ -0,0 +1,24 @@ +import { InferenceModel } from "@/v2/parsing/inference/inferenceModel.js"; +import { InferenceFile } from "@/v2/index.js"; +import { StringDict } from "@/parsing/index.js"; + +export abstract class BaseInference { + /** + * Model info for the inference. + */ + public model: InferenceModel; + /** + * File info for the inference. + */ + public file: InferenceFile; + /** + * ID of the inference. + */ + public id: string; + + protected constructor(serverResponse: StringDict) { + this.id = serverResponse["id"]; + this.model = new InferenceModel(serverResponse["model"]); + this.file = new InferenceFile(serverResponse["file"]); + } +} diff --git a/src/v2/parsing/inference/baseInferenceResponse.ts b/src/v2/parsing/inference/baseInferenceResponse.ts new file mode 100644 index 00000000..5904e44c --- /dev/null +++ b/src/v2/parsing/inference/baseInferenceResponse.ts @@ -0,0 +1,22 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { BaseResponse } from "@/v2/parsing/baseResponse.js"; +import { BaseInference } from "./baseInference.js"; + +export abstract class BaseInferenceResponse extends BaseResponse { + /** + * The inference result for a crop utility request. + */ + public inference: T; + + /** + * @param serverResponse JSON response from the server. + */ + protected constructor(serverResponse: StringDict) { + super(serverResponse); + this.inference = this.setInferenceType(serverResponse["inference"]); + } + + public abstract setInferenceType(inferenceResponse: StringDict): T; +} + +export type InferenceResponseConstructor = new (serverResponse: StringDict) => T; diff --git a/src/v2/parsing/inference/classification/classificationInference.ts b/src/v2/parsing/inference/classification/classificationInference.ts new file mode 100644 index 00000000..916c71dd --- /dev/null +++ b/src/v2/parsing/inference/classification/classificationInference.ts @@ -0,0 +1,24 @@ +import { StringDict } from "@/parsing/index.js"; +import { BaseInference } from "@/v2/parsing/inference/baseInference.js"; + +export class ClassificationInference extends BaseInference { + /** + * Result of a classification inference. + */ + result: any; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.result = serverResponse["result"]; + } + + toString(): string { + return ( + "Inference\n" + + "#########\n" + + this.model.toString() + "\n" + + this.file.toString() + "\n" + + this.result.toString() + "\n" + ); + } +} diff --git a/src/v2/parsing/inference/classification/classificationResponse.ts b/src/v2/parsing/inference/classification/classificationResponse.ts new file mode 100644 index 00000000..e348e76e --- /dev/null +++ b/src/v2/parsing/inference/classification/classificationResponse.ts @@ -0,0 +1,10 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { BaseInferenceResponse } from "@/v2/parsing/inference/index.js"; +import { ClassificationInference } from "./classificationInference.js"; + +export class ClassificationResponse extends BaseInferenceResponse { + + setInferenceType(inferenceResponse: StringDict): ClassificationInference { + return new ClassificationInference(inferenceResponse); + } +} diff --git a/src/v2/parsing/inference/crop/cropInference.ts b/src/v2/parsing/inference/crop/cropInference.ts new file mode 100644 index 00000000..19e68c98 --- /dev/null +++ b/src/v2/parsing/inference/crop/cropInference.ts @@ -0,0 +1,25 @@ +import { StringDict } from "@/parsing/index.js"; +import { BaseInference } from "@/v2/parsing/inference/baseInference.js"; +import { CropResult } from "@/v2/parsing/inference/crop/cropResult.js"; + +export class CropInference extends BaseInference { + /** + * Result of a crop utility inference. + */ + result: CropResult; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.result = new CropResult(serverResponse["result"]); + } + + toString(): string { + return ( + "Inference\n" + + "#########\n" + + this.model.toString() + "\n" + + this.file.toString() + "\n" + + this.result.toString() + "\n" + ); + } +} diff --git a/src/v2/parsing/inference/crop/cropItem.ts b/src/v2/parsing/inference/crop/cropItem.ts new file mode 100644 index 00000000..c1481a07 --- /dev/null +++ b/src/v2/parsing/inference/crop/cropItem.ts @@ -0,0 +1,16 @@ +import { FieldLocation } from "@/v2/parsing/inference/field/index.js"; +import { StringDict } from "@/parsing/index.js"; + +export class CropItem { + objectType: string; + location: FieldLocation; + + constructor(serverResponse: StringDict) { + this.objectType = serverResponse["objectType"]; + this.location = new FieldLocation(serverResponse["location"]); + } + + toString(): string { + return `${this.objectType} :: ${this.location}`; + } +} diff --git a/src/v2/parsing/inference/crop/cropResponse.ts b/src/v2/parsing/inference/crop/cropResponse.ts new file mode 100644 index 00000000..3f8f1941 --- /dev/null +++ b/src/v2/parsing/inference/crop/cropResponse.ts @@ -0,0 +1,10 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { CropInference } from "./cropInference.js"; +import { BaseInferenceResponse } from "@/v2/parsing/inference/baseInferenceResponse.js"; + +export class CropResponse extends BaseInferenceResponse { + + setInferenceType(inferenceResponse: StringDict): CropInference { + return new CropInference(inferenceResponse); + } +} diff --git a/src/v2/parsing/inference/crop/cropResult.ts b/src/v2/parsing/inference/crop/cropResult.ts new file mode 100644 index 00000000..b2bcb51a --- /dev/null +++ b/src/v2/parsing/inference/crop/cropResult.ts @@ -0,0 +1,17 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { CropItem } from "@/v2/parsing/inference/crop/cropItem.js"; + +export class CropResult { + /** + * Fields contained in the inference. + */ + public crop: CropItem[] = []; + + constructor(serverResponse: StringDict) { + this.crop = serverResponse["crop"].map((cropItem: StringDict) => new CropItem(cropItem)); + } + + toString(): string { + return `Crop\n====\n${this.crop}`; + } +} diff --git a/src/v2/parsing/dataSchemaActiveOption.ts b/src/v2/parsing/inference/extraction/dataSchemaActiveOption.ts similarity index 100% rename from src/v2/parsing/dataSchemaActiveOption.ts rename to src/v2/parsing/inference/extraction/dataSchemaActiveOption.ts diff --git a/src/v2/parsing/inferenceActiveOptions.ts b/src/v2/parsing/inference/extraction/extractionActiveOptions.ts similarity index 97% rename from src/v2/parsing/inferenceActiveOptions.ts rename to src/v2/parsing/inference/extraction/extractionActiveOptions.ts index 1f5febea..7846205f 100644 --- a/src/v2/parsing/inferenceActiveOptions.ts +++ b/src/v2/parsing/inference/extraction/extractionActiveOptions.ts @@ -1,7 +1,7 @@ import { StringDict } from "@/parsing/stringDict.js"; import { DataSchemaActiveOption } from "./dataSchemaActiveOption.js"; -export class InferenceActiveOptions { +export class ExtractionActiveOptions { /** * Whether the RAG feature was activated. */ diff --git a/src/v2/parsing/inference/extraction/extractionInference.ts b/src/v2/parsing/inference/extraction/extractionInference.ts new file mode 100644 index 00000000..7db6d240 --- /dev/null +++ b/src/v2/parsing/inference/extraction/extractionInference.ts @@ -0,0 +1,32 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { ExtractionResult } from "./extractionResult.js"; +import { ExtractionActiveOptions } from "./extractionActiveOptions.js"; +import { BaseInference } from "@/v2/parsing/inference/baseInference.js"; + +export class ExtractionInference extends BaseInference { + /** + * Result of the inference. + */ + public result: ExtractionResult; + /** + * Active options for the inference. + */ + public activeOptions: ExtractionActiveOptions; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.result = new ExtractionResult(serverResponse["result"]); + this.activeOptions = new ExtractionActiveOptions(serverResponse["active_options"]); + } + + toString(): string { + return ( + "Inference\n" + + "#########\n" + + this.model.toString() + "\n" + + this.file.toString() + "\n" + + this.activeOptions.toString() + "\n" + + this.result.toString() + "\n" + ); + } +} diff --git a/src/v2/parsing/inference/extraction/extractionResponse.ts b/src/v2/parsing/inference/extraction/extractionResponse.ts new file mode 100644 index 00000000..e61e836c --- /dev/null +++ b/src/v2/parsing/inference/extraction/extractionResponse.ts @@ -0,0 +1,10 @@ +import { ExtractionInference } from "./extractionInference.js"; +import { StringDict } from "@/parsing/stringDict.js"; +import { BaseInferenceResponse } from "@/v2/parsing/inference/baseInferenceResponse.js"; + +export class ExtractionResponse extends BaseInferenceResponse { + + setInferenceType(inferenceResponse: StringDict): ExtractionInference { + return new ExtractionInference(inferenceResponse); + } +} diff --git a/src/v2/parsing/inferenceResult.ts b/src/v2/parsing/inference/extraction/extractionResult.ts similarity index 76% rename from src/v2/parsing/inferenceResult.ts rename to src/v2/parsing/inference/extraction/extractionResult.ts index 5acd4284..e1a89d50 100644 --- a/src/v2/parsing/inferenceResult.ts +++ b/src/v2/parsing/inference/extraction/extractionResult.ts @@ -1,9 +1,9 @@ -import { InferenceFields } from "./field/index.js"; +import { InferenceFields } from "@/v2/parsing/inference/field/index.js"; import { StringDict } from "@/parsing/stringDict.js"; -import { RawText } from "./rawText.js"; -import { RagMetadata } from "./ragMetadata.js"; +import { RawText } from "../field/rawText.js"; +import { RagMetadata } from "../field/ragMetadata.js"; -export class InferenceResult { +export class ExtractionResult { /** * Fields contained in the inference. */ diff --git a/src/v2/parsing/field/baseField.ts b/src/v2/parsing/inference/field/baseField.ts similarity index 100% rename from src/v2/parsing/field/baseField.ts rename to src/v2/parsing/inference/field/baseField.ts diff --git a/src/v2/parsing/field/fieldConfidence.ts b/src/v2/parsing/inference/field/fieldConfidence.ts similarity index 100% rename from src/v2/parsing/field/fieldConfidence.ts rename to src/v2/parsing/inference/field/fieldConfidence.ts diff --git a/src/v2/parsing/field/fieldFactory.ts b/src/v2/parsing/inference/field/fieldFactory.ts similarity index 100% rename from src/v2/parsing/field/fieldFactory.ts rename to src/v2/parsing/inference/field/fieldFactory.ts diff --git a/src/v2/parsing/field/fieldLocation.ts b/src/v2/parsing/inference/field/fieldLocation.ts similarity index 100% rename from src/v2/parsing/field/fieldLocation.ts rename to src/v2/parsing/inference/field/fieldLocation.ts diff --git a/src/v2/parsing/field/index.ts b/src/v2/parsing/inference/field/index.ts similarity index 77% rename from src/v2/parsing/field/index.ts rename to src/v2/parsing/inference/field/index.ts index 7b4a651e..da7d5b53 100644 --- a/src/v2/parsing/field/index.ts +++ b/src/v2/parsing/inference/field/index.ts @@ -4,3 +4,5 @@ export { FieldLocation } from "./fieldLocation.js"; export { ListField } from "./listField.js"; export { ObjectField } from "./objectField.js"; export { SimpleField } from "./simpleField.js"; +export { RawText } from "./rawText.js"; +export { RagMetadata } from "./ragMetadata.js"; diff --git a/src/v2/parsing/field/inferenceFields.ts b/src/v2/parsing/inference/field/inferenceFields.ts similarity index 100% rename from src/v2/parsing/field/inferenceFields.ts rename to src/v2/parsing/inference/field/inferenceFields.ts diff --git a/src/v2/parsing/field/listField.ts b/src/v2/parsing/inference/field/listField.ts similarity index 100% rename from src/v2/parsing/field/listField.ts rename to src/v2/parsing/inference/field/listField.ts diff --git a/src/v2/parsing/field/objectField.ts b/src/v2/parsing/inference/field/objectField.ts similarity index 100% rename from src/v2/parsing/field/objectField.ts rename to src/v2/parsing/inference/field/objectField.ts diff --git a/src/v2/parsing/ragMetadata.ts b/src/v2/parsing/inference/field/ragMetadata.ts similarity index 100% rename from src/v2/parsing/ragMetadata.ts rename to src/v2/parsing/inference/field/ragMetadata.ts diff --git a/src/v2/parsing/rawText.ts b/src/v2/parsing/inference/field/rawText.ts similarity index 100% rename from src/v2/parsing/rawText.ts rename to src/v2/parsing/inference/field/rawText.ts diff --git a/src/v2/parsing/rawTextPage.ts b/src/v2/parsing/inference/field/rawTextPage.ts similarity index 100% rename from src/v2/parsing/rawTextPage.ts rename to src/v2/parsing/inference/field/rawTextPage.ts diff --git a/src/v2/parsing/field/simpleField.ts b/src/v2/parsing/inference/field/simpleField.ts similarity index 100% rename from src/v2/parsing/field/simpleField.ts rename to src/v2/parsing/inference/field/simpleField.ts diff --git a/src/v2/parsing/inference/index.ts b/src/v2/parsing/inference/index.ts new file mode 100644 index 00000000..ee712816 --- /dev/null +++ b/src/v2/parsing/inference/index.ts @@ -0,0 +1,34 @@ +// Inference +export { InferenceFile } from "./inferenceFile.js"; +export { InferenceModel } from "./inferenceModel.js"; +export { BaseInference } from "./baseInference.js"; +export { BaseInferenceResponse } from "./baseInferenceResponse.js"; +export type { InferenceResponseConstructor } from "./baseInferenceResponse.js"; + +// Fields +export * as field from "./field/index.js"; + +// Extraction +export { ExtractionInference } from "./extraction/extractionInference.js"; +export { ExtractionActiveOptions } from "./extraction/extractionActiveOptions.js"; +export { ExtractionResponse } from "./extraction/extractionResponse.js"; +export { ExtractionResult } from "./extraction/extractionResult.js"; + +// Classification +export { ClassificationResponse } from "./classification/classificationResponse.js"; +export { ClassificationInference } from "./classification/classificationInference.js"; + +// Crop +export { CropInference } from "./crop/cropInference.js"; +export { CropItem } from "./crop/cropItem.js"; +export { CropResponse } from "./crop/cropResponse.js"; +export { CropResult } from "./crop/cropResult.js"; + +// OCR +export { OcrResponse } from "./ocr/ocrResponse.js"; +export { OcrInference } from "./ocr/ocrInference.js"; + +// Split +export { SplitResponse } from "./split/splitResponse.js"; +export { SplitInference } from "./split/splitInference.js"; + diff --git a/src/v2/parsing/inferenceFile.ts b/src/v2/parsing/inference/inferenceFile.ts similarity index 100% rename from src/v2/parsing/inferenceFile.ts rename to src/v2/parsing/inference/inferenceFile.ts diff --git a/src/v2/parsing/inferenceModel.ts b/src/v2/parsing/inference/inferenceModel.ts similarity index 100% rename from src/v2/parsing/inferenceModel.ts rename to src/v2/parsing/inference/inferenceModel.ts diff --git a/src/v2/parsing/inference/ocr/ocrInference.ts b/src/v2/parsing/inference/ocr/ocrInference.ts new file mode 100644 index 00000000..c767fccf --- /dev/null +++ b/src/v2/parsing/inference/ocr/ocrInference.ts @@ -0,0 +1,24 @@ +import { StringDict } from "@/parsing/index.js"; +import { BaseInference } from "@/v2/parsing/inference/baseInference.js"; + +export class OcrInference extends BaseInference { + /** + * Result of an OCR inference. + */ + result: any; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.result = serverResponse["result"]; + } + + toString(): string { + return ( + "Inference\n" + + "#########\n" + + this.model.toString() + "\n" + + this.file.toString() + "\n" + + this.result.toString() + "\n" + ); + } +} diff --git a/src/v2/parsing/inference/ocr/ocrResponse.ts b/src/v2/parsing/inference/ocr/ocrResponse.ts new file mode 100644 index 00000000..f0c8e452 --- /dev/null +++ b/src/v2/parsing/inference/ocr/ocrResponse.ts @@ -0,0 +1,10 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { BaseInferenceResponse } from "@/v2/parsing/inference/baseInferenceResponse.js"; +import { OcrInference } from "./ocrInference.js"; + +export class OcrResponse extends BaseInferenceResponse { + + setInferenceType(inferenceResponse: StringDict): OcrInference { + return new OcrInference(inferenceResponse); + } +} diff --git a/src/v2/parsing/inference/split/splitInference.ts b/src/v2/parsing/inference/split/splitInference.ts new file mode 100644 index 00000000..a195a9b9 --- /dev/null +++ b/src/v2/parsing/inference/split/splitInference.ts @@ -0,0 +1,24 @@ +import { StringDict } from "@/parsing/index.js"; +import { BaseInference } from "@/v2/parsing/inference/baseInference.js"; + +export class SplitInference extends BaseInference { + /** + * Result of a split inference. + */ + result: any; + + constructor(serverResponse: StringDict) { + super(serverResponse); + this.result = serverResponse["result"]; + } + + toString(): string { + return ( + "Inference\n" + + "#########\n" + + this.model.toString() + "\n" + + this.file.toString() + "\n" + + this.result.toString() + "\n" + ); + } +} diff --git a/src/v2/parsing/inference/split/splitResponse.ts b/src/v2/parsing/inference/split/splitResponse.ts new file mode 100644 index 00000000..3336d93a --- /dev/null +++ b/src/v2/parsing/inference/split/splitResponse.ts @@ -0,0 +1,10 @@ +import { StringDict } from "@/parsing/stringDict.js"; +import { BaseInferenceResponse } from "@/v2/parsing/inference/baseInferenceResponse.js"; +import { SplitInference } from "./splitInference.js"; + +export class SplitResponse extends BaseInferenceResponse { + + setInferenceType(inferenceResponse: StringDict): SplitInference { + return new SplitInference(inferenceResponse); + } +} diff --git a/src/v2/parsing/inferenceResponse.ts b/src/v2/parsing/inferenceResponse.ts deleted file mode 100644 index 95090df2..00000000 --- a/src/v2/parsing/inferenceResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BaseResponse } from "./baseResponse.js"; -import { Inference } from "./inference.js"; -import { StringDict } from "@/parsing/stringDict.js"; - -export class InferenceResponse extends BaseResponse { - /** - * Inference result. - */ - public inference: Inference; - - constructor(serverResponse: StringDict) { - super(serverResponse); - this.inference = new Inference(serverResponse["inference"]); - } -} diff --git a/src/v2/parsing/localResponse.ts b/src/v2/parsing/localResponse.ts index 4f0399ab..657e7b7b 100644 --- a/src/v2/parsing/localResponse.ts +++ b/src/v2/parsing/localResponse.ts @@ -1,7 +1,7 @@ import { StringDict } from "@/parsing/stringDict.js"; import { MindeeError } from "@/errors/index.js"; -import { BaseResponse } from "./baseResponse.js"; import { LocalResponseBase } from "@/parsing/localResponseBase.js"; +import { BaseResponse } from "./baseResponse.js"; /** * Local response loaded from a file. diff --git a/tests/v2/client.integration.ts b/tests/v2/client.integration.ts index 1dcb4e44..a3f02c0c 100644 --- a/tests/v2/client.integration.ts +++ b/tests/v2/client.integration.ts @@ -3,14 +3,14 @@ import path from "node:path"; import { Client, - InferenceParameters, + ExtractionParameters, PathInput, UrlInput, Base64Input, - InferenceResponse, + ExtractionResponse, } from "@/index.js"; -import { Inference } from "@/v2/parsing/index.js"; -import { SimpleField } from "@/v2/parsing/field/index.js"; +import { ExtractionInference } from "@/v2/parsing/index.js"; +import { SimpleField } from "@/v2/parsing/inference/field/index.js"; import { MindeeHttpErrorV2 } from "@/v2/http/index.js"; import * as fs from "node:fs"; import { RESOURCE_PATH, V2_PRODUCT_PATH, V2_RESOURCE_PATH } from "../index.js"; @@ -25,7 +25,7 @@ function check422(err: unknown) { expect(errObj.errors).to.be.instanceOf(Array); } -function checkEmptyActiveOptions(inference: Inference) { +function checkEmptyActiveOptions(inference: ExtractionInference) { expect(inference.activeOptions).to.not.be.null; expect(inference.activeOptions?.rag).to.be.false; expect(inference.activeOptions?.rawText).to.be.false; @@ -80,11 +80,12 @@ describe("MindeeV2 – Client Integration Tests", () => { webhookIds: [], alias: "ts_integration_empty_multiple" }; - const response = await client.enqueueAndGetInference(source, params); - + const response = await client.enqueueAndGetInference( + ExtractionInference, source, params + ); expect(response).to.exist; - expect(response.inference).to.be.instanceOf(Inference); - const inference: Inference = response.inference; + expect(response.inference).to.be.instanceOf(ExtractionInference); + const inference: ExtractionInference = response.inference; expect(inference.file?.name).to.equal("multipage_cut-2.pdf"); expect(inference.file.pageCount).to.equal(2); @@ -108,10 +109,11 @@ describe("MindeeV2 – Client Integration Tests", () => { alias: "ts_integration_binary_filled_single" }; - const response = await client.enqueueAndGetInference(source, params); - - expect(response.inference).to.be.instanceOf(Inference); - const inference: Inference = response.inference; + const response = await client.enqueueAndGetInference( + ExtractionInference, source, params + ); + expect(response.inference).to.be.instanceOf(ExtractionInference); + const inference: ExtractionInference = response.inference; expect(inference.file?.name).to.equal("default_sample.jpg"); expect(inference.model?.id).to.equal(modelId); @@ -136,7 +138,7 @@ describe("MindeeV2 – Client Integration Tests", () => { it("Filled, single-page image – Base64Input - enqueueAndGetInference must succeed", async () => { const data = fs.readFileSync(sampleBase64Path, "utf8"); const source = new Base64Input({ inputString: data, filename: "receipt.jpg" }); - const params = new InferenceParameters({ + const params = new ExtractionParameters({ modelId, rag: false, rawText: false, @@ -146,10 +148,11 @@ describe("MindeeV2 – Client Integration Tests", () => { alias: "ts_integration_base64_filled_single" }); - const response = await client.enqueueAndGetInference(source, params); - - expect(response.inference).to.be.instanceOf(Inference); - const inference: Inference = response.inference; + const response = await client.enqueueAndGetInference( + ExtractionInference, source, params + ); + expect(response.inference).to.be.instanceOf(ExtractionInference); + const inference: ExtractionInference = response.inference; expect(inference.file?.name).to.equal("receipt.jpg"); expect(inference.model?.id).to.equal(modelId); @@ -168,7 +171,7 @@ describe("MindeeV2 – Client Integration Tests", () => { const badParams = { modelId: "00000000-0000-0000-0000-000000000000" }; try { - await client.enqueueInference(source, badParams); + await client.enqueueExtraction(source, badParams); expect.fail("Expected the call to throw, but it succeeded."); } catch (err) { check422(err); @@ -177,7 +180,10 @@ describe("MindeeV2 – Client Integration Tests", () => { it("Invalid job ID – getInference must raise 422", async () => { try { - await client.getInference("00000000-0000-0000-0000-000000000000"); + await client.getInference( + ExtractionInference, + "00000000-0000-0000-0000-000000000000" + ); expect.fail("Expected the call to throw, but it succeeded."); } catch (err) { check422(err); @@ -187,7 +193,7 @@ describe("MindeeV2 – Client Integration Tests", () => { it("HTTPS URL – enqueue & get inference must succeed", async () => { const url = process.env.MINDEE_V2_SE_TESTS_BLANK_PDF_URL ?? "error-no-url-found"; const source = new UrlInput({ url }); - const params = new InferenceParameters({ + const params = new ExtractionParameters({ modelId, rag: false, rawText: false, @@ -196,15 +202,16 @@ describe("MindeeV2 – Client Integration Tests", () => { webhookIds: [], alias: "ts_integration_url_source" }); - const response: InferenceResponse = await client.enqueueAndGetInference(source, params); - + const response: ExtractionResponse = await client.enqueueAndGetInference( + ExtractionInference, source, params + ); expect(response).to.exist; - expect(response.inference).to.be.instanceOf(Inference); + expect(response.inference).to.be.instanceOf(ExtractionInference); }).timeout(60000); it("Data Schema Override - Overrides the data schema successfully", async () => { const source = new PathInput({ inputPath: emptyPdfPath }); - const params = new InferenceParameters({ + const params = new ExtractionParameters({ modelId, rag: false, rawText: false, @@ -214,10 +221,11 @@ describe("MindeeV2 – Client Integration Tests", () => { dataSchema: dataSchemaReplace, alias: "ts_integration_data_schema_replace" }); - const response = await client.enqueueAndGetInference(source, params); - + const response = await client.enqueueAndGetInference( + ExtractionInference, source, params + ); expect(response).to.exist; - expect(response.inference).to.be.instanceOf(Inference); + expect(response.inference).to.be.instanceOf(ExtractionInference); expect(response.inference.result.fields.get("test_replace")).to.exist; expect((response.inference.result.fields.get("test_replace") as SimpleField).value).to.be.equals("a test value"); diff --git a/tests/v2/client.spec.ts b/tests/v2/client.spec.ts index 03c130fc..bdb43af7 100644 --- a/tests/v2/client.spec.ts +++ b/tests/v2/client.spec.ts @@ -1,11 +1,12 @@ import { expect } from "chai"; import { MockAgent, setGlobalDispatcher } from "undici"; import path from "node:path"; -import { Client, PathInput, InferenceResponse } from "@/index.js"; +import { Client, PathInput } from "@/index.js"; import { MindeeHttpErrorV2 } from "@/v2/http/index.js"; import assert from "node:assert/strict"; import { RESOURCE_PATH, V2_RESOURCE_PATH } from "../index.js"; -import { LocalResponse } from "@/v2/index.js"; +import fs from "node:fs/promises"; +import { CropInference, ExtractionInference } from "@/v2/parsing/index.js"; const mockAgent = new MockAgent(); setGlobalDispatcher(mockAgent); @@ -20,36 +21,29 @@ function dummyEnvvars(): void { process.env.MINDEE_V2_API_HOST = "v2-client-host"; } -function setNockInterceptors(): void { +async function setInterceptor(statusCode: number, filePath: string): Promise { + const fileObj = await fs.readFile(filePath, { encoding: "utf-8" }); + mockPool + .intercept({ path: /.*/, method: "GET" }) + .reply(statusCode, fileObj); +} + +async function setAllInterceptors(): Promise { mockPool .intercept({ path: /.*/, method: "POST" }) .reply( 400, { status: 400, detail: "forced failure from test", title: "Bad Request", code: "400-001" } ); - - mockPool - .intercept({ path: /.*/, method: "GET" }) - .reply(200, { - job: { - id: "12345678-1234-1234-1234-123456789ABC", - model_id: "87654321-4321-4321-4321-CBA987654321", - filename: "default_sample.jpg", - alias: "dummy-alias.jpg", - created_at: "2025-07-03T14:27:58.974451", - status: "Processing", - polling_url: - "https://api-v2.mindee.net/v2/jobs/12345678-1234-1234-1234-123456789ABC", - result_url: null, - webhooks: [], - error: null, - }, - }); + await setInterceptor( + 200, + path.join(V2_RESOURCE_PATH, "job/ok_processing.json") + ); } -const fileTypesDir = path.join(RESOURCE_PATH, "file_types"); - describe("MindeeV2 - ClientV2", () => { + const fileTypesDir = path.join(RESOURCE_PATH, "file_types"); + before(() => { dummyEnvvars(); }); @@ -62,8 +56,8 @@ describe("MindeeV2 - ClientV2", () => { describe("Client configured via environment variables", () => { let client: Client; - beforeEach(() => { - setNockInterceptors(); + beforeEach(async () => { + await setAllInterceptors(); client = new Client({ apiKey: "dummy", debug: true, dispatcher: mockAgent }); }); @@ -75,12 +69,26 @@ describe("MindeeV2 - ClientV2", () => { expect(api.settings.baseHeaders["User-Agent"]).to.match(/mindee/i); }); - it("enqueue(path) rejects with MindeeHttpErrorV2 on 400", async () => { + it("enqueueInference(path) rejects with MindeeHttpErrorV2 on 400", async () => { + const filePath = path.join(fileTypesDir, "receipt.jpg"); + const inputDoc = new PathInput({ inputPath: filePath }); + + await assert.rejects( + client.enqueueExtraction(inputDoc, { modelId: "dummy-model", textContext: "hello" }), + (error: any) => { + assert.strictEqual(error instanceof MindeeHttpErrorV2, true); + assert.strictEqual(error.status, 400); + return true; + } + ); + }); + + it("enqueueUtility(path) rejects with MindeeHttpErrorV2 on 400", async () => { const filePath = path.join(fileTypesDir, "receipt.jpg"); const inputDoc = new PathInput({ inputPath: filePath }); await assert.rejects( - client.enqueueInference(inputDoc, { modelId: "dummy-model", textContext: "hello" }), + client.enqueueInference(CropInference, inputDoc, { modelId: "dummy-model" }), (error: any) => { assert.strictEqual(error instanceof MindeeHttpErrorV2, true); assert.strictEqual(error.status, 400); @@ -89,11 +97,12 @@ describe("MindeeV2 - ClientV2", () => { ); }); - it("enqueueAndParse(path) rejects with MindeeHttpErrorV2 on 400", async () => { + it("enqueueAndGetInference(path) rejects with MindeeHttpErrorV2 on 400", async () => { const filePath = path.join(fileTypesDir, "receipt.jpg"); const inputDoc = new PathInput({ inputPath: filePath }); await assert.rejects( client.enqueueAndGetInference( + ExtractionInference, inputDoc, { modelId: "dummy-model", rag: false } ), @@ -105,22 +114,6 @@ describe("MindeeV2 - ClientV2", () => { ); }); - it("loading an inference works on stored JSON fixtures", async () => { - const jsonPath = path.join( - V2_RESOURCE_PATH, - "products", - "financial_document", - "complete.json" - ); - - const localResponse = new LocalResponse(jsonPath); - const response: InferenceResponse = await localResponse.deserializeResponse(InferenceResponse); - - expect(response.inference.model.id).to.equal( - "12345678-1234-1234-1234-123456789abc" - ); - }); - it("bubble-up HTTP errors with details", async () => { const input = new PathInput({ inputPath: path.join( @@ -131,7 +124,7 @@ describe("MindeeV2 - ClientV2", () => { ), }); await assert.rejects( - client.enqueueInference(input, { modelId: "dummy-model" }), + client.enqueueExtraction(input, { modelId: "dummy-model" }), (error: any) => { expect(error).to.be.instanceOf(MindeeHttpErrorV2); expect(error.status).to.equal(400); @@ -141,7 +134,7 @@ describe("MindeeV2 - ClientV2", () => { ); }); - it("parseQueued(jobId) returns a fully-formed JobResponse", async () => { + it("getJob(jobId) returns a fully-formed JobResponse", async () => { const resp = await client.getJob( "12345678-1234-1234-1234-123456789ABC" ); diff --git a/tests/v2/client/inferenceParameter.spec.ts b/tests/v2/client/inferenceParameter.spec.ts index a3fab355..8d3e6b4c 100644 --- a/tests/v2/client/inferenceParameter.spec.ts +++ b/tests/v2/client/inferenceParameter.spec.ts @@ -1,7 +1,7 @@ import { StringDict } from "@/parsing/index.js"; import path from "path"; import { V2_RESOURCE_PATH } from "../../index.js"; -import { InferenceParameters } from "@/index.js"; +import { ExtractionParameters } from "@/index.js"; import { expect } from "chai"; import { DataSchema } from "@/index.js"; import { promises as fs } from "fs"; @@ -16,7 +16,7 @@ describe("MindeeV2 - Inference Parameter", () => { describe("Polling Options", () => { it("should provide sensible defaults", () => { - const paramsInstance = new InferenceParameters({ + const paramsInstance = new ExtractionParameters({ modelId: modelIdValue, }); expect(paramsInstance.modelId).to.equal(modelIdValue); @@ -37,22 +37,22 @@ describe("MindeeV2 - Inference Parameter", () => { }); it("shouldn't replace when unset", () => { - const params = new InferenceParameters({ + const params = new ExtractionParameters({ modelId: modelIdValue, }); expect(params.dataSchema).to.be.undefined; }); it("should equate no matter the type", () => { - const paramsDict = new InferenceParameters({ + const paramsDict = new ExtractionParameters({ modelId: modelIdValue, dataSchema: expectedDataSchemaDict, }); - const paramsString = new InferenceParameters({ + const paramsString = new ExtractionParameters({ modelId: modelIdValue, dataSchema: expectedDataSchemaString, }); - const paramsObject = new InferenceParameters({ + const paramsObject = new ExtractionParameters({ modelId: modelIdValue, dataSchema: expectedDataSchemaObject, }); diff --git a/tests/v2/parsing/inference.spec.ts b/tests/v2/parsing/inference.spec.ts index 13a81749..86a29069 100644 --- a/tests/v2/parsing/inference.spec.ts +++ b/tests/v2/parsing/inference.spec.ts @@ -1,9 +1,19 @@ -import { expect } from "chai"; import path from "node:path"; -import { LocalResponse, InferenceResponse, RawText, RagMetadata } from "@/v2/index.js"; -import { FieldConfidence, ListField, ObjectField, SimpleField } from "@/v2/parsing/field/index.js"; import { promises as fs } from "node:fs"; +import { expect } from "chai"; import { Polygon } from "@/geometry/index.js"; +import { + FieldConfidence, + ListField, + ObjectField, + SimpleField, +} from "@/v2/parsing/inference/field/index.js"; +import { + LocalResponse, + ExtractionResponse, + RagMetadata, + RawText, +} from "@/v2/parsing/index.js"; import { V2_RESOURCE_PATH } from "../../index.js"; const findocPath = path.join(V2_RESOURCE_PATH, "products", "financial_document"); @@ -13,10 +23,10 @@ const standardFieldPath = path.join(inferencePath, "standard_field_types.json"); const standardFieldRstPath = path.join(inferencePath, "standard_field_types.rst"); const locationFieldPath = path.join(findocPath, "complete_with_coordinates.json"); -async function loadV2Inference(resourcePath: string): Promise { +async function loadV2Inference(resourcePath: string): Promise { const localResponse = new LocalResponse(resourcePath); await localResponse.init(); - return localResponse.deserializeResponse(InferenceResponse); + return localResponse.deserializeResponse(ExtractionResponse); } describe("MindeeV2 - Inference Response", async () => { diff --git a/tests/v2/parsing/job.spec.ts b/tests/v2/parsing/job.spec.ts index 1c6a291b..81d4f41a 100644 --- a/tests/v2/parsing/job.spec.ts +++ b/tests/v2/parsing/job.spec.ts @@ -1,4 +1,8 @@ -import { JobResponse, LocalResponse, ErrorResponse } from "@/v2/index.js"; +import { + JobResponse, + LocalResponse, + ErrorResponse, +} from "@/v2/index.js"; import path from "node:path"; import { V2_RESOURCE_PATH } from "../../index.js"; import { expect } from "chai"; diff --git a/tests/v2/parsing/localResponse.spec.ts b/tests/v2/parsing/localResponse.spec.ts index 9e423704..a91f6089 100644 --- a/tests/v2/parsing/localResponse.spec.ts +++ b/tests/v2/parsing/localResponse.spec.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs/promises"; import { expect } from "chai"; -import { InferenceResponse, LocalResponse } from "@/v2/index.js"; +import { ExtractionResponse, LocalResponse } from "@/v2/index.js"; import path from "path"; import { V2_RESOURCE_PATH } from "../../index.js"; @@ -16,8 +16,8 @@ async function assertLocalResponse(localResponse: LocalResponse) { expect(localResponse.isValidHmacSignature(dummySecretKey, "invalid signature")).to.be.false; expect(localResponse.getHmacSignature(dummySecretKey)).to.eq(signature); expect(localResponse.isValidHmacSignature(dummySecretKey, signature)).to.be.true; - const inferenceResponse = await localResponse.deserializeResponse(InferenceResponse); - expect(inferenceResponse).to.be.an.instanceof(InferenceResponse); + const inferenceResponse = await localResponse.deserializeResponse(ExtractionResponse); + expect(inferenceResponse).to.be.an.instanceof(ExtractionResponse); expect(inferenceResponse.inference).to.not.be.null; } @@ -40,9 +40,23 @@ describe("MindeeV2 - Load Local Response", () => { it("should deserialize a prediction.", async () => { const fileObj = await fs.readFile(filePath, { encoding: "utf-8" }); const localResponse = new LocalResponse(fileObj); - const response = await localResponse.deserializeResponse(InferenceResponse); - expect(response).to.be.an.instanceof(InferenceResponse); + const response = await localResponse.deserializeResponse(ExtractionResponse); + expect(response).to.be.an.instanceof(ExtractionResponse); expect(JSON.stringify(response.getRawHttp())).to.eq(JSON.stringify(JSON.parse(fileObj))); }); + + it("loading an inference works on catalog model", async () => { + const jsonPath = path.join( + V2_RESOURCE_PATH, + "products", + "financial_document", + "complete.json" + ); + const localResponse = new LocalResponse(jsonPath); + const response: ExtractionResponse = await localResponse.deserializeResponse(ExtractionResponse); + expect(response.inference.model.id).to.equal( + "12345678-1234-1234-1234-123456789abc" + ); + }); });