diff --git a/apps/docs/content/docs/en/execution/files.mdx b/apps/docs/content/docs/en/execution/files.mdx new file mode 100644 index 000000000..14c5121d4 --- /dev/null +++ b/apps/docs/content/docs/en/execution/files.mdx @@ -0,0 +1,134 @@ +--- +title: Passing Files +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' + +Sim makes it easy to work with files throughout your workflows. Blocks can receive files, process them, and pass them to other blocks seamlessly. + +## File Objects + +When blocks output files (like Gmail attachments, generated images, or parsed documents), they return a standardized file object: + +```json +{ + "name": "report.pdf", + "url": "https://...", + "base64": "JVBERi0xLjQK...", + "type": "application/pdf", + "size": 245678 +} +``` + +You can access any of these properties when referencing files from previous blocks. + +## Passing Files Between Blocks + +Reference files from previous blocks using the tag dropdown. Click in any file input field and type `<` to see available outputs. + +**Common patterns:** + +``` +// Single file from a block + + +// Pass the whole file object + + +// Access specific properties + + +``` + +Most blocks accept the full file object and extract what they need automatically. You don't need to manually extract `base64` or `url` in most cases. + +## Triggering Workflows with Files + +When calling a workflow via API that expects file input, include files in your request: + + + + ```bash + curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \ + -H "Content-Type: application/json" \ + -H "x-api-key: YOUR_API_KEY" \ + -d '{ + "document": { + "name": "report.pdf", + "base64": "JVBERi0xLjQK...", + "type": "application/pdf" + } + }' + ``` + + + ```bash + curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \ + -H "Content-Type: application/json" \ + -H "x-api-key: YOUR_API_KEY" \ + -d '{ + "document": { + "name": "report.pdf", + "url": "https://example.com/report.pdf", + "type": "application/pdf" + } + }' + ``` + + + +The workflow's Start block should have an input field configured to receive the file parameter. + +## Receiving Files in API Responses + +When a workflow outputs files, they're included in the response: + +```json +{ + "success": true, + "output": { + "generatedFile": { + "name": "output.png", + "url": "https://...", + "base64": "iVBORw0KGgo...", + "type": "image/png", + "size": 34567 + } + } +} +``` + +Use `url` for direct downloads or `base64` for inline processing. + +## Blocks That Work with Files + +**File inputs:** +- **File** - Parse documents, images, and text files +- **Vision** - Analyze images with AI models +- **Mistral Parser** - Extract text from PDFs + +**File outputs:** +- **Gmail** - Email attachments +- **Slack** - Downloaded files +- **TTS** - Generated audio files +- **Video Generator** - Generated videos +- **Image Generator** - Generated images + +**File storage:** +- **Supabase** - Upload/download from storage +- **S3** - AWS S3 operations +- **Google Drive** - Drive file operations +- **Dropbox** - Dropbox file operations + + + Files are automatically available to downstream blocks. The execution engine handles all file transfer and format conversion. + + +## Best Practices + +1. **Use file objects directly** - Pass the full file object rather than extracting individual properties. Blocks handle the conversion automatically. + +2. **Check file types** - Ensure the file type matches what the receiving block expects. The Vision block needs images, the File block handles documents. + +3. **Consider file size** - Large files increase execution time. For very large files, consider using storage blocks (S3, Supabase) for intermediate storage. diff --git a/apps/docs/content/docs/en/execution/meta.json b/apps/docs/content/docs/en/execution/meta.json index 37cac68f5..fd2124b9d 100644 --- a/apps/docs/content/docs/en/execution/meta.json +++ b/apps/docs/content/docs/en/execution/meta.json @@ -1,3 +1,3 @@ { - "pages": ["index", "basics", "api", "logging", "costs"] + "pages": ["index", "basics", "files", "api", "logging", "costs"] } diff --git a/apps/sim/app/api/tools/discord/send-message/route.ts b/apps/sim/app/api/tools/discord/send-message/route.ts index 273657a61..c597ae467 100644 --- a/apps/sim/app/api/tools/discord/send-message/route.ts +++ b/apps/sim/app/api/tools/discord/send-message/route.ts @@ -4,6 +4,7 @@ import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateNumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -15,7 +16,7 @@ const DiscordSendMessageSchema = z.object({ botToken: z.string().min(1, 'Bot token is required'), channelId: z.string().min(1, 'Channel ID is required'), content: z.string().optional().nullable(), - files: z.array(z.any()).optional().nullable(), + files: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/gmail/draft/route.ts b/apps/sim/app/api/tools/gmail/draft/route.ts index 627ab0ad4..7a6c6cf0c 100644 --- a/apps/sim/app/api/tools/gmail/draft/route.ts +++ b/apps/sim/app/api/tools/gmail/draft/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { @@ -28,7 +29,7 @@ const GmailDraftSchema = z.object({ replyToMessageId: z.string().optional().nullable(), cc: z.string().optional().nullable(), bcc: z.string().optional().nullable(), - attachments: z.array(z.any()).optional().nullable(), + attachments: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/gmail/send/route.ts b/apps/sim/app/api/tools/gmail/send/route.ts index 535587aa0..26c0ce3f7 100644 --- a/apps/sim/app/api/tools/gmail/send/route.ts +++ b/apps/sim/app/api/tools/gmail/send/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { @@ -28,7 +29,7 @@ const GmailSendSchema = z.object({ replyToMessageId: z.string().optional().nullable(), cc: z.string().optional().nullable(), bcc: z.string().optional().nullable(), - attachments: z.array(z.any()).optional().nullable(), + attachments: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/google_drive/upload/route.ts b/apps/sim/app/api/tools/google_drive/upload/route.ts index 9cf53e41d..3549245fd 100644 --- a/apps/sim/app/api/tools/google_drive/upload/route.ts +++ b/apps/sim/app/api/tools/google_drive/upload/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { @@ -20,7 +21,7 @@ const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/upload/drive/v3/files' const GoogleDriveUploadSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), fileName: z.string().min(1, 'File name is required'), - file: z.any().optional().nullable(), + file: RawFileInputSchema.optional().nullable(), mimeType: z.string().optional().nullable(), folderId: z.string().optional().nullable(), }) diff --git a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts index dcaa0f738..bcfcb0b40 100644 --- a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts +++ b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { resolveMentionsForChannel, type TeamsMention } from '@/tools/microsoft_teams/utils' @@ -16,7 +17,7 @@ const TeamsWriteChannelSchema = z.object({ teamId: z.string().min(1, 'Team ID is required'), channelId: z.string().min(1, 'Channel ID is required'), content: z.string().min(1, 'Message content is required'), - files: z.array(z.any()).optional().nullable(), + files: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts index 14454fafa..6b940e17c 100644 --- a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts +++ b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { resolveMentionsForChat, type TeamsMention } from '@/tools/microsoft_teams/utils' @@ -15,7 +16,7 @@ const TeamsWriteChatSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), chatId: z.string().min(1, 'Chat ID is required'), content: z.string().min(1, 'Message content is required'), - files: z.array(z.any()).optional().nullable(), + files: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/mistral/parse/route.ts b/apps/sim/app/api/tools/mistral/parse/route.ts index a40e5d502..89ff35b77 100644 --- a/apps/sim/app/api/tools/mistral/parse/route.ts +++ b/apps/sim/app/api/tools/mistral/parse/route.ts @@ -18,7 +18,8 @@ const logger = createLogger('MistralParseAPI') const MistralParseSchema = z.object({ apiKey: z.string().min(1, 'API key is required'), - filePath: z.string().min(1, 'File path is required'), + filePath: z.string().min(1, 'File path is required').optional(), + fileData: z.unknown().optional(), resultType: z.string().optional(), pages: z.array(z.number()).optional(), includeImageBase64: z.boolean().optional(), @@ -49,66 +50,96 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = MistralParseSchema.parse(body) + const fileData = validatedData.fileData + const filePath = typeof fileData === 'string' ? fileData : validatedData.filePath + + if (!fileData && (!filePath || filePath.trim() === '')) { + return NextResponse.json( + { + success: false, + error: 'File input is required', + }, + { status: 400 } + ) + } + logger.info(`[${requestId}] Mistral parse request`, { - filePath: validatedData.filePath, - isWorkspaceFile: isInternalFileUrl(validatedData.filePath), + hasFileData: Boolean(fileData), + filePath, + isWorkspaceFile: filePath ? isInternalFileUrl(filePath) : false, userId, }) - let fileUrl = validatedData.filePath + const mistralBody: any = { + model: 'mistral-ocr-latest', + } - if (isInternalFileUrl(validatedData.filePath)) { - try { - const storageKey = extractStorageKey(validatedData.filePath) - - const context = inferContextFromKey(storageKey) - - const hasAccess = await verifyFileAccess( - storageKey, - userId, - undefined, // customConfig - context, // context - false // isLocal - ) - - if (!hasAccess) { - logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, { - userId, - key: storageKey, - context, - }) - return NextResponse.json( - { - success: false, - error: 'File not found', - }, - { status: 404 } - ) - } - - fileUrl = await StorageService.generatePresignedDownloadUrl(storageKey, context, 5 * 60) - logger.info(`[${requestId}] Generated presigned URL for ${context} file`) - } catch (error) { - logger.error(`[${requestId}] Failed to generate presigned URL:`, error) + if (fileData && typeof fileData === 'object') { + const base64 = (fileData as { base64?: string }).base64 + const mimeType = (fileData as { type?: string }).type || 'application/pdf' + if (!base64) { return NextResponse.json( { success: false, - error: 'Failed to generate file access URL', + error: 'File base64 content is required', }, - { status: 500 } + { status: 400 } ) } - } else if (validatedData.filePath?.startsWith('/')) { - const baseUrl = getBaseUrl() - fileUrl = `${baseUrl}${validatedData.filePath}` - } + const base64Payload = base64.startsWith('data:') + ? base64 + : `data:${mimeType};base64,${base64}` + mistralBody.document = { + type: 'document_base64', + document_base64: base64Payload, + } + } else if (filePath) { + let fileUrl = filePath - const mistralBody: any = { - model: 'mistral-ocr-latest', - document: { + if (isInternalFileUrl(filePath)) { + try { + const storageKey = extractStorageKey(filePath) + + const context = inferContextFromKey(storageKey) + + const hasAccess = await verifyFileAccess(storageKey, userId, undefined, context, false) + + if (!hasAccess) { + logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, { + userId, + key: storageKey, + context, + }) + return NextResponse.json( + { + success: false, + error: 'File not found', + }, + { status: 404 } + ) + } + + fileUrl = await StorageService.generatePresignedDownloadUrl(storageKey, context, 5 * 60) + logger.info(`[${requestId}] Generated presigned URL for ${context} file`) + } catch (error) { + logger.error(`[${requestId}] Failed to generate presigned URL:`, error) + return NextResponse.json( + { + success: false, + error: 'Failed to generate file access URL', + }, + { status: 500 } + ) + } + } else if (filePath.startsWith('/')) { + const baseUrl = getBaseUrl() + fileUrl = `${baseUrl}${filePath}` + } + + mistralBody.document = { type: 'document_url', document_url: fileUrl, - }, + } } if (validatedData.pages) { diff --git a/apps/sim/app/api/tools/onedrive/upload/route.ts b/apps/sim/app/api/tools/onedrive/upload/route.ts index 759b41da3..c7ffcaf7a 100644 --- a/apps/sim/app/api/tools/onedrive/upload/route.ts +++ b/apps/sim/app/api/tools/onedrive/upload/route.ts @@ -5,6 +5,7 @@ import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' import { getExtensionFromMimeType, processSingleFileToUserFile, @@ -29,7 +30,7 @@ const ExcelValuesSchema = z.union([ const OneDriveUploadSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), fileName: z.string().min(1, 'File name is required'), - file: z.any().optional(), + file: RawFileInputSchema.optional(), folderId: z.string().optional().nullable(), mimeType: z.string().nullish(), values: ExcelValuesSchema.optional().nullable(), @@ -88,25 +89,9 @@ export async function POST(request: NextRequest) { ) } - let fileToProcess - if (Array.isArray(rawFile)) { - if (rawFile.length === 0) { - return NextResponse.json( - { - success: false, - error: 'No file provided', - }, - { status: 400 } - ) - } - fileToProcess = rawFile[0] - } else { - fileToProcess = rawFile - } - let userFile try { - userFile = processSingleFileToUserFile(fileToProcess, requestId, logger) + userFile = processSingleFileToUserFile(rawFile, requestId, logger) } catch (error) { return NextResponse.json( { diff --git a/apps/sim/app/api/tools/outlook/draft/route.ts b/apps/sim/app/api/tools/outlook/draft/route.ts index 39bb3f5ef..eeee0f14e 100644 --- a/apps/sim/app/api/tools/outlook/draft/route.ts +++ b/apps/sim/app/api/tools/outlook/draft/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -18,7 +19,7 @@ const OutlookDraftSchema = z.object({ contentType: z.enum(['text', 'html']).optional().nullable(), cc: z.string().optional().nullable(), bcc: z.string().optional().nullable(), - attachments: z.array(z.any()).optional().nullable(), + attachments: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/outlook/send/route.ts b/apps/sim/app/api/tools/outlook/send/route.ts index 329318880..88578bcef 100644 --- a/apps/sim/app/api/tools/outlook/send/route.ts +++ b/apps/sim/app/api/tools/outlook/send/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -20,7 +21,7 @@ const OutlookSendSchema = z.object({ bcc: z.string().optional().nullable(), replyToMessageId: z.string().optional().nullable(), conversationId: z.string().optional().nullable(), - attachments: z.array(z.any()).optional().nullable(), + attachments: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/s3/put-object/route.ts b/apps/sim/app/api/tools/s3/put-object/route.ts index c33f250bc..c55950bc9 100644 --- a/apps/sim/app/api/tools/s3/put-object/route.ts +++ b/apps/sim/app/api/tools/s3/put-object/route.ts @@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -17,7 +18,7 @@ const S3PutObjectSchema = z.object({ region: z.string().min(1, 'Region is required'), bucketName: z.string().min(1, 'Bucket name is required'), objectKey: z.string().min(1, 'Object key is required'), - file: z.any().optional().nullable(), + file: RawFileInputSchema.optional().nullable(), content: z.string().optional().nullable(), contentType: z.string().optional().nullable(), acl: z.string().optional().nullable(), diff --git a/apps/sim/app/api/tools/sftp/upload/route.ts b/apps/sim/app/api/tools/sftp/upload/route.ts index 90f5e6ab7..54851e595 100644 --- a/apps/sim/app/api/tools/sftp/upload/route.ts +++ b/apps/sim/app/api/tools/sftp/upload/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { @@ -26,14 +27,7 @@ const UploadSchema = z.object({ privateKey: z.string().nullish(), passphrase: z.string().nullish(), remotePath: z.string().min(1, 'Remote path is required'), - files: z - .union([z.array(z.any()), z.string(), z.number(), z.null(), z.undefined()]) - .transform((val) => { - if (Array.isArray(val)) return val - if (val === null || val === undefined || val === '') return undefined - return undefined - }) - .nullish(), + files: RawFileInputArraySchema.optional().nullable(), fileContent: z.string().nullish(), fileName: z.string().nullish(), overwrite: z.boolean().default(true), diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts index 26ce0b1d2..b5826c6ec 100644 --- a/apps/sim/app/api/tools/sharepoint/upload/route.ts +++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -16,7 +17,7 @@ const SharepointUploadSchema = z.object({ driveId: z.string().optional().nullable(), folderPath: z.string().optional().nullable(), fileName: z.string().optional().nullable(), - files: z.array(z.any()).optional().nullable(), + files: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/slack/send-message/route.ts b/apps/sim/app/api/tools/slack/send-message/route.ts index 3938b89d1..21f60faf6 100644 --- a/apps/sim/app/api/tools/slack/send-message/route.ts +++ b/apps/sim/app/api/tools/slack/send-message/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { sendSlackMessage } from '../utils' export const dynamic = 'force-dynamic' @@ -16,7 +17,7 @@ const SlackSendMessageSchema = z userId: z.string().optional().nullable(), text: z.string().min(1, 'Message text is required'), thread_ts: z.string().optional().nullable(), - files: z.array(z.any()).optional().nullable(), + files: RawFileInputArraySchema.optional().nullable(), }) .refine((data) => data.channel || data.userId, { message: 'Either channel or userId is required', diff --git a/apps/sim/app/api/tools/smtp/send/route.ts b/apps/sim/app/api/tools/smtp/send/route.ts index 910ae4368..ca2fdf41c 100644 --- a/apps/sim/app/api/tools/smtp/send/route.ts +++ b/apps/sim/app/api/tools/smtp/send/route.ts @@ -4,6 +4,7 @@ import nodemailer from 'nodemailer' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -28,7 +29,7 @@ const SmtpSendSchema = z.object({ cc: z.string().optional().nullable(), bcc: z.string().optional().nullable(), replyTo: z.string().optional().nullable(), - attachments: z.array(z.any()).optional().nullable(), + attachments: RawFileInputArraySchema.optional().nullable(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/supabase/storage-upload/route.ts b/apps/sim/app/api/tools/supabase/storage-upload/route.ts index 46122fc19..c0677bb35 100644 --- a/apps/sim/app/api/tools/supabase/storage-upload/route.ts +++ b/apps/sim/app/api/tools/supabase/storage-upload/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { FileInputSchema } from '@/lib/uploads/utils/file-schemas' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -16,7 +17,7 @@ const SupabaseStorageUploadSchema = z.object({ bucket: z.string().min(1, 'Bucket name is required'), fileName: z.string().min(1, 'File name is required'), path: z.string().optional().nullable(), - fileData: z.any(), + fileData: FileInputSchema, contentType: z.string().optional().nullable(), upsert: z.boolean().optional().default(false), }) diff --git a/apps/sim/app/api/tools/telegram/send-document/route.ts b/apps/sim/app/api/tools/telegram/send-document/route.ts index 8435ee68f..27d3277d4 100644 --- a/apps/sim/app/api/tools/telegram/send-document/route.ts +++ b/apps/sim/app/api/tools/telegram/send-document/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { convertMarkdownToHTML } from '@/tools/telegram/utils' @@ -14,7 +15,7 @@ const logger = createLogger('TelegramSendDocumentAPI') const TelegramSendDocumentSchema = z.object({ botToken: z.string().min(1, 'Bot token is required'), chatId: z.string().min(1, 'Chat ID is required'), - files: z.array(z.any()).optional().nullable(), + files: RawFileInputArraySchema.optional().nullable(), caption: z.string().optional().nullable(), }) diff --git a/apps/sim/app/api/tools/vision/analyze/route.ts b/apps/sim/app/api/tools/vision/analyze/route.ts index 165005142..5b35f1370 100644 --- a/apps/sim/app/api/tools/vision/analyze/route.ts +++ b/apps/sim/app/api/tools/vision/analyze/route.ts @@ -1,10 +1,13 @@ +import { GoogleGenAI } from '@google/genai' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { convertUsageMetadata, extractTextContent } from '@/providers/google/utils' export const dynamic = 'force-dynamic' @@ -13,8 +16,8 @@ const logger = createLogger('VisionAnalyzeAPI') const VisionAnalyzeSchema = z.object({ apiKey: z.string().min(1, 'API key is required'), imageUrl: z.string().optional().nullable(), - imageFile: z.any().optional().nullable(), - model: z.string().optional().default('gpt-4o'), + imageFile: RawFileInputSchema.optional().nullable(), + model: z.string().optional().default('gpt-5.2'), prompt: z.string().optional().nullable(), }) @@ -88,7 +91,8 @@ export async function POST(request: NextRequest) { const defaultPrompt = 'Please analyze this image and describe what you see in detail.' const prompt = validatedData.prompt || defaultPrompt - const isClaude = validatedData.model.startsWith('claude-3') + const isClaude = validatedData.model.startsWith('claude-') + const isGemini = validatedData.model.startsWith('gemini-') const apiUrl = isClaude ? 'https://api.anthropic.com/v1/messages' : 'https://api.openai.com/v1/chat/completions' @@ -106,6 +110,65 @@ export async function POST(request: NextRequest) { let requestBody: any + if (isGemini) { + let base64Payload = imageSource + if (!base64Payload.startsWith('data:')) { + const response = await fetch(base64Payload) + if (!response.ok) { + return NextResponse.json( + { success: false, error: 'Failed to fetch image for Gemini' }, + { status: 400 } + ) + } + const contentType = + response.headers.get('content-type') || validatedData.imageFile?.type || 'image/jpeg' + const arrayBuffer = await response.arrayBuffer() + const base64 = Buffer.from(arrayBuffer).toString('base64') + base64Payload = `data:${contentType};base64,${base64}` + } + + const base64Marker = ';base64,' + const markerIndex = base64Payload.indexOf(base64Marker) + if (!base64Payload.startsWith('data:') || markerIndex === -1) { + return NextResponse.json( + { success: false, error: 'Invalid base64 image format' }, + { status: 400 } + ) + } + const rawMimeType = base64Payload.slice('data:'.length, markerIndex) + const mediaType = rawMimeType.split(';')[0] || 'image/jpeg' + const base64Data = base64Payload.slice(markerIndex + base64Marker.length) + if (!base64Data) { + return NextResponse.json( + { success: false, error: 'Invalid base64 image format' }, + { status: 400 } + ) + } + + const ai = new GoogleGenAI({ apiKey: validatedData.apiKey }) + const geminiResponse = await ai.models.generateContent({ + model: validatedData.model, + contents: [ + { + role: 'user', + parts: [{ text: prompt }, { inlineData: { mimeType: mediaType, data: base64Data } }], + }, + ], + }) + + const content = extractTextContent(geminiResponse.candidates?.[0]) + const usage = convertUsageMetadata(geminiResponse.usageMetadata) + + return NextResponse.json({ + success: true, + output: { + content, + model: validatedData.model, + tokens: usage.totalTokenCount || undefined, + }, + }) + } + if (isClaude) { if (imageSource.startsWith('data:')) { const base64Match = imageSource.match(/^data:([^;]+);base64,(.+)$/) @@ -172,7 +235,7 @@ export async function POST(request: NextRequest) { ], }, ], - max_tokens: 1000, + max_completion_tokens: 1000, } } diff --git a/apps/sim/app/api/tools/wordpress/upload/route.ts b/apps/sim/app/api/tools/wordpress/upload/route.ts index 8c2604bce..5cf9a1b6f 100644 --- a/apps/sim/app/api/tools/wordpress/upload/route.ts +++ b/apps/sim/app/api/tools/wordpress/upload/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' import { getFileExtension, getMimeTypeFromExtension, @@ -19,7 +20,7 @@ const WORDPRESS_COM_API_BASE = 'https://public-api.wordpress.com/wp/v2/sites' const WordPressUploadSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), siteId: z.string().min(1, 'Site ID is required'), - file: z.any().optional().nullable(), + file: RawFileInputSchema.optional().nullable(), filename: z.string().optional().nullable(), title: z.string().optional().nullable(), caption: z.string().optional().nullable(), diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx index 86be4ba5e..50bcc9c6a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx @@ -179,7 +179,7 @@ export function A2aDeploy({ newFields.push({ id: crypto.randomUUID(), name: 'files', - type: 'files', + type: 'file[]', value: '', collapsed: false, }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx index 8900c2318..f12ceb3e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx @@ -26,7 +26,7 @@ import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/ interface Field { id: string name: string - type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' + type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]' value?: string description?: string collapsed?: boolean @@ -57,7 +57,7 @@ const TYPE_OPTIONS: ComboboxOption[] = [ { label: 'Boolean', value: 'boolean' }, { label: 'Object', value: 'object' }, { label: 'Array', value: 'array' }, - { label: 'Files', value: 'files' }, + { label: 'Files', value: 'file[]' }, ] /** @@ -448,7 +448,7 @@ export function FieldFormat({ ) } - if (field.type === 'files') { + if (field.type === 'file[]') { const lineCount = fieldValue.split('\n').length const gutterWidth = calculateGutterWidth(lineCount) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index bc982daec..0d2569690 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -1746,7 +1746,7 @@ export const TagDropdown: React.FC = ({ mergedSubBlocks ) - if (fieldType === 'files' || fieldType === 'file[]' || fieldType === 'array') { + if (fieldType === 'file' || fieldType === 'file[]' || fieldType === 'array') { const blockName = parts[0] const remainingPath = parts.slice(2).join('.') processedTag = `${blockName}.${arrayFieldName}[0].${remainingPath}` diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts index af5f67529..233f06e58 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts @@ -188,7 +188,7 @@ export function useBlockOutputFields({ baseOutputs = { input: { type: 'string', description: 'User message' }, conversationId: { type: 'string', description: 'Conversation ID' }, - files: { type: 'files', description: 'Uploaded files' }, + files: { type: 'file[]', description: 'Uploaded files' }, } } else { const inputFormatValue = mergedSubBlocks?.inputFormat?.value diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index e5e3d3007..40f60971b 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -417,11 +417,11 @@ async function executeWebhookJobInternal( if (triggerBlock?.subBlocks?.inputFormat?.value) { const inputFormat = triggerBlock.subBlocks.inputFormat.value as unknown as Array<{ name: string - type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' + type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]' }> logger.debug(`[${requestId}] Processing generic webhook files from inputFormat`) - const fileFields = inputFormat.filter((field) => field.type === 'files') + const fileFields = inputFormat.filter((field) => field.type === 'file[]') if (fileFields.length > 0 && typeof input === 'object' && input !== null) { const executionContext = { diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts index 2efb6612f..34fa5d0cc 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -26,7 +26,7 @@ export const ChatTriggerBlock: BlockConfig = { outputs: { input: { type: 'string', description: 'User message' }, conversationId: { type: 'string', description: 'Conversation ID' }, - files: { type: 'files', description: 'Uploaded files' }, + files: { type: 'file[]', description: 'Uploaded files' }, }, triggers: { enabled: true, diff --git a/apps/sim/blocks/blocks/dropbox.ts b/apps/sim/blocks/blocks/dropbox.ts index cffc5ac84..b08f6403d 100644 --- a/apps/sim/blocks/blocks/dropbox.ts +++ b/apps/sim/blocks/blocks/dropbox.ts @@ -60,12 +60,25 @@ export const DropboxBlock: BlockConfig = { required: true, }, { - id: 'fileContent', - title: 'File Content', - type: 'long-input', - placeholder: 'Base64 encoded file content or file reference', - condition: { field: 'operation', value: 'dropbox_upload' }, + id: 'uploadFile', + title: 'File', + type: 'file-upload', + canonicalParamId: 'fileContent', + placeholder: 'Upload file to send to Dropbox', + mode: 'basic', + multiple: false, required: true, + condition: { field: 'operation', value: 'dropbox_upload' }, + }, + { + id: 'fileContent', + title: 'File', + type: 'short-input', + canonicalParamId: 'fileContent', + placeholder: 'Reference file from previous blocks', + mode: 'advanced', + required: true, + condition: { field: 'operation', value: 'dropbox_upload' }, }, { id: 'mode', @@ -337,7 +350,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, path: { type: 'string', description: 'Path in Dropbox' }, autorename: { type: 'boolean', description: 'Auto-rename on conflict' }, // Upload inputs - fileContent: { type: 'string', description: 'Base64 encoded file content' }, + uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' }, + fileContent: { type: 'json', description: 'File reference or UserFile object' }, fileName: { type: 'string', description: 'Optional filename' }, mode: { type: 'string', description: 'Write mode: add or overwrite' }, mute: { type: 'boolean', description: 'Mute notifications' }, @@ -360,7 +374,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, }, outputs: { // Upload/Download outputs - file: { type: 'json', description: 'File metadata' }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, content: { type: 'string', description: 'File content (base64)' }, temporaryLink: { type: 'string', description: 'Temporary download link' }, // List folder outputs diff --git a/apps/sim/blocks/blocks/file.ts b/apps/sim/blocks/blocks/file.ts index 7e478f42a..9867fa979 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { DocumentIcon } from '@/components/icons' import type { BlockConfig, SubBlockType } from '@/blocks/types' import { createVersionedToolSelector } from '@/blocks/utils' -import type { FileParserOutput } from '@/tools/file/types' +import type { FileParserOutput, FileParserV3Output } from '@/tools/file/types' const logger = createLogger('FileBlock') @@ -116,7 +116,7 @@ export const FileBlock: BlockConfig = { }, outputs: { files: { - type: 'json', + type: 'file[]', description: 'Array of parsed file objects with content, metadata, and file properties', }, combinedContent: { @@ -124,7 +124,7 @@ export const FileBlock: BlockConfig = { description: 'All file contents merged into a single text string', }, processedFiles: { - type: 'files', + type: 'file[]', description: 'Array of UserFile objects for downstream use (attachments, uploads, etc.)', }, }, @@ -133,9 +133,9 @@ export const FileBlock: BlockConfig = { export const FileV2Block: BlockConfig = { ...FileBlock, type: 'file_v2', - name: 'File', + name: 'File (Legacy)', description: 'Read and parse multiple files', - hideFromToolbar: false, + hideFromToolbar: true, subBlocks: [ { id: 'file', @@ -209,7 +209,7 @@ export const FileV2Block: BlockConfig = { }, outputs: { files: { - type: 'json', + type: 'file[]', description: 'Array of parsed file objects with content, metadata, and file properties', }, combinedContent: { @@ -218,3 +218,110 @@ export const FileV2Block: BlockConfig = { }, }, } + +export const FileV3Block: BlockConfig = { + type: 'file_v3', + name: 'File', + description: 'Read and parse multiple files', + longDescription: 'Upload files or reference files from previous blocks to extract text content.', + docsLink: 'https://docs.sim.ai/tools/file', + category: 'tools', + bgColor: '#40916C', + icon: DocumentIcon, + subBlocks: [ + { + id: 'file', + title: 'Files', + type: 'file-upload' as SubBlockType, + canonicalParamId: 'fileInput', + acceptedTypes: + '.pdf,.csv,.doc,.docx,.txt,.md,.xlsx,.xls,.html,.htm,.pptx,.ppt,.json,.xml,.rtf', + placeholder: 'Upload files to process', + multiple: true, + mode: 'basic', + maxSize: 100, + required: true, + }, + { + id: 'fileRef', + title: 'Files', + type: 'short-input' as SubBlockType, + canonicalParamId: 'fileInput', + placeholder: 'File reference from previous block', + mode: 'advanced', + required: true, + }, + ], + tools: { + access: ['file_parser_v3'], + config: { + tool: () => 'file_parser_v3', + params: (params) => { + const fileInput = params.fileInput ?? params.file ?? params.filePath + if (!fileInput) { + logger.error('No file input provided') + throw new Error('File input is required') + } + + if (typeof fileInput === 'string') { + return { + filePath: fileInput.trim(), + fileType: params.fileType || 'auto', + workspaceId: params._context?.workspaceId, + workflowId: params._context?.workflowId, + executionId: params._context?.executionId, + } + } + + if (Array.isArray(fileInput)) { + const filePaths = fileInput + .map((file) => (file as { url?: string; path?: string }).url || file.path) + .filter((path): path is string => Boolean(path)) + if (filePaths.length === 0) { + logger.error('No valid file paths found in file input array') + throw new Error('File input is required') + } + return { + filePath: filePaths.length === 1 ? filePaths[0] : filePaths, + fileType: params.fileType || 'auto', + workspaceId: params._context?.workspaceId, + workflowId: params._context?.workflowId, + executionId: params._context?.executionId, + } + } + + if (typeof fileInput === 'object') { + const filePath = (fileInput as { url?: string; path?: string }).url || fileInput.path + if (!filePath) { + logger.error('File input object missing path or url') + throw new Error('File input is required') + } + return { + filePath, + fileType: params.fileType || 'auto', + workspaceId: params._context?.workspaceId, + workflowId: params._context?.workflowId, + executionId: params._context?.executionId, + } + } + + logger.error('Invalid file input format') + throw new Error('File input is required') + }, + }, + }, + inputs: { + fileInput: { type: 'json', description: 'File input (upload or UserFile reference)' }, + fileType: { type: 'string', description: 'File type' }, + }, + outputs: { + files: { + type: 'file[]', + description: 'Parsed files as UserFile objects', + }, + combinedContent: { + type: 'string', + description: 'All file contents merged into a single text string', + }, + }, +} diff --git a/apps/sim/blocks/blocks/gmail.ts b/apps/sim/blocks/blocks/gmail.ts index 223b69ec7..e0138a88d 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -516,7 +516,7 @@ Return ONLY the search query - no explanations, no extra text.`, // Tool outputs content: { type: 'string', description: 'Response content' }, metadata: { type: 'json', description: 'Email metadata' }, - attachments: { type: 'json', description: 'Email attachments array' }, + attachments: { type: 'file[]', description: 'Email attachments array' }, // Trigger outputs email_id: { type: 'string', description: 'Gmail message ID' }, thread_id: { type: 'string', description: 'Gmail thread ID' }, @@ -579,7 +579,7 @@ export const GmailV2Block: BlockConfig = { date: { type: 'string', description: 'Date' }, body: { type: 'string', description: 'Email body text (best-effort)' }, results: { type: 'json', description: 'Search/read summary results' }, - attachments: { type: 'json', description: 'Downloaded attachments (if enabled)' }, + attachments: { type: 'file[]', description: 'Downloaded attachments (if enabled)' }, // Draft-specific outputs draftId: { diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 209bd12f9..23660a238 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -861,7 +861,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr permissionId: { type: 'string', description: 'Permission ID to remove' }, }, outputs: { - file: { type: 'json', description: 'File metadata or downloaded file data' }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, files: { type: 'json', description: 'List of files' }, metadata: { type: 'json', description: 'Complete file metadata (from download)' }, content: { type: 'string', description: 'File content as text' }, diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index 016d04a74..a724d7e12 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -315,12 +315,26 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, required: true, }, { - id: 'imageUrl', - title: 'Image URL', - type: 'short-input', - placeholder: 'Public URL of the image (PNG, JPEG, or GIF)', - condition: { field: 'operation', value: 'add_image' }, + id: 'imageFile', + title: 'Image', + type: 'file-upload', + canonicalParamId: 'imageSource', + placeholder: 'Upload image (PNG, JPEG, or GIF)', + mode: 'basic', + multiple: false, required: true, + acceptedTypes: '.png,.jpg,.jpeg,.gif', + condition: { field: 'operation', value: 'add_image' }, + }, + { + id: 'imageUrl', + title: 'Image', + type: 'short-input', + canonicalParamId: 'imageSource', + placeholder: 'Reference image from previous blocks or enter URL', + mode: 'advanced', + required: true, + condition: { field: 'operation', value: 'add_image' }, }, { id: 'imageWidth', @@ -809,7 +823,9 @@ Return ONLY the text content - no explanations, no markdown formatting markers, placeholderIdMappings: { type: 'string', description: 'JSON array of placeholder ID mappings' }, // Add image operation pageObjectId: { type: 'string', description: 'Slide object ID for image' }, - imageUrl: { type: 'string', description: 'Image URL' }, + imageFile: { type: 'json', description: 'Uploaded image (UserFile)' }, + imageUrl: { type: 'string', description: 'Image URL or reference' }, + imageSource: { type: 'json', description: 'Image source (file or URL)' }, imageWidth: { type: 'number', description: 'Image width in points' }, imageHeight: { type: 'number', description: 'Image height in points' }, positionX: { type: 'number', description: 'X position in points' }, diff --git a/apps/sim/blocks/blocks/google_vault.ts b/apps/sim/blocks/blocks/google_vault.ts index 25a6a9fb9..47e53d56d 100644 --- a/apps/sim/blocks/blocks/google_vault.ts +++ b/apps/sim/blocks/blocks/google_vault.ts @@ -526,7 +526,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, description: 'Single hold object (for create_matters_holds or list_matters_holds with holdId)', }, - file: { type: 'json', description: 'Downloaded export file (UserFile) from execution files' }, + file: { type: 'file', description: 'Downloaded export file (UserFile) from execution files' }, nextPageToken: { type: 'string', description: 'Token for fetching next page of results (for list operations)', diff --git a/apps/sim/blocks/blocks/image_generator.ts b/apps/sim/blocks/blocks/image_generator.ts index befe3ecb4..e2efad69d 100644 --- a/apps/sim/blocks/blocks/image_generator.ts +++ b/apps/sim/blocks/blocks/image_generator.ts @@ -149,7 +149,7 @@ export const ImageGeneratorBlock: BlockConfig = { }, outputs: { content: { type: 'string', description: 'Generation response' }, - image: { type: 'string', description: 'Generated image URL' }, + image: { type: 'file', description: 'Generated image file (UserFile)' }, metadata: { type: 'json', description: 'Generation metadata' }, }, } diff --git a/apps/sim/blocks/blocks/imap.ts b/apps/sim/blocks/blocks/imap.ts index 33cc6e0ec..e23727b20 100644 --- a/apps/sim/blocks/blocks/imap.ts +++ b/apps/sim/blocks/blocks/imap.ts @@ -44,7 +44,7 @@ export const ImapBlock: BlockConfig = { bodyHtml: { type: 'string', description: 'HTML email body' }, mailbox: { type: 'string', description: 'Mailbox/folder where email was received' }, hasAttachments: { type: 'boolean', description: 'Whether email has attachments' }, - attachments: { type: 'json', description: 'Array of email attachments' }, + attachments: { type: 'file[]', description: 'Array of email attachments' }, timestamp: { type: 'string', description: 'Event timestamp' }, }, triggers: { diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index d2d61b77e..66d42a43d 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -1049,7 +1049,7 @@ Return ONLY the comment text - no explanations.`, // jira_get_attachments outputs attachments: { - type: 'json', + type: 'file[]', description: 'Array of attachments with id, filename, size, mimeType, created, author', }, diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index f57682932..300c51b70 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -2341,7 +2341,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n cycles: { type: 'json', description: 'Cycles list' }, // Attachment outputs attachment: { type: 'json', description: 'Attachment data' }, - attachments: { type: 'json', description: 'Attachments list' }, + attachments: { type: 'file[]', description: 'Attachments list' }, // Relation outputs relation: { type: 'json', description: 'Issue relation data' }, relations: { type: 'json', description: 'Issue relations list' }, diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index 498d2ae6c..69dedc8af 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -462,7 +462,7 @@ export const MicrosoftTeamsBlock: BlockConfig = { messages: { type: 'json', description: 'Array of message objects' }, totalAttachments: { type: 'number', description: 'Total number of attachments' }, attachmentTypes: { type: 'json', description: 'Array of attachment content types' }, - attachments: { type: 'array', description: 'Downloaded message attachments' }, + attachments: { type: 'file[]', description: 'Downloaded message attachments' }, updatedContent: { type: 'boolean', description: 'Whether content was successfully updated/sent', diff --git a/apps/sim/blocks/blocks/mistral_parse.ts b/apps/sim/blocks/blocks/mistral_parse.ts index 62773a71c..42c5b63a1 100644 --- a/apps/sim/blocks/blocks/mistral_parse.ts +++ b/apps/sim/blocks/blocks/mistral_parse.ts @@ -159,14 +159,16 @@ export const MistralParseV2Block: BlockConfig = { placeholder: 'Upload a PDF document', mode: 'basic', maxSize: 50, + required: true, }, { id: 'filePath', - title: 'PDF Document', + title: 'File Reference', type: 'short-input' as SubBlockType, canonicalParamId: 'document', - placeholder: 'Document URL', + placeholder: 'File reference from previous block', mode: 'advanced', + required: true, }, { id: 'resultType', @@ -216,7 +218,7 @@ export const MistralParseV2Block: BlockConfig = { throw new Error('PDF document is required') } if (typeof documentInput === 'object') { - parameters.fileUpload = documentInput + parameters.fileData = documentInput } else if (typeof documentInput === 'string') { parameters.filePath = documentInput.trim() } @@ -254,8 +256,8 @@ export const MistralParseV2Block: BlockConfig = { }, }, inputs: { - document: { type: 'json', description: 'Document input (file upload or URL reference)' }, - filePath: { type: 'string', description: 'PDF document URL (advanced mode)' }, + document: { type: 'json', description: 'Document input (file upload or file reference)' }, + filePath: { type: 'string', description: 'File reference (advanced mode)' }, fileUpload: { type: 'json', description: 'Uploaded PDF file (basic mode)' }, apiKey: { type: 'string', description: 'Mistral API key' }, resultType: { type: 'string', description: 'Output format type' }, diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index d9753cced..494bfa3b0 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -393,7 +393,7 @@ export const OneDriveBlock: BlockConfig = { deleted: { type: 'boolean', description: 'Whether the file was deleted' }, fileId: { type: 'string', description: 'The ID of the deleted file' }, file: { - type: 'json', + type: 'file', description: 'The OneDrive file object, including details such as id, name, size, and more.', }, files: { diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 9bf8c312e..cfbe25304 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -440,7 +440,7 @@ export const OutlookBlock: BlockConfig = { sentDateTime: { type: 'string', description: 'Email sent timestamp' }, hasAttachments: { type: 'boolean', description: 'Whether email has attachments' }, attachments: { - type: 'json', + type: 'file[]', description: 'Email attachments (if includeAttachments is enabled)', }, isRead: { type: 'boolean', description: 'Whether email is read' }, diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index b6bd6fb8e..1d3b939d4 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -803,7 +803,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n outputs: { deals: { type: 'json', description: 'Array of deal objects' }, deal: { type: 'json', description: 'Single deal object' }, - files: { type: 'json', description: 'Array of file objects' }, + files: { type: 'file[]', description: 'Array of file objects' }, messages: { type: 'json', description: 'Array of mail message objects' }, pipelines: { type: 'json', description: 'Array of pipeline objects' }, projects: { type: 'json', description: 'Array of project objects' }, diff --git a/apps/sim/blocks/blocks/s3.ts b/apps/sim/blocks/blocks/s3.ts index 6dba63175..f364a7888 100644 --- a/apps/sim/blocks/blocks/s3.ts +++ b/apps/sim/blocks/blocks/s3.ts @@ -418,6 +418,7 @@ export const S3Block: BlockConfig = { type: 'string', description: 'S3 URI (s3://bucket/key) for use with other AWS services', }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, objects: { type: 'json', description: 'List of objects (for list operation)' }, deleted: { type: 'boolean', description: 'Deletion status' }, metadata: { type: 'json', description: 'Operation metadata' }, diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 5fe1dfb6d..b6cdbfbda 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -522,7 +522,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, description: 'Array of SharePoint list items with fields', }, uploadedFiles: { - type: 'json', + type: 'file[]', description: 'Array of uploaded file objects with id, name, webUrl, size', }, fileCount: { diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 7e432f27f..9bd6292b7 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -859,7 +859,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, // slack_download outputs file: { - type: 'json', + type: 'file', description: 'Downloaded file stored in execution files', }, diff --git a/apps/sim/blocks/blocks/spotify.ts b/apps/sim/blocks/blocks/spotify.ts index c4341e6af..417724f6e 100644 --- a/apps/sim/blocks/blocks/spotify.ts +++ b/apps/sim/blocks/blocks/spotify.ts @@ -450,10 +450,24 @@ export const SpotifyBlock: BlockConfig = { // === PLAYLIST COVER === { - id: 'imageBase64', - title: 'Image (Base64)', - type: 'long-input', - placeholder: 'Base64-encoded JPEG image (max 256KB)', + id: 'coverImageFile', + title: 'Cover Image', + type: 'file-upload', + canonicalParamId: 'coverImage', + placeholder: 'Upload cover image (JPEG, max 256KB)', + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: '.jpg,.jpeg', + condition: { field: 'operation', value: 'spotify_add_playlist_cover' }, + }, + { + id: 'coverImageRef', + title: 'Cover Image', + type: 'short-input', + canonicalParamId: 'coverImage', + placeholder: 'Reference image from previous blocks', + mode: 'advanced', required: true, condition: { field: 'operation', value: 'spotify_add_playlist_cover' }, }, @@ -804,7 +818,9 @@ export const SpotifyBlock: BlockConfig = { newName: { type: 'string', description: 'New playlist name' }, description: { type: 'string', description: 'Playlist description' }, public: { type: 'boolean', description: 'Whether playlist is public' }, - imageBase64: { type: 'string', description: 'Base64-encoded JPEG image' }, + coverImage: { type: 'json', description: 'Cover image (UserFile)' }, + coverImageFile: { type: 'json', description: 'Cover image upload (basic mode)' }, + coverImageRef: { type: 'json', description: 'Cover image reference (advanced mode)' }, range_start: { type: 'number', description: 'Start index for reorder' }, insert_before: { type: 'number', description: 'Insert before index' }, range_length: { type: 'number', description: 'Number of items to move' }, diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index 8b5fc75f7..49c59cb35 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -675,9 +675,9 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e { id: 'fileContent', title: 'File Content', - type: 'code', + type: 'short-input', canonicalParamId: 'fileData', - placeholder: 'Base64 encoded for binary files, or plain text', + placeholder: 'File reference from previous block', condition: { field: 'operation', value: 'storage_upload' }, mode: 'advanced', required: true, @@ -1173,7 +1173,7 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e description: 'Row count for count operations', }, file: { - type: 'files', + type: 'file', description: 'Downloaded file stored in execution files', }, publicUrl: { diff --git a/apps/sim/blocks/blocks/telegram.ts b/apps/sim/blocks/blocks/telegram.ts index 6c40812a4..e45f27cee 100644 --- a/apps/sim/blocks/blocks/telegram.ts +++ b/apps/sim/blocks/blocks/telegram.ts @@ -65,39 +65,91 @@ export const TelegramBlock: BlockConfig = { required: true, condition: { field: 'operation', value: 'telegram_message' }, }, + { + id: 'photoFile', + title: 'Photo', + type: 'file-upload', + canonicalParamId: 'photo', + placeholder: 'Upload photo', + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: '.jpg,.jpeg,.png,.gif,.webp', + condition: { field: 'operation', value: 'telegram_send_photo' }, + }, { id: 'photo', title: 'Photo', type: 'short-input', - placeholder: 'Enter photo URL or file_id', - description: 'Photo to send. Pass a file_id or HTTP URL', + canonicalParamId: 'photo', + placeholder: 'Reference photo from previous blocks or enter URL/file_id', + mode: 'advanced', required: true, condition: { field: 'operation', value: 'telegram_send_photo' }, }, + { + id: 'videoFile', + title: 'Video', + type: 'file-upload', + canonicalParamId: 'video', + placeholder: 'Upload video', + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: '.mp4,.mov,.avi,.mkv,.webm', + condition: { field: 'operation', value: 'telegram_send_video' }, + }, { id: 'video', title: 'Video', type: 'short-input', - placeholder: 'Enter video URL or file_id', - description: 'Video to send. Pass a file_id or HTTP URL', + canonicalParamId: 'video', + placeholder: 'Reference video from previous blocks or enter URL/file_id', + mode: 'advanced', required: true, condition: { field: 'operation', value: 'telegram_send_video' }, }, + { + id: 'audioFile', + title: 'Audio', + type: 'file-upload', + canonicalParamId: 'audio', + placeholder: 'Upload audio', + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: '.mp3,.m4a,.wav,.ogg,.flac', + condition: { field: 'operation', value: 'telegram_send_audio' }, + }, { id: 'audio', title: 'Audio', type: 'short-input', - placeholder: 'Enter audio URL or file_id', - description: 'Audio file to send. Pass a file_id or HTTP URL', + canonicalParamId: 'audio', + placeholder: 'Reference audio from previous blocks or enter URL/file_id', + mode: 'advanced', required: true, condition: { field: 'operation', value: 'telegram_send_audio' }, }, + { + id: 'animationFile', + title: 'Animation', + type: 'file-upload', + canonicalParamId: 'animation', + placeholder: 'Upload animation (GIF)', + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: '.gif,.mp4', + condition: { field: 'operation', value: 'telegram_send_animation' }, + }, { id: 'animation', title: 'Animation', type: 'short-input', - placeholder: 'Enter animation URL or file_id', - description: 'Animation (GIF) to send. Pass a file_id or HTTP URL', + canonicalParamId: 'animation', + placeholder: 'Reference animation from previous blocks or enter URL/file_id', + mode: 'advanced', required: true, condition: { field: 'operation', value: 'telegram_send_animation' }, }, @@ -215,42 +267,50 @@ export const TelegramBlock: BlockConfig = { ...commonParams, messageId: params.messageId, } - case 'telegram_send_photo': - if (!params.photo) { - throw new Error('Photo URL or file_id is required.') + case 'telegram_send_photo': { + const photoSource = params.photoFile || params.photo + if (!photoSource) { + throw new Error('Photo is required.') } return { ...commonParams, - photo: params.photo, + photo: photoSource, caption: params.caption, } - case 'telegram_send_video': - if (!params.video) { - throw new Error('Video URL or file_id is required.') + } + case 'telegram_send_video': { + const videoSource = params.videoFile || params.video + if (!videoSource) { + throw new Error('Video is required.') } return { ...commonParams, - video: params.video, + video: videoSource, caption: params.caption, } - case 'telegram_send_audio': - if (!params.audio) { - throw new Error('Audio URL or file_id is required.') + } + case 'telegram_send_audio': { + const audioSource = params.audioFile || params.audio + if (!audioSource) { + throw new Error('Audio is required.') } return { ...commonParams, - audio: params.audio, + audio: audioSource, caption: params.caption, } - case 'telegram_send_animation': - if (!params.animation) { - throw new Error('Animation URL or file_id is required.') + } + case 'telegram_send_animation': { + const animationSource = params.animationFile || params.animation + if (!animationSource) { + throw new Error('Animation is required.') } return { ...commonParams, - animation: params.animation, + animation: animationSource, caption: params.caption, } + } case 'telegram_send_document': { // Handle file upload const fileParam = params.attachmentFiles || params.files @@ -274,10 +334,14 @@ export const TelegramBlock: BlockConfig = { botToken: { type: 'string', description: 'Telegram bot token' }, chatId: { type: 'string', description: 'Chat identifier' }, text: { type: 'string', description: 'Message text' }, - photo: { type: 'string', description: 'Photo URL or file_id' }, - video: { type: 'string', description: 'Video URL or file_id' }, - audio: { type: 'string', description: 'Audio URL or file_id' }, - animation: { type: 'string', description: 'Animation URL or file_id' }, + photoFile: { type: 'json', description: 'Uploaded photo (UserFile)' }, + photo: { type: 'json', description: 'Photo reference or URL/file_id' }, + videoFile: { type: 'json', description: 'Uploaded video (UserFile)' }, + video: { type: 'json', description: 'Video reference or URL/file_id' }, + audioFile: { type: 'json', description: 'Uploaded audio (UserFile)' }, + audio: { type: 'json', description: 'Audio reference or URL/file_id' }, + animationFile: { type: 'json', description: 'Uploaded animation (UserFile)' }, + animation: { type: 'json', description: 'Animation reference or URL/file_id' }, attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)', diff --git a/apps/sim/blocks/blocks/tts.ts b/apps/sim/blocks/blocks/tts.ts index c3ab813fd..eebc8acd3 100644 --- a/apps/sim/blocks/blocks/tts.ts +++ b/apps/sim/blocks/blocks/tts.ts @@ -578,7 +578,7 @@ export const TtsBlock: BlockConfig = { outputs: { audioUrl: { type: 'string', description: 'URL to the generated audio file' }, - audioFile: { type: 'json', description: 'Generated audio file object (UserFile)' }, + audioFile: { type: 'file', description: 'Generated audio file object (UserFile)' }, duration: { type: 'number', description: 'Audio duration in seconds', diff --git a/apps/sim/blocks/blocks/video_generator.ts b/apps/sim/blocks/blocks/video_generator.ts index 88672a17b..2743cf2c1 100644 --- a/apps/sim/blocks/blocks/video_generator.ts +++ b/apps/sim/blocks/blocks/video_generator.ts @@ -420,7 +420,7 @@ export const VideoGeneratorBlock: BlockConfig = { outputs: { videoUrl: { type: 'string', description: 'Generated video URL' }, - videoFile: { type: 'json', description: 'Video file object with metadata' }, + videoFile: { type: 'file', description: 'Video file object with metadata' }, duration: { type: 'number', description: 'Video duration in seconds' }, width: { type: 'number', description: 'Video width in pixels' }, height: { type: 'number', description: 'Video height in pixels' }, diff --git a/apps/sim/blocks/blocks/vision.ts b/apps/sim/blocks/blocks/vision.ts index 8a94d2240..58d6c1354 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -3,10 +3,27 @@ import type { BlockConfig } from '@/blocks/types' import { AuthMode } from '@/blocks/types' import type { VisionResponse } from '@/tools/vision/types' +const VISION_MODEL_OPTIONS = [ + { label: 'GPT 5.2', id: 'gpt-5.2' }, + { label: 'GPT 5.1', id: 'gpt-5.1' }, + { label: 'GPT 5', id: 'gpt-5' }, + { label: 'GPT 5 Mini', id: 'gpt-5-mini' }, + { label: 'GPT 5 Nano', id: 'gpt-5-nano' }, + { label: 'Claude Opus 4.5', id: 'claude-opus-4-5' }, + { label: 'Claude Sonnet 4.5', id: 'claude-sonnet-4-5' }, + { label: 'Claude Haiku 4.5', id: 'claude-haiku-4-5' }, + { label: 'Gemini 3 Pro Preview', id: 'gemini-3-pro-preview' }, + { label: 'Gemini 3 Flash Preview', id: 'gemini-3-flash-preview' }, + { label: 'Gemini 2.5 Pro', id: 'gemini-2.5-pro' }, + { label: 'Gemini 2.5 Flash', id: 'gemini-2.5-flash' }, + { label: 'Gemini 2.5 Flash Lite', id: 'gemini-2.5-flash-lite' }, +] + export const VisionBlock: BlockConfig = { type: 'vision', - name: 'Vision', + name: 'Vision (Legacy)', description: 'Analyze images with vision models', + hideFromToolbar: true, authMode: AuthMode.ApiKey, longDescription: 'Integrate Vision into the workflow. Can analyze images with vision models.', docsLink: 'https://docs.sim.ai/tools/vision', @@ -47,12 +64,8 @@ export const VisionBlock: BlockConfig = { id: 'model', title: 'Vision Model', type: 'dropdown', - options: [ - { label: 'gpt-4o', id: 'gpt-4o' }, - { label: 'claude-3-opus', id: 'claude-3-opus-20240229' }, - { label: 'claude-3-sonnet', id: 'claude-3-sonnet-20240229' }, - ], - value: () => 'gpt-4o', + options: VISION_MODEL_OPTIONS, + value: () => 'gpt-5.2', }, { id: 'prompt', @@ -87,3 +100,62 @@ export const VisionBlock: BlockConfig = { tokens: { type: 'number', description: 'Token usage' }, }, } + +export const VisionV2Block: BlockConfig = { + ...VisionBlock, + type: 'vision_v2', + name: 'Vision', + description: 'Analyze images with vision models', + hideFromToolbar: false, + subBlocks: [ + { + id: 'imageFile', + title: 'Image File', + type: 'file-upload', + canonicalParamId: 'imageFile', + placeholder: 'Upload an image file', + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: '.jpg,.jpeg,.png,.gif,.webp', + }, + { + id: 'imageFileReference', + title: 'Image File Reference', + type: 'short-input', + canonicalParamId: 'imageFile', + placeholder: 'Reference an image from previous blocks', + mode: 'advanced', + required: true, + }, + { + id: 'model', + title: 'Vision Model', + type: 'dropdown', + options: VISION_MODEL_OPTIONS, + value: () => 'gpt-5.2', + }, + { + id: 'prompt', + title: 'Prompt', + type: 'long-input', + placeholder: 'Enter prompt for image analysis', + required: true, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your API key', + password: true, + required: true, + }, + ], + inputs: { + apiKey: { type: 'string', description: 'Provider API key' }, + imageFile: { type: 'json', description: 'Image file (UserFile)' }, + imageFileReference: { type: 'json', description: 'Image file reference' }, + model: { type: 'string', description: 'Vision model' }, + prompt: { type: 'string', description: 'Analysis prompt' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 225a3b224..6aa34b6c2 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -28,7 +28,7 @@ import { ElasticsearchBlock } from '@/blocks/blocks/elasticsearch' import { ElevenLabsBlock } from '@/blocks/blocks/elevenlabs' import { EvaluatorBlock } from '@/blocks/blocks/evaluator' import { ExaBlock } from '@/blocks/blocks/exa' -import { FileBlock, FileV2Block } from '@/blocks/blocks/file' +import { FileBlock, FileV2Block, FileV3Block } from '@/blocks/blocks/file' import { FirecrawlBlock } from '@/blocks/blocks/firecrawl' import { FirefliesBlock } from '@/blocks/blocks/fireflies' import { FunctionBlock } from '@/blocks/blocks/function' @@ -139,7 +139,7 @@ import { TwilioVoiceBlock } from '@/blocks/blocks/twilio_voice' import { TypeformBlock } from '@/blocks/blocks/typeform' import { VariablesBlock } from '@/blocks/blocks/variables' import { VideoGeneratorBlock, VideoGeneratorV2Block } from '@/blocks/blocks/video_generator' -import { VisionBlock } from '@/blocks/blocks/vision' +import { VisionBlock, VisionV2Block } from '@/blocks/blocks/vision' import { WaitBlock } from '@/blocks/blocks/wait' import { WealthboxBlock } from '@/blocks/blocks/wealthbox' import { WebflowBlock } from '@/blocks/blocks/webflow' @@ -192,6 +192,7 @@ export const registry: Record = { exa: ExaBlock, file: FileBlock, file_v2: FileV2Block, + file_v3: FileV3Block, firecrawl: FirecrawlBlock, fireflies: FirefliesBlock, function: FunctionBlock, @@ -314,6 +315,7 @@ export const registry: Record = { video_generator: VideoGeneratorBlock, video_generator_v2: VideoGeneratorV2Block, vision: VisionBlock, + vision_v2: VisionV2Block, wait: WaitBlock, wealthbox: WealthboxBlock, webflow: WebflowBlock, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index c59ad427c..352f10642 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -9,7 +9,8 @@ export type PrimitiveValueType = | 'boolean' | 'json' | 'array' - | 'files' + | 'file' + | 'file[]' | 'any' export type BlockCategory = 'blocks' | 'tools' | 'triggers' diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index d17da0e7c..2e5258504 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -137,7 +137,7 @@ export class BlockExecutor { normalizedOutput = this.normalizeOutput(output) } - if (ctx.includeFileBase64 && containsUserFileWithMetadata(normalizedOutput)) { + if (containsUserFileWithMetadata(normalizedOutput)) { normalizedOutput = (await hydrateUserFilesWithBase64(normalizedOutput, { requestId: ctx.metadata.requestId, executionId: ctx.executionId, diff --git a/apps/sim/executor/utils/block-reference.test.ts b/apps/sim/executor/utils/block-reference.test.ts index 6f110c2bc..470522b77 100644 --- a/apps/sim/executor/utils/block-reference.test.ts +++ b/apps/sim/executor/utils/block-reference.test.ts @@ -133,7 +133,7 @@ describe('resolveBlockReference', () => { 'block-1': { input: { type: 'string' }, conversationId: { type: 'string' }, - files: { type: 'files' }, + files: { type: 'file[]' }, }, }, }) @@ -206,7 +206,7 @@ describe('resolveBlockReference', () => { }, }, blockOutputSchemas: { - 'block-1': { files: { type: 'files' } }, + 'block-1': { files: { type: 'file[]' } }, }, }) @@ -218,7 +218,7 @@ describe('resolveBlockReference', () => { const ctx = createContext({ blockData: { 'block-1': { files: [] } }, blockOutputSchemas: { - 'block-1': { files: { type: 'files' } }, + 'block-1': { files: { type: 'file[]' } }, }, }) diff --git a/apps/sim/executor/utils/block-reference.ts b/apps/sim/executor/utils/block-reference.ts index 590e9d869..4ae41a2b1 100644 --- a/apps/sim/executor/utils/block-reference.ts +++ b/apps/sim/executor/utils/block-reference.ts @@ -32,7 +32,7 @@ export class InvalidFieldError extends Error { function isFileType(value: unknown): boolean { if (typeof value !== 'object' || value === null) return false const typed = value as { type?: string } - return typed.type === 'file[]' || typed.type === 'files' + return typed.type === 'file' || typed.type === 'file[]' } function isArrayType(value: unknown): value is { type: 'array'; items?: unknown } { diff --git a/apps/sim/lib/execution/files.ts b/apps/sim/lib/execution/files.ts index 9eb26905e..d80f2ae77 100644 --- a/apps/sim/lib/execution/files.ts +++ b/apps/sim/lib/execution/files.ts @@ -163,7 +163,7 @@ export async function processInputFileFields( } const inputFormat = extractInputFormatFromBlock(startBlock) - const fileFields = inputFormat.filter((field) => field.type === 'files') + const fileFields = inputFormat.filter((field) => field.type === 'file[]') if (fileFields.length === 0) { return input diff --git a/apps/sim/lib/mcp/workflow-tool-schema.ts b/apps/sim/lib/mcp/workflow-tool-schema.ts index 7af927ff1..4678e96b3 100644 --- a/apps/sim/lib/mcp/workflow-tool-schema.ts +++ b/apps/sim/lib/mcp/workflow-tool-schema.ts @@ -153,7 +153,7 @@ export function generateToolInputSchema(inputFormat: InputFormatField[]): McpToo // Handle array types if (fieldType === 'array') { - if (field.type === 'files') { + if (field.type === 'file[]') { property.items = { type: 'object', properties: { diff --git a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts index bbf2a123e..6c237668c 100644 --- a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { isUserFileWithMetadata } from '@/lib/core/utils/user-file' +import { StorageService } from '@/lib/uploads' import type { ExecutionContext } from '@/lib/uploads/contexts/execution/utils' import { generateExecutionFileKey, generateFileId } from '@/lib/uploads/contexts/execution/utils' import type { UserFile } from '@/executor/types' @@ -89,10 +90,7 @@ export async function uploadExecutionFile( } try { - const { uploadFile, generatePresignedDownloadUrl } = await import( - '@/lib/uploads/core/storage-service' - ) - const fileInfo = await uploadFile({ + const fileInfo = await StorageService.uploadFile({ file: fileBuffer, fileName: storageKey, contentType, @@ -102,21 +100,24 @@ export async function uploadExecutionFile( metadata, // Pass metadata for cloud storage and database tracking }) - // Generate presigned URL for file access (10 minutes expiration) - const fullUrl = await generatePresignedDownloadUrl(fileInfo.key, 'execution', 600) + const presignedUrl = await StorageService.generatePresignedDownloadUrl( + fileInfo.key, + 'execution', + 5 * 60 + ) const userFile: UserFile = { id: fileId, name: fileName, size: fileBuffer.length, type: contentType, - url: fullUrl, // Presigned URL for external access and downstream workflow usage + url: presignedUrl, key: fileInfo.key, - context: 'execution', // Preserve context in file object + context: 'execution', + base64: fileBuffer.toString('base64'), } logger.info(`Successfully uploaded execution file: ${fileName} (${fileBuffer.length} bytes)`, { - url: fullUrl, key: fileInfo.key, }) return userFile @@ -135,8 +136,7 @@ export async function downloadExecutionFile(userFile: UserFile): Promise logger.info(`Downloading execution file: ${userFile.name}`) try { - const { downloadFile } = await import('@/lib/uploads/core/storage-service') - const fileBuffer = await downloadFile({ + const fileBuffer = await StorageService.downloadFile({ key: userFile.key, context: 'execution', }) diff --git a/apps/sim/lib/uploads/utils/file-schemas.ts b/apps/sim/lib/uploads/utils/file-schemas.ts new file mode 100644 index 000000000..0939131ff --- /dev/null +++ b/apps/sim/lib/uploads/utils/file-schemas.ts @@ -0,0 +1,24 @@ +import { z } from 'zod' + +export const RawFileInputSchema = z + .object({ + id: z.string().optional(), + key: z.string().optional(), + path: z.string().optional(), + url: z.string().optional(), + name: z.string().min(1), + size: z.number().nonnegative(), + type: z.string().optional(), + uploadedAt: z.union([z.string(), z.date()]).optional(), + expiresAt: z.union([z.string(), z.date()]).optional(), + context: z.string().optional(), + base64: z.string().optional(), + }) + .passthrough() + .refine((data) => Boolean(data.key || data.path || data.url), { + message: 'File must include key, path, or url', + }) + +export const RawFileInputArraySchema = z.array(RawFileInputSchema) + +export const FileInputSchema = z.union([RawFileInputSchema, z.string()]) diff --git a/apps/sim/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts index 7b1d925ec..e234f7069 100644 --- a/apps/sim/lib/uploads/utils/file-utils.ts +++ b/apps/sim/lib/uploads/utils/file-utils.ts @@ -458,17 +458,23 @@ function isCompleteUserFile(file: RawFileInput): file is UserFile { /** * Converts a single raw file object to UserFile format - * @param file - Raw file object + * @param file - Raw file object (must be a single file, not an array) * @param requestId - Request ID for logging * @param logger - Logger instance * @returns UserFile object - * @throws Error if file has no storage key + * @throws Error if file is an array or has no storage key */ export function processSingleFileToUserFile( file: RawFileInput, requestId: string, logger: Logger ): UserFile { + if (Array.isArray(file)) { + const errorMsg = `Expected a single file but received an array with ${file.length} file(s). Use a file input that accepts multiple files, or select a specific file from the array (e.g., {{block.files[0]}}).` + logger.error(`[${requestId}] ${errorMsg}`) + throw new Error(errorMsg) + } + if (isCompleteUserFile(file)) { return file } @@ -495,21 +501,51 @@ export function processSingleFileToUserFile( /** * Converts raw file objects (from file-upload or variable references) to UserFile format - * @param files - Array of raw file objects + * Accepts either a single file or an array of files and normalizes to array output + * @param files - Single file or array of raw file objects * @param requestId - Request ID for logging * @param logger - Logger instance * @returns Array of UserFile objects */ export function processFilesToUserFiles( - files: RawFileInput[], + files: RawFileInput | RawFileInput[], requestId: string, logger: Logger ): UserFile[] { + const filesArray = Array.isArray(files) ? files : [files] const userFiles: UserFile[] = [] - for (const file of files) { + for (const file of filesArray) { try { - const userFile = processSingleFileToUserFile(file, requestId, logger) + if (Array.isArray(file)) { + logger.warn(`[${requestId}] Skipping nested array in file input`) + continue + } + + if (isCompleteUserFile(file)) { + userFiles.push(file) + continue + } + + const storageKey = file.key || (file.path ? extractStorageKey(file.path) : null) + + if (!storageKey) { + logger.warn(`[${requestId}] Skipping file without storage key: ${file.name || 'unknown'}`) + continue + } + + const userFile: UserFile = { + id: file.id || `file-${Date.now()}`, + name: file.name, + url: file.url || file.path || '', + size: file.size, + type: file.type || 'application/octet-stream', + key: storageKey, + } + + logger.info( + `[${requestId}] Converted file to UserFile: ${userFile.name} (key: ${userFile.key})` + ) userFiles.push(userFile) } catch (error) { logger.warn( diff --git a/apps/sim/lib/webhooks/attachment-processor.ts b/apps/sim/lib/webhooks/attachment-processor.ts index cf2adbcef..0cbbf494e 100644 --- a/apps/sim/lib/webhooks/attachment-processor.ts +++ b/apps/sim/lib/webhooks/attachment-processor.ts @@ -77,7 +77,7 @@ export class WebhookAttachmentProcessor { userId?: string } ): Promise { - return uploadFileFromRawData( + const userFile = await uploadFileFromRawData( { name: attachment.name, data: attachment.data, @@ -86,5 +86,14 @@ export class WebhookAttachmentProcessor { executionContext, executionContext.userId ) + + if (userFile.base64) { + return userFile + } + + return { + ...userFile, + base64: attachment.data.toString('base64'), + } } } diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index 96833fa87..edb95fdf0 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -123,13 +123,13 @@ function filterOutputsByCondition( const CHAT_OUTPUTS: OutputDefinition = { input: { type: 'string', description: 'User message' }, conversationId: { type: 'string', description: 'Conversation ID' }, - files: { type: 'files', description: 'Uploaded files' }, + files: { type: 'file[]', description: 'Uploaded files' }, } const UNIFIED_START_OUTPUTS: OutputDefinition = { input: { type: 'string', description: 'Primary user input or message' }, conversationId: { type: 'string', description: 'Conversation thread identifier' }, - files: { type: 'files', description: 'User uploaded files' }, + files: { type: 'file[]', description: 'User uploaded files' }, } function applyInputFormatFields( @@ -341,6 +341,17 @@ function expandFileTypeProperties(path: string): string[] { return USER_FILE_ACCESSIBLE_PROPERTIES.map((prop) => `${path}.${prop}`) } +type FileOutputType = 'file' | 'file[]' + +function isFileOutputDefinition(value: unknown): value is { type: FileOutputType } { + if (!value || typeof value !== 'object' || !('type' in value)) { + return false + } + + const { type } = value as { type?: unknown } + return type === 'file' || type === 'file[]' +} + export function getBlockOutputPaths( blockType: string, subBlocks?: Record, @@ -373,13 +384,7 @@ function getFilePropertyType(outputs: OutputDefinition, pathParts: string[]): st current = (current as Record)[part] } - if ( - current && - typeof current === 'object' && - 'type' in current && - ((current as { type: unknown }).type === 'files' || - (current as { type: unknown }).type === 'file[]') - ) { + if (isFileOutputDefinition(current)) { return USER_FILE_PROPERTY_TYPES[lastPart as keyof typeof USER_FILE_PROPERTY_TYPES] } @@ -485,7 +490,7 @@ function generateOutputPaths(outputs: Record, prefix = ''): string[ paths.push(currentPath) } else if (typeof value === 'object' && value !== null) { if ('type' in value && typeof value.type === 'string') { - if (value.type === 'files' || value.type === 'file[]') { + if (isFileOutputDefinition(value)) { paths.push(...expandFileTypeProperties(currentPath)) continue } @@ -546,7 +551,7 @@ function generateOutputPathsWithTypes( paths.push({ path: currentPath, type: value }) } else if (typeof value === 'object' && value !== null) { if ('type' in value && typeof value.type === 'string') { - if (value.type === 'files' || value.type === 'file[]') { + if (isFileOutputDefinition(value)) { paths.push({ path: currentPath, type: value.type }) for (const prop of USER_FILE_ACCESSIBLE_PROPERTIES) { paths.push({ diff --git a/apps/sim/lib/workflows/operations/deployment-utils.ts b/apps/sim/lib/workflows/operations/deployment-utils.ts index c0dce11aa..7da79ce2b 100644 --- a/apps/sim/lib/workflows/operations/deployment-utils.ts +++ b/apps/sim/lib/workflows/operations/deployment-utils.ts @@ -98,7 +98,7 @@ export function getInputFormatExample( case 'array': exampleData[field.name] = [1, 2, 3] break - case 'files': + case 'file[]': exampleData[field.name] = [ { data: 'data:application/pdf;base64,...', diff --git a/apps/sim/lib/workflows/types.ts b/apps/sim/lib/workflows/types.ts index 4596ce9e9..9e51d7ff1 100644 --- a/apps/sim/lib/workflows/types.ts +++ b/apps/sim/lib/workflows/types.ts @@ -1,6 +1,6 @@ export interface InputFormatField { name?: string - type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' | string + type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]' | string description?: string value?: unknown } diff --git a/apps/sim/tools/discord/types.ts b/apps/sim/tools/discord/types.ts index cad8f68c7..9573309e9 100644 --- a/apps/sim/tools/discord/types.ts +++ b/apps/sim/tools/discord/types.ts @@ -1,3 +1,5 @@ +import type { UserFile } from '@/executor/types' + export interface DiscordMessage { id: string content: string @@ -58,7 +60,7 @@ export interface DiscordSendMessageParams extends DiscordAuthParams { description?: string color?: string | number } - files?: any[] + files?: UserFile[] } export interface DiscordGetMessagesParams extends DiscordAuthParams { diff --git a/apps/sim/tools/dropbox/download.ts b/apps/sim/tools/dropbox/download.ts index e489b3d21..24292ebda 100644 --- a/apps/sim/tools/dropbox/download.ts +++ b/apps/sim/tools/dropbox/download.ts @@ -4,7 +4,7 @@ import type { ToolConfig } from '@/tools/types' export const dropboxDownloadTool: ToolConfig = { id: 'dropbox_download', name: 'Dropbox Download File', - description: 'Download a file from Dropbox and get a temporary link', + description: 'Download a file from Dropbox with metadata and content', version: '1.0.0', oauth: { @@ -22,7 +22,7 @@ export const dropboxDownloadTool: ToolConfig { if (!params.accessToken) { @@ -30,45 +30,73 @@ export const dropboxDownloadTool: ToolConfig ({ - path: params.path, - }), }, - transformResponse: async (response) => { - const data = await response.json() - + transformResponse: async (response, params) => { if (!response.ok) { + const errorText = await response.text() return { success: false, - error: data.error_summary || data.error?.message || 'Failed to download file', + error: errorText || 'Failed to download file', output: {}, } } + const apiResultHeader = + response.headers.get('dropbox-api-result') || response.headers.get('Dropbox-API-Result') + const metadata = apiResultHeader ? JSON.parse(apiResultHeader) : undefined + const contentType = response.headers.get('content-type') || 'application/octet-stream' + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + const resolvedName = metadata?.name || params?.path?.split('/').pop() || 'download' + + let temporaryLink: string | undefined + if (params?.accessToken) { + try { + const linkResponse = await fetch('https://api.dropboxapi.com/2/files/get_temporary_link', { + method: 'POST', + headers: { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ path: params.path }), + }) + if (linkResponse.ok) { + const linkData = await linkResponse.json() + temporaryLink = linkData.link + } + } catch { + temporaryLink = undefined + } + } + return { success: true, output: { - file: data.metadata, - content: '', // Content will be available via the temporary link - temporaryLink: data.link, + file: { + name: resolvedName, + mimeType: contentType, + data: buffer.toString('base64'), + size: buffer.length, + }, + content: buffer.toString('base64'), + metadata, + temporaryLink, }, } }, outputs: { file: { - type: 'object', + type: 'file', + description: 'Downloaded file stored in execution files', + }, + metadata: { + type: 'json', description: 'The file metadata', - properties: { - id: { type: 'string', description: 'Unique identifier for the file' }, - name: { type: 'string', description: 'Name of the file' }, - path_display: { type: 'string', description: 'Display path of the file' }, - size: { type: 'number', description: 'Size of the file in bytes' }, - }, }, temporaryLink: { type: 'string', diff --git a/apps/sim/tools/dropbox/types.ts b/apps/sim/tools/dropbox/types.ts index b48f30cc8..f789bcfce 100644 --- a/apps/sim/tools/dropbox/types.ts +++ b/apps/sim/tools/dropbox/types.ts @@ -1,4 +1,4 @@ -import type { ToolResponse } from '@/tools/types' +import type { ToolFileData, ToolResponse } from '@/tools/types' // ===== Core Types ===== @@ -91,8 +91,9 @@ export interface DropboxDownloadParams extends DropboxBaseParams { export interface DropboxDownloadResponse extends ToolResponse { output: { - file?: DropboxFileMetadata + file?: ToolFileData content?: string // Base64 encoded file content + metadata?: DropboxFileMetadata temporaryLink?: string } } diff --git a/apps/sim/tools/file/index.ts b/apps/sim/tools/file/index.ts index 236461d1a..6714c7ddd 100644 --- a/apps/sim/tools/file/index.ts +++ b/apps/sim/tools/file/index.ts @@ -1,4 +1,5 @@ -import { fileParserTool, fileParserV2Tool } from '@/tools/file/parser' +import { fileParserTool, fileParserV2Tool, fileParserV3Tool } from '@/tools/file/parser' export const fileParseTool = fileParserTool export { fileParserV2Tool } +export { fileParserV3Tool } diff --git a/apps/sim/tools/file/parser.ts b/apps/sim/tools/file/parser.ts index a20c6dd4c..5e3e32ca4 100644 --- a/apps/sim/tools/file/parser.ts +++ b/apps/sim/tools/file/parser.ts @@ -7,6 +7,8 @@ import type { FileParserInput, FileParserOutput, FileParserOutputData, + FileParserV3Output, + FileParserV3OutputData, } from '@/tools/file/types' import type { ToolConfig } from '@/tools/types' @@ -29,6 +31,66 @@ interface ToolBodyParams extends Partial { } } +const parseFileParserResponse = async (response: Response): Promise => { + logger.info('Received response status:', response.status) + + const result = (await response.json()) as FileParseApiResponse | FileParseApiMultiResponse + logger.info('Response parsed successfully') + + // Handle multiple files response + if ('results' in result) { + logger.info('Processing multiple files response') + + // Extract individual file results + const fileResults: FileParseResult[] = result.results.map((fileResult) => { + return fileResult.output || (fileResult as unknown as FileParseResult) + }) + + // Collect UserFile objects from results + const processedFiles: UserFile[] = fileResults + .filter((file): file is FileParseResult & { file: UserFile } => Boolean(file.file)) + .map((file) => file.file) + + // Combine all file contents with clear dividers + const combinedContent = fileResults + .map((file, index) => { + const divider = `\n${'='.repeat(80)}\n` + + return file.content + (index < fileResults.length - 1 ? divider : '') + }) + .join('\n') + + // Create the base output + const output: FileParserOutputData = { + files: fileResults, + combinedContent, + ...(processedFiles.length > 0 && { processedFiles }), + } + + return { + success: true, + output, + } + } + + // Handle single file response + logger.info('Successfully parsed file:', result.output?.name || 'unknown') + + const fileOutput: FileParseResult = result.output || (result as unknown as FileParseResult) + + // For a single file, create the output with just array format + const output: FileParserOutputData = { + files: [fileOutput], + combinedContent: fileOutput?.content || result.content || '', + ...(fileOutput?.file && { processedFiles: [fileOutput.file] }), + } + + return { + success: true, + output, + } +} + export const fileParserTool: ToolConfig = { id: 'file_parser', name: 'File Parser', @@ -38,7 +100,7 @@ export const fileParserTool: ToolConfig = { params: { filePath: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'Path to the file(s). Can be a single path, URL, or an array of paths.', }, @@ -111,65 +173,7 @@ export const fileParserTool: ToolConfig = { }, }, - transformResponse: async (response: Response): Promise => { - logger.info('Received response status:', response.status) - - const result = (await response.json()) as FileParseApiResponse | FileParseApiMultiResponse - logger.info('Response parsed successfully') - - // Handle multiple files response - if ('results' in result) { - logger.info('Processing multiple files response') - - // Extract individual file results - const fileResults: FileParseResult[] = result.results.map((fileResult) => { - return fileResult.output || (fileResult as unknown as FileParseResult) - }) - - // Collect UserFile objects from results - const processedFiles: UserFile[] = fileResults - .filter((file): file is FileParseResult & { file: UserFile } => Boolean(file.file)) - .map((file) => file.file) - - // Combine all file contents with clear dividers - const combinedContent = fileResults - .map((file, index) => { - const divider = `\n${'='.repeat(80)}\n` - - return file.content + (index < fileResults.length - 1 ? divider : '') - }) - .join('\n') - - // Create the base output - const output: FileParserOutputData = { - files: fileResults, - combinedContent, - ...(processedFiles.length > 0 && { processedFiles }), - } - - return { - success: true, - output, - } - } - - // Handle single file response - logger.info('Successfully parsed file:', result.output?.name || 'unknown') - - const fileOutput: FileParseResult = result.output || (result as unknown as FileParseResult) - - // For a single file, create the output with just array format - const output: FileParserOutputData = { - files: [fileOutput], - combinedContent: fileOutput?.content || result.content || '', - ...(fileOutput?.file && { processedFiles: [fileOutput.file] }), - } - - return { - success: true, - output, - } - }, + transformResponse: parseFileParserResponse, outputs: { files: { type: 'array', description: 'Array of parsed files with content and metadata' }, @@ -186,7 +190,7 @@ export const fileParserV2Tool: ToolConfig = { params: fileParserTool.params, request: fileParserTool.request, - transformResponse: fileParserTool.transformResponse, + transformResponse: parseFileParserResponse, outputs: { files: { @@ -199,3 +203,34 @@ export const fileParserV2Tool: ToolConfig = { }, }, } + +export const fileParserV3Tool: ToolConfig = { + id: 'file_parser_v3', + name: 'File Parser', + description: 'Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)', + version: '3.0.0', + params: fileParserTool.params, + request: fileParserTool.request, + transformResponse: async (response: Response): Promise => { + const parsed = await parseFileParserResponse(response) + const output = parsed.output as FileParserOutputData + const files = + Array.isArray(output.processedFiles) && output.processedFiles.length > 0 + ? output.processedFiles + : [] + + const cleanedOutput: FileParserV3OutputData = { + files, + combinedContent: output.combinedContent, + } + + return { + success: true, + output: cleanedOutput, + } + }, + outputs: { + files: { type: 'file[]', description: 'Parsed files as UserFile objects' }, + combinedContent: { type: 'string', description: 'Combined content of all parsed files' }, + }, +} diff --git a/apps/sim/tools/file/types.ts b/apps/sim/tools/file/types.ts index 252c9f21a..086e16b9c 100644 --- a/apps/sim/tools/file/types.ts +++ b/apps/sim/tools/file/types.ts @@ -34,6 +34,17 @@ export interface FileParserOutput extends ToolResponse { output: FileParserOutputData } +export interface FileParserV3OutputData { + /** Array of parsed files as UserFile objects */ + files: UserFile[] + /** Combined text content from all files */ + combinedContent: string +} + +export interface FileParserV3Output extends ToolResponse { + output: FileParserV3OutputData +} + /** API response structure for single file parse */ export interface FileParseApiResponse { success: boolean diff --git a/apps/sim/tools/google_drive/download.ts b/apps/sim/tools/google_drive/download.ts index 0f6d2b8ef..2def338f8 100644 --- a/apps/sim/tools/google_drive/download.ts +++ b/apps/sim/tools/google_drive/download.ts @@ -224,14 +224,8 @@ export const downloadTool: ToolConfig = { +const mistralParserV2Params = { + fileData: { + type: 'object', + required: false, + visibility: 'hidden', + description: 'File data from a previous block', + }, + filePath: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'File path or URL (legacy)', + }, + resultType: mistralParserTool.params.resultType, + includeImageBase64: mistralParserTool.params.includeImageBase64, + pages: mistralParserTool.params.pages, + imageLimit: mistralParserTool.params.imageLimit, + imageMinSize: mistralParserTool.params.imageMinSize, + apiKey: mistralParserTool.params.apiKey, +} satisfies ToolConfig['params'] + +export const mistralParserV2Tool: ToolConfig = { id: 'mistral_parser_v2', name: 'Mistral PDF Parser', description: 'Parse PDF documents using Mistral OCR API', version: '2.0.0', - params: mistralParserTool.params, - request: mistralParserTool.request, + params: mistralParserV2Params, + request: { + url: '/api/tools/mistral/parse', + method: 'POST', + headers: (params) => { + 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') + } + 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') + } + + const fileData = params.fileData ?? params.filePath + if (!fileData) { + throw new Error('File input is required') + } + + const requestBody: Record = { + apiKey: params.apiKey, + resultType: params.resultType || 'markdown', + } + + if (typeof fileData === 'string') { + requestBody.filePath = fileData.trim() + } else { + requestBody.fileData = fileData + } + + if (params.pages) { + requestBody.pages = params.pages + } + if (params.includeImageBase64 !== undefined) { + requestBody.includeImageBase64 = params.includeImageBase64 + } + if (params.imageLimit !== undefined) { + requestBody.imageLimit = params.imageLimit + } + if (params.imageMinSize !== undefined) { + requestBody.imageMinSize = params.imageMinSize + } + + return requestBody + }, + }, transformResponse: async (response: Response) => { let ocrResult diff --git a/apps/sim/tools/mistral/types.ts b/apps/sim/tools/mistral/types.ts index db912b76c..2ac78c2a0 100644 --- a/apps/sim/tools/mistral/types.ts +++ b/apps/sim/tools/mistral/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { OutputProperty, ToolResponse } from '@/tools/types' /** @@ -137,7 +138,7 @@ export const MISTRAL_PARSER_METADATA_OUTPUT: OutputProperty = { export interface MistralParserInput { filePath: string - fileUpload?: any + fileUpload?: UserFile _internalFilePath?: string apiKey: string resultType?: 'markdown' | 'text' | 'json' @@ -147,6 +148,17 @@ export interface MistralParserInput { imageMinSize?: number } +export interface MistralParserV2Input { + fileData?: UserFile | string + filePath?: string + apiKey: string + resultType?: 'markdown' | 'text' | 'json' + includeImageBase64?: boolean + pages?: number[] + imageLimit?: number + imageMinSize?: number +} + export interface MistralOcrUsageInfo { pagesProcessed: number docSizeBytes: number | null diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index 6ceb005c5..df30e2655 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface MicrosoftGraphDriveItem { @@ -91,7 +92,7 @@ export interface OneDriveToolParams { folderName?: string fileId?: string fileName?: string - file?: unknown // UserFile or UserFile array + file?: UserFile content?: string mimeType?: string query?: string diff --git a/apps/sim/tools/outlook/types.ts b/apps/sim/tools/outlook/types.ts index 6e7e630e2..a494245eb 100644 --- a/apps/sim/tools/outlook/types.ts +++ b/apps/sim/tools/outlook/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { OutputProperty, ToolResponse } from '@/tools/types' /** @@ -117,7 +118,7 @@ export interface OutlookSendParams { conversationId?: string cc?: string bcc?: string - attachments?: any[] + attachments?: UserFile[] } export interface OutlookSendResponse extends ToolResponse { @@ -150,7 +151,7 @@ export interface OutlookDraftParams { subject: string body: string contentType?: 'text' | 'html' - attachments?: any[] + attachments?: UserFile[] } export interface OutlookDraftResponse extends ToolResponse { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 0d396d8c0..4f342e027 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -239,7 +239,7 @@ import { exaResearchTool, exaSearchTool, } from '@/tools/exa' -import { fileParserV2Tool, fileParseTool } from '@/tools/file' +import { fileParserV2Tool, fileParserV3Tool, fileParseTool } from '@/tools/file' import { firecrawlAgentTool, firecrawlCrawlTool, @@ -1778,6 +1778,7 @@ export const tools: Record = { vision_tool: visionTool, file_parser: fileParseTool, file_parser_v2: fileParserV2Tool, + file_parser_v3: fileParserV3Tool, firecrawl_scrape: firecrawlScrapeTool, firecrawl_search: firecrawlSearchTool, firecrawl_crawl: firecrawlCrawlTool, diff --git a/apps/sim/tools/s3/get_object.ts b/apps/sim/tools/s3/get_object.ts index 89f2f4a7b..585604265 100644 --- a/apps/sim/tools/s3/get_object.ts +++ b/apps/sim/tools/s3/get_object.ts @@ -50,7 +50,7 @@ export const s3GetObjectTool: ToolConfig = { ) } }, - method: 'HEAD', + method: 'GET', headers: (params) => { try { // Parse S3 URI if not already parsed @@ -66,7 +66,7 @@ export const s3GetObjectTool: ToolConfig = { const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '') const dateStamp = amzDate.slice(0, 8) - const method = 'HEAD' + const method = 'GET' const encodedPath = encodeS3PathComponent(params.objectKey) const canonicalUri = `/${encodedPath}` const canonicalQueryString = '' @@ -108,11 +108,18 @@ export const s3GetObjectTool: ToolConfig = { params.objectKey = objectKey } - // Get file metadata + if (!response.ok) { + const errorText = await response.text() + throw new Error( + `Failed to download S3 object: ${response.status} ${response.statusText} ${errorText}` + ) + } + const contentType = response.headers.get('content-type') || 'application/octet-stream' - const contentLength = Number.parseInt(response.headers.get('content-length') || '0', 10) const lastModified = response.headers.get('last-modified') || new Date().toISOString() const fileName = params.objectKey.split('/').pop() || params.objectKey + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) // Generate pre-signed URL for download const url = generatePresignedUrl(params, 3600) @@ -121,9 +128,15 @@ export const s3GetObjectTool: ToolConfig = { success: true, output: { url, + file: { + name: fileName, + mimeType: contentType, + data: buffer.toString('base64'), + size: buffer.length, + }, metadata: { fileType: contentType, - size: contentLength, + size: buffer.length, name: fileName, lastModified: lastModified, }, @@ -136,6 +149,10 @@ export const s3GetObjectTool: ToolConfig = { type: 'string', description: 'Pre-signed URL for downloading the S3 object', }, + file: { + type: 'file', + description: 'Downloaded file stored in execution files', + }, metadata: { type: 'object', description: 'File metadata including type, size, name, and last modified date', diff --git a/apps/sim/tools/s3/types.ts b/apps/sim/tools/s3/types.ts index 67f1fc49d..44612c44c 100644 --- a/apps/sim/tools/s3/types.ts +++ b/apps/sim/tools/s3/types.ts @@ -1,8 +1,10 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface S3Response extends ToolResponse { output: { url?: string + file?: UserFile objects?: Array<{ key: string size: number diff --git a/apps/sim/tools/sendgrid/types.ts b/apps/sim/tools/sendgrid/types.ts index a3f292883..cc9593a70 100644 --- a/apps/sim/tools/sendgrid/types.ts +++ b/apps/sim/tools/sendgrid/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' // Shared type definitions @@ -48,6 +49,14 @@ export interface SendGridPersonalization { dynamic_template_data?: Record } +export interface SendGridAttachment { + content: string + filename: string + type?: string + disposition?: string + content_id?: string +} + export interface SendGridMailBody { personalizations: SendGridPersonalization[] from: { email: string; name?: string } @@ -55,7 +64,7 @@ export interface SendGridMailBody { template_id?: string content?: Array<{ type: 'text/plain' | 'text/html'; value?: string }> reply_to?: { email: string; name?: string } - attachments?: any[] + attachments?: SendGridAttachment[] | UserFile[] } export interface SendGridContactObject { @@ -95,7 +104,7 @@ export interface SendMailParams extends SendGridBaseParams { bcc?: string replyTo?: string replyToName?: string - attachments?: string + attachments?: UserFile[] | SendGridAttachment[] | string templateId?: string dynamicTemplateData?: string } diff --git a/apps/sim/tools/sftp/types.ts b/apps/sim/tools/sftp/types.ts index e1ed9004e..de32d3cc2 100644 --- a/apps/sim/tools/sftp/types.ts +++ b/apps/sim/tools/sftp/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface SftpConnectionConfig { @@ -12,7 +13,7 @@ export interface SftpConnectionConfig { // Upload file params export interface SftpUploadParams extends SftpConnectionConfig { remotePath: string - files?: any[] // UserFile array from file-upload component + files?: UserFile[] fileContent?: string // Direct content for text files fileName?: string // File name when using direct content overwrite?: boolean diff --git a/apps/sim/tools/sharepoint/types.ts b/apps/sim/tools/sharepoint/types.ts index 5be8061ce..c587d03f9 100644 --- a/apps/sim/tools/sharepoint/types.ts +++ b/apps/sim/tools/sharepoint/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface SharepointSite { @@ -180,7 +181,7 @@ export interface SharepointToolParams { driveId?: string folderPath?: string fileName?: string - files?: any[] + files?: UserFile[] } export interface GraphApiResponse { diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 9dfc8e621..54c9bc21f 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { OutputProperty, ToolResponse } from '@/tools/types' /** @@ -516,7 +517,7 @@ export interface SlackMessageParams extends SlackBaseParams { userId?: string text: string thread_ts?: string - files?: any[] + files?: UserFile[] } export interface SlackCanvasParams extends SlackBaseParams { diff --git a/apps/sim/tools/smtp/types.ts b/apps/sim/tools/smtp/types.ts index 9792262a9..a7b93891b 100644 --- a/apps/sim/tools/smtp/types.ts +++ b/apps/sim/tools/smtp/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface SmtpConnectionConfig { @@ -21,7 +22,7 @@ export interface SmtpSendMailParams extends SmtpConnectionConfig { cc?: string bcc?: string replyTo?: string - attachments?: any[] + attachments?: UserFile[] } export interface SmtpSendMailResult extends ToolResponse { diff --git a/apps/sim/tools/telegram/types.ts b/apps/sim/tools/telegram/types.ts index f1ab4eb9d..0e54deeb4 100644 --- a/apps/sim/tools/telegram/types.ts +++ b/apps/sim/tools/telegram/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface TelegramMessage { @@ -116,7 +117,7 @@ export interface TelegramSendAnimationParams extends TelegramAuthParams { } export interface TelegramSendDocumentParams extends TelegramAuthParams { - files?: any + files?: UserFile[] caption?: string } diff --git a/apps/sim/tools/vision/tool.ts b/apps/sim/tools/vision/tool.ts index 2eb186bec..a9c334a19 100644 --- a/apps/sim/tools/vision/tool.ts +++ b/apps/sim/tools/vision/tool.ts @@ -52,7 +52,7 @@ export const visionTool: ToolConfig = { apiKey: params.apiKey, imageUrl: params.imageUrl || null, imageFile: params.imageFile || null, - model: params.model || 'gpt-4o', + model: params.model || 'gpt-5.2', prompt: params.prompt || null, } }, diff --git a/apps/sim/tools/vision/types.ts b/apps/sim/tools/vision/types.ts index a0c3412b2..cda8c4559 100644 --- a/apps/sim/tools/vision/types.ts +++ b/apps/sim/tools/vision/types.ts @@ -1,9 +1,10 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface VisionParams { apiKey: string imageUrl?: string - imageFile?: any + imageFile?: UserFile model?: string prompt?: string } diff --git a/apps/sim/triggers/generic/webhook.ts b/apps/sim/triggers/generic/webhook.ts index 5dd24a489..92235cfe2 100644 --- a/apps/sim/triggers/generic/webhook.ts +++ b/apps/sim/triggers/generic/webhook.ts @@ -53,7 +53,7 @@ export const genericWebhookTrigger: TriggerConfig = { title: 'Input Format', type: 'input-format', description: - 'Define the expected JSON input schema for this webhook (optional). Use type "files" for file uploads.', + 'Define the expected JSON input schema for this webhook (optional). Use type "file[]" for file uploads.', mode: 'trigger', }, { diff --git a/apps/sim/triggers/microsoftteams/webhook.ts b/apps/sim/triggers/microsoftteams/webhook.ts index e1d6f181d..abec61982 100644 --- a/apps/sim/triggers/microsoftteams/webhook.ts +++ b/apps/sim/triggers/microsoftteams/webhook.ts @@ -98,7 +98,7 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = { }, message: { raw: { - attachments: { type: 'array', description: 'Array of attachments' }, + attachments: { type: 'file[]', description: 'Array of attachments' }, channelData: { team: { id: { type: 'string', description: 'Team ID' } }, tenant: { id: { type: 'string', description: 'Tenant ID' } },