UNPKG

donobu

Version:

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

228 lines 10.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FlowsPersistenceSqlite = void 0; const FlowNotFoundException_1 = require("../exceptions/FlowNotFoundException"); const JsonUtils_1 = require("../utils/JsonUtils"); /** * A persistence implementation that uses SQLite for all data storage, including binary files. */ class FlowsPersistenceSqlite { constructor(db) { this.db = db; } static async create(db) { return new FlowsPersistenceSqlite(db); } async saveMetadata(flowMetadata) { // Check if the flow already exists. const existingFlowStmt = this.db.prepare('SELECT id FROM flow_metadata WHERE id = ?'); const existingFlow = existingFlowStmt.get(flowMetadata.id); // Start a transaction for data consistency. const transaction = this.db.transaction(() => { if (existingFlow) { // If flow exists, use UPDATE instead of REPLACE to avoid triggering ON DELETE CASCADE. const updateStmt = this.db.prepare('UPDATE flow_metadata SET name = ?, metadata = ?, created_at = ?, run_mode = ?, state = ? WHERE id = ?'); updateStmt.run(flowMetadata.name, JSON.stringify(flowMetadata), flowMetadata.startedAt || new Date().getTime(), flowMetadata.runMode, flowMetadata.state, flowMetadata.id); } else { // If it is a new flow, use INSERT. const insertStmt = this.db.prepare('INSERT INTO flow_metadata (id, name, metadata, created_at, run_mode, state) VALUES (?, ?, ?, ?, ?, ?)'); insertStmt.run(flowMetadata.id, flowMetadata.name, JSON.stringify(flowMetadata), flowMetadata.startedAt || new Date().getTime(), flowMetadata.runMode, flowMetadata.state); } }); transaction(); } async getMetadataByFlowId(flowId) { const stmt = this.db.prepare('SELECT metadata FROM flow_metadata WHERE id = ?'); const row = stmt.get(flowId); if (!row) { throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId); } return JSON.parse(row.metadata); } async getMetadataByFlowName(flowName) { const stmt = this.db.prepare('SELECT metadata FROM flow_metadata WHERE name = ? LIMIT 1'); const row = stmt.get(flowName); if (!row) { throw FlowNotFoundException_1.FlowNotFoundException.forName(flowName); } return JSON.parse(row.metadata); } async savePngScreenShot(flowId, bytes) { // Ensure flow exists. await this.getMetadataByFlowId(flowId); // Generate a unique filename. const fileId = `${new Date().toISOString()}.screenshot.png`; const binaryId = `${flowId}-${fileId}`; const stmt = this.db.prepare('INSERT INTO binary_files (id, flow_id, file_id, mime_type, content) VALUES (?, ?, ?, ?, ?)'); stmt.run(binaryId, flowId, fileId, FlowsPersistenceSqlite.SCREENSHOT_MIME_TYPE, bytes); return fileId; } async getPngScreenShot(flowId, screenShotId) { const stmt = this.db.prepare('SELECT mime_type, content FROM binary_files WHERE flow_id = ? AND file_id = ?'); const row = stmt.get(flowId, screenShotId); if (row && row.mime_type === FlowsPersistenceSqlite.SCREENSHOT_MIME_TYPE) { return row.content; } else { return null; } } async saveToolCall(flowId, toolCall) { const stmt = this.db.prepare('INSERT OR REPLACE INTO tool_calls (id, flow_id, created_at, tool_call) VALUES (?, ?, ?, ?)'); stmt.run(toolCall.id, flowId, toolCall.startedAt, JSON.stringify(toolCall)); } async getToolCalls(flowId) { // Check if flow exists await this.getMetadataByFlowId(flowId); const stmt = this.db.prepare('SELECT tool_call FROM tool_calls WHERE flow_id = ? ORDER BY created_at'); const rows = stmt.all(flowId); return rows.map((row) => JSON.parse(row.tool_call)); } async getFlows(query) { // Sanitize inputs const validLimit = Math.max(1, Math.min(100, query.limit || 100)); const offset = query.pageToken ? parseInt(query.pageToken, 10) || 0 : 0; // Build the WHERE clause dynamically based on query parameters let whereConditions = []; let params = []; if (query.name) { whereConditions.push('name = ?'); params.push(query.name); } if (query.runMode) { whereConditions.push('run_mode = ?'); params.push(query.runMode); } if (query.state) { whereConditions.push('state = ?'); params.push(query.state); } // Add startedAfter condition if provided if (query.startedAfter !== undefined) { whereConditions.push('created_at >= ?'); params.push(query.startedAfter); } // Add startedBefore condition if provided if (query.startedBefore !== undefined) { whereConditions.push('created_at <= ?'); params.push(query.startedBefore); } // Build the full SQL query let sql = 'SELECT metadata FROM flow_metadata'; if (whereConditions.length > 0) { sql += ' WHERE ' + whereConditions.join(' AND '); } // Always order by created_at descending sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; // Add limit and offset to params params.push(validLimit + 1); params.push(offset); // Execute the query with parameters const stmt = this.db.prepare(sql); const rows = stmt.all(...params); // Check if there are more results by fetching one extra const hasMore = rows.length > validLimit; const results = hasMore ? rows.slice(0, validLimit) : rows; // Generate next token if needed const nextPageToken = hasMore ? `${offset + validLimit}` : undefined; return { items: results.map((row) => JSON.parse(row.metadata)), nextPageToken, }; } async setVideo(flowId, bytes) { // Ensure flow exists. await this.getMetadataByFlowId(flowId); const binaryId = `${flowId}-${FlowsPersistenceSqlite.VIDEO_FILE_ID}`; const stmt = this.db.prepare('INSERT OR REPLACE INTO binary_files (id, flow_id, file_id, mime_type, content) VALUES (?, ?, ?, ?, ?)'); stmt.run(binaryId, flowId, FlowsPersistenceSqlite.VIDEO_FILE_ID, FlowsPersistenceSqlite.VIDEO_MIME_TYPE, bytes); } async getVideoSegment(flowId, startOffset, length) { const stmt = this.db.prepare('SELECT content FROM binary_files WHERE flow_id = ? AND file_id = ?'); const row = stmt.get(flowId, FlowsPersistenceSqlite.VIDEO_FILE_ID); if (!row) { return null; } const videoBuffer = row.content; const totalLength = videoBuffer.length; if (startOffset >= totalLength) { return null; } const adjustedLength = Math.min(length, totalLength - startOffset); const segmentBuffer = Buffer.alloc(adjustedLength); // Copy the segment from the full video buffer. videoBuffer.copy(segmentBuffer, 0, startOffset, startOffset + adjustedLength); return { bytes: segmentBuffer, totalLength: totalLength, startOffset: startOffset, }; } async getFlowFile(flowId, fileId) { const stmt = this.db.prepare('SELECT content FROM binary_files WHERE flow_id = ? AND file_id = ?'); const row = stmt.get(flowId, fileId); return row ? row.content : null; } async setFlowFile(flowId, fileId, fileBytes) { // Ensure flow exists await this.getMetadataByFlowId(flowId); const binaryId = `${flowId}-${fileId}`; const mimeType = this.getMimeTypeFromFileId(fileId); const stmt = this.db.prepare('INSERT OR REPLACE INTO binary_files (id, flow_id, file_id, mime_type, content) VALUES (?, ?, ?, ?, ?)'); stmt.run(binaryId, flowId, fileId, mimeType, fileBytes); } async setBrowserState(flowId, browserState) { const serializedBrowserState = Buffer.from(JSON.stringify(browserState), 'utf-8'); await this.setFlowFile(flowId, FlowsPersistenceSqlite.BROWSER_STATE_FILE_ID, serializedBrowserState); } async getBrowserState(flowId) { const browserStateRaw = await this.getFlowFile(flowId, FlowsPersistenceSqlite.BROWSER_STATE_FILE_ID); if (browserStateRaw) { const browserState = JsonUtils_1.JsonUtils.jsonStringToJsonObject(browserStateRaw.toString('utf-8')); if (browserState) { return browserState; } else { throw new Error(`Cannot load malformed browser state from flow ${flowId}`); } } else { return null; } } async deleteFlow(flowId) { // Delete from database with cascading delete to related tables const stmt = this.db.prepare('DELETE FROM flow_metadata WHERE id = ?'); stmt.run(flowId); } // Database management close() { this.db.close(); } // Utility functions getMimeTypeFromFileId(fileId) { if (fileId.endsWith('.png')) return 'image/png'; if (fileId.endsWith('.jpg') || fileId.endsWith('.jpeg')) return 'image/jpeg'; if (fileId.endsWith('.webm')) return 'video/webm'; if (fileId.endsWith('.mp4')) return 'video/mp4'; if (fileId.endsWith('.json')) return 'application/json'; return 'application/octet-stream'; } // Adds possibility to vacuum/optimize the database async optimize() { this.db.pragma('vacuum'); this.db.pragma('optimize'); } } exports.FlowsPersistenceSqlite = FlowsPersistenceSqlite; FlowsPersistenceSqlite.SCREENSHOT_MIME_TYPE = 'image/png'; FlowsPersistenceSqlite.VIDEO_MIME_TYPE = 'video/webm'; FlowsPersistenceSqlite.BROWSER_STATE_FILE_ID = 'browserstate.json'; FlowsPersistenceSqlite.VIDEO_FILE_ID = 'video.webm'; //# sourceMappingURL=FlowsPersistenceSqlite.js.map