@browserless/agent 14.0.2 → 14.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@browserless/agent",
3
3
  "description": "Drive a Puppeteer page toward a natural-language goal using finite decisions.",
4
4
  "homepage": "https://browserless.js.org",
5
- "version": "14.0.2",
5
+ "version": "14.0.4",
6
6
  "main": "src",
7
7
  "author": {
8
8
  "email": "hello@microlink.io",
@@ -59,5 +59,5 @@
59
59
  "access": "public"
60
60
  },
61
61
  "types": "src/index.d.ts",
62
- "gitHead": "2f8b09a1509de4868c5ca2981ed147af8638968d"
62
+ "gitHead": "bf9a31cbe256022543902ec9e748dec6b5abf06b"
63
63
  }
package/src/model.js CHANGED
@@ -345,7 +345,14 @@ const fieldText = async (goal, action, state, history, model, options) => {
345
345
  system: TEXT_VALUE,
346
346
  prompt: JSON.stringify({
347
347
  goal,
348
- field: { label: action.label, role: action.role, value: action.value },
348
+ field: {
349
+ id: action.id,
350
+ node: action.node,
351
+ label: action.label,
352
+ role: action.role,
353
+ value: action.value,
354
+ context: state.scopes?.[action.node] ?? ''
355
+ },
349
356
  page: { title: state.title, text: state.text },
350
357
  recent_actions: history.slice(-TEXT_HISTORY_LENGTH)
351
358
  }),
package/src/snapshot.js CHANGED
@@ -1,4 +1,4 @@
1
- /* global location, scrollX, scrollY, innerWidth, innerHeight */
1
+ /* global HTMLElement, getComputedStyle, location, scrollX, scrollY, innerWidth, innerHeight */
2
2
  // Adapted from browser-use/jev-ultrafast snapshot.js (MIT). See README.md.
3
3
  module.exports = function snapshot () {
4
4
  return (() => {
@@ -169,6 +169,8 @@ module.exports = function snapshot () {
169
169
  .filter(safe)
170
170
  .map(e => [identity(e), e.value, e.checked, e.selectedIndex, e.disabled, e.readOnly])
171
171
  ]
172
+ // A named form control shadows the form's own innerText property.
173
+ const readInnerText = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'innerText').get
172
174
  cache.guard = e => {
173
175
  if (!e?.isConnected || !visibleNow(e)) return null
174
176
  const scope =
@@ -195,7 +197,9 @@ module.exports = function snapshot () {
195
197
  ])
196
198
  : null,
197
199
  e.getAttribute('href'),
198
- scope?.innerText?.slice(0, 6000) || '',
200
+ (scope instanceof HTMLElement
201
+ ? readInnerText.call(scope).slice(0, 6000)
202
+ : scope?.textContent?.slice(0, 6000)) || '',
199
203
  [...(cache.closest(e, 'form')?.querySelectorAll('input,textarea,select') || [])]
200
204
  .filter(safe)
201
205
  .map(field => [
@@ -279,6 +283,74 @@ module.exports = function snapshot () {
279
283
  }
280
284
  }
281
285
  }
286
+ const collected = new Set(actions.map(action => cache.nodes.get(action.node)))
287
+ const listSelector = 'ul,ol,[role="list"],[role="listbox"],[role="menu"],[role="tablist"]'
288
+ const pointer = element => {
289
+ if (getComputedStyle(element).cursor !== 'pointer') return false
290
+ const parent = flatParent(element)
291
+ if (!parent || getComputedStyle(parent).cursor !== 'pointer') return true
292
+ return parent.matches(listSelector)
293
+ }
294
+ const inLayer = element => {
295
+ for (let current = element; current; current = flatParent(current)) {
296
+ if (current === document.body || current === document.documentElement) return false
297
+ const position = getComputedStyle(current).position
298
+ if (position === 'absolute' || position === 'fixed' || position === 'sticky') return true
299
+ }
300
+ return false
301
+ }
302
+ const pointerLists = new WeakMap()
303
+ const inPointerList = element => {
304
+ const parent = flatParent(element)
305
+ if (!parent?.matches(listSelector)) return false
306
+ let eligible = pointerLists.get(parent)
307
+ if (eligible == null) {
308
+ eligible = [...parent.children].filter(pointer).length >= 2
309
+ pointerLists.set(parent, eligible)
310
+ }
311
+ return eligible
312
+ }
313
+ const candidates = []
314
+ for (const element of queryAll('li,div,span,td,dd,p')) {
315
+ if (collected.has(element) || !visible(element) || !pointer(element)) continue
316
+ if (element.matches(':disabled') || cache.closest(element, '[aria-disabled="true"]')) continue
317
+ if (element.querySelector(selector)) continue
318
+ const rect = element.getBoundingClientRect()
319
+ const x = rect.x + rect.width / 2
320
+ const y = rect.y + rect.height / 2
321
+ if (
322
+ rect.width <= 0 ||
323
+ rect.height <= 0 ||
324
+ x < 0 ||
325
+ y < 0 ||
326
+ x >= innerWidth ||
327
+ y >= innerHeight
328
+ ) {
329
+ continue
330
+ }
331
+ const hit = cache.elementFromPoint(x, y)
332
+ if (hit && !cache.contains(element, hit)) continue
333
+ if (!inLayer(element) && !inPointerList(element)) continue
334
+ const label = (name(element) || '').replace(/\s+/g, ' ').trim().slice(0, 120)
335
+ if (!label) continue
336
+ candidates.push({ element, rect, label })
337
+ }
338
+ const rows = candidates.filter(
339
+ ({ element }) =>
340
+ !candidates.some(
341
+ other => other.element !== element && cache.contains(element, other.element)
342
+ )
343
+ )
344
+ for (const { element, rect, label } of rows.slice(0, 40)) {
345
+ actions.push({
346
+ node: identity(element),
347
+ role: 'button',
348
+ label,
349
+ rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
350
+ kind: 'click',
351
+ value: ''
352
+ })
353
+ }
282
354
  const words = []
283
355
  const range = document.createRange()
284
356
  let length = 0
@@ -350,8 +422,18 @@ module.exports = function snapshot () {
350
422
  const height = document.documentElement.scrollHeight
351
423
  const pageKey = cache.pageKey()
352
424
  const guards = {}
425
+ const scopes = {}
353
426
  for (const a of actions) {
354
- if (!(a.node in guards)) guards[a.node] = cache.guard(cache.nodes.get(a.node))
427
+ if (a.node in guards) continue
428
+ const element = cache.nodes.get(a.node)
429
+ guards[a.node] = cache.guard(element)
430
+ const scope =
431
+ cache.closest(element, 'form,dialog,[role="dialog"],article,li,tr,[role="row"]') ||
432
+ flatParent(element)
433
+ scopes[a.node] =
434
+ (scope instanceof HTMLElement
435
+ ? readInnerText.call(scope).slice(0, 1000)
436
+ : scope?.textContent?.slice(0, 1000)) || ''
355
437
  }
356
438
  // Compare meaning and identity. Geometry is always resolved and hit-tested just before input.
357
439
  const semantics = actions.map(({ rect, ...action }) => action)
@@ -391,6 +473,7 @@ module.exports = function snapshot () {
391
473
  marker,
392
474
  pageKey,
393
475
  guards,
476
+ scopes,
394
477
  omittedActions
395
478
  }
396
479
  })()