donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
228 lines • 10.3 kB
JavaScript
;
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