diff --git a/package-lock.json b/package-lock.json index 9b5ec3d..8ea9539 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "autoprefixer": "^10.4.14", "class-variance-authority": "^0.7.1", "clsx": "^1.2.1", + "cron-parser": "^5.4.0", "lucide-react": "^0.263.1", "openai": "^6.3.0", "postcss": "^8.4.24", @@ -6962,6 +6963,18 @@ "node": ">=10" } }, + "node_modules/cron-parser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.4.0.tgz", + "integrity": "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==", + "license": "MIT", + "dependencies": { + "luxon": "^3.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -12358,6 +12371,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", diff --git a/package.json b/package.json index 113ddf7..e259c17 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "autoprefixer": "^10.4.14", "class-variance-authority": "^0.7.1", "clsx": "^1.2.1", + "cron-parser": "^5.4.0", "lucide-react": "^0.263.1", "openai": "^6.3.0", "postcss": "^8.4.24", diff --git a/src/components/ExecutionPanel.tsx b/src/components/ExecutionPanel.tsx index 6edc5d1..61a6da1 100644 --- a/src/components/ExecutionPanel.tsx +++ b/src/components/ExecutionPanel.tsx @@ -29,7 +29,6 @@ export const ExecutionPanel: React.FC = () => { executeWorkflow, clearExecutionResults, executionMode, - currentExecution, getExecutionStats, panelStates, togglePanel, diff --git a/src/services/executionEngine.ts b/src/services/executionEngine.ts index c172bdd..a9d8d8c 100644 --- a/src/services/executionEngine.ts +++ b/src/services/executionEngine.ts @@ -118,7 +118,11 @@ export class ExecutionEngine { onNodeUpdate("validation", "error", null, errorMessage); } - throw new Error(`Workflow validation failed: ${errorMessage}`); + // Store the failed execution plan + this.activeExecutions.set(executionId, failedPlan); + this.executionHistory.push(failedPlan); + + return failedPlan; } const plan = this.createExecutionPlan(workflowId, nodes, edges, options); @@ -626,6 +630,7 @@ export class ExecutionEngine { // Evaluate the condition using Function constructor (safer than eval) // This creates a function in a controlled scope + // eslint-disable-next-line no-new-func const evaluateExpression = new Function("return " + evaluatedCondition); return evaluateExpression(); } catch (error) { diff --git a/src/services/processors/llmProcessor.ts b/src/services/processors/llmProcessor.ts index dabf77b..12d2168 100644 --- a/src/services/processors/llmProcessor.ts +++ b/src/services/processors/llmProcessor.ts @@ -1,5 +1,9 @@ import { callOpenAI } from "../openaiService"; import { ExecutionContext, ExecutionPlan } from "../executionEngine"; +import { + substituteVariables, + NodeOutput, +} from "../../utils/variableSubstitution"; export default async function llmProcessor( context: ExecutionContext, @@ -8,17 +12,49 @@ export default async function llmProcessor( const { config, inputs } = context; try { - // Get input data from previous nodes - const inputData = Array.from(inputs.values()).pop() || ""; + // Get the prompt from config (may contain variable placeholders) + let prompt = config.prompt || ""; - const result = await callOpenAI(inputData, { + // If no prompt in config, fall back to input data from previous nodes + if (!prompt) { + prompt = Array.from(inputs.values()).pop() || ""; + } + + // Apply variable substitution to the prompt if it contains placeholders + if (prompt.includes("{{")) { + const nodeLabelToId = (plan as any).nodeLabelToId || new Map(); + + // Create node outputs map for substitution + const nodeOutputs = new Map(); + plan.nodes.forEach((node) => { + if (node.outputs.has("output")) { + nodeOutputs.set(node.nodeId, { + nodeId: node.nodeId, + output: node.outputs.get("output"), + data: node.outputs.get("output"), + status: node.status, + }); + } + }); + + // Apply variable substitution to the prompt + prompt = substituteVariables(prompt, nodeOutputs, nodeLabelToId); + + console.log(`LLM prompt after variable substitution:`, { + originalPrompt: config.prompt, + substitutedPrompt: prompt, + nodeLabelToId: Array.from(nodeLabelToId.entries()), + }); + } + + const result = await callOpenAI(prompt, { model: config.model || "gpt-3.5-turbo", temperature: config.temperature || 0.7, maxTokens: config.maxTokens || 1000, }); return { - input: inputData, + input: prompt, output: result.content, model: config.model || "gpt-3.5-turbo", usage: result.usage, diff --git a/src/services/workflowScheduler.ts b/src/services/workflowScheduler.ts index 632ee8d..2298b0d 100644 --- a/src/services/workflowScheduler.ts +++ b/src/services/workflowScheduler.ts @@ -1,3 +1,5 @@ +import { CronExpressionParser, CronExpressionOptions } from "cron-parser"; + export interface ScheduleConfig { id: string; workflowId: string; @@ -85,6 +87,16 @@ class WorkflowScheduler { "id" | "createdAt" | "updatedAt" | "runCount" | "status" > ): Promise { + // Validate cron expression if it's a cron trigger + if (schedule.trigger.type === "cron") { + const cronConfig = schedule.trigger.config as CronConfig; + if (!this.isValidCronExpression(cronConfig.expression)) { + throw new Error( + `Invalid cron expression: "${cronConfig.expression}". Please use standard cron format (minute hour day month weekday).` + ); + } + } + const id = `schedule_${Date.now()}_${Math.random() .toString(36) .substr(2, 9)}`; @@ -110,6 +122,16 @@ class WorkflowScheduler { const schedule = this.schedules.get(id); if (!schedule) return false; + // Validate cron expression if trigger is being updated + if (updates.trigger && updates.trigger.type === "cron") { + const cronConfig = updates.trigger.config as CronConfig; + if (!this.isValidCronExpression(cronConfig.expression)) { + throw new Error( + `Invalid cron expression: "${cronConfig.expression}". Please use standard cron format (minute hour day month weekday).` + ); + } + } + const updatedSchedule = { ...schedule, ...updates, @@ -316,24 +338,103 @@ class WorkflowScheduler { } } + // Utility methods + getSupportedCronExamples(): Array<{ + expression: string; + description: string; + }> { + return [ + { expression: "0 9 * * 1-5", description: "Every weekday at 9:00 AM" }, + { expression: "30 14 * * *", description: "Every day at 2:30 PM" }, + { + expression: "0 0 1 * *", + description: "First day of every month at midnight", + }, + { expression: "0 12 * * 0", description: "Every Sunday at noon" }, + { expression: "*/15 * * * *", description: "Every 15 minutes" }, + { expression: "0 */2 * * *", description: "Every 2 hours" }, + { expression: "0 0 * * 1", description: "Every Monday at midnight" }, + ]; + } + + validateCronExpression(expression: string): { + isValid: boolean; + error?: string; + nextRun?: Date; + } { + try { + if (!this.isValidCronExpression(expression)) { + return { + isValid: false, + error: + "Invalid cron expression format. Use: minute hour day month weekday", + }; + } + + const interval = CronExpressionParser.parse(expression); + const nextRun = interval.next().toDate(); + + return { + isValid: true, + nextRun, + }; + } catch (error) { + return { + isValid: false, + error: + error instanceof Error ? error.message : "Unknown cron parsing error", + }; + } + } + private calculateNextCronRun(config: CronConfig): Date | null { - // Simple cron parser - in production, use a proper cron library - const now = new Date(); - const [minute, hour] = config.expression.split(" "); + try { + // Validate cron expression format + if (!this.isValidCronExpression(config.expression)) { + console.error(`Invalid cron expression: ${config.expression}`); + return null; + } - // This is a simplified implementation - // In production, use a library like 'node-cron' or 'cron-parser' - const nextRun = new Date(now); - nextRun.setMinutes(parseInt(minute) || 0); - nextRun.setHours(parseInt(hour) || 0); - nextRun.setSeconds(0); - nextRun.setMilliseconds(0); + // Parse the cron expression with timezone support + const options: CronExpressionOptions = { + currentDate: new Date(), + tz: config.timezone || "UTC", + }; - if (nextRun <= now) { - nextRun.setDate(nextRun.getDate() + 1); + const interval = CronExpressionParser.parse(config.expression, options); + const nextRun = interval.next().toDate(); + + console.log(`Next cron run for "${config.expression}":`, { + nextRun: nextRun.toISOString(), + timezone: config.timezone || "UTC", + }); + + return nextRun; + } catch (error) { + console.error( + `Cron parsing error for expression "${config.expression}":`, + error + ); + return null; + } + } + + private isValidCronExpression(expression: string): boolean { + // Basic validation for cron expression format + const cronPattern = + /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([012]?\d|3[01])) (\*|([0]?\d|1[0-2])) (\*|([0-6]))$/; + + if (!cronPattern.test(expression)) { + return false; } - return nextRun; + // Additional validation using cron-parser + try { + CronExpressionParser.parse(expression); + return true; + } catch { + return false; + } } private calculateNextIntervalRun(config: IntervalConfig): Date {