Skip to content

Commit

Permalink
feat: support detail error in handler
Browse files Browse the repository at this point in the history
  • Loading branch information
NarwhalChen committed Jan 13, 2025
1 parent 317c690 commit bb7b3d7
Show file tree
Hide file tree
Showing 15 changed files with 807 additions and 712 deletions.
86 changes: 86 additions & 0 deletions backend/src/build-system/errors.ts
Original file line number Diff line number Diff line change
@@ -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';
}
}

127 changes: 83 additions & 44 deletions backend/src/build-system/handlers/backend/code-generate/index.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,57 @@
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<string> {
readonly id = 'op:BACKEND:CODE';
readonly logger: Logger = new Logger('BackendCodeHandler');

/**
* Executes the handler to generate backend code.
* @param context - The builder context containing configuration and utilities.
* @returns A BuildResult containing the generated code and related data.
*/
async run(context: BuilderContext): Promise<BuildResult<string>> {
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';

Expand All @@ -63,43 +68,77 @@ export class BackendCodeHandler implements BuildHandler<string> {
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<string> {
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}`);
}
}
}
Loading

0 comments on commit bb7b3d7

Please sign in to comment.