donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
435 lines • 19.3 kB
JavaScript
"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 DonobuFlow_1 = require("./managers/DonobuFlow");
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.extract = async (params) => {
await maybeInitDonobu();
if (!gptClient) {
throw Error('Cannot extract an object from page without a GPT client set up!');
}
const persistenceLayer = await donobu.flowsPersistenceFactory.createPersistenceLayer();
const toolCallHistory = await persistenceLayer.getToolCalls(page._donobuFlowMetadata.id);
const structuredOutputMessage = await (0, DonobuFlow_1.extractFromPage)(params.instruction ??
'Generate an object conforming to the given JSON-schema', params.schema, page, toolCallHistory, gptClient);
page._donobuFlowMetadata.resultJsonSchema = params.schema;
page._donobuFlowMetadata.result = structuredOutputMessage.output;
await persistenceLayer.saveMetadata(page._donobuFlowMetadata);
return structuredOutputMessage.output;
};
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=playwrightTestExtensions.js.map