UNPKG

donobu

Version:

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

249 lines (248 loc) 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeGenerator = void 0; const AnalyzePageTextTool_1 = require("../tools/AnalyzePageTextTool"); const AssertPageTextTool_1 = require("../tools/AssertPageTextTool"); const AssertTool_1 = require("../tools/AssertTool"); const ChangeWebBrowserTabTool_1 = require("../tools/ChangeWebBrowserTabTool"); const ChooseSelectOptionTool_1 = require("../tools/ChooseSelectOptionTool"); const ClickTool_1 = require("../tools/ClickTool"); const CreateBrowserCookieReportTool_1 = require("../tools/CreateBrowserCookieReportTool"); const GoForwardOrBackTool_1 = require("../tools/GoForwardOrBackTool"); const GoToWebpageTool_1 = require("../tools/GoToWebpageTool"); const HoverOverElementTool_1 = require("../tools/HoverOverElementTool"); const InputRandomizedEmailAddressTool_1 = require("../tools/InputRandomizedEmailAddressTool"); const InputTextTool_1 = require("../tools/InputTextTool"); const MakeCommentTool_1 = require("../tools/MakeCommentTool"); const MarkObjectiveCompleteTool_1 = require("../tools/MarkObjectiveCompleteTool"); const MarkObjectiveNotCompletableTool_1 = require("../tools/MarkObjectiveNotCompletableTool"); const PressKeyTool_1 = require("../tools/PressKeyTool"); const ReloadPageTool_1 = require("../tools/ReloadPageTool"); const RunAccessibilityTestTool_1 = require("../tools/RunAccessibilityTestTool"); const ScrollPageTool_1 = require("../tools/ScrollPageTool"); const SummarizeLearningsTool_1 = require("../tools/SummarizeLearningsTool"); const JsonUtils_1 = require("../utils/JsonUtils"); const ToolManager_1 = require("./ToolManager"); class CodeGenerator { constructor(gptConfigsManager, agentsManager) { this.gptConfigsManager = gptConfigsManager; this.agentsManager = agentsManager; } /** Creates a Node.js Microsoft Playwright script to replay the given flow. */ async getFlowAsPlaywrightScript(flowMetadata, toolCalls) { // These tools are not supported in the generated script because they have // static outputs that have no side effects, so they are not actually // doing anything. const unsupportedToolsByName = new Set([ MakeCommentTool_1.MakeCommentTool.NAME, MarkObjectiveCompleteTool_1.MarkObjectiveCompleteTool.NAME, MarkObjectiveNotCompletableTool_1.MarkObjectiveNotCompletableTool.NAME, SummarizeLearningsTool_1.SummarizeLearningsTool.NAME, ]); const isGptClientRequired = flowMetadata.resultJsonSchema || toolCalls .map((toolCall) => { return toolCall.name; }) .map((toolCallName) => { return ToolManager_1.ToolManager.ALL_TOOLS.find((t) => t.name === toolCallName); }) .filter((tool) => { return tool?.requiresGpt; }).length > 0; let gptConfig = null; // Check if the flow metadata has a specific GPT config name. // If it does, try to get it from the GptConfigsManager. // If it doesn't, try to get the default GPT config from the AgentsManager. // // This is done so that we can generate code using the preferred GPT client. if (isGptClientRequired) { try { if (flowMetadata.gptConfigName) { gptConfig = await this.gptConfigsManager.get(flowMetadata.gptConfigName); } } catch (_error) { // This is expected if the config does not exist. } if (!gptConfig) { const gptConfigName = await this.agentsManager.get('flow-runner'); if (gptConfigName) { try { gptConfig = await this.gptConfigsManager.get(gptConfigName); } catch (_error) { // This is expected if the config does not exist. } } } } const gptSetupNote = isGptClientRequired ? ` * * Also, since this test is using tools that require the usage of an LLM, be * sure to have an appropriate LLM API key available as an environment variable * (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc). ` : ' '; const hasObjective = (flowMetadata.overallObjective?.trim().length ?? 0) > 0; const testDetails = hasObjective ? `{ annotation: { type: 'objective', description: \`${this.sanitizeForTemplateLiteral(flowMetadata.overallObjective)}\` } }` : '{}'; const testName = flowMetadata.name ? flowMetadata.name // Escape backslashes first. .replace(/\\/g, '\\\\') // Escape single quotes. .replace(/'/g, "\\'") // Escape newlines. .replace(/\n/g, '\\n') // Escape carriage returns. .replace(/\r/g, '\\r') : `Test for ${flowMetadata.targetWebsite}`; const useStorageState = flowMetadata.browser && flowMetadata.browser.initialState ? `storageState: getBrowserStorageStateFixture(${JSON.stringify(flowMetadata.browser.initialState, null, 2)}),` : undefined; let useGptClientImport = undefined; let useGptClient = undefined; if (isGptClientRequired) { if (gptConfig && gptConfig.type === 'ANTHROPIC_AWS_BEDROCK') { useGptClientImport = "import { anthropicAwsBedrockClientFixture } from 'donobu';"; useGptClient = `gptClient: anthropicAwsBedrockClientFixture('${gptConfig.modelName}', '${gptConfig.region}'),`; } else { useGptClientImport = "import { gptClientFixture } from 'donobu';"; useGptClient = `gptClient: gptClientFixture(),`; } } let testExtension = ''; if (useStorageState || useGptClient) { testExtension = `.extend({${useStorageState ?? ''}${useGptClient ?? ''}})`; } const scriptedToolCalls = toolCalls .filter((toolCall) => !unsupportedToolsByName.has(toolCall.name)) .map((toolCall) => { return CodeGenerator.convertProposedToolCallToPlaywrightCode(toolCall); }) .join('\n'); const resultJson = flowMetadata.resultJsonSchema ? `// Extract an object from the page using the following JSON-schema. const extractedObject = await page.extract({schema: ${JSON.stringify(flowMetadata.resultJsonSchema)}}); testInfo.attach('extracted-object', { body: JSON.stringify(extractedObject), contentType: 'application/json' });` : ''; const script = `/** * Be sure that Donobu is installed before running this script... * 'npm install donobu' or 'yarn add donobu' * * Also, be sure that Playwright's browsers are installed... * 'npx playwright install' ${gptSetupNote}*/ import { test } from 'donobu';${useGptClientImport ? '\n' + useGptClientImport : ''}${useStorageState ? "\nimport { getBrowserStorageStateFixture } from 'donobu';" : ''} const testTitle = '${testName}'; const testDetails = ${testDetails}; test${testExtension}(testTitle, testDetails, async ({ page }${flowMetadata.resultJsonSchema ? ', testInfo' : ''}) => { ${scriptedToolCalls} ${resultJson} }); `; return this.prettifyCode(script); } /** * Maps a proposed Donobu tool call to valid NodeJS Playwright code that uses * the `DonobuExtendedPage` extension. */ static convertProposedToolCallToPlaywrightCode(proposedToolCall) { const rawParams = JsonUtils_1.JsonUtils.objectToJson(proposedToolCall.parameters); const rationale = rawParams.rationale && rawParams.rationale.trim().length > 0 ? rawParams.rationale .split('\n') .map((line) => `// ${line}`.trim()) .join('\n') + '\n' : ''; // Delete fields that should not be directly mapped. delete rawParams.rationale; delete rawParams.whyThisAnnotation; delete rawParams.annotation; const hasNonEmptyParameters = Object.keys(rawParams).length > 0; const serializedParams = JSON.stringify(rawParams, null, 2); switch (proposedToolCall.name) { case AssertTool_1.AssertTool.NAME: { return `${rationale}await page.visuallyAssert(${serializedParams});`; } case ChangeWebBrowserTabTool_1.ChangeWebBrowserTabTool.NAME: { return `${rationale}page = page.context().pages().find((tab) => tab.url() === '${rawParams.tabUrl}');`; } case ChooseSelectOptionTool_1.ChooseSelectOptionTool.NAME: { return `${rationale}await page.chooseSelectOption(${serializedParams});`; } case ClickTool_1.ClickTool.NAME: { return `${rationale}await page.clickElement(${serializedParams});`; } case GoForwardOrBackTool_1.GoForwardOrBackTool.NAME: if (rawParams.direction === 'FORWARD') { return `${rationale}await page.goForward();`; } else if (rawParams.direction === 'BACK') { return `${rationale}await page.goBack();`; } else { throw new Error(`Invalid ${GoForwardOrBackTool_1.GoForwardOrBackTool.NAME} params: ${serializedParams}`); } case GoToWebpageTool_1.GoToWebpageTool.NAME: return `${rationale}await page.goto('${rawParams.url}');`; case ReloadPageTool_1.ReloadPageTool.NAME: return `${rationale}await page.reload();`; case ScrollPageTool_1.ScrollPageTool.NAME: return `${rationale}await page.scroll(${serializedParams});`; // The following tools can be mapped directly. case AnalyzePageTextTool_1.AnalyzePageTextTool.NAME: case AssertPageTextTool_1.AssertPageTextTool.NAME: case CreateBrowserCookieReportTool_1.CreateBrowserCookieReportTool.NAME: case HoverOverElementTool_1.HoverOverElementTool.NAME: case InputRandomizedEmailAddressTool_1.InputRandomizedEmailAddressTool.NAME: case InputTextTool_1.InputTextTool.NAME: case PressKeyTool_1.PressKeyTool.NAME: case RunAccessibilityTestTool_1.RunAccessibilityTestTool.NAME: { return `${rationale}await page.${proposedToolCall.name}(${hasNonEmptyParameters ? serializedParams : ''});`; } default: const toolName = proposedToolCall.name; const toolCallScript = hasNonEmptyParameters ? `${rationale}await page.run('${toolName}', ${serializedParams});` : `${rationale}await page.run('${toolName}');`; return toolCallScript; } } async prettifyCode(code) { // Shenanigans way of importing prettier due to issues with Jest. // See https://github.com/jestjs/jest/issues/14305 const prettier = require('prettier'); const formattedCode = prettier.format(code, { parser: 'typescript', semi: true, singleQuote: true, }); return formattedCode; } /** * Sanitizes a JSON string to be safely used within a template literal (backtick string) * Prevents both backtick termination and string interpolation from triggering * * @param jsonString - The JSON string to sanitize * @returns The sanitized string that can be safely used within backticks */ sanitizeForTemplateLiteral(jsonString) { return (jsonString // Escape backticks to prevent template literal termination .replace(/`/g, '\\`') // Escape ${...} patterns to prevent string interpolation .replace(/\${/g, '\\${')); } } exports.CodeGenerator = CodeGenerator; //# sourceMappingURL=CodeGenerator.js.map