205 lines
5.7 KiB
TypeScript
205 lines
5.7 KiB
TypeScript
import { WorkflowState } from './types';
|
|
|
|
/**
|
|
* Replace variable references like {{state.variables.node_1.price}} with actual values
|
|
*/
|
|
export function substituteVariables(text: string, state: WorkflowState): string {
|
|
if (!text) return text;
|
|
|
|
// Find all {{variable}} patterns
|
|
const pattern = /\{\{([^}]+)\}\}/g;
|
|
|
|
return text.replace(pattern, (match, expression) => {
|
|
try {
|
|
// Clean up the expression
|
|
const cleanExpr = expression.trim();
|
|
|
|
// Evaluate the expression against state
|
|
const value = evaluateExpression(cleanExpr, state);
|
|
|
|
// Convert value to string
|
|
if (value === null || value === undefined) {
|
|
return match; // Keep original if not found
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
return String(value);
|
|
} catch (e) {
|
|
console.warn(`Failed to substitute variable: ${expression}`, e);
|
|
return match; // Keep original on error
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Safely evaluate expression like "state.variables.node_1.price" or simpler "node_1.price"
|
|
*/
|
|
function evaluateExpression(expression: string, state: WorkflowState): any {
|
|
// Support both patterns:
|
|
// 1. Full: "state.variables.node_1.price"
|
|
// 2. Simple: "node_1.price" (auto-adds state.variables prefix)
|
|
|
|
let normalizedExpr = expression;
|
|
|
|
// If expression doesn't start with "state", assume it's a simple node reference
|
|
if (!expression.startsWith('state.')) {
|
|
// Check if it's a known shorthand
|
|
if (expression === 'input' || expression.startsWith('input.')) {
|
|
// Support both input.query (maps to state.variables.input.input.query) and input directly
|
|
// Try nested first (for JSON inputs), then direct
|
|
normalizedExpr = `state.variables.${expression}`;
|
|
} else if (expression === 'lastOutput' || expression.startsWith('lastOutput.')) {
|
|
normalizedExpr = `state.variables.${expression}`;
|
|
} else {
|
|
// Assume it's a node reference like "scrape_website.markdown"
|
|
normalizedExpr = `state.variables.${expression}`;
|
|
}
|
|
}
|
|
|
|
// Parse dot notation path
|
|
const parts = normalizedExpr.split('.');
|
|
|
|
let current: any = { state };
|
|
|
|
for (const part of parts) {
|
|
if (current === null || current === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
// Handle array indexing like items[0]
|
|
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
if (arrayMatch) {
|
|
const [, arrayName, index] = arrayMatch;
|
|
current = current[arrayName]?.[parseInt(index)];
|
|
} else {
|
|
current = current[part];
|
|
}
|
|
}
|
|
|
|
// Special handling for input.X pattern - try multiple resolution strategies
|
|
if (current === undefined && normalizedExpr.startsWith('state.variables.input.')) {
|
|
const inputPath = normalizedExpr.replace('state.variables.input.', '');
|
|
|
|
// Strategy 1: Try nested .input.input.X (for JSON inputs)
|
|
const nestedValue = state.variables?.input?.input?.[inputPath];
|
|
if (nestedValue !== undefined) {
|
|
return nestedValue;
|
|
}
|
|
|
|
// Strategy 2: Try direct variable access (e.g., state.variables.query when input.query is requested)
|
|
if (state.variables?.[inputPath] !== undefined) {
|
|
return state.variables[inputPath];
|
|
}
|
|
|
|
// Strategy 3: Try traversing the full nested path manually
|
|
const nestedPath = `state.variables.input.input.${inputPath}`;
|
|
const nestedParts = nestedPath.split('.');
|
|
let nestedCurrent: any = { state };
|
|
for (const part of nestedParts) {
|
|
if (nestedCurrent === null || nestedCurrent === undefined) break;
|
|
nestedCurrent = nestedCurrent[part];
|
|
}
|
|
if (nestedCurrent !== undefined) {
|
|
return nestedCurrent;
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Extract all variable references from text
|
|
*/
|
|
export function extractVariableReferences(text: string): string[] {
|
|
if (!text) return [];
|
|
|
|
const pattern = /\{\{([^}]+)\}\}/g;
|
|
const matches: string[] = [];
|
|
let match;
|
|
|
|
while ((match = pattern.exec(text)) !== null) {
|
|
matches.push(match[1].trim());
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
/**
|
|
* Validate that all variable references exist in state
|
|
*/
|
|
export function validateVariableReferences(
|
|
text: string,
|
|
state: WorkflowState
|
|
): { valid: boolean; missing: string[] } {
|
|
const references = extractVariableReferences(text);
|
|
const missing: string[] = [];
|
|
|
|
for (const ref of references) {
|
|
try {
|
|
const value = evaluateExpression(ref, state);
|
|
if (value === undefined) {
|
|
missing.push(ref);
|
|
}
|
|
} catch (e) {
|
|
missing.push(ref);
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: missing.length === 0,
|
|
missing,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get available variables from state for autocomplete/picker
|
|
*/
|
|
export function getAvailableVariables(state: WorkflowState): Array<{
|
|
path: string;
|
|
value: any;
|
|
type: string;
|
|
}> {
|
|
const variables: Array<{ path: string; value: any; type: string }> = [];
|
|
|
|
// Add top-level variables
|
|
variables.push({
|
|
path: 'state.variables.input',
|
|
value: state.variables.input,
|
|
type: typeof state.variables.input,
|
|
});
|
|
|
|
variables.push({
|
|
path: 'state.variables.lastOutput',
|
|
value: state.variables.lastOutput,
|
|
type: typeof state.variables.lastOutput,
|
|
});
|
|
|
|
// Add all custom variables
|
|
Object.keys(state.variables).forEach(key => {
|
|
if (key !== 'input' && key !== 'lastOutput') {
|
|
const value = state.variables[key];
|
|
variables.push({
|
|
path: `state.variables.${key}`,
|
|
value,
|
|
type: typeof value,
|
|
});
|
|
|
|
// If it's an object, add nested properties
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
Object.keys(value).forEach(nestedKey => {
|
|
variables.push({
|
|
path: `state.variables.${key}.${nestedKey}`,
|
|
value: value[nestedKey],
|
|
type: typeof value[nestedKey],
|
|
});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return variables;
|
|
}
|