UNPKG

donobu

Version:

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

295 lines 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PageAi = exports.PageAiRunner = void 0; const v4_1 = require("zod/v4"); const GptClient_1 = require("../../clients/GptClient"); const PageAiException_1 = require("../../exceptions/PageAiException"); const DonobuFlow_1 = require("../../managers/DonobuFlow"); const DonobuFlowsManager_1 = require("../../managers/DonobuFlowsManager"); const ToolManager_1 = require("../../managers/ToolManager"); const WebTargetInspector_1 = require("../../managers/WebTargetInspector"); const ControlPanel_1 = require("../../models/ControlPanel"); const Logger_1 = require("../../utils/Logger"); const originalGotoRegistry_1 = require("../page/originalGotoRegistry"); const cache_1 = require("./cache/cache"); const cacheEntryBuilder_1 = require("./cache/cacheEntryBuilder"); const cacheLocator_1 = require("./cache/cacheLocator"); /** * Prepares and executes a Donobu autonomous flow. * * Responsibilities: * - Gather environment data based on allowed env vars. * - Seed `DonobuFlow` with deterministic metadata (run mode, allowed tools, etc.). * - Run the flow, updating persisted metadata regardless of outcome. * * The runner does **not** perform cache lookups or updates. That is deferred to * the higher-level {@link PageAi} facade so that the runner can stay focused on * “how do we execute a flow safely?” rather than “should we execute a flow at * all?”. */ class PageAiRunner { constructor(donobu, gptClient) { this.donobu = donobu; this.gptClient = gptClient; } /** * Executes a flow using the provided configuration. * * @param config Runtime information prepared by the higher-level facade. * @returns Parsed result (if a schema was provided) along with the executed flow. * @throws PageAiException when the underlying flow fails. */ async run(config) { const envData = await this.donobu.envDataManager.getByNames(config.envVarNames); if (config.envVals) { for (const [k, v] of Object.entries(config.envVals)) { if (v === undefined) { delete envData[k]; } else { envData[k] = v; } } } const originalFlowMetadata = { ...config.page._dnb.donobuFlowMetadata, }; const augmentedMetadata = { ...originalFlowMetadata, envVars: config.envVarNames, runMode: config.runMode, overallObjective: config.instruction, resultJsonSchema: config.jsonSchema, result: null, maxToolCalls: config.maxToolCalls, state: 'UNSTARTED', allowedTools: config.allowedTools, }; const toolManager = await this.buildToolManager(config.allowedTools); const persistence = await this.donobu.flowsPersistenceRegistry.get(); const pageWithOriginalGoto = Object.create(config.page); pageWithOriginalGoto.goto = (0, originalGotoRegistry_1.getOriginalGoto)(config.page); const donobuFlow = new DonobuFlow_1.DonobuFlow(this.donobu.flowsManager, envData, persistence, this.gptClient, toolManager, config.page._dnb.interactionVisualizer, [], [], [], new WebTargetInspector_1.WebTargetInspector({ type: 'web', current: pageWithOriginalGoto }, config.page.context(), config.page._dnb.interactionVisualizer), augmentedMetadata, config.page._dnb.controlPanelFactory ? await config.page._dnb.controlPanelFactory(augmentedMetadata.id) : new ControlPanel_1.NoOpControlPanel()); const flowContext = { flowId: augmentedMetadata.id }; const rawResult = await Logger_1.loggingContext.run(flowContext, async () => { Logger_1.loggingContext.enterWith(flowContext); return donobuFlow.run(); }); const updatedMetadata = { ...originalFlowMetadata, inputTokensUsed: donobuFlow.metadata.inputTokensUsed, completionTokensUsed: donobuFlow.metadata.completionTokensUsed, }; try { await persistence.setFlowMetadata(updatedMetadata); } catch (error) { Logger_1.appLogger.warn(`Failed to update metadata for flow: ${updatedMetadata.id}`, error); } if (donobuFlow.metadata.state !== 'SUCCESS') { throw new PageAiException_1.PageAiException(config.instruction, donobuFlow.metadata); } const parsedResult = config.schema ? (0, GptClient_1.parseOrLogAndThrow)(rawResult, config.schema) : undefined; return { donobuFlow, parsedResult: parsedResult, }; } /** * Builds a `ToolManager` constrained to the tools the flow is allowed to use. * We always allow the objective bookkeeping tools even if the caller supplied * a narrower list. */ async buildToolManager(allowedToolsByName) { const registry = this.donobu.toolRegistry; if (allowedToolsByName.length === 0) { return new ToolManager_1.ToolManager(registry.defaultTools()); } const allTools = registry.allTools(); const minimalToolsByName = registry.minimalTools().map((t) => t.name); const semifinalToolNames = new Set([ ...allowedToolsByName, ...minimalToolsByName, ]); const finalTools = allTools.filter((tool) => semifinalToolNames.has(tool.name)); return new ToolManager_1.ToolManager(finalTools); } } exports.PageAiRunner = PageAiRunner; /** * High-level API used by Playwright tests. It resolves caching policy, * delegates execution to {@link PageAiRunner}, and records successful runs back * into the configured cache. */ class PageAi { /** * @param donobu Donobu stack providing flow managers and shared services. * @param gptClient GPT client used for autonomous reasoning. * @param cache Pluggable cache implementation the facade should consult. */ constructor(donobu, gptClient, cache) { this.cache = cache; this.donobu = donobu; this.runner = new PageAiRunner(donobu, gptClient); } /** * Builds a `PageAi` instance configured to use the file-backed cache stored * alongside the current test file. This mirrors the behaviour relied upon by * the Playwright fixture. */ static withFileCache(donobu, gptClient, cacheFilepath) { return new PageAi(donobu, gptClient, new cache_1.FilePageAiCache(cacheFilepath)); } static withInMemoryCache(donobu, gptClient) { return new PageAi(donobu, gptClient, new cache_1.InMemoryPageAiCache()); } async ai(page, instruction, options) { const startedAt = Date.now(); let cacheHit = false; let cacheStored = false; let thrownError = undefined; try { const descriptor = this.buildDescriptor(page, instruction, options); // Keep the per-page metadata in sync with the env vars needed for this invocation so cached // replays can resolve interpolations via runTool. page._dnb.donobuFlowMetadata.envVars = descriptor.envVarNames; const cachedEntry = descriptor.useCache ? await this.cache.get(descriptor.key) : null; cacheHit = !!cachedEntry; if (cachedEntry) { page._dnb.donobuFlowMetadata.runMode = 'DETERMINISTIC'; page._dnb.envVals = descriptor.envVals; try { await cachedEntry.run({ page }); } finally { page._dnb.envVals = undefined; } return this.synthesizeResultFromMetadata(page, instruction, descriptor, options); } else { const runResult = await this.runner.run({ page, instruction, schema: descriptor.schema, jsonSchema: descriptor.jsonSchema, allowedTools: descriptor.allowedTools, maxToolCalls: descriptor.maxToolCalls, envVarNames: descriptor.envVarNames, envVals: descriptor.envVals, runMode: 'AUTONOMOUS', gptClient: options?.gptClient, }); if (descriptor.useCache) { const preparedToolCalls = await (0, DonobuFlowsManager_1.prepareToolCallsForRerun)( // Only retain successfully run tool calls, otherwise when a cache file // with some bad calls in it runs in the future, the test will blow up // when the first bad tool call is read. runResult.donobuFlow.invokedToolCalls.filter((tc) => { return tc.outcome.isSuccessful; }), { areElementIdsVolatile: options?.volatileElementIds, disableSelectorFailover: options?.noSelectorFailover, }, this.donobu.toolRegistry); const cacheEntry = cacheEntryBuilder_1.PageAiCacheEntryBuilder.fromMetadata(descriptor.key.pageUrl, runResult.donobuFlow.metadata, preparedToolCalls); await this.cache.put(cacheEntry); cacheStored = true; } return runResult.parsedResult; } } catch (e) { thrownError = e; throw e; } finally { page._dnb.aiInvocations.push({ kind: 'act', description: instruction, startedAt, endedAt: Date.now(), cacheHit, cacheStored, passed: thrownError === undefined, error: thrownError !== undefined ? { message: thrownError?.message } : undefined, }); } } /** * Invalidates cache entries matching the provided invocation parameters. * * Returns `true` when a matching record existed and was removed. */ async invalidate(page, instruction, options) { const descriptor = this.buildDescriptor(page, instruction, options); return this.cache.delete(descriptor.key); } async synthesizeResultFromMetadata(page, instruction, descriptor, options) { if (!descriptor.schema) { return undefined; } const gptClient = options?.gptClient ?? page._dnb.gptClient; if (!gptClient) { throw new Error('Cannot synthesize structured result without a configured GPT client.'); } const toolCallHistory = await page._dnb.persistence.getToolCalls(page._dnb.donobuFlowMetadata.id); const screenshot = await page.screenshot({ type: 'jpeg' }); const structuredOutput = await (0, DonobuFlow_1.extractFromPage)(instruction, descriptor.schema, screenshot, toolCallHistory, gptClient); const parsedResult = structuredOutput.output; page._dnb.donobuFlowMetadata.resultJsonSchema = descriptor.jsonSchema; page._dnb.donobuFlowMetadata.result = JSON.parse(JSON.stringify(structuredOutput.output)); await page._dnb.persistence.setFlowMetadata(page._dnb.donobuFlowMetadata); return parsedResult; } /** * Normalises user-provided options into a single structure that can be reused * across cache operations and flow execution. */ buildDescriptor(page, instruction, options) { const useCache = options?.cache !== undefined ? options.cache : true; const schema = (options?.schema ?? undefined); const jsonSchema = schema ? v4_1.z.toJSONSchema(schema) : null; const maxToolCalls = options?.maxToolCalls ?? DonobuFlowsManager_1.DonobuFlowsManager.DEFAULT_MAX_TOOL_CALLS; const allowedTools = options?.allowedTools ?? []; const envVals = options?.envVals; const envVarNames = (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(instruction, [ ...(options?.envVars ?? []), ...Object.keys(envVals ?? {}), ]); return { key: this.buildCacheKey(page, instruction, jsonSchema, allowedTools, maxToolCalls, envVarNames), schema, jsonSchema: jsonSchema, allowedTools, maxToolCalls, envVarNames, envVals, useCache, }; } /** * Computes the cache key that uniquely identifies a `page.ai.act(...)` invocation. * Keep this logic in sync with any external cache generators (e.g. the code * generator) so that hits and invalidations behave the same everywhere. */ buildCacheKey(page, instruction, schema, allowedTools, maxToolCalls, envVars) { return { deviceType: 'web', pageUrl: (0, cacheLocator_1.extractCacheKeyHostname)(page.url()), instruction, schema, allowedTools, maxToolCalls, envVars, }; } } exports.PageAi = PageAi; //# sourceMappingURL=PageAi.js.map