@@ -13,12 +13,25 @@ import { getCopilotModel } from '@/lib/copilot/config'
1313import { TITLE_GENERATION_SYSTEM_PROMPT , TITLE_GENERATION_USER_PROMPT } from '@/lib/copilot/prompts'
1414import { env } from '@/lib/env'
1515import { createLogger } from '@/lib/logs/console/logger'
16+ import { downloadFile } from '@/lib/uploads'
17+ import { downloadFromS3WithConfig } from '@/lib/uploads/s3/s3-client'
18+ import { S3_COPILOT_CONFIG , USE_S3_STORAGE } from '@/lib/uploads/setup'
1619import { db } from '@/db'
1720import { copilotChats } from '@/db/schema'
1821import { executeProviderRequest } from '@/providers'
22+ import { createAnthropicFileContent , isSupportedFileType } from './file-utils'
1923
2024const logger = createLogger ( 'CopilotChatAPI' )
2125
26+ // Schema for file attachments
27+ const FileAttachmentSchema = z . object ( {
28+ id : z . string ( ) ,
29+ s3_key : z . string ( ) ,
30+ filename : z . string ( ) ,
31+ media_type : z . string ( ) ,
32+ size : z . number ( ) ,
33+ } )
34+
2235// Schema for chat messages
2336const ChatMessageSchema = z . object ( {
2437 message : z . string ( ) . min ( 1 , 'Message is required' ) ,
@@ -29,6 +42,7 @@ const ChatMessageSchema = z.object({
2942 createNewChat : z . boolean ( ) . optional ( ) . default ( false ) ,
3043 stream : z . boolean ( ) . optional ( ) . default ( true ) ,
3144 implicitFeedback : z . string ( ) . optional ( ) ,
45+ fileAttachments : z . array ( FileAttachmentSchema ) . optional ( ) ,
3246} )
3347
3448// Sim Agent API configuration
@@ -145,6 +159,7 @@ export async function POST(req: NextRequest) {
145159 createNewChat,
146160 stream,
147161 implicitFeedback,
162+ fileAttachments,
148163 } = ChatMessageSchema . parse ( body )
149164
150165 logger . info ( `[${ tracker . requestId } ] Processing copilot chat request` , {
@@ -195,15 +210,91 @@ export async function POST(req: NextRequest) {
195210 }
196211 }
197212
213+ // Process file attachments if present
214+ const processedFileContents : any [ ] = [ ]
215+ if ( fileAttachments && fileAttachments . length > 0 ) {
216+ logger . info ( `[${ tracker . requestId } ] Processing ${ fileAttachments . length } file attachments` )
217+
218+ for ( const attachment of fileAttachments ) {
219+ try {
220+ // Check if file type is supported
221+ if ( ! isSupportedFileType ( attachment . media_type ) ) {
222+ logger . warn ( `[${ tracker . requestId } ] Unsupported file type: ${ attachment . media_type } ` )
223+ continue
224+ }
225+
226+ // Download file from S3
227+ logger . info ( `[${ tracker . requestId } ] Downloading file: ${ attachment . s3_key } ` )
228+ let fileBuffer : Buffer
229+ if ( USE_S3_STORAGE ) {
230+ fileBuffer = await downloadFromS3WithConfig ( attachment . s3_key , S3_COPILOT_CONFIG )
231+ } else {
232+ // Fallback to generic downloadFile for other storage providers
233+ fileBuffer = await downloadFile ( attachment . s3_key )
234+ }
235+
236+ // Convert to Anthropic format
237+ const fileContent = createAnthropicFileContent ( fileBuffer , attachment . media_type )
238+ if ( fileContent ) {
239+ processedFileContents . push ( fileContent )
240+ logger . info (
241+ `[${ tracker . requestId } ] Processed file: ${ attachment . filename } (${ attachment . media_type } )`
242+ )
243+ }
244+ } catch ( error ) {
245+ logger . error (
246+ `[${ tracker . requestId } ] Failed to process file ${ attachment . filename } :` ,
247+ error
248+ )
249+ // Continue processing other files
250+ }
251+ }
252+ }
253+
198254 // Build messages array for sim agent with conversation history
199255 const messages = [ ]
200256
201- // Add conversation history
257+ // Add conversation history (need to rebuild these with file support if they had attachments)
202258 for ( const msg of conversationHistory ) {
203- messages . push ( {
204- role : msg . role ,
205- content : msg . content ,
206- } )
259+ if ( msg . fileAttachments && msg . fileAttachments . length > 0 ) {
260+ // This is a message with file attachments - rebuild with content array
261+ const content : any [ ] = [ { type : 'text' , text : msg . content } ]
262+
263+ // Process file attachments for historical messages
264+ for ( const attachment of msg . fileAttachments ) {
265+ try {
266+ if ( isSupportedFileType ( attachment . media_type ) ) {
267+ let fileBuffer : Buffer
268+ if ( USE_S3_STORAGE ) {
269+ fileBuffer = await downloadFromS3WithConfig ( attachment . s3_key , S3_COPILOT_CONFIG )
270+ } else {
271+ // Fallback to generic downloadFile for other storage providers
272+ fileBuffer = await downloadFile ( attachment . s3_key )
273+ }
274+ const fileContent = createAnthropicFileContent ( fileBuffer , attachment . media_type )
275+ if ( fileContent ) {
276+ content . push ( fileContent )
277+ }
278+ }
279+ } catch ( error ) {
280+ logger . error (
281+ `[${ tracker . requestId } ] Failed to process historical file ${ attachment . filename } :` ,
282+ error
283+ )
284+ }
285+ }
286+
287+ messages . push ( {
288+ role : msg . role ,
289+ content,
290+ } )
291+ } else {
292+ // Regular text-only message
293+ messages . push ( {
294+ role : msg . role ,
295+ content : msg . content ,
296+ } )
297+ }
207298 }
208299
209300 // Add implicit feedback if provided
@@ -214,11 +305,27 @@ export async function POST(req: NextRequest) {
214305 } )
215306 }
216307
217- // Add current user message
218- messages . push ( {
219- role : 'user' ,
220- content : message ,
221- } )
308+ // Add current user message with file attachments
309+ if ( processedFileContents . length > 0 ) {
310+ // Message with files - use content array format
311+ const content : any [ ] = [ { type : 'text' , text : message } ]
312+
313+ // Add file contents
314+ for ( const fileContent of processedFileContents ) {
315+ content . push ( fileContent )
316+ }
317+
318+ messages . push ( {
319+ role : 'user' ,
320+ content,
321+ } )
322+ } else {
323+ // Text-only message
324+ messages . push ( {
325+ role : 'user' ,
326+ content : message ,
327+ } )
328+ }
222329
223330 // Start title generation in parallel if this is a new chat with first message
224331 if ( actualChatId && ! currentChat ?. title && conversationHistory . length === 0 ) {
@@ -270,6 +377,7 @@ export async function POST(req: NextRequest) {
270377 role : 'user' ,
271378 content : message ,
272379 timestamp : new Date ( ) . toISOString ( ) ,
380+ ...( fileAttachments && fileAttachments . length > 0 && { fileAttachments } ) ,
273381 }
274382
275383 // Create a pass-through stream that captures the response
@@ -590,6 +698,7 @@ export async function POST(req: NextRequest) {
590698 role : 'user' ,
591699 content : message ,
592700 timestamp : new Date ( ) . toISOString ( ) ,
701+ ...( fileAttachments && fileAttachments . length > 0 && { fileAttachments } ) ,
593702 }
594703
595704 const assistantMessage = {
0 commit comments