@cutleryapp/agent 1.0.22 → 1.0.23
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/mcp-executor.js +83 -6
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -147,17 +147,24 @@ class TestExecutor {
|
|
|
147
147
|
catch { /* fall to AI */ }
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
// 7. AI
|
|
150
|
+
// 7. AI — single-shot for deterministic steps, full loop for intent steps
|
|
151
151
|
if (!handled) {
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
const isDeterministic = /^(click|fill|enter|type|verify|check|assert|select|choose|wait|hover|scroll)/i.test(lower.trim());
|
|
153
|
+
if (isDeterministic) {
|
|
154
|
+
console.log(` 🤖 Quick AI selector lookup for: "${raw}"`);
|
|
155
|
+
await aiSingleShot(page, raw);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(` 🤖 AI intent loop for: "${raw}"`);
|
|
159
|
+
await aiStepFallback(page, raw);
|
|
160
|
+
}
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
catch (err) {
|
|
157
|
-
// MCP execution failed —
|
|
158
|
-
console.log(` ⚠️ MCP step failed (${err.message}), trying AI...`);
|
|
164
|
+
// MCP execution failed — single-shot AI recovery, no loop
|
|
165
|
+
console.log(` ⚠️ MCP step failed (${err.message.split('\n')[0]}), trying AI...`);
|
|
159
166
|
try {
|
|
160
|
-
await
|
|
167
|
+
await aiSingleShot(page, raw);
|
|
161
168
|
}
|
|
162
169
|
catch (aiErr) {
|
|
163
170
|
stepError = err.message;
|
|
@@ -293,6 +300,76 @@ Set "done": true with empty "actions" array when the goal is fully accomplished.
|
|
|
293
300
|
* and returns a SEQUENCE of actions to accomplish it — then executes them one by one.
|
|
294
301
|
* After each action it re-screenshots so the AI can verify progress and adapt.
|
|
295
302
|
*/
|
|
303
|
+
/** Single-shot AI: one DOM extract + screenshot → one action → done. No looping. */
|
|
304
|
+
async function aiSingleShot(page, stepText) {
|
|
305
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
306
|
+
if (!openaiKey)
|
|
307
|
+
throw new Error(`No OPENAI_API_KEY for: "${stepText}"`);
|
|
308
|
+
const { default: OpenAI } = await import('openai');
|
|
309
|
+
const openai = new OpenAI({ apiKey: openaiKey });
|
|
310
|
+
const domElements = await extractDomElements(page);
|
|
311
|
+
const screenshotBuffer = await page.screenshot({ type: 'png' });
|
|
312
|
+
const base64 = screenshotBuffer.toString('base64');
|
|
313
|
+
const response = await openai.chat.completions.create({
|
|
314
|
+
model: 'gpt-4o',
|
|
315
|
+
max_tokens: 300,
|
|
316
|
+
messages: [{
|
|
317
|
+
role: 'user',
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: 'text',
|
|
321
|
+
text: `You are a Playwright selector expert. Given this test step and the current page, return a single JSON action.
|
|
322
|
+
|
|
323
|
+
Step: "${stepText}"
|
|
324
|
+
|
|
325
|
+
## REAL PAGE ELEMENTS (use these — do NOT guess selectors):
|
|
326
|
+
${domElements}
|
|
327
|
+
|
|
328
|
+
Return ONLY valid JSON, one of:
|
|
329
|
+
{"action":"click","selector":"EXACT_SELECTOR"}
|
|
330
|
+
{"action":"fill","selector":"EXACT_SELECTOR","value":"VALUE"}
|
|
331
|
+
{"action":"verify","text":"TEXT_TO_CHECK","not":false}
|
|
332
|
+
{"action":"select","selector":"EXACT_SELECTOR","value":"OPTION"}
|
|
333
|
+
{"action":"wait","ms":1000}
|
|
334
|
+
|
|
335
|
+
Rules:
|
|
336
|
+
- Pick selector from the DOM list above using id, name, data-test, aria-label, class exactly as shown
|
|
337
|
+
- For "icon" steps: find element whose class/id/data-test contains the icon keyword
|
|
338
|
+
- For verify: check if text appears in page body`
|
|
339
|
+
},
|
|
340
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${base64}` } }
|
|
341
|
+
]
|
|
342
|
+
}]
|
|
343
|
+
});
|
|
344
|
+
const raw2 = (response.choices[0]?.message?.content || '')
|
|
345
|
+
.trim().replace(/```json\n?/gi, '').replace(/```/g, '').trim();
|
|
346
|
+
if (!raw2 || raw2 === 'NOT_FOUND')
|
|
347
|
+
throw new Error(`AI could not find element for: "${stepText}"`);
|
|
348
|
+
const act = JSON.parse(raw2);
|
|
349
|
+
console.log(` 🤖 AI action: ${JSON.stringify(act)}`);
|
|
350
|
+
if (act.action === 'click') {
|
|
351
|
+
const ok = await tryAIClick(page, act.selector);
|
|
352
|
+
if (!ok)
|
|
353
|
+
throw new Error(`AI click failed: ${act.selector}`);
|
|
354
|
+
}
|
|
355
|
+
else if (act.action === 'fill') {
|
|
356
|
+
await tryAIFill(page, act.selector, act.value || '');
|
|
357
|
+
}
|
|
358
|
+
else if (act.action === 'verify') {
|
|
359
|
+
const content = await page.textContent('body') || '';
|
|
360
|
+
const found = content.includes(act.text);
|
|
361
|
+
if (act.not && found)
|
|
362
|
+
throw new Error(`Text "${act.text}" should NOT be visible`);
|
|
363
|
+
if (!act.not && !found)
|
|
364
|
+
throw new Error(`Expected text not found: "${act.text}"`);
|
|
365
|
+
}
|
|
366
|
+
else if (act.action === 'select') {
|
|
367
|
+
await page.locator(act.selector).first().selectOption({ label: act.value });
|
|
368
|
+
}
|
|
369
|
+
else if (act.action === 'wait') {
|
|
370
|
+
await page.waitForTimeout(act.ms || 1000);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
296
373
|
/** Extract real interactive elements from the DOM for AI selector accuracy */
|
|
297
374
|
async function extractDomElements(page) {
|
|
298
375
|
try {
|