Skip to content

Commit 251bd3b

Browse files
authored
Merge branch 'staging' into feat/workspace-files-folders
2 parents f6c44af + 214355b commit 251bd3b

18 files changed

Lines changed: 435 additions & 31 deletions

File tree

apps/sim/app/api/mothership/execute/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
5151
chatId,
5252
messageId: providedMessageId,
5353
requestId: providedRequestId,
54+
fileAttachments,
5455
workflowId,
5556
executionId,
5657
} = validation.data.body
@@ -89,6 +90,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
8990
messageId,
9091
isHosted: true,
9192
workspaceContext: workspaceContextWithMothershipTools,
93+
...(fileAttachments && fileAttachments.length > 0 ? { fileAttachments } : {}),
9294
...(integrationTools.length > 0 ? { integrationTools } : {}),
9395
...(mothershipToolRuntime.tools.length > 0
9496
? { mothershipTools: mothershipToolRuntime.tools }

apps/sim/app/api/tools/file/manage/route.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1010
import { ensureWorkspaceFileFolderPath } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager'
1111
import {
1212
fetchWorkspaceFileBuffer,
13-
resolveWorkspaceFileReference,
13+
getWorkspaceFile,
14+
getWorkspaceFileByName,
1415
updateWorkspaceFileContent,
1516
uploadWorkspaceFile,
1617
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
@@ -42,6 +43,51 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4243

4344
try {
4445
switch (body.operation) {
46+
case 'get': {
47+
const { fileId, fileInput } = body
48+
const selectedFileId =
49+
fileId ||
50+
(fileInput && typeof fileInput === 'object' && !Array.isArray(fileInput)
51+
? typeof fileInput.id === 'string'
52+
? fileInput.id
53+
: typeof fileInput.fileId === 'string'
54+
? fileInput.fileId
55+
: ''
56+
: '')
57+
58+
if (!selectedFileId) {
59+
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
60+
}
61+
62+
const file = await getWorkspaceFile(workspaceId, selectedFileId)
63+
if (!file) {
64+
return NextResponse.json(
65+
{ success: false, error: `File not found: "${selectedFileId}"` },
66+
{ status: 404 }
67+
)
68+
}
69+
70+
logger.info('File retrieved', {
71+
fileId: file.id,
72+
name: file.name,
73+
})
74+
75+
return NextResponse.json({
76+
success: true,
77+
data: {
78+
file: {
79+
id: file.id,
80+
name: file.name,
81+
url: ensureAbsoluteUrl(file.path),
82+
size: file.size,
83+
type: file.type,
84+
key: file.key,
85+
context: 'workspace',
86+
},
87+
},
88+
})
89+
}
90+
4591
case 'write': {
4692
const { fileName, content, contentType } = body
4793
const { folderSegments, leafName } = splitWorkspaceFilePath(fileName)

apps/sim/blocks/blocks/file.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
266266
type: 'dropdown' as SubBlockType,
267267
options: [
268268
{ label: 'Read', id: 'file_parser_v3' },
269+
{ label: 'Get', id: 'file_get' },
269270
{ label: 'Write', id: 'file_write' },
270271
{ label: 'Append', id: 'file_append' },
271272
],
@@ -294,6 +295,28 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
294295
required: { field: 'operation', value: 'file_parser_v3' },
295296
condition: { field: 'operation', value: 'file_parser_v3' },
296297
},
298+
{
299+
id: 'getFile',
300+
title: 'File',
301+
type: 'file-upload' as SubBlockType,
302+
canonicalParamId: 'getFileInput',
303+
acceptedTypes: '*',
304+
placeholder: 'Select a workspace file',
305+
multiple: false,
306+
mode: 'basic',
307+
condition: { field: 'operation', value: 'file_get' },
308+
required: { field: 'operation', value: 'file_get' },
309+
},
310+
{
311+
id: 'getFileId',
312+
title: 'File ID',
313+
type: 'short-input' as SubBlockType,
314+
canonicalParamId: 'getFileInput',
315+
placeholder: 'Workspace file ID',
316+
mode: 'advanced',
317+
condition: { field: 'operation', value: 'file_get' },
318+
required: { field: 'operation', value: 'file_get' },
319+
},
297320
{
298321
id: 'fileName',
299322
title: 'File Name',
@@ -349,7 +372,7 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
349372
},
350373
],
351374
tools: {
352-
access: ['file_parser_v3', 'file_write', 'file_append'],
375+
access: ['file_parser_v3', 'file_get', 'file_write', 'file_append'],
353376
config: {
354377
tool: (params) => params.operation || 'file_parser_v3',
355378
params: (params) => {
@@ -390,6 +413,25 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
390413
}
391414
}
392415

416+
if (operation === 'file_get') {
417+
const getInput = params.getFileInput
418+
if (!getInput) {
419+
throw new Error('File is required for get')
420+
}
421+
422+
if (typeof getInput === 'string') {
423+
return {
424+
fileId: getInput.trim(),
425+
workspaceId: params._context?.workspaceId,
426+
}
427+
}
428+
429+
return {
430+
fileInput: normalizeFileInput(getInput, { single: true }),
431+
workspaceId: params._context?.workspaceId,
432+
}
433+
}
434+
393435
const fileInput = params.fileInput
394436
if (!fileInput) {
395437
logger.error('No file input provided')
@@ -428,9 +470,13 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
428470
},
429471
},
430472
inputs: {
431-
operation: { type: 'string', description: 'Operation to perform (read, write, or append)' },
473+
operation: {
474+
type: 'string',
475+
description: 'Operation to perform (read, get, write, or append)',
476+
},
432477
fileInput: { type: 'json', description: 'File input for read' },
433478
fileType: { type: 'string', description: 'File type for read' },
479+
getFileInput: { type: 'json', description: 'Selected file or workspace file ID for get' },
434480
fileName: { type: 'string', description: 'Name for a new file (write)' },
435481
content: { type: 'string', description: 'File content to write' },
436482
contentType: { type: 'string', description: 'MIME content type for write' },
@@ -446,6 +492,10 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
446492
type: 'string',
447493
description: 'All file contents merged into a single text string (read)',
448494
},
495+
file: {
496+
type: 'file',
497+
description: 'Workspace file object (get)',
498+
},
449499
id: {
450500
type: 'string',
451501
description: 'File ID (write)',

apps/sim/blocks/blocks/mothership.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ export const MothershipBlock: BlockConfig<MothershipResponse> = {
4141
type: 'short-input',
4242
placeholder: 'e.g., user-123, session-abc, customer-456',
4343
},
44+
{
45+
id: 'attachmentFiles',
46+
title: 'Attachments',
47+
type: 'file-upload',
48+
canonicalParamId: 'files',
49+
placeholder: 'Upload files to attach',
50+
mode: 'basic',
51+
multiple: true,
52+
required: false,
53+
},
54+
{
55+
id: 'fileReferences',
56+
title: 'Attachments',
57+
type: 'short-input',
58+
canonicalParamId: 'files',
59+
placeholder: 'Reference files from previous blocks',
60+
mode: 'advanced',
61+
required: false,
62+
},
4463
],
4564
tools: {
4665
access: [],
@@ -54,6 +73,10 @@ export const MothershipBlock: BlockConfig<MothershipResponse> = {
5473
type: 'string',
5574
description: 'Mothership chat ID to continue; generated when omitted',
5675
},
76+
files: {
77+
type: 'file',
78+
description: 'Files to send to Mothership as attachments',
79+
},
5780
},
5881
outputs: {
5982
content: { type: 'string', description: 'Generated response content' },

apps/sim/executor/handlers/mothership/mothership-handler.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ const {
1313
mockGenerateId,
1414
mockIsExecutionCancelled,
1515
mockIsRedisCancellationEnabled,
16+
mockReadUserFileContent,
1617
} = vi.hoisted(() => ({
1718
mockBuildAuthHeaders: vi.fn(),
1819
mockBuildAPIUrl: vi.fn(),
1920
mockExtractAPIErrorMessage: vi.fn(),
2021
mockGenerateId: vi.fn(),
2122
mockIsExecutionCancelled: vi.fn(),
2223
mockIsRedisCancellationEnabled: vi.fn(),
24+
mockReadUserFileContent: vi.fn(),
2325
}))
2426

2527
vi.mock('@/executor/utils/http', () => ({
@@ -37,6 +39,10 @@ vi.mock('@/lib/execution/cancellation', () => ({
3739
isRedisCancellationEnabled: mockIsRedisCancellationEnabled,
3840
}))
3941

42+
vi.mock('@/lib/execution/payloads/materialization.server', () => ({
43+
readUserFileContent: mockReadUserFileContent,
44+
}))
45+
4046
function createAbortError(): Error {
4147
const error = new Error('The operation was aborted')
4248
error.name = 'AbortError'
@@ -78,13 +84,14 @@ describe('MothershipBlockHandler', () => {
7884
mockIsExecutionCancelled.mockReset()
7985
mockIsRedisCancellationEnabled.mockReset()
8086
mockIsRedisCancellationEnabled.mockReturnValue(false)
87+
mockReadUserFileContent.mockReset()
8188

8289
block = {
8390
id: 'mothership-block-1',
8491
metadata: { id: BlockType.MOTHERSHIP, name: 'Mothership' },
8592
position: { x: 0, y: 0 },
8693
config: { tool: BlockType.MOTHERSHIP, params: {} },
87-
inputs: { prompt: 'string', conversationId: 'string' },
94+
inputs: { prompt: 'string', conversationId: 'string', files: 'file[]' },
8895
outputs: {},
8996
enabled: true,
9097
} as SerializedBlock
@@ -212,6 +219,80 @@ describe('MothershipBlockHandler', () => {
212219
expect(mockGenerateId).toHaveBeenCalledTimes(2)
213220
})
214221

222+
it('embeds attached files for the mothership execute request', async () => {
223+
const fileContent = Buffer.from('hello mothership', 'utf8').toString('base64')
224+
mockGenerateId.mockReturnValueOnce('chat-uuid')
225+
mockGenerateId.mockReturnValueOnce('message-uuid')
226+
mockGenerateId.mockReturnValueOnce('request-uuid')
227+
mockReadUserFileContent.mockResolvedValueOnce(fileContent)
228+
229+
fetchMock.mockResolvedValue(
230+
new Response(
231+
JSON.stringify({
232+
content: 'analyzed',
233+
model: 'mothership',
234+
conversationId: 'chat-uuid',
235+
tokens: {},
236+
toolCalls: [],
237+
}),
238+
{
239+
status: 200,
240+
headers: { 'Content-Type': 'application/json' },
241+
}
242+
)
243+
)
244+
245+
const result = await handler.execute(context, block, {
246+
prompt: 'Analyze this file',
247+
files: [
248+
{
249+
name: 'notes.txt',
250+
key: 'workspace/workspace-1/notes.txt',
251+
size: 16,
252+
type: 'text/plain',
253+
},
254+
],
255+
})
256+
257+
expect(result).toMatchObject({
258+
content: 'analyzed',
259+
model: 'mothership',
260+
conversationId: 'chat-uuid',
261+
})
262+
expect(mockReadUserFileContent).toHaveBeenCalledWith(
263+
expect.objectContaining({
264+
id: expect.stringMatching(/^file-/),
265+
key: 'workspace/workspace-1/notes.txt',
266+
name: 'notes.txt',
267+
url: '',
268+
size: 16,
269+
type: 'text/plain',
270+
}),
271+
expect.objectContaining({
272+
encoding: 'base64',
273+
userId: 'user-1',
274+
workspaceId: 'workspace-1',
275+
workflowId: 'workflow-1',
276+
executionId: 'execution-1',
277+
requestId: 'request-uuid',
278+
})
279+
)
280+
281+
const [, options] = fetchMock.mock.calls[0] as [string, RequestInit]
282+
const body = JSON.parse(String(options.body))
283+
expect(body.fileAttachments).toEqual([
284+
{
285+
type: 'document',
286+
source: {
287+
type: 'base64',
288+
media_type: 'text/plain',
289+
data: fileContent,
290+
},
291+
filename: 'notes.txt',
292+
},
293+
])
294+
})
295+
215296
it('propagates local aborts to the mothership request', async () => {
216297
const abortController = new AbortController()
217298
context.abortSignal = abortController.signal

0 commit comments

Comments
 (0)