@plust/datasleuth
Version:
Build LLM-powered research pipelines and output structured data.
355 lines • 17.1 kB
JavaScript
/**
* Flow control utilities for the research pipeline
* Implements conditional iteration and evaluation steps
*/
import { createStep } from '../utils/steps.js';
import { ValidationError, ConfigurationError, ProcessingError, MaxIterationsError, } from '../types/errors.js';
import { createStepLogger } from '../utils/logging.js';
/**
* Evaluates the current state against specified criteria
*/
async function executeEvaluateStep(state, options) {
const stepLogger = createStepLogger('Evaluate');
const { criteriaFn, criteriaName = 'CustomEvaluation', confidenceThreshold = 0.7, storeResult = true, retry = { maxRetries: 2, baseDelay: 1000 }, } = options;
try {
// Validate inputs
if (!criteriaFn || typeof criteriaFn !== 'function') {
throw new ValidationError({
message: 'No criteria function provided for evaluation',
step: 'Evaluate',
details: { options },
suggestions: [
'Provide a function that returns a boolean or Promise<boolean>',
'Example: evaluate({ criteriaFn: (state) => state.data.searchResults.length > 5 })',
],
});
}
if (confidenceThreshold < 0 || confidenceThreshold > 1) {
throw new ValidationError({
message: `Invalid confidence threshold: ${confidenceThreshold}. Must be between 0 and 1.`,
step: 'Evaluate',
details: { confidenceThreshold },
suggestions: [
'Confidence threshold must be between 0.0 and 1.0',
'Recommended values are between 0.5 and 0.9',
],
});
}
stepLogger.info(`Evaluating criteria: ${criteriaName}`);
// Execute the criteria function
let result;
try {
result = await criteriaFn(state);
if (typeof result !== 'boolean') {
throw new ValidationError({
message: `Criteria function must return a boolean value, got ${typeof result}`,
step: 'Evaluate',
details: {
returnValue: result,
returnType: typeof result,
},
suggestions: [
'Ensure your criteriaFn returns a boolean (true/false) value',
'Convert non-boolean results to boolean using !!value',
],
});
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
stepLogger.error(`Error executing criteria function: ${errorMessage}`);
throw new ProcessingError({
message: `Failed to execute evaluation criteria "${criteriaName}": ${errorMessage}`,
step: 'Evaluate',
details: {
criteriaName,
error,
},
retry: true,
suggestions: [
'Check your criteriaFn implementation for errors',
'Ensure criteriaFn properly handles the state structure',
'Add error handling inside your criteriaFn',
],
});
}
// Calculate confidence score based on threshold and result
// This produces higher confidence when criteria passes, scaled by threshold
const confidenceScore = result
? 0.5 + confidenceThreshold * 0.5
: 0.5 - confidenceThreshold * 0.5;
stepLogger.info(`Evaluation "${criteriaName}" ${result ? 'passed' : 'failed'} with confidence ${confidenceScore.toFixed(2)}`);
// Store results in state if requested
if (storeResult) {
const evaluationResult = {
passed: result,
confidenceScore,
timestamp: new Date().toISOString(),
criteria: criteriaName,
};
return {
...state,
data: {
...state.data,
evaluations: {
...(state.data.evaluations || {}),
[criteriaName]: evaluationResult, // Store only the properly typed object
},
},
metadata: {
...state.metadata,
lastEvaluation: criteriaName,
lastEvaluationResult: result,
confidenceScore: Math.max(state.metadata.confidenceScore || 0, confidenceScore),
},
};
}
return state;
}
catch (error) {
// If it's already one of our error types, just rethrow
if (error instanceof ValidationError ||
error instanceof ConfigurationError ||
error instanceof ProcessingError) {
throw error;
}
// Otherwise wrap in a ProcessingError
const errorMessage = error instanceof Error ? error.message : String(error);
stepLogger.error(`Evaluation error: ${errorMessage}`);
throw new ProcessingError({
message: `Evaluation "${criteriaName}" failed: ${errorMessage}`,
step: 'Evaluate',
details: { error, criteriaName },
retry: true,
suggestions: [
'Check the implementation of your criteria function',
'Verify that the state contains the expected data structure',
'Add defensive checks in your criteria function to handle missing data',
],
});
}
}
/**
* Creates an evaluation step for the research pipeline
*
* @param options Configuration options for evaluation
* @returns An evaluation step for the research pipeline
*/
export function evaluate(options) {
return createStep('Evaluate',
// Wrapper function that matches the expected signature
async (state, opts) => {
return executeEvaluateStep(state, options);
}, options);
}
/**
* Creates a composite step that repeats the given steps until a condition is met
*
* @param conditionStep Step that evaluates whether to continue repeating
* @param stepsToRepeat Array of steps to repeat until condition is met
* @param options Configuration options
* @returns A composite step that handles the iteration
*/
export function repeatUntil(conditionStep, stepsToRepeat, options = {}) {
const { maxIterations = 5, throwOnMaxIterations = false, continueOnError = false, retry = { maxRetries: 1, baseDelay: 1000 }, } = options;
// Validate inputs before returning step
if (!conditionStep || typeof conditionStep.execute !== 'function') {
throw new ValidationError({
message: 'Invalid condition step provided to repeatUntil',
step: 'RepeatUntil',
details: { conditionStep },
suggestions: [
'Condition step must be created using evaluate() or another step factory',
'Example: repeatUntil(evaluate({ criteriaFn: ... }), [step1, step2])',
],
});
}
if (!stepsToRepeat || !Array.isArray(stepsToRepeat) || stepsToRepeat.length === 0) {
throw new ValidationError({
message: 'No steps to repeat provided to repeatUntil',
step: 'RepeatUntil',
details: { stepsToRepeat },
suggestions: [
'Provide at least one step to repeat',
'Example: repeatUntil(condition, [searchWeb(), extractContent()])',
],
});
}
if (maxIterations <= 0) {
throw new ValidationError({
message: `Invalid maxIterations value: ${maxIterations}. Must be greater than 0.`,
step: 'RepeatUntil',
details: { maxIterations },
suggestions: ['Provide a positive integer for maxIterations', 'Default is 5 iterations'],
});
}
// Create the repeating step
return createStep('RepeatUntil', async (state) => {
const stepLogger = createStepLogger('RepeatUntil');
try {
let currentState = { ...state };
let iterations = 0;
let conditionMet = false;
const iterationErrors = [];
stepLogger.info(`Starting repeatUntil loop with max ${maxIterations} iterations`);
// Execute steps until condition is met or max iterations reached
while (iterations < maxIterations && !conditionMet) {
iterations += 1;
stepLogger.info(`Executing iteration ${iterations}/${maxIterations}`);
try {
// Execute the condition step
stepLogger.debug(`Evaluating condition (${conditionStep.name})`);
const conditionState = await conditionStep.execute(currentState);
// Check if condition is met by checking evaluations in state
const evaluations = conditionState.data.evaluations || {};
const evaluationKeys = Object.keys(evaluations);
// Try to find the most recent evaluation
const evaluationKey = evaluationKeys.length > 0 ? evaluationKeys[evaluationKeys.length - 1] : null;
if (evaluationKey) {
// Get the evaluation result, which should now always be an EvaluationResult object
const evaluation = evaluations[evaluationKey];
// Check the passed property to determine if condition is met
if (evaluation &&
typeof evaluation === 'object' &&
'passed' in evaluation &&
evaluation.passed) {
conditionMet = true;
currentState = conditionState;
stepLogger.info(`Condition met in iteration ${iterations}, exiting loop`);
break;
}
}
stepLogger.debug(`Condition not met, executing ${stepsToRepeat.length} steps in iteration ${iterations}`);
// Execute the steps to repeat
for (const step of stepsToRepeat) {
try {
stepLogger.debug(`Executing step ${step.name} in iteration ${iterations}`);
currentState = await step.execute(conditionState);
}
catch (stepError) {
const errorMessage = stepError instanceof Error ? stepError.message : String(stepError);
stepLogger.error(`Error in step ${step.name} during iteration ${iterations}: ${errorMessage}`);
// Add to iteration errors
iterationErrors.push(stepError instanceof Error ? stepError : new Error(errorMessage));
// If we should not continue on error, rethrow
if (!continueOnError) {
throw new ProcessingError({
message: `Step ${step.name} failed during iteration ${iterations}: ${errorMessage}`,
step: 'RepeatUntil',
details: {
iteration: iterations,
step: step.name,
originalError: stepError,
},
retry: false,
suggestions: [
'Set continueOnError=true to continue despite step failures',
'Check the specific step for configuration errors',
'Examine the original error for more details',
],
});
}
// Otherwise log and continue with next step
stepLogger.warn(`Continuing with next step after error due to continueOnError=true`);
}
}
}
catch (iterationError) {
const errorMessage = iterationError instanceof Error ? iterationError.message : String(iterationError);
stepLogger.error(`Error during iteration ${iterations}: ${errorMessage}`);
// Add to iteration errors
iterationErrors.push(iterationError instanceof Error ? iterationError : new Error(errorMessage));
// If we should not continue on error, rethrow
if (!continueOnError) {
throw iterationError;
}
// Otherwise log and continue with next iteration
stepLogger.warn(`Continuing with next iteration after error due to continueOnError=true`);
}
}
// Check if we hit max iterations without meeting condition
if (!conditionMet) {
const maxIterationsMessage = `Maximum iterations (${maxIterations}) reached without meeting condition`;
stepLogger.warn(maxIterationsMessage);
if (throwOnMaxIterations) {
throw new MaxIterationsError({
message: maxIterationsMessage,
step: 'RepeatUntil',
details: {
maxIterations,
completedIterations: iterations,
conditionStepName: conditionStep.name,
},
retry: false,
suggestions: [
'Increase maxIterations',
'Adjust your condition to be less strict',
'Set throwOnMaxIterations=false to continue without error',
],
});
}
}
// Update state with iteration information
const finalState = {
...currentState,
data: {
...currentState.data,
iterations: {
...(currentState.data.iterations || {}),
[conditionStep.name]: {
completed: iterations,
conditionMet,
maxReached: iterations >= maxIterations,
iterationErrors: iterationErrors.length > 0,
errorCount: iterationErrors.length,
},
},
},
metadata: {
...currentState.metadata,
repeatUntilComplete: true,
repeatUntilConditionMet: conditionMet,
repeatUntilIterations: iterations,
},
// Add any iteration errors to the state errors
errors: [...currentState.errors, ...iterationErrors],
};
stepLogger.info(`RepeatUntil complete after ${iterations} iterations, condition met: ${conditionMet}`);
return finalState;
}
catch (error) {
// Handle already typed errors
if (error instanceof ValidationError ||
error instanceof ConfigurationError ||
error instanceof ProcessingError ||
error instanceof MaxIterationsError) {
throw error;
}
// Otherwise wrap in ProcessingError
const errorMessage = error instanceof Error ? error.message : String(error);
stepLogger.error(`RepeatUntil execution failed: ${errorMessage}`);
throw new ProcessingError({
message: `RepeatUntil execution failed: ${errorMessage}`,
step: 'RepeatUntil',
details: {
error,
conditionStepName: conditionStep.name,
repeatingSteps: stepsToRepeat.map((s) => s.name),
},
retry: false,
suggestions: [
'Check the condition step implementation',
'Verify the steps to repeat are properly configured',
'Consider setting continueOnError=true to handle step failures',
],
});
}
}, options, {
// Add retry configuration
retryable: true,
maxRetries: retry.maxRetries || 1,
retryDelay: retry.baseDelay || 1000,
backoffFactor: 2,
});
}
//# sourceMappingURL=flowControl.js.map