diff --git a/sim/blocks/blocks/mistral-parse.ts b/sim/blocks/blocks/mistral-parse.ts new file mode 100644 index 00000000000..bdb91f120c5 --- /dev/null +++ b/sim/blocks/blocks/mistral-parse.ts @@ -0,0 +1,199 @@ +import { MistralParserOutput } from '@/tools/mistral/parser' +import { BlockConfig } from '../types' +import { MistralIcon } from '@/components/icons' + +export const MistralParseBlock: BlockConfig = { + type: 'mistral_parse', + name: 'Mistral PDF Parser', + description: 'Extract text from PDF documents', + longDescription: + 'Extract text and structure from PDF documents using Mistral\'s OCR API. Enter a URL to a PDF document, configure processing options, and get the content in your preferred format.', + category: 'tools', + bgColor: '#000000', + icon: MistralIcon, + subBlocks: [ + { + id: 'filePath', + title: 'PDF Document URL', + type: 'short-input', + layout: 'full', + placeholder: 'Enter full URL to a PDF document (https://example.com/document.pdf)', + }, + { + id: 'resultType', + title: 'Output Format', + type: 'dropdown', + layout: 'full', + options: [ + { id: 'markdown', label: 'Markdown (Formatted)' }, + { id: 'text', label: 'Plain Text' }, + { id: 'json', label: 'JSON (Raw)' } + ], + }, + { + id: 'pages', + title: 'Specific Pages', + type: 'short-input', + layout: 'full', + placeholder: 'e.g. 0,1,2 (leave empty for all pages)', + }, + /* + * Image-related parameters - temporarily disabled + * Uncomment if PDF image extraction is needed + * + { + id: 'includeImageBase64', + title: 'Include PDF Images', + type: 'switch', + layout: 'half', + }, + { + id: 'imageLimit', + title: 'Max Images', + type: 'short-input', + layout: 'half', + placeholder: 'Maximum number of images to extract', + }, + { + id: 'imageMinSize', + title: 'Min Image Size (px)', + type: 'short-input', + layout: 'half', + placeholder: 'Min width/height in pixels', + }, + */ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your Mistral API key', + password: true, + }, + ], + tools: { + access: ['mistral_parser'], + config: { + tool: () => 'mistral_parser', + params: (params) => { + // Basic validation + if (!params || !params.apiKey || params.apiKey.trim() === '') { + throw new Error('Mistral API key is required'); + } + + if (!params || !params.filePath || params.filePath.trim() === '') { + throw new Error('PDF Document URL is required'); + } + + // Validate URL format + let validatedUrl; + try { + // Try to create a URL object to validate format + validatedUrl = new URL(params.filePath.trim()); + + // Ensure URL is using HTTP or HTTPS protocol + if (!['http:', 'https:'].includes(validatedUrl.protocol)) { + throw new Error(`URL must use HTTP or HTTPS protocol. Found: ${validatedUrl.protocol}`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Invalid URL format: ${errorMessage}`); + } + + // Process pages input (convert from comma-separated string to array of numbers) + let pagesArray: number[] | undefined = undefined; + if (params.pages && params.pages.trim() !== '') { + try { + pagesArray = params.pages + .split(',') + .map((p: string) => p.trim()) + .filter((p: string) => p.length > 0) + .map((p: string) => { + const num = parseInt(p, 10); + if (isNaN(num) || num < 0) { + throw new Error(`Invalid page number: ${p}`); + } + return num; + }); + + if (pagesArray && pagesArray.length === 0) { + pagesArray = undefined; + } + } catch (error: any) { + throw new Error(`Page number format error: ${error.message}`); + } + } + + // Process numeric inputs + let imageLimit: number | undefined = undefined; + if (params.imageLimit && params.imageLimit.trim() !== '') { + const limit = parseInt(params.imageLimit, 10); + if (!isNaN(limit) && limit > 0) { + imageLimit = limit; + } else { + throw new Error('Image limit must be a positive number'); + } + } + + let imageMinSize: number | undefined = undefined; + if (params.imageMinSize && params.imageMinSize.trim() !== '') { + const size = parseInt(params.imageMinSize, 10); + if (!isNaN(size) && size > 0) { + imageMinSize = size; + } else { + throw new Error('Minimum image size must be a positive number'); + } + } + + // Return structured parameters for the tool + const parameters: any = { + filePath: validatedUrl.toString(), + apiKey: params.apiKey.trim(), + resultType: params.resultType || 'markdown', + }; + + // Add optional parameters if they're defined + if (pagesArray && pagesArray.length > 0) { + parameters.pages = pagesArray; + } + + /* + * Image-related parameters - temporarily disabled + * Uncomment if PDF image extraction is needed + * + if (typeof params.includeImageBase64 === 'boolean') { + parameters.includeImageBase64 = params.includeImageBase64; + } + + if (imageLimit !== undefined) { + parameters.imageLimit = imageLimit; + } + + if (imageMinSize !== undefined) { + parameters.imageMinSize = imageMinSize; + } + */ + + return parameters; + }, + }, + }, + inputs: { + filePath: { type: 'string', required: true }, + apiKey: { type: 'string', required: true }, + resultType: { type: 'string', required: false }, + pages: { type: 'string', required: false }, + // Image-related inputs - temporarily disabled + // includeImageBase64: { type: 'boolean', required: false }, + // imageLimit: { type: 'string', required: false }, + // imageMinSize: { type: 'string', required: false }, + }, + outputs: { + response: { + type: { + content: 'string', + metadata: 'json', + }, + }, + }, +} \ No newline at end of file diff --git a/sim/blocks/index.ts b/sim/blocks/index.ts index adf5e258b65..efbe565b80b 100644 --- a/sim/blocks/index.ts +++ b/sim/blocks/index.ts @@ -8,6 +8,7 @@ import { GoogleDocsBlock } from './blocks/docs' import { GoogleDriveBlock } from './blocks/drive' import { EvaluatorBlock } from './blocks/evaluator' import { ExaBlock } from './blocks/exa' +import { MistralParseBlock } from './blocks/mistral-parse' import { FileBlock } from './blocks/file' import { FirecrawlBlock } from './blocks/firecrawl' import { FunctionBlock } from './blocks/function' @@ -42,11 +43,12 @@ export { AgentBlock, AirtableBlock, ApiBlock, - FileBlock, + MistralParseBlock, FunctionBlock, VisionBlock, FirecrawlBlock, // GuestyBlock, + FileBlock, JinaBlock, TranslateBlock, SlackBlock, @@ -86,8 +88,9 @@ const blocks: Record = { confluence: ConfluenceBlock, evaluator: EvaluatorBlock, exa: ExaBlock, - file: FileBlock, + mistral_parse: MistralParseBlock, firecrawl: FirecrawlBlock, + file: FileBlock, function: FunctionBlock, github: GitHubBlock, gmail: GmailBlock, diff --git a/sim/components/icons.tsx b/sim/components/icons.tsx index c5843f8d6e0..4efc74806e7 100644 --- a/sim/components/icons.tsx +++ b/sim/components/icons.tsx @@ -1819,3 +1819,59 @@ export function DocumentIcon(props: SVGProps) { ) } + +export function MistralIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/sim/tools/index.ts b/sim/tools/index.ts index 6dfb41f2d9f..7b2a6c718d9 100644 --- a/sim/tools/index.ts +++ b/sim/tools/index.ts @@ -20,6 +20,7 @@ import { guestyGuestTool, guestyReservationTool } from './guesty' import { requestTool as httpRequest } from './http/request' import { contactsTool as hubspotContacts } from './hubspot/contacts' import { readUrlTool } from './jina/reader' +import { mistralParserTool } from './mistral' import { notionReadTool, notionWriteTool } from './notion' import { dalleTool } from './openai/dalle' import { embeddingsTool as openAIEmbeddings } from './openai/embeddings' @@ -116,6 +117,7 @@ export const tools: Record = { airtable_read: airtableReadTool, airtable_write: airtableWriteTool, airtable_update: airtableUpdateTool, + mistral_parser: mistralParserTool, } // Get a tool by its ID @@ -295,7 +297,7 @@ function getCustomTool(customToolId: string): ToolConfig | undefined { }, // Response handling - transformResponse: async (response: Response) => { + transformResponse: async (response: Response, params: Record) => { const data = await response.json() if (!data.success) { @@ -597,7 +599,7 @@ async function handleInternalRequest( // Use the tool's response transformer if available if (tool.transformResponse) { - return await tool.transformResponse(response) + return await tool.transformResponse(response, params) } // Default response handling diff --git a/sim/tools/mistral/index.ts b/sim/tools/mistral/index.ts new file mode 100644 index 00000000000..02f638712b2 --- /dev/null +++ b/sim/tools/mistral/index.ts @@ -0,0 +1,3 @@ +import { mistralParserTool } from './parser' + +export { mistralParserTool } \ No newline at end of file diff --git a/sim/tools/mistral/parser.ts b/sim/tools/mistral/parser.ts new file mode 100644 index 00000000000..ca7d828f78b --- /dev/null +++ b/sim/tools/mistral/parser.ts @@ -0,0 +1,469 @@ +import { ToolConfig, ToolResponse } from '../types' + +/** + * Input parameters for the Mistral OCR parser tool + */ +export interface MistralParserInput { + /** URL to a PDF document to be processed */ + filePath: string; + + /** Mistral API key for authentication */ + apiKey: string; + + /** Output format for the extracted content (default: 'markdown') */ + resultType?: 'markdown' | 'text' | 'json'; + + /** Whether to include base64-encoded images in the response */ + includeImageBase64?: boolean; + + /** Specific pages to process (zero-indexed) */ + pages?: number[]; + + /** Maximum number of images to extract from the PDF */ + imageLimit?: number; + + /** Minimum height and width (in pixels) for images to extract */ + imageMinSize?: number; +} + +/** + * Usage information returned by the Mistral OCR API + */ +export interface MistralOcrUsageInfo { + /** Number of pages processed in the document */ + pagesProcessed: number; + + /** Size of the document in bytes */ + docSizeBytes: number; +} + +/** + * Metadata about the processed document + */ +export interface MistralParserMetadata { + /** Unique identifier for this OCR job */ + jobId: string; + + /** File type of the document (typically 'pdf') */ + fileType: string; + + /** Filename extracted from the document URL */ + fileName: string; + + /** Source type (always 'url' for now) */ + source: 'url'; + + /** Original URL to the document */ + sourceUrl: string; + + /** Total number of pages in the document */ + pageCount: number; + + /** Usage statistics from the OCR processing */ + usageInfo?: MistralOcrUsageInfo; + + /** The Mistral OCR model used for processing */ + model: string; + + /** The output format that was requested */ + resultType?: 'markdown' | 'text' | 'json'; + + /** ISO timestamp when the document was processed */ + processedAt: string; +} + +/** + * Output data structure from the Mistral OCR parser + */ +export interface MistralParserOutputData { + /** Extracted content in the requested format */ + content: string; + + /** Metadata about the parsed document and processing */ + metadata: MistralParserMetadata; +} + +/** + * Complete response from the Mistral OCR parser tool + */ +export interface MistralParserOutput extends ToolResponse { + /** The output data containing content and metadata */ + output: MistralParserOutputData; +} + +export const mistralParserTool: ToolConfig = { + id: 'mistral_parser', + name: 'Mistral PDF Parser', + description: 'Parse PDF documents using Mistral OCR API', + version: '1.0.0', + + params: { + filePath: { + type: 'string', + required: true, + description: 'URL to a PDF document to be processed', + }, + resultType: { + type: 'string', + required: false, + description: 'Type of parsed result (markdown, text, or json). Defaults to markdown.', + }, + apiKey: { + type: 'string', + required: true, + requiredForToolCall: true, + description: 'Mistral API key (MISTRAL_API_KEY)', + }, + includeImageBase64: { + type: 'boolean', + required: false, + description: 'Include base64-encoded images in the response', + }, + pages: { + type: 'array', + required: false, + description: 'Specific pages to process (array of page numbers, starting from 0)', + }, + // Note: The following image-related parameters are still supported by the parser + // but are disabled in the UI. They can be re-enabled if needed. + imageLimit: { + type: 'number', + required: false, + description: 'Maximum number of images to extract from the PDF', + }, + imageMinSize: { + type: 'number', + required: false, + description: 'Minimum height and width of images to extract from the PDF', + }, + }, + + request: { + url: 'https://api.mistral.ai/v1/ocr', + method: 'POST', + headers: (params) => { + console.log('Setting up headers with API key:', params.apiKey ? `${params.apiKey.substring(0, 5)}...` : 'Missing'); + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${params.apiKey}`, + }; + }, + body: (params) => { + if (!params || typeof params !== 'object') { + throw new Error('Invalid parameters: Parameters must be provided as an object'); + } + + // Validate required parameters + if (!params.apiKey || typeof params.apiKey !== 'string' || params.apiKey.trim() === '') { + throw new Error('Missing or invalid API key: A valid Mistral API key is required'); + } + + if (!params.filePath || typeof params.filePath !== 'string' || params.filePath.trim() === '') { + throw new Error('Missing or invalid file path: Please provide a URL to a PDF document'); + } + + // Validate and normalize URL + let url; + try { + url = new URL(params.filePath.trim()); + + // Validate protocol + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error(`Invalid protocol: ${url.protocol}. URL must use HTTP or HTTPS protocol`); + } + + // Validate file appears to be a PDF (loose check) + const pathname = url.pathname.toLowerCase(); + if (!pathname.endsWith('.pdf') && !pathname.includes('pdf')) { + console.warn( + 'Warning: URL does not appear to be a PDF document. ' + + 'If this is incorrect, the document may still be processed if it is a valid PDF.' + ); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error( + `Invalid URL format: ${errorMessage}. ` + + 'Please provide a valid HTTP or HTTPS URL to a PDF document (e.g., https://example.com/document.pdf)' + ); + } + + // Create the request body with required parameters + const requestBody: Record = { + model: "mistral-ocr-latest", + document: { + type: "document_url", + document_url: url.toString() + } + }; + + // Add optional parameters with proper validation + // Include images (base64) + if (params.includeImageBase64 !== undefined) { + if (typeof params.includeImageBase64 !== 'boolean') { + console.warn('includeImageBase64 parameter should be a boolean, using default (false)'); + } else { + requestBody.include_image_base64 = params.includeImageBase64; + } + } + + // Page selection + if (params.pages !== undefined) { + if (Array.isArray(params.pages) && params.pages.length > 0) { + // Validate all page numbers are non-negative integers + const validPages = params.pages.filter( + (page) => typeof page === 'number' && Number.isInteger(page) && page >= 0 + ); + + if (validPages.length > 0) { + requestBody.pages = validPages; + + if (validPages.length !== params.pages.length) { + console.warn( + `Some invalid page numbers were removed. ` + + `Using ${validPages.length} valid pages: ${validPages.join(', ')}` + ); + } + } else { + console.warn('No valid page numbers provided, processing all pages'); + } + } else if (params.pages.length === 0) { + console.warn('Empty pages array provided, processing all pages'); + } + } + + // Image limit + if (params.imageLimit !== undefined) { + const imageLimit = Number(params.imageLimit); + if (Number.isInteger(imageLimit) && imageLimit > 0) { + requestBody.image_limit = imageLimit; + } else { + console.warn('imageLimit must be a positive integer, ignoring this parameter'); + } + } + + // Minimum image size + if (params.imageMinSize !== undefined) { + const imageMinSize = Number(params.imageMinSize); + if (Number.isInteger(imageMinSize) && imageMinSize > 0) { + requestBody.image_min_size = imageMinSize; + } else { + console.warn('imageMinSize must be a positive integer, ignoring this parameter'); + } + } + + // Log the request (with sensitive data redacted) + console.log('Mistral OCR request:', { + url: url.toString(), + hasApiKey: !!params.apiKey, + model: requestBody.model, + options: { + includesImages: requestBody.include_image_base64 ?? 'not specified', + pages: requestBody.pages ?? 'all pages', + imageLimit: requestBody.image_limit ?? 'no limit', + imageMinSize: requestBody.image_min_size ?? 'no minimum', + } + }); + + return requestBody; + }, + }, + + transformResponse: async (response, params?) => { + try { + // Verify response status + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Mistral OCR API error: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`); + } + + // Parse response data with proper error handling + let ocrResult; + try { + ocrResult = await response.json(); + } catch (jsonError) { + throw new Error(`Failed to parse Mistral OCR response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`); + } + + if (!ocrResult || typeof ocrResult !== 'object') { + throw new Error('Invalid response format from Mistral OCR API'); + } + + // Set default values and extract from params if available + let resultType: 'markdown' | 'text' | 'json' = 'markdown'; + let sourceUrl = ''; + + if (params && typeof params === 'object') { + if (params.filePath && typeof params.filePath === 'string') { + sourceUrl = params.filePath.trim(); + } + + if (params.resultType && ['markdown', 'text', 'json'].includes(params.resultType)) { + resultType = params.resultType as 'markdown' | 'text' | 'json'; + } + } else if (ocrResult.document && typeof ocrResult.document === 'object' && + ocrResult.document.document_url && typeof ocrResult.document.document_url === 'string') { + sourceUrl = ocrResult.document.document_url; + } + + // Process content from pages + let content = ''; + const pageCount = ocrResult.pages && Array.isArray(ocrResult.pages) ? ocrResult.pages.length : 0; + + if (pageCount > 0) { + content = ocrResult.pages + .map((page: any) => (page && typeof page.markdown === 'string') ? page.markdown : '') + .filter(Boolean) + .join('\n\n'); + } else { + console.warn('No pages found in OCR result, returning raw response'); + content = JSON.stringify(ocrResult, null, 2); + } + + // Process based on requested result type + if (resultType === 'text') { + // Strip markdown formatting + content = content + .replace(/\#\#*\s/g, '') // Remove markdown headers + .replace(/\*\*/g, '') // Remove bold markers + .replace(/\*/g, '') // Remove italic markers + .replace(/\n{3,}/g, '\n\n'); // Normalize newlines + } else if (resultType === 'json') { + // Return the structured data as JSON string + content = JSON.stringify(ocrResult, null, 2); + } + + // Extract file information with proper validation + let fileName = 'document.pdf'; + let fileType = 'pdf'; + + if (sourceUrl) { + try { + const url = new URL(sourceUrl); + const pathSegments = url.pathname.split('/'); + const lastSegment = pathSegments[pathSegments.length - 1]; + + if (lastSegment && lastSegment.length > 0) { + fileName = lastSegment; + const fileExtParts = fileName.split('.'); + if (fileExtParts.length > 1) { + fileType = fileExtParts[fileExtParts.length - 1].toLowerCase(); + } + } + } catch (urlError) { + console.warn('Failed to parse document URL:', urlError); + } + } + + // Generate a tracking ID with timestamp and random component for uniqueness + const timestamp = Date.now(); + const randomId = Math.random().toString(36).substring(2, 10); + const jobId = `mistral-ocr-${timestamp}-${randomId}`; + + // Map API response fields to our schema with proper type checking + const usageInfo = ocrResult.usage_info && typeof ocrResult.usage_info === 'object' + ? { + pagesProcessed: typeof ocrResult.usage_info.pages_processed === 'number' + ? ocrResult.usage_info.pages_processed + : Number(ocrResult.usage_info.pages_processed), + docSizeBytes: typeof ocrResult.usage_info.doc_size_bytes === 'number' + ? ocrResult.usage_info.doc_size_bytes + : Number(ocrResult.usage_info.doc_size_bytes) + } + : undefined; + + // Return properly structured response + const parserResponse: MistralParserOutput = { + success: true, + output: { + content, + metadata: { + jobId, + fileType, + fileName, + source: 'url', + sourceUrl, + pageCount, + usageInfo, + model: typeof ocrResult.model === 'string' ? ocrResult.model : 'mistral-ocr-latest', + resultType, + processedAt: new Date().toISOString(), + }, + }, + }; + + return parserResponse; + } catch (error) { + console.error('Error processing OCR result:', error); + throw error; + } + }, + + transformError: (error) => { + console.error('Mistral OCR processing error:', error); + + // Helper function to extract message from various error types + const getErrorMessage = (err: any): string => { + if (typeof err === 'string') return err; + if (err instanceof Error) return err.message; + if (err && typeof err === 'object') { + if (err.message) return String(err.message); + if (err.error) return typeof err.error === 'string' ? err.error : JSON.stringify(err.error); + } + return 'Unknown error'; + }; + + // Get base error message + const errorMsg = getErrorMessage(error); + + // Handle common API error status codes + if (typeof error === 'object' && error !== null) { + const status = error.status || (error.response && error.response.status); + + if (status) { + switch (status) { + case 400: + return 'Mistral OCR Error: The request was invalid. Please check your PDF URL and parameters.'; + case 401: + return 'Mistral OCR Error: Invalid API key. Please check your Mistral API key.'; + case 403: + return 'Mistral OCR Error: Access forbidden. Your API key may not have permission to use the OCR service.'; + case 404: + return 'Mistral OCR Error: The PDF document could not be found. Please check that the URL is accessible.'; + case 413: + return 'Mistral OCR Error: The PDF document is too large for processing.'; + case 415: + return 'Mistral OCR Error: Unsupported file format. Please ensure the URL points to a valid PDF document.'; + case 429: + return 'Mistral OCR Error: Rate limit exceeded. Please try again later.'; + case 500: + case 502: + case 503: + case 504: + return 'Mistral OCR Error: Service temporarily unavailable. Please try again later.'; + } + } + } + + // Handle common network and URL errors + if (errorMsg.includes('URL') || errorMsg.includes('protocol') || errorMsg.includes('http')) { + return 'Mistral OCR Error: Invalid PDF URL format. Please provide a complete URL starting with https:// to your PDF document.'; + } + + if (errorMsg.includes('ETIMEDOUT') || errorMsg.includes('timeout') || errorMsg.includes('ECONNABORTED')) { + return 'Mistral OCR Error: The request timed out. The PDF document may be too large or the server is unresponsive.'; + } + + if (errorMsg.includes('ENOTFOUND') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ECONNRESET')) { + return 'Mistral OCR Error: Could not connect to the document URL. Please verify the document is accessible.'; + } + + if (errorMsg.includes('JSON') || errorMsg.includes('Unexpected token') || errorMsg.includes('parse')) { + return 'Mistral OCR Error: Failed to parse the response from the OCR service.'; + } + + // Default error message with the original error for context + return `Mistral OCR Error: ${errorMsg}`; + }, +} \ No newline at end of file