explorbot 0.1.3 → 0.1.5
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/dist/rules/researcher/container-rules.md +15 -0
- package/dist/rules/researcher/section-example.md +12 -0
- package/dist/src/ai/researcher/cache.js +7 -1
- package/dist/src/ai/researcher.js +56 -92
- package/dist/src/explorer.js +12 -1
- package/package.json +1 -1
- package/rules/researcher/container-rules.md +15 -0
- package/rules/researcher/section-example.md +12 -0
- package/src/ai/researcher/cache.ts +6 -1
- package/src/ai/researcher.ts +57 -93
- package/src/explorer.ts +10 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<container_rules>
|
|
2
|
+
Container CSS must be a SINGLE semantic selector — one class, one id, or one attribute. No spaces, no combinators, no descendant paths.
|
|
3
|
+
|
|
4
|
+
- INVALID: bare tags (`div`, `section`, `nav`), combinators (`div > .content`, `.a .b`), layout/utility classes (flex-, col-, mt-, p-, bg-, text-, items-, rounded-)
|
|
5
|
+
- VALID: semantic class names that describe what the section IS (`.product-list`, `.sidebar-menu`, `.user-profile`, `.search-results`), semantic roles (`[role="dialog"]`), semantic ids (`#main-content`)
|
|
6
|
+
|
|
7
|
+
The container must uniquely identify a semantic wrapper, not a path through the DOM.
|
|
8
|
+
</container_rules>
|
|
9
|
+
|
|
10
|
+
<css_selector_rules>
|
|
11
|
+
CSS selectors inside the UI map must point to the actual interactive element (input, button, a, select), not to wrapper divs.
|
|
12
|
+
|
|
13
|
+
- Prefer distinguishing attributes on the interactive element (`type`, `value`, `name`, `href`, `aria-label`) over wrapper ids.
|
|
14
|
+
- For buttons with similar text, include `type` or `value` or form context to stay unique.
|
|
15
|
+
</css_selector_rules>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<section_example>
|
|
2
|
+
## List
|
|
3
|
+
|
|
4
|
+
Product catalog showing available items with sorting and filtering.
|
|
5
|
+
|
|
6
|
+
> Container: '.product-list'
|
|
7
|
+
|
|
8
|
+
| Element | ARIA | CSS | eidx |
|
|
9
|
+
| 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
|
|
10
|
+
| 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
|
|
11
|
+
| 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
|
|
12
|
+
</section_example>
|
|
@@ -17,10 +17,16 @@ function getStatesDir() {
|
|
|
17
17
|
function getFingerprintWorker() {
|
|
18
18
|
if (!fingerprintWorker) {
|
|
19
19
|
const ext = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
20
|
-
fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url)
|
|
20
|
+
fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url));
|
|
21
21
|
}
|
|
22
22
|
return fingerprintWorker;
|
|
23
23
|
}
|
|
24
|
+
export function clearResearchCache() {
|
|
25
|
+
for (const key of Object.keys(memoryCache))
|
|
26
|
+
delete memoryCache[key];
|
|
27
|
+
for (const key of Object.keys(memoryCacheTimestamps))
|
|
28
|
+
delete memoryCacheTimestamps[key];
|
|
29
|
+
}
|
|
24
30
|
export function getCachedResearch(hash) {
|
|
25
31
|
if (!hash)
|
|
26
32
|
return '';
|
|
@@ -71,10 +71,6 @@ export class Researcher extends ResearcherBase {
|
|
|
71
71
|
You are senior QA focused on exploritary testig of web application.
|
|
72
72
|
</role>
|
|
73
73
|
|
|
74
|
-
<wording>
|
|
75
|
-
In the UI map and all descriptions, name concrete UI parts (visible labels, headings, regions, ARIA roles). Do not use vague placeholders like "the page", "the element", "the button", "the input", "the link", "the form", "the table", "the list", or "the item". Do not use filler such as "comprehensive", "All required", "All elements", or "All necessary".
|
|
76
|
-
</wording>
|
|
77
|
-
|
|
78
74
|
${customPrompt || ''}
|
|
79
75
|
`;
|
|
80
76
|
}
|
|
@@ -135,6 +131,7 @@ export class Researcher extends ResearcherBase {
|
|
|
135
131
|
const prompt = await this.buildResearchPrompt();
|
|
136
132
|
conversation.addUserText(prompt);
|
|
137
133
|
let invocationResult;
|
|
134
|
+
let activeConversation = conversation;
|
|
138
135
|
try {
|
|
139
136
|
invocationResult = await this.provider.invokeConversation(conversation, undefined, { agentName: 'researcher' });
|
|
140
137
|
}
|
|
@@ -145,29 +142,16 @@ export class Researcher extends ResearcherBase {
|
|
|
145
142
|
}
|
|
146
143
|
throw error;
|
|
147
144
|
}
|
|
148
|
-
tag('warning').log('Output truncated, retrying with focused
|
|
145
|
+
tag('warning').log('Output truncated, retrying with fresh focused conversation (ARIA only)...');
|
|
149
146
|
retriesLeft = 0;
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
activeConversation = this.provider.startConversation(this.getSystemMessage(), 'researcher');
|
|
148
|
+
activeConversation.addUserText(this.buildFocusedRetryPrompt());
|
|
149
|
+
invocationResult = await this.provider.invokeConversation(activeConversation, undefined, { agentName: 'researcher' });
|
|
152
150
|
}
|
|
153
151
|
if (!invocationResult)
|
|
154
152
|
throw new Error('Failed to get response from provider');
|
|
155
153
|
const result = new ResearchResult(invocationResult.response.text, state.url);
|
|
156
154
|
debugLog(`Original research response length: ${result.text.length} chars`);
|
|
157
|
-
const errorSection = mdq(result.text).query('section("Error Page Detected")');
|
|
158
|
-
if (errorSection.count() > 0) {
|
|
159
|
-
if (result.text.length < 500) {
|
|
160
|
-
if (!opts._skipErrorPageRetry && (await this.waitForPageLoad(screenshot))) {
|
|
161
|
-
return this.research(state, { ...opts, force: true, _skipErrorPageRetry: true });
|
|
162
|
-
}
|
|
163
|
-
tag('warning').log(`AI detected error page at ${state.url}`);
|
|
164
|
-
if (stateHash)
|
|
165
|
-
saveResearch(stateHash, result.text);
|
|
166
|
-
await this.hooksRunner.runAfterHook('researcher', state.url);
|
|
167
|
-
return result.text;
|
|
168
|
-
}
|
|
169
|
-
result.text = errorSection.replace('');
|
|
170
|
-
}
|
|
171
155
|
const interrupted = () => executionController.isInterrupted();
|
|
172
156
|
// Stage 2: Test containers + locators
|
|
173
157
|
result.parseLocators();
|
|
@@ -204,7 +188,7 @@ export class Researcher extends ResearcherBase {
|
|
|
204
188
|
}
|
|
205
189
|
// Stage 3: Fix broken sections via AI conversation continuation
|
|
206
190
|
if (!interrupted() && fix && result.locators.some((l) => l.valid === false)) {
|
|
207
|
-
await this.fixBrokenSections(result,
|
|
191
|
+
await this.fixBrokenSections(result, activeConversation);
|
|
208
192
|
}
|
|
209
193
|
// Focused section: parse AI declaration, then ARIA fallback
|
|
210
194
|
const focusMatch = result.text.match(/^>\s*Focused:\s*(.+)/m);
|
|
@@ -357,32 +341,29 @@ export class Researcher extends ResearcherBase {
|
|
|
357
341
|
}
|
|
358
342
|
researchRules() {
|
|
359
343
|
const sections = this.getConfiguredSections();
|
|
344
|
+
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
360
345
|
return dedent `
|
|
361
346
|
<task>
|
|
362
|
-
Examine the
|
|
363
|
-
Identify the
|
|
364
|
-
|
|
365
|
-
Provide a comprehensive UI map report in markdown format.
|
|
347
|
+
Examine the page and explain its main purpose from the user perspective.
|
|
348
|
+
Identify the primary user actions and break the page into sections.
|
|
349
|
+
Provide a UI map report in markdown.
|
|
366
350
|
</task>
|
|
367
351
|
|
|
368
352
|
<rules>
|
|
369
353
|
- Explain what the user can achieve on this page.
|
|
370
354
|
- Focus on primary user actions and interactive elements only.
|
|
371
355
|
- Research all menus and navigational areas.
|
|
372
|
-
- Ignore
|
|
356
|
+
- Ignore decorative sidebars, footer-only links, and external links.
|
|
373
357
|
- Detect layout patterns: list/detail split, 2-pane, or 3-pane layouts.
|
|
374
|
-
-
|
|
375
|
-
- UI map
|
|
376
|
-
-
|
|
377
|
-
-
|
|
378
|
-
- NEVER skip elements that have an eidx attribute. Every element with eidx MUST appear in the UI map table, even if it has no text or accessible name. Describe icon-only elements using their SVG class (e.g., md-icon-dots-horizontal → "More actions (ellipsis)") or their visual appearance.
|
|
379
|
-
- ARIA locator must be JSON with role and text keys (NOT "name").
|
|
380
|
-
- Note elements likely to have hover interactions (elements with title attribute, aria-describedby, navigation menu items with submenus) and mark them with "(hover)" in the UI map.
|
|
358
|
+
- Every element with an eidx attribute MUST appear in the UI map — describe icon-only buttons by their visual role.
|
|
359
|
+
- Every UI map row needs a CSS selector; ARIA may be "-" for icon-only buttons, CSS must never be "-".
|
|
360
|
+
- ARIA locator JSON uses keys "role" and "text" (NOT "name").
|
|
361
|
+
- Mark elements with likely hover interactions (title, aria-describedby, menu items with submenus) as "(hover)".
|
|
381
362
|
</rules>
|
|
382
363
|
|
|
383
364
|
${generalLocatorRuleText}
|
|
384
365
|
|
|
385
|
-
${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element'
|
|
366
|
+
${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules'], currentUrl)}
|
|
386
367
|
|
|
387
368
|
<section_identification>
|
|
388
369
|
Identify page sections in this priority order:
|
|
@@ -390,23 +371,12 @@ export class Researcher extends ResearcherBase {
|
|
|
390
371
|
.map(([name, description]) => `* ${name}: ${description}`)
|
|
391
372
|
.join('\n')}
|
|
392
373
|
|
|
393
|
-
- Sections can overlap
|
|
394
|
-
- Never name a section "Focus" or "Focused" —
|
|
395
|
-
-
|
|
396
|
-
- Each section
|
|
397
|
-
- UI map CSS locators must be relative to the section container.
|
|
374
|
+
- Sections can overlap; prefer more detailed sections over broader ones.
|
|
375
|
+
- Never name a section "Focus" or "Focused" — use what it contains (Detail, Modal, Form, Content, List).
|
|
376
|
+
- Omit sections that are not present or not relevant.
|
|
377
|
+
- Each section needs a container CSS locator; UI map CSS locators are relative to it.
|
|
398
378
|
</section_identification>
|
|
399
379
|
|
|
400
|
-
<container_rules>
|
|
401
|
-
CRITICAL: Container CSS must be a SINGLE selector — one class, one ID, or one attribute.
|
|
402
|
-
No spaces, no >, no combinators, no nesting.
|
|
403
|
-
- INVALID: '.filterbar-filter-btn-div button', 'div.static nav', 'div > .content'
|
|
404
|
-
- INVALID: 'div', 'section', 'nav', 'div:first' (bare tags are not containers)
|
|
405
|
-
- INVALID: Tailwind/Bootstrap utility classes that describe layout or styling (e.g. flex-none, d-flex, col-md-6, items-center, mt-4, p-2, bg-white, text-sm, rounded-lg). These are visual, not semantic.
|
|
406
|
-
- VALID: Semantic class names that describe WHAT the section IS — e.g. '.product-list', '.sidebar-menu', '.user-profile', '[role="dialog"]', '.search-results'
|
|
407
|
-
Container must uniquely identify a semantic wrapper, not a path through the DOM.
|
|
408
|
-
</container_rules>
|
|
409
|
-
|
|
410
380
|
<section_format>
|
|
411
381
|
## Section Name
|
|
412
382
|
|
|
@@ -416,40 +386,16 @@ export class Researcher extends ResearcherBase {
|
|
|
416
386
|
|
|
417
387
|
| Element | ARIA | CSS | eidx |
|
|
418
388
|
</section_format>
|
|
419
|
-
<section_example>
|
|
420
|
-
## List
|
|
421
|
-
|
|
422
|
-
Product catalog showing available items with sorting and filtering.
|
|
423
|
-
|
|
424
|
-
> Container: '.product-list'
|
|
425
|
-
|
|
426
|
-
| Element | ARIA | CSS | eidx |
|
|
427
|
-
| 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
|
|
428
|
-
| 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
|
|
429
|
-
| 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
|
|
430
|
-
</section_example>
|
|
431
389
|
|
|
432
390
|
<focused_section>
|
|
433
|
-
At the
|
|
391
|
+
At the end of your output, declare the primary focus area on a single line:
|
|
434
392
|
|
|
435
393
|
> Focused: <exact section name>
|
|
436
394
|
|
|
437
|
-
|
|
438
|
-
-
|
|
439
|
-
-
|
|
440
|
-
- Navigation is NEVER focused — it exists on every page
|
|
441
|
-
- Menu/toolbar is NEVER focused — it contains actions, not the main content
|
|
442
|
-
- The focused section is the one the user came to this page to interact with
|
|
395
|
+
- If a dialog/modal/drawer/overlay exists, it is focused.
|
|
396
|
+
- Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
|
|
397
|
+
- Navigation and menu/toolbar are never focused.
|
|
443
398
|
</focused_section>
|
|
444
|
-
|
|
445
|
-
<css_selector_rules>
|
|
446
|
-
CSS selectors MUST point to the actual interactive element (input, button, a, select), NOT to container divs.
|
|
447
|
-
- If a submit button is inside a wrapper div, target the input/button directly
|
|
448
|
-
- Bad: '#submit-wrapper' (div container)
|
|
449
|
-
- Good: '#submit-wrapper input[type="submit"]' or 'input[type="submit"][value="Submit"]'
|
|
450
|
-
- For buttons with similar text, include distinguishing attributes like type, value, or form context
|
|
451
|
-
|
|
452
|
-
</css_selector_rules>
|
|
453
399
|
`;
|
|
454
400
|
}
|
|
455
401
|
async buildResearchPrompt() {
|
|
@@ -475,17 +421,6 @@ export class Researcher extends ResearcherBase {
|
|
|
475
421
|
return dedent `
|
|
476
422
|
Analyze this web page and provide a comprehensive research report in markdown format.
|
|
477
423
|
|
|
478
|
-
<error_detection>
|
|
479
|
-
IMPORTANT: First check if this looks like an error page (404, 500, access denied,
|
|
480
|
-
not found, server error, forbidden, or similar). If so, respond ONLY with:
|
|
481
|
-
|
|
482
|
-
## Error Page Detected
|
|
483
|
-
Type: [error type]
|
|
484
|
-
Reason: [what indicates this is an error page]
|
|
485
|
-
|
|
486
|
-
Then stop - do not provide normal research output for error pages.
|
|
487
|
-
</error_detection>
|
|
488
|
-
|
|
489
424
|
${this.researchRules()}
|
|
490
425
|
|
|
491
426
|
URL: ${this.actionResult.url || 'Unknown'}
|
|
@@ -537,11 +472,40 @@ export class Researcher extends ResearcherBase {
|
|
|
537
472
|
`;
|
|
538
473
|
}
|
|
539
474
|
buildFocusedRetryPrompt() {
|
|
475
|
+
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
476
|
+
const example = RulesLoader.loadRules('researcher', ['section-example'], currentUrl);
|
|
477
|
+
const uiMapTable = RulesLoader.loadRules('researcher', ['ui-map-table'], currentUrl);
|
|
478
|
+
const url = this.actionResult?.url || 'Unknown';
|
|
479
|
+
const title = this.actionResult?.title || 'Unknown';
|
|
480
|
+
const aria = this.actionResult?.getCompactARIA() || '';
|
|
540
481
|
return dedent `
|
|
541
|
-
|
|
482
|
+
Previous response was truncated. Restart with a minimal output.
|
|
483
|
+
|
|
484
|
+
<task>
|
|
485
|
+
Output a UI map for ONE section only — the main interactive area of this page.
|
|
486
|
+
Skip navigation, sidebar, and footer. Max 15 elements.
|
|
487
|
+
Every element with an eidx MUST appear. Every row needs CSS; ARIA may be "-" for icon-only.
|
|
488
|
+
End with a single line: \`> Focused: <section name>\`.
|
|
489
|
+
</task>
|
|
490
|
+
|
|
491
|
+
<section_format>
|
|
492
|
+
## Section Name
|
|
493
|
+
|
|
494
|
+
> Container: '.container-css-selector'
|
|
495
|
+
|
|
496
|
+
| Element | ARIA | CSS | eidx |
|
|
497
|
+
</section_format>
|
|
498
|
+
|
|
499
|
+
${example}
|
|
500
|
+
|
|
501
|
+
${uiMapTable}
|
|
502
|
+
|
|
503
|
+
URL: ${url}
|
|
504
|
+
Title: ${title}
|
|
542
505
|
|
|
543
|
-
|
|
544
|
-
|
|
506
|
+
<aria>
|
|
507
|
+
${aria}
|
|
508
|
+
</aria>
|
|
545
509
|
`;
|
|
546
510
|
}
|
|
547
511
|
async textContent(state) {
|
package/dist/src/explorer.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createDebug, log, tag } from './utils/logger.js';
|
|
|
18
18
|
import { WebElement, extractElementData } from "./utils/web-element.js";
|
|
19
19
|
const debugLog = createDebug('explorbot:explorer');
|
|
20
20
|
const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
|
|
21
|
+
const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
|
|
21
22
|
class Explorer {
|
|
22
23
|
aiProvider;
|
|
23
24
|
playwrightHelper;
|
|
@@ -228,7 +229,17 @@ class Explorer {
|
|
|
228
229
|
await action.execute(`I.executeScript(() => { window.history.pushState({}, '', ${serializedUrl}); window.dispatchEvent(new PopStateEvent('popstate')); })`);
|
|
229
230
|
}
|
|
230
231
|
else {
|
|
231
|
-
|
|
232
|
+
try {
|
|
233
|
+
await action.execute(`I.amOnPage(${serializedUrl})`);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
237
|
+
if (!RECOVERABLE_NAVIGATION_ERRORS.test(msg))
|
|
238
|
+
throw err;
|
|
239
|
+
tag('warning').log(`Navigation warning (continuing after load): ${msg.split('\n')[0]}`);
|
|
240
|
+
await this.playwrightHelper.page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => { });
|
|
241
|
+
await action.capturePageState();
|
|
242
|
+
}
|
|
232
243
|
}
|
|
233
244
|
if (wait !== undefined) {
|
|
234
245
|
debugLog('Waiting for', wait);
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<container_rules>
|
|
2
|
+
Container CSS must be a SINGLE semantic selector — one class, one id, or one attribute. No spaces, no combinators, no descendant paths.
|
|
3
|
+
|
|
4
|
+
- INVALID: bare tags (`div`, `section`, `nav`), combinators (`div > .content`, `.a .b`), layout/utility classes (flex-, col-, mt-, p-, bg-, text-, items-, rounded-)
|
|
5
|
+
- VALID: semantic class names that describe what the section IS (`.product-list`, `.sidebar-menu`, `.user-profile`, `.search-results`), semantic roles (`[role="dialog"]`), semantic ids (`#main-content`)
|
|
6
|
+
|
|
7
|
+
The container must uniquely identify a semantic wrapper, not a path through the DOM.
|
|
8
|
+
</container_rules>
|
|
9
|
+
|
|
10
|
+
<css_selector_rules>
|
|
11
|
+
CSS selectors inside the UI map must point to the actual interactive element (input, button, a, select), not to wrapper divs.
|
|
12
|
+
|
|
13
|
+
- Prefer distinguishing attributes on the interactive element (`type`, `value`, `name`, `href`, `aria-label`) over wrapper ids.
|
|
14
|
+
- For buttons with similar text, include `type` or `value` or form context to stay unique.
|
|
15
|
+
</css_selector_rules>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<section_example>
|
|
2
|
+
## List
|
|
3
|
+
|
|
4
|
+
Product catalog showing available items with sorting and filtering.
|
|
5
|
+
|
|
6
|
+
> Container: '.product-list'
|
|
7
|
+
|
|
8
|
+
| Element | ARIA | CSS | eidx |
|
|
9
|
+
| 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
|
|
10
|
+
| 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
|
|
11
|
+
| 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
|
|
12
|
+
</section_example>
|
|
@@ -22,11 +22,16 @@ function getStatesDir(): string {
|
|
|
22
22
|
function getFingerprintWorker(): Worker {
|
|
23
23
|
if (!fingerprintWorker) {
|
|
24
24
|
const ext = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
25
|
-
fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url)
|
|
25
|
+
fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url));
|
|
26
26
|
}
|
|
27
27
|
return fingerprintWorker;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function clearResearchCache(): void {
|
|
31
|
+
for (const key of Object.keys(memoryCache)) delete memoryCache[key];
|
|
32
|
+
for (const key of Object.keys(memoryCacheTimestamps)) delete memoryCacheTimestamps[key];
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export function getCachedResearch(hash: string): string {
|
|
31
36
|
if (!hash) return '';
|
|
32
37
|
const now = Date.now();
|
package/src/ai/researcher.ts
CHANGED
|
@@ -98,15 +98,11 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
98
98
|
You are senior QA focused on exploritary testig of web application.
|
|
99
99
|
</role>
|
|
100
100
|
|
|
101
|
-
<wording>
|
|
102
|
-
In the UI map and all descriptions, name concrete UI parts (visible labels, headings, regions, ARIA roles). Do not use vague placeholders like "the page", "the element", "the button", "the input", "the link", "the form", "the table", "the list", or "the item". Do not use filler such as "comprehensive", "All required", "All elements", or "All necessary".
|
|
103
|
-
</wording>
|
|
104
|
-
|
|
105
101
|
${customPrompt || ''}
|
|
106
102
|
`;
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; _retriesLeft?: number
|
|
105
|
+
async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; _retriesLeft?: number } = {}): Promise<string> {
|
|
110
106
|
const { screenshot = false, force = false, deep = false, data = false, fix = true } = opts;
|
|
111
107
|
const maxRetries = (this.explorer.getConfig().ai?.agents?.researcher as any)?.retries ?? 2;
|
|
112
108
|
let retriesLeft = opts._retriesLeft ?? maxRetries;
|
|
@@ -175,6 +171,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
175
171
|
conversation.addUserText(prompt);
|
|
176
172
|
|
|
177
173
|
let invocationResult: Awaited<ReturnType<typeof this.provider.invokeConversation>>;
|
|
174
|
+
let activeConversation = conversation;
|
|
178
175
|
try {
|
|
179
176
|
invocationResult = await this.provider.invokeConversation(conversation, undefined, { agentName: 'researcher' });
|
|
180
177
|
} catch (error) {
|
|
@@ -184,30 +181,17 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
184
181
|
}
|
|
185
182
|
throw error;
|
|
186
183
|
}
|
|
187
|
-
tag('warning').log('Output truncated, retrying with focused
|
|
184
|
+
tag('warning').log('Output truncated, retrying with fresh focused conversation (ARIA only)...');
|
|
188
185
|
retriesLeft = 0;
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
activeConversation = this.provider.startConversation(this.getSystemMessage(), 'researcher');
|
|
187
|
+
activeConversation.addUserText(this.buildFocusedRetryPrompt());
|
|
188
|
+
invocationResult = await this.provider.invokeConversation(activeConversation, undefined, { agentName: 'researcher' });
|
|
191
189
|
}
|
|
192
190
|
if (!invocationResult) throw new Error('Failed to get response from provider');
|
|
193
191
|
|
|
194
192
|
const result = new ResearchResult(invocationResult.response.text, state.url);
|
|
195
193
|
debugLog(`Original research response length: ${result.text.length} chars`);
|
|
196
194
|
|
|
197
|
-
const errorSection = mdq(result.text).query('section("Error Page Detected")');
|
|
198
|
-
if (errorSection.count() > 0) {
|
|
199
|
-
if (result.text.length < 500) {
|
|
200
|
-
if (!opts._skipErrorPageRetry && (await this.waitForPageLoad(screenshot))) {
|
|
201
|
-
return this.research(state, { ...opts, force: true, _skipErrorPageRetry: true });
|
|
202
|
-
}
|
|
203
|
-
tag('warning').log(`AI detected error page at ${state.url}`);
|
|
204
|
-
if (stateHash) saveResearch(stateHash, result.text);
|
|
205
|
-
await this.hooksRunner.runAfterHook('researcher', state.url);
|
|
206
|
-
return result.text;
|
|
207
|
-
}
|
|
208
|
-
result.text = errorSection.replace('');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
195
|
const interrupted = () => executionController.isInterrupted();
|
|
212
196
|
|
|
213
197
|
// Stage 2: Test containers + locators
|
|
@@ -251,7 +235,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
251
235
|
|
|
252
236
|
// Stage 3: Fix broken sections via AI conversation continuation
|
|
253
237
|
if (!interrupted() && fix && result.locators.some((l) => l.valid === false)) {
|
|
254
|
-
await this.fixBrokenSections(result,
|
|
238
|
+
await this.fixBrokenSections(result, activeConversation);
|
|
255
239
|
}
|
|
256
240
|
|
|
257
241
|
// Focused section: parse AI declaration, then ARIA fallback
|
|
@@ -417,32 +401,29 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
417
401
|
|
|
418
402
|
private researchRules(): string {
|
|
419
403
|
const sections = this.getConfiguredSections();
|
|
404
|
+
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
420
405
|
return dedent`
|
|
421
406
|
<task>
|
|
422
|
-
Examine the
|
|
423
|
-
Identify the
|
|
424
|
-
|
|
425
|
-
Provide a comprehensive UI map report in markdown format.
|
|
407
|
+
Examine the page and explain its main purpose from the user perspective.
|
|
408
|
+
Identify the primary user actions and break the page into sections.
|
|
409
|
+
Provide a UI map report in markdown.
|
|
426
410
|
</task>
|
|
427
411
|
|
|
428
412
|
<rules>
|
|
429
413
|
- Explain what the user can achieve on this page.
|
|
430
414
|
- Focus on primary user actions and interactive elements only.
|
|
431
415
|
- Research all menus and navigational areas.
|
|
432
|
-
- Ignore
|
|
416
|
+
- Ignore decorative sidebars, footer-only links, and external links.
|
|
433
417
|
- Detect layout patterns: list/detail split, 2-pane, or 3-pane layouts.
|
|
434
|
-
-
|
|
435
|
-
- UI map
|
|
436
|
-
-
|
|
437
|
-
-
|
|
438
|
-
- NEVER skip elements that have an eidx attribute. Every element with eidx MUST appear in the UI map table, even if it has no text or accessible name. Describe icon-only elements using their SVG class (e.g., md-icon-dots-horizontal → "More actions (ellipsis)") or their visual appearance.
|
|
439
|
-
- ARIA locator must be JSON with role and text keys (NOT "name").
|
|
440
|
-
- Note elements likely to have hover interactions (elements with title attribute, aria-describedby, navigation menu items with submenus) and mark them with "(hover)" in the UI map.
|
|
418
|
+
- Every element with an eidx attribute MUST appear in the UI map — describe icon-only buttons by their visual role.
|
|
419
|
+
- Every UI map row needs a CSS selector; ARIA may be "-" for icon-only buttons, CSS must never be "-".
|
|
420
|
+
- ARIA locator JSON uses keys "role" and "text" (NOT "name").
|
|
421
|
+
- Mark elements with likely hover interactions (title, aria-describedby, menu items with submenus) as "(hover)".
|
|
441
422
|
</rules>
|
|
442
423
|
|
|
443
424
|
${generalLocatorRuleText}
|
|
444
425
|
|
|
445
|
-
${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element'
|
|
426
|
+
${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules'], currentUrl)}
|
|
446
427
|
|
|
447
428
|
<section_identification>
|
|
448
429
|
Identify page sections in this priority order:
|
|
@@ -450,23 +431,12 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
450
431
|
.map(([name, description]) => `* ${name}: ${description}`)
|
|
451
432
|
.join('\n')}
|
|
452
433
|
|
|
453
|
-
- Sections can overlap
|
|
454
|
-
- Never name a section "Focus" or "Focused" —
|
|
455
|
-
-
|
|
456
|
-
- Each section
|
|
457
|
-
- UI map CSS locators must be relative to the section container.
|
|
434
|
+
- Sections can overlap; prefer more detailed sections over broader ones.
|
|
435
|
+
- Never name a section "Focus" or "Focused" — use what it contains (Detail, Modal, Form, Content, List).
|
|
436
|
+
- Omit sections that are not present or not relevant.
|
|
437
|
+
- Each section needs a container CSS locator; UI map CSS locators are relative to it.
|
|
458
438
|
</section_identification>
|
|
459
439
|
|
|
460
|
-
<container_rules>
|
|
461
|
-
CRITICAL: Container CSS must be a SINGLE selector — one class, one ID, or one attribute.
|
|
462
|
-
No spaces, no >, no combinators, no nesting.
|
|
463
|
-
- INVALID: '.filterbar-filter-btn-div button', 'div.static nav', 'div > .content'
|
|
464
|
-
- INVALID: 'div', 'section', 'nav', 'div:first' (bare tags are not containers)
|
|
465
|
-
- INVALID: Tailwind/Bootstrap utility classes that describe layout or styling (e.g. flex-none, d-flex, col-md-6, items-center, mt-4, p-2, bg-white, text-sm, rounded-lg). These are visual, not semantic.
|
|
466
|
-
- VALID: Semantic class names that describe WHAT the section IS — e.g. '.product-list', '.sidebar-menu', '.user-profile', '[role="dialog"]', '.search-results'
|
|
467
|
-
Container must uniquely identify a semantic wrapper, not a path through the DOM.
|
|
468
|
-
</container_rules>
|
|
469
|
-
|
|
470
440
|
<section_format>
|
|
471
441
|
## Section Name
|
|
472
442
|
|
|
@@ -476,40 +446,16 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
476
446
|
|
|
477
447
|
| Element | ARIA | CSS | eidx |
|
|
478
448
|
</section_format>
|
|
479
|
-
<section_example>
|
|
480
|
-
## List
|
|
481
|
-
|
|
482
|
-
Product catalog showing available items with sorting and filtering.
|
|
483
|
-
|
|
484
|
-
> Container: '.product-list'
|
|
485
|
-
|
|
486
|
-
| Element | ARIA | CSS | eidx |
|
|
487
|
-
| 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
|
|
488
|
-
| 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
|
|
489
|
-
| 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
|
|
490
|
-
</section_example>
|
|
491
449
|
|
|
492
450
|
<focused_section>
|
|
493
|
-
At the
|
|
451
|
+
At the end of your output, declare the primary focus area on a single line:
|
|
494
452
|
|
|
495
453
|
> Focused: <exact section name>
|
|
496
454
|
|
|
497
|
-
|
|
498
|
-
-
|
|
499
|
-
-
|
|
500
|
-
- Navigation is NEVER focused — it exists on every page
|
|
501
|
-
- Menu/toolbar is NEVER focused — it contains actions, not the main content
|
|
502
|
-
- The focused section is the one the user came to this page to interact with
|
|
455
|
+
- If a dialog/modal/drawer/overlay exists, it is focused.
|
|
456
|
+
- Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
|
|
457
|
+
- Navigation and menu/toolbar are never focused.
|
|
503
458
|
</focused_section>
|
|
504
|
-
|
|
505
|
-
<css_selector_rules>
|
|
506
|
-
CSS selectors MUST point to the actual interactive element (input, button, a, select), NOT to container divs.
|
|
507
|
-
- If a submit button is inside a wrapper div, target the input/button directly
|
|
508
|
-
- Bad: '#submit-wrapper' (div container)
|
|
509
|
-
- Good: '#submit-wrapper input[type="submit"]' or 'input[type="submit"][value="Submit"]'
|
|
510
|
-
- For buttons with similar text, include distinguishing attributes like type, value, or form context
|
|
511
|
-
|
|
512
|
-
</css_selector_rules>
|
|
513
459
|
`;
|
|
514
460
|
}
|
|
515
461
|
|
|
@@ -540,17 +486,6 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
540
486
|
return dedent`
|
|
541
487
|
Analyze this web page and provide a comprehensive research report in markdown format.
|
|
542
488
|
|
|
543
|
-
<error_detection>
|
|
544
|
-
IMPORTANT: First check if this looks like an error page (404, 500, access denied,
|
|
545
|
-
not found, server error, forbidden, or similar). If so, respond ONLY with:
|
|
546
|
-
|
|
547
|
-
## Error Page Detected
|
|
548
|
-
Type: [error type]
|
|
549
|
-
Reason: [what indicates this is an error page]
|
|
550
|
-
|
|
551
|
-
Then stop - do not provide normal research output for error pages.
|
|
552
|
-
</error_detection>
|
|
553
|
-
|
|
554
489
|
${this.researchRules()}
|
|
555
490
|
|
|
556
491
|
URL: ${this.actionResult.url || 'Unknown'}
|
|
@@ -603,11 +538,40 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
603
538
|
}
|
|
604
539
|
|
|
605
540
|
private buildFocusedRetryPrompt(): string {
|
|
541
|
+
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
542
|
+
const example = RulesLoader.loadRules('researcher', ['section-example'], currentUrl);
|
|
543
|
+
const uiMapTable = RulesLoader.loadRules('researcher', ['ui-map-table'], currentUrl);
|
|
544
|
+
const url = this.actionResult?.url || 'Unknown';
|
|
545
|
+
const title = this.actionResult?.title || 'Unknown';
|
|
546
|
+
const aria = this.actionResult?.getCompactARIA() || '';
|
|
606
547
|
return dedent`
|
|
607
|
-
|
|
548
|
+
Previous response was truncated. Restart with a minimal output.
|
|
549
|
+
|
|
550
|
+
<task>
|
|
551
|
+
Output a UI map for ONE section only — the main interactive area of this page.
|
|
552
|
+
Skip navigation, sidebar, and footer. Max 15 elements.
|
|
553
|
+
Every element with an eidx MUST appear. Every row needs CSS; ARIA may be "-" for icon-only.
|
|
554
|
+
End with a single line: \`> Focused: <section name>\`.
|
|
555
|
+
</task>
|
|
556
|
+
|
|
557
|
+
<section_format>
|
|
558
|
+
## Section Name
|
|
559
|
+
|
|
560
|
+
> Container: '.container-css-selector'
|
|
561
|
+
|
|
562
|
+
| Element | ARIA | CSS | eidx |
|
|
563
|
+
</section_format>
|
|
564
|
+
|
|
565
|
+
${example}
|
|
566
|
+
|
|
567
|
+
${uiMapTable}
|
|
568
|
+
|
|
569
|
+
URL: ${url}
|
|
570
|
+
Title: ${title}
|
|
608
571
|
|
|
609
|
-
|
|
610
|
-
|
|
572
|
+
<aria>
|
|
573
|
+
${aria}
|
|
574
|
+
</aria>
|
|
611
575
|
`;
|
|
612
576
|
}
|
|
613
577
|
|
package/src/explorer.ts
CHANGED
|
@@ -37,6 +37,7 @@ declare namespace CodeceptJS {
|
|
|
37
37
|
|
|
38
38
|
const debugLog = createDebug('explorbot:explorer');
|
|
39
39
|
const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
|
|
40
|
+
const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
|
|
40
41
|
|
|
41
42
|
interface TabInfo {
|
|
42
43
|
url: string;
|
|
@@ -289,7 +290,15 @@ class Explorer {
|
|
|
289
290
|
if (statePush) {
|
|
290
291
|
await action.execute(`I.executeScript(() => { window.history.pushState({}, '', ${serializedUrl}); window.dispatchEvent(new PopStateEvent('popstate')); })`);
|
|
291
292
|
} else {
|
|
292
|
-
|
|
293
|
+
try {
|
|
294
|
+
await action.execute(`I.amOnPage(${serializedUrl})`);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
297
|
+
if (!RECOVERABLE_NAVIGATION_ERRORS.test(msg)) throw err;
|
|
298
|
+
tag('warning').log(`Navigation warning (continuing after load): ${msg.split('\n')[0]}`);
|
|
299
|
+
await this.playwrightHelper.page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
|
|
300
|
+
await action.capturePageState();
|
|
301
|
+
}
|
|
293
302
|
}
|
|
294
303
|
|
|
295
304
|
if (wait !== undefined) {
|