explorbot 0.1.15 → 0.1.16
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/package.json +1 -1
- package/dist/src/ai/researcher/focus.js +51 -10
- package/dist/src/ai/researcher/sections.js +8 -4
- package/dist/src/ai/researcher.js +9 -24
- package/dist/src/ai/tester.js +7 -2
- package/package.json +1 -1
- package/src/ai/researcher/focus.ts +57 -8
- package/src/ai/researcher/sections.ts +7 -3
- package/src/ai/researcher.ts +8 -23
- package/src/ai/tester.ts +7 -2
package/dist/package.json
CHANGED
|
@@ -1,20 +1,61 @@
|
|
|
1
|
-
import { detectFocusArea } from "../../utils/aria.js";
|
|
2
1
|
import { mdq } from "../../utils/markdown-query.js";
|
|
3
2
|
export const FOCUSED_MARKER = '> **Focused**';
|
|
4
3
|
const FOCUS_SKIP_SECTIONS = new Set(['navigation', 'menu']);
|
|
5
4
|
export function hasFocusedSection(text) {
|
|
6
5
|
return text.includes(FOCUSED_MARKER);
|
|
7
6
|
}
|
|
8
|
-
export function
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
15
|
-
|
|
7
|
+
export async function detectFocusedSection(page, sections) {
|
|
8
|
+
const candidates = [];
|
|
9
|
+
for (const section of sections) {
|
|
10
|
+
if (!section.containerCss)
|
|
11
|
+
continue;
|
|
12
|
+
const key = section.name.toLowerCase().replace(/^section:\s*/, '');
|
|
13
|
+
if (FOCUS_SKIP_SECTIONS.has(key))
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
const locator = page.locator(section.containerCss).first();
|
|
17
|
+
if (!(await locator.isVisible()))
|
|
18
|
+
continue;
|
|
19
|
+
const probe = await locator.evaluate((el) => {
|
|
20
|
+
const dialogSelector = '[role="dialog"], [role="alertdialog"], [aria-modal="true"]';
|
|
21
|
+
const isDialog = el.matches(dialogSelector) || !!el.querySelector(dialogSelector);
|
|
22
|
+
let cur = el;
|
|
23
|
+
let maxZ = 0;
|
|
24
|
+
while (cur && cur !== document.body) {
|
|
25
|
+
const cs = window.getComputedStyle(cur);
|
|
26
|
+
if (cs.position !== 'static') {
|
|
27
|
+
const z = Number.parseInt(cs.zIndex, 10);
|
|
28
|
+
if (!Number.isNaN(z) && z > maxZ)
|
|
29
|
+
maxZ = z;
|
|
30
|
+
}
|
|
31
|
+
cur = cur.parentElement;
|
|
32
|
+
}
|
|
33
|
+
const shadow = window.getComputedStyle(el).boxShadow;
|
|
34
|
+
const hasShadow = !!shadow && shadow !== 'none';
|
|
35
|
+
return { isDialog, zIndex: maxZ, hasShadow };
|
|
36
|
+
});
|
|
37
|
+
candidates.push({ name: section.name, ...probe });
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
16
40
|
}
|
|
17
|
-
|
|
41
|
+
if (candidates.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const dialogs = candidates.filter((c) => c.isDialog);
|
|
44
|
+
const pool = dialogs.length > 0 ? dialogs : candidates;
|
|
45
|
+
const winner = pool.reduce((best, c) => {
|
|
46
|
+
if (!best)
|
|
47
|
+
return c;
|
|
48
|
+
if (c.zIndex !== best.zIndex)
|
|
49
|
+
return c.zIndex > best.zIndex ? c : best;
|
|
50
|
+
if (c.hasShadow !== best.hasShadow)
|
|
51
|
+
return c.hasShadow ? c : best;
|
|
52
|
+
return best;
|
|
53
|
+
}, null);
|
|
54
|
+
if (!winner)
|
|
55
|
+
return null;
|
|
56
|
+
if (dialogs.length === 0 && winner.zIndex === 0 && !winner.hasShadow)
|
|
57
|
+
return null;
|
|
58
|
+
return winner.name;
|
|
18
59
|
}
|
|
19
60
|
export function markSectionAsFocused(result, sectionName) {
|
|
20
61
|
if (hasFocusedSection(result.text))
|
|
@@ -3,6 +3,8 @@ import { executionController } from "../../execution-controller.js";
|
|
|
3
3
|
import { tag } from '../../utils/logger.js';
|
|
4
4
|
import { RulesLoader } from "../../utils/rules-loader.js";
|
|
5
5
|
import { locatorRule as generalLocatorRuleText } from '../rules.js';
|
|
6
|
+
import { markSectionAsFocused } from "./focus.js";
|
|
7
|
+
import { ResearchResult } from "./research-result.js";
|
|
6
8
|
export function WithSections(Base) {
|
|
7
9
|
return class extends Base {
|
|
8
10
|
async researchBySections() {
|
|
@@ -40,10 +42,12 @@ export function WithSections(Base) {
|
|
|
40
42
|
if (parts.length === 0) {
|
|
41
43
|
throw new Error('Per-section research produced no sections — AI responses all empty or NOT_PRESENT');
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
if (focusCss)
|
|
45
|
-
merged
|
|
46
|
-
|
|
45
|
+
const merged = parts.join('\n\n');
|
|
46
|
+
if (!focusCss)
|
|
47
|
+
return merged;
|
|
48
|
+
const focused = new ResearchResult(merged, this.actionResult?.url || '');
|
|
49
|
+
markSectionAsFocused(focused, 'Focus');
|
|
50
|
+
return focused.text;
|
|
47
51
|
}
|
|
48
52
|
async _detectFocusCss() {
|
|
49
53
|
const focusSections = this.explorer.getConfig().ai?.agents?.researcher?.focusSections;
|
|
@@ -16,7 +16,7 @@ import { ContextLengthError } from './provider.js';
|
|
|
16
16
|
import { findSimilarResearch, getCachedResearch, saveResearch } from "./researcher/cache.js";
|
|
17
17
|
import { WithCoordinates } from "./researcher/coordinates.js";
|
|
18
18
|
import { WithDeepAnalysis } from "./researcher/deep-analysis.js";
|
|
19
|
-
import {
|
|
19
|
+
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from "./researcher/focus.js";
|
|
20
20
|
import { WithLocators } from "./researcher/locators.js";
|
|
21
21
|
import { extractValidContainers, formatResearchSummary, parseResearchSections } from "./researcher/parser.js";
|
|
22
22
|
import { ResearchResult } from "./researcher/research-result.js";
|
|
@@ -186,18 +186,13 @@ export class Researcher extends ResearcherBase {
|
|
|
186
186
|
if (!interrupted() && fix && result.locators.some((l) => l.valid === false)) {
|
|
187
187
|
await this.fixBrokenSections(result, activeConversation);
|
|
188
188
|
}
|
|
189
|
-
// Focused section:
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
result.text = result.text.replace(focusMatch[0], '');
|
|
193
|
-
markSectionAsFocused(result, focusMatch[1].trim());
|
|
194
|
-
}
|
|
195
|
-
if (!hasFocusedSection(result.text)) {
|
|
189
|
+
// Focused section: unified Playwright probe (HTML+CSS+visibility).
|
|
190
|
+
// Must run BEFORE visuallyAnnotateContainers — annotation overlays inject z-index 99998+ which would pollute the scoring.
|
|
191
|
+
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
196
192
|
const sections = parseResearchSections(result.text);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
markSectionAsFocused(result, focusedName);
|
|
193
|
+
const focused = await detectFocusedSection(this.explorer.playwrightHelper.page, sections);
|
|
194
|
+
if (focused)
|
|
195
|
+
markSectionAsFocused(result, focused);
|
|
201
196
|
}
|
|
202
197
|
// Stage 4: Visual analysis
|
|
203
198
|
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
@@ -232,8 +227,8 @@ export class Researcher extends ResearcherBase {
|
|
|
232
227
|
await this.backfillCoordinates(result);
|
|
233
228
|
await this.backfillBrokenLocators(result);
|
|
234
229
|
}
|
|
235
|
-
// Focused section: final fallback
|
|
236
|
-
if (!hasFocusedSection(result.text)) {
|
|
230
|
+
// Focused section: final fallback (vision-only — without a screenshot we don't infer focus)
|
|
231
|
+
if (this.hasScreenshotToAnalyze && !hasFocusedSection(result.text)) {
|
|
237
232
|
const sections = parseResearchSections(result.text);
|
|
238
233
|
const fallback = pickDefaultFocusedSection(sections);
|
|
239
234
|
if (fallback)
|
|
@@ -388,16 +383,6 @@ export class Researcher extends ResearcherBase {
|
|
|
388
383
|
|
|
389
384
|
| Element | ARIA | CSS | eidx |
|
|
390
385
|
</section_format>
|
|
391
|
-
|
|
392
|
-
<focused_section>
|
|
393
|
-
At the end of your output, declare the primary focus area on a single line:
|
|
394
|
-
|
|
395
|
-
> Focused: <exact section name>
|
|
396
|
-
|
|
397
|
-
- If a dialog/modal/drawer/overlay exists, it is focused.
|
|
398
|
-
- Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
|
|
399
|
-
- Navigation and menu/toolbar are never focused.
|
|
400
|
-
</focused_section>
|
|
401
386
|
`;
|
|
402
387
|
}
|
|
403
388
|
async buildResearchPrompt() {
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -329,10 +329,15 @@ export class Tester extends TaskAgent {
|
|
|
329
329
|
: undefined,
|
|
330
330
|
catch: async ({ error, stop }) => {
|
|
331
331
|
tag('error').log(`Test execution error: ${error}`);
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
332
333
|
if (!task.hasFinished) {
|
|
333
|
-
task.addNote(`Execution error: ${
|
|
334
|
+
task.addNote(`Execution error: ${message}`);
|
|
334
335
|
}
|
|
335
|
-
|
|
336
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
337
|
+
stop();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
conversation.addUserText(`Previous AI call failed: ${message}. Take a different approach on the next step.`);
|
|
336
341
|
},
|
|
337
342
|
});
|
|
338
343
|
if (task.hasFinished)
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
2
|
import { mdq } from '../../utils/markdown-query.ts';
|
|
3
3
|
import type { ResearchSection } from './parser.ts';
|
|
4
4
|
import type { ResearchResult } from './research-result.ts';
|
|
@@ -10,16 +10,65 @@ export function hasFocusedSection(text: string): boolean {
|
|
|
10
10
|
return text.includes(FOCUSED_MARKER);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
interface FocusProbe {
|
|
14
|
+
name: string;
|
|
15
|
+
isDialog: boolean;
|
|
16
|
+
zIndex: number;
|
|
17
|
+
hasShadow: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function detectFocusedSection(page: Page, sections: ResearchSection[]): Promise<string | null> {
|
|
21
|
+
const candidates: FocusProbe[] = [];
|
|
22
|
+
|
|
23
|
+
for (const section of sections) {
|
|
24
|
+
if (!section.containerCss) continue;
|
|
25
|
+
const key = section.name.toLowerCase().replace(/^section:\s*/, '');
|
|
26
|
+
if (FOCUS_SKIP_SECTIONS.has(key)) continue;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const locator = page.locator(section.containerCss).first();
|
|
30
|
+
if (!(await locator.isVisible())) continue;
|
|
31
|
+
|
|
32
|
+
const probe = await locator.evaluate((el) => {
|
|
33
|
+
const dialogSelector = '[role="dialog"], [role="alertdialog"], [aria-modal="true"]';
|
|
34
|
+
const isDialog = el.matches(dialogSelector) || !!el.querySelector(dialogSelector);
|
|
35
|
+
|
|
36
|
+
let cur: Element | null = el;
|
|
37
|
+
let maxZ = 0;
|
|
38
|
+
while (cur && cur !== document.body) {
|
|
39
|
+
const cs = window.getComputedStyle(cur);
|
|
40
|
+
if (cs.position !== 'static') {
|
|
41
|
+
const z = Number.parseInt(cs.zIndex, 10);
|
|
42
|
+
if (!Number.isNaN(z) && z > maxZ) maxZ = z;
|
|
43
|
+
}
|
|
44
|
+
cur = cur.parentElement;
|
|
45
|
+
}
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
47
|
+
const shadow = window.getComputedStyle(el).boxShadow;
|
|
48
|
+
const hasShadow = !!shadow && shadow !== 'none';
|
|
49
|
+
|
|
50
|
+
return { isDialog, zIndex: maxZ, hasShadow };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
candidates.push({ name: section.name, ...probe });
|
|
54
|
+
} catch {}
|
|
20
55
|
}
|
|
21
56
|
|
|
22
|
-
return null;
|
|
57
|
+
if (candidates.length === 0) return null;
|
|
58
|
+
|
|
59
|
+
const dialogs = candidates.filter((c) => c.isDialog);
|
|
60
|
+
const pool = dialogs.length > 0 ? dialogs : candidates;
|
|
61
|
+
|
|
62
|
+
const winner = pool.reduce<FocusProbe | null>((best, c) => {
|
|
63
|
+
if (!best) return c;
|
|
64
|
+
if (c.zIndex !== best.zIndex) return c.zIndex > best.zIndex ? c : best;
|
|
65
|
+
if (c.hasShadow !== best.hasShadow) return c.hasShadow ? c : best;
|
|
66
|
+
return best;
|
|
67
|
+
}, null);
|
|
68
|
+
|
|
69
|
+
if (!winner) return null;
|
|
70
|
+
if (dialogs.length === 0 && winner.zIndex === 0 && !winner.hasShadow) return null;
|
|
71
|
+
return winner.name;
|
|
23
72
|
}
|
|
24
73
|
|
|
25
74
|
export function markSectionAsFocused(result: ResearchResult, sectionName: string): void {
|
|
@@ -7,7 +7,9 @@ import { tag } from '../../utils/logger.js';
|
|
|
7
7
|
import { RulesLoader } from '../../utils/rules-loader.ts';
|
|
8
8
|
import type { Provider } from '../provider.js';
|
|
9
9
|
import { locatorRule as generalLocatorRuleText } from '../rules.js';
|
|
10
|
+
import { markSectionAsFocused } from './focus.ts';
|
|
10
11
|
import type { Constructor } from './mixin.ts';
|
|
12
|
+
import { ResearchResult } from './research-result.ts';
|
|
11
13
|
|
|
12
14
|
export interface SectionMethods {
|
|
13
15
|
researchBySections(): Promise<string>;
|
|
@@ -54,9 +56,11 @@ export function WithSections<T extends Constructor>(Base: T) {
|
|
|
54
56
|
throw new Error('Per-section research produced no sections — AI responses all empty or NOT_PRESENT');
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
if (focusCss) merged
|
|
59
|
-
|
|
59
|
+
const merged = parts.join('\n\n');
|
|
60
|
+
if (!focusCss) return merged;
|
|
61
|
+
const focused = new ResearchResult(merged, this.actionResult?.url || '');
|
|
62
|
+
markSectionAsFocused(focused, 'Focus');
|
|
63
|
+
return focused.text;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
private async _detectFocusCss(): Promise<string | null> {
|
package/src/ai/researcher.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { ContextLengthError, type Provider } from './provider.js';
|
|
|
24
24
|
import { findSimilarResearch, getCachedResearch, saveResearch } from './researcher/cache.ts';
|
|
25
25
|
import { type CoordinateMethods, WithCoordinates } from './researcher/coordinates.ts';
|
|
26
26
|
import { type DeepAnalysisMethods, WithDeepAnalysis } from './researcher/deep-analysis.ts';
|
|
27
|
-
import {
|
|
27
|
+
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from './researcher/focus.ts';
|
|
28
28
|
import { type LocatorMethods, WithLocators } from './researcher/locators.ts';
|
|
29
29
|
import { extractValidContainers, formatResearchSummary, parseResearchSections } from './researcher/parser.ts';
|
|
30
30
|
import { ResearchResult } from './researcher/research-result.ts';
|
|
@@ -234,17 +234,12 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
234
234
|
await this.fixBrokenSections(result, activeConversation);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
// Focused section:
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
result.text = result.text.replace(focusMatch[0], '');
|
|
241
|
-
markSectionAsFocused(result, focusMatch[1].trim());
|
|
242
|
-
}
|
|
243
|
-
if (!hasFocusedSection(result.text)) {
|
|
237
|
+
// Focused section: unified Playwright probe (HTML+CSS+visibility).
|
|
238
|
+
// Must run BEFORE visuallyAnnotateContainers — annotation overlays inject z-index 99998+ which would pollute the scoring.
|
|
239
|
+
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
244
240
|
const sections = parseResearchSections(result.text);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
if (focusedName) markSectionAsFocused(result, focusedName);
|
|
241
|
+
const focused = await detectFocusedSection(this.explorer.playwrightHelper.page, sections);
|
|
242
|
+
if (focused) markSectionAsFocused(result, focused);
|
|
248
243
|
}
|
|
249
244
|
|
|
250
245
|
// Stage 4: Visual analysis
|
|
@@ -281,8 +276,8 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
281
276
|
await this.backfillBrokenLocators(result);
|
|
282
277
|
}
|
|
283
278
|
|
|
284
|
-
// Focused section: final fallback
|
|
285
|
-
if (!hasFocusedSection(result.text)) {
|
|
279
|
+
// Focused section: final fallback (vision-only — without a screenshot we don't infer focus)
|
|
280
|
+
if (this.hasScreenshotToAnalyze && !hasFocusedSection(result.text)) {
|
|
286
281
|
const sections = parseResearchSections(result.text);
|
|
287
282
|
const fallback = pickDefaultFocusedSection(sections);
|
|
288
283
|
if (fallback) markSectionAsFocused(result, fallback);
|
|
@@ -451,16 +446,6 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
451
446
|
|
|
452
447
|
| Element | ARIA | CSS | eidx |
|
|
453
448
|
</section_format>
|
|
454
|
-
|
|
455
|
-
<focused_section>
|
|
456
|
-
At the end of your output, declare the primary focus area on a single line:
|
|
457
|
-
|
|
458
|
-
> Focused: <exact section name>
|
|
459
|
-
|
|
460
|
-
- If a dialog/modal/drawer/overlay exists, it is focused.
|
|
461
|
-
- Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
|
|
462
|
-
- Navigation and menu/toolbar are never focused.
|
|
463
|
-
</focused_section>
|
|
464
449
|
`;
|
|
465
450
|
}
|
|
466
451
|
|
package/src/ai/tester.ts
CHANGED
|
@@ -387,10 +387,15 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
387
387
|
: undefined,
|
|
388
388
|
catch: async ({ error, stop }) => {
|
|
389
389
|
tag('error').log(`Test execution error: ${error}`);
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
390
391
|
if (!task.hasFinished) {
|
|
391
|
-
task.addNote(`Execution error: ${
|
|
392
|
+
task.addNote(`Execution error: ${message}`);
|
|
392
393
|
}
|
|
393
|
-
|
|
394
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
395
|
+
stop();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
conversation.addUserText(`Previous AI call failed: ${message}. Take a different approach on the next step.`);
|
|
394
399
|
},
|
|
395
400
|
}
|
|
396
401
|
);
|