UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

423 lines 18.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.test = void 0; exports.getOrCreateDonobuStack = getOrCreateDonobuStack; exports.getBrowserStorageState = getBrowserStorageState; exports.getBrowserStorageStateFixture = getBrowserStorageStateFixture; exports.anthropicClient = anthropicClient; exports.anthropicClientFixture = anthropicClientFixture; exports.anthropicAwsBedrockClient = anthropicAwsBedrockClient; exports.anthropicAwsBedrockClientFixture = anthropicAwsBedrockClientFixture; exports.googleGeminiClient = googleGeminiClient; exports.googleGeminiClientFixture = googleGeminiClientFixture; exports.openAiClient = openAiClient; exports.openAiClientFixture = openAiClientFixture; exports.vercelAiClientFixture = vercelAiClientFixture; exports.gptClient = gptClient; exports.gptClientFixture = gptClientFixture; /** * This code extends the Playwright Page object and test fixture with * test-related Donobu methods. */ const uuid_1 = require("uuid"); const test_1 = require("@playwright/test"); const RequestContextHolder_1 = require("./managers/RequestContextHolder"); const ToolManager_1 = require("./managers/ToolManager"); const ToolTipper_1 = require("./managers/ToolTipper"); const PlaywrightUtils_1 = require("./utils/PlaywrightUtils"); const MiscUtils_1 = require("./utils/MiscUtils"); const DonobuDeploymentEnvironment_1 = require("./models/DonobuDeploymentEnvironment"); const GptClientFactory_1 = require("./clients/GptClientFactory"); const VercelAiGptClient_1 = require("./clients/VercelAiGptClient"); const DonobuStack_1 = require("./managers/DonobuStack"); const DEFAULT_FLOW_URL = 'https://example.com'; const DEFAULT_GPT_MODEL_FOR_PLATFORM = { ANTHROPIC: 'claude-3-5-sonnet-latest', GOOGLE_GEMINI: 'gemini-2.0-flash', OPENAI: 'gpt-4o', }; let donobuStack = undefined; async function getOrCreateDonobuStack() { if (!donobuStack) { donobuStack = await (0, DonobuStack_1.setupDonobuStack)(DonobuDeploymentEnvironment_1.DonobuDeploymentEnvironment.LOCAL, new RequestContextHolder_1.RequestContextHolder()); return donobuStack; } else { return donobuStack; } } async function getBrowserStorageState(flowIdOrName) { return (await getOrCreateDonobuStack()).flowsManager.getBrowserStorageState(flowIdOrName); } function getBrowserStorageStateFixture(flowIdOrName) { return async ({}, use) => { const storage = await (await getOrCreateDonobuStack()).flowsManager.getBrowserStorageState(flowIdOrName); await use(storage); }; } async function anthropicClient(modelName, apiKey) { const divinedApiKey = apiKey || process.env.ANTHROPIC_API_KEY; if (!divinedApiKey) { throw new Error('An API key is required to use the Anthropic API. Please provide one as an argument or set it using the ANTHROPIC_API_KEY environment variable.'); } return GptClientFactory_1.GptClientFactory.createFromGptConfig({ type: 'ANTHROPIC', apiKey: divinedApiKey, modelName: modelName, }); } function anthropicClientFixture(modelName, apiKey) { return async ({}, use) => { const client = await anthropicClient(modelName, apiKey); await use(client); }; } async function anthropicAwsBedrockClient(modelName, region, accessKeyId, secretAccessKey) { const divinedAccessKeyId = accessKeyId || process.env.AWS_ACCESS_KEY_ID; const divinedSecretAccessKey = secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY; if (!divinedAccessKeyId || !divinedSecretAccessKey) { throw new Error('AWS access key ID and secret access key are required to use the Anthropic AWS Bedrock API. Please provide them as arguments or set them using the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.'); } return GptClientFactory_1.GptClientFactory.createFromGptConfig({ type: 'ANTHROPIC_AWS_BEDROCK', region: region, modelName: modelName, accessKeyId: divinedAccessKeyId, secretAccessKey: divinedSecretAccessKey, }); } function anthropicAwsBedrockClientFixture(modelName, region, accessKeyId, secretAccessKey) { return async ({}, use) => { const client = await anthropicAwsBedrockClient(modelName, region, accessKeyId, secretAccessKey); await use(client); }; } async function googleGeminiClient(modelName, apiKey) { const divinedApiKey = apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY; if (!divinedApiKey) { throw new Error('An API key is required to use the Google Gemini API. Please provide one as an argument or set it using the GOOGLE_GENERATIVE_AI_API_KEY environment variable.'); } return GptClientFactory_1.GptClientFactory.createFromGptConfig({ type: 'GOOGLE_GEMINI', apiKey: divinedApiKey, modelName: modelName, }); } function googleGeminiClientFixture(modelName, apiKey) { return async ({}, use) => { const client = await googleGeminiClient(modelName, apiKey); await use(client); }; } async function openAiClient(modelName, apiKey) { const divinedApiKey = apiKey || process.env.OPENAI_API_KEY; if (!divinedApiKey) { throw new Error('An API key is required to use the OpenAI API. Please provide one as an argument or set it using the OPENAI_API_KEY environment variable.'); } return GptClientFactory_1.GptClientFactory.createFromGptConfig({ type: 'OPENAI', apiKey: divinedApiKey, modelName: modelName, }); } function openAiClientFixture(modelName, apiKey) { return async ({}, use) => { const client = await openAiClient(modelName, apiKey); await use(client); }; } function vercelAiClientFixture(model) { return async ({}, use) => { const client = new VercelAiGptClient_1.VercelAiGptClient(model); await use(client); }; } /** * This fixture resolves a GPT client based on environment variables. * Supplying different environment variables will result in different GPT * clients being created. Here is a mapping of client type to environment * variables for how to create the various types of clients... * * Anthropic GPT client requires: * - ANTHROPIC_API_KEY * * Google GPT client requires: * - GOOGLE_GENERATIVE_AI_API_KEY * * OpenAI GPT client requires: * - OPENAI_API_KEY * * If the modelName is not specified, it will be defaulted according to the * DEFAULT_GPT_MODEL_FOR_PLATFORM mapping. */ async function gptClient(modelName) { // Check which API keys are available const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasGoogleKey = !!process.env.GOOGLE_GENERATIVE_AI_API_KEY; const hasOpenAiKey = !!process.env.OPENAI_API_KEY; // Priority order: Anthropic, Google, OpenAI if (hasAnthropicKey) { const model = modelName || DEFAULT_GPT_MODEL_FOR_PLATFORM.ANTHROPIC; return anthropicClient(model); } else if (hasGoogleKey) { const model = modelName || DEFAULT_GPT_MODEL_FOR_PLATFORM.GOOGLE_GEMINI; return googleGeminiClient(model); } else if (hasOpenAiKey) { const model = modelName || DEFAULT_GPT_MODEL_FOR_PLATFORM.OPENAI; return openAiClient(model); } else { const donobu = await getOrCreateDonobuStack(); const defaultClient = (await donobu.flowsManager.createGptClient(null)) .gptClient; if (defaultClient) { return defaultClient; } else { // No API keys are available and there is not flow-runner set up, throw an error. throw new Error(`No API keys found for any supported GPT providers. Please set one of the following environment variables: ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, or OPENAI_API_KEY. Alternatively, set a flow-runner agent via POST /api/agents/flow-runner or via the Donobu app.`); } } } function gptClientFixture(modelName) { return async ({}, use) => { const client = await gptClient(modelName); await use(client); }; } __exportStar(require("playwright/test"), exports); exports.test = test_1.test.extend({ // 1) Declare `gptClient` as an "option" fixture with a default of `undefined`. gptClient: [ undefined, // default { option: true }, // so that test.use({ gptClient: ... }) can override ], // Override the default page fixture page: async ({ page, gptClient }, use, testInfo) => { // Patch the page with "run", but do it lazily const donobu = await getOrCreateDonobuStack(); await page .context() .addInitScript(PlaywrightUtils_1.PlaywrightUtils.accessibilityTestInitScript()); await page .context() .addInitScript(PlaywrightUtils_1.PlaywrightUtils.clickableElementsTrackerInitScript()); await page .context() .addInitScript(PlaywrightUtils_1.PlaywrightUtils.dialogPromptTrackerInitScript()); await page .context() .addInitScript(PlaywrightUtils_1.PlaywrightUtils.smartSelectorGeneratorInitScript()); let isInitialized = false; async function maybeInitDonobu() { if (isInitialized) { return; } const donobuTestFlowMetadata = { id: (0, uuid_1.v4)(), name: testInfo.title, createdWithDonobuVersion: MiscUtils_1.MiscUtils.DONOBU_VERSION, browser: { using: { type: 'device', deviceName: 'Desktop Chromium', headless: true, }, }, gptConfigName: null, hasGptConfigNameOverride: false, customTools: null, defaultToolTipDurationMilliseconds: 0, runMode: 'DETERMINISTIC', isControlPanelEnabled: false, callbackUrl: null, targetWebsite: DEFAULT_FLOW_URL, overallObjective: testInfo.annotations.find((v) => { return v.type === 'objective'; })?.description ?? null, allowedTools: [], resultJsonSchema: null, result: null, inputTokensUsed: 0, completionTokensUsed: 0, iterations: 0, maxIterations: 0, startedAt: new Date().getTime(), completedAt: null, state: 'RUNNING_ACTION', nextState: null, }; const persistenceLayer = await donobu.flowsPersistenceFactory.createPersistenceLayer(); await persistenceLayer.saveMetadata(donobuTestFlowMetadata); page._donobuFlowMetadata = donobuTestFlowMetadata; isInitialized = true; } const originalGoto = page.goto; // Intercept 'page.goto' so that Donobu can later re-run it as a standard // Donobu action if necessary. This can happen if you run a Playwright // test using npx, but later decide to rerun that run using the Donobu UI. page.goto = async (url, options) => { await maybeInitDonobu(); const startedAt = new Date().getTime(); const flowId = page._donobuFlowMetadata.id; const persistenceLayer = await donobu.flowsPersistenceFactory.createPersistenceLayer(); // Update the target website to whatever the first place we are // navigating to is. if (page._donobuFlowMetadata.targetWebsite === DEFAULT_FLOW_URL) { page._donobuFlowMetadata = { ...page._donobuFlowMetadata, targetWebsite: url, }; await persistenceLayer.saveMetadata(page._donobuFlowMetadata); } try { const resp = await originalGoto.call(page, url, options); const postCallImage = await PlaywrightUtils_1.PlaywrightUtils.takePngScreenshot(page); const postCallImageId = await persistenceLayer.savePngScreenShot(flowId, postCallImage); const completedAt = new Date().getTime(); await persistenceLayer.saveToolCall(flowId, { id: MiscUtils_1.MiscUtils.createAdHocToolCallId(), toolName: 'goToWebpage', parameters: { url: url, }, outcome: { isSuccessful: true, forLlm: `Successfully navigated to ${url}`, metadata: null, }, postCallImageId: postCallImageId, page: url, startedAt: startedAt, completedAt: completedAt, }); return resp; } catch (error) { const postCallImage = await PlaywrightUtils_1.PlaywrightUtils.takePngScreenshot(page); const postCallImageId = await persistenceLayer.savePngScreenShot(flowId, postCallImage); const completedAt = new Date().getTime(); await persistenceLayer.saveToolCall(flowId, { id: MiscUtils_1.MiscUtils.createAdHocToolCallId(), toolName: 'goToWebpage', parameters: { url: url, }, outcome: { isSuccessful: false, forLlm: `FAILED! ${typeof error}: ${error.message}`, metadata: null, }, postCallImageId: postCallImageId, page: url, startedAt: startedAt, completedAt: completedAt, }); throw error; } }; page.run = async (toolName, toolParams) => { await maybeInitDonobu(); const persistenceLayer = await donobu.flowsPersistenceFactory.createPersistenceLayer(); const clonedParams = toolParams ? JSON.parse(JSON.stringify(toolParams)) : {}; clonedParams.rationale || (clonedParams.rationale = ''); const toolCallContext = { flowsManager: donobu.flowsManager, persistence: persistenceLayer, gptClient: gptClient ?? null, toolTipper: new ToolTipper_1.ToolTipper(0), proposedToolCalls: [], invokedToolCalls: [], page: page, metadata: page._donobuFlowMetadata, toolCallId: clonedParams.toolCallId ?? MiscUtils_1.MiscUtils.createAdHocToolCallId(), }; return new ToolManager_1.ToolManager(ToolManager_1.ToolManager.ALL_TOOLS) .invokeTool(toolCallContext, toolName, clonedParams, false) .then(async (result) => { await persistenceLayer.saveMetadata(page._donobuFlowMetadata); await persistenceLayer.saveToolCall(page._donobuFlowMetadata.id, result); return result; }) .then((result) => result.outcome); }; page.analyzePageText = async (params) => { return page.run('analyzePageText', params); }; page.assertPageText = async (params) => { return page.run('assertPageText', params); }; page.chooseSelectOption = async (params) => { return page.run('chooseSelectOption', params); }; page.clickElement = async (params) => { return page.run('click', params); }; page.createCookieReport = async () => { return page.run('createCookieReport'); }; page.detectBrokenLinks = async () => { return page.run('detectBrokenLinks'); }; page.hoverOverElement = async (params) => { return page.run('hover', params); }; page.inputRandomizedEmailAddress = async (params) => { return page.run('inputRandomizedEmailAddress', params); }; page.inputText = async (params) => { return page.run('inputText', params); }; page.pressKey = async (params) => { return page.run('pressKey', params); }; page.runAccessibilityTest = async () => { return page.run('runAccessibilityTest'); }; page.scroll = async (params) => { return page.run('scrollPage', params); }; page.visuallyAssert = async (params) => { return page.run('assert', params); }; try { // Let Playwright continue and give tests the patched page. await use(page); } finally { try { const persistenceLayer = await donobu.flowsPersistenceFactory.createPersistenceLayer(); if (page._donobuFlowMetadata) { page._donobuFlowMetadata.state = 'SUCCESS'; page._donobuFlowMetadata.completedAt = new Date().getTime(); await persistenceLayer.saveMetadata(page._donobuFlowMetadata); } } catch (error) { // Log but don't throw, to ensure cleanup continues. console.error('Error during test cleanup:', error); } } }, }); //# sourceMappingURL=test.js.map