@cutleryapp/agent 1.0.22 → 1.0.24

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.
@@ -147,17 +147,24 @@ class TestExecutor {
147
147
  catch { /* fall to AI */ }
148
148
  }
149
149
  }
150
- // 7. AI fallback — for anything not handled or ambiguous
150
+ // 7. AI — single-shot for deterministic steps, full loop for intent steps
151
151
  if (!handled) {
152
- console.log(` 🤖 MCP could not handle step, using AI: "${raw}"`);
153
- await aiStepFallback(page, raw);
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 — let AI try to recover
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 aiStepFallback(page, raw);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "Local agent that connects your machine to the Cutlery QA platform and runs UI tests via Playwright",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {