@@ -10,6 +10,8 @@ import type {
10
10
InsightExtractOption ,
11
11
Rect ,
12
12
ReferenceImage ,
13
+ TMultimodalPrompt ,
14
+ TUserPrompt ,
13
15
UIContext ,
14
16
} from '@/types' ;
15
17
import {
@@ -18,7 +20,11 @@ import {
18
20
getAIConfigInBoolean ,
19
21
vlLocateMode ,
20
22
} from '@midscene/shared/env' ;
21
- import { cropByRect , paddingToMatchBlockByBase64 } from '@midscene/shared/img' ;
23
+ import {
24
+ cropByRect ,
25
+ paddingToMatchBlockByBase64 ,
26
+ preProcessImageUrl ,
27
+ } from '@midscene/shared/img' ;
22
28
import { getDebug } from '@midscene/shared/logger' ;
23
29
import { assert } from '@midscene/shared/utils' ;
24
30
import type {
@@ -56,17 +62,73 @@ import { callToGetJSONObject } from './service-caller/index';
56
62
57
63
export type AIArgs = [
58
64
ChatCompletionSystemMessageParam ,
59
- ChatCompletionUserMessageParam ,
65
+ ... ChatCompletionUserMessageParam [ ] ,
60
66
] ;
61
67
62
68
const debugInspect = getDebug ( 'ai:inspect' ) ;
63
69
const debugSection = getDebug ( 'ai:section' ) ;
64
70
71
+ const extraTextFromUserPrompt = ( prompt : TUserPrompt ) : string => {
72
+ if ( typeof prompt === 'string' ) {
73
+ return prompt ;
74
+ } else {
75
+ return prompt . prompt ;
76
+ }
77
+ } ;
78
+
79
+ const promptsToChatParam = async (
80
+ multimodalPrompt : TMultimodalPrompt ,
81
+ ) : Promise < ChatCompletionUserMessageParam [ ] > => {
82
+ const msgs : ChatCompletionUserMessageParam [ ] = [ ] ;
83
+ if ( multimodalPrompt ?. images ?. length ) {
84
+ msgs . push ( {
85
+ role : 'user' ,
86
+ content : [
87
+ {
88
+ type : 'text' ,
89
+ text : 'Next, I will provide all the reference images.' ,
90
+ } ,
91
+ ] ,
92
+ } ) ;
93
+
94
+ for ( const item of multimodalPrompt . images ) {
95
+ const base64 = await preProcessImageUrl (
96
+ item . url ,
97
+ ! ! multimodalPrompt . convertHttpImage2Base64 ,
98
+ ) ;
99
+
100
+ msgs . push ( {
101
+ role : 'user' ,
102
+ content : [
103
+ {
104
+ type : 'text' ,
105
+ text : `reference image ${ item . name } :` ,
106
+ } ,
107
+ ] ,
108
+ } ) ;
109
+
110
+ msgs . push ( {
111
+ role : 'user' ,
112
+ content : [
113
+ {
114
+ type : 'image_url' ,
115
+ image_url : {
116
+ url : base64 ,
117
+ detail : 'high' ,
118
+ } ,
119
+ } ,
120
+ ] ,
121
+ } ) ;
122
+ }
123
+ }
124
+ return msgs ;
125
+ } ;
126
+
65
127
export async function AiLocateElement <
66
128
ElementType extends BaseElement = BaseElement ,
67
129
> ( options : {
68
130
context : UIContext < ElementType > ;
69
- targetElementDescription : string ;
131
+ targetElementDescription : TUserPrompt ;
70
132
referenceImage ?: ReferenceImage ;
71
133
callAI ?: typeof callAiFn < AIElementResponse | [ number , number ] > ;
72
134
searchConfig ?: Awaited < ReturnType < typeof AiLocateSection > > ;
@@ -90,7 +152,7 @@ export async function AiLocateElement<
90
152
91
153
const userInstructionPrompt = await findElementPrompt . format ( {
92
154
pageDescription : description ,
93
- targetElementDescription,
155
+ targetElementDescription : extraTextFromUserPrompt ( targetElementDescription ) ,
94
156
} ) ;
95
157
const systemPrompt = systemPromptToLocateElement ( vlLocateMode ( ) ) ;
96
158
@@ -137,6 +199,14 @@ export async function AiLocateElement<
137
199
} ,
138
200
] ;
139
201
202
+ if ( typeof targetElementDescription !== 'string' ) {
203
+ const addOns = await promptsToChatParam ( {
204
+ images : targetElementDescription . images ,
205
+ convertHttpImage2Base64 : targetElementDescription . convertHttpImage2Base64 ,
206
+ } ) ;
207
+ msgs . push ( ...addOns ) ;
208
+ }
209
+
140
210
const callAIFn =
141
211
callAI || callToGetJSONObject < AIElementResponse | [ number , number ] > ;
142
212
@@ -211,7 +281,7 @@ export async function AiLocateElement<
211
281
212
282
export async function AiLocateSection ( options : {
213
283
context : UIContext < BaseElement > ;
214
- sectionDescription : string ;
284
+ sectionDescription : TUserPrompt ;
215
285
callAI ?: typeof callAiFn < AISectionLocatorResponse > ;
216
286
} ) : Promise < {
217
287
rect ?: Rect ;
@@ -225,7 +295,7 @@ export async function AiLocateSection(options: {
225
295
226
296
const systemPrompt = systemPromptToLocateSection ( vlLocateMode ( ) ) ;
227
297
const sectionLocatorInstructionText = await sectionLocatorInstruction . format ( {
228
- sectionDescription,
298
+ sectionDescription : extraTextFromUserPrompt ( sectionDescription ) ,
229
299
} ) ;
230
300
const msgs : AIArgs = [
231
301
{ role : 'system' , content : systemPrompt } ,
@@ -247,6 +317,14 @@ export async function AiLocateSection(options: {
247
317
} ,
248
318
] ;
249
319
320
+ if ( typeof sectionDescription !== 'string' ) {
321
+ const addOns = await promptsToChatParam ( {
322
+ images : sectionDescription . images ,
323
+ convertHttpImage2Base64 : sectionDescription . convertHttpImage2Base64 ,
324
+ } ) ;
325
+ msgs . push ( ...addOns ) ;
326
+ }
327
+
250
328
const result = await callAiFn < AISectionLocatorResponse > (
251
329
msgs ,
252
330
AIActionType . EXTRACT_DATA ,
@@ -304,10 +382,11 @@ export async function AiExtractElementInfo<
304
382
ElementType extends BaseElement = BaseElement ,
305
383
> ( options : {
306
384
dataQuery : string | Record < string , string > ;
385
+ multimodalPrompt ?: TMultimodalPrompt ;
307
386
context : UIContext < ElementType > ;
308
387
extractOption ?: InsightExtractOption ;
309
388
} ) {
310
- const { dataQuery, context, extractOption } = options ;
389
+ const { dataQuery, context, extractOption, multimodalPrompt } = options ;
311
390
const systemPrompt = systemPromptToExtract ( ) ;
312
391
313
392
const { screenshotBase64 } = context ;
@@ -348,6 +427,14 @@ export async function AiExtractElementInfo<
348
427
} ,
349
428
] ;
350
429
430
+ if ( multimodalPrompt ) {
431
+ const addOns = await promptsToChatParam ( {
432
+ images : multimodalPrompt . images ,
433
+ convertHttpImage2Base64 : multimodalPrompt . convertHttpImage2Base64 ,
434
+ } ) ;
435
+ msgs . push ( ...addOns ) ;
436
+ }
437
+
351
438
const result = await callAiFn < AIDataExtractionResponse < T > > (
352
439
msgs ,
353
440
AIActionType . EXTRACT_DATA ,
@@ -361,17 +448,19 @@ export async function AiExtractElementInfo<
361
448
362
449
export async function AiAssert <
363
450
ElementType extends BaseElement = BaseElement ,
364
- > ( options : { assertion : string ; context : UIContext < ElementType > } ) {
451
+ > ( options : { assertion : TUserPrompt ; context : UIContext < ElementType > } ) {
365
452
const { assertion, context } = options ;
366
453
367
- assert ( assertion , 'assertion should be a string ' ) ;
454
+ assert ( assertion , 'assertion should not be empty ' ) ;
368
455
369
456
const { screenshotBase64 } = context ;
370
457
371
458
const systemPrompt = systemPromptToAssert ( {
372
459
isUITars : getAIConfigInBoolean ( MIDSCENE_USE_VLM_UI_TARS ) ,
373
460
} ) ;
374
461
462
+ const assertionText = extraTextFromUserPrompt ( assertion ) ;
463
+
375
464
const msgs : AIArgs = [
376
465
{ role : 'system' , content : systemPrompt } ,
377
466
{
@@ -389,14 +478,22 @@ export async function AiAssert<
389
478
text : `
390
479
Here is the assertion. Please tell whether it is truthy according to the screenshot.
391
480
=====================================
392
- ${ assertion }
481
+ ${ assertionText }
393
482
=====================================
394
483
` ,
395
484
} ,
396
485
] ,
397
486
} ,
398
487
] ;
399
488
489
+ if ( typeof assertion !== 'string' ) {
490
+ const addOns = await promptsToChatParam ( {
491
+ images : assertion . images ,
492
+ convertHttpImage2Base64 : assertion . convertHttpImage2Base64 ,
493
+ } ) ;
494
+ msgs . push ( ...addOns ) ;
495
+ }
496
+
400
497
const { content : assertResult , usage } = await callAiFn < AIAssertionResponse > (
401
498
msgs ,
402
499
AIActionType . ASSERT ,
0 commit comments