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 1/2] 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 2/2] 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`; + }, +};