From bb7b3d779e2f8168bff8ba06d85aee243b0b1fd3 Mon Sep 17 00:00:00 2001 From: NarwhalChen Date: Mon, 13 Jan 2025 14:18:39 -0800 Subject: [PATCH] feat: support detail error in handler --- backend/src/build-system/errors.ts | 86 +++++++ .../handlers/backend/code-generate/index.ts | 127 +++++++---- .../backend/file-review/file-review.ts | 214 ++++++++++-------- .../backend/requirements-document/index.ts | 88 +++---- .../database/requirements-document/index.ts | 75 +++--- .../handlers/database/schemas/schemas.ts | 206 ++++++++--------- .../handlers/file-manager/file-arch/index.ts | 105 +++++---- .../file-manager/file-generate/index.ts | 93 ++++---- .../file-manager/file-structure/index.ts | 179 ++++++--------- .../product-requirements-document/prd.ts | 56 ++--- .../build-system/handlers/ux/datamap/index.ts | 49 ++-- .../handlers/ux/sitemap-document/uxsmd.ts | 68 +++--- .../handlers/ux/sitemap-structure/index.ts | 49 ++-- .../handlers/ux/sitemap-structure/sms-page.ts | 85 +++---- backend/src/build-system/retry-handler.ts | 39 ++-- 15 files changed, 807 insertions(+), 712 deletions(-) create mode 100644 backend/src/build-system/errors.ts diff --git a/backend/src/build-system/errors.ts b/backend/src/build-system/errors.ts new file mode 100644 index 00000000..557554c9 --- /dev/null +++ b/backend/src/build-system/errors.ts @@ -0,0 +1,86 @@ +/** + * Error thrown when a required file is not found. + */ +export class FileNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'FileNotFoundError'; + } + } + + /** + * Error thrown when there is an issue modifying a file. + */ + export class FileModificationError extends Error { + constructor(message: string) { + super(message); + this.name = 'FileModificationError'; + } + } + + /** + * Error thrown when parsing a response fails. + */ + export class ResponseParsingError extends Error { + constructor(message: string) { + super(message); + this.name = 'ResponseParsingError'; + } + } + + export class ResponseTagError extends Error { + constructor(message: string) { + super(message); + this.name = 'ResponseTagError'; + } + } + + export class ModelTimeoutError extends Error { + constructor(message: string) { + super(message); + this.name = 'ModelTimeoutError'; + } + } + + export class TemporaryServiceUnavailableError extends Error { + constructor(message: string) { + super(message); + this.name = 'TemporaryServiceUnavailableError'; + } + } + + export class RateLimitExceededError extends Error { + constructor(message: string) { + super(message); + this.name = 'RateLimitExceededError'; + } + } + + export class MissingConfigurationError extends Error { + constructor(message: string) { + super(message); + this.name = 'MissingConfigurationError'; + } + } + + export class InvalidParameterError extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidParameterError'; + } + } + + export class FileWriteError extends Error { + constructor(message: string) { + super(message); + this.name = 'FileWriteError'; + } + } + + export class ParsingError extends Error { + constructor(message: string) { + super(message); + this.name = 'ParsingError'; + } + } + \ No newline at end of file diff --git a/backend/src/build-system/handlers/backend/code-generate/index.ts b/backend/src/build-system/handlers/backend/code-generate/index.ts index 32e46997..39686156 100644 --- a/backend/src/build-system/handlers/backend/code-generate/index.ts +++ b/backend/src/build-system/handlers/backend/code-generate/index.ts @@ -1,21 +1,26 @@ import { BuildHandler, BuildResult } from 'src/build-system/types'; import { BuilderContext } from 'src/build-system/context'; import { generateBackendCodePrompt } from './prompt'; -import { Logger } from '@nestjs/common'; import { saveGeneratedCode } from 'src/build-system/utils/files'; import * as path from 'path'; import { formatResponse } from 'src/build-system/utils/strings'; import { - NonRetryableError, - RetryableError, -} from 'src/build-system/retry-handler'; + ModelTimeoutError, + TemporaryServiceUnavailableError, + RateLimitExceededError, + MissingConfigurationError, + InvalidParameterError, + FileWriteError, + ParsingError, + ResponseTagError, +} from 'src/build-system/errors'; + /** * BackendCodeHandler is responsible for generating the backend codebase * based on the provided sitemap and data mapping documents. */ export class BackendCodeHandler implements BuildHandler { readonly id = 'op:BACKEND:CODE'; - readonly logger: Logger = new Logger('BackendCodeHandler'); /** * Executes the handler to generate backend code. @@ -23,30 +28,30 @@ export class BackendCodeHandler implements BuildHandler { * @returns A BuildResult containing the generated code and related data. */ async run(context: BuilderContext): Promise> { - this.logger.log('Generating Backend Codebase...'); - // Retrieve project name and database type from context const projectName = context.getGlobalContext('projectName') || 'Default Project Name'; const databaseType = context.getGlobalContext('databaseType') || 'Default database type'; - // Destructure arguments with default values for optional parameters + // Retrieve required documents const sitemapDoc = context.getNodeData('op:UX:SMD'); const datamapDoc = context.getNodeData('op:UX:DATAMAP:DOC'); const databaseSchemas = context.getNodeData('op:DATABASE:SCHEMAS'); + const backendRequirementDoc = + context.getNodeData('op:BACKEND:REQ')?.overview || ''; + // Validate required data if (!sitemapDoc || !datamapDoc || !databaseSchemas) { - return { - success: false, - error: new NonRetryableError( - 'Missing required parameters: sitemapDoc, datamapDoc, or databaseSchemas.', - ), - }; + throw new MissingConfigurationError( + 'Missing required configuration: sitemapDoc, datamapDoc, or databaseSchemas.', + ); + } + + if (typeof databaseSchemas !== 'object') { + throw new InvalidParameterError('databaseSchemas should be a valid object.'); } - const backendRequirementDoc = - context.getNodeData('op:BACKEND:REQ')?.overview || ''; const currentFile = 'index.js'; const dependencyFile = 'dependencies.json'; @@ -63,43 +68,77 @@ export class BackendCodeHandler implements BuildHandler { dependencyFile, ); + let modelResponse: string; try { - // Invoke the language model to generate the backend code - const modelResponse = await context.model.chatSync({ - model: 'gpt-4o-mini', - messages: [{ content: backendCodePrompt, role: 'system' }], - }); - - const generatedCode = formatResponse(modelResponse); + modelResponse = await this.callModel(context, backendCodePrompt); + } catch (error) { + if ( + error instanceof ModelTimeoutError || + error instanceof TemporaryServiceUnavailableError || + error instanceof RateLimitExceededError + ) { + throw error; // Retryable errors will be handled externally. + } + throw new Error(`Unexpected model error: ${error.message}`); + } + let generatedCode: string; + try { + generatedCode = formatResponse(modelResponse); if (!generatedCode) { - throw new RetryableError('Generated code is empty.'); + throw new ResponseTagError('Response tag extraction failed.'); } + } catch (error) { + if (error instanceof ResponseTagError) { + throw error; + } + throw new ParsingError('Error occurred while parsing the model response.'); + } - const uuid = context.getGlobalContext('projectUUID'); - saveGeneratedCode(path.join(uuid, 'backend', currentFile), generatedCode); - - this.logger.debug('Backend code generated and parsed successfully.'); + // Save the generated code to the specified location + const uuid = context.getGlobalContext('projectUUID'); + const savePath = path.join(uuid, 'backend', currentFile); - // TODO: return backend API as output - return { - success: true, - data: generatedCode, - }; + try { + saveGeneratedCode(savePath, generatedCode); } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn(`Retryable error encountered: ${error.message}`); - return { - success: false, - error, - }; + throw new FileWriteError(`Failed to save backend code to ${savePath}: ${error.message}`); + } + + return { + success: true, + data: generatedCode, + }; + } + + /** + * Calls the language model to generate backend code. + * @param context The builder context. + * @param prompt The generated prompt. + */ + private async callModel(context: BuilderContext, prompt: string): Promise { + try { + const modelResponse = await context.model.chatSync({ + model: 'gpt-4o-mini', + messages: [{ content: prompt, role: 'system' }], + }); + + if (!modelResponse) { + throw new ModelTimeoutError('The model did not respond within the expected time.'); } - this.logger.error('Non-retryable error encountered:', error); - return { - success: false, - error: new NonRetryableError('Failed to generate backend code.'), - }; + return modelResponse; + } catch (error) { + if (error.message.includes('timeout')) { + throw new ModelTimeoutError('Timeout occurred while communicating with the model.'); + } + if (error.message.includes('service unavailable')) { + throw new TemporaryServiceUnavailableError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + throw new RateLimitExceededError('Rate limit exceeded for model service.'); + } + throw new Error(`Unexpected model error: ${error.message}`); } } } diff --git a/backend/src/build-system/handlers/backend/file-review/file-review.ts b/backend/src/build-system/handlers/backend/file-review/file-review.ts index 23124e3d..a8fbf6f9 100644 --- a/backend/src/build-system/handlers/backend/file-review/file-review.ts +++ b/backend/src/build-system/handlers/backend/file-review/file-review.ts @@ -7,9 +7,13 @@ import * as path from 'path'; import { prompts } from './prompt'; import { formatResponse } from 'src/build-system/utils/strings'; import { - NonRetryableError, - RetryableError, -} from 'src/build-system/retry-handler'; + FileNotFoundError, + FileModificationError, + ResponseParsingError, + ModelTimeoutError, + TemporaryServiceUnavailableError, + RateLimitExceededError, +} from 'src/build-system/errors'; /** * Responsible for reviewing all related source root files and considering modifications @@ -23,127 +27,151 @@ export class BackendFileReviewHandler implements BuildHandler { async run(context: BuilderContext): Promise> { this.logger.log('Starting backend file modification process...'); - try { - const backendPath = - context.getGlobalContext('backendPath') || './backend'; - const projectName = - context.getGlobalContext('projectName') || 'Default Project Name'; - const description = - context.getGlobalContext('description') || - 'Default Project Description'; - const projectOverview = ` + const backendPath = + context.getGlobalContext('backendPath') || './backend'; + const projectName = + context.getGlobalContext('projectName') || 'Default Project Name'; + const description = + context.getGlobalContext('description') || + 'Default Project Description'; + const projectOverview = ` project name: ${projectName} project description: ${description}, `; - const backendRequirement = context.getNodeData('op:BACKEND:REQ').overview; - const backendCode = [context.getNodeData('op:BACKEND:CODE')]; + const backendRequirement = context.getNodeData('op:BACKEND:REQ')?.overview; + const backendCode = [context.getNodeData('op:BACKEND:CODE')]; + + if (!backendRequirement) { + throw new FileNotFoundError('Backend requirements are missing.'); + } - // 1. Identify files to modify + let files: string[]; + try { this.logger.log(`Scanning backend directory: ${backendPath}`); - const files = await fs.readdir(backendPath); + files = await fs.readdir(backendPath); if (!files.length) { - throw new NonRetryableError('No files found in the backend directory.'); + throw new FileNotFoundError('No files found in the backend directory.'); } this.logger.debug(`Found files: ${files.join(', ')}`); + } catch (error) { + this.handleFileSystemError(error); + } - const filePrompt = prompts.identifyBackendFilesToModify( - files, - backendRequirement, - projectOverview, - backendCode, - ); + const filePrompt = prompts.identifyBackendFilesToModify( + files, + backendRequirement, + projectOverview, + backendCode, + ); - const modelResponse = await context.model.chatSync({ + let modelResponse: string; + try { + modelResponse = await context.model.chatSync({ model: 'gpt-4o-mini', messages: [{ content: filePrompt, role: 'system' }], }); + } catch (error) { + this.handleModelError(error); + } - const filesToModify = this.parseFileIdentificationResponse(modelResponse); - if (!filesToModify.length) { - throw new RetryableError('No files identified for modification.'); - } - this.logger.log(`Files to modify: ${filesToModify.join(', ')}`); - - // 2. Modify each identified file - for (const fileName of filesToModify) { - const filePath = path.join(backendPath, fileName); - try { - const currentContent = await fs.readFile(filePath, 'utf-8'); - - const modificationPrompt = prompts.generateFileModificationPrompt( - fileName, - currentContent, - backendRequirement, - projectOverview, - backendCode, - ); + const filesToModify = this.parseFileIdentificationResponse(modelResponse); + if (!filesToModify.length) { + throw new FileModificationError('No files identified for modification.'); + } + this.logger.log(`Files to modify: ${filesToModify.join(', ')}`); + + for (const fileName of filesToModify) { + const filePath = path.join(backendPath, fileName); + try { + const currentContent = await fs.readFile(filePath, 'utf-8'); + const modificationPrompt = prompts.generateFileModificationPrompt( + fileName, + currentContent, + backendRequirement, + projectOverview, + backendCode, + ); + + const response = await context.model.chatSync({ + model: 'gpt-4o-mini', + messages: [{ content: modificationPrompt, role: 'system' }], + }); - const response = await context.model.chatSync({ - model: 'gpt-4o-mini', - messages: [{ content: modificationPrompt, role: 'system' }], - }); - - const newContent = formatResponse(response); - if (!newContent) { - throw new RetryableError( - `Failed to generate content for file: ${fileName}.`, - ); - } - - await fs.writeFile(filePath, newContent, 'utf-8'); - this.logger.log(`Successfully modified ${fileName}`); - } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error for file ${fileName}: ${error.message}`, - ); - } else { - this.logger.error( - `Non-retryable error for file ${fileName}:`, - error, - ); - throw error; - } + const newContent = formatResponse(response); + if (!newContent) { + throw new FileModificationError( + `Failed to generate content for file: ${fileName}.`, + ); } - } - return { - success: true, - data: `Modified files: ${filesToModify.join(', ')}`, - }; - } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn(`Retryable error encountered: ${error.message}`); - return { - success: false, - error, - }; + await fs.writeFile(filePath, newContent, 'utf-8'); + this.logger.log(`Successfully modified ${fileName}`); + } catch (error) { + this.handleFileProcessingError(fileName, error); } - - this.logger.error('Non-retryable error encountered:', error); - return { - success: false, - error: new NonRetryableError('Failed to modify backend files.'), - }; } + + return { + success: true, + data: `Modified files: ${filesToModify.join(', ')}`, + }; } + /** + * Parses the file identification response from the model. + */ parseFileIdentificationResponse(response: string): string[] { try { const parsedResponse = JSON.parse(formatResponse(response)); if (!Array.isArray(parsedResponse)) { - throw new NonRetryableError( - 'File identification response is not an array.', - ); + throw new ResponseParsingError('File identification response is not an array.'); } this.logger.log('Parsed file identification response:', parsedResponse); return parsedResponse; } catch (error) { this.logger.error('Error parsing file identification response:', error); - throw new NonRetryableError( - 'Failed to parse file identification response.', - ); + throw new ResponseParsingError('Failed to parse file identification response.'); + } + } + + /** + * Handles file system errors. + */ + private handleFileSystemError(error: any): never { + this.logger.error('File system error encountered:', error); + throw new FileNotFoundError(`File system operation failed: ${error.message}`); + } + + /** + * Handles model-related errors. + */ + private handleModelError(error: any): never { + if ( + error instanceof ModelTimeoutError || + error instanceof TemporaryServiceUnavailableError || + error instanceof RateLimitExceededError + ) { + this.logger.warn(`Retryable model error: ${error.message}`); + throw error; + } + this.logger.error('Non-retryable model error encountered:', error); + throw new ResponseParsingError(`Model error: ${error.message}`); + } + + /** + * Handles errors during file processing. + */ + private handleFileProcessingError(fileName: string, error: any): never { + if ( + error instanceof ModelTimeoutError || + error instanceof TemporaryServiceUnavailableError || + error instanceof RateLimitExceededError + ) { + this.logger.warn(`Retryable error for file ${fileName}: ${error.message}`); + throw error; } + this.logger.error(`Non-retryable error for file ${fileName}:`, error); + throw new FileModificationError(`Error processing file ${fileName}: ${error.message}`); } } diff --git a/backend/src/build-system/handlers/backend/requirements-document/index.ts b/backend/src/build-system/handlers/backend/requirements-document/index.ts index 46eb1aae..cc41c4df 100644 --- a/backend/src/build-system/handlers/backend/requirements-document/index.ts +++ b/backend/src/build-system/handlers/backend/requirements-document/index.ts @@ -1,15 +1,15 @@ import { BuildHandler, BuildResult } from 'src/build-system/types'; import { BuilderContext } from 'src/build-system/context'; -import { - generateBackendImplementationPrompt, - generateBackendOverviewPrompt, -} from './prompt'; +import { generateBackendOverviewPrompt } from './prompt'; import { Logger } from '@nestjs/common'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - NonRetryableError, - RetryableError, -} from 'src/build-system/retry-handler'; + ResponseParsingError, + MissingConfigurationError, + ModelTimeoutError, + TemporaryServiceUnavailableError, + RateLimitExceededError, +} from 'src/build-system/errors'; type BackendRequirementResult = { overview: string; @@ -20,6 +20,7 @@ type BackendRequirementResult = { packages: Record; }; }; + /** * BackendRequirementHandler is responsible for generating the backend requirements document. * Core Content Generation: API Endpoints, System Overview @@ -28,7 +29,7 @@ export class BackendRequirementHandler implements BuildHandler { readonly id = 'op:BACKEND:REQ'; - readonly logger: Logger = new Logger('BackendRequirementHandler'); + private readonly logger: Logger = new Logger('BackendRequirementHandler'); async run( context: BuilderContext, @@ -49,12 +50,9 @@ export class BackendRequirementHandler this.logger.error( 'Missing required parameters: dbRequirements, datamapDoc, or sitemapDoc', ); - return { - success: false, - error: new NonRetryableError( - 'Missing required parameters: dbRequirements, datamapDoc, or sitemapDoc.', - ), - }; + throw new MissingConfigurationError( + 'Missing required parameters: dbRequirements, datamapDoc, or sitemapDoc.', + ); } const overviewPrompt = generateBackendOverviewPrompt( @@ -69,33 +67,13 @@ export class BackendRequirementHandler let backendOverview: string; try { - backendOverview = await context.model.chatSync({ - model: 'gpt-4o-mini', - messages: [{ content: overviewPrompt, role: 'system' }], - }); - + backendOverview = await this.callModel(context, overviewPrompt); if (!backendOverview || backendOverview.trim() === '') { - throw new RetryableError('Generated backend overview is empty.'); + throw new ResponseParsingError('Generated backend overview is empty.'); } } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during backend overview generation: ${error.message}`, - ); - return { - success: false, - error, - }; - } - - this.logger.error( - 'Non-retryable error generating backend overview:', - error, - ); - return { - success: false, - error: new NonRetryableError('Failed to generate backend overview.'), - }; + this.logger.error('Error during backend overview generation:', error); + throw error; // Pass error to upper-level handler } // Return generated data @@ -112,4 +90,38 @@ export class BackendRequirementHandler }, }; } + + /** + * Calls the language model to generate backend overview. + * @param context The builder context. + * @param prompt The generated prompt. + */ + private async callModel( + context: BuilderContext, + prompt: string, + ): Promise { + try { + const modelResponse = await context.model.chatSync({ + model: 'gpt-4o-mini', + messages: [{ content: prompt, role: 'system' }], + }); + + if (!modelResponse) { + throw new ModelTimeoutError('The model did not respond within the expected time.'); + } + + return modelResponse; + } catch (error) { + if (error.message.includes('timeout')) { + throw new ModelTimeoutError('Timeout occurred while communicating with the model.'); + } + if (error.message.includes('service unavailable')) { + throw new TemporaryServiceUnavailableError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + throw new RateLimitExceededError('Rate limit exceeded for model service.'); + } + throw new Error(`Unexpected model error: ${error.message}`); + } + } } diff --git a/backend/src/build-system/handlers/database/requirements-document/index.ts b/backend/src/build-system/handlers/database/requirements-document/index.ts index b618974d..e62d8d91 100644 --- a/backend/src/build-system/handlers/database/requirements-document/index.ts +++ b/backend/src/build-system/handlers/database/requirements-document/index.ts @@ -5,13 +5,16 @@ import { prompts } from './prompt'; import { Logger } from '@nestjs/common'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - NonRetryableError, - RetryableError, -} from 'src/build-system/retry-handler'; + MissingConfigurationError, + ResponseParsingError, + ModelTimeoutError, + TemporaryServiceUnavailableError, + RateLimitExceededError, +} from 'src/build-system/errors'; export class DatabaseRequirementHandler implements BuildHandler { readonly id = 'op:DATABASE_REQ'; - readonly logger = new Logger('DatabaseRequirementHandler'); + private readonly logger = new Logger('DatabaseRequirementHandler'); async run(context: BuilderContext): Promise> { this.logger.log('Generating Database Requirements Document...'); @@ -22,10 +25,7 @@ export class DatabaseRequirementHandler implements BuildHandler { if (!datamapDoc) { this.logger.error('Data mapping document is missing.'); - return { - success: false, - error: new NonRetryableError('Missing required parameter: datamapDoc.'), - }; + throw new MissingConfigurationError('Missing required parameter: datamapDoc.'); } const prompt = prompts.generateDatabaseRequirementPrompt( @@ -33,36 +33,16 @@ export class DatabaseRequirementHandler implements BuildHandler { datamapDoc, ); - const model = ModelProvider.getInstance(); let dbRequirementsContent: string; try { - dbRequirementsContent = await model.chatSync({ - model: 'gpt-4o-mini', - messages: [{ content: prompt, role: 'system' }], - }); - + dbRequirementsContent = await this.callModel(prompt); if (!dbRequirementsContent || dbRequirementsContent.trim() === '') { - throw new RetryableError( - 'Generated database requirements content is empty.', - ); + throw new ResponseParsingError('Generated database requirements content is empty.'); } } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn(`Retryable error encountered: ${error.message}`); - return { - success: false, - error, - }; - } - - this.logger.error('Non-retryable error encountered:', error); - return { - success: false, - error: new NonRetryableError( - 'Failed to generate database requirements document.', - ), - }; + this.logger.error('Error during database requirements generation:', error); + throw error; // Propagate error to upper-level handler } return { @@ -70,4 +50,35 @@ export class DatabaseRequirementHandler implements BuildHandler { data: removeCodeBlockFences(dbRequirementsContent), }; } + + /** + * Calls the language model to generate database requirements. + * @param prompt The generated prompt. + */ + private async callModel(prompt: string): Promise { + const model = ModelProvider.getInstance(); + try { + const modelResponse = await model.chatSync({ + model: 'gpt-4o-mini', + messages: [{ content: prompt, role: 'system' }], + }); + + if (!modelResponse) { + throw new ModelTimeoutError('The model did not respond within the expected time.'); + } + + return modelResponse; + } catch (error) { + if (error.message.includes('timeout')) { + throw new ModelTimeoutError('Timeout occurred while communicating with the model.'); + } + if (error.message.includes('service unavailable')) { + throw new TemporaryServiceUnavailableError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + throw new RateLimitExceededError('Rate limit exceeded for model service.'); + } + throw new Error(`Unexpected model error: ${error.message}`); + } + } } diff --git a/backend/src/build-system/handlers/database/schemas/schemas.ts b/backend/src/build-system/handlers/database/schemas/schemas.ts index c28e5936..b82a1a23 100644 --- a/backend/src/build-system/handlers/database/schemas/schemas.ts +++ b/backend/src/build-system/handlers/database/schemas/schemas.ts @@ -12,9 +12,13 @@ import { saveGeneratedCode } from 'src/build-system/utils/files'; import * as path from 'path'; import { formatResponse } from 'src/build-system/utils/strings'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + MissingConfigurationError, + ResponseParsingError, + FileWriteError, + ModelTimeoutError, + TemporaryServiceUnavailableError, + RateLimitExceededError, +} from 'src/build-system/errors'; /** * DBSchemaHandler is responsible for generating database schemas based on provided requirements. @@ -23,11 +27,6 @@ export class DBSchemaHandler implements BuildHandler { readonly id = 'op:DATABASE:SCHEMAS'; private readonly logger: Logger = new Logger('DBSchemaHandler'); - /** - * Executes the handler to generate database schemas. - * @param context - The builder context containing configuration and utilities. - * @returns A BuildResult containing the generated schema content and related data. - */ async run(context: BuilderContext): Promise { this.logger.log('Generating Database Schemas...'); @@ -39,10 +38,7 @@ export class DBSchemaHandler implements BuildHandler { const dbRequirements = context.getNodeData('op:DATABASE_REQ'); if (!dbRequirements) { this.logger.error('Missing database requirements.'); - return { - success: false, - error: new NonRetryableError('Missing required database requirements.'), - }; + throw new MissingConfigurationError('Missing required database requirements.'); } if (!isSupportedDatabaseType(databaseType)) { @@ -50,12 +46,9 @@ export class DBSchemaHandler implements BuildHandler { this.logger.error( `Unsupported database type: ${databaseType}. Supported types: ${supportedTypes}`, ); - return { - success: false, - error: new NonRetryableError( - `Unsupported database type: ${databaseType}. Supported types: ${supportedTypes}.`, - ), - }; + throw new MissingConfigurationError( + `Unsupported database type: ${databaseType}. Supported types: ${supportedTypes}.`, + ); } let fileExtension: string; @@ -63,84 +56,111 @@ export class DBSchemaHandler implements BuildHandler { fileExtension = getSchemaFileExtension(databaseType as DatabaseType); } catch (error) { this.logger.error('Error determining schema file extension:', error); - return { - success: false, - error: new NonRetryableError( - `Failed to determine schema file extension for database type: ${databaseType}.`, - ), - }; + throw new ResponseParsingError( + `Failed to determine schema file extension for database type: ${databaseType}.`, + ); } this.logger.debug(`Schema file extension: .${fileExtension}`); - // Step 1: Analyze database requirements + const dbAnalysis = await this.analyzeDatabaseRequirements( + context, + projectName, + dbRequirements, + databaseType, + ); + + const schemaContent = await this.generateDatabaseSchema( + context, + dbAnalysis, + databaseType, + fileExtension, + ); + + await this.validateDatabaseSchema(context, schemaContent, databaseType); + + const schemaFileName = `schema.${fileExtension}`; + const uuid = context.getGlobalContext('projectUUID'); + + try { + saveGeneratedCode( + path.join(uuid, 'backend', schemaFileName), + schemaContent, + ); + this.logger.log(`Schema file (${schemaFileName}) written successfully.`); + } catch (error) { + this.logger.error('Error writing schema file:', error); + throw new FileWriteError('Failed to write schema file.'); + } + + return { + success: true, + data: schemaContent, + }; + } + + private async analyzeDatabaseRequirements( + context: BuilderContext, + projectName: string, + dbRequirements: any, + databaseType: string, + ): Promise { const analysisPrompt = prompts.analyzeDatabaseRequirements( projectName, dbRequirements, databaseType, ); - let dbAnalysis: string; try { const analysisResponse = await context.model.chatSync({ model: 'gpt-4o-mini', messages: [{ content: analysisPrompt, role: 'system' }], }); - dbAnalysis = analysisResponse; - if (!dbAnalysis || dbAnalysis.trim() === '') { - throw new RetryableError( - 'Database requirements analysis returned empty.', - ); + + if (!analysisResponse || analysisResponse.trim() === '') { + throw new ResponseParsingError('Database requirements analysis returned empty.'); } + + return analysisResponse; } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn(`Retryable error during analysis: ${error.message}`); - return { success: false, error }; - } - this.logger.error('Non-retryable error during analysis:', error); - return { - success: false, - error: new NonRetryableError( - 'Failed to analyze database requirements.', - ), - }; + this.handleModelErrors(error, 'analyze database requirements'); } + } - this.logger.debug('Database requirements analyzed successfully.'); + private async generateDatabaseSchema( + context: BuilderContext, + dbAnalysis: string, + databaseType: string, + fileExtension: string, + ): Promise { + const schemaPrompt = prompts.generateDatabaseSchema( + dbAnalysis, + databaseType, + fileExtension, + ); - // Step 2: Generate database schema based on analysis - let schemaContent: string; try { - const schemaPrompt = prompts.generateDatabaseSchema( - dbAnalysis, - databaseType, - fileExtension, - ); const schemaResponse = await context.model.chatSync({ model: 'gpt-4o-mini', messages: [{ content: schemaPrompt, role: 'system' }], }); - schemaContent = formatResponse(schemaResponse); + + const schemaContent = formatResponse(schemaResponse); if (!schemaContent || schemaContent.trim() === '') { - throw new RetryableError('Generated database schema is empty.'); + throw new ResponseParsingError('Generated database schema is empty.'); } + + return schemaContent; } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during schema generation: ${error.message}`, - ); - return { success: false, error }; - } - this.logger.error('Non-retryable error during schema generation:', error); - return { - success: false, - error: new NonRetryableError('Failed to generate database schema.'), - }; + this.handleModelErrors(error, 'generate database schema'); } + } - this.logger.debug('Database schema generated successfully.'); - - // Step 3: Validate the generated schema + private async validateDatabaseSchema( + context: BuilderContext, + schemaContent: string, + databaseType: string, + ): Promise { const validationPrompt = prompts.validateDatabaseSchema( schemaContent, databaseType, @@ -151,48 +171,30 @@ export class DBSchemaHandler implements BuildHandler { model: 'gpt-4o-mini', messages: [{ content: validationPrompt, role: 'system' }], }); + const validationResponse = formatResponse(validationResult); if (validationResponse.includes('Error')) { - throw new RetryableError( - `Schema validation failed: ${validationResponse}`, - ); + throw new ResponseParsingError(`Schema validation failed: ${validationResponse}`); } } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during schema validation: ${error.message}`, - ); - return { success: false, error }; - } - this.logger.error('Non-retryable error during schema validation:', error); - return { - success: false, - error: new NonRetryableError('Failed to validate the database schema.'), - }; + this.handleModelErrors(error, 'validate database schema'); } + } - this.logger.debug('Schema validation passed.'); - - // Step 4: Save the schema to a file - const schemaFileName = `schema.${fileExtension}`; - const uuid = context.getGlobalContext('projectUUID'); - try { - saveGeneratedCode( - path.join(uuid, 'backend', schemaFileName), - schemaContent, - ); - this.logger.log(`Schema file (${schemaFileName}) written successfully.`); - } catch (error) { - this.logger.error('Error writing schema file:', error); - return { - success: false, - error: new NonRetryableError('Failed to write schema file.'), - }; + private handleModelErrors(error: any, stage: string): never { + switch (error.name) { + case 'ModelTimeoutError': + this.logger.warn(`Retryable error during ${stage}: ${error.message}`); + throw new ModelTimeoutError(error.message); + case 'TemporaryServiceUnavailableError': + this.logger.warn(`Retryable error during ${stage}: ${error.message}`); + throw new TemporaryServiceUnavailableError(error.message); + case 'RateLimitExceededError': + this.logger.warn(`Retryable error during ${stage}: ${error.message}`); + throw new RateLimitExceededError(error.message); + default: + this.logger.error(`Non-retryable error during ${stage}:`, error); + throw new ResponseParsingError(`Failed to ${stage}.`); } - - return { - success: true, - data: schemaContent, - }; } } diff --git a/backend/src/build-system/handlers/file-manager/file-arch/index.ts b/backend/src/build-system/handlers/file-manager/file-arch/index.ts index 5255e0c8..3a7dd766 100644 --- a/backend/src/build-system/handlers/file-manager/file-arch/index.ts +++ b/backend/src/build-system/handlers/file-manager/file-arch/index.ts @@ -8,9 +8,12 @@ import { parseGenerateTag, } from 'src/build-system/utils/strings'; import { - NonRetryableError, - RetryableError, -} from 'src/build-system/retry-handler'; + ResponseParsingError, + InvalidParameterError, + ModelTimeoutError, + TemporaryServiceUnavailableError, + RateLimitExceededError, +} from 'src/build-system/errors'; export class FileArchGenerateHandler implements BuildHandler { readonly id = 'op:FILE:ARCH'; @@ -23,12 +26,9 @@ export class FileArchGenerateHandler implements BuildHandler { const datamapDoc = context.getNodeData('op:UX:DATAMAP:DOC'); if (!fileStructure || !datamapDoc) { - return { - success: false, - error: new NonRetryableError( - 'Missing required parameters: fileStructure or datamapDoc', - ), - }; + throw new InvalidParameterError( + 'Missing required parameters: fileStructure or datamapDoc.', + ); } const prompt = generateFileArchPrompt( @@ -37,52 +37,33 @@ export class FileArchGenerateHandler implements BuildHandler { ); try { - const fileArchContent = await context.model.chatSync({ - model: 'gpt-4o-mini', - messages: [{ content: prompt, role: 'system' }], - }); + const fileArchContent = await this.callModel(context, prompt); const tagContent = parseGenerateTag(fileArchContent); const jsonData = extractJsonFromText(tagContent); - if (jsonData == null) { - this.logger.error('Failed to extract JSON from text'); - throw new RetryableError('Failed to extract JSON from text'); + if (!jsonData) { + this.logger.error('Failed to extract JSON from text.'); + throw new ResponseParsingError('Failed to extract JSON from text.'); } - // Validate the extracted JSON data if (!this.validateJsonData(jsonData)) { - this.logger.error('File architecture JSON validation failed'); - throw new RetryableError('File architecture JSON validation failed'); + this.logger.error('File architecture JSON validation failed.'); + throw new ResponseParsingError('File architecture JSON validation failed.'); } - this.logger.log('File architecture document generated successfully'); + this.logger.log('File architecture document generated successfully.'); return { success: true, data: formatResponse(fileArchContent), }; } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn(`Retryable error encountered: ${error.message}`); - // You can handle retry logic outside of this method - return { - success: false, - error, - }; - } else { - this.logger.error('Non-retryable error encountered:', error); - return { - success: false, - error: new NonRetryableError( - 'Unexpected error during JSON processing', - ), - }; - } + this.handleModelErrors(error, 'generate file architecture'); } } /** - * Validate the structure and content of the JSON data. + * Validates the structure and content of the JSON data. * @param jsonData The JSON data to validate. * @returns A boolean indicating whether the JSON data is valid. */ @@ -92,13 +73,11 @@ export class FileArchGenerateHandler implements BuildHandler { const validPathRegex = /^[a-zA-Z0-9_\-/.]+$/; for (const [file, details] of Object.entries(jsonData.files)) { - // Validate the file path if (!validPathRegex.test(file)) { this.logger.error(`Invalid file path: ${file}`); return false; } - // Validate dependencies for (const dependency of details.dependsOn) { if (!validPathRegex.test(dependency)) { this.logger.error( @@ -107,7 +86,6 @@ export class FileArchGenerateHandler implements BuildHandler { return false; } - // Ensure no double slashes or trailing slashes if (dependency.includes('//') || dependency.endsWith('/')) { this.logger.error( `Malformed dependency path "${dependency}" in file "${file}".`, @@ -118,4 +96,51 @@ export class FileArchGenerateHandler implements BuildHandler { } return true; } + + /** + * Calls the language model to generate file architecture. + * @param context The builder context. + * @param prompt The generated prompt. + */ + private async callModel( + context: BuilderContext, + prompt: string, + ): Promise { + try { + const modelResponse = await context.model.chatSync({ + model: 'gpt-4o-mini', + messages: [{ content: prompt, role: 'system' }], + }); + + if (!modelResponse) { + throw new ModelTimeoutError('The model did not respond within the expected time.'); + } + + return modelResponse; + } catch (error) { + this.handleModelErrors(error, 'call model'); + } + } + + /** + * Handles model-related errors and logs them. + * @param error The error encountered. + * @param stage The stage where the error occurred. + */ + private handleModelErrors(error: any, stage: string): never { + switch (error.name) { + case 'ModelTimeoutError': + this.logger.warn(`Retryable error during ${stage}: ${error.message}`); + throw new ModelTimeoutError(error.message); + case 'TemporaryServiceUnavailableError': + this.logger.warn(`Retryable error during ${stage}: ${error.message}`); + throw new TemporaryServiceUnavailableError(error.message); + case 'RateLimitExceededError': + this.logger.warn(`Retryable error during ${stage}: ${error.message}`); + throw new RateLimitExceededError(error.message); + default: + this.logger.error(`Non-retryable error during ${stage}:`, error); + throw new InvalidParameterError(`Unexpected error during ${stage}.`); + } + } } diff --git a/backend/src/build-system/handlers/file-manager/file-generate/index.ts b/backend/src/build-system/handlers/file-manager/file-generate/index.ts index 7cfd1a61..73ad8134 100644 --- a/backend/src/build-system/handlers/file-manager/file-generate/index.ts +++ b/backend/src/build-system/handlers/file-manager/file-generate/index.ts @@ -9,9 +9,13 @@ import { getProjectPath } from 'src/config/common-path'; import normalizePath from 'normalize-path'; import toposort from 'toposort'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + InvalidParameterError, + ResponseParsingError, + FileWriteError, + ModelTimeoutError, + TemporaryServiceUnavailableError, + RateLimitExceededError, +} from 'src/build-system/errors'; export class FileGeneratorHandler implements BuildHandler { readonly id = 'op:FILE:GENERATE'; @@ -25,52 +29,29 @@ export class FileGeneratorHandler implements BuildHandler { if (!fileArchDoc) { this.logger.error('File architecture document is missing.'); - return { - success: false, - error: new NonRetryableError( - 'Missing required parameter: fileArchDoc.', - ), - }; + throw new InvalidParameterError('Missing required parameter: fileArchDoc.'); } const projectSrcPath = getProjectPath(uuid); try { await this.generateFiles(fileArchDoc, projectSrcPath); - } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn(`Retryable error encountered: ${error.message}`); - return { - success: false, - error, - }; - } - - this.logger.error( - 'Non-retryable error encountered during file generation:', - error, - ); + this.logger.log('All files generated successfully.'); return { - success: false, - error: new NonRetryableError( - 'Failed to generate files and dependencies.', - ), + success: true, + data: 'Files and dependencies created successfully.', }; + } catch (error) { + this.handleErrors(error, 'generate files and dependencies'); } - - return { - success: true, - data: 'Files and dependencies created successfully.', - }; } - async generateFiles( - markdownContent: string, - projectSrcPath: string, - ): Promise { + async generateFiles(markdownContent: string, projectSrcPath: string): Promise { const jsonData = extractJsonFromMarkdown(markdownContent); + if (!jsonData || !jsonData.files) { - throw new RetryableError('Invalid or empty file architecture data.'); + this.logger.error('Invalid or empty file architecture data.'); + throw new ResponseParsingError('Invalid or empty file architecture data.'); } const { graph, nodes } = this.buildDependencyGraph(jsonData); @@ -78,15 +59,15 @@ export class FileGeneratorHandler implements BuildHandler { try { this.detectCycles(graph); } catch (error) { - throw new NonRetryableError( - `Circular dependency detected: ${error.message}`, - ); + this.logger.error('Circular dependency detected.', error); + throw new InvalidParameterError(`Circular dependency detected: ${error.message}`); } try { this.validateAgainstVirtualDirectory(nodes); } catch (error) { - throw new NonRetryableError(error.message); + this.logger.error('Validation against virtual directory failed.', error); + throw new InvalidParameterError(error.message); } const sortedFiles = this.getSortedFiles(graph, nodes); @@ -97,11 +78,9 @@ export class FileGeneratorHandler implements BuildHandler { try { await this.createFile(fullPath); } catch (error) { - throw new RetryableError(`Failed to create file: ${file}`); + throw new FileWriteError(`Failed to create file: ${file}`); } } - - this.logger.log('All files generated successfully.'); } private buildDependencyGraph(jsonData: { @@ -114,7 +93,7 @@ export class FileGeneratorHandler implements BuildHandler { nodes.add(fileName); details.dependsOn.forEach((dep) => { const resolvedDep = this.resolveDependency(fileName, dep); - graph.push([resolvedDep, fileName]); // [dependency, dependent] + graph.push([resolvedDep, fileName]); nodes.add(resolvedDep); }); }); @@ -127,18 +106,13 @@ export class FileGeneratorHandler implements BuildHandler { toposort(graph); } catch (error) { if (error.message.includes('cycle')) { - throw new Error( - `Circular dependency detected in the file structure: ${error.message}`, - ); + throw new InvalidParameterError(`Circular dependency detected: ${error.message}`); } throw error; } } - private getSortedFiles( - graph: [string, string][], - nodes: Set, - ): string[] { + private getSortedFiles(graph: [string, string][], nodes: Set): string[] { const sortedFiles = toposort(graph).reverse(); Array.from(nodes).forEach((node) => { @@ -173,7 +147,7 @@ export class FileGeneratorHandler implements BuildHandler { }); if (invalidFiles.length > 0) { - throw new Error( + throw new InvalidParameterError( `The following files do not exist in the project structure:\n${invalidFiles.join('\n')}`, ); } @@ -181,12 +155,23 @@ export class FileGeneratorHandler implements BuildHandler { private async createFile(filePath: string): Promise { const dir = path.dirname(filePath); - await fs.mkdir(dir, { recursive: true }); const content = `// Generated file: ${path.basename(filePath)}`; await fs.writeFile(filePath, content, 'utf8'); - this.logger.log(`File created: ${filePath}`); } + + private handleErrors(error: any, stage: string): never { + switch (error.name) { + case 'ModelTimeoutError': + case 'TemporaryServiceUnavailableError': + case 'RateLimitExceededError': + this.logger.warn(`Retryable error during ${stage}: ${error.message}`); + throw error; + default: + this.logger.error(`Non-retryable error during ${stage}:`, error); + throw new InvalidParameterError(`Unexpected error during ${stage}.`); + } + } } diff --git a/backend/src/build-system/handlers/file-manager/file-structure/index.ts b/backend/src/build-system/handlers/file-manager/file-structure/index.ts index dc4be250..37855afd 100644 --- a/backend/src/build-system/handlers/file-manager/file-structure/index.ts +++ b/backend/src/build-system/handlers/file-manager/file-structure/index.ts @@ -9,9 +9,9 @@ import { prompts } from './prompt'; import { Logger } from '@nestjs/common'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + ResponseParsingError, + MissingConfigurationError, +} from 'src/build-system/errors'; /** * FileStructureHandler is responsible for generating the project's file and folder structure @@ -30,48 +30,13 @@ export class FileStructureHandler implements BuildHandler { // Retrieve projectName from context const projectName = context.getGlobalContext('projectName') || 'Default Project Name'; - this.logger.debug(`Project Name: ${projectName}`); - const sitemapDoc = context.getNodeData('op:UX:SMD'); const datamapDoc = context.getNodeData('op:UX:DATAMAP:DOC'); const projectPart = opts?.projectPart ?? 'frontend'; const framework = context.getGlobalContext('framework') ?? 'react'; // Validate required arguments - if (!sitemapDoc || typeof sitemapDoc !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid sitemapDoc.'), - }; - } - if (!datamapDoc || typeof datamapDoc !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid datamapDoc.'), - }; - } - if (!framework || typeof framework !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid framework.'), - }; - } - if ( - !projectPart || - (projectPart !== 'frontend' && projectPart !== 'backend') - ) { - return { - success: false, - error: new NonRetryableError( - 'Invalid projectPart. Must be either "frontend" or "backend".', - ), - }; - } - - this.logger.debug(`Project Part: ${projectPart}`); - this.logger.debug( - 'Sitemap Documentation and Data Analysis Document are provided.', - ); + this.validateInputs(sitemapDoc, datamapDoc, framework, projectPart); // Generate the common file structure prompt const prompt = prompts.generateCommonFileStructurePrompt( @@ -84,99 +49,38 @@ export class FileStructureHandler implements BuildHandler { let fileStructureContent: string; try { - fileStructureContent = await context.model.chatSync({ - model: 'gpt-4o-mini', - messages: [{ content: prompt, role: 'system' }], - }); - - if (!fileStructureContent || fileStructureContent.trim() === '') { - throw new RetryableError('Generated file structure content is empty.'); - } + fileStructureContent = await this.callModel(context, prompt, 'file structure'); } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during file structure generation: ${error.message}`, - ); - return { success: false, error }; - } - - this.logger.error( - 'Non-retryable error during file structure generation:', - error, - ); - return { - success: false, - error: new NonRetryableError('Failed to generate file structure.'), - }; + return { success: false, error }; } - this.logger.debug('Generated file structure content.'); - - // Convert the tree structure to JSON using the appropriate prompt - const convertToJsonPrompt = - prompts.convertTreeToJsonPrompt(fileStructureContent); + // Convert the tree structure to JSON + const convertToJsonPrompt = prompts.convertTreeToJsonPrompt(fileStructureContent); let fileStructureJsonContent: string; try { - fileStructureJsonContent = await context.model.chatSync({ - model: 'gpt-4o-mini', - messages: [{ content: convertToJsonPrompt, role: 'system' }], - }); - - if (!fileStructureJsonContent || fileStructureJsonContent.trim() === '') { - throw new RetryableError('Failed to convert file structure to JSON.'); - } + fileStructureJsonContent = await this.callModel(context, convertToJsonPrompt, 'tree-to-JSON conversion'); } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during tree-to-JSON conversion: ${error.message}`, - ); - return { success: false, error }; - } - - this.logger.error( - 'Non-retryable error during tree-to-JSON conversion:', - error, - ); - return { - success: false, - error: new NonRetryableError( - 'Failed to convert file structure to JSON.', - ), - }; + return { success: false, error }; } - this.logger.debug('Converted file structure to JSON.'); - // Build the virtual directory try { const successBuild = context.buildVirtualDirectory( fileStructureJsonContent, ); if (!successBuild) { - throw new RetryableError('Failed to build virtual directory.'); + throw new ResponseParsingError('Failed to build virtual directory.'); } } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during virtual directory build: ${error.message}`, - ); - return { success: false, error }; - } - - this.logger.error( - 'Non-retryable error during virtual directory build:', - error, - ); + this.logger.error('Non-retryable error during virtual directory build:', error); return { success: false, - error: new NonRetryableError('Failed to build virtual directory.'), + error: new ResponseParsingError('Failed to build virtual directory.'), }; } - this.logger.log( - 'File structure JSON content and virtual directory built successfully.', - ); + this.logger.log('File structure JSON content and virtual directory built successfully.'); return { success: true, @@ -186,4 +90,59 @@ export class FileStructureHandler implements BuildHandler { }, }; } + + private validateInputs( + sitemapDoc: any, + datamapDoc: any, + framework: string, + projectPart: string, + ): void { + if (!sitemapDoc || typeof sitemapDoc !== 'string') { + throw new MissingConfigurationError('Missing or invalid sitemapDoc.'); + } + if (!datamapDoc || typeof datamapDoc !== 'string') { + throw new MissingConfigurationError('Missing or invalid datamapDoc.'); + } + if (!framework || typeof framework !== 'string') { + throw new MissingConfigurationError('Missing or invalid framework.'); + } + if (!['frontend', 'backend'].includes(projectPart)) { + throw new MissingConfigurationError('Invalid projectPart. Must be either "frontend" or "backend".'); + } + } + + private async callModel( + context: BuilderContext, + prompt: string, + stage: string, + ): Promise { + try { + const response = await context.model.chatSync({ + model: 'gpt-4o-mini', + messages: [{ content: prompt, role: 'system' }], + }); + + if (!response || response.trim() === '') { + throw new ResponseParsingError(`Generated content is empty during ${stage}.`); + } + + return response; + } catch (error) { + this.handleErrors(error, stage); + throw error; + } + } + + private handleErrors(error: any, stage: string): void { + switch (error.name) { + case 'ModelTimeoutError': + case 'TemporaryServiceUnavailableError': + case 'RateLimitExceededError': + this.logger.error(`Error during ${stage}: ${error.message}`); + throw new Error(`Unexpected error during ${stage}.`); + default: + this.logger.error(`Non-retryable error during ${stage}:`, error); + throw new ResponseParsingError(`Error during ${stage}: ${error.message}`); + } + } } diff --git a/backend/src/build-system/handlers/product-manager/product-requirements-document/prd.ts b/backend/src/build-system/handlers/product-manager/product-requirements-document/prd.ts index b0c74aa6..6f5115a5 100644 --- a/backend/src/build-system/handlers/product-manager/product-requirements-document/prd.ts +++ b/backend/src/build-system/handlers/product-manager/product-requirements-document/prd.ts @@ -5,9 +5,9 @@ import { ModelProvider } from 'src/common/model-provider'; import { Logger } from '@nestjs/common'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + MissingConfigurationError, + ResponseParsingError, +} from 'src/build-system/errors'; export class PRDHandler implements BuildHandler { readonly id = 'op:PRD'; @@ -25,22 +25,13 @@ export class PRDHandler implements BuildHandler { // Validate extracted data if (!projectName || typeof projectName !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid projectName.'), - }; + throw new MissingConfigurationError('Missing or invalid projectName.'); } if (!description || typeof description !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid description.'), - }; + throw new MissingConfigurationError('Missing or invalid description.'); } if (!platform || typeof platform !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid platform.'), - }; + throw new MissingConfigurationError('Missing or invalid platform.'); } // Generate the prompt dynamically @@ -55,7 +46,7 @@ export class PRDHandler implements BuildHandler { const prdContent = await this.generatePRDFromLLM(prompt); if (!prdContent || prdContent.trim() === '') { - throw new RetryableError('Generated PRD content is empty.'); + throw new ResponseParsingError('Generated PRD content is empty.'); } return { @@ -63,21 +54,8 @@ export class PRDHandler implements BuildHandler { data: removeCodeBlockFences(prdContent), }; } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during PRD generation: ${error.message}`, - ); - return { - success: false, - error, - }; - } - - this.logger.error('Non-retryable error during PRD generation:', error); - return { - success: false, - error: new NonRetryableError('Failed to generate PRD.'), - }; + this.logger.error('Error during PRD generation:', error); + throw new ResponseParsingError('Failed to generate PRD.'); } } @@ -89,11 +67,23 @@ export class PRDHandler implements BuildHandler { messages: [{ content: prompt, role: 'system' }], }); + if (!prdContent || prdContent.trim() === '') { + throw new ResponseParsingError('LLM server returned empty PRD content.'); + } + this.logger.log('Received full PRD content from LLM server.'); return prdContent; } catch (error) { - this.logger.error('Error communicating with the LLM server:', error); - throw new RetryableError('Failed to communicate with the LLM server.'); + if (error.message.includes('timeout')) { + this.logger.error('Timeout error communicating with the LLM server.'); + throw new ResponseParsingError('Timeout occurred while communicating with the LLM server.'); + } + if (error.message.includes('service unavailable')) { + this.logger.error('LLM server is temporarily unavailable.'); + throw new ResponseParsingError('LLM server is temporarily unavailable.'); + } + this.logger.error('Unexpected error communicating with the LLM server:', error); + throw new ResponseParsingError('Unexpected error during communication with the LLM server.'); } } } diff --git a/backend/src/build-system/handlers/ux/datamap/index.ts b/backend/src/build-system/handlers/ux/datamap/index.ts index f5b8cb9b..1950fd19 100644 --- a/backend/src/build-system/handlers/ux/datamap/index.ts +++ b/backend/src/build-system/handlers/ux/datamap/index.ts @@ -5,9 +5,9 @@ import { prompts } from './prompt'; import { Logger } from '@nestjs/common'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + MissingConfigurationError, + ResponseParsingError, +} from 'src/build-system/errors'; /** * Handler for generating the UX Data Map document. @@ -26,16 +26,10 @@ export class UXDatamapHandler implements BuildHandler { // Validate required data if (!projectName || typeof projectName !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid projectName.'), - }; + throw new MissingConfigurationError('Missing or invalid projectName.'); } if (!sitemapDoc || typeof sitemapDoc !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid sitemapDoc.'), - }; + throw new MissingConfigurationError('Missing or invalid sitemapDoc.'); } // Generate the prompt @@ -53,7 +47,7 @@ export class UXDatamapHandler implements BuildHandler { }); if (!uxDatamapContent || uxDatamapContent.trim() === '') { - throw new RetryableError('Generated UX Data Map content is empty.'); + throw new ResponseParsingError('Generated UX Data Map content is empty.'); } this.logger.log('Successfully generated UX Data Map content.'); @@ -62,26 +56,21 @@ export class UXDatamapHandler implements BuildHandler { data: removeCodeBlockFences(uxDatamapContent), }; } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during UX Data Map generation: ${error.message}`, - ); - return { - success: false, - error, - }; + if (error.message.includes('timeout')) { + this.logger.error('Timeout occurred while communicating with the model.'); + throw new ResponseParsingError('Timeout occurred while generating UX Data Map.'); + } + if (error.message.includes('service unavailable')) { + this.logger.error('Model service is temporarily unavailable.'); + throw new ResponseParsingError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + this.logger.error('Rate limit exceeded during model communication.'); + throw new ResponseParsingError('Rate limit exceeded while generating UX Data Map.'); } - this.logger.error( - 'Non-retryable error during UX Data Map generation:', - error, - ); - return { - success: false, - error: new NonRetryableError( - 'Failed to generate UX Data Map document.', - ), - }; + this.logger.error('Unexpected error during UX Data Map generation:', error); + throw new ResponseParsingError('Unexpected error during UX Data Map generation.'); } } } diff --git a/backend/src/build-system/handlers/ux/sitemap-document/uxsmd.ts b/backend/src/build-system/handlers/ux/sitemap-document/uxsmd.ts index f2914e54..330da039 100644 --- a/backend/src/build-system/handlers/ux/sitemap-document/uxsmd.ts +++ b/backend/src/build-system/handlers/ux/sitemap-document/uxsmd.ts @@ -5,9 +5,9 @@ import { ModelProvider } from 'src/common/model-provider'; import { Logger } from '@nestjs/common'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + MissingConfigurationError, + ResponseParsingError, +} from 'src/build-system/errors'; export class UXSMDHandler implements BuildHandler { readonly id = 'op:UX:SMD'; @@ -24,37 +24,25 @@ export class UXSMDHandler implements BuildHandler { // Validate required data if (!projectName || typeof projectName !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid projectName.'), - }; + throw new MissingConfigurationError('Missing or invalid projectName.'); } if (!platform || typeof platform !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid platform.'), - }; + throw new MissingConfigurationError('Missing or invalid platform.'); } if (!prdContent || typeof prdContent !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid PRD content.'), - }; + throw new MissingConfigurationError('Missing or invalid PRD content.'); } // Generate the prompt dynamically - const prompt = prompts.generateUxsmdrompt( - projectName, - prdContent, - platform, - ); + const prompt = prompts.generateUxsmdrompt(projectName, prdContent, platform); try { // Generate UXSMD content using the language model const uxsmdContent = await this.generateUXSMDFromLLM(prompt); if (!uxsmdContent || uxsmdContent.trim() === '') { - throw new RetryableError('Generated UXSMD content is empty.'); + this.logger.error('Generated UXSMD content is empty.'); + throw new ResponseParsingError('Generated UXSMD content is empty.'); } // Store the generated document in the context @@ -66,21 +54,19 @@ export class UXSMDHandler implements BuildHandler { data: removeCodeBlockFences(uxsmdContent), }; } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during UXSMD generation: ${error.message}`, - ); - return { - success: false, - error, - }; + this.logger.error('Error during UXSMD generation:', error); + + if (error.message.includes('timeout')) { + throw new ResponseParsingError('Timeout occurred while generating UXSMD.'); + } + if (error.message.includes('service unavailable')) { + throw new ResponseParsingError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + throw new ResponseParsingError('Rate limit exceeded while generating UXSMD.'); } - this.logger.error('Non-retryable error during UXSMD generation:', error); - return { - success: false, - error: new NonRetryableError('Failed to generate UXSMD document.'), - }; + throw new ResponseParsingError('Unexpected error during UXSMD generation.'); } } @@ -97,8 +83,18 @@ export class UXSMDHandler implements BuildHandler { this.logger.log('Received full UXSMD content from LLM server.'); return uxsmdContent; } catch (error) { - this.logger.error('Error communicating with the LLM server:', error); - throw new RetryableError('Failed to communicate with the LLM server.'); + if (error.message.includes('timeout')) { + throw new ResponseParsingError('Timeout occurred while communicating with the model.'); + } + if (error.message.includes('service unavailable')) { + throw new ResponseParsingError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + throw new ResponseParsingError('Rate limit exceeded while communicating with the model.'); + } + + this.logger.error('Unexpected error communicating with the LLM server:', error); + throw new ResponseParsingError('Failed to communicate with the LLM server.'); } } } diff --git a/backend/src/build-system/handlers/ux/sitemap-structure/index.ts b/backend/src/build-system/handlers/ux/sitemap-structure/index.ts index bf64c891..e1e32851 100644 --- a/backend/src/build-system/handlers/ux/sitemap-structure/index.ts +++ b/backend/src/build-system/handlers/ux/sitemap-structure/index.ts @@ -5,9 +5,9 @@ import { prompts } from './prompt'; import { Logger } from '@nestjs/common'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + MissingConfigurationError, + ResponseParsingError, +} from 'src/build-system/errors'; // UXSMS: UX Sitemap Structure export class UXSitemapStructureHandler implements BuildHandler { @@ -24,16 +24,10 @@ export class UXSitemapStructureHandler implements BuildHandler { // Validate required parameters if (!projectName || typeof projectName !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid projectName.'), - }; + throw new MissingConfigurationError('Missing or invalid projectName.'); } if (!sitemapDoc || typeof sitemapDoc !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid sitemap document.'), - }; + throw new MissingConfigurationError('Missing or invalid sitemap document.'); } // Generate the prompt dynamically @@ -51,7 +45,8 @@ export class UXSitemapStructureHandler implements BuildHandler { }); if (!uxStructureContent || uxStructureContent.trim() === '') { - throw new RetryableError( + this.logger.error('Generated UX Sitemap Structure content is empty.'); + throw new ResponseParsingError( 'Generated UX Sitemap Structure content is empty.', ); } @@ -62,26 +57,22 @@ export class UXSitemapStructureHandler implements BuildHandler { data: removeCodeBlockFences(uxStructureContent), }; } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during UX Sitemap Structure generation: ${error.message}`, - ); - return { - success: false, - error, - }; - } - this.logger.error( - 'Non-retryable error during UX Sitemap Structure generation:', + 'Error during UX Sitemap Structure generation:', error, ); - return { - success: false, - error: new NonRetryableError( - 'Failed to generate UX Sitemap Structure document.', - ), - }; + + if (error.message.includes('timeout')) { + throw new ResponseParsingError('Timeout occurred while generating UX Sitemap Structure.'); + } + if (error.message.includes('service unavailable')) { + throw new ResponseParsingError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + throw new ResponseParsingError('Rate limit exceeded while generating UX Sitemap Structure.'); + } + + throw new ResponseParsingError('Unexpected error during UX Sitemap Structure generation.'); } } } diff --git a/backend/src/build-system/handlers/ux/sitemap-structure/sms-page.ts b/backend/src/build-system/handlers/ux/sitemap-structure/sms-page.ts index 955904c4..8281a748 100644 --- a/backend/src/build-system/handlers/ux/sitemap-structure/sms-page.ts +++ b/backend/src/build-system/handlers/ux/sitemap-structure/sms-page.ts @@ -5,9 +5,9 @@ import { ModelProvider } from 'src/common/model-provider'; import { prompts } from './prompt'; import { removeCodeBlockFences } from 'src/build-system/utils/strings'; import { - RetryableError, - NonRetryableError, -} from 'src/build-system/retry-handler'; + MissingConfigurationError, + ResponseParsingError, +} from 'src/build-system/errors'; export class Level2UXSitemapStructureHandler implements BuildHandler { readonly id = 'op:UX:SMS:LEVEL2'; @@ -23,47 +23,29 @@ export class Level2UXSitemapStructureHandler implements BuildHandler { // Validate required data if (!projectName || typeof projectName !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid projectName.'), - }; + throw new MissingConfigurationError('Missing or invalid projectName.'); } if (!sitemapDoc || typeof sitemapDoc !== 'string') { - return { - success: false, - error: new NonRetryableError('Missing or invalid sitemap document.'), - }; + throw new MissingConfigurationError('Missing or invalid sitemap document.'); } if (!uxStructureDoc || typeof uxStructureDoc !== 'string') { - return { - success: false, - error: new NonRetryableError( - 'Missing or invalid UX Structure document.', - ), - }; + throw new MissingConfigurationError('Missing or invalid UX Structure document.'); } // Extract sections from the UX Structure Document const sections = this.extractAllSections(uxStructureDoc); if (sections.length === 0) { - this.logger.error( - 'No valid sections found in the UX Structure Document.', - ); - return { - success: false, - error: new NonRetryableError( - 'No valid sections found in the UX Structure Document.', - ), - }; + this.logger.error('No valid sections found in the UX Structure Document.'); + throw new ResponseParsingError('No valid sections found in the UX Structure Document.'); } // Process each section with the refined Level 2 prompt const modelProvider = ModelProvider.getInstance(); const refinedSections = []; - try { - for (const section of sections) { + for (const section of sections) { + try { const prompt = prompts.generateLevel2UXSiteMapStructrePrompt( projectName, section.content, @@ -77,7 +59,8 @@ export class Level2UXSitemapStructureHandler implements BuildHandler { }); if (!refinedContent || refinedContent.trim() === '') { - throw new RetryableError( + this.logger.error(`Generated content for section "${section.title}" is empty.`); + throw new ResponseParsingError( `Generated content for section "${section.title}" is empty.`, ); } @@ -86,28 +69,28 @@ export class Level2UXSitemapStructureHandler implements BuildHandler { title: section.title, content: refinedContent, }); - } - } catch (error) { - if (error instanceof RetryableError) { - this.logger.warn( - `Retryable error during section refinement: ${error.message}`, - ); - return { - success: false, + } catch (error) { + if (error.message.includes('timeout')) { + this.logger.warn(`Timeout error during section refinement: ${error.message}`); + throw new ResponseParsingError('Timeout occurred while refining sections.'); + } + if (error.message.includes('service unavailable')) { + this.logger.warn(`Service unavailable during section refinement: ${error.message}`); + throw new ResponseParsingError('Model service is temporarily unavailable.'); + } + if (error.message.includes('rate limit')) { + this.logger.warn(`Rate limit exceeded during section refinement: ${error.message}`); + throw new ResponseParsingError('Rate limit exceeded while refining sections.'); + } + + this.logger.error( + `Unexpected error during section refinement for "${section.title}":`, error, - }; + ); + throw new ResponseParsingError( + `Unexpected error during section refinement for "${section.title}".`, + ); } - - this.logger.error( - 'Non-retryable error during section refinement:', - error, - ); - return { - success: false, - error: new NonRetryableError( - 'Failed to refine sections in the UX Sitemap Structure.', - ), - }; } // Combine the refined sections into the final document @@ -115,9 +98,7 @@ export class Level2UXSitemapStructureHandler implements BuildHandler { .map((section) => `## **${section.title}**\n${section.content}`) .join('\n\n'); - this.logger.log( - 'Successfully generated Level 2 UX Sitemap Structure document.', - ); + this.logger.log('Successfully generated Level 2 UX Sitemap Structure document.'); return { success: true, diff --git a/backend/src/build-system/retry-handler.ts b/backend/src/build-system/retry-handler.ts index 05555aaa..c6c320af 100644 --- a/backend/src/build-system/retry-handler.ts +++ b/backend/src/build-system/retry-handler.ts @@ -1,50 +1,51 @@ import { Logger } from '@nestjs/common'; -export class RetryableError extends Error { - constructor(message: string) { - super(message); - this.name = 'RetryableError'; - } -} - -export class NonRetryableError extends Error { - constructor(message: string) { - super(message); - this.name = 'NonRetryableError'; - } -} export class RetryHandler { private readonly logger = new Logger(RetryHandler.name); private static instance: RetryHandler; private retryCounts: Map = new Map(); + private readonly MAX_RETRY_COUNT = 3; public static getInstance() { if (this.instance) return this.instance; return new RetryHandler(); } + + private async addRetryDelay(retryCount: number): Promise { + const delay = (retryCount + 1) * 1000; // Exponential backoff: 1s, 2s, 3s + return new Promise((resolve) => setTimeout(resolve, delay)); + } + + async retryMethod( error: Error, - upperMethod: (...args: any[]) => void, + upperMethod: (...args: any[]) => Promise, args: any[], ): Promise { const errorName = error.name; let retryCount = this.retryCounts.get(errorName) || 0; const isRetrying = this.retryCounts.has(errorName); let res; - switch (errorName) { - case 'RetryableError': - case 'GeneratedTagError': - case 'TimeoutError': + switch (errorName) { + case 'ModelTimeoutError': + case 'TemporaryServiceUnavailableError': + case 'RateLimitExceededError': + case 'ResponseTagError': + case 'ParsingError': + // Optionally add a delay between retries this.logger.warn( `Retryable error occurred: ${error.message}. Retrying...`, ); if (!isRetrying) this.retryCounts.set(errorName, 0); else this.retryCounts.set(errorName, this.retryCounts.get(errorName) + 1); - while (retryCount < 3) { + while (retryCount < this.MAX_RETRY_COUNT) { try { + + await this.addRetryDelay(retryCount); this.logger.log(`retryCount: ${retryCount}`); res = await upperMethod(...args); + } catch (e) { if (e.name === errorName) { retryCount++;