feat(Mistral AI Node): New node (#16631)

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
RomanDavydchuk
2025-07-01 09:19:01 +03:00
committed by GitHub
parent 9517d11a7e
commit c11e4bd0a8
15 changed files with 1843 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
import type FormData from 'form-data';
import type {
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import type { Page } from './types';
export async function mistralApiRequest(
this: IExecuteFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject | FormData = {},
qs: IDataObject = {},
): Promise<any> {
const options: IHttpRequestOptions = {
method,
body,
qs,
url: `https://api.mistral.ai${resource}`,
json: true,
};
if (Object.keys(body).length === 0) {
delete options.body;
}
if (Object.keys(qs).length === 0) {
delete options.qs;
}
try {
return await this.helpers.httpRequestWithAuthentication.call(this, 'mistralCloudApi', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function encodeBinaryData(
this: IExecuteFunctions,
itemIndex: number,
): Promise<{ dataUrl: string; fileName: string | undefined }> {
const binaryProperty = this.getNodeParameter('binaryProperty', itemIndex);
const binaryData = this.helpers.assertBinaryData(itemIndex, binaryProperty);
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryProperty);
const base64Data = binaryDataBuffer.toString('base64');
const dataUrl = `data:${binaryData.mimeType};base64,${base64Data}`;
return { dataUrl, fileName: binaryData.fileName };
}
export function processResponseData(response: IDataObject): IDataObject {
const pages = response.pages as Page[];
return {
...response,
extractedText: pages.map((page) => page.markdown).join('\n\n'),
pageCount: pages.length,
};
}

View File

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.mistralAi",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Utility"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/mistral/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.mistralai/"
}
]
}
}

View File

@@ -0,0 +1,316 @@
import FormData from 'form-data';
import chunk from 'lodash/chunk';
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeApiError, NodeConnectionTypes } from 'n8n-workflow';
import { document } from './descriptions';
import { encodeBinaryData, mistralApiRequest, processResponseData } from './GenericFunctions';
import type { BatchItemResult, BatchJob } from './types';
export class MistralAi implements INodeType {
description: INodeTypeDescription = {
displayName: 'Mistral AI',
name: 'mistralAi',
icon: {
light: 'file:mistralAi.svg',
dark: 'file:mistralAi.svg',
},
group: ['transform'],
version: 1,
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
description: 'Consume Mistral AI API',
defaults: {
name: 'Mistral AI',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
credentials: [
{
name: 'mistralCloudApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Document',
value: 'document',
},
],
default: 'document',
},
...document.description,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
if (resource === 'document') {
if (operation === 'extractText') {
const enableBatch = this.getNodeParameter('options.batch', 0, false) as boolean;
if (enableBatch) {
try {
const deleteFiles = this.getNodeParameter('options.deleteFiles', 0, true) as boolean;
const model = this.getNodeParameter('model', 0) as string;
const batchSize = this.getNodeParameter('options.batchSize', 0, 50) as number;
const itemsWithIndex = items.map((item, index) => ({
...item,
index,
}));
const fileIds = [];
for (const batch of chunk(itemsWithIndex, batchSize)) {
const entries = [];
for (const item of batch) {
const documentType = this.getNodeParameter('documentType', item.index) as
| 'document_url'
| 'image_url';
const { dataUrl, fileName } = await encodeBinaryData.call(this, item.index);
entries.push({
custom_id: item.index.toString(),
body: {
document: {
type: documentType,
document_name: documentType === 'document_url' ? fileName : undefined,
[documentType]: dataUrl,
},
},
});
}
const formData = new FormData();
formData.append(
'file',
Buffer.from(entries.map((entry) => JSON.stringify(entry)).join('\n')),
{
filename: 'batch_file.jsonl',
contentType: 'application/json',
},
);
formData.append('purpose', 'batch');
const fileResponse = await mistralApiRequest.call(
this,
'POST',
'/v1/files',
formData,
);
fileIds.push(fileResponse.id);
}
const jobIds = [];
for (const fileId of fileIds) {
const body: IDataObject = {
model,
input_files: [fileId],
endpoint: '/v1/ocr',
};
jobIds.push((await mistralApiRequest.call(this, 'POST', '/v1/batch/jobs', body)).id);
}
const jobResults: BatchJob[] = [];
for (const jobId of jobIds) {
let job = (await mistralApiRequest.call(
this,
'GET',
`/v1/batch/jobs/${jobId}`,
)) as BatchJob;
while (job.status === 'QUEUED' || job.status === 'RUNNING') {
await new Promise((resolve) => setTimeout(resolve, 2000));
job = (await mistralApiRequest.call(
this,
'GET',
`/v1/batch/jobs/${jobId}`,
)) as BatchJob;
}
jobResults.push(job);
}
if (deleteFiles) {
for (const fileId of fileIds) {
try {
await mistralApiRequest.call(this, 'DELETE', `/v1/files/${fileId}`);
} catch {}
}
}
for (const jobResult of jobResults) {
if (
jobResult.status !== 'SUCCESS' ||
(jobResult.errors && jobResult.errors.length > 0)
) {
for (let i = 0; i < items.length; i++) {
if (this.continueOnFail()) {
const errorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
error: 'Batch job failed or returned errors',
}),
{ itemData: { item: i } },
);
returnData.push(...errorData);
} else {
throw new NodeApiError(this.getNode(), {
message: `Batch job failed with status: ${jobResult.status}`,
});
}
}
continue;
} else {
const fileResponse = (await mistralApiRequest.call(
this,
'GET',
`/v1/files/${jobResult.output_file}/content`,
)) as string | BatchItemResult;
if (deleteFiles) {
try {
await mistralApiRequest.call(
this,
'DELETE',
`/v1/files/${jobResult.output_file}`,
);
} catch {}
}
let batchResult: BatchItemResult[];
if (typeof fileResponse === 'string') {
batchResult = fileResponse
.trim()
.split('\n')
.map((json) => JSON.parse(json) as BatchItemResult);
} else {
// If the response is not a string, it is a single item result
batchResult = [fileResponse];
}
for (const result of batchResult) {
const index = parseInt(result.custom_id, 10);
if (result.error) {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: result.error }),
{ itemData: { item: index } },
);
returnData.push(...executionData);
} else {
const data = processResponseData(result.response.body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(data),
{ itemData: { item: index } },
);
returnData.push(...executionData);
}
}
}
}
} catch (error) {
if (this.continueOnFail()) {
const executionError = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
error: error instanceof Error ? error.message : JSON.stringify(error),
}),
{ itemData: { item: 0 } },
);
returnData.push(...executionError);
} else {
throw new NodeApiError(this.getNode(), error);
}
}
} else {
let responseData: IDataObject;
for (let i = 0; i < items.length; i++) {
try {
const model = this.getNodeParameter('model', i) as string;
const inputType = this.getNodeParameter('inputType', i) as 'binary' | 'url';
const documentType = this.getNodeParameter('documentType', i) as
| 'document_url'
| 'image_url';
if (inputType === 'binary') {
const { dataUrl, fileName } = await encodeBinaryData.call(this, i);
const body: IDataObject = {
model,
document: {
type: documentType,
document_name: documentType === 'document_url' ? fileName : undefined,
[documentType]: dataUrl,
},
};
responseData = (await mistralApiRequest.call(
this,
'POST',
'/v1/ocr',
body,
)) as IDataObject;
responseData = processResponseData(responseData);
} else {
const url = this.getNodeParameter('url', i) as string;
const body: IDataObject = {
model,
document: {
type: documentType,
[documentType]: url,
},
};
responseData = (await mistralApiRequest.call(
this,
'POST',
'/v1/ocr',
body,
)) as IDataObject;
responseData = processResponseData(responseData);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionError = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
error: error instanceof Error ? error.message : JSON.stringify(error),
}),
{ itemData: { item: i } },
);
returnData.push(...executionError);
} else {
throw new NodeApiError(this.getNode(), error);
}
}
}
}
}
}
return [returnData];
}
}

View File

@@ -0,0 +1,28 @@
import type { INodeProperties } from 'n8n-workflow';
import * as extractText from './extractText.operation';
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['document'],
},
},
options: [
{
name: 'Extract Text',
value: 'extractText',
description: 'Extract text from document using OCR',
action: 'Extract text',
},
],
default: 'extractText',
},
...extractText.description,
];

View File

@@ -0,0 +1,141 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
const properties: INodeProperties[] = [
{
displayName: 'Model',
name: 'model',
type: 'options',
options: [
{
name: 'mistral-ocr-latest',
value: 'mistral-ocr-latest',
},
],
description: 'The OCR model to use',
required: true,
default: 'mistral-ocr-latest',
},
{
displayName: 'Document Type',
name: 'documentType',
type: 'options',
options: [
{
name: 'Document',
value: 'document_url',
},
{
name: 'Image',
value: 'image_url',
},
],
description: 'The type of document to process',
required: true,
default: 'document_url',
},
{
displayName: 'Input Type',
name: 'inputType',
type: 'options',
options: [
{
name: 'Binary Data',
value: 'binary',
},
{
name: 'URL',
value: 'url',
},
],
description: 'How the document will be provided',
required: true,
default: 'binary',
disabledOptions: {
show: {
'options.batch': [true],
},
},
},
{
displayName: 'Input Binary Field',
name: 'binaryProperty',
type: 'string',
description: 'Name of the input binary field that contains the file to process',
placeholder: 'e.g. data',
hint: 'Uploaded document files must not exceed 50 MB in size and should be no longer than 1,000 pages.',
required: true,
default: 'data',
displayOptions: {
show: {
inputType: ['binary'],
},
},
},
{
displayName: 'URL',
name: 'url',
type: 'string',
description: 'URL of the document or image to process',
placeholder: 'e.g. https://example.com/document.pdf',
required: true,
default: '',
displayOptions: {
show: {
inputType: ['url'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Enable Batch Processing',
name: 'batch',
type: 'boolean',
description:
'Whether to process multiple documents in a single API call (more cost-efficient)',
default: false,
},
{
displayName: 'Batch Size',
name: 'batchSize',
type: 'number',
description: 'Maximum number of documents to process in a single batch',
default: 50,
typeOptions: { maxValue: 2048 },
required: true,
displayOptions: {
show: {
batch: [true],
},
},
},
{
displayName: 'Delete Files After Processing',
name: 'deleteFiles',
type: 'boolean',
default: true,
description: 'Whether to delete the files on Mistral Cloud after processing',
displayOptions: {
show: {
batch: [true],
},
},
},
],
},
];
const displayOptions = {
show: {
resource: ['document'],
operation: ['extractText'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);

View File

@@ -0,0 +1 @@
export * as document from './document/Document.resource';

View File

@@ -0,0 +1,262 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="216"
height="216"
version="1.1"
id="svg41"
sodipodi:docname="mistral.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview41"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.936488"
inkscape:cx="197.78072"
inkscape:cy="79.00901"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg41" />
<style
id="style1"><![CDATA[.I{fill:#ff7000}.J{fill:#ff4900}.K{fill:#ffa300}.L{fill:#1c1c1b icc-color(adobe-rgb-1998, 0.13299561, 0.13299561, 0.1289978)}]]></style>
<defs
id="defs10">
<clipPath
id="A">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-206.251,-140.139)"
id="path1" />
</clipPath>
<clipPath
id="B">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-247.436,-104.865)"
id="path2" />
</clipPath>
<clipPath
id="C">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-285.938,-102.089)"
id="path3" />
</clipPath>
<clipPath
id="D">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-337.769,-131.877)"
id="path4" />
</clipPath>
<clipPath
id="E">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-377.247,-132.319)"
id="path5" />
</clipPath>
<clipPath
id="F">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-418.107,-114.634)"
id="path6" />
</clipPath>
<clipPath
id="G">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-450.023,-140.139)"
id="path7" />
</clipPath>
<clipPath
id="H">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-217.694,-44.794)"
id="path8" />
</clipPath>
<clipPath
id="I">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
transform="translate(-247.436,-35.025)"
id="path9" />
</clipPath>
<clipPath
id="J">
<path
d="M 0,184.252 H 481.89 V 0 H 0 Z"
id="path10" />
</clipPath>
<path
id="K"
d="m 173.987,134.362 h -37.795 l 9.633,-37.776 h 37.796 z" />
</defs>
<g
transform="matrix(1,0,0.254535,1,-51.362792,-7.4725007)"
id="g32">
<g
class="L"
id="g22">
<path
d="M 98.397,134.362 H 60.602 l 9.633,-37.776 h 37.796 z"
id="path11" />
<path
d="M 126.558,172.138 H 88.763 l 9.633,-37.776 h 37.796 z"
id="path12" />
<path
d="M 136.192,134.362 H 98.397 l 9.633,-37.776 h 37.796 z"
id="path13" />
<use
xlink:href="#K"
id="use13" />
<path
d="M 108.031,96.585 H 70.236 l 9.633,-37.776 h 37.796 z"
id="path14" />
<use
xlink:href="#K"
x="9.6339998"
y="-37.777"
id="use14" />
<path
d="M 60.602,134.362 H 22.807 L 32.44,96.586 h 37.796 z"
id="path15" />
<path
d="M 70.236,96.585 H 32.441 L 42.074,58.809 H 79.87 Z"
id="path16" />
<path
d="M 79.87,58.809 H 42.075 l 9.633,-37.776 h 37.796 z"
id="path17" />
<use
xlink:href="#K"
x="57.063"
y="-75.553001"
id="use17" />
<path
d="M 50.968,172.138 H 13.173 l 9.633,-37.776 h 37.796 z"
id="path18" />
<path
d="M 41.334,209.915 H 3.539 l 9.633,-37.776 h 37.796 z"
id="path19" />
<use
xlink:href="#K"
x="37.794998"
id="use19" />
<use
xlink:href="#K"
x="47.429001"
y="-37.777"
id="use20" />
<use
xlink:href="#K"
x="28.160999"
y="37.776001"
id="use21" />
<use
xlink:href="#K"
x="18.527"
y="75.553001"
id="use22" />
</g>
<path
d="M 114.115,134.359 H 76.321 l 9.633,-37.776 h 37.796 z"
class="I"
id="path22" />
<use
xlink:href="#K"
x="-31.709999"
y="37.772999"
class="J"
id="use23" />
<g
class="I"
id="g25">
<use
xlink:href="#K"
x="-22.076"
y="-0.003"
id="use24" />
<use
xlink:href="#K"
x="15.719"
y="-0.003"
id="use25" />
</g>
<g
class="K"
id="g26">
<path
d="M 123.749,96.582 H 85.955 l 9.633,-37.776 h 37.796 z"
id="path25" />
<use
xlink:href="#K"
x="25.353001"
y="-37.779999"
id="use26" />
</g>
<path
d="M 76.32,134.359 H 38.526 l 9.633,-37.776 h 37.796 z"
class="I"
id="path26" />
<path
d="M 85.954,96.582 H 48.16 l 9.633,-37.776 h 37.796 z"
class="K"
id="path27" />
<g
fill="#ffce00"
id="g28">
<path
d="M 95.588,58.806 H 57.794 L 67.427,21.03 h 37.796 z"
id="path28" />
<use
xlink:href="#K"
x="72.781998"
y="-75.556"
id="use28" />
</g>
<path
d="M 66.686,172.135 H 28.892 l 9.633,-37.776 h 37.796 z"
class="J"
id="path29" />
<path
d="M 57.052,209.912 H 19.258 l 9.633,-37.776 h 37.796 z"
fill="#ff0107"
id="path30" />
<use
xlink:href="#K"
x="53.514"
y="-0.003"
class="I"
id="use30" />
<path
d="M 237.135,96.582 H 199.34 l 9.633,-37.776 h 37.796 z"
class="K"
id="path31" />
<use
xlink:href="#K"
x="43.880001"
y="37.772999"
class="J"
id="use31" />
<use
xlink:href="#K"
x="34.245998"
y="75.550003"
fill="#ff0107"
id="use32" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,67 @@
import { encodeBinaryData, processResponseData } from '../GenericFunctions';
describe('Mistral OCR Generic Functions', () => {
describe('encodeBinaryData', () => {
const binaryBuffer = Buffer.from('testdata');
const base64 = binaryBuffer.toString('base64');
const context = {
getNodeParameter: jest.fn(),
helpers: {
assertBinaryData: jest.fn(),
getBinaryDataBuffer: jest.fn(),
},
} as any;
beforeEach(() => {
jest.clearAllMocks();
});
it('should encode binary data to a data URL', async () => {
context.getNodeParameter.mockReturnValue('binaryProp1');
context.helpers.assertBinaryData.mockReturnValue({
mimeType: 'image/png',
fileName: 'file.png',
});
context.helpers.getBinaryDataBuffer.mockResolvedValue(binaryBuffer);
const result = await encodeBinaryData.call(context, 0);
expect(context.getNodeParameter).toHaveBeenCalledWith('binaryProperty', 0);
expect(context.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'binaryProp1');
expect(context.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'binaryProp1');
expect(result).toEqual({
dataUrl: `data:image/png;base64,${base64}`,
fileName: 'file.png',
});
});
});
describe('processResponseData', () => {
it('should extract text and page count from pages', () => {
const input = {
pages: [
{ markdown: 'Page 1 markdown', text: 'Page 1 text' },
{ markdown: 'Page 2 markdown', text: 'Page 2 text' },
],
otherProp: 'test',
};
const result = processResponseData(input);
expect(result.extractedText).toBe('Page 1 markdown\n\nPage 2 markdown');
expect(result.pageCount).toBe(2);
expect(result.otherProp).toBe('test');
});
it('should handle empty pages array', () => {
const input = { pages: [] };
const result = processResponseData(input);
expect(result.extractedText).toBe('');
expect(result.pageCount).toBe(0);
});
});
});

View File

@@ -0,0 +1,216 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import path from 'path';
import batchResult from './fixtures/batch.json';
import documentResult from './fixtures/document.json';
import imageResult from './fixtures/image.json';
describe('Mistral AI Node', () => {
const credentials = {
mistralCloudApi: {
apiKey: 'API-KEY',
},
};
const mistralAiNock = nock('https://api.mistral.ai');
describe('Document -> Extract Text', () => {
beforeAll(() => {
// Document by URL
mistralAiNock
.post('/v1/ocr', {
model: 'mistral-ocr-latest',
document: {
type: 'document_url',
document_url: 'https://example.com/document.pdf',
},
})
.reply(200, documentResult);
// Image by URL
mistralAiNock
.post('/v1/ocr', {
model: 'mistral-ocr-latest',
document: {
type: 'image_url',
image_url: 'https://example.com/image.jpg',
},
})
.reply(200, imageResult);
// Document from binary
mistralAiNock
.post('/v1/ocr', {
model: 'mistral-ocr-latest',
document: {
type: 'document_url',
document_name: 'sample.pdf',
document_url: 'data:application/pdf;base64,abcdefgh',
},
})
.reply(200, documentResult);
// Image from binary
mistralAiNock
.post('/v1/ocr', {
model: 'mistral-ocr-latest',
document: {
type: 'image_url',
image_url: 'data:image/jpeg;base64,abcdefgh',
},
})
.reply(200, imageResult);
// Batching
mistralAiNock
.post(
'/v1/files',
(body: string) =>
body.includes(
JSON.stringify({
document: {
type: 'document_url',
document_name: 'sample_1.pdf',
document_url: 'data:application/pdf;base64,abcdefgh',
},
}),
) &&
body.includes(
JSON.stringify({
document: {
type: 'document_url',
document_name: 'sample_2.pdf',
document_url: 'data:application/pdf;base64,aaaaaaaa',
},
}),
),
)
.reply(200, { id: 'input-file-1' });
mistralAiNock
.post('/v1/files', (body: string) =>
body.includes(
JSON.stringify({
document: {
type: 'document_url',
document_name: 'sample_3.pdf',
document_url: 'data:application/pdf;base64,aaaabbbb',
},
}),
),
)
.reply(200, { id: 'input-file-2' });
mistralAiNock
.post('/v1/batch/jobs', {
model: 'mistral-ocr-latest',
input_files: ['input-file-1'],
endpoint: '/v1/ocr',
})
.reply(200, { id: 'job-1' });
mistralAiNock
.post('/v1/batch/jobs', {
model: 'mistral-ocr-latest',
input_files: ['input-file-2'],
endpoint: '/v1/ocr',
})
.reply(200, { id: 'job-2' });
mistralAiNock.get('/v1/batch/jobs/job-1').reply(200, {
status: 'SUCCESS',
output_file: 'output-file-1',
});
mistralAiNock.get('/v1/batch/jobs/job-2').reply(200, {
status: 'SUCCESS',
output_file: 'output-file-2',
});
mistralAiNock.get('/v1/files/output-file-1/content').reply(
200,
batchResult
.slice(0, 2)
.map((item) => JSON.stringify(item))
.join('\n'),
);
mistralAiNock.get('/v1/files/output-file-2/content').reply(200, batchResult[2]);
// Batching with delete files
mistralAiNock
.post(
'/v1/files',
(body: string) =>
body.includes(
JSON.stringify({
document: {
type: 'document_url',
document_name: 'sample_1.pdf',
document_url: 'data:application/pdf;base64,abcdefgh',
},
}),
) &&
body.includes(
JSON.stringify({
document: {
type: 'document_url',
document_name: 'sample_2.pdf',
document_url: 'data:application/pdf;base64,aaaaaaaa',
},
}),
),
)
.reply(200, { id: 'input-file-1' });
mistralAiNock
.post('/v1/files', (body: string) =>
body.includes(
JSON.stringify({
document: {
type: 'document_url',
document_name: 'sample_3.pdf',
document_url: 'data:application/pdf;base64,aaaabbbb',
},
}),
),
)
.reply(200, { id: 'input-file-2' });
mistralAiNock
.post('/v1/batch/jobs', {
model: 'mistral-ocr-latest',
input_files: ['input-file-1'],
endpoint: '/v1/ocr',
})
.reply(200, { id: 'job-1' });
mistralAiNock
.post('/v1/batch/jobs', {
model: 'mistral-ocr-latest',
input_files: ['input-file-2'],
endpoint: '/v1/ocr',
})
.reply(200, { id: 'job-2' });
mistralAiNock.get('/v1/batch/jobs/job-1').reply(200, {
status: 'SUCCESS',
output_file: 'output-file-1',
});
mistralAiNock.get('/v1/batch/jobs/job-2').reply(200, {
status: 'SUCCESS',
output_file: 'output-file-2',
});
mistralAiNock.delete('/v1/files/input-file-1').reply(200);
mistralAiNock.delete('/v1/files/input-file-2').reply(200);
mistralAiNock.get('/v1/files/output-file-1/content').reply(
200,
batchResult
.slice(0, 2)
.map((item) => JSON.stringify(item))
.join('\n'),
);
mistralAiNock.get('/v1/files/output-file-2/content').reply(200, batchResult[2]);
mistralAiNock.delete('/v1/files/output-file-1').reply(200);
mistralAiNock.delete('/v1/files/output-file-2').reply(200);
});
afterAll(() => mistralAiNock.done());
new NodeTestHarness({
additionalPackagePaths: [path.join(__dirname, '../../../../@n8n/nodes-langchain')],
}).setupTests({
credentials,
workflowFiles: ['workflow.json'],
});
});
});

View File

@@ -0,0 +1,77 @@
[
{
"custom_id": "0",
"response": {
"body": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 1",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
}
}
}
},
{
"custom_id": "1",
"response": {
"body": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 2",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
}
}
}
},
{
"custom_id": "2",
"response": {
"body": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 3",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
}
}
}
}
]

View File

@@ -0,0 +1,20 @@
{
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
}
}

View File

@@ -0,0 +1,20 @@
{
"pages": [
{
"index": 0,
"markdown": "# EXAMPLE",
"images": [],
"dimensions": {
"dpi": 200,
"height": 408,
"width": 612
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 76734
}
}

View File

@@ -0,0 +1,582 @@
{
"name": "Mistral AI",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-400, -380],
"id": "48cf408c-de44-4ed2-8a43-d16a4fa5b95b",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"inputType": "url",
"url": "https://example.com/document.pdf"
},
"type": "n8n-nodes-base.mistralAi",
"typeVersion": 1,
"position": [-180, -680],
"id": "d7af696f-26a4-419e-9377-02c91d0ea23d",
"name": "Document by URL",
"credentials": {
"mistralCloudApi": {
"id": "l4fJwkB8bVdo6Evk",
"name": "Mistral Cloud account"
}
}
},
{
"parameters": {
"inputType": "url",
"documentType": "image_url",
"url": "https://example.com/image.jpg"
},
"type": "n8n-nodes-base.mistralAi",
"typeVersion": 1,
"position": [-180, -480],
"id": "9bddff7e-00ab-412c-9cf3-52f88e941641",
"name": "Image by URL",
"credentials": {
"mistralCloudApi": {
"id": "l4fJwkB8bVdo6Evk",
"name": "Mistral Cloud account"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.mistralAi",
"typeVersion": 1,
"position": [260, -380],
"id": "cd64700f-abfb-48c7-b744-c68365a7cda2",
"name": "Document from binary",
"credentials": {
"mistralCloudApi": {
"id": "l4fJwkB8bVdo6Evk",
"name": "Mistral Cloud account"
}
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "5c5233de-3b47-4860-8598-f50059061fee",
"name": "data",
"value": "abcdefgh",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [-180, -280],
"id": "ebc64cb6-524b-4ae3-8a1f-c51c33e8043b",
"name": "Set mock data"
},
{
"parameters": {
"operation": "toBinary",
"sourceProperty": "data",
"options": {
"fileName": "sample.pdf",
"mimeType": "application/pdf"
}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [40, -380],
"id": "9f656043-c21e-4eb8-b591-a609634e487f",
"name": "Create PDF"
},
{
"parameters": {
"documentType": "image_url"
},
"type": "n8n-nodes-base.mistralAi",
"typeVersion": 1,
"position": [260, -180],
"id": "8f2cc502-c9b0-4561-9ff9-0734d336de6d",
"name": "Image from binary",
"credentials": {
"mistralCloudApi": {
"id": "l4fJwkB8bVdo6Evk",
"name": "Mistral Cloud account"
}
}
},
{
"parameters": {
"operation": "toBinary",
"sourceProperty": "data",
"options": {
"fileName": "sample.jpg",
"mimeType": "image/jpeg"
}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [40, -180],
"id": "e1398740-1b36-493b-8950-caf153d6fdd9",
"name": "Create JPG"
},
{
"parameters": {
"fieldToSplitOut": "data",
"options": {}
},
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [40, 20],
"id": "c6810b4b-804c-44e6-89e0-a73d585750ee",
"name": "Split Out"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "0fc6f054-a7f0-466f-b54c-612379b9c049",
"name": "data",
"value": "[\"abcdefgh\", \"aaaaaaaa\", \"aaaabbbb\"]",
"type": "array"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [-180, 20],
"id": "887989fe-2829-43d5-86e6-333f399035b3",
"name": "Set mock data for multiple files"
},
{
"parameters": {
"operation": "toBinary",
"sourceProperty": "data",
"options": {
"fileName": "=sample_{{ $itemIndex + 1 }}.pdf",
"mimeType": "application/pdf"
}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [260, 20],
"id": "27ef8f5b-ee55-45f9-b66a-741cc6ccde21",
"name": "Create PDFs"
},
{
"parameters": {
"options": {
"batch": true,
"batchSize": 2,
"deleteFiles": false
}
},
"type": "n8n-nodes-base.mistralAi",
"typeVersion": 1,
"position": [480, -80],
"id": "ad4b06b9-65e7-4b9c-b06a-bcbf0df5999a",
"name": "Batching",
"credentials": {
"mistralCloudApi": {
"id": "l4fJwkB8bVdo6Evk",
"name": "Mistral Cloud account"
}
}
},
{
"parameters": {
"options": {
"batch": true,
"batchSize": 2
}
},
"type": "n8n-nodes-base.mistralAi",
"typeVersion": 1,
"position": [480, 120],
"id": "ca14c489-02d2-459f-b694-d83112d95d13",
"name": "Batching with delete files",
"credentials": {
"mistralCloudApi": {
"id": "l4fJwkB8bVdo6Evk",
"name": "Mistral Cloud account"
}
}
}
],
"pinData": {
"Document by URL": [
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file",
"pageCount": 1
}
}
],
"Image by URL": [
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# EXAMPLE",
"images": [],
"dimensions": {
"dpi": 200,
"height": 408,
"width": 612
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 76734
},
"extractedText": "# EXAMPLE",
"pageCount": 1
}
}
],
"Document from binary": [
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file",
"pageCount": 1
}
}
],
"Image from binary": [
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# EXAMPLE",
"images": [],
"dimensions": {
"dpi": 200,
"height": 408,
"width": 612
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 76734
},
"extractedText": "# EXAMPLE",
"pageCount": 1
}
}
],
"Batching": [
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 1",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file 1",
"pageCount": 1
}
},
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 2",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file 2",
"pageCount": 1
}
},
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 3",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file 3",
"pageCount": 1
}
}
],
"Batching with delete files": [
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 1",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file 1",
"pageCount": 1
}
},
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 2",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file 2",
"pageCount": 1
}
},
{
"json": {
"pages": [
{
"index": 0,
"markdown": "# Dummy PDF file 3",
"images": [],
"dimensions": {
"dpi": 200,
"height": 2339,
"width": 1653
}
}
],
"model": "mistral-ocr-2505-completion",
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 13264
},
"extractedText": "# Dummy PDF file 3",
"pageCount": 1
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Document by URL",
"type": "main",
"index": 0
},
{
"node": "Image by URL",
"type": "main",
"index": 0
},
{
"node": "Set mock data",
"type": "main",
"index": 0
},
{
"node": "Set mock data for multiple files",
"type": "main",
"index": 0
}
]
]
},
"Set mock data": {
"main": [
[
{
"node": "Create PDF",
"type": "main",
"index": 0
},
{
"node": "Create JPG",
"type": "main",
"index": 0
}
]
]
},
"Create PDF": {
"main": [
[
{
"node": "Document from binary",
"type": "main",
"index": 0
}
]
]
},
"Create JPG": {
"main": [
[
{
"node": "Image from binary",
"type": "main",
"index": 0
}
]
]
},
"Set mock data for multiple files": {
"main": [
[
{
"node": "Split Out",
"type": "main",
"index": 0
}
]
]
},
"Split Out": {
"main": [
[
{
"node": "Create PDFs",
"type": "main",
"index": 0
}
]
]
},
"Create PDFs": {
"main": [
[
{
"node": "Batching",
"type": "main",
"index": 0
},
{
"node": "Batching with delete files",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "2c53fedc-f105-4f02-b1e0-8e6dcf4da1fa",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "e115be144a6a5547dbfca93e774dfffa178aa94a181854c13e2ce5e14d195b2e"
},
"id": "N6UCbkc3HPvoTa0M",
"tags": []
}

View File

@@ -0,0 +1,32 @@
import type { IDataObject } from 'n8n-workflow';
export interface BatchJob {
id: string;
status:
| 'QUEUED'
| 'RUNNING'
| 'SUCCESS'
| 'FAILED'
| 'TIMEOUT_EXCEEDED'
| 'CANCELLATION_REQUESTED'
| 'CANCELLED';
output_file: string;
errors: IDataObject[];
}
export interface BatchItemResult {
id: string;
custom_id: string;
response: {
body: {
pages: Page[];
};
};
error?: IDataObject;
}
export interface Page {
index: number;
markdown: string;
images: IDataObject[];
}