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:
Nikhil-Doye
2025-10-20 14:43:23 -04:00
parent fe93de79ae
commit 53fbbe22c7
6 changed files with 183 additions and 19 deletions
+22
View File
@@ -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",
+1
View File
@@ -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",
-1
View File
@@ -29,7 +29,6 @@ export const ExecutionPanel: React.FC = () => {
executeWorkflow,
clearExecutionResults,
executionMode,
currentExecution,
getExecutionStats,
panelStates,
togglePanel,
+6 -1
View File
@@ -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) {
+40 -4
View File
@@ -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,
+114 -13
View File
@@ -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 {