mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-03 03:04:57 -05:00
progress on files
This commit is contained in:
134
apps/docs/content/docs/en/execution/files.mdx
Normal file
134
apps/docs/content/docs/en/execution/files.mdx
Normal file
@@ -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
|
||||
<gmail.attachments[0]>
|
||||
|
||||
// Pass the whole file object
|
||||
<file_parser.files[0]>
|
||||
|
||||
// Access specific properties
|
||||
<gmail.attachments[0].name>
|
||||
<gmail.attachments[0].base64>
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
<Tabs items={['Base64', 'URL']}>
|
||||
<Tab value="Base64">
|
||||
```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"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="URL">
|
||||
```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"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
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
|
||||
|
||||
<Callout type="info">
|
||||
Files are automatically available to downstream blocks. The execution engine handles all file transfer and format conversion.
|
||||
</Callout>
|
||||
|
||||
## 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.
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"pages": ["index", "basics", "api", "logging", "costs"]
|
||||
"pages": ["index", "basics", "files", "api", "logging", "costs"]
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -179,7 +179,7 @@ export function A2aDeploy({
|
||||
newFields.push({
|
||||
id: crypto.randomUUID(),
|
||||
name: 'files',
|
||||
type: 'files',
|
||||
type: 'file[]',
|
||||
value: '',
|
||||
collapsed: false,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1746,7 +1746,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
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}`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -60,12 +60,25 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
|
||||
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
|
||||
|
||||
@@ -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<FileParserOutput> = {
|
||||
},
|
||||
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<FileParserOutput> = {
|
||||
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<FileParserOutput> = {
|
||||
export const FileV2Block: BlockConfig<FileParserOutput> = {
|
||||
...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<FileParserOutput> = {
|
||||
},
|
||||
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<FileParserOutput> = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const FileV3Block: BlockConfig<FileParserV3Output> = {
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<GmailToolResponse> = {
|
||||
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: {
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -149,7 +149,7 @@ export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
|
||||
},
|
||||
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' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -462,7 +462,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
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',
|
||||
|
||||
@@ -159,14 +159,16 @@ export const MistralParseV2Block: BlockConfig<MistralParserOutput> = {
|
||||
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<MistralParserOutput> = {
|
||||
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<MistralParserOutput> = {
|
||||
},
|
||||
},
|
||||
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' },
|
||||
|
||||
@@ -393,7 +393,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
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: {
|
||||
|
||||
@@ -440,7 +440,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
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' },
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -418,6 +418,7 @@ export const S3Block: BlockConfig<S3Response> = {
|
||||
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' },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
|
||||
@@ -450,10 +450,24 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
|
||||
|
||||
// === 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<ToolResponse> = {
|
||||
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' },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -65,39 +65,91 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
|
||||
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<TelegramResponse> = {
|
||||
...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<TelegramResponse> = {
|
||||
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)',
|
||||
|
||||
@@ -578,7 +578,7 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
|
||||
|
||||
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',
|
||||
|
||||
@@ -420,7 +420,7 @@ export const VideoGeneratorBlock: BlockConfig<VideoBlockResponse> = {
|
||||
|
||||
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' },
|
||||
|
||||
@@ -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<VisionResponse> = {
|
||||
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<VisionResponse> = {
|
||||
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<VisionResponse> = {
|
||||
tokens: { type: 'number', description: 'Token usage' },
|
||||
},
|
||||
}
|
||||
|
||||
export const VisionV2Block: BlockConfig<VisionResponse> = {
|
||||
...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' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<string, BlockConfig> = {
|
||||
exa: ExaBlock,
|
||||
file: FileBlock,
|
||||
file_v2: FileV2Block,
|
||||
file_v3: FileV3Block,
|
||||
firecrawl: FirecrawlBlock,
|
||||
fireflies: FirefliesBlock,
|
||||
function: FunctionBlock,
|
||||
@@ -314,6 +315,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
video_generator: VideoGeneratorBlock,
|
||||
video_generator_v2: VideoGeneratorV2Block,
|
||||
vision: VisionBlock,
|
||||
vision_v2: VisionV2Block,
|
||||
wait: WaitBlock,
|
||||
wealthbox: WealthboxBlock,
|
||||
webflow: WebflowBlock,
|
||||
|
||||
@@ -9,7 +9,8 @@ export type PrimitiveValueType =
|
||||
| 'boolean'
|
||||
| 'json'
|
||||
| 'array'
|
||||
| 'files'
|
||||
| 'file'
|
||||
| 'file[]'
|
||||
| 'any'
|
||||
|
||||
export type BlockCategory = 'blocks' | 'tools' | 'triggers'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]' } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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 } {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<Buffer>
|
||||
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',
|
||||
})
|
||||
|
||||
24
apps/sim/lib/uploads/utils/file-schemas.ts
Normal file
24
apps/sim/lib/uploads/utils/file-schemas.ts
Normal file
@@ -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()])
|
||||
@@ -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(
|
||||
|
||||
@@ -77,7 +77,7 @@ export class WebhookAttachmentProcessor {
|
||||
userId?: string
|
||||
}
|
||||
): Promise<UserFile> {
|
||||
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'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, SubBlockWithValue>,
|
||||
@@ -373,13 +384,7 @@ function getFilePropertyType(outputs: OutputDefinition, pathParts: string[]): st
|
||||
current = (current as Record<string, unknown>)[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<string, any>, 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({
|
||||
|
||||
@@ -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,...',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ToolConfig } from '@/tools/types'
|
||||
export const dropboxDownloadTool: ToolConfig<DropboxDownloadParams, DropboxDownloadResponse> = {
|
||||
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<DropboxDownloadParams, DropboxDownl
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.dropboxapi.com/2/files/get_temporary_link',
|
||||
url: 'https://content.dropboxapi.com/2/files/download',
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
@@ -30,45 +30,73 @@ export const dropboxDownloadTool: ToolConfig<DropboxDownloadParams, DropboxDownl
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Dropbox-API-Arg': JSON.stringify({ path: params.path }),
|
||||
}
|
||||
},
|
||||
body: (params) => ({
|
||||
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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<FileParserInput> {
|
||||
}
|
||||
}
|
||||
|
||||
const parseFileParserResponse = async (response: Response): Promise<FileParserOutput> => {
|
||||
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<FileParserInput, FileParserOutput> = {
|
||||
id: 'file_parser',
|
||||
name: 'File Parser',
|
||||
@@ -38,7 +100,7 @@ export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
|
||||
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<FileParserInput, FileParserOutput> = {
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<FileParserOutput> => {
|
||||
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<FileParserInput, FileParserOutput> = {
|
||||
|
||||
params: fileParserTool.params,
|
||||
request: fileParserTool.request,
|
||||
transformResponse: fileParserTool.transformResponse,
|
||||
transformResponse: parseFileParserResponse,
|
||||
|
||||
outputs: {
|
||||
files: {
|
||||
@@ -199,3 +203,34 @@ export const fileParserV2Tool: ToolConfig<FileParserInput, FileParserOutput> = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const fileParserV3Tool: ToolConfig<FileParserInput, FileParserV3Output> = {
|
||||
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<FileParserV3Output> => {
|
||||
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' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -224,14 +224,8 @@ export const downloadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveDownload
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'object',
|
||||
description: 'Downloaded file data',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type of the file' },
|
||||
data: { type: 'string', description: 'File content as base64-encoded string' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
},
|
||||
type: 'file',
|
||||
description: 'Downloaded file stored in execution files',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
// User information returned in various file metadata fields
|
||||
@@ -322,7 +323,7 @@ export interface GoogleDriveToolParams {
|
||||
folderSelector?: string
|
||||
fileId?: string
|
||||
fileName?: string
|
||||
file?: any // UserFile object
|
||||
file?: UserFile
|
||||
content?: string
|
||||
mimeType?: string
|
||||
query?: string
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface MicrosoftTeamsAttachment {
|
||||
@@ -71,7 +72,7 @@ export interface MicrosoftTeamsToolParams {
|
||||
teamId?: string
|
||||
content?: string
|
||||
includeAttachments?: boolean
|
||||
files?: any[] // UserFile array for attachments
|
||||
files?: UserFile[]
|
||||
reactionType?: string // For reaction operations
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import type {
|
||||
MistralParserInput,
|
||||
MistralParserOutput,
|
||||
MistralParserV2Input,
|
||||
MistralParserV2Output,
|
||||
} from '@/tools/mistral/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
@@ -420,14 +421,84 @@ export const mistralParserTool: ToolConfig<MistralParserInput, MistralParserOutp
|
||||
},
|
||||
}
|
||||
|
||||
export const mistralParserV2Tool: ToolConfig<MistralParserInput, MistralParserV2Output> = {
|
||||
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<MistralParserV2Input, MistralParserV2Output> = {
|
||||
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<string, unknown> = {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, ToolConfig> = {
|
||||
vision_tool: visionTool,
|
||||
file_parser: fileParseTool,
|
||||
file_parser_v2: fileParserV2Tool,
|
||||
file_parser_v3: fileParserV3Tool,
|
||||
firecrawl_scrape: firecrawlScrapeTool,
|
||||
firecrawl_search: firecrawlSearchTool,
|
||||
firecrawl_crawl: firecrawlCrawlTool,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, unknown>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const visionTool: ToolConfig<VisionParams, VisionResponse> = {
|
||||
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,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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' } },
|
||||
|
||||
Reference in New Issue
Block a user