mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-14 00:15:09 -05:00
* refactor(tool-input): eliminate SyncWrappers, add canonical toggle and dependsOn gating Replace 17+ individual SyncWrapper components with a single centralized ToolSubBlockRenderer that bridges the subblock store with StoredTool.params via synthetic store keys. This reduces ~1000 lines of duplicated wrapper code and ensures tool-input renders subblock components identically to the standalone SubBlock path. - Add ToolSubBlockRenderer with bidirectional store sync - Add basic/advanced mode toggle (ArrowLeftRight) using collaborative functions - Add dependsOn gating via useDependsOnGate (fields disable instead of hiding) - Add paramVisibility field to SubBlockConfig for tool-input visibility control - Pass canonicalModeOverrides through getSubBlocksForToolInput - Show (optional) label for non-user-only fields (LLM can inject at runtime) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): restore optional indicator, fix folder selector and canonical toggle, extract components - Attach resolved paramVisibility to subblocks from getSubBlocksForToolInput - Add labelSuffix prop to SubBlock for "(optional)" badge on user-or-llm params - Fix folder selector missing for tools with canonicalParamId (e.g. Google Drive) - Fix canonical toggle not clickable by letting SubBlock handle dependsOn internally - Extract ParameterWithLabel, ToolSubBlockRenderer, ToolCredentialSelector to components/tools/ - Extract StoredTool interface to types.ts, selection helpers to utils.ts - Remove dead code (mcpError, refreshTools, oldParamIds, initialParams) - Strengthen typing: replace any with proper types on icon components and evaluateParameterCondition * add sibling values to subblock context since subblock store isn't relevant in tool input, and removed unused param * cleanup * fix(tool-input): render uncovered tool params alongside subblocks The SubBlock-first rendering path was hard-returning after rendering subblocks, so tool params without matching subblocks (like inputMapping for workflow tools) were never rendered. Now renders subblocks first, then any remaining displayParams not covered by subblocks via the legacy ParameterWithLabel fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): auto-refresh workflow inputs after redeploy After redeploying a child workflow via the stale badge, the workflow state cache was not invalidated, so WorkflowInputMapperInput kept showing stale input fields until page refresh. Now invalidates workflowKeys.state on deploy success. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): correct workflow selector visibility and tighten (optional) spacing - Set workflowId param to user-only in workflow_executor tool config so "Select Workflow" no longer shows "(optional)" indicator - Tighten (optional) label spacing with -ml-[3px] to counteract parent Label's gap-[6px], making it feel inline with the label text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): align (optional) text to baseline instead of center Use items-baseline instead of items-center on Label flex containers so the smaller (optional) text aligns with the label text baseline rather than sitting slightly below it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): increase top padding of expanded tool body Bump the expanded tool body container's top padding from 8px to 12px for more breathing room between the header bar and the first parameter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): apply extra top padding only to SubBlock-first path Revert container padding to py-[8px] (MCP tools were correct). Wrap SubBlock-first output in a div with pt-[4px] so only registry tools get extra breathing room from the container top. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): increase gap between SubBlock params for visual clarity SubBlock's internal gap (10px between label and input) matched the between-parameter gap (10px), making them indistinguishable. Increase the between-parameter gap to 14px so consecutive parameters are visually distinct, matching the separation seen in ParameterWithLabel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix spacing and optional tag * update styling + move predeploy checks earlier for first time deploys * update change detection to account for synthetic tool ids * fix remaining blocks who had files visibility set to hidden * cleanup * add catch --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
275 lines
9.2 KiB
TypeScript
275 lines
9.2 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
|
|
import type { UserFile } from '@/executor/types'
|
|
import type {
|
|
FileParseApiMultiResponse,
|
|
FileParseApiResponse,
|
|
FileParseResult,
|
|
FileParserInput,
|
|
FileParserOutput,
|
|
FileParserOutputData,
|
|
FileParserV3Output,
|
|
FileParserV3OutputData,
|
|
FileUploadInput,
|
|
} from '@/tools/file/types'
|
|
import type { ToolConfig } from '@/tools/types'
|
|
|
|
const logger = createLogger('FileParserTool')
|
|
|
|
interface ToolBodyParams extends Partial<FileParserInput> {
|
|
files?: FileUploadInput[]
|
|
_context?: {
|
|
workspaceId?: string
|
|
workflowId?: string
|
|
executionId?: string
|
|
}
|
|
}
|
|
|
|
const parseFileParserResponse = async (response: Response): Promise<FileParserOutput> => {
|
|
logger.info('Received response status:', response.status)
|
|
|
|
const result = (await response.json()) as FileParseApiResponse | FileParseApiMultiResponse
|
|
logger.info('Response parsed successfully')
|
|
|
|
// Handle multiple files response
|
|
if ('results' in result) {
|
|
logger.info('Processing multiple files response')
|
|
|
|
// Extract individual file results
|
|
const fileResults: FileParseResult[] = result.results.map((fileResult) => {
|
|
return fileResult.output || (fileResult as unknown as FileParseResult)
|
|
})
|
|
|
|
// Collect UserFile objects from results
|
|
const processedFiles: UserFile[] = fileResults
|
|
.filter((file): file is FileParseResult & { file: UserFile } => Boolean(file.file))
|
|
.map((file) => file.file)
|
|
|
|
// Combine all file contents with clear dividers
|
|
const combinedContent = fileResults
|
|
.map((file, index) => {
|
|
const divider = `\n${'='.repeat(80)}\n`
|
|
|
|
return file.content + (index < fileResults.length - 1 ? divider : '')
|
|
})
|
|
.join('\n')
|
|
|
|
// Create the base output
|
|
const output: FileParserOutputData = {
|
|
files: fileResults,
|
|
combinedContent,
|
|
...(processedFiles.length > 0 && { processedFiles }),
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output,
|
|
}
|
|
}
|
|
|
|
// Handle single file response
|
|
logger.info('Successfully parsed file:', result.output?.name || 'unknown')
|
|
|
|
const fileOutput: FileParseResult = result.output || (result as unknown as FileParseResult)
|
|
|
|
// For a single file, create the output with just array format
|
|
const output: FileParserOutputData = {
|
|
files: [fileOutput],
|
|
combinedContent: fileOutput?.content || result.content || '',
|
|
...(fileOutput?.file && { processedFiles: [fileOutput.file] }),
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output,
|
|
}
|
|
}
|
|
|
|
export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
|
|
id: 'file_parser',
|
|
name: 'File Parser',
|
|
description: 'Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)',
|
|
version: '1.0.0',
|
|
|
|
params: {
|
|
filePath: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'hidden',
|
|
description: 'Path to the file(s). Can be a single path, URL, or an array of paths.',
|
|
},
|
|
file: {
|
|
type: 'file',
|
|
required: false,
|
|
visibility: 'user-only',
|
|
description: 'Uploaded file(s) to parse',
|
|
},
|
|
fileType: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'hidden',
|
|
description: 'Type of file to parse (auto-detected if not specified)',
|
|
},
|
|
},
|
|
|
|
request: {
|
|
url: '/api/files/parse',
|
|
method: 'POST',
|
|
headers: () => ({
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
body: (params: ToolBodyParams) => {
|
|
logger.info('Request parameters received by tool body:', params)
|
|
|
|
if (!params) {
|
|
logger.error('Tool body received no parameters')
|
|
throw new Error('No parameters provided to tool body')
|
|
}
|
|
|
|
let determinedFilePath: string | string[] | null = null
|
|
const determinedFileType: string | undefined = params.fileType
|
|
|
|
const resolveFilePath = (fileInput: unknown): string | null => {
|
|
if (!fileInput || typeof fileInput !== 'object') return null
|
|
|
|
if ('path' in fileInput && typeof (fileInput as { path?: unknown }).path === 'string') {
|
|
return (fileInput as { path: string }).path
|
|
}
|
|
|
|
if ('url' in fileInput && typeof (fileInput as { url?: unknown }).url === 'string') {
|
|
return (fileInput as { url: string }).url
|
|
}
|
|
|
|
if ('key' in fileInput && typeof (fileInput as { key?: unknown }).key === 'string') {
|
|
const fileRecord = fileInput as Record<string, unknown>
|
|
const key = fileRecord.key as string
|
|
const context =
|
|
typeof fileRecord.context === 'string' ? fileRecord.context : inferContextFromKey(key)
|
|
return `/api/files/serve/${encodeURIComponent(key)}?context=${context}`
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Determine the file path(s) based on input parameters.
|
|
// Precedence: direct filePath > file array > single file object > legacy files array
|
|
// 1. Check for direct filePath (URL or single path from upload)
|
|
if (params.filePath) {
|
|
logger.info('Tool body found direct filePath:', params.filePath)
|
|
determinedFilePath = params.filePath
|
|
}
|
|
// 2. Check for file upload (array)
|
|
else if (params.file && Array.isArray(params.file) && params.file.length > 0) {
|
|
logger.info('Tool body processing file array upload')
|
|
const filePaths = params.file
|
|
.map((file) => resolveFilePath(file))
|
|
.filter(Boolean) as string[]
|
|
if (filePaths.length !== params.file.length) {
|
|
throw new Error('Invalid file input: One or more files are missing path or URL')
|
|
}
|
|
determinedFilePath = filePaths
|
|
}
|
|
// 3. Check for file upload (single object)
|
|
else if (params.file && !Array.isArray(params.file)) {
|
|
logger.info('Tool body processing single file object upload')
|
|
const resolvedPath = resolveFilePath(params.file)
|
|
if (!resolvedPath) {
|
|
throw new Error('Invalid file input: Missing path or URL')
|
|
}
|
|
determinedFilePath = resolvedPath
|
|
}
|
|
// 4. Check for deprecated multiple files case (from older blocks?)
|
|
else if (params.files && Array.isArray(params.files)) {
|
|
logger.info('Tool body processing legacy files array:', params.files.length)
|
|
if (params.files.length > 0) {
|
|
const filePaths = params.files
|
|
.map((file) => resolveFilePath(file))
|
|
.filter(Boolean) as string[]
|
|
if (filePaths.length !== params.files.length) {
|
|
throw new Error('Invalid file input: One or more files are missing path or URL')
|
|
}
|
|
determinedFilePath = filePaths
|
|
} else {
|
|
logger.warn('Legacy files array provided but is empty')
|
|
}
|
|
}
|
|
|
|
// Final check if filePath was determined
|
|
if (!determinedFilePath) {
|
|
logger.error('Tool body could not determine filePath from parameters:', params)
|
|
throw new Error('Missing required parameter: filePath')
|
|
}
|
|
|
|
logger.info('Tool body determined filePath:', determinedFilePath)
|
|
return {
|
|
filePath: determinedFilePath,
|
|
fileType: determinedFileType,
|
|
workspaceId: params.workspaceId || params._context?.workspaceId,
|
|
workflowId: params._context?.workflowId,
|
|
executionId: params._context?.executionId,
|
|
}
|
|
},
|
|
},
|
|
|
|
transformResponse: parseFileParserResponse,
|
|
|
|
outputs: {
|
|
files: { type: 'array', description: 'Array of parsed files with content and metadata' },
|
|
combinedContent: { type: 'string', description: 'Combined content of all parsed files' },
|
|
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: parseFileParserResponse,
|
|
|
|
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',
|
|
},
|
|
},
|
|
}
|
|
|
|
export const fileParserV3Tool: ToolConfig<FileParserInput, FileParserV3Output> = {
|
|
id: 'file_parser_v3',
|
|
name: 'File Parser',
|
|
description: 'Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)',
|
|
version: '3.0.0',
|
|
params: fileParserTool.params,
|
|
request: fileParserTool.request,
|
|
transformResponse: async (response: Response): Promise<FileParserV3Output> => {
|
|
const parsed = await parseFileParserResponse(response)
|
|
const output = parsed.output as FileParserOutputData
|
|
const files =
|
|
Array.isArray(output.processedFiles) && output.processedFiles.length > 0
|
|
? output.processedFiles
|
|
: []
|
|
|
|
const cleanedOutput: FileParserV3OutputData = {
|
|
files,
|
|
combinedContent: output.combinedContent,
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output: cleanedOutput,
|
|
}
|
|
},
|
|
outputs: {
|
|
files: { type: 'file[]', description: 'Parsed files as UserFile objects' },
|
|
combinedContent: { type: 'string', description: 'Combined content of all parsed files' },
|
|
},
|
|
}
|