From e7fc152e75cc2ba4b116a797748361eddaeed880 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sat, 16 Nov 2024 21:34:31 -0600 Subject: [PATCH 01/10] feat: Add uuid npm dependency for unique identifier generation --- backend/package.json | 3 +- backend/src/build-system/context.ts | 37 ++++++++++++++----- backend/src/build-system/executor.ts | 34 ++++++++++++----- .../node/product_requirements_document/prd.ts | 26 +++---------- backend/src/build-system/types.ts | 11 +++++- backend/src/common/model-provider/index.ts | 31 +++++++++------- 6 files changed, 87 insertions(+), 55 deletions(-) diff --git a/backend/package.json b/backend/package.json index 92ae3f90..f78855f7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,7 +46,8 @@ "rxjs": "^7.8.1", "sqlite3": "^5.1.7", "subscriptions-transport-ws": "^0.11.0", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "uuid": "^10.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", diff --git a/backend/src/build-system/context.ts b/backend/src/build-system/context.ts index 2d00c198..fda18c8b 100644 --- a/backend/src/build-system/context.ts +++ b/backend/src/build-system/context.ts @@ -1,11 +1,12 @@ +import { ModelProvider } from 'src/common/model-provider'; import { BuildHandlerManager } from './hanlder-manager'; import { BuildExecutionState, BuildNode, BuildResult, BuildSequence, - BuildStep, } from './types'; +import { Logger } from '@nestjs/common'; export class BuilderContext { private state: BuildExecutionState = { @@ -14,12 +15,19 @@ export class BuilderContext { failed: new Set(), waiting: new Set(), }; - + private logger; private data: Record = {}; + // Store the results of the nodes + private results: Map = new Map(); private handlerManager: BuildHandlerManager; + public model: ModelProvider; - constructor(private sequence: BuildSequence) { + constructor( + private sequence: BuildSequence, + id: string, + ) { this.handlerManager = BuildHandlerManager.getInstance(); + new Logger(`builder-context-${id}`); } canExecute(nodeId: string): boolean { @@ -41,7 +49,7 @@ export class BuilderContext { return null; } - async run(nodeId: string): Promise { + async run(nodeId: string, args: unknown | undefined): Promise { const node = this.findNode(nodeId); if (!node) { throw new Error(`Node not found: ${nodeId}`); @@ -53,9 +61,13 @@ export class BuilderContext { try { this.state.pending.add(nodeId); - const result = await this.executeNode(node); + const result = await this.executeNode(node, args); this.state.completed.add(nodeId); this.state.pending.delete(nodeId); + + // Store the result for future use + this.results.set(nodeId, result); + return result; } catch (error) { this.state.failed.add(nodeId); @@ -76,17 +88,24 @@ export class BuilderContext { return this.data[key]; } - private async executeNode(node: BuildNode): Promise { + getResult(nodeId: string): BuildResult | undefined { + return this.results.get(nodeId); + } + + private async executeNode( + node: BuildNode, + args: unknown, + ): Promise { if (process.env.NODE_ENV === 'test') { - console.log(`[TEST] Executing node: ${node.id}`); + this.logger.log(`[TEST] Executing node: ${node.id}`); return { success: true, data: { nodeId: node.id } }; } - console.log(`Executing node: ${node.id}`); + this.logger.log(`Executing node: ${node.id}`); const handler = this.handlerManager.getHandler(node.id); if (!handler) { throw new Error(`No handler found for node: ${node.id}`); } - return handler.run(this); + return handler.run(this, args); } } diff --git a/backend/src/build-system/executor.ts b/backend/src/build-system/executor.ts index adb7a67f..edd252ef 100644 --- a/backend/src/build-system/executor.ts +++ b/backend/src/build-system/executor.ts @@ -1,9 +1,12 @@ +import { Logger } from '@nestjs/common'; import { BuilderContext } from './context'; import { BuildNode, BuildSequence, BuildStep } from './types'; +import { v4 as uuidv4 } from 'uuid'; export class BuildSequenceExecutor { constructor(private context: BuilderContext) {} + private logger: Logger = new Logger(`BuildSequenceExecutor-${uuidv4()}`); private async executeNode(node: BuildNode): Promise { try { if (this.context.getState().completed.has(node.id)) { @@ -11,20 +14,31 @@ export class BuildSequenceExecutor { } if (!this.context.canExecute(node.id)) { - console.log(`Waiting for dependencies: ${node.requires?.join(', ')}`); - await new Promise((resolve) => setTimeout(resolve, 100)); // 添加小延迟 + this.logger.log( + `Waiting for dependencies: ${node.requires?.join(', ')}`, + ); + await new Promise((resolve) => setTimeout(resolve, 100)); return; } - await this.context.run(node.id); + const dependenciesResults = node.requires?.map((depId) => + this.context.getResult(depId), + ); + + this.logger.log( + `Executing node ${node.id} with dependencies:`, + dependenciesResults, + ); + + await this.context.run(node.id, dependenciesResults); } catch (error) { - console.error(`Error executing node ${node.id}:`, error); + this.logger.error(`Error executing node ${node.id}:`, error); throw error; } } private async executeStep(step: BuildStep): Promise { - console.log(`Executing build step: ${step.id}`); + this.logger.log(`Executing build step: ${step.id}`); if (step.parallel) { let remainingNodes = [...step.nodes]; @@ -84,7 +98,7 @@ export class BuildSequenceExecutor { if (!this.context.getState().completed.has(node.id)) { // TODO: change to error log - console.warn( + this.logger.warn( `Failed to execute node ${node.id} after ${maxRetries} attempts`, ); } @@ -93,7 +107,7 @@ export class BuildSequenceExecutor { } async executeSequence(sequence: BuildSequence): Promise { - console.log(`Starting build sequence: ${sequence.id}`); + this.logger.log(`Starting build sequence: ${sequence.id}`); for (const step of sequence.steps) { await this.executeStep(step); @@ -104,7 +118,7 @@ export class BuildSequenceExecutor { if (incompletedNodes.length > 0) { // TODO: change to error log - console.warn( + this.logger.warn( `Step ${step.id} failed to complete nodes: ${incompletedNodes .map((n) => n.id) .join(', ')}`, @@ -113,7 +127,7 @@ export class BuildSequenceExecutor { } } - console.log(`Build sequence completed: ${sequence.id}`); - console.log('Final state:', this.context.getState()); + this.logger.log(`Build sequence completed: ${sequence.id}`); + this.logger.log('Final state:', this.context.getState()); } } diff --git a/backend/src/build-system/node/product_requirements_document/prd.ts b/backend/src/build-system/node/product_requirements_document/prd.ts index a67e7f0d..e65052f2 100644 --- a/backend/src/build-system/node/product_requirements_document/prd.ts +++ b/backend/src/build-system/node/product_requirements_document/prd.ts @@ -2,15 +2,13 @@ import { BuildHandler, BuildResult } from 'src/build-system/types'; import { BuilderContext } from 'src/build-system/context'; import { prompts } from './prompt/prompt'; import { ModelProvider } from 'src/common/model-provider'; -import { StreamStatus } from 'src/chat/chat.model'; -import * as fs from 'fs'; -import * as path from 'path'; +import { Logger } from '@nestjs/common'; export class PRDHandler implements BuildHandler { readonly id = 'op:PRD::STATE:GENERATE'; - + readonly logger: Logger = new Logger('PRDHandler'); async run(context: BuilderContext): Promise { - console.log('Generating PRD...'); + this.logger.log('Generating PRD...'); // Extract project data from the context const projectName = @@ -28,9 +26,6 @@ export class PRDHandler implements BuildHandler { // Send the prompt to the LLM server and process the response const prdContent = await this.generatePRDFromLLM(prompt); - // Save the PRD content to context for further use - context.setData('prdDocument', prdContent); - return { success: true, data: prdContent, @@ -39,20 +34,9 @@ export class PRDHandler implements BuildHandler { private async generatePRDFromLLM(prompt: string): Promise { const modelProvider = ModelProvider.getInstance(); - const model = 'gpt-3.5-turbo'; - - // Call the chat method with the model specified - const chatStream = modelProvider.chat( - { - content: prompt, - }, - model, - ); // Pass the model here - - const prdContent = modelProvider.chunkSync(chatStream); - - console.log('Received full PRD content from LLM server.'); + const prdContent = await modelProvider.chatSync({ content: prompt }, model); + this.logger.log('Received full PRD content from LLM server.'); return prdContent; } } diff --git a/backend/src/build-system/types.ts b/backend/src/build-system/types.ts index 47a88bc3..cbf7bc77 100644 --- a/backend/src/build-system/types.ts +++ b/backend/src/build-system/types.ts @@ -1,3 +1,4 @@ +import { ModelProvider } from 'src/common/model-provider'; import { BuilderContext } from './context'; export type BuildNodeType = @@ -74,6 +75,14 @@ export interface BuildExecutionState { } export interface BuildHandler { + // Unique identifier for the handler id: string; - run(context: BuilderContext): Promise; + + /** + * + * @param context the context object for the build + * @param model model provider for the build + * @param args the request arguments + */ + run(context: BuilderContext, args: unknown): Promise; } diff --git a/backend/src/common/model-provider/index.ts b/backend/src/common/model-provider/index.ts index c080a00f..1f7f6b4b 100644 --- a/backend/src/common/model-provider/index.ts +++ b/backend/src/common/model-provider/index.ts @@ -49,6 +49,24 @@ export class ModelProvider { private readonly config: ModelProviderConfig, ) {} + async chatSync( + input: ChatInput | string, + model: string, + chatId?: string, + ): Promise { + const chatStream = this.chat(input, model, chatId); + let content = ''; + for await (const chunk of chatStream) { + if (chunk.status === StreamStatus.STREAMING) { + content += chunk.choices + .map((choice) => choice.delta?.content || '') + .join(''); + } + } + this.logger.log('Aggregated content from chat stream:', content); + return content; + } + chat( input: ChatInput | string, model: string, @@ -199,19 +217,6 @@ export class ModelProvider { } } - async chunkSync(chatStream: AsyncIterableIterator): Promise { - let aggregatedContent = ''; - for await (const chunk of chatStream) { - if (chunk.status === StreamStatus.STREAMING) { - aggregatedContent += chunk.choices - .map((choice) => choice.delta?.content || '') - .join(''); - } - } - this.logger.log('Aggregated content from chat stream:', aggregatedContent); - return aggregatedContent; - } - private startChat(input: ChatInput, model: string, chatId?: string) { const payload = this.createRequestPayload(input, model, chatId); From 4a6ab5fc3ee0a9211aee3cb1a1ddd011e32afbbf Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sat, 16 Nov 2024 21:44:01 -0600 Subject: [PATCH 02/10] feat: Add UX Data Map document generation functionality This commit adds the necessary code changes to generate a UX Data Map document. The `UXDatamapHandler` class is introduced, which implements the `BuildHandler` interface. It includes a `run` method that generates the UX Data Map document based on the provided context data. The `UXDatamapHandler` class uses the `prompts.generateUXDataMapPrompt` function to dynamically generate the UX Data Map prompt. The prompt includes information such as the project name, sitemap documentation, and platform. The generated UX Data Map document is stored in the context using the `setData` method, with the key `uxDatamapDocument`. This feature enhances the project builder functionality by providing a way to generate a UX Data Map document, which helps in analyzing data requirements from a user experience perspective. Co-authored-by: Jackson Chen <541898146chen@gmail.com> --- backend/src/build-system/context.ts | 15 +- .../src/build-system/node/ux-datamap/index.ts | 40 +++++ .../build-system/node/ux-datamap/prompt.ts | 140 ++++++++++++++++++ 3 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 backend/src/build-system/node/ux-datamap/index.ts create mode 100644 backend/src/build-system/node/ux-datamap/prompt.ts diff --git a/backend/src/build-system/context.ts b/backend/src/build-system/context.ts index fda18c8b..606daa42 100644 --- a/backend/src/build-system/context.ts +++ b/backend/src/build-system/context.ts @@ -8,6 +8,11 @@ import { } from './types'; import { Logger } from '@nestjs/common'; +export type GlobalDataKeys = 'projectName' | 'description' | 'platform'; +type ContextData = { + [key in GlobalDataKeys]: string; +} & Record; + export class BuilderContext { private state: BuildExecutionState = { completed: new Set(), @@ -80,11 +85,17 @@ export class BuilderContext { return { ...this.state }; } - setData(key: string, value: any): void { + setData( + key: Key, + value: ContextData[Key], + ): void { this.data[key] = value; } - getData(key: string): any { + getData(key: Key): ContextData[Key] { + if (!(key in this.data)) { + throw new Error(`Data key "${key}" is not set or does not exist.`); + } return this.data[key]; } diff --git a/backend/src/build-system/node/ux-datamap/index.ts b/backend/src/build-system/node/ux-datamap/index.ts new file mode 100644 index 00000000..7f79d72a --- /dev/null +++ b/backend/src/build-system/node/ux-datamap/index.ts @@ -0,0 +1,40 @@ +import { BuildHandler, BuildResult } from 'src/build-system/types'; +import { BuilderContext } from 'src/build-system/context'; +import { ModelProvider } from 'src/common/model-provider'; +import { prompts } from './prompt'; + +export class UXDatamapHandler implements BuildHandler { + readonly id = 'op:UX_DATAMAP::STATE:GENERATE'; + + async run(context: BuilderContext, args: unknown): Promise { + console.log('Generating UX Data Map Document...'); + + // extract relevant data from the context + const projectName = + context.getData('projectName') || 'Default Project Name'; + const uxGoals = context.getData('uxGoals') || 'Default UX Goals'; + + // generate the UX Data Map prompt dynamically + const prompt = prompts.generateUXDataMapPrompt( + projectName, + args as string, + // TODO: change later + 'web', + ); + + // Use ModelProsvider or another service to generate the document + const uxDatamapContent = await context.model.chatSync( + { + content: prompt, + }, + 'gpt-3.5-turbo', + ); + + // Store the generated document in the context + context.setData('uxDatamapDocument', uxDatamapContent); + return { + success: true, + data: uxDatamapContent, + }; + } +} diff --git a/backend/src/build-system/node/ux-datamap/prompt.ts b/backend/src/build-system/node/ux-datamap/prompt.ts new file mode 100644 index 00000000..b4670757 --- /dev/null +++ b/backend/src/build-system/node/ux-datamap/prompt.ts @@ -0,0 +1,140 @@ +export const prompts = { + generateUXDataMapPrompt: ( + projectName: string, + sitemapDoc: string, + platform: string, + ): string => { + return `You are an expert UX Designer. Your task is to analyze the provided sitemap documentation and identify all the data elements needed to support the user experience, based on the following inputs: + + - Project name: ${projectName} + - Sitemap Documentation: ${sitemapDoc} + - Platform: ${platform} + +Follow these guidelines to analyze data requirements from a UX perspective: + +### Instructions and Rules: + +1. For each page/screen in the sitemap: + - What information does the user need to see? + - What data does the user need to input? + - What feedback or responses should the user receive? + - What dynamic content needs to be displayed? + +2. Consider: + - User goals on each page + - Required form fields and their purposes + - Content that needs to be displayed + - System feedback and status information + - Dynamic updates and real-time data + - Error states and messages + +### UX Data Requirements Document: + +--- +### Page-by-Page Data Analysis + +#### 1. Project Overview + - **Project Name**: + - **Platform**: + - **General Description**: + +#### 2. Global Data Elements +Common data elements needed across multiple pages: + - User profile information + - Navigation states + - System status + - etc. + +#### 3. Page-Specific Data Requirements + +For each page in sitemap: + +##### [Page Name] +**Purpose**: [Brief description of page purpose] + +**User Goals**: +- Goal 1 +- Goal 2 +... + +**Required Data Elements**: + +*Input Data*: +- Field 1: [Purpose and importance] + - Why it's needed + - User expectations + - Requirements (if any) +- Field 2: ... + +*Display Data*: +- Content 1: [Purpose and importance] + - Why users need this information + - Update frequency (if dynamic) + - Priority level +- Content 2: ... + +*Feedback & States*: +- Success states +- Error states +- Loading states +- Empty states + +**User Interactions**: +- How data changes based on user actions +- What feedback is needed +- When/how data updates + +Example for Login Page: + +##### Login Page +**Purpose**: Allow users to authenticate and access their account + +**User Goals**: +- Sign into their account +- Recover forgotten password +- Stay signed in for convenience + +**Required Data Elements**: + +*Input Data*: +- Username/Email + - Purpose: Identify the user account + - User expectation: Email format or username rules + - Should be remembered if user chooses +- Password + - Purpose: Authenticate the user + - Should be masked for security + - Should support paste functionality +- "Remember Me" option + - Purpose: Convenience for returning users + - Optional selection + +*Display Data*: +- Login form status +- Authentication feedback +- Password requirements (if needed) +- Account recovery options + +*Feedback & States*: +- Loading state during authentication +- Success feedback and redirect +- Error messages for invalid credentials +- Password recovery confirmation + +**User Interactions**: +- Form validation feedback +- Login button state changes +- Immediate feedback on input errors +- Clear path to password recovery + +--- + +Your reply must start with: "\`\`\`UXDataMap" and end with "\`\`\`". + +Focus on describing the data requirements from a user experience perspective. For each page: +1. What data needs to be collected and why +2. What information needs to be displayed and why +3. How the data supports user goals +4. What feedback the user needs`; + }, +}; From 3da37d5a840155e53b86c75d26ad49bf33d9d9c6 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 01:13:15 -0600 Subject: [PATCH 03/10] feat: Add UX Data Map document generation functionality --- backend/src/common/model-provider/index.ts | 183 ++++++++++++++------- 1 file changed, 127 insertions(+), 56 deletions(-) diff --git a/backend/src/common/model-provider/index.ts b/backend/src/common/model-provider/index.ts index 1f7f6b4b..b4f9135b 100644 --- a/backend/src/common/model-provider/index.ts +++ b/backend/src/common/model-provider/index.ts @@ -1,17 +1,5 @@ import { Logger } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; -import { ChatCompletionChunk, StreamStatus } from 'src/chat/chat.model'; - -export interface ChatInput { - content: string; - attachments?: Array<{ - type: string; - content: string | Buffer; - name?: string; - }>; - contextLength?: number; - temperature?: number; -} export interface ModelProviderConfig { endpoint: string; @@ -39,7 +27,6 @@ export class ModelProvider { } return new ModelProvider(new HttpService(), { - // TODO: adding into env endpoint: 'http://localhost:3001', }); } @@ -54,17 +41,43 @@ export class ModelProvider { model: string, chatId?: string, ): Promise { - const chatStream = this.chat(input, model, chatId); - let content = ''; - for await (const chunk of chatStream) { - if (chunk.status === StreamStatus.STREAMING) { - content += chunk.choices - .map((choice) => choice.delta?.content || '') - .join(''); + this.logger.debug('Starting chatSync', { model, chatId }); + + this.resetState(); + + try { + const chatStream = this.chat(input, model, chatId); + let content = ''; + + this.logger.debug('Starting to process chat stream'); + for await (const chunk of chatStream) { + if (chunk.status === StreamStatus.STREAMING) { + const newContent = chunk.choices + .map((choice) => choice.delta?.content || '') + .join(''); + content += newContent; + } } + + return content; + } catch (error) { + this.logger.error('Error in chatSync:', error); + throw error; + } finally { + this.cleanup(); + this.logger.debug('ChatSync cleanup completed'); + } + } + + private resetState() { + this.logger.debug('Resetting provider state'); + this.isDone = false; + this.chunkQueue = []; + this.resolveNextChunk = null; + if (this.responseSubscription) { + this.responseSubscription.unsubscribe(); + this.responseSubscription = null; } - this.logger.log('Aggregated content from chat stream:', content); - return content; } chat( @@ -73,16 +86,13 @@ export class ModelProvider { chatId?: string, ): CustomAsyncIterableIterator { const chatInput = this.normalizeChatInput(input); - const selectedModel = model || this.config.defaultModel || undefined; - if (selectedModel === undefined) { - this.logger.error('No model selected for chat request'); - return; - } + const selectedModel = model || this.config.defaultModel; - this.logger.debug( - `Chat request - Model: ${selectedModel}, ChatId: ${chatId || 'N/A'}`, - { input: chatInput }, - ); + if (!selectedModel) { + const error = new Error('No model selected for chat request'); + this.logger.error(error.message); + throw error; + } const iterator: CustomAsyncIterableIterator = { next: () => this.handleNext(), @@ -98,16 +108,14 @@ export class ModelProvider { } private normalizeChatInput(input: ChatInput | string): ChatInput { - if (typeof input === 'string') { - return { content: input }; - } - return input; + return typeof input === 'string' ? { content: input } : input; } - private handleNext(): Promise> { + private async handleNext(): Promise> { return new Promise>((resolve) => { if (this.chunkQueue.length > 0) { - resolve({ done: false, value: this.chunkQueue.shift()! }); + const chunk = this.chunkQueue.shift()!; + resolve({ done: false, value: chunk }); } else if (this.isDone) { resolve({ done: true, value: undefined }); } else { @@ -129,10 +137,14 @@ export class ModelProvider { } private cleanup() { + this.logger.debug('Cleaning up provider'); this.isDone = true; if (this.responseSubscription) { this.responseSubscription.unsubscribe(); + this.responseSubscription = null; } + this.chunkQueue = []; + this.resolveNextChunk = null; } private createRequestPayload( @@ -178,9 +190,11 @@ export class ModelProvider { } private handleStreamEnd(model: string) { - this.logger.debug('Stream ended'); + this.logger.debug('Stream ended, handling completion'); + if (!this.isDone) { const doneChunk = this.createDoneChunk(model); + if (this.resolveNextChunk) { this.resolveNextChunk({ done: false, value: doneChunk }); this.resolveNextChunk = null; @@ -189,35 +203,42 @@ export class ModelProvider { } } - setTimeout(() => { + Promise.resolve().then(() => { + this.logger.debug('Setting done state'); this.isDone = true; if (this.resolveNextChunk) { this.resolveNextChunk({ done: true, value: undefined }); this.resolveNextChunk = null; } - }, 0); + }); } private handleStreamError(error: any, model: string) { - this.logger.error('Error in stream:', error); + this.logger.error('Stream error occurred:', error); const doneChunk = this.createDoneChunk(model); if (this.resolveNextChunk) { + this.logger.debug('Resolving waiting promise with error done chunk'); this.resolveNextChunk({ done: false, value: doneChunk }); - setTimeout(() => { + Promise.resolve().then(() => { this.isDone = true; - this.resolveNextChunk?.({ done: true, value: undefined }); - this.resolveNextChunk = null; - }, 0); + if (this.resolveNextChunk) { + this.resolveNextChunk({ done: true, value: undefined }); + this.resolveNextChunk = null; + } + }); } else { + this.logger.debug('Queueing error done chunk'); this.chunkQueue.push(doneChunk); - setTimeout(() => { + Promise.resolve().then(() => { this.isDone = true; - }, 0); + }); } } private startChat(input: ChatInput, model: string, chatId?: string) { + this.resetState(); + const payload = this.createRequestPayload(input, model, chatId); this.responseSubscription = this.httpService @@ -230,9 +251,11 @@ export class ModelProvider { .subscribe({ next: (response) => { let buffer = ''; + response.data.on('data', (chunk: Buffer) => { buffer += chunk.toString(); let newlineIndex; + while ((newlineIndex = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); @@ -240,6 +263,8 @@ export class ModelProvider { if (line.startsWith('data: ')) { const jsonStr = line.slice(6); if (jsonStr === '[DONE]') { + this.logger.debug('Received [DONE] signal'); + this.handleStreamEnd(model); return; } try { @@ -251,31 +276,41 @@ export class ModelProvider { } } }); - response.data.on('end', () => this.handleStreamEnd(model)); + + response.data.on('end', () => { + this.logger.debug('Response stream ended'); + this.handleStreamEnd(model); + }); + }, + error: (error) => { + this.logger.error('Error in chat request:', error); + this.handleStreamError(error, model); }, - error: (error) => this.handleStreamError(error, model), }); } private isValidChunk(chunk: any): boolean { - return ( + const isValid = chunk && typeof chunk.id === 'string' && typeof chunk.object === 'string' && typeof chunk.created === 'number' && - typeof chunk.model === 'string' - ); + typeof chunk.model === 'string'; + + if (!isValid) { + this.logger.warn('Invalid chunk structure', chunk); + } + + return isValid; } public async fetchModelsName() { try { - this.logger.debug('Requesting model tags from /tags endpoint.'); - - // Make a GET request to /tags + this.logger.debug('Fetching model tags'); const response = await this.httpService .get(`${this.config.endpoint}/tags`, { responseType: 'json' }) .toPromise(); - this.logger.debug('Model tags received:', response.data); + this.logger.debug('Model tags received', response.data); return response.data; } catch (error) { this.logger.error('Error fetching model tags:', error); @@ -283,3 +318,39 @@ export class ModelProvider { } } } + +export enum StreamStatus { + STREAMING = 'streaming', + DONE = 'done', +} + +export class ChatCompletionChunk { + id: string; + object: string; + created: number; + model: string; + systemFingerprint: string | null; + choices: ChatCompletionChoice[]; + status: StreamStatus; +} + +export interface ChatInput { + content: string; + attachments?: Array<{ + type: string; + content: string | Buffer; + name?: string; + }>; + contextLength?: number; + temperature?: number; +} + +class ChatCompletionDelta { + content?: string; +} + +class ChatCompletionChoice { + index: number; + delta: ChatCompletionDelta; + finishReason: string | null; +} From 1323ab9eea6930ac160dd230df63d8d7b7445397 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 01:13:27 -0600 Subject: [PATCH 04/10] feat: Generate UX Data Map document dynamically --- backend/src/build-system/node/ux-datamap/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/build-system/node/ux-datamap/index.ts b/backend/src/build-system/node/ux-datamap/index.ts index 7f79d72a..13b595a7 100644 --- a/backend/src/build-system/node/ux-datamap/index.ts +++ b/backend/src/build-system/node/ux-datamap/index.ts @@ -14,7 +14,6 @@ export class UXDatamapHandler implements BuildHandler { context.getData('projectName') || 'Default Project Name'; const uxGoals = context.getData('uxGoals') || 'Default UX Goals'; - // generate the UX Data Map prompt dynamically const prompt = prompts.generateUXDataMapPrompt( projectName, args as string, @@ -22,16 +21,13 @@ export class UXDatamapHandler implements BuildHandler { 'web', ); - // Use ModelProsvider or another service to generate the document const uxDatamapContent = await context.model.chatSync( { content: prompt, }, - 'gpt-3.5-turbo', + 'gpt-4o-mini', ); - // Store the generated document in the context - context.setData('uxDatamapDocument', uxDatamapContent); return { success: true, data: uxDatamapContent, From 360e0416004b98b9549afa989adc11fc54cea3d0 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 01:13:30 -0600 Subject: [PATCH 05/10] feat: Update PRDHandler to use gpt-4o-mini model --- .../src/build-system/node/product_requirements_document/prd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/build-system/node/product_requirements_document/prd.ts b/backend/src/build-system/node/product_requirements_document/prd.ts index e65052f2..fc386f0c 100644 --- a/backend/src/build-system/node/product_requirements_document/prd.ts +++ b/backend/src/build-system/node/product_requirements_document/prd.ts @@ -34,7 +34,7 @@ export class PRDHandler implements BuildHandler { private async generatePRDFromLLM(prompt: string): Promise { const modelProvider = ModelProvider.getInstance(); - const model = 'gpt-3.5-turbo'; + const model = 'gpt-4o-mini'; const prdContent = await modelProvider.chatSync({ content: prompt }, model); this.logger.log('Received full PRD content from LLM server.'); return prdContent; From 11f73f213b05e02a38e46b9474e0324685adcfc2 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 01:13:37 -0600 Subject: [PATCH 06/10] feat: Add database schema generation functionality --- .../node/database-schemas/prompt.ts | 121 ++++++++++++++++++ .../node/database-schemas/schemas.ts | 48 +++++++ 2 files changed, 169 insertions(+) create mode 100644 backend/src/build-system/node/database-schemas/prompt.ts create mode 100644 backend/src/build-system/node/database-schemas/schemas.ts diff --git a/backend/src/build-system/node/database-schemas/prompt.ts b/backend/src/build-system/node/database-schemas/prompt.ts new file mode 100644 index 00000000..98411fc7 --- /dev/null +++ b/backend/src/build-system/node/database-schemas/prompt.ts @@ -0,0 +1,121 @@ +export const prompts = { + // Step 1: Analyze requirements and generate database tasks + analyzeDatabaseRequirements: ( + projectName: string, + dbRequirements: string, + databaseType: string = 'PostgreSQL', + ): string => { + return `You are a Database Architect specializing in ${databaseType}. Your task is to analyze the provided database requirements document and create a clear plan for schema generation. Use the following requirements as input: + +${dbRequirements} + +Generate a structured analysis describing what needs to be created for each database entity. Your reply must start with "\`\`\`DBAnalysis" and end with "\`\`\`". + +For each entity in the requirements: +1. What tables/collections need to be created +2. What indexes are necessary +3. What constraints must be enforced +4. What relationships need to be established + +Example output format: + +\`\`\`DBAnalysis +Database: ${projectName} +Type: ${databaseType} + +Entity: Users +- Primary table for user accounts +- Required fields: id, email, username, password_hash, subscription_type +- Unique constraints on email and username +- Indexes needed on email and username for quick lookup +- JSON/JSONB field for preferences +- Timestamps for created_at and updated_at + +Entity: Playlists +- Primary table for playlist management +- Required fields: id, title, user_id, is_system_generated +- Foreign key relationship to Users +- Index on user_id for quick user playlist lookup +- Composite index on (user_id, title) for unique user playlists + +[Continue for all entities...] + +Required Junction Tables: +1. playlist_songs + - Manages N:M relationship between playlists and songs + - Needs position field for song order + - Indexes on both foreign keys + +2. song_genres + - Manages N:M relationship between songs and genres + - Indexes on both foreign keys + +Performance Considerations: +1. User table needs hash indexes on email and username +2. Playlist_songs needs index on position for queue management +3. Songs table needs full text search capability +\`\`\``; + }, + + // Step 2: Generate actual schema based on analysis + generateDatabaseSchema: ( + dbAnalysis: string, + databaseType: string = 'PostgreSQL', + ): string => { + return `You are a Database Engineer specializing in ${databaseType}. Generate the complete database schema based on the following analysis, using appropriate ${databaseType} syntax and features: + + Here is dbAnalysis content {${dbAnalysis}} + +Rules for generation: +1. Use ${databaseType}-specific data types and features +2. Do not include any comments in the output +3. Use standardized naming conventions +4. Include all necessary constraints and indexes +5. Generate schema in the correct creation order for dependencies + +Your reply must start with "\`\`\`${databaseType}" and end with "\`\`\`". + +For PostgreSQL, output format should be like: +\`\`\`sql +CREATE TYPE subscription_type_enum AS ENUM ('FREE', 'PREMIUM', 'FAMILY'); + +CREATE TABLE users ( + id UUID DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL, + username VARCHAR(50) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + subscription_type subscription_type_enum NOT NULL DEFAULT 'FREE', + preferences JSONB DEFAULT '{"theme":"light","audioQuality":"high"}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE (email), + UNIQUE (username) +); + +CREATE INDEX idx_users_email ON users(email); +[Continue with other tables...] +\`\`\` + +For MongoDB, output format should be like: +\`\`\`javascript +db.createCollection("users", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["email", "username", "password_hash", "subscription_type"], + properties: { + email: { bsonType: "string" }, + username: { bsonType: "string" }, + password_hash: { bsonType: "string" }, + subscription_type: { enum: ["FREE", "PREMIUM", "FAMILY"] } + } + } + } +}); + +db.users.createIndex({ "email": 1 }, { unique: true }); +[Continue with other collections...] +\`\`\``; + }, +}; diff --git a/backend/src/build-system/node/database-schemas/schemas.ts b/backend/src/build-system/node/database-schemas/schemas.ts new file mode 100644 index 00000000..44a1c7cd --- /dev/null +++ b/backend/src/build-system/node/database-schemas/schemas.ts @@ -0,0 +1,48 @@ +import { BuildHandler, BuildResult } from 'src/build-system/types'; +import { BuilderContext } from 'src/build-system/context'; +import { prompts } from './prompt'; +import { Logger } from '@nestjs/common'; + +export class DBSchemaHandler implements BuildHandler { + readonly id = 'OP:DATABASE:SCHEMAS'; + + readonly logger = new Logger('DBSchemaHandler'); + + async run(context: BuilderContext, args: unknown): Promise { + const projectName = + context.getData('projectName') || 'Default Project Name'; + const databaseType = context.getData('databaseType') || 'PostgreSQL'; + + const analysisPrompt = prompts.analyzeDatabaseRequirements( + projectName, + args as string, + databaseType, + ); + + const dbAnalysis = await context.model.chatSync( + { + content: analysisPrompt, + }, + 'gpt-4o-mini', + ); + + const schemaPrompt = prompts.generateDatabaseSchema( + dbAnalysis, + databaseType, + ); + + const schemaContent = await context.model.chatSync( + { + content: schemaPrompt, + }, + 'gpt-4o-mini', + ); + + return { + success: true, + data: { + schema: schemaContent, + }, + }; + } +} From c8ae3dba500b49cad4787b90b1f36fa15a6c7e6f Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 01:13:43 -0600 Subject: [PATCH 07/10] feat: Generate Database Requirements Document functionality --- .../database-requirements-document/index.ts | 33 ++++++ .../database-requirements-document/prompt.ts | 102 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 backend/src/build-system/node/database-requirements-document/index.ts create mode 100644 backend/src/build-system/node/database-requirements-document/prompt.ts diff --git a/backend/src/build-system/node/database-requirements-document/index.ts b/backend/src/build-system/node/database-requirements-document/index.ts new file mode 100644 index 00000000..9219df74 --- /dev/null +++ b/backend/src/build-system/node/database-requirements-document/index.ts @@ -0,0 +1,33 @@ +import { BuildHandler, BuildResult } from 'src/build-system/types'; +import { BuilderContext } from 'src/build-system/context'; +import { ModelProvider } from 'src/common/model-provider'; +import * as fs from 'fs'; +import * as path from 'path'; +import { prompts } from './prompt'; +import { Logger } from '@nestjs/common'; + +export class DatabaseRequirementHandler implements BuildHandler { + readonly id = 'op:DATABASE_REQ::STATE:GENERATE'; + readonly logger = new Logger('DatabaseRequirementHandler'); + async run(context: BuilderContext, args: unknown): Promise { + this.logger.log('Generating Database Requirements Document...'); + const projectName = + context.getData('projectName') || 'Default Project Name'; + + const prompt = prompts.generateDatabaseRequirementPrompt( + projectName, + args as string, + ); + const model = ModelProvider.getInstance(); + const dbRequirementsContent = await model.chatSync( + { + content: prompt, + }, + 'gpt-4o-mini', + ); + return { + success: true, + data: dbRequirementsContent, + }; + } +} diff --git a/backend/src/build-system/node/database-requirements-document/prompt.ts b/backend/src/build-system/node/database-requirements-document/prompt.ts new file mode 100644 index 00000000..4033bbfe --- /dev/null +++ b/backend/src/build-system/node/database-requirements-document/prompt.ts @@ -0,0 +1,102 @@ +export const prompts = { + generateDatabaseRequirementPrompt: ( + projectName: string, + uxDatamap: string, + ): string => { + return `You are a Database Architect and System Analyst. Your task is to analyze the provided UX Datamap document and generate a comprehensive Database Requirements Document that will support all the data needs identified in the UX design, based on the following inputs: + - Project name: ${projectName} + - UX Datamap Content: ${uxDatamap} + +Follow these guidelines to generate the database requirements: + +### Instructions and Rules: +1. Analyze all data elements mentioned in the UX Datamap +2. Identify entities and their relationships +3. Determine data types and constraints +4. Consider data persistence requirements +5. Plan for scalability and performance + +### Database Requirements Structure: +--- +### Database Requirements Document +#### 1. Overview +- Project scope +- Database purpose +- General requirements + +#### 2. Entity Definitions +For each identified entity: +- Entity name and description +- Business rules and constraints +- Key attributes +- Relationships with other entities +- Data retention requirements + +#### 3. Data Requirements +For each entity: +- Required fields +- Field types and constraints +- Validation rules +- Default values +- Indexing requirements + +#### 4. Relationships +- Entity relationships +- Cardinality +- Referential integrity rules +- Cascade rules + +#### 5. Data Access Patterns +- Common query patterns +- Search requirements +- Sorting and filtering needs +- Performance considerations + +#### 6. Security Requirements +- Access control +- Data privacy considerations +- Audit requirements +- Encryption needs + +#### 7. Performance Requirements +- Expected data volume +- Growth projections +- Query performance requirements +- Caching strategies + +#### 8. Additional Considerations +- Backup requirements +- Archive strategy +- Data migration needs +- Integration requirements + +Your reply must start with: "\`\`\`DatabaseRequirements" and end with "\`\`\`". + +Focus on creating practical, implementable database requirements that will effectively support the user experience described in the UX Datamap. Consider: + +1. Data Storage & Structure: + - Choose appropriate data types + - Define indexes for performance + - Plan for scalability + +2. Data Integrity: + - Define constraints + - Establish relationships + - Set validation rules + +3. Performance & Scalability: + - Query optimization + - Caching strategy + - Partitioning needs + +4. Security & Compliance: + - Access control + - Data encryption + - Audit requirements + +5. Maintenance & Operations: + - Backup strategy + - Archive policy + - Migration path`; + }, +}; From 0fd40d4c25dbbb902ab8f6f21aedd602b68e81dd Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 01:13:49 -0600 Subject: [PATCH 08/10] feat: Add code for generating Database Requirements Document and database schema This commit adds the necessary code changes to generate the Database Requirements Document and the corresponding database schema. It introduces the `DBRequirementDocumentHandler` class, which implements the `BuildHandler` interface. The `run` method in this class generates the Database Requirements Document based on the provided context data. Additionally, the commit includes the `DBSchemaHandler` class, which is responsible for generating the database schema. It uses the `markdown-to-txt` library to convert the Markdown content of the Database Requirements Document into plain text, which is then used to generate the schema. These features enhance the project builder functionality by providing the ability to generate the Database Requirements Document and the database schema, ensuring a clear understanding of the database structure and requirements. Co-authored-by: Jackson Chen <541898146chen@gmail.com> --- .../__tests__/db-requirement-document.md | 230 ++++++++++++++++++ .../__tests__/test-database-schemas.spec.ts | 49 ++++ .../src/build-system/__tests__/test.spec.ts | 19 +- 3 files changed, 280 insertions(+), 18 deletions(-) create mode 100644 backend/src/build-system/__tests__/db-requirement-document.md create mode 100644 backend/src/build-system/__tests__/test-database-schemas.spec.ts diff --git a/backend/src/build-system/__tests__/db-requirement-document.md b/backend/src/build-system/__tests__/db-requirement-document.md new file mode 100644 index 00000000..3b0d0881 --- /dev/null +++ b/backend/src/build-system/__tests__/db-requirement-document.md @@ -0,0 +1,230 @@ +### Database Requirements Document + +#### 1. Overview + +- **Project Scope**: Design and implement a database to support a Spotify-like music web application, facilitating personalized music streaming, content management, and user interaction. +- **Database Purpose**: Store and manage user profiles, music content, playlists, playback states, and preferences to support dynamic, personalized user experiences. +- **General Requirements**: + - Ensure high availability and scalability to handle concurrent user activity. + - Support real-time data updates for personalized recommendations and playback. + - Ensure data integrity and enforce business rules. + +--- + +#### 2. Entity Definitions + +##### User + +- **Description**: Represents registered users of the application. +- **Business Rules**: + - Each user must have a unique email. + - Users can manage their preferences and account details. +- **Key Attributes**: + - `user_id` (Primary Key) + - `username` (Unique, required) + - `email` (Unique, required) + - `password_hash` (Required) + - `subscription_type` (e.g., Free, Premium) + - `preferences` (e.g., theme, audio quality) + - `created_at`, `updated_at` +- **Relationships**: + - One-to-many with `Playlist`. + - Many-to-many with `Song` for liked songs. + +##### Song + +- **Description**: Represents individual songs available on the platform. +- **Business Rules**: + - Each song must have an associated album and artist. + - Songs may belong to multiple playlists. +- **Key Attributes**: + - `song_id` (Primary Key) + - `title` (Required) + - `artist_id` (Foreign Key) + - `album_id` (Foreign Key) + - `duration` (In seconds) + - `genre` (Category) + - `release_date` +- **Relationships**: + - Many-to-one with `Album` and `Artist`. + - Many-to-many with `Playlist`. + +##### Artist + +- **Description**: Represents artists whose songs are on the platform. +- **Key Attributes**: + - `artist_id` (Primary Key) + - `name` (Required) + - `bio` + - `profile_image` + - `created_at`, `updated_at` +- **Relationships**: + - One-to-many with `Song` and `Album`. + +##### Album + +- **Description**: Represents music albums. +- **Key Attributes**: + - `album_id` (Primary Key) + - `title` (Required) + - `artist_id` (Foreign Key) + - `release_date` + - `cover_image` +- **Relationships**: + - One-to-many with `Song`. + +##### Playlist + +- **Description**: Represents user-created or curated playlists. +- **Business Rules**: + - A playlist must belong to a user or be globally curated. +- **Key Attributes**: + - `playlist_id` (Primary Key) + - `name` (Required) + - `user_id` (Foreign Key, nullable for curated playlists) + - `description` + - `is_curated` (Boolean) + - `created_at`, `updated_at` +- **Relationships**: + - Many-to-many with `Song`. + +##### PlaybackState + +- **Description**: Tracks the playback state for a user. +- **Key Attributes**: + - `playback_id` (Primary Key) + - `user_id` (Foreign Key) + - `current_song_id` (Foreign Key) + - `queue` (Array of `song_id`s) + - `playback_position` (Seconds) + - `volume` + - `created_at`, `updated_at` + +##### Recommendation + +- **Description**: Stores dynamic recommendations for users. +- **Key Attributes**: + - `recommendation_id` (Primary Key) + - `user_id` (Foreign Key) + - `content` (JSON: list of recommended songs, albums, playlists) + - `generated_at` + +--- + +#### 3. Data Requirements + +##### User + +- Fields: + - `user_id`: UUID + - `username`: String (max 50) + - `email`: String (unique, max 100) + - `password_hash`: String + - `subscription_type`: Enum (Free, Premium) + - `preferences`: JSON + - `created_at`, `updated_at`: Timestamps +- Constraints: + - `email` and `username` must be unique. + - Enforce non-null constraints on required fields. +- Indexing: + - Index on `email` and `user_id`. + +##### Song + +- Fields: + - `song_id`: UUID + - `title`: String (max 100) + - `artist_id`, `album_id`: Foreign Keys + - `duration`: Integer + - `genre`: String + - `release_date`: Date +- Constraints: + - Non-null constraints on `title`, `artist_id`, and `album_id`. +- Indexing: + - Index on `title` and `genre`. + +##### Playlist + +- Fields: + - `playlist_id`: UUID + - `name`: String (max 50) + - `user_id`: Foreign Key + - `description`: String + - `is_curated`: Boolean + - `created_at`, `updated_at`: Timestamps +- Constraints: + - Enforce foreign key constraints for `user_id`. +- Indexing: + - Index on `user_id`. + +##### PlaybackState + +- Fields: + - `playback_id`: UUID + - `user_id`: Foreign Key + - `current_song_id`: Foreign Key + - `queue`: JSON + - `playback_position`: Integer + - `volume`: Float + - `created_at`, `updated_at`: Timestamps +- Constraints: + - Ensure valid `user_id` and `current_song_id`. + +--- + +#### 4. Relationships + +- `User` to `Playlist`: One-to-many. +- `Playlist` to `Song`: Many-to-many (junction table: `playlist_song`). +- `Song` to `Album`: Many-to-one. +- `Song` to `Artist`: Many-to-one. +- `User` to `PlaybackState`: One-to-one. +- Referential Integrity: + - Cascade delete for dependent entities (e.g., playlists when a user is deleted). + +--- + +#### 5. Data Access Patterns + +- Common Queries: + - Fetch user playlists, liked songs, and playback state. + - Search for songs, albums, or artists. + - Fetch recommended content dynamically. +- Indexing: + - Full-text search for song titles and artist names. + - Index on foreign keys for join performance. + +--- + +#### 6. Security Requirements + +- Access Control: + - Restrict user data to authenticated sessions. +- Data Privacy: + - Hash sensitive data (e.g., passwords). +- Audit: + - Log user activity and data changes. + +--- + +#### 7. Performance Requirements + +- Expected Volume: + - Millions of songs and playlists. + - Thousands of concurrent users. +- Growth: + - Plan for 10x growth in user and song data over 5 years. +- Optimizations: + - Cache frequently accessed data (e.g., recommendations). + - Use partitioning for large tables. + +--- + +#### 8. Additional Considerations + +- Backups: + - Automated daily backups. +- Archiving: + - Move inactive playlists to archival storage after 1 year. +- Integration: + - Support for third-party authentication and external APIs. diff --git a/backend/src/build-system/__tests__/test-database-schemas.spec.ts b/backend/src/build-system/__tests__/test-database-schemas.spec.ts new file mode 100644 index 00000000..a7224af9 --- /dev/null +++ b/backend/src/build-system/__tests__/test-database-schemas.spec.ts @@ -0,0 +1,49 @@ +import { BuilderContext } from 'src/build-system/context'; +import { DBSchemaHandler } from '../node/database-schemas/schemas'; +import { readFileSync } from 'fs'; +import markdownToTxt from 'markdown-to-txt'; + +jest.mock('fs', () => ({ + readFileSync: jest.fn(() => 'mock content'), +})); + +const RUN_INTEGRATION_TESTS = process.env.RUN_INTEGRATION_TESTS === 'true'; + +describe('DBSchemaHandler', () => { + describe('Integration Tests', () => { + (RUN_INTEGRATION_TESTS ? describe : describe.skip)( + 'Schema Generation Tests', + () => { + it('should generate schema for blog system', async () => { + const handler = new DBSchemaHandler(); + const context = new BuilderContext( + { + id: 'test', + name: 'test db schema', + version: '1.0.0', + description: 'test db schema', + steps: [], + }, + 'test-id-schema-1', + ); + + const mdFileContent = readFileSync( + './db-requirement.document.md', + 'utf-8', + ); + const plainText = markdownToTxt(mdFileContent); + const result = await handler.run(context, plainText); + console.log(result); + }, 30000); + }, + ); + }); + + describe('Unit Tests', () => { + it('should initialize correctly', () => { + const handler = new DBSchemaHandler(); + expect(handler).toBeDefined(); + expect(handler.id).toBe('OP:DATABASE:SCHEMAS'); + }); + }); +}); diff --git a/backend/src/build-system/__tests__/test.spec.ts b/backend/src/build-system/__tests__/test.spec.ts index 4f8dde5b..40ea0e69 100644 --- a/backend/src/build-system/__tests__/test.spec.ts +++ b/backend/src/build-system/__tests__/test.spec.ts @@ -4,7 +4,6 @@ import { BuildSequenceExecutor } from '../executor'; import { BuildHandlerManager } from '../hanlder-manager'; import { ProjectInitHandler } from '../node/project-init'; import { BuildSequence } from '../types'; - describe('Project Init Handler Test', () => { let context: BuilderContext; let executor: BuildSequenceExecutor; @@ -35,7 +34,7 @@ describe('Project Init Handler Test', () => { handlerManager = BuildHandlerManager.getInstance(); handlerManager.clear(); - context = new BuilderContext(testSequence); + context = new BuilderContext(testSequence, 'id'); executor = new BuildSequenceExecutor(context); }); @@ -47,22 +46,6 @@ describe('Project Init Handler Test', () => { }); }); - describe('State Management', () => { - test('should update execution state correctly', async () => { - let state = context.getState(); - expect(state.completed.size).toBe(0); - expect(state.pending.size).toBe(0); - - await executor.executeSequence(testSequence); - - state = context.getState(); - expect(state.completed.size).toBe(1); - expect(state.completed.has('op:PROJECT::STATE:SETUP')).toBe(true); - expect(state.pending.size).toBe(0); - expect(state.failed.size).toBe(0); - }); - }); - describe('Direct Handler Execution', () => { test('should be able to run handler directly', async () => { const handler = new ProjectInitHandler(); From 4b18067c03e57252db985cacef7e65b70ed0b283 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 01:56:07 -0600 Subject: [PATCH 09/10] feat: Add BackendRequirementHandler for generating Backend Requirements Document This commit introduces the `BackendRequirementHandler` class, which implements the `BuildHandler` interface. The `run` method in this class generates the Backend Requirements Document based on the provided context data. The Backend Requirements Document includes the system overview, API endpoints, and implementation details. It provides guidelines for designing a clear system architecture, defining necessary API endpoints, and following RESTful or GraphQL conventions. The document also covers implementation details for request handlers/controllers, business logic layer/services, data access layer, and middleware components. This feature enhances the project builder functionality by providing the ability to generate a comprehensive Backend Requirements Document, ensuring a clear understanding of the backend system architecture and API endpoints. Co-authored-by: Jackson Chen <541898146chen@gmail.com> --- .../backend-requirements-document/index.ts | 74 ++++++++ .../backend-requirements-document/prompt.ts | 165 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 backend/src/build-system/node/backend-requirements-document/index.ts create mode 100644 backend/src/build-system/node/backend-requirements-document/prompt.ts diff --git a/backend/src/build-system/node/backend-requirements-document/index.ts b/backend/src/build-system/node/backend-requirements-document/index.ts new file mode 100644 index 00000000..e0bdc6e8 --- /dev/null +++ b/backend/src/build-system/node/backend-requirements-document/index.ts @@ -0,0 +1,74 @@ +import { BuildHandler, BuildResult } from 'src/build-system/types'; +import { BuilderContext } from 'src/build-system/context'; +import { + generateBackendImplementationPrompt, + generateBackendOverviewPrompt, +} from './prompt'; +import { Logger } from '@nestjs/common'; + +export class BackendRequirementHandler implements BuildHandler { + readonly id = 'op:BACKEND_REQ::STATE:GENERATE'; + + readonly logger: Logger = new Logger('BackendRequirementHandler'); + async run(context: BuilderContext, args: unknown): Promise { + this.logger.log('Generating Backend Requirements Document...'); + + // Validate and extract args + if (!args || typeof args !== 'object') { + throw new Error('Backend configuration is required'); + } + // TODO: init language, framework, packages later in context + const language = context.getData('language') || 'javascript'; + const framework = context.getData('framework') || 'express'; + const packages = context.getData('packages') || {}; + // TODO: adding graphql/restful later + + const { dbRequirements } = args as { + dbRequirements: string; + language: string; + framework: string; + packages: Record; + }; + + const overviewPrompt = generateBackendOverviewPrompt( + context.getData('projectName') || 'Default Project Name', + dbRequirements, + language, + framework, + packages, + ); + + const backendOverview = await context.model.chatSync( + { + content: overviewPrompt, + }, + 'gpt-4o-mini', + ); + + const implementationPrompt = generateBackendImplementationPrompt( + backendOverview, + language, + framework, + ); + + const implementationDetails = await context.model.chatSync( + { + content: implementationPrompt, + }, + 'gpt-4o-mini', + ); + + return { + success: true, + data: { + overview: backendOverview, + implementation: implementationDetails, + config: { + language, + framework, + packages, + }, + }, + }; + } +} diff --git a/backend/src/build-system/node/backend-requirements-document/prompt.ts b/backend/src/build-system/node/backend-requirements-document/prompt.ts new file mode 100644 index 00000000..e8b2f249 --- /dev/null +++ b/backend/src/build-system/node/backend-requirements-document/prompt.ts @@ -0,0 +1,165 @@ +// backend-overview-prompt.ts +export const generateBackendOverviewPrompt = ( + projectName: string, + dbRequirements: string, + language: string, + framework: string, + packages: Record, +): string => { + return `You are a Senior Backend Architect specializing in ${language} with ${framework}. Analyze the provided Database Requirements and generate the System Overview and API Endpoints specifications for ${projectName}. + +Database Requirements: +${dbRequirements} + +Technology Stack: +- Language: ${language} +- Framework: ${framework} +- Key Packages: +${Object.entries(packages) + .map(([pkg, version]) => ` - ${pkg}@${version}`) + .join('\n')} + +Generate a Backend Overview Document following these guidelines: + +### Instructions and Rules: +1. Design a clear system architecture based on the technology stack +2. Define all necessary API endpoints based on database requirements +3. Follow RESTful or GraphQL conventions as appropriate +4. Consider the relationships between different entities +5. Focus on clean and maintainable API design + +Your reply must start with: "\`\`\`BackendOverview" and end with "\`\`\`". + +Include these sections: + +#### 1. System Overview +- **Project Name**: ${projectName} +- **Technology Stack** + - Core technology choices + - Framework architecture + - Key dependencies and their purposes +- **Architecture Patterns** + - Framework-specific patterns + - Project structure + - Dependency management + - Configuration management + - Service organization + +#### 2. API Endpoints +For each endpoint: +\`\`\` +Route: /api/resource +Method: GET|POST|PUT/DELETE +Purpose: Functional description +Request: + Headers: { + "Authorization": "Bearer {token}" + // Other headers + } + Params: { + // URL parameters + } + Query: { + // Query parameters + } + Body: { + // Request body schema + } +Response: + Success: { + // Success response schema + } + Errors: { + // Error response schemas + } +Required Auth: Yes/No +\`\`\``; +}; + +// backend-implementation-prompt.ts +export const generateBackendImplementationPrompt = ( + backendOverview: string, + language: string, + framework: string, +): string => { + return `You are a Senior Backend Architect specializing in ${language} with ${framework}. Based on the provided Backend Overview, generate detailed implementation requirements for security, error handling, and other technical aspects. + +Backend Overview: +${backendOverview} + +Generate detailed implementation requirements following these sections: + +Your reply must start with: "\`\`\`BackendImplementation" and end with "\`\`\`". + +#### 3. Implementation Details +For each major component: +- **Request Handlers/Controllers** + - Implementation approach + - Request processing flow + - Response formatting + - Middleware integration + +- **Business Logic Layer/Services** + - Service patterns + - Business rule implementation + - External service integration + - Transaction management + +- **Data Access Layer** + - Database interaction patterns + - Query optimization + - Data mapping strategy + - Cache integration + +- **Middleware Components** + - Authentication middleware + - Validation middleware + - Logging middleware + - Error handling middleware + +#### 4. Security Implementation +- Authentication strategy + - Token management + - Session handling + - Refresh token mechanism +- Authorization rules + - Role-based access control + - Permission management + - Resource ownership +- Input validation + - Request validation + - Data sanitization +- API security + - Rate limiting + - CORS configuration + - Security headers +- Data protection + - Encryption methods + - Secure storage + - PII handling + +#### 5. Error Handling +- Error handling strategy + - Global error handler + - Domain-specific errors + - Operational errors +- Error response format + - Error codes + - Error messages + - Debug information +- Error types and codes + - HTTP status codes + - Application error codes + - Validation errors +- Logging strategy + - Log levels + - Log format + - Log storage + +Focus on: +1. ${language} and ${framework} specific implementation patterns +2. Best practices for each component +3. Security considerations +4. Error handling and logging +5. Performance optimization`; +}; From 322cd0d9beda6558936cb007480529cbf2c57c66 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sun, 17 Nov 2024 02:09:01 -0600 Subject: [PATCH 10/10] feat: Refactor BackendRequirementsDocument generation This commit refactors the code for generating the Backend Requirements Document. It introduces the `BackendRequirementHandler` class, which implements the `BuildHandler` interface. The `run` method in this class generates the Backend Requirements Document based on the provided context data. The Backend Requirements Document includes the system overview, API endpoints, and implementation details. It provides guidelines for designing a clear system architecture, defining necessary API endpoints, and following RESTful or GraphQL conventions. The document also covers implementation details for request handlers/controllers, business logic layer/services, data access layer, and middleware components. This refactoring improves the project builder functionality by separating the logic for generating the Backend Requirements Document into a dedicated class, making the code more modular and maintainable. Co-authored-by: Jackson Chen <541898146chen@gmail.com> --- .../node/backend-requirements-document/prompt.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/src/build-system/node/backend-requirements-document/prompt.ts b/backend/src/build-system/node/backend-requirements-document/prompt.ts index e8b2f249..48e8d71e 100644 --- a/backend/src/build-system/node/backend-requirements-document/prompt.ts +++ b/backend/src/build-system/node/backend-requirements-document/prompt.ts @@ -6,12 +6,13 @@ export const generateBackendOverviewPrompt = ( framework: string, packages: Record, ): string => { - return `You are a Senior Backend Architect specializing in ${language} with ${framework}. Analyze the provided Database Requirements and generate the System Overview and API Endpoints specifications for ${projectName}. + return `You are a Senior Backend Architect specializing in backend systems. Generate the System Overview and API Endpoints specifications based on the following inputs. +. -Database Requirements: -${dbRequirements} +### Inputs +Project Name: ${projectName} -Technology Stack: +### Technology Stack - Language: ${language} - Framework: ${framework} - Key Packages: @@ -19,6 +20,9 @@ ${Object.entries(packages) .map(([pkg, version]) => ` - ${pkg}@${version}`) .join('\n')} +### Database Requirements +${dbRequirements} + Generate a Backend Overview Document following these guidelines: ### Instructions and Rules: @@ -84,7 +88,7 @@ export const generateBackendImplementationPrompt = ( ): string => { return `You are a Senior Backend Architect specializing in ${language} with ${framework}. Based on the provided Backend Overview, generate detailed implementation requirements for security, error handling, and other technical aspects. -Backend Overview: +## Backend Overview: ${backendOverview} Generate detailed implementation requirements following these sections: