UNPKG

@plust/datasleuth

Version:

Build LLM-powered research pipelines and output structured data.

316 lines 13.2 kB
/** * Research track implementation for parallel research paths * A track represents a distinct research path that can run in parallel with others */ import { createStep } from '../utils/steps.js'; import { z } from 'zod'; import { ValidationError, ConfigurationError, ProcessingError } from '../types/errors.js'; import { createStepLogger } from '../utils/logging.js'; /** * Schema for track result */ const trackResultSchema = z.object({ name: z.string(), results: z.array(z.any()), data: z.record(z.any()), // Changed from optional to required metadata: z.record(z.any()).optional(), errors: z.array(z.object({ message: z.string(), step: z.string().optional(), code: z.string().optional(), })), // Removed optional here completed: z.boolean(), }); /** * Executes a research track with the given options */ async function executeTrackStep(state, options) { const stepLogger = createStepLogger('Track'); const { name, steps, isolate = false, includeInResults = true, description, metadata = {}, continueOnError = false, retry = { maxRetries: 0, baseDelay: 1000 }, } = options; // Validate required parameters if (!name) { throw new ValidationError({ message: 'Track name is required', step: 'Track', details: { options }, suggestions: [ 'Provide a unique name for each track', 'The name is used to identify the track in results and logs', ], }); } if (!steps || !Array.isArray(steps) || steps.length === 0) { throw new ValidationError({ message: 'Track requires at least one step', step: 'Track', details: { options }, suggestions: [ 'Provide at least one step in the steps array', 'Steps should be created using factory functions like searchWeb(), analyze(), etc.', ], }); } stepLogger.info(`Starting research track: ${name}${description ? ` (${description})` : ''}`); stepLogger.debug(`Track configuration: isolate=${isolate}, continueOnError=${continueOnError}, steps=${steps.length}`); // Create a local state for this track // If isolate is true, we start with a fresh data object // Otherwise, we use the existing state's data as a starting point const trackState = { ...state, data: isolate ? {} : { ...state.data }, metadata: { ...state.metadata, currentTrack: name, trackDescription: description, ...metadata, }, results: [], errors: [], }; // Execute all steps in the track let currentState = trackState; try { for (const step of steps) { try { // Check if step is properly structured if (!step || typeof step.execute !== 'function') { throw new ConfigurationError({ message: `Invalid step in track "${name}"`, step: 'Track', details: { invalidStep: step }, suggestions: [ 'Ensure all steps are created using factory functions like searchWeb(), analyze(), etc.', 'Check for undefined or null values in the steps array', ], }); } stepLogger.debug(`Executing step "${step.name}" in track "${name}"`); // Update current step in metadata currentState = { ...currentState, metadata: { ...currentState.metadata, currentStep: step.name, }, }; // Execute the step currentState = await step.execute(currentState); stepLogger.debug(`Step "${step.name}" completed successfully in track "${name}"`); } catch (stepError) { stepLogger.error(`Error in step "${step.name}" of track "${name}": ${stepError instanceof Error ? stepError.message : String(stepError)}`); // Add error to current state const errorMessage = stepError instanceof Error ? stepError.message : String(stepError); const errorStep = currentState.metadata.currentStep || step.name || 'unknown'; currentState = { ...currentState, errors: [ ...currentState.errors, stepError instanceof Error ? stepError : new Error(errorMessage), ], }; // If continueOnError is false, throw to exit the track if (!continueOnError) { throw new ProcessingError({ message: `Track "${name}" failed at step "${errorStep}": ${errorMessage}`, step: 'Track', details: { trackName: name, failedStep: errorStep, originalError: stepError, }, retry: false, suggestions: [ 'Set continueOnError to true if you want the track to continue despite failures', 'Check the specific step configuration for issues', 'Examine the original error for more details on the failure', ], }); } // Otherwise log and continue stepLogger.warn(`Continuing track "${name}" after error in step "${errorStep}" due to continueOnError=true`); } } // Create the track result const trackResult = { name, results: currentState.results, data: currentState.data || {}, // Initialize with empty object if undefined metadata: { ...metadata, description, completedAt: new Date().toISOString(), hasErrors: currentState.errors.length > 0, errorCount: currentState.errors.length, }, errors: currentState.errors.map((err) => ({ message: err instanceof Error ? err.message : String(err), step: err instanceof Error && 'step' in err ? err.step : currentState.metadata.currentStep || 'unknown', code: err instanceof Error && 'code' in err ? err.code : 'TRACK_STEP_ERROR', })), completed: currentState.errors.length === 0, // Changed this line - If there are errors, the track is not completed }; stepLogger.info(`Track "${name}" completed${trackResult.errors.length > 0 ? ` with ${trackResult.errors.length} errors` : ' successfully'}`); // If this track should be included in results, add it to the state results if (includeInResults) { // Create combined state - if not isolate, merge track data with parent state if (!isolate) { return { ...state, data: { ...currentState.data, // Use currentState.data instead of state.data to preserve track changes tracks: { ...(state.data.tracks || {}), [name]: trackResult, }, }, results: [...state.results, { track: trackResult }], }; } else { // If isolated, don't merge data but just add track to tracks return { ...state, data: { ...state.data, tracks: { ...(state.data.tracks || {}), [name]: trackResult, }, }, results: [...state.results, { track: trackResult }], }; } } else { // Same data handling logic as above, but don't add to results if (!isolate) { return { ...state, data: { ...currentState.data, // Use currentState.data to preserve track changes tracks: { ...(state.data.tracks || {}), [name]: trackResult, }, }, }; } else { return { ...state, data: { ...state.data, tracks: { ...(state.data.tracks || {}), [name]: trackResult, }, }, }; } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); stepLogger.error(`Track "${name}" failed: ${errorMessage}`); // Create an error track result const trackResult = { name, results: currentState.results, data: currentState.data, metadata: { ...metadata, description, error: errorMessage, failedAt: new Date().toISOString(), failedStep: currentState.metadata.currentStep || 'unknown', }, errors: [ ...currentState.errors, { message: errorMessage, step: currentState.metadata.currentStep || 'unknown', code: error instanceof Error && 'code' in error ? error.code : 'TRACK_EXECUTION_ERROR', }, ], completed: false, }; // If error is already a properly formatted error, just rethrow after adding track to state // Otherwise wrap in ProcessingError const errorToThrow = error instanceof ValidationError || error instanceof ConfigurationError || error instanceof ProcessingError ? error : new ProcessingError({ message: `Track "${name}" execution failed: ${errorMessage}`, step: 'Track', details: { trackName: name, failedStep: currentState.metadata.currentStep || 'unknown', originalError: error, }, retry: false, suggestions: [ 'Check the configuration of the steps in the track', 'Look at the specific error in the track result for more details', 'Consider setting continueOnError=true to complete partial results', ], }); // Add the failed track to state data before throwing const updatedState = { ...state, data: { ...state.data, tracks: { ...(state.data.tracks || {}), [name]: trackResult, }, }, // If we're including in results, add the failed track result ...(includeInResults ? { results: [...state.results, { track: trackResult }], } : {}), }; // If retry is enabled at the track level, we'll log but return the state // This allows the parent (usually parallel step) to handle retry if (retry.maxRetries && retry.maxRetries > 0) { stepLogger.info(`Track "${name}" is configured for retry (maxRetries=${retry.maxRetries})`); updatedState.metadata = { ...updatedState.metadata, retryTrack: name, retryError: errorMessage, }; return updatedState; } // Otherwise throw to allow higher-level retry mechanisms to handle it throw errorToThrow; } } /** * Creates a track step for the research pipeline * * @param options Options for the research track * @returns A track step for the research pipeline */ export function track(options) { return createStep('Track', // Wrapper function that matches the expected signature async (state, opts) => { return executeTrackStep(state, options); }, options, { // Mark as retryable if retry options are provided retryable: !!options.retry?.maxRetries, maxRetries: options.retry?.maxRetries || 0, retryDelay: options.retry?.baseDelay || 1000, backoffFactor: 2, // Track is a super-step that's always required optional: false, }); } //# sourceMappingURL=track.js.map