4
4
Constants,
5
5
RunStatus,
6
6
CacheKeys,
7
+ FileSources,
7
8
ContentTypes,
8
9
EModelEndpoint,
9
10
ViolationTypes,
11
+ ImageVisionTool,
10
12
AssistantStreamEvents,
11
13
} = require ( 'librechat-data-provider' ) ;
12
14
const {
@@ -17,9 +19,10 @@ const {
17
19
addThreadMetadata,
18
20
saveAssistantMessage,
19
21
} = require ( '~/server/services/Threads' ) ;
22
+ const { sendResponse, sendMessage, sleep, isEnabled, countTokens } = require ( '~/server/utils' ) ;
20
23
const { runAssistant, createOnTextProgress } = require ( '~/server/services/AssistantService' ) ;
21
24
const { addTitle, initializeClient } = require ( '~/server/services/Endpoints/assistants' ) ;
22
- const { sendResponse , sendMessage , sleep , isEnabled , countTokens } = require ( '~/server/utils ' ) ;
25
+ const { formatMessage , createVisionPrompt } = require ( '~/app/clients/prompts ' ) ;
23
26
const { createRun, StreamRunManager } = require ( '~/server/services/Runs' ) ;
24
27
const { getTransactions } = require ( '~/models/Transaction' ) ;
25
28
const checkBalance = require ( '~/models/checkBalance' ) ;
@@ -100,6 +103,16 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
100
103
let parentMessageId = _parentId ;
101
104
/** @type {TMessage[] } */
102
105
let previousMessages = [ ] ;
106
+ /** @type {import('librechat-data-provider').TConversation | null } */
107
+ let conversation = null ;
108
+ /** @type {string[] } */
109
+ let file_ids = [ ] ;
110
+ /** @type {Set<string> } */
111
+ let attachedFileIds = new Set ( ) ;
112
+ /** @type {TMessage | null } */
113
+ let requestMessage = null ;
114
+ /** @type {undefined | Promise<ChatCompletion> } */
115
+ let visionPromise ;
103
116
104
117
const userMessageId = v4 ( ) ;
105
118
const responseMessageId = v4 ( ) ;
@@ -258,7 +271,10 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
258
271
throw new Error ( 'Missing assistant_id' ) ;
259
272
}
260
273
261
- if ( isEnabled ( process . env . CHECK_BALANCE ) ) {
274
+ const checkBalanceBeforeRun = async ( ) => {
275
+ if ( ! isEnabled ( process . env . CHECK_BALANCE ) ) {
276
+ return ;
277
+ }
262
278
const transactions =
263
279
( await getTransactions ( {
264
280
user : req . user . id ,
@@ -288,7 +304,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
288
304
amount : promptTokens ,
289
305
} ,
290
306
} ) ;
291
- }
307
+ } ;
292
308
293
309
/** @type {{ openai: OpenAIClient } } */
294
310
const { openai : _openai , client } = await initializeClient ( {
@@ -300,103 +316,168 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
300
316
301
317
openai = _openai ;
302
318
303
- // if (thread_id) {
304
- // previousMessages = await checkMessageGaps({ openai, thread_id, conversationId });
305
- // }
306
-
307
319
if ( previousMessages . length ) {
308
320
parentMessageId = previousMessages [ previousMessages . length - 1 ] . messageId ;
309
321
}
310
322
311
- const userMessage = {
323
+ let userMessage = {
312
324
role : 'user' ,
313
325
content : text ,
314
326
metadata : {
315
327
messageId : userMessageId ,
316
328
} ,
317
329
} ;
318
330
319
- let thread_file_ids = [ ] ;
320
- if ( convoId ) {
321
- const convo = await getConvo ( req . user . id , convoId ) ;
322
- if ( convo && convo . file_ids ) {
323
- thread_file_ids = convo . file_ids ;
324
- }
331
+ /** @type {CreateRunBody | undefined } */
332
+ const body = {
333
+ assistant_id,
334
+ model,
335
+ } ;
336
+
337
+ if ( promptPrefix ) {
338
+ body . additional_instructions = promptPrefix ;
325
339
}
326
340
327
- const file_ids = files . map ( ( { file_id } ) => file_id ) ;
328
- if ( file_ids . length || thread_file_ids . length ) {
329
- userMessage . file_ids = file_ids ;
330
- openai . attachedFileIds = new Set ( [ ...file_ids , ...thread_file_ids ] ) ;
341
+ if ( instructions ) {
342
+ body . instructions = instructions ;
331
343
}
332
344
333
- // TODO: may allow multiple messages to be created beforehand in a future update
334
- const initThreadBody = {
335
- messages : [ userMessage ] ,
336
- metadata : {
337
- user : req . user . id ,
338
- conversationId,
339
- } ,
345
+ const getRequestFileIds = async ( ) => {
346
+ let thread_file_ids = [ ] ;
347
+ if ( convoId ) {
348
+ const convo = await getConvo ( req . user . id , convoId ) ;
349
+ if ( convo && convo . file_ids ) {
350
+ thread_file_ids = convo . file_ids ;
351
+ }
352
+ }
353
+
354
+ file_ids = files . map ( ( { file_id } ) => file_id ) ;
355
+ if ( file_ids . length || thread_file_ids . length ) {
356
+ userMessage . file_ids = file_ids ;
357
+ attachedFileIds = new Set ( [ ...file_ids , ...thread_file_ids ] ) ;
358
+ }
340
359
} ;
341
360
342
- const result = await initThread ( { openai, body : initThreadBody , thread_id } ) ;
343
- thread_id = result . thread_id ;
361
+ const addVisionPrompt = async ( ) => {
362
+ if ( ! req . body . endpointOption . attachments ) {
363
+ return ;
364
+ }
344
365
345
- createOnTextProgress ( {
346
- openai,
347
- conversationId,
348
- userMessageId,
349
- messageId : responseMessageId ,
350
- thread_id,
351
- } ) ;
366
+ const assistant = await openai . beta . assistants . retrieve ( assistant_id ) ;
367
+ const visionToolIndex = assistant . tools . findIndex (
368
+ ( tool ) => tool . function . name === ImageVisionTool . function . name ,
369
+ ) ;
352
370
353
- const requestMessage = {
354
- user : req . user . id ,
355
- text,
356
- messageId : userMessageId ,
357
- parentMessageId,
358
- // TODO: make sure client sends correct format for `files`, use zod
359
- files,
360
- file_ids,
361
- conversationId,
362
- isCreatedByUser : true ,
363
- assistant_id,
364
- thread_id,
365
- model : assistant_id ,
366
- } ;
371
+ if ( visionToolIndex === - 1 ) {
372
+ return ;
373
+ }
367
374
368
- previousMessages . push ( requestMessage ) ;
375
+ const attachments = await req . body . endpointOption . attachments ;
376
+ let visionMessage = {
377
+ role : 'user' ,
378
+ content : '' ,
379
+ } ;
380
+ const files = await client . addImageURLs ( visionMessage , attachments ) ;
381
+ if ( ! visionMessage . image_urls ?. length ) {
382
+ return ;
383
+ }
369
384
370
- await saveUserMessage ( { ...requestMessage , model } ) ;
385
+ const imageCount = visionMessage . image_urls . length ;
386
+ const plural = imageCount > 1 ;
387
+ visionMessage . content = createVisionPrompt ( plural ) ;
388
+ visionMessage = formatMessage ( { message : visionMessage , endpoint : EModelEndpoint . openAI } ) ;
371
389
372
- const conversation = {
373
- conversationId,
374
- // TODO: title feature
375
- title : 'New Chat' ,
376
- endpoint : EModelEndpoint . assistants ,
377
- promptPrefix : promptPrefix ,
378
- instructions : instructions ,
379
- assistant_id,
380
- // model,
381
- } ;
390
+ visionPromise = openai . chat . completions . create ( {
391
+ model : 'gpt-4-vision-preview' ,
392
+ messages : [ visionMessage ] ,
393
+ max_tokens : 4000 ,
394
+ } ) ;
382
395
383
- if ( file_ids . length ) {
384
- conversation . file_ids = file_ids ;
385
- }
396
+ const pluralized = plural ? 's' : '' ;
397
+ body . additional_instructions = `${
398
+ body . additional_instructions ? `${ body . additional_instructions } \n` : ''
399
+ } The user has uploaded ${ imageCount } image${ pluralized } .
400
+ Use the \`${ ImageVisionTool . function . name } \` tool to retrieve ${
401
+ plural ? '' : 'a '
402
+ } detailed text description${ pluralized } for ${ plural ? 'each' : 'the' } image${ pluralized } .`;
386
403
387
- /** @type {CreateRunBody } */
388
- const body = {
389
- assistant_id,
390
- model,
404
+ return files ;
391
405
} ;
392
406
393
- if ( promptPrefix ) {
394
- body . additional_instructions = promptPrefix ;
395
- }
407
+ const initializeThread = async ( ) => {
408
+ /** @type {[ undefined | MongoFile[]] }*/
409
+ const [ processedFiles ] = await Promise . all ( [ addVisionPrompt ( ) , getRequestFileIds ( ) ] ) ;
410
+ // TODO: may allow multiple messages to be created beforehand in a future update
411
+ const initThreadBody = {
412
+ messages : [ userMessage ] ,
413
+ metadata : {
414
+ user : req . user . id ,
415
+ conversationId,
416
+ } ,
417
+ } ;
396
418
397
- if ( instructions ) {
398
- body . instructions = instructions ;
399
- }
419
+ if ( processedFiles ) {
420
+ for ( const file of processedFiles ) {
421
+ if ( file . source !== FileSources . openai ) {
422
+ attachedFileIds . delete ( file . file_id ) ;
423
+ const index = file_ids . indexOf ( file . file_id ) ;
424
+ if ( index > - 1 ) {
425
+ file_ids . splice ( index , 1 ) ;
426
+ }
427
+ }
428
+ }
429
+
430
+ userMessage . file_ids = file_ids ;
431
+ }
432
+
433
+ const result = await initThread ( { openai, body : initThreadBody , thread_id } ) ;
434
+ thread_id = result . thread_id ;
435
+
436
+ createOnTextProgress ( {
437
+ openai,
438
+ conversationId,
439
+ userMessageId,
440
+ messageId : responseMessageId ,
441
+ thread_id,
442
+ } ) ;
443
+
444
+ requestMessage = {
445
+ user : req . user . id ,
446
+ text,
447
+ messageId : userMessageId ,
448
+ parentMessageId,
449
+ // TODO: make sure client sends correct format for `files`, use zod
450
+ files,
451
+ file_ids,
452
+ conversationId,
453
+ isCreatedByUser : true ,
454
+ assistant_id,
455
+ thread_id,
456
+ model : assistant_id ,
457
+ } ;
458
+
459
+ previousMessages . push ( requestMessage ) ;
460
+
461
+ /* asynchronous */
462
+ saveUserMessage ( { ...requestMessage , model } ) ;
463
+
464
+ conversation = {
465
+ conversationId,
466
+ title : 'New Chat' ,
467
+ endpoint : EModelEndpoint . assistants ,
468
+ promptPrefix : promptPrefix ,
469
+ instructions : instructions ,
470
+ assistant_id,
471
+ // model,
472
+ } ;
473
+
474
+ if ( file_ids . length ) {
475
+ conversation . file_ids = file_ids ;
476
+ }
477
+ } ;
478
+
479
+ const promises = [ initializeThread ( ) , checkBalanceBeforeRun ( ) ] ;
480
+ await Promise . all ( promises ) ;
400
481
401
482
const sendInitialResponse = ( ) => {
402
483
sendMessage ( res , {
@@ -421,6 +502,8 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
421
502
422
503
const processRun = async ( retry = false ) => {
423
504
if ( req . app . locals [ EModelEndpoint . azureOpenAI ] ?. assistants ) {
505
+ openai . attachedFileIds = attachedFileIds ;
506
+ openai . visionPromise = visionPromise ;
424
507
if ( retry ) {
425
508
response = await runAssistant ( {
426
509
openai,
@@ -463,9 +546,11 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
463
546
req,
464
547
res,
465
548
openai,
549
+ handlers,
466
550
thread_id,
551
+ visionPromise,
552
+ attachedFileIds,
467
553
responseMessage : openai . responseMessage ,
468
- handlers,
469
554
// streamOptions: {
470
555
471
556
// },
0 commit comments