@@ -5,12 +5,20 @@ import { BlockType, isMcpTool } from '@/executor/constants'
55import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
66import type { ExecutionContext , StreamingExecution } from '@/executor/types'
77import { executeProviderRequest } from '@/providers'
8- import { getProviderFromModel , transformBlockTool } from '@/providers/utils'
8+ import {
9+ getProviderFromModel ,
10+ supportsFileAttachments ,
11+ transformBlockTool ,
12+ } from '@/providers/utils'
913import type { SerializedBlock , SerializedWorkflow } from '@/serializer/types'
1014import { executeTool } from '@/tools'
1115
1216process . env . NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
1317
18+ const { mockReadUserFileContent } = vi . hoisted ( ( ) => ( {
19+ mockReadUserFileContent : vi . fn ( ) ,
20+ } ) )
21+
1422vi . mock ( '@/lib/core/config/feature-flags' , ( ) => ( {
1523 isHosted : false ,
1624 isProd : false ,
@@ -26,6 +34,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
2634
2735vi . mock ( '@/providers/utils' , ( ) => ( {
2836 getProviderFromModel : vi . fn ( ) . mockReturnValue ( 'mock-provider' ) ,
37+ supportsFileAttachments : vi . fn ( ) . mockReturnValue ( false ) ,
2938 transformBlockTool : vi . fn ( ) ,
3039 getBaseModelProviders : vi . fn ( ) . mockReturnValue ( { openai : { } , anthropic : { } } ) ,
3140 getApiKey : vi . fn ( ) . mockReturnValue ( 'mock-api-key' ) ,
@@ -45,6 +54,10 @@ vi.mock('@/providers/utils', () => ({
4554 } ) ,
4655} ) )
4756
57+ vi . mock ( '@/lib/execution/payloads/materialization.server' , ( ) => ( {
58+ readUserFileContent : mockReadUserFileContent ,
59+ } ) )
60+
4861vi . mock ( '@/blocks' , ( ) => ( {
4962 getAllBlocks : vi . fn ( ) . mockReturnValue ( [ ] ) ,
5063} ) )
@@ -113,6 +126,7 @@ setupGlobalFetchMock()
113126const mockGetAllBlocks = getAllBlocks as Mock
114127const mockExecuteTool = executeTool as Mock
115128const mockGetProviderFromModel = getProviderFromModel as Mock
129+ const mockSupportsFileAttachments = supportsFileAttachments as Mock
116130const mockTransformBlockTool = transformBlockTool as Mock
117131const mockFetch = global . fetch as unknown as Mock
118132const mockExecuteProviderRequest = executeProviderRequest as Mock
@@ -164,6 +178,8 @@ describe('AgentBlockHandler', () => {
164178 } as SerializedWorkflow ,
165179 }
166180 mockGetProviderFromModel . mockReturnValue ( 'mock-provider' )
181+ mockSupportsFileAttachments . mockReturnValue ( false )
182+ mockReadUserFileContent . mockResolvedValue ( 'ZmlsZQ==' )
167183
168184 mockExecuteProviderRequest . mockResolvedValue ( {
169185 content : 'Mocked response content' ,
@@ -1121,6 +1137,113 @@ describe('AgentBlockHandler', () => {
11211137 expect ( requestBody . messages [ 6 ] . content ) . toBe ( 'Continue our conversation.' )
11221138 } )
11231139
1140+ it ( 'should pass files messages as provider file attachments when the model supports files' , async ( ) => {
1141+ const inputs = {
1142+ model : 'gpt-4o' ,
1143+ messages : [
1144+ { role : 'user' as const , content : 'Summarize the attached file.' } ,
1145+ {
1146+ role : 'files' as const ,
1147+ files : [
1148+ {
1149+ name : 'brief.pdf' ,
1150+ path : '/api/files/serve/test-workspace/brief.pdf' ,
1151+ key : 'workspace/test-workspace/brief.pdf' ,
1152+ size : 128 ,
1153+ type : 'application/pdf' ,
1154+ } ,
1155+ ] ,
1156+ } ,
1157+ ] ,
1158+ apiKey : 'test-api-key' ,
1159+ }
1160+
1161+ mockGetProviderFromModel . mockReturnValue ( 'openai' )
1162+ mockSupportsFileAttachments . mockReturnValue ( true )
1163+ mockReadUserFileContent . mockResolvedValue ( 'cGRm' )
1164+
1165+ await handler . execute (
1166+ {
1167+ ...mockContext ,
1168+ userId : 'test-user' ,
1169+ workspaceId : 'test-workspace' ,
1170+ executionId : 'test-execution' ,
1171+ } ,
1172+ mockBlock ,
1173+ inputs
1174+ )
1175+
1176+ const requestBody = mockExecuteProviderRequest . mock . calls [ 0 ] [ 1 ]
1177+
1178+ expect ( requestBody . messages ) . toEqual ( [
1179+ { role : 'user' , content : 'Summarize the attached file.' } ,
1180+ ] )
1181+ expect ( requestBody . fileAttachments ) . toEqual ( [
1182+ {
1183+ name : 'brief.pdf' ,
1184+ type : 'application/pdf' ,
1185+ base64 : 'cGRm' ,
1186+ } ,
1187+ ] )
1188+ expect ( mockReadUserFileContent ) . toHaveBeenCalledWith (
1189+ expect . objectContaining ( {
1190+ name : 'brief.pdf' ,
1191+ key : 'workspace/test-workspace/brief.pdf' ,
1192+ type : 'application/pdf' ,
1193+ } ) ,
1194+ expect . objectContaining ( {
1195+ userId : 'test-user' ,
1196+ workspaceId : 'test-workspace' ,
1197+ executionId : 'test-execution' ,
1198+ encoding : 'base64' ,
1199+ } )
1200+ )
1201+ } )
1202+
1203+ it ( 'should ignore files messages when the selected model does not support files' , async ( ) => {
1204+ const inputs = {
1205+ model : 'deepseek-chat' ,
1206+ messages : [
1207+ { role : 'user' as const , content : 'Summarize the attached file.' } ,
1208+ {
1209+ role : 'files' as const ,
1210+ files : [
1211+ {
1212+ name : 'brief.pdf' ,
1213+ path : '/api/files/serve/test-workspace/brief.pdf' ,
1214+ key : 'workspace/test-workspace/brief.pdf' ,
1215+ size : 128 ,
1216+ type : 'application/pdf' ,
1217+ } ,
1218+ ] ,
1219+ } ,
1220+ ] ,
1221+ apiKey : 'test-api-key' ,
1222+ }
1223+
1224+ mockGetProviderFromModel . mockReturnValue ( 'deepseek' )
1225+ mockSupportsFileAttachments . mockReturnValue ( false )
1226+
1227+ await handler . execute (
1228+ {
1229+ ...mockContext ,
1230+ userId : 'test-user' ,
1231+ workspaceId : 'test-workspace' ,
1232+ executionId : 'test-execution' ,
1233+ } ,
1234+ mockBlock ,
1235+ inputs
1236+ )
1237+
1238+ const requestBody = mockExecuteProviderRequest . mock . calls [ 0 ] [ 1 ]
1239+
1240+ expect ( requestBody . messages ) . toEqual ( [
1241+ { role : 'user' , content : 'Summarize the attached file.' } ,
1242+ ] )
1243+ expect ( requestBody . fileAttachments ) . toBeUndefined ( )
1244+ expect ( mockReadUserFileContent ) . not . toHaveBeenCalled ( )
1245+ } )
1246+
11241247 it ( 'should preserve multiple system messages when no explicit systemPrompt is provided' , async ( ) => {
11251248 const inputs = {
11261249 model : 'gpt-4o' ,
0 commit comments