mirror of
https://github.com/Nikhil-Doye/workflow-builder.git
synced 2026-07-22 02:01:56 +02:00
Add cron-parser integration and enhance workflow scheduling validation
- Introduced cron-parser for improved cron expression handling in the WorkflowScheduler. - Added validation for cron expressions during schedule creation and updates, ensuring correct format and providing user feedback on errors. - Implemented utility methods for generating supported cron examples and validating cron expressions, enhancing usability and clarity in scheduling workflows. - Updated llmProcessor to support variable substitution in prompts, improving flexibility in input handling.
This commit is contained in:
Generated
+22
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -29,7 +29,6 @@ export const ExecutionPanel: React.FC = () => {
|
||||
executeWorkflow,
|
||||
clearExecutionResults,
|
||||
executionMode,
|
||||
currentExecution,
|
||||
getExecutionStats,
|
||||
panelStates,
|
||||
togglePanel,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string, NodeOutput>();
|
||||
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,
|
||||
|
||||
@@ -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<string> {
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user