explorbot 0.3.2 → 0.3.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/bin/explorbot-cli.ts +14 -3
- package/dist/bin/explorbot-cli.js +12 -3
- package/dist/models.json +2 -0
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +1 -0
- package/dist/src/action-result.js +17 -12
- package/dist/src/action.d.ts +10 -0
- package/dist/src/action.js +16 -10
- package/dist/src/ai/conversation.d.ts +2 -1
- package/dist/src/ai/conversation.js +9 -4
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +20 -4
- package/dist/src/ai/pilot.js +9 -0
- package/dist/src/ai/planner.js +1 -1
- package/dist/src/ai/provider.js +28 -2
- package/dist/src/ai/researcher/deep-analysis.js +5 -1
- package/dist/src/ai/researcher/sections.js +0 -1
- package/dist/src/ai/researcher.js +0 -1
- package/dist/src/ai/rules.js +0 -1
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +8 -9
- package/dist/src/ai/tools.d.ts +2 -0
- package/dist/src/ai/tools.js +21 -4
- package/dist/src/commands/init-command.js +74 -24
- package/dist/src/components/InitWizard.d.ts +2 -1
- package/dist/src/components/InitWizard.js +8 -4
- package/dist/src/explorer.js +1 -0
- package/dist/src/knowledge-tracker.d.ts +3 -1
- package/dist/src/knowledge-tracker.js +4 -4
- package/dist/src/utils/aria.js +1 -1
- package/docs/basics/providers.md +2 -4
- package/models.json +2 -0
- package/package.json +1 -1
- package/src/action-result.ts +20 -15
- package/src/action.ts +21 -12
- package/src/ai/conversation.ts +11 -5
- package/src/ai/navigator.ts +22 -4
- package/src/ai/pilot.ts +9 -0
- package/src/ai/planner.ts +1 -1
- package/src/ai/provider.ts +28 -2
- package/src/ai/researcher/deep-analysis.ts +6 -1
- package/src/ai/researcher/sections.ts +0 -1
- package/src/ai/researcher.ts +0 -1
- package/src/ai/rules.ts +0 -1
- package/src/ai/tester.ts +8 -7
- package/src/ai/tools.ts +20 -4
- package/src/commands/init-command.ts +81 -22
- package/src/components/InitWizard.tsx +8 -4
- package/src/explorer.ts +1 -0
- package/src/knowledge-tracker.ts +4 -4
- package/src/utils/aria.ts +1 -1
package/dist/src/ai/tools.js
CHANGED
|
@@ -343,16 +343,17 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
343
343
|
if (action.lastError) {
|
|
344
344
|
const message = errorText(action.lastError);
|
|
345
345
|
await commitNote(activeNote, TestResult.FAILED, toolResult, action);
|
|
346
|
-
let formSuggestion = '
|
|
346
|
+
let formSuggestion = 'Commands after the failing one never ran. Retry only those, using click() or form().';
|
|
347
347
|
if (message.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN)) {
|
|
348
348
|
const disambiguated = await disambiguateElements(action.lastError, explanation, ai);
|
|
349
349
|
if (disambiguated) {
|
|
350
350
|
formSuggestion = `Multiple elements matched. Add step.opts({ elementIndex: ${disambiguated.position} }) to the failing command. Fallback locator: ${disambiguated.xpath}`;
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
|
-
return failedToolResult('form', `Form execution FAILED! ${message}`, {
|
|
353
|
+
return failedToolResult('form', `Form execution FAILED! ${message}\n${formatExecutedSteps(action.executedSteps, codeLines.length)}`, {
|
|
354
354
|
...toolResult,
|
|
355
355
|
code: codeBlock,
|
|
356
|
+
attempts: action.executedSteps,
|
|
356
357
|
suggestion: formSuggestion,
|
|
357
358
|
}, action.lastError);
|
|
358
359
|
}
|
|
@@ -369,6 +370,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
369
370
|
...toolResult,
|
|
370
371
|
message: `Form completed successfully with ${lines.length} commands.`,
|
|
371
372
|
commandsExecuted: lines.length,
|
|
373
|
+
attempts: action.executedSteps,
|
|
372
374
|
code: codeBlock,
|
|
373
375
|
suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
|
|
374
376
|
}, action);
|
|
@@ -698,18 +700,24 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
698
700
|
const actionResult = ActionResult.fromState(currentState);
|
|
699
701
|
const experience = renderExperienceRecipes(explorer.activeTest?.getAppliedExperience(actionResult) ?? []);
|
|
700
702
|
const success = await navigator.resolveState(instruction, actionResult, { experience });
|
|
703
|
+
const attempts = navigator.executedSteps;
|
|
704
|
+
let stepReport = '';
|
|
705
|
+
if (attempts.length)
|
|
706
|
+
stepReport = `\n${formatExecutedSteps(attempts)}`;
|
|
701
707
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, instruction);
|
|
702
708
|
if (success) {
|
|
703
709
|
return successToolResult('interact', {
|
|
704
710
|
...toolResult,
|
|
705
|
-
message: `Successfully executed: ${instruction}`,
|
|
711
|
+
message: `Successfully executed: ${instruction}${stepReport}`,
|
|
712
|
+
attempts,
|
|
706
713
|
});
|
|
707
714
|
}
|
|
708
715
|
let reason = '';
|
|
709
716
|
if (navigator.lastFailureReason)
|
|
710
717
|
reason = `: ${navigator.lastFailureReason}`;
|
|
711
|
-
return failedToolResult('interact', `Failed to execute: ${instruction}${reason}`, {
|
|
718
|
+
return failedToolResult('interact', `Failed to execute: ${instruction}${reason}${stepReport}`, {
|
|
712
719
|
...toolResult,
|
|
720
|
+
attempts,
|
|
713
721
|
suggestion: 'The action could not be completed. Try a different instruction or use more specific element descriptions.',
|
|
714
722
|
});
|
|
715
723
|
}
|
|
@@ -1071,6 +1079,15 @@ export function isMajorPageChange(pageDiff) {
|
|
|
1071
1079
|
export function hasFailedRequest(pageDiff) {
|
|
1072
1080
|
return (pageDiff.requests ?? []).some((request) => request.status >= 400);
|
|
1073
1081
|
}
|
|
1082
|
+
export function formatExecutedSteps(steps, requestedCount = steps.length) {
|
|
1083
|
+
if (!steps.length)
|
|
1084
|
+
return `No command ran of ${requestedCount} requested.`;
|
|
1085
|
+
const lines = steps.map((step) => ` ${step.success ? 'OK' : 'FAILED'} ${step.command}`);
|
|
1086
|
+
const notRun = requestedCount - steps.length;
|
|
1087
|
+
if (notRun > 0)
|
|
1088
|
+
lines.push(` NOT RUN ${notRun} more`);
|
|
1089
|
+
return lines.join('\n');
|
|
1090
|
+
}
|
|
1074
1091
|
function hasObservablePageChange(data) {
|
|
1075
1092
|
if (!data?.pageDiff)
|
|
1076
1093
|
return false;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import dedent from 'dedent';
|
|
5
4
|
import { ConfigParser, PROVIDERS } from "../config.js";
|
|
6
5
|
import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "../global-config.js";
|
|
7
6
|
import { getCliName } from "../utils/cli-name.js";
|
|
8
7
|
import { log, tag } from '../utils/logger.js';
|
|
9
8
|
import { relativeToCwd } from "../utils/next-steps.js";
|
|
10
|
-
function defaultConfigTemplate() {
|
|
9
|
+
function defaultConfigTemplate(provider, esm) {
|
|
10
|
+
let moduleExport = 'module.exports = config;';
|
|
11
|
+
if (esm)
|
|
12
|
+
moduleExport = 'export default config;';
|
|
11
13
|
return `// 'provider/model-id' uses a bundled provider.
|
|
12
|
-
// It is also possible to import provider as a module from Vercel AI SDK.
|
|
14
|
+
// It is also possible to import provider as a module from Vercel AI SDK.
|
|
13
15
|
// https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
|
|
14
16
|
|
|
15
17
|
const config = {
|
|
@@ -19,7 +21,7 @@ const config = {
|
|
|
19
21
|
},
|
|
20
22
|
|
|
21
23
|
ai: {
|
|
22
|
-
${modelLines(
|
|
24
|
+
${modelLines(provider)}
|
|
23
25
|
},
|
|
24
26
|
|
|
25
27
|
reporter: {
|
|
@@ -32,16 +34,17 @@ ${modelLines('openrouter')}
|
|
|
32
34
|
},
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
${moduleExport}
|
|
36
38
|
`;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
function envTemplate(provider) {
|
|
41
|
+
const keyLines = Object.entries(PROVIDERS).map(([name, { envKey }]) => {
|
|
42
|
+
if (name === provider)
|
|
43
|
+
return `${envKey}=`;
|
|
44
|
+
return `# ${envKey}=`;
|
|
45
|
+
});
|
|
46
|
+
return `# AI provider API keys
|
|
47
|
+
${keyLines.join('\n')}
|
|
45
48
|
|
|
46
49
|
# Langfuse Tracing
|
|
47
50
|
LANGFUSE_SECRET_KEY=
|
|
@@ -49,10 +52,11 @@ LANGFUSE_PUBLIC_KEY=
|
|
|
49
52
|
LANGFUSE_BASE_URL=
|
|
50
53
|
|
|
51
54
|
# Testomat.io API key to publish run results
|
|
52
|
-
TESTOMATIO
|
|
53
|
-
|
|
55
|
+
TESTOMATIO=`;
|
|
56
|
+
}
|
|
54
57
|
export async function runInit(options) {
|
|
55
|
-
|
|
58
|
+
const localRequested = !!(options.configPath || options.path);
|
|
59
|
+
if (options.global || (options.provider && !localRequested)) {
|
|
56
60
|
await runGlobalInit(options);
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
@@ -61,8 +65,12 @@ export async function runInit(options) {
|
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
63
67
|
const choice = await renderInitWizard('choose');
|
|
64
|
-
if (choice
|
|
65
|
-
|
|
68
|
+
if (choice !== 'local')
|
|
69
|
+
return;
|
|
70
|
+
const provider = await renderLocalProviderWizard();
|
|
71
|
+
if (!provider)
|
|
72
|
+
return;
|
|
73
|
+
runInitCommand({ ...options, provider });
|
|
66
74
|
}
|
|
67
75
|
export function writeGlobalConfig(provider, apiKey) {
|
|
68
76
|
if (!PROVIDERS[provider]) {
|
|
@@ -87,7 +95,7 @@ export function writeGlobalConfig(provider, apiKey) {
|
|
|
87
95
|
tag('substep').log(chalk.yellow(`${getCliName()} sites`));
|
|
88
96
|
}
|
|
89
97
|
export function runInitCommand(options) {
|
|
90
|
-
const
|
|
98
|
+
const provider = options.provider || 'openrouter';
|
|
91
99
|
const force = options.force ?? false;
|
|
92
100
|
const customPath = options.path;
|
|
93
101
|
const originalCwd = process.cwd();
|
|
@@ -100,13 +108,15 @@ export function runInitCommand(options) {
|
|
|
100
108
|
process.chdir(dir);
|
|
101
109
|
log(`Working in directory: ${relativeToCwd(dir)}`);
|
|
102
110
|
}
|
|
111
|
+
const configName = 'explorbot.config.js';
|
|
112
|
+
const configPath = options.configPath ?? `./${configName}`;
|
|
103
113
|
try {
|
|
104
114
|
let outPath = resolve(configPath);
|
|
105
115
|
if (existsSync(outPath) && statSync(outPath).isDirectory()) {
|
|
106
|
-
outPath = join(outPath,
|
|
116
|
+
outPath = join(outPath, configName);
|
|
107
117
|
}
|
|
108
118
|
else if (!extname(outPath)) {
|
|
109
|
-
outPath = join(outPath,
|
|
119
|
+
outPath = join(outPath, configName);
|
|
110
120
|
}
|
|
111
121
|
const dir = dirname(outPath);
|
|
112
122
|
if (!existsSync(dir)) {
|
|
@@ -118,23 +128,28 @@ export function runInitCommand(options) {
|
|
|
118
128
|
log('Use --force to overwrite existing file');
|
|
119
129
|
process.exit(1);
|
|
120
130
|
}
|
|
121
|
-
|
|
131
|
+
const esm = extname(outPath) !== '.js' || isModuleProject(dirname(outPath));
|
|
132
|
+
writeFileSync(outPath, defaultConfigTemplate(provider, esm), 'utf8');
|
|
122
133
|
log(`Created config file: ${relativeToCwd(outPath)}`);
|
|
123
134
|
const envPath = resolve(process.cwd(), '.env');
|
|
124
135
|
if (!existsSync(envPath)) {
|
|
125
|
-
writeFileSync(envPath, `${
|
|
136
|
+
writeFileSync(envPath, `${envTemplate(provider)}\n`, 'utf8');
|
|
126
137
|
log(`Created env file: ${relativeToCwd(envPath)}`);
|
|
127
138
|
}
|
|
128
139
|
else {
|
|
129
140
|
log(`Env file already exists: ${relativeToCwd(envPath)}`);
|
|
130
141
|
}
|
|
142
|
+
const missing = missingRoles(provider);
|
|
143
|
+
if (missing.length) {
|
|
144
|
+
tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${relativeToCwd(outPath)}`);
|
|
145
|
+
}
|
|
131
146
|
log('');
|
|
132
147
|
log('Next steps:');
|
|
133
148
|
log('1. Configure AI provider in .env');
|
|
134
149
|
log('2. Set AI models config file');
|
|
135
150
|
log('3. Set web application URL in the config file');
|
|
136
151
|
log('4. Add initial knowledge (how to authorize to the application, etc.)');
|
|
137
|
-
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to
|
|
152
|
+
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
138
153
|
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
139
154
|
log('5. Launch application on a relative URL');
|
|
140
155
|
tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
|
|
@@ -187,6 +202,23 @@ async function renderInitWizard(mode) {
|
|
|
187
202
|
}), { exitOnCtrlC: false, patchConsole: false });
|
|
188
203
|
});
|
|
189
204
|
}
|
|
205
|
+
async function renderLocalProviderWizard() {
|
|
206
|
+
const [{ render }, React, InitWizard] = await Promise.all([import('ink'), import('react'), import('../components/InitWizard.js').then((m) => m.default)]);
|
|
207
|
+
return new Promise((resolve) => {
|
|
208
|
+
const finish = (provider) => {
|
|
209
|
+
unmount();
|
|
210
|
+
resolve(provider);
|
|
211
|
+
};
|
|
212
|
+
const { unmount } = render(React.createElement(InitWizard, {
|
|
213
|
+
mode: 'local',
|
|
214
|
+
globalConfigExists: !!findGlobalConfig(),
|
|
215
|
+
onLocal: () => finish(null),
|
|
216
|
+
onComplete: () => finish(null),
|
|
217
|
+
onCancel: () => finish(null),
|
|
218
|
+
onLocalProvider: (provider) => finish(provider),
|
|
219
|
+
}), { exitOnCtrlC: false, patchConsole: false });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
190
222
|
function modelLines(provider) {
|
|
191
223
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
192
224
|
const roles = [
|
|
@@ -216,9 +248,27 @@ ${modelLines(provider)}
|
|
|
216
248
|
},
|
|
217
249
|
};
|
|
218
250
|
|
|
219
|
-
|
|
251
|
+
module.exports = config;
|
|
220
252
|
`;
|
|
221
253
|
}
|
|
254
|
+
function isModuleProject(configDir) {
|
|
255
|
+
let currentDir = resolve(configDir);
|
|
256
|
+
while (true) {
|
|
257
|
+
const packagePath = join(currentDir, 'package.json');
|
|
258
|
+
if (existsSync(packagePath)) {
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(readFileSync(packagePath, 'utf8')).type === 'module';
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const parentDir = dirname(currentDir);
|
|
267
|
+
if (parentDir === currentDir)
|
|
268
|
+
return false;
|
|
269
|
+
currentDir = parentDir;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
222
272
|
function missingRoles(provider) {
|
|
223
273
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
224
274
|
return ['model', 'visionModel', 'agenticModel'].filter((role) => !recommended[role]);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface InitWizardProps {
|
|
3
|
-
mode: 'choose' | 'global';
|
|
3
|
+
mode: 'choose' | 'global' | 'local';
|
|
4
4
|
globalConfigExists: boolean;
|
|
5
5
|
onLocal: () => void;
|
|
6
6
|
onComplete: () => void;
|
|
7
7
|
onCancel: () => void;
|
|
8
|
+
onLocalProvider?: (provider: string) => void;
|
|
8
9
|
}
|
|
9
10
|
declare const InitWizard: React.FC<InitWizardProps>;
|
|
10
11
|
export default InitWizard;
|
|
@@ -7,7 +7,7 @@ import { ConfigParser, PROVIDERS, createModel } from '../config.js';
|
|
|
7
7
|
import { globalDir } from '../global-config.js';
|
|
8
8
|
import InputReadline from './InputReadline.js';
|
|
9
9
|
const PROVIDER_NAMES = Object.keys(PROVIDERS);
|
|
10
|
-
const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel }) => {
|
|
10
|
+
const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel, onLocalProvider }) => {
|
|
11
11
|
const [step, setStep] = useState(mode === 'choose' ? 'target' : 'provider');
|
|
12
12
|
const [targetIndex, setTargetIndex] = useState(0);
|
|
13
13
|
const [providerIndex, setProviderIndex] = useState(0);
|
|
@@ -67,8 +67,12 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel })
|
|
|
67
67
|
setProviderIndex((index) => Math.max(0, index - 1));
|
|
68
68
|
if (key.downArrow)
|
|
69
69
|
setProviderIndex((index) => Math.min(PROVIDER_NAMES.length - 1, index + 1));
|
|
70
|
-
if (key.return)
|
|
71
|
-
|
|
70
|
+
if (key.return) {
|
|
71
|
+
if (mode === 'local')
|
|
72
|
+
onLocalProvider?.(provider);
|
|
73
|
+
else
|
|
74
|
+
setStep('key');
|
|
75
|
+
}
|
|
72
76
|
return;
|
|
73
77
|
}
|
|
74
78
|
if (status)
|
|
@@ -121,7 +125,7 @@ const InitWizard = ({ mode, globalConfigExists, onLocal, onComplete, onCancel })
|
|
|
121
125
|
React.createElement(Box, { marginTop: 1 },
|
|
122
126
|
React.createElement(Text, { dimColor: true },
|
|
123
127
|
"Config goes to ",
|
|
124
|
-
globalDir(),
|
|
128
|
+
mode === 'local' ? 'the current directory' : globalDir(),
|
|
125
129
|
" | ",
|
|
126
130
|
step === 'key' ? 'Enter: continue' : '↑↓: select | Enter: confirm',
|
|
127
131
|
" | Ctrl+C: exit"))));
|
package/dist/src/explorer.js
CHANGED
|
@@ -203,6 +203,7 @@ class Explorer {
|
|
|
203
203
|
const projectRoot = configParser.getProjectRoot();
|
|
204
204
|
global.output_dir = configParser.getStatesDir();
|
|
205
205
|
global.codecept_dir = projectRoot;
|
|
206
|
+
global.codeceptjs = codeceptjs;
|
|
206
207
|
configParser.validateConfig(this.config);
|
|
207
208
|
codeceptjs.container.create(this.convertToCodeceptConfig(this.config), {});
|
|
208
209
|
}
|
|
@@ -17,7 +17,9 @@ export declare class KnowledgeTracker {
|
|
|
17
17
|
renderRelevantKnowledge(state: ActionResult): string;
|
|
18
18
|
renderRelevantContext(state: ActionResult): string;
|
|
19
19
|
renderApplicationSpec(state: ActionResult): string;
|
|
20
|
-
addKnowledge(urlPattern: string, description: string
|
|
20
|
+
addKnowledge(urlPattern: string, description: string, opts?: {
|
|
21
|
+
replace?: boolean;
|
|
22
|
+
}): {
|
|
21
23
|
filename: string;
|
|
22
24
|
filePath: string;
|
|
23
25
|
isNewFile: boolean;
|
|
@@ -73,7 +73,7 @@ export class KnowledgeTracker {
|
|
|
73
73
|
renderApplicationSpec(state) {
|
|
74
74
|
return this.applicationSpec?.renderFor(state) || '';
|
|
75
75
|
}
|
|
76
|
-
addKnowledge(urlPattern, description) {
|
|
76
|
+
addKnowledge(urlPattern, description, opts) {
|
|
77
77
|
const configParser = ConfigParser.getInstance();
|
|
78
78
|
const configPath = configParser.getConfigPath();
|
|
79
79
|
if (!configPath) {
|
|
@@ -102,11 +102,11 @@ export class KnowledgeTracker {
|
|
|
102
102
|
const existingDescription = parsed.content.trim();
|
|
103
103
|
// Append new knowledge with separator
|
|
104
104
|
let newContent;
|
|
105
|
-
if (existingDescription) {
|
|
106
|
-
newContent =
|
|
105
|
+
if (opts?.replace || !existingDescription) {
|
|
106
|
+
newContent = description;
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
|
-
newContent = description
|
|
109
|
+
newContent = `${existingDescription}\n\n---\n\n${description}`;
|
|
110
110
|
}
|
|
111
111
|
const fileContent = matter.stringify(newContent, frontmatter);
|
|
112
112
|
writeFileSync(filePath, fileContent, 'utf8');
|
package/dist/src/utils/aria.js
CHANGED
|
@@ -569,7 +569,7 @@ export function parseAriaLocator(ariaStr) {
|
|
|
569
569
|
const trimmed = ariaStr.trim();
|
|
570
570
|
if (trimmed === '-' || trimmed === '' || trimmed === '"-"')
|
|
571
571
|
return null;
|
|
572
|
-
const match = trimmed.match(/\{\s*["']?role["']?\s*:\s*['"]([^'"]+)['"]\s*,\s*["']?text["']?\s*:\s*['"]([^'"]*)['"]\s*\}/);
|
|
572
|
+
const match = trimmed.match(/\{\s*["']?role["']?\s*:\s*['"]([^'"]+)['"]\s*,\s*["']?(?:text|name)["']?\s*:\s*['"]([^'"]*)['"]\s*\}/);
|
|
573
573
|
if (!match)
|
|
574
574
|
return null;
|
|
575
575
|
return { role: match[1], text: match[2] };
|
package/docs/basics/providers.md
CHANGED
|
@@ -154,14 +154,12 @@ Set the recommended model in the exported config:
|
|
|
154
154
|
```javascript
|
|
155
155
|
export default {
|
|
156
156
|
ai: {
|
|
157
|
+
model: anthropic('claude-haiku-4-5-20251001'),
|
|
158
|
+
visionModel: anthropic('claude-haiku-4-5-20251001'),
|
|
157
159
|
agenticModel: anthropic('claude-haiku-4-5-20251001'),
|
|
158
160
|
},
|
|
159
161
|
};
|
|
160
162
|
```
|
|
161
|
-
|
|
162
|
-
> [!NOTE]
|
|
163
|
-
> This provider currently doesn't serve `model` and `visionModel`, which is required for Explorbot to run at optimal cost and speed.
|
|
164
|
-
> It is recommended to pair it with another AI provider.
|
|
165
163
|
<!-- END provider:anthropic -->
|
|
166
164
|
|
|
167
165
|
### Azure OpenAI
|
package/models.json
CHANGED
package/package.json
CHANGED
package/src/action-result.ts
CHANGED
|
@@ -550,17 +550,9 @@ export class ActionResult implements ActionResultData {
|
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
if (diff.htmlParts.length > 0) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const filteredHtml = htmlCombinedSnapshot(part.subtree, htmlConfig?.combined);
|
|
557
|
-
const minified = await minifyHtml(filteredHtml);
|
|
558
|
-
if (minified) {
|
|
559
|
-
processedParts.push({ ...part, subtree: minified });
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
if (processedParts.length > 0) {
|
|
563
|
-
pageDiff.htmlParts = collapseHtmlParts(processedParts);
|
|
553
|
+
const collapsed = collapseHtmlParts(await diff.cleanedHtmlParts());
|
|
554
|
+
if (collapsed.length > 0) {
|
|
555
|
+
pageDiff.htmlParts = collapsed;
|
|
564
556
|
}
|
|
565
557
|
}
|
|
566
558
|
|
|
@@ -601,10 +593,12 @@ function collapseHtmlParts(parts: HtmlDiffPart[]): HtmlDiffPart[] {
|
|
|
601
593
|
const fullPageReRender = total > HTML_PARTS_TOTAL_BUDGET || parts.length > HTML_PARTS_COUNT_LIMIT;
|
|
602
594
|
|
|
603
595
|
if (fullPageReRender) {
|
|
604
|
-
return parts
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
596
|
+
return parts
|
|
597
|
+
.filter((part) => part.added.length > 0 || part.removed.length > 0)
|
|
598
|
+
.map((part) => ({
|
|
599
|
+
...part,
|
|
600
|
+
subtree: `<html><head></head><body>...collapsed (${part.subtree.length} chars, ${part.added.length} added, ${part.removed.length} removed)...</body></html>`,
|
|
601
|
+
}));
|
|
608
602
|
}
|
|
609
603
|
|
|
610
604
|
return parts.map((part) => {
|
|
@@ -660,6 +654,17 @@ export class Diff {
|
|
|
660
654
|
return this._htmlDiffResult.parts;
|
|
661
655
|
}
|
|
662
656
|
|
|
657
|
+
async cleanedHtmlParts(): Promise<HtmlDiffPart[]> {
|
|
658
|
+
const htmlConfig = ConfigParser.getInstance().getConfig().html;
|
|
659
|
+
const cleaned: HtmlDiffPart[] = [];
|
|
660
|
+
for (const part of this.htmlParts) {
|
|
661
|
+
const minified = await minifyHtml(htmlCombinedSnapshot(part.subtree, htmlConfig?.combined));
|
|
662
|
+
if (!minified) continue;
|
|
663
|
+
cleaned.push({ ...part, subtree: minified });
|
|
664
|
+
}
|
|
665
|
+
return cleaned;
|
|
666
|
+
}
|
|
667
|
+
|
|
663
668
|
get ariaChanged(): string | null {
|
|
664
669
|
return this._ariaDiffResult;
|
|
665
670
|
}
|
package/src/action.ts
CHANGED
|
@@ -37,6 +37,7 @@ class Action {
|
|
|
37
37
|
public playwrightHelper: any;
|
|
38
38
|
public playwrightGroupId: string | null = null;
|
|
39
39
|
public assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
40
|
+
public executedSteps: ExecutedStep[] = [];
|
|
40
41
|
public lastValue: unknown;
|
|
41
42
|
private recorder?: PlaywrightRecorder;
|
|
42
43
|
private recovery: RecoveryRunner;
|
|
@@ -329,9 +330,9 @@ class Action {
|
|
|
329
330
|
|
|
330
331
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
331
332
|
|
|
332
|
-
const executedSteps:
|
|
333
|
+
const executedSteps: ExecutedStep[] = [];
|
|
333
334
|
const assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
334
|
-
const
|
|
335
|
+
const detachSteps = attachStepLogger(executedSteps, assertionSteps);
|
|
335
336
|
const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
|
|
336
337
|
this.playwrightGroupId = groupId;
|
|
337
338
|
const detachResponses = this.captureResponses();
|
|
@@ -360,12 +361,13 @@ class Action {
|
|
|
360
361
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
361
362
|
await recorder.promise();
|
|
362
363
|
this.lastValue = await returned;
|
|
364
|
+
if (!recorder.isRunning()) throw new Error('CodeceptJS recorder is stopped, commands were skipped and never reached the browser');
|
|
363
365
|
}
|
|
364
366
|
|
|
365
367
|
this.restorePageTimeout();
|
|
366
368
|
|
|
367
369
|
if (executedSteps.length > 0) {
|
|
368
|
-
codeString = executedSteps.join('\n');
|
|
370
|
+
codeString = executedSteps.map((step) => step.command).join('\n');
|
|
369
371
|
}
|
|
370
372
|
|
|
371
373
|
const pageState = await this.captureOnce({ codeBlock: codeString });
|
|
@@ -382,10 +384,11 @@ class Action {
|
|
|
382
384
|
this.assertionSteps = [];
|
|
383
385
|
throw err;
|
|
384
386
|
} finally {
|
|
387
|
+
this.executedSteps = executedSteps;
|
|
385
388
|
this.restorePageTimeout();
|
|
386
389
|
detachResponses();
|
|
387
390
|
if (groupId) await this.recorder!.endAction();
|
|
388
|
-
|
|
391
|
+
detachSteps();
|
|
389
392
|
if (stepSpan) {
|
|
390
393
|
stepSpan.end();
|
|
391
394
|
}
|
|
@@ -487,11 +490,13 @@ const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeEl
|
|
|
487
490
|
|
|
488
491
|
type StepListener = (step: any, error?: any) => void;
|
|
489
492
|
|
|
490
|
-
const attachStepLogger = (target:
|
|
493
|
+
export const attachStepLogger = (target: ExecutedStep[], assertionsTarget?: Array<{ name: string; args: any[] }>): (() => void) => {
|
|
491
494
|
const listener: StepListener = (step, error) => {
|
|
492
495
|
if (!step?.toCode) return;
|
|
493
496
|
if (step.name?.startsWith('grab')) return;
|
|
494
|
-
|
|
497
|
+
const executed: ExecutedStep = { command: step.toCode(), success: !error };
|
|
498
|
+
if (error) executed.error = errorToString(error);
|
|
499
|
+
target.push(executed);
|
|
495
500
|
if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
|
|
496
501
|
assertionsTarget.push({ name: step.name, args: step.args || [] });
|
|
497
502
|
}
|
|
@@ -503,12 +508,10 @@ const attachStepLogger = (target: string[], assertionsTarget?: Array<{ name: str
|
|
|
503
508
|
};
|
|
504
509
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, listener);
|
|
505
510
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
511
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
511
|
+
return () => {
|
|
512
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
513
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
514
|
+
};
|
|
512
515
|
};
|
|
513
516
|
|
|
514
517
|
const readFocusedElement = () => {
|
|
@@ -530,3 +533,9 @@ const readFocusedElement = () => {
|
|
|
530
533
|
if (typeof value === 'string' && value) focused.value = value.slice(0, 200);
|
|
531
534
|
return focused;
|
|
532
535
|
};
|
|
536
|
+
|
|
537
|
+
export interface ExecutedStep {
|
|
538
|
+
command: string;
|
|
539
|
+
success: boolean;
|
|
540
|
+
error?: string;
|
|
541
|
+
}
|
package/src/ai/conversation.ts
CHANGED
|
@@ -5,12 +5,13 @@ export interface ToolExecution {
|
|
|
5
5
|
input: any;
|
|
6
6
|
output: any;
|
|
7
7
|
wasSuccessful: boolean;
|
|
8
|
+
reasoning?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution {
|
|
11
|
+
export function toToolExecution(toolName: string, input: any, rawOutput: any, reasoning?: string): ToolExecution {
|
|
11
12
|
let output = rawOutput;
|
|
12
13
|
if (rawOutput?.type === 'json' && rawOutput?.value) output = rawOutput.value;
|
|
13
|
-
return { toolName, input, output, wasSuccessful: output?.success !== false };
|
|
14
|
+
return { toolName, input, output, wasSuccessful: output?.success !== false, reasoning };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function toolExecutionLabel(input: Record<string, any> | undefined): string {
|
|
@@ -213,13 +214,17 @@ export class Conversation {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
getToolExecutions(): ToolExecution[] {
|
|
216
|
-
const toolCalls = new Map<string, any>();
|
|
217
|
+
const toolCalls = new Map<string, { input: any; reasoning?: string }>();
|
|
217
218
|
for (const message of this.messages) {
|
|
218
219
|
if (message.role !== 'assistant') continue;
|
|
219
220
|
if (!Array.isArray(message.content)) continue;
|
|
221
|
+
const reasoning = message.content
|
|
222
|
+
.filter((part: any) => part.type === 'reasoning' && part.text?.trim())
|
|
223
|
+
.map((part: any) => part.text.trim())
|
|
224
|
+
.join('\n');
|
|
220
225
|
for (const part of message.content) {
|
|
221
226
|
if (part.type !== 'tool-call') continue;
|
|
222
|
-
toolCalls.set(part.toolCallId, part.input);
|
|
227
|
+
toolCalls.set(part.toolCallId, { input: part.input, reasoning });
|
|
223
228
|
}
|
|
224
229
|
}
|
|
225
230
|
|
|
@@ -230,7 +235,8 @@ export class Conversation {
|
|
|
230
235
|
for (const part of message.content) {
|
|
231
236
|
if (part.type !== 'tool-result') continue;
|
|
232
237
|
if (part.toolName === NARRATION_TOOL) continue;
|
|
233
|
-
|
|
238
|
+
const call = toolCalls.get(part.toolCallId);
|
|
239
|
+
executions.push(toToolExecution(part.toolName, call?.input || {}, part.output, call?.reasoning));
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|