meld
Version:
Meld: A template language for LLM prompts
728 lines (631 loc) • 23.6 kB
text/typescript
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
}
);
}
}
}