UNPKG

meld

Version:

Meld: A template language for LLM prompts

728 lines (631 loc) 23.6 kB
import type { DirectiveNode, DirectiveKind, DirectiveData } from 'meld-spec'; import { directiveLogger } from '../../../core/utils/logger.js'; import { IDirectiveService, IDirectiveHandler, DirectiveContext } from './IDirectiveService.js'; import { IValidationService } from '@services/resolution/ValidationService/IValidationService.js'; import { IStateService } from '@services/state/StateService/IStateService.js'; import { IPathService } from '@services/fs/PathService/IPathService.js'; import { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js'; import { IParserService } from '@services/pipeline/ParserService/IParserService.js'; import { IInterpreterService } from '@services/pipeline/InterpreterService/IInterpreterService.js'; import { MeldDirectiveError } from '@core/errors/MeldDirectiveError.js'; import { ICircularityService } from '@services/resolution/CircularityService/ICircularityService.js'; import { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js'; import { DirectiveError, DirectiveErrorCode, DirectiveErrorSeverity } from './errors/DirectiveError.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; import type { ILogger } from './handlers/execution/EmbedDirectiveHandler.js'; // Import all handlers import { TextDirectiveHandler } from './handlers/definition/TextDirectiveHandler.js'; import { DataDirectiveHandler } from './handlers/definition/DataDirectiveHandler.js'; import { PathDirectiveHandler } from './handlers/definition/PathDirectiveHandler.js'; import { DefineDirectiveHandler } from './handlers/definition/DefineDirectiveHandler.js'; import { RunDirectiveHandler } from './handlers/execution/RunDirectiveHandler.js'; import { EmbedDirectiveHandler } from './handlers/execution/EmbedDirectiveHandler.js'; import { ImportDirectiveHandler } from './handlers/execution/ImportDirectiveHandler.js'; export class MeldLLMXMLError extends Error { constructor( message: string, public readonly code: string, public readonly details?: any ) { super(message); this.name = 'MeldLLMXMLError'; Object.setPrototypeOf(this, MeldLLMXMLError.prototype); } } /** * Service responsible for handling directives */ export class DirectiveService implements IDirectiveService { private validationService?: IValidationService; private stateService?: IStateService; private pathService?: IPathService; private fileSystemService?: IFileSystemService; private parserService?: IParserService; private interpreterService?: IInterpreterService; private circularityService?: ICircularityService; private resolutionService?: IResolutionService; private initialized = false; private logger: ILogger; private handlers: Map<string, IDirectiveHandler> = new Map(); constructor(logger?: ILogger) { this.logger = logger || directiveLogger; } initialize( validationService: IValidationService, stateService: IStateService, pathService: IPathService, fileSystemService: IFileSystemService, parserService: IParserService, interpreterService: IInterpreterService, circularityService: ICircularityService, resolutionService: IResolutionService ): void { this.validationService = validationService; this.stateService = stateService; this.pathService = pathService; this.fileSystemService = fileSystemService; this.parserService = parserService; this.interpreterService = interpreterService; this.circularityService = circularityService; this.resolutionService = resolutionService; this.initialized = true; // Register default handlers this.registerDefaultHandlers(); this.logger.debug('DirectiveService initialized', { handlers: Array.from(this.handlers.keys()) }); } /** * Register all default directive handlers */ public registerDefaultHandlers(): void { // Definition handlers const textHandler = new TextDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ); // Set FileSystemService if available if (this.fileSystemService) { textHandler.setFileSystemService(this.fileSystemService); } this.registerHandler(textHandler); this.registerHandler( new DataDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ) ); this.registerHandler( new PathDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ) ); this.registerHandler( new DefineDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ) ); // Execution handlers this.registerHandler( new RunDirectiveHandler( this.validationService!, this.resolutionService!, this.stateService!, this.fileSystemService! ) ); this.registerHandler( new EmbedDirectiveHandler( this.validationService!, this.resolutionService!, this.stateService!, this.circularityService!, this.fileSystemService!, this.parserService!, this.interpreterService!, this.logger ) ); this.registerHandler( new ImportDirectiveHandler( this.validationService!, this.resolutionService!, this.stateService!, this.fileSystemService!, this.parserService!, this.interpreterService!, this.circularityService! ) ); } /** * Register a new directive handler */ registerHandler(handler: IDirectiveHandler): void { if (!this.initialized) { throw new Error('DirectiveService must be initialized before registering handlers'); } if (!handler.kind) { throw new Error('Handler must have a kind property'); } this.handlers.set(handler.kind, handler); this.logger.debug(`Registered handler for directive: ${handler.kind}`); } /** * Handle a directive node */ public async handleDirective(node: DirectiveNode, context: DirectiveContext): Promise<IStateService> { return this.processDirective(node, context); } /** * Process multiple directives in sequence */ async processDirectives(nodes: DirectiveNode[], parentContext?: DirectiveContext): Promise<IStateService> { let currentState = parentContext?.state?.clone() || this.stateService!.createChildState(); for (const node of nodes) { // Create a new context with the current state as both parent and state // This ensures that subsequent directives can see variables defined by previous directives const nodeContext = { currentFilePath: parentContext?.currentFilePath || '', parentState: currentState, state: currentState.clone() }; // Process directive and get the updated state const result = await this.processDirective(node, nodeContext); // If transformation is enabled, we don't merge states since the directive // will be replaced with a text node and its state will be handled separately if (!currentState.isTransformationEnabled?.()) { // Update currentState directly with the result so next directives have access to it currentState = result; } else { // Even if transformation is enabled, we need to make sure variables defined in one directive // are available to subsequent directives if (result !== nodeContext.state) { // Only apply the new state if it actually changed (as a result of directive execution) currentState = result; } } } return currentState; } /** * Create execution context for a directive */ private createContext(node: DirectiveNode, parentContext?: DirectiveContext): DirectiveContext { if (!this.stateService) { throw new Error('DirectiveService must be initialized before use'); } const state = parentContext?.state?.clone() || this.stateService.createChildState(); return { currentFilePath: parentContext?.currentFilePath || '', parentState: parentContext?.state, state }; } /** * Update the interpreter service reference */ updateInterpreterService(interpreterService: IInterpreterService): void { this.interpreterService = interpreterService; this.logger.debug('Updated interpreter service reference'); } /** * Check if a handler exists for a directive kind */ hasHandler(kind: string): boolean { return this.handlers.has(kind); } /** * Validate a directive node */ async validateDirective(node: DirectiveNode): Promise<void> { try { await this.validationService!.validate(node); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); const errorForLog = error instanceof Error ? error : new Error(String(error)); this.logger.error('Failed to validate directive', { kind: node.directive.kind, location: node.location, error: errorForLog }); throw new DirectiveError( errorMessage, node.directive.kind, DirectiveErrorCode.VALIDATION_FAILED, { node } ); } } /** * Create a child context for nested directives */ public createChildContext(parentContext: DirectiveContext, filePath: string): DirectiveContext { return { currentFilePath: filePath, state: parentContext.state.createChildState(), parentState: parentContext.state }; } supportsDirective(kind: string): boolean { return this.handlers.has(kind); } getSupportedDirectives(): string[] { return Array.from(this.handlers.keys()); } private ensureInitialized(): void { if (!this.initialized) { throw new Error('DirectiveService must be initialized before use'); } } private async handleTextDirective(node: DirectiveNode): Promise<void> { const directive = node.directive; this.logger.debug('Processing text directive', { identifier: directive.identifier, location: node.location }); try { // Value is already interpolated by meld-ast await this.stateService!.setTextVar(directive.identifier, directive.value); this.logger.debug('Text directive processed successfully', { identifier: directive.identifier, location: node.location }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); const errorForLog = error instanceof Error ? error : new Error(String(error)); this.logger.error('Failed to process text directive', { identifier: directive.identifier, location: node.location, error: errorForLog }); throw new MeldDirectiveError( errorMessage, 'text', { location: node.location?.start } ); } } private async handleDataDirective(node: DirectiveNode): Promise<void> { const directive = node.directive; this.logger.debug('Processing data directive', { identifier: directive.identifier, location: node.location }); try { // Value is already interpolated by meld-ast let value = directive.value; if (typeof value === 'string') { value = JSON.parse(value); } await this.stateService!.setDataVar(directive.identifier, value); this.logger.debug('Data directive processed successfully', { identifier: directive.identifier, location: node.location }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); const errorForLog = error instanceof Error ? error : new Error(String(error)); this.logger.error('Failed to process data directive', { identifier: directive.identifier, location: node.location, error: errorForLog }); throw new MeldDirectiveError( errorMessage, 'data', { location: node.location?.start } ); } } private async handleImportDirective(node: DirectiveNode): Promise<void> { const directive = node.directive; this.logger.debug('Processing import directive', { path: directive.path, section: directive.section, fuzzy: directive.fuzzy, location: node.location }); try { // Path is already interpolated by meld-ast const fullPath = await this.pathService!.resolvePath(directive.path); // Check for circular imports this.circularityService!.beginImport(fullPath); try { // Check if file exists if (!await this.fileSystemService!.exists(fullPath)) { throw new Error(`Import file not found: ${fullPath}`); } // Create a child state for the import const childState = await this.stateService!.createChildState(); // Read the file content const content = await this.fileSystemService!.readFile(fullPath); // If a section is specified, extract it (section name is already interpolated) let processedContent = content; if (directive.section) { processedContent = await this.extractSection( content, directive.section, directive.fuzzy || 0 ); } // Parse and interpret the content const parsedNodes = await this.parserService!.parse(processedContent); await this.interpreterService!.interpret(parsedNodes, { initialState: childState, filePath: fullPath, mergeState: true }); this.logger.debug('Import content processed', { path: fullPath, section: directive.section, location: node.location }); } finally { // Always end import tracking, even if there was an error this.circularityService!.endImport(fullPath); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); const errorForLog = error instanceof Error ? error : new Error(String(error)); this.logger.error('Failed to process import directive', { path: directive.path, section: directive.section, location: node.location, error: errorForLog }); throw new MeldDirectiveError( errorMessage, 'import', { location: node.location?.start } ); } } private async extractSection( content: string, section: string, fuzzyMatch: number ): Promise<string> { try { // Split content into lines const lines = content.split('\n'); const headings: { title: string; line: number; level: number }[] = []; // Find all headings and their levels for (let i = 0; i < lines.length; i++) { const line = lines[i]; const match = line.match(/^(#{1,6})\s+(.+)$/); if (match) { headings.push({ title: match[2], line: i, level: match[1].length }); } } // Find best matching heading let bestMatch: typeof headings[0] | undefined; let bestScore = 0; for (const heading of headings) { const score = this.calculateSimilarity(heading.title, section); if (score > fuzzyMatch && score > bestScore) { bestScore = score; bestMatch = heading; } } if (!bestMatch) { // Find closest match for error message let closestMatch = ''; let closestScore = 0; for (const heading of headings) { const score = this.calculateSimilarity(heading.title, section); if (score > closestScore) { closestScore = score; closestMatch = heading.title; } } throw new MeldLLMXMLError( 'Section not found', 'SECTION_NOT_FOUND', { title: section, bestMatch: closestMatch } ); } // Find the end of the section (next heading of same or higher level) let endLine = lines.length; for (let i = bestMatch.line + 1; i < lines.length; i++) { const line = lines[i]; const match = line.match(/^(#{1,6})\s+/); if (match && match[1].length <= bestMatch.level) { endLine = i; break; } } // Extract the section content return lines.slice(bestMatch.line, endLine).join('\n'); } catch (error) { if (error instanceof MeldLLMXMLError) { throw error; } throw new MeldLLMXMLError( error instanceof Error ? error.message : 'Unknown error during section extraction', 'PARSE_ERROR', error ); } } private calculateSimilarity(str1: string, str2: string): number { // Convert strings to lowercase for case-insensitive comparison const s1 = str1.toLowerCase(); const s2 = str2.toLowerCase(); if (s1 === s2) return 1.0; // Calculate Levenshtein distance const len1 = s1.length; const len2 = s2.length; const matrix: number[][] = []; for (let i = 0; i <= len1; i++) { matrix[i] = [i]; } for (let j = 0; j <= len2; j++) { matrix[0][j] = j; } for (let i = 1; i <= len1; i++) { for (let j = 1; j <= len2; j++) { const cost = s1[i - 1] === s2[j - 1] ? 0 : 1; matrix[i][j] = Math.min( matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost ); } } // Convert distance to similarity score between 0 and 1 const maxLen = Math.max(len1, len2); return maxLen === 0 ? 1.0 : 1.0 - matrix[len1][len2] / maxLen; } private async handleEmbedDirective(node: DirectiveNode): Promise<void> { const directive = node.directive; this.logger.debug('Processing embed directive', { path: directive.path, section: directive.section, fuzzy: directive.fuzzy, names: directive.names, location: node.location }); try { // Path is already interpolated by meld-ast const fullPath = await this.pathService!.resolvePath(directive.path); // Check for circular imports this.circularityService!.beginImport(fullPath); try { // Check if file exists if (!await this.fileSystemService!.exists(fullPath)) { throw new Error(`Embed file not found: ${fullPath}`); } // Create a child state for the import const childState = await this.stateService!.createChildState(); // Read the file content const content = await this.fileSystemService!.readFile(fullPath); // If a section is specified, extract it (section name is already interpolated) let processedContent = content; if (directive.section) { processedContent = await this.extractSection( content, directive.section, directive.fuzzy || 0 ); } // Parse and interpret the content const parsedNodes = await this.parserService!.parse(processedContent); await this.interpreterService!.interpret(parsedNodes, { initialState: childState, filePath: fullPath, mergeState: true }); this.logger.debug('Embed content processed', { path: fullPath, section: directive.section, location: node.location }); } finally { // Always end import tracking, even if there was an error this.circularityService!.endImport(fullPath); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); const errorForLog = error instanceof Error ? error : new Error(String(error)); this.logger.error('Failed to process embed directive', { path: directive.path, section: directive.section, location: node.location, error: errorForLog }); throw new MeldDirectiveError( errorMessage, 'embed', { location: node.location?.start } ); } } /** * Process a directive node, validating and executing it * Values in the directive will already be interpolated by meld-ast * @returns The updated state after directive execution * @throws {MeldDirectiveError} If directive processing fails */ public async processDirective(node: DirectiveNode, context: DirectiveContext): Promise<IStateService> { // Add initialization check before any other processing if (!this.initialized) { throw new MeldDirectiveError( 'DirectiveService must be initialized before use', 'initialization', { severity: ErrorSeverity.Fatal } ); } try { // Get the handler for this directive kind const { kind } = node.directive; const handler = this.handlers.get(kind); if (!handler) { throw new DirectiveError( `No handler found for directive: ${kind}`, kind, DirectiveErrorCode.HANDLER_NOT_FOUND, { node } ); } // Validate directive before handling await this.validateDirective(node); // Execute the directive and handle both possible return types const result = await handler.execute(node, context); // If result is a DirectiveResult, return its state if ('state' in result) { return result.state; } // Otherwise, result is already an IStateService return result; } catch (error) { // If it's already a DirectiveError or MeldDirectiveError, just rethrow if (error instanceof DirectiveError || error instanceof MeldDirectiveError) { throw error; } // Simplify error messages for common cases let message = error instanceof Error ? error.message : String(error); let code = DirectiveErrorCode.EXECUTION_FAILED; let severity = ErrorSeverity.Recoverable; if (message.includes('file not found') || message.includes('no such file')) { message = `Referenced file not found: ${node.directive.path || node.directive.value}`; code = DirectiveErrorCode.FILE_NOT_FOUND; severity = DirectiveErrorSeverity[code]; } else if (message.includes('circular import') || message.includes('circular reference')) { message = 'Circular import detected'; code = DirectiveErrorCode.CIRCULAR_REFERENCE; severity = DirectiveErrorSeverity[code]; } else if (message.includes('parameter count') || message.includes('wrong number of parameters')) { message = 'Invalid parameter count'; code = DirectiveErrorCode.VALIDATION_FAILED; severity = DirectiveErrorSeverity[code]; } else if (message.includes('invalid path') || message.includes('path validation failed')) { message = 'Invalid path'; code = DirectiveErrorCode.VALIDATION_FAILED; severity = DirectiveErrorSeverity[code]; } throw new DirectiveError( message, node.directive?.kind || 'unknown', code, { node, context, cause: error instanceof Error ? error : undefined } ); } } }