Compare commits

..

1 Commits

Author SHA1 Message Date
Vikhyath Mondreti
43049eb380 fix(copilot): legacy tool display names 2026-01-20 21:07:30 -08:00
53 changed files with 391 additions and 3370 deletions

View File

@@ -14,7 +14,7 @@
</p>
<p align="center">
<a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a> <a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
<a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a> <a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20Studio%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
</p>
### Build Workflows with Ease

View File

@@ -4093,23 +4093,6 @@ export function SQSIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function TextractIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='10 14 60 52'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
>
<path
d='M22.0624102,50 C24.3763895,53.603 28.4103535,56 33.0003125,56 C40.1672485,56 45.9991964,50.168 45.9991964,43 C45.9991964,35.832 40.1672485,30 33.0003125,30 C27.6033607,30 22.9664021,33.307 21.0024196,38 L23.2143999,38 C25.0393836,34.444 28.7363506,32 33.0003125,32 C39.0652583,32 43.9992143,36.935 43.9992143,43 C43.9992143,49.065 39.0652583,54 33.0003125,54 C29.5913429,54 26.5413702,52.441 24.5213882,50 L22.0624102,50 Z M37.0002768,45 L37.0002768,43 L41.9992321,43 C41.9992321,38.038 37.9622682,34 33.0003125,34 C28.0373568,34 23.9993929,38.038 23.9993929,43 L28.9993482,43 L28.9993482,45 L24.2313908,45 C25.1443826,49.002 28.7253507,52 33.0003125,52 C35.1362934,52 37.0992759,51.249 38.6442621,50 L34.0003036,50 L34.0003036,48 L40.4782457,48 C41.0812403,47.102 41.5202364,46.087 41.7682342,45 L37.0002768,45 Z M21.0024196,48 L23.2143999,48 C22.4434068,46.498 22.0004107,44.801 22.0004107,43 C22.0004107,41.959 22.1554093,40.955 22.4264069,40 L20.3634253,40 C20.1344274,40.965 19.9994286,41.966 19.9994286,43 C19.9994286,44.771 20.3584254,46.46 21.0024196,48 L21.0024196,48 Z M19.7434309,50 L17.0004554,50 L17.0004554,48 L18.8744386,48 C18.5344417,47.04 18.2894438,46.038 18.1494451,45 L15.4144695,45 L16.707458,46.293 L15.2924706,47.707 L12.2924974,44.707 C11.9025009,44.316 11.9025009,43.684 12.2924974,43.293 L15.2924706,40.293 L16.707458,41.707 L15.4144695,43 L18.0004464,43 C18.0004464,41.973 18.1044455,40.97 18.3024437,40 L17.0004554,40 L17.0004554,38 L18.8744386,38 C20.9404202,32.184 26.4833707,28 33.0003125,28 C37.427273,28 41.4002375,29.939 44.148213,33 L59.0000804,33 L59.0000804,35 L45.6661994,35 C47.1351863,37.318 47.9991786,40.058 47.9991786,43 L59.0000804,43 L59.0000804,45 L47.8501799,45 C46.8681887,52.327 40.5912447,58 33.0003125,58 C27.2563638,58 22.2624084,54.752 19.7434309,50 L19.7434309,50 Z M37.0002768,39 C37.0002768,38.448 36.5522808,38 36.0002857,38 L29.9993482,38 C29.4473442,38 28.9993482,38.448 28.9993482,39 L28.9993482,41 L31.0003304,41 L31.0003304,40 L32.0003214,40 L32.0003214,43 L31.0003304,43 L31.0003304,45 L35.0002946,45 L35.0002946,43 L34.0003036,43 L34.0003036,40 L35.0002946,40 L35.0002946,41 L37.0002768,41 L37.0002768,39 Z M49.0001696,40 L59.0000804,40 L59.0000804,38 L49.0001696,38 L49.0001696,40 Z M49.0001696,50 L59.0000804,50 L59.0000804,48 L49.0001696,48 L49.0001696,50 Z M57.0000982,27 L60.5850662,27 L57.0000982,23.414 L57.0000982,27 Z M63.7070383,27.293 C63.8940367,27.48 64.0000357,27.735 64.0000357,28 L64.0000357,63 C64.0000357,63.552 63.5520397,64 63.0000446,64 L32.0003304,64 C31.4473264,64 31.0003304,63.552 31.0003304,63 L31.0003304,59 L33.0003125,59 L33.0003125,62 L62.0000536,62 L62.0000536,29 L56.0001071,29 C55.4471121,29 55.0001161,28.552 55.0001161,28 L55.0001161,22 L33.0003125,22 L33.0003125,27 L31.0003304,27 L31.0003304,21 C31.0003304,20.448 31.4473264,20 32.0003304,20 L56.0001071,20 C56.2651048,20 56.5191025,20.105 56.7071008,20.293 L63.7070383,27.293 Z M68,24.166 L68,61 C68,61.552 67.552004,62 67.0000089,62 L65.0000268,62 L65.0000268,60 L66.0000179,60 L66.0000179,24.612 L58.6170838,18 L36.0002857,18 L36.0002857,19 L34.0003036,19 L34.0003036,17 C34.0003036,16.448 34.4472996,16 35.0003036,16 L59.0000804,16 C59.2460782,16 59.483076,16.091 59.6660744,16.255 L67.666003,23.42 C67.8780011,23.61 68,23.881 68,24.166 L68,24.166 Z'
fill='currentColor'
/>
</svg>
)
}
export function McpIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -110,7 +110,6 @@ import {
SupabaseIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
TinybirdIcon,
TranslateIcon,
TrelloIcon,
@@ -144,7 +143,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
calendly: CalendlyIcon,
circleback: CirclebackIcon,
clay: ClayIcon,
confluence_v2: ConfluenceIcon,
confluence: ConfluenceIcon,
cursor_v2: CursorIcon,
datadog: DatadogIcon,
discord: DiscordIcon,
@@ -154,7 +153,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
exa: ExaAIIcon,
file_v2: DocumentIcon,
file: DocumentIcon,
firecrawl: FirecrawlIcon,
fireflies: FirefliesIcon,
github_v2: GithubIcon,
@@ -196,7 +195,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse_v2: MistralIcon,
mistral_parse: MistralIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,
@@ -238,7 +237,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
supabase: SupabaseIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract: TextractIcon,
tinybird: TinybirdIcon,
translate: TranslateIcon,
trello: TrelloIcon,
@@ -246,7 +244,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
twilio_sms: TwilioIcon,
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
video_generator_v2: VideoIcon,
video_generator: VideoIcon,
vision: EyeIcon,
wealthbox: WealthboxIcon,
webflow: WebflowIcon,

View File

@@ -6,7 +6,7 @@ description: Interact with Confluence
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="confluence_v2"
type="confluence"
color="#E0E0E0"
/>

View File

@@ -6,7 +6,7 @@ description: Read and parse multiple files
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file_v2"
type="file"
color="#40916C"
/>
@@ -48,7 +48,7 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | array | Array of parsed files with content, metadata, and file properties |
| `combinedContent` | string | All file contents merged into a single text string |
| `files` | array | Array of parsed files |
| `combinedContent` | string | Combined content of all parsed files |

View File

@@ -106,7 +106,6 @@
"supabase",
"tavily",
"telegram",
"textract",
"tinybird",
"translate",
"trello",

View File

@@ -6,7 +6,7 @@ description: Extract text from PDF documents
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="mistral_parse_v2"
type="mistral_parse"
color="#000000"
/>
@@ -54,37 +54,18 @@ Parse PDF documents using Mistral OCR API
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `pages` | array | Array of page objects from Mistral OCR |
| ↳ `index` | number | Page index \(zero-based\) |
| ↳ `markdown` | string | Extracted markdown content |
| ↳ `images` | array | Images extracted from this page with bounding boxes |
| ↳ `id` | string | Image identifier \(e.g., img-0.jpeg\) |
| ↳ `top_left_x` | number | Top-left X coordinate in pixels |
| ↳ `top_left_y` | number | Top-left Y coordinate in pixels |
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
| ↳ `id` | string | Image identifier \(e.g., img-0.jpeg\) |
| ↳ `top_left_x` | number | Top-left X coordinate in pixels |
| ↳ `top_left_y` | number | Top-left Y coordinate in pixels |
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
| ↳ `dimensions` | object | Page dimensions |
| ↳ `dpi` | number | Dots per inch |
| ↳ `height` | number | Page height in pixels |
| ↳ `width` | number | Page width in pixels |
| ↳ `dpi` | number | Dots per inch |
| ↳ `height` | number | Page height in pixels |
| ↳ `width` | number | Page width in pixels |
| ↳ `tables` | array | Extracted tables as HTML/markdown \(when table_format is set\). Referenced via placeholders like \[tbl-0.html\] |
| ↳ `hyperlinks` | array | Array of URL strings detected in the page \(e.g., \[ |
| ↳ `header` | string | Page header content \(when extract_header=true\) |
| ↳ `footer` | string | Page footer content \(when extract_footer=true\) |
| `model` | string | Mistral OCR model identifier \(e.g., mistral-ocr-latest\) |
| `usage_info` | object | Usage and processing statistics |
| ↳ `pages_processed` | number | Total number of pages processed |
| ↳ `doc_size_bytes` | number | Document file size in bytes |
| `document_annotation` | string | Structured annotation data as JSON string \(when applicable\) |
| `success` | boolean | Whether the PDF was parsed successfully |
| `content` | string | Extracted content in the requested format \(markdown, text, or JSON\) |
| `metadata` | object | Processing metadata including jobId, fileType, pageCount, and usage info |
| ↳ `jobId` | string | Unique job identifier |
| ↳ `fileType` | string | File type \(e.g., pdf\) |
| ↳ `fileName` | string | Original file name |
| ↳ `source` | string | Source type \(url\) |
| ↳ `pageCount` | number | Number of pages processed |
| ↳ `model` | string | Mistral model used |
| ↳ `resultType` | string | Output format \(markdown, text, json\) |
| ↳ `processedAt` | string | Processing timestamp |
| ↳ `sourceUrl` | string | Source URL if applicable |
| ↳ `usageInfo` | object | Usage statistics from OCR processing |

View File

@@ -58,7 +58,6 @@ Upload a file to an AWS S3 bucket
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `url` | string | URL of the uploaded S3 object |
| `uri` | string | S3 URI of the uploaded object \(s3://bucket/key\) |
| `metadata` | object | Upload metadata including ETag and location |
### `s3_get_object`
@@ -150,7 +149,6 @@ Copy an object within or between AWS S3 buckets
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `url` | string | URL of the copied S3 object |
| `uri` | string | S3 URI of the copied object \(s3://bucket/key\) |
| `metadata` | object | Copy operation metadata |

View File

@@ -1,120 +0,0 @@
---
title: AWS Textract
description: Extract text, tables, and forms from documents
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="textract"
color="linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)"
/>
{/* MANUAL-CONTENT-START:intro */}
[AWS Textract](https://aws.amazon.com/textract/) is a powerful AI service from Amazon Web Services designed to automatically extract printed text, handwriting, tables, forms, key-value pairs, and other structured data from scanned documents and images. Textract leverages advanced optical character recognition (OCR) and document analysis to transform documents into actionable data, enabling automation, analytics, compliance, and more.
With AWS Textract, you can:
- **Extract text from images and documents**: Recognize printed text and handwriting in formats such as PDF, JPEG, PNG, or TIFF
- **Detect and extract tables**: Automatically find tables and output their structured content
- **Parse forms and key-value pairs**: Pull structured data from forms, including fields and their corresponding values
- **Identify signatures and layout features**: Detect signatures, geometric layout, and relationships between document elements
- **Customize extraction with queries**: Extract specific fields and answers using query-based extraction (e.g., "What is the invoice number?")
In Sim, the AWS Textract integration empowers your agents to intelligently process documents as part of their workflows. This unlocks automation scenarios such as data entry from invoices, onboarding documents, contracts, receipts, and more. Your agents can extract relevant data, analyze structured forms, and generate summaries or reports directly from document uploads or URLs. By connecting Sim with AWS Textract, you can reduce manual effort, improve data accuracy, and streamline your business processes with robust document understanding.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate AWS Textract into your workflow to extract text, tables, forms, and key-value pairs from documents. Single-page mode supports JPEG, PNG, and single-page PDF. Multi-page mode supports multi-page PDF and TIFF.
## Tools
### `textract_parser`
Parse documents using AWS Textract OCR and document analysis
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessKeyId` | string | Yes | AWS Access Key ID |
| `secretAccessKey` | string | Yes | AWS Secret Access Key |
| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) |
| `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. |
| `filePath` | string | No | URL to a document to be processed \(JPEG, PNG, or single-page PDF\). |
| `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). |
| `fileUpload` | object | No | File upload data from file-upload component |
| `featureTypes` | array | No | Feature types to detect: TABLES, FORMS, QUERIES, SIGNATURES, LAYOUT. If not specified, only text detection is performed. |
| `items` | string | No | Feature type |
| `queries` | array | No | Custom queries to extract specific information. Only used when featureTypes includes QUERIES. |
| `items` | object | No | Query configuration |
| `properties` | string | No | The query text |
| `Text` | string | No | No description |
| `Alias` | string | No | No description |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `blocks` | array | Array of Block objects containing detected text, tables, forms, and other elements |
| ↳ `BlockType` | string | Type of block \(PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.\) |
| ↳ `Id` | string | Unique identifier for the block |
| ↳ `Text` | string | Query text |
| ↳ `TextType` | string | Type of text \(PRINTED or HANDWRITING\) |
| ↳ `Confidence` | number | Confidence score \(0-100\) |
| ↳ `Page` | number | Page number |
| ↳ `Geometry` | object | Location and bounding box information |
| ↳ `BoundingBox` | object | Height as ratio of document height |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Polygon` | array | Polygon coordinates |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `BoundingBox` | object | Height as ratio of document height |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Polygon` | array | Polygon coordinates |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `Relationships` | array | Relationships to other blocks |
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
| ↳ `Ids` | array | IDs of related blocks |
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
| ↳ `Ids` | array | IDs of related blocks |
| ↳ `EntityTypes` | array | Entity types for KEY_VALUE_SET \(KEY or VALUE\) |
| ↳ `SelectionStatus` | string | For checkboxes: SELECTED or NOT_SELECTED |
| ↳ `RowIndex` | number | Row index for table cells |
| ↳ `ColumnIndex` | number | Column index for table cells |
| ↳ `RowSpan` | number | Row span for merged cells |
| ↳ `ColumnSpan` | number | Column span for merged cells |
| ↳ `Query` | object | Query information for QUERY blocks |
| ↳ `Text` | string | Query text |
| ↳ `Alias` | string | Query alias |
| ↳ `Pages` | array | Pages to search |
| ↳ `Alias` | string | Query alias |
| ↳ `Pages` | array | Pages to search |
| `documentMetadata` | object | Metadata about the analyzed document |
| ↳ `pages` | number | Number of pages in the document |
| `modelVersion` | string | Version of the Textract model used for processing |

View File

@@ -6,7 +6,7 @@ description: Generate videos from text using AI
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="video_generator_v2"
type="video_generator"
color="#181C1E"
/>

View File

@@ -5,11 +5,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
import {
extractStorageKey,
inferContextFromKey,
isInternalFileUrl,
} from '@/lib/uploads/utils/file-utils'
import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils'
import { verifyFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -51,13 +47,13 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Mistral parse request`, {
filePath: validatedData.filePath,
isWorkspaceFile: isInternalFileUrl(validatedData.filePath),
isWorkspaceFile: validatedData.filePath.includes('/api/files/serve/'),
userId,
})
let fileUrl = validatedData.filePath
if (isInternalFileUrl(validatedData.filePath)) {
if (validatedData.filePath?.includes('/api/files/serve/')) {
try {
const storageKey = extractStorageKey(validatedData.filePath)

View File

@@ -5,11 +5,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
import {
extractStorageKey,
inferContextFromKey,
isInternalFileUrl,
} from '@/lib/uploads/utils/file-utils'
import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils'
import { verifyFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -52,13 +48,13 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Pulse parse request`, {
filePath: validatedData.filePath,
isWorkspaceFile: isInternalFileUrl(validatedData.filePath),
isWorkspaceFile: validatedData.filePath.includes('/api/files/serve/'),
userId,
})
let fileUrl = validatedData.filePath
if (isInternalFileUrl(validatedData.filePath)) {
if (validatedData.filePath?.includes('/api/files/serve/')) {
try {
const storageKey = extractStorageKey(validatedData.filePath)
const context = inferContextFromKey(storageKey)

View File

@@ -5,11 +5,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
import {
extractStorageKey,
inferContextFromKey,
isInternalFileUrl,
} from '@/lib/uploads/utils/file-utils'
import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils'
import { verifyFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -48,13 +44,13 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Reducto parse request`, {
filePath: validatedData.filePath,
isWorkspaceFile: isInternalFileUrl(validatedData.filePath),
isWorkspaceFile: validatedData.filePath.includes('/api/files/serve/'),
userId,
})
let fileUrl = validatedData.filePath
if (isInternalFileUrl(validatedData.filePath)) {
if (validatedData.filePath?.includes('/api/files/serve/')) {
try {
const storageKey = extractStorageKey(validatedData.filePath)
const context = inferContextFromKey(storageKey)

View File

@@ -79,13 +79,11 @@ export async function POST(request: NextRequest) {
// Generate public URL for destination (properly encode the destination key)
const encodedDestKey = validatedData.destinationKey.split('/').map(encodeURIComponent).join('/')
const url = `https://${validatedData.destinationBucket}.s3.${validatedData.region}.amazonaws.com/${encodedDestKey}`
const uri = `s3://${validatedData.destinationBucket}/${validatedData.destinationKey}`
return NextResponse.json({
success: true,
output: {
url,
uri,
copySourceVersionId: result.CopySourceVersionId,
versionId: result.VersionId,
etag: result.CopyObjectResult?.ETag,

View File

@@ -117,13 +117,11 @@ export async function POST(request: NextRequest) {
const encodedKey = validatedData.objectKey.split('/').map(encodeURIComponent).join('/')
const url = `https://${validatedData.bucketName}.s3.${validatedData.region}.amazonaws.com/${encodedKey}`
const uri = `s3://${validatedData.bucketName}/${validatedData.objectKey}`
return NextResponse.json({
success: true,
output: {
url,
uri,
etag: result.ETag,
location: url,
key: validatedData.objectKey,

View File

@@ -1,637 +0,0 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import {
validateAwsRegion,
validateExternalUrl,
validateS3BucketName,
} from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { StorageService } from '@/lib/uploads'
import {
extractStorageKey,
inferContextFromKey,
isInternalFileUrl,
} from '@/lib/uploads/utils/file-utils'
import { verifyFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
export const maxDuration = 300 // 5 minutes for large multi-page PDF processing
const logger = createLogger('TextractParseAPI')
const QuerySchema = z.object({
Text: z.string().min(1),
Alias: z.string().optional(),
Pages: z.array(z.string()).optional(),
})
const TextractParseSchema = z
.object({
accessKeyId: z.string().min(1, 'AWS Access Key ID is required'),
secretAccessKey: z.string().min(1, 'AWS Secret Access Key is required'),
region: z.string().min(1, 'AWS region is required'),
processingMode: z.enum(['sync', 'async']).optional().default('sync'),
filePath: z.string().optional(),
s3Uri: z.string().optional(),
featureTypes: z
.array(z.enum(['TABLES', 'FORMS', 'QUERIES', 'SIGNATURES', 'LAYOUT']))
.optional(),
queries: z.array(QuerySchema).optional(),
})
.superRefine((data, ctx) => {
const regionValidation = validateAwsRegion(data.region, 'AWS region')
if (!regionValidation.isValid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: regionValidation.error,
path: ['region'],
})
}
})
function getSignatureKey(
key: string,
dateStamp: string,
regionName: string,
serviceName: string
): Buffer {
const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest()
const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest()
const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest()
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
return kSigning
}
function signAwsRequest(
method: string,
host: string,
uri: string,
body: string,
accessKeyId: string,
secretAccessKey: string,
region: string,
service: string,
amzTarget: string
): Record<string, string> {
const date = new Date()
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '')
const dateStamp = amzDate.slice(0, 8)
const payloadHash = crypto.createHash('sha256').update(body).digest('hex')
const canonicalHeaders =
`content-type:application/x-amz-json-1.1\n` +
`host:${host}\n` +
`x-amz-date:${amzDate}\n` +
`x-amz-target:${amzTarget}\n`
const signedHeaders = 'content-type;host;x-amz-date;x-amz-target'
const canonicalRequest = `${method}\n${uri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`
const algorithm = 'AWS4-HMAC-SHA256'
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`
const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${crypto.createHash('sha256').update(canonicalRequest).digest('hex')}`
const signingKey = getSignatureKey(secretAccessKey, dateStamp, region, service)
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex')
const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
return {
'Content-Type': 'application/x-amz-json-1.1',
Host: host,
'X-Amz-Date': amzDate,
'X-Amz-Target': amzTarget,
Authorization: authorizationHeader,
}
}
async function fetchDocumentBytes(url: string): Promise<{ bytes: string; contentType: string }> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch document: ${response.statusText}`)
}
const arrayBuffer = await response.arrayBuffer()
const bytes = Buffer.from(arrayBuffer).toString('base64')
const contentType = response.headers.get('content-type') || 'application/octet-stream'
return { bytes, contentType }
}
function parseS3Uri(s3Uri: string): { bucket: string; key: string } {
const match = s3Uri.match(/^s3:\/\/([^/]+)\/(.+)$/)
if (!match) {
throw new Error(
`Invalid S3 URI format: ${s3Uri}. Expected format: s3://bucket-name/path/to/object`
)
}
const bucket = match[1]
const key = match[2]
const bucketValidation = validateS3BucketName(bucket, 'S3 bucket name')
if (!bucketValidation.isValid) {
throw new Error(bucketValidation.error)
}
if (key.includes('..') || key.startsWith('/')) {
throw new Error('S3 key contains invalid path traversal sequences')
}
return { bucket, key }
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function callTextractAsync(
host: string,
amzTarget: string,
body: Record<string, unknown>,
accessKeyId: string,
secretAccessKey: string,
region: string
): Promise<Record<string, unknown>> {
const bodyString = JSON.stringify(body)
const headers = signAwsRequest(
'POST',
host,
'/',
bodyString,
accessKeyId,
secretAccessKey,
region,
'textract',
amzTarget
)
const response = await fetch(`https://${host}/`, {
method: 'POST',
headers,
body: bodyString,
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Textract API error: ${response.statusText}`
try {
const errorJson = JSON.parse(errorText)
if (errorJson.Message) {
errorMessage = errorJson.Message
} else if (errorJson.__type) {
errorMessage = `${errorJson.__type}: ${errorJson.message || errorText}`
}
} catch {
// Use default error message
}
throw new Error(errorMessage)
}
return response.json()
}
async function pollForJobCompletion(
host: string,
jobId: string,
accessKeyId: string,
secretAccessKey: string,
region: string,
useAnalyzeDocument: boolean,
requestId: string
): Promise<Record<string, unknown>> {
const pollIntervalMs = 5000 // 5 seconds between polls
const maxPollTimeMs = 180000 // 3 minutes maximum polling time
const maxAttempts = Math.ceil(maxPollTimeMs / pollIntervalMs)
const getTarget = useAnalyzeDocument
? 'Textract.GetDocumentAnalysis'
: 'Textract.GetDocumentTextDetection'
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await callTextractAsync(
host,
getTarget,
{ JobId: jobId },
accessKeyId,
secretAccessKey,
region
)
const jobStatus = result.JobStatus as string
if (jobStatus === 'SUCCEEDED') {
logger.info(`[${requestId}] Async job completed successfully after ${attempt + 1} polls`)
let allBlocks = (result.Blocks as unknown[]) || []
let nextToken = result.NextToken as string | undefined
while (nextToken) {
const nextResult = await callTextractAsync(
host,
getTarget,
{ JobId: jobId, NextToken: nextToken },
accessKeyId,
secretAccessKey,
region
)
allBlocks = allBlocks.concat((nextResult.Blocks as unknown[]) || [])
nextToken = nextResult.NextToken as string | undefined
}
return {
...result,
Blocks: allBlocks,
}
}
if (jobStatus === 'FAILED') {
throw new Error(`Textract job failed: ${result.StatusMessage || 'Unknown error'}`)
}
if (jobStatus === 'PARTIAL_SUCCESS') {
logger.warn(`[${requestId}] Job completed with partial success: ${result.StatusMessage}`)
let allBlocks = (result.Blocks as unknown[]) || []
let nextToken = result.NextToken as string | undefined
while (nextToken) {
const nextResult = await callTextractAsync(
host,
getTarget,
{ JobId: jobId, NextToken: nextToken },
accessKeyId,
secretAccessKey,
region
)
allBlocks = allBlocks.concat((nextResult.Blocks as unknown[]) || [])
nextToken = nextResult.NextToken as string | undefined
}
return {
...result,
Blocks: allBlocks,
}
}
logger.info(`[${requestId}] Job status: ${jobStatus}, attempt ${attempt + 1}/${maxAttempts}`)
await sleep(pollIntervalMs)
}
throw new Error(
`Timeout waiting for Textract job to complete (max ${maxPollTimeMs / 1000} seconds)`
)
}
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Textract parse attempt`, {
error: authResult.error || 'Missing userId',
})
return NextResponse.json(
{
success: false,
error: authResult.error || 'Unauthorized',
},
{ status: 401 }
)
}
const userId = authResult.userId
const body = await request.json()
const validatedData = TextractParseSchema.parse(body)
const processingMode = validatedData.processingMode || 'sync'
const featureTypes = validatedData.featureTypes ?? []
const useAnalyzeDocument = featureTypes.length > 0
const host = `textract.${validatedData.region}.amazonaws.com`
logger.info(`[${requestId}] Textract parse request`, {
processingMode,
filePath: validatedData.filePath?.substring(0, 50),
s3Uri: validatedData.s3Uri?.substring(0, 50),
featureTypes,
userId,
})
if (processingMode === 'async') {
if (!validatedData.s3Uri) {
return NextResponse.json(
{
success: false,
error: 'S3 URI is required for multi-page processing (s3://bucket/key)',
},
{ status: 400 }
)
}
const { bucket: s3Bucket, key: s3Key } = parseS3Uri(validatedData.s3Uri)
logger.info(`[${requestId}] Starting async Textract job`, { s3Bucket, s3Key })
const startTarget = useAnalyzeDocument
? 'Textract.StartDocumentAnalysis'
: 'Textract.StartDocumentTextDetection'
const startBody: Record<string, unknown> = {
DocumentLocation: {
S3Object: {
Bucket: s3Bucket,
Name: s3Key,
},
},
}
if (useAnalyzeDocument) {
startBody.FeatureTypes = featureTypes
if (
validatedData.queries &&
validatedData.queries.length > 0 &&
featureTypes.includes('QUERIES')
) {
startBody.QueriesConfig = {
Queries: validatedData.queries.map((q) => ({
Text: q.Text,
Alias: q.Alias,
Pages: q.Pages,
})),
}
}
}
const startResult = await callTextractAsync(
host,
startTarget,
startBody,
validatedData.accessKeyId,
validatedData.secretAccessKey,
validatedData.region
)
const jobId = startResult.JobId as string
if (!jobId) {
throw new Error('Failed to start Textract job: No JobId returned')
}
logger.info(`[${requestId}] Async job started`, { jobId })
const textractData = await pollForJobCompletion(
host,
jobId,
validatedData.accessKeyId,
validatedData.secretAccessKey,
validatedData.region,
useAnalyzeDocument,
requestId
)
logger.info(`[${requestId}] Textract async parse successful`, {
pageCount: (textractData.DocumentMetadata as { Pages?: number })?.Pages ?? 0,
blockCount: (textractData.Blocks as unknown[])?.length ?? 0,
})
return NextResponse.json({
success: true,
output: {
blocks: textractData.Blocks ?? [],
documentMetadata: {
pages: (textractData.DocumentMetadata as { Pages?: number })?.Pages ?? 0,
},
modelVersion: (textractData.AnalyzeDocumentModelVersion ??
textractData.DetectDocumentTextModelVersion) as string | undefined,
},
})
}
if (!validatedData.filePath) {
return NextResponse.json(
{
success: false,
error: 'File path is required for single-page processing',
},
{ status: 400 }
)
}
let fileUrl = validatedData.filePath
const isInternalFilePath = validatedData.filePath && isInternalFileUrl(validatedData.filePath)
if (isInternalFilePath) {
try {
const storageKey = extractStorageKey(validatedData.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 (validatedData.filePath?.startsWith('/')) {
// Reject arbitrary absolute paths that don't contain /api/files/serve/
logger.warn(`[${requestId}] Invalid internal path`, {
userId,
path: validatedData.filePath.substring(0, 50),
})
return NextResponse.json(
{
success: false,
error: 'Invalid file path. Only uploaded files are supported for internal paths.',
},
{ status: 400 }
)
} else {
const urlValidation = validateExternalUrl(fileUrl, 'Document URL')
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] SSRF attempt blocked`, {
userId,
url: fileUrl.substring(0, 100),
error: urlValidation.error,
})
return NextResponse.json(
{
success: false,
error: urlValidation.error,
},
{ status: 400 }
)
}
}
const { bytes, contentType } = await fetchDocumentBytes(fileUrl)
// Track if this is a PDF for better error messaging
const isPdf = contentType.includes('pdf') || fileUrl.toLowerCase().endsWith('.pdf')
const uri = '/'
let textractBody: Record<string, unknown>
let amzTarget: string
if (useAnalyzeDocument) {
amzTarget = 'Textract.AnalyzeDocument'
textractBody = {
Document: {
Bytes: bytes,
},
FeatureTypes: featureTypes,
}
if (
validatedData.queries &&
validatedData.queries.length > 0 &&
featureTypes.includes('QUERIES')
) {
textractBody.QueriesConfig = {
Queries: validatedData.queries.map((q) => ({
Text: q.Text,
Alias: q.Alias,
Pages: q.Pages,
})),
}
}
} else {
amzTarget = 'Textract.DetectDocumentText'
textractBody = {
Document: {
Bytes: bytes,
},
}
}
const bodyString = JSON.stringify(textractBody)
const headers = signAwsRequest(
'POST',
host,
uri,
bodyString,
validatedData.accessKeyId,
validatedData.secretAccessKey,
validatedData.region,
'textract',
amzTarget
)
const textractResponse = await fetch(`https://${host}${uri}`, {
method: 'POST',
headers,
body: bodyString,
})
if (!textractResponse.ok) {
const errorText = await textractResponse.text()
logger.error(`[${requestId}] Textract API error:`, errorText)
let errorMessage = `Textract API error: ${textractResponse.statusText}`
let isUnsupportedFormat = false
try {
const errorJson = JSON.parse(errorText)
if (errorJson.Message) {
errorMessage = errorJson.Message
} else if (errorJson.__type) {
errorMessage = `${errorJson.__type}: ${errorJson.message || errorText}`
}
// Check for unsupported document format error
isUnsupportedFormat =
errorJson.__type === 'UnsupportedDocumentException' ||
errorJson.Message?.toLowerCase().includes('unsupported document') ||
errorText.toLowerCase().includes('unsupported document')
} catch {
isUnsupportedFormat = errorText.toLowerCase().includes('unsupported document')
}
// Provide helpful message for unsupported format (likely multi-page PDF)
if (isUnsupportedFormat && isPdf) {
errorMessage =
'This document format is not supported in Single Page mode. If this is a multi-page PDF, please use "Multi-Page (PDF, TIFF via S3)" mode instead, which requires uploading your document to S3 first. Single Page mode only supports JPEG, PNG, and single-page PDF files.'
}
return NextResponse.json(
{
success: false,
error: errorMessage,
},
{ status: textractResponse.status }
)
}
const textractData = await textractResponse.json()
logger.info(`[${requestId}] Textract parse successful`, {
pageCount: textractData.DocumentMetadata?.Pages ?? 0,
blockCount: textractData.Blocks?.length ?? 0,
})
return NextResponse.json({
success: true,
output: {
blocks: textractData.Blocks ?? [],
documentMetadata: {
pages: textractData.DocumentMetadata?.Pages ?? 0,
},
modelVersion:
textractData.AnalyzeDocumentModelVersion ??
textractData.DetectDocumentTextModelVersion ??
undefined,
},
})
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{
success: false,
error: 'Invalid request data',
details: error.errors,
},
{ status: 400 }
)
}
logger.error(`[${requestId}] Error in Textract parse:`, error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
},
{ status: 500 }
)
}
}

View File

@@ -129,6 +129,10 @@ export function OutputSelect({
? baselineWorkflow.blocks?.[block.id]?.subBlocks?.responseFormat?.value
: subBlockValues?.[block.id]?.responseFormat
const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id)
const operationValue =
shouldUseBaseline && baselineWorkflow
? baselineWorkflow.blocks?.[block.id]?.subBlocks?.operation?.value
: subBlockValues?.[block.id]?.operation
let outputsToProcess: Record<string, unknown> = {}
@@ -142,20 +146,10 @@ export function OutputSelect({
outputsToProcess = blockConfig?.outputs || {}
}
} else {
// Build subBlocks object for tool selector
const rawSubBlockValues =
shouldUseBaseline && baselineWorkflow
? baselineWorkflow.blocks?.[block.id]?.subBlocks
: subBlockValues?.[block.id]
const subBlocks: Record<string, { value: unknown }> = {}
if (rawSubBlockValues && typeof rawSubBlockValues === 'object') {
for (const [key, val] of Object.entries(rawSubBlockValues)) {
// Handle both { value: ... } and raw value formats
subBlocks[key] = val && typeof val === 'object' && 'value' in val ? val : { value: val }
}
}
const toolOutputs = blockConfig ? getToolOutputs(blockConfig, subBlocks) : {}
const toolOutputs =
blockConfig && typeof operationValue === 'string'
? getToolOutputs(blockConfig, operationValue)
: {}
outputsToProcess =
Object.keys(toolOutputs).length > 0 ? toolOutputs : blockConfig?.outputs || {}
}

View File

@@ -8,10 +8,9 @@ import { Button, Combobox } from '@/components/emcn/components'
import { Progress } from '@/components/ui/progress'
import { cn } from '@/lib/core/utils/cn'
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
import { getExtensionFromMimeType } from '@/lib/uploads/utils/file-utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
const logger = createLogger('FileUpload')
@@ -86,47 +85,14 @@ export function FileUpload({
}
}
/**
* Checks if a file's MIME type matches the accepted types
* Supports exact matches, wildcard patterns (e.g., 'image/*'), and '*' for all types
*/
const isFileTypeAccepted = (fileType: string | undefined, accepted: string): boolean => {
if (accepted === '*') return true
if (!fileType) return false
const acceptedList = accepted.split(',').map((t) => t.trim().toLowerCase())
const normalizedFileType = fileType.toLowerCase()
return acceptedList.some((acceptedType) => {
if (acceptedType === normalizedFileType) return true
if (acceptedType.endsWith('/*')) {
const typePrefix = acceptedType.slice(0, -1) // 'image/' from 'image/*'
return normalizedFileType.startsWith(typePrefix)
}
if (acceptedType.startsWith('.')) {
const extension = acceptedType.slice(1).toLowerCase()
const fileExtension = getExtensionFromMimeType(normalizedFileType)
if (fileExtension === extension) return true
return normalizedFileType.endsWith(`/${extension}`)
}
return false
})
}
const availableWorkspaceFiles = workspaceFiles.filter((workspaceFile) => {
const existingFiles = Array.isArray(value) ? value : value ? [value] : []
const isAlreadySelected = existingFiles.some(
return !existingFiles.some(
(existing) =>
existing.name === workspaceFile.name ||
existing.path?.includes(workspaceFile.key) ||
existing.key === workspaceFile.key
)
return !isAlreadySelected
})
useEffect(() => {
@@ -455,23 +421,23 @@ export function FileUpload({
return (
<div
key={fileKey}
className='relative rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:bg-[var(--surface-5)] dark:hover:bg-[var(--border-1)]'
className='flex items-center justify-between rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:bg-[var(--surface-5)] dark:hover:bg-[var(--border-1)]'
>
<div className='truncate pr-[24px] text-sm' title={file.name}>
<div className='flex-1 truncate pr-2 text-sm' title={file.name}>
<span className='text-[var(--text-primary)]'>{truncateMiddle(file.name)}</span>
<span className='ml-2 text-[var(--text-muted)]'>({formatFileSize(file.size)})</span>
</div>
<Button
type='button'
variant='ghost'
className='-translate-y-1/2 absolute top-1/2 right-[4px] h-6 w-6 p-0'
className='h-5 w-5 shrink-0 p-0'
onClick={(e) => handleRemoveFile(file, e)}
disabled={isDeleting}
>
{isDeleting ? (
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
<div className='h-3.5 w-3.5 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<X className='h-4 w-4 opacity-50' />
<X className='h-3.5 w-3.5' />
)}
</Button>
</div>
@@ -502,30 +468,19 @@ export function FileUpload({
const comboboxOptions = useMemo(
() => [
{ label: 'Upload New File', value: '__upload_new__' },
...availableWorkspaceFiles.map((file) => {
const isAccepted =
!acceptedTypes || acceptedTypes === '*' || isFileTypeAccepted(file.type, acceptedTypes)
return {
label: file.name,
value: file.id,
disabled: !isAccepted,
}
}),
...availableWorkspaceFiles.map((file) => ({
label: file.name,
value: file.id,
})),
],
[availableWorkspaceFiles, acceptedTypes]
[availableWorkspaceFiles]
)
const handleComboboxChange = (value: string) => {
setInputValue(value)
const selectedFile = availableWorkspaceFiles.find((file) => file.id === value)
const isAcceptedType =
selectedFile &&
(!acceptedTypes ||
acceptedTypes === '*' ||
isFileTypeAccepted(selectedFile.type, acceptedTypes))
const isValidOption = value === '__upload_new__' || isAcceptedType
const isValidOption =
value === '__upload_new__' || availableWorkspaceFiles.some((file) => file.id === value)
if (!isValidOption) {
return

View File

@@ -28,7 +28,6 @@ interface Field {
name: string
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files'
value?: string
description?: string
collapsed?: boolean
}
@@ -42,9 +41,7 @@ interface FieldFormatProps {
placeholder?: string
showType?: boolean
showValue?: boolean
showDescription?: boolean
valuePlaceholder?: string
descriptionPlaceholder?: string
config?: any
}
@@ -76,7 +73,6 @@ const createDefaultField = (): Field => ({
name: '',
type: 'string',
value: '',
description: '',
collapsed: false,
})
@@ -97,9 +93,7 @@ export function FieldFormat({
placeholder = 'fieldName',
showType = true,
showValue = false,
showDescription = false,
valuePlaceholder = 'Enter default value',
descriptionPlaceholder = 'Describe this field',
}: FieldFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
@@ -560,18 +554,6 @@ export function FieldFormat({
</div>
)}
{showDescription && (
<div className='flex flex-col gap-[6px]'>
<Label className='text-[13px]'>Description</Label>
<Input
value={field.description ?? ''}
onChange={(e) => updateField(field.id, 'description', e.target.value)}
placeholder={descriptionPlaceholder}
disabled={isReadOnly}
/>
</div>
)}
{showValue && (
<div className='flex flex-col gap-[6px]'>
<Label className='text-[13px]'>Value</Label>
@@ -586,10 +568,8 @@ export function FieldFormat({
)
}
export function InputFormat(
props: Omit<FieldFormatProps, 'title' | 'placeholder' | 'showDescription'>
) {
return <FieldFormat {...props} title='Input' placeholder='firstName' showDescription={true} />
export function InputFormat(props: Omit<FieldFormatProps, 'title' | 'placeholder'>) {
return <FieldFormat {...props} title='Input' placeholder='firstName' />
}
export function ResponseFormat(

View File

@@ -214,42 +214,17 @@ const getOutputTypeForPath = (
outputPath: string,
mergedSubBlocksOverride?: Record<string, any>
): string => {
if (block?.triggerMode && blockConfig?.triggers?.enabled) {
return getBlockOutputType(block.type, outputPath, mergedSubBlocksOverride, true)
}
if (block?.type === 'starter') {
const startWorkflowValue =
mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow')
if (startWorkflowValue === 'chat') {
const chatModeTypes: Record<string, string> = {
input: 'string',
conversationId: 'string',
files: 'files',
}
return chatModeTypes[outputPath] || 'any'
}
const inputFormatValue =
mergedSubBlocksOverride?.inputFormat?.value ?? getSubBlockValue(blockId, 'inputFormat')
if (inputFormatValue && Array.isArray(inputFormatValue)) {
const field = inputFormatValue.find(
(f: { name?: string; type?: string }) => f.name === outputPath
)
if (field?.type) return field.type
}
} else if (blockConfig?.category === 'triggers') {
const blockState = useWorkflowStore.getState().blocks[blockId]
const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {})
return getBlockOutputType(block.type, outputPath, subBlocks)
} else if (blockConfig?.tools?.config?.tool) {
const blockState = useWorkflowStore.getState().blocks[blockId]
const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {})
return getToolOutputType(blockConfig, subBlocks, outputPath)
}
const subBlocks =
mergedSubBlocksOverride ?? useWorkflowStore.getState().blocks[blockId]?.subBlocks
const triggerMode = block?.triggerMode && blockConfig?.triggers?.enabled
if (blockConfig?.tools?.config?.tool) {
const operationValue = getSubBlockValue(blockId, 'operation')
if (operationValue) {
return getToolOutputType(blockConfig, operationValue, outputPath)
}
}
return getBlockOutputType(block?.type ?? '', outputPath, subBlocks, triggerMode)
}
@@ -1214,7 +1189,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
: allTags
}
} else {
const toolOutputPaths = getToolOutputPaths(blockConfig, mergedSubBlocks)
const operationValue =
mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation')
const toolOutputPaths = operationValue
? getToolOutputPaths(blockConfig, operationValue, mergedSubBlocks)
: []
if (toolOutputPaths.length > 0) {
blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`)
@@ -1534,6 +1513,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
if (dynamicOutputs.length > 0) {
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
// For self-reference, only show url and resumeEndpoint (not response format fields)
blockTags = isSelfReference
? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint'))
: allTags
@@ -1541,7 +1521,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
blockTags = [`${normalizedBlockName}.url`, `${normalizedBlockName}.resumeEndpoint`]
}
} else {
const toolOutputPaths = getToolOutputPaths(blockConfig, mergedSubBlocks)
const operationValue =
mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation')
const toolOutputPaths = operationValue
? getToolOutputPaths(blockConfig, operationValue, mergedSubBlocks)
: []
if (toolOutputPaths.length > 0) {
blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`)

View File

@@ -2,15 +2,13 @@
import { useMemo } from 'react'
import { extractFieldsFromSchema } from '@/lib/core/utils/response-format'
import {
getBlockOutputPaths,
getBlockOutputs,
getToolOutputs,
} from '@/lib/workflows/blocks/block-outputs'
import { getBlockOutputPaths, getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers'
import type { SchemaField } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item'
import { getBlock } from '@/blocks'
import type { BlockConfig } from '@/blocks/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getTool } from '@/tools/utils'
const RESERVED_KEYS = new Set(['type', 'description'])
@@ -26,6 +24,64 @@ const getSubBlockValue = (blockId: string, property: string): any => {
return useSubBlockStore.getState().getValue(blockId, property)
}
/**
* Generates output paths for a tool-based block
*/
const generateToolOutputPaths = (blockConfig: BlockConfig, operation: string): string[] => {
if (!blockConfig?.tools?.config?.tool) return []
try {
const toolId = blockConfig.tools.config.tool({ operation })
if (!toolId) return []
const toolConfig = getTool(toolId)
if (!toolConfig?.outputs) return []
return generateOutputPaths(toolConfig.outputs)
} catch {
return []
}
}
/**
* Recursively generates all output paths from an outputs schema
*/
const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[] => {
const paths: string[] = []
for (const [key, value] of Object.entries(outputs)) {
const currentPath = prefix ? `${prefix}.${key}` : key
if (typeof value === 'string') {
paths.push(currentPath)
} else if (typeof value === 'object' && value !== null) {
if ('type' in value && typeof value.type === 'string') {
paths.push(currentPath)
// Handle nested objects and arrays
if (value.type === 'object' && value.properties) {
paths.push(...generateOutputPaths(value.properties, currentPath))
} else if (value.type === 'array' && value.items?.properties) {
paths.push(...generateOutputPaths(value.items.properties, currentPath))
} else if (
value.type === 'array' &&
value.items &&
typeof value.items === 'object' &&
!('type' in value.items)
) {
paths.push(...generateOutputPaths(value.items, currentPath))
}
} else {
const subPaths = generateOutputPaths(value, currentPath)
paths.push(...subPaths)
}
} else {
paths.push(currentPath)
}
}
return paths
}
/**
* Extracts nested fields from array or object properties
*/
@@ -99,6 +155,26 @@ const createFieldFromOutput = (
return field
}
/**
* Gets tool outputs for a block's operation
*/
const getToolOutputs = (
blockConfig: BlockConfig | null,
operation?: string
): Record<string, any> => {
if (!blockConfig?.tools?.config?.tool || !operation) return {}
try {
const toolId = blockConfig.tools.config.tool({ operation })
if (!toolId) return {}
const toolConfig = getTool(toolId)
return toolConfig?.outputs || {}
} catch {
return {}
}
}
interface UseBlockOutputFieldsParams {
blockId: string
blockType: string
@@ -223,11 +299,14 @@ export function useBlockOutputFields({
baseOutputs = getBlockOutputs(blockType, mergedSubBlocks)
} else {
// For tool-based blocks, try to get tool outputs first
const toolOutputs = blockConfig ? getToolOutputs(blockConfig, mergedSubBlocks) : {}
const operationValue =
operation ?? mergedSubBlocks?.operation?.value ?? getSubBlockValue(blockId, 'operation')
const toolOutputs = operationValue ? getToolOutputs(blockConfig, operationValue) : {}
if (Object.keys(toolOutputs).length > 0) {
baseOutputs = toolOutputs
} else {
// Use getBlockOutputs which handles inputFormat merging
baseOutputs = getBlockOutputs(blockType, mergedSubBlocks, triggerMode)
}
}

View File

@@ -107,26 +107,14 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
condition: { field: 'operation', value: 'a2a_send_message' },
},
{
id: 'fileUpload',
id: 'files',
title: 'Files',
type: 'file-upload',
canonicalParamId: 'files',
placeholder: 'Upload files to send',
description: 'Files to include with the message (FilePart)',
condition: { field: 'operation', value: 'a2a_send_message' },
mode: 'basic',
multiple: true,
},
{
id: 'fileReference',
title: 'Files',
type: 'short-input',
canonicalParamId: 'files',
placeholder: 'Reference files from previous blocks',
description: 'Files to include with the message (FilePart)',
condition: { field: 'operation', value: 'a2a_send_message' },
mode: 'advanced',
},
{
id: 'taskId',
title: 'Task ID',
@@ -245,14 +233,6 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
type: 'array',
description: 'Files to include with the message',
},
fileUpload: {
type: 'array',
description: 'Uploaded files (basic mode)',
},
fileReference: {
type: 'json',
description: 'File reference from previous blocks (advanced mode)',
},
historyLength: {
type: 'number',
description: 'Number of history messages to include',

View File

@@ -5,9 +5,8 @@ import type { ConfluenceResponse } from '@/tools/confluence/types'
export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
type: 'confluence',
name: 'Confluence (Legacy)',
name: 'Confluence',
description: 'Interact with Confluence',
hideFromToolbar: true,
authMode: AuthMode.OAuth,
longDescription:
'Integrate Confluence into the workflow. Can read, create, update, delete pages, manage comments, attachments, labels, and search content.',
@@ -358,342 +357,3 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
status: { type: 'string', description: 'Space status' },
},
}
export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
...ConfluenceBlock,
type: 'confluence_v2',
name: 'Confluence',
hideFromToolbar: false,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Read Page', id: 'read' },
{ label: 'Create Page', id: 'create' },
{ label: 'Update Page', id: 'update' },
{ label: 'Delete Page', id: 'delete' },
{ label: 'Search Content', id: 'search' },
{ label: 'Create Comment', id: 'create_comment' },
{ label: 'List Comments', id: 'list_comments' },
{ label: 'Update Comment', id: 'update_comment' },
{ label: 'Delete Comment', id: 'delete_comment' },
{ label: 'Upload Attachment', id: 'upload_attachment' },
{ label: 'List Attachments', id: 'list_attachments' },
{ label: 'Delete Attachment', id: 'delete_attachment' },
{ label: 'List Labels', id: 'list_labels' },
{ label: 'Get Space', id: 'get_space' },
{ label: 'List Spaces', id: 'list_spaces' },
],
value: () => 'read',
},
{
id: 'domain',
title: 'Domain',
type: 'short-input',
placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)',
required: true,
},
{
id: 'credential',
title: 'Confluence Account',
type: 'oauth-input',
serviceId: 'confluence',
requiredScopes: [
'read:confluence-content.all',
'read:confluence-space.summary',
'read:space:confluence',
'read:space-details:confluence',
'write:confluence-content',
'write:confluence-space',
'write:confluence-file',
'read:content:confluence',
'read:page:confluence',
'write:page:confluence',
'read:comment:confluence',
'write:comment:confluence',
'delete:comment:confluence',
'read:attachment:confluence',
'write:attachment:confluence',
'delete:attachment:confluence',
'delete:page:confluence',
'read:label:confluence',
'write:label:confluence',
'search:confluence',
'read:me',
'offline_access',
],
placeholder: 'Select Confluence account',
required: true,
},
{
id: 'pageId',
title: 'Select Page',
type: 'file-selector',
canonicalParamId: 'pageId',
serviceId: 'confluence',
placeholder: 'Select Confluence page',
dependsOn: ['credential', 'domain'],
mode: 'basic',
},
{
id: 'manualPageId',
title: 'Page ID',
type: 'short-input',
canonicalParamId: 'pageId',
placeholder: 'Enter Confluence page ID',
mode: 'advanced',
},
{
id: 'spaceId',
title: 'Space ID',
type: 'short-input',
placeholder: 'Enter Confluence space ID',
required: true,
condition: { field: 'operation', value: ['create', 'get_space'] },
},
{
id: 'title',
title: 'Title',
type: 'short-input',
placeholder: 'Enter title for the page',
condition: { field: 'operation', value: ['create', 'update'] },
},
{
id: 'content',
title: 'Content',
type: 'long-input',
placeholder: 'Enter content for the page',
condition: { field: 'operation', value: ['create', 'update'] },
},
{
id: 'parentId',
title: 'Parent Page ID',
type: 'short-input',
placeholder: 'Enter parent page ID (optional)',
condition: { field: 'operation', value: 'create' },
},
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'Enter search query',
required: true,
condition: { field: 'operation', value: 'search' },
},
{
id: 'comment',
title: 'Comment Text',
type: 'long-input',
placeholder: 'Enter comment text',
required: true,
condition: { field: 'operation', value: ['create_comment', 'update_comment'] },
},
{
id: 'commentId',
title: 'Comment ID',
type: 'short-input',
placeholder: 'Enter comment ID',
required: true,
condition: { field: 'operation', value: ['update_comment', 'delete_comment'] },
},
{
id: 'attachmentId',
title: 'Attachment ID',
type: 'short-input',
placeholder: 'Enter attachment ID',
required: true,
condition: { field: 'operation', value: 'delete_attachment' },
},
{
id: 'attachmentFileUpload',
title: 'File',
type: 'file-upload',
canonicalParamId: 'attachmentFile',
placeholder: 'Select file to upload',
condition: { field: 'operation', value: 'upload_attachment' },
mode: 'basic',
},
{
id: 'attachmentFileReference',
title: 'File',
type: 'short-input',
canonicalParamId: 'attachmentFile',
placeholder: 'Reference file from previous blocks',
condition: { field: 'operation', value: 'upload_attachment' },
mode: 'advanced',
},
{
id: 'attachmentFileName',
title: 'File Name',
type: 'short-input',
placeholder: 'Optional custom file name',
condition: { field: 'operation', value: 'upload_attachment' },
},
{
id: 'attachmentComment',
title: 'Comment',
type: 'short-input',
placeholder: 'Optional comment for the attachment',
condition: { field: 'operation', value: 'upload_attachment' },
},
{
id: 'labelName',
title: 'Label Name',
type: 'short-input',
placeholder: 'Enter label name',
required: true,
condition: { field: 'operation', value: ['add_label', 'remove_label'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Enter maximum number of results (default: 25)',
condition: {
field: 'operation',
value: ['search', 'list_comments', 'list_attachments', 'list_spaces'],
},
},
],
tools: {
access: [
'confluence_retrieve',
'confluence_update',
'confluence_create_page',
'confluence_delete_page',
'confluence_search',
'confluence_create_comment',
'confluence_list_comments',
'confluence_update_comment',
'confluence_delete_comment',
'confluence_upload_attachment',
'confluence_list_attachments',
'confluence_delete_attachment',
'confluence_list_labels',
'confluence_get_space',
'confluence_list_spaces',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'read':
return 'confluence_retrieve'
case 'create':
return 'confluence_create_page'
case 'update':
return 'confluence_update'
case 'delete':
return 'confluence_delete_page'
case 'search':
return 'confluence_search'
case 'create_comment':
return 'confluence_create_comment'
case 'list_comments':
return 'confluence_list_comments'
case 'update_comment':
return 'confluence_update_comment'
case 'delete_comment':
return 'confluence_delete_comment'
case 'upload_attachment':
return 'confluence_upload_attachment'
case 'list_attachments':
return 'confluence_list_attachments'
case 'delete_attachment':
return 'confluence_delete_attachment'
case 'list_labels':
return 'confluence_list_labels'
case 'get_space':
return 'confluence_get_space'
case 'list_spaces':
return 'confluence_list_spaces'
default:
return 'confluence_retrieve'
}
},
params: (params) => {
const {
credential,
pageId,
manualPageId,
operation,
attachmentFileUpload,
attachmentFileReference,
attachmentFile,
attachmentFileName,
attachmentComment,
...rest
} = params
const effectivePageId = (pageId || manualPageId || '').trim()
const requiresPageId = [
'read',
'update',
'delete',
'create_comment',
'list_comments',
'list_attachments',
'list_labels',
'upload_attachment',
]
const requiresSpaceId = ['create', 'get_space']
if (requiresPageId.includes(operation) && !effectivePageId) {
throw new Error('Page ID is required. Please select a page or enter a page ID manually.')
}
if (requiresSpaceId.includes(operation) && !rest.spaceId) {
throw new Error('Space ID is required for this operation.')
}
if (operation === 'upload_attachment') {
const fileInput = attachmentFileUpload || attachmentFileReference || attachmentFile
if (!fileInput) {
throw new Error('File is required for upload attachment operation.')
}
return {
credential,
pageId: effectivePageId,
operation,
file: fileInput,
fileName: attachmentFileName,
comment: attachmentComment,
...rest,
}
}
return {
credential,
pageId: effectivePageId || undefined,
operation,
...rest,
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
domain: { type: 'string', description: 'Confluence domain' },
credential: { type: 'string', description: 'Confluence access token' },
pageId: { type: 'string', description: 'Page identifier' },
manualPageId: { type: 'string', description: 'Manual page identifier' },
spaceId: { type: 'string', description: 'Space identifier' },
title: { type: 'string', description: 'Page title' },
content: { type: 'string', description: 'Page content' },
parentId: { type: 'string', description: 'Parent page identifier' },
query: { type: 'string', description: 'Search query' },
comment: { type: 'string', description: 'Comment text' },
commentId: { type: 'string', description: 'Comment identifier' },
attachmentId: { type: 'string', description: 'Attachment identifier' },
attachmentFile: { type: 'json', description: 'File to upload as attachment' },
attachmentFileUpload: { type: 'json', description: 'Uploaded file (basic mode)' },
attachmentFileReference: { type: 'json', description: 'File reference (advanced mode)' },
attachmentFileName: { type: 'string', description: 'Custom file name for attachment' },
attachmentComment: { type: 'string', description: 'Comment for the attachment' },
labelName: { type: 'string', description: 'Label name' },
limit: { type: 'number', description: 'Maximum number of results' },
},
}

View File

@@ -1,14 +1,13 @@
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'
const logger = createLogger('FileBlock')
export const FileBlock: BlockConfig<FileParserOutput> = {
type: 'file',
name: 'File (Legacy)',
name: 'File',
description: 'Read and parse multiple files',
longDescription: `Integrate File into the workflow. Can upload a file manually or insert a file url.`,
bestPractices: `
@@ -18,7 +17,6 @@ export const FileBlock: BlockConfig<FileParserOutput> = {
category: 'tools',
bgColor: '#40916C',
icon: DocumentIcon,
hideFromToolbar: true,
subBlocks: [
{
id: 'inputMethod',
@@ -129,92 +127,3 @@ export const FileBlock: BlockConfig<FileParserOutput> = {
},
},
}
export const FileV2Block: BlockConfig<FileParserOutput> = {
...FileBlock,
type: 'file_v2',
name: 'File',
description: 'Read and parse multiple files',
hideFromToolbar: false,
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,
},
{
id: 'filePath',
title: 'Files',
type: 'short-input' as SubBlockType,
canonicalParamId: 'fileInput',
placeholder: 'File URL',
mode: 'advanced',
},
],
tools: {
access: ['file_parser_v2'],
config: {
tool: createVersionedToolSelector({
baseToolSelector: () => 'file_parser',
suffix: '_v2',
fallbackToolId: 'file_parser_v2',
}),
params: (params) => {
const fileInput = params.file || params.filePath || params.fileInput
if (!fileInput) {
logger.error('No file input provided')
throw new Error('File is required')
}
if (typeof fileInput === 'string') {
return {
filePath: fileInput.trim(),
fileType: params.fileType || 'auto',
workspaceId: params._context?.workspaceId,
}
}
if (Array.isArray(fileInput) && fileInput.length > 0) {
const filePaths = fileInput.map((file) => file.path)
return {
filePath: filePaths.length === 1 ? filePaths[0] : filePaths,
fileType: params.fileType || 'auto',
}
}
if (fileInput?.path) {
return {
filePath: fileInput.path,
fileType: params.fileType || 'auto',
}
}
logger.error('Invalid file input format')
throw new Error('Invalid file input')
},
},
},
inputs: {
fileInput: { type: 'json', description: 'File input (upload or URL reference)' },
filePath: { type: 'string', description: 'File URL (advanced mode)' },
file: { type: 'json', description: 'Uploaded file data (basic mode)' },
fileType: { type: 'string', description: 'File type' },
},
outputs: {
files: {
type: 'json',
description: 'Array of parsed file objects with content, metadata, and file properties',
},
combinedContent: {
type: 'string',
description: 'All file contents merged into a single text string',
},
},
}

View File

@@ -1,13 +1,11 @@
import { MistralIcon } from '@/components/icons'
import { AuthMode, type BlockConfig, type SubBlockType } from '@/blocks/types'
import { createVersionedToolSelector } from '@/blocks/utils'
import type { MistralParserOutput } from '@/tools/mistral/types'
export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
type: 'mistral_parse',
name: 'Mistral Parser (Legacy)',
name: 'Mistral Parser',
description: 'Extract text from PDF documents',
hideFromToolbar: true,
authMode: AuthMode.ApiKey,
longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.`,
docsLink: 'https://docs.sim.ai/tools/mistral_parse',
@@ -15,6 +13,7 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
bgColor: '#000000',
icon: MistralIcon,
subBlocks: [
// Show input method selection
{
id: 'inputMethod',
title: 'Select Input Method',
@@ -24,6 +23,8 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
{ id: 'upload', label: 'Upload PDF Document' },
],
},
// URL input - conditional on inputMethod
{
id: 'filePath',
title: 'PDF Document URL',
@@ -34,6 +35,8 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
value: 'url',
},
},
// File upload option
{
id: 'fileUpload',
title: 'Upload PDF',
@@ -43,8 +46,9 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
field: 'inputMethod',
value: 'upload',
},
maxSize: 50,
maxSize: 50, // 50MB max via direct upload
},
{
id: 'resultType',
title: 'Output Format',
@@ -61,6 +65,28 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
type: 'short-input',
placeholder: 'e.g. 0,1,2 (leave empty for all pages)',
},
/*
* Image-related parameters - temporarily disabled
* Uncomment if PDF image extraction is needed
*
{
id: 'includeImageBase64',
title: 'Include PDF Images',
type: 'switch',
},
{
id: 'imageLimit',
title: 'Max Images',
type: 'short-input',
placeholder: 'Maximum number of images to extract',
},
{
id: 'imageMinSize',
title: 'Min Image Size (px)',
type: 'short-input',
placeholder: 'Min width/height in pixels',
},
*/
{
id: 'apiKey',
title: 'API Key',
@@ -75,15 +101,18 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
config: {
tool: () => 'mistral_parser',
params: (params) => {
// Basic validation
if (!params || !params.apiKey || params.apiKey.trim() === '') {
throw new Error('Mistral API key is required')
}
const parameters: Record<string, unknown> = {
// Build parameters object - file processing is now handled at the tool level
const parameters: any = {
apiKey: params.apiKey.trim(),
resultType: params.resultType || 'markdown',
}
// Set filePath or fileUpload based on input method
const inputMethod = params.inputMethod || 'url'
if (inputMethod === 'url') {
if (!params.filePath || params.filePath.trim() === '') {
@@ -94,9 +123,11 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
if (!params.fileUpload) {
throw new Error('Please upload a PDF document')
}
// Pass the entire fileUpload object to the tool
parameters.fileUpload = params.fileUpload
}
// Convert pages input from string to array of numbers if provided
let pagesArray: number[] | undefined
if (params.pages && params.pages.trim() !== '') {
try {
@@ -115,12 +146,12 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
if (pagesArray && pagesArray.length === 0) {
pagesArray = undefined
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
throw new Error(`Page number format error: ${errorMessage}`)
} catch (error: any) {
throw new Error(`Page number format error: ${error.message}`)
}
}
// Add optional parameters
if (pagesArray && pagesArray.length > 0) {
parameters.pages = pagesArray
}
@@ -142,129 +173,3 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
metadata: { type: 'json', description: 'Processing metadata' },
},
}
export const MistralParseV2Block: BlockConfig<MistralParserOutput> = {
...MistralParseBlock,
type: 'mistral_parse_v2',
name: 'Mistral Parser',
description: 'Extract text from PDF documents',
hideFromToolbar: false,
subBlocks: [
{
id: 'fileUpload',
title: 'PDF Document',
type: 'file-upload' as SubBlockType,
canonicalParamId: 'document',
acceptedTypes: 'application/pdf',
placeholder: 'Upload a PDF document',
mode: 'basic',
maxSize: 50,
},
{
id: 'filePath',
title: 'PDF Document',
type: 'short-input' as SubBlockType,
canonicalParamId: 'document',
placeholder: 'Document URL',
mode: 'advanced',
},
{
id: 'resultType',
title: 'Output Format',
type: 'dropdown',
options: [
{ id: 'markdown', label: 'Markdown' },
{ id: 'text', label: 'Plain Text' },
{ id: 'json', label: 'JSON' },
],
},
{
id: 'pages',
title: 'Specific Pages',
type: 'short-input',
placeholder: 'e.g. 0,1,2 (leave empty for all pages)',
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input' as SubBlockType,
placeholder: 'Enter your Mistral API key',
password: true,
required: true,
},
],
tools: {
access: ['mistral_parser_v2'],
config: {
tool: createVersionedToolSelector({
baseToolSelector: () => 'mistral_parser',
suffix: '_v2',
fallbackToolId: 'mistral_parser_v2',
}),
params: (params) => {
if (!params || !params.apiKey || params.apiKey.trim() === '') {
throw new Error('Mistral API key is required')
}
const parameters: Record<string, unknown> = {
apiKey: params.apiKey.trim(),
resultType: params.resultType || 'markdown',
}
const documentInput = params.fileUpload || params.filePath || params.document
if (!documentInput) {
throw new Error('PDF document is required')
}
if (typeof documentInput === 'object') {
parameters.fileUpload = documentInput
} else if (typeof documentInput === 'string') {
parameters.filePath = documentInput.trim()
}
let pagesArray: number[] | undefined
if (params.pages && params.pages.trim() !== '') {
try {
pagesArray = params.pages
.split(',')
.map((p: string) => p.trim())
.filter((p: string) => p.length > 0)
.map((p: string) => {
const num = Number.parseInt(p, 10)
if (Number.isNaN(num) || num < 0) {
throw new Error(`Invalid page number: ${p}`)
}
return num
})
if (pagesArray && pagesArray.length === 0) {
pagesArray = undefined
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
throw new Error(`Page number format error: ${errorMessage}`)
}
}
if (pagesArray && pagesArray.length > 0) {
parameters.pages = pagesArray
}
return parameters
},
},
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'PDF document URL (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' },
pages: { type: 'string', description: 'Page selection' },
},
outputs: {
pages: { type: 'array', description: 'Array of page objects from Mistral OCR' },
model: { type: 'string', description: 'Mistral OCR model identifier' },
usage_info: { type: 'json', description: 'Usage statistics from the API' },
document_annotation: { type: 'string', description: 'Structured annotation data' },
},
}

View File

@@ -15,22 +15,34 @@ export const PulseBlock: BlockConfig<PulseParserOutput> = {
icon: PulseIcon,
subBlocks: [
{
id: 'fileUpload',
title: 'Document',
type: 'file-upload' as SubBlockType,
canonicalParamId: 'document',
acceptedTypes: 'application/pdf,image/*,.docx,.pptx,.xlsx',
placeholder: 'Upload a document',
mode: 'basic',
maxSize: 50,
id: 'inputMethod',
title: 'Select Input Method',
type: 'dropdown' as SubBlockType,
options: [
{ id: 'url', label: 'Document URL' },
{ id: 'upload', label: 'Upload Document' },
],
},
{
id: 'filePath',
title: 'Document',
title: 'Document URL',
type: 'short-input' as SubBlockType,
canonicalParamId: 'document',
placeholder: 'Document URL',
mode: 'advanced',
placeholder: 'Enter full URL to a document (https://example.com/document.pdf)',
condition: {
field: 'inputMethod',
value: 'url',
},
},
{
id: 'fileUpload',
title: 'Upload Document',
type: 'file-upload' as SubBlockType,
acceptedTypes: 'application/pdf,image/*,.docx,.pptx,.xlsx',
condition: {
field: 'inputMethod',
value: 'upload',
},
maxSize: 50,
},
{
id: 'pages',
@@ -72,14 +84,17 @@ export const PulseBlock: BlockConfig<PulseParserOutput> = {
apiKey: params.apiKey.trim(),
}
const documentInput = params.fileUpload || params.filePath || params.document
if (!documentInput) {
throw new Error('Document is required')
}
if (typeof documentInput === 'object') {
parameters.fileUpload = documentInput
} else if (typeof documentInput === 'string') {
parameters.filePath = documentInput.trim()
const inputMethod = params.inputMethod || 'url'
if (inputMethod === 'url') {
if (!params.filePath || params.filePath.trim() === '') {
throw new Error('Document URL is required')
}
parameters.filePath = params.filePath.trim()
} else if (inputMethod === 'upload') {
if (!params.fileUpload) {
throw new Error('Please upload a document')
}
parameters.fileUpload = params.fileUpload
}
if (params.pages && params.pages.trim() !== '') {
@@ -102,9 +117,9 @@ export const PulseBlock: BlockConfig<PulseParserOutput> = {
},
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'Document URL (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded document file (basic mode)' },
inputMethod: { type: 'string', description: 'Input method selection' },
filePath: { type: 'string', description: 'Document URL' },
fileUpload: { type: 'json', description: 'Uploaded document file' },
apiKey: { type: 'string', description: 'Pulse API key' },
pages: { type: 'string', description: 'Page range selection' },
chunking: {

View File

@@ -14,22 +14,34 @@ export const ReductoBlock: BlockConfig<ReductoParserOutput> = {
icon: ReductoIcon,
subBlocks: [
{
id: 'fileUpload',
title: 'PDF Document',
type: 'file-upload' as SubBlockType,
canonicalParamId: 'document',
acceptedTypes: 'application/pdf',
placeholder: 'Upload a PDF document',
mode: 'basic',
maxSize: 50,
id: 'inputMethod',
title: 'Select Input Method',
type: 'dropdown' as SubBlockType,
options: [
{ id: 'url', label: 'PDF Document URL' },
{ id: 'upload', label: 'Upload PDF Document' },
],
},
{
id: 'filePath',
title: 'PDF Document',
title: 'PDF Document URL',
type: 'short-input' as SubBlockType,
canonicalParamId: 'document',
placeholder: 'Document URL',
mode: 'advanced',
placeholder: 'Enter full URL to a PDF document (https://example.com/document.pdf)',
condition: {
field: 'inputMethod',
value: 'url',
},
},
{
id: 'fileUpload',
title: 'Upload PDF',
type: 'file-upload' as SubBlockType,
acceptedTypes: 'application/pdf',
condition: {
field: 'inputMethod',
value: 'upload',
},
maxSize: 50,
},
{
id: 'pages',
@@ -68,15 +80,17 @@ export const ReductoBlock: BlockConfig<ReductoParserOutput> = {
apiKey: params.apiKey.trim(),
}
const documentInput = params.fileUpload || params.filePath || params.document
if (!documentInput) {
throw new Error('PDF document is required')
}
if (typeof documentInput === 'object') {
parameters.fileUpload = documentInput
} else if (typeof documentInput === 'string') {
parameters.filePath = documentInput.trim()
const inputMethod = params.inputMethod || 'url'
if (inputMethod === 'url') {
if (!params.filePath || params.filePath.trim() === '') {
throw new Error('PDF Document URL is required')
}
parameters.filePath = params.filePath.trim()
} else if (inputMethod === 'upload') {
if (!params.fileUpload) {
throw new Error('Please upload a PDF document')
}
parameters.fileUpload = params.fileUpload
}
let pagesArray: number[] | undefined
@@ -116,9 +130,9 @@ export const ReductoBlock: BlockConfig<ReductoParserOutput> = {
},
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'PDF document URL (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded PDF file (basic mode)' },
inputMethod: { type: 'string', description: 'Input method selection' },
filePath: { type: 'string', description: 'PDF document URL' },
fileUpload: { type: 'json', description: 'Uploaded PDF file' },
apiKey: { type: 'string', description: 'Reducto API key' },
pages: { type: 'string', description: 'Page selection' },
tableOutputFormat: { type: 'string', description: 'Table output format' },

View File

@@ -414,10 +414,6 @@ export const S3Block: BlockConfig<S3Response> = {
},
outputs: {
url: { type: 'string', description: 'URL of S3 object' },
uri: {
type: 'string',
description: 'S3 URI (s3://bucket/key) for use with other AWS services',
},
objects: { type: 'json', description: 'List of objects (for list operation)' },
deleted: { type: 'boolean', description: 'Deletion status' },
metadata: { type: 'json', description: 'Operation metadata' },

View File

@@ -1,191 +0,0 @@
import { TextractIcon } from '@/components/icons'
import { AuthMode, type BlockConfig, type SubBlockType } from '@/blocks/types'
import type { TextractParserOutput } from '@/tools/textract/types'
export const TextractBlock: BlockConfig<TextractParserOutput> = {
type: 'textract',
name: 'AWS Textract',
description: 'Extract text, tables, and forms from documents',
authMode: AuthMode.ApiKey,
longDescription: `Integrate AWS Textract into your workflow to extract text, tables, forms, and key-value pairs from documents. Single-page mode supports JPEG, PNG, and single-page PDF. Multi-page mode supports multi-page PDF and TIFF.`,
docsLink: 'https://docs.sim.ai/tools/textract',
category: 'tools',
bgColor: 'linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)',
icon: TextractIcon,
subBlocks: [
{
id: 'processingMode',
title: 'Processing Mode',
type: 'dropdown' as SubBlockType,
options: [
{ id: 'sync', label: 'Single Page (JPEG, PNG, 1-page PDF)' },
{ id: 'async', label: 'Multi-Page (PDF, TIFF via S3)' },
],
tooltip:
'Single Page uses synchronous API for JPEG, PNG, or single-page PDF. Multi-Page uses async API for multi-page PDF/TIFF stored in S3.',
},
{
id: 'fileUpload',
title: 'Document',
type: 'file-upload' as SubBlockType,
canonicalParamId: 'document',
acceptedTypes: 'image/jpeg,image/png,application/pdf',
placeholder: 'Upload JPEG, PNG, or single-page PDF (max 10MB)',
condition: {
field: 'processingMode',
value: 'async',
not: true,
},
mode: 'basic',
maxSize: 10,
},
{
id: 'filePath',
title: 'Document',
type: 'short-input' as SubBlockType,
canonicalParamId: 'document',
placeholder: 'URL to JPEG, PNG, or single-page PDF',
condition: {
field: 'processingMode',
value: 'async',
not: true,
},
mode: 'advanced',
},
{
id: 's3Uri',
title: 'S3 URI',
type: 'short-input' as SubBlockType,
placeholder: 's3://bucket-name/path/to/document.pdf',
condition: {
field: 'processingMode',
value: 'async',
},
},
{
id: 'region',
title: 'AWS Region',
type: 'short-input' as SubBlockType,
placeholder: 'e.g., us-east-1',
required: true,
},
{
id: 'accessKeyId',
title: 'AWS Access Key ID',
type: 'short-input' as SubBlockType,
placeholder: 'Enter your AWS Access Key ID',
password: true,
required: true,
},
{
id: 'secretAccessKey',
title: 'AWS Secret Access Key',
type: 'short-input' as SubBlockType,
placeholder: 'Enter your AWS Secret Access Key',
password: true,
required: true,
},
{
id: 'extractTables',
title: 'Extract Tables',
type: 'switch' as SubBlockType,
},
{
id: 'extractForms',
title: 'Extract Forms (Key-Value Pairs)',
type: 'switch' as SubBlockType,
},
{
id: 'detectSignatures',
title: 'Detect Signatures',
type: 'switch' as SubBlockType,
},
{
id: 'analyzeLayout',
title: 'Analyze Document Layout',
type: 'switch' as SubBlockType,
},
],
tools: {
access: ['textract_parser'],
config: {
tool: () => 'textract_parser',
params: (params) => {
if (!params.accessKeyId || params.accessKeyId.trim() === '') {
throw new Error('AWS Access Key ID is required')
}
if (!params.secretAccessKey || params.secretAccessKey.trim() === '') {
throw new Error('AWS Secret Access Key is required')
}
if (!params.region || params.region.trim() === '') {
throw new Error('AWS Region is required')
}
const processingMode = params.processingMode || 'sync'
const parameters: Record<string, unknown> = {
accessKeyId: params.accessKeyId.trim(),
secretAccessKey: params.secretAccessKey.trim(),
region: params.region.trim(),
processingMode,
}
if (processingMode === 'async') {
if (!params.s3Uri || params.s3Uri.trim() === '') {
throw new Error('S3 URI is required for multi-page processing')
}
parameters.s3Uri = params.s3Uri.trim()
} else {
const documentInput = params.fileUpload || params.filePath || params.document
if (!documentInput) {
throw new Error('Document is required')
}
if (typeof documentInput === 'object') {
parameters.fileUpload = documentInput
} else if (typeof documentInput === 'string') {
parameters.filePath = documentInput.trim()
}
}
const featureTypes: string[] = []
if (params.extractTables) featureTypes.push('TABLES')
if (params.extractForms) featureTypes.push('FORMS')
if (params.detectSignatures) featureTypes.push('SIGNATURES')
if (params.analyzeLayout) featureTypes.push('LAYOUT')
if (featureTypes.length > 0) {
parameters.featureTypes = featureTypes
}
return parameters
},
},
},
inputs: {
processingMode: { type: 'string', description: 'Document type: single-page or multi-page' },
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'Document URL (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded document file (basic mode)' },
s3Uri: { type: 'string', description: 'S3 URI for multi-page processing (s3://bucket/key)' },
extractTables: { type: 'boolean', description: 'Extract tables from document' },
extractForms: { type: 'boolean', description: 'Extract form key-value pairs' },
detectSignatures: { type: 'boolean', description: 'Detect signatures' },
analyzeLayout: { type: 'boolean', description: 'Analyze document layout' },
region: { type: 'string', description: 'AWS region' },
accessKeyId: { type: 'string', description: 'AWS Access Key ID' },
secretAccessKey: { type: 'string', description: 'AWS Secret Access Key' },
},
outputs: {
blocks: {
type: 'json',
description: 'Array of detected blocks (PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.)',
},
documentMetadata: {
type: 'json',
description: 'Document metadata containing pages count',
},
modelVersion: {
type: 'string',
description: 'Version of the Textract model used for processing',
},
},
}

View File

@@ -4,9 +4,8 @@ import type { VideoBlockResponse } from '@/tools/video/types'
export const VideoGeneratorBlock: BlockConfig<VideoBlockResponse> = {
type: 'video_generator',
name: 'Video Generator (Legacy)',
name: 'Video Generator',
description: 'Generate videos from text using AI',
hideFromToolbar: true,
authMode: AuthMode.ApiKey,
longDescription:
'Generate high-quality videos from text prompts using leading AI providers. Supports multiple models, aspect ratios, resolutions, and provider-specific features like world consistency, camera controls, and audio generation.',
@@ -428,378 +427,3 @@ export const VideoGeneratorBlock: BlockConfig<VideoBlockResponse> = {
model: { type: 'string', description: 'Model used' },
},
}
export const VideoGeneratorV2Block: BlockConfig<VideoBlockResponse> = {
...VideoGeneratorBlock,
type: 'video_generator_v2',
name: 'Video Generator',
hideFromToolbar: false,
subBlocks: [
{
id: 'provider',
title: 'Provider',
type: 'dropdown',
options: [
{ label: 'Runway Gen-4', id: 'runway' },
{ label: 'Google Veo 3', id: 'veo' },
{ label: 'Luma Dream Machine', id: 'luma' },
{ label: 'MiniMax Hailuo', id: 'minimax' },
{ label: 'Fal.ai (Multi-Model)', id: 'falai' },
],
value: () => 'runway',
required: true,
},
{
id: 'model',
title: 'Model',
type: 'dropdown',
condition: { field: 'provider', value: 'veo' },
options: [
{ label: 'Veo 3', id: 'veo-3' },
{ label: 'Veo 3 Fast', id: 'veo-3-fast' },
{ label: 'Veo 3.1', id: 'veo-3.1' },
],
value: () => 'veo-3',
required: false,
},
{
id: 'model',
title: 'Model',
type: 'dropdown',
condition: { field: 'provider', value: 'luma' },
options: [{ label: 'Ray 2', id: 'ray-2' }],
value: () => 'ray-2',
required: false,
},
{
id: 'model',
title: 'Model',
type: 'dropdown',
condition: { field: 'provider', value: 'minimax' },
options: [{ label: 'Hailuo 2.3', id: 'hailuo-02' }],
value: () => 'hailuo-02',
required: false,
},
{
id: 'endpoint',
title: 'Quality Endpoint',
type: 'dropdown',
condition: { field: 'provider', value: 'minimax' },
options: [
{ label: 'Pro', id: 'pro' },
{ label: 'Standard', id: 'standard' },
],
value: () => 'standard',
required: false,
},
{
id: 'model',
title: 'Model',
type: 'dropdown',
condition: { field: 'provider', value: 'falai' },
options: [
{ label: 'Google Veo 3.1', id: 'veo-3.1' },
{ label: 'OpenAI Sora 2', id: 'sora-2' },
{ label: 'Kling 2.5 Turbo Pro', id: 'kling-2.5-turbo-pro' },
{ label: 'Kling 2.1 Pro', id: 'kling-2.1-pro' },
{ label: 'MiniMax Hailuo 2.3 Pro', id: 'minimax-hailuo-2.3-pro' },
{ label: 'MiniMax Hailuo 2.3 Standard', id: 'minimax-hailuo-2.3-standard' },
{ label: 'WAN 2.1', id: 'wan-2.1' },
{ label: 'LTXV 0.9.8', id: 'ltxv-0.9.8' },
],
value: () => 'veo-3.1',
required: true,
},
{
id: 'prompt',
title: 'Prompt',
type: 'long-input',
placeholder: 'Describe the video you want to generate...',
required: true,
},
{
id: 'duration',
title: 'Duration (seconds)',
type: 'dropdown',
condition: { field: 'provider', value: 'runway' },
options: [
{ label: '5', id: '5' },
{ label: '10', id: '10' },
],
value: () => '5',
required: false,
},
{
id: 'duration',
title: 'Duration (seconds)',
type: 'dropdown',
condition: { field: 'provider', value: 'veo' },
options: [
{ label: '4', id: '4' },
{ label: '6', id: '6' },
{ label: '8', id: '8' },
],
value: () => '8',
required: false,
},
{
id: 'duration',
title: 'Duration (seconds)',
type: 'dropdown',
condition: { field: 'provider', value: 'luma' },
options: [
{ label: '5', id: '5' },
{ label: '9', id: '9' },
],
value: () => '5',
required: false,
},
{
id: 'duration',
title: 'Duration (seconds)',
type: 'dropdown',
condition: { field: 'provider', value: 'minimax' },
options: [
{ label: '6', id: '6' },
{ label: '10', id: '10' },
],
value: () => '6',
required: false,
},
{
id: 'duration',
title: 'Duration (seconds)',
type: 'dropdown',
condition: {
field: 'model',
value: [
'kling-2.5-turbo-pro',
'kling-2.1-pro',
'minimax-hailuo-2.3-pro',
'minimax-hailuo-2.3-standard',
],
},
options: [
{ label: '5', id: '5' },
{ label: '8', id: '8' },
{ label: '10', id: '10' },
],
value: () => '5',
required: false,
},
{
id: 'aspectRatio',
title: 'Aspect Ratio',
type: 'dropdown',
condition: { field: 'provider', value: 'veo' },
options: [
{ label: '16:9', id: '16:9' },
{ label: '9:16', id: '9:16' },
],
value: () => '16:9',
required: false,
},
{
id: 'aspectRatio',
title: 'Aspect Ratio',
type: 'dropdown',
condition: { field: 'provider', value: 'runway' },
options: [
{ label: '16:9', id: '16:9' },
{ label: '9:16', id: '9:16' },
{ label: '1:1', id: '1:1' },
],
value: () => '16:9',
required: false,
},
{
id: 'aspectRatio',
title: 'Aspect Ratio',
type: 'dropdown',
condition: { field: 'provider', value: 'luma' },
options: [
{ label: '16:9', id: '16:9' },
{ label: '9:16', id: '9:16' },
{ label: '1:1', id: '1:1' },
],
value: () => '16:9',
required: false,
},
{
id: 'aspectRatio',
title: 'Aspect Ratio',
type: 'dropdown',
condition: {
field: 'model',
value: [
'kling-2.5-turbo-pro',
'kling-2.1-pro',
'minimax-hailuo-2.3-pro',
'minimax-hailuo-2.3-standard',
],
},
options: [
{ label: '16:9', id: '16:9' },
{ label: '9:16', id: '9:16' },
],
value: () => '16:9',
required: false,
},
{
id: 'resolution',
title: 'Resolution',
type: 'dropdown',
condition: { field: 'provider', value: 'veo' },
options: [
{ label: '720p', id: '720p' },
{ label: '1080p', id: '1080p' },
],
value: () => '1080p',
required: false,
},
{
id: 'resolution',
title: 'Resolution',
type: 'dropdown',
condition: { field: 'provider', value: 'luma' },
options: [
{ label: '540p', id: '540p' },
{ label: '720p', id: '720p' },
{ label: '1080p', id: '1080p' },
],
value: () => '1080p',
required: false,
},
{
id: 'visualReferenceUpload',
title: 'Reference Image',
type: 'file-upload',
canonicalParamId: 'visualReference',
condition: { field: 'provider', value: 'runway' },
placeholder: 'Upload reference image',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.jpg,.jpeg,.png,.webp',
},
{
id: 'visualReferenceInput',
title: 'Reference Image',
type: 'short-input',
canonicalParamId: 'visualReference',
condition: { field: 'provider', value: 'runway' },
placeholder: 'Reference image from previous blocks',
mode: 'advanced',
},
{
id: 'cameraControl',
title: 'Camera Controls',
type: 'long-input',
condition: { field: 'provider', value: 'luma' },
placeholder: 'JSON: [{ "key": "pan_right" }, { "key": "zoom_in" }]',
required: false,
},
{
id: 'promptOptimizer',
title: 'Prompt Optimizer',
type: 'switch',
condition: { field: 'provider', value: 'minimax' },
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your provider API key',
password: true,
required: true,
},
],
tools: {
access: ['video_runway', 'video_veo', 'video_luma', 'video_minimax', 'video_falai'],
config: {
tool: (params) => {
switch (params.provider) {
case 'runway':
return 'video_runway'
case 'veo':
return 'video_veo'
case 'luma':
return 'video_luma'
case 'minimax':
return 'video_minimax'
case 'falai':
return 'video_falai'
default:
return 'video_runway'
}
},
params: (params) => {
const visualRef =
params.visualReferenceUpload || params.visualReferenceInput || params.visualReference
return {
provider: params.provider,
apiKey: params.apiKey,
model: params.model,
endpoint: params.endpoint,
prompt: params.prompt,
duration: params.duration ? Number(params.duration) : undefined,
aspectRatio: params.aspectRatio,
resolution: params.resolution,
visualReference: visualRef,
consistencyMode: params.consistencyMode,
stylePreset: params.stylePreset,
promptOptimizer: params.promptOptimizer,
cameraControl: params.cameraControl
? typeof params.cameraControl === 'string'
? JSON.parse(params.cameraControl)
: params.cameraControl
: undefined,
}
},
},
},
inputs: {
provider: {
type: 'string',
description: 'Video generation provider (runway, veo, luma, minimax)',
},
apiKey: { type: 'string', description: 'Provider API key' },
model: {
type: 'string',
description: 'Provider-specific model',
},
endpoint: {
type: 'string',
description: 'Quality endpoint for MiniMax (pro, standard)',
},
prompt: { type: 'string', description: 'Text prompt for video generation' },
duration: { type: 'number', description: 'Video duration in seconds' },
aspectRatio: {
type: 'string',
description: 'Aspect ratio (16:9, 9:16, 1:1) - not available for MiniMax',
},
resolution: {
type: 'string',
description: 'Video resolution - not available for MiniMax (fixed per endpoint)',
},
visualReference: { type: 'json', description: 'Reference image for Runway (UserFile)' },
visualReferenceUpload: { type: 'json', description: 'Uploaded reference image (basic mode)' },
visualReferenceInput: {
type: 'json',
description: 'Reference image from previous blocks (advanced mode)',
},
consistencyMode: {
type: 'string',
description: 'Consistency mode for Runway (character, object, style, location)',
},
stylePreset: { type: 'string', description: 'Style preset for Runway' },
promptOptimizer: {
type: 'boolean',
description: 'Enable prompt optimization for MiniMax (default: true)',
},
cameraControl: {
type: 'json',
description: 'Camera controls for Luma (pan, zoom, tilt, truck, tracking)',
},
},
}

View File

@@ -14,7 +14,7 @@ import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
import { CirclebackBlock } from '@/blocks/blocks/circleback'
import { ClayBlock } from '@/blocks/blocks/clay'
import { ConditionBlock } from '@/blocks/blocks/condition'
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
import { ConfluenceBlock } from '@/blocks/blocks/confluence'
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
import { DatadogBlock } from '@/blocks/blocks/datadog'
import { DiscordBlock } from '@/blocks/blocks/discord'
@@ -25,7 +25,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 } from '@/blocks/blocks/file'
import { FirecrawlBlock } from '@/blocks/blocks/firecrawl'
import { FirefliesBlock } from '@/blocks/blocks/fireflies'
import { FunctionBlock } from '@/blocks/blocks/function'
@@ -74,7 +74,7 @@ import { MemoryBlock } from '@/blocks/blocks/memory'
import { MicrosoftExcelBlock, MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel'
import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner'
import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams'
import { MistralParseBlock, MistralParseV2Block } from '@/blocks/blocks/mistral_parse'
import { MistralParseBlock } from '@/blocks/blocks/mistral_parse'
import { MongoDBBlock } from '@/blocks/blocks/mongodb'
import { MySQLBlock } from '@/blocks/blocks/mysql'
import { Neo4jBlock } from '@/blocks/blocks/neo4j'
@@ -123,7 +123,6 @@ import { SttBlock } from '@/blocks/blocks/stt'
import { SupabaseBlock } from '@/blocks/blocks/supabase'
import { TavilyBlock } from '@/blocks/blocks/tavily'
import { TelegramBlock } from '@/blocks/blocks/telegram'
import { TextractBlock } from '@/blocks/blocks/textract'
import { ThinkingBlock } from '@/blocks/blocks/thinking'
import { TinybirdBlock } from '@/blocks/blocks/tinybird'
import { TranslateBlock } from '@/blocks/blocks/translate'
@@ -133,7 +132,7 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio'
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 { VideoGeneratorBlock } from '@/blocks/blocks/video_generator'
import { VisionBlock } from '@/blocks/blocks/vision'
import { WaitBlock } from '@/blocks/blocks/wait'
import { WealthboxBlock } from '@/blocks/blocks/wealthbox'
@@ -170,7 +169,6 @@ export const registry: Record<string, BlockConfig> = {
clay: ClayBlock,
condition: ConditionBlock,
confluence: ConfluenceBlock,
confluence_v2: ConfluenceV2Block,
cursor: CursorBlock,
cursor_v2: CursorV2Block,
datadog: DatadogBlock,
@@ -183,7 +181,6 @@ export const registry: Record<string, BlockConfig> = {
evaluator: EvaluatorBlock,
exa: ExaBlock,
file: FileBlock,
file_v2: FileV2Block,
firecrawl: FirecrawlBlock,
fireflies: FirefliesBlock,
function: FunctionBlock,
@@ -239,7 +236,6 @@ export const registry: Record<string, BlockConfig> = {
microsoft_planner: MicrosoftPlannerBlock,
microsoft_teams: MicrosoftTeamsBlock,
mistral_parse: MistralParseBlock,
mistral_parse_v2: MistralParseV2Block,
mongodb: MongoDBBlock,
mysql: MySQLBlock,
neo4j: Neo4jBlock,
@@ -290,7 +286,6 @@ export const registry: Record<string, BlockConfig> = {
supabase: SupabaseBlock,
tavily: TavilyBlock,
telegram: TelegramBlock,
textract: TextractBlock,
thinking: ThinkingBlock,
tinybird: TinybirdBlock,
translate: TranslateBlock,
@@ -301,7 +296,6 @@ export const registry: Record<string, BlockConfig> = {
typeform: TypeformBlock,
variables: VariablesBlock,
video_generator: VideoGeneratorBlock,
video_generator_v2: VideoGeneratorV2Block,
vision: VisionBlock,
wait: WaitBlock,
wealthbox: WealthboxBlock,

View File

@@ -4093,23 +4093,6 @@ export function SQSIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function TextractIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='10 14 60 52'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
>
<path
d='M22.0624102,50 C24.3763895,53.603 28.4103535,56 33.0003125,56 C40.1672485,56 45.9991964,50.168 45.9991964,43 C45.9991964,35.832 40.1672485,30 33.0003125,30 C27.6033607,30 22.9664021,33.307 21.0024196,38 L23.2143999,38 C25.0393836,34.444 28.7363506,32 33.0003125,32 C39.0652583,32 43.9992143,36.935 43.9992143,43 C43.9992143,49.065 39.0652583,54 33.0003125,54 C29.5913429,54 26.5413702,52.441 24.5213882,50 L22.0624102,50 Z M37.0002768,45 L37.0002768,43 L41.9992321,43 C41.9992321,38.038 37.9622682,34 33.0003125,34 C28.0373568,34 23.9993929,38.038 23.9993929,43 L28.9993482,43 L28.9993482,45 L24.2313908,45 C25.1443826,49.002 28.7253507,52 33.0003125,52 C35.1362934,52 37.0992759,51.249 38.6442621,50 L34.0003036,50 L34.0003036,48 L40.4782457,48 C41.0812403,47.102 41.5202364,46.087 41.7682342,45 L37.0002768,45 Z M21.0024196,48 L23.2143999,48 C22.4434068,46.498 22.0004107,44.801 22.0004107,43 C22.0004107,41.959 22.1554093,40.955 22.4264069,40 L20.3634253,40 C20.1344274,40.965 19.9994286,41.966 19.9994286,43 C19.9994286,44.771 20.3584254,46.46 21.0024196,48 L21.0024196,48 Z M19.7434309,50 L17.0004554,50 L17.0004554,48 L18.8744386,48 C18.5344417,47.04 18.2894438,46.038 18.1494451,45 L15.4144695,45 L16.707458,46.293 L15.2924706,47.707 L12.2924974,44.707 C11.9025009,44.316 11.9025009,43.684 12.2924974,43.293 L15.2924706,40.293 L16.707458,41.707 L15.4144695,43 L18.0004464,43 C18.0004464,41.973 18.1044455,40.97 18.3024437,40 L17.0004554,40 L17.0004554,38 L18.8744386,38 C20.9404202,32.184 26.4833707,28 33.0003125,28 C37.427273,28 41.4002375,29.939 44.148213,33 L59.0000804,33 L59.0000804,35 L45.6661994,35 C47.1351863,37.318 47.9991786,40.058 47.9991786,43 L59.0000804,43 L59.0000804,45 L47.8501799,45 C46.8681887,52.327 40.5912447,58 33.0003125,58 C27.2563638,58 22.2624084,54.752 19.7434309,50 L19.7434309,50 Z M37.0002768,39 C37.0002768,38.448 36.5522808,38 36.0002857,38 L29.9993482,38 C29.4473442,38 28.9993482,38.448 28.9993482,39 L28.9993482,41 L31.0003304,41 L31.0003304,40 L32.0003214,40 L32.0003214,43 L31.0003304,43 L31.0003304,45 L35.0002946,45 L35.0002946,43 L34.0003036,43 L34.0003036,40 L35.0002946,40 L35.0002946,41 L37.0002768,41 L37.0002768,39 Z M49.0001696,40 L59.0000804,40 L59.0000804,38 L49.0001696,38 L49.0001696,40 Z M49.0001696,50 L59.0000804,50 L59.0000804,48 L49.0001696,48 L49.0001696,50 Z M57.0000982,27 L60.5850662,27 L57.0000982,23.414 L57.0000982,27 Z M63.7070383,27.293 C63.8940367,27.48 64.0000357,27.735 64.0000357,28 L64.0000357,63 C64.0000357,63.552 63.5520397,64 63.0000446,64 L32.0003304,64 C31.4473264,64 31.0003304,63.552 31.0003304,63 L31.0003304,59 L33.0003125,59 L33.0003125,62 L62.0000536,62 L62.0000536,29 L56.0001071,29 C55.4471121,29 55.0001161,28.552 55.0001161,28 L55.0001161,22 L33.0003125,22 L33.0003125,27 L31.0003304,27 L31.0003304,21 C31.0003304,20.448 31.4473264,20 32.0003304,20 L56.0001071,20 C56.2651048,20 56.5191025,20.105 56.7071008,20.293 L63.7070383,27.293 Z M68,24.166 L68,61 C68,61.552 67.552004,62 67.0000089,62 L65.0000268,62 L65.0000268,60 L66.0000179,60 L66.0000179,24.612 L58.6170838,18 L36.0002857,18 L36.0002857,19 L34.0003036,19 L34.0003036,17 C34.0003036,16.448 34.4472996,16 35.0003036,16 L59.0000804,16 C59.2460782,16 59.483076,16.091 59.6660744,16.255 L67.666003,23.42 C67.8780011,23.61 68,23.881 68,24.166 L68,24.166 Z'
fill='currentColor'
/>
</svg>
)
}
export function McpIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -10,7 +10,7 @@ import {
GetBlockConfigInput,
GetBlockConfigResult,
} from '@/lib/copilot/tools/shared/schemas'
import { getBlock } from '@/blocks/registry'
import { getLatestBlock } from '@/blocks/registry'
interface GetBlockConfigArgs {
blockType: string
@@ -40,8 +40,7 @@ export class GetBlockConfigClientTool extends BaseClientTool {
},
getDynamicText: (params, state) => {
if (params?.blockType && typeof params.blockType === 'string') {
// Look up the block config to get the human-readable name
const blockConfig = getBlock(params.blockType)
const blockConfig = getLatestBlock(params.blockType)
const blockName = (blockConfig?.name ?? params.blockType.replace(/_/g, ' ')).toLowerCase()
const opSuffix = params.operation ? ` (${params.operation})` : ''

View File

@@ -10,7 +10,7 @@ import {
GetBlockOptionsInput,
GetBlockOptionsResult,
} from '@/lib/copilot/tools/shared/schemas'
import { getBlock } from '@/blocks/registry'
import { getLatestBlock } from '@/blocks/registry'
interface GetBlockOptionsArgs {
blockId: string
@@ -43,8 +43,7 @@ export class GetBlockOptionsClientTool extends BaseClientTool {
(params as any)?.block_id ||
(params as any)?.block_type
if (typeof blockId === 'string') {
// Look up the block config to get the human-readable name
const blockConfig = getBlock(blockId)
const blockConfig = getLatestBlock(blockId)
const blockName = (blockConfig?.name ?? blockId.replace(/_/g, ' ')).toLowerCase()
switch (state) {

View File

@@ -5,7 +5,7 @@ import {
GetBlockConfigResult,
type GetBlockConfigResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { registry as blockRegistry } from '@/blocks/registry'
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types'
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
@@ -452,9 +452,12 @@ export const getBlockConfigServerTool: BaseServerTool<
const inputs = extractInputsFromSubBlocks(subBlocks, operation, trigger)
const outputs = extractOutputs(blockConfig, operation, trigger)
const latestBlock = getLatestBlock(blockType)
const displayName = latestBlock?.name ?? blockConfig.name
const result = {
blockType,
blockName: blockConfig.name,
blockName: displayName,
operation,
trigger,
inputs,

View File

@@ -5,7 +5,7 @@ import {
GetBlockOptionsResult,
type GetBlockOptionsResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { registry as blockRegistry } from '@/blocks/registry'
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
import { tools as toolsRegistry } from '@/tools/registry'
@@ -113,9 +113,12 @@ export const getBlockOptionsServerTool: BaseServerTool<
}
}
const latestBlock = getLatestBlock(blockId)
const displayName = latestBlock?.name ?? blockConfig.name
const result = {
blockId,
blockName: blockConfig.name,
blockName: displayName,
operations,
}

View File

@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from 'vitest'
import {
validateAirtableId,
validateAlphanumericId,
validateAwsRegion,
validateEnum,
validateExternalUrl,
validateFileExtension,
@@ -17,7 +16,6 @@ import {
validateNumericId,
validatePathSegment,
validateProxyUrl,
validateS3BucketName,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation'
import { sanitizeForLogging } from '@/lib/core/security/redaction'
@@ -1171,282 +1169,3 @@ describe('validateAirtableId', () => {
})
})
})
describe('validateAwsRegion', () => {
describe('valid standard regions', () => {
it.concurrent('should accept us-east-1', () => {
const result = validateAwsRegion('us-east-1')
expect(result.isValid).toBe(true)
expect(result.sanitized).toBe('us-east-1')
})
it.concurrent('should accept us-west-2', () => {
const result = validateAwsRegion('us-west-2')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept eu-west-1', () => {
const result = validateAwsRegion('eu-west-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept eu-central-1', () => {
const result = validateAwsRegion('eu-central-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept ap-southeast-1', () => {
const result = validateAwsRegion('ap-southeast-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept ap-northeast-1', () => {
const result = validateAwsRegion('ap-northeast-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept sa-east-1', () => {
const result = validateAwsRegion('sa-east-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept me-south-1', () => {
const result = validateAwsRegion('me-south-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept af-south-1', () => {
const result = validateAwsRegion('af-south-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept ca-central-1', () => {
const result = validateAwsRegion('ca-central-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept il-central-1', () => {
const result = validateAwsRegion('il-central-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept regions with double-digit numbers', () => {
const result = validateAwsRegion('ap-northeast-12')
expect(result.isValid).toBe(true)
})
})
describe('valid GovCloud regions', () => {
it.concurrent('should accept us-gov-west-1', () => {
const result = validateAwsRegion('us-gov-west-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept us-gov-east-1', () => {
const result = validateAwsRegion('us-gov-east-1')
expect(result.isValid).toBe(true)
})
})
describe('valid China regions', () => {
it.concurrent('should accept cn-north-1', () => {
const result = validateAwsRegion('cn-north-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept cn-northwest-1', () => {
const result = validateAwsRegion('cn-northwest-1')
expect(result.isValid).toBe(true)
})
})
describe('valid ISO regions', () => {
it.concurrent('should accept us-iso-east-1', () => {
const result = validateAwsRegion('us-iso-east-1')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept us-isob-east-1', () => {
const result = validateAwsRegion('us-isob-east-1')
expect(result.isValid).toBe(true)
})
})
describe('invalid regions', () => {
it.concurrent('should reject null', () => {
const result = validateAwsRegion(null)
expect(result.isValid).toBe(false)
expect(result.error).toContain('required')
})
it.concurrent('should reject empty string', () => {
const result = validateAwsRegion('')
expect(result.isValid).toBe(false)
expect(result.error).toContain('required')
})
it.concurrent('should reject uppercase regions', () => {
const result = validateAwsRegion('US-EAST-1')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject invalid format - missing number', () => {
const result = validateAwsRegion('us-east')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject invalid format - wrong separators', () => {
const result = validateAwsRegion('us_east_1')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject invalid format - too many parts', () => {
const result = validateAwsRegion('us-east-1-extra')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject path traversal attempts', () => {
const result = validateAwsRegion('../etc/passwd')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject arbitrary strings', () => {
const result = validateAwsRegion('not-a-region')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject invalid prefix', () => {
const result = validateAwsRegion('xx-east-1')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject invalid direction', () => {
const result = validateAwsRegion('us-middle-1')
expect(result.isValid).toBe(false)
})
it.concurrent('should use custom param name in errors', () => {
const result = validateAwsRegion('', 'awsRegion')
expect(result.error).toContain('awsRegion')
})
})
})
describe('validateS3BucketName', () => {
describe('valid bucket names', () => {
it.concurrent('should accept simple bucket name', () => {
const result = validateS3BucketName('my-bucket')
expect(result.isValid).toBe(true)
expect(result.sanitized).toBe('my-bucket')
})
it.concurrent('should accept bucket name with numbers', () => {
const result = validateS3BucketName('bucket123')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept bucket name with periods', () => {
const result = validateS3BucketName('my.bucket.name')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept 3 character bucket name', () => {
const result = validateS3BucketName('abc')
expect(result.isValid).toBe(true)
})
it.concurrent('should accept 63 character bucket name', () => {
const result = validateS3BucketName('a'.repeat(63))
expect(result.isValid).toBe(true)
})
it.concurrent('should accept minimum valid bucket name (3 chars)', () => {
const result = validateS3BucketName('a1b')
expect(result.isValid).toBe(true)
})
})
describe('invalid bucket names - null/empty', () => {
it.concurrent('should reject null', () => {
const result = validateS3BucketName(null)
expect(result.isValid).toBe(false)
expect(result.error).toContain('required')
})
it.concurrent('should reject empty string', () => {
const result = validateS3BucketName('')
expect(result.isValid).toBe(false)
expect(result.error).toContain('required')
})
})
describe('invalid bucket names - length', () => {
it.concurrent('should reject 2 character bucket name', () => {
const result = validateS3BucketName('ab')
expect(result.isValid).toBe(false)
expect(result.error).toContain('between 3 and 63')
})
it.concurrent('should reject 64 character bucket name', () => {
const result = validateS3BucketName('a'.repeat(64))
expect(result.isValid).toBe(false)
expect(result.error).toContain('between 3 and 63')
})
})
describe('invalid bucket names - format', () => {
it.concurrent('should reject uppercase letters', () => {
const result = validateS3BucketName('MyBucket')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject underscores', () => {
const result = validateS3BucketName('my_bucket')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject starting with hyphen', () => {
const result = validateS3BucketName('-mybucket')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject ending with hyphen', () => {
const result = validateS3BucketName('mybucket-')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject starting with period', () => {
const result = validateS3BucketName('.mybucket')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject ending with period', () => {
const result = validateS3BucketName('mybucket.')
expect(result.isValid).toBe(false)
})
it.concurrent('should reject consecutive periods', () => {
const result = validateS3BucketName('my..bucket')
expect(result.isValid).toBe(false)
expect(result.error).toContain('consecutive periods')
})
it.concurrent('should reject IP address format', () => {
const result = validateS3BucketName('192.168.1.1')
expect(result.isValid).toBe(false)
expect(result.error).toContain('IP address')
})
it.concurrent('should reject special characters', () => {
const result = validateS3BucketName('my@bucket')
expect(result.isValid).toBe(false)
})
})
describe('error messages', () => {
it.concurrent('should use custom param name in errors', () => {
const result = validateS3BucketName('', 's3Bucket')
expect(result.error).toContain('s3Bucket')
})
})
})

View File

@@ -6,12 +6,18 @@ import * as ipaddr from 'ipaddr.js'
const logger = createLogger('InputValidation')
/**
* Result type for validation functions
*/
export interface ValidationResult {
isValid: boolean
error?: string
sanitized?: string
}
/**
* Options for path segment validation
*/
export interface PathSegmentOptions {
/** Name of the parameter for error messages */
paramName?: string
@@ -62,6 +68,7 @@ export function validatePathSegment(
customPattern,
} = options
// Check for null/undefined
if (value === null || value === undefined || value === '') {
return {
isValid: false,
@@ -69,6 +76,7 @@ export function validatePathSegment(
}
}
// Check length
if (value.length > maxLength) {
logger.warn('Path segment exceeds maximum length', {
paramName,
@@ -81,6 +89,7 @@ export function validatePathSegment(
}
}
// Check for null bytes (potential for bypass attacks)
if (value.includes('\0') || value.includes('%00')) {
logger.warn('Path segment contains null bytes', { paramName })
return {
@@ -89,6 +98,7 @@ export function validatePathSegment(
}
}
// Check for path traversal patterns
const pathTraversalPatterns = [
'..',
'./',
@@ -117,6 +127,7 @@ export function validatePathSegment(
}
}
// Check for directory separators
if (value.includes('/') || value.includes('\\')) {
logger.warn('Path segment contains directory separators', { paramName })
return {
@@ -125,6 +136,7 @@ export function validatePathSegment(
}
}
// Use custom pattern if provided
if (customPattern) {
if (!customPattern.test(value)) {
logger.warn('Path segment failed custom pattern validation', {
@@ -139,6 +151,7 @@ export function validatePathSegment(
return { isValid: true, sanitized: value }
}
// Build allowed character pattern
let pattern = '^[a-zA-Z0-9'
if (allowHyphens) pattern += '\\-'
if (allowUnderscores) pattern += '_'
@@ -1070,138 +1083,6 @@ export function validateAirtableId(
return { isValid: true, sanitized: value }
}
/**
* Validates an AWS region identifier
*
* Supported region formats:
* - Standard: us-east-1, eu-west-2, ap-southeast-1, sa-east-1, af-south-1
* - GovCloud: us-gov-east-1, us-gov-west-1
* - China: cn-north-1, cn-northwest-1
* - Israel: il-central-1
* - ISO partitions: us-iso-east-1, us-isob-east-1
*
* @param value - The AWS region to validate
* @param paramName - Name of the parameter for error messages
* @returns ValidationResult
*
* @example
* ```typescript
* const result = validateAwsRegion(region, 'region')
* if (!result.isValid) {
* return NextResponse.json({ error: result.error }, { status: 400 })
* }
* ```
*/
export function validateAwsRegion(
value: string | null | undefined,
paramName = 'region'
): ValidationResult {
if (value === null || value === undefined || value === '') {
return {
isValid: false,
error: `${paramName} is required`,
}
}
// AWS region patterns:
// - Standard: af|ap|ca|eu|me|sa|us|il followed by direction and number
// - GovCloud: us-gov-east-1, us-gov-west-1
// - China: cn-north-1, cn-northwest-1
// - ISO: us-iso-east-1, us-iso-west-1, us-isob-east-1
const awsRegionPattern =
/^(af|ap|ca|cn|eu|il|me|sa|us|us-gov|us-iso|us-isob)-(central|north|northeast|northwest|south|southeast|southwest|east|west)-\d{1,2}$/
if (!awsRegionPattern.test(value)) {
logger.warn('Invalid AWS region format', {
paramName,
value: value.substring(0, 50),
})
return {
isValid: false,
error: `${paramName} must be a valid AWS region (e.g., us-east-1, eu-west-2, us-gov-west-1)`,
}
}
return { isValid: true, sanitized: value }
}
/**
* Validates an S3 bucket name according to AWS naming rules
*
* S3 bucket names must:
* - Be 3-63 characters long
* - Start and end with a letter or number
* - Contain only lowercase letters, numbers, and hyphens
* - Not contain consecutive periods
* - Not be formatted as an IP address
*
* @param value - The S3 bucket name to validate
* @param paramName - Name of the parameter for error messages
* @returns ValidationResult
*
* @example
* ```typescript
* const result = validateS3BucketName(bucket, 'bucket')
* if (!result.isValid) {
* return NextResponse.json({ error: result.error }, { status: 400 })
* }
* ```
*/
export function validateS3BucketName(
value: string | null | undefined,
paramName = 'bucket'
): ValidationResult {
if (value === null || value === undefined || value === '') {
return {
isValid: false,
error: `${paramName} is required`,
}
}
if (value.length < 3 || value.length > 63) {
logger.warn('S3 bucket name length invalid', {
paramName,
length: value.length,
})
return {
isValid: false,
error: `${paramName} must be between 3 and 63 characters`,
}
}
const bucketNamePattern = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$|^[a-z0-9]$/
if (!bucketNamePattern.test(value)) {
logger.warn('Invalid S3 bucket name format', {
paramName,
value: value.substring(0, 63),
})
return {
isValid: false,
error: `${paramName} must start and end with a letter or number, and contain only lowercase letters, numbers, hyphens, and periods`,
}
}
if (value.includes('..')) {
logger.warn('S3 bucket name contains consecutive periods', { paramName })
return {
isValid: false,
error: `${paramName} cannot contain consecutive periods`,
}
}
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/
if (ipPattern.test(value)) {
logger.warn('S3 bucket name formatted as IP address', { paramName })
return {
isValid: false,
error: `${paramName} cannot be formatted as an IP address`,
}
}
return { isValid: true, sanitized: value }
}
/**
* Validates a Google Calendar ID
*

View File

@@ -455,27 +455,3 @@ export async function generatePresignedDownloadUrl(
export function hasCloudStorage(): boolean {
return USE_BLOB_STORAGE || USE_S3_STORAGE
}
/**
* Get S3 bucket and key information for a storage key
* Useful for services that need direct S3 access (e.g., AWS Textract async)
*/
export function getS3InfoForKey(
key: string,
context: StorageContext
): { bucket: string; key: string } {
if (!USE_S3_STORAGE) {
throw new Error('S3 storage is not configured. Cannot retrieve S3 info for key.')
}
const config = getStorageConfig(context)
if (!config.bucket) {
throw new Error(`S3 bucket not configured for context: ${context}`)
}
return {
bucket: config.bucket,
key,
}
}

View File

@@ -562,26 +562,14 @@ function generateOutputPathsWithTypes(
* Gets the tool outputs for a block operation.
*
* @param blockConfig - The block configuration containing tools config
* @param subBlocks - SubBlock values to pass to the tool selector
* @param operation - The selected operation for the tool
* @returns Outputs schema for the tool, or empty object on error
*/
export function getToolOutputs(
blockConfig: BlockConfig,
subBlocks?: Record<string, SubBlockWithValue>
): Record<string, any> {
export function getToolOutputs(blockConfig: BlockConfig, operation: string): Record<string, any> {
if (!blockConfig?.tools?.config?.tool) return {}
try {
// Build params object from subBlock values for tool selector
// This allows tool selectors to use any field (operation, provider, etc.)
const params: Record<string, any> = {}
if (subBlocks) {
for (const [key, subBlock] of Object.entries(subBlocks)) {
params[key] = subBlock.value
}
}
const toolId = blockConfig.tools.config.tool(params)
const toolId = blockConfig.tools.config.tool({ operation })
if (!toolId) return {}
const toolConfig = getTool(toolId)
@@ -589,7 +577,7 @@ export function getToolOutputs(
return toolConfig.outputs
} catch (error) {
logger.warn('Failed to get tool outputs', { error })
logger.warn('Failed to get tool outputs for operation', { operation, error })
return {}
}
}
@@ -598,14 +586,16 @@ export function getToolOutputs(
* Generates output paths for a tool-based block.
*
* @param blockConfig - The block configuration containing tools config
* @param subBlocks - SubBlock values for tool selection and condition evaluation
* @param operation - The selected operation for the tool
* @param subBlocks - Optional subBlock values for condition evaluation
* @returns Array of output paths for the tool, or empty array on error
*/
export function getToolOutputPaths(
blockConfig: BlockConfig,
operation: string,
subBlocks?: Record<string, SubBlockWithValue>
): string[] {
const outputs = getToolOutputs(blockConfig, subBlocks)
const outputs = getToolOutputs(blockConfig, operation)
if (!outputs || Object.keys(outputs).length === 0) return []
@@ -640,16 +630,16 @@ export function getOutputPathsFromSchema(outputs: Record<string, any>): string[]
* Gets the output type for a specific path in a tool's outputs.
*
* @param blockConfig - The block configuration containing tools config
* @param subBlocks - SubBlock values for tool selection
* @param operation - The selected operation for the tool
* @param path - The dot-separated path to the output field
* @returns The type of the output field, or 'any' if not found
*/
export function getToolOutputType(
blockConfig: BlockConfig,
subBlocks: Record<string, SubBlockWithValue> | undefined,
operation: string,
path: string
): string {
const outputs = getToolOutputs(blockConfig, subBlocks)
const outputs = getToolOutputs(blockConfig, operation)
if (!outputs || Object.keys(outputs).length === 0) return 'any'
const pathsWithTypes = generateOutputPathsWithTypes(outputs)

View File

@@ -34,51 +34,6 @@ import { mergeToolParameters } from '@/tools/params'
const logger = createLogger('ProviderUtils')
/**
* Checks if a workflow description is a default/placeholder description
*/
function isDefaultWorkflowDescription(
description: string | null | undefined,
name?: string
): boolean {
if (!description) return true
const normalizedDesc = description.toLowerCase().trim()
return (
description === name ||
normalizedDesc === 'new workflow' ||
normalizedDesc === 'your first workflow - start building here!'
)
}
/**
* Fetches workflow metadata (name and description) from the API
*/
async function fetchWorkflowMetadata(
workflowId: string
): Promise<{ name: string; description: string | null } | null> {
try {
const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http')
const headers = await buildAuthHeaders()
const url = buildAPIUrl(`/api/workflows/${workflowId}`)
const response = await fetch(url.toString(), { headers })
if (!response.ok) {
logger.warn(`Failed to fetch workflow metadata for ${workflowId}`)
return null
}
const { data } = await response.json()
return {
name: data?.name || 'Workflow',
description: data?.description || null,
}
} catch (error) {
logger.error('Error fetching workflow metadata:', error)
return null
}
}
/**
* Client-safe provider metadata.
* This object contains only model lists and patterns - no executeRequest implementations.
@@ -524,30 +479,16 @@ export async function transformBlockTool(
const llmSchema = await createLLMToolSchema(toolConfig, userProvidedParams)
let uniqueToolId = toolConfig.id
let toolName = toolConfig.name
let toolDescription = toolConfig.description
if (toolId === 'workflow_executor' && userProvidedParams.workflowId) {
uniqueToolId = `${toolConfig.id}_${userProvidedParams.workflowId}`
const workflowMetadata = await fetchWorkflowMetadata(userProvidedParams.workflowId)
if (workflowMetadata) {
toolName = workflowMetadata.name || toolConfig.name
if (
workflowMetadata.description &&
!isDefaultWorkflowDescription(workflowMetadata.description, workflowMetadata.name)
) {
toolDescription = workflowMetadata.description
}
}
} else if (toolId.startsWith('knowledge_') && userProvidedParams.knowledgeBaseId) {
uniqueToolId = `${toolConfig.id}_${userProvidedParams.knowledgeBaseId}`
}
return {
id: uniqueToolId,
name: toolName,
description: toolDescription,
name: toolConfig.name,
description: toolConfig.description,
params: userProvidedParams,
parameters: llmSchema,
}

View File

@@ -1,4 +1,3 @@
import { fileParserTool, fileParserV2Tool } from '@/tools/file/parser'
import { fileParserTool } from '@/tools/file/parser'
export const fileParseTool = fileParserTool
export { fileParserV2Tool }

View File

@@ -177,25 +177,3 @@ export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
processedFiles: { type: 'file[]', description: 'Array of UserFile objects for downstream use' },
},
}
export const fileParserV2Tool: ToolConfig<FileParserInput, FileParserOutput> = {
id: 'file_parser_v2',
name: 'File Parser',
description: 'Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)',
version: '2.0.0',
params: fileParserTool.params,
request: fileParserTool.request,
transformResponse: fileParserTool.transformResponse,
outputs: {
files: {
type: 'array',
description: 'Array of parsed files with content, metadata, and file properties',
},
combinedContent: {
type: 'string',
description: 'All file contents merged into a single text string',
},
},
}

View File

@@ -1,3 +1,3 @@
import { mistralParserTool, mistralParserV2Tool } from '@/tools/mistral/parser'
import { mistralParserTool } from '@/tools/mistral/parser'
export { mistralParserTool, mistralParserV2Tool }
export { mistralParserTool }

View File

@@ -1,10 +1,6 @@
import { createLogger } from '@sim/logger'
import { getBaseUrl } from '@/lib/core/utils/urls'
import type {
MistralParserInput,
MistralParserOutput,
MistralParserV2Output,
} from '@/tools/mistral/types'
import type { MistralParserInput, MistralParserOutput } from '@/tools/mistral/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MistralParserTool')
@@ -419,138 +415,3 @@ export const mistralParserTool: ToolConfig<MistralParserInput, MistralParserOutp
},
},
}
export const mistralParserV2Tool: ToolConfig<MistralParserInput, 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,
transformResponse: async (response: Response) => {
let ocrResult
try {
ocrResult = await response.json()
} catch (jsonError) {
throw new Error(
`Failed to parse Mistral OCR response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`
)
}
if (!ocrResult || typeof ocrResult !== 'object') {
throw new Error('Invalid response format from Mistral OCR API')
}
// Extract the actual Mistral data (may be nested in output from our API route)
const mistralData =
ocrResult.output && typeof ocrResult.output === 'object' && !ocrResult.pages
? ocrResult.output
: ocrResult
// Return raw Mistral API structure - no transformation
return {
success: true,
output: {
pages: mistralData.pages ?? [],
model: mistralData.model ?? 'mistral-ocr-latest',
usage_info: mistralData.usage_info ?? { pages_processed: 0, doc_size_bytes: null },
document_annotation: mistralData.document_annotation ?? null,
},
}
},
outputs: {
pages: {
type: 'array',
description: 'Array of page objects from Mistral OCR',
items: {
type: 'object',
properties: {
index: { type: 'number', description: 'Page index (zero-based)' },
markdown: { type: 'string', description: 'Extracted markdown content' },
images: {
type: 'array',
description: 'Images extracted from this page with bounding boxes',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Image identifier (e.g., img-0.jpeg)' },
top_left_x: { type: 'number', description: 'Top-left X coordinate in pixels' },
top_left_y: { type: 'number', description: 'Top-left Y coordinate in pixels' },
bottom_right_x: {
type: 'number',
description: 'Bottom-right X coordinate in pixels',
},
bottom_right_y: {
type: 'number',
description: 'Bottom-right Y coordinate in pixels',
},
image_base64: {
type: 'string',
description: 'Base64-encoded image data (when include_image_base64=true)',
optional: true,
},
},
},
},
dimensions: {
type: 'object',
description: 'Page dimensions',
properties: {
dpi: { type: 'number', description: 'Dots per inch' },
height: { type: 'number', description: 'Page height in pixels' },
width: { type: 'number', description: 'Page width in pixels' },
},
},
tables: {
type: 'array',
description:
'Extracted tables as HTML/markdown (when table_format is set). Referenced via placeholders like [tbl-0.html]',
},
hyperlinks: {
type: 'array',
description:
'Array of URL strings detected in the page (e.g., ["https://...", "mailto:..."])',
items: {
type: 'string',
description: 'URL or mailto link',
},
},
header: {
type: 'string',
description: 'Page header content (when extract_header=true)',
optional: true,
},
footer: {
type: 'string',
description: 'Page footer content (when extract_footer=true)',
optional: true,
},
},
},
},
model: {
type: 'string',
description: 'Mistral OCR model identifier (e.g., mistral-ocr-latest)',
},
usage_info: {
type: 'object',
description: 'Usage and processing statistics',
properties: {
pages_processed: { type: 'number', description: 'Total number of pages processed' },
doc_size_bytes: {
type: 'number',
description: 'Document file size in bytes',
optional: true,
},
},
},
document_annotation: {
type: 'string',
description: 'Structured annotation data as JSON string (when applicable)',
optional: true,
},
},
}

View File

@@ -1,80 +1,98 @@
import type { ToolResponse } from '@/tools/types'
/**
* Input parameters for the Mistral OCR parser tool
*/
export interface MistralParserInput {
/** URL to a PDF document to be processed */
filePath: string
/** File upload data (from file-upload component) */
fileUpload?: any
/** Internal file path flag (for presigned URL conversion) */
_internalFilePath?: string
/** Mistral API key for authentication */
apiKey: string
/** Output format for the extracted content (default: 'markdown') */
resultType?: 'markdown' | 'text' | 'json'
/** Whether to include base64-encoded images in the response */
includeImageBase64?: boolean
/** Specific pages to process (zero-indexed) */
pages?: number[]
/** Maximum number of images to extract from the PDF */
imageLimit?: number
/** Minimum height and width (in pixels) for images to extract */
imageMinSize?: number
}
/**
* Usage information returned by the Mistral OCR API
*/
export interface MistralOcrUsageInfo {
/** Number of pages processed in the document */
pagesProcessed: number
/** Size of the document in bytes */
docSizeBytes: number | null
}
/**
* Metadata about the processed document
*/
export interface MistralParserMetadata {
/** Unique identifier for this OCR job */
jobId: string
/** File type of the document (typically 'pdf') */
fileType: string
/** Filename extracted from the document URL */
fileName: string
/** Source type (always 'url' for now) */
source: 'url'
/** Original URL to the document (only included for user-provided URLs) */
sourceUrl?: string
/** Total number of pages in the document */
pageCount: number
/** Usage statistics from the OCR processing */
usageInfo?: MistralOcrUsageInfo
/** The Mistral OCR model used for processing */
model: string
/** The output format that was requested */
resultType?: 'markdown' | 'text' | 'json'
/** ISO timestamp when the document was processed */
processedAt: string
}
/**
* Output data structure from the Mistral OCR parser
*/
export interface MistralParserOutputData {
/** Extracted content in the requested format */
content: string
/** Metadata about the parsed document and processing */
metadata: MistralParserMetadata
}
/**
* Complete response from the Mistral OCR parser tool
*/
export interface MistralParserOutput extends ToolResponse {
/** The output data containing content and metadata */
output: MistralParserOutputData
}
export interface MistralOcrImage {
id: string
top_left_x: number
top_left_y: number
bottom_right_x: number
bottom_right_y: number
image_base64?: string
}
export interface MistralOcrDimensions {
dpi: number
height: number
width: number
}
export interface MistralOcrPage {
index: number
markdown: string
images: MistralOcrImage[]
dimensions: MistralOcrDimensions
tables: unknown[]
hyperlinks: unknown[]
header: string | null
footer: string | null
}
export interface MistralOcrUsageInfoRaw {
pages_processed: number
doc_size_bytes: number | null
}
export interface MistralParserV2Output extends ToolResponse {
output: {
pages: MistralOcrPage[]
model: string
usage_info: MistralOcrUsageInfoRaw
document_annotation: string | null
}
}

View File

@@ -518,7 +518,7 @@ async function applyDynamicSchemaForWorkflow(
for (const field of workflowInputFields) {
propertySchema.properties[field.name] = {
type: field.type || 'string',
description: field.description || `Input field: ${field.name}`,
description: `Input field: ${field.name}`,
}
propertySchema.required.push(field.name)
}
@@ -533,10 +533,11 @@ async function applyDynamicSchemaForWorkflow(
/**
* Fetches workflow input fields from the API.
* No local caching - relies on React Query caching on the client side.
*/
async function fetchWorkflowInputFields(
workflowId: string
): Promise<Array<{ name: string; type: string; description?: string }>> {
): Promise<Array<{ name: string; type: string }>> {
try {
const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http')

View File

@@ -204,7 +204,7 @@ import {
exaResearchTool,
exaSearchTool,
} from '@/tools/exa'
import { fileParserV2Tool, fileParseTool } from '@/tools/file'
import { fileParseTool } from '@/tools/file'
import {
firecrawlAgentTool,
firecrawlCrawlTool,
@@ -979,7 +979,7 @@ import {
microsoftTeamsWriteChannelTool,
microsoftTeamsWriteChatTool,
} from '@/tools/microsoft_teams'
import { mistralParserTool, mistralParserV2Tool } from '@/tools/mistral'
import { mistralParserTool } from '@/tools/mistral'
import {
mongodbDeleteTool,
mongodbExecuteTool,
@@ -1500,7 +1500,6 @@ import {
telegramSendPhotoTool,
telegramSendVideoTool,
} from '@/tools/telegram'
import { textractParserTool } from '@/tools/textract'
import { thinkingTool } from '@/tools/thinking'
import { tinybirdEventsTool, tinybirdQueryTool } from '@/tools/tinybird'
import {
@@ -1683,7 +1682,6 @@ export const tools: Record<string, ToolConfig> = {
function_execute: functionExecuteTool,
vision_tool: visionTool,
file_parser: fileParseTool,
file_parser_v2: fileParserV2Tool,
firecrawl_scrape: firecrawlScrapeTool,
firecrawl_search: firecrawlSearchTool,
firecrawl_crawl: firecrawlCrawlTool,
@@ -2457,9 +2455,7 @@ export const tools: Record<string, ToolConfig> = {
apollo_task_search: apolloTaskSearchTool,
apollo_email_accounts: apolloEmailAccountsTool,
mistral_parser: mistralParserTool,
mistral_parser_v2: mistralParserV2Tool,
reducto_parser: reductoParserTool,
textract_parser: textractParserTool,
thinking_tool: thinkingTool,
tinybird_events: tinybirdEventsTool,
tinybird_query: tinybirdQueryTool,

View File

@@ -95,7 +95,6 @@ export const s3CopyObjectTool: ToolConfig = {
success: true,
output: {
url: data.output.url,
uri: data.output.uri,
metadata: {
copySourceVersionId: data.output.copySourceVersionId,
versionId: data.output.versionId,
@@ -110,10 +109,6 @@ export const s3CopyObjectTool: ToolConfig = {
type: 'string',
description: 'URL of the copied S3 object',
},
uri: {
type: 'string',
description: 'S3 URI of the copied object (s3://bucket/key)',
},
metadata: {
type: 'object',
description: 'Copy operation metadata',

View File

@@ -102,7 +102,6 @@ export const s3PutObjectTool: ToolConfig = {
success: true,
output: {
url: data.output.url,
uri: data.output.uri,
metadata: {
etag: data.output.etag,
location: data.output.location,
@@ -118,10 +117,6 @@ export const s3PutObjectTool: ToolConfig = {
type: 'string',
description: 'URL of the uploaded S3 object',
},
uri: {
type: 'string',
description: 'S3 URI of the uploaded object (s3://bucket/key)',
},
metadata: {
type: 'object',
description: 'Upload metadata including ETag and location',

View File

@@ -1,2 +0,0 @@
export { textractParserTool } from '@/tools/textract/parser'
export * from '@/tools/textract/types'

View File

@@ -1,288 +0,0 @@
import { createLogger } from '@sim/logger'
import type { TextractParserInput, TextractParserOutput } from '@/tools/textract/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('TextractParserTool')
export const textractParserTool: ToolConfig<TextractParserInput, TextractParserOutput> = {
id: 'textract_parser',
name: 'AWS Textract Parser',
description: 'Parse documents using AWS Textract OCR and document analysis',
version: '1.0.0',
params: {
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS Access Key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS Secret Access Key',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region for Textract service (e.g., us-east-1)',
},
processingMode: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Document type: single-page or multi-page. Defaults to single-page.',
},
filePath: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'URL to a document to be processed (JPEG, PNG, or single-page PDF).',
},
s3Uri: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'S3 URI for multi-page processing (s3://bucket/key).',
},
fileUpload: {
type: 'object',
required: false,
visibility: 'hidden',
description: 'File upload data from file-upload component',
},
featureTypes: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description:
'Feature types to detect: TABLES, FORMS, QUERIES, SIGNATURES, LAYOUT. If not specified, only text detection is performed.',
items: {
type: 'string',
description: 'Feature type',
},
},
queries: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description:
'Custom queries to extract specific information. Only used when featureTypes includes QUERIES.',
items: {
type: 'object',
description: 'Query configuration',
properties: {
Text: { type: 'string', description: 'The query text' },
Alias: { type: 'string', description: 'Optional alias for the result' },
},
},
},
},
request: {
url: '/api/tools/textract/parse',
method: 'POST',
headers: () => {
return {
'Content-Type': 'application/json',
Accept: 'application/json',
}
},
body: (params) => {
const processingMode = params.processingMode || 'sync'
const requestBody: Record<string, unknown> = {
accessKeyId: params.accessKeyId?.trim(),
secretAccessKey: params.secretAccessKey?.trim(),
region: params.region?.trim(),
processingMode,
}
if (processingMode === 'async') {
requestBody.s3Uri = params.s3Uri?.trim()
} else {
// Handle file upload by extracting the path
if (params.fileUpload && !params.filePath) {
const uploadPath = params.fileUpload.path || params.fileUpload.url
if (uploadPath) {
requestBody.filePath = uploadPath
}
} else {
requestBody.filePath = params.filePath?.trim()
}
}
if (params.featureTypes && Array.isArray(params.featureTypes)) {
requestBody.featureTypes = params.featureTypes
}
if (params.queries && Array.isArray(params.queries)) {
requestBody.queries = params.queries
}
return requestBody
},
},
transformResponse: async (response) => {
try {
let apiResult
try {
apiResult = await response.json()
} catch (jsonError) {
throw new Error(
`Failed to parse Textract response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`
)
}
if (!apiResult || typeof apiResult !== 'object') {
throw new Error('Invalid response format from Textract API')
}
if (!apiResult.success) {
throw new Error(apiResult.error || 'Request failed')
}
const textractData = apiResult.output ?? apiResult
return {
success: true,
output: {
blocks: textractData.Blocks ?? textractData.blocks ?? [],
documentMetadata: {
pages:
textractData.DocumentMetadata?.Pages ?? textractData.documentMetadata?.pages ?? 0,
},
modelVersion:
textractData.modelVersion ??
textractData.AnalyzeDocumentModelVersion ??
textractData.analyzeDocumentModelVersion ??
textractData.DetectDocumentTextModelVersion ??
textractData.detectDocumentTextModelVersion ??
undefined,
},
}
} catch (error) {
logger.error('Error processing Textract result:', error)
throw error
}
},
outputs: {
blocks: {
type: 'array',
description:
'Array of Block objects containing detected text, tables, forms, and other elements',
items: {
type: 'object',
properties: {
BlockType: {
type: 'string',
description: 'Type of block (PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.)',
},
Id: { type: 'string', description: 'Unique identifier for the block' },
Text: {
type: 'string',
description: 'The text content (for LINE and WORD blocks)',
optional: true,
},
TextType: {
type: 'string',
description: 'Type of text (PRINTED or HANDWRITING)',
optional: true,
},
Confidence: { type: 'number', description: 'Confidence score (0-100)', optional: true },
Page: { type: 'number', description: 'Page number', optional: true },
Geometry: {
type: 'object',
description: 'Location and bounding box information',
optional: true,
properties: {
BoundingBox: {
type: 'object',
properties: {
Height: { type: 'number', description: 'Height as ratio of document height' },
Left: { type: 'number', description: 'Left position as ratio of document width' },
Top: { type: 'number', description: 'Top position as ratio of document height' },
Width: { type: 'number', description: 'Width as ratio of document width' },
},
},
Polygon: {
type: 'array',
description: 'Polygon coordinates',
items: {
type: 'object',
properties: {
X: { type: 'number', description: 'X coordinate' },
Y: { type: 'number', description: 'Y coordinate' },
},
},
},
},
},
Relationships: {
type: 'array',
description: 'Relationships to other blocks',
optional: true,
items: {
type: 'object',
properties: {
Type: {
type: 'string',
description: 'Relationship type (CHILD, VALUE, ANSWER, etc.)',
},
Ids: { type: 'array', description: 'IDs of related blocks' },
},
},
},
EntityTypes: {
type: 'array',
description: 'Entity types for KEY_VALUE_SET (KEY or VALUE)',
optional: true,
},
SelectionStatus: {
type: 'string',
description: 'For checkboxes: SELECTED or NOT_SELECTED',
optional: true,
},
RowIndex: { type: 'number', description: 'Row index for table cells', optional: true },
ColumnIndex: {
type: 'number',
description: 'Column index for table cells',
optional: true,
},
RowSpan: { type: 'number', description: 'Row span for merged cells', optional: true },
ColumnSpan: {
type: 'number',
description: 'Column span for merged cells',
optional: true,
},
Query: {
type: 'object',
description: 'Query information for QUERY blocks',
optional: true,
properties: {
Text: { type: 'string', description: 'Query text' },
Alias: { type: 'string', description: 'Query alias', optional: true },
Pages: { type: 'array', description: 'Pages to search', optional: true },
},
},
},
},
},
documentMetadata: {
type: 'object',
description: 'Metadata about the analyzed document',
properties: {
pages: { type: 'number', description: 'Number of pages in the document' },
},
},
modelVersion: {
type: 'string',
description: 'Version of the Textract model used for processing',
optional: true,
},
},
}

View File

@@ -1,110 +0,0 @@
import type { ToolResponse } from '@/tools/types'
export type TextractProcessingMode = 'sync' | 'async'
export interface TextractParserInput {
accessKeyId: string
secretAccessKey: string
region: string
processingMode?: TextractProcessingMode
filePath?: string
s3Uri?: string
fileUpload?: {
url?: string
path?: string
}
featureTypes?: TextractFeatureType[]
queries?: TextractQuery[]
}
export type TextractFeatureType = 'TABLES' | 'FORMS' | 'QUERIES' | 'SIGNATURES' | 'LAYOUT'
export interface TextractQuery {
Text: string
Alias?: string
Pages?: string[]
}
export interface TextractBoundingBox {
Height: number
Left: number
Top: number
Width: number
}
export interface TextractPolygonPoint {
X: number
Y: number
}
export interface TextractGeometry {
BoundingBox: TextractBoundingBox
Polygon: TextractPolygonPoint[]
RotationAngle?: number
}
export interface TextractRelationship {
Type: string
Ids: string[]
}
export interface TextractBlock {
BlockType: string
Id: string
Text?: string
TextType?: string
Confidence?: number
Geometry?: TextractGeometry
Relationships?: TextractRelationship[]
Page?: number
EntityTypes?: string[]
SelectionStatus?: string
RowIndex?: number
ColumnIndex?: number
RowSpan?: number
ColumnSpan?: number
Query?: {
Text: string
Alias?: string
Pages?: string[]
}
}
export interface TextractDocumentMetadataRaw {
Pages: number
}
export interface TextractDocumentMetadata {
pages: number
}
export interface TextractApiResponse {
Blocks: TextractBlock[]
DocumentMetadata: TextractDocumentMetadataRaw
AnalyzeDocumentModelVersion?: string
DetectDocumentTextModelVersion?: string
}
export interface TextractNormalizedOutput {
blocks: TextractBlock[]
documentMetadata: TextractDocumentMetadata
modelVersion?: string
}
export interface TextractAsyncJobResponse {
JobStatus: 'IN_PROGRESS' | 'SUCCEEDED' | 'FAILED' | 'PARTIAL_SUCCESS'
StatusMessage?: string
Blocks?: TextractBlock[]
DocumentMetadata?: TextractDocumentMetadataRaw
NextToken?: string
AnalyzeDocumentModelVersion?: string
DetectDocumentTextModelVersion?: string
}
export interface TextractStartJobResponse {
JobId: string
}
export interface TextractParserOutput extends ToolResponse {
output: TextractNormalizedOutput
}