diff --git a/examples/2048_recorder.ts b/examples/2048_recorder.ts new file mode 100644 index 000000000..a67d8400b --- /dev/null +++ b/examples/2048_recorder.ts @@ -0,0 +1,106 @@ +// This is an example of how to use the recorder to record actions and then generate a test from the recorded actions. +// Key Differences: +// - It replaces a playwright call in the original 2048 with a stagehand act that gets recorded. +// - It only runs the play loop one time. +// - It logs the playwright code to the console at the end of the example. + +import { Stagehand } from "../lib"; +import { z } from "zod"; +async function example() { + console.log("🎮 Starting 2048 bot..."); + const stagehand = new Stagehand({ + env: "LOCAL", + verbose: 1, + debugDom: true, + domSettleTimeoutMs: 100, + enableRecording: true, + }); + try { + console.log("🌟 Initializing Stagehand..."); + await stagehand.init(); + console.log("🌐 Navigating to 2048..."); + await stagehand.page.goto("https://ovolve.github.io/2048-AI/"); + console.log("⌛ Waiting for game to initialize..."); + await stagehand.page.waitForSelector(".grid-container", { timeout: 10000 }); + // Main game loop + while (true) { + console.log(`🔄 Game loop iteration...`); + // Add a small delay for UI updates + await new Promise((resolve) => setTimeout(resolve, 300)); + // Get current game state + const gameState = await stagehand.page.extract({ + instruction: `Extract the current game state: + 1. Score from the score counter + 2. All tile values in the 4x4 grid (empty spaces as 0) + 3. Highest tile value present`, + schema: z.object({ + score: z.number(), + highestTile: z.number(), + grid: z.array(z.array(z.number())), + }), + }); + const transposedGrid = gameState.grid[0].map((_, colIndex) => + gameState.grid.map((row) => row[colIndex]), + ); + const grid = transposedGrid.map((row, rowIndex) => ({ + [`row${rowIndex + 1}`]: row, + })); + console.log("Game State:", { + score: gameState.score, + highestTile: gameState.highestTile, + grid: grid, + }); + // Analyze board and decide next move + const analysis = await stagehand.page.extract({ + instruction: `Based on the current game state: + - Score: ${gameState.score} + - Highest tile: ${gameState.highestTile} + - Grid: This is a 4x4 matrix ordered by row (top to bottom) and column (left to right). The rows are stacked vertically, and tiles can move vertically between rows or horizontally between columns:\n${grid + .map((row) => { + const rowName = Object.keys(row)[0]; + return ` ${rowName}: ${row[rowName].join(", ")}`; + }) + .join("\n")} + What is the best move (up/down/left/right)? Consider: + 1. Keeping high value tiles in corners (bottom left, bottom right, top left, top right) + 2. Maintaining a clear path to merge tiles + 3. Avoiding moves that could block merges + 4. Only adjacent tiles of the same value can merge + 5. Making a move will move all tiles in that direction until they hit a tile of a different value or the edge of the board + 6. Tiles cannot move past the edge of the board + 7. Each move must move at least one tile`, + schema: z.object({ + move: z.enum(["up", "down", "left", "right"]), + confidence: z.number(), + reasoning: z.string(), + }), + }); + console.log("Move Analysis:", analysis); + await stagehand.page.act({ + action: `Press the ${analysis.move} key`, + }); + console.log("🎯 Executed move:", analysis.move); + break; + } + } catch (error) { + console.error("❌ Error in game loop:", error); + const isGameOver = await stagehand.page.evaluate(() => { + return document.querySelector(".game-over") !== null; + }); + if (isGameOver) { + console.log("🏁 Game Over!"); + return; + } + throw error; // Re-throw non-game-over errors + } + + const playwrightCode = await stagehand.dumpRecordedActionsCode({ + testFramework: "playwright", + language: "typescript", + }); + console.log("Playwright Code:\n\n", playwrightCode); + await stagehand.close(); +} +(async () => { + await example(); +})(); diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index b00e9f685..296a08fae 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -9,6 +9,8 @@ import { StagehandActHandler } from "./handlers/actHandler"; import { StagehandContext } from "./StagehandContext"; import { Page } from "../types/page"; import { + DumpRecordedActionsCodeOptions, + DumpRecordedActionsCodeResult, ExtractOptions, ExtractResult, ObserveOptions, @@ -65,6 +67,7 @@ export class StagehandPage { verbose: this.stagehand.verbose, llmProvider: this.stagehand.llmProvider, enableCaching: this.stagehand.enableCaching, + enableRecording: this.stagehand.enableRecording, logger: this.stagehand.logger, stagehandPage: this, stagehandContext: this.intContext, @@ -361,6 +364,16 @@ export class StagehandPage { }); } + async dumpRecordedActionsCode( + options: DumpRecordedActionsCodeOptions, + ): Promise { + if (!this.actHandler) { + throw new Error("Act handler not initialized"); + } + + return this.actHandler.dumpRecordedActionsCode(options); + } + async extract({ instruction, schema, diff --git a/lib/cache/ActionRecorder.ts b/lib/cache/ActionRecorder.ts new file mode 100644 index 000000000..9797c424e --- /dev/null +++ b/lib/cache/ActionRecorder.ts @@ -0,0 +1,214 @@ +import { LogLine } from "../../types/log"; +import { BaseCache, CacheEntry } from "./BaseCache"; + +export interface PlaywrightCommand { + method: string; + args: string[]; +} + +export interface ActionRecorderEntry extends CacheEntry { + data: { + url: string; + playwrightCommand: PlaywrightCommand; + componentString: string; + xpaths: string[]; + newStepString: string; + completed: boolean; + previousSelectors: string[]; + action: string; + }; +} + +/** + * ActionRecorder handles logging and retrieving actions along with their Playwright commands for test framework code generation purposes. + */ +export class ActionRecorder extends BaseCache { + constructor( + logger: (message: LogLine) => void, + cacheDir?: string, + cacheFile?: string, + ) { + logger({ + category: "action_recorder", + message: + "initializing action recorder at " + + cacheDir + + " with file " + + cacheFile, + level: 1, + }); + super(logger, cacheDir, cacheFile || "action_recorder.json"); + this.resetCache(); + } + + public async addActionStep({ + url, + action, + previousSelectors, + playwrightCommand, + componentString, + xpaths, + newStepString, + completed, + requestId, + }: { + url: string; + action: string; + previousSelectors: string[]; + playwrightCommand: PlaywrightCommand; + componentString: string; + requestId: string; + xpaths: string[]; + newStepString: string; + completed: boolean; + }): Promise { + this.logger({ + category: "action_recorder", + message: "adding action step to recorder", + level: 1, + auxiliary: { + action: { + value: action, + type: "string", + }, + requestId: { + value: requestId, + type: "string", + }, + url: { + value: url, + type: "string", + }, + previousSelectors: { + value: JSON.stringify(previousSelectors), + type: "object", + }, + playwrightCommand: { + value: JSON.stringify(playwrightCommand), + type: "object", + }, + }, + }); + + await this.set( + { url, action, previousSelectors }, + { + url, + playwrightCommand, + componentString, + xpaths, + newStepString, + completed, + previousSelectors, + action, + }, + requestId, + ); + } + + /** + * Retrieves all actions for a specific trajectory. + * @param trajectoryId - Unique identifier for the trajectory. + * @param requestId - The identifier for the current request. + * @returns An array of TrajectoryEntry objects or null if not found. + */ + public async getActionStep({ + url, + action, + previousSelectors, + requestId, + }: { + url: string; + action: string; + previousSelectors: string[]; + requestId: string; + }): Promise { + const data = await super.get({ url, action, previousSelectors }, requestId); + if (!data) { + return null; + } + + return data; + } + + public async removeActionStep(cacheHashObj: { + url: string; + action: string; + previousSelectors: string[]; + requestId: string; + }): Promise { + await super.delete(cacheHashObj); + } + + /** + * Clears all actions for a specific trajectory. + * @param trajectoryId - Unique identifier for the trajectory. + * @param requestId - The identifier for the current request. + */ + public async clearAction(requestId: string): Promise { + await super.deleteCacheForRequestId(requestId); + this.logger({ + category: "action_recorder", + message: "cleared action for ID", + level: 1, + auxiliary: { + requestId: { + value: requestId, + type: "string", + }, + }, + }); + } + + /** + * Gets all recorded actions sorted by timestamp. + * @returns An array of all recorded actions with their data. + */ + public async getAllActions(): Promise { + if (!(await this.acquireLock())) { + this.logger({ + category: "action_recorder", + message: "Failed to acquire lock for getting all actions", + level: 2, + }); + return []; + } + + try { + const cache = this.readCache(); + const entries = Object.values(cache) as ActionRecorderEntry[]; + return entries.sort((a, b) => a.timestamp - b.timestamp); + } catch (error) { + this.logger({ + category: "action_recorder", + message: "Error getting all actions", + level: 2, + auxiliary: { + error: { + value: error.message, + type: "string", + }, + trace: { + value: error.stack, + type: "string", + }, + }, + }); + return []; + } finally { + this.releaseLock(); + } + } + + /** + * Resets the entire action cache. + */ + public async resetCache(): Promise { + await super.resetCache(); + this.logger({ + category: "action_recorder", + message: "Action recorder has been reset.", + level: 1, + }); + } +} diff --git a/lib/handlers/actHandler.ts b/lib/handlers/actHandler.ts index d3f5165dd..8436a906b 100644 --- a/lib/handlers/actHandler.ts +++ b/lib/handlers/actHandler.ts @@ -5,6 +5,7 @@ import { PlaywrightCommandMethodNotSupportedException, } from "../../types/playwright"; import { ActionCache } from "../cache/ActionCache"; +import { ActionRecorder } from "../cache/ActionRecorder"; import { act, fillInVariables, verifyActCompletion } from "../inference"; import { LLMClient } from "../llm/LLMClient"; import { LLMProvider } from "../llm/LLMProvider"; @@ -12,14 +13,19 @@ import { StagehandContext } from "../StagehandContext"; import { StagehandPage } from "../StagehandPage"; import { generateId } from "../utils"; import { ScreenshotService } from "../vision"; +import { DumpRecordedActionsCodeOptions } from "../../types/stagehand"; +import { DumpRecordedActionsCodeResult } from "../../types/stagehand"; +import { TestCodeGenerator } from "./testCodeGenerator"; export class StagehandActHandler { private readonly stagehandPage: StagehandPage; private readonly verbose: 0 | 1 | 2; private readonly llmProvider: LLMProvider; private readonly enableCaching: boolean; + private readonly enableRecording: boolean; private readonly logger: (logLine: LogLine) => void; private readonly actionCache: ActionCache | undefined; + private readonly actionRecorder: ActionRecorder | undefined; private readonly actions: { [key: string]: { result: string; action: string }; }; @@ -29,6 +35,7 @@ export class StagehandActHandler { verbose, llmProvider, enableCaching, + enableRecording, logger, stagehandPage, userProvidedInstructions, @@ -36,6 +43,7 @@ export class StagehandActHandler { verbose: 0 | 1 | 2; llmProvider: LLMProvider; enableCaching: boolean; + enableRecording: boolean; logger: (logLine: LogLine) => void; llmClient: LLMClient; stagehandPage: StagehandPage; @@ -45,8 +53,12 @@ export class StagehandActHandler { this.verbose = verbose; this.llmProvider = llmProvider; this.enableCaching = enableCaching; + this.enableRecording = enableRecording; this.logger = logger; this.actionCache = enableCaching ? new ActionCache(this.logger) : undefined; + this.actionRecorder = enableRecording + ? new ActionRecorder(this.logger) + : undefined; this.actions = {}; this.stagehandPage = stagehandPage; this.userProvidedInstructions = userProvidedInstructions; @@ -881,6 +893,18 @@ export class StagehandActHandler { domSettleTimeoutMs, ); + this._recordActionIfEnabled({ + url: this.stagehandPage.page.url(), + action, + previousSelectors: [], + playwrightCommand: cachedStep.playwrightCommand, + componentString: cachedStep.componentString, + xpaths: [validXpath], + newStepString: cachedStep.newStepString, + completed: cachedStep.completed, + requestId, + }); + steps = steps + cachedStep.newStepString; await this.stagehandPage.page.evaluate( ({ chunksSeen }: { chunksSeen: number[] }) => { @@ -958,6 +982,52 @@ export class StagehandActHandler { } } + private async _recordActionIfEnabled({ + url, + action, + previousSelectors, + playwrightCommand, + componentString, + xpaths, + newStepString, + completed, + requestId, + }: { + url: string; + action: string; + previousSelectors: string[]; + playwrightCommand: { + method: string; + args: string[]; + }; + componentString: string; + xpaths: string[]; + newStepString: string; + completed: boolean; + requestId: string; + }) { + this.logger({ + category: "action", + message: "recording action, enableRecording is " + this.enableRecording, + level: 1, + }); + if (!this.enableRecording) { + return; + } + + await this.actionRecorder?.addActionStep({ + url, + action, + previousSelectors, + playwrightCommand, + componentString, + xpaths, + newStepString, + completed, + requestId, + }); + } + public async act({ action, steps = "", @@ -1335,41 +1405,43 @@ export class StagehandActHandler { steps += newStepString; + const cacheRecord = { + action, + url: originalUrl, + previousSelectors, + playwrightCommand: { + method, + args: responseArgs.map((arg) => arg?.toString() || ""), + }, + componentString, + requestId, + xpaths, + newStepString, + completed: response.completed, + }; + if (this.enableCaching) { - this.actionCache - .addActionStep({ - action, - url: originalUrl, - previousSelectors, - playwrightCommand: { - method, - args: responseArgs.map((arg) => arg?.toString() || ""), - }, - componentString, - requestId, - xpaths, - newStepString, - completed: response.completed, - }) - .catch((e) => { - this.logger({ - category: "action", - message: "error adding action step to cache", - level: 1, - auxiliary: { - error: { - value: e.message, - type: "string", - }, - trace: { - value: e.stack, - type: "string", - }, + this.actionCache.addActionStep(cacheRecord).catch((e) => { + this.logger({ + category: "action", + message: "error adding action step to cache", + level: 1, + auxiliary: { + error: { + value: e.message, + type: "string", }, - }); + trace: { + value: e.stack, + type: "string", + }, + }, }); + }); } + this._recordActionIfEnabled(cacheRecord); + if (this.stagehandPage.page.url() !== initialUrl) { steps += ` Result (Important): Page URL changed from ${initialUrl} to ${this.stagehandPage.page.url()}\n\n`; } @@ -1515,4 +1587,20 @@ export class StagehandActHandler { }; } } + + async dumpRecordedActionsCode( + options: DumpRecordedActionsCodeOptions, + ): Promise { + const generator = new TestCodeGenerator({ + actionRecorder: this.actionRecorder, + logger: this.logger, + llmProvider: this.llmProvider, + }); + + const code = await generator.generateCode( + options.language, + options.testFramework, + ); + return { code }; + } } diff --git a/lib/handlers/testCodeGenerator.ts b/lib/handlers/testCodeGenerator.ts new file mode 100644 index 000000000..bdb1799ed --- /dev/null +++ b/lib/handlers/testCodeGenerator.ts @@ -0,0 +1,235 @@ +import { convertPlaywrightCodeToFramework } from "../inference"; +import { LogLine } from "../../types/log"; +import { ActionRecorder, ActionRecorderEntry } from "../cache/ActionRecorder"; +import { LLMProvider } from "../llm/LLMProvider"; + +export class TestCodeGenerator { + private readonly actionRecorder: ActionRecorder | undefined; + private readonly logger: (logLine: LogLine) => void; + private readonly llmProvider: LLMProvider; + + constructor({ + actionRecorder, + logger, + llmProvider, + }: { + actionRecorder?: ActionRecorder; + logger: (logLine: LogLine) => void; + llmProvider: LLMProvider; + }) { + this.actionRecorder = actionRecorder; + this.logger = logger; + this.llmProvider = llmProvider; + } + + private _getUrlFromActions(): Promise { + if (!this.actionRecorder) { + return Promise.resolve(""); + } + return this.actionRecorder.getAllActions().then((actions) => { + const sortedActions = actions.sort( + (a, b) => (a.timestamp || 0) - (b.timestamp || 0), + ); + this.logger({ + category: "action", + message: "getting url from actions", + level: 1, + auxiliary: { + actions: { + value: sortedActions.map((action) => action.data.url).join(", "), + type: "string", + }, + }, + }); + return sortedActions.length > 0 ? sortedActions[0].data.url || "" : ""; + }); + } + + private _getRecordedActions(): Promise { + if (!this.actionRecorder) { + return Promise.resolve([]); + } + return this.actionRecorder + .getAllActions() + .then((actions) => + actions.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)), + ); + } + + private _convertXPathToPlaywrightSelector(xpath: string): string { + let playwrightSelector = xpath; + if (xpath.startsWith("/html/body")) { + playwrightSelector = `//body${xpath.replace("/html/body", "")}`; + } else if (xpath.startsWith("/")) { + playwrightSelector = `//${xpath.substring(1)}`; + } + return `xpath=${playwrightSelector}`; + } + + private async _buildPythonPlaywrightCode(): Promise { + const url = await this._getUrlFromActions(); + + const codeLines: string[] = [ + "from playwright.sync_api import sync_playwright", + "", + "def run():", + " with sync_playwright() as p:", + " browser = p.chromium.launch(headless=False)", + " context = browser.new_context()", + " page = context.new_page()", + "", + ` page.goto('${url}')`, + "", + ]; + + if (!this.actionRecorder) { + return codeLines.join("\n"); + } + + const sortedActions = await this._getRecordedActions(); + + for (const action of sortedActions) { + const data = action.data; + const command = data.playwrightCommand; + const xpath = data.xpaths?.[0]; + + if (!xpath) { + continue; + } + + const playwrightSelector = this._convertXPathToPlaywrightSelector(xpath); + + if (command.method === "click") { + codeLines.push(` page.locator('${playwrightSelector}').click()`); + } else if (command.method === "fill") { + codeLines.push( + ` page.locator('${playwrightSelector}').fill('${command.args[0]}')`, + ); + } else if (command.method === "press") { + codeLines.push(` page.keyboard.press('${command.args[0]}')`); + } else if (command.method === "type") { + codeLines.push( + ` page.locator('${playwrightSelector}').type('${command.args[0]}')`, + ); + } else if (command.method === "scrollIntoView") { + codeLines.push( + ` page.locator('${playwrightSelector}').scrollIntoViewIfNeeded()`, + ); + } + } + + codeLines.push( + ...[ + "", + " context.close()", + " browser.close()", + "", + "if __name__ == '__main__':", + " run()", + ], + ); + + return codeLines.join("\n"); + } + + private async _buildTypescriptPlaywrightCode(): Promise { + const url = await this._getUrlFromActions(); + + const codeLines: string[] = [ + "import { chromium } from '@playwright/test';", + "", + "async function run() {", + " const browser = await chromium.launch({ headless: false });", + " const context = await browser.newContext();", + " const page = await context.newPage();", + "", + ` await page.goto('${url}');`, + "", + ]; + + if (!this.actionRecorder) { + this.logger({ + category: "action", + message: "no actions recorded", + level: 1, + }); + return codeLines.join("\n"); + } + + const sortedActions = await this._getRecordedActions(); + + for (const action of sortedActions) { + const data = action.data; + const command = data.playwrightCommand; + const xpath = data.xpaths?.[0]; + + if (!xpath) { + continue; + } + + const playwrightSelector = this._convertXPathToPlaywrightSelector(xpath); + + if (command.method === "click") { + codeLines.push( + ` await page.locator('${playwrightSelector}').click();`, + ); + } else if (command.method === "fill") { + codeLines.push( + ` await page.locator('${playwrightSelector}').fill('${command.args[0]}');`, + ); + } else if (command.method === "press") { + codeLines.push(` await page.keyboard.press('${command.args[0]}');`); + } else if (command.method === "type") { + codeLines.push( + ` await page.locator('${playwrightSelector}').type('${command.args[0]}');`, + ); + } else if (command.method === "scrollIntoView") { + codeLines.push( + ` await page.locator('${playwrightSelector}').scrollIntoViewIfNeeded();`, + ); + } + } + + codeLines.push( + ...[ + "", + " await context.close();", + " await browser.close();", + "}", + "", + "run().catch(console.error);", + ], + ); + + return codeLines.join("\n"); + } + + async generateCode(language: string, testFramework: string): Promise { + // Get code in requested language + const getCodeForLanguage = async (language: string) => { + switch (language) { + case "typescript": + return this._buildTypescriptPlaywrightCode(); + case "python": + return this._buildPythonPlaywrightCode(); + default: + throw new Error(`Unsupported language: ${language}`); + } + }; + + // For non-Playwright frameworks, convert using LLM + if (testFramework !== "playwright") { + const playwrightCode = await getCodeForLanguage(language); + return await convertPlaywrightCodeToFramework( + playwrightCode, + testFramework, + this.llmProvider.getClient("gpt-4o"), + Math.random().toString(36).substring(2), + this.logger, + ); + } + + // For Playwright, return code directly + return await getCodeForLanguage(language); + } +} diff --git a/lib/index.ts b/lib/index.ts index af6b0daa5..e12554dd2 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -14,6 +14,8 @@ import { ActOptions, ActResult, ConstructorParams, + DumpRecordedActionsCodeOptions, + DumpRecordedActionsCodeResult, ExtractOptions, ExtractResult, InitFromPageOptions, @@ -316,6 +318,7 @@ export class Stagehand { public readonly headless: boolean; public verbose: 0 | 1 | 2; public llmProvider: LLMProvider; + public enableRecording: boolean; public enableCaching: boolean; private apiKey: string | undefined; @@ -342,6 +345,7 @@ export class Stagehand { browserbaseSessionCreateParams, domSettleTimeoutMs, enableCaching, + enableRecording, browserbaseSessionID, modelName, modelClientOptions, @@ -354,6 +358,9 @@ export class Stagehand { this.enableCaching = enableCaching ?? (process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true"); + this.enableRecording = + enableRecording ?? + (process.env.ENABLE_RECORDING && process.env.ENABLE_RECORDING === "true"); this.llmProvider = llmProvider || new LLMProvider(this.logger, this.enableCaching); this.intEnv = env; @@ -618,6 +625,12 @@ export class Stagehand { } } } + + async dumpRecordedActionsCode( + options: DumpRecordedActionsCodeOptions, + ): Promise { + return await this.stagehandPage.dumpRecordedActionsCode(options); + } } export * from "../types/browser"; diff --git a/lib/inference.ts b/lib/inference.ts index f23db9e2d..e1fd136fa 100644 --- a/lib/inference.ts +++ b/lib/inference.ts @@ -21,6 +21,8 @@ import { buildRefineUserPrompt, buildVerifyActCompletionSystemPrompt, buildVerifyActCompletionUserPrompt, + buildCodeConverterSystemPrompt, + buildCodeConverterUserPrompt, } from "./prompt"; export async function verifyActCompletion({ @@ -359,3 +361,25 @@ export async function observe({ return parsedResponse; } + +export async function convertPlaywrightCodeToFramework( + playwrightCode: string, + targetFramework: string, + llmClient: LLMClient, + requestId: string, + logger: (message: LogLine) => void, +) { + const response = await llmClient.createChatCompletion({ + options: { + messages: [ + buildCodeConverterSystemPrompt(), + buildCodeConverterUserPrompt(playwrightCode, targetFramework), + ], + temperature: 0.1, + requestId, + }, + logger, + }); + + return response.choices[0].message.content || ""; +} diff --git a/lib/prompt.ts b/lib/prompt.ts index 17ecc2ba3..691c7014b 100644 --- a/lib/prompt.ts +++ b/lib/prompt.ts @@ -388,3 +388,60 @@ export function buildObserveUserMessage( ${isUsingAccessibilityTree ? "Accessibility Tree" : "DOM"}: ${domElements}`, }; } + +// code conversion +export function buildCodeConverterSystemPrompt(): ChatMessage { + return { + role: "system", + content: `You are a code converter. You will be given Playwright code and asked to convert it to a different framework. You will return the converted code in the same language as the input code. + + IMPORTANT: + - You will NOT preface the code with any text. + - You will NOT give any explanation of the code. + - You WILL ONLY return the requested code. + + For example, if we are converting to cypress in typescript and the input code is: + + """ + import { chromium } from 'playwright'; + + (async () => { + const browser = await chromium.launch({ headless: false }); + const context = await browser.newContext(); + const page = await context.newPage(); + + await page.goto('https://ovolve.github.io/2048-AI/'); + + await page.keyboard.press('ArrowLeft'); + + await context.close(); + await browser.close(); + })(); + """ + + The output should be: + + """ + describe('2048 Game Test', () => { + it('should press the left arrow key', () => { + // Visit the 2048 game page + cy.visit('https://ovolve.github.io/2048-AI/'); + + // Press the left arrow key + cy.get('body').trigger('keydown', { keyCode: 37 }); + }); + }); + """ + `, + }; +} + +export function buildCodeConverterUserPrompt( + playwrightCode: string, + targetFramework: string, +): ChatMessage { + return { + role: "user", + content: `Convert the following Playwright code to ${targetFramework}: \n\`\`\`\n${playwrightCode}\n\`\`\`. Return ONLY the converted code, no other text or comments.`, + }; +} diff --git a/package.json b/package.json index 049e33539..3f4e86d61 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "types": "./dist/index.d.ts", "scripts": { "2048": "npm run build-dom-scripts && tsx examples/2048.ts", + "2048-recorder": "npm run build-dom-scripts && tsx examples/2048_recorder.ts", "popup": "npm run build-dom-scripts && tsx examples/popup.ts", "example": "npm run build-dom-scripts && tsx examples/example.ts", "debug-url": "npm run build-dom-scripts && tsx examples/debugUrl.ts", diff --git a/types/stagehand.ts b/types/stagehand.ts index f47378b0b..9df27ee55 100644 --- a/types/stagehand.ts +++ b/types/stagehand.ts @@ -18,6 +18,7 @@ export interface ConstructorParams { domSettleTimeoutMs?: number; browserbaseSessionCreateParams?: Browserbase.Sessions.SessionCreateParams; enableCaching?: boolean; + enableRecording?: boolean; browserbaseSessionID?: string; modelName?: AvailableModel; llmClient?: LLMClient; @@ -94,3 +95,12 @@ export interface ObserveResult { selector: string; description: string; } + +export interface DumpRecordedActionsCodeOptions { + language: "typescript" | "python"; + testFramework: "playwright" | "puppeteer" | "cypress"; +} + +export interface DumpRecordedActionsCodeResult { + code: string; +}